Repository navigation
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-callsvs the currenttext-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) |
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
github-actions commentedon Jul 20, 2026
🤖 Automated triage reproduction notes (agent-authored — trust but verify)
Agent analysis
Repro:
_build_messages_snapshot()inpython/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 thatFlowState(_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 bothaccumulated_textandpending_tool_callspopulated (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.python/packages/ag-ui/tests/ag_ui/test_issue_7223_snapshot_order.py1.0.0rc81.0.0rc8