Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
68 changes: 39 additions & 29 deletions dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 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**

Expand All @@ -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.
Expand Down
152 changes: 118 additions & 34 deletions dotnet/src/Microsoft.Agents.AI/Harness/AgentMode/AgentModeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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>
Expand All @@ -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));
Comment thread
westey-m marked this conversation as resolved.
}

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);
}
Expand All @@ -77,15 +125,20 @@ public string GetMode(AgentSession? session)
/// </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);
}

Expand All @@ -94,20 +147,54 @@ protected override ValueTask<AIContext> 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<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)
Expand All @@ -119,27 +206,24 @@ private AITool[] CreateTools(AgentModeState state, AgentSession? session)
AIFunctionFactory.Create(
(string mode) =>
{
if (mode != PlanMode && mode != ExecuteMode)
{
throw new ArgumentException($"Invalid mode: {mode}. Supported modes are \"{PlanMode}\" and \"{ExecuteMode}\".", nameof(mode));
}
this.ValidateMode(mode);

state.CurrentMode = mode;
this._sessionState.SaveState(session, state);
return $"Mode changed to \"{mode}\".";
},
new AIFunctionFactoryOptions
{
Name = "SetMode",
Description = "Switch the agent's operating mode. Supported modes: \"plan\" and \"execute\".",
Name = "AgentMode_Set",
Description = $"Switch the agent's operating mode. Supported modes: \"{this._modeNamesDisplay}\".",
SerializerOptions = serializerOptions,
}),

AIFunctionFactory.Create(
() => state.CurrentMode,
new AIFunctionFactoryOptions
{
Name = "GetMode",
Name = "AgentMode_Get",
Description = "Get the agent's current operating mode.",
SerializerOptions = serializerOptions,
}),
Expand Down
Loading
Loading