From 0d4bbd662dc2712e45b4face3f6806898dcc3101 Mon Sep 17 00:00:00 2001 From: VLooong Date: Sat, 9 May 2026 16:47:21 +0800 Subject: [PATCH 1/7] fix(core): fix list hash stability for equivalent messages --- .../agentscope/core/session/ListHashUtil.java | 3 +- .../core/session/mysql/MysqlSessionTest.java | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/session/ListHashUtil.java b/agentscope-core/src/main/java/io/agentscope/core/session/ListHashUtil.java index e31b3d847c..642d2424a7 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/session/ListHashUtil.java +++ b/agentscope-core/src/main/java/io/agentscope/core/session/ListHashUtil.java @@ -16,6 +16,7 @@ package io.agentscope.core.session; import io.agentscope.core.state.State; +import io.agentscope.core.util.JsonUtils; import java.util.List; /** @@ -88,7 +89,7 @@ public static String computeHash(List values) { for (int idx : sampleIndices) { State item = values.get(idx); - int itemHash = item != null ? item.hashCode() : 0; + int itemHash = item != null ? JsonUtils.getJsonCodec().toJson(item).hashCode() : 0; sb.append(idx).append(":").append(itemHash).append(","); } diff --git a/agentscope-extensions/agentscope-extensions-session-mysql/src/test/java/io/agentscope/core/session/mysql/MysqlSessionTest.java b/agentscope-extensions/agentscope-extensions-session-mysql/src/test/java/io/agentscope/core/session/mysql/MysqlSessionTest.java index fd1811df61..9b35c0da66 100644 --- a/agentscope-extensions/agentscope-extensions-session-mysql/src/test/java/io/agentscope/core/session/mysql/MysqlSessionTest.java +++ b/agentscope-extensions/agentscope-extensions-session-mysql/src/test/java/io/agentscope/core/session/mysql/MysqlSessionTest.java @@ -25,6 +25,10 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.MsgRole; +import io.agentscope.core.message.TextBlock; +import io.agentscope.core.session.ListHashUtil; import io.agentscope.core.state.SessionKey; import io.agentscope.core.state.SimpleSessionKey; import io.agentscope.core.state.State; @@ -282,6 +286,45 @@ void testSaveAndGetListState() throws SQLException { assertEquals("value2", loaded.get(1).value()); } + @Test + @DisplayName("Should compute same hash for equivalent message lists") + void testComputeHashEquivalentMessageLists() { + List first = + List.of( + Msg.builder() + .id("m-user-1") + .timestamp("2026-05-08 14:00:00.000") + .role(MsgRole.USER) + .content(TextBlock.builder().text("hello").build()) + .build(), + Msg.builder() + .id("m-assistant-1") + .timestamp("2026-05-08 14:00:01.000") + .role(MsgRole.ASSISTANT) + .content(TextBlock.builder().text("hello").build()) + .build()); + + List second = + List.of( + Msg.builder() + .id("m-user-1") + .timestamp("2026-05-08 14:00:00.000") + .role(MsgRole.USER) + .content(TextBlock.builder().text("hello").build()) + .build(), + Msg.builder() + .id("m-assistant-1") + .timestamp("2026-05-08 14:00:01.000") + .role(MsgRole.ASSISTANT) + .content(TextBlock.builder().text("hello").build()) + .build()); + + String h1 = ListHashUtil.computeHash(first); + String h2 = ListHashUtil.computeHash(second); + + assertEquals(h1, h2); + } + @Test @DisplayName("Should commit incremental list save when connection auto-commit is disabled") void testSaveListIncrementalAppendCommitsWhenAutoCommitDisabled() throws SQLException { From 774b4cdd06ffa74969d1184b790852670e30ad9d Mon Sep 17 00:00:00 2001 From: VLooong Date: Sat, 9 May 2026 17:42:43 +0800 Subject: [PATCH 2/7] Move ListHashUtil hash test to core --- .../core/session/ListHashUtilTest.java | 49 +++++++++++++++++++ .../core/session/mysql/MysqlSessionTest.java | 43 ---------------- 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/agentscope-core/src/test/java/io/agentscope/core/session/ListHashUtilTest.java b/agentscope-core/src/test/java/io/agentscope/core/session/ListHashUtilTest.java index dfd10e20b6..212cd4ded2 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/session/ListHashUtilTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/session/ListHashUtilTest.java @@ -55,6 +55,55 @@ void testComputeHashSameListSameHash() { assertEquals(hash1, hash2); } + @Test + void testComputeHashEquivalentMessageLists() { + List first = + List.of( + Msg.builder() + .id("m-user-1") + .timestamp("2026-05-08 14:00:00.000") + .role(MsgRole.USER) + .content(TextBlock.builder().text("hello").build()) + .build(), + Msg.builder() + .id("m-assistant-1") + .timestamp("2026-05-08 14:00:01.000") + .role(MsgRole.ASSISTANT) + .content(TextBlock.builder().text("hello").build()) + .build()); + + List second = + List.of( + Msg.builder() + .id("m-user-1") + .timestamp("2026-05-08 14:00:00.000") + .role(MsgRole.USER) + .content(TextBlock.builder().text("hello").build()) + .build(), + Msg.builder() + .id("m-assistant-1") + .timestamp("2026-05-08 14:00:01.000") + .role(MsgRole.ASSISTANT) + .content(TextBlock.builder().text("hello").build()) + .build()); + + String h1 = ListHashUtil.computeHash(first); + String h2 = ListHashUtil.computeHash(second); + + assertEquals(h1, h2); + } + + @Test + void testComputeHashListWithNullItem() { + List list = new ArrayList<>(); + list.add(null); + + String hash1 = ListHashUtil.computeHash(list); + String hash2 = ListHashUtil.computeHash(list); + + assertEquals(hash1, hash2); + } + @Test void testComputeHashListModifiedDifferentHash() { List list = createMsgList(5); diff --git a/agentscope-extensions/agentscope-extensions-session-mysql/src/test/java/io/agentscope/core/session/mysql/MysqlSessionTest.java b/agentscope-extensions/agentscope-extensions-session-mysql/src/test/java/io/agentscope/core/session/mysql/MysqlSessionTest.java index 9b35c0da66..fd1811df61 100644 --- a/agentscope-extensions/agentscope-extensions-session-mysql/src/test/java/io/agentscope/core/session/mysql/MysqlSessionTest.java +++ b/agentscope-extensions/agentscope-extensions-session-mysql/src/test/java/io/agentscope/core/session/mysql/MysqlSessionTest.java @@ -25,10 +25,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import io.agentscope.core.message.Msg; -import io.agentscope.core.message.MsgRole; -import io.agentscope.core.message.TextBlock; -import io.agentscope.core.session.ListHashUtil; import io.agentscope.core.state.SessionKey; import io.agentscope.core.state.SimpleSessionKey; import io.agentscope.core.state.State; @@ -286,45 +282,6 @@ void testSaveAndGetListState() throws SQLException { assertEquals("value2", loaded.get(1).value()); } - @Test - @DisplayName("Should compute same hash for equivalent message lists") - void testComputeHashEquivalentMessageLists() { - List first = - List.of( - Msg.builder() - .id("m-user-1") - .timestamp("2026-05-08 14:00:00.000") - .role(MsgRole.USER) - .content(TextBlock.builder().text("hello").build()) - .build(), - Msg.builder() - .id("m-assistant-1") - .timestamp("2026-05-08 14:00:01.000") - .role(MsgRole.ASSISTANT) - .content(TextBlock.builder().text("hello").build()) - .build()); - - List second = - List.of( - Msg.builder() - .id("m-user-1") - .timestamp("2026-05-08 14:00:00.000") - .role(MsgRole.USER) - .content(TextBlock.builder().text("hello").build()) - .build(), - Msg.builder() - .id("m-assistant-1") - .timestamp("2026-05-08 14:00:01.000") - .role(MsgRole.ASSISTANT) - .content(TextBlock.builder().text("hello").build()) - .build()); - - String h1 = ListHashUtil.computeHash(first); - String h2 = ListHashUtil.computeHash(second); - - assertEquals(h1, h2); - } - @Test @DisplayName("Should commit incremental list save when connection auto-commit is disabled") void testSaveListIncrementalAppendCommitsWhenAutoCommitDisabled() throws SQLException { From 0392f38c2228835b28a500f1e54e1a938a577765 Mon Sep 17 00:00:00 2001 From: VLooong Date: Tue, 12 May 2026 17:27:28 +0800 Subject: [PATCH 3/7] refactor(core): add value-based equals and hashCode methods to the Msg related classes. --- .../agentscope/core/message/AudioBlock.java | 17 ++++ .../agentscope/core/message/Base64Source.java | 18 ++++ .../agentscope/core/message/ImageBlock.java | 19 ++++ .../java/io/agentscope/core/message/Msg.java | 23 +++++ .../io/agentscope/core/message/TextBlock.java | 18 ++++ .../core/message/ThinkingBlock.java | 19 ++++ .../core/message/ToolResultBlock.java | 21 +++++ .../agentscope/core/message/ToolUseBlock.java | 22 +++++ .../io/agentscope/core/message/URLSource.java | 17 ++++ .../agentscope/core/message/VideoBlock.java | 29 ++++++ .../io/agentscope/core/model/ChatUsage.java | 21 +++++ .../agentscope/core/session/ListHashUtil.java | 3 +- .../core/session/ListHashUtilTest.java | 90 +++++++++++++++++++ 13 files changed, 315 insertions(+), 2 deletions(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/AudioBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/AudioBlock.java index 02f102b5ab..0d8f8cf784 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/AudioBlock.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/AudioBlock.java @@ -56,6 +56,23 @@ public Source getSource() { return source; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AudioBlock)) { + return false; + } + AudioBlock that = (AudioBlock) o; + return Objects.equals(this.source, that.source); + } + + @Override + public int hashCode() { + return Objects.hash(this.source); + } + /** * Creates a new builder for constructing AudioBlock instances. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/Base64Source.java b/agentscope-core/src/main/java/io/agentscope/core/message/Base64Source.java index 35408aec33..beafd3c158 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/Base64Source.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/Base64Source.java @@ -75,6 +75,24 @@ public String getData() { return data; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Base64Source)) { + return false; + } + Base64Source that = (Base64Source) o; + return Objects.equals(this.mediaType, that.mediaType) + && Objects.equals(this.data, that.data); + } + + @Override + public int hashCode() { + return Objects.hash(this.mediaType, this.data); + } + /** * Creates a new builder for constructing Base64Source instances. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/ImageBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/ImageBlock.java index b57866f024..c1f41ba07d 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/ImageBlock.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/ImageBlock.java @@ -97,6 +97,25 @@ public Integer getMaxPixels() { return maxPixels; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ImageBlock)) { + return false; + } + ImageBlock that = (ImageBlock) o; + return Objects.equals(this.source, that.source) + && Objects.equals(this.minPixels, that.minPixels) + && Objects.equals(this.maxPixels, that.maxPixels); + } + + @Override + public int hashCode() { + return Objects.hash(this.source, this.minPixels, this.maxPixels); + } + /** * Creates a new builder for constructing ImageBlock instances. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/Msg.java b/agentscope-core/src/main/java/io/agentscope/core/message/Msg.java index f5c7e291c1..3924bfc406 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/Msg.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/Msg.java @@ -109,6 +109,29 @@ private Msg( this.timestamp = timestamp; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (!(o instanceof Msg)) { + return false; + } else { + Msg that = (Msg) o; + return Objects.equals(this.id, that.id) + && Objects.equals(this.name, that.name) + && this.role == that.role + && Objects.equals(this.content, that.content) + && Objects.equals(this.metadata, that.metadata) + && Objects.equals(this.timestamp, that.timestamp); + } + } + + @Override + public int hashCode() { + return Objects.hash( + this.id, this.name, this.role, this.content, this.metadata, this.timestamp); + } + /** * Creates a new message builder with a randomly generated ID. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/TextBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/TextBlock.java index 622be04920..85d9e837d8 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/TextBlock.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/TextBlock.java @@ -17,6 +17,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; /** * Represents plain text content in a message. @@ -56,6 +57,23 @@ public String toString() { return text; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (!(o instanceof TextBlock)) { + return false; + } else { + TextBlock that = (TextBlock) o; + return Objects.equals(this.text, that.text); + } + } + + @Override + public int hashCode() { + return Objects.hash(this.text); + } + /** * Creates a new builder for constructing TextBlock instances. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/ThinkingBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/ThinkingBlock.java index fc337fdb3b..71699695c2 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/ThinkingBlock.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/ThinkingBlock.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.HashMap; import java.util.Map; +import java.util.Objects; /** * Represents reasoning or thinking content in a message. @@ -81,6 +82,24 @@ public Map getMetadata() { return metadata; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ThinkingBlock)) { + return false; + } + ThinkingBlock that = (ThinkingBlock) o; + return Objects.equals(this.thinking, that.thinking) + && Objects.equals(this.metadata, that.metadata); + } + + @Override + public int hashCode() { + return Objects.hash(this.thinking, this.metadata); + } + /** * Creates a new builder for constructing ThinkingBlock instances. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/ToolResultBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/ToolResultBlock.java index f1caf26c38..fe85b4ce12 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/ToolResultBlock.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/ToolResultBlock.java @@ -22,6 +22,7 @@ import java.beans.Transient; import java.util.List; import java.util.Map; +import java.util.Objects; /** * Represents the result of a tool execution. @@ -289,6 +290,26 @@ public ToolResultBlock withIdAndName(String id, String name) { return new ToolResultBlock(id, name, this.output, this.metadata); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ToolResultBlock)) { + return false; + } + ToolResultBlock that = (ToolResultBlock) o; + return Objects.equals(this.id, that.id) + && Objects.equals(this.name, that.name) + && Objects.equals(this.output, that.output) + && Objects.equals(this.metadata, that.metadata); + } + + @Override + public int hashCode() { + return Objects.hash(this.id, this.name, this.output, this.metadata); + } + /** * Creates a new builder for constructing ToolResultBlock instances. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/ToolUseBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/ToolUseBlock.java index f83d792498..051600b2b7 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/ToolUseBlock.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/ToolUseBlock.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Objects; /** * Represents a tool use request within a message. @@ -144,6 +145,27 @@ public Map getMetadata() { return metadata; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ToolUseBlock)) { + return false; + } + ToolUseBlock that = (ToolUseBlock) o; + return Objects.equals(this.id, that.id) + && Objects.equals(this.name, that.name) + && Objects.equals(this.input, that.input) + && Objects.equals(this.content, that.content) + && Objects.equals(this.metadata, that.metadata); + } + + @Override + public int hashCode() { + return Objects.hash(this.id, this.name, this.input, this.content, this.metadata); + } + /** * Creates a new builder for constructing a ToolUseBlock. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/URLSource.java b/agentscope-core/src/main/java/io/agentscope/core/message/URLSource.java index 35d25e2b2e..00c0a48b53 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/URLSource.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/URLSource.java @@ -60,6 +60,23 @@ public String getUrl() { return url; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof URLSource)) { + return false; + } + URLSource that = (URLSource) o; + return Objects.equals(this.url, that.url); + } + + @Override + public int hashCode() { + return Objects.hash(this.url); + } + /** * Creates a new builder for constructing URLSource instances. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/VideoBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/VideoBlock.java index 09dc51cc98..a3733e1123 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/VideoBlock.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/VideoBlock.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; /** * Represents video content in a message. @@ -135,6 +136,34 @@ public Integer getTotalPixels() { return totalPixels; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof VideoBlock)) { + return false; + } + VideoBlock that = (VideoBlock) o; + return Objects.equals(this.source, that.source) + && Objects.equals(this.fps, that.fps) + && Objects.equals(this.maxFrames, that.maxFrames) + && Objects.equals(this.minPixels, that.minPixels) + && Objects.equals(this.maxPixels, that.maxPixels) + && Objects.equals(this.totalPixels, that.totalPixels); + } + + @Override + public int hashCode() { + return Objects.hash( + this.source, + this.fps, + this.maxFrames, + this.minPixels, + this.maxPixels, + this.totalPixels); + } + /** * Creates a new builder for constructing VideoBlock instances. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/ChatUsage.java b/agentscope-core/src/main/java/io/agentscope/core/model/ChatUsage.java index 556eedde7c..ee37dfed09 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/ChatUsage.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/ChatUsage.java @@ -15,6 +15,8 @@ */ package io.agentscope.core.model; +import java.util.Objects; + /** * Represents token usage information for chat completion responses. * @@ -76,6 +78,25 @@ public double getTime() { return time; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ChatUsage)) { + return false; + } + ChatUsage that = (ChatUsage) o; + return this.inputTokens == that.inputTokens + && this.outputTokens == that.outputTokens + && Double.compare(this.time, that.time) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(this.inputTokens, this.outputTokens, this.time); + } + /** * Creates a new builder for ChatUsage. * diff --git a/agentscope-core/src/main/java/io/agentscope/core/session/ListHashUtil.java b/agentscope-core/src/main/java/io/agentscope/core/session/ListHashUtil.java index 642d2424a7..e31b3d847c 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/session/ListHashUtil.java +++ b/agentscope-core/src/main/java/io/agentscope/core/session/ListHashUtil.java @@ -16,7 +16,6 @@ package io.agentscope.core.session; import io.agentscope.core.state.State; -import io.agentscope.core.util.JsonUtils; import java.util.List; /** @@ -89,7 +88,7 @@ public static String computeHash(List values) { for (int idx : sampleIndices) { State item = values.get(idx); - int itemHash = item != null ? JsonUtils.getJsonCodec().toJson(item).hashCode() : 0; + int itemHash = item != null ? item.hashCode() : 0; sb.append(idx).append(":").append(itemHash).append(","); } diff --git a/agentscope-core/src/test/java/io/agentscope/core/session/ListHashUtilTest.java b/agentscope-core/src/test/java/io/agentscope/core/session/ListHashUtilTest.java index 212cd4ded2..d526d8372e 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/session/ListHashUtilTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/session/ListHashUtilTest.java @@ -20,11 +20,16 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.agentscope.core.formatter.openai.dto.OpenAIReasoningDetail; +import io.agentscope.core.message.MessageMetadataKeys; import io.agentscope.core.message.Msg; import io.agentscope.core.message.MsgRole; import io.agentscope.core.message.TextBlock; +import io.agentscope.core.message.ThinkingBlock; +import io.agentscope.core.model.ChatUsage; import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; /** @@ -104,6 +109,91 @@ void testComputeHashListWithNullItem() { assertEquals(hash1, hash2); } + @Test + void testComputeHashEquivalentMessagesWithChatUsage() { + List first = + List.of( + Msg.builder() + .id("m-assistant-usage") + .timestamp("2026-05-08 14:00:01.000") + .role(MsgRole.ASSISTANT) + .content(TextBlock.builder().text("hello").build()) + .metadata( + Map.of( + MessageMetadataKeys.CHAT_USAGE, + ChatUsage.builder() + .inputTokens(10) + .outputTokens(20) + .time(1.5) + .build())) + .build()); + + List second = + List.of( + Msg.builder() + .id("m-assistant-usage") + .timestamp("2026-05-08 14:00:01.000") + .role(MsgRole.ASSISTANT) + .content(TextBlock.builder().text("hello").build()) + .metadata( + Map.of( + MessageMetadataKeys.CHAT_USAGE, + ChatUsage.builder() + .inputTokens(10) + .outputTokens(20) + .time(1.5) + .build())) + .build()); + + assertEquals(ListHashUtil.computeHash(first), ListHashUtil.computeHash(second)); + } + + @Test + void testComputeHashEquivalentThinkingBlocksWithReasoningDetails() { + OpenAIReasoningDetail detail = new OpenAIReasoningDetail(); + detail.setId("reasoning-1"); + detail.setType("reasoning.text"); + detail.setData("encrypted-data"); + detail.setText("visible reasoning"); + detail.setFormat("openai-responses-v1"); + detail.setIndex(0); + List first = + List.of( + Msg.builder() + .id("m-assistant-thinking") + .timestamp("2026-05-08 14:00:01.000") + .role(MsgRole.ASSISTANT) + .content( + ThinkingBlock.builder() + .thinking("thinking") + .metadata( + Map.of( + ThinkingBlock + .METADATA_REASONING_DETAILS, + List.of(detail))) + .build()) + .build()); + + List second = + List.of( + Msg.builder() + .id("m-assistant-thinking") + .timestamp("2026-05-08 14:00:01.000") + .role(MsgRole.ASSISTANT) + .content( + ThinkingBlock.builder() + .thinking("thinking") + .metadata( + Map.of( + ThinkingBlock + .METADATA_REASONING_DETAILS, + List.of(detail))) + .build()) + .build()); + + assertEquals(ListHashUtil.computeHash(first), ListHashUtil.computeHash(second)); + } + @Test void testComputeHashListModifiedDifferentHash() { List list = createMsgList(5); From cd4e0fe0dac973cc1a12c4d261650a86bd3ebdc5 Mon Sep 17 00:00:00 2001 From: VLooong Date: Wed, 13 May 2026 11:45:17 +0800 Subject: [PATCH 4/7] test(core): add test --- .../openai/dto/OpenAIReasoningDetail.java | 23 +++++++++++++++ .../dto/OpenAIMessageReasoningFieldTest.java | 28 +++++++++++++++++++ .../core/model/OpenAIChatModelTest.java | 16 +++++++++++ .../core/session/ListHashUtilTest.java | 22 +++++++++------ 4 files changed, 80 insertions(+), 9 deletions(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/dto/OpenAIReasoningDetail.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/dto/OpenAIReasoningDetail.java index ddce36886a..be277f48cb 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/dto/OpenAIReasoningDetail.java +++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/dto/OpenAIReasoningDetail.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; /** * OpenAI Reasoning Detail DTO (OpenRouter specific for Gemini). @@ -90,4 +91,26 @@ public Integer getIndex() { public void setIndex(Integer index) { this.index = index; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof OpenAIReasoningDetail)) { + return false; + } + OpenAIReasoningDetail that = (OpenAIReasoningDetail) o; + return Objects.equals(this.id, that.id) + && Objects.equals(this.type, that.type) + && Objects.equals(this.data, that.data) + && Objects.equals(this.text, that.text) + && Objects.equals(this.format, that.format) + && Objects.equals(this.index, that.index); + } + + @Override + public int hashCode() { + return Objects.hash(this.id, this.type, this.data, this.text, this.format, this.index); + } } diff --git a/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/dto/OpenAIMessageReasoningFieldTest.java b/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/dto/OpenAIMessageReasoningFieldTest.java index d40226e16b..f56fbdaf10 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/dto/OpenAIMessageReasoningFieldTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/dto/OpenAIMessageReasoningFieldTest.java @@ -16,6 +16,7 @@ package io.agentscope.core.formatter.openai.dto; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -168,4 +169,31 @@ void testDeserializeVllmStreamingChunk() { assertEquals("Step 1: parse the input...", delta.getReasoningContent()); } + + @Test + @DisplayName("Should compare reasoning details by value") + void testReasoningDetailEqualsAndHashCodeUseValues() { + OpenAIReasoningDetail first = reasoningDetail("reasoning.text"); + OpenAIReasoningDetail second = reasoningDetail("reasoning.text"); + OpenAIReasoningDetail different = reasoningDetail("reasoning.summary"); + + assertEquals(first, first); + assertNotEquals(first, null); + assertNotEquals(first, "other"); + assertEquals(first, second); + assertEquals(second, first); + assertEquals(first.hashCode(), second.hashCode()); + assertNotEquals(first, different); + } + + private OpenAIReasoningDetail reasoningDetail(String type) { + OpenAIReasoningDetail detail = new OpenAIReasoningDetail(); + detail.setId("reasoning-1"); + detail.setType(type); + detail.setData("encrypted-data"); + detail.setText("visible reasoning"); + detail.setFormat("openai-responses-v1"); + detail.setIndex(0); + return detail; + } } diff --git a/agentscope-core/src/test/java/io/agentscope/core/model/OpenAIChatModelTest.java b/agentscope-core/src/test/java/io/agentscope/core/model/OpenAIChatModelTest.java index 617d778907..79ae13185e 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/model/OpenAIChatModelTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/model/OpenAIChatModelTest.java @@ -17,6 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -248,6 +249,21 @@ void testGetModelName() { assertEquals("gpt-4", model.getModelName()); } + @Test + @DisplayName("Should compare chat usage by value") + void testChatUsageEqualsAndHashCodeUseValues() { + ChatUsage first = ChatUsage.builder().inputTokens(10).outputTokens(20).time(1.5).build(); + ChatUsage second = ChatUsage.builder().inputTokens(10).outputTokens(20).time(1.5).build(); + ChatUsage different = + ChatUsage.builder().inputTokens(10).outputTokens(21).time(1.5).build(); + + assertEquals(first, first); + assertNotEquals(first, null); + assertEquals(first, second); + assertEquals(first.hashCode(), second.hashCode()); + assertNotEquals(first, different); + } + @Test @DisplayName("Should build model with custom endpoint path") void testBuildModelWithEndpointPath() throws Exception { diff --git a/agentscope-core/src/test/java/io/agentscope/core/session/ListHashUtilTest.java b/agentscope-core/src/test/java/io/agentscope/core/session/ListHashUtilTest.java index d526d8372e..030a8cc836 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/session/ListHashUtilTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/session/ListHashUtilTest.java @@ -150,13 +150,6 @@ void testComputeHashEquivalentMessagesWithChatUsage() { @Test void testComputeHashEquivalentThinkingBlocksWithReasoningDetails() { - OpenAIReasoningDetail detail = new OpenAIReasoningDetail(); - detail.setId("reasoning-1"); - detail.setType("reasoning.text"); - detail.setData("encrypted-data"); - detail.setText("visible reasoning"); - detail.setFormat("openai-responses-v1"); - detail.setIndex(0); List first = List.of( Msg.builder() @@ -170,7 +163,7 @@ void testComputeHashEquivalentThinkingBlocksWithReasoningDetails() { Map.of( ThinkingBlock .METADATA_REASONING_DETAILS, - List.of(detail))) + List.of(createReasoningDetail()))) .build()) .build()); @@ -187,7 +180,7 @@ void testComputeHashEquivalentThinkingBlocksWithReasoningDetails() { Map.of( ThinkingBlock .METADATA_REASONING_DETAILS, - List.of(detail))) + List.of(createReasoningDetail()))) .build()) .build()); @@ -344,4 +337,15 @@ private List createMsgList(int size) { } return list; } + + private OpenAIReasoningDetail createReasoningDetail() { + OpenAIReasoningDetail detail = new OpenAIReasoningDetail(); + detail.setId("reasoning-1"); + detail.setType("reasoning.text"); + detail.setData("encrypted-data"); + detail.setText("visible reasoning"); + detail.setFormat("openai-responses-v1"); + detail.setIndex(0); + return detail; + } } From a94bbb03adc41cd380a91b1ff00415500ffcc273 Mon Sep 17 00:00:00 2001 From: VLooong Date: Wed, 13 May 2026 12:22:36 +0800 Subject: [PATCH 5/7] test(core): add Msg test --- .../io/agentscope/core/message/MsgTest.java | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/agentscope-core/src/test/java/io/agentscope/core/message/MsgTest.java b/agentscope-core/src/test/java/io/agentscope/core/message/MsgTest.java index 23fafec953..6a6cb5dbd1 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/message/MsgTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/message/MsgTest.java @@ -16,9 +16,11 @@ package io.agentscope.core.message; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; @@ -205,4 +207,167 @@ void testTextContentConvenienceMethod() { assertTrue(msg.getFirstContentBlock() instanceof TextBlock); assertEquals("Hello World", ((TextBlock) msg.getFirstContentBlock()).getText()); } + + @Test + void testContentBlocksEqualsAndHashCodeUseValues() { + assertValueEquality( + TextBlock.builder().text("hello").build(), + TextBlock.builder().text("hello").build(), + TextBlock.builder().text("different").build()); + + assertValueEquality( + URLSource.builder().url("https://example.com/image.png").build(), + URLSource.builder().url("https://example.com/image.png").build(), + URLSource.builder().url("https://example.com/other.png").build()); + + assertValueEquality( + Base64Source.builder().mediaType("image/png").data("abc").build(), + Base64Source.builder().mediaType("image/png").data("abc").build(), + Base64Source.builder().mediaType("image/png").data("def").build()); + + assertValueEquality( + ImageBlock.builder() + .source(URLSource.builder().url("https://example.com/image.png").build()) + .minPixels(128) + .maxPixels(512) + .build(), + ImageBlock.builder() + .source(URLSource.builder().url("https://example.com/image.png").build()) + .minPixels(128) + .maxPixels(512) + .build(), + ImageBlock.builder() + .source(URLSource.builder().url("https://example.com/image.png").build()) + .minPixels(128) + .maxPixels(1024) + .build()); + + assertValueEquality( + AudioBlock.builder() + .source(Base64Source.builder().mediaType("audio/mp3").data("abc").build()) + .build(), + AudioBlock.builder() + .source(Base64Source.builder().mediaType("audio/mp3").data("abc").build()) + .build(), + AudioBlock.builder() + .source(Base64Source.builder().mediaType("audio/mp3").data("def").build()) + .build()); + + assertValueEquality( + VideoBlock.builder() + .source(URLSource.builder().url("https://example.com/video.mp4").build()) + .fps(2.0f) + .maxFrames(12) + .minPixels(128) + .maxPixels(512) + .totalPixels(2048) + .build(), + VideoBlock.builder() + .source(URLSource.builder().url("https://example.com/video.mp4").build()) + .fps(2.0f) + .maxFrames(12) + .minPixels(128) + .maxPixels(512) + .totalPixels(2048) + .build(), + VideoBlock.builder() + .source(URLSource.builder().url("https://example.com/video.mp4").build()) + .fps(2.0f) + .maxFrames(12) + .minPixels(128) + .maxPixels(512) + .totalPixels(4096) + .build()); + + assertValueEquality( + ThinkingBlock.builder().thinking("thinking").metadata(Map.of("k", "v")).build(), + ThinkingBlock.builder().thinking("thinking").metadata(Map.of("k", "v")).build(), + ThinkingBlock.builder() + .thinking("thinking") + .metadata(Map.of("k", "different")) + .build()); + + assertValueEquality( + ToolUseBlock.builder() + .id("call-1") + .name("search") + .input(Map.of("query", "agent")) + .content("{\"query\":\"agent\"}") + .metadata(Map.of("signature", "sig-1")) + .build(), + ToolUseBlock.builder() + .id("call-1") + .name("search") + .input(Map.of("query", "agent")) + .content("{\"query\":\"agent\"}") + .metadata(Map.of("signature", "sig-1")) + .build(), + ToolUseBlock.builder() + .id("call-1") + .name("search") + .input(Map.of("query", "agent")) + .content("{\"query\":\"agent\"}") + .metadata(Map.of("signature", "sig-2")) + .build()); + + assertValueEquality( + ToolResultBlock.of( + "call-1", + "search", + List.of(TextBlock.builder().text("result").build()), + Map.of("status", "ok")), + ToolResultBlock.of( + "call-1", + "search", + List.of(TextBlock.builder().text("result").build()), + Map.of("status", "ok")), + ToolResultBlock.of( + "call-1", + "search", + List.of(TextBlock.builder().text("result").build()), + Map.of("status", "different"))); + } + + @Test + void testMsgEqualsAndHashCodeUseValues() { + Msg first = + Msg.builder() + .id("msg-1") + .name("assistant") + .role(MsgRole.ASSISTANT) + .content(TextBlock.builder().text("hello").build()) + .metadata(Map.of("k", "v")) + .timestamp("2026-05-13 11:00:00.000") + .build(); + Msg second = + Msg.builder() + .id("msg-1") + .name("assistant") + .role(MsgRole.ASSISTANT) + .content(TextBlock.builder().text("hello").build()) + .metadata(Map.of("k", "v")) + .timestamp("2026-05-13 11:00:00.000") + .build(); + Msg different = + Msg.builder() + .id("msg-1") + .name("assistant") + .role(MsgRole.ASSISTANT) + .content(TextBlock.builder().text("hello").build()) + .metadata(Map.of("k", "v")) + .timestamp("2026-05-13 11:00:01.000") + .build(); + + assertValueEquality(first, second, different); + } + + private void assertValueEquality(Object first, Object second, Object different) { + assertEquals(first, first); + assertNotEquals(first, null); + assertNotEquals(first, "other"); + assertEquals(first, second); + assertEquals(second, first); + assertEquals(first.hashCode(), second.hashCode()); + assertNotEquals(first, different); + } } From 32c03f1a12acfa75613b7e889f60c825c67c3e8f Mon Sep 17 00:00:00 2001 From: VLooong Date: Tue, 26 May 2026 20:53:46 +0800 Subject: [PATCH 6/7] fix(core): stabilize ToolUseBlock hashCode for Gemini thoughtSignature --- .../gemini/GeminiMessageConverter.java | 15 ++++- .../gemini/GeminiResponseParser.java | 5 +- .../agentscope/core/message/ToolUseBlock.java | 27 ++++++-- .../accumulator/ToolCallsAccumulatorTest.java | 12 ++-- .../gemini/GeminiMessageConverterTest.java | 44 ++++++++++++- .../gemini/GeminiResponseParserTest.java | 11 ++-- .../core/message/ToolUseBlockTest.java | 66 +++++++++++++++++++ 7 files changed, 160 insertions(+), 20 deletions(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMessageConverter.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMessageConverter.java index 34cd30dce3..a373e3bd44 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMessageConverter.java +++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMessageConverter.java @@ -122,13 +122,22 @@ public List convertMessages(List msgs) { // Build Part with FunctionCall and optional thought signature Part.Builder partBuilder = Part.builder().functionCall(functionCall); - // Check for thought signature in metadata + // Check for thought signature in metadata (always stored as Base64 String, + // see ToolUseBlock#normalizeMetadata) Map metadata = tub.getMetadata(); if (metadata != null && metadata.containsKey(ToolUseBlock.METADATA_THOUGHT_SIGNATURE)) { Object signature = metadata.get(ToolUseBlock.METADATA_THOUGHT_SIGNATURE); - if (signature instanceof byte[]) { - partBuilder.thoughtSignature((byte[]) signature); + if (signature instanceof String encodedSignature + && !encodedSignature.isEmpty()) { + try { + partBuilder.thoughtSignature( + Base64.getDecoder().decode(encodedSignature)); + } catch (IllegalArgumentException e) { + log.warn( + "Invalid Base64 thought signature in ToolUseBlock metadata," + + " skipping"); + } } } diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiResponseParser.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiResponseParser.java index de879e408d..fe56eb5c13 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiResponseParser.java +++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiResponseParser.java @@ -32,6 +32,7 @@ import java.time.Duration; import java.time.Instant; import java.util.ArrayList; +import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -199,7 +200,9 @@ protected void parseToolCall( Map metadata = null; if (thoughtSignature != null) { metadata = new HashMap<>(); - metadata.put(ToolUseBlock.METADATA_THOUGHT_SIGNATURE, thoughtSignature); + metadata.put( + ToolUseBlock.METADATA_THOUGHT_SIGNATURE, + Base64.getEncoder().encodeToString(thoughtSignature)); } blocks.add( diff --git a/agentscope-core/src/main/java/io/agentscope/core/message/ToolUseBlock.java b/agentscope-core/src/main/java/io/agentscope/core/message/ToolUseBlock.java index 051600b2b7..f14d55b458 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/message/ToolUseBlock.java +++ b/agentscope-core/src/main/java/io/agentscope/core/message/ToolUseBlock.java @@ -17,6 +17,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -34,7 +35,7 @@ */ public final class ToolUseBlock extends ContentBlock { - /** Metadata key for Gemini thought signature (byte[] value). */ + /** Metadata key for provider thought signatures (String value). */ public static final String METADATA_THOUGHT_SIGNATURE = "thoughtSignature"; private final String id; @@ -94,7 +95,25 @@ public ToolUseBlock( this.metadata = metadata == null ? Collections.emptyMap() - : Collections.unmodifiableMap(new HashMap<>(metadata)); + : Collections.unmodifiableMap(normalizeMetadata(metadata)); + } + + /** + * Normalizes metadata values to ensure stable {@link #equals(Object)} and {@link #hashCode()} + * across serialization round-trips. + * + *

Specifically, a {@code byte[]} stored under {@link #METADATA_THOUGHT_SIGNATURE} is + * converted to a Base64-encoded {@code String}, because {@code byte[]} relies on identity + * equality and would otherwise cause two semantically equal blocks to hash differently after + * deserialization. + */ + private static Map normalizeMetadata(Map metadata) { + Map copy = new HashMap<>(metadata); + Object signature = copy.get(METADATA_THOUGHT_SIGNATURE); + if (signature instanceof byte[] bytes) { + copy.put(METADATA_THOUGHT_SIGNATURE, Base64.getEncoder().encodeToString(bytes)); + } + return copy; } /** @@ -136,7 +155,7 @@ public String getContent() { /** * Gets the provider-specific metadata. * - *

For Gemini, this may contain the thought signature under the key + *

For Gemini, this may contain a Base64-encoded thought signature under the key * {@link #METADATA_THOUGHT_SIGNATURE}. * * @return The metadata map, or an empty map if not set @@ -233,7 +252,7 @@ public Builder content(String content) { * Sets the provider-specific metadata. * *

For Gemini, use {@link ToolUseBlock#METADATA_THOUGHT_SIGNATURE} as the key - * to store thought signatures. + * to store Base64-encoded thought signatures. * * @param metadata The metadata map * @return This builder for chaining diff --git a/agentscope-core/src/test/java/io/agentscope/core/agent/accumulator/ToolCallsAccumulatorTest.java b/agentscope-core/src/test/java/io/agentscope/core/agent/accumulator/ToolCallsAccumulatorTest.java index 3e54465266..3191bc9fce 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/agent/accumulator/ToolCallsAccumulatorTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/agent/accumulator/ToolCallsAccumulatorTest.java @@ -15,13 +15,13 @@ */ package io.agentscope.core.agent.accumulator; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import io.agentscope.core.message.ToolUseBlock; +import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -46,7 +46,7 @@ void setUp() { @DisplayName("Should accumulate metadata from tool call chunks") void testAccumulateMetadata() { // First chunk with thoughtSignature - byte[] signature = "test-thought-signature".getBytes(); + String signature = Base64.getEncoder().encodeToString("test-thought-signature".getBytes()); Map metadata = new HashMap<>(); metadata.put(ToolUseBlock.METADATA_THOUGHT_SIGNATURE, signature); @@ -77,9 +77,8 @@ void testAccumulateMetadata() { // Verify metadata is preserved assertNotNull(toolCall.getMetadata()); assertTrue(toolCall.getMetadata().containsKey(ToolUseBlock.METADATA_THOUGHT_SIGNATURE)); - assertArrayEquals( - signature, - (byte[]) toolCall.getMetadata().get(ToolUseBlock.METADATA_THOUGHT_SIGNATURE)); + assertEquals( + signature, toolCall.getMetadata().get(ToolUseBlock.METADATA_THOUGHT_SIGNATURE)); } @Test @@ -107,7 +106,7 @@ void testAccumulateWithoutMetadata() { @DisplayName("Should handle parallel tool calls with different metadata") void testParallelToolCallsWithMetadata() { // First tool call with metadata - byte[] sig1 = "sig-1".getBytes(); + String sig1 = Base64.getEncoder().encodeToString("sig-1".getBytes()); Map metadata1 = new HashMap<>(); metadata1.put(ToolUseBlock.METADATA_THOUGHT_SIGNATURE, sig1); @@ -139,6 +138,7 @@ void testParallelToolCallsWithMetadata() { result.stream().filter(t -> "call_a".equals(t.getId())).findFirst().orElse(null); assertNotNull(resultA); assertTrue(resultA.getMetadata().containsKey(ToolUseBlock.METADATA_THOUGHT_SIGNATURE)); + assertEquals(sig1, resultA.getMetadata().get(ToolUseBlock.METADATA_THOUGHT_SIGNATURE)); // Second call should not have metadata ToolUseBlock resultB = diff --git a/agentscope-core/src/test/java/io/agentscope/core/formatter/gemini/GeminiMessageConverterTest.java b/agentscope-core/src/test/java/io/agentscope/core/formatter/gemini/GeminiMessageConverterTest.java index f65dd4f292..1a236ef4a7 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/formatter/gemini/GeminiMessageConverterTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/formatter/gemini/GeminiMessageConverterTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -770,8 +771,9 @@ void testConvertToolUseBlockWithThoughtSignature() { input.put("query", "test"); byte[] thoughtSignature = "test-signature".getBytes(); + String encodedSignature = Base64.getEncoder().encodeToString(thoughtSignature); Map metadata = new HashMap<>(); - metadata.put(ToolUseBlock.METADATA_THOUGHT_SIGNATURE, thoughtSignature); + metadata.put(ToolUseBlock.METADATA_THOUGHT_SIGNATURE, encodedSignature); ToolUseBlock toolUseBlock = ToolUseBlock.builder() @@ -804,6 +806,43 @@ void testConvertToolUseBlockWithThoughtSignature() { assertArrayEquals(thoughtSignature, part.thoughtSignature().get()); } + @Test + @DisplayName("Should normalize legacy byte[] thoughtSignature metadata to Base64 String") + void testConvertToolUseBlockWithLegacyByteArrayThoughtSignature() { + Map input = new HashMap<>(); + input.put("query", "test"); + + byte[] thoughtSignature = "legacy-signature".getBytes(); + Map metadata = new HashMap<>(); + metadata.put(ToolUseBlock.METADATA_THOUGHT_SIGNATURE, thoughtSignature); + + ToolUseBlock toolUseBlock = + ToolUseBlock.builder() + .id("call_with_legacy_sig") + .name("search") + .input(input) + .metadata(metadata) + .build(); + + // Constructor normalizes byte[] to Base64 String so equals/hashCode stay stable + Object stored = toolUseBlock.getMetadata().get(ToolUseBlock.METADATA_THOUGHT_SIGNATURE); + assertInstanceOf(String.class, stored); + assertEquals(Base64.getEncoder().encodeToString(thoughtSignature), stored); + + Msg msg = + Msg.builder() + .name("assistant") + .content(List.of(toolUseBlock)) + .role(MsgRole.ASSISTANT) + .build(); + + List result = converter.convertMessages(List.of(msg)); + + Part part = result.get(0).parts().get().get(0); + assertTrue(part.thoughtSignature().isPresent()); + assertArrayEquals(thoughtSignature, part.thoughtSignature().get()); + } + @Test @DisplayName("Should convert ToolUseBlock without thoughtSignature") void testConvertToolUseBlockWithoutThoughtSignature() { @@ -842,8 +881,9 @@ void testThoughtSignatureRoundTrip() { input.put("location", "Tokyo"); byte[] signature = "gemini3-thought-sig-abc123".getBytes(); + String encodedSignature = Base64.getEncoder().encodeToString(signature); Map metadata = new HashMap<>(); - metadata.put(ToolUseBlock.METADATA_THOUGHT_SIGNATURE, signature); + metadata.put(ToolUseBlock.METADATA_THOUGHT_SIGNATURE, encodedSignature); // Simulate assistant message with tool call (as would be constructed from parsed response) ToolUseBlock toolUseBlock = diff --git a/agentscope-core/src/test/java/io/agentscope/core/formatter/gemini/GeminiResponseParserTest.java b/agentscope-core/src/test/java/io/agentscope/core/formatter/gemini/GeminiResponseParserTest.java index 542d979869..82029ac734 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/formatter/gemini/GeminiResponseParserTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/formatter/gemini/GeminiResponseParserTest.java @@ -15,7 +15,6 @@ */ package io.agentscope.core.formatter.gemini; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -34,6 +33,7 @@ import io.agentscope.core.model.ChatResponse; import io.agentscope.core.model.ChatUsage; import java.time.Instant; +import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -353,9 +353,9 @@ void testParseToolCallWithThoughtSignature() { // Verify thought signature is stored in metadata assertNotNull(toolUse.getMetadata()); assertTrue(toolUse.getMetadata().containsKey(ToolUseBlock.METADATA_THOUGHT_SIGNATURE)); - byte[] extractedSig = - (byte[]) toolUse.getMetadata().get(ToolUseBlock.METADATA_THOUGHT_SIGNATURE); - assertArrayEquals(thoughtSignature, extractedSig); + String extractedSig = + (String) toolUse.getMetadata().get(ToolUseBlock.METADATA_THOUGHT_SIGNATURE); + assertEquals(Base64.getEncoder().encodeToString(thoughtSignature), extractedSig); } @Test @@ -432,6 +432,9 @@ void testParseParallelFunctionCallsWithThoughtSignature() { ToolUseBlock toolUse1 = (ToolUseBlock) chatResponse.getContent().get(0); assertEquals("call-1", toolUse1.getId()); assertTrue(toolUse1.getMetadata().containsKey(ToolUseBlock.METADATA_THOUGHT_SIGNATURE)); + assertEquals( + Base64.getEncoder().encodeToString(thoughtSignature), + toolUse1.getMetadata().get(ToolUseBlock.METADATA_THOUGHT_SIGNATURE)); // Second tool call should not have signature ToolUseBlock toolUse2 = (ToolUseBlock) chatResponse.getContent().get(1); diff --git a/agentscope-core/src/test/java/io/agentscope/core/message/ToolUseBlockTest.java b/agentscope-core/src/test/java/io/agentscope/core/message/ToolUseBlockTest.java index f91a51958e..c217bbe3b9 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/message/ToolUseBlockTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/message/ToolUseBlockTest.java @@ -17,11 +17,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Base64; +import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; @@ -185,6 +188,69 @@ void testRoundTripSerialization() throws JsonProcessingException { assertEquals(original.getMetadata(), deserialized.getMetadata()); } + @Test + void testThoughtSignatureStringRoundTripKeepsValueHashCode() throws JsonProcessingException { + ToolUseBlock original = + ToolUseBlock.builder() + .id("tool-gemini") + .name("search") + .input(Map.of("query", "agent")) + .metadata(Map.of(ToolUseBlock.METADATA_THOUGHT_SIGNATURE, "dGVzdA==")) + .build(); + + String json = objectMapper.writeValueAsString(original); + ToolUseBlock deserialized = objectMapper.readValue(json, ToolUseBlock.class); + + assertEquals(original, deserialized); + assertEquals(original.hashCode(), deserialized.hashCode()); + } + + @Test + void testConstructorNormalizesByteArrayThoughtSignatureToBase64String() { + byte[] rawSignature = "raw-thought-signature".getBytes(); + Map metadata = new HashMap<>(); + metadata.put(ToolUseBlock.METADATA_THOUGHT_SIGNATURE, rawSignature); + + ToolUseBlock block = + ToolUseBlock.builder() + .id("tool-bytes") + .name("search") + .input(Map.of("query", "agent")) + .metadata(metadata) + .build(); + + Object stored = block.getMetadata().get(ToolUseBlock.METADATA_THOUGHT_SIGNATURE); + assertInstanceOf(String.class, stored); + assertEquals(Base64.getEncoder().encodeToString(rawSignature), stored); + } + + @Test + void testByteArrayAndBase64StringMetadataProduceEqualBlocks() { + byte[] rawSignature = "same-signature".getBytes(); + String encoded = Base64.getEncoder().encodeToString(rawSignature); + + Map bytesMetadata = new HashMap<>(); + bytesMetadata.put(ToolUseBlock.METADATA_THOUGHT_SIGNATURE, rawSignature); + ToolUseBlock fromBytes = + ToolUseBlock.builder() + .id("tool-id") + .name("search") + .input(Map.of("query", "agent")) + .metadata(bytesMetadata) + .build(); + + ToolUseBlock fromString = + ToolUseBlock.builder() + .id("tool-id") + .name("search") + .input(Map.of("query", "agent")) + .metadata(Map.of(ToolUseBlock.METADATA_THOUGHT_SIGNATURE, encoded)) + .build(); + + assertEquals(fromString, fromBytes); + assertEquals(fromString.hashCode(), fromBytes.hashCode()); + } + @Test void testInputMapIsUnmodifiable() { Map inputMap = Map.of("key1", "value1", "key2", "value2"); From 628b3a05e85afcea8e152e86db5115f5e985c1a1 Mon Sep 17 00:00:00 2001 From: VLooong Date: Thu, 4 Jun 2026 18:56:57 +0800 Subject: [PATCH 7/7] fix: variable name updated --- .../src/main/java/io/agentscope/core/tool/ToolCallParam.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/ToolCallParam.java b/agentscope-core/src/main/java/io/agentscope/core/tool/ToolCallParam.java index 1eb7b01e58..0596f1ecb6 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/tool/ToolCallParam.java +++ b/agentscope-core/src/main/java/io/agentscope/core/tool/ToolCallParam.java @@ -171,7 +171,7 @@ private Builder(ToolCallParam source) { this.toolUseBlock = source.toolUseBlock; this.input = source.input.isEmpty() ? null : source.input; this.agent = source.agent; - this.context = source.context; + this.runtimeContext = source.runtimeContext; this.emitter = source.emitter; }