Describe the bug
Toolkit.callTool(ToolCallParam) returns a ToolResultBlock whose id and name are null, while Toolkit.callTools(...) stamps both correctly for the same tool and the same call.
Root cause: the two public entry points do not share the stage that stamps them.
Toolkit.callTools(...) → ToolExecutor.executeAll(...) → executeWithInfrastructure(...), which applies .map(result -> result.withIdAndName(toolCall.getId(), toolCall.getName())).
Toolkit.callTool(param) → ToolExecutor.execute(param) directly, which bypasses executeWithInfrastructure(...) entirely.
withIdAndName(...) is called in exactly two places in ToolExecutor, both inside executeWithInfrastructure(...), so nothing on the callTool path ever stamps the result.
This matters because the id and name are what pair a result back to its call: ToolResultMessageBuilder.buildToolResultMsg(...) and the tool-call pairing model both key off them. A caller following the usage example in Toolkit.callTool's own javadoc gets back a block it cannot correlate.
To Reproduce
- Add the following test to
agentscope-core/src/test/java/io/agentscope/core/tool/CallToolIdNameReproTest.java:
package io.agentscope.core.tool;
import io.agentscope.core.message.ToolResultBlock;
import io.agentscope.core.message.ToolUseBlock;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
/** Repro: Toolkit.callTool vs Toolkit.callTools — id/name stamping on the returned block. */
class CallToolIdNameReproTest {
static class EchoTools {
@Tool(name = "echo", description = "Echoes back")
public String echo() {
return "ok";
}
}
private static ToolUseBlock call(String id) {
return ToolUseBlock.builder()
.id(id).name("echo").input(Map.of()).content("{}").build();
}
@Test
void compare() {
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new EchoTools());
ToolResultBlock viaCallTool = toolkit
.callTool(ToolCallParam.builder().toolUseBlock(call("call-single")).build())
.block();
List<ToolResultBlock> viaCallTools =
toolkit.callTools(List.of(call("call-batch")), null, null, null).block();
ToolResultBlock batch = viaCallTools.get(0);
System.out.println("REPRO callTool id=" + viaCallTool.getId()
+ " name=" + viaCallTool.getName() + " state=" + viaCallTool.getState());
System.out.println("REPRO callTools id=" + batch.getId()
+ " name=" + batch.getName() + " state=" + batch.getState());
}
}
- Run it from the repository root:
mvn -pl agentscope-core test -Dtest=CallToolIdNameReproTest -Dsurefire.failIfNoSpecifiedTests=false
Expected behavior
Both entry points stamp the result identically:
REPRO callTool id=call-single name=echo state=RUNNING
REPRO callTools id=call-batch name=echo state=RUNNING
Error messages
No exception is thrown. Actual output on main at c5db8f72:
REPRO callTool id=null name=null state=RUNNING
REPRO callTools id=call-batch name=echo state=RUNNING
Environment (please complete the following information):
- AgentScope-Java Version: 2.0.3-SNAPSHOT (reproduced on
main at commit c5db8f72)
- Java Version: 25 (the issue is version-independent)
- OS: macOS (the issue is OS-independent)
Additional context
The stale javadoc on ToolExecutor.execute(...) suggests how the paths drifted apart — it still reads:
/**
* Execute a single tool call with full infrastructure support.
*/
Mono<ToolResultBlock> execute(ToolCallParam param) {
but execute(...) is the method that applies none of it; executeWithInfrastructure(...) is where the infrastructure lives, and it is private. This looks like an incomplete extraction: executeAll(...) was repointed at the new wrapper and Toolkit.callTool(...) was left on the inner method.
Beyond the missing id/name, the same divergence means the callTool path also skips applyScheduling, applyTimeout, applyRetry and applyShutdownGuard. The practical consequences are that a configured ExecutionConfig timeout and retry policy are silently inert for direct calls, and a tool invoked this way is not covered by the graceful-shutdown guard. I have kept this report scoped to the id/name defect because it is unambiguous and independently verifiable; the retry behaviour overlaps with #2829, whose author notes they are preparing a fix in this area.
Two notes for whoever picks this up:
- If the intended contract is that
Toolkit.callTool(...) is a deliberately thin, infrastructure-free entry point, then the id/name stamping and the execute(...) javadoc should still be corrected, and the difference documented on Toolkit.callTool(...) so callers are not surprised.
- Repointing
Toolkit.callTool(...) at executeWithInfrastructure(...) would fix all of the above at once, but it is an observable behaviour change for external callers currently relying on the no-timeout semantics, so it likely belongs in a minor release rather than a patch.
Nothing in the repository calls Toolkit.callTool(...) today — the only occurrences are the usage examples in its own javadoc — so the exposure is entirely to external users following the documented example.
Describe the bug
Toolkit.callTool(ToolCallParam)returns aToolResultBlockwhoseidandnamearenull, whileToolkit.callTools(...)stamps both correctly for the same tool and the same call.Root cause: the two public entry points do not share the stage that stamps them.
Toolkit.callTools(...)→ToolExecutor.executeAll(...)→executeWithInfrastructure(...), which applies.map(result -> result.withIdAndName(toolCall.getId(), toolCall.getName())).Toolkit.callTool(param)→ToolExecutor.execute(param)directly, which bypassesexecuteWithInfrastructure(...)entirely.withIdAndName(...)is called in exactly two places inToolExecutor, both insideexecuteWithInfrastructure(...), so nothing on thecallToolpath ever stamps the result.This matters because the id and name are what pair a result back to its call:
ToolResultMessageBuilder.buildToolResultMsg(...)and the tool-call pairing model both key off them. A caller following the usage example inToolkit.callTool's own javadoc gets back a block it cannot correlate.To Reproduce
agentscope-core/src/test/java/io/agentscope/core/tool/CallToolIdNameReproTest.java:Expected behavior
Both entry points stamp the result identically:
Error messages
No exception is thrown. Actual output on
mainatc5db8f72:Environment (please complete the following information):
mainat commitc5db8f72)Additional context
The stale javadoc on
ToolExecutor.execute(...)suggests how the paths drifted apart — it still reads:but
execute(...)is the method that applies none of it;executeWithInfrastructure(...)is where the infrastructure lives, and it is private. This looks like an incomplete extraction:executeAll(...)was repointed at the new wrapper andToolkit.callTool(...)was left on the inner method.Beyond the missing id/name, the same divergence means the
callToolpath also skipsapplyScheduling,applyTimeout,applyRetryandapplyShutdownGuard. The practical consequences are that a configuredExecutionConfigtimeout and retry policy are silently inert for direct calls, and a tool invoked this way is not covered by the graceful-shutdown guard. I have kept this report scoped to the id/name defect because it is unambiguous and independently verifiable; the retry behaviour overlaps with #2829, whose author notes they are preparing a fix in this area.Two notes for whoever picks this up:
Toolkit.callTool(...)is a deliberately thin, infrastructure-free entry point, then the id/name stamping and theexecute(...)javadoc should still be corrected, and the difference documented onToolkit.callTool(...)so callers are not surprised.Toolkit.callTool(...)atexecuteWithInfrastructure(...)would fix all of the above at once, but it is an observable behaviour change for external callers currently relying on the no-timeout semantics, so it likely belongs in a minor release rather than a patch.Nothing in the repository calls
Toolkit.callTool(...)today — the only occurrences are the usage examples in its own javadoc — so the exposure is entirely to external users following the documented example.