Skip to content

Python: [Bug]: Streaming a response that is still in progress drops its continuation_token #8826

Closed

Description

Description

A streamed response that is still in progress reports itself as complete: the aggregated response loses its continuation_token on the next event.

ContinuationToken is documented as "Presence on a response indicates the operation is still in progress; None means the operation is complete" (core/agent_framework/_types.py:2439). The OpenAI Responses client only stamps the token on the response.created (in_progress/queued) and response.in_progress events; every other event builds its ChatResponseUpdate with continuation_token=None (_chat_client.py:3527, :3738-3742, :4133).

_process_update, the single aggregation point behind ChatResponse.from_updates / AgentResponse.from_updates, copies the field unconditionally (core/agent_framework/_types.py:2238):

    if update.response_id:
        response.response_id = update.response_id
    if update.created_at is not None:
        response.created_at = update.created_at
    ...
    response.continuation_token = update.continuation_token   # the only unguarded one

The eight sibling fields are all guarded, so a token the provider has not withdrawn is cleared by the next event that does not repeat it.

What happened: the caller gets continuation_token is None while the provider is still generating. OpenAIChatClient acts on that reading and drops the token from the caller's options (_chat_client.py:964, :4420), so the next function-loop iteration POSTs instead of retrieving the background response, and a polling loop stops early.

What I expected: the aggregated response keeps the token until the provider stops sending one, which is what _chat_client.py:964 already assumes ("Once the background response completes, drop the continuation_token").

Suggested fix: guard that assignment with is not None, like the fields above it. Providers that mean to end the operation stop sending a token, and the existing None check then does its job.

Code Sample

from agent_framework import ChatResponse, ChatResponseUpdate, Content

events = [
    ChatResponseUpdate(
        contents=[Content.from_text(text="working")],
        response_id="resp_1",
        model="gpt-5",
        continuation_token={"response_id": "resp_1"},
    ),
    ChatResponseUpdate(contents=[Content.from_text(text="...still working")]),
]

aggregated = ChatResponse.from_updates(events)
print(aggregated.response_id, aggregated.model, aggregated.continuation_token)

Output on current main:

resp_1 gpt-5 None

response_id and model survive the second event; the token does not.

Package Versions

agent-framework-core: 1.19.0 (main @ 06e9b489)

Python Version

3.11 (reproduced against a source checkout of python/packages/core)

Activity

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

    @github-actions
    Contributor

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

    Agent analysis

    The defect reproduces in python/packages/core/agent_framework/_types.py::_process_update at line 2238 when a token-bearing update is followed by an update whose continuation_token defaults to None. Minimal repro: aggregate those two ChatResponseUpdate objects with ChatResponse.from_updates and assert that the first token remains; the assertion receives None.

    • Failing test: python/packages/core/tests/core/test_types.py
    • Files examined: python/packages/core/agent_framework/_types.py, python/packages/openai/agent_framework_openai/_chat_client.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_response_update_preserves_continuation_token_when_later_update_omits_it
    • 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 29, 2026
  5. feiiiiii5 commented on Sep 29, 2026

    @feiiiiii5
    ContributorAuthor

    Reproduced this end to end as well, not only through from_updates: an Agent.run(..., stream=True) against a provider that stamps the token on its first event only ends with get_final_response().continuation_token is None, while response_id and model from those same events survive — the same two-updates repro as in the description, one layer down.

    The change I have ready is the one-line guard in _process_update (if update.continuation_token is not None: before the assignment, matching the eight fields above it) plus two tests in test_types.py. pytest over test_types.py, test_agents.py and test_workflow_agent.py (588 tests) passes, and the new test fails on main with assert None == {'response_id': 'resp_1'}.

    Please assign the issue to me if you would like me to open the PR. If someone else is already on it, that is fine — I will leave it alone.

  6. eavanvalkenburg commented on Sep 30, 2026

    @eavanvalkenburg
    Member

    The core team will take care of this one.

  7. liwenjie200543 commented on Sep 30, 2026

    @liwenjie200543

    I'd like to work on this. The diagnosis matches current main: _process_update (core/agent_framework/_types.py) is the only unguarded field copy in the aggregation, and the OpenAI Responses consumers (packages/openai/agent_framework_openai/_chat_client.py:964, :4420) read a None token on the aggregate as "operation complete", so a streaming background run gets its token wiped by the next event and the caller re-POSTs instead of polling.

    One addition to the suggested fix, found while checking the consumers: a bare is not None guard is not enough on its own. After a terminal event (response.completed/incomplete/failed, which carry no token) the stale token would survive on the aggregate and those same consumers would never drop it from the caller's options. My branch pairs the guard with a withdrawal on terminal updates — an update that carries finish_reason clears the token — which keeps the documented "presence means in progress, None means complete" contract intact in both directions.

    Reproduction from the issue fails on current main and passes with the fix; regression tests added to tests/core/test_types.py (367 passed, plus ruff check/format clean). PR coming right after this comment.

  8. feiiiiii5 commented on Sep 30, 2026

    @feiiiiii5
    ContributorAuthor

    Thanks — standing down on my side.

    For whoever picks it up, the two-updates repro is the whole thing: a ChatResponseUpdate that carries continuation_token, followed by one whose continuation_token defaults to None, aggregated with ChatResponse.from_updates drops the token. The triage notes point at _process_update in python/packages/core/agent_framework/_types.py, which agrees with where I landed.

    The line I had ready was the same shape as the existing response_id handling a few lines up — keep the last non-None token rather than overwriting it with the incoming default. I also hit it one layer down through a streaming Agent.run(..., stream=True), where get_final_response().continuation_token comes back None while response_id and model from those same events survive, so it is reachable through the public API and not only through the constructor.

    No PR from me and nothing in flight, so there is no branch to rebase or close. If the fix stalls or you want the failing test, say the word and I will open one.

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