Python: Python + .NET: add signal protocol for autonomous agent loops - #8993
Draft
Mark Licata (marklicata) wants to merge 2 commits into
Draft
Mark Licata (marklicata) wants to merge 2 commits into
Mark Licata (marklicata) wants to merge 2 commits into
Conversation
Introduces a text-based signal convention so agents can communicate
loop termination intent without custom predicate code per team.
Signal tokens (case-sensitive, scanned from assistant messages only):
TASK_COMPLETE: <summary> -- agent signals completion, loop stops
NEED_INPUT: <question> -- agent needs human input, loop stops
New module: agent_framework._harness._signals
- SignalParser: drop-in should_continue predicate for AgentLoopMiddleware
- LoopExitReason: typed exit reason strings matching FinishReason convention
("completed", "iteration_cap_reached", "need_input", "cancelled")
- signal_should_continue(): factory returning a SignalParser instance
- get_loop_exit_reason(response): reads assistant text first, then falls
back to additional_properties; signal always wins over iteration cap
- LOOP_EXIT_REASON_KEY, LOOP_NEED_INPUT_KEY: documented convention keys
AgentLoopMiddleware (_loop.py):
- Stamps loop_exit_reason='iteration_cap_reached' in additional_properties
when max_iterations fires (both streaming and non-streaming paths)
- No import of _signals; uses string literal to keep modules decoupled
Exports: 8 new public symbols in agent_framework namespace + __init__.pyi
CHANGELOG.md and python/packages/core/AGENTS.md updated.
Tests: 117 total (17 new signal protocol tests covering unit, integration,
streaming cap, and nudge-text false-positive regression).
Generated with Amplifier
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Introduces SignalProtocolLoopEvaluator and LoopExitReason to match
the Python signal protocol, using the same token strings and
AdditionalProperties key so tooling can treat both languages uniformly.
New types (Microsoft.Agents.AI namespace):
- SignalProtocolLoopEvaluator: LoopEvaluator that scans LastResponse.Text
for TASK_COMPLETE: and NEED_INPUT: tokens. Stamps context.AdditionalProperties
before returning Stop() so the exit reason survives into the response.
- LoopExitReason: static class with const string values and AdditionalPropertiesKey
("loop_exit_reason") matching the Python convention.
LoopAgent (non-streaming path):
- Stamps LoopExitReason.IterationCapReached when max_iterations fires.
- Copies exit reason from context.AdditionalProperties to the returned
AgentResponse when an evaluator stops the loop.
Note: streaming path does not stamp AdditionalProperties (the streaming
response is assembled by the caller from yielded updates; there is no
single response object to stamp inside the generator). Non-streaming
is fully covered. See LoopExitReason XML docs.
PublicAPI.Unshipped.txt updated for all target frameworks.
Tests: 3343 total (11 new SignalProtocolLoopEvaluatorTests, all passing).
Generated with Amplifier
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Mark Licata (marklicata)
deployed
to
github-app-auth
October 2, 2026 23:08 — with
GitHub Actions
Active
Mark Licata (marklicata)
deployed
to
github-app-auth
October 2, 2026 23:25 — with
GitHub Actions
Active
Contributor
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
Review effort: Lite
Findings: 1
Open (8)
This stampsiteration_cap_reachedsolely based on hitting the cap, even if the last response… · Newstring.Contains(string)uses culture-sensitive comparison by default. Since these protocol tokens… · New These hard-coded protocol strings duplicate values that are now part of a public convention. If… · New These hard-coded protocol strings duplicate values that are now part of a public convention. If… · New Concatenating assistant messages without a separator can create false positives across message… · New These assertions will raise aTypeErrorifresponse.additional_propertiesis everNone… · New These assertions will raise aTypeErrorifresponse.additional_propertiesis everNone… · New This uses a magic string key (\"loop_exit_reason\") while adjacent tests use… · New
What changed in this PR
Adds a cross-language (Python + .NET) text-based “signal protocol” for autonomous agent loops so agents can stop looping by emitting TASK_COMPLETE: or NEED_INPUT: tokens, and surfaces a typed loop-exit reason for consumers.
Changes:
- Python: introduces
_harness._signalsutilities (SignalParser,signal_should_continue,LoopExitReason,get_loop_exit_reason) and stampsloop_exit_reason=iteration_cap_reachedwhen the iteration cap fires (streaming + non-streaming). - .NET: adds
SignalProtocolLoopEvaluator+LoopExitReasonconstants and stampsloop_exit_reasonfor non-streamingLoopAgentresults and evaluator-driven stops. - Updates tests, public exports/API listings, and docs/changelog.
| File | Description |
|---|---|
| python/packages/core/agent_framework/_harness/_signals.py | Adds signal-token parsing + loop-exit reason derivation helper. |
| python/packages/core/agent_framework/_harness/_loop.py | Stamps loop_exit_reason when max-iteration cap fires (streaming + non-streaming). |
| python/packages/core/tests/core/test_harness_loop.py | Adds Python unit tests for signal parsing and exit-reason behavior. |
| python/packages/core/agent_framework/__init__.py | Re-exports new public signal-protocol symbols. |
| python/packages/core/agent_framework/__init__.pyi | Adds typing exports for new public signal-protocol symbols. |
| python/packages/core/AGENTS.md | Documents the new signal protocol and exit-reason semantics. |
| python/CHANGELOG.md | Notes the new signal protocol and exit-reason stamping behavior. |
| dotnet/src/Microsoft.Agents.AI/Harness/Loop/SignalProtocolLoopEvaluator.cs | Adds evaluator implementing the token convention and stamping exit reason in context properties. |
| dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopExitReason.cs | Adds typed exit-reason constants + docs about non-streaming stamping. |
| dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopAgent.cs | Stamps exit reason on non-streaming results for cap-reached and evaluator-stop scenarios. |
| dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopEvaluator.cs | Updates docs to include SignalProtocolLoopEvaluator as an OOTB evaluator. |
| dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/Loop/SignalProtocolLoopEvaluatorTests.cs | Adds unit tests for evaluator stop/continue behavior and stamping. |
| dotnet/src/Microsoft.Agents.AI/PublicAPI/net472/PublicAPI.Unshipped.txt | Declares new public APIs for net472. |
| dotnet/src/Microsoft.Agents.AI/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt | Declares new public APIs for netstandard2.0. |
| dotnet/src/Microsoft.Agents.AI/PublicAPI/net8.0/PublicAPI.Unshipped.txt | Declares new public APIs for net8.0. |
| dotnet/src/Microsoft.Agents.AI/PublicAPI/net9.0/PublicAPI.Unshipped.txt | Declares new public APIs for net9.0. |
| dotnet/src/Microsoft.Agents.AI/PublicAPI/net10.0/PublicAPI.Unshipped.txt | Declares new public APIs for net10.0. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
202
to
208
| if (iteration >= this._maxIterations) | ||
| { | ||
| this.LogMaxIterationsReached(iteration); | ||
| return this.BuildResult(response, transcript, aggregatedUsage); | ||
| AgentResponse capResult = this.BuildResult(response, transcript, aggregatedUsage); | ||
| (capResult.AdditionalProperties ??= new())[LoopExitReason.AdditionalPropertiesKey] = LoopExitReason.IterationCapReached; | ||
| return capResult; | ||
| } |
Comment on lines
+51
to
+63
| string text = context.LastResponse.Text; | ||
|
|
||
| if (text.Contains(TaskCompleteToken)) | ||
| { | ||
| context.AdditionalProperties[LoopExitReason.AdditionalPropertiesKey] = LoopExitReason.Completed; | ||
| return new ValueTask<LoopEvaluation>(LoopEvaluation.Stop()); | ||
| } | ||
|
|
||
| if (text.Contains(NeedInputToken)) | ||
| { | ||
| context.AdditionalProperties[LoopExitReason.AdditionalPropertiesKey] = LoopExitReason.NeedInput; | ||
| return new ValueTask<LoopEvaluation>(LoopEvaluation.Stop()); | ||
| } |
Comment on lines
+606
to
+607
| if cap_fired and final_result is not None: | ||
| final_result.additional_properties.setdefault("loop_exit_reason", "iteration_cap_reached") |
Comment on lines
+716
to
+717
| if cap_fired and holder["final"] is not None: | ||
| holder["final"].additional_properties.setdefault("loop_exit_reason", "iteration_cap_reached") |
Comment on lines
+164
to
+168
| assistant_text = "".join(m.text or "" for m in response.messages if m.role == "assistant") | ||
| if TASK_COMPLETE_TOKEN in assistant_text: | ||
| return LoopExitReason.completed | ||
| if NEED_INPUT_TOKEN in assistant_text: | ||
| return LoopExitReason.need_input |
|
|
||
| assert client.call_count == 1 | ||
| assert get_loop_exit_reason(response) == LoopExitReason.completed | ||
| assert LOOP_EXIT_REASON_KEY not in response.additional_properties |
|
|
||
| assert client.call_count == 1 | ||
| assert get_loop_exit_reason(response) == LoopExitReason.need_input | ||
| assert LOOP_EXIT_REASON_KEY not in response.additional_properties |
| response = await agent.run("task") | ||
|
|
||
| assert client.call_count == 2 | ||
| assert response.additional_properties.get("loop_exit_reason") == LoopExitReason.iteration_cap_reached |
5 tasks done
This branch was successfully 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
Closes #8860
Adds a text-based signal protocol so agents running inside
AgentLoopMiddleware(Python) orLoopAgent(.NET) can communicate loop termination intent without custom predicate code per team.Two tokens, emitted by the agent in its response text:
TASK_COMPLETE: <summary>— agent signals it is done; loop stopsNEED_INPUT: <question>— agent needs human input; loop stopsDescription & Review Guide
What are the major changes?
Python (
agent_framework._harness._signals):SignalParser/signal_should_continue()— drop-inshould_continuepredicate forAgentLoopMiddleware; scans assistant messages only (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 exit reason from assistant text first, thenadditional_properties; signal always wins over the iteration capAgentLoopMiddlewarenow stampsadditional_properties["loop_exit_reason"] = "iteration_cap_reached"when the cap fires (both streaming and non-streaming paths)__init__.pyiupdated;CHANGELOG.mdandAGENTS.mdupdated.NET (
Microsoft.Agents.AI):SignalProtocolLoopEvaluator—LoopEvaluatormatching the Python token convention; stampscontext.AdditionalProperties["loop_exit_reason"]before returningStop()LoopExitReason— static class with const string values andAdditionalPropertiesKey; XML docs note that exit reason stamping applies to non-streaming onlyLoopAgent(non-streaming): stamps cap and evaluator-stop exit reasons on the returnedAgentResponse.AdditionalPropertiesPublicAPI.Unshipped.txtupdated for all 5 target frameworksWhat is the impact of these changes?
No breaking changes. All new API is additive and marked
[Experimental]. Teams not usingSignalParser/SignalProtocolLoopEvaluatorsee no behavior change.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_reached)LoopExitReasonXML docs — exit reason is only stamped on non-streaming responsesRelated Issue
Closes #8860
Contribution Checklist