diff --git a/agentscope-core/src/main/java/io/agentscope/core/util/MessageUtils.java b/agentscope-core/src/main/java/io/agentscope/core/util/MessageUtils.java index f28c3a627..2131936ec 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/util/MessageUtils.java +++ b/agentscope-core/src/main/java/io/agentscope/core/util/MessageUtils.java @@ -19,6 +19,7 @@ import io.agentscope.core.message.MsgRole; import io.agentscope.core.message.ToolUseBlock; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -30,6 +31,8 @@ */ public final class MessageUtils { + private static final String COMPRESS_META_KEY = "_compress_meta"; + private MessageUtils() { throw new UnsupportedOperationException("Utility class cannot be instantiated"); } @@ -53,6 +56,9 @@ public static List extractRecentToolCalls(List messages, Stri for (int i = messages.size() - 1; i >= 0; i--) { Msg msg = messages.get(i); if (msg.getRole() == MsgRole.ASSISTANT && Objects.equals(msg.getName(), agentName)) { + if (isCompressedMessage(msg)) { + continue; + } List toolCalls = msg.getContentBlocks(ToolUseBlock.class); if (!toolCalls.isEmpty()) { return toolCalls; @@ -63,4 +69,9 @@ public static List extractRecentToolCalls(List messages, Stri return List.of(); } + + private static boolean isCompressedMessage(Msg msg) { + Map metadata = msg.getMetadata(); + return metadata.get(COMPRESS_META_KEY) instanceof Map; + } } diff --git a/agentscope-core/src/test/java/io/agentscope/core/util/MessageUtilsTest.java b/agentscope-core/src/test/java/io/agentscope/core/util/MessageUtilsTest.java new file mode 100644 index 000000000..8dc838675 --- /dev/null +++ b/agentscope-core/src/test/java/io/agentscope/core/util/MessageUtilsTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.core.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.MsgRole; +import io.agentscope.core.message.TextBlock; +import io.agentscope.core.message.ToolUseBlock; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("MessageUtils Tests") +class MessageUtilsTest { + + @Test + @DisplayName("Should skip compressed assistant messages when extracting recent tool calls") + void testExtractRecentToolCallsSkipsCompressedAssistantMessages() { + List messages = new ArrayList<>(); + messages.add(createAssistantToolUseMessage("real-tool", "real-call")); + messages.add(createCompressedAssistantToolUseMessage("compressed-tool", "compressed-call")); + + List toolCalls = MessageUtils.extractRecentToolCalls(messages, "assistant"); + + assertEquals(1, toolCalls.size()); + assertEquals("real-tool", toolCalls.get(0).getName()); + assertEquals("real-call", toolCalls.get(0).getId()); + assertFalse(toolCalls.stream().anyMatch(block -> "compressed-call".equals(block.getId()))); + } + + @Test + @DisplayName("Should return empty list when messages are null or empty") + void testExtractRecentToolCallsWithEmptyInput() { + assertTrue(MessageUtils.extractRecentToolCalls(null, "assistant").isEmpty()); + assertTrue(MessageUtils.extractRecentToolCalls(List.of(), "assistant").isEmpty()); + } + + @Test + @DisplayName("Should not skip assistant messages when _compress_meta is not a map") + void testExtractRecentToolCallsDoesNotSkipAssistantMessagesWithInvalidCompressMetadata() { + Msg message = + createAssistantToolUseMessage( + "real-tool", "real-call", Map.of("_compress_meta", "not-a-map")); + + List toolCalls = + MessageUtils.extractRecentToolCalls(List.of(message), "assistant"); + + assertEquals(1, toolCalls.size()); + assertEquals("real-call", toolCalls.get(0).getId()); + } + + private Msg createAssistantToolUseMessage(String toolName, String callId) { + return createAssistantToolUseMessage(toolName, callId, Map.of()); + } + + private Msg createAssistantToolUseMessage( + String toolName, String callId, Map metadata) { + return Msg.builder() + .role(MsgRole.ASSISTANT) + .name("assistant") + .content( + List.of( + ToolUseBlock.builder() + .name(toolName) + .id(callId) + .input(new HashMap<>()) + .build())) + .metadata(metadata) + .build(); + } + + private Msg createCompressedAssistantToolUseMessage(String toolName, String callId) { + Map metadata = new HashMap<>(); + metadata.put("_compress_meta", Map.of("offloaduuid", "uuid-123")); + + return Msg.builder() + .role(MsgRole.ASSISTANT) + .name("assistant") + .content( + List.of( + ToolUseBlock.builder() + .name(toolName) + .id(callId) + .input(new HashMap<>()) + .build(), + TextBlock.builder().text("Compressed summary").build())) + .metadata(metadata) + .build(); + } +}