agentscope-ai / agentscope-ai/agentscope-java

[Bug]: Toolkit.callTool returns a ToolResultBlock with null id and name (bypasses executeWithInfrastructure)

Đang mở
#3,114 2 bình luận 0 reaction 0 người được giao Xem trên GitHub
area/core/tool bug
Ngôn ngữ chính
Java
Star
5.6k
Fork
1.3k
Merge trung bình
4 ngày 12 giờ
Pull request đã merge (30 ngày)
77

Mô tả

**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.

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.