From 6f5815f796995bc1c0b6b2e602cbaddbc8f85ac6 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:51:13 +0000 Subject: [PATCH 1/5] Add a ModeProvider for managing agent modes --- .../Microsoft.Agents.AI/AgentJsonUtilities.cs | 5 +- .../Harness/AgentMode/AgentModeProvider.cs | 137 +++++++++ .../Harness/AgentMode/AgentModeState.cs | 20 ++ .../AgentMode/AgentModeProviderTests.cs | 289 ++++++++++++++++++ 4 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs create mode 100644 dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeState.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/AgentMode/AgentModeProviderTests.cs diff --git a/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs index a531f650fa..c006f20b2d 100644 --- a/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs @@ -70,7 +70,7 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(TextSearchProvider.TextSearchProviderState))] [JsonSerializable(typeof(ChatHistoryMemoryProvider.State))] - // Harness types + // TODO harness types [JsonSerializable(typeof(TodoState))] [JsonSerializable(typeof(TodoItem))] [JsonSerializable(typeof(TodoItemInput))] @@ -78,6 +78,9 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(List), TypeInfoPropertyName = "TodoItemList")] [JsonSerializable(typeof(List), TypeInfoPropertyName = "TodoItemInputList")] + // Agent-mode harness types + [JsonSerializable(typeof(AgentModeState))] + [ExcludeFromCodeCoverage] internal sealed partial class JsonContext : JsonSerializerContext; } diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs new file mode 100644 index 0000000000..6526341532 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Microsoft.Shared.DiagnosticIds; + +namespace Microsoft.Agents.AI; + +/// +/// An that tracks the agent's operating mode (e.g., "plan" or "execute") +/// in the session state and provides tools for querying and switching modes. +/// +/// +/// +/// The enables agents to operate in distinct modes during long-running +/// complex tasks. The current mode is persisted in the session's +/// and is included in the instructions provided to the agent on each invocation. +/// +/// +/// This provider exposes the following tools to the agent: +/// +/// SetMode — Switch the agent's operating mode. +/// GetMode — Retrieve the agent's current operating mode. +/// +/// +/// +/// Public helper methods and allow external code +/// to programmatically read and change the mode. +/// +/// +[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] +public sealed class AgentModeProvider : AIContextProvider +{ + /// + /// The "plan" mode, indicating the agent is planning work. + /// + public const string ModePlan = "plan"; + + /// + /// The "execute" mode, indicating the agent is executing work. + /// + public const string ModeExecute = "execute"; + + private readonly ProviderSessionState _sessionState; + private IReadOnlyList? _stateKeys; + + /// + /// Initializes a new instance of the class. + /// + public AgentModeProvider() + { + this._sessionState = new ProviderSessionState( + _ => new AgentModeState(), + this.GetType().Name, + AgentJsonUtilities.DefaultOptions); + } + + /// + public override IReadOnlyList StateKeys => this._stateKeys ??= [this._sessionState.StateKey]; + + /// + /// Gets the current operating mode from the session state. + /// + /// The agent session to read the mode from. + /// The current mode string. + public string GetMode(AgentSession? session) + { + return this._sessionState.GetOrInitializeState(session).CurrentMode; + } + + /// + /// Sets the operating mode in the session state. + /// + /// The agent session to update the mode in. + /// The new mode to set. + public void SetMode(AgentSession? session, string mode) + { + AgentModeState state = this._sessionState.GetOrInitializeState(session); + state.CurrentMode = mode; + this._sessionState.SaveState(session, state); + } + + /// + protected override ValueTask ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default) + { + AgentModeState state = this._sessionState.GetOrInitializeState(context.Session); + + string instructions = $""" + You are currently operating in "{state.CurrentMode}" mode. + Available modes: + - "plan": Use this mode when analyzing requirements, breaking down tasks, and creating plans. + - "execute": Use this mode when implementing changes, writing code, and carrying out planned work. + Use the SetMode tool to switch between modes as your work progresses. Only use SetMode if the user explicitly instructs you to change modes. + Use the GetMode tool to check your current operating mode. + """; + + return new ValueTask(new AIContext + { + Instructions = instructions, + Tools = this.CreateTools(state, context.Session), + }); + } + + private AITool[] CreateTools(AgentModeState state, AgentSession? session) + { + var serializerOptions = AgentJsonUtilities.DefaultOptions; + + return + [ + AIFunctionFactory.Create( + (string mode) => + { + state.CurrentMode = mode; + this._sessionState.SaveState(session, state); + return $"Mode changed to \"{mode}\"."; + }, + new AIFunctionFactoryOptions + { + Name = "SetMode", + Description = "Switch the agent's operating mode. Supported modes: \"plan\" and \"execute\".", + SerializerOptions = serializerOptions, + }), + + AIFunctionFactory.Create( + () => state.CurrentMode, + new AIFunctionFactoryOptions + { + Name = "GetMode", + Description = "Get the agent's current operating mode.", + SerializerOptions = serializerOptions, + }), + ]; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeState.cs b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeState.cs new file mode 100644 index 0000000000..c234bcea96 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeState.cs @@ -0,0 +1,20 @@ +// 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 +{ + /// + /// Gets or sets the current operating mode of the agent. + /// + [JsonPropertyName("currentMode")] + public string CurrentMode { get; set; } = AgentModeProvider.ModePlan; +} diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/AgentMode/AgentModeProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/AgentMode/AgentModeProviderTests.cs new file mode 100644 index 0000000000..6a02ffc5cb --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/AgentMode/AgentModeProviderTests.cs @@ -0,0 +1,289 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Moq; + +namespace Microsoft.Agents.AI.UnitTests; + +/// +/// Unit tests for the class. +/// +public class AgentModeProviderTests +{ + #region ProvideAIContextAsync Tests + + /// + /// Verify that the provider returns tools and instructions. + /// + [Fact] + public async Task ProvideAIContextAsync_ReturnsToolsAndInstructionsAsync() + { + // Arrange + var provider = new AgentModeProvider(); + var agent = new Mock().Object; + var session = new ChatClientAgentSession(); +#pragma warning disable MAAI001 + var context = new AIContextProvider.InvokingContext(agent, session, new AIContext()); +#pragma warning restore MAAI001 + + // Act + AIContext result = await provider.InvokingAsync(context); + + // Assert + Assert.NotNull(result.Instructions); + Assert.NotNull(result.Tools); + Assert.Equal(2, result.Tools!.Count()); + } + + /// + /// Verify that the instructions include the current mode. + /// + [Fact] + public async Task ProvideAIContextAsync_InstructionsIncludeCurrentModeAsync() + { + // Arrange + var provider = new AgentModeProvider(); + var agent = new Mock().Object; + var session = new ChatClientAgentSession(); +#pragma warning disable MAAI001 + var context = new AIContextProvider.InvokingContext(agent, session, new AIContext()); +#pragma warning restore MAAI001 + + // Act + AIContext result = await provider.InvokingAsync(context); + + // Assert + Assert.Contains("plan", result.Instructions); + } + + #endregion + + #region SetMode Tool Tests + + /// + /// Verify that SetMode changes the mode. + /// + [Fact] + public async Task SetMode_ChangesModeAsync() + { + // Arrange + var (tools, state) = await CreateToolsWithStateAsync(); + AIFunction setMode = GetTool(tools, "SetMode"); + + // Act + await setMode.InvokeAsync(new AIFunctionArguments() { ["mode"] = "execute" }); + + // Assert + Assert.Equal("execute", state.CurrentMode); + } + + /// + /// Verify that SetMode returns a confirmation message. + /// + [Fact] + public async Task SetMode_ReturnsConfirmationAsync() + { + // Arrange + var (tools, _) = await CreateToolsWithStateAsync(); + AIFunction setMode = GetTool(tools, "SetMode"); + + // Act + object? result = await setMode.InvokeAsync(new AIFunctionArguments() { ["mode"] = "execute" }); + + // Assert + Assert.Equal("Mode changed to \"execute\".", GetStringResult(result)); + } + + #endregion + + #region GetMode Tool Tests + + /// + /// Verify that GetMode returns the default mode. + /// + [Fact] + public async Task GetMode_ReturnsDefaultModeAsync() + { + // Arrange + var (tools, _) = await CreateToolsWithStateAsync(); + AIFunction getMode = GetTool(tools, "GetMode"); + + // Act + object? result = await getMode.InvokeAsync(new AIFunctionArguments()); + + // Assert + Assert.Equal("plan", GetStringResult(result)); + } + + /// + /// Verify that GetMode returns the mode after SetMode. + /// + [Fact] + public async Task GetMode_ReturnsUpdatedModeAfterSetAsync() + { + // Arrange + var (tools, _) = await CreateToolsWithStateAsync(); + AIFunction setMode = GetTool(tools, "SetMode"); + AIFunction getMode = GetTool(tools, "GetMode"); + + // Act + await setMode.InvokeAsync(new AIFunctionArguments() { ["mode"] = "execute" }); + object? result = await getMode.InvokeAsync(new AIFunctionArguments()); + + // Assert + Assert.Equal("execute", GetStringResult(result)); + } + + #endregion + + #region Public Helper Method Tests + + /// + /// Verify that the public GetMode helper returns the default mode. + /// + [Fact] + public void PublicGetMode_ReturnsDefaultMode() + { + // Arrange + var provider = new AgentModeProvider(); + var session = new ChatClientAgentSession(); + + // Act + string mode = provider.GetMode(session); + + // Assert + Assert.Equal(AgentModeProvider.ModePlan, mode); + } + + /// + /// Verify that the public SetMode helper changes the mode. + /// + [Fact] + public void PublicSetMode_ChangesMode() + { + // Arrange + var provider = new AgentModeProvider(); + var session = new ChatClientAgentSession(); + + // Act + provider.SetMode(session, AgentModeProvider.ModeExecute); + string mode = provider.GetMode(session); + + // Assert + Assert.Equal(AgentModeProvider.ModeExecute, mode); + } + + /// + /// Verify that public helper changes are reflected in tool results. + /// + [Fact] + public async Task PublicSetMode_ReflectedInToolResultsAsync() + { + // Arrange + var provider = new AgentModeProvider(); + var agent = new Mock().Object; + var session = new ChatClientAgentSession(); + + // Set mode via public helper + provider.SetMode(session, AgentModeProvider.ModeExecute); + +#pragma warning disable MAAI001 + var context = new AIContextProvider.InvokingContext(agent, session, new AIContext()); +#pragma warning restore MAAI001 + + // Act + AIContext result = await provider.InvokingAsync(context); + AIFunction getMode = GetTool(result.Tools!, "GetMode"); + object? modeResult = await getMode.InvokeAsync(new AIFunctionArguments()); + + // Assert + Assert.Equal("execute", GetStringResult(modeResult)); + Assert.Contains("execute", result.Instructions); + } + + #endregion + + #region State Persistence Tests + + /// + /// Verify that state persists across invocations. + /// + [Fact] + public async Task State_PersistsAcrossInvocationsAsync() + { + // Arrange + var provider = new AgentModeProvider(); + var agent = new Mock().Object; + var session = new ChatClientAgentSession(); +#pragma warning disable MAAI001 + var context = new AIContextProvider.InvokingContext(agent, session, new AIContext()); +#pragma warning restore MAAI001 + + // Act — first invocation changes mode + AIContext result1 = await provider.InvokingAsync(context); + AIFunction setMode = GetTool(result1.Tools!, "SetMode"); + await setMode.InvokeAsync(new AIFunctionArguments() { ["mode"] = "execute" }); + + // Second invocation should see the updated mode + AIContext result2 = await provider.InvokingAsync(context); + AIFunction getMode = GetTool(result2.Tools!, "GetMode"); + object? modeResult = await getMode.InvokeAsync(new AIFunctionArguments()); + + // Assert + Assert.Equal("execute", GetStringResult(modeResult)); + Assert.Contains("execute", result2.Instructions); + } + + #endregion + + #region Constants Tests + + /// + /// Verify that mode constants have expected values. + /// + [Fact] + public void ModeConstants_HaveExpectedValues() + { + // Assert + Assert.Equal("plan", AgentModeProvider.ModePlan); + Assert.Equal("execute", AgentModeProvider.ModeExecute); + } + + #endregion + + #region Helper Methods + + private static async Task<(IEnumerable Tools, AgentModeState State)> CreateToolsWithStateAsync() + { + var provider = new AgentModeProvider(); + var agent = new Mock().Object; + var session = new ChatClientAgentSession(); +#pragma warning disable MAAI001 + var context = new AIContextProvider.InvokingContext(agent, session, new AIContext()); +#pragma warning restore MAAI001 + + AIContext result = await provider.InvokingAsync(context); + + // Retrieve the state from the session to verify mutations + session.StateBag.TryGetValue("AgentModeProvider", out var state, AgentJsonUtilities.DefaultOptions); + + return (result.Tools!, state!); + } + + private static AIFunction GetTool(IEnumerable tools, string name) + { + return (AIFunction)tools.First(t => t is AIFunction f && f.Name == name); + } + + private static string GetStringResult(object? result) + { + var element = Assert.IsType(result); + return element.GetString()!; + } + + #endregion +} From ad1ba83c3dc91e7188f73369fa70f90e088875bb Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:55:13 +0000 Subject: [PATCH 2/5] Fix typo --- .../Harness/AgentMode/AgentModeProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs index 6526341532..e8ed3fe06f 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs @@ -37,12 +37,12 @@ public sealed class AgentModeProvider : AIContextProvider /// /// The "plan" mode, indicating the agent is planning work. /// - public const string ModePlan = "plan"; + public const string PlanMode = "plan"; /// /// The "execute" mode, indicating the agent is executing work. /// - public const string ModeExecute = "execute"; + public const string ExecuteMode = "execute"; private readonly ProviderSessionState _sessionState; private IReadOnlyList? _stateKeys; From af2a08526ff8a38ffe9297c12cb7b23c835ccd5e Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:55:40 +0000 Subject: [PATCH 3/5] Fix typo --- .../src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeState.cs b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeState.cs index c234bcea96..e16c9c9289 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeState.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeState.cs @@ -16,5 +16,5 @@ internal sealed class AgentModeState /// Gets or sets the current operating mode of the agent. /// [JsonPropertyName("currentMode")] - public string CurrentMode { get; set; } = AgentModeProvider.ModePlan; + public string CurrentMode { get; set; } = AgentModeProvider.PlanMode; } From c154fb01fb02e580a89fd1ebeec1bd738d6d8355 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:56:15 +0000 Subject: [PATCH 4/5] Fix typo --- .../Harness/AgentMode/AgentModeProviderTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 6a02ffc5cb..6ab842c5f1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/AgentMode/AgentModeProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/AgentMode/AgentModeProviderTests.cs @@ -156,7 +156,7 @@ public void PublicGetMode_ReturnsDefaultMode() string mode = provider.GetMode(session); // Assert - Assert.Equal(AgentModeProvider.ModePlan, mode); + Assert.Equal(AgentModeProvider.PlanMode, mode); } /// @@ -170,11 +170,11 @@ public void PublicSetMode_ChangesMode() var session = new ChatClientAgentSession(); // Act - provider.SetMode(session, AgentModeProvider.ModeExecute); + provider.SetMode(session, AgentModeProvider.ExecuteMode); string mode = provider.GetMode(session); // Assert - Assert.Equal(AgentModeProvider.ModeExecute, mode); + Assert.Equal(AgentModeProvider.ExecuteMode, mode); } /// @@ -189,7 +189,7 @@ public async Task PublicSetMode_ReflectedInToolResultsAsync() var session = new ChatClientAgentSession(); // Set mode via public helper - provider.SetMode(session, AgentModeProvider.ModeExecute); + provider.SetMode(session, AgentModeProvider.ExecuteMode); #pragma warning disable MAAI001 var context = new AIContextProvider.InvokingContext(agent, session, new AIContext()); @@ -249,8 +249,8 @@ public async Task State_PersistsAcrossInvocationsAsync() public void ModeConstants_HaveExpectedValues() { // Assert - Assert.Equal("plan", AgentModeProvider.ModePlan); - Assert.Equal("execute", AgentModeProvider.ModeExecute); + Assert.Equal("plan", AgentModeProvider.PlanMode); + Assert.Equal("execute", AgentModeProvider.ExecuteMode); } #endregion From 914b6c8d9cf4b3496139c002aa70080e96ef39e6 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:50:28 +0000 Subject: [PATCH 5/5] Address PR comments --- .../Microsoft.Agents.AI/AgentJsonUtilities.cs | 4 +- .../Harness/AgentMode/AgentModeProvider.cs | 11 ++++ .../AgentMode/AgentModeProviderTests.cs | 52 +++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs index c006f20b2d..04f0bcdd03 100644 --- a/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs @@ -70,7 +70,7 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(TextSearchProvider.TextSearchProviderState))] [JsonSerializable(typeof(ChatHistoryMemoryProvider.State))] - // TODO harness types + // TodoProvider types [JsonSerializable(typeof(TodoState))] [JsonSerializable(typeof(TodoItem))] [JsonSerializable(typeof(TodoItemInput))] @@ -78,7 +78,7 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(List), TypeInfoPropertyName = "TodoItemList")] [JsonSerializable(typeof(List), TypeInfoPropertyName = "TodoItemInputList")] - // Agent-mode harness types + // AgentModeProvider types [JsonSerializable(typeof(AgentModeState))] [ExcludeFromCodeCoverage] diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs index e8ed3fe06f..1a0ee68941 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading; @@ -78,6 +79,11 @@ public string GetMode(AgentSession? session) /// The new mode to set. public void SetMode(AgentSession? session, string mode) { + if (mode != PlanMode && mode != ExecuteMode) + { + throw new ArgumentException($"Invalid mode: {mode}. Supported modes are \"{PlanMode}\" and \"{ExecuteMode}\".", nameof(mode)); + } + AgentModeState state = this._sessionState.GetOrInitializeState(session); state.CurrentMode = mode; this._sessionState.SaveState(session, state); @@ -113,6 +119,11 @@ private AITool[] CreateTools(AgentModeState state, AgentSession? session) AIFunctionFactory.Create( (string mode) => { + if (mode != PlanMode && mode != ExecuteMode) + { + throw new ArgumentException($"Invalid mode: {mode}. Supported modes are \"{PlanMode}\" and \"{ExecuteMode}\".", nameof(mode)); + } + state.CurrentMode = mode; this._sessionState.SaveState(session, state); return $"Mode changed to \"{mode}\"."; 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 6ab842c5f1..59393d4430 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/AgentMode/AgentModeProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/AgentMode/AgentModeProviderTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; @@ -98,6 +99,26 @@ public async Task SetMode_ReturnsConfirmationAsync() Assert.Equal("Mode changed to \"execute\".", GetStringResult(result)); } + /// + /// Verify that SetMode with an unsupported value throws and does not persist the mode. + /// + [Fact] + public async Task SetMode_InvalidMode_ThrowsAsync() + { + // Arrange + var (tools, provider, session) = await CreateToolsWithProviderAndSessionAsync(); + AIFunction setMode = GetTool(tools, "SetMode"); + AIFunction getMode = GetTool(tools, "GetMode"); + + // Act & Assert + await Assert.ThrowsAsync(async () => + await setMode.InvokeAsync(new AIFunctionArguments() { ["mode"] = "foo" })); + + // Verify mode was not changed from default + object? currentMode = await getMode.InvokeAsync(new AIFunctionArguments()); + Assert.Equal(AgentModeProvider.PlanMode, GetStringResult(currentMode)); + } + #endregion #region GetMode Tool Tests @@ -177,6 +198,24 @@ public void PublicSetMode_ChangesMode() Assert.Equal(AgentModeProvider.ExecuteMode, mode); } + /// + /// Verify that the public SetMode helper throws for an unsupported value and does not persist the mode. + /// + [Fact] + public void PublicSetMode_InvalidMode_Throws() + { + // Arrange + var provider = new AgentModeProvider(); + var session = new ChatClientAgentSession(); + + // Act & Assert + Assert.Throws(() => provider.SetMode(session, "foo")); + + // Verify mode was not changed from default + string mode = provider.GetMode(session); + Assert.Equal(AgentModeProvider.PlanMode, mode); + } + /// /// Verify that public helper changes are reflected in tool results. /// @@ -274,6 +313,19 @@ public void ModeConstants_HaveExpectedValues() return (result.Tools!, state!); } + private static async Task<(IEnumerable Tools, AgentModeProvider Provider, AgentSession Session)> CreateToolsWithProviderAndSessionAsync() + { + var provider = new AgentModeProvider(); + var agent = new Mock().Object; + var session = new ChatClientAgentSession(); +#pragma warning disable MAAI001 + var context = new AIContextProvider.InvokingContext(agent, session, new AIContext()); +#pragma warning restore MAAI001 + + AIContext result = await provider.InvokingAsync(context); + return (result.Tools!, provider, session); + } + private static AIFunction GetTool(IEnumerable tools, string name) { return (AIFunction)tools.First(t => t is AIFunction f && f.Name == name);