agentscope-ai / agentscope-ai/agentscope-java
[Bug]: Toolkit.callTool returns a ToolResultBlock with null id and name (bypasses executeWithInfrastructure)
- 主要語言
- Java
- 星號
- 5.6k
- 分支
- 1.3k
- 平均合併
- 4 天 12 小時
- 30 天內合併 PR
- 77
描述
**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**
1. Add the following test to `agentscope-core/src/test/java/io/agentscope/core/tool/CallToolIdNameReproTest.java`:
```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 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());
}
}
```
2. 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:
```java
/**
* Execute a single tool call with full infrastructure support.
*/
Mono 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.
貢獻指南
評估
這個 Issue 還沒有評估資料。