Skip to content

Python: [Bug]: AG-UI snapshot places assistant text after tool calls, discarding the model's emission order #7223

Closed

Description

Description

Summary

_build_messages_snapshot always emits a turn's assistant text message after its assistant tool-call message (and after the tool results):

# agent_framework_ag_ui/_agent_run.py  (_build_messages_snapshot, ~L1532–1571 on 1.0.0rc8)
# Add assistant message with tool calls only (no content)
if flow.pending_tool_calls:
    all_messages.append(tool_call_message)          # tool-call message first
# Add tool results
all_messages.extend(flow.tool_results)
# Add text-only assistant message if there is accumulated text
# This is a separate message from the tool calls message to maintain
# the expected AG-UI protocol format (see issue #3619)
if flow.accumulated_text:
    all_messages.append({... "content": flow.accumulated_text})   # text message last
# (1.0.0rc8 adds a third accumulator with the same fixed-position problem — see
# "New in rc8" below)
all_messages.extend(flow.reasoning_messages)

A client that renders messages in array order and pins each tool's generative UI to its tool-call message (e.g. CopilotKit) therefore shows the model's lead-in narration below the tool chips it introduces:

[tool: microsoft_docs_fetch] [tool: microsoft_docs_fetch]
"Now let me research the key Microsoft Learn pages…"     ← reads as if it comes after the fetches

Keeping the text as a separate message (issue #3619) is fine. The problem is the order: the builder unconditionally puts tool calls first, even when the model produced the text before the tool calls.

Why a naive "text always first" fix is wrong

The text that accompanies a tool-using turn is not always a lead-in. Surveying real advisor threads, the assistant text that follows a tool-call message is a mix:

  • Lead-in — "Now let me research…", "Let me load the landing-zone skill…" → should render before the tool chips.
  • Post-tool summary — "I've loaded the skill and reviewed the CAF methodologies…", "Great — I've gathered the research…", "Here's the plan I built…" → should render after the tool chips.

In our sample the summaries outnumbered the lead-ins. So simply reordering every text message before its tool-call message would hoist summaries above the tools that produced them — worse than the status quo. A downstream consumer cannot fix this safely: lead-in vs summary is not recoverable from the normalized snapshot (both look like "text after tool-call").

The signal exists at stream time and is discarded

During streaming, the relative order is known: a lead-in arrives as TEXT_MESSAGE_* events before the TOOL_CALL_* events; a post-tool summary arrives after the tool results. _build_messages_snapshot collapses this by accumulating all text into flow.accumulated_text and all calls into flow.pending_tool_calls, then emitting tool-calls-then-text regardless of the order they streamed in.

The turn's true shape can be a sequence — e.g. text₁ → tool_calls → (results) → text₂ — but the snapshot flattens it to tool_calls → results → (text₁+text₂).

New in rc8: a third accumulator with the same problem

Between rc7 and rc8, FlowState gained reasoning_messages (agent_framework_ag_ui/_run_common.py:467) so reasoning content survives a MESSAGES_SNAPSHOT reconciliation. _build_messages_snapshot appends it last, unconditionally, after the text message — the identical fixed-position pattern this report is about, now with three message kinds (tool-calls, text, reasoning) instead of two. It doesn't change the fix: preserving stream order for reasoning_messages alongside pending_tool_calls/accumulated_text falls out of the same segment-tracking approach proposed below rather than needing separate handling.

Code Sample

Error Messages / Stack Traces

Package Versions

agent-framework-ag-ui: 1.0.0rc8, agent-framework-core: 1.11.0

Python Version

No response

Additional Context

Proposed fix

Preserve the order in which the model emitted text vs. tool calls, instead of always tool-calls-first. Concretely, track segment boundaries as the run streams (a text run, a tool-call run, another text run …) and, in _build_messages_snapshot, emit the separate assistant messages in that recorded order rather than in the fixed tool-calls-then-results-then-text order. Tool results still follow their tool-call message; only the position of text message(s) relative to the tool-call message follows the model.

This keeps the #3619 separation, adds no new protocol surface, and makes both cases correct with no downstream heuristics:

  • lead-in text streamed before the calls → emitted before the tool-call message → renders above the chips;
  • summary text streamed after the results → emitted after them → renders below the chips.

Alternatives, if preserving order is too invasive:

  • B. Tag the text message with the stream position (e.g. additional_properties = {"emitted_before_tool_calls": bool}) so order-sensitive clients can place it correctly without guessing.
  • C. A host option to choose text-before-tool-calls vs the current text-after, for apps that know their model narrates lead-ins. (Coarse; does not handle mixed turns.)

Option A is preferred because it needs no client cooperation and handles a turn that contains both a lead-in and a summary.

Evidence / references (re-verified against agent-framework-ag-ui 1.0.0rc8)

What Location
Snapshot appends tool-call message, then results, then text, then reasoning agent_framework_ag_ui/_agent_run.py _build_messages_snapshot (~L1532–1571)
"separate message … to maintain the expected AG-UI protocol format (see #3619)" same function, comment above the text-message append
Snapshot built once per turn and emitted + persisted _agent_run.py run_agent_stream (~L2275: snapshot_event = _build_messages_snapshot(...), persisted via _save_thread_snapshot at ~L2295)
Text/tool/reasoning accumulation that drops stream order flow.accumulated_text, flow.pending_tool_calls, flow.tool_results, flow.reasoning_messages on FlowState (agent_framework_ag_ui/_run_common.py:453)

Activity

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

    @github-actions
    Contributor

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

    Agent analysis

    Repro: _build_messages_snapshot() in python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py (lines 1540-1569) always appends the tool-call message before the text message, regardless of stream order. The underlying issue is that FlowState (_run_common.py:453) accumulates text and tool calls into separate fields (accumulated_text, pending_tool_calls) with no ordering metadata. Minimal repro: construct a FlowState with both accumulated_text and pending_tool_calls populated (simulating text streamed before tool calls), call _build_messages_snapshot(flow, []), and assert the text message index is less than the tool-call message index — it fails because text is always placed at index 2 while tool-call is at index 0.

    • Failing test: python/packages/ag-ui/tests/ag_ui/test_issue_7223_snapshot_order.py
    • Files examined: agent_framework_ag_ui/_agent_run.py, agent_framework_ag_ui/_run_common.py, tests/ag_ui/test_run.py
    • Tests run: tests/ag_ui/test_issue_7223_snapshot_order.py
    • Reported version: 1.0.0rc8
    • Current version: 1.0.0rc8
  4. added
    ag-uiUsage: [Issues, PRs], Target: AG-UI protocol integration
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

ag-uiUsage: [Issues, PRs], Target: AG-UI protocol integrationpythonUsage: [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