From 6589bafa15b714033e7bbf41ea99c9b998f1ceaa Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Thu, 17 Sep 2026 11:40:57 +0000
Subject: [PATCH] Allow disabling function tools on ModeProvider
---
.../Harness/AgentMode/AgentModeProvider.cs | 104 +++++++++---
.../AgentMode/AgentModeProviderOptions.cs | 18 +++
.../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 5 +
.../PublicAPI/net472/PublicAPI.Unshipped.txt | 5 +
.../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 5 +
.../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 5 +
.../netstandard2.0/PublicAPI.Unshipped.txt | 5 +
.../AgentMode/AgentModeProviderTests.cs | 153 ++++++++++++++++++
8 files changed, 281 insertions(+), 19 deletions(-)
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs
index ac12296f995..f01ae98764b 100644
--- a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs
@@ -27,14 +27,18 @@ namespace Microsoft.Agents.AI;
/// (autonomous execution).
///
///
-/// This provider exposes the following tools to the agent:
+/// By default, this provider exposes the following tools to the agent:
///
/// - mode_set — Switch the agent's operating mode.
/// - mode_get — Retrieve the agent's current operating mode.
///
+/// Set or
+/// to omit the corresponding built-in tool while
+/// retaining mode state and workflow instructions.
///
///
-/// Public helper methods and allow external code
+/// Public helper methods and
+/// allow external code
/// to programmatically read and change the mode.
///
///
@@ -44,16 +48,19 @@ namespace Microsoft.Agents.AI;
///
public sealed class AgentModeProvider : AIContextProvider, IDisposable
{
+ private const string ModeGetInstructions = "Use the mode_get tool to check your current operating mode.\n";
+ private const string ModeSetInstructions =
+ "Use the mode_set tool to switch between modes as your work progresses. Only use mode_set if the user explicitly instructs/allows you to change modes.\n\n";
+ private const string PlanModeTransition =
+ "7. When approval is granted, always switch to execute mode (using the `mode_set` tool), and follow the steps for *Execute mode*.";
+
private const string DefaultInstructions =
"""
## Agent Mode
- You can operate in different modes. Depending on the mode you are in, you will be required to follow different processes.
- Use the mode_get tool to check your current operating mode.
- Use the mode_set tool to switch between modes as your work progresses. Only use mode_set if the user explicitly instructs/allows you to change modes.
-
- You are currently operating in the {current_mode} mode.
+ {mode_get_instructions}{mode_set_instructions}You are currently operating in the {current_mode} mode.
### Mandatory Mode based Workflow
@@ -80,7 +87,7 @@ 3. Do not proceed until you have received all the needed clarifications.
4. Do short exploratory research if it helps with being able to ask sensible clarifications from the user.
5. Write the plan to a memory file, so that it is retained even if compaction happens. Make sure to update the plan file if the user requests changes.
6. Present the plan to the user and ask for approval to switch to execute mode and process the plan.
- 7. When approval is granted, always switch to execute mode (using the `mode_set` tool), and follow the steps for *Execute mode*.
+ {plan_mode_transition}
"""),
new(
"execute",
@@ -102,7 +109,11 @@ 4. Mark tasks as completed as you finish them.
private readonly ProviderSessionState _sessionState;
private readonly IReadOnlyList _modes;
private readonly string _defaultMode;
- private readonly string? _instructions;
+ private readonly string _instructions;
+ private readonly bool _disableModeSetTool;
+ private readonly bool _disableModeGetTool;
+ private readonly bool _usesDefaultInstructions;
+ private readonly bool _usesDefaultModes;
private readonly HashSet _validModeNames;
private readonly string _modeNamesDisplay;
private readonly ConditionalWeakTable _sessionLocks = new();
@@ -115,6 +126,7 @@ 4. Mark tasks as completed as you finish them.
/// Optional settings that control provider behavior. When , defaults are used.
public AgentModeProvider(AgentModeProviderOptions? options = null)
{
+ this._usesDefaultModes = options?.Modes is null;
this._modes = options?.Modes ?? s_defaultModes;
if (this._modes.Count == 0)
@@ -122,7 +134,10 @@ public AgentModeProvider(AgentModeProviderOptions? options = null)
throw new ArgumentException("At least one mode must be configured.", nameof(options));
}
+ this._usesDefaultInstructions = options?.Instructions is null;
this._instructions = options?.Instructions ?? DefaultInstructions;
+ this._disableModeSetTool = options?.DisableModeSetTool ?? false;
+ this._disableModeGetTool = options?.DisableModeGetTool ?? false;
this._validModeNames = new HashSet(StringComparer.Ordinal);
var modeNamesList = new List(this._modes.Count);
@@ -202,7 +217,30 @@ public async Task GetModeAsync(AgentSession session, CancellationToken c
/// A that represents the asynchronous operation.
/// is .
/// is not a configured mode.
- public async Task SetModeAsync(AgentSession session, string mode, CancellationToken cancellationToken = default)
+ public Task SetModeAsync(AgentSession session, string mode, CancellationToken cancellationToken = default) =>
+ this.SetModeAsync(session, mode, disableNotification: false, cancellationToken);
+
+ ///
+ /// Sets the operating mode in the session state, optionally suppressing the mode-change notification.
+ ///
+ /// The agent session to update the mode in.
+ /// The new mode to set.
+ ///
+ /// to avoid notifying the agent about the mode change on its next invocation;
+ /// otherwise, . Use when the agent changes mode through
+ /// a custom function tool and has already observed the tool result. This also clears any pending
+ /// mode-change notification.
+ ///
+ /// The to monitor for cancellation requests.
+ /// A that represents the asynchronous operation.
+ /// is .
+ /// is not a configured mode.
+#pragma warning disable RS0026 // The required Boolean parameter distinguishes this overload from the existing optional-cancellation overload.
+ public async Task SetModeAsync(
+ AgentSession session,
+ string mode,
+ bool disableNotification,
+ CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(session);
@@ -216,7 +254,11 @@ public async Task SetModeAsync(AgentSession session, string mode, CancellationTo
string previousMode = state.CurrentMode;
state.CurrentMode = mode;
- if (!string.Equals(previousMode, mode, StringComparison.Ordinal))
+ if (disableNotification)
+ {
+ state.PreviousModeForNotification = null;
+ }
+ else if (!string.Equals(previousMode, mode, StringComparison.Ordinal))
{
state.PreviousModeForNotification = previousMode;
}
@@ -228,6 +270,7 @@ public async Task SetModeAsync(AgentSession session, string mode, CancellationTo
sessionLock.Release();
}
}
+#pragma warning restore RS0026
///
protected override async ValueTask ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default)
@@ -284,15 +327,32 @@ private string BuildInstructions(string currentMode)
var modesListBuilder = new StringBuilder();
foreach (var mode in this._modes)
{
+ string modeInstructions = mode.Instructions;
+ if (this._usesDefaultModes && mode.Name == "plan")
+ {
+ modeInstructions = modeInstructions.Replace(
+ "{plan_mode_transition}",
+ this._disableModeSetTool ? string.Empty : PlanModeTransition);
+ }
+
modesListBuilder.AppendLine($"#### {mode.Name}");
modesListBuilder.AppendLine();
- modesListBuilder.AppendLine(mode.Instructions.TrimEnd());
+ modesListBuilder.AppendLine(modeInstructions.TrimEnd());
modesListBuilder.AppendLine();
}
var modesListText = modesListBuilder.ToString();
+ string instructions = this._instructions;
+ if (this._usesDefaultInstructions)
+ {
+ instructions = instructions
+ .Replace("{mode_get_instructions}", this._disableModeGetTool ? string.Empty : ModeGetInstructions)
+ .Replace(
+ "{mode_set_instructions}",
+ this._disableModeSetTool ? string.Empty : ModeSetInstructions);
+ }
- return new StringBuilder(this._instructions)
+ return new StringBuilder(instructions)
.Replace("{available_modes}", modesListText)
.Replace("{current_mode}", currentMode)
.ToString();
@@ -322,10 +382,11 @@ private SemaphoreSlim GetSessionLock(AgentSession? session)
private AITool[] CreateTools(AgentSession? session)
{
var serializerOptions = AgentJsonUtilities.DefaultOptions;
+ var tools = new List(2);
- return
- [
- AIFunctionFactory.Create(
+ if (!this._disableModeSetTool)
+ {
+ tools.Add(AIFunctionFactory.Create(
async (string mode) =>
{
this.ValidateMode(mode);
@@ -350,9 +411,12 @@ private AITool[] CreateTools(AgentSession? session)
Name = "mode_set",
Description = $"Switch the agent's operating mode. Supported modes: \"{this._modeNamesDisplay}\".",
SerializerOptions = serializerOptions,
- }),
+ }));
+ }
- AIFunctionFactory.Create(
+ if (!this._disableModeGetTool)
+ {
+ tools.Add(AIFunctionFactory.Create(
async () =>
{
SemaphoreSlim sessionLock = this.GetSessionLock(session);
@@ -371,7 +435,9 @@ private AITool[] CreateTools(AgentSession? session)
Name = "mode_get",
Description = "Get the agent's current operating mode.",
SerializerOptions = serializerOptions,
- }),
- ];
+ }));
+ }
+
+ return tools.ToArray();
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProviderOptions.cs
index 45471fba798..c711de29306 100644
--- a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProviderOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProviderOptions.cs
@@ -42,6 +42,24 @@ public sealed class AgentModeProviderOptions
///
public string? DefaultMode { get; set; }
+ ///
+ /// Gets or sets a value indicating whether the built-in mode_set tool is disabled.
+ ///
+ ///
+ /// When (the default), the provider exposes the mode_set tool.
+ /// When , the tool is not exposed, while mode state and instructions remain enabled.
+ ///
+ public bool DisableModeSetTool { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the built-in mode_get tool is disabled.
+ ///
+ ///
+ /// When (the default), the provider exposes the mode_get tool.
+ /// When , the tool is not exposed, while mode state and instructions remain enabled.
+ ///
+ public bool DisableModeGetTool { get; set; }
+
///
/// Represents an agent operating mode with a name and instructions.
///
diff --git a/dotnet/src/Microsoft.Agents.AI/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI/PublicAPI/net10.0/PublicAPI.Unshipped.txt
index 26f303a9aa5..1ec4e243d09 100644
--- a/dotnet/src/Microsoft.Agents.AI/PublicAPI/net10.0/PublicAPI.Unshipped.txt
+++ b/dotnet/src/Microsoft.Agents.AI/PublicAPI/net10.0/PublicAPI.Unshipped.txt
@@ -8,6 +8,11 @@
*REMOVED*[MAAI001]abstract 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!>!
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.get -> System.TimeSpan
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.set -> void
+Microsoft.Agents.AI.AgentModeProvider.SetModeAsync(Microsoft.Agents.AI.AgentSession! session, string! mode, bool disableNotification, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.get -> bool
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.set -> void
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.get -> bool
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.set -> void
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.get -> string?
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.set -> void
[MAAI001]const Microsoft.Agents.AI.FileAccessProvider.ReadLinesToolName = "file_access_read_lines" -> string!
diff --git a/dotnet/src/Microsoft.Agents.AI/PublicAPI/net472/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI/PublicAPI/net472/PublicAPI.Unshipped.txt
index 26f303a9aa5..1ec4e243d09 100644
--- a/dotnet/src/Microsoft.Agents.AI/PublicAPI/net472/PublicAPI.Unshipped.txt
+++ b/dotnet/src/Microsoft.Agents.AI/PublicAPI/net472/PublicAPI.Unshipped.txt
@@ -8,6 +8,11 @@
*REMOVED*[MAAI001]abstract 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!>!
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.get -> System.TimeSpan
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.set -> void
+Microsoft.Agents.AI.AgentModeProvider.SetModeAsync(Microsoft.Agents.AI.AgentSession! session, string! mode, bool disableNotification, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.get -> bool
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.set -> void
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.get -> bool
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.set -> void
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.get -> string?
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.set -> void
[MAAI001]const Microsoft.Agents.AI.FileAccessProvider.ReadLinesToolName = "file_access_read_lines" -> string!
diff --git a/dotnet/src/Microsoft.Agents.AI/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI/PublicAPI/net8.0/PublicAPI.Unshipped.txt
index 26f303a9aa5..1ec4e243d09 100644
--- a/dotnet/src/Microsoft.Agents.AI/PublicAPI/net8.0/PublicAPI.Unshipped.txt
+++ b/dotnet/src/Microsoft.Agents.AI/PublicAPI/net8.0/PublicAPI.Unshipped.txt
@@ -8,6 +8,11 @@
*REMOVED*[MAAI001]abstract 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!>!
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.get -> System.TimeSpan
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.set -> void
+Microsoft.Agents.AI.AgentModeProvider.SetModeAsync(Microsoft.Agents.AI.AgentSession! session, string! mode, bool disableNotification, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.get -> bool
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.set -> void
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.get -> bool
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.set -> void
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.get -> string?
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.set -> void
[MAAI001]const Microsoft.Agents.AI.FileAccessProvider.ReadLinesToolName = "file_access_read_lines" -> string!
diff --git a/dotnet/src/Microsoft.Agents.AI/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI/PublicAPI/net9.0/PublicAPI.Unshipped.txt
index 26f303a9aa5..1ec4e243d09 100644
--- a/dotnet/src/Microsoft.Agents.AI/PublicAPI/net9.0/PublicAPI.Unshipped.txt
+++ b/dotnet/src/Microsoft.Agents.AI/PublicAPI/net9.0/PublicAPI.Unshipped.txt
@@ -8,6 +8,11 @@
*REMOVED*[MAAI001]abstract 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!>!
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.get -> System.TimeSpan
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.set -> void
+Microsoft.Agents.AI.AgentModeProvider.SetModeAsync(Microsoft.Agents.AI.AgentSession! session, string! mode, bool disableNotification, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.get -> bool
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.set -> void
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.get -> bool
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.set -> void
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.get -> string?
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.set -> void
[MAAI001]const Microsoft.Agents.AI.FileAccessProvider.ReadLinesToolName = "file_access_read_lines" -> string!
diff --git a/dotnet/src/Microsoft.Agents.AI/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
index 26f303a9aa5..1ec4e243d09 100644
--- a/dotnet/src/Microsoft.Agents.AI/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
+++ b/dotnet/src/Microsoft.Agents.AI/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
@@ -8,6 +8,11 @@
*REMOVED*[MAAI001]abstract 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!>!
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.get -> System.TimeSpan
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.set -> void
+Microsoft.Agents.AI.AgentModeProvider.SetModeAsync(Microsoft.Agents.AI.AgentSession! session, string! mode, bool disableNotification, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.get -> bool
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.set -> void
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.get -> bool
+Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.set -> void
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.get -> string?
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.set -> void
[MAAI001]const Microsoft.Agents.AI.FileAccessProvider.ReadLinesToolName = "file_access_read_lines" -> string!
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 cb38565780b..bbc1c2a7c99 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/AgentMode/AgentModeProviderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/AgentMode/AgentModeProviderTests.cs
@@ -40,6 +40,47 @@ public async Task ProvideAIContextAsync_ReturnsToolsAndInstructionsAsync()
Assert.Equal(2, result.Tools!.Count());
}
+ ///
+ /// Verify that each built-in mode tool can be disabled independently.
+ ///
+ [Theory]
+ [InlineData(false, false, "mode_set,mode_get")]
+ [InlineData(false, true, "mode_set")]
+ [InlineData(true, false, "mode_get")]
+ [InlineData(true, true, "")]
+ public async Task ProvideAIContextAsync_DisablesConfiguredToolsAsync(
+ bool disableModeSetTool,
+ bool disableModeGetTool,
+ string expectedToolNames)
+ {
+ // Arrange
+ var provider = new AgentModeProvider(new AgentModeProviderOptions
+ {
+ DisableModeSetTool = disableModeSetTool,
+ DisableModeGetTool = disableModeGetTool,
+ });
+ 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
+ string[] expectedNames = expectedToolNames.Length == 0 ? [] : expectedToolNames.Split(',');
+ Assert.Equal(expectedNames, result.Tools!.Cast().Select(tool => tool.Name));
+ Assert.Equal(!disableModeSetTool, result.Instructions!.Contains("mode_set", StringComparison.Ordinal));
+ Assert.Equal(!disableModeGetTool, result.Instructions.Contains("mode_get", StringComparison.Ordinal));
+ Assert.Contains("### Mandatory Mode based Workflow", result.Instructions);
+ Assert.Contains("You are currently operating in the plan mode.", result.Instructions);
+ Assert.DoesNotContain("{mode_get_instructions}", result.Instructions);
+ Assert.DoesNotContain("{mode_set_instructions}", result.Instructions);
+ Assert.DoesNotContain("{plan_mode_transition}", result.Instructions);
+ Assert.Equal("plan", await provider.GetModeAsync(session));
+ }
+
///
/// Verify that the instructions include the current mode.
///
@@ -198,6 +239,53 @@ public async Task PublicSetMode_ChangesModeAsync()
Assert.Equal("execute", mode);
}
+ ///
+ /// Verify that the public SetMode helper can suppress the mode-change notification.
+ ///
+ [Fact]
+ public async Task PublicSetMode_DisableNotification_DoesNotInjectNotificationAsync()
+ {
+ // 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
+ await provider.SetModeAsync(session, "execute", disableNotification: true);
+ AIContext result = await provider.InvokingAsync(context);
+
+ // Assert
+ Assert.Equal("execute", await provider.GetModeAsync(session));
+ Assert.Null(result.Messages);
+ }
+
+ ///
+ /// Verify that suppressing a notification clears an earlier pending mode-change notification.
+ ///
+ [Fact]
+ public async Task PublicSetMode_DisableNotification_ClearsPendingNotificationAsync()
+ {
+ // 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
+ await provider.SetModeAsync(session, "execute");
+
+ // Act
+ await provider.SetModeAsync(session, "plan", disableNotification: true);
+ AIContext result = await provider.InvokingAsync(context);
+
+ // Assert
+ Assert.Equal("plan", await provider.GetModeAsync(session));
+ Assert.Null(result.Messages);
+ }
+
///
/// Verify that the public SetMode helper throws for an unsupported value and does not persist the mode.
///
@@ -303,6 +391,39 @@ public async Task Options_CustomInstructions_OverridesDefaultAsync()
Assert.Equal("Custom mode instructions.", result.Instructions);
}
+ ///
+ /// Verify that disabling tools does not rewrite custom instructions or custom mode guidance.
+ ///
+ [Fact]
+ public async Task Options_DisabledTools_PreserveCustomInstructionsAsync()
+ {
+ // Arrange
+ var options = new AgentModeProviderOptions
+ {
+ DisableModeSetTool = true,
+ DisableModeGetTool = true,
+ Instructions = "Use custom mode_set and mode_get behavior in {current_mode}.\n{available_modes}",
+ Modes =
+ [
+ new AgentModeProviderOptions.AgentMode("draft", "Custom mode_set and mode_get guidance."),
+ ],
+ };
+ var provider = new AgentModeProvider(options);
+ 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.Empty(result.Tools!);
+ Assert.Contains("Use custom mode_set and mode_get behavior in draft.", result.Instructions);
+ Assert.Contains("Custom mode_set and mode_get guidance.", result.Instructions);
+ }
+
///
/// Verify that custom modes are used.
///
@@ -535,6 +656,38 @@ public async Task ExternalModeChange_InjectsNotificationMessageAsync()
Assert.Contains("execute", message.Text);
}
+ ///
+ /// Verify that disabling both tools preserves mode state, instructions, and external-change notifications.
+ ///
+ [Fact]
+ public async Task DisabledTools_PreserveStateInstructionsAndNotificationsAsync()
+ {
+ // Arrange
+ var provider = new AgentModeProvider(new AgentModeProviderOptions
+ {
+ DisableModeSetTool = true,
+ DisableModeGetTool = true,
+ });
+ 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
+ _ = await provider.InvokingAsync(context);
+
+ // Act
+ await provider.SetModeAsync(session, "execute");
+ AIContext result = await provider.InvokingAsync(context);
+
+ // Assert
+ Assert.Empty(result.Tools!);
+ Assert.Contains("You are currently operating in the execute mode.", result.Instructions);
+ Assert.Equal("execute", await provider.GetModeAsync(session));
+ ChatMessage message = Assert.Single(result.Messages!);
+ Assert.Contains("plan", message.Text);
+ Assert.Contains("execute", message.Text);
+ }
+
///
/// Verify that the notification is only injected once (cleared after first read).
///