Repository navigation
.NET: add SignalProtocolLoopEvaluator for LoopAgent #9163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Mark Licata (marklicata)
wants to merge
5
commits into
microsoft:main
Choose a base branch
from
marklicata:agent-signal-protocol-dotnet
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
d64b47c
.NET: add SignalProtocolLoopEvaluator for LoopAgent
marklicata b245466
Address review feedback: assistant-only signal matching, clear stale …
marklicata 56ee092
Merge remote-tracking branch 'origin/main' into agent-signal-protocol…
marklicata 4f96e75
Require signal tokens at start of line
marklicata c798403
Merge remote-tracking branch 'origin/main' into agent-signal-protocol…
marklicata File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
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
42 changes: 42 additions & 0 deletions
42
dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopExitReason.cs
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using System.Diagnostics.CodeAnalysis; | ||
| using Microsoft.Shared.DiagnosticIds; | ||
|
|
||
| namespace Microsoft.Agents.AI; | ||
|
|
||
| /// <summary> | ||
| /// Provides typed exit-reason strings for <see cref="LoopAgent"/> runs. | ||
| /// Values are plain strings to remain JSON-transparent. The reason is surfaced under | ||
| /// <see cref="AdditionalPropertiesKey"/> in <c>AgentResponse.AdditionalProperties</c>. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// Exit reasons are stamped by <see cref="LoopAgent"/> on <strong>non-streaming</strong> runs only. | ||
| /// In streaming mode the response is assembled from yielded updates by the caller, so there is no | ||
| /// single <c>AgentResponse</c> object available inside the generator to stamp. | ||
| /// </para> | ||
| /// <para> | ||
| /// Evaluators such as <see cref="SignalProtocolLoopEvaluator"/> stamp the exit reason on | ||
| /// <see cref="LoopContext.AdditionalProperties"/> before returning <c>Stop()</c>; the loop then | ||
| /// copies that value to the returned <c>AgentResponse.AdditionalProperties</c>. | ||
| /// </para> | ||
| /// </remarks> | ||
| [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] | ||
| public static class LoopExitReason | ||
| { | ||
| /// <summary>The key used in <c>AgentResponse.AdditionalProperties</c> to surface the loop exit reason.</summary> | ||
| public const string AdditionalPropertiesKey = "loop_exit_reason"; | ||
|
|
||
| /// <summary>A <c>TASK_COMPLETE:</c> signal was detected in the agent's response.</summary> | ||
| public const string Completed = "completed"; | ||
|
|
||
| /// <summary>The max-iterations cap fired before any terminal signal was emitted.</summary> | ||
| public const string IterationCapReached = "iteration_cap_reached"; | ||
|
|
||
| /// <summary>A <c>NEED_INPUT:</c> signal was detected; the loop stopped to surface a question.</summary> | ||
| public const string NeedInput = "need_input"; | ||
|
|
||
| /// <summary>External cancellation (reserved for future use).</summary> | ||
| public const string Cancelled = "cancelled"; | ||
| } |
120 changes: 120 additions & 0 deletions
120
dotnet/src/Microsoft.Agents.AI/Harness/Loop/SignalProtocolLoopEvaluator.cs
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using System; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.Extensions.AI; | ||
| using Microsoft.Shared.DiagnosticIds; | ||
| using Microsoft.Shared.Diagnostics; | ||
|
|
||
| namespace Microsoft.Agents.AI; | ||
|
|
||
| /// <summary> | ||
| /// A <see cref="LoopEvaluator"/> implementing the GT1 signal protocol: the loop stops when an assistant message in the | ||
| /// agent's latest response contains <see cref="TaskCompleteToken"/> or <see cref="NeedInputToken"/> at the start of a | ||
| /// line, and otherwise continues with feedback reminding the agent to emit one of the signals. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// A token only counts when it starts its own line: it must appear at the start of the message or right after a line | ||
| /// break, optionally preceded by spaces or tabs. Mentions anywhere else on a line (for example | ||
| /// "I'll emit <c>TASK_COMPLETE:</c> when done", or a token quoted in backticks) are ignored, so the agent can talk about | ||
| /// the protocol without accidentally ending the loop. | ||
| /// </para> | ||
| /// <para> | ||
| /// <see cref="TaskCompleteToken"/> takes priority over <see cref="NeedInputToken"/>. When a signal is detected, the | ||
| /// matching <see cref="LoopExitReason"/> is stamped into <see cref="LoopContext.AdditionalProperties"/> under | ||
| /// <see cref="LoopExitReason.AdditionalPropertiesKey"/>. | ||
| /// </para> | ||
| /// </remarks> | ||
| [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] | ||
| public sealed class SignalProtocolLoopEvaluator : LoopEvaluator | ||
| { | ||
| /// <summary>The token signalling that the task is complete. Only recognized at the start of a line.</summary> | ||
| public const string TaskCompleteToken = "TASK_COMPLETE:"; | ||
|
|
||
| /// <summary>The token signalling that the agent needs input from the user. Only recognized at the start of a line.</summary> | ||
| public const string NeedInputToken = "NEED_INPUT:"; | ||
|
|
||
| /// <summary>The default feedback produced while neither signal is present.</summary> | ||
| public const string DefaultFeedbackMessage = | ||
| "Continue working on the task. When done, start a new line with '" + TaskCompleteToken + " <summary>'. " + | ||
| "If you need input, start a new line with '" + NeedInputToken + " <question>'."; | ||
|
|
||
| private readonly string _feedbackMessage; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="SignalProtocolLoopEvaluator"/> class. | ||
| /// </summary> | ||
| /// <param name="feedbackMessage">Optional custom feedback used when no signal is present. When <see langword="null"/>, <see cref="DefaultFeedbackMessage"/> is used.</param> | ||
| public SignalProtocolLoopEvaluator(string? feedbackMessage = null) | ||
| { | ||
| this._feedbackMessage = feedbackMessage ?? DefaultFeedbackMessage; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public override ValueTask<LoopEvaluation> EvaluateAsync(LoopContext context, CancellationToken cancellationToken = default) | ||
| { | ||
| _ = Throw.IfNull(context); | ||
|
|
||
| if (AssistantMessageHasSignal(context, TaskCompleteToken)) | ||
| { | ||
| context.AdditionalProperties[LoopExitReason.AdditionalPropertiesKey] = LoopExitReason.Completed; | ||
| return new ValueTask<LoopEvaluation>(LoopEvaluation.Stop()); | ||
| } | ||
|
|
||
| if (AssistantMessageHasSignal(context, NeedInputToken)) | ||
| { | ||
| context.AdditionalProperties[LoopExitReason.AdditionalPropertiesKey] = LoopExitReason.NeedInput; | ||
| return new ValueTask<LoopEvaluation>(LoopEvaluation.Stop()); | ||
| } | ||
|
|
||
| return new ValueTask<LoopEvaluation>(LoopEvaluation.Continue(this._feedbackMessage)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Determines whether any assistant-role message in the latest response contains <paramref name="token"/> at the | ||
| /// start of a line. Messages are matched individually so a token cannot be formed across message boundaries, and | ||
| /// non-assistant messages (for example tool results or echoed user input) never trigger a signal. | ||
| /// </summary> | ||
| private static bool AssistantMessageHasSignal(LoopContext context, string token) | ||
| { | ||
| foreach (ChatMessage message in context.LastResponse.Messages) | ||
| { | ||
| if (message.Role == ChatRole.Assistant && ContainsTokenAtLineStart(message.Text, token)) | ||
| { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Determines whether <paramref name="text"/> contains <paramref name="token"/> at the start of a line, allowing only | ||
| /// spaces and tabs between the preceding line break (or the start of the text) and the token. | ||
| /// </summary> | ||
| private static bool ContainsTokenAtLineStart(string text, string token) | ||
| { | ||
| int index = text.IndexOf(token, StringComparison.Ordinal); | ||
| while (index >= 0) | ||
| { | ||
| int i = index - 1; | ||
| while (i >= 0 && (text[i] == ' ' || text[i] == '\t')) | ||
| { | ||
| i--; | ||
| } | ||
|
|
||
| // Checking for '\n' covers both "\n" and "\r\n" line endings. | ||
| if (i < 0 || text[i] == '\n') | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| index = text.IndexOf(token, index + token.Length, StringComparison.Ordinal); | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
| } |
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
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.