Recover tool calls which Toolkit never sees - #7574
Open
candrewlee14 wants to merge 1 commit into
Open
Conversation
🦋 Changeset detectedLatest commit: 5267d26 The changes in this PR will be included in the next version bump. This PR includes changesets to release 30 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Contributor
Bundle Size AnalysisGenerated from PR build output; treat the content below as untrusted.
|
candrewlee14
force-pushed
the
fix/tool-call-recovery-opt-in
branch
from
September 2, 2026 01:11
6f18e2e to
2632a0e
Compare
candrewlee14
force-pushed
the
fix/tool-call-recovery-opt-in
branch
from
September 2, 2026 01:34
2632a0e to
63e3da1
Compare
candrewlee14
force-pushed
the
fix/tool-call-recovery-opt-in
branch
from
September 2, 2026 04:37
63e3da1 to
c62edc4
Compare
candrewlee14
force-pushed
the
fix/tool-call-recovery-opt-in
branch
from
September 2, 2026 04:47
c62edc4 to
6306aeb
Compare
candrewlee14
force-pushed
the
fix/tool-call-recovery-opt-in
branch
from
September 2, 2026 04:53
6306aeb to
d3bbf0b
Compare
candrewlee14
force-pushed
the
fix/tool-call-recovery-opt-in
branch
from
September 2, 2026 05:13
d3bbf0b to
2b237e7
Compare
candrewlee14
force-pushed
the
fix/tool-call-recovery-opt-in
branch
from
September 2, 2026 05:17
2b237e7 to
3d93382
Compare
candrewlee14
force-pushed
the
fix/tool-call-recovery-opt-in
branch
from
September 2, 2026 05:22
3d93382 to
ae1bab7
Compare
candrewlee14
force-pushed
the
fix/tool-call-recovery-opt-in
branch
from
September 2, 2026 05:29
ae1bab7 to
1a53a2f
Compare
A tool call's parameter failure is routed through the tool's `failureMode`, but only when `Toolkit` resolves the call. Two calls never reach it: one made with `disableToolCallResolution: true`, and one naming a tool which is not in the toolkit. Both fail the whole operation today. A call which names a tool in the toolkit now follows that tool's `failureMode` in both cases, producing the same failed `tool-result` `Toolkit` itself would have produced. A call which names no tool has no declaration to decide for it, so `generateText` and `streamText` accept `unknownToolCalls`; with `"return"` it comes back as a `tool-call-error` part which `Prompt.fromResponseParts` adds to history as a failed tool result. The default is unchanged. Recovery is driven by the decode which already validates the response: parts are decoded one at a time, and a failure is recoverable only when the part is a tool call which can never reach a handler. Parameters are never validated twice, and any other decode failure fails the operation as before. `HttpRequestDetails` and `HttpResponseDetails` move to a leaf module so `Response` can reference `AiError` without a cycle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ptuky5ZxuPvZhR5wxtMoX1
candrewlee14
force-pushed
the
fix/tool-call-recovery-opt-in
branch
from
September 2, 2026 05:46
1a53a2f to
5267d26
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rewritten on top of #7588, and re-shaped to follow @IMax153's review of #7452.
What the review asked for, and what changed
The review asked for this to be opt-in on tool declarations, like
failureMode— or a different API altogether — and forgenerateTextnot to start succeeding where existing callers might silently miss an invalid call.Since then #7588 landed the first half: a parameter failure on a call which reaches a handler routes through the tool's own
failureMode. Everything this PR previously did for those calls is gone, along with all provider package changes.What remains is calls
Toolkitnever sees, and they split cleanly:A call which names a tool in the toolkit now follows that tool's
failureMode, even whendisableToolCallResolution: truemeansToolkitwill not run. With"return"it produces the same failedtool-resultToolkititself would have produced. No new API — this is the per-tool opt-in the review asked for, extended to the one place it was not being honoured.A call which names no tool at all has no declaration to carry a flag, so this is the "different API" case, kept as narrow as possible: one option, on the operation, defaulted off.
One rule: the tool decides for its own calls; the operation decides only for calls that belong to no tool.
The silent-success concern is answered by the types rather than by convention: with the default,
toolCallErrorsisArray<never>and"tool-call-error"is not assignable to the response part type, so no existing caller's behavior or types change, and nobody reaches the new part without asking for it.Why the unknown-tool case is worth recovering
Response.Part(toolkit)builds its union fromObject.values(toolkit.tools), so a name that is not in the toolkit has no member and the decode fails the operation.resolveToolCallsthen skips the call (if (!tool) return Stream.empty), so even if it decoded, it would be left without a result — which the next request rejects.For an agent whose toolkit changes between turns, a stale tool name is an ordinary event rather than a hallucination: the model saw the tool a moment ago and it is gone now. Failing the turn is a poor answer when telling the model to try again is available.
Supersedes #6432.
The four detailed review points from #7452
needsApprovaldouble-decode — no longer applicable; this PR does not touch approval or parameter decoding, which unstable/ai: route tool param validation through failureMode and drop ToolParameterValidationError.toolParams #7588 owns.Schemamust use the provider codec — no longer applicable; no codec is bypassed.Formatter(internal/json.ts). Regression-tested with your1nexample: it becomes a model-visible error andChat.exportJsonstill succeeds.{"brokenstays{"broken.Types
unknownToolCallsthreads likedisableToolCallResolution:Response.Part<Tools, ParametersMode, UnknownToolCalls>,StreamPart,GenerateTextResponse,ExtractUnknownToolCalls<Options>, andChatforwards it. With the default,toolCallErrorsisArray<never>; with"return",ToolCallErrorPartjoins the union. An option whose value is not statically known keeps the part in the type.HttpRequestDetailsandHttpResponseDetailsmove tointernal/http-details.tssoResponsecan referenceAiErrorwithout a cycle; both modules re-export them unchanged.Implementation
Recovery is driven by the decode which already validates the response rather than by a separate validation pass: parts are decoded one at a time, and a failure is recoverable only when the part is a tool call which can never reach a handler. Parameters are never validated twice, and any other decode failure fails the operation exactly as it does today.
The returned
tool-call-erroris itself decoded rather than constructed. A tool call may fail to decode for a reason which has nothing to do with the tool — a non-stringid, invalid metadata — and such a call keeps failing instead of being returned under a misleading error. The helper is generic in the decoded part type and its only source of one is that decode, so an unvalidated part cannot be returned.Verification
test/unstable/ai: 1079 passed, 44 skipped ·packages/ai/*: 209 passed · doctests: 119 passed · tstyche: 128 assertionstsc -b,oxlint,dprint check,jsdocs,circular: clean. No provider package is touched.generateText/streamText/generateObject, resolution enabled and disabled,failureMode"return"and"error", an option value which is not statically known, and a 300-run property test asserting that a returned part always satisfies the response schema.If the operation-level option is still the wrong shape for the unknown-tool case, the alternatives I can see are a toolkit-level policy (a second type parameter on
Toolkit, which would flow toChatandgenerateObjectfor free) or dropping that half entirely and keeping only thefailureModechange. Happy to take either.🤖 Generated with Claude Code
https://claude.ai/code/session_01Ptuky5ZxuPvZhR5wxtMoX1