Merge branch 'main' into test-it-bump-aaip210b2

This commit is contained in:
Roger Barreto
2026-05-18 17:03:59 +01:00
committed by GitHub
75 changed files with 3817 additions and 487 deletions
@@ -458,8 +458,9 @@ internal static class ChatResponseUpdateAGUIExtensions
// This ensures all AGUI events have a valid messageId regardless of agent type.
if (string.IsNullOrWhiteSpace(chatResponse.MessageId))
{
streamingMessageId ??= Guid.NewGuid().ToString("N");
chatResponse.MessageId = streamingMessageId;
chatResponse.MessageId = ContainsToolResult(chatResponse)
? Guid.NewGuid().ToString("N")
: (streamingMessageId ??= Guid.NewGuid().ToString("N"));
}
if (chatResponse is { Contents.Count: > 0 } &&
@@ -725,4 +726,17 @@ internal static class ChatResponseUpdateAGUIExtensions
_ => JsonSerializer.Serialize(functionResultContent.Result, options.GetTypeInfo(functionResultContent.Result.GetType())),
};
}
private static bool ContainsToolResult(ChatResponseUpdate chatResponse)
{
foreach (AIContent content in chatResponse.Contents)
{
if (content is FunctionResultContent)
{
return true;
}
}
return false;
}
}
@@ -1,6 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Microsoft.Agents.AI.Compaction;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
@@ -10,7 +13,8 @@ namespace Microsoft.Agents.AI;
/// <summary>
/// A pre-configured <see cref="DelegatingAIAgent"/> that wraps a <see cref="ChatClientAgent"/> with
/// function invocation, per-service-call chat history persistence, and in-loop compaction.
/// function invocation, per-service-call chat history persistence, in-loop compaction, and a rich set
/// of default context providers and agent decorators.
/// </summary>
/// <remarks>
/// <para>
@@ -23,6 +27,27 @@ namespace Microsoft.Agents.AI;
/// </list>
/// </para>
/// <para>
/// By default, the following context providers are included (each can be disabled via <see cref="HarnessAgentOptions"/>):
/// <list type="bullet">
/// <item><description><see cref="TodoProvider"/> — todo list management.</description></item>
/// <item><description><see cref="AgentModeProvider"/> — agent mode tracking (plan/execute).</description></item>
/// <item><description><see cref="FileMemoryProvider"/> — file-based session memory.</description></item>
/// <item><description><see cref="FileAccessProvider"/> — shared file access.</description></item>
/// <item><description><see cref="AgentSkillsProvider"/> — skill discovery and loading.</description></item>
/// </list>
/// </para>
/// <para>
/// The agent is also wrapped with the following decorators by default (each can be disabled):
/// <list type="bullet">
/// <item><description><see cref="ToolApprovalAgent"/> — "don't ask again" tool approval rules.</description></item>
/// <item><description><see cref="OpenTelemetryAgent"/> — OpenTelemetry instrumentation.</description></item>
/// </list>
/// </para>
/// <para>
/// A <see cref="HostedWebSearchTool"/> is added to the chat options by default (can be disabled via
/// <see cref="HarnessAgentOptions.DisableWebSearch"/>).
/// </para>
/// <para>
/// The underlying <see cref="ChatClientAgent"/> is configured with
/// <see cref="ChatClientAgentOptions.UseProvidedChatClientAsIs"/> and
/// <see cref="ChatClientAgentOptions.RequirePerServiceCallChatHistoryPersistence"/> set to <see langword="true"/>
@@ -48,7 +73,9 @@ public sealed class HarnessAgent : DelegatingAIAgent
- Think through the task before acting. Break complex work into clear steps.
- Use the tools available to you to gather information, perform actions, and verify results.
- Explain your reasoning between tool calls so the user can follow your progress.
- 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.
- If a tool call fails or returns unexpected results, adapt your approach rather than repeating the same call.
- When you have completed the task, present a clear and concise summary of what you did and what you found.
""";
@@ -74,15 +101,15 @@ public sealed class HarnessAgent : DelegatingAIAgent
/// additional context providers, and chat history provider.
/// When <see langword="null"/>, the agent uses built-in default settings.
/// </param>
/// <exception cref="System.ArgumentNullException">
/// <exception cref="ArgumentNullException">
/// <paramref name="chatClient"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="System.ArgumentOutOfRangeException">
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="maxContextWindowTokens"/> is not positive, or
/// <paramref name="maxOutputTokens"/> is negative or greater than or equal to <paramref name="maxContextWindowTokens"/>.
/// </exception>
public HarnessAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options = null)
: base(BuildInnerAgent(
: base(BuildAgent(
Throw.IfNull(chatClient),
maxContextWindowTokens,
maxOutputTokens,
@@ -90,6 +117,25 @@ public sealed class HarnessAgent : DelegatingAIAgent
{
}
private static AIAgent BuildAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options)
{
ChatClientAgent innerAgent = BuildInnerAgent(chatClient, maxContextWindowTokens, maxOutputTokens, options);
AIAgentBuilder builder = innerAgent.AsBuilder();
if (options?.DisableToolApproval is not true)
{
builder.UseToolApproval();
}
if (options?.DisableOpenTelemetry is not true)
{
builder.UseOpenTelemetry();
}
return builder.Build();
}
private static ChatClientAgent BuildInnerAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options)
{
var compactionStrategy = new ContextWindowCompactionStrategy(
@@ -102,15 +148,28 @@ public sealed class HarnessAgent : DelegatingAIAgent
ChatReducer = compactionStrategy.AsChatReducer(),
});
string instructions = options?.ChatOptions?.Instructions ?? DefaultInstructions;
string harnessInstructions = options?.HarnessInstructions ?? DefaultInstructions;
string? agentInstructions = options?.ChatOptions?.Instructions;
ChatOptions chatOptions = BuildChatOptions(options?.ChatOptions, instructions, maxOutputTokens);
string instructions = (string.IsNullOrWhiteSpace(harnessInstructions), string.IsNullOrWhiteSpace(agentInstructions)) switch
{
(true, true) => harnessInstructions,
(true, false) => agentInstructions!,
(false, true) => harnessInstructions,
(false, false) => $"{harnessInstructions}\n\n{agentInstructions}",
};
ChatOptions chatOptions = BuildChatOptions(options, instructions, maxOutputTokens);
var compactionProvider = new CompactionProvider(compactionStrategy);
IEnumerable<AIContextProvider> contextProviders = BuildContextProviders(options);
return chatClient
.AsBuilder()
.UseFunctionInvocation()
.UseFunctionInvocation(configure: options?.MaximumIterationsPerRequest is int maxIterations
? ficc => ficc.MaximumIterationsPerRequest = maxIterations
: null)
.UseMessageInjection()
.UsePerServiceCallChatHistoryPersistence()
.UseAIContextProviders(compactionProvider)
@@ -121,17 +180,78 @@ public sealed class HarnessAgent : DelegatingAIAgent
Description = options?.Description,
ChatOptions = chatOptions,
ChatHistoryProvider = chatHistoryProvider,
AIContextProviders = options?.AIContextProviders,
AIContextProviders = contextProviders,
UseProvidedChatClientAsIs = true,
RequirePerServiceCallChatHistoryPersistence = true,
});
}
private static ChatOptions BuildChatOptions(ChatOptions? source, string instructions, int maxOutputTokens)
private static ChatOptions BuildChatOptions(HarnessAgentOptions? options, string instructions, int maxOutputTokens)
{
ChatOptions result = source?.Clone() ?? new ChatOptions();
ChatOptions result = options?.ChatOptions?.Clone() ?? new ChatOptions();
result.Instructions = instructions;
result.MaxOutputTokens ??= maxOutputTokens;
if (options?.DisableWebSearch is not true)
{
result.Tools ??= [];
result.Tools.Add(new HostedWebSearchTool());
}
return result;
}
private static List<AIContextProvider> BuildContextProviders(HarnessAgentOptions? options)
{
var providers = new List<AIContextProvider>();
if (options?.DisableTodoProvider is not true)
{
providers.Add(new TodoProvider());
}
if (options?.DisableAgentModeProvider is not true)
{
providers.Add(new AgentModeProvider(options?.AgentModeProviderOptions));
}
if (options?.DisableFileMemory is not true)
{
AgentFileStore fileMemoryStore = options?.FileMemoryStore
?? new FileSystemAgentFileStore(
Path.Combine(Directory.GetCurrentDirectory(), "agent-file-memory"));
providers.Add(new FileMemoryProvider(
fileMemoryStore,
_ => new FileMemoryState
{
WorkingFolder = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss") + "_" + Guid.NewGuid().ToString(),
}));
}
if (options?.DisableFileAccess is not true)
{
AgentFileStore fileAccessStore = options?.FileAccessStore
?? new FileSystemAgentFileStore(
Path.Combine(Directory.GetCurrentDirectory(), "working"));
providers.Add(new FileAccessProvider(fileAccessStore));
}
if (options?.DisableAgentSkillsProvider is not true)
{
AgentSkillsProvider skillsProvider = options?.AgentSkillsSource is AgentSkillsSource source
? new AgentSkillsProvider(source)
: new AgentSkillsProvider(Directory.GetCurrentDirectory());
providers.Add(skillsProvider);
}
if (options?.AIContextProviders is IEnumerable<AIContextProvider> userProviders)
{
providers.AddRange(userProviders);
}
return providers;
}
}
@@ -36,13 +36,31 @@ public sealed class HarnessAgentOptions
/// Use <see cref="ChatOptions.Tools"/> to supply additional tools the agent can invoke.
/// </para>
/// <para>
/// Use <see cref="ChatOptions.Instructions"/> to override the <see cref="HarnessAgent"/>'s built-in
/// default instructions. When <see cref="ChatOptions.Instructions"/> is <see langword="null"/> or not set,
/// the default instructions are used.
/// Use <see cref="ChatOptions.Instructions"/> to provide agent-specific instructions (e.g., research methodology,
/// data analysis workflow). These are combined with <see cref="HarnessInstructions"/> to form the final instructions
/// sent to the model: harness instructions appear first, followed by agent-specific instructions.
/// When <see cref="ChatOptions.Instructions"/> is <see langword="null"/>, only <see cref="HarnessInstructions"/>
/// (or the default) is used.
/// </para>
/// </remarks>
public ChatOptions? ChatOptions { get; set; }
/// <summary>
/// Gets or sets the harness-level instructions that control general tool usage and behavior patterns.
/// </summary>
/// <remarks>
/// <para>
/// Harness instructions provide guidance on how to use tools, explain reasoning, and structure work.
/// They are combined with <see cref="ChatOptions"/>.<see cref="ChatOptions.Instructions"/> (agent-specific instructions)
/// to produce the final instructions sent to the model: harness instructions first, then agent-specific instructions.
/// </para>
/// <para>
/// When <see langword="null"/> (the default), <see cref="HarnessAgent.DefaultInstructions"/> is used.
/// Set to <see cref="string.Empty"/> to omit harness instructions entirely.
/// </para>
/// </remarks>
public string? HarnessInstructions { get; set; }
/// <summary>
/// Gets or sets the <see cref="ChatHistoryProvider"/> to use for storing chat history.
/// </summary>
@@ -61,4 +79,131 @@ public sealed class HarnessAgentOptions
/// <see cref="ChatClientAgentOptions.AIContextProviders"/>.
/// </remarks>
public IEnumerable<AIContextProvider>? AIContextProviders { get; set; }
/// <summary>
/// Gets or sets the maximum number of function-invocation loop iterations per request.
/// </summary>
/// <remarks>
/// When set, this value is passed to <see cref="FunctionInvokingChatClient.MaximumIterationsPerRequest"/>.
/// When <see langword="null"/>, the <see cref="FunctionInvokingChatClient"/> default is used.
/// </remarks>
public int? MaximumIterationsPerRequest { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the <see cref="ToolApprovalAgent"/> wrapper is disabled.
/// </summary>
/// <remarks>
/// When <see langword="false"/> (the default), the agent is wrapped with tool approval middleware
/// that supports "don't ask again" auto-approval rules.
/// </remarks>
public bool DisableToolApproval { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the <see cref="FileMemoryProvider"/> is disabled.
/// </summary>
/// <remarks>
/// When <see langword="false"/> (the default), a <see cref="FileMemoryProvider"/> is included in the
/// agent's context providers, using either <see cref="FileMemoryStore"/> or a default
/// <see cref="FileSystemAgentFileStore"/> rooted at <c>{cwd}/agent-file-memory/{timestamp}_{guid}</c>.
/// </remarks>
public bool DisableFileMemory { get; set; }
/// <summary>
/// Gets or sets a custom <see cref="AgentFileStore"/> for the <see cref="FileMemoryProvider"/>.
/// </summary>
/// <remarks>
/// When <see langword="null"/> and <see cref="DisableFileMemory"/> is <see langword="false"/>,
/// a default <see cref="FileSystemAgentFileStore"/> is created.
/// This property is ignored when <see cref="DisableFileMemory"/> is <see langword="true"/>.
/// </remarks>
public AgentFileStore? FileMemoryStore { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the <see cref="FileAccessProvider"/> is disabled.
/// </summary>
/// <remarks>
/// When <see langword="false"/> (the default), a <see cref="FileAccessProvider"/> is included in the
/// agent's context providers, using either <see cref="FileAccessStore"/> or a default
/// <see cref="FileSystemAgentFileStore"/> rooted at <c>{cwd}/working</c>.
/// </remarks>
public bool DisableFileAccess { get; set; }
/// <summary>
/// Gets or sets a custom <see cref="AgentFileStore"/> for the <see cref="FileAccessProvider"/>.
/// </summary>
/// <remarks>
/// When <see langword="null"/> and <see cref="DisableFileAccess"/> is <see langword="false"/>,
/// a default <see cref="FileSystemAgentFileStore"/> is created.
/// This property is ignored when <see cref="DisableFileAccess"/> is <see langword="true"/>.
/// </remarks>
public AgentFileStore? FileAccessStore { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the <see cref="HostedWebSearchTool"/> is disabled.
/// </summary>
/// <remarks>
/// When <see langword="false"/> (the default), a <see cref="HostedWebSearchTool"/> is added
/// to <see cref="ChatOptions"/>.<see cref="ChatOptions.Tools"/>.
/// </remarks>
public bool DisableWebSearch { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the <see cref="TodoProvider"/> is disabled.
/// </summary>
/// <remarks>
/// When <see langword="false"/> (the default), a <see cref="TodoProvider"/> is included
/// in the agent's context providers for tracking work items.
/// </remarks>
public bool DisableTodoProvider { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the <see cref="AgentModeProvider"/> is disabled.
/// </summary>
/// <remarks>
/// When <see langword="false"/> (the default), an <see cref="AgentModeProvider"/> is included
/// in the agent's context providers. Use <see cref="AgentModeProviderOptions"/> to configure
/// custom modes.
/// </remarks>
public bool DisableAgentModeProvider { get; set; }
/// <summary>
/// Gets or sets custom options for the <see cref="AgentModeProvider"/>.
/// </summary>
/// <remarks>
/// When <see langword="null"/>, the <see cref="AgentModeProvider"/> uses its built-in default
/// modes ("plan" and "execute"). This property is ignored when
/// <see cref="DisableAgentModeProvider"/> is <see langword="true"/>.
/// </remarks>
public AgentModeProviderOptions? AgentModeProviderOptions { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the <see cref="AgentSkillsProvider"/> is disabled.
/// </summary>
/// <remarks>
/// When <see langword="false"/> (the default), an <see cref="AgentSkillsProvider"/> is included
/// in the agent's context providers. Use <see cref="AgentSkillsSource"/> to provide a custom
/// skills source; otherwise, the provider defaults to file-based skill discovery from the current
/// working directory.
/// </remarks>
public bool DisableAgentSkillsProvider { get; set; }
/// <summary>
/// Gets or sets a custom <see cref="AI.AgentSkillsSource"/> for the <see cref="AgentSkillsProvider"/>.
/// </summary>
/// <remarks>
/// When <see langword="null"/> and <see cref="DisableAgentSkillsProvider"/> is <see langword="false"/>,
/// the provider defaults to file-based skill discovery from the current working directory.
/// This property is ignored when <see cref="DisableAgentSkillsProvider"/> is <see langword="true"/>.
/// </remarks>
public AgentSkillsSource? AgentSkillsSource { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the <see cref="OpenTelemetryAgent"/> wrapper is disabled.
/// </summary>
/// <remarks>
/// When <see langword="false"/> (the default), the agent is wrapped with an
/// <see cref="OpenTelemetryAgent"/> that provides OpenTelemetry instrumentation
/// following the Semantic Conventions for Generative AI systems.
/// </remarks>
public bool DisableOpenTelemetry { get; set; }
}
@@ -120,9 +120,24 @@ public static class OpenAIResponseClientExtensions
return Throw.IfNull(responseClient)
.AsIChatClient(model)
.AsBuilder()
.ConfigureOptions(x => x.RawRepresentationFactory = _ => includeReasoningEncryptedContent
? new CreateResponseOptions() { StoredOutputEnabled = false, IncludedProperties = { IncludedResponseProperty.ReasoningEncryptedContent } }
: new CreateResponseOptions() { StoredOutputEnabled = false })
.ConfigureOptions(x =>
{
var previousFactory = x.RawRepresentationFactory;
x.RawRepresentationFactory = state =>
{
var responseOptions = previousFactory?.Invoke(state) as CreateResponseOptions ?? new CreateResponseOptions();
responseOptions.StoredOutputEnabled = false;
if (includeReasoningEncryptedContent &&
!responseOptions.IncludedProperties.Contains(IncludedResponseProperty.ReasoningEncryptedContent))
{
responseOptions.IncludedProperties.Add(IncludedResponseProperty.ReasoningEncryptedContent);
}
return responseOptions;
};
})
.Build();
}
}
@@ -74,9 +74,11 @@ internal static partial class AgentJsonUtilities
[JsonSerializable(typeof(TodoState))]
[JsonSerializable(typeof(TodoItem))]
[JsonSerializable(typeof(TodoItemInput))]
[JsonSerializable(typeof(TodoCompleteInput))]
[JsonSerializable(typeof(List<int>), TypeInfoPropertyName = "IntList")]
[JsonSerializable(typeof(List<TodoItem>), TypeInfoPropertyName = "TodoItemList")]
[JsonSerializable(typeof(List<TodoItemInput>), TypeInfoPropertyName = "TodoItemInputList")]
[JsonSerializable(typeof(List<TodoCompleteInput>), TypeInfoPropertyName = "TodoCompleteInputList")]
// AgentModeProvider types
[JsonSerializable(typeof(AgentModeState))]
@@ -95,12 +97,12 @@ internal static partial class AgentJsonUtilities
[JsonSerializable(typeof(FileListEntry))]
[JsonSerializable(typeof(List<FileListEntry>), TypeInfoPropertyName = "FileListEntryList")]
// SubAgentsProvider types
[JsonSerializable(typeof(SubAgentState))]
[JsonSerializable(typeof(SubAgentRuntimeState))]
[JsonSerializable(typeof(SubTaskInfo))]
[JsonSerializable(typeof(SubTaskStatus))]
[JsonSerializable(typeof(List<SubTaskInfo>), TypeInfoPropertyName = "SubTaskInfoList")]
// BackgroundAgentsProvider types
[JsonSerializable(typeof(BackgroundAgentState))]
[JsonSerializable(typeof(BackgroundAgentRuntimeState))]
[JsonSerializable(typeof(BackgroundTaskInfo))]
[JsonSerializable(typeof(BackgroundTaskStatus))]
[JsonSerializable(typeof(List<BackgroundTaskInfo>), TypeInfoPropertyName = "BackgroundTaskInfoList")]
[ExcludeFromCodeCoverage]
internal sealed partial class JsonContext : JsonSerializerContext;
@@ -45,20 +45,54 @@ public sealed class AgentModeProvider : AIContextProvider
"""
## Agent Mode
You can operate in different modes. Depending on the mode you are in, you will be required to follow different processes.
- 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 AgentMode_Get tool to check your current operating mode.
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.
{available_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."),
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."),
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 `AgentMode_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;
@@ -187,12 +221,15 @@ public sealed class AgentModeProvider : AIContextProvider
private string BuildInstructions(string currentMode)
{
// Build list of modes text:
var modesListBuilder = new StringBuilder();
foreach (var mode in this._modes)
{
modesListBuilder.AppendLine($"- \"{mode.Name}\": {mode.Description}");
modesListBuilder.AppendLine($"#### {mode.Name}");
modesListBuilder.AppendLine();
modesListBuilder.AppendLine(mode.Description.TrimEnd());
modesListBuilder.AppendLine();
}
var modesListText = modesListBuilder.ToString();
return new StringBuilder(this._instructions)
@@ -7,15 +7,15 @@ using System.Threading.Tasks;
namespace Microsoft.Agents.AI;
/// <summary>
/// Holds non-serializable runtime references for in-flight sub-tasks within a single parent session.
/// Holds non-serializable runtime references for in-flight background tasks within a single parent session.
/// </summary>
/// <remarks>
/// Properties are marked with <see cref="JsonIgnoreAttribute"/> because <see cref="Task{TResult}"/>
/// and <see cref="AgentSession"/> are not JSON-serializable. After deserialization (e.g., after a restart),
/// a fresh empty instance is created and any previously-running tasks are marked as
/// <see cref="SubTaskStatus.Lost"/> by <see cref="SubAgentsProvider"/>.
/// <see cref="BackgroundTaskStatus.Lost"/> by <see cref="BackgroundAgentsProvider"/>.
/// </remarks>
internal sealed class SubAgentRuntimeState
internal sealed class BackgroundAgentRuntimeState
{
/// <summary>
/// Gets the mapping of task IDs to their in-flight <see cref="Task{AgentResponse}"/> instances.
@@ -24,9 +24,9 @@ internal sealed class SubAgentRuntimeState
public Dictionary<int, Task<AgentResponse>> InFlightTasks { get; } = [];
/// <summary>
/// Gets the mapping of task IDs to their sub-agent <see cref="AgentSession"/> instances,
/// Gets the mapping of task IDs to their background agent <see cref="AgentSession"/> instances,
/// needed for <c>ContinueTask</c>.
/// </summary>
[JsonIgnore]
public Dictionary<int, AgentSession> SubTaskSessions { get; } = [];
public Dictionary<int, AgentSession> BackgroundTaskSessions { get; } = [];
}
@@ -8,21 +8,21 @@ using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI;
/// <summary>
/// Represents the serializable state of sub-tasks managed by the <see cref="SubAgentsProvider"/>,
/// Represents the serializable state of background tasks managed by the <see cref="BackgroundAgentsProvider"/>,
/// stored in the session's <see cref="AgentSessionStateBag"/>.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
internal sealed class SubAgentState
internal sealed class BackgroundAgentState
{
/// <summary>
/// Gets or sets the next ID to assign to a new sub-task.
/// Gets or sets the next ID to assign to a new background task.
/// </summary>
[JsonPropertyName("nextTaskId")]
public int NextTaskId { get; set; } = 1;
/// <summary>
/// Gets the list of sub-task metadata entries.
/// Gets the list of background task metadata entries.
/// </summary>
[JsonPropertyName("tasks")]
public List<SubTaskInfo> Tasks { get; set; } = [];
public List<BackgroundTaskInfo> Tasks { get; set; } = [];
}
@@ -15,56 +15,56 @@ using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// An <see cref="AIContextProvider"/> that enables an agent to delegate work to sub-agents asynchronously.
/// An <see cref="AIContextProvider"/> that enables an agent to delegate work to background agents asynchronously.
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="SubAgentsProvider"/> allows a parent agent to start sub-tasks on child agents,
/// wait for their completion, and retrieve results. Each sub-task runs in its own session and
/// The <see cref="BackgroundAgentsProvider"/> allows a parent agent to start background tasks on child agents,
/// wait for their completion, and retrieve results. Each background task runs in its own session and
/// executes concurrently.
/// </para>
/// <para>
/// This provider exposes the following tools to the agent:
/// <list type="bullet">
/// <item><description><c>SubAgents_StartTask</c> — Start a sub-task on a named agent with text input. Returns the task ID.</description></item>
/// <item><description><c>SubAgents_WaitForFirstCompletion</c> — Block until the first of the specified tasks completes. Returns the completed task's ID.</description></item>
/// <item><description><c>SubAgents_GetTaskResults</c> — Retrieve the text output of a completed sub-task.</description></item>
/// <item><description><c>SubAgents_GetAllTasks</c> — List all sub-tasks with their IDs, statuses, descriptions, and agent names.</description></item>
/// <item><description><c>SubAgents_ContinueTask</c> — Send follow-up input to a completed sub-task's session to resume work.</description></item>
/// <item><description><c>SubAgents_ClearCompletedTask</c> — Remove a completed sub-task and release its session to free memory.</description></item>
/// <item><description><c>BackgroundAgents_StartTask</c> — Start a background task on a named agent with text input. Returns the task ID.</description></item>
/// <item><description><c>BackgroundAgents_WaitForFirstCompletion</c> — Block until the first of the specified tasks completes. Returns the completed task's ID.</description></item>
/// <item><description><c>BackgroundAgents_GetTaskResults</c> — Retrieve the text output of a completed background task.</description></item>
/// <item><description><c>BackgroundAgents_GetAllTasks</c> — List all background tasks with their IDs, statuses, descriptions, and agent names.</description></item>
/// <item><description><c>BackgroundAgents_ContinueTask</c> — Send follow-up input to a completed background task's session to resume work.</description></item>
/// <item><description><c>BackgroundAgents_ClearCompletedTask</c> — Remove a completed background task and release its session to free memory.</description></item>
/// </list>
/// </para>
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class SubAgentsProvider : AIContextProvider
public sealed class BackgroundAgentsProvider : AIContextProvider
{
private const string DefaultInstructions =
"""
## SubAgents
You have access to sub-agents that can perform work on your behalf.
## BackgroundAgents
You have access to background agents that can perform work on your behalf.
- Use the `SubAgents_*` list of tools to start tasks on sub agents and check their results.
- Creating a sub task does not block, and sub-tasks run concurrently.
- Use the `BackgroundAgents_*` list of tools to start tasks on background agents and check their results.
- Creating a background task does not block, and background tasks run concurrently.
- Important: Always wait for outstanding tasks to finish before you finish processing.
- Important: After retrieving results from a completed task, clear it with SubAgents_ClearCompletedTask to free memory, unless you plan to continue it with SubAgents_ContinueTask.
- Important: After retrieving results from a completed task, clear it with BackgroundAgents_ClearCompletedTask to free memory, unless you plan to continue it with BackgroundAgents_ContinueTask.
{sub_agents}
{background_agents}
""";
private readonly Dictionary<string, AIAgent> _agents;
private readonly ProviderSessionState<SubAgentState> _sessionState;
private readonly ProviderSessionState<SubAgentRuntimeState> _runtimeSessionState;
private readonly ProviderSessionState<BackgroundAgentState> _sessionState;
private readonly ProviderSessionState<BackgroundAgentRuntimeState> _runtimeSessionState;
private readonly string _instructions;
private IReadOnlyList<string>? _stateKeys;
/// <summary>
/// Initializes a new instance of the <see cref="SubAgentsProvider"/> class.
/// Initializes a new instance of the <see cref="BackgroundAgentsProvider"/> class.
/// </summary>
/// <param name="agents">The collection of sub-agents available for delegation.</param>
/// <param name="agents">The collection of background agents available for delegation.</param>
/// <param name="options">Optional settings controlling the provider behavior.</param>
/// <exception cref="ArgumentNullException"><paramref name="agents"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">An agent has a null or empty name, or agent names are not unique.</exception>
public SubAgentsProvider(IEnumerable<AIAgent> agents, SubAgentsProviderOptions? options = null)
public BackgroundAgentsProvider(IEnumerable<AIAgent> agents, BackgroundAgentsProviderOptions? options = null)
{
_ = Throw.IfNull(agents);
@@ -74,15 +74,15 @@ public sealed class SubAgentsProvider : AIContextProvider
string agentListText = options?.AgentListBuilder is not null
? options.AgentListBuilder(this._agents)
: BuildDefaultAgentListText(this._agents);
this._instructions = baseInstructions.Replace("{sub_agents}", agentListText);
this._instructions = baseInstructions.Replace("{background_agents}", agentListText);
this._sessionState = new ProviderSessionState<SubAgentState>(
_ => new SubAgentState(),
this._sessionState = new ProviderSessionState<BackgroundAgentState>(
_ => new BackgroundAgentState(),
this.GetType().Name,
AgentJsonUtilities.DefaultOptions);
this._runtimeSessionState = new ProviderSessionState<SubAgentRuntimeState>(
_ => new SubAgentRuntimeState(),
this._runtimeSessionState = new ProviderSessionState<BackgroundAgentRuntimeState>(
_ => new BackgroundAgentRuntimeState(),
this.GetType().Name + "_Runtime",
AgentJsonUtilities.DefaultOptions);
}
@@ -93,8 +93,8 @@ public sealed class SubAgentsProvider : AIContextProvider
/// <inheritdoc />
protected override ValueTask<AIContext> ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
SubAgentState state = this._sessionState.GetOrInitializeState(context.Session);
SubAgentRuntimeState runtimeState = this._runtimeSessionState.GetOrInitializeState(context.Session);
BackgroundAgentState state = this._sessionState.GetOrInitializeState(context.Session);
BackgroundAgentRuntimeState runtimeState = this._runtimeSessionState.GetOrInitializeState(context.Session);
return new ValueTask<AIContext>(new AIContext
{
@@ -113,12 +113,12 @@ public sealed class SubAgentsProvider : AIContextProvider
{
if (string.IsNullOrWhiteSpace(agent.Name))
{
throw new ArgumentException("All sub-agents must have a non-empty Name.", nameof(agents));
throw new ArgumentException("All background agents must have a non-empty Name.", nameof(agents));
}
if (dict.ContainsKey(agent.Name))
{
throw new ArgumentException($"Duplicate sub-agent name: '{agent.Name}'. Agent names must be unique (case-insensitive).", nameof(agents));
throw new ArgumentException($"Duplicate background agent name: '{agent.Name}'. Agent names must be unique (case-insensitive).", nameof(agents));
}
dict[agent.Name] = agent;
@@ -126,19 +126,19 @@ public sealed class SubAgentsProvider : AIContextProvider
if (dict.Count == 0)
{
throw new ArgumentException("At least one sub-agent must be provided.", nameof(agents));
throw new ArgumentException("At least one background agent must be provided.", nameof(agents));
}
return dict;
}
/// <summary>
/// Builds the default text listing available sub-agents and their descriptions.
/// Builds the default text listing available background agents and their descriptions.
/// </summary>
private static string BuildDefaultAgentListText(IReadOnlyDictionary<string, AIAgent> agents)
{
var sb = new StringBuilder();
sb.AppendLine("Available sub-agents:");
sb.AppendLine("Available background agents:");
foreach (var kvp in agents)
{
sb.Append("- ").Append(kvp.Key);
@@ -156,12 +156,12 @@ public sealed class SubAgentsProvider : AIContextProvider
/// <summary>
/// Refreshes the status of in-flight tasks in the given state for the specified session.
/// </summary>
private void TryRefreshTaskState(SubAgentState state, SubAgentRuntimeState runtimeState, AgentSession? session)
private void TryRefreshTaskState(BackgroundAgentState state, BackgroundAgentRuntimeState runtimeState, AgentSession? session)
{
bool changed = false;
foreach (SubTaskInfo task in state.Tasks)
foreach (BackgroundTaskInfo task in state.Tasks)
{
if (task.Status != SubTaskStatus.Running)
if (task.Status != BackgroundTaskStatus.Running)
{
continue;
}
@@ -169,7 +169,7 @@ public sealed class SubAgentsProvider : AIContextProvider
if (!runtimeState.InFlightTasks.TryGetValue(task.Id, out Task<AgentResponse>? inFlight))
{
// In-flight reference lost (e.g., after restart/deserialization).
task.Status = SubTaskStatus.Lost;
task.Status = BackgroundTaskStatus.Lost;
changed = true;
continue;
}
@@ -188,32 +188,32 @@ public sealed class SubAgentsProvider : AIContextProvider
}
/// <summary>
/// Finalizes a task by extracting results from the completed Task and updating the SubTaskInfo.
/// Finalizes a task by extracting results from the completed Task and updating the BackgroundTaskInfo.
/// </summary>
private static void FinalizeTask(SubTaskInfo taskInfo, Task<AgentResponse> completedTask, SubAgentRuntimeState runtimeState)
private static void FinalizeTask(BackgroundTaskInfo taskInfo, Task<AgentResponse> completedTask, BackgroundAgentRuntimeState runtimeState)
{
if (completedTask.Status == TaskStatus.RanToCompletion)
{
taskInfo.Status = SubTaskStatus.Completed;
taskInfo.Status = BackgroundTaskStatus.Completed;
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits — task is already completed
taskInfo.ResultText = completedTask.Result.Text;
#pragma warning restore VSTHRD002
}
else if (completedTask.IsFaulted)
{
taskInfo.Status = SubTaskStatus.Failed;
taskInfo.Status = BackgroundTaskStatus.Failed;
taskInfo.ErrorText = completedTask.Exception?.InnerException?.Message ?? completedTask.Exception?.Message ?? "Unknown error";
}
else if (completedTask.IsCanceled)
{
taskInfo.Status = SubTaskStatus.Failed;
taskInfo.Status = BackgroundTaskStatus.Failed;
taskInfo.ErrorText = "Task was canceled.";
}
runtimeState.InFlightTasks.Remove(taskInfo.Id);
}
private AITool[] CreateTools(SubAgentState state, SubAgentRuntimeState runtimeState, AgentSession? session)
private AITool[] CreateTools(BackgroundAgentState state, BackgroundAgentRuntimeState runtimeState, AgentSession? session)
{
var serializerOptions = AgentJsonUtilities.DefaultOptions;
@@ -221,43 +221,43 @@ public sealed class SubAgentsProvider : AIContextProvider
[
AIFunctionFactory.Create(
async (
[Description("The name of the sub agent to delegate the task to.")] string agentName,
[Description("The request to pass to the sub agent.")] string input,
[Description("The name of the background agent to delegate the task to.")] string agentName,
[Description("The request to pass to the background agent.")] string input,
[Description("A description of the task used to identify the task later.")] string description) =>
{
if (!this._agents.TryGetValue(agentName, out AIAgent? agent))
{
return $"Error: No sub-agent found with name '{agentName}'. Available agents: {string.Join(", ", this._agents.Keys)}";
return $"Error: No background agent found with name '{agentName}'. Available agents: {string.Join(", ", this._agents.Keys)}";
}
int taskId = state.NextTaskId++;
var taskInfo = new SubTaskInfo
var taskInfo = new BackgroundTaskInfo
{
Id = taskId,
AgentName = agentName,
Description = description,
Status = SubTaskStatus.Running,
Status = BackgroundTaskStatus.Running,
};
state.Tasks.Add(taskInfo);
// Create a dedicated session for this sub-task so it can be continued later.
// Create a dedicated session for this background task so it can be continued later.
AgentSession subSession = await agent.CreateSessionAsync().ConfigureAwait(false);
// Wrap in Task.Run to fork the ExecutionContext. AIAgent.RunAsync is a non-async
// method that synchronously sets the static AsyncLocal CurrentRunContext. Without
// this isolation, the sub-agent's RunAsync would overwrite the outer (calling)
// this isolation, the background agent's RunAsync would overwrite the outer (calling)
// agent's CurrentRunContext, corrupting all subsequent tool invocations in the
// same FICC batch.
runtimeState.InFlightTasks[taskId] = Task.Run(() => agent.RunAsync(input, subSession));
runtimeState.SubTaskSessions[taskId] = subSession;
runtimeState.BackgroundTaskSessions[taskId] = subSession;
this._sessionState.SaveState(session, state);
return $"Sub-task {taskId} started on agent '{agentName}'.";
return $"Background task {taskId} started on agent '{agentName}'.";
},
new AIFunctionFactoryOptions
{
Name = "SubAgents_StartTask",
Description = "Start a sub-task on a named sub-agent. Returns a confirmation message containing the task ID.",
Name = "BackgroundAgents_StartTask",
Description = "Start a background task on a named background agent. Returns a confirmation message containing the task ID.",
SerializerOptions = serializerOptions,
}),
@@ -287,7 +287,7 @@ public sealed class SubAgentsProvider : AIContextProvider
this._sessionState.SaveState(session, state);
// Check if any of the requested IDs are already complete.
SubTaskInfo? alreadyComplete = state.Tasks.FirstOrDefault(t => taskIds.Contains(t.Id) && t.Status != SubTaskStatus.Running);
BackgroundTaskInfo? alreadyComplete = state.Tasks.FirstOrDefault(t => taskIds.Contains(t.Id) && t.Status != BackgroundTaskStatus.Running);
if (alreadyComplete is not null)
{
return $"Task {alreadyComplete.Id} is not running; current status: {alreadyComplete.Status}.";
@@ -303,7 +303,7 @@ public sealed class SubAgentsProvider : AIContextProvider
var completedEntry = waitableTasks.First(t => t.Task == completedTask);
// Finalize the completed task.
SubTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == completedEntry.Id);
BackgroundTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == completedEntry.Id);
if (taskInfo is not null)
{
FinalizeTask(taskInfo, completedEntry.Task, runtimeState);
@@ -314,8 +314,8 @@ public sealed class SubAgentsProvider : AIContextProvider
},
new AIFunctionFactoryOptions
{
Name = "SubAgents_WaitForFirstCompletion",
Description = "Block until the first of the specified sub-tasks completes. Provide one or more task IDs. Returns a status message containing the ID of the task that completed first.",
Name = "BackgroundAgents_WaitForFirstCompletion",
Description = "Block until the first of the specified background tasks completes. Provide one or more task IDs. Returns a status message containing the ID of the task that completed first.",
SerializerOptions = serializerOptions,
}),
@@ -324,7 +324,7 @@ public sealed class SubAgentsProvider : AIContextProvider
{
this.TryRefreshTaskState(state, runtimeState, session);
SubTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == taskId);
BackgroundTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == taskId);
if (taskInfo is null)
{
return $"Error: No task found with ID {taskId}.";
@@ -332,17 +332,17 @@ public sealed class SubAgentsProvider : AIContextProvider
return taskInfo.Status switch
{
SubTaskStatus.Completed => taskInfo.ResultText ?? "(no output)",
SubTaskStatus.Failed => $"Task failed: {taskInfo.ErrorText ?? "Unknown error"}",
SubTaskStatus.Lost => "Task state was lost (reference unavailable).",
SubTaskStatus.Running => $"Task {taskId} is still running.",
BackgroundTaskStatus.Completed => taskInfo.ResultText ?? "(no output)",
BackgroundTaskStatus.Failed => $"Task failed: {taskInfo.ErrorText ?? "Unknown error"}",
BackgroundTaskStatus.Lost => "Task state was lost (reference unavailable).",
BackgroundTaskStatus.Running => $"Task {taskId} is still running.",
_ => $"Task {taskId} has status: {taskInfo.Status}.",
};
},
new AIFunctionFactoryOptions
{
Name = "SubAgents_GetTaskResults",
Description = "Get the text output of a sub-task by its ID. Returns the result text if complete, or status information if still running or failed.",
Name = "BackgroundAgents_GetTaskResults",
Description = "Get the text output of a background task by its ID. Returns the result text if complete, or status information if still running or failed.",
SerializerOptions = serializerOptions,
}),
@@ -358,7 +358,7 @@ public sealed class SubAgentsProvider : AIContextProvider
var sb = new StringBuilder();
sb.AppendLine("Tasks:");
foreach (SubTaskInfo task in state.Tasks)
foreach (BackgroundTaskInfo task in state.Tasks)
{
sb.Append("- Task ").Append(task.Id).Append(" [").Append(task.Status).Append("] (").Append(task.AgentName).Append("): ").AppendLine(task.Description);
}
@@ -367,8 +367,8 @@ public sealed class SubAgentsProvider : AIContextProvider
},
new AIFunctionFactoryOptions
{
Name = "SubAgents_GetAllTasks",
Description = "List all sub-tasks with their IDs, statuses, agent names, and descriptions.",
Name = "BackgroundAgents_GetAllTasks",
Description = "List all background tasks with their IDs, statuses, agent names, and descriptions.",
SerializerOptions = serializerOptions,
}),
@@ -377,18 +377,18 @@ public sealed class SubAgentsProvider : AIContextProvider
{
this.TryRefreshTaskState(state, runtimeState, session);
SubTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == taskId);
BackgroundTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == taskId);
if (taskInfo is null)
{
return $"Error: No task found with ID {taskId}.";
}
if (taskInfo.Status == SubTaskStatus.Lost)
if (taskInfo.Status == BackgroundTaskStatus.Lost)
{
return $"Error: Task {taskId} cannot be continued because its session was lost (e.g., after a session restore). Start a new task instead.";
}
if (taskInfo.Status == SubTaskStatus.Running)
if (taskInfo.Status == BackgroundTaskStatus.Running)
{
return $"Error: Task {taskId} is still running. Wait for it to complete before continuing.";
}
@@ -398,17 +398,17 @@ public sealed class SubAgentsProvider : AIContextProvider
return $"Error: Agent '{taskInfo.AgentName}' is no longer available.";
}
if (!runtimeState.SubTaskSessions.TryGetValue(taskId, out AgentSession? subSession))
if (!runtimeState.BackgroundTaskSessions.TryGetValue(taskId, out AgentSession? subSession))
{
return $"Error: Session for task {taskId} is no longer available.";
}
// Reset task state and start a new run on the existing session.
taskInfo.Status = SubTaskStatus.Running;
taskInfo.Status = BackgroundTaskStatus.Running;
taskInfo.ResultText = null;
taskInfo.ErrorText = null;
// Wrap in Task.Run to isolate the ExecutionContext (see StartSubTask comment).
// Wrap in Task.Run to isolate the ExecutionContext (see StartBackgroundTask comment).
runtimeState.InFlightTasks[taskId] = Task.Run(() => agent.RunAsync(text, subSession));
this._sessionState.SaveState(session, state);
@@ -416,8 +416,8 @@ public sealed class SubAgentsProvider : AIContextProvider
},
new AIFunctionFactoryOptions
{
Name = "SubAgents_ContinueTask",
Description = "Send follow-up input to a completed or failed sub-task to resume its work. The sub-task's session is preserved, so the agent retains conversational context.",
Name = "BackgroundAgents_ContinueTask",
Description = "Send follow-up input to a completed or failed background task to resume its work. The background task's session is preserved, so the agent retains conversational context.",
SerializerOptions = serializerOptions,
}),
@@ -426,13 +426,13 @@ public sealed class SubAgentsProvider : AIContextProvider
{
this.TryRefreshTaskState(state, runtimeState, session);
SubTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == taskId);
BackgroundTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == taskId);
if (taskInfo is null)
{
return $"Error: No task found with ID {taskId}.";
}
if (taskInfo.Status == SubTaskStatus.Running)
if (taskInfo.Status == BackgroundTaskStatus.Running)
{
return $"Error: Task {taskId} is still running. Wait for it to complete before clearing.";
}
@@ -442,15 +442,15 @@ public sealed class SubAgentsProvider : AIContextProvider
// Clean up runtime references.
runtimeState.InFlightTasks.Remove(taskId);
runtimeState.SubTaskSessions.Remove(taskId);
runtimeState.BackgroundTaskSessions.Remove(taskId);
this._sessionState.SaveState(session, state);
return $"Task {taskId} cleared.";
},
new AIFunctionFactoryOptions
{
Name = "SubAgents_ClearCompletedTask",
Description = "Remove a completed or failed sub-task and release its session to free memory. Use this after retrieving results when you no longer need to continue the task.",
Name = "BackgroundAgents_ClearCompletedTask",
Description = "Remove a completed or failed background task and release its session to free memory. Use this after retrieving results when you no longer need to continue the task.",
SerializerOptions = serializerOptions,
}),
];
@@ -8,21 +8,21 @@ using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI;
/// <summary>
/// Options controlling the behavior of <see cref="SubAgentsProvider"/>.
/// Options controlling the behavior of <see cref="BackgroundAgentsProvider"/>.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class SubAgentsProviderOptions
public sealed class BackgroundAgentsProviderOptions
{
/// <summary>
/// Gets or sets custom instructions provided to the agent for using the sub-agent tools.
/// Gets or sets custom instructions provided to the agent for using the background agent tools.
/// </summary>
/// <remarks>
/// Use the <c>{sub_agents}</c> placeholder to allow the provider to inject
/// the formatted list of available sub agents.
/// Use the <c>{background_agents}</c> placeholder to allow the provider to inject
/// the formatted list of available background agents.
/// </remarks>
/// <value>
/// When <see langword="null"/> (the default), the provider uses built-in instructions
/// that guide the agent on how to use the sub-agent tools.
/// that guide the agent on how to use the background agent tools.
/// The agent list is always appended after the instructions regardless of this setting.
/// </value>
public string? Instructions { get; set; }
@@ -33,7 +33,7 @@ public sealed class SubAgentsProviderOptions
/// <value>
/// When <see langword="null"/> (the default), the provider generates a standard list of agent names and descriptions.
/// When set, this function receives the dictionary of available agents (keyed by name) and should return
/// a formatted string describing the available sub-agents.
/// a formatted string describing the available background agents.
/// </value>
public Func<IReadOnlyDictionary<string, AIAgent>, string>? AgentListBuilder { get; set; }
}
@@ -7,43 +7,43 @@ using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI;
/// <summary>
/// Represents the metadata and result of a sub-task managed by the <see cref="SubAgentsProvider"/>.
/// Represents the metadata and result of a background task managed by the <see cref="BackgroundAgentsProvider"/>.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class SubTaskInfo
public sealed class BackgroundTaskInfo
{
/// <summary>
/// Gets or sets the unique identifier for this sub-task.
/// Gets or sets the unique identifier for this background task.
/// </summary>
[JsonPropertyName("id")]
public int Id { get; set; }
/// <summary>
/// Gets or sets the name of the agent that is executing this sub-task.
/// Gets or sets the name of the agent that is executing this background task.
/// </summary>
[JsonPropertyName("agentName")]
public string AgentName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a description of what this sub-task is doing.
/// Gets or sets a description of what this background task is doing.
/// </summary>
[JsonPropertyName("description")]
public string Description { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the current status of this sub-task.
/// Gets or sets the current status of this background task.
/// </summary>
[JsonPropertyName("status")]
public SubTaskStatus Status { get; set; }
public BackgroundTaskStatus Status { get; set; }
/// <summary>
/// Gets or sets the text result of the sub-task, populated when the task completes successfully.
/// Gets or sets the text result of the background task, populated when the task completes successfully.
/// </summary>
[JsonPropertyName("resultText")]
public string? ResultText { get; set; }
/// <summary>
/// Gets or sets the error message if the sub-task failed.
/// Gets or sets the error message if the background task failed.
/// </summary>
[JsonPropertyName("errorText")]
public string? ErrorText { get; set; }
@@ -6,28 +6,28 @@ using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI;
/// <summary>
/// Represents the status of a sub-task managed by the <see cref="SubAgentsProvider"/>.
/// Represents the status of a background task managed by the <see cref="BackgroundAgentsProvider"/>.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public enum SubTaskStatus
public enum BackgroundTaskStatus
{
/// <summary>
/// The sub-task is currently running.
/// The background task is currently running.
/// </summary>
Running,
/// <summary>
/// The sub-task completed successfully.
/// The background task completed successfully.
/// </summary>
Completed,
/// <summary>
/// The sub-task failed with an error.
/// The background task failed with an error.
/// </summary>
Failed,
/// <summary>
/// The sub-task's in-flight reference was lost (e.g., after a restart),
/// The background task's in-flight reference was lost (e.g., after a restart),
/// and its final state cannot be determined.
/// </summary>
Lost,
@@ -55,7 +55,7 @@ public sealed class FileMemoryProvider : AIContextProvider, IDisposable
- Use descriptive file names (e.g., "projectarchitecture.md", "userpreferences.md").
- Include a description when saving a file to help with future discovery.
- Before starting new tasks, use FileMemory_ListFiles and FileMemory_SearchFiles to check for relevant existing memories.
- Before starting new tasks, use FileMemory_ListFiles and FileMemory_SearchFiles to check for relevant existing memories to avoid duplicate work.
- Keep memories up-to-date by overwriting files when information changes.
- When you receive large amounts of data (e.g., downloaded web pages, API responses, research results),
save them to files if they will be required later, so that they are not lost when older context is compacted or truncated.
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI;
/// <summary>
/// Represents the input for completing a single todo item via the <see cref="TodoProvider"/>.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
internal sealed class TodoCompleteInput
{
/// <summary>
/// Gets or sets the ID of the todo item to mark as complete.
/// </summary>
[JsonPropertyName("id")]
public int Id { get; set; }
/// <summary>
/// Gets or sets the reason describing how or why the item was completed.
/// </summary>
[JsonPropertyName("reason")]
public string Reason { get; set; } = string.Empty;
}
@@ -48,13 +48,13 @@ public sealed class TodoProvider : AIContextProvider, IDisposable
You have access to a todo list for tracking work items.
While planning, make sure that you break down complex tasks into manageable todo items and add them to the list.
Ask questions from the user where clarification is needed to create effective todos.
If the user provides feedback on your plan, adjust your todos accordingly by adding new items or removing irrelevant ones.
If the user provides feedback on your plan, adjust your todos accordingly by adding new items or removing irrelevant/old ones.
During execution, use the todo list to keep track of what needs to be done, mark items as complete when finished, and remove any items that are no longer needed.
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.
When a user changes the topic or changes their mind, ensure that you update the todo list accordingly by removing irrelevant/old items or adding new ones as needed.
Use these tools to manage your tasks:
- 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_Complete to mark items as done when finished (supports one or many at once). Include a reason describing how the items were completed.
- 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).
@@ -235,14 +235,14 @@ public sealed class TodoProvider : AIContextProvider, IDisposable
}),
AIFunctionFactory.Create(
async (List<int> ids) =>
async (List<TodoCompleteInput> items) =>
{
SemaphoreSlim sessionLock = this.GetSessionLock(session);
await sessionLock.WaitAsync().ConfigureAwait(false);
try
{
TodoState state = this._sessionState.GetOrInitializeState(session);
var idSet = new HashSet<int>(ids);
var idSet = new HashSet<int>(items.Select(i => i.Id));
int completed = 0;
foreach (TodoItem item in state.Items)
{
@@ -268,7 +268,7 @@ public sealed class TodoProvider : AIContextProvider, IDisposable
new AIFunctionFactoryOptions
{
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.",
Description = "Mark one or more todo items as complete. Each entry has an ID and a reason describing how/why the item was completed. Returns the number of items that were found and marked complete.",
SerializerOptions = serializerOptions,
}),