Skip to content

Python: [Bug]: Streaming response aggregation is O(n^2) in the number of text chunks #8908

Closed

Description

Description

_coalesce_text_content (agent_framework/_types.py) folds streamed text chunks with first_new_content += content. Each Content.__add__ builds a new text string, merges additional_properties, and rebuilds raw_representation via self_list + other_list, so aggregating n chunks costs O(n^2).

Measured with ChatResponse.from_updates (one text chunk per update, each with a small raw_representation):

chunks 2,000 4,000 8,000 16,000
time 0.014 s 0.036 s 0.134 s 0.532 s

Doubling the input roughly quadruples the time. Long reasoning or tool-heavy streams, and code paths that re-aggregate per iteration, pay this repeatedly.

Suggested fix: collect each run of mergeable chunks and build one Content at the end, using "".join(texts), a single flattened raw-representation list, and one props merge.

Code Sample

import time
from agent_framework import Content, ChatResponse, ChatResponseUpdate

for n in (2000, 4000, 8000, 16000):
    ups = [ChatResponseUpdate(role="assistant", message_id="m",
                              contents=[Content.from_text("tok ", raw_representation={"i": i})])
           for i in range(n)]
    t = time.perf_counter(); ChatResponse.from_updates(ups)
    print(n, round(time.perf_counter() - t, 3))

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.

Activity

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

    @github-actions
    Contributor

    🤖 Automated triage reproduction notes (agent-authored — trust but verify)

    Agent analysis

    The bug is in python/packages/core/agent_framework/_types.py::_coalesce_text_content around line 2300, where consecutive text chunks trigger repeated Content addition and copying of the accumulated raw-representation list. Minimal repro: create 100 ChatResponseUpdate instances containing text and non-null raw_representation, instrument _combine_raw_representations, and call ChatResponse.from_updates; 4,951 elements are processed instead of linear work.

    • Failing test: python/packages/core/tests/core/test_types.py::test_chat_response_from_updates_coalesces_raw_representations_in_linear_work
    • Files examined: python/packages/core/agent_framework/_types.py, python/packages/core/tests/core/test_types.py, python/packages/core/pyproject.toml
    • Tests run: python/packages/core/tests/core/test_types.py, test_chat_response_from_updates_coalesces_raw_representations_in_linear_work, issue timing benchmark
    • Reported version: 1.19.0
    • Current version: 1.19.0
  4. added
    agentsUsage: [Issues, PRs], Target: Single agent
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Sep 30, 2026
  5. he-yufeng commented on Sep 30, 2026

    @he-yufeng
    Contributor

    Verified on current main (7634356). The mechanism is as described: _coalesce_text_content folds chunks with first_new_content += content, and every Content.__add__ rebuilds the text string, re-merges additional_properties, and re-concatenates raw_representation lists, so n chunks cost O(n^2).

    Repro on main with the same script from the issue:

    2000  -> 0.006s
    4000  -> 0.020s
    8000  -> 0.076s
    16000 -> 0.311s
    

    Doubling the input roughly quadruples the time, matching the reported table.

    Two boundaries the fix has to preserve, both living in the current += path: the AdditionItemMismatch split for text_reasoning chunks with conflicting ids, and the _MODEL_OUTPUT_KIND_KEY split for text chunks. A join-based fast path needs to keep both segment boundaries intact, build the text with one "".join per segment, flatten raw_representation in a single list concat, and merge additional_properties once per segment instead of once per chunk.

    Happy to open a PR along those lines if the direction sounds right.

  6. he-yufeng commented on Oct 1, 2026

    @he-yufeng
    Contributor

    Opened draft #8953 with the one-pass fold discussed above. Both split boundaries are preserved (the model_output_kind split for text, the id and reasoning-text/summary splits for text_reasoning), and one extra semantic turned up while reproducing the old path: the fold's initial deepcopy discards the run head's raw_representation via _SHALLOW_COPY_FIELDS, so the new fold deliberately skips the head when flattening raws. An oracle test folds the same mixed streams with the old repeated-+= implementation and asserts identical output. Same-machine benchmark: 16,000 chunks 0.302s -> 0.011s.

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