Repository navigation
fix: keep tool input when the approval request arrives before it (#1872) - #2391
Conversation
🦋 Changeset detectedLatest commit: f41e4f2 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 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 |
🟡 agents import sizes: 1 entry point grew
Changed exports (105)
…and 55 more exports in the workflow artifact. How this worksEach runtime export is bundled on its own, minified, and gzipped. Changes smaller than 100 B, or smaller than 1% and 1 KiB, are ignored. Growth over 10% or 5 KiB is marked 🔴. This report is informational and does not fail CI. The workflow artifact contains every measurement. Compared |
…re it (#1872) Co-authored-by: Cursor <cursoragent@cursor.com>
ba804ae to
dfa4bad
Compare
| if (p.input !== undefined && !inputFromRawText.has(part)) return false; | ||
| p.input = normalizeToolInput(chunk.input).input; |
There was a problem hiding this comment.
🔴 Recovered approvals retain incomplete tool input
After recovery recreates an approval part, applyLateToolInput rejects the canonical input if partial deltas already populated it. provisionalInput only tracks the original part object, so the recovered approval keeps incomplete arguments.
Learn more
The provisional-input marker exists only in a WeakSet keyed by the original tool-part object. An approval part rebuilt from an early-persisted message or reconstructed stream can contain the fallback input without that marker. When the canonical tool-input-available arrives after recovery, the non-undefined fallback input triggers the first-write guard, so the corrected arguments never reach storage or the approved call. StreamAccumulator copies existing parts, and reconcileOrphanPartial preserves existing tool parts on orphan recovery.
Example: Deltas contain only {"path": before the approval request, so the fallback input becomes {}. An interruption restores the approval part from storage; a later canonical {path:"notes.txt"} cannot replace {}.
Recommended fix: Persist or reconstruct whether the approval input is provisional, or distinguish canonical input from fallback input when rebuilding approval parts. Cover an interruption between the approval request and canonical input.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const lateToolInput = isLateToolInputChunk( | ||
| accumulator.parts, | ||
| streamChunk | ||
| ); | ||
| const { action } = accumulator.applyChunk(streamChunk); | ||
| this._applyActionApprovalDescriptorToParts( |
There was a problem hiding this comment.
🔍 Action descriptor can lag tool input
For text-only deltas, Think constructs the action descriptor before parsing the tool input at approval. Its descriptor can contain {} while the tool part holds the reconstructed arguments; check consumers of descriptor input.
Was this helpful? React with 👍 or 👎 to provide feedback.
… recovery keep it Addresses review on #2391: store and broadcast a late tool-input-available followed by the approval request again (so stream replay and live clients get the input without losing the approval card), treat an older emitter's partial delta input as provisional, and build Think's approval descriptor input from streamed delta text. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
| if (chunk.providerExecuted != null) { | ||
| p.providerExecuted = chunk.providerExecuted; | ||
| } | ||
| if (chunk.providerMetadata != null) { | ||
| p.callProviderMetadata = chunk.providerMetadata; | ||
| } |
There was a problem hiding this comment.
🟡 Late tool input drops display title
When tool-input-available supplies a title after approval, applyLateToolInput retains the arguments but drops that title. The approval card loses its tool label if tool-input-start omitted it.
Learn more
The normal tool-input-available branch copies chunk.title to the tool part, but the new late-input branch only copies the input and provider fields. A stream that supplies its display title with the canonical input therefore loses it when approval precedes input availability.
Example: tool-input-start identifies deleteFile without a title; after approval, tool-input-available supplies title: "Delete notes.txt". The approval part retains no title.
Recommended fix: Copy chunk.title in applyLateToolInput under the same condition used by the normal available-input handler, and test both chunk orders.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if (lateToolInput) { | ||
| for (const forwarded of lateToolInputForwardChunks( | ||
| accumulator.parts, | ||
| streamChunk, | ||
| approvalRequests.get(streamChunk.toolCallId ?? "") | ||
| )) { | ||
| const chunkBody = JSON.stringify(forwarded); | ||
| const seq = await this._storeChunkDurably( | ||
| streamId, | ||
| forwarded, | ||
| chunkBody, | ||
| flushState | ||
| ); | ||
| this._broadcastChat({ | ||
| type: MSG_CHAT_RESPONSE, | ||
| id: requestId, | ||
| body: chunkBody, | ||
| done: false, | ||
| ...(seq !== undefined && { seq }) | ||
| }); | ||
| await callback.onEvent(chunkBody); | ||
| } | ||
| continue; |
There was a problem hiding this comment.
🔍 Think forwarding differs from described behavior
The description says Think suppresses late input. Both streaming paths instead store and broadcast it with a repeated approval request; RPC also emits both events. Confirm which stream contract consumers need.
Was this helpful? React with 👍 or 👎 to provide feedback.
Co-authored-by: Cursor <cursoragent@cursor.com>
agents
@cloudflare/ai-chat
@cloudflare/codemode
hono-agents
@cloudflare/shell
@cloudflare/think
@cloudflare/voice
@cloudflare/worker-bundler
commit: |
…gine Ports four upstream changes to the legacy AIChatAgent onto AGUIChatAgent: - cloudflare#2352: clear a stale auto-continuation when the active stream finishes with stop and the tool batch is complete (finishReason read from RUN_FINISHED via the accumulator). - cloudflare#2391: a tool call whose arguments complete after its approval request refreshes the persisted approval snapshot; the event-to-chunk projection re-sends the approval request after the late tool-input-available. - cloudflare#2392: settle an approved tool call that never ran once a new turn moves past it. The pre-turn repair now also runs on submitted turns. - cloudflare#2040: reconcile reused tool-call IDs one-to-one in the AG-UI reconciler (same-call claim, per-row result merge, stale-copy drop). Test plumbing: the legacy-wire WebSocket wrapper translated each frame once per listener through a stateful projector, so a second listener lost chunks. It now translates once per frame.
Fixes #1872. Supersedes #2021.
Problem
A tool call that needs approval could reach the approval card with
input: undefined:tool-input-deltachunks with the text ininputTextDelta. The message builder only read the olderinputfield, so the streamed arguments were dropped.tool-approval-requestbeforetool-input-available. The latetool-input-availablewas either ignored, or it moved the part back toinput-availableand hid the approval card.Change
applyChunkToParts(agents/chat) collects theinputTextDeltatext per tool part. Ontool-approval-requestit parses that text if the input isn't set yet. It still takeschunk.inputfrom older emitters.tool-input-availablethat arrives after the approval request fills in the input but keeps the approval state. It only replaces input that is missing or came from the partial deltas, and it also takes the chunk'stitle. New exported helpers:isLateToolInputChunk,applyLateToolInputandlateToolInputForwardChunks.tool-input-available, followed by the part'stool-approval-requestagain. The AI SDK client fills in the input on the first and goes back to showing the approval on the second, and stream replay rebuilds the same state. AIChatAgent also re-persists the approval snapshot. Think's RPC stream passes both chunks toonEvent. Nothing is forwarded once the user has responded to the approval.Tests
message-builder-approval-input.test.ts: tests covering both chunk orders, the forwarded chunk pair,, the oldinputfield, malformed text, and not overwriting canonical input.ai-chatlate-tool-input.test.ts: the client, rebuilt from the streamed chunks, shows the approval with its input, and the persisted part carries it too.Credit to #2021 for the original reproduction and fix direction.