Python: Preserve AG-UI tool message IDs across snapshots - #7510
Conversation
There was a problem hiding this comment.
🟡 Not ready to approve
Some ToolCallStartEvent emitters in _run_common.py still parent tool calls to flow.message_id while snapshots use the newly allocated segment id, which can reintroduce the stream/snapshot ID mismatch for those tool-call paths.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
This PR updates the Python AG-UI streaming/snapshot implementation so tool-call “assistant messages” can keep a stable identity between streamed events and the final MESSAGES_SNAPSHOT, preserving client-side ordering for interleaved text/tool-call turns.
Changes:
- Allocate and track a dedicated message ID for streamed tool-call segments and use it as
ToolCallStartEvent.parent_message_id. - Reuse the tracked tool-call segment ID when building the corresponding snapshot assistant
tool_callsmessage. - Add a regression test asserting that the snapshot tool-call message ID matches the streamed tool-call parent message ID after leading text.
File summaries
| File | Description |
|---|---|
| python/packages/ag-ui/tests/ag_ui/test_run.py | Adds regression coverage ensuring snapshot tool-call message IDs reuse the streamed tool-call parent ID. |
| python/packages/ag-ui/agent_framework_ag_ui/_run_common.py | Tracks per-tool-call-segment message IDs during streaming and uses them for ToolCallStartEvent.parent_message_id. |
| python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py | Prefers the tracked tool-call segment ID when emitting snapshot tool_calls messages. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
| def _track_tool_call_segment(flow: FlowState, tool_call_id: str) -> str: | ||
| """Record a tool call and return the message ID used by its stream events.""" | ||
| segment: dict[str, Any] | ||
| if flow.snapshot_segments and flow.snapshot_segments[-1]["kind"] == "tool_calls": | ||
| flow.snapshot_segments[-1]["call_ids"].append(tool_call_id) | ||
| segment = flow.snapshot_segments[-1] |
Motivation & Context
When an AG-UI run streams assistant text followed by a tool call, the stream uses the open text message ID as the tool call's parent. The final
MESSAGES_SNAPSHOTrepresents text and tool calls as separate messages, but currently assigns the tool-call message a new ID. The reference AG-UI client merges snapshots by ID, so the tool call and its result are appended after later assistant text instead of remaining in their original position.This affects any AG-UI frontend that follows the reference ID-based merge behavior and is reproducible with interleaved assistant text and tool calls.
Description & Review Guide
ToolCallStartEvent.parent_message_idand the corresponding snapshot assistant message.Related Issue
Fixes #7491
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix) — a workflow keeps the label and title prefix in sync automatically.