Skip to content

Python: fix: keep AG-UI workflow reasoning in thread snapshots - #8058

Draft
Manjunath Janardhan (manjunathshiva) wants to merge 2 commits into
microsoft:mainfrom
manjunathshiva:python-agui-workflow-reasoning-snapshot-8054
Draft

Python: fix: keep AG-UI workflow reasoning in thread snapshots#8058
Manjunath Janardhan (manjunathshiva) wants to merge 2 commits into
microsoft:mainfrom
manjunathshiva:python-agui-workflow-reasoning-snapshot-8054

Conversation

@manjunathshiva

Copy link
Copy Markdown
Contributor

Motivation & Context

With an AG-UI snapshot store enabled, a Workflow run'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_content delegates to for text_reasoning content, persists each reasoning message into flow.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.observe folds TextMessage* and ToolCall* events into the synthesized snapshot but had no Reasoning* 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?

    _WorkflowSnapshotBuilder now folds reasoning events, mirroring the existing text handling:

    • observe gains branches for ReasoningMessageStartEvent, ReasoningMessageContentEvent, ReasoningMessageEndEvent / ReasoningEndEvent, and ReasoningEncryptedValueEvent.
    • New _observe_reasoning_* / _flush_open_reasoning_message helpers accumulate deltas per message_id and emit entries in the same shape the agent path 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 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-694 drops role == "reasoning" when converting back to provider messages — it is UI-only state, so it cannot break the tool_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 touch flow, so observe sees strictly more than _build_messages_snapshot would. The second shape — emitting a terminal MessagesSnapshotEvent from 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_message is called from build(), _observe_text_start and _observe_tool_call_start; the runner normally closes a reasoning block via _close_reasoning_block before any non-reasoning content, but raw ToolCall* events that bypass _emit_content can arrive with a block still open, which is what the tool-call-start flush covers.

Related Issue

Fixes #8054

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

`_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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment thread python/packages/ag-ui/agent_framework_ag_ui/_workflow.py
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: AG-UI workflow reasoning is dropped from thread snapshots, so intermediate output vanishes on hydration

2 participants