Skip to content

ToolResult and ToolProgress drop the toolUseId the protocol sends #4

Description

@mrwogu

Summary

ToolResult and ToolProgress arrive with no way to tell which call they
answer. ToolResult exposes tool_name, content and is_error; the tool
name is an empty string on every result I have captured, and there is no
correlation id.

The information exists on the wire. The tool_result notification carries
toolUseId, which matches the id of the tool_use frame, and the SDK drops
it during mapping.

Tested on 2026-08-02 with:

  • Droid CLI 0.186.0
  • droid-sdk-python==0.1.2
  • Factory protocol 1.147.0

What the SDK exposes

ToolUse      -> ['tool_name', 'tool_input', 'tool_use_id']
ToolResult   -> ['tool_name', 'content', 'is_error']
ToolProgress -> ['tool_name', 'content']

ToolUse has tool_use_id. Neither of the other two does.

What the wire carries

Frames from a single turn, captured by teeing ProcessTransport.read_messages:

{"type":"tool_call","toolUse":{"type":"tool_use","id":"chatcmpl-tool-a83c4a92","name":"ToolSearch","input":{"query":"weather"}}}
{"type":"tool_execution_phase_changed","toolUseId":"chatcmpl-tool-a83c4a92","toolName":"ToolSearch","phase":"queued"}
{"type":"tool_execution_phase_changed","toolUseId":"chatcmpl-tool-a83c4a92","toolName":"ToolSearch","phase":"executing"}
{"type":"tool_result","toolUseId":"chatcmpl-tool-a83c4a92","messageId":"6a6b09d2","content":"Error: No tools matched","isError":true}

The result frame carries toolUseId. The SDK turns it into
ToolResult(tool_name="", content="Error: No tools matched", is_error=True).

Reproduction

class TeeTransport(ProcessTransport):
    async def read_messages(self):
        async for message in super().read_messages():
            print("WIRE:", json.dumps(message, ensure_ascii=False))
            yield message


transport = TeeTransport(
    exec_path="droid",
    cwd="/tmp",
    exec_args=["exec", "--input-format", "stream-jsonrpc",
               "--output-format", "stream-jsonrpc"],
)
client = DroidClient(transport=transport)
async with client:
    await client.initialize_session(
        machine_id="probe",
        cwd="/tmp",
        mcp_servers=[],
        model_id="glm-5.2",
        interaction_mode=DroidInteractionMode.Auto,
        autonomy_level=AutonomyLevel.Off,
        skip_permissions_unsafe=False,
        enabled_tool_ids=[],
    )
    await client.add_user_message(
        text="Search your tool catalog for a weather tool, then call it for Gdansk."
    )
    async for event in client.receive_response():
        if isinstance(event, (ToolUse, ToolResult, ToolProgress)):
            print(type(event).__name__, repr(event.tool_name))

Output:

ToolUse    'ToolSearch'
ToolResult ''

Impact

I maintain an OpenAI-compatible bridge on top of the SDK. It disables every
native tool and applies a policy per tool: two Droid meta tools that cannot be
disabled are tolerated, everything else fails the turn closed. With no
attribution on the result, the policy cannot be applied to results at all.

The workaround is to remember that a tolerated call happened earlier in the
turn and assume a nameless result belongs to it. That falls apart as soon as two
different tools are in flight, which is exactly when the policy matters.

The empty string is a second, smaller problem: "" is indistinguishable from a
tool that reported no name, so there is no way to detect the missing data other
than knowing the SDK substitutes it.

Suggested fix

  • Add tool_use_id to ToolResult and ToolProgress, populated from
    toolUseId on the notification.
  • Report tool_name as None when the frame does not carry one, instead of
    substituting "".

The CLI could also include toolName on the tool_result frame the way it
already does on tool_execution_phase_changed, but the correlation id alone is
enough to solve this in the SDK.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions