diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index e1b7c9a71b..d494768139 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -122,7 +122,7 @@ - + diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/ToolFormatters/SubAgentToolFormatter.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/ToolFormatters/BackgroundAgentToolFormatter.cs similarity index 78% rename from dotnet/samples/02-agents/Harness/Harness_Shared_Console/ToolFormatters/SubAgentToolFormatter.cs rename to dotnet/samples/02-agents/Harness/Harness_Shared_Console/ToolFormatters/BackgroundAgentToolFormatter.cs index 915491d354..4907abbd89 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/ToolFormatters/SubAgentToolFormatter.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/ToolFormatters/BackgroundAgentToolFormatter.cs @@ -6,26 +6,26 @@ namespace Harness.Shared.Console.ToolFormatters; /// -/// Formats SubAgents_* tool calls with human-readable details +/// Formats BackgroundAgents_* tool calls with human-readable details /// for task start, continue, wait, and result retrieval operations. /// -public sealed class SubAgentToolFormatter : ToolCallFormatter +public sealed class BackgroundAgentToolFormatter : ToolCallFormatter { /// - public override bool CanFormat(FunctionCallContent call) => call.Name.StartsWith("SubAgents_", StringComparison.Ordinal); + public override bool CanFormat(FunctionCallContent call) => call.Name.StartsWith("BackgroundAgents_", StringComparison.Ordinal); /// public override string? FormatDetail(FunctionCallContent call) => call.Name switch { - "SubAgents_StartTask" => FormatStartSubTask(call), - "SubAgents_WaitForFirstCompletion" => FormatIdList(call, "taskIds", "Wait for"), - "SubAgents_GetTaskResults" => FormatSingleId(call, "taskId"), - "SubAgents_ContinueTask" => FormatContinueTask(call), - "SubAgents_ClearCompletedTask" => FormatSingleId(call, "taskId"), + "BackgroundAgents_StartTask" => FormatStartBackgroundTask(call), + "BackgroundAgents_WaitForFirstCompletion" => FormatIdList(call, "taskIds", "Wait for"), + "BackgroundAgents_GetTaskResults" => FormatSingleId(call, "taskId"), + "BackgroundAgents_ContinueTask" => FormatContinueTask(call), + "BackgroundAgents_ClearCompletedTask" => FormatSingleId(call, "taskId"), _ => null, }; - private static string? FormatStartSubTask(FunctionCallContent call) + private static string? FormatStartBackgroundTask(FunctionCallContent call) { string? agentName = GetStringArgumentValue(call, "agentName"); string? description = GetStringArgumentValue(call, "description"); diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/ToolFormatters/TodoToolFormatter.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/ToolFormatters/TodoToolFormatter.cs index 98e041ede7..b907c4afb1 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/ToolFormatters/TodoToolFormatter.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/ToolFormatters/TodoToolFormatter.cs @@ -19,7 +19,7 @@ public sealed class TodoToolFormatter : ToolCallFormatter public override string? FormatDetail(FunctionCallContent call) => call.Name switch { "TodoList_Add" => FormatAddTodos(call), - "TodoList_Complete" => FormatIdList(call, "ids", "Complete"), + "TodoList_Complete" => FormatCompleteTodos(call), "TodoList_Remove" => FormatIdList(call, "ids", "Remove"), _ => null, }; @@ -64,6 +64,50 @@ public sealed class TodoToolFormatter : ToolCallFormatter return sb.ToString(); } + private static string? FormatCompleteTodos(FunctionCallContent call) + { + if (call.Arguments?.TryGetValue("items", out object? itemsObj) != true || itemsObj is null) + { + return null; + } + + var entries = new List<(int Id, string? Reason)>(); + + if (itemsObj is JsonElement jsonArray && jsonArray.ValueKind == JsonValueKind.Array) + { + foreach (JsonElement item in jsonArray.EnumerateArray()) + { + if (!item.TryGetProperty("id", out JsonElement idElement) || !idElement.TryGetInt32(out int id)) + { + continue; + } + + string? reason = item.TryGetProperty("reason", out JsonElement reasonElement) + ? reasonElement.GetString() + : null; + entries.Add((id, reason)); + } + } + + if (entries.Count == 0) + { + return null; + } + + var sb = new StringBuilder(); + for (int i = 0; i < entries.Count; i++) + { + string connector = i < entries.Count - 1 ? "├─" : "└─"; + sb.Append($"\n {connector} Complete #{entries[i].Id}"); + if (!string.IsNullOrEmpty(entries[i].Reason)) + { + sb.Append($" — {Truncate(entries[i].Reason!, 80)}"); + } + } + + return sb.ToString(); + } + private static string? FormatIdList(FunctionCallContent call, string paramName, string verb) { List? ids = GetIntListArgumentValue(call, paramName); diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/ToolFormatters/ToolCallFormatter.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/ToolFormatters/ToolCallFormatter.cs index f8a131dd74..e8edfa5177 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/ToolFormatters/ToolCallFormatter.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/ToolFormatters/ToolCallFormatter.cs @@ -56,7 +56,7 @@ public static List BuildDefaultToolFormatters() [ new TodoToolFormatter(), new ModeToolFormatter(), - new SubAgentToolFormatter(), + new BackgroundAgentToolFormatter(), new FileMemoryToolFormatter(), new WebSearchToolFormatter(), new FallbackToolFormatter(), diff --git a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithSubAgents/Harness_Step02_Research_WithSubAgents.csproj b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Harness_Step02_Research_WithBackgroundAgents.csproj similarity index 100% rename from dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithSubAgents/Harness_Step02_Research_WithSubAgents.csproj rename to dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Harness_Step02_Research_WithBackgroundAgents.csproj diff --git a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithSubAgents/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs similarity index 81% rename from dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithSubAgents/Program.cs rename to dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs index bb4c50e0d7..c88c958247 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithSubAgents/Program.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. -// This sample demonstrates how to use the SubAgentsProvider to delegate work to sub-agents. +// This sample demonstrates how to use the BackgroundAgentsProvider to delegate work to background agents. // A parent agent is given a list of stock tickers and instructed to find the closing price -// for each ticker on December 31, 2025. It delegates the web searches to a sub-agent. +// for each ticker on December 31, 2025. It delegates the web searches to a background agent. // The HarnessAgent provides built-in WebSearch (HostedWebSearchTool) so no manual web search -// tool configuration is needed on the sub-agent. +// tool configuration is needed on the background agent. // // Special commands: // /exit — End the session. @@ -26,7 +26,7 @@ const int MaxContextWindowTokens = 1_050_000; const int MaxOutputTokens = 128_000; -// --- Sub-agent: Web Search Agent --- +// --- Background agent: Web Search Agent --- // This agent uses the HarnessAgent's built-in HostedWebSearchTool to search the web. // Features not needed by this sub-agent are disabled. AIAgent webSearchAgent = @@ -55,26 +55,26 @@ }); // --- Parent agent: Stock Price Researcher --- -// This agent orchestrates the sub-agent to look up stock prices in parallel. +// This agent orchestrates the background agent to look up stock prices in parallel. var parentInstructions = """ - You are a stock price research assistant. You have access to a web search sub-agent that can look up information on the web. + You are a stock price research assistant. You have access to a web search background agent that can look up information on the web. When given a list of stock tickers, your job is to find the closing price for each ticker on December 31, 2025. ## Workflow - 1. For each ticker, start a sub-task on the WebSearchAgent asking it to find the closing price on December 31, 2025. - - Start all sub-tasks before waiting for any of them to complete, so they run concurrently. - 2. Wait for all sub-tasks to complete. - 3. Retrieve the results from each sub-task. + 1. For each ticker, start a background task on the WebSearchAgent asking it to find the closing price on December 31, 2025. + - Start all background tasks before waiting for any of them to complete, so they run concurrently. + 2. Wait for all background tasks to complete. + 3. Retrieve the results from each background task. 4. Present a summary table with the ticker symbol and closing price for each stock. 5. Clear all completed tasks to free memory. ## Important - - Always delegate web searches to the WebSearchAgent sub-agent. Do not try to answer from memory. - - If a sub-task fails or returns unclear results, continue the task with a more specific query. + - Always delegate web searches to the WebSearchAgent background agent. Do not try to answer from memory. + - If a background task fails or returns unclear results, continue the task with a more specific query. - Present results in a clean markdown table format. """; @@ -94,7 +94,7 @@ 5. Clear all completed tasks to free memory. .AsHarnessAgent(MaxContextWindowTokens, MaxOutputTokens, new HarnessAgentOptions { Name = "StockPriceResearcher", - Description = "An agent that researches stock prices using sub-agents.", + Description = "An agent that researches stock prices using background agents.", DisableTodoProvider = true, DisableAgentModeProvider = true, DisableFileMemory = true, // If enabled, this would allow the agent to store memories as files in a directory associated with the current session @@ -103,7 +103,7 @@ 5. Clear all completed tasks to free memory. DisableWebSearch = true, AIContextProviders = [ - new SubAgentsProvider([webSearchAgent]), + new BackgroundAgentsProvider([webSearchAgent]), ], ChatOptions = new ChatOptions { diff --git a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithSubAgents/README.md b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/README.md similarity index 53% rename from dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithSubAgents/README.md rename to dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/README.md index a52ebe0372..c04f68d13c 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithSubAgents/README.md +++ b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/README.md @@ -1,24 +1,24 @@ -# Harness Step 02 — SubAgents (Stock Price Research) +# Harness Step 02 — BackgroundAgents (Stock Price Research) -This sample demonstrates how to use the **SubAgentsProvider** to delegate work from a parent agent to sub-agents. Both agents use `HarnessAgent` for pre-configured function invocation, per-service-call persistence, and context-window compaction. +This sample demonstrates how to use the **BackgroundAgentsProvider** to delegate work from a parent agent to background agents. Both agents use `HarnessAgent` for pre-configured function invocation, per-service-call persistence, and context-window compaction. ## What It Does -A parent agent receives a list of stock tickers and uses a web-search sub-agent to find the closing price for each ticker on December 31, 2025. The sub-tasks run concurrently, and results are presented in a summary table. +A parent agent receives a list of stock tickers and uses a web-search background agent to find the closing price for each ticker on December 31, 2025. The background tasks run concurrently, and results are presented in a summary table. ### Architecture ``` -┌─────────────────────────────────┐ -│ StockPriceResearcher │ -│ (Parent Agent) │ -│ │ -│ SubAgentsProvider │ -│ ├─ SubAgents_StartTask │ -│ ├─ SubAgents_WaitFor... │ -│ ├─ SubAgents_GetTaskResults │ -│ └─ ... │ -└────────────┬────────────────────┘ +┌────────────────────────────────────────┐ +│ StockPriceResearcher │ +│ (Parent Agent) │ +│ │ +│ BackgroundAgentsProvider │ +│ ├─ BackgroundAgents_StartTask │ +│ ├─ BackgroundAgents_WaitFor... │ +│ ├─ BackgroundAgents_GetTaskResults │ +│ └─ ... │ +└────────────┬───────────────────────────┘ │ delegates to ▼ ┌─────────────────────────────────┐ @@ -40,7 +40,7 @@ A parent agent receives a list of stock tickers and uses a web-search sub-agent ## Running the Sample ```bash -cd dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithSubAgents +cd dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents dotnet run ``` @@ -50,4 +50,4 @@ When prompted, enter a list of stock tickers such as: BAC, MSFT, BA ``` -The parent agent will delegate each ticker lookup to the web search sub-agent concurrently and present the results in a table. +The parent agent will delegate each ticker lookup to the web search background agent concurrently and present the results in a table. diff --git a/dotnet/samples/02-agents/Harness/README.md b/dotnet/samples/02-agents/Harness/README.md index d868323648..16fad9ac62 100644 --- a/dotnet/samples/02-agents/Harness/README.md +++ b/dotnet/samples/02-agents/Harness/README.md @@ -7,5 +7,5 @@ Samples demonstrating the [Harness AIContextProviders](../../../src/Microsoft.Ag | Sample | Description | | --- | --- | | [Harness_Step01_Research](./Harness_Step01_Research/README.md) | Using a ChatClientAgent with TodoProvider and AgentModeProvider for research, showcasing planning mode and todo management | -| [Harness_Step02_Research_WithSubAgents](./Harness_Step02_Research_WithSubAgents/README.md) | Using SubAgentsProvider to delegate stock price lookups to a web-search sub-agent concurrently | +| [Harness_Step02_Research_WithBackgroundAgents](./Harness_Step02_Research_WithBackgroundAgents/README.md) | Using BackgroundAgentsProvider to delegate stock price lookups to a web-search background agent concurrently | | [Harness_Step03_DataProcessing](./Harness_Step03_DataProcessing/README.md) | Using FileAccessProvider to give an agent access to CSV data files for reading, analysis, and output generation | diff --git a/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs index f3bc543f03..e28144f45c 100644 --- a/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs @@ -74,9 +74,11 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(TodoState))] [JsonSerializable(typeof(TodoItem))] [JsonSerializable(typeof(TodoItemInput))] + [JsonSerializable(typeof(TodoCompleteInput))] [JsonSerializable(typeof(List), TypeInfoPropertyName = "IntList")] [JsonSerializable(typeof(List), TypeInfoPropertyName = "TodoItemList")] [JsonSerializable(typeof(List), TypeInfoPropertyName = "TodoItemInputList")] + [JsonSerializable(typeof(List), TypeInfoPropertyName = "TodoCompleteInputList")] // AgentModeProvider types [JsonSerializable(typeof(AgentModeState))] @@ -95,12 +97,12 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(FileListEntry))] [JsonSerializable(typeof(List), TypeInfoPropertyName = "FileListEntryList")] - // SubAgentsProvider types - [JsonSerializable(typeof(SubAgentState))] - [JsonSerializable(typeof(SubAgentRuntimeState))] - [JsonSerializable(typeof(SubTaskInfo))] - [JsonSerializable(typeof(SubTaskStatus))] - [JsonSerializable(typeof(List), TypeInfoPropertyName = "SubTaskInfoList")] + // BackgroundAgentsProvider types + [JsonSerializable(typeof(BackgroundAgentState))] + [JsonSerializable(typeof(BackgroundAgentRuntimeState))] + [JsonSerializable(typeof(BackgroundTaskInfo))] + [JsonSerializable(typeof(BackgroundTaskStatus))] + [JsonSerializable(typeof(List), TypeInfoPropertyName = "BackgroundTaskInfoList")] [ExcludeFromCodeCoverage] internal sealed partial class JsonContext : JsonSerializerContext; diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubAgentRuntimeState.cs b/dotnet/src/Microsoft.Agents.AI/Harness/BackgroundAgents/BackgroundAgentRuntimeState.cs similarity index 67% rename from dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubAgentRuntimeState.cs rename to dotnet/src/Microsoft.Agents.AI/Harness/BackgroundAgents/BackgroundAgentRuntimeState.cs index 7b3096dba8..f8e2f3accc 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubAgentRuntimeState.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/BackgroundAgents/BackgroundAgentRuntimeState.cs @@ -7,15 +7,15 @@ namespace Microsoft.Agents.AI; /// -/// 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. /// /// /// Properties are marked with because /// and 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 -/// by . +/// by . /// -internal sealed class SubAgentRuntimeState +internal sealed class BackgroundAgentRuntimeState { /// /// Gets the mapping of task IDs to their in-flight instances. @@ -24,9 +24,9 @@ internal sealed class SubAgentRuntimeState public Dictionary> InFlightTasks { get; } = []; /// - /// Gets the mapping of task IDs to their sub-agent instances, + /// Gets the mapping of task IDs to their background agent instances, /// needed for ContinueTask. /// [JsonIgnore] - public Dictionary SubTaskSessions { get; } = []; + public Dictionary BackgroundTaskSessions { get; } = []; } diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubAgentState.cs b/dotnet/src/Microsoft.Agents.AI/Harness/BackgroundAgents/BackgroundAgentState.cs similarity index 62% rename from dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubAgentState.cs rename to dotnet/src/Microsoft.Agents.AI/Harness/BackgroundAgents/BackgroundAgentState.cs index 4e086fb910..223ebd98c5 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubAgentState.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/BackgroundAgents/BackgroundAgentState.cs @@ -8,21 +8,21 @@ namespace Microsoft.Agents.AI; /// -/// Represents the serializable state of sub-tasks managed by the , +/// Represents the serializable state of background tasks managed by the , /// stored in the session's . /// [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] -internal sealed class SubAgentState +internal sealed class BackgroundAgentState { /// - /// 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. /// [JsonPropertyName("nextTaskId")] public int NextTaskId { get; set; } = 1; /// - /// Gets the list of sub-task metadata entries. + /// Gets the list of background task metadata entries. /// [JsonPropertyName("tasks")] - public List Tasks { get; set; } = []; + public List Tasks { get; set; } = []; } diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubAgentsProvider.cs b/dotnet/src/Microsoft.Agents.AI/Harness/BackgroundAgents/BackgroundAgentsProvider.cs similarity index 63% rename from dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubAgentsProvider.cs rename to dotnet/src/Microsoft.Agents.AI/Harness/BackgroundAgents/BackgroundAgentsProvider.cs index 254e082523..3e347f9983 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubAgentsProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/BackgroundAgents/BackgroundAgentsProvider.cs @@ -15,56 +15,56 @@ namespace Microsoft.Agents.AI; /// -/// An that enables an agent to delegate work to sub-agents asynchronously. +/// An that enables an agent to delegate work to background agents asynchronously. /// /// /// -/// The 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 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. /// /// /// This provider exposes the following tools to the agent: /// -/// SubAgents_StartTask — Start a sub-task on a named agent with text input. Returns the task ID. -/// SubAgents_WaitForFirstCompletion — Block until the first of the specified tasks completes. Returns the completed task's ID. -/// SubAgents_GetTaskResults — Retrieve the text output of a completed sub-task. -/// SubAgents_GetAllTasks — List all sub-tasks with their IDs, statuses, descriptions, and agent names. -/// SubAgents_ContinueTask — Send follow-up input to a completed sub-task's session to resume work. -/// SubAgents_ClearCompletedTask — Remove a completed sub-task and release its session to free memory. +/// BackgroundAgents_StartTask — Start a background task on a named agent with text input. Returns the task ID. +/// BackgroundAgents_WaitForFirstCompletion — Block until the first of the specified tasks completes. Returns the completed task's ID. +/// BackgroundAgents_GetTaskResults — Retrieve the text output of a completed background task. +/// BackgroundAgents_GetAllTasks — List all background tasks with their IDs, statuses, descriptions, and agent names. +/// BackgroundAgents_ContinueTask — Send follow-up input to a completed background task's session to resume work. +/// BackgroundAgents_ClearCompletedTask — Remove a completed background task and release its session to free memory. /// /// /// [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 _agents; - private readonly ProviderSessionState _sessionState; - private readonly ProviderSessionState _runtimeSessionState; + private readonly ProviderSessionState _sessionState; + private readonly ProviderSessionState _runtimeSessionState; private readonly string _instructions; private IReadOnlyList? _stateKeys; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The collection of sub-agents available for delegation. + /// The collection of background agents available for delegation. /// Optional settings controlling the provider behavior. /// is . /// An agent has a null or empty name, or agent names are not unique. - public SubAgentsProvider(IEnumerable agents, SubAgentsProviderOptions? options = null) + public BackgroundAgentsProvider(IEnumerable agents, BackgroundAgentsProviderOptions? options = null) { _ = Throw.IfNull(agents); @@ -74,15 +74,15 @@ public SubAgentsProvider(IEnumerable agents, SubAgentsProviderOptions? 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( - _ => new SubAgentState(), + this._sessionState = new ProviderSessionState( + _ => new BackgroundAgentState(), this.GetType().Name, AgentJsonUtilities.DefaultOptions); - this._runtimeSessionState = new ProviderSessionState( - _ => new SubAgentRuntimeState(), + this._runtimeSessionState = new ProviderSessionState( + _ => new BackgroundAgentRuntimeState(), this.GetType().Name + "_Runtime", AgentJsonUtilities.DefaultOptions); } @@ -93,8 +93,8 @@ public SubAgentsProvider(IEnumerable agents, SubAgentsProviderOptions? /// protected override ValueTask 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(new AIContext { @@ -113,12 +113,12 @@ private static Dictionary ValidateAndBuildAgentDictionary(IEnum { 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 @@ private static Dictionary ValidateAndBuildAgentDictionary(IEnum 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; } /// - /// Builds the default text listing available sub-agents and their descriptions. + /// Builds the default text listing available background agents and their descriptions. /// private static string BuildDefaultAgentListText(IReadOnlyDictionary 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 @@ private static string BuildDefaultAgentListText(IReadOnlyDictionary /// Refreshes the status of in-flight tasks in the given state for the specified session. /// - 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 @@ private void TryRefreshTaskState(SubAgentState state, SubAgentRuntimeState runti if (!runtimeState.InFlightTasks.TryGetValue(task.Id, out Task? 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 @@ private void TryRefreshTaskState(SubAgentState state, SubAgentRuntimeState runti } /// - /// 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. /// - private static void FinalizeTask(SubTaskInfo taskInfo, Task completedTask, SubAgentRuntimeState runtimeState) + private static void FinalizeTask(BackgroundTaskInfo taskInfo, Task 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 @@ private AITool[] CreateTools(SubAgentState state, SubAgentRuntimeState runtimeSt [ 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 @@ private AITool[] CreateTools(SubAgentState state, SubAgentRuntimeState runtimeSt 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 @@ private AITool[] CreateTools(SubAgentState state, SubAgentRuntimeState runtimeSt 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 @@ private AITool[] CreateTools(SubAgentState state, SubAgentRuntimeState runtimeSt }, 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 @@ private AITool[] CreateTools(SubAgentState state, SubAgentRuntimeState runtimeSt { 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 @@ private AITool[] CreateTools(SubAgentState state, SubAgentRuntimeState runtimeSt 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 @@ private AITool[] CreateTools(SubAgentState state, SubAgentRuntimeState runtimeSt 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 @@ private AITool[] CreateTools(SubAgentState state, SubAgentRuntimeState runtimeSt }, 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 @@ private AITool[] CreateTools(SubAgentState state, SubAgentRuntimeState runtimeSt { 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 @@ private AITool[] CreateTools(SubAgentState state, SubAgentRuntimeState runtimeSt 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 @@ private AITool[] CreateTools(SubAgentState state, SubAgentRuntimeState runtimeSt }, 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 @@ private AITool[] CreateTools(SubAgentState state, SubAgentRuntimeState runtimeSt { 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 @@ private AITool[] CreateTools(SubAgentState state, SubAgentRuntimeState runtimeSt // 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, }), ]; diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubAgentsProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI/Harness/BackgroundAgents/BackgroundAgentsProviderOptions.cs similarity index 72% rename from dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubAgentsProviderOptions.cs rename to dotnet/src/Microsoft.Agents.AI/Harness/BackgroundAgents/BackgroundAgentsProviderOptions.cs index 27a4c1530d..83d8cd959f 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubAgentsProviderOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/BackgroundAgents/BackgroundAgentsProviderOptions.cs @@ -8,21 +8,21 @@ namespace Microsoft.Agents.AI; /// -/// Options controlling the behavior of . +/// Options controlling the behavior of . /// [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] -public sealed class SubAgentsProviderOptions +public sealed class BackgroundAgentsProviderOptions { /// - /// 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. /// /// - /// Use the {sub_agents} placeholder to allow the provider to inject - /// the formatted list of available sub agents. + /// Use the {background_agents} placeholder to allow the provider to inject + /// the formatted list of available background agents. /// /// /// When (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. /// public string? Instructions { get; set; } @@ -33,7 +33,7 @@ public sealed class SubAgentsProviderOptions /// /// When (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. /// public Func, string>? AgentListBuilder { get; set; } } diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubTaskInfo.cs b/dotnet/src/Microsoft.Agents.AI/Harness/BackgroundAgents/BackgroundTaskInfo.cs similarity index 62% rename from dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubTaskInfo.cs rename to dotnet/src/Microsoft.Agents.AI/Harness/BackgroundAgents/BackgroundTaskInfo.cs index 91b6084ece..98f36c7e9d 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubTaskInfo.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/BackgroundAgents/BackgroundTaskInfo.cs @@ -7,43 +7,43 @@ namespace Microsoft.Agents.AI; /// -/// Represents the metadata and result of a sub-task managed by the . +/// Represents the metadata and result of a background task managed by the . /// [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] -public sealed class SubTaskInfo +public sealed class BackgroundTaskInfo { /// - /// Gets or sets the unique identifier for this sub-task. + /// Gets or sets the unique identifier for this background task. /// [JsonPropertyName("id")] public int Id { get; set; } /// - /// 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. /// [JsonPropertyName("agentName")] public string AgentName { get; set; } = string.Empty; /// - /// Gets or sets a description of what this sub-task is doing. + /// Gets or sets a description of what this background task is doing. /// [JsonPropertyName("description")] public string Description { get; set; } = string.Empty; /// - /// Gets or sets the current status of this sub-task. + /// Gets or sets the current status of this background task. /// [JsonPropertyName("status")] - public SubTaskStatus Status { get; set; } + public BackgroundTaskStatus Status { get; set; } /// - /// 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. /// [JsonPropertyName("resultText")] public string? ResultText { get; set; } /// - /// Gets or sets the error message if the sub-task failed. + /// Gets or sets the error message if the background task failed. /// [JsonPropertyName("errorText")] public string? ErrorText { get; set; } diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubTaskStatus.cs b/dotnet/src/Microsoft.Agents.AI/Harness/BackgroundAgents/BackgroundTaskStatus.cs similarity index 57% rename from dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubTaskStatus.cs rename to dotnet/src/Microsoft.Agents.AI/Harness/BackgroundAgents/BackgroundTaskStatus.cs index f5e66f6f72..b3dfeee671 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubTaskStatus.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/BackgroundAgents/BackgroundTaskStatus.cs @@ -6,28 +6,28 @@ namespace Microsoft.Agents.AI; /// -/// Represents the status of a sub-task managed by the . +/// Represents the status of a background task managed by the . /// [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] -public enum SubTaskStatus +public enum BackgroundTaskStatus { /// - /// The sub-task is currently running. + /// The background task is currently running. /// Running, /// - /// The sub-task completed successfully. + /// The background task completed successfully. /// Completed, /// - /// The sub-task failed with an error. + /// The background task failed with an error. /// Failed, /// - /// 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. /// Lost, diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoCompleteInput.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoCompleteInput.cs new file mode 100644 index 0000000000..355c494337 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoCompleteInput.cs @@ -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; + +/// +/// Represents the input for completing a single todo item via the . +/// +[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] +internal sealed class TodoCompleteInput +{ + /// + /// Gets or sets the ID of the todo item to mark as complete. + /// + [JsonPropertyName("id")] + public int Id { get; set; } + + /// + /// Gets or sets the reason describing how or why the item was completed. + /// + [JsonPropertyName("reason")] + public string Reason { get; set; } = string.Empty; +} diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProvider.cs b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProvider.cs index bb39e71c20..429c6b5646 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/Todo/TodoProvider.cs @@ -54,7 +54,7 @@ Ask questions from the user where clarification is needed to create effective to 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 @@ private AITool[] CreateTools(AgentSession? session) }), AIFunctionFactory.Create( - async (List ids) => + async (List items) => { SemaphoreSlim sessionLock = this.GetSessionLock(session); await sessionLock.WaitAsync().ConfigureAwait(false); try { TodoState state = this._sessionState.GetOrInitializeState(session); - var idSet = new HashSet(ids); + var idSet = new HashSet(items.Select(i => i.Id)); int completed = 0; foreach (TodoItem item in state.Items) { @@ -268,7 +268,7 @@ private AITool[] CreateTools(AgentSession? session) 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, }), diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/SubAgents/SubAgentsProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/BackgroundAgents/BackgroundAgentsProviderTests.cs similarity index 77% rename from dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/SubAgents/SubAgentsProviderTests.cs rename to dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/BackgroundAgents/BackgroundAgentsProviderTests.cs index 4f76d610b3..b8a6051a4c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/SubAgents/SubAgentsProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/BackgroundAgents/BackgroundAgentsProviderTests.cs @@ -13,9 +13,9 @@ namespace Microsoft.Agents.AI.UnitTests; /// -/// Unit tests for the class. +/// Unit tests for the class. /// -public class SubAgentsProviderTests +public class BackgroundAgentsProviderTests { #region Constructor Tests @@ -26,7 +26,7 @@ public class SubAgentsProviderTests public void Constructor_NullAgents_Throws() { // Act & Assert - Assert.Throws(() => new SubAgentsProvider(null!)); + Assert.Throws(() => new BackgroundAgentsProvider(null!)); } /// @@ -36,7 +36,7 @@ public void Constructor_NullAgents_Throws() public void Constructor_EmptyAgents_Throws() { // Act & Assert - Assert.Throws(() => new SubAgentsProvider(Array.Empty())); + Assert.Throws(() => new BackgroundAgentsProvider(Array.Empty())); } /// @@ -49,7 +49,7 @@ public void Constructor_AgentWithNullName_Throws() var agent = CreateMockAgent(null!, "desc"); // Act & Assert - Assert.Throws(() => new SubAgentsProvider(new[] { agent })); + Assert.Throws(() => new BackgroundAgentsProvider(new[] { agent })); } /// @@ -62,7 +62,7 @@ public void Constructor_AgentWithEmptyName_Throws() var agent = CreateMockAgent("", "desc"); // Act & Assert - Assert.Throws(() => new SubAgentsProvider(new[] { agent })); + Assert.Throws(() => new BackgroundAgentsProvider(new[] { agent })); } /// @@ -76,7 +76,7 @@ public void Constructor_DuplicateNames_Throws() var agent2 = CreateMockAgent("research", "Agent 2"); // Act & Assert - Assert.Throws(() => new SubAgentsProvider(new[] { agent1, agent2 })); + Assert.Throws(() => new BackgroundAgentsProvider(new[] { agent1, agent2 })); } /// @@ -90,7 +90,7 @@ public void Constructor_ValidAgents_Succeeds() var agent2 = CreateMockAgent("Writer", "Writer agent"); // Act - var provider = new SubAgentsProvider(new[] { agent1, agent2 }); + var provider = new BackgroundAgentsProvider(new[] { agent1, agent2 }); // Assert Assert.NotNull(provider); @@ -108,7 +108,7 @@ public async Task ProvideAIContextAsync_ReturnsToolsAndInstructionsAsync() { // Arrange var agent = CreateMockAgent("Research", "Research agent"); - var provider = new SubAgentsProvider(new[] { agent }); + var provider = new BackgroundAgentsProvider(new[] { agent }); var context = CreateInvokingContext(); // Act @@ -129,7 +129,7 @@ public async Task ProvideAIContextAsync_InstructionsIncludeAgentInfoAsync() // Arrange var agent1 = CreateMockAgent("Research", "Performs research"); var agent2 = CreateMockAgent("Writer", "Writes content"); - var provider = new SubAgentsProvider(new[] { agent1, agent2 }); + var provider = new BackgroundAgentsProvider(new[] { agent1, agent2 }); var context = CreateInvokingContext(); // Act @@ -144,22 +144,22 @@ public async Task ProvideAIContextAsync_InstructionsIncludeAgentInfoAsync() #endregion - #region StartSubTask Tests + #region StartBackgroundTask Tests /// - /// Verify that StartSubTask returns a task ID. + /// Verify that StartBackgroundTask returns a task ID. /// [Fact] - public async Task StartSubTask_ReturnsTaskIdAsync() + public async Task StartBackgroundTask_ReturnsTaskIdAsync() { // Arrange var tcs = new TaskCompletionSource(); var agent = CreateMockAgentWithRunResult("Research", tcs.Task); var (tools, _) = await CreateToolsWithProviderAsync(agent); - AIFunction startSubTask = GetTool(tools, "SubAgents_StartTask"); + AIFunction startBackgroundTask = GetTool(tools, "BackgroundAgents_StartTask"); // Act - object? result = await startSubTask.InvokeAsync(new AIFunctionArguments + object? result = await startBackgroundTask.InvokeAsync(new AIFunctionArguments { ["agentName"] = "Research", ["input"] = "Find information about AI", @@ -175,18 +175,18 @@ public async Task StartSubTask_ReturnsTaskIdAsync() } /// - /// Verify that StartSubTask with invalid agent name returns an error. + /// Verify that StartBackgroundTask with invalid agent name returns an error. /// [Fact] - public async Task StartSubTask_InvalidAgentName_ReturnsErrorAsync() + public async Task StartBackgroundTask_InvalidAgentName_ReturnsErrorAsync() { // Arrange var agent = CreateMockAgent("Research", "Research agent"); var (tools, _) = await CreateToolsWithProviderAsync(agent); - AIFunction startSubTask = GetTool(tools, "SubAgents_StartTask"); + AIFunction startBackgroundTask = GetTool(tools, "BackgroundAgents_StartTask"); // Act - object? result = await startSubTask.InvokeAsync(new AIFunctionArguments + object? result = await startBackgroundTask.InvokeAsync(new AIFunctionArguments { ["agentName"] = "NonExistent", ["input"] = "Some input", @@ -200,10 +200,10 @@ public async Task StartSubTask_InvalidAgentName_ReturnsErrorAsync() } /// - /// Verify that StartSubTask assigns sequential IDs. + /// Verify that StartBackgroundTask assigns sequential IDs. /// [Fact] - public async Task StartSubTask_AssignsSequentialIdsAsync() + public async Task StartBackgroundTask_AssignsSequentialIdsAsync() { // Arrange var tcs1 = new TaskCompletionSource(); @@ -215,16 +215,16 @@ public async Task StartSubTask_AssignsSequentialIdsAsync() return callCount == 1 ? tcs1.Task : tcs2.Task; }); var (tools, _) = await CreateToolsWithProviderAsync(agent); - AIFunction startSubTask = GetTool(tools, "SubAgents_StartTask"); + AIFunction startBackgroundTask = GetTool(tools, "BackgroundAgents_StartTask"); // Act - object? result1 = await startSubTask.InvokeAsync(new AIFunctionArguments + object? result1 = await startBackgroundTask.InvokeAsync(new AIFunctionArguments { ["agentName"] = "Research", ["input"] = "Task 1", ["description"] = "First task", }); - object? result2 = await startSubTask.InvokeAsync(new AIFunctionArguments + object? result2 = await startBackgroundTask.InvokeAsync(new AIFunctionArguments { ["agentName"] = "Research", ["input"] = "Task 2", @@ -253,11 +253,11 @@ public async Task WaitForFirstCompletion_ReturnsCompletedTaskIdAsync() var tcs = new TaskCompletionSource(); var agent = CreateMockAgentWithRunResult("Research", tcs.Task); var (tools, _) = await CreateToolsWithProviderAsync(agent); - AIFunction startSubTask = GetTool(tools, "SubAgents_StartTask"); - AIFunction waitForFirst = GetTool(tools, "SubAgents_WaitForFirstCompletion"); + AIFunction startBackgroundTask = GetTool(tools, "BackgroundAgents_StartTask"); + AIFunction waitForFirst = GetTool(tools, "BackgroundAgents_WaitForFirstCompletion"); // Start one task - await startSubTask.InvokeAsync(new AIFunctionArguments + await startBackgroundTask.InvokeAsync(new AIFunctionArguments { ["agentName"] = "Research", ["input"] = "Task 1", @@ -288,7 +288,7 @@ public async Task WaitForFirstCompletion_EmptyList_ReturnsErrorAsync() // Arrange var agent = CreateMockAgent("Research", "Research agent"); var (tools, _) = await CreateToolsWithProviderAsync(agent); - AIFunction waitForFirst = GetTool(tools, "SubAgents_WaitForFirstCompletion"); + AIFunction waitForFirst = GetTool(tools, "BackgroundAgents_WaitForFirstCompletion"); // Act object? result = await waitForFirst.InvokeAsync(new AIFunctionArguments @@ -302,24 +302,24 @@ public async Task WaitForFirstCompletion_EmptyList_ReturnsErrorAsync() #endregion - #region GetSubTaskResults Tests + #region GetBackgroundTaskResults Tests /// - /// Verify that GetSubTaskResults returns the result text of a completed task. + /// Verify that GetBackgroundTaskResults returns the result text of a completed task. /// [Fact] - public async Task GetSubTaskResults_CompletedTask_ReturnsResultTextAsync() + public async Task GetBackgroundTaskResults_CompletedTask_ReturnsResultTextAsync() { // Arrange var tcs = new TaskCompletionSource(); var agent = CreateMockAgentWithRunResult("Research", tcs.Task); var (tools, _) = await CreateToolsWithProviderAsync(agent); - AIFunction startSubTask = GetTool(tools, "SubAgents_StartTask"); - AIFunction waitForFirst = GetTool(tools, "SubAgents_WaitForFirstCompletion"); - AIFunction getResults = GetTool(tools, "SubAgents_GetTaskResults"); + AIFunction startBackgroundTask = GetTool(tools, "BackgroundAgents_StartTask"); + AIFunction waitForFirst = GetTool(tools, "BackgroundAgents_WaitForFirstCompletion"); + AIFunction getResults = GetTool(tools, "BackgroundAgents_GetTaskResults"); // Start a task - await startSubTask.InvokeAsync(new AIFunctionArguments + await startBackgroundTask.InvokeAsync(new AIFunctionArguments { ["agentName"] = "Research", ["input"] = "Research AI", @@ -346,20 +346,20 @@ await waitForFirst.InvokeAsync(new AIFunctionArguments } /// - /// Verify that GetSubTaskResults for a still-running task returns status info. + /// Verify that GetBackgroundTaskResults for a still-running task returns status info. /// [Fact] - public async Task GetSubTaskResults_RunningTask_ReturnsStatusAsync() + public async Task GetBackgroundTaskResults_RunningTask_ReturnsStatusAsync() { // Arrange var tcs = new TaskCompletionSource(); var agent = CreateMockAgentWithRunResult("Research", tcs.Task); var (tools, _) = await CreateToolsWithProviderAsync(agent); - AIFunction startSubTask = GetTool(tools, "SubAgents_StartTask"); - AIFunction getResults = GetTool(tools, "SubAgents_GetTaskResults"); + AIFunction startBackgroundTask = GetTool(tools, "BackgroundAgents_StartTask"); + AIFunction getResults = GetTool(tools, "BackgroundAgents_GetTaskResults"); // Start a task (don't complete it) - await startSubTask.InvokeAsync(new AIFunctionArguments + await startBackgroundTask.InvokeAsync(new AIFunctionArguments { ["agentName"] = "Research", ["input"] = "Research AI", @@ -379,15 +379,15 @@ await startSubTask.InvokeAsync(new AIFunctionArguments } /// - /// Verify that GetSubTaskResults for a nonexistent task returns an error. + /// Verify that GetBackgroundTaskResults for a nonexistent task returns an error. /// [Fact] - public async Task GetSubTaskResults_NonexistentTask_ReturnsErrorAsync() + public async Task GetBackgroundTaskResults_NonexistentTask_ReturnsErrorAsync() { // Arrange var agent = CreateMockAgent("Research", "Research agent"); var (tools, _) = await CreateToolsWithProviderAsync(agent); - AIFunction getResults = GetTool(tools, "SubAgents_GetTaskResults"); + AIFunction getResults = GetTool(tools, "BackgroundAgents_GetTaskResults"); // Act object? result = await getResults.InvokeAsync(new AIFunctionArguments @@ -400,21 +400,21 @@ public async Task GetSubTaskResults_NonexistentTask_ReturnsErrorAsync() } /// - /// Verify that GetSubTaskResults for a failed task returns the error. + /// Verify that GetBackgroundTaskResults for a failed task returns the error. /// [Fact] - public async Task GetSubTaskResults_FailedTask_ReturnsErrorTextAsync() + public async Task GetBackgroundTaskResults_FailedTask_ReturnsErrorTextAsync() { // Arrange var tcs = new TaskCompletionSource(); var agent = CreateMockAgentWithRunResult("Research", tcs.Task); var (tools, _) = await CreateToolsWithProviderAsync(agent); - AIFunction startSubTask = GetTool(tools, "SubAgents_StartTask"); - AIFunction waitForFirst = GetTool(tools, "SubAgents_WaitForFirstCompletion"); - AIFunction getResults = GetTool(tools, "SubAgents_GetTaskResults"); + AIFunction startBackgroundTask = GetTool(tools, "BackgroundAgents_StartTask"); + AIFunction waitForFirst = GetTool(tools, "BackgroundAgents_WaitForFirstCompletion"); + AIFunction getResults = GetTool(tools, "BackgroundAgents_GetTaskResults"); // Start a task - await startSubTask.InvokeAsync(new AIFunctionArguments + await startBackgroundTask.InvokeAsync(new AIFunctionArguments { ["agentName"] = "Research", ["input"] = "Research AI", @@ -456,11 +456,11 @@ public async Task GetAllTasks_ReturnsRunningTasksAsync() var tcs = new TaskCompletionSource(); var agent = CreateMockAgentWithRunResult("Research", tcs.Task); var (tools, _) = await CreateToolsWithProviderAsync(agent); - AIFunction startSubTask = GetTool(tools, "SubAgents_StartTask"); - AIFunction getAllTasks = GetTool(tools, "SubAgents_GetAllTasks"); + AIFunction startBackgroundTask = GetTool(tools, "BackgroundAgents_StartTask"); + AIFunction getAllTasks = GetTool(tools, "BackgroundAgents_GetAllTasks"); // Start a task - await startSubTask.InvokeAsync(new AIFunctionArguments + await startBackgroundTask.InvokeAsync(new AIFunctionArguments { ["agentName"] = "Research", ["input"] = "Research AI", @@ -490,12 +490,12 @@ public async Task GetAllTasks_ShowsCompletedTasksAsync() var tcs = new TaskCompletionSource(); var agent = CreateMockAgentWithRunResult("Research", tcs.Task); var (tools, _) = await CreateToolsWithProviderAsync(agent); - AIFunction startSubTask = GetTool(tools, "SubAgents_StartTask"); - AIFunction waitForFirst = GetTool(tools, "SubAgents_WaitForFirstCompletion"); - AIFunction getAllTasks = GetTool(tools, "SubAgents_GetAllTasks"); + AIFunction startBackgroundTask = GetTool(tools, "BackgroundAgents_StartTask"); + AIFunction waitForFirst = GetTool(tools, "BackgroundAgents_WaitForFirstCompletion"); + AIFunction getAllTasks = GetTool(tools, "BackgroundAgents_GetAllTasks"); // Start and complete a task - await startSubTask.InvokeAsync(new AIFunctionArguments + await startBackgroundTask.InvokeAsync(new AIFunctionArguments { ["agentName"] = "Research", ["input"] = "Research AI", @@ -525,7 +525,7 @@ public async Task GetAllTasks_NoTasks_ReturnsNoneAsync() // Arrange var agent = CreateMockAgent("Research", "Research agent"); var (tools, _) = await CreateToolsWithProviderAsync(agent); - AIFunction getAllTasks = GetTool(tools, "SubAgents_GetAllTasks"); + AIFunction getAllTasks = GetTool(tools, "BackgroundAgents_GetAllTasks"); // Act object? result = await getAllTasks.InvokeAsync(new AIFunctionArguments()); @@ -554,13 +554,13 @@ public async Task ContinueTask_CompletedTask_ResumesAsync() return callCount == 1 ? tcs1.Task : tcs2.Task; }); var (tools, _) = await CreateToolsWithProviderAsync(agent); - AIFunction startSubTask = GetTool(tools, "SubAgents_StartTask"); - AIFunction waitForFirst = GetTool(tools, "SubAgents_WaitForFirstCompletion"); - AIFunction continueTask = GetTool(tools, "SubAgents_ContinueTask"); - AIFunction getResults = GetTool(tools, "SubAgents_GetTaskResults"); + AIFunction startBackgroundTask = GetTool(tools, "BackgroundAgents_StartTask"); + AIFunction waitForFirst = GetTool(tools, "BackgroundAgents_WaitForFirstCompletion"); + AIFunction continueTask = GetTool(tools, "BackgroundAgents_ContinueTask"); + AIFunction getResults = GetTool(tools, "BackgroundAgents_GetTaskResults"); // Start and complete a task - await startSubTask.InvokeAsync(new AIFunctionArguments + await startBackgroundTask.InvokeAsync(new AIFunctionArguments { ["agentName"] = "Research", ["input"] = "Research AI", @@ -606,11 +606,11 @@ public async Task ContinueTask_RunningTask_ReturnsErrorAsync() var tcs = new TaskCompletionSource(); var agent = CreateMockAgentWithRunResult("Research", tcs.Task); var (tools, _) = await CreateToolsWithProviderAsync(agent); - AIFunction startSubTask = GetTool(tools, "SubAgents_StartTask"); - AIFunction continueTask = GetTool(tools, "SubAgents_ContinueTask"); + AIFunction startBackgroundTask = GetTool(tools, "BackgroundAgents_StartTask"); + AIFunction continueTask = GetTool(tools, "BackgroundAgents_ContinueTask"); // Start a task (don't complete it) - await startSubTask.InvokeAsync(new AIFunctionArguments + await startBackgroundTask.InvokeAsync(new AIFunctionArguments { ["agentName"] = "Research", ["input"] = "Research AI", @@ -639,7 +639,7 @@ public async Task ContinueTask_NonexistentTask_ReturnsErrorAsync() // Arrange var agent = CreateMockAgent("Research", "Research agent"); var (tools, _) = await CreateToolsWithProviderAsync(agent); - AIFunction continueTask = GetTool(tools, "SubAgents_ContinueTask"); + AIFunction continueTask = GetTool(tools, "BackgroundAgents_ContinueTask"); // Act object? result = await continueTask.InvokeAsync(new AIFunctionArguments @@ -666,13 +666,13 @@ public async Task ClearCompletedTask_RemovesTerminalTaskAsync() var tcs = new TaskCompletionSource(); var agent = CreateMockAgentWithRunResult("Research", tcs.Task); var (tools, _) = await CreateToolsWithProviderAsync(agent); - AIFunction startSubTask = GetTool(tools, "SubAgents_StartTask"); - AIFunction waitForFirst = GetTool(tools, "SubAgents_WaitForFirstCompletion"); - AIFunction clearTask = GetTool(tools, "SubAgents_ClearCompletedTask"); - AIFunction getResults = GetTool(tools, "SubAgents_GetTaskResults"); + AIFunction startBackgroundTask = GetTool(tools, "BackgroundAgents_StartTask"); + AIFunction waitForFirst = GetTool(tools, "BackgroundAgents_WaitForFirstCompletion"); + AIFunction clearTask = GetTool(tools, "BackgroundAgents_ClearCompletedTask"); + AIFunction getResults = GetTool(tools, "BackgroundAgents_GetTaskResults"); // Start and complete a task - await startSubTask.InvokeAsync(new AIFunctionArguments + await startBackgroundTask.InvokeAsync(new AIFunctionArguments { ["agentName"] = "Research", ["input"] = "Research AI", @@ -711,11 +711,11 @@ public async Task ClearCompletedTask_RunningTask_ReturnsErrorAsync() var tcs = new TaskCompletionSource(); var agent = CreateMockAgentWithRunResult("Research", tcs.Task); var (tools, _) = await CreateToolsWithProviderAsync(agent); - AIFunction startSubTask = GetTool(tools, "SubAgents_StartTask"); - AIFunction clearTask = GetTool(tools, "SubAgents_ClearCompletedTask"); + AIFunction startBackgroundTask = GetTool(tools, "BackgroundAgents_StartTask"); + AIFunction clearTask = GetTool(tools, "BackgroundAgents_ClearCompletedTask"); // Start a task (don't complete it) - await startSubTask.InvokeAsync(new AIFunctionArguments + await startBackgroundTask.InvokeAsync(new AIFunctionArguments { ["agentName"] = "Research", ["input"] = "Research AI", @@ -743,7 +743,7 @@ public async Task ClearCompletedTask_NonexistentTask_ReturnsErrorAsync() // Arrange var agent = CreateMockAgent("Research", "Research agent"); var (tools, _) = await CreateToolsWithProviderAsync(agent); - AIFunction clearTask = GetTool(tools, "SubAgents_ClearCompletedTask"); + AIFunction clearTask = GetTool(tools, "BackgroundAgents_ClearCompletedTask"); // Act object? result = await clearTask.InvokeAsync(new AIFunctionArguments @@ -767,7 +767,7 @@ public void StateKeys_ReturnsExpectedKeys() { // Arrange var agent = CreateMockAgent("Research", "Research agent"); - var provider = new SubAgentsProvider(new[] { agent }); + var provider = new BackgroundAgentsProvider(new[] { agent }); // Act var keys = provider.StateKeys; @@ -782,23 +782,23 @@ public void StateKeys_ReturnsExpectedKeys() #region CurrentRunContext Isolation Tests /// - /// Verify that StartSubTask does not corrupt CurrentRunContext of the calling agent. + /// Verify that StartBackgroundTask does not corrupt CurrentRunContext of the calling agent. /// Because RunAsync is a non-async method that synchronously sets the static AsyncLocal - /// CurrentRunContext, the provider must isolate the sub-agent call to prevent overwriting + /// CurrentRunContext, the provider must isolate the background agent call to prevent overwriting /// the outer agent's context. /// [Fact] - public async Task StartSubTask_DoesNotCorruptCurrentRunContextAsync() + public async Task StartBackgroundTask_DoesNotCorruptCurrentRunContextAsync() { // Arrange var tcs = new TaskCompletionSource(); var agent = CreateMockAgentWithRunResult("Research", tcs.Task); var (tools, _) = await CreateToolsWithProviderAsync(agent); - var startTool = GetTool(tools, "SubAgents_StartTask"); + var startTool = GetTool(tools, "BackgroundAgents_StartTask"); AgentRunContext? contextBefore = AIAgent.CurrentRunContext; - // Act — invoke StartSubTask; this calls agent.RunAsync internally. + // Act — invoke StartBackgroundTask; this calls agent.RunAsync internally. var args = new AIFunctionArguments(new Dictionary { ["agentName"] = "Research", @@ -826,16 +826,16 @@ public async Task CustomInstructions_OverridesDefaultInstructionsAsync() { // Arrange var agent = CreateMockAgent("Research", "Research agent"); - const string CustomInstructions = "These are custom sub-agent instructions.\n{sub_agents}"; - var options = new SubAgentsProviderOptions { Instructions = CustomInstructions }; - var provider = new SubAgentsProvider(new[] { agent }, options); + const string CustomInstructions = "These are custom background agent instructions.\n{background_agents}"; + var options = new BackgroundAgentsProviderOptions { Instructions = CustomInstructions }; + var provider = new BackgroundAgentsProvider(new[] { agent }, options); var context = CreateInvokingContext(); // Act AIContext result = await provider.InvokingAsync(context); // Assert — custom instructions replace default, agent list is injected via {sub_agents} placeholder - Assert.Contains("These are custom sub-agent instructions.", result.Instructions); + Assert.Contains("These are custom background agent instructions.", result.Instructions); Assert.Contains("Research", result.Instructions); } @@ -847,15 +847,15 @@ public async Task DefaultInstructions_ContainsToolReferenceAndAgentListAsync() { // Arrange var agent = CreateMockAgent("Research", "Research agent"); - var provider = new SubAgentsProvider(new[] { agent }); + var provider = new BackgroundAgentsProvider(new[] { agent }); var context = CreateInvokingContext(); // Act AIContext result = await provider.InvokingAsync(context); // Assert — instructions contain tool usage guidance and agent list - Assert.Contains("SubAgents_*", result.Instructions); - Assert.Contains("SubAgents_ClearCompletedTask", result.Instructions); + Assert.Contains("BackgroundAgents_*", result.Instructions); + Assert.Contains("BackgroundAgents_ClearCompletedTask", result.Instructions); Assert.Contains("Research", result.Instructions); Assert.Contains("Research agent", result.Instructions); } @@ -868,11 +868,11 @@ public async Task CustomAgentListBuilder_UsedForAgentListAsync() { // Arrange var agent = CreateMockAgent("Research", "Research agent"); - var options = new SubAgentsProviderOptions + var options = new BackgroundAgentsProviderOptions { AgentListBuilder = agents => $"Custom list: {string.Join(", ", agents.Keys)}", }; - var provider = new SubAgentsProvider(new[] { agent }, options); + var provider = new BackgroundAgentsProvider(new[] { agent }, options); var context = CreateInvokingContext(); // Act @@ -880,7 +880,7 @@ public async Task CustomAgentListBuilder_UsedForAgentListAsync() // Assert — custom agent list builder output is in instructions Assert.Contains("Custom list: Research", result.Instructions); - Assert.DoesNotContain("Available sub-agents:", result.Instructions); + Assert.DoesNotContain("Available background agents:", result.Instructions); } #endregion @@ -935,9 +935,9 @@ private static AIAgent CreateMockAgentWithCallback(string name, Func Tools, SubAgentsProvider Provider)> CreateToolsWithProviderAsync(AIAgent agent) + private static async Task<(IEnumerable Tools, BackgroundAgentsProvider Provider)> CreateToolsWithProviderAsync(AIAgent agent) { - var provider = new SubAgentsProvider(new[] { agent }); + var provider = new BackgroundAgentsProvider(new[] { agent }); var context = CreateInvokingContext(); AIContext result = await provider.InvokingAsync(context); diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/Todo/TodoProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/Todo/TodoProviderTests.cs index d2724478d3..c733d8efe7 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/Todo/TodoProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/Todo/TodoProviderTests.cs @@ -116,7 +116,7 @@ public async Task CompleteTodos_MarksItemCompleteAsync() await addTodos.InvokeAsync(new AIFunctionArguments() { ["todos"] = new List { new() { Title = "Test", Description = null } } }); // Act - object? result = await completeTodos.InvokeAsync(new AIFunctionArguments() { ["ids"] = new List { 1 } }); + object? result = await completeTodos.InvokeAsync(new AIFunctionArguments() { ["items"] = new List { new() { Id = 1, Reason = "Done" } } }); // Assert Assert.True(state.Items[0].IsComplete); @@ -139,7 +139,7 @@ await addTodos.InvokeAsync(new AIFunctionArguments() }); // Act - object? result = await completeTodos.InvokeAsync(new AIFunctionArguments() { ["ids"] = new List { 1, 3 } }); + object? result = await completeTodos.InvokeAsync(new AIFunctionArguments() { ["items"] = new List { new() { Id = 1, Reason = "Done" }, new() { Id = 3, Reason = "Done" } } }); // Assert Assert.True(state.Items[0].IsComplete); @@ -159,12 +159,35 @@ public async Task CompleteTodos_ReturnsZeroForMissingIdsAsync() AIFunction completeTodos = GetTool(tools, "TodoList_Complete"); // Act - object? result = await completeTodos.InvokeAsync(new AIFunctionArguments() { ["ids"] = new List { 999 } }); + object? result = await completeTodos.InvokeAsync(new AIFunctionArguments() { ["items"] = new List { new() { Id = 999, Reason = "Done" } } }); // Assert Assert.Equal(0, GetIntResult(result)); } + /// + /// Verify that CompleteTodos accepts an optional reason parameter. + /// + [Fact] + public async Task CompleteTodos_AcceptsReasonParameterAsync() + { + // Arrange + var (tools, state) = await CreateToolsWithStateAsync(); + AIFunction addTodos = GetTool(tools, "TodoList_Add"); + AIFunction completeTodos = GetTool(tools, "TodoList_Complete"); + await addTodos.InvokeAsync(new AIFunctionArguments() { ["todos"] = new List { new() { Title = "Research topic" } } }); + + // Act + object? result = await completeTodos.InvokeAsync(new AIFunctionArguments() + { + ["items"] = new List { new() { Id = 1, Reason = "Found the answer in the documentation." } }, + }); + + // Assert + Assert.True(state.Items[0].IsComplete); + Assert.Equal(1, GetIntResult(result)); + } + #endregion #region RemoveTodos Tests @@ -249,7 +272,7 @@ await addTodos.InvokeAsync(new AIFunctionArguments() { ["todos"] = new List { new() { Title = "Done", Description = null }, new() { Title = "Pending", Description = null } }, }); - await completeTodos.InvokeAsync(new AIFunctionArguments() { ["ids"] = new List { 1 } }); + await completeTodos.InvokeAsync(new AIFunctionArguments() { ["items"] = new List { new() { Id = 1, Reason = "Done" } } }); // Act object? result = await getRemainingTodos.InvokeAsync(new AIFunctionArguments()); @@ -279,7 +302,7 @@ await addTodos.InvokeAsync(new AIFunctionArguments() { ["todos"] = new List { new() { Title = "Done", Description = null }, new() { Title = "Pending", Description = null } }, }); - await completeTodos.InvokeAsync(new AIFunctionArguments() { ["ids"] = new List { 1 } }); + await completeTodos.InvokeAsync(new AIFunctionArguments() { ["items"] = new List { new() { Id = 1, Reason = "Done" } } }); // Act object? result = await getAllTodos.InvokeAsync(new AIFunctionArguments()); @@ -376,7 +399,7 @@ await addTodos.InvokeAsync(new AIFunctionArguments() { ["todos"] = new List { new() { Title = "Done", Description = null }, new() { Title = "Pending", Description = null } }, }); - await completeTodos.InvokeAsync(new AIFunctionArguments() { ["ids"] = new List { 1 } }); + await completeTodos.InvokeAsync(new AIFunctionArguments() { ["items"] = new List { new() { Id = 1, Reason = "Done" } } }); // Act var remaining = await provider.GetRemainingTodosAsync(session); @@ -543,7 +566,7 @@ await addTodos.InvokeAsync(new AIFunctionArguments() new() { Title = "Second", Description = "Has details" }, }, }); - await completeTodos.InvokeAsync(new AIFunctionArguments() { ["ids"] = new List { 1 } }); + await completeTodos.InvokeAsync(new AIFunctionArguments() { ["items"] = new List { new() { Id = 1, Reason = "Done" } } }); // Act — second invocation should see the updated list in messages AIContext result2 = await provider.InvokingAsync(context); @@ -762,7 +785,7 @@ await Task.WhenAll( { ["todos"] = new List { new() { Title = "New C" } }, }).AsTask(), - completeTodos.InvokeAsync(new AIFunctionArguments() { ["ids"] = new List { 1, 2, 3 } }).AsTask()); + completeTodos.InvokeAsync(new AIFunctionArguments() { ["items"] = new List { new() { Id = 1, Reason = "Done" }, new() { Id = 2, Reason = "Done" }, new() { Id = 3, Reason = "Done" } } }).AsTask()); // Assert object? allResult = await getAllTodos.InvokeAsync(new AIFunctionArguments());