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
Summary
Every observable surface of a tool call carries a
toolCallId— thetool_startandtoolevents,LlmToolResultMessage, observations — except the one handed to the tool implementation itself: theToolContextpassed to adefineTool'srun()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 everydefineToolviathis.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_toolCallIdand never forwards it, callingparseToolInput(toolDef, params, signal)(packages/runtime/src/tool.ts), which builds the run context as only{ input, signal }. The surroundingwrapModelToolstamps that same id on thetool_start/toolevents it emits, so the id exists everywhere except insiderun().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 inToolContext, 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
ToolContextcarries areadonly toolCallId: stringthat matches thetoolCallIdon 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 (
parseToolInputturns out to be the only place aToolContextis 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