Repository navigation
Python: add signal protocol for AgentLoopMiddleware autonomous loops - #9162
Mark Licata (marklicata) wants to merge 5 commits into
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Exit metadata can remain stale, and the public API does not consistently match its documented experimental scope and exit-reason contract.
4 open findings
What changed in this PR
Adds a Python signal protocol for autonomous AgentLoopMiddleware termination and typed exit-reason reporting.
Changes:
- Adds task-complete and human-input signal parsing.
- Stamps iteration-cap metadata for streaming and non-streaming loops.
- Exports and tests the new public APIs.
| File | Description |
|---|---|
test_harness_loop.py |
Tests signal parsing and loop exits. |
AGENTS.md |
Documents the signal protocol. |
_signals.py |
Implements signals and exit reasons. |
_loop.py |
Stamps iteration-cap metadata. |
__init__.pyi |
Exposes typed public imports. |
__init__.py |
Adds runtime public exports. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
…imental markers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Per review feedback, fold the small signal protocol (LoopExitReason, SignalParser, signal_should_continue, get_loop_exit_reason and the token / exit-reason-key constants) into _harness/_loop.py alongside AgentLoopMiddleware and delete _harness/_signals.py. The middleware now stamps the cap reason via the shared LOOP_EXIT_REASON_KEY / LoopExitReason.iteration_cap_reached constants. Public exports are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SignalProtocolLoopEvaluator now only recognizes TASK_COMPLETE: / NEED_INPUT: when the token starts a line of an assistant message (optionally preceded by spaces or tabs). Mid-sentence or backtick-quoted mentions such as "I'll emit `TASK_COMPLETE:` when done" no longer stop the loop. Matches the Python signal protocol (microsoft#9162). DefaultFeedbackMessage now tells the model to start a new line with the token; the PublicAPI.Unshipped.txt entries are updated because the analyzer tracks const values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TASK_COMPLETE: and NEED_INPUT: now count as signals only when they start a line (leading whitespace allowed) in an assistant message, so mid-sentence or backtick-quoted mentions no longer stop the loop or stamp an exit reason. SignalParser and get_loop_exit_reason share one detection helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-python Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| # A token only counts when it starts a line (leading spaces/tabs allowed), so a mid-sentence or | ||
| # backtick-quoted mention such as "I'll emit `TASK_COMPLETE:` when done" is not a signal. | ||
| # The ``text`` group captures the rest of the token's line. | ||
| _TASK_COMPLETE_PATTERN = re.compile(rf"^[ \t]*{re.escape(TASK_COMPLETE_TOKEN)}(?P<text>[^\n]*)", re.MULTILINE) |
There was a problem hiding this comment.
This start-of-line rule still treats a token inside a fenced Markdown code block as a real signal. For example, an assistant explaining the protocol with a fenced TASK_COMPLETE: example line terminates the loop. Please exclude fenced-code regions, or otherwise require the signal to occur outside code, and add fenced TASK_COMPLETE: and NEED_INPUT: regressions.


Motivation & Context
Agents running inside
AgentLoopMiddlewarehave no standard way to say "I'm done" or "I need a human". Each team writes its ownshould_continuepredicate. This adds a text-based signal protocol so the agent can communicate loop termination intent directly in its response text:TASK_COMPLETE: <summary>: agent signals it is done; loop stopsNEED_INPUT: <question>: agent needs human input; loop stopsThis is the Python half of the work previously proposed in #8993, split per language at the reviewers' request. The .NET counterpart is #9163.
Description & Review Guide
What are the major changes?
SignalParser/signal_should_continue()(agent_framework._harness._signals): drop-inshould_continuepredicate forAgentLoopMiddleware. It scans assistant messages only, so user-role nudges injected between iterations are ignored.LoopExitReason: typed exit reason strings"completed","iteration_cap_reached","need_input","cancelled"get_loop_exit_reason(response): reads the exit reason from assistant text first, thenadditional_properties; a signal always wins over the iteration capAgentLoopMiddlewarenow stampsadditional_properties["loop_exit_reason"] = "iteration_cap_reached"when the cap fires (streaming and non-streaming)agent_framework;__init__.pyiandpackages/core/AGENTS.mdupdatedWhat is the impact of these changes?
Additive and marked experimental. Loops that don't use
SignalParsersee no behavior change beyond the newloop_exit_reasonstamp when the cap fires.What do you want reviewers to focus on?
get_loop_exit_reason: assistant text first,additional_propertiessecond, so a signal on the final allowed iteration is reported ascompleted, notiteration_cap_reachedRelated Issue
Part of #8860, together with #9163 for .NET. The issue is closed manually once both PRs have merged.
Contribution Checklist
🤖 Generated with Claude Code