mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
282 lines
12 KiB
C#
282 lines
12 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.AI;
|
|
using Microsoft.Shared.DiagnosticIds;
|
|
|
|
namespace Microsoft.Agents.AI;
|
|
|
|
/// <summary>
|
|
/// An <see cref="AIContextProvider"/> that tracks the agent's operating mode (e.g., "plan" or "execute")
|
|
/// in the session state and provides tools for querying and switching modes.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The <see cref="AgentModeProvider"/> enables agents to operate in distinct modes during long-running
|
|
/// complex tasks. The current mode is persisted in the session's <see cref="AgentSessionStateBag"/>
|
|
/// 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>mode_set</c> — Switch the agent's operating mode.</description></item>
|
|
/// <item><description><c>mode_get</c> — Retrieve the agent's current operating mode.</description></item>
|
|
/// </list>
|
|
/// </para>
|
|
/// <para>
|
|
/// Public helper methods <see cref="GetMode"/> and <see cref="SetMode"/> allow external code
|
|
/// to programmatically read and change the mode.
|
|
/// </para>
|
|
/// </remarks>
|
|
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
|
public sealed class AgentModeProvider : AIContextProvider
|
|
{
|
|
private const string DefaultInstructions =
|
|
"""
|
|
## Agent Mode
|
|
|
|
- You can operate in different modes. Depending on the mode you are in, you will be required to follow different processes.
|
|
- 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.
|
|
|
|
Use the mode_get tool to check your current operating mode.
|
|
Use the mode_set tool to switch between modes as your work progresses. Only use mode_set if the user explicitly instructs/allows you to change modes.
|
|
|
|
You are currently operating in the {current_mode} mode.
|
|
|
|
### Mandatory Mode based Workflow
|
|
|
|
For every new substantive user request, including short factual questions, your behavior is determined by the mode you are in.
|
|
|
|
{available_modes}
|
|
""";
|
|
|
|
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.
|
|
|
|
Process to follow when in plan mode:
|
|
1. Analyze the request with the purpose of building a research plan.
|
|
2. Create a list of todo items.
|
|
3. If needed, use the provided tools to do some exploratory checks to help build a plan and determine what clarifying questions you may need from the user.
|
|
4. Ask for clarifications from the user where needed.
|
|
1. Ask each clarification one by one.
|
|
2. When asking for clarification and you have specific options in mind, present them to the user, so they can choose the option instead of having to retype the entire response.
|
|
3. Do not proceed until you have received all the needed clarifications.
|
|
4. Do short exploratory research if it helps with being able to ask sensible clarifications from the user.
|
|
5. Write the plan to a memory file, so that it is retained even if compaction happens. Make sure to update the plan file if the user requests changes.
|
|
6. Present the plan to the user and ask for approval to switch to execute mode and process the plan.
|
|
7. When approval is granted, always switch to execute mode (using the `mode_set` tool), and follow the steps for *Execute mode*.
|
|
"""),
|
|
new(
|
|
"execute",
|
|
"""
|
|
Use this mode when carrying out approved plans. Work autonomously using your best judgment — do not ask the user questions or wait for feedback.
|
|
|
|
Process to follow when in execute mode:
|
|
1. If you don't have a plan or tasks yet, analyze the user request and create tasks and a plan. (**Skip this step if you came from plan mode**)
|
|
2. Work autonomously — use your best judgment 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.
|
|
"""),
|
|
];
|
|
|
|
private readonly ProviderSessionState<AgentModeState> _sessionState;
|
|
private readonly IReadOnlyList<AgentModeProviderOptions.AgentMode> _modes;
|
|
private readonly string _defaultMode;
|
|
private readonly string? _instructions;
|
|
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>
|
|
/// <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._instructions = options?.Instructions ?? DefaultInstructions;
|
|
|
|
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 { CurrentMode = this._defaultMode },
|
|
this.GetType().Name,
|
|
AgentJsonUtilities.DefaultOptions);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override IReadOnlyList<string> StateKeys => this._stateKeys ??= [this._sessionState.StateKey];
|
|
|
|
/// <summary>
|
|
/// Gets the current operating mode from the session state.
|
|
/// </summary>
|
|
/// <param name="session">The agent session to read the mode from.</param>
|
|
/// <returns>The current mode string.</returns>
|
|
public string GetMode(AgentSession? session)
|
|
{
|
|
return this._sessionState.GetOrInitializeState(session).CurrentMode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the operating mode in the session state.
|
|
/// </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)
|
|
{
|
|
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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override ValueTask<AIContext> ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
|
{
|
|
AgentModeState state = this._sessionState.GetOrInitializeState(context.Session);
|
|
|
|
string instructions = this.BuildInstructions(state.CurrentMode);
|
|
|
|
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 BuildInstructions(string currentMode)
|
|
{
|
|
var modesListBuilder = new StringBuilder();
|
|
foreach (var mode in this._modes)
|
|
{
|
|
modesListBuilder.AppendLine($"#### {mode.Name}");
|
|
modesListBuilder.AppendLine();
|
|
modesListBuilder.AppendLine(mode.Description.TrimEnd());
|
|
modesListBuilder.AppendLine();
|
|
}
|
|
|
|
var modesListText = modesListBuilder.ToString();
|
|
|
|
return new StringBuilder(this._instructions)
|
|
.Replace("{available_modes}", modesListText)
|
|
.Replace("{current_mode}", currentMode)
|
|
.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)
|
|
{
|
|
var serializerOptions = AgentJsonUtilities.DefaultOptions;
|
|
|
|
return
|
|
[
|
|
AIFunctionFactory.Create(
|
|
(string mode) =>
|
|
{
|
|
this.ValidateMode(mode);
|
|
|
|
state.CurrentMode = mode;
|
|
this._sessionState.SaveState(session, state);
|
|
return $"Mode changed to \"{mode}\".";
|
|
},
|
|
new AIFunctionFactoryOptions
|
|
{
|
|
Name = "mode_set",
|
|
Description = $"Switch the agent's operating mode. Supported modes: \"{this._modeNamesDisplay}\".",
|
|
SerializerOptions = serializerOptions,
|
|
}),
|
|
|
|
AIFunctionFactory.Create(
|
|
() => state.CurrentMode,
|
|
new AIFunctionFactoryOptions
|
|
{
|
|
Name = "mode_get",
|
|
Description = "Get the agent's current operating mode.",
|
|
SerializerOptions = serializerOptions,
|
|
}),
|
|
];
|
|
}
|
|
}
|