fix(ai/cursor): preserve large MCP tool-call args during streaming#2625
Merged
Conversation
Cursor's `cursor-agent` API streams MCP tool-call arguments via cumulative
`args_text_delta` snapshots ("aggregated args text so far" per agent.proto)
and a final `tool_call_completed` frame that carries an `McpArgs` map. Two
bugs in `processInteractionUpdate` corrupted large arguments — most visibly
the built-in `task` tool's `tasks` array on multi-subagent dispatches, which
failed downstream schema validation with
`tasks: Invalid input: expected array, received undefined`:
- Each cumulative snapshot was concatenated onto the buffer, so partialJson
grew as `prefix1 + prefix1prefix2 + ... + full` — garbled JSON the
streaming parser could only partially repair.
- The completion frame's `McpArgs` map unconditionally overwrote the
streamed args, but that map omits oversized parameters entirely and
downgrades unparsable values to their raw string fallback, so large keys
present in the stream were dropped or downgraded.
Strip the already-buffered prefix from each `args_text_delta` snapshot
(falling back to append when the snapshot doesn't extend the buffer) and
merge the decoded `McpArgs` map into the streamed args instead of
overwriting — preserving streamed keys the completion frame omits and the
structured value when the completion frame downgrades it to a string.
The new `cursor-streaming-args.test.ts` covers the merge helper directly
and end-to-end via `processInteractionUpdate`; with the old implementation
restored the same suite fails on the three regression cases above.
Fixes #2615
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.
Repro
The Cursor (
cursor-agent) provider drops large MCP tool-call arguments — most visibly the built-intasktool'stasksarray on multi-subagent dispatches, which fails downstream schema validation withtasks: Invalid input: expected array, received undefined.Reproduced by driving
processInteractionUpdate(inpackages/ai/src/providers/cursor.ts) with the same sequence the cursor-agent stream emits for ataskdispatch: a series of cumulativeargs_text_deltasnapshots followed by atool_call_completedframe whoseMcpArgsmap omits the oversizedtaskskey. Against the pre-fix implementation the newpackages/ai/test/cursor-streaming-args.test.tsfails three assertions exactly matching the report — garbledpartialJson, doubled-up duplicate snapshots, andtasksdropped from the finalarguments.Cause
Two bugs in
processInteractionUpdate:args_text_deltacarries the cumulative args text so far peragent.proto'sPartialToolCallUpdate.args_text_delta("Aggregated args text so far"), but the handler concatenated each snapshot onto the buffer (partialJson = current + snapshot). For a 3-step stream the buffer grew asprefix1 + prefix1prefix2 + … + full, producing garbled JSON the streaming parser could only partially repair.tool_call_completedcarries anMcpArgsmap that omits oversized parameters entirely and downgrades unparsable values to their raw string fallback (viadecodeMcpArgValue'sparseToolArgsJsonfallback). The handler unconditionally overwrotestate.currentToolCall.argumentswith that map, so large keys that survived the stream (e.g.task.tasks) disappeared from the final tool call.Fix
args_text_deltasnapshot (snapshot.startsWith(current) ? snapshot.slice(current.length) : snapshot), so cumulative snapshots are treated as snapshots while genuine incremental fragments still append. Empty chunks short-circuit before emitting a no-optoolcall_delta.mergeCursorMcpToolCallArgs(streamed, completion)and call it ontoolCallCompletedinstead of overwriting. The merge: keeps streamed keys the completion frame omits, keeps the streamed structured value when the completion frame downgrades to a string, and otherwise lets the completion frame win.processInteractionUpdate,BlockState,ToolCallState,UsageState, andmergeCursorMcpToolCallArgsso the regression test can drive the streaming state machine directly.Verification
cd packages/ai && bun test test/cursor-streaming-args.test.ts test/cursor-exec-handlers.test.ts— 20 pass, 0 fail. Full package suite:cd packages/ai && bun test— 1718 pass, 337 skip (env-gated E2E), 0 fail.bun checkfrompackages/aiis clean (formatter + types).Fixes #2615