Skip to content

Add signal-based completion detection and typed exit reasons to AgentLoopMiddleware #8860

Description

Summary

AgentLoopMiddleware supports two completion detection mechanisms today: a developer-supplied should_continue predicate, and a judge-driven model that evaluates whether the original request was answered. Neither produces typed exit reasons — the caller cannot distinguish "agent finished the task" from "iteration cap fired" from "cancelled externally."

This issue proposes adding a third, lighter completion mechanism (text signal scanning) and typed exit reasons to the existing AgentLoopMiddleware in both the Python and .NET implementations.

We intend to take on implementation.

User need

When a user hands a task to an autonomous agent, they need a clear answer to whether it completed successfully. Today, every autonomous loop exit looks the same to the caller. Users also need a standard way for an agent to pause and surface a specific question — "I need your decision on X before I can continue" — without the loop terminating entirely.

Developer need

Teams building autonomous agents on MAF reimplement completion detection from scratch. The existing should_continue interface supports this, but there is no standard implementation. Building a signal-scanner predicate is simple; not having a canonical one means every team invents their own convention, and none of them are interoperable.

The default max_iterations=10 (Python) is appropriate for judge-driven quality refinement loops. For autonomous task-completion loops, this default silently terminates most real tasks before they complete. Guidance is needed.

What's already there

  • Python: AgentLoopMiddleware (python/packages/core/agent_framework/_harness/_loop.py) — full loop middleware with should_continue predicate, max_iterations safety cap (default 10), judge-driven completion, feedback log, session snapshot/restore, and nudge injection.
  • .NET: AgentLoopMiddleware equivalent in dotnet/src/Microsoft.Agents.AI/Harness/Loop/ and dotnet/src/Microsoft.Agents.AI.Harness/.
  • Both: The loop already injects a nudge asking the agent to say when it is complete. There is no detection of that signal in either implementation.

Proposal

1. signal_should_continue helper — a ~50-line should_continue predicate implementation that scans the agent's last response text for two tokens:

TASK_COMPLETE: <summary>   →  returns False (stop), exit reason = "completed"
NEED_INPUT: <question>     →  pauses loop, surfaces question, waits for human resume()
(no signal, no tools)      →  returns True (continue), nudge injected

2. Typed loop exit reason — a LoopExitReason (enum or string literal union) surfaced on the response returned by the loop:

Exit reason Trigger
completed Signal detected or judge verdict answered=True
iteration_cap_reached max_iterations fired
budget_exceeded Token cap hit (if budget integration is added separately)
time_exceeded Wall-clock cap hit (if budget integration is added separately)
cancelled External cancellation

3. Documentation guidance on max_iterations — explicitly document that the default is calibrated for short quality-refinement loops. For autonomous task-completion loops, 50–150 is the appropriate range.

Scope

  • Both Python and .NET implementations are in scope. The Python harness (python/packages/core/agent_framework/_harness/) and .NET harness (dotnet/src/Microsoft.Agents.AI/Harness/ and dotnet/src/Microsoft.Agents.AI.Harness/) maintain feature parity; changes to one require equivalent changes in the other.
  • New signal_should_continue function and LoopExitReason type (experimental-flagged in both languages)
  • No new packages, no new dependencies

Notes

The signal convention (TASK_COMPLETE: / NEED_INPUT:) is drawn from a production runtime where it ran at scale. The implementation is a should_continue predicate that slots into the existing loop interface — it does not require changes to the loop's core mechanics in either language.

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Sep 29, 2026
  2. changed the title [-]Add signal-based completion detection and typed exit reasons to AgentLoopMiddleware[/-] [+]Python: Add signal-based completion detection and typed exit reasons to AgentLoopMiddleware[/+] on Sep 29, 2026
  3. changed the title [-]Python: Add signal-based completion detection and typed exit reasons to AgentLoopMiddleware[/-] [+]Add signal-based completion detection and typed exit reasons to AgentLoopMiddleware[/+] on Sep 29, 2026
  4. removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Sep 29, 2026
  5. eavanvalkenburg commented on Nov 2, 2026

    @eavanvalkenburg
    Member

    Mark Licata (@marklicata) I would like to narrow the Python implementation to a sample for now, with a small core change to report and preserve loop-exit metadata.

    AgentResponse.additional_properties already gives us the structure we need. Our own judge callback can set loop_exit_reason = "completed" when the judge says the task is answered. The loop can set "iteration_cap_reached" when its cap stops execution. A sample should_continue callback can teach and parse the TASK_COMPLETE: / NEED_INPUT: convention, set "completed" or "need_input" on the response, and return False. The question can stay in the response text, and the calling application keeps control of how it collects input and resumes the conversation. Cancellation should continue to propagate normally.

    There is one small core detail to address: the current non-streaming loop collects response metadata before calling should_continue, then uses that earlier copy on the aggregated response. We should preserve metadata changes made by the callback, consistently for aggregated responses and streaming final responses. The sample should also explain the iteration-cap precedence: the cap currently skips should_continue, so any sample-specific rule that a signal on the last allowed iteration wins needs explicit final-response classification.

    This keeps the text tokens, parsing rules, and quoting/code-fence handling in the sample rather than making them a framework contract. We do not need public SignalParser, signal_should_continue(), LoopExitReason, or get_loop_exit_reason() APIs for this first version. Core coverage should focus on metadata preservation and framework-owned stop reasons; the sample should cover its parsing behavior and explain what to set when.

    Especially for Python, this is enough structure for a v0. Once we get feedback from people using it, and there is demand for a more extensive version, we can reconsider a more complete integration. I would rather establish that need through usage than commit to the full public API now. This proposes a narrower Python scope for #9162; it is not a request to treat all of the original cross-language issue requirements as delivered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

agentsUsage: [Issues, PRs], Target: Single agentpythonUsage: [Issues, PRs], Target: Python

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions