Skip to content

DUX-10302: Fix Bedrock tool-call argument routing by contentBlockIndex - #22

Closed
staging-supernova-dx-appf-io[bot] wants to merge 2 commits into
mainfrom
DUX-10302-fix-bedrock-tool-call-corruption
Closed

DUX-10302: Fix Bedrock tool-call argument routing by contentBlockIndex#22
staging-supernova-dx-appf-io[bot] wants to merge 2 commits into
mainfrom
DUX-10302-fix-bedrock-tool-call-corruption

Conversation

@staging-supernova-dx-appf-io

@staging-supernova-dx-appf-io staging-supernova-dx-appf-io Bot commented Jul 30, 2026

Copy link
Copy Markdown

Fix tool-call argument corruption in Bedrock streaming paths

Problem

Tool-call arguments were being corrupted when the Bedrock Converse or InvokeModel APIs streamed multiple parallel tool calls. Delta fragments were not routed by contentBlockIndex, causing all argument deltas to accumulate on whichever tool call started most recently rather than their correct content block.

Root cause

  • extract_tool_call_delta in converse/streaming.rb and bedrock_invoke_model/streaming.rb discarded the contentBlockIndex and keyed every delta under nil
  • StreamAccumulator#find_tool_call(nil) would resolve to @tool_calls[@latest_tool_call_id] — the most recently started tool call
  • When extract_tool_call_start keyed tool calls by their index (or a stable id), but deltas came in under nil, they couldn't be correctly routed to their corresponding tool-use block

Solution

  1. Converse streaming: Key tool-call start and delta events by contentBlockIndex instead of nil
  2. InvokeModel streaming: Apply the same index-routing fix for consistency
  3. StreamAccumulator:
    • Support @tool_call_ids_by_index to route deltas to the correct tool call by content block index
    • Wrap JSON.parse in a new ToolCallArgumentsCorrupted error that distinguishes genuine truncation (finish_reason indicates max tokens) from wire-level routing bugs
  4. Tests: Add regression tests for interleaved parallel tool calls, truncated arguments, and wire-level event routing

Files changed

  • lib/ruby_llm/error.rb: Added ToolCallArgumentsCorrupted exception
  • lib/ruby_llm/protocols/converse/streaming.rb: Route deltas by contentBlockIndex
  • lib/ruby_llm/protocols/bedrock_invoke_model/streaming.rb: Apply same fix to InvokeModel
  • lib/ruby_llm/stream_accumulator.rb: Add index-based routing and improved error handling
  • spec/ruby_llm/protocols/converse/streaming_spec.rb: Regression tests for parallel tool calls
  • spec/ruby_llm/protocols/bedrock_invoke_model_spec.rb: Wire-level test and fix assertion
  • spec/ruby_llm/stream_accumulator_spec.rb: Test truncated argument handling

Fixes DUX-10302


Agent session: https://staging.supernova.dx.appf.io/coders/e1e061e4-dd9a-4892-ac77-1ef1336bbb32

…able tool args

Bedrock's Converse stream can interleave content blocks: a delta for block 0
may arrive after block 1's contentBlockStart. The stream key for tool-use
start/delta events was the toolUseId (start) and nil (delta), so every delta
fell back to whichever tool call started most recently, splicing one tool
call's argument fragments into another's and corrupting both. Key both events
by contentBlockIndex when present so the accumulator's index routing can tell
parallel tool-use blocks apart, falling back to the old behavior for the flat
start/delta shapes that carry no index.

When accumulated arguments still fail to parse, raise
ToolCallArgumentsTruncatedError naming the affected tool call instead of a bare
JSON::ParserError. The error carries finish_reason so callers can distinguish a
genuine output-token-cap truncation from a "complete" turn whose arguments are
corrupt regardless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Co-authored-by: Sam Boland <sam.boland@appfolio.com>
@kermitapp

kermitapp Bot commented Jul 30, 2026

Copy link
Copy Markdown

@staging-supernova-dx-appf-io

Copy link
Copy Markdown
Author

🤖 Supernova Code Review — View trace

♻️ Re-run this automation

@staging-supernova-dx-appf-io staging-supernova-dx-appf-io Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed this PR — the fix is correct and well-targeted.

  • extract_tool_call_delta now keys deltas by contentBlockIndex, consistent with extract_tool_call_start. Verified against StreamAccumulator#find_tool_call/@tool_call_ids_by_index, which was already introduced by the earlier "Fix Anthropic streaming parallel tool calls" commit — this PR correctly wires the Bedrock delta path into that existing index-routing infrastructure rather than duplicating it.
  • The legacy flat start/delta shapes (no contentBlockIndex) still fall back to the previous "latest started tool call" behavior, preserving backward compatibility, and this is covered by a regression test.
  • ToolCallArgumentsTruncatedError is a reasonable addition — it carries tool_call_id, tool_name, raw_arguments, and finish_reason so callers can distinguish a legitimate token-cap truncation from a routing/corruption bug, and it's raised consistently from parse_tool_call_arguments.
  • New specs cover the exact interleaved-delta corruption scenario described in the PR body, the legacy shape, and the new truncation error, in both streaming_spec.rb and stream_accumulator_spec.rb.

No actionable findings — no inline comments.

♻️ Re-run this automation

@samuelboland

Copy link
Copy Markdown

Which chat spawned the coder that created this?

@staging-supernova-dx-appf-io

Copy link
Copy Markdown
Author

@samuelboland

Which chat spawned the coder that created this?

This coder session (titled "ToolCallStreamRouting W1-1: honor contentBlockIndex in Converse tool-call deltas") was spawned from a task description you gave describing the Bedrock Converse streaming tool-call corruption bug directly — the session log only shows the task brief itself, not a separate upstream chat transcript, so I can't trace it back further than that initial instruction within this tool's visibility.


🔍 View trace

…-level test and drop breadcrumb

Address PR #22 review follow-ups:
- lib/ruby_llm/protocols/bedrock_invoke_model/streaming.rb had the identical
  nil/id-keyed tool-call routing bug just fixed in the Converse protocol.
  Key start/delta events by index so parallel tool-use blocks route correctly,
  with a fallback to the legacy keying when index is absent. Latent only
  (invoke_model protocol isn't exercised in production), fixed to keep the
  two protocols from diverging.
- Add a parallel-tool-call regression test through the real flat wire framing
  (:event-type header + Aws::EventStream::Encoder) in the Converse spec, since
  the existing hand-nested-hash tests don't exercise nest_event_under_type.
- Log a debug breadcrumb when a tool call fragment's stream_key can't be
  resolved to a started tool call, instead of silently dropping it.

Co-authored-by: Sam Boland <sam.boland@appfolio.com>
@staging-supernova-dx-appf-io staging-supernova-dx-appf-io Bot changed the title DUX-10302: Fix tool-call argument corruption in Bedrock Converse streaming DUX-10302: Fix Bedrock tool-call argument routing by contentBlockIndex Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants