From 9cc413593ef2f33e144751f7b4f067a6b163ead4 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:31:04 +0000 Subject: [PATCH] Graduate mode and todo providers --- .../Commands/ModeCommandHandler.cs | 4 +- .../HarnessAgentRunner.cs | 16 +- .../Harness_Shared_Console/HarnessConsole.cs | 4 +- .../Observers/ConsoleObserver.cs | 4 +- .../Observers/PlanningOutputObserver.cs | 6 +- .../Harness/AgentMode/AgentModeProvider.cs | 155 ++++++++++++++---- .../AgentMode/AgentModeProviderOptions.cs | 20 +-- .../Harness/AgentMode/AgentModeState.cs | 3 - .../Loop/TodoCompletionLoopEvaluator.cs | 2 +- .../Harness/Todo/TodoCompleteInput.cs | 3 - .../Harness/Todo/TodoItem.cs | 3 - .../Harness/Todo/TodoItemInput.cs | 3 - .../Harness/Todo/TodoProvider.cs | 14 +- .../Harness/Todo/TodoProviderOptions.cs | 3 - .../Harness/Todo/TodoState.cs | 3 - .../AgentMode/AgentModeProviderTests.cs | 40 ++--- .../Loop/TodoCompletionLoopEvaluatorTests.cs | 6 +- 17 files changed, 181 insertions(+), 108 deletions(-) diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Commands/ModeCommandHandler.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Commands/ModeCommandHandler.cs index 09c2cd3cb5b..eae3f50768d 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Commands/ModeCommandHandler.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Commands/ModeCommandHandler.cs @@ -43,7 +43,7 @@ public override async ValueTask TryHandleAsync(string input, AgentSession string[] parts = input.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (parts.Length < 2) { - string current = this._modeProvider.GetMode(session); + string current = await this._modeProvider.GetModeAsync(session).ConfigureAwait(false); await ux.WriteInfoLineAsync($"Current mode: {current}").ConfigureAwait(false); return true; } @@ -52,7 +52,7 @@ public override async ValueTask TryHandleAsync(string input, AgentSession try { - this._modeProvider.SetMode(session, newMode); + await this._modeProvider.SetModeAsync(session, newMode).ConfigureAwait(false); ux.CurrentMode = newMode; await ux.WriteInfoLineAsync($"Switched to {newMode} mode.", ModeColors.Get(newMode, this._modeColors)).ConfigureAwait(false); } diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessAgentRunner.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessAgentRunner.cs index ced5444c5ad..f9c77820228 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessAgentRunner.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessAgentRunner.cs @@ -93,7 +93,7 @@ internal async Task OnUserInputAsync(string text) { if (await handler.TryHandleAsync(text, this._session, this._ux).ConfigureAwait(false)) { - this._ux.CurrentMode = this._modeProvider?.GetMode(this._session); + this._ux.CurrentMode = this._modeProvider is null ? null : await this._modeProvider.GetModeAsync(this._session).ConfigureAwait(false); return; } } @@ -136,7 +136,7 @@ internal async Task StartAgentTurnAsync(IList messages) { if (messages.Count == 0) { - this.CompleteTurn(); + await this.CompleteTurnAsync().ConfigureAwait(false); return; } @@ -158,10 +158,10 @@ private async Task RunAgentLoopAsync(IList messages) var runOptions = new AgentRunOptions(); foreach (var observer in this._observers) { - observer.ConfigureRunOptions(runOptions, this._agent, this._session); + await observer.ConfigureRunOptionsAsync(runOptions, this._agent, this._session).ConfigureAwait(false); } - this._ux.CurrentMode = this._modeProvider?.GetMode(this._session); + this._ux.CurrentMode = this._modeProvider is null ? null : await this._modeProvider.GetModeAsync(this._session).ConfigureAwait(false); this._ux.BeginStreaming(); this._ux.BeginStreamingOutput(); @@ -171,7 +171,7 @@ private async Task RunAgentLoopAsync(IList messages) { if (this._modeProvider is not null) { - string currentMode = this._modeProvider.GetMode(this._session); + string currentMode = await this._modeProvider.GetModeAsync(this._session).ConfigureAwait(false); if (currentMode != this._ux.CurrentMode) { this._ux.CurrentMode = currentMode; @@ -261,13 +261,13 @@ private async Task RunAgentLoopAsync(IList messages) nextMessages = drained.Count > 0 ? [.. drained] : null; } - this.CompleteTurn(); + await this.CompleteTurnAsync().ConfigureAwait(false); } - private void CompleteTurn() + private async Task CompleteTurnAsync() { this._ux.EndStreaming(); - this._ux.CurrentMode = this._modeProvider?.GetMode(this._session); + this._ux.CurrentMode = this._modeProvider is null ? null : await this._modeProvider.GetModeAsync(this._session).ConfigureAwait(false); } /// diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessConsole.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessConsole.cs index 572e3d4a7f1..94fc8d8f100 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessConsole.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessConsole.cs @@ -40,9 +40,11 @@ public static async Task RunAgentAsync(AIAgent agent, string userPrompt, Harness ? await options.SessionFactory(agent) : await agent.CreateSessionAsync(); + string? initialMode = modeProvider is null ? null : await modeProvider.GetModeAsync(session); + using var component = new HarnessAppComponent( placeholder: userPrompt, - initialMode: modeProvider?.GetMode(session), + initialMode: initialMode, inputEnabled: messageInjector is not null, runnerFactory: ux => new HarnessAgentRunner( agent: agent, diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Observers/ConsoleObserver.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Observers/ConsoleObserver.cs index f5a04d9719e..cd86bf7c9df 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Observers/ConsoleObserver.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Observers/ConsoleObserver.cs @@ -20,9 +20,7 @@ public abstract class ConsoleObserver /// The run options to configure. /// The agent being interacted with. /// The current agent session. - public virtual void ConfigureRunOptions(AgentRunOptions options, AIAgent agent, AgentSession session) - { - } + public virtual ValueTask ConfigureRunOptionsAsync(AgentRunOptions options, AIAgent agent, AgentSession session) => default; /// /// Called for each in the response stream, regardless of diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Observers/PlanningOutputObserver.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Observers/PlanningOutputObserver.cs index bf534c7d6ec..d06493b18d9 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Observers/PlanningOutputObserver.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Observers/PlanningOutputObserver.cs @@ -40,9 +40,9 @@ public PlanningOutputObserver(AgentModeProvider modeProvider, string planModeNam } /// - public override void ConfigureRunOptions(AgentRunOptions options, AIAgent agent, AgentSession session) + public override async ValueTask ConfigureRunOptionsAsync(AgentRunOptions options, AIAgent agent, AgentSession session) { - if (this.IsPlanningMode(this._modeProvider.GetMode(session))) + if (this.IsPlanningMode(await this._modeProvider.GetModeAsync(session).ConfigureAwait(false))) { options.ResponseFormat = ChatResponseFormat.ForJsonSchema(); } @@ -205,7 +205,7 @@ private ChoiceFollowUpQuestion BuildApprovalAction(PlanningQuestion question, Ag if (selection == ApproveOption) { - this._modeProvider.SetMode(session, this._executionModeName); + await this._modeProvider.SetModeAsync(session, this._executionModeName).ConfigureAwait(false); await ux.WriteInfoLineAsync( $"✅ Switched to {this._executionModeName} mode.", ModeColors.Get(this._executionModeName, this._modeColors)).ConfigureAwait(false); diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs index 034dbe42107..9f81f6c4e62 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs @@ -2,12 +2,12 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; -using Microsoft.Shared.DiagnosticIds; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; @@ -34,12 +34,15 @@ namespace Microsoft.Agents.AI; /// /// /// -/// Public helper methods and allow external code +/// Public helper methods and allow external code /// to programmatically read and change the mode. /// +/// +/// All operations are thread-safe; concurrent reads and mutations on the same session are serialized +/// using a per-session lock to prevent lost updates or inconsistent reads. +/// /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] -public sealed class AgentModeProvider : AIContextProvider +public sealed class AgentModeProvider : AIContextProvider, IDisposable { private const string DefaultInstructions = """ @@ -102,6 +105,8 @@ 4. Mark tasks as completed as you finish them. private readonly string? _instructions; private readonly HashSet _validModeNames; private readonly string _modeNamesDisplay; + private readonly ConditionalWeakTable _sessionLocks = new(); + private readonly SemaphoreSlim _nullSessionLock = new(1, 1); private IReadOnlyList? _stateKeys; /// @@ -159,14 +164,33 @@ public AgentModeProvider(AgentModeProviderOptions? options = null) /// public override IReadOnlyList StateKeys => this._stateKeys ??= [this._sessionState.StateKey]; + /// + public void Dispose() + { + this._nullSessionLock.Dispose(); + } + /// /// Gets the current operating mode from the session state. /// /// The agent session to read the mode from. + /// The to monitor for cancellation requests. /// The current mode string. - public string GetMode(AgentSession? session) + /// is . + public async Task GetModeAsync(AgentSession session, CancellationToken cancellationToken = default) { - return this._sessionState.GetOrInitializeState(session).CurrentMode; + _ = Throw.IfNull(session); + + SemaphoreSlim sessionLock = this.GetSessionLock(session); + await sessionLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + return this._sessionState.GetOrInitializeState(session).CurrentMode; + } + finally + { + sessionLock.Release(); + } } /// @@ -174,50 +198,81 @@ public string GetMode(AgentSession? session) /// /// The agent session to update the mode in. /// The new mode to set. + /// The to monitor for cancellation requests. + /// A that represents the asynchronous operation. + /// is . /// is not a configured mode. - public void SetMode(AgentSession? session, string mode) + public async Task SetModeAsync(AgentSession session, string mode, CancellationToken cancellationToken = default) { + _ = Throw.IfNull(session); + this.ValidateMode(mode); - AgentModeState state = this._sessionState.GetOrInitializeState(session); - string previousMode = state.CurrentMode; - state.CurrentMode = mode; + SemaphoreSlim sessionLock = this.GetSessionLock(session); + await sessionLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + AgentModeState state = this._sessionState.GetOrInitializeState(session); + string previousMode = state.CurrentMode; + state.CurrentMode = mode; - if (!string.Equals(previousMode, mode, StringComparison.Ordinal)) + if (!string.Equals(previousMode, mode, StringComparison.Ordinal)) + { + state.PreviousModeForNotification = previousMode; + } + + this._sessionState.SaveState(session, state); + } + finally { - state.PreviousModeForNotification = previousMode; + sessionLock.Release(); } - - this._sessionState.SaveState(session, state); } /// - protected override ValueTask ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default) + protected override async ValueTask ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default) { - AgentModeState state = this._sessionState.GetOrInitializeState(context.Session); + string currentMode; + string? previousModeForNotification; - string instructions = this.BuildInstructions(state.CurrentMode); + SemaphoreSlim sessionLock = this.GetSessionLock(context.Session); + await sessionLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + AgentModeState state = this._sessionState.GetOrInitializeState(context.Session); + currentMode = state.CurrentMode; + previousModeForNotification = state.PreviousModeForNotification; + + // If the mode was changed externally (e.g., via /mode command), clear the pending + // notification flag now that we are about to surface it to the agent. + if (previousModeForNotification != null) + { + state.PreviousModeForNotification = null; + this._sessionState.SaveState(context.Session, state); + } + } + finally + { + sessionLock.Release(); + } var aiContext = new AIContext { - Instructions = instructions, - Tools = this.CreateTools(state, context.Session), + Instructions = this.BuildInstructions(currentMode), + Tools = this.CreateTools(context.Session), }; // If the mode was changed externally (e.g., via /mode command), inject a notification message // so the agent clearly sees the change rather than relying solely on the system instructions. - if (state.PreviousModeForNotification != null) + if (previousModeForNotification != null) { - string previousMode = state.PreviousModeForNotification; - state.PreviousModeForNotification = null; - aiContext.Messages = [ - new ChatMessage(ChatRole.User, $"[Mode changed: The operating mode has been switched from \"{previousMode}\" to \"{state.CurrentMode}\". You must now adjust your behavior to match the \"{state.CurrentMode}\" mode.]"), + new ChatMessage(ChatRole.User, $"[Mode changed: The operating mode has been switched from \"{previousModeForNotification}\" to \"{currentMode}\". You must now adjust your behavior to match the \"{currentMode}\" mode.]"), ]; } - return new ValueTask(aiContext); + return aiContext; } private string BuildInstructions(string currentMode) @@ -227,7 +282,7 @@ private string BuildInstructions(string currentMode) { modesListBuilder.AppendLine($"#### {mode.Name}"); modesListBuilder.AppendLine(); - modesListBuilder.AppendLine(mode.Description.TrimEnd()); + modesListBuilder.AppendLine(mode.Instructions.TrimEnd()); modesListBuilder.AppendLine(); } @@ -247,19 +302,43 @@ private void ValidateMode(string mode) } } - private AITool[] CreateTools(AgentModeState state, AgentSession? session) + /// + /// Returns the per-session semaphore used to serialize all mode operations. + /// + private SemaphoreSlim GetSessionLock(AgentSession? session) + { + if (session is null) + { + return this._nullSessionLock; + } + + return this._sessionLocks.GetValue(session, _ => new SemaphoreSlim(1, 1)); + } + + private AITool[] CreateTools(AgentSession? session) { var serializerOptions = AgentJsonUtilities.DefaultOptions; return [ AIFunctionFactory.Create( - (string mode) => + async (string mode) => { this.ValidateMode(mode); - state.CurrentMode = mode; - this._sessionState.SaveState(session, state); + SemaphoreSlim sessionLock = this.GetSessionLock(session); + await sessionLock.WaitAsync().ConfigureAwait(false); + try + { + AgentModeState state = this._sessionState.GetOrInitializeState(session); + state.CurrentMode = mode; + this._sessionState.SaveState(session, state); + } + finally + { + sessionLock.Release(); + } + return $"Mode changed to \"{mode}\"."; }, new AIFunctionFactoryOptions @@ -270,7 +349,19 @@ private AITool[] CreateTools(AgentModeState state, AgentSession? session) }), AIFunctionFactory.Create( - () => state.CurrentMode, + async () => + { + SemaphoreSlim sessionLock = this.GetSessionLock(session); + await sessionLock.WaitAsync().ConfigureAwait(false); + try + { + return this._sessionState.GetOrInitializeState(session).CurrentMode; + } + finally + { + sessionLock.Release(); + } + }, new AIFunctionFactoryOptions { Name = "mode_get", diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProviderOptions.cs index f65c80aba52..45471fba798 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProviderOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProviderOptions.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; @@ -11,7 +9,6 @@ namespace Microsoft.Agents.AI; /// /// Options controlling the behavior of . /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public sealed class AgentModeProviderOptions { /// @@ -46,22 +43,21 @@ public sealed class AgentModeProviderOptions public string? DefaultMode { get; set; } /// - /// Represents an agent operating mode with a name and description. + /// Represents an agent operating mode with a name and instructions. /// - [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public sealed class AgentMode { /// /// Initializes a new instance of the class. /// /// The name of the mode. - /// A description of when and how to use this mode. - /// or is . - /// or is empty or whitespace. - public AgentMode(string name, string description) + /// Instructions for the agent describing when and how to operate in this mode. + /// or is . + /// or is empty or whitespace. + public AgentMode(string name, string instructions) { this.Name = Throw.IfNullOrWhitespace(name); - this.Description = Throw.IfNullOrWhitespace(description); + this.Instructions = Throw.IfNullOrWhitespace(instructions); } /// @@ -70,8 +66,8 @@ public AgentMode(string name, string description) public string Name { get; } /// - /// Gets a description of when and how to use this mode. + /// Gets the instructions for the agent describing when and how to operate in this mode. /// - public string Description { get; } + public string Instructions { get; } } } diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeState.cs b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeState.cs index 63cc25eb1c7..678a1b8c5e6 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeState.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeState.cs @@ -1,15 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Agents.AI; /// /// Represents the state of the agent's operating mode, stored in the session's . /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] internal sealed class AgentModeState { /// diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Loop/TodoCompletionLoopEvaluator.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/TodoCompletionLoopEvaluator.cs index ba12c7d63b4..f21116b0189 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/Loop/TodoCompletionLoopEvaluator.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/Loop/TodoCompletionLoopEvaluator.cs @@ -109,7 +109,7 @@ public override async ValueTask EvaluateAsync(LoopContext contex ?? throw new InvalidOperationException( $"{nameof(TodoCompletionLoopEvaluator)} was configured with modes but no {nameof(AgentModeProvider)} could be resolved from the agent via GetService."); - string currentMode = modeProvider.GetMode(context.Session); + string currentMode = await modeProvider.GetModeAsync(context.Session, cancellationToken).ConfigureAwait(false); if (!this._modes.Contains(currentMode)) { return LoopEvaluation.Stop(); diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoCompleteInput.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoCompleteInput.cs index 355c494337c..b63b86dc54e 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoCompleteInput.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoCompleteInput.cs @@ -1,15 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Agents.AI; /// /// Represents the input for completing a single todo item via the . /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] internal sealed class TodoCompleteInput { /// diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoItem.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoItem.cs index b9540ffcbd6..decbe4c1c28 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoItem.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoItem.cs @@ -1,15 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Agents.AI; /// /// Represents a single todo item managed by the . /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public sealed class TodoItem { /// diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoItemInput.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoItemInput.cs index aa15a3e4361..7c11a94ed1e 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoItemInput.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoItemInput.cs @@ -1,15 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Agents.AI; /// /// Represents the input for creating a new todo item via the . /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] internal sealed class TodoItemInput { /// diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProvider.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProvider.cs index 599d9e1a83d..1d24b81d6e8 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProvider.cs @@ -2,14 +2,13 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; -using Microsoft.Shared.DiagnosticIds; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; @@ -38,7 +37,6 @@ namespace Microsoft.Agents.AI; /// using a per-session lock to prevent duplicate IDs, lost updates, or inconsistent reads. /// /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public sealed class TodoProvider : AIContextProvider, IDisposable { private const string DefaultInstructions = @@ -107,8 +105,11 @@ public void Dispose() /// The agent session to read todos from. /// The to monitor for cancellation requests. /// A list of all todo items. The items are live references to internal state. - public async Task> GetAllTodosAsync(AgentSession? session, CancellationToken cancellationToken = default) + /// is . + public async Task> GetAllTodosAsync(AgentSession session, CancellationToken cancellationToken = default) { + _ = Throw.IfNull(session); + SemaphoreSlim sessionLock = this.GetSessionLock(session); await sessionLock.WaitAsync(cancellationToken).ConfigureAwait(false); try @@ -132,8 +133,11 @@ public async Task> GetAllTodosAsync(AgentSession? sessio /// The agent session to read todos from. /// The to monitor for cancellation requests. /// A list of incomplete todo items. The items are live references to internal state. - public async Task> GetRemainingTodosAsync(AgentSession? session, CancellationToken cancellationToken = default) + /// is . + public async Task> GetRemainingTodosAsync(AgentSession session, CancellationToken cancellationToken = default) { + _ = Throw.IfNull(session); + SemaphoreSlim sessionLock = this.GetSessionLock(session); await sessionLock.WaitAsync(cancellationToken).ConfigureAwait(false); try diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProviderOptions.cs index 2cb331a7ae4..b3d18f8a5d1 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProviderOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProviderOptions.cs @@ -2,15 +2,12 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Agents.AI; /// /// Options controlling the behavior of . /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public sealed class TodoProviderOptions { /// diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoState.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoState.cs index 5b62d6d1eb6..4f3443c1407 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoState.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoState.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Agents.AI; @@ -11,7 +9,6 @@ namespace Microsoft.Agents.AI; /// Represents the state of the todo list managed by the , /// stored in the session's . /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] internal sealed class TodoState { /// diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/AgentMode/AgentModeProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/AgentMode/AgentModeProviderTests.cs index 6bcda669802..cb38565780b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/AgentMode/AgentModeProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/AgentMode/AgentModeProviderTests.cs @@ -167,14 +167,14 @@ public async Task GetMode_ReturnsUpdatedModeAfterSetAsync() /// Verify that the public GetMode helper returns the default mode. /// [Fact] - public void PublicGetMode_ReturnsDefaultMode() + public async Task PublicGetMode_ReturnsDefaultModeAsync() { // Arrange var provider = new AgentModeProvider(); var session = new ChatClientAgentSession(); // Act - string mode = provider.GetMode(session); + string mode = await provider.GetModeAsync(session); // Assert Assert.Equal("plan", mode); @@ -184,15 +184,15 @@ public void PublicGetMode_ReturnsDefaultMode() /// Verify that the public SetMode helper changes the mode. /// [Fact] - public void PublicSetMode_ChangesMode() + public async Task PublicSetMode_ChangesModeAsync() { // Arrange var provider = new AgentModeProvider(); var session = new ChatClientAgentSession(); // Act - provider.SetMode(session, "execute"); - string mode = provider.GetMode(session); + await provider.SetModeAsync(session, "execute"); + string mode = await provider.GetModeAsync(session); // Assert Assert.Equal("execute", mode); @@ -202,17 +202,17 @@ public void PublicSetMode_ChangesMode() /// Verify that the public SetMode helper throws for an unsupported value and does not persist the mode. /// [Fact] - public void PublicSetMode_InvalidMode_Throws() + public async Task PublicSetMode_InvalidMode_ThrowsAsync() { // Arrange var provider = new AgentModeProvider(); var session = new ChatClientAgentSession(); // Act & Assert - Assert.Throws(() => provider.SetMode(session, "foo")); + await Assert.ThrowsAsync(() => provider.SetModeAsync(session, "foo")); // Verify mode was not changed from default - string mode = provider.GetMode(session); + string mode = await provider.GetModeAsync(session); Assert.Equal("plan", mode); } @@ -228,7 +228,7 @@ public async Task PublicSetMode_ReflectedInToolResultsAsync() var session = new ChatClientAgentSession(); // Set mode via public helper - provider.SetMode(session, "execute"); + await provider.SetModeAsync(session, "execute"); #pragma warning disable MAAI001 var context = new AIContextProvider.InvokingContext(agent, session, new AIContext()); @@ -307,7 +307,7 @@ public async Task Options_CustomInstructions_OverridesDefaultAsync() /// Verify that custom modes are used. /// [Fact] - public void Options_CustomModes_AreUsed() + public async Task Options_CustomModes_AreUsedAsync() { // Arrange var options = new AgentModeProviderOptions @@ -322,7 +322,7 @@ public void Options_CustomModes_AreUsed() var session = new ChatClientAgentSession(); // Act - string mode = provider.GetMode(session); + string mode = await provider.GetModeAsync(session); // Assert — default mode is first in list Assert.Equal("draft", mode); @@ -332,7 +332,7 @@ public void Options_CustomModes_AreUsed() /// Verify that SetMode validates against custom modes. /// [Fact] - public void Options_CustomModes_SetModeValidatesAgainstList() + public async Task Options_CustomModes_SetModeValidatesAgainstListAsync() { // Arrange var options = new AgentModeProviderOptions @@ -347,20 +347,20 @@ public void Options_CustomModes_SetModeValidatesAgainstList() var session = new ChatClientAgentSession(); // Act — valid mode - provider.SetMode(session, "review"); + await provider.SetModeAsync(session, "review"); // Assert - Assert.Equal("review", provider.GetMode(session)); + Assert.Equal("review", await provider.GetModeAsync(session)); // Act & Assert — invalid mode (plan is no longer valid) - Assert.Throws(() => provider.SetMode(session, "plan")); + await Assert.ThrowsAsync(() => provider.SetModeAsync(session, "plan")); } /// /// Verify that a custom default mode is used. /// [Fact] - public void Options_CustomDefaultMode_IsUsed() + public async Task Options_CustomDefaultMode_IsUsedAsync() { // Arrange var options = new AgentModeProviderOptions @@ -376,7 +376,7 @@ public void Options_CustomDefaultMode_IsUsed() var session = new ChatClientAgentSession(); // Act - string mode = provider.GetMode(session); + string mode = await provider.GetModeAsync(session); // Assert Assert.Equal("review", mode); @@ -517,7 +517,7 @@ public async Task ExternalModeChange_InjectsNotificationMessageAsync() var session = new ChatClientAgentSession(); // Change mode externally (simulating /mode command) - provider.SetMode(session, "execute"); + await provider.SetModeAsync(session, "execute"); #pragma warning disable MAAI001 var context = new AIContextProvider.InvokingContext(agent, session, new AIContext()); @@ -545,7 +545,7 @@ public async Task ExternalModeChange_NotificationClearedAfterFirstReadAsync() var provider = new AgentModeProvider(); var agent = new Mock().Object; var session = new ChatClientAgentSession(); - provider.SetMode(session, "execute"); + await provider.SetModeAsync(session, "execute"); #pragma warning disable MAAI001 var context = new AIContextProvider.InvokingContext(agent, session, new AIContext()); @@ -603,7 +603,7 @@ public async Task ExternalModeChange_SameMode_NoNotificationAsync() var session = new ChatClientAgentSession(); // Set to same default mode - provider.SetMode(session, "plan"); + await provider.SetModeAsync(session, "plan"); #pragma warning disable MAAI001 var context = new AIContextProvider.InvokingContext(agent, session, new AIContext()); diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/Loop/TodoCompletionLoopEvaluatorTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/Loop/TodoCompletionLoopEvaluatorTests.cs index ad4664c7080..8704ab63d88 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/Loop/TodoCompletionLoopEvaluatorTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/Loop/TodoCompletionLoopEvaluatorTests.cs @@ -139,7 +139,7 @@ public async Task EvaluateAsync_ModeMatches_RemainingTodos_ContinuesAsync() var todoProvider = new TodoProvider(); var modeProvider = new AgentModeProvider(); var session = new ChatClientAgentSession(); - modeProvider.SetMode(session, "execute"); + await modeProvider.SetModeAsync(session, "execute"); SeedTodos(session, (1, "Work item", null, false)); AIAgent agent = CreateAgent(todoProvider, modeProvider); var evaluator = new TodoCompletionLoopEvaluator(new TodoCompletionLoopEvaluatorOptions { Modes = ["execute"] }); @@ -163,7 +163,7 @@ public async Task EvaluateAsync_ModeMatches_NoRemainingTodos_StopsAsync() var todoProvider = new TodoProvider(); var modeProvider = new AgentModeProvider(); var session = new ChatClientAgentSession(); - modeProvider.SetMode(session, "execute"); + await modeProvider.SetModeAsync(session, "execute"); SeedTodos(session, (1, "Already done", null, true)); AIAgent agent = CreateAgent(todoProvider, modeProvider); var evaluator = new TodoCompletionLoopEvaluator(new TodoCompletionLoopEvaluatorOptions { Modes = ["execute"] }); @@ -187,7 +187,7 @@ public async Task EvaluateAsync_ModeDoesNotMatch_StopsEvenWithRemainingTodosAsyn var todoProvider = new TodoProvider(); var modeProvider = new AgentModeProvider(); var session = new ChatClientAgentSession(); - modeProvider.SetMode(session, "plan"); + await modeProvider.SetModeAsync(session, "plan"); SeedTodos(session, (1, "Still open", null, false)); AIAgent agent = CreateAgent(todoProvider, modeProvider); var evaluator = new TodoCompletionLoopEvaluator(new TodoCompletionLoopEvaluatorOptions { Modes = ["execute"] });