From 0d4bbd662dc2712e45b4face3f6806898dcc3101 Mon Sep 17 00:00:00 2001 From: VLooong Date: Sat, 9 May 2026 16:47:21 +0800 Subject: [PATCH 1/5] 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 e31b3d847..642d2424a 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 fd1811df6..9b35c0da6 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/5] 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 dfd10e20b..212cd4ded 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 9b35c0da6..fd1811df6 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/5] 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 02f102b5a..0d8f8cf78 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 35408aec3..beafd3c15 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 b57866f02..c1f41ba07 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 f5c7e291c..3924bfc40 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 622be0492..85d9e837d 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 fc337fdb3..71699695c 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 f1caf26c3..fe85b4ce1 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 f83d79249..051600b2b 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 35d25e2b2..00c0a48b5 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 09dc51cc9..a3733e112 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 556eedde7..ee37dfed0 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 642d2424a..e31b3d847 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 212cd4ded..d526d8372 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/5] 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 ddce36886..be277f48c 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 d40226e16..f56fbdaf1 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 617d77890..79ae13185 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 d526d8372..030a8cc83 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/5] 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 23fafec95..6a6cb5dbd 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); + } }