.NET: Make Todo, Mode and FileMemory providers more configurable (#5477)

* Make Todo, Mode and FileMemory providers more configurable

* Address PR comments.
This commit is contained in:
westey
2026-04-27 10:43:26 +01:00
committed by GitHub
parent 08aeb67a9a
commit f747d8a6d4
13 changed files with 775 additions and 143 deletions
@@ -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.
/// </para>
/// <para>
/// The set of available modes is configurable via <see cref="AgentModeProviderOptions.Modes"/>.
/// By default, two modes are provided: <c>"plan"</c> (interactive planning) and <c>"execute"</c>
/// (autonomous execution).
/// </para>
/// <para>
/// This provider exposes the following tools to the agent:
/// <list type="bullet">
/// <item><description><c>SetMode</c> — Switch the agent's operating mode.</description></item>
/// <item><description><c>GetMode</c> — Retrieve the agent's current operating mode.</description></item>
/// <item><description><c>AgentMode_Set</c> — Switch the agent's operating mode.</description></item>
/// <item><description><c>AgentMode_Get</c> — Retrieve the agent's current operating mode.</description></item>
/// </list>
/// </para>
/// <para>
@@ -35,26 +41,68 @@ namespace Microsoft.Agents.AI;
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class AgentModeProvider : AIContextProvider
{
/// <summary>
/// The "plan" mode, indicating the agent is planning work.
/// </summary>
public const string PlanMode = "plan";
/// <summary>
/// The "execute" mode, indicating the agent is executing work.
/// </summary>
public const string ExecuteMode = "execute";
private static readonly IReadOnlyList<AgentModeProviderOptions.AgentMode> 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<AgentModeState> _sessionState;
private readonly IReadOnlyList<AgentModeProviderOptions.AgentMode> _modes;
private readonly string _defaultMode;
private readonly string? _customInstructions;
private readonly HashSet<string> _validModeNames;
private readonly string _modeNamesDisplay;
private IReadOnlyList<string>? _stateKeys;
/// <summary>
/// Initializes a new instance of the <see cref="AgentModeProvider"/> class.
/// </summary>
public AgentModeProvider()
/// <param name="options">Optional settings that control provider behavior. When <see langword="null"/>, defaults are used.</param>
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._customInstructions = options?.Instructions;
this._validModeNames = new HashSet<string>(StringComparer.Ordinal);
var modeNamesList = new List<string>(this._modes.Count);
for (int i = 0; i < this._modes.Count; i++)
{
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));
}
this._sessionState = new ProviderSessionState<AgentModeState>(
_ => new AgentModeState(),
_ => new AgentModeState { CurrentMode = this._defaultMode },
this.GetType().Name,
AgentJsonUtilities.DefaultOptions);
}
@@ -77,15 +125,20 @@ public sealed class AgentModeProvider : AIContextProvider
/// </summary>
/// <param name="session">The agent session to update the mode in.</param>
/// <param name="mode">The new mode to set.</param>
/// <exception cref="ArgumentException"><paramref name="mode"/> is not a configured mode.</exception>
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,20 +147,54 @@ public sealed class AgentModeProvider : AIContextProvider
{
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<AIContext>(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>(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))
{
throw new ArgumentException($"Invalid mode: \"{mode}\". Supported modes are: \"{this._modeNamesDisplay}\".", nameof(mode));
}
}
private AITool[] CreateTools(AgentModeState state, AgentSession? session)
@@ -119,10 +206,7 @@ public sealed class AgentModeProvider : AIContextProvider
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 +214,8 @@ public sealed class AgentModeProvider : AIContextProvider
},
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: \"{this._modeNamesDisplay}\".",
SerializerOptions = serializerOptions,
}),
@@ -139,7 +223,7 @@ public sealed class AgentModeProvider : AIContextProvider
() => state.CurrentMode,
new AIFunctionFactoryOptions
{
Name = "GetMode",
Name = "AgentMode_Get",
Description = "Get the agent's current operating mode.",
SerializerOptions = serializerOptions,
}),
@@ -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;
/// <summary>
/// Options controlling the behavior of <see cref="AgentModeProvider"/>.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class AgentModeProviderOptions
{
/// <summary>
/// Gets or sets custom instructions provided to the agent for using the mode tools.
/// </summary>
/// <value>
/// When <see langword="null"/> (the default), the provider generates instructions dynamically
/// from the configured <see cref="Modes"/> list.
/// </value>
public string? Instructions { get; set; }
/// <summary>
/// Gets or sets the list of available modes the agent can operate in.
/// </summary>
/// <value>
/// When <see langword="null"/> (the default), the provider uses two built-in modes:
/// <c>"plan"</c> (interactive planning) and <c>"execute"</c> (autonomous execution).
/// </value>
public IReadOnlyList<AgentMode>? Modes { get; set; }
/// <summary>
/// Gets or sets the initial mode for new sessions.
/// </summary>
/// <value>
/// When <see langword="null"/> (the default), the first mode in the <see cref="Modes"/> list is used.
/// Must match the <see cref="AgentMode.Name"/> of one of the configured modes.
/// </value>
public string? DefaultMode { get; set; }
/// <summary>
/// Represents an agent operating mode with a name and description.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class AgentMode
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentMode"/> class.
/// </summary>
/// <param name="name">The name of the mode.</param>
/// <param name="description">A description of when and how to use this mode.</param>
/// <exception cref="ArgumentNullException"><paramref name="name"/> or <paramref name="description"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="name"/> or <paramref name="description"/> is empty or whitespace.</exception>
public AgentMode(string name, string description)
{
this.Name = Throw.IfNullOrWhitespace(name);
this.Description = Throw.IfNullOrWhitespace(description);
}
/// <summary>
/// Gets the name of the mode.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets a description of when and how to use this mode.
/// </summary>
public string Description { get; }
}
}
@@ -16,5 +16,12 @@ internal sealed class AgentModeState
/// Gets or sets the current operating mode of the agent.
/// </summary>
[JsonPropertyName("currentMode")]
public string CurrentMode { get; set; } = AgentModeProvider.PlanMode;
public string CurrentMode { get; set; } = "plan";
/// <summary>
/// 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.
/// </summary>
[JsonPropertyName("previousModeForNotification")]
public string? PreviousModeForNotification { get; set; }
}
@@ -58,6 +58,7 @@ public sealed class FileMemoryProvider : AIContextProvider
private readonly AgentFileStore _fileStore;
private readonly ProviderSessionState<FileMemoryState> _sessionState;
private readonly string _instructions;
private IReadOnlyList<string>? _stateKeys;
private AITool[]? _tools;
@@ -70,12 +71,14 @@ public sealed class FileMemoryProvider : AIContextProvider
/// Use this to customize the working folder (e.g., per-user or per-session subfolders).
/// When <see langword="null"/>, the default initializer creates state with an empty working folder.
/// </param>
/// <param name="options">Optional settings that control provider behavior. When <see langword="null"/>, defaults are used.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="fileStore"/> is <see langword="null"/>.</exception>
public FileMemoryProvider(AgentFileStore fileStore, Func<AgentSession?, FileMemoryState>? stateInitializer = null)
public FileMemoryProvider(AgentFileStore fileStore, Func<AgentSession?, FileMemoryState>? stateInitializer = null, FileMemoryProviderOptions? options = null)
{
Throw.IfNull(fileStore);
this._fileStore = fileStore;
this._instructions = options?.Instructions ?? DefaultInstructions;
this._sessionState = new ProviderSessionState<FileMemoryState>(
stateInitializer ?? (_ => new FileMemoryState()),
this.GetType().Name,
@@ -98,7 +101,7 @@ public sealed class FileMemoryProvider : AIContextProvider
return new AIContext
{
Instructions = DefaultInstructions,
Instructions = this._instructions,
Tools = this._tools ??= this.CreateTools(),
};
}
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI;
/// <summary>
/// Options controlling the behavior of <see cref="FileMemoryProvider"/>.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class FileMemoryProviderOptions
{
/// <summary>
/// Gets or sets custom instructions provided to the agent for using the file memory tools.
/// </summary>
/// <value>
/// When <see langword="null"/> (the default), the provider uses built-in instructions
/// that guide the agent on how to use file-based memory effectively.
/// </value>
public string? Instructions { get; set; }
}
@@ -23,11 +23,11 @@ namespace Microsoft.Agents.AI;
/// <para>
/// This provider exposes the following tools to the agent:
/// <list type="bullet">
/// <item><description><c>AddTodos</c> — Add one or more todo items, each with a title and optional description.</description></item>
/// <item><description><c>CompleteTodos</c> — Mark one or more todo items as complete by their IDs.</description></item>
/// <item><description><c>RemoveTodos</c> — Remove one or more todo items by their IDs.</description></item>
/// <item><description><c>GetRemainingTodos</c> — Retrieve only incomplete todo items.</description></item>
/// <item><description><c>GetAllTodos</c> — Retrieve all todo items (complete and incomplete).</description></item>
/// <item><description><c>TodoList_Add</c> — Add one or more todo items, each with a title and optional description.</description></item>
/// <item><description><c>TodoList_Complete</c> — Mark one or more todo items as complete by their IDs.</description></item>
/// <item><description><c>TodoList_Remove</c> — Remove one or more todo items by their IDs.</description></item>
/// <item><description><c>TodoList_GetRemaining</c> — Retrieve only incomplete todo items.</description></item>
/// <item><description><c>TodoList_GetAll</c> — Retrieve all todo items (complete and incomplete).</description></item>
/// </list>
/// </para>
/// </remarks>
@@ -44,21 +44,24 @@ public sealed class TodoProvider : AIContextProvider
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<TodoState> _sessionState;
private readonly string _instructions;
private IReadOnlyList<string>? _stateKeys;
/// <summary>
/// Initializes a new instance of the <see cref="TodoProvider"/> class.
/// </summary>
public TodoProvider()
/// <param name="options">Optional settings that control provider behavior. When <see langword="null"/>, defaults are used.</param>
public TodoProvider(TodoProviderOptions? options = null)
{
this._instructions = options?.Instructions ?? DefaultInstructions;
this._sessionState = new ProviderSessionState<TodoState>(
_ => new TodoState(),
this.GetType().Name,
@@ -95,7 +98,7 @@ public sealed class TodoProvider : AIContextProvider
return new ValueTask<AIContext>(new AIContext
{
Instructions = DefaultInstructions,
Instructions = this._instructions,
Tools = this.CreateTools(state, context.Session),
});
}
@@ -129,7 +132,7 @@ public sealed class TodoProvider : AIContextProvider
},
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 @@ public sealed class TodoProvider : AIContextProvider
},
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 @@ public sealed class TodoProvider : AIContextProvider
},
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 @@ public sealed class TodoProvider : AIContextProvider
() => 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 @@ public sealed class TodoProvider : AIContextProvider
() => state.Items,
new AIFunctionFactoryOptions
{
Name = "GetAllTodos",
Name = "TodoList_GetAll",
Description = "Retrieve the full list of todo items, both complete and incomplete.",
SerializerOptions = serializerOptions,
}),
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI;
/// <summary>
/// Options controlling the behavior of <see cref="TodoProvider"/>.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class TodoProviderOptions
{
/// <summary>
/// Gets or sets custom instructions provided to the agent for using the todo tools.
/// </summary>
/// <value>
/// When <see langword="null"/> (the default), the provider uses built-in instructions
/// that guide the agent on how to manage todos effectively.
/// </value>
public string? Instructions { get; set; }
}