.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
Unverified
parent 08aeb67a9a
commit f747d8a6d4
13 changed files with 775 additions and 143 deletions
@@ -282,16 +282,6 @@ public static class HarnessConsole
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 @@ public static class HarnessConsole
private static ConsoleColor GetModeColor(string mode) => mode switch
{
AgentModeProvider.PlanMode => ConsoleColor.Cyan,
AgentModeProvider.ExecuteMode => ConsoleColor.Green,
"plan" => ConsoleColor.Cyan,
"execute" => ConsoleColor.Green,
_ => ConsoleColor.Gray,
};
}
@@ -23,15 +23,15 @@ public static class ToolCallFormatter
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),
@@ -33,11 +33,15 @@ const int MaxOutputTokens = 128_000;
// 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 @@ var instructions =
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.
7. When approval is granted, always switch to execute mode (using the `AgentMode_Set` tool), and follow the steps for *Execute mode*.
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.
*Execute Mode*
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.
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.
When the task is complete, switch back to plan mode for the next request, even if the next request is just a short question.
## 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 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.
**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 @@ var instructions =
**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.
@@ -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; }
}
@@ -73,7 +73,7 @@ public class AgentModeProviderTests
{
// 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 class AgentModeProviderTests
{
// 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 class AgentModeProviderTests
{
// 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<ArgumentException>(async () =>
@@ -116,7 +116,7 @@ public class AgentModeProviderTests
// 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 class AgentModeProviderTests
{
// 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 class AgentModeProviderTests
{
// 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 class AgentModeProviderTests
string mode = provider.GetMode(session);
// Assert
Assert.Equal(AgentModeProvider.PlanMode, mode);
Assert.Equal("plan", mode);
}
/// <summary>
@@ -191,11 +191,11 @@ public class AgentModeProviderTests
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);
}
/// <summary>
@@ -213,7 +213,7 @@ public class AgentModeProviderTests
// Verify mode was not changed from default
string mode = provider.GetMode(session);
Assert.Equal(AgentModeProvider.PlanMode, mode);
Assert.Equal("plan", mode);
}
/// <summary>
@@ -228,7 +228,7 @@ public class AgentModeProviderTests
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 class AgentModeProviderTests
// 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 class AgentModeProviderTests
// 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,341 @@ public class AgentModeProviderTests
#endregion
#region Constants Tests
#region Options Tests
/// <summary>
/// Verify that mode constants have expected values.
/// Verify that custom instructions override the default.
/// </summary>
[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<AIAgent>().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("plan", AgentModeProvider.PlanMode);
Assert.Equal("execute", AgentModeProvider.ExecuteMode);
Assert.Equal("Custom mode instructions.", result.Instructions);
}
/// <summary>
/// Verify that custom modes are used.
/// </summary>
[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);
}
/// <summary>
/// Verify that SetMode validates against custom modes.
/// </summary>
[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<ArgumentException>(() => provider.SetMode(session, "plan"));
}
/// <summary>
/// Verify that a custom default mode is used.
/// </summary>
[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);
}
/// <summary>
/// Verify that an invalid default mode throws.
/// </summary>
[Fact]
public void Options_InvalidDefaultMode_Throws()
{
// Arrange
var options = new AgentModeProviderOptions
{
Modes =
[
new AgentModeProviderOptions.AgentMode("draft", "Drafting mode."),
],
DefaultMode = "nonexistent",
};
// Act & Assert
Assert.Throws<ArgumentException>(() => new AgentModeProvider(options));
}
/// <summary>
/// Verify that an empty modes list throws.
/// </summary>
[Fact]
public void Options_EmptyModes_Throws()
{
// Arrange
var options = new AgentModeProviderOptions
{
Modes = [],
};
// Act & Assert
Assert.Throws<ArgumentException>(() => new AgentModeProvider(options));
}
/// <summary>
/// Verify that custom modes appear in generated instructions.
/// </summary>
[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<AIAgent>().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);
}
/// <summary>
/// Verify that AgentMode requires non-empty name and description.
/// </summary>
[Fact]
public void AgentMode_RequiresNameAndDescription()
{
// Act & Assert
Assert.Throws<ArgumentException>(() => new AgentModeProviderOptions.AgentMode("", "desc"));
Assert.Throws<ArgumentException>(() => new AgentModeProviderOptions.AgentMode("name", ""));
Assert.ThrowsAny<ArgumentException>(() => new AgentModeProviderOptions.AgentMode(null!, "desc"));
Assert.ThrowsAny<ArgumentException>(() => new AgentModeProviderOptions.AgentMode("name", null!));
}
/// <summary>
/// Verify that duplicate mode names throw.
/// </summary>
[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<ArgumentException>(() => new AgentModeProvider(options));
Assert.Contains("duplicate", ex.Message, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Verify that a null entry in the modes list throws.
/// </summary>
[Fact]
public void Options_NullModeEntry_Throws()
{
// Arrange
var options = new AgentModeProviderOptions
{
Modes = new List<AgentModeProviderOptions.AgentMode> { null! },
};
// Act & Assert
var ex = Assert.Throws<ArgumentException>(() => new AgentModeProvider(options));
Assert.Contains("must not be null", ex.Message, StringComparison.OrdinalIgnoreCase);
}
#endregion
#region External Mode Change Notification Tests
/// <summary>
/// Verify that an external mode change injects a notification message.
/// </summary>
[Fact]
public async Task ExternalModeChange_InjectsNotificationMessageAsync()
{
// Arrange
var provider = new AgentModeProvider();
var agent = new Mock<AIAgent>().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);
}
/// <summary>
/// Verify that the notification is only injected once (cleared after first read).
/// </summary>
[Fact]
public async Task ExternalModeChange_NotificationClearedAfterFirstReadAsync()
{
// Arrange
var provider = new AgentModeProvider();
var agent = new Mock<AIAgent>().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);
}
/// <summary>
/// Verify that tool-based mode change does not inject a notification message.
/// </summary>
[Fact]
public async Task ToolModeChange_DoesNotInjectNotificationAsync()
{
// Arrange
var provider = new AgentModeProvider();
var agent = new Mock<AIAgent>().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);
}
/// <summary>
/// Verify that setting the same mode externally does not inject a notification.
/// </summary>
[Fact]
public async Task ExternalModeChange_SameMode_NoNotificationAsync()
{
// Arrange
var provider = new AgentModeProvider();
var agent = new Mock<AIAgent>().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.Null(result.Messages);
}
#endregion
@@ -552,4 +552,51 @@ public class FileMemoryProviderTests
}
#endregion
#region Options Tests
/// <summary>
/// Verify that custom instructions override the default.
/// </summary>
[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<AIAgent>().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);
}
/// <summary>
/// Verify that null options uses default instructions.
/// </summary>
[Fact]
public async Task Options_Null_UsesDefaultInstructionsAsync()
{
// Arrange
var provider = new FileMemoryProvider(new InMemoryAgentFileStore());
var agent = new Mock<AIAgent>().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
}
@@ -51,7 +51,7 @@ public class TodoProviderTests
{
// 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 class TodoProviderTests
{
// 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 class TodoProviderTests
{
// 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<TodoItemInput> { new() { Title = "Test", Description = null } } });
// Act
@@ -131,8 +131,8 @@ public class TodoProviderTests
{
// 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<TodoItemInput> { new() { Title = "First" }, new() { Title = "Second" }, new() { Title = "Third" } },
@@ -156,7 +156,7 @@ public class TodoProviderTests
{
// 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<int> { 999 } });
@@ -177,8 +177,8 @@ public class TodoProviderTests
{
// 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<TodoItemInput> { new() { Title = "Test", Description = null } } });
// Act
@@ -197,8 +197,8 @@ public class TodoProviderTests
{
// 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<TodoItemInput> { new() { Title = "First" }, new() { Title = "Second" }, new() { Title = "Third" } },
@@ -221,7 +221,7 @@ public class TodoProviderTests
{
// 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<int> { 999 } });
@@ -242,9 +242,9 @@ public class TodoProviderTests
{
// 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<TodoItemInput> { new() { Title = "Done", Description = null }, new() { Title = "Pending", Description = null } },
@@ -272,9 +272,9 @@ public class TodoProviderTests
{
// 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<TodoItemInput> { new() { Title = "Done", Description = null }, new() { Title = "Pending", Description = null } },
@@ -309,12 +309,12 @@ public class TodoProviderTests
// 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<TodoItemInput> { 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 class TodoProviderTests
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<TodoItemInput> { new() { Title = "First", Description = null }, new() { Title = "Second", Description = null } },
@@ -370,8 +370,8 @@ public class TodoProviderTests
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<TodoItemInput> { new() { Title = "Done", Description = null }, new() { Title = "Pending", Description = null } },
@@ -442,4 +442,51 @@ public class TodoProviderTests
}
#endregion
#region Options Tests
/// <summary>
/// Verify that custom instructions override the default.
/// </summary>
[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<AIAgent>().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);
}
/// <summary>
/// Verify that null options uses default instructions.
/// </summary>
[Fact]
public async Task Options_Null_UsesDefaultInstructionsAsync()
{
// Arrange
var provider = new TodoProvider();
var agent = new Mock<AIAgent>().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
}