mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'main' into test-it-bump-aaip210b2
This commit is contained in:
@@ -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)
|
||||
|
||||
+5
-5
@@ -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; } = [];
|
||||
}
|
||||
+5
-5
@@ -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; } = [];
|
||||
}
|
||||
+81
-81
@@ -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,
|
||||
}),
|
||||
];
|
||||
+7
-7
@@ -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; }
|
||||
}
|
||||
+9
-9
@@ -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
-6
@@ -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,
|
||||
}),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user