Repository navigation
Python: [Bug]: Streamed parallel tool calls get merged into the wrong call when their argument deltas interleave #8336
Description
Description
When a model streams two or more tool calls in the same turn and their argument
chunks interleave (call A's next fragment arrives before call B's previous
fragment has finished), ChatResponse.from_updates / AgentResponse.from_updates
merge the fragments into the wrong call. Depending on how the chunks interleave,
one call ends up empty and the other ends up with the two calls' arguments mashed
together into invalid JSON, or both calls get split across several duplicate
function_call content items that never reassemble into one complete call.
Root cause is in _process_update in agent_framework/_types.py: it only ever
checks whether message.contents[-1] (the last content item appended so far)
is a function_call, and merges the incoming chunk into that one. It never
looks for the actual call the chunk belongs to. As long as tool calls stream one
fully to completion before the next one starts, "the last item" happens to be
the right target. As soon as two calls are in flight at the same time — which is
exactly what parallel tool calling is for — that assumption breaks.
This isn't just theoretical: OpenAIResponsesClient already tracks
call_id/name per output_index precisely so every
response.function_call_arguments.delta event carries the correct, real
call_id (see function_call_ids in _chat_client.py). Even with every chunk
correctly tagged, the aggregation still corrupts the result, because it never
searches past the trailing item for a matching call_id.
Expected behavior
Streaming two (or more) tool calls whose argument deltas interleave should
produce one FunctionCallContent per call in the final response, each with its
own arguments fully and correctly assembled — the same result as if the two
calls had streamed back-to-back instead of interleaved.
Actual behavior
The final assistant message contains corrupted and/or duplicated
function_call content: one call's arguments end up empty while the other's
contain a garbled concatenation of both calls' fragments (invalid JSON), or
both calls are split across multiple partial, non-mergeable content items.
Function invocation downstream then either fails to parse the arguments or
invokes the wrong function with the wrong (or partial) arguments.
Code Sample
from agent_framework import ChatResponse, ChatResponseUpdate, Content
# Mirrors how OpenAIResponsesClient streams two parallel tool calls: every
# delta is correctly tagged with its own call_id, but the deltas for the two
# calls interleave rather than each call finishing before the next starts.
updates = [
ChatResponseUpdate(contents=[Content.from_function_call(call_id="call_1", name="get_weather", arguments="")]),
ChatResponseUpdate(contents=[Content.from_function_call(call_id="call_2", name="get_time", arguments="")]),
ChatResponseUpdate(contents=[Content.from_function_call(call_id="call_1", name="get_weather", arguments='{"location":')]),
ChatResponseUpdate(contents=[Content.from_function_call(call_id="call_2", name="get_time", arguments='{"timezone":')]),
ChatResponseUpdate(contents=[Content.from_function_call(call_id="call_1", name="get_weather", arguments='"NYC"}')]),
ChatResponseUpdate(contents=[Content.from_function_call(call_id="call_2", name="get_time", arguments='"EST"}')]),
]
response = ChatResponse.from_updates(updates)
for c in response.messages[0].contents:
print(c.call_id, c.name, repr(c.arguments))Output today:
call_1 get_weather ''
call_2 get_time ''
call_1 get_weather '{"location":'
call_2 get_time '{"timezone":'
call_1 get_weather '"NYC"}'
call_2 get_time '"EST"}'
Six fragments instead of two complete calls — get_weather and get_time
never end up with usable arguments.
Error Messages / Stack Traces
No exception is raised; the corruption is silent. Depending on the exact
interleaving, the downstream symptom is either a json.JSONDecodeError when
parse_arguments() is called on the corrupted content, or a tool invoked with
wrong/partial arguments.
Package Versions
agent-framework-core: 1.18.0
Python Version
Python 3.12
Additional Context
A second, narrower variant of the same defect class exists in
OpenAIChatCompletionClient._parse_tool_calls_from_openai: continuation deltas
for the legacy Chat Completions streaming API only carry a call_id on the
first chunk for each tool call, so once two calls are in flight their untagged
continuation fragments can't be told apart even if the aggregation above is
fixed. That's tracked separately since it needs index-tracking in that specific
client rather than a change to the shared aggregation logic.
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
github-actions commentedon Sep 12, 2026
🤖 Automated triage reproduction notes (agent-authored — trust but verify)
Agent analysis
The bug reproduces in
python/packages/core/agent_framework/_types.py::_process_update, specifically the trailing-item merge branch at lines 2036-2040, when function-call argument chunks with distinctcall_idvalues interleave. Minimal repro: aggregate six alternatingChatResponseUpdatechunks for two calls and assert thatChatResponse.from_updatesreturns two complete function calls; current behavior returns six fragments.python/packages/core/tests/core/test_types.py::test_interleaved_function_call_chunks_merge_by_call_id1.18.01.18.0Python: Fix streamed parallel tool calls merging into the wrong call (m…