Skip to content

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

Description

@anirbandas18

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:
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());
    }
}
  1. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/core/toolTool, skill, RAG abstractionsbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions