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]); } }