Python: fix: keep AG-UI workflow reasoning in thread snapshots - #8058
Draft
Manjunath Janardhan (manjunathshiva) wants to merge 2 commits into
Draft
Conversation
`_emit_text_reasoning` persists each workflow reasoning message into
`flow.reasoning_messages`, and `_WorkflowSnapshotBuilder.observe` folded
`TextMessage*` and `ToolCall*` events into the synthesized snapshot but had
no `Reasoning*` branch. Reasoning events fell through silently, so
intermediate output rendered live and then vanished when the thread was
hydrated from a snapshot -- while the same run through the agent path kept
it.
Fold reasoning into the builder: accumulate deltas per message_id and emit
entries in the shape the agent path already produces
({"id", "role": "reasoning", "content", ["encryptedValue"]}). Reasoning is
flushed at build() and at text-message and tool-call starts so a block that
streamed before other output replays in the position it streamed in.
Reasoning deliberately does not close an open tool-call group: it is UI-only
state that `agui_messages_to_agent_framework` drops, so it cannot break the
tool_calls/result adjacency providers require.
Fixes microsoft#8054
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Manjunath Janardhan (manjunathshiva)
deployed
to
github-app-auth
September 4, 2026 10:58 — with
GitHub Actions
Active
Manjunath Janardhan (manjunathshiva)
deployed
to
github-app-auth
September 4, 2026 10:58 — with
GitHub Actions
Active
Manjunath Janardhan (manjunathshiva)
deployed
to
github-app-auth
September 4, 2026 10:58 — with
GitHub Actions
Active
Copilot started reviewing on behalf of
Manjunath Janardhan (manjunathshiva)
September 4, 2026 10:59
View session
Manjunath Janardhan (manjunathshiva)
deployed
to
github-app-auth
September 4, 2026 10:59 — with
GitHub Actions
Active
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The moderate encrypted-only reasoning loss must be fixed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Preserves AG-UI workflow reasoning in thread snapshots so hydrated output matches live streaming.
Changes:
- Folds reasoning events and encrypted values into workflow snapshots.
- Preserves reasoning output order.
- Adds unit and end-to-end regression coverage.
File summaries
| File | Summary |
|---|---|
python/packages/ag-ui/tests/ag_ui/test_workflow_agent.py |
Tests reasoning persistence through snapshot hydration. |
python/packages/ag-ui/tests/ag_ui/test_snapshots.py |
Tests reasoning synthesis, ordering, and encryption. |
python/packages/ag-ui/agent_framework_ag_ui/_workflow.py |
Adds reasoning snapshot synthesis; encrypted-only reasoning can be dropped when the message-end event precedes its encrypted value. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Address review: an encrypted-value-only reasoning message was dropped for the event order `_emit_text_reasoning` produces without a flow, where REASONING_MESSAGE_END precedes REASONING_ENCRYPTED_VALUE. The message carries no display text, so closing it at REASONING_MESSAGE_END discarded it and left the encrypted value with nothing to attach to. The underlying mistake was conflating two protocol levels. REASONING_MESSAGE_END closes the message; REASONING_END closes the block; and an encrypted value is block-scoped, so it legitimately trails the message end. Keep the message open past REASONING_MESSAGE_END and let REASONING_END, a new REASONING_START, intervening text/tool output, or build() finalize it. Handle REASONING_START so a new block also closes anything the previous one left open. Adds regression coverage for the reported order, for an empty block with neither text nor an encrypted value still being dropped, for a block closed by the next REASONING_START, for an encrypted value arriving after intervening text, and for end events naming a message that was never opened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Manjunath Janardhan (manjunathshiva)
deployed
to
github-app-auth
September 4, 2026 11:15 — with
GitHub Actions
Active
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation & Context
With an AG-UI snapshot store enabled, a
Workflowrun's intermediate reasoning renders live and then disappears when the thread is hydrated. The same run through the agent path keeps it, so live and replayed output disagree.The reasoning is already recorded and simply never read back.
_emit_text_reasoning(_run_common.py:1051), which_emit_contentdelegates to fortext_reasoningcontent, persists each reasoning message intoflow.reasoning_messages— its docstring says so outright. The agent runner honours that contract (_agent_run.py:2100-2102, emitted terminally at:3202-3212); the workflow runner calls the same_emit_content, filling the same list, and never consumes it._WorkflowSnapshotBuilder.observefoldsTextMessage*andToolCall*events into the synthesized snapshot but had noReasoning*branch, so reasoning events fell through silently and never reached the store.Evan Mattson (@moonbox3) raised this in review on #8003 and confirmed a follow-up PR was the right home for it ("Follow up is fine, thanks."). This is that follow-up, built on top of #8003 now that it has merged.
Description & Review Guide
What are the major changes?
_WorkflowSnapshotBuildernow folds reasoning events, mirroring the existing text handling:observegains branches forReasoningMessageStartEvent,ReasoningMessageContentEvent,ReasoningMessageEndEvent/ReasoningEndEvent, andReasoningEncryptedValueEvent._observe_reasoning_*/_flush_open_reasoning_messagehelpers accumulate deltas permessage_idand emit entries in the same shape the agent path produces:{"id", "role": "reasoning", "content", ["encryptedValue"]}.build(), and at text-message and tool-call starts, so a block that streamed before other output is replayed in the position it streamed in.What is the impact of these changes?
Reasoning that streamed during a workflow run is still present after the thread is hydrated from a snapshot. Nothing else about the snapshot changes: reasoning is deliberately not allowed to close an open tool-call group, because
_message_adapters.py:691-694dropsrole == "reasoning"when converting back to provider messages — it is UI-only state, so it cannot break thetool_calls/result adjacency that providers require.This is the smaller of the two shapes discussed on the issue. It reads the real event stream rather than
flow, which matters for workflows: request_info/interrupt tool calls and executor passthrough events are yielded directly and never touchflow, soobservesees strictly more than_build_messages_snapshotwould. The second shape — emitting a terminalMessagesSnapshotEventfrom the workflow runner — is left to the issue, since an emitted snapshot is treated as authoritative and would need to reproduce those bypassing events too.What do you want reviewers to focus on?
The flush points.
_flush_open_reasoning_messageis called frombuild(),_observe_text_startand_observe_tool_call_start; the runner normally closes a reasoning block via_close_reasoning_blockbefore any non-reasoning content, but rawToolCall*events that bypass_emit_contentcan arrive with a block still open, which is what the tool-call-start flush covers.Related Issue
Fixes #8054
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.