Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,22 @@ protected override async Task<AgentResponse> RunCoreAsync(
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 202 to 208

// Ask the evaluators whether to continue; stop when none of them request a re-invocation.
LoopNextStep step = await this.EvaluateAndBuildNextAsync(context, feedbackLog, initialSessionSnapshot, cancellationToken).ConfigureAwait(false);
if (!step.ShouldContinue)
{
return this.BuildResult(response, transcript, aggregatedUsage);
AgentResponse evalResult = this.BuildResult(response, transcript, aggregatedUsage);
if (context.AdditionalProperties.TryGetValue(LoopExitReason.AdditionalPropertiesKey, out object? exitReason))
{
(evalResult.AdditionalProperties ??= new())[LoopExitReason.AdditionalPropertiesKey] = exitReason;
}

return evalResult;
}

currentMessages = step.Messages;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ namespace Microsoft.Agents.AI;
/// </para>
/// <para>
/// Out-of-the-box implementations include <see cref="AIJudgeLoopEvaluator"/>, <see cref="DelegateLoopEvaluator"/>,
/// <see cref="CompletionMarkerLoopEvaluator"/>, and <see cref="TodoCompletionLoopEvaluator"/>. Implementations should be stateless and safe to share across
/// <see cref="CompletionMarkerLoopEvaluator"/>, <see cref="SignalProtocolLoopEvaluator"/>, and
/// <see cref="TodoCompletionLoopEvaluator"/>. Implementations should be stateless and safe to share across
/// concurrent loop runs; any per-run state must be stored on the supplied <see cref="LoopContext"/>.
/// </para>
/// </remarks>
Expand Down
42 changes: 42 additions & 0 deletions dotnet/src/Microsoft.Agents.AI/Harness/Loop/LoopExitReason.cs
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";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
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 the agent's latest response
/// contains <see cref="TaskCompleteToken"/> or <see cref="NeedInputToken"/>, and otherwise continues with feedback
/// reminding the agent to emit one of the signals.
/// </summary>
/// <remarks>
/// <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"/>.
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class SignalProtocolLoopEvaluator : LoopEvaluator
{
/// <summary>The token signalling that the task is complete.</summary>
public const string TaskCompleteToken = "TASK_COMPLETE:";

/// <summary>The token signalling that the agent needs input from the user.</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, emit '" + TaskCompleteToken + " <summary>'. " +
"If you need input, emit '" + 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);

string text = context.LastResponse.Text;

if (text.Contains(TaskCompleteToken, StringComparison.Ordinal))
{
context.AdditionalProperties[LoopExitReason.AdditionalPropertiesKey] = LoopExitReason.Completed;
return new ValueTask<LoopEvaluation>(LoopEvaluation.Stop());
}

if (text.Contains(NeedInputToken, StringComparison.Ordinal))
{
context.AdditionalProperties[LoopExitReason.AdditionalPropertiesKey] = LoopExitReason.NeedInput;
return new ValueTask<LoopEvaluation>(LoopEvaluation.Stop());
}
Comment thread
marklicata marked this conversation as resolved.

return new ValueTask<LoopEvaluation>(LoopEvaluation.Continue(this._feedbackMessage));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,15 @@ Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.set -> void
[MAAI001]virtual Microsoft.Agents.AI.AgentFileStore.SearchAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.Agents.AI.FileSearchResult!>!>!
[MAAI001]Microsoft.Agents.AI.FunctionInvocationContextExtensions
[MAAI001]static Microsoft.Agents.AI.FunctionInvocationContextExtensions.WrapWithPendingMiddleware(this Microsoft.Extensions.AI.FunctionInvocationContext! context, Microsoft.Extensions.AI.AIFunction! function) -> Microsoft.Extensions.AI.AIFunction!
[MAAI001]Microsoft.Agents.AI.LoopExitReason
[MAAI001]Microsoft.Agents.AI.SignalProtocolLoopEvaluator
[MAAI001]Microsoft.Agents.AI.SignalProtocolLoopEvaluator.SignalProtocolLoopEvaluator(string? feedbackMessage = null) -> void
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.AdditionalPropertiesKey = "loop_exit_reason" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.Cancelled = "cancelled" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.Completed = "completed" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.IterationCapReached = "iteration_cap_reached" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.NeedInput = "need_input" -> string!
[MAAI001]const Microsoft.Agents.AI.SignalProtocolLoopEvaluator.DefaultFeedbackMessage = "Continue working on the task. When done, emit 'TASK_COMPLETE: <summary>'. If you need input, emit 'NEED_INPUT: <question>'." -> string!
[MAAI001]const Microsoft.Agents.AI.SignalProtocolLoopEvaluator.NeedInputToken = "NEED_INPUT:" -> string!
[MAAI001]const Microsoft.Agents.AI.SignalProtocolLoopEvaluator.TaskCompleteToken = "TASK_COMPLETE:" -> string!
[MAAI001]override Microsoft.Agents.AI.SignalProtocolLoopEvaluator.EvaluateAsync(Microsoft.Agents.AI.LoopContext! context, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.LoopEvaluation!>
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,15 @@ Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.set -> void
[MAAI001]virtual Microsoft.Agents.AI.AgentFileStore.SearchAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.Agents.AI.FileSearchResult!>!>!
[MAAI001]Microsoft.Agents.AI.FunctionInvocationContextExtensions
[MAAI001]static Microsoft.Agents.AI.FunctionInvocationContextExtensions.WrapWithPendingMiddleware(this Microsoft.Extensions.AI.FunctionInvocationContext! context, Microsoft.Extensions.AI.AIFunction! function) -> Microsoft.Extensions.AI.AIFunction!
[MAAI001]Microsoft.Agents.AI.LoopExitReason
[MAAI001]Microsoft.Agents.AI.SignalProtocolLoopEvaluator
[MAAI001]Microsoft.Agents.AI.SignalProtocolLoopEvaluator.SignalProtocolLoopEvaluator(string? feedbackMessage = null) -> void
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.AdditionalPropertiesKey = "loop_exit_reason" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.Cancelled = "cancelled" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.Completed = "completed" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.IterationCapReached = "iteration_cap_reached" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.NeedInput = "need_input" -> string!
[MAAI001]const Microsoft.Agents.AI.SignalProtocolLoopEvaluator.DefaultFeedbackMessage = "Continue working on the task. When done, emit 'TASK_COMPLETE: <summary>'. If you need input, emit 'NEED_INPUT: <question>'." -> string!
[MAAI001]const Microsoft.Agents.AI.SignalProtocolLoopEvaluator.NeedInputToken = "NEED_INPUT:" -> string!
[MAAI001]const Microsoft.Agents.AI.SignalProtocolLoopEvaluator.TaskCompleteToken = "TASK_COMPLETE:" -> string!
[MAAI001]override Microsoft.Agents.AI.SignalProtocolLoopEvaluator.EvaluateAsync(Microsoft.Agents.AI.LoopContext! context, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.LoopEvaluation!>
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,15 @@ Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.set -> void
[MAAI001]virtual Microsoft.Agents.AI.AgentFileStore.SearchAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.Agents.AI.FileSearchResult!>!>!
[MAAI001]Microsoft.Agents.AI.FunctionInvocationContextExtensions
[MAAI001]static Microsoft.Agents.AI.FunctionInvocationContextExtensions.WrapWithPendingMiddleware(this Microsoft.Extensions.AI.FunctionInvocationContext! context, Microsoft.Extensions.AI.AIFunction! function) -> Microsoft.Extensions.AI.AIFunction!
[MAAI001]Microsoft.Agents.AI.LoopExitReason
[MAAI001]Microsoft.Agents.AI.SignalProtocolLoopEvaluator
[MAAI001]Microsoft.Agents.AI.SignalProtocolLoopEvaluator.SignalProtocolLoopEvaluator(string? feedbackMessage = null) -> void
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.AdditionalPropertiesKey = "loop_exit_reason" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.Cancelled = "cancelled" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.Completed = "completed" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.IterationCapReached = "iteration_cap_reached" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.NeedInput = "need_input" -> string!
[MAAI001]const Microsoft.Agents.AI.SignalProtocolLoopEvaluator.DefaultFeedbackMessage = "Continue working on the task. When done, emit 'TASK_COMPLETE: <summary>'. If you need input, emit 'NEED_INPUT: <question>'." -> string!
[MAAI001]const Microsoft.Agents.AI.SignalProtocolLoopEvaluator.NeedInputToken = "NEED_INPUT:" -> string!
[MAAI001]const Microsoft.Agents.AI.SignalProtocolLoopEvaluator.TaskCompleteToken = "TASK_COMPLETE:" -> string!
[MAAI001]override Microsoft.Agents.AI.SignalProtocolLoopEvaluator.EvaluateAsync(Microsoft.Agents.AI.LoopContext! context, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.LoopEvaluation!>
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,15 @@ Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.set -> void
[MAAI001]virtual Microsoft.Agents.AI.AgentFileStore.SearchAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.Agents.AI.FileSearchResult!>!>!
[MAAI001]Microsoft.Agents.AI.FunctionInvocationContextExtensions
[MAAI001]static Microsoft.Agents.AI.FunctionInvocationContextExtensions.WrapWithPendingMiddleware(this Microsoft.Extensions.AI.FunctionInvocationContext! context, Microsoft.Extensions.AI.AIFunction! function) -> Microsoft.Extensions.AI.AIFunction!
[MAAI001]Microsoft.Agents.AI.LoopExitReason
[MAAI001]Microsoft.Agents.AI.SignalProtocolLoopEvaluator
[MAAI001]Microsoft.Agents.AI.SignalProtocolLoopEvaluator.SignalProtocolLoopEvaluator(string? feedbackMessage = null) -> void
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.AdditionalPropertiesKey = "loop_exit_reason" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.Cancelled = "cancelled" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.Completed = "completed" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.IterationCapReached = "iteration_cap_reached" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.NeedInput = "need_input" -> string!
[MAAI001]const Microsoft.Agents.AI.SignalProtocolLoopEvaluator.DefaultFeedbackMessage = "Continue working on the task. When done, emit 'TASK_COMPLETE: <summary>'. If you need input, emit 'NEED_INPUT: <question>'." -> string!
[MAAI001]const Microsoft.Agents.AI.SignalProtocolLoopEvaluator.NeedInputToken = "NEED_INPUT:" -> string!
[MAAI001]const Microsoft.Agents.AI.SignalProtocolLoopEvaluator.TaskCompleteToken = "TASK_COMPLETE:" -> string!
[MAAI001]override Microsoft.Agents.AI.SignalProtocolLoopEvaluator.EvaluateAsync(Microsoft.Agents.AI.LoopContext! context, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.LoopEvaluation!>
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,15 @@ Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.set -> void
[MAAI001]virtual Microsoft.Agents.AI.AgentFileStore.SearchAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.Agents.AI.FileSearchResult!>!>!
[MAAI001]Microsoft.Agents.AI.FunctionInvocationContextExtensions
[MAAI001]static Microsoft.Agents.AI.FunctionInvocationContextExtensions.WrapWithPendingMiddleware(this Microsoft.Extensions.AI.FunctionInvocationContext! context, Microsoft.Extensions.AI.AIFunction! function) -> Microsoft.Extensions.AI.AIFunction!
[MAAI001]Microsoft.Agents.AI.LoopExitReason
[MAAI001]Microsoft.Agents.AI.SignalProtocolLoopEvaluator
[MAAI001]Microsoft.Agents.AI.SignalProtocolLoopEvaluator.SignalProtocolLoopEvaluator(string? feedbackMessage = null) -> void
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.AdditionalPropertiesKey = "loop_exit_reason" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.Cancelled = "cancelled" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.Completed = "completed" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.IterationCapReached = "iteration_cap_reached" -> string!
[MAAI001]const Microsoft.Agents.AI.LoopExitReason.NeedInput = "need_input" -> string!
[MAAI001]const Microsoft.Agents.AI.SignalProtocolLoopEvaluator.DefaultFeedbackMessage = "Continue working on the task. When done, emit 'TASK_COMPLETE: <summary>'. If you need input, emit 'NEED_INPUT: <question>'." -> string!
[MAAI001]const Microsoft.Agents.AI.SignalProtocolLoopEvaluator.NeedInputToken = "NEED_INPUT:" -> string!
[MAAI001]const Microsoft.Agents.AI.SignalProtocolLoopEvaluator.TaskCompleteToken = "TASK_COMPLETE:" -> string!
[MAAI001]override Microsoft.Agents.AI.SignalProtocolLoopEvaluator.EvaluateAsync(Microsoft.Agents.AI.LoopContext! context, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.LoopEvaluation!>
Loading
Loading