From 42f4043a49803f7b00325a4185892dd799da7fd1 Mon Sep 17 00:00:00 2001
From: artboy <80608452+itxaiohanglover@users.noreply.github.com>
Date: Wed, 13 May 2026 21:41:17 +0800
Subject: [PATCH 1/5] fix(tool): make DefaultToolEmitter public and add
getToolUseBlock() getter
---
.../core/tool/DefaultToolEmitter.java | 21 +++++++++++++++----
1 file changed, 17 insertions(+), 4 deletions(-)
diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/DefaultToolEmitter.java b/agentscope-core/src/main/java/io/agentscope/core/tool/DefaultToolEmitter.java
index d003893fc..8121a865c 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/tool/DefaultToolEmitter.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/tool/DefaultToolEmitter.java
@@ -22,10 +22,14 @@
/**
* Default implementation of ToolEmitter that delivers chunks to a callback.
*
- *
This class is package-private and created internally by the framework when a tool method
- * declares a ToolEmitter parameter. Each tool invocation gets its own DefaultToolEmitter instance.
+ *
Created internally by the framework when a tool method declares a ToolEmitter parameter. Each
+ * tool invocation gets its own DefaultToolEmitter instance.
+ *
+ *
Users can cast a {@link ToolEmitter} to {@code DefaultToolEmitter} to access the underlying
+ * {@link ToolUseBlock} (e.g. to obtain the {@code toolCallId} for asynchronous session restoration
+ * after a {@link ToolSuspendException}).
*/
-class DefaultToolEmitter implements ToolEmitter {
+public class DefaultToolEmitter implements ToolEmitter {
private final ToolUseBlock toolUseBlock;
private final BiConsumer chunkCallback;
@@ -36,12 +40,21 @@ class DefaultToolEmitter implements ToolEmitter {
* @param toolUseBlock The tool use block identifying the tool call
* @param chunkCallback Callback to deliver chunks to hooks (may be null)
*/
- DefaultToolEmitter(
+ public DefaultToolEmitter(
ToolUseBlock toolUseBlock, BiConsumer chunkCallback) {
this.toolUseBlock = toolUseBlock;
this.chunkCallback = chunkCallback;
}
+ /**
+ * Returns the tool use block containing the tool call ID and metadata.
+ *
+ * @return the tool use block
+ */
+ public ToolUseBlock getToolUseBlock() {
+ return toolUseBlock;
+ }
+
/**
* Emits a tool result chunk to the registered callback.
*
From 52a08ea16fa2bd17fee91c3119aa56f414e1a980 Mon Sep 17 00:00:00 2001
From: artboy <80608452+itxaiohanglover@users.noreply.github.com>
Date: Wed, 13 May 2026 21:41:37 +0800
Subject: [PATCH 2/5] test: add Issue1349ReproduceTest in external package
---
.../reproduce/Issue1349ReproduceTest.java | 80 +++++++++++++++++++
1 file changed, 80 insertions(+)
create mode 100644 agentscope-core/src/test/java/io/agentscope/reproduce/Issue1349ReproduceTest.java
diff --git a/agentscope-core/src/test/java/io/agentscope/reproduce/Issue1349ReproduceTest.java b/agentscope-core/src/test/java/io/agentscope/reproduce/Issue1349ReproduceTest.java
new file mode 100644
index 000000000..29c274446
--- /dev/null
+++ b/agentscope-core/src/test/java/io/agentscope/reproduce/Issue1349ReproduceTest.java
@@ -0,0 +1,80 @@
+/*
+ * Reproduce test for Issue #1349:
+ * DefaultToolEmitter is package-private, users outside io.agentscope.core.tool
+ * cannot cast ToolEmitter to DefaultToolEmitter to access toolCallId.
+ */
+package io.agentscope.reproduce;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.agentscope.core.message.ToolResultBlock;
+import io.agentscope.core.message.ToolUseBlock;
+import io.agentscope.core.tool.DefaultToolEmitter;
+import io.agentscope.core.tool.ToolEmitter;
+import java.util.Map;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class Issue1349ReproduceTest {
+
+ @Test
+ @DisplayName(
+ "Issue #1349: External code should be able to cast ToolEmitter to DefaultToolEmitter"
+ + " and get toolCallId")
+ void testCanAccessDefaultToolEmitterFromExternalPackage() {
+ // Simulate what the framework does internally:
+ // Create a ToolUseBlock with a toolCallId
+ ToolUseBlock toolUseBlock =
+ ToolUseBlock.builder()
+ .id("call_abc123")
+ .name("my_tool")
+ .input(Map.of())
+ .content("{}")
+ .build();
+
+ // Create a DefaultToolEmitter (as ToolExecutor does internally)
+ ToolEmitter emitter = new DefaultToolEmitter(toolUseBlock, null);
+
+ // === The actual problem: external user needs toolCallId for async session restoration ===
+ // Before fix: this line would NOT compile because DefaultToolEmitter is package-private
+ // After fix: we can cast and access the ToolUseBlock
+ assertTrue(
+ emitter instanceof DefaultToolEmitter,
+ "Should be able to check instanceof DefaultToolEmitter from external package");
+
+ DefaultToolEmitter defaultEmitter = (DefaultToolEmitter) emitter;
+ assertNotNull(
+ defaultEmitter.getToolUseBlock(),
+ "Should be able to call getToolUseBlock() from external package");
+ assertEquals(
+ "call_abc123",
+ defaultEmitter.getToolUseBlock().getId(),
+ "Should be able to access toolCallId from external package");
+ }
+
+ @Test
+ @DisplayName("Issue #1349: Emit should still work after making class public")
+ void testEmitStillWorksAfterFix() {
+ ToolUseBlock toolUseBlock =
+ ToolUseBlock.builder()
+ .id("call_xyz")
+ .name("test_tool")
+ .input(Map.of())
+ .content("{}")
+ .build();
+
+ final boolean[] callbackFired = {false};
+ DefaultToolEmitter emitter =
+ new DefaultToolEmitter(
+ toolUseBlock,
+ (useBlock, chunk) -> {
+ callbackFired[0] = true;
+ assertEquals("call_xyz", useBlock.getId());
+ });
+
+ emitter.emit(ToolResultBlock.text("progress update"));
+ assertTrue(callbackFired[0], "emit() callback should fire correctly");
+ }
+}
From 6f2c556c08900b83ecede165c78589a9414e27e0 Mon Sep 17 00:00:00 2001
From: artboy <80608452+itxaiohanglover@users.noreply.github.com>
Date: Wed, 13 May 2026 21:50:13 +0800
Subject: [PATCH 3/5] chore: remove test file (license header issue)
---
.../reproduce/Issue1349ReproduceTest.java | 80 -------------------
1 file changed, 80 deletions(-)
delete mode 100644 agentscope-core/src/test/java/io/agentscope/reproduce/Issue1349ReproduceTest.java
diff --git a/agentscope-core/src/test/java/io/agentscope/reproduce/Issue1349ReproduceTest.java b/agentscope-core/src/test/java/io/agentscope/reproduce/Issue1349ReproduceTest.java
deleted file mode 100644
index 29c274446..000000000
--- a/agentscope-core/src/test/java/io/agentscope/reproduce/Issue1349ReproduceTest.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Reproduce test for Issue #1349:
- * DefaultToolEmitter is package-private, users outside io.agentscope.core.tool
- * cannot cast ToolEmitter to DefaultToolEmitter to access toolCallId.
- */
-package io.agentscope.reproduce;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import io.agentscope.core.message.ToolResultBlock;
-import io.agentscope.core.message.ToolUseBlock;
-import io.agentscope.core.tool.DefaultToolEmitter;
-import io.agentscope.core.tool.ToolEmitter;
-import java.util.Map;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-
-class Issue1349ReproduceTest {
-
- @Test
- @DisplayName(
- "Issue #1349: External code should be able to cast ToolEmitter to DefaultToolEmitter"
- + " and get toolCallId")
- void testCanAccessDefaultToolEmitterFromExternalPackage() {
- // Simulate what the framework does internally:
- // Create a ToolUseBlock with a toolCallId
- ToolUseBlock toolUseBlock =
- ToolUseBlock.builder()
- .id("call_abc123")
- .name("my_tool")
- .input(Map.of())
- .content("{}")
- .build();
-
- // Create a DefaultToolEmitter (as ToolExecutor does internally)
- ToolEmitter emitter = new DefaultToolEmitter(toolUseBlock, null);
-
- // === The actual problem: external user needs toolCallId for async session restoration ===
- // Before fix: this line would NOT compile because DefaultToolEmitter is package-private
- // After fix: we can cast and access the ToolUseBlock
- assertTrue(
- emitter instanceof DefaultToolEmitter,
- "Should be able to check instanceof DefaultToolEmitter from external package");
-
- DefaultToolEmitter defaultEmitter = (DefaultToolEmitter) emitter;
- assertNotNull(
- defaultEmitter.getToolUseBlock(),
- "Should be able to call getToolUseBlock() from external package");
- assertEquals(
- "call_abc123",
- defaultEmitter.getToolUseBlock().getId(),
- "Should be able to access toolCallId from external package");
- }
-
- @Test
- @DisplayName("Issue #1349: Emit should still work after making class public")
- void testEmitStillWorksAfterFix() {
- ToolUseBlock toolUseBlock =
- ToolUseBlock.builder()
- .id("call_xyz")
- .name("test_tool")
- .input(Map.of())
- .content("{}")
- .build();
-
- final boolean[] callbackFired = {false};
- DefaultToolEmitter emitter =
- new DefaultToolEmitter(
- toolUseBlock,
- (useBlock, chunk) -> {
- callbackFired[0] = true;
- assertEquals("call_xyz", useBlock.getId());
- });
-
- emitter.emit(ToolResultBlock.text("progress update"));
- assertTrue(callbackFired[0], "emit() callback should fire correctly");
- }
-}
From 36f68f16d4817d0337f55b7c61f219d39429a2ad Mon Sep 17 00:00:00 2001
From: artboy <80608452+itxaiohanglover@users.noreply.github.com>
Date: Thu, 14 May 2026 09:51:40 +0800
Subject: [PATCH 4/5] test: add DefaultToolEmitterTest for getToolUseBlock()
coverage
---
.../core/tool/DefaultToolEmitterTest.java | 83 +++++++++++++++++++
1 file changed, 83 insertions(+)
create mode 100644 agentscope-core/src/test/java/io/agentscope/core/tool/DefaultToolEmitterTest.java
diff --git a/agentscope-core/src/test/java/io/agentscope/core/tool/DefaultToolEmitterTest.java b/agentscope-core/src/test/java/io/agentscope/core/tool/DefaultToolEmitterTest.java
new file mode 100644
index 000000000..3a08b69db
--- /dev/null
+++ b/agentscope-core/src/test/java/io/agentscope/core/tool/DefaultToolEmitterTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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.tool;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import io.agentscope.core.message.ToolResultBlock;
+import io.agentscope.core.message.ToolUseBlock;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class DefaultToolEmitterTest {
+
+ @Test
+ void testGetToolUseBlock() {
+ ToolUseBlock toolUseBlock =
+ ToolUseBlock.builder()
+ .id("call_test")
+ .name("test_tool")
+ .input(Map.of())
+ .content("{}")
+ .build();
+
+ DefaultToolEmitter emitter = new DefaultToolEmitter(toolUseBlock, null);
+
+ assertNotNull(emitter.getToolUseBlock());
+ assertEquals("call_test", emitter.getToolUseBlock().getId());
+ assertEquals("test_tool", emitter.getToolUseBlock().getName());
+ }
+
+ @Test
+ void testEmitWithNullCallbackDoesNotThrow() {
+ ToolUseBlock toolUseBlock =
+ ToolUseBlock.builder().id("x").name("y").input(Map.of()).content("{}").build();
+
+ DefaultToolEmitter emitter = new DefaultToolEmitter(toolUseBlock, null);
+ emitter.emit(ToolResultBlock.text("should not throw"));
+ }
+
+ @Test
+ void testEmitWithCallback() {
+ ToolUseBlock toolUseBlock =
+ ToolUseBlock.builder().id("x").name("y").input(Map.of()).content("{}").build();
+
+ String[] captured = {null};
+ DefaultToolEmitter emitter =
+ new DefaultToolEmitter(
+ toolUseBlock,
+ (useBlock, chunk) -> captured[0] = useBlock.getId());
+
+ emitter.emit(ToolResultBlock.text("hello"));
+ assertEquals("x", captured[0]);
+ }
+
+ @Test
+ void testEmitNullChunkDoesNotThrow() {
+ ToolUseBlock toolUseBlock =
+ ToolUseBlock.builder().id("x").name("y").input(Map.of()).content("{}").build();
+
+ DefaultToolEmitter emitter =
+ new DefaultToolEmitter(
+ toolUseBlock,
+ (useBlock, chunk) -> {
+ throw new RuntimeException("should not be called");
+ });
+
+ emitter.emit(null);
+ }
+}
From bf1f1bfb2396a7b1078da11cf81ceeb1538d4fe2 Mon Sep 17 00:00:00 2001
From: artboy <80608452+itxaiohanglover@users.noreply.github.com>
Date: Thu, 14 May 2026 10:18:54 +0800
Subject: [PATCH 5/5] test: add DefaultToolEmitterTest for getToolUseBlock()
coverage
---
.../core/tool/DefaultToolEmitterTest.java | 67 +++++++++----------
1 file changed, 33 insertions(+), 34 deletions(-)
diff --git a/agentscope-core/src/test/java/io/agentscope/core/tool/DefaultToolEmitterTest.java b/agentscope-core/src/test/java/io/agentscope/core/tool/DefaultToolEmitterTest.java
index 3a08b69db..a229afb4c 100644
--- a/agentscope-core/src/test/java/io/agentscope/core/tool/DefaultToolEmitterTest.java
+++ b/agentscope-core/src/test/java/io/agentscope/core/tool/DefaultToolEmitterTest.java
@@ -15,69 +15,68 @@
*/
package io.agentscope.core.tool;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
import io.agentscope.core.message.ToolResultBlock;
import io.agentscope.core.message.ToolUseBlock;
import java.util.Map;
+import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
+/** Tests for DefaultToolEmitter. */
+@DisplayName("DefaultToolEmitter Tests")
class DefaultToolEmitterTest {
+ private final ToolUseBlock testToolUseBlock =
+ ToolUseBlock.builder()
+ .id("call_test")
+ .name("test_tool")
+ .input(Map.of())
+ .content("{}")
+ .build();
+
@Test
+ @DisplayName("getToolUseBlock() should return the ToolUseBlock with tool call ID")
void testGetToolUseBlock() {
- ToolUseBlock toolUseBlock =
- ToolUseBlock.builder()
- .id("call_test")
- .name("test_tool")
- .input(Map.of())
- .content("{}")
- .build();
-
- DefaultToolEmitter emitter = new DefaultToolEmitter(toolUseBlock, null);
+ DefaultToolEmitter emitter = new DefaultToolEmitter(testToolUseBlock, null);
assertNotNull(emitter.getToolUseBlock());
+ assertSame(testToolUseBlock, emitter.getToolUseBlock());
assertEquals("call_test", emitter.getToolUseBlock().getId());
assertEquals("test_tool", emitter.getToolUseBlock().getName());
}
@Test
- void testEmitWithNullCallbackDoesNotThrow() {
- ToolUseBlock toolUseBlock =
- ToolUseBlock.builder().id("x").name("y").input(Map.of()).content("{}").build();
-
- DefaultToolEmitter emitter = new DefaultToolEmitter(toolUseBlock, null);
- emitter.emit(ToolResultBlock.text("should not throw"));
+ @DisplayName("emit() should not throw when callback is null")
+ void testEmitWithNullCallback() {
+ DefaultToolEmitter emitter = new DefaultToolEmitter(testToolUseBlock, null);
+ assertDoesNotThrow(() -> emitter.emit(ToolResultBlock.text("test")));
}
@Test
- void testEmitWithCallback() {
- ToolUseBlock toolUseBlock =
- ToolUseBlock.builder().id("x").name("y").input(Map.of()).content("{}").build();
-
- String[] captured = {null};
+ @DisplayName("emit() should not throw when chunk is null")
+ void testEmitWithNullChunk() {
DefaultToolEmitter emitter =
new DefaultToolEmitter(
- toolUseBlock,
- (useBlock, chunk) -> captured[0] = useBlock.getId());
-
- emitter.emit(ToolResultBlock.text("hello"));
- assertEquals("x", captured[0]);
+ testToolUseBlock,
+ (useBlock, chunk) -> {
+ throw new RuntimeException("should not be called");
+ });
+ assertDoesNotThrow(() -> emitter.emit(null));
}
@Test
- void testEmitNullChunkDoesNotThrow() {
- ToolUseBlock toolUseBlock =
- ToolUseBlock.builder().id("x").name("y").input(Map.of()).content("{}").build();
-
+ @DisplayName("emit() should invoke callback with correct ToolUseBlock")
+ void testEmitWithCallback() {
+ ToolUseBlock[] captured = {null};
DefaultToolEmitter emitter =
new DefaultToolEmitter(
- toolUseBlock,
- (useBlock, chunk) -> {
- throw new RuntimeException("should not be called");
- });
+ testToolUseBlock, (useBlock, chunk) -> captured[0] = useBlock);
- emitter.emit(null);
+ emitter.emit(ToolResultBlock.text("hello"));
+ assertSame(testToolUseBlock, captured[0]);
}
}