fix(anthropic): route input_json deltas by content-block index#3009
Open
mcavage-docker wants to merge 3 commits into
Open
fix(anthropic): route input_json deltas by content-block index#3009mcavage-docker wants to merge 3 commits into
mcavage-docker wants to merge 3 commits into
Conversation
Parallel tool_use blocks streamed from Anthropic share a single streamAdapter.toolID field that is overwritten on every ContentBlockStartEvent. Subsequent InputJSONDelta events for the first block then carry the second block's tool ID, and the runtime accumulator (keyed by ToolCall.ID in pkg/runtime/streaming.go) concatenates both calls' argument fragments into one buffer. The first tool's args end up empty; the second tool's args contain interleaved fragments that fail to parse, surfacing as invalid character 's' looking for beginning of value invalid character '-' after object key:value pair (the exact character depends on where the chunk boundaries fall). Track tool IDs per content-block index via toolIDByBlock map[int64]string and route InputJSONDelta events using eventVariant.Index. Apply the same fix to betaStreamAdapter, which shared the bug. Adds pkg/model/provider/anthropic/parallel_toolcall_test.go covering both the standard and Beta adapters with interleaved input_json_delta events; both tests fail without this fix and pass with it. Note: pkg/model/provider/bedrock/adapter.go carries the same single-currentToolID pattern and is likely vulnerable to the same bug on parallel Converse tool calls; left for a follow-up PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
docker-agent
reviewed
Jun 5, 2026
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟢 APPROVE
The fix is correct and well-targeted. Replacing the single toolID string field with a toolIDByBlock map[int64]string keyed by content-block index is the right solution for routing InputJSONDelta events across parallel tool_use blocks. Both streamAdapter and betaStreamAdapter are patched consistently, and the new regression tests faithfully reproduce the interleaved-delta scenario described in the PR.
Verification summary
| Finding | Severity | Verdict |
|---|---|---|
| Silent empty-string ToolCall ID on missing block index | Medium | ✅ DISMISSED — Anthropic API guarantees content_block_start before any delta; retry path restarts from the beginning so indices are re-populated before deltas arrive |
toolIDByBlock not cleared on stream retry |
Medium | ✅ DISMISSED — retry replaces the stream from the beginning; all content_block_start events are re-delivered before any deltas, overwriting any stale entries |
Redundant nil-guard inside Recv() |
Low | Informational note (see inline comment) |
One low-severity observation is left as an inline comment for awareness. No blockers found.
dgageot
previously approved these changes
Jun 8, 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.
Summary
Parallel
tool_useblocks streamed from Anthropic share a singlestreamAdapter.toolIDfield that gets overwritten on everyContentBlockStartEvent. SubsequentInputJSONDeltaevents for the first block carry the second block's tool ID, and the runtime accumulator (keyed byToolCall.IDinpkg/runtime/streaming.go) concatenates both calls' argument fragments into one buffer. The first tool's args end up empty; the second tool's args contain interleaved fragments that fail to parse, surfacing as:(the exact character depends on where the chunk boundaries fall.)
This fix tracks tool IDs per content-block index via
toolIDByBlock map[int64]stringand routesInputJSONDeltaevents usingeventVariant.Index. The same fix is applied tobetaStreamAdapter, which shared the bug.How it manifests in production
Observed against a local GM agent team (cagent v1.57.0) running Claude Opus with ~18 parallel MCP tool calls in a single turn. Five calls failed with the JSON errors above before reaching the MCP gateway; the other thirteen succeeded. Retrying the same calls in isolation always succeeded — which made it look like a transient parser corruption issue, but the actual condition is deterministic: it only fires when the provider interleaves
input_json_deltaevents across blocks, which is more likely under heavier fan-out.The bug is present on current
main(verified at HEAD03386b31). Bug was never reported via gh search for "parallel tool", "invalid character", or "tool call arguments" before this PR.Test plan
pkg/model/provider/anthropic/parallel_toolcall_test.go(added in this PR) feeds a fake SSE decoder a sequence of two paralleltool_useblocks with interleavedinput_json_deltaevents, mirroring what Anthropic emits for parallel tool calls. Covers bothstreamAdapterandbetaStreamAdapter.Without the fix:
With the fix:
Follow-up
`pkg/model/provider/bedrock/adapter.go` carries the same single-`currentToolID` pattern and is likely vulnerable to the same bug on parallel Bedrock Converse tool calls. Not patched here to keep the diff focused — happy to file a follow-up PR if useful.
🤖 Generated with Claude Code