Repository navigation
Description
Description
_merge_content_item_lists (agent_framework/_types.py) decides whether an incoming code-interpreter input chunk is a delta or a full snapshot by prefix comparison:
if incoming_text.startswith(existing_text): return deepcopy(incoming) # treated as full value
if existing_text.startswith(incoming_text): return existing # treated as already presentThe OpenAI Responses client emits response.code_interpreter_call_code.delta chunks followed by a .done chunk with the full code, all with the same call_id. Any delta that happens to be a prefix of the accumulated text is silently dropped (very common: a second import line). After that, the final .done snapshot no longer matches either prefix test, so it is concatenated onto the corrupted text.
Expected: the aggregated code_interpreter_tool_call.inputs[0].text equals the code the service executed.
Actual: deltas are dropped, and the full snapshot is appended to the damaged text.
Impact: ChatResponse.from_updates / AgentResponse aggregation, history providers (e.g. Cosmos), telemetry, and replay all see wrong code.
Related: #5793 and PR #5801 (which introduced the coalescing). This looks like an edge case the heuristic in #5801 does not cover.
Suggested fix: don't infer delta vs snapshot. Have the provider mark the .done chunk explicitly (e.g. an additional_properties flag), then replace on that marker and always concatenate otherwise.
Code Sample
from agent_framework import Content, ChatResponse, ChatResponseUpdate
deltas = ["import", " os", "\n", "import", " sys"]
full = "".join(deltas)
mk = lambda t: ChatResponseUpdate(role="assistant", contents=[
Content.from_code_interpreter_tool_call(call_id="ci_1", inputs=[Content.from_text(t)])])
r = ChatResponse.from_updates([mk(d) for d in deltas])
print(repr(r.messages[0].contents[0].inputs[0].text)) # 'import os\n sys'
r = ChatResponse.from_updates([mk(d) for d in deltas] + [mk(full)]) # + .done snapshot
print(repr(r.messages[0].contents[0].inputs[0].text)) # 'import os\n sysimport os\nimport sys'
# expected: 'import os\nimport sys'Error Messages / Stack Traces
Package Versions
agent-framework-core 1.19.0 (main @ 853c456)
Python Version
Python 3.11
Additional Context
Found during a code review of main @ 853c456 (2026-09-30); the repro above was run against that commit.
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
github-actions commentedon Sep 30, 2026
🤖 Automated triage reproduction notes (agent-authored — trust but verify)
Agent analysis
python/packages/core/agent_framework/_types.py::_merge_content_item_listsdrops an incoming code-interpreter delta when it prefixes accumulated text, then concatenates the later full snapshot. The minimal repro aggregates same-call updates["import", " os", "\n", "import", " sys"], optionally followed by their joined value, throughChatResponse.from_updates;test_code_interpreter_stream_preserves_input_codedemonstrates both failures.python/packages/core/tests/core/test_types.py::test_code_interpreter_stream_preserves_input_code1.19.01.19.0he-yufeng commentedon Sep 30, 2026
Reproduced on current main (7634356) with the exact script from the issue:
The second
importdelta is dropped because"import os\n".startswith("import")satisfies the already-present branch, and once the accumulated text is damaged the final snapshot fails both prefix tests and gets concatenated, exactly as described.The
_merge_content_item_listsheuristic (_types.py:2333) is doing something undecidable: for a stream that is legally allowed to repeat any prefix (a secondimportline, a repeated decorator, a re-typed identifier), no string comparison can tell "delta that happens to be a prefix" apart from "snapshot of everything so far". Any pure-string tweak just moves which inputs corrupt.So the explicit-marker direction looks like the only clean fix to me: the Responses client knows which chunk is the
.donesnapshot, and_merge_content_item_listsshould replace on that marker and concatenate otherwise. The marker can ride inadditional_propertiesso the aggregation layer stays provider-agnostic, and providers without a done event keep today's heuristic as a fallback.Happy to take this one together with the O(n^2) coalescing in #8908, since both live in the same aggregation path and the tests would share fixtures. If you would rather keep them as separate PRs I can split them.
Python: Concatenate code-interpreter stream deltas with prefix overlap (