Repository navigation
Python: fire loop turn-scoped after_run only on clean streaming exit - #9818
Draft
RongJie G (CorgiBoyG) wants to merge 1 commit into
Draft
RongJie G (CorgiBoyG) wants to merge 1 commit into
RongJie G (CorgiBoyG) wants to merge 1 commit into
Conversation
The streaming AgentLoopMiddleware path fired turn-scoped after_run providers (after_run_once_per_turn, e.g. CompactionProvider) from a finally block, so an iteration that raised - or an early stream close - still ran them with the stale final response from a prior iteration. That diverged from the non-streaming path and the normal non-loop run contract, both of which skip after_run when the run raises. Gate the streaming finally call on a completed_cleanly flag set only after a clean loop exit (should_continue stop, max_iterations, approval escape hatch, or MiddlewareTermination), converting the normal-stop returns to break. Normal completion behavior is unchanged; exception and early-close paths no longer fire turn-scoped after_run. Adds deterministic regressions for the streaming exception, streaming early-close, and the non-streaming exception control. Fixes microsoft#9817 Signed-off-by: CorgiBoyG <111257566+CorgiBoyG@users.noreply.github.com>
RongJie G (CorgiBoyG)
temporarily deployed
to
github-app-auth
October 29, 2026 03:42 — with
GitHub Actions
Inactive
RongJie G (CorgiBoyG)
temporarily deployed
to
github-app-auth
October 29, 2026 03:42 — with
GitHub Actions
Inactive
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues were identified for the change restricting streaming turn-scoped finalization to clean loop exits.
No open issues 🎉
What changed in this PR
Addresses #9817 by aligning streaming loop turn-scoped after_run behavior with non-streaming runs, preventing finalization with a stale response after an exception or early stream closure.
Changes:
- Gates turn-scoped providers on clean loop completion while preserving unconditional iteration-token cleanup.
- Converts normal-stop returns to breaks so successful exits still trigger finalization.
- Adds regression tests for streaming exceptions, early closure, and the non-streaming exception control.
| File | Description |
|---|---|
python/packages/core/tests/core/test_harness_loop.py |
Adds regression coverage for failed and early-closed loop runs. |
python/packages/core/agent_framework/_harness/_loop.py |
Restricts streaming turn-scoped finalization to clean exits. |
Review effort: Balanced
This branch was previously deployed
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
A streaming
AgentLoopMiddlewarerun fires turn-scopedafter_runproviders (after_run_once_per_turn = True, e.g.CompactionProvider) from afinallyblock, so an iteration that raises — or an early stream close — still runs them with the stale final response from a prior iteration. The non-streaming path keeps that call outside itstry/finally, and a normal non-loop run only fires result hooks on successful finalization. Streaming therefore diverged from both, silently advancing once-per-turn finalization (e.g. the compaction/summarization turn boundary) off a superseded response on a failed turn.Description & Review Guide
returns becomebreak, acompleted_cleanlyflag is set after the loop body, and thefinallycall is gated on that flag. An exception, or an earlyaclose()/GeneratorExit, leaves the flag unset and skips the call. Only the loop-iteration token cleanup remains unconditional infinally.max_iterations, approval escape hatch,MiddlewareTerminationall still fire turn-scopedafter_runexactly once). Exception and early-close paths now skip it, matching the non-streaming path and the non-loop run contract.completed_cleanly = Truebefore the generator is finalized, and that no exception/early-close path sets it.Contribution Checklist
tests/core/: 7056 passed, 19 skipped, 0 failed).AI Assistance
This is an AI-assisted contribution. The defect was found by source audit; AI assisted with drafting the regression tests, the fix, and this description. The root-cause analysis, the empirical contract verification, and the red-on-main / green-on-fix validation were reviewed and confirmed by me.
Related Issue
Fixes #9817
Draft pending maintainer confirmation of the scoped fix direction.