Skip to content

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.

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Sep 12, 2026
  2. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on Sep 12, 2026
  3. github-actions commented on Sep 12, 2026

    @github-actions
    Contributor

    🤖 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 distinct call_id values interleave. Minimal repro: aggregate six alternating ChatResponseUpdate chunks for two calls and assert that ChatResponse.from_updates returns two complete function calls; current behavior returns six fragments.

    • Failing test: python/packages/core/tests/core/test_types.py::test_interleaved_function_call_chunks_merge_by_call_id
    • Files examined: python/AGENTS.md, python/packages/core/AGENTS.md, python/packages/core/agent_framework/_types.py, python/packages/core/tests/core/test_types.py, python/packages/core/pyproject.toml
    • Tests run: test_function_call_merge_in_process_update_and_usage_aggregation, test_function_call_incompatible_ids_are_not_merged, test_interleaved_function_call_chunks_merge_by_call_id
    • Reported version: 1.18.0
    • Current version: 1.18.0
  4. added
    agentsUsage: [Issues, PRs], Target: Single agent
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Sep 12, 2026
  5. added a commit that references this issue on Sep 14, 2026
    0ff9e40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

agentsUsage: [Issues, PRs], Target: Single agentpythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions