Skip to content

Python: fix(ag-ui): dedupe client-replayed transcripts on resume - #8149

Merged
Evan Mattson (moonbox3) merged 10 commits into
microsoft:mainfrom
ktz03:fix/ag-ui-resume-transcript-dedupe
Sep 14, 2026
Merged

Evan Mattson (moonbox3) merged 10 commits into
microsoft:mainfrom
ktz03:fix/ag-ui-resume-transcript-dedupe

Conversation

@ktz03

@ktz03 LI (ktz03) commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Motivation & Context

On AG-UI resume, clients often replay the full transcript in messages while also sending resume. Without dedupe, the same turn is persisted twice and replayed twice after hydrate (#8140). Replayed approval retries can also fall through to a fresh agent run after retained results are available.

Fixes #8140.

Description & Review Guide

  • What are the major changes? Dedupe client-replayed transcripts on resume; when approval resume entries are fully handled via retained results, re-emit those results even if reconstruction refilled raw_messages.
  • What is the impact of these changes? Prevents duplicate turns and avoids re-invoking the agent on approval retry.
  • What do you want reviewers to focus on? resume reconstruction / retained-approval short-circuit in _agent_run.py.

Related Issue

Fixes #8140

No other open PR targets this issue.

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.

)

Route non-service-session (and workflow) resume seeding through _reconstruct_messages_from_thread_snapshot so a reference AG-UI client that replays its transcript is not double-persisted into the thread snapshot.

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

Empty-message resumes can lose stored history in both agent and workflow paths.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Updates AG-UI resume handling to deduplicate client-replayed transcripts using snapshot reconstruction.

Changes:

  • Applies reconstruction to agent and workflow resume history.
  • Adds transcript deduplication regression tests.
File summaries
File Review
python/packages/ag-ui/tests/ag_ui/test_resume_transcript_dedupe.py Adds tests, but misses empty-message integration coverage and the required copyright header.
python/packages/ag-ui/agent_framework_ag_ui/_workflow.py Reconstruction drops stored history for empty checkpoint-resume input.
python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py Reconstruction drops stored history for empty approval-resume input.
Review details

Suppressed comments (1)

python/packages/ag-ui/tests/ag_ui/test_resume_transcript_dedupe.py:1

  • This new Python file is missing the required first-line copyright header mandated by python/CODING_STANDARD.md:453 and used throughout this test directory.
"""Regression for AG-UI resume with client-replayed transcript (#8140)."""
  • Files reviewed: 3/3 changed files
  • Comments generated: 3
  • 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/_agent_run.py Outdated
Comment thread python/packages/ag-ui/agent_framework_ag_ui/_workflow.py Outdated
Comment thread python/packages/ag-ui/tests/ag_ui/test_resume_transcript_dedupe.py Outdated
@ktz03

Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree

Use the transcript reconstructor only when the client sends a non-empty messages list; empty approval/checkpoint resumes still go through resume_seeded_messages so stored history is preserved.

Also add the required copyright header and document the empty-input reconstructor contract in tests.
@ktz03

Copy link
Copy Markdown
Contributor Author

Addressed Copilot review:

  1. Empty messages on resume — agent (_agent_run) and workflow (_workflow) now use _reconstruct_messages_from_thread_snapshot only when the client sent a non-empty transcript; empty approval/checkpoint resumes still go through resume_seeded_messages so stored history is kept.
  2. Tests — added the required copyright header, documented the empty-input reconstructor contract, and clarified the partial-turn case.

Existing test_endpoint / test_workflow_agent empty-resume assertions should stay green with this branching.

@ktz03

Copy link
Copy Markdown
Contributor Author

Addressed on head 3bb98c18:

  • Empty messages on approval/checkpoint resume again goes through resume_seeded_messages (agent + workflow), so stored history is preserved.
  • Non-empty / client-replayed transcripts still use _reconstruct_messages_from_thread_snapshot for dedupe.
  • Added a unit test documenting the reconstructor’s empty-input contract ([] unchanged → call sites must seed).

Existing test_endpoint / test_workflow_agent empty-resume assertions should stay green with this branching. Please re-run Copilot review on the latest head when convenient.

Comment thread python/packages/ag-ui/agent_framework_ag_ui/_workflow.py Outdated
Comment thread python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py
Add reconcile_resume_messages for empty vs replayed resume shapes, use it from agent and workflow runners, and mark generic reconstructed resumes as seeded so save-time prepend does not duplicate history.
@ktz03

Copy link
Copy Markdown
Contributor Author

Thanks Evan Mattson (@moonbox3) — addressed both points on this head:

  1. ThreadSnapshotSession.reconcile_resume_messages now owns the empty-vs-nonempty resume decision (empty → resume_seeded_messages, non-empty → overlap reconstructor). Agent approval resume and workflow resume/checkpoint seeding both call this single method.
  2. Generic non-service resumes (resume_payload without approval-interrupt ids) also go through reconcile and set seeded_resume_from_snapshot = True, so save-time resume_seeded_messages no longer double-prepends after a client-replayed transcript.

Added a unit test covering the session-owned empty vs replayed shapes.

@moonbox3

Copy link
Copy Markdown
Contributor

Please resolve individual comments with a note if addressed or not. Then resolve when complete.

@ktz03

Copy link
Copy Markdown
Contributor Author

Evan Mattson (@moonbox3) Done — replied on each review thread with addressed/partial notes and resolved all five threads. Please take another look when convenient.

@moonbox3 Evan Mattson (moonbox3) 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.

Please also fix the failing CI/CD checks.

Comment thread python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py Outdated
Generic/predictive resumes with messages:[] must not prepend stored history into the turn input — confirm_changes synthesizes only the resume tool message and short-circuits without another model call. Seeding that history caused approval mismatch warnings and an extra stream invocation (test_agent_endpoint_confirm_changes_clears_persisted_interrupt).

Only non-empty client-replayed generic resumes go through reconcile_resume_messages + seeded_resume_from_snapshot. Approval and workflow/checkpoint paths still seed empty via reconcile.
@ktz03

Copy link
Copy Markdown
Contributor Author

Evan Mattson (@moonbox3) Fixed on head \�682021\ — empty generic resumes no longer seed via
econcile_resume_messages([])\ (that was causing the confirm_changes \call_count\ regression / CI failure). Only non-empty client-replayed generic resumes are reconciled and marked seeded; empty confirm_changes stays on the save-time prepend path as you described. Please re-check when convenient.

@eavanvalkenburg

Copy link
Copy Markdown
Member

LI (@ktz03) make sure to use the defined PR template

@ktz03

Copy link
Copy Markdown
Contributor Author

Eduard van Valkenburg (@eavanvalkenburg) Updated the description to follow the repo PR template — please take another look when convenient.

Per review: keep resume_seeded_messages as blind prepend (save-time / empty seeds). Use reconstruct only for non-empty client-replayed transcripts. Remove dedicated small test file and fold coverage into test_snapshot_session.
@ktz03

Copy link
Copy Markdown
Contributor Author

Eduard van Valkenburg (@eavanvalkenburg) Addressed both review points on latest head:

  1. No separate reconcile_* method — resume_seeded_messages stays blind prepend; non-empty client replays use _reconstruct_messages_from_thread_snapshot at the call site.
  2. Removed the small dedicated test file; coverage folded into test_snapshot_session.

This should also clear the confirm_changes / prepend CI regressions from the earlier combine attempt. Please re-check when convenient.

Annotate stored/incoming as list[dict[str, Any]] so Test Typing Checks pass.
@ktz03

Copy link
Copy Markdown
Contributor Author

Typing CI failed only on the folded replay fixture in test_snapshot_session (mypy inferred Mapping from nested toolCalls). Annotated stored/incoming as list[dict[str, Any]] on latest head — unit tests were already green across versions.

Comment thread python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py Outdated
Apply eavanvalkenburg suggestion: always overlap-merge via _reconstruct_messages_from_thread_snapshot; only set seeded_resume_from_snapshot when resume carries a non-empty transcript.
@ktz03

Copy link
Copy Markdown
Contributor Author

Eduard van Valkenburg (@eavanvalkenburg) Applied your suggestion on aeff82b: collapsed the duplicate _reconstruct_messages_from_thread_snapshot branches. Generic/non-service path always reconstructs; seeded_resume_from_snapshot is set only when resume_payload carries a non-empty client-replayed transcript (empty confirm_changes still relies on save-time prepend).

@ktz03

Copy link
Copy Markdown
Contributor Author

Eduard van Valkenburg (@eavanvalkenburg) Evan Mattson (@moonbox3) Friendly ping: eavan's duplicate-reconstruct suggestion is applied on aeff82b (CI was green on that head). Happy to take any remaining nits — otherwise a re-review when convenient would help move #8149 forward.

@FOWEPJF255

Copy link
Copy Markdown
Contributor

Following up: CI is still green after the review threads. Dedupe of client-replayed transcripts on AG-UI resume remains an important HITL correctness fix.

Anything left from the earlier maintainer notes that still needs a push, or is this waiting on a final review pass?

When resume entries are fully handled via retained results, re-emit those results even if reconstruction refilled raw_messages, so a client-replayed approval retry does not invoke the agent again.
@ktz03

Copy link
Copy Markdown
Contributor Author

Eduard van Valkenburg (@eavanvalkenburg) Updated PR body to the repository template and addressed the latest review feedback on this head — please re-review when convenient.

@moonbox3
Evan Mattson (moonbox3) added this pull request to the merge queue Sep 14, 2026
Merged via the queue into microsoft:main with commit 61d6c2c Sep 14, 2026
40 checks passed
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 resume duplicates the interrupted turn when the client replays its transcript

5 participants