Skip to content

fix: preserve parallel tool call deltas - #1830

Merged
akshaydeo merged 3 commits into
maximhq:mainfrom
KTS-o7:fix/streaming-tool-call-accumulator
Mar 1, 2026
Merged

akshaydeo merged 3 commits into
maximhq:mainfrom
KTS-o7:fix/streaming-tool-call-accumulator

Conversation

@KTS-o7

@KTS-o7 KTS-o7 commented Feb 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • route streaming tool call deltas by tool_call.id/tool_call.index to avoid cross-call merging during parallel tool calling
  • add regression test for interleaved parallel tool-call deltas

Changes

  • framework/streaming/accumulator.go: build id/index lookup maps, update or create tool calls based on identity
  • framework/streaming/accumulator_test.go: add interleaved parallel tool-call accumulator test

Type of change

  • Bug fix

Affected areas

  • Core streaming accumulator

How to test

  • go test ./streaming (from framework/)

Checklist

  • Tests added/updated
  • Tests run locally

Fixes #1829

Match tool call deltas by id/index during chat stream accumulation to prevent interleaved tool calls from being merged, and add a regression test.
@coderabbitai

coderabbitai Bot commented Feb 28, 2026 •

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3fe7d51 and 66b7ebd.

📒 Files selected for processing (1)
  • framework/streaming/responses.go

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Fixed streaming for parallel function/tool calls so argument deltas are routed to the correct call, preventing unintended argument merging.
  • Tests

    • Added tests validating interleaved/parallel accumulation of function/tool-call arguments during streaming.

Walkthrough

Accumulate and routing logic updated so streaming function-call argument deltas are routed by tool_call ID or Index to the correct ChatMessage/tool-call entry (or a new stub message) instead of always appending to the most recent entry, preventing merged/incorrect tool-call accumulation across parallel calls.

Changes

Cohort / File(s) Summary
Tool Call Accumulation
framework/streaming/accumulator.go
Rewrote accumulation to build idToIndex and indexToIndex maps, route deltas to existing tool calls by ID or Index, append new tool calls when unmatched, merge arguments incrementally, and normalize empty argument braces.
Responses Stream Routing
framework/streaming/responses.go
When handling FunctionCallArgumentsDelta, route the delta to the message matching resp.ItemID if present (fall back to last message); create a new stub message with that ID when ItemID is present but unmatched; maintain existing item-appending behavior.
Tests
framework/streaming/accumulator_test.go
Added tests validating interleaved/parallel accumulation and correct routing of function-call argument deltas; note: two identical test functions were added/duplicated in the file.
Changelogs
framework/changelog.md, transports/changelog.md
Added entries documenting the fix that routes parallel tool-call deltas by id/index to avoid argument/name merging during streaming.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant StreamHandler
  participant Accumulator
  participant MessageStore

  Client->>StreamHandler: stream chunk (Item / ArgumentsDelta, includes resp.ItemID?)
  StreamHandler->>MessageStore: append Item as new ChatMessage (if resp.Item present)
  StreamHandler->>Accumulator: send ArgumentsDelta (include resp.ItemID)
  Accumulator->>MessageStore: lookup target by ToolCall ID or Index (use idToIndex / indexToIndex)
  alt target found
    Accumulator->>MessageStore: update existing tool-call arguments/name
  else target not found but resp.ItemID present
    Accumulator->>MessageStore: create stub ChatMessage with resp.ItemID, add delta
  else fallback
    Accumulator->>MessageStore: append delta to last message (fallback)
  end
  MessageStore-->>Client: updated ChatMessage(s) in stream state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hop through streams where deltas play,
I map each id and index on my way.
No tangled names, no merged mistake—
Each argument finds the right lake. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'fix: preserve parallel tool call deltas' directly and clearly summarizes the main change: fixing parallel tool call handling in streaming to prevent delta merging.
Description check ✅ Passed The PR description covers all key template sections: Summary, Changes, Type of change (Bug fix), Affected areas (Core streaming), How to test, and Checklist items are marked complete.
Linked Issues check ✅ Passed All coding requirements from issue #1829 are met: streaming deltas are now routed by tool_call.id/index in both accumulator and responses APIs, with regression tests added to validate interleaved parallel tool-call handling.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue #1829: accumulator refactoring, responses API routing fix, regression tests, and changelog entries document the parallel tool call delta preservation fix.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
framework/streaming/accumulator_test.go (1)

291-309: Prefer bifrost.Ptr(...) for pointer construction consistency in tests.

♻️ Proposed consistency refactor
 	accumulator.accumulateToolCallsInMessage(message, []schemas.ChatAssistantMessageToolCall{
-		makeDelta(0, &toolCallID0, &toolNameAdd, ""),
+		makeDelta(0, bifrost.Ptr(toolCallID0), bifrost.Ptr(toolNameAdd), ""),
 	})
 	accumulator.accumulateToolCallsInMessage(message, []schemas.ChatAssistantMessageToolCall{
-		makeDelta(1, &toolCallID1, &toolNameMultiply, ""),
+		makeDelta(1, bifrost.Ptr(toolCallID1), bifrost.Ptr(toolNameMultiply), ""),
 	})

Based on learnings: In the maximhq/bifrost repository, prefer using bifrost.Ptr() to create pointers instead of the address operator (&), including in test utilities/code paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/streaming/accumulator_test.go` around lines 291 - 309, Replace
direct address-of pointer constructions used in the makeDelta invocations with
bifrost.Ptr(...) for consistency: update the calls to makeDelta in the
accumulator_test.go block (the ones passed into
accumulator.accumulateToolCallsInMessage) so that expressions like &toolCallID0,
&toolNameAdd, &toolCallID1, &toolNameMultiply are replaced by
bifrost.Ptr(toolCallID0), bifrost.Ptr(toolNameAdd), etc.; leave nil values as-is
and keep the same order of arguments so accumulateToolCallsInMessage and
makeDelta usage remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@framework/streaming/accumulator_test.go`:
- Around line 291-309: Replace direct address-of pointer constructions used in
the makeDelta invocations with bifrost.Ptr(...) for consistency: update the
calls to makeDelta in the accumulator_test.go block (the ones passed into
accumulator.accumulateToolCallsInMessage) so that expressions like &toolCallID0,
&toolNameAdd, &toolCallID1, &toolNameMultiply are replaced by
bifrost.Ptr(toolCallID0), bifrost.Ptr(toolNameAdd), etc.; leave nil values as-is
and keep the same order of arguments so accumulateToolCallsInMessage and
makeDelta usage remain unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c5efc18 and 1170cb7.

📒 Files selected for processing (2)
  • framework/streaming/accumulator.go
  • framework/streaming/accumulator_test.go

@KTS-o7

KTS-o7 commented Feb 28, 2026

Copy link
Copy Markdown
Contributor Author

CodeRabbit notes: TestAccumulateToolCallsInterleavedParallel appears once in framework/streaming/accumulator_test.go (no duplicate). No new exported funcs beyond tests; docstring coverage unchanged.

@Pratham-Mishra04

Copy link
Copy Markdown
Collaborator

Hey @KTS-o7, thanks for the PR! ❤️

This looks good overall. Just two quick items:

  1. Same issue in Responses API: The same parallel tool call merging bug exists in framework/streaming/responses.go (lines 510-521). The buildCompleteMessageFromResponsesStreamChunks() function routes FunctionCallArgumentsDelta by recency instead of by call_id, causing parallel tool arguments to merge. Would appreciate a similar fix there with a regression test.

  2. Changelog entries: Please add entries to framework/changelog.md and transports/changelog.md following the format guide

Thanks!

@KTS-o7

KTS-o7 commented Feb 28, 2026

Copy link
Copy Markdown
Contributor Author

Will do

- Route FunctionCallArgumentsDelta by ItemID in buildCompleteMessageFromResponsesStreamChunks to prevent parallel tool call arguments from merging
- Add regression test TestBuildCompleteMessageFromResponsesStreamChunksParallelToolCalls
- Add changelog entries to framework/changelog.md and transports/changelog.md
@KTS-o7

KTS-o7 commented Feb 28, 2026

Copy link
Copy Markdown
Contributor Author

@Pratham-Mishra04 Both items are done:

  1. Applied the same fix to the Responses API — FunctionCallArgumentsDelta in buildCompleteMessageFromResponsesStreamChunks now routes by ItemID instead of appending to the last message. Added regression test TestBuildCompleteMessageFromResponsesStreamChunksParallelToolCalls.
  2. Added changelog entries to framework/changelog.md and transports/changelog.md.

All tests pass. Ready for re-review!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@framework/streaming/responses.go`:
- Around line 523-531: The current logic falls back to the last message when
resp.ItemID is provided but not found, which can merge deltas into the wrong
message; change the flow in the block that searches messages for resp.ItemID so
that if resp.ItemID != nil and no matching messages[i].ID is found you do not
set targetIdx or append the delta — instead skip calling
a.appendFunctionArgumentsDeltaToResponsesMessage (or log/return early). Update
the search for resp.ItemID (the loop that assigns targetIdx) to detect the “not
found” case and only call
a.appendFunctionArgumentsDeltaToResponsesMessage(&messages[targetIdx],
*resp.Delta) when a valid match was located.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1170cb7 and 3fe7d51.

📒 Files selected for processing (4)
  • framework/changelog.md
  • framework/streaming/accumulator_test.go
  • framework/streaming/responses.go
  • transports/changelog.md
✅ Files skipped from review due to trivial changes (1)
  • transports/changelog.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • framework/streaming/accumulator_test.go

Comment thread framework/streaming/responses.go Resolved
@Pratham-Mishra04

Copy link
Copy Markdown
Collaborator

Oh great, that was super fast! I noticed a comment from CodeRabbit though, could you please resolve it?

…ming

When resp.ItemID is present but no matching message is found, create a stub message with that ID instead of falling back to the last message. Prevents parallel tool call argument deltas from merging into the wrong call when the OutputItemAdded event arrives out of order.
@KTS-o7

KTS-o7 commented Feb 28, 2026

Copy link
Copy Markdown
Contributor Author

@Pratham-Mishra04 CodeRabbit comment resolved — when resp.ItemID is present but no matching message is found, we now create a stub message with that ID instead of falling back to the last message. This closes the loophole where an unmatched ItemID could still merge into the wrong call. Pushed and all tests pass.

@akshaydeo
akshaydeo merged commit eedf1bb into maximhq:main Mar 1, 2026
2 checks passed
loss-and-quick pushed a commit to loss-and-quick/bifrost that referenced this pull request Mar 7, 2026
* fix: preserve parallel tool call deltas

Match tool call deltas by id/index during chat stream accumulation to prevent interleaved tool calls from being merged, and add a regression test.

* fix: route parallel tool call deltas by id in Responses API streaming

- Route FunctionCallArgumentsDelta by ItemID in buildCompleteMessageFromResponsesStreamChunks to prevent parallel tool call arguments from merging
- Add regression test TestBuildCompleteMessageFromResponsesStreamChunksParallelToolCalls
- Add changelog entries to framework/changelog.md and transports/changelog.md

* fix: create stub message when ItemID unmatched in Responses API streaming

When resp.ItemID is present but no matching message is found, create a stub message with that ID instead of falling back to the last message. Prevents parallel tool call argument deltas from merging into the wrong call when the OutputItemAdded event arrives out of order.
occcat pushed a commit to occcat/bifrost that referenced this pull request Sep 2, 2026
* fix: preserve parallel tool call deltas

Match tool call deltas by id/index during chat stream accumulation to prevent interleaved tool calls from being merged, and add a regression test.

* fix: route parallel tool call deltas by id in Responses API streaming

- Route FunctionCallArgumentsDelta by ItemID in buildCompleteMessageFromResponsesStreamChunks to prevent parallel tool call arguments from merging
- Add regression test TestBuildCompleteMessageFromResponsesStreamChunksParallelToolCalls
- Add changelog entries to framework/changelog.md and transports/changelog.md

* fix: create stub message when ItemID unmatched in Responses API streaming

When resp.ItemID is present but no matching message is found, create a stub message with that ID instead of falling back to the last message. Prevents parallel tool call argument deltas from merging into the wrong call when the OutputItemAdded event arrives out of order.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streaming tool call deltas merge in parallel tool calling

3 participants