Skip to content

defineTool run() receives no toolCallId, so a tool cannot correlate its execution with its own tool events #453

Description

@steven4354

Summary

Every observable surface of a tool call carries a toolCallId — the tool_start and tool events, LlmToolResultMessage, observations — except the one handed to the tool implementation itself: the ToolContext passed to a defineTool's run() is only { input, signal }. A tool cannot correlate its own execution with the events it emits, or key application state on the specific call that invoked it.

Evidence

Session.createCustomTools (packages/runtime/src/session.ts) wraps every defineTool via this.wrapModelTool(tool, 'custom', (_toolCallId, params, signal) => …) — the prepare callback receives the model turn's tool call id as its first parameter but binds it as _toolCallId and never forwards it, calling parseToolInput(toolDef, params, signal) (packages/runtime/src/tool.ts), which builds the run context as only { input, signal }. The surrounding wrapModelTool stamps that same id on the tool_start/tool events it emits, so the id exists everywhere except inside run().

Why this matters

Tools that create durable, addressable side effects need to name them after the call that raised them — for example registering a pending approval/confirmation record that a later event-stream consumer resolves by matching the tool event's toolCallId. Without the id in ToolContext, the tool has to mint its own correlation key and the application has to maintain a mapping between that key and the real tool event ids, or give up on correlating entirely.

Expected behavior

ToolContext carries a readonly toolCallId: string that matches the toolCallId on the tool's emitted events.

Current workaround

A downstream application patches @flue/runtime (1.0.0-beta.9) to rename the discarded parameter and spread it into the run context — a two-line change at the wrap site plus the type field.

Implemented with tests on a fork branch (parseToolInput turns out to be the only place a ToolContext is constructed, so the field can be required; the internal no-model path synthesizes an id the same way action invocation ids are minted): steven4354@bb9d775

Metadata

Metadata

Assignees

No one assigned

    Labels

    fix pendingFixed on a development branch; lands with the next release

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions