From f4861980dbb3b4ef82f3c43cb22b12ab66e1c05f Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Fri, 24 Apr 2026 16:17:09 +0000
Subject: [PATCH 1/2] Make Todo, Mode and FileMemory providers more
configurable
---
.../Harness_Shared_Console/HarnessConsole.cs | 14 +-
.../ToolCallFormatter.cs | 14 +-
.../Harness_Step01_Research/Program.cs | 68 ++--
.../Harness/AgentMode/AgentModeProvider.cs | 134 +++++--
.../AgentMode/AgentModeProviderOptions.cs | 73 ++++
.../Harness/AgentMode/AgentModeState.cs | 9 +-
.../Harness/FileMemory/FileMemoryProvider.cs | 7 +-
.../FileMemory/FileMemoryProviderOptions.cs | 22 ++
.../Harness/Todo/TodoProvider.cs | 37 +-
.../Harness/Todo/TodoProviderOptions.cs | 22 ++
.../AgentMode/AgentModeProviderTests.cs | 328 ++++++++++++++++--
.../FileMemory/FileMemoryProviderTests.cs | 47 +++
.../Harness/Todo/TodoProviderTests.cs | 93 +++--
13 files changed, 722 insertions(+), 146 deletions(-)
create mode 100644 dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProviderOptions.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI/Harness/FileMemory/FileMemoryProviderOptions.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProviderOptions.cs
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 9e46751001..00b997c5bd 100644
--- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessConsole.cs
+++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessConsole.cs
@@ -282,16 +282,6 @@ private static void HandleModeCommand(AgentModeProvider? modeProvider, AgentSess
string newMode = parts[1];
- // Normalize to known mode values for case-insensitive matching.
- if (string.Equals(newMode, AgentModeProvider.PlanMode, StringComparison.OrdinalIgnoreCase))
- {
- newMode = AgentModeProvider.PlanMode;
- }
- else if (string.Equals(newMode, AgentModeProvider.ExecuteMode, StringComparison.OrdinalIgnoreCase))
- {
- newMode = AgentModeProvider.ExecuteMode;
- }
-
try
{
modeProvider.SetMode(session, newMode);
@@ -383,8 +373,8 @@ private static void WriteTokenCount(long? count, int? budget)
private static ConsoleColor GetModeColor(string mode) => mode switch
{
- AgentModeProvider.PlanMode => ConsoleColor.Cyan,
- AgentModeProvider.ExecuteMode => ConsoleColor.Green,
+ "plan" => ConsoleColor.Cyan,
+ "execute" => ConsoleColor.Green,
_ => ConsoleColor.Gray,
};
}
diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/ToolCallFormatter.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/ToolCallFormatter.cs
index e3430ce10d..50fc192c59 100644
--- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/ToolCallFormatter.cs
+++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/ToolCallFormatter.cs
@@ -23,15 +23,15 @@ public static string Format(FunctionCallContent call)
string? detail = call.Name switch
{
// Todo tools
- "AddTodos" => FormatAddTodos(call),
- "CompleteTodos" => FormatIdList(call, "ids", "Complete"),
- "RemoveTodos" => FormatIdList(call, "ids", "Remove"),
- "GetRemainingTodos" => null,
- "GetAllTodos" => null,
+ "TodoList_Add" => FormatAddTodos(call),
+ "TodoList_Complete" => FormatIdList(call, "ids", "Complete"),
+ "TodoList_Remove" => FormatIdList(call, "ids", "Remove"),
+ "TodoList_GetRemaining" => null,
+ "TodoList_GetAll" => null,
// Mode tools
- "SetMode" => FormatStringArg(call, "mode"),
- "GetMode" => null,
+ "AgentMode_Set" => FormatStringArg(call, "mode"),
+ "AgentMode_Get" => null,
// Sub-agent tools
"StartSubTask" => FormatStartSubTask(call),
diff --git a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs
index e1cc2b6ff7..6045708cc1 100644
--- a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs
+++ b/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs
@@ -33,11 +33,15 @@
// and research-focused instructions including the mandatory planning workflow.
var instructions =
"""
- You are a research assistant. When given a research topic, research it thoroughly using web search and web browsing. Use your knowledge to form good search queries and hypotheses, but always verify claims with the tools available to you rather than relying on memory alone.
+ You are a research assistant. When given a research topic, research it thoroughly using web search and web browsing.
+ Use your knowledge to form good search queries and hypotheses, but always verify claims with the tools available to you rather than relying on memory alone.
- **Mandatory planning workflow**
+ ## Mandatory planning workflow
- For every new substantive user request, including short factual questions, you must begin in plan mode and follow this sequence:
+ For every new substantive user request, including short factual questions, your behavior is determined by the mode you are in.
+ If you are in plan mode, start with the *Plan Mode* steps, and if you are in execute mode, skip directly to the *Execute Mode* steps below.
+
+ *Plan Mode*
1. Analyze the request.
2. Ask for clarifications where needed.
@@ -47,31 +51,37 @@ 3. Create one or more todo items.
4. 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.
5. Present the plan to the user.
6. Ask for approval to switch to execute mode and process the plan.
- 7. When approval is granted, always switch to execute mode, execute the plan and complete the todos.
- 8. In execute mode, work autonomously — use your best judgement to make decisions and keep progressing without asking the user questions. The goal is to have a complete, useful result ready when the user returns.
- 9. If you encounter ambiguity or an unexpected situation during execution, choose the most reasonable option, note your choice, and keep going.
- 10. Continue working, thinking and calling tools until you have the research result for the user.
-
- Explain your reasoning and thought process as you work through the tasks.
- Explain what you learned and what you are going to do next between tool calls, so the user can follow along with your thought process.
- When calling many tools in a row, provide an explanation to the user after each 4 tool calls (or fewer) to help the user understand what you're doing and why.
- Do not answer the underlying question before the plan has been presented and approved.
- This rule applies even when the answer seems obvious or the task seems small.
- For short requests, use a brief micro-plan rather than skipping planning.
-
- The only exceptions are:
- - greetings,
- - pure acknowledgments,
- - clarification questions needed to form the plan,
- - follow-up questions about results you have already presented,
- - meta-discussion about the workflow itself.
-
- When the task is complete, switch back to plan mode for the next request, even if the next request is just a short question.
+ 7. When approval is granted, always switch to execute mode (using the `AgentMode_Set` tool), and follow the steps for *Execute mode*.
+
+ *Execute Mode*
+
+ 1. If you don't have a plan or tasks yet, analyse the user request and create tasks and a plan. (**Skip this step if you came from plan mode**)
+ 2. Work autonomously — use your best judgement to make decisions and keep progressing without asking the user questions. The goal is to have a complete, useful result ready when the user returns.
+ 3. If you encounter ambiguity or an unexpected situation during execution, choose the most reasonable option, note your choice, and keep going.
+ 4. Mark tasks as completed as you finish them.
+ 5. Continue working, thinking and calling tools until you have the research result for the user.
+
+ ## General Instructions
+
+ - You must check the current mode after any user input, since the user may have changed the mode themselves,
+ e.g. the user may have switched to 'plan' mode after a previous research task finished in 'execute' mode, meaning they want to review a plan first before execution.
+ - Explain your reasoning and thought process as you work through tasks.
+ - Explain what you learned and what you are going to do next between tool calls, so the user can follow along with your thought process.
+ - Avoid making more than 4 tool calls in a row without explaining what you are doing.
+ - Do answer the underlying question before the plan has been presented and approved.
+ - This rule applies even when the answer seems obvious or the task seems small.
+ - For short requests, use a brief micro-plan rather than skipping planning. The only exceptions are:
+ - greetings,
+ - pure acknowledgments,
+ - clarification questions needed to form the plan,
+ - follow-up questions about results you have already presented,
+ - meta-discussion about the workflow itself.
**Todo management**
Mark each todo complete as you finish it so the list stays current.
If a todo turns out to be unnecessary or is blocked, remove it and briefly explain why.
+ Once the user finishes with a topic and moves onto a new one, clean up old completed todos by deleting them.
**Research quality**
@@ -90,12 +100,12 @@ Track your sources — you will need them when presenting results.
**File memory**
- When you download web pages or receive large amounts of data, save them to file memory using the FileMemory_SaveFile tool.
- This ensures the data remains accessible even if older context is compacted or truncated during long research sessions.
- Use descriptive file names (e.g., "openai_pricing_page.md") and include a brief description for large files.
- Also save intermediate notes and findings as you go — this helps with long multi-step research where early findings inform later steps.
- Before starting new research, check file memory with FileMemory_ListFiles and FileMemory_SearchFiles for relevant prior downloads.
- When a temporary file is no longer needed, delete it to keep file memory tidy.
+ Use the FileMemory_* tools to:
+ - Store downloaded search results or web pages.
+ - Store plans.
+ - Read the current plan to make sure tasks were done according to plan.
+ - Store findings.
+ - Check for relevant previously downloaded data / findings before starting new research.
""";
// Create a compaction strategy based on the model's context window.
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs
index 80f634b3b6..e706ed88f6 100644
--- a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
@@ -21,10 +22,15 @@ namespace Microsoft.Agents.AI;
/// and is included in the instructions provided to the agent on each invocation.
///
///
+/// The set of available modes is configurable via .
+/// By default, two modes are provided: "plan" (interactive planning) and "execute"
+/// (autonomous execution).
+///
+///
/// This provider exposes the following tools to the agent:
///
-/// - SetMode — Switch the agent's operating mode.
-/// - GetMode — Retrieve the agent's current operating mode.
+/// - AgentMode_Set — Switch the agent's operating mode.
+/// - AgentMode_Get — Retrieve the agent's current operating mode.
///
///
///
@@ -35,26 +41,48 @@ namespace Microsoft.Agents.AI;
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class AgentModeProvider : AIContextProvider
{
- ///
- /// The "plan" mode, indicating the agent is planning work.
- ///
- public const string PlanMode = "plan";
-
- ///
- /// The "execute" mode, indicating the agent is executing work.
- ///
- public const string ExecuteMode = "execute";
+ private static readonly IReadOnlyList s_defaultModes =
+ [
+ new("plan", "Use this mode when analyzing requirements, breaking down tasks, and creating plans. This is the interactive mode — ask clarifying questions, discuss options, and get user approval before proceeding."),
+ new("execute", "Use this mode when carrying out approved plans. Work autonomously using your best judgement — do not ask the user questions or wait for feedback. Make reasonable decisions on your own so that there is a complete, useful result when the user returns. If you encounter ambiguity, choose the most reasonable option and note your choice."),
+ ];
private readonly ProviderSessionState _sessionState;
+ private readonly IReadOnlyList _modes;
+ private readonly string _defaultMode;
+ private readonly string? _customInstructions;
+ private readonly HashSet _validModeNames;
private IReadOnlyList? _stateKeys;
///
/// Initializes a new instance of the class.
///
- public AgentModeProvider()
+ /// Optional settings that control provider behavior. When , defaults are used.
+ public AgentModeProvider(AgentModeProviderOptions? options = null)
{
+ this._modes = options?.Modes ?? s_defaultModes;
+
+ if (this._modes.Count == 0)
+ {
+ throw new ArgumentException("At least one mode must be configured.", nameof(options));
+ }
+
+ this._defaultMode = options?.DefaultMode ?? this._modes[0].Name;
+ this._customInstructions = options?.Instructions;
+
+ this._validModeNames = new HashSet(StringComparer.Ordinal);
+ foreach (var mode in this._modes)
+ {
+ this._validModeNames.Add(mode.Name);
+ }
+
+ if (!this._validModeNames.Contains(this._defaultMode))
+ {
+ throw new ArgumentException($"Default mode \"{this._defaultMode}\" is not in the configured modes list.", nameof(options));
+ }
+
this._sessionState = new ProviderSessionState(
- _ => new AgentModeState(),
+ _ => new AgentModeState { CurrentMode = this._defaultMode },
this.GetType().Name,
AgentJsonUtilities.DefaultOptions);
}
@@ -77,15 +105,20 @@ public string GetMode(AgentSession? session)
///
/// The agent session to update the mode in.
/// The new mode to set.
+ /// is not a configured mode.
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));
- }
+ this.ValidateMode(mode);
AgentModeState state = this._sessionState.GetOrInitializeState(session);
+ string previousMode = state.CurrentMode;
state.CurrentMode = mode;
+
+ if (!string.Equals(previousMode, mode, StringComparison.Ordinal))
+ {
+ state.PreviousModeForNotification = previousMode;
+ }
+
this._sessionState.SaveState(session, state);
}
@@ -94,35 +127,68 @@ protected override ValueTask ProvideAIContextAsync(InvokingContext co
{
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. This is the interactive mode — ask clarifying questions, discuss options, and get user approval before proceeding.
- - "execute": Use this mode when carrying out approved plans. Work autonomously using your best judgement — do not ask the user questions or wait for feedback. Make reasonable decisions on your own so that there is a complete, useful result when the user returns. If you encounter ambiguity, choose the most reasonable option and note your choice.
- 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.
- """;
+ string instructions = this._customInstructions ?? this.BuildDefaultInstructions(state.CurrentMode);
- return new ValueTask(new AIContext
+ var aiContext = new AIContext
{
Instructions = instructions,
Tools = this.CreateTools(state, 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)
+ {
+ 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.]"),
+ ];
+ }
+
+ return new ValueTask(aiContext);
+ }
+
+ private string BuildDefaultInstructions(string currentMode)
+ {
+ var sb = new StringBuilder();
+ sb.Append($"You are currently operating in \"{currentMode}\" mode.");
+ sb.AppendLine();
+ sb.AppendLine("Available modes:");
+
+ foreach (var mode in this._modes)
+ {
+ sb.AppendLine($"- \"{mode.Name}\": {mode.Description}");
+ }
+
+ sb.AppendLine("Use the AgentMode_Set tool to switch between modes as your work progresses. Only use AgentMode_Set if the user explicitly instructs/allows you to change modes.");
+ sb.Append("Use the AgentMode_Get tool to check your current operating mode.");
+
+ return sb.ToString();
+ }
+
+ private void ValidateMode(string mode)
+ {
+ if (!this._validModeNames.Contains(mode))
+ {
+ var modeNames = string.Join("\", \"", this._validModeNames);
+ throw new ArgumentException($"Invalid mode: \"{mode}\". Supported modes are: \"{modeNames}\".", nameof(mode));
+ }
}
private AITool[] CreateTools(AgentModeState state, AgentSession? session)
{
var serializerOptions = AgentJsonUtilities.DefaultOptions;
+ var modeNames = string.Join("\", \"", this._validModeNames);
return
[
AIFunctionFactory.Create(
(string mode) =>
{
- if (mode != PlanMode && mode != ExecuteMode)
- {
- throw new ArgumentException($"Invalid mode: {mode}. Supported modes are \"{PlanMode}\" and \"{ExecuteMode}\".", nameof(mode));
- }
+ this.ValidateMode(mode);
state.CurrentMode = mode;
this._sessionState.SaveState(session, state);
@@ -130,8 +196,8 @@ private AITool[] CreateTools(AgentModeState state, AgentSession? session)
},
new AIFunctionFactoryOptions
{
- Name = "SetMode",
- Description = "Switch the agent's operating mode. Supported modes: \"plan\" and \"execute\".",
+ Name = "AgentMode_Set",
+ Description = $"Switch the agent's operating mode. Supported modes: \"{modeNames}\".",
SerializerOptions = serializerOptions,
}),
@@ -139,7 +205,7 @@ private AITool[] CreateTools(AgentModeState state, AgentSession? session)
() => state.CurrentMode,
new AIFunctionFactoryOptions
{
- Name = "GetMode",
+ Name = "AgentMode_Get",
Description = "Get the agent's current operating mode.",
SerializerOptions = serializerOptions,
}),
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProviderOptions.cs
new file mode 100644
index 0000000000..8b0379be28
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProviderOptions.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Shared.DiagnosticIds;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// Options controlling the behavior of .
+///
+[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
+public sealed class AgentModeProviderOptions
+{
+ ///
+ /// Gets or sets custom instructions provided to the agent for using the mode tools.
+ ///
+ ///
+ /// When (the default), the provider generates instructions dynamically
+ /// from the configured list.
+ ///
+ public string? Instructions { get; set; }
+
+ ///
+ /// Gets or sets the list of available modes the agent can operate in.
+ ///
+ ///
+ /// When (the default), the provider uses two built-in modes:
+ /// "plan" (interactive planning) and "execute" (autonomous execution).
+ ///
+ public IReadOnlyList? Modes { get; set; }
+
+ ///
+ /// Gets or sets the initial mode for new sessions.
+ ///
+ ///
+ /// When (the default), the first mode in the list is used.
+ /// Must match the of one of the configured modes.
+ ///
+ public string? DefaultMode { get; set; }
+
+ ///
+ /// Represents an agent operating mode with a name and description.
+ ///
+ [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)
+ {
+ this.Name = Throw.IfNullOrWhitespace(name);
+ this.Description = Throw.IfNullOrWhitespace(description);
+ }
+
+ ///
+ /// Gets the name of the mode.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets a description of when and how to use this mode.
+ ///
+ public string Description { get; }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeState.cs b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeState.cs
index e16c9c9289..63cc25eb1c 100644
--- a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeState.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeState.cs
@@ -16,5 +16,12 @@ internal sealed class AgentModeState
/// Gets or sets the current operating mode of the agent.
///
[JsonPropertyName("currentMode")]
- public string CurrentMode { get; set; } = AgentModeProvider.PlanMode;
+ public string CurrentMode { get; set; } = "plan";
+
+ ///
+ /// Gets or sets the previous mode before the last external change, if a mode change notification is pending.
+ /// When non-null, indicates that the mode was changed externally and a notification should be injected.
+ ///
+ [JsonPropertyName("previousModeForNotification")]
+ public string? PreviousModeForNotification { get; set; }
}
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/FileMemory/FileMemoryProvider.cs b/dotnet/src/Microsoft.Agents.AI/Harness/FileMemory/FileMemoryProvider.cs
index 49aaf6e79a..0a6f8c9cdd 100644
--- a/dotnet/src/Microsoft.Agents.AI/Harness/FileMemory/FileMemoryProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/FileMemory/FileMemoryProvider.cs
@@ -58,6 +58,7 @@ This ensures important data remains accessible across long-running sessions.
private readonly AgentFileStore _fileStore;
private readonly ProviderSessionState _sessionState;
+ private readonly string _instructions;
private IReadOnlyList? _stateKeys;
private AITool[]? _tools;
@@ -70,12 +71,14 @@ This ensures important data remains accessible across long-running sessions.
/// Use this to customize the working folder (e.g., per-user or per-session subfolders).
/// When , the default initializer creates state with an empty working folder.
///
+ /// Optional settings that control provider behavior. When , defaults are used.
/// Thrown when is .
- public FileMemoryProvider(AgentFileStore fileStore, Func? stateInitializer = null)
+ public FileMemoryProvider(AgentFileStore fileStore, Func? stateInitializer = null, FileMemoryProviderOptions? options = null)
{
Throw.IfNull(fileStore);
this._fileStore = fileStore;
+ this._instructions = options?.Instructions ?? DefaultInstructions;
this._sessionState = new ProviderSessionState(
stateInitializer ?? (_ => new FileMemoryState()),
this.GetType().Name,
@@ -98,7 +101,7 @@ protected override async ValueTask ProvideAIContextAsync(InvokingCont
return new AIContext
{
- Instructions = DefaultInstructions,
+ Instructions = this._instructions,
Tools = this._tools ??= this.CreateTools(),
};
}
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/FileMemory/FileMemoryProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI/Harness/FileMemory/FileMemoryProviderOptions.cs
new file mode 100644
index 0000000000..c8e911daa6
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/FileMemory/FileMemoryProviderOptions.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Shared.DiagnosticIds;
+
+namespace Microsoft.Agents.AI;
+
+///
+/// Options controlling the behavior of .
+///
+[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
+public sealed class FileMemoryProviderOptions
+{
+ ///
+ /// Gets or sets custom instructions provided to the agent for using the file memory tools.
+ ///
+ ///
+ /// When (the default), the provider uses built-in instructions
+ /// that guide the agent on how to use file-based memory effectively.
+ ///
+ public string? Instructions { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProvider.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProvider.cs
index e39db5e197..336aac855e 100644
--- a/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProvider.cs
@@ -23,11 +23,11 @@ namespace Microsoft.Agents.AI;
///
/// This provider exposes the following tools to the agent:
///
-/// - AddTodos — Add one or more todo items, each with a title and optional description.
-/// - CompleteTodos — Mark one or more todo items as complete by their IDs.
-/// - RemoveTodos — Remove one or more todo items by their IDs.
-/// - GetRemainingTodos — Retrieve only incomplete todo items.
-/// - GetAllTodos — Retrieve all todo items (complete and incomplete).
+/// - TodoList_Add — Add one or more todo items, each with a title and optional description.
+/// - TodoList_Complete — Mark one or more todo items as complete by their IDs.
+/// - TodoList_Remove — Remove one or more todo items by their IDs.
+/// - TodoList_GetRemaining — Retrieve only incomplete todo items.
+/// - TodoList_GetAll — Retrieve all todo items (complete and incomplete).
///
///
///
@@ -44,21 +44,24 @@ Ask questions from the user where clarification is needed to create effective to
When a user changes the topic or changes their mind, ensure that you update the todo list accordingly by removing irrelevant items or adding new ones as needed.
Use these tools to manage your tasks:
- - Use AddTodos to break down complex work into trackable items (supports adding one or many at once).
- - Use CompleteTodos to mark items as done when finished (supports one or many at once).
- - Use GetRemainingTodos to check what work is still pending.
- - Use GetAllTodos to review the full list including completed items.
- - Use RemoveTodos to remove items that are no longer needed (supports one or many at once).
+ - Use TodoList_Add to break down complex work into trackable items (supports adding one or many at once).
+ - Use TodoList_Complete to mark items as done when finished (supports one or many at once).
+ - Use TodoList_GetRemaining to check what work is still pending.
+ - Use TodoList_GetAll to review the full list including completed items.
+ - Use TodoList_Remove to remove items that are no longer needed (supports one or many at once).
""";
private readonly ProviderSessionState _sessionState;
+ private readonly string _instructions;
private IReadOnlyList? _stateKeys;
///
/// Initializes a new instance of the class.
///
- public TodoProvider()
+ /// Optional settings that control provider behavior. When , defaults are used.
+ public TodoProvider(TodoProviderOptions? options = null)
{
+ this._instructions = options?.Instructions ?? DefaultInstructions;
this._sessionState = new ProviderSessionState(
_ => new TodoState(),
this.GetType().Name,
@@ -95,7 +98,7 @@ protected override ValueTask ProvideAIContextAsync(InvokingContext co
return new ValueTask(new AIContext
{
- Instructions = DefaultInstructions,
+ Instructions = this._instructions,
Tools = this.CreateTools(state, context.Session),
});
}
@@ -129,7 +132,7 @@ private AITool[] CreateTools(TodoState state, AgentSession? session)
},
new AIFunctionFactoryOptions
{
- Name = "AddTodos",
+ Name = "TodoList_Add",
Description = "Add one or more todo items. Each item has a title and an optional description. Returns the list of created todo items.",
SerializerOptions = serializerOptions,
}),
@@ -157,7 +160,7 @@ private AITool[] CreateTools(TodoState state, AgentSession? session)
},
new AIFunctionFactoryOptions
{
- Name = "CompleteTodos",
+ Name = "TodoList_Complete",
Description = "Mark one or more todo items as complete by their IDs. Returns the number of items that were found and marked complete.",
SerializerOptions = serializerOptions,
}),
@@ -177,7 +180,7 @@ private AITool[] CreateTools(TodoState state, AgentSession? session)
},
new AIFunctionFactoryOptions
{
- Name = "RemoveTodos",
+ Name = "TodoList_Remove",
Description = "Remove one or more todo items by their IDs. Returns the number of items that were found and removed.",
SerializerOptions = serializerOptions,
}),
@@ -186,7 +189,7 @@ private AITool[] CreateTools(TodoState state, AgentSession? session)
() => state.Items.Where(t => !t.IsComplete).ToList(),
new AIFunctionFactoryOptions
{
- Name = "GetRemainingTodos",
+ Name = "TodoList_GetRemaining",
Description = "Retrieve the list of incomplete todo items.",
SerializerOptions = serializerOptions,
}),
@@ -195,7 +198,7 @@ private AITool[] CreateTools(TodoState state, AgentSession? session)
() => state.Items,
new AIFunctionFactoryOptions
{
- Name = "GetAllTodos",
+ Name = "TodoList_GetAll",
Description = "Retrieve the full list of todo items, both complete and incomplete.",
SerializerOptions = serializerOptions,
}),
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProviderOptions.cs
new file mode 100644
index 0000000000..e451ff67d9
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProviderOptions.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+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
+{
+ ///
+ /// Gets or sets custom instructions provided to the agent for using the todo tools.
+ ///
+ ///
+ /// When (the default), the provider uses built-in instructions
+ /// that guide the agent on how to manage todos effectively.
+ ///
+ public string? Instructions { get; set; }
+}
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 59393d4430..3d95d543d9 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/AgentMode/AgentModeProviderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/AgentMode/AgentModeProviderTests.cs
@@ -73,7 +73,7 @@ public async Task SetMode_ChangesModeAsync()
{
// Arrange
var (tools, state) = await CreateToolsWithStateAsync();
- AIFunction setMode = GetTool(tools, "SetMode");
+ AIFunction setMode = GetTool(tools, "AgentMode_Set");
// Act
await setMode.InvokeAsync(new AIFunctionArguments() { ["mode"] = "execute" });
@@ -90,7 +90,7 @@ public async Task SetMode_ReturnsConfirmationAsync()
{
// Arrange
var (tools, _) = await CreateToolsWithStateAsync();
- AIFunction setMode = GetTool(tools, "SetMode");
+ AIFunction setMode = GetTool(tools, "AgentMode_Set");
// Act
object? result = await setMode.InvokeAsync(new AIFunctionArguments() { ["mode"] = "execute" });
@@ -107,8 +107,8 @@ public async Task SetMode_InvalidMode_ThrowsAsync()
{
// Arrange
var (tools, provider, session) = await CreateToolsWithProviderAndSessionAsync();
- AIFunction setMode = GetTool(tools, "SetMode");
- AIFunction getMode = GetTool(tools, "GetMode");
+ AIFunction setMode = GetTool(tools, "AgentMode_Set");
+ AIFunction getMode = GetTool(tools, "AgentMode_Get");
// Act & Assert
await Assert.ThrowsAsync(async () =>
@@ -116,7 +116,7 @@ await Assert.ThrowsAsync(async () =>
// Verify mode was not changed from default
object? currentMode = await getMode.InvokeAsync(new AIFunctionArguments());
- Assert.Equal(AgentModeProvider.PlanMode, GetStringResult(currentMode));
+ Assert.Equal("plan", GetStringResult(currentMode));
}
#endregion
@@ -131,7 +131,7 @@ public async Task GetMode_ReturnsDefaultModeAsync()
{
// Arrange
var (tools, _) = await CreateToolsWithStateAsync();
- AIFunction getMode = GetTool(tools, "GetMode");
+ AIFunction getMode = GetTool(tools, "AgentMode_Get");
// Act
object? result = await getMode.InvokeAsync(new AIFunctionArguments());
@@ -148,8 +148,8 @@ public async Task GetMode_ReturnsUpdatedModeAfterSetAsync()
{
// Arrange
var (tools, _) = await CreateToolsWithStateAsync();
- AIFunction setMode = GetTool(tools, "SetMode");
- AIFunction getMode = GetTool(tools, "GetMode");
+ AIFunction setMode = GetTool(tools, "AgentMode_Set");
+ AIFunction getMode = GetTool(tools, "AgentMode_Get");
// Act
await setMode.InvokeAsync(new AIFunctionArguments() { ["mode"] = "execute" });
@@ -177,7 +177,7 @@ public void PublicGetMode_ReturnsDefaultMode()
string mode = provider.GetMode(session);
// Assert
- Assert.Equal(AgentModeProvider.PlanMode, mode);
+ Assert.Equal("plan", mode);
}
///
@@ -191,11 +191,11 @@ public void PublicSetMode_ChangesMode()
var session = new ChatClientAgentSession();
// Act
- provider.SetMode(session, AgentModeProvider.ExecuteMode);
+ provider.SetMode(session, "execute");
string mode = provider.GetMode(session);
// Assert
- Assert.Equal(AgentModeProvider.ExecuteMode, mode);
+ Assert.Equal("execute", mode);
}
///
@@ -213,7 +213,7 @@ public void PublicSetMode_InvalidMode_Throws()
// Verify mode was not changed from default
string mode = provider.GetMode(session);
- Assert.Equal(AgentModeProvider.PlanMode, mode);
+ 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, AgentModeProvider.ExecuteMode);
+ provider.SetMode(session, "execute");
#pragma warning disable MAAI001
var context = new AIContextProvider.InvokingContext(agent, session, new AIContext());
@@ -236,7 +236,7 @@ public async Task PublicSetMode_ReflectedInToolResultsAsync()
// Act
AIContext result = await provider.InvokingAsync(context);
- AIFunction getMode = GetTool(result.Tools!, "GetMode");
+ AIFunction getMode = GetTool(result.Tools!, "AgentMode_Get");
object? modeResult = await getMode.InvokeAsync(new AIFunctionArguments());
// Assert
@@ -264,12 +264,12 @@ public async Task State_PersistsAcrossInvocationsAsync()
// Act — first invocation changes mode
AIContext result1 = await provider.InvokingAsync(context);
- AIFunction setMode = GetTool(result1.Tools!, "SetMode");
+ AIFunction setMode = GetTool(result1.Tools!, "AgentMode_Set");
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");
+ AIFunction getMode = GetTool(result2.Tools!, "AgentMode_Get");
object? modeResult = await getMode.InvokeAsync(new AIFunctionArguments());
// Assert
@@ -279,17 +279,303 @@ public async Task State_PersistsAcrossInvocationsAsync()
#endregion
- #region Constants Tests
+ #region Options Tests
///
- /// Verify that mode constants have expected values.
+ /// Verify that custom instructions override the default.
///
[Fact]
- public void ModeConstants_HaveExpectedValues()
+ public async Task Options_CustomInstructions_OverridesDefaultAsync()
{
+ // Arrange
+ var options = new AgentModeProviderOptions { Instructions = "Custom mode instructions." };
+ 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.Equal("Custom mode instructions.", result.Instructions);
+ }
+
+ ///
+ /// Verify that custom modes are used.
+ ///
+ [Fact]
+ public void Options_CustomModes_AreUsed()
+ {
+ // Arrange
+ var options = new AgentModeProviderOptions
+ {
+ Modes =
+ [
+ new AgentModeProviderOptions.AgentMode("draft", "Drafting mode."),
+ new AgentModeProviderOptions.AgentMode("review", "Review mode."),
+ ],
+ };
+ var provider = new AgentModeProvider(options);
+ var session = new ChatClientAgentSession();
+
+ // Act
+ string mode = provider.GetMode(session);
+
+ // Assert — default mode is first in list
+ Assert.Equal("draft", mode);
+ }
+
+ ///
+ /// Verify that SetMode validates against custom modes.
+ ///
+ [Fact]
+ public void Options_CustomModes_SetModeValidatesAgainstList()
+ {
+ // Arrange
+ var options = new AgentModeProviderOptions
+ {
+ Modes =
+ [
+ new AgentModeProviderOptions.AgentMode("draft", "Drafting mode."),
+ new AgentModeProviderOptions.AgentMode("review", "Review mode."),
+ ],
+ };
+ var provider = new AgentModeProvider(options);
+ var session = new ChatClientAgentSession();
+
+ // Act — valid mode
+ provider.SetMode(session, "review");
+
+ // Assert
+ Assert.Equal("review", provider.GetMode(session));
+
+ // Act & Assert — invalid mode (plan is no longer valid)
+ Assert.Throws(() => provider.SetMode(session, "plan"));
+ }
+
+ ///
+ /// Verify that a custom default mode is used.
+ ///
+ [Fact]
+ public void Options_CustomDefaultMode_IsUsed()
+ {
+ // Arrange
+ var options = new AgentModeProviderOptions
+ {
+ Modes =
+ [
+ new AgentModeProviderOptions.AgentMode("draft", "Drafting mode."),
+ new AgentModeProviderOptions.AgentMode("review", "Review mode."),
+ ],
+ DefaultMode = "review",
+ };
+ var provider = new AgentModeProvider(options);
+ var session = new ChatClientAgentSession();
+
+ // Act
+ string mode = provider.GetMode(session);
+
+ // Assert
+ Assert.Equal("review", mode);
+ }
+
+ ///
+ /// Verify that an invalid default mode throws.
+ ///
+ [Fact]
+ public void Options_InvalidDefaultMode_Throws()
+ {
+ // Arrange
+ var options = new AgentModeProviderOptions
+ {
+ Modes =
+ [
+ new AgentModeProviderOptions.AgentMode("draft", "Drafting mode."),
+ ],
+ DefaultMode = "nonexistent",
+ };
+
+ // Act & Assert
+ Assert.Throws(() => new AgentModeProvider(options));
+ }
+
+ ///
+ /// Verify that an empty modes list throws.
+ ///
+ [Fact]
+ public void Options_EmptyModes_Throws()
+ {
+ // Arrange
+ var options = new AgentModeProviderOptions
+ {
+ Modes = [],
+ };
+
+ // Act & Assert
+ Assert.Throws(() => new AgentModeProvider(options));
+ }
+
+ ///
+ /// Verify that custom modes appear in generated instructions.
+ ///
+ [Fact]
+ public async Task Options_CustomModes_AppearInInstructionsAsync()
+ {
+ // Arrange
+ var options = new AgentModeProviderOptions
+ {
+ Modes =
+ [
+ new AgentModeProviderOptions.AgentMode("draft", "Drafting mode description."),
+ new AgentModeProviderOptions.AgentMode("review", "Review mode description."),
+ ],
+ };
+ 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.Contains("draft", result.Instructions);
+ Assert.Contains("Drafting mode description.", result.Instructions);
+ Assert.Contains("review", result.Instructions);
+ Assert.Contains("Review mode description.", result.Instructions);
+ }
+
+ ///
+ /// Verify that AgentMode requires non-empty name and description.
+ ///
+ [Fact]
+ public void AgentMode_RequiresNameAndDescription()
+ {
+ // Act & Assert
+ Assert.Throws(() => new AgentModeProviderOptions.AgentMode("", "desc"));
+ Assert.Throws(() => new AgentModeProviderOptions.AgentMode("name", ""));
+ Assert.ThrowsAny(() => new AgentModeProviderOptions.AgentMode(null!, "desc"));
+ Assert.ThrowsAny(() => new AgentModeProviderOptions.AgentMode("name", null!));
+ }
+
+ #endregion
+
+ #region External Mode Change Notification Tests
+
+ ///
+ /// Verify that an external mode change injects a notification message.
+ ///
+ [Fact]
+ public async Task ExternalModeChange_InjectsNotificationMessageAsync()
+ {
+ // Arrange
+ var provider = new AgentModeProvider();
+ var agent = new Mock().Object;
+ var session = new ChatClientAgentSession();
+
+ // Change mode externally (simulating /mode command)
+ provider.SetMode(session, "execute");
+
+#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.Messages);
+ Assert.Single(result.Messages!);
+ ChatMessage message = result.Messages!.First();
+ Assert.Equal(ChatRole.User, message.Role);
+ Assert.Contains("plan", message.Text);
+ Assert.Contains("execute", message.Text);
+ }
+
+ ///
+ /// Verify that the notification is only injected once (cleared after first read).
+ ///
+ [Fact]
+ public async Task ExternalModeChange_NotificationClearedAfterFirstReadAsync()
+ {
+ // Arrange
+ var provider = new AgentModeProvider();
+ var agent = new Mock().Object;
+ var session = new ChatClientAgentSession();
+ provider.SetMode(session, "execute");
+
+#pragma warning disable MAAI001
+ var context = new AIContextProvider.InvokingContext(agent, session, new AIContext());
+#pragma warning restore MAAI001
+
+ // Act — first call should have the notification
+ AIContext result1 = await provider.InvokingAsync(context);
+ Assert.NotNull(result1.Messages);
+
+ // Second call should NOT have the notification
+ AIContext result2 = await provider.InvokingAsync(context);
+
+ // Assert
+ Assert.Null(result2.Messages);
+ }
+
+ ///
+ /// Verify that tool-based mode change does not inject a notification message.
+ ///
+ [Fact]
+ public async Task ToolModeChange_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
+
+ // First call to initialize
+ AIContext result1 = await provider.InvokingAsync(context);
+ AIFunction setMode = GetTool(result1.Tools!, "AgentMode_Set");
+
+ // Change mode via the tool (agent-initiated)
+ await setMode.InvokeAsync(new AIFunctionArguments() { ["mode"] = "execute" });
+
+ // Act — next call should NOT have a notification
+ AIContext result2 = await provider.InvokingAsync(context);
+
+ // Assert
+ Assert.Null(result2.Messages);
+ }
+
+ ///
+ /// Verify that setting the same mode externally does not inject a notification.
+ ///
+ [Fact]
+ public async Task ExternalModeChange_SameMode_NoNotificationAsync()
+ {
+ // Arrange
+ var provider = new AgentModeProvider();
+ var agent = new Mock().Object;
+ var session = new ChatClientAgentSession();
+
+ // Set to same default mode
+ provider.SetMode(session, "plan");
+
+#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.Equal("plan", AgentModeProvider.PlanMode);
- Assert.Equal("execute", AgentModeProvider.ExecuteMode);
+ Assert.Null(result.Messages);
}
#endregion
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/FileMemory/FileMemoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/FileMemory/FileMemoryProviderTests.cs
index 83b2bf3d1d..eaa3286ad1 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/FileMemory/FileMemoryProviderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/FileMemory/FileMemoryProviderTests.cs
@@ -552,4 +552,51 @@ private static AIFunction GetTool(IEnumerable tools, string name)
}
#endregion
+
+ #region Options Tests
+
+ ///
+ /// Verify that custom instructions override the default.
+ ///
+ [Fact]
+ public async Task Options_CustomInstructions_OverridesDefaultAsync()
+ {
+ // Arrange
+ var options = new FileMemoryProviderOptions { Instructions = "Custom file memory instructions." };
+ var provider = new FileMemoryProvider(new InMemoryAgentFileStore(), options: 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.Equal("Custom file memory instructions.", result.Instructions);
+ }
+
+ ///
+ /// Verify that null options uses default instructions.
+ ///
+ [Fact]
+ public async Task Options_Null_UsesDefaultInstructionsAsync()
+ {
+ // Arrange
+ var provider = new FileMemoryProvider(new InMemoryAgentFileStore());
+ 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("file-based memory", result.Instructions);
+ }
+
+ #endregion
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/Todo/TodoProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/Todo/TodoProviderTests.cs
index 726de7e50a..984a2ca1bc 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/Todo/TodoProviderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/Todo/TodoProviderTests.cs
@@ -51,7 +51,7 @@ public async Task AddTodos_CreatesSingleItemAsync()
{
// Arrange
var (tools, state) = await CreateToolsWithStateAsync();
- AIFunction addTodos = GetTool(tools, "AddTodos");
+ AIFunction addTodos = GetTool(tools, "TodoList_Add");
// Act
await addTodos.InvokeAsync(new AIFunctionArguments()
@@ -75,7 +75,7 @@ public async Task AddTodos_CreatesMultipleItemsWithIncrementingIdsAsync()
{
// Arrange
var (tools, state) = await CreateToolsWithStateAsync();
- AIFunction addTodos = GetTool(tools, "AddTodos");
+ AIFunction addTodos = GetTool(tools, "TodoList_Add");
// Act
await addTodos.InvokeAsync(new AIFunctionArguments()
@@ -111,8 +111,8 @@ public async Task CompleteTodos_MarksItemCompleteAsync()
{
// Arrange
var (tools, state) = await CreateToolsWithStateAsync();
- AIFunction addTodos = GetTool(tools, "AddTodos");
- AIFunction completeTodos = GetTool(tools, "CompleteTodos");
+ AIFunction addTodos = GetTool(tools, "TodoList_Add");
+ AIFunction completeTodos = GetTool(tools, "TodoList_Complete");
await addTodos.InvokeAsync(new AIFunctionArguments() { ["todos"] = new List { new() { Title = "Test", Description = null } } });
// Act
@@ -131,8 +131,8 @@ public async Task CompleteTodos_MarksMultipleItemsCompleteAsync()
{
// Arrange
var (tools, state) = await CreateToolsWithStateAsync();
- AIFunction addTodos = GetTool(tools, "AddTodos");
- AIFunction completeTodos = GetTool(tools, "CompleteTodos");
+ AIFunction addTodos = GetTool(tools, "TodoList_Add");
+ AIFunction completeTodos = GetTool(tools, "TodoList_Complete");
await addTodos.InvokeAsync(new AIFunctionArguments()
{
["todos"] = new List { new() { Title = "First" }, new() { Title = "Second" }, new() { Title = "Third" } },
@@ -156,7 +156,7 @@ public async Task CompleteTodos_ReturnsZeroForMissingIdsAsync()
{
// Arrange
var (tools, _) = await CreateToolsWithStateAsync();
- AIFunction completeTodos = GetTool(tools, "CompleteTodos");
+ AIFunction completeTodos = GetTool(tools, "TodoList_Complete");
// Act
object? result = await completeTodos.InvokeAsync(new AIFunctionArguments() { ["ids"] = new List { 999 } });
@@ -177,8 +177,8 @@ public async Task RemoveTodos_RemovesItemAsync()
{
// Arrange
var (tools, state) = await CreateToolsWithStateAsync();
- AIFunction addTodos = GetTool(tools, "AddTodos");
- AIFunction removeTodos = GetTool(tools, "RemoveTodos");
+ AIFunction addTodos = GetTool(tools, "TodoList_Add");
+ AIFunction removeTodos = GetTool(tools, "TodoList_Remove");
await addTodos.InvokeAsync(new AIFunctionArguments() { ["todos"] = new List { new() { Title = "Test", Description = null } } });
// Act
@@ -197,8 +197,8 @@ public async Task RemoveTodos_RemovesMultipleItemsAsync()
{
// Arrange
var (tools, state) = await CreateToolsWithStateAsync();
- AIFunction addTodos = GetTool(tools, "AddTodos");
- AIFunction removeTodos = GetTool(tools, "RemoveTodos");
+ AIFunction addTodos = GetTool(tools, "TodoList_Add");
+ AIFunction removeTodos = GetTool(tools, "TodoList_Remove");
await addTodos.InvokeAsync(new AIFunctionArguments()
{
["todos"] = new List { new() { Title = "First" }, new() { Title = "Second" }, new() { Title = "Third" } },
@@ -221,7 +221,7 @@ public async Task RemoveTodos_ReturnsZeroForMissingIdsAsync()
{
// Arrange
var (tools, _) = await CreateToolsWithStateAsync();
- AIFunction removeTodos = GetTool(tools, "RemoveTodos");
+ AIFunction removeTodos = GetTool(tools, "TodoList_Remove");
// Act
object? result = await removeTodos.InvokeAsync(new AIFunctionArguments() { ["ids"] = new List { 999 } });
@@ -242,9 +242,9 @@ public async Task GetRemainingTodos_ReturnsOnlyIncompleteAsync()
{
// Arrange
var (tools, _) = await CreateToolsWithStateAsync();
- AIFunction addTodos = GetTool(tools, "AddTodos");
- AIFunction completeTodos = GetTool(tools, "CompleteTodos");
- AIFunction getRemainingTodos = GetTool(tools, "GetRemainingTodos");
+ AIFunction addTodos = GetTool(tools, "TodoList_Add");
+ AIFunction completeTodos = GetTool(tools, "TodoList_Complete");
+ AIFunction getRemainingTodos = GetTool(tools, "TodoList_GetRemaining");
await addTodos.InvokeAsync(new AIFunctionArguments()
{
["todos"] = new List { new() { Title = "Done", Description = null }, new() { Title = "Pending", Description = null } },
@@ -272,9 +272,9 @@ public async Task GetAllTodos_ReturnsAllItemsAsync()
{
// Arrange
var (tools, _) = await CreateToolsWithStateAsync();
- AIFunction addTodos = GetTool(tools, "AddTodos");
- AIFunction completeTodos = GetTool(tools, "CompleteTodos");
- AIFunction getAllTodos = GetTool(tools, "GetAllTodos");
+ AIFunction addTodos = GetTool(tools, "TodoList_Add");
+ AIFunction completeTodos = GetTool(tools, "TodoList_Complete");
+ AIFunction getAllTodos = GetTool(tools, "TodoList_GetAll");
await addTodos.InvokeAsync(new AIFunctionArguments()
{
["todos"] = new List { new() { Title = "Done", Description = null }, new() { Title = "Pending", Description = null } },
@@ -309,12 +309,12 @@ public async Task State_PersistsInSessionStateBagAsync()
// Act — first invocation adds a todo
AIContext result1 = await provider.InvokingAsync(context);
- AIFunction addTodos = (AIFunction)result1.Tools!.First(t => t is AIFunction f && f.Name == "AddTodos");
+ AIFunction addTodos = (AIFunction)result1.Tools!.First(t => t is AIFunction f && f.Name == "TodoList_Add");
await addTodos.InvokeAsync(new AIFunctionArguments() { ["todos"] = new List { new() { Title = "Persisted", Description = null } } });
// Second invocation should see the same state
AIContext result2 = await provider.InvokingAsync(context);
- AIFunction getAllTodos = (AIFunction)result2.Tools!.First(t => t is AIFunction f && f.Name == "GetAllTodos");
+ AIFunction getAllTodos = (AIFunction)result2.Tools!.First(t => t is AIFunction f && f.Name == "TodoList_GetAll");
object? allResult = await getAllTodos.InvokeAsync(new AIFunctionArguments());
// Assert
@@ -341,7 +341,7 @@ public async Task PublicGetAllTodos_ReturnsAllItemsAsync()
var context = new AIContextProvider.InvokingContext(agent, session, new AIContext());
#pragma warning restore MAAI001
AIContext result = await provider.InvokingAsync(context);
- AIFunction addTodos = GetTool(result.Tools!, "AddTodos");
+ AIFunction addTodos = GetTool(result.Tools!, "TodoList_Add");
await addTodos.InvokeAsync(new AIFunctionArguments()
{
["todos"] = new List { new() { Title = "First", Description = null }, new() { Title = "Second", Description = null } },
@@ -370,8 +370,8 @@ public async Task PublicGetRemainingTodos_ReturnsOnlyIncompleteAsync()
var context = new AIContextProvider.InvokingContext(agent, session, new AIContext());
#pragma warning restore MAAI001
AIContext result = await provider.InvokingAsync(context);
- AIFunction addTodos = GetTool(result.Tools!, "AddTodos");
- AIFunction completeTodos = GetTool(result.Tools!, "CompleteTodos");
+ AIFunction addTodos = GetTool(result.Tools!, "TodoList_Add");
+ AIFunction completeTodos = GetTool(result.Tools!, "TodoList_Complete");
await addTodos.InvokeAsync(new AIFunctionArguments()
{
["todos"] = new List { new() { Title = "Done", Description = null }, new() { Title = "Pending", Description = null } },
@@ -442,4 +442,51 @@ private static List GetArrayResult(object? result)
}
#endregion
+
+ #region Options Tests
+
+ ///
+ /// Verify that custom instructions override the default.
+ ///
+ [Fact]
+ public async Task Options_CustomInstructions_OverridesDefaultAsync()
+ {
+ // Arrange
+ var options = new TodoProviderOptions { Instructions = "Custom todo instructions." };
+ var provider = new TodoProvider(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.Equal("Custom todo instructions.", result.Instructions);
+ }
+
+ ///
+ /// Verify that null options uses default instructions.
+ ///
+ [Fact]
+ public async Task Options_Null_UsesDefaultInstructionsAsync()
+ {
+ // Arrange
+ var provider = new TodoProvider();
+ 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("todo list", result.Instructions);
+ }
+
+ #endregion
}
From b4166ea37e36f36e81942cc89ec5213cacb64ec2 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Fri, 24 Apr 2026 16:36:00 +0000
Subject: [PATCH 2/2] Address PR comments.
---
.../Harness_Step01_Research/Program.cs | 2 +-
.../Harness/AgentMode/AgentModeProvider.cs | 32 ++++++++++++----
.../AgentMode/AgentModeProviderTests.cs | 38 +++++++++++++++++++
3 files changed, 64 insertions(+), 8 deletions(-)
diff --git a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs
index 6045708cc1..bbbc3416a9 100644
--- a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs
+++ b/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs
@@ -68,7 +68,7 @@ 4. Mark tasks as completed as you finish them.
- Explain your reasoning and thought process as you work through tasks.
- Explain what you learned and what you are going to do next between tool calls, so the user can follow along with your thought process.
- Avoid making more than 4 tool calls in a row without explaining what you are doing.
- - Do answer the underlying question before the plan has been presented and approved.
+ - Do not answer the underlying question before the plan has been presented and approved.
- This rule applies even when the answer seems obvious or the task seems small.
- For short requests, use a brief micro-plan rather than skipping planning. The only exceptions are:
- greetings,
diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs
index e706ed88f6..3615d76532 100644
--- a/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs
@@ -52,6 +52,7 @@ public sealed class AgentModeProvider : AIContextProvider
private readonly string _defaultMode;
private readonly string? _customInstructions;
private readonly HashSet _validModeNames;
+ private readonly string _modeNamesDisplay;
private IReadOnlyList? _stateKeys;
///
@@ -67,15 +68,34 @@ public AgentModeProvider(AgentModeProviderOptions? options = null)
throw new ArgumentException("At least one mode must be configured.", nameof(options));
}
- this._defaultMode = options?.DefaultMode ?? this._modes[0].Name;
this._customInstructions = options?.Instructions;
this._validModeNames = new HashSet(StringComparer.Ordinal);
- foreach (var mode in this._modes)
+ var modeNamesList = new List(this._modes.Count);
+ for (int i = 0; i < this._modes.Count; i++)
{
- this._validModeNames.Add(mode.Name);
+ var mode = this._modes[i];
+ if (mode is null)
+ {
+ throw new ArgumentException($"Configured mode at index {i} must not be null.", nameof(options));
+ }
+
+ if (string.IsNullOrEmpty(mode.Name))
+ {
+ throw new ArgumentException($"Configured mode at index {i} must have a non-empty name.", nameof(options));
+ }
+
+ if (!this._validModeNames.Add(mode.Name))
+ {
+ throw new ArgumentException($"Configured modes contain a duplicate mode name \"{mode.Name}\".", nameof(options));
+ }
+
+ modeNamesList.Add(mode.Name);
}
+ this._modeNamesDisplay = string.Join("\", \"", modeNamesList);
+ this._defaultMode = options?.DefaultMode ?? modeNamesList[0];
+
if (!this._validModeNames.Contains(this._defaultMode))
{
throw new ArgumentException($"Default mode \"{this._defaultMode}\" is not in the configured modes list.", nameof(options));
@@ -173,15 +193,13 @@ private void ValidateMode(string mode)
{
if (!this._validModeNames.Contains(mode))
{
- var modeNames = string.Join("\", \"", this._validModeNames);
- throw new ArgumentException($"Invalid mode: \"{mode}\". Supported modes are: \"{modeNames}\".", nameof(mode));
+ throw new ArgumentException($"Invalid mode: \"{mode}\". Supported modes are: \"{this._modeNamesDisplay}\".", nameof(mode));
}
}
private AITool[] CreateTools(AgentModeState state, AgentSession? session)
{
var serializerOptions = AgentJsonUtilities.DefaultOptions;
- var modeNames = string.Join("\", \"", this._validModeNames);
return
[
@@ -197,7 +215,7 @@ private AITool[] CreateTools(AgentModeState state, AgentSession? session)
new AIFunctionFactoryOptions
{
Name = "AgentMode_Set",
- Description = $"Switch the agent's operating mode. Supported modes: \"{modeNames}\".",
+ Description = $"Switch the agent's operating mode. Supported modes: \"{this._modeNamesDisplay}\".",
SerializerOptions = serializerOptions,
}),
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 3d95d543d9..61671d4904 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/AgentMode/AgentModeProviderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/AgentMode/AgentModeProviderTests.cs
@@ -463,6 +463,44 @@ public void AgentMode_RequiresNameAndDescription()
Assert.ThrowsAny(() => new AgentModeProviderOptions.AgentMode("name", null!));
}
+ ///
+ /// Verify that duplicate mode names throw.
+ ///
+ [Fact]
+ public void Options_DuplicateModeNames_Throws()
+ {
+ // Arrange
+ var options = new AgentModeProviderOptions
+ {
+ Modes =
+ [
+ new AgentModeProviderOptions.AgentMode("draft", "First draft."),
+ new AgentModeProviderOptions.AgentMode("draft", "Second draft."),
+ ],
+ };
+
+ // Act & Assert
+ var ex = Assert.Throws(() => new AgentModeProvider(options));
+ Assert.Contains("duplicate", ex.Message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Verify that a null entry in the modes list throws.
+ ///
+ [Fact]
+ public void Options_NullModeEntry_Throws()
+ {
+ // Arrange
+ var options = new AgentModeProviderOptions
+ {
+ Modes = new List { null! },
+ };
+
+ // Act & Assert
+ var ex = Assert.Throws(() => new AgentModeProvider(options));
+ Assert.Contains("must not be null", ex.Message, StringComparison.OrdinalIgnoreCase);
+ }
+
#endregion
#region External Mode Change Notification Tests