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 @@ -27,14 +27,18 @@ namespace Microsoft.Agents.AI;
/// (autonomous execution).
/// </para>
/// <para>
/// This provider exposes the following tools to the agent:
/// By default, 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>
/// Set <see cref="AgentModeProviderOptions.DisableModeSetTool"/> or
/// <see cref="AgentModeProviderOptions.DisableModeGetTool"/> to omit the corresponding built-in tool while
/// retaining mode state and workflow instructions.
/// </para>
/// <para>
/// Public helper methods <see cref="GetModeAsync"/> and <see cref="SetModeAsync"/> allow external code
/// Public helper methods <see cref="GetModeAsync"/> and
/// <see cref="SetModeAsync(AgentSession, string, CancellationToken)"/> allow external code
/// to programmatically read and change the mode.
/// </para>
/// <para>
Expand All @@ -44,16 +48,19 @@ namespace Microsoft.Agents.AI;
/// </remarks>
public sealed class AgentModeProvider : AIContextProvider, IDisposable
{
private const string ModeGetInstructions = "Use the mode_get tool to check your current operating mode.\n";
private const string ModeSetInstructions =
"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.\n\n";
private const string PlanModeTransition =
"7. When approval is granted, always switch to execute mode (using the `mode_set` tool), and follow the steps for *Execute mode*.";

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.

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.
{mode_get_instructions}{mode_set_instructions}You are currently operating in the {current_mode} mode.

### Mandatory Mode based Workflow

Expand All @@ -80,7 +87,7 @@ 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*.
{plan_mode_transition}
"""),
new(
"execute",
Expand All @@ -102,7 +109,11 @@ 4. Mark tasks as completed as you finish them.
private readonly ProviderSessionState<AgentModeState> _sessionState;
private readonly IReadOnlyList<AgentModeProviderOptions.AgentMode> _modes;
private readonly string _defaultMode;
private readonly string? _instructions;
private readonly string _instructions;
private readonly bool _disableModeSetTool;
private readonly bool _disableModeGetTool;
private readonly bool _usesDefaultInstructions;
private readonly bool _usesDefaultModes;
private readonly HashSet<string> _validModeNames;
private readonly string _modeNamesDisplay;
private readonly ConditionalWeakTable<AgentSession, SemaphoreSlim> _sessionLocks = new();
Expand All @@ -115,14 +126,18 @@ 4. Mark tasks as completed as you finish them.
/// <param name="options">Optional settings that control provider behavior. When <see langword="null"/>, defaults are used.</param>
public AgentModeProvider(AgentModeProviderOptions? options = null)
{
this._usesDefaultModes = options?.Modes is 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._usesDefaultInstructions = options?.Instructions is null;
this._instructions = options?.Instructions ?? DefaultInstructions;
this._disableModeSetTool = options?.DisableModeSetTool ?? false;
this._disableModeGetTool = options?.DisableModeGetTool ?? false;

this._validModeNames = new HashSet<string>(StringComparer.Ordinal);
var modeNamesList = new List<string>(this._modes.Count);
Expand Down Expand Up @@ -202,7 +217,30 @@ public async Task<string> GetModeAsync(AgentSession session, CancellationToken c
/// <returns>A <see cref="Task"/> that represents the asynchronous operation.</returns>
/// <exception cref="ArgumentNullException"><paramref name="session"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="mode"/> is not a configured mode.</exception>
public async Task SetModeAsync(AgentSession session, string mode, CancellationToken cancellationToken = default)
public Task SetModeAsync(AgentSession session, string mode, CancellationToken cancellationToken = default) =>
this.SetModeAsync(session, mode, disableNotification: false, cancellationToken);

/// <summary>
/// Sets the operating mode in the session state, optionally suppressing the mode-change notification.
/// </summary>
/// <param name="session">The agent session to update the mode in.</param>
/// <param name="mode">The new mode to set.</param>
/// <param name="disableNotification">
/// <see langword="true"/> to avoid notifying the agent about the mode change on its next invocation;
/// otherwise, <see langword="false"/>. Use <see langword="true"/> when the agent changes mode through
/// a custom function tool and has already observed the tool result. This also clears any pending
/// mode-change notification.
/// </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous operation.</returns>
/// <exception cref="ArgumentNullException"><paramref name="session"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="mode"/> is not a configured mode.</exception>
#pragma warning disable RS0026 // The required Boolean parameter distinguishes this overload from the existing optional-cancellation overload.
public async Task SetModeAsync(
Comment thread
westey-m marked this conversation as resolved.
AgentSession session,
string mode,
bool disableNotification,
CancellationToken cancellationToken = default)
Comment thread
westey-m marked this conversation as resolved.
{
_ = Throw.IfNull(session);

Expand All @@ -216,7 +254,11 @@ public async Task SetModeAsync(AgentSession session, string mode, CancellationTo
string previousMode = state.CurrentMode;
state.CurrentMode = mode;

if (!string.Equals(previousMode, mode, StringComparison.Ordinal))
if (disableNotification)
{
state.PreviousModeForNotification = null;
}
else if (!string.Equals(previousMode, mode, StringComparison.Ordinal))
{
state.PreviousModeForNotification = previousMode;
}
Expand All @@ -228,6 +270,7 @@ public async Task SetModeAsync(AgentSession session, string mode, CancellationTo
sessionLock.Release();
}
}
#pragma warning restore RS0026

/// <inheritdoc />
protected override async ValueTask<AIContext> ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -284,15 +327,32 @@ private string BuildInstructions(string currentMode)
var modesListBuilder = new StringBuilder();
foreach (var mode in this._modes)
{
string modeInstructions = mode.Instructions;
if (this._usesDefaultModes && mode.Name == "plan")
{
modeInstructions = modeInstructions.Replace(
"{plan_mode_transition}",
this._disableModeSetTool ? string.Empty : PlanModeTransition);
}

modesListBuilder.AppendLine($"#### {mode.Name}");
modesListBuilder.AppendLine();
modesListBuilder.AppendLine(mode.Instructions.TrimEnd());
modesListBuilder.AppendLine(modeInstructions.TrimEnd());
modesListBuilder.AppendLine();
}

var modesListText = modesListBuilder.ToString();
string instructions = this._instructions;
if (this._usesDefaultInstructions)
{
instructions = instructions
.Replace("{mode_get_instructions}", this._disableModeGetTool ? string.Empty : ModeGetInstructions)
.Replace(
"{mode_set_instructions}",
this._disableModeSetTool ? string.Empty : ModeSetInstructions);
}

return new StringBuilder(this._instructions)
return new StringBuilder(instructions)
.Replace("{available_modes}", modesListText)
.Replace("{current_mode}", currentMode)
.ToString();
Expand Down Expand Up @@ -322,10 +382,11 @@ private SemaphoreSlim GetSessionLock(AgentSession? session)
private AITool[] CreateTools(AgentSession? session)
{
var serializerOptions = AgentJsonUtilities.DefaultOptions;
var tools = new List<AITool>(2);

return
[
AIFunctionFactory.Create(
if (!this._disableModeSetTool)
{
tools.Add(AIFunctionFactory.Create(
async (string mode) =>
{
this.ValidateMode(mode);
Expand All @@ -350,9 +411,12 @@ private AITool[] CreateTools(AgentSession? session)
Name = "mode_set",
Description = $"Switch the agent's operating mode. Supported modes: \"{this._modeNamesDisplay}\".",
SerializerOptions = serializerOptions,
}),
}));
}

AIFunctionFactory.Create(
if (!this._disableModeGetTool)
{
tools.Add(AIFunctionFactory.Create(
async () =>
{
SemaphoreSlim sessionLock = this.GetSessionLock(session);
Expand All @@ -371,7 +435,9 @@ private AITool[] CreateTools(AgentSession? session)
Name = "mode_get",
Description = "Get the agent's current operating mode.",
SerializerOptions = serializerOptions,
}),
];
}));
}

return tools.ToArray();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ public sealed class AgentModeProviderOptions
/// </value>
public string? DefaultMode { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the built-in <c>mode_set</c> tool is disabled.
/// </summary>
/// <value>
/// When <see langword="false"/> (the default), the provider exposes the <c>mode_set</c> tool.
/// When <see langword="true"/>, the tool is not exposed, while mode state and instructions remain enabled.
/// </value>
public bool DisableModeSetTool { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the built-in <c>mode_get</c> tool is disabled.
/// </summary>
/// <value>
/// When <see langword="false"/> (the default), the provider exposes the <c>mode_get</c> tool.
/// When <see langword="true"/>, the tool is not exposed, while mode state and instructions remain enabled.
/// </value>
public bool DisableModeGetTool { get; set; }

/// <summary>
/// Represents an agent operating mode with a name and instructions.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
*REMOVED*[MAAI001]abstract Microsoft.Agents.AI.AgentFileStore.SearchAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.Agents.AI.FileSearchResult!>!>!
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.get -> System.TimeSpan
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.set -> void
Microsoft.Agents.AI.AgentModeProvider.SetModeAsync(Microsoft.Agents.AI.AgentSession! session, string! mode, bool disableNotification, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.get -> bool
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.set -> void
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.get -> bool
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.set -> void
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.get -> string?
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.set -> void
[MAAI001]const Microsoft.Agents.AI.FileAccessProvider.ReadLinesToolName = "file_access_read_lines" -> string!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
*REMOVED*[MAAI001]abstract Microsoft.Agents.AI.AgentFileStore.SearchAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.Agents.AI.FileSearchResult!>!>!
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.get -> System.TimeSpan
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.set -> void
Microsoft.Agents.AI.AgentModeProvider.SetModeAsync(Microsoft.Agents.AI.AgentSession! session, string! mode, bool disableNotification, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.get -> bool
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.set -> void
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.get -> bool
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.set -> void
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.get -> string?
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.set -> void
[MAAI001]const Microsoft.Agents.AI.FileAccessProvider.ReadLinesToolName = "file_access_read_lines" -> string!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
*REMOVED*[MAAI001]abstract Microsoft.Agents.AI.AgentFileStore.SearchAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.Agents.AI.FileSearchResult!>!>!
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.get -> System.TimeSpan
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.set -> void
Microsoft.Agents.AI.AgentModeProvider.SetModeAsync(Microsoft.Agents.AI.AgentSession! session, string! mode, bool disableNotification, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.get -> bool
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.set -> void
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.get -> bool
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.set -> void
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.get -> string?
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.set -> void
[MAAI001]const Microsoft.Agents.AI.FileAccessProvider.ReadLinesToolName = "file_access_read_lines" -> string!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
*REMOVED*[MAAI001]abstract Microsoft.Agents.AI.AgentFileStore.SearchAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.Agents.AI.FileSearchResult!>!>!
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.get -> System.TimeSpan
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.set -> void
Microsoft.Agents.AI.AgentModeProvider.SetModeAsync(Microsoft.Agents.AI.AgentSession! session, string! mode, bool disableNotification, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.get -> bool
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.set -> void
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.get -> bool
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.set -> void
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.get -> string?
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.set -> void
[MAAI001]const Microsoft.Agents.AI.FileAccessProvider.ReadLinesToolName = "file_access_read_lines" -> string!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
*REMOVED*[MAAI001]abstract Microsoft.Agents.AI.AgentFileStore.SearchAsync(string! directory, string! regexPattern, string? globPattern = null, bool recursive = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.Agents.AI.FileSearchResult!>!>!
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.get -> System.TimeSpan
[MAAI001]Microsoft.Agents.AI.BackgroundAgentsProviderOptions.WaitTimeout.set -> void
Microsoft.Agents.AI.AgentModeProvider.SetModeAsync(Microsoft.Agents.AI.AgentSession! session, string! mode, bool disableNotification, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.get -> bool
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeGetTool.set -> void
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.get -> bool
Microsoft.Agents.AI.AgentModeProviderOptions.DisableModeSetTool.set -> void
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.get -> string?
[MAAI001]Microsoft.Agents.AI.FileLineEdit.ExpectedLine.set -> void
[MAAI001]const Microsoft.Agents.AI.FileAccessProvider.ReadLinesToolName = "file_access_read_lines" -> string!
Expand Down
Loading
Loading