Remaining follow-ups from #1340 / #1339. Each is non-blocking and self-contained.
Follow-ups 1 (preserve streaming preliminary tool results), 2 — output half (per-tool typing for ToolCallResultContext.output), 4 (needsApproval × ToolCallDecision ordering test), and 5 (counter-based "block prevents execute" test) were addressed in #1823.
2. Per-tool typing for ToolCallDecision.input
The output half of this item shipped in #1823 (narrowing on ctx.toolName now narrows ctx.output on afterToolCall, and ctx.input on beforeToolCall). The remaining half:
ToolCallDecision.input is Record<string, unknown> for { action: "allow", input } and { action: "substitute", input? }. Users would expect this to be the input shape of the tool whose call is being intercepted.
This requires threading the tool name being decided about into the return type — either a second generic parameter on ToolCallDecision (the tool name) or making it a discriminated mapped type. Unlike the output half, the AI SDK doesn't give us this for free, and it would be a breaking change to the exported ToolCallDecision type, so it's deferred.
3. Skip _wrapToolsWithDecision when no subscriber overrides beforeToolCall and no extensions subscribe
Think wraps every tool on every turn, even when beforeToolCall is the default no-op and no extensions subscribe — pure overhead in that case. Could detect those conditions at construction time (or memoize per-turn) and pass the original toolset straight through to streamText.
Note: since #1340/#1823 the wrapper also enforces action approval-input integrity and the AsyncIterable handling, so a skip path must additionally require that no approval-configured actions are present (not just "no beforeToolCall override and no extension subscribers"). Worth doing only if profiling shows it matters — premature otherwise.
Remaining follow-ups from #1340 / #1339. Each is non-blocking and self-contained.
2. Per-tool typing for
ToolCallDecision.inputThe
outputhalf of this item shipped in #1823 (narrowing onctx.toolNamenow narrowsctx.outputonafterToolCall, andctx.inputonbeforeToolCall). The remaining half:ToolCallDecision.inputisRecord<string, unknown>for{ action: "allow", input }and{ action: "substitute", input? }. Users would expect this to be the input shape of the tool whose call is being intercepted.This requires threading the tool name being decided about into the return type — either a second generic parameter on
ToolCallDecision(the tool name) or making it a discriminated mapped type. Unlike theoutputhalf, the AI SDK doesn't give us this for free, and it would be a breaking change to the exportedToolCallDecisiontype, so it's deferred.3. Skip
_wrapToolsWithDecisionwhen no subscriber overridesbeforeToolCalland no extensions subscribeThink wraps every tool on every turn, even when
beforeToolCallis the default no-op and no extensions subscribe — pure overhead in that case. Could detect those conditions at construction time (or memoize per-turn) and pass the original toolset straight through tostreamText.Note: since #1340/#1823 the wrapper also enforces action approval-input integrity and the AsyncIterable handling, so a skip path must additionally require that no approval-configured actions are present (not just "no
beforeToolCalloverride and no extension subscribers"). Worth doing only if profiling shows it matters — premature otherwise.