feat(think): add repairInterruptedToolPart hook for client-resolved tools (#1631)#1635
Merged
Merged
Conversation
🦋 Changeset detectedLatest commit: fd8a39d The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
This was referenced May 31, 2026
Merged
threepointone
force-pushed
the
fix/chat-recovery-preserve-settled-work
branch
2 times, most recently
from
June 1, 2026 14:14
a4f8698 to
f4bfb14
Compare
…ools (#1631) Transcript repair flips an interrupted tool call (no settled result) to an errored tool-result — right for server tools, wrong for client-resolved tools like ask_user, where the call is a question that should be preserved as text so the model sees normal Q->A conversation. Because repair runs and persists before beforeTurn, subclasses had no way to shape this for the current turn. Extract the flip into a protected repairInterruptedToolPart(part) hook (default unchanged). Overriding it (e.g. ask_user -> text part carrying the prompt) now takes effect during repair, on the same turn. Test agent demonstrates the ask_user->text override; tests cover override, default fallback, and that settled calls are left untouched. Co-authored-by: Cursor <cursoragent@cursor.com>
threepointone
force-pushed
the
feat/think-repair-interrupted-tool-part-hook
branch
from
June 1, 2026 14:49
60cf123 to
fd8a39d
Compare
Contributor
Author
Rebased onto
|
agents
@cloudflare/ai-chat
@cloudflare/codemode
hono-agents
@cloudflare/shell
@cloudflare/think
@cloudflare/voice
@cloudflare/worker-bundler
commit: |
threepointone
added a commit
that referenced
this pull request
Jun 1, 2026
…sted payload (#1631) Lets products build a terminal-state policy without re-deriving anything: - ChatRecoveryContext (onChatRecovery) gains recoveryRootRequestId — the stable request id for the whole continuation chain, the right key for per-incident budget tracking / fresh-incident detection (no re-deriving from message IDs). - ChatRecoveryExhaustedContext (onExhausted) gains recoveryRootRequestId, terminalMessage (exact user-facing text), partialText/partialParts (what the turn produced before it was given up on), and streamId/createdAt — enough to render/persist a terminal banner AND emit correlated terminal telemetry (msSinceTurnStart, stream correlation) directly. streamId + createdAt were added after verifying the payload against the actual consumer (g3's _emitExhaustedRecovery): it reads both from the recovery context for telemetry, and they already exist on ChatRecoveryContext (the Pick source), so adding them to the exhausted context is additive and unblocks re-homing the exhaustion handler onto onExhausted with zero re-derivation (D4). Shared types in `agents`; wired through think + ai-chat (_exhaustChatRecovery now receives streamId + createdAt). Test agents capture the exhausted context; tests assert both contexts (incl. streamId + createdAt) in both packages. Rebased onto main (dropping the merged #1633/#1634/#1635 commits); adapted the exhausted-ctx test to N1/#1638's alarm-debounce and gave the think harness an explicit return shape (the context's MessagePart[] over-instantiates the RPC stub type). Co-authored-by: Cursor <cursoragent@cursor.com>
threepointone
added a commit
that referenced
this pull request
Jun 1, 2026
…sted payload (#1631) (#1636) Lets products build a terminal-state policy without re-deriving anything: - ChatRecoveryContext (onChatRecovery) gains recoveryRootRequestId — the stable request id for the whole continuation chain, the right key for per-incident budget tracking / fresh-incident detection (no re-deriving from message IDs). - ChatRecoveryExhaustedContext (onExhausted) gains recoveryRootRequestId, terminalMessage (exact user-facing text), partialText/partialParts (what the turn produced before it was given up on), and streamId/createdAt — enough to render/persist a terminal banner AND emit correlated terminal telemetry (msSinceTurnStart, stream correlation) directly. streamId + createdAt were added after verifying the payload against the actual consumer (g3's _emitExhaustedRecovery): it reads both from the recovery context for telemetry, and they already exist on ChatRecoveryContext (the Pick source), so adding them to the exhausted context is additive and unblocks re-homing the exhaustion handler onto onExhausted with zero re-derivation (D4). Shared types in `agents`; wired through think + ai-chat (_exhaustChatRecovery now receives streamId + createdAt). Test agents capture the exhausted context; tests assert both contexts (incl. streamId + createdAt) in both packages. Rebased onto main (dropping the merged #1633/#1634/#1635 commits); adapted the exhausted-ctx test to N1/#1638's alarm-debounce and gave the think harness an explicit return shape (the context's MessagePart[] over-instantiates the RPC stub type). Co-authored-by: Cursor <cursoragent@cursor.com>
This was referenced Jun 1, 2026
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.
Stack
This is PR 3 of 4 for #1631. Stacked; base is
fix/chat-recovery-preserve-settled-work(PR #1634). Retargets tomainas the stack merges. Diff shown is only this PR's commit.repairInterruptedToolParthook (this PR) · minoronExhaustedpayload · minorThis is a public-API addition (a new
protectedoverride point) — the one most worth a careful API-shape review.@cloudflare/thinkonly (ai-chat has no transcript-repair pass).Problem (#1631, item 2)
Transcript repair (
_repairToolTranscriptParts) flips a tool call with no settled result to an errored tool-result — preserving the record and keeping the provider from 400ing (AI_MissingToolResultsError). That's the right default for server tools.It's wrong for client-resolved tools like
ask_user— a question with no serverexecute, answered by the user's next message. There, the interrupted call is a question, and the right repair is to preserve it as a text part carrying the prompt, so the model sees a normal Q→A exchange and compaction keeps the question verbatim. Flipping it tooutput-errorturns "the assistant asked X" into "a tool errored" and loses the prose.Crucially, repair runs and persists before
beforeTurn:So a subclass had no way to shape this for the current turn — any
beforeTurnfix-up lands a turn late and races the framework's persistedoutput-error.Fix
Extract the per-orphan flip into a
protectedoverridable hook that repair consults during the repair pass (before persist), defaulting to the existing behavior:_repairToolTranscriptPartsnow callsthis.repairInterruptedToolPart({ ...part, input: normalizedInput }). Default behavior is byte-for-byte unchanged. A subclass returning a text part forask_usertakes effect on the same turn, because it runs inside the repair pass.Contract (documented on the method): a returned tool part must carry a settled result (so the
ignoreIncompleteToolCallsbackstop /_incompleteToolCallIdsdon't drop it); returning a non-tool part (e.g. text) is fine.Files
packages/think/src/think.ts— newprotected repairInterruptedToolPart;_repairToolTranscriptPartsrefactored to call it (default unchanged).packages/think/src/tests/agents/client-tools.ts—ThinkClientToolsAgentoverrides the hook (ask_user → text) to demonstrate the intended usage + arepairToolTranscriptPartsForTestharness.packages/think/src/tests/client-tools.test.ts— tests..changeset/think-repair-interrupted-tool-part-hook.md(minor · think).Tests
ask_userinto a text part carrying the prompt (the override).ask_usertools (default preserved).ask_useruntouched (only interrupted calls are repaired).The pre-existing repair tests (
message-reconciliation.test.ts: orphan →output-error) still pass — the refactor is behavior-preserving for the default, and the demo override only matchestool-ask_user(other agents' orphans are unaffected).Verification
tsc --noEmitclean (think);oxlintclean;oxfmtapplied.client-tools+message-reconciliationsuites: 58 passed. Full think suite: 460 passed.API review notes
repairInterruptedToolPart(part) -> part. Alternatives considered: abeforeTranscriptRepair(messages)pre-hook (rejected — needs extra persist-gating to avoid the repair's early-return swallowing hook-only changes; per-part is cleaner and persists naturally).think. ai-chat doesn't run this repair pass, so no change there.Test plan
repairInterruptedToolPartMade with Cursor