Skip to content

Python: [Bug]: AgentLoopMiddleware fires turn-scoped after_run on a failed/early-closed streaming iteration using a stale prior response #9817

Description

@CorgiBoyG

Observed Behavior

When a successful AgentLoopMiddleware streaming iteration is followed by an exception or early close, the loop still invokes turn-scoped after_run providers (after_run_once_per_turn = True). The callback's SessionContext contains the final response from the previous iteration.

The streaming loop invokes the deferred providers unconditionally in its generator's finally block. The non-streaming path invokes them after try/finally, so a propagating exception skips the callback.

Source baseline:

Expected Behavior

Streaming and non-streaming loop runs should agree on whether a failed turn invokes deferred providers. Normal agent run finalization also skips after_run when an exception propagates.

The deferred callback should run after an intentional stop (should_continue, max_iterations, pending approval or handled MiddlewareTermination), and should be skipped when an iteration fails or the stream is abandoned.

Impact

Turn-scoped providers receive a stale response and can execute side effects after an incomplete streaming loop. With a configured after_strategy, CompactionProvider can run stored-history compaction through this unexpected callback. Its compaction strategy operates on session history rather than directly consuming the response.

The counting regressions establish the incorrect callback and streaming/non-streaming asymmetry. Provider-specific state consequences depend on the configured after_run behavior.

Steps to Reproduce

  1. Register a provider with after_run_once_per_turn = True that counts after_run calls.
  2. Run an agent with AgentLoopMiddleware(always_continue, max_iterations=5).
  3. Use a client that succeeds on iteration 1 and raises on iteration 2.
  4. Consume the streaming response and observe the expected RuntimeError.
  5. Observe one deferred callback, compared with zero for the equivalent non-streaming run.

For early close, continue pulling until the second client iteration has started, then close the stream. This ensures a previous completed response exists, without relying on the number of transport updates.

Proposed Fix

Gate deferred provider finalization on a flag set only after the streaming loop ends cleanly. Convert its three intentional-stop returns to breaks. Keep inner-stream closure and iteration-token cleanup unchanged.

Draft implementation and deterministic regressions are in #9818. On current main (03341b15), substituting the unfixed streaming method produces two expected regression failures (iteration exception and early close); the non-streaming control passes. With the fix, all three pass, and 1,201 related tests pass with the current middleware termination changes.

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 29, 2026
  2. CorgiBoyG commented on Oct 29, 2026

    @CorgiBoyG
    ContributorAuthor

    I'd like to work on this. I have a minimal fix (gate the streaming loop's turn-scoped after-providers on a clean exit) plus a deterministic red-on-main / green-on-fix regression covering the streaming exception, streaming early-close, and the non-streaming control. I'll open a PR shortly.

  3. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on Oct 29, 2026
  4. github-actions commented on Oct 29, 2026

    @github-actions
    Contributor

    🤖 Automated triage reproduction notes (agent-authored — trust but verify)

    Agent analysis

    The bug is in python/packages/core/agent_framework/_harness/_loop.py::_process_streaming, where lines 723-725 invoke turn-scoped after-providers unconditionally during generator cleanup. It occurs when a completed iteration is followed by an exception or when the outer stream is closed after that iteration, leaving holder["final"] populated with the stale response. Minimal repro: configure an after_run_once_per_turn counting provider, run AgentLoopMiddleware(always_continue) with a client that succeeds once then raises, consume the streaming response, and assert the provider was not called.

    • Failing test: python/packages/core/tests/core/test_harness_loop.py
    • Files examined: python/packages/core/agent_framework/_harness/_loop.py, python/packages/core/tests/core/test_harness_loop.py, python/packages/core/pyproject.toml
    • Tests run: test_turn_scoped_after_run_fires_once_per_loop, test_turn_scoped_after_run_fires_once_per_loop_streaming, test_turn_scoped_after_run_skipped_when_loop_iteration_raises[non_streaming], test_turn_scoped_after_run_skipped_when_loop_iteration_raises[streaming], test_turn_scoped_after_run_skipped_when_streaming_loop_closed_early
    • Current version: 1.23.0
  5. added
    compactionUsage: [Issues, PRs], Target: compaction
    harness[Issues, PRs], Target: harness-level items
    middlewareUsage: [Issues, PRs], Target: middleware related features
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 29, 2026
  6. CorgiBoyG commented on Oct 29, 2026

    @CorgiBoyG
    ContributorAuthor

    Opened draft PR #9818 with the scoped fix and the red-on-main / green-on-fix regression. Draft pending maintainer confirmation of the direction.

  7. added a commit that references this issue on Nov 1, 2026
    e93cf0d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

compactionUsage: [Issues, PRs], Target: compactionharness[Issues, PRs], Target: harness-level itemsmiddlewareUsage: [Issues, PRs], Target: middleware related featurespythonUsage: [Issues, PRs], Target: Pythonready-for-implementationMaintainers agree the problem and direction are ready for implementation.reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions