Repository navigation
Python: fix(ag-ui): dedupe client-replayed transcripts on resume #8149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Evan Mattson (moonbox3)
merged 10 commits into
microsoft:main
from
ktz03:fix/ag-ui-resume-transcript-dedupe
Sep 14, 2026
Merged
Changes from 3 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
74f3ed5
fix(ag-ui): dedupe client-replayed transcripts on resume (#8140)
ktz03 3bb98c1
fix(ag-ui): keep resume seeding for empty messages
ktz03 4b66801
fix(ag-ui): centralize resume message reconcile on ThreadSnapshotSession
ktz03 e682021
fix(ag-ui): keep empty confirm_changes resumes unseeded
ktz03 40a0733
refactor(ag-ui): fold reconcile_resume_messages into resume_seeded_me…
ktz03 4b0f547
fix(ag-ui): restore resume_seeded prepend; fold #8140 tests
ktz03 10827a5
test(ag-ui): type folded #8140 snapshot replay fixture for mypy
ktz03 aeff82b
fix(ag-ui): collapse duplicate reconstruct on generic resume
ktz03 c0328fc
fix(ag-ui): short-circuit retained approval retries with transcript
ktz03 b9e7169
Merge branch 'main' into fix/ag-ui-resume-transcript-dedupe
moonbox3 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
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
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
92 changes: 92 additions & 0 deletions
92
python/packages/ag-ui/tests/ag_ui/test_resume_transcript_dedupe.py
|
moonbox3 marked this conversation as resolved.
Resolved
|
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| # Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| """Regression for AG-UI resume with client-replayed transcript (#8140).""" | ||
|
|
||
| from agent_framework_ag_ui._run_common import _reconstruct_messages_from_thread_snapshot | ||
| from agent_framework_ag_ui._snapshot_session import ThreadSnapshotSession | ||
| from agent_framework_ag_ui._snapshots import AGUIThreadSnapshot | ||
|
|
||
|
|
||
| def _stored_history() -> list[dict]: | ||
| return [ | ||
| {"id": "u1", "role": "user", "content": "please run the tool"}, | ||
| { | ||
| "id": "a1", | ||
| "role": "assistant", | ||
| "content": "", | ||
| "toolCalls": [{"id": "c1", "type": "function", "function": {"name": "needs_approval", "arguments": "{}"}}], | ||
| }, | ||
| ] | ||
|
|
||
|
|
||
| def test_resume_with_replayed_transcript_does_not_duplicate_history(): | ||
| """Clients that re-send the full transcript on resume must not double-write.""" | ||
| stored = _stored_history() | ||
| incoming = [ | ||
| *stored, | ||
| {"id": "u2", "role": "user", "content": "approved"}, | ||
| ] | ||
|
|
||
| reconstructed = _reconstruct_messages_from_thread_snapshot( | ||
| stored_messages=stored, | ||
| incoming_messages=incoming, | ||
| stored_interrupt=[{"interruptId": "int-1"}], | ||
| ) | ||
|
|
||
| assert [m.get("id") for m in reconstructed] == ["u1", "a1", "u2"] | ||
| assert len(reconstructed) == 3 | ||
|
|
||
|
|
||
| def test_resume_with_partial_new_turn_still_seeds_history(): | ||
| """Non-empty resume suffix is overlapped onto stored history without duplication.""" | ||
| stored = _stored_history() | ||
| incoming = [{"id": "u2", "role": "user", "content": "approved"}] | ||
|
ktz03 marked this conversation as resolved.
Resolved
|
||
|
|
||
| reconstructed = _reconstruct_messages_from_thread_snapshot( | ||
| stored_messages=stored, | ||
| incoming_messages=incoming, | ||
| stored_interrupt=[{"interruptId": "int-1"}], | ||
| ) | ||
|
|
||
| assert [m.get("id") for m in reconstructed] == ["u1", "a1", "u2"] | ||
|
|
||
|
|
||
| def test_reconstruct_with_empty_incoming_returns_empty(): | ||
| """Empty incoming is unchanged; call sites must use resume seeding instead.""" | ||
| stored = _stored_history() | ||
|
|
||
| assert ( | ||
| _reconstruct_messages_from_thread_snapshot( | ||
| stored_messages=stored, | ||
| incoming_messages=[], | ||
| stored_interrupt=[{"interruptId": "int-1"}], | ||
| ) | ||
| == [] | ||
| ) | ||
|
|
||
|
|
||
| def test_reconcile_resume_messages_empty_vs_replayed(): | ||
| """Session-owned reconcile covers interrupt-only and replayed transcript shapes.""" | ||
| stored = _stored_history() | ||
| session = ThreadSnapshotSession( | ||
| store=None, | ||
| scope=None, | ||
| thread_id="t1", | ||
| stored=AGUIThreadSnapshot( | ||
| messages=stored, | ||
| state=None, | ||
| interrupt=[{"interruptId": "int-1"}], | ||
| session_state=None, | ||
| ), | ||
| ) | ||
|
|
||
| empty = session.reconcile_resume_messages([]) | ||
| assert [m.get("id") for m in empty] == ["u1", "a1"] | ||
|
|
||
| replayed = session.reconcile_resume_messages( | ||
| [ | ||
| *stored, | ||
| {"id": "u2", "role": "user", "content": "approved"}, | ||
| ] | ||
| ) | ||
| assert [m.get("id") for m in replayed] == ["u1", "a1", "u2"] | ||
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.