.NET: Require TODO finish reason and rename SubAgents to BackgroundAgents (#5902)

* Require TODO finish reason and rename SubAgents to BackgroundAgents

* Address PR comments
This commit is contained in:
westey
2026-05-18 16:37:25 +01:00
committed by GitHub
Unverified
parent ddc0fcf81f
commit 7cea5e162a
19 changed files with 361 additions and 266 deletions
+1 -1
View File
@@ -122,7 +122,7 @@
<File Path="samples/02-agents/Harness/README.md" />
<Project Path="samples/02-agents/Harness/Harness_Shared_Console/Harness_Shared_Console.csproj" />
<Project Path="samples/02-agents/Harness/Harness_Step01_Research/Harness_Step01_Research.csproj" />
<Project Path="samples/02-agents/Harness/Harness_Step02_Research_WithSubAgents/Harness_Step02_Research_WithSubAgents.csproj" />
<Project Path="samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Harness_Step02_Research_WithBackgroundAgents.csproj" />
<Project Path="samples/02-agents/Harness/Harness_Step03_DataProcessing/Harness_Step03_DataProcessing.csproj" />
<Project Path="samples/02-agents/Harness/ConsoleReactiveFramework/ConsoleReactiveFramework.csproj" />
<Project Path="samples/02-agents/Harness/ConsoleReactiveComponents/ConsoleReactiveComponents.csproj" />
@@ -6,26 +6,26 @@ using Microsoft.Extensions.AI;
namespace Harness.Shared.Console.ToolFormatters;
/// <summary>
/// Formats <c>SubAgents_*</c> tool calls with human-readable details
/// Formats <c>BackgroundAgents_*</c> tool calls with human-readable details
/// for task start, continue, wait, and result retrieval operations.
/// </summary>
public sealed class SubAgentToolFormatter : ToolCallFormatter
public sealed class BackgroundAgentToolFormatter : ToolCallFormatter
{
/// <inheritdoc/>
public override bool CanFormat(FunctionCallContent call) => call.Name.StartsWith("SubAgents_", StringComparison.Ordinal);
public override bool CanFormat(FunctionCallContent call) => call.Name.StartsWith("BackgroundAgents_", StringComparison.Ordinal);
/// <inheritdoc/>
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");
@@ -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<int>? ids = GetIntListArgumentValue(call, paramName);
@@ -56,7 +56,7 @@ public abstract class ToolCallFormatter
[
new TodoToolFormatter(),
new ModeToolFormatter(),
new SubAgentToolFormatter(),
new BackgroundAgentToolFormatter(),
new FileMemoryToolFormatter(),
new WebSearchToolFormatter(),
new FallbackToolFormatter(),
@@ -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 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYME
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 @@ AIAgent webSearchAgent =
});
// --- 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 @@ AIAgent parentAgent =
.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 @@ AIAgent parentAgent =
DisableWebSearch = true,
AIContextProviders =
[
new SubAgentsProvider([webSearchAgent]),
new BackgroundAgentsProvider([webSearchAgent]),
],
ChatOptions = new ChatOptions
{
@@ -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.
+1 -1
View File
@@ -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 |
@@ -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;
@@ -7,15 +7,15 @@ using System.Threading.Tasks;
namespace Microsoft.Agents.AI;
/// <summary>
/// Holds non-serializable runtime references for in-flight sub-tasks within a single parent session.
/// Holds non-serializable runtime references for in-flight background tasks within a single parent session.
/// </summary>
/// <remarks>
/// Properties are marked with <see cref="JsonIgnoreAttribute"/> because <see cref="Task{TResult}"/>
/// and <see cref="AgentSession"/> are not JSON-serializable. After deserialization (e.g., after a restart),
/// a fresh empty instance is created and any previously-running tasks are marked as
/// <see cref="SubTaskStatus.Lost"/> by <see cref="SubAgentsProvider"/>.
/// <see cref="BackgroundTaskStatus.Lost"/> by <see cref="BackgroundAgentsProvider"/>.
/// </remarks>
internal sealed class SubAgentRuntimeState
internal sealed class BackgroundAgentRuntimeState
{
/// <summary>
/// Gets the mapping of task IDs to their in-flight <see cref="Task{AgentResponse}"/> instances.
@@ -24,9 +24,9 @@ internal sealed class SubAgentRuntimeState
public Dictionary<int, Task<AgentResponse>> InFlightTasks { get; } = [];
/// <summary>
/// Gets the mapping of task IDs to their sub-agent <see cref="AgentSession"/> instances,
/// Gets the mapping of task IDs to their background agent <see cref="AgentSession"/> instances,
/// needed for <c>ContinueTask</c>.
/// </summary>
[JsonIgnore]
public Dictionary<int, AgentSession> SubTaskSessions { get; } = [];
public Dictionary<int, AgentSession> BackgroundTaskSessions { get; } = [];
}
@@ -8,21 +8,21 @@ using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI;
/// <summary>
/// Represents the serializable state of sub-tasks managed by the <see cref="SubAgentsProvider"/>,
/// Represents the serializable state of background tasks managed by the <see cref="BackgroundAgentsProvider"/>,
/// stored in the session's <see cref="AgentSessionStateBag"/>.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
internal sealed class SubAgentState
internal sealed class BackgroundAgentState
{
/// <summary>
/// Gets or sets the next ID to assign to a new sub-task.
/// Gets or sets the next ID to assign to a new background task.
/// </summary>
[JsonPropertyName("nextTaskId")]
public int NextTaskId { get; set; } = 1;
/// <summary>
/// Gets the list of sub-task metadata entries.
/// Gets the list of background task metadata entries.
/// </summary>
[JsonPropertyName("tasks")]
public List<SubTaskInfo> Tasks { get; set; } = [];
public List<BackgroundTaskInfo> Tasks { get; set; } = [];
}
@@ -15,56 +15,56 @@ using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// An <see cref="AIContextProvider"/> that enables an agent to delegate work to sub-agents asynchronously.
/// An <see cref="AIContextProvider"/> that enables an agent to delegate work to background agents asynchronously.
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="SubAgentsProvider"/> allows a parent agent to start sub-tasks on child agents,
/// wait for their completion, and retrieve results. Each sub-task runs in its own session and
/// The <see cref="BackgroundAgentsProvider"/> allows a parent agent to start background tasks on child agents,
/// wait for their completion, and retrieve results. Each background task runs in its own session and
/// executes concurrently.
/// </para>
/// <para>
/// This provider exposes the following tools to the agent:
/// <list type="bullet">
/// <item><description><c>SubAgents_StartTask</c> — Start a sub-task on a named agent with text input. Returns the task ID.</description></item>
/// <item><description><c>SubAgents_WaitForFirstCompletion</c> — Block until the first of the specified tasks completes. Returns the completed task's ID.</description></item>
/// <item><description><c>SubAgents_GetTaskResults</c> — Retrieve the text output of a completed sub-task.</description></item>
/// <item><description><c>SubAgents_GetAllTasks</c> — List all sub-tasks with their IDs, statuses, descriptions, and agent names.</description></item>
/// <item><description><c>SubAgents_ContinueTask</c> — Send follow-up input to a completed sub-task's session to resume work.</description></item>
/// <item><description><c>SubAgents_ClearCompletedTask</c> — Remove a completed sub-task and release its session to free memory.</description></item>
/// <item><description><c>BackgroundAgents_StartTask</c> — Start a background task on a named agent with text input. Returns the task ID.</description></item>
/// <item><description><c>BackgroundAgents_WaitForFirstCompletion</c> — Block until the first of the specified tasks completes. Returns the completed task's ID.</description></item>
/// <item><description><c>BackgroundAgents_GetTaskResults</c> — Retrieve the text output of a completed background task.</description></item>
/// <item><description><c>BackgroundAgents_GetAllTasks</c> — List all background tasks with their IDs, statuses, descriptions, and agent names.</description></item>
/// <item><description><c>BackgroundAgents_ContinueTask</c> — Send follow-up input to a completed background task's session to resume work.</description></item>
/// <item><description><c>BackgroundAgents_ClearCompletedTask</c> — Remove a completed background task and release its session to free memory.</description></item>
/// </list>
/// </para>
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class SubAgentsProvider : AIContextProvider
public sealed class BackgroundAgentsProvider : AIContextProvider
{
private const string DefaultInstructions =
"""
## SubAgents
You have access to sub-agents that can perform work on your behalf.
## BackgroundAgents
You have access to background agents that can perform work on your behalf.
- Use the `SubAgents_*` list of tools to start tasks on sub agents and check their results.
- Creating a sub task does not block, and sub-tasks run concurrently.
- Use the `BackgroundAgents_*` list of tools to start tasks on background agents and check their results.
- Creating a background task does not block, and background tasks run concurrently.
- Important: Always wait for outstanding tasks to finish before you finish processing.
- Important: After retrieving results from a completed task, clear it with SubAgents_ClearCompletedTask to free memory, unless you plan to continue it with SubAgents_ContinueTask.
- Important: After retrieving results from a completed task, clear it with BackgroundAgents_ClearCompletedTask to free memory, unless you plan to continue it with BackgroundAgents_ContinueTask.
{sub_agents}
{background_agents}
""";
private readonly Dictionary<string, AIAgent> _agents;
private readonly ProviderSessionState<SubAgentState> _sessionState;
private readonly ProviderSessionState<SubAgentRuntimeState> _runtimeSessionState;
private readonly ProviderSessionState<BackgroundAgentState> _sessionState;
private readonly ProviderSessionState<BackgroundAgentRuntimeState> _runtimeSessionState;
private readonly string _instructions;
private IReadOnlyList<string>? _stateKeys;
/// <summary>
/// Initializes a new instance of the <see cref="SubAgentsProvider"/> class.
/// Initializes a new instance of the <see cref="BackgroundAgentsProvider"/> class.
/// </summary>
/// <param name="agents">The collection of sub-agents available for delegation.</param>
/// <param name="agents">The collection of background agents available for delegation.</param>
/// <param name="options">Optional settings controlling the provider behavior.</param>
/// <exception cref="ArgumentNullException"><paramref name="agents"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">An agent has a null or empty name, or agent names are not unique.</exception>
public SubAgentsProvider(IEnumerable<AIAgent> agents, SubAgentsProviderOptions? options = null)
public BackgroundAgentsProvider(IEnumerable<AIAgent> agents, BackgroundAgentsProviderOptions? options = null)
{
_ = Throw.IfNull(agents);
@@ -74,15 +74,15 @@ public sealed class SubAgentsProvider : AIContextProvider
string agentListText = options?.AgentListBuilder is not null
? options.AgentListBuilder(this._agents)
: BuildDefaultAgentListText(this._agents);
this._instructions = baseInstructions.Replace("{sub_agents}", agentListText);
this._instructions = baseInstructions.Replace("{background_agents}", agentListText);
this._sessionState = new ProviderSessionState<SubAgentState>(
_ => new SubAgentState(),
this._sessionState = new ProviderSessionState<BackgroundAgentState>(
_ => new BackgroundAgentState(),
this.GetType().Name,
AgentJsonUtilities.DefaultOptions);
this._runtimeSessionState = new ProviderSessionState<SubAgentRuntimeState>(
_ => new SubAgentRuntimeState(),
this._runtimeSessionState = new ProviderSessionState<BackgroundAgentRuntimeState>(
_ => new BackgroundAgentRuntimeState(),
this.GetType().Name + "_Runtime",
AgentJsonUtilities.DefaultOptions);
}
@@ -93,8 +93,8 @@ public sealed class SubAgentsProvider : AIContextProvider
/// <inheritdoc />
protected override ValueTask<AIContext> ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
SubAgentState state = this._sessionState.GetOrInitializeState(context.Session);
SubAgentRuntimeState runtimeState = this._runtimeSessionState.GetOrInitializeState(context.Session);
BackgroundAgentState state = this._sessionState.GetOrInitializeState(context.Session);
BackgroundAgentRuntimeState runtimeState = this._runtimeSessionState.GetOrInitializeState(context.Session);
return new ValueTask<AIContext>(new AIContext
{
@@ -113,12 +113,12 @@ public sealed class SubAgentsProvider : AIContextProvider
{
if (string.IsNullOrWhiteSpace(agent.Name))
{
throw new ArgumentException("All sub-agents must have a non-empty Name.", nameof(agents));
throw new ArgumentException("All background agents must have a non-empty Name.", nameof(agents));
}
if (dict.ContainsKey(agent.Name))
{
throw new ArgumentException($"Duplicate sub-agent name: '{agent.Name}'. Agent names must be unique (case-insensitive).", nameof(agents));
throw new ArgumentException($"Duplicate background agent name: '{agent.Name}'. Agent names must be unique (case-insensitive).", nameof(agents));
}
dict[agent.Name] = agent;
@@ -126,19 +126,19 @@ public sealed class SubAgentsProvider : AIContextProvider
if (dict.Count == 0)
{
throw new ArgumentException("At least one sub-agent must be provided.", nameof(agents));
throw new ArgumentException("At least one background agent must be provided.", nameof(agents));
}
return dict;
}
/// <summary>
/// Builds the default text listing available sub-agents and their descriptions.
/// Builds the default text listing available background agents and their descriptions.
/// </summary>
private static string BuildDefaultAgentListText(IReadOnlyDictionary<string, AIAgent> agents)
{
var sb = new StringBuilder();
sb.AppendLine("Available sub-agents:");
sb.AppendLine("Available background agents:");
foreach (var kvp in agents)
{
sb.Append("- ").Append(kvp.Key);
@@ -156,12 +156,12 @@ public sealed class SubAgentsProvider : AIContextProvider
/// <summary>
/// Refreshes the status of in-flight tasks in the given state for the specified session.
/// </summary>
private void TryRefreshTaskState(SubAgentState state, SubAgentRuntimeState runtimeState, AgentSession? session)
private void TryRefreshTaskState(BackgroundAgentState state, BackgroundAgentRuntimeState runtimeState, AgentSession? session)
{
bool changed = false;
foreach (SubTaskInfo task in state.Tasks)
foreach (BackgroundTaskInfo task in state.Tasks)
{
if (task.Status != SubTaskStatus.Running)
if (task.Status != BackgroundTaskStatus.Running)
{
continue;
}
@@ -169,7 +169,7 @@ public sealed class SubAgentsProvider : AIContextProvider
if (!runtimeState.InFlightTasks.TryGetValue(task.Id, out Task<AgentResponse>? inFlight))
{
// In-flight reference lost (e.g., after restart/deserialization).
task.Status = SubTaskStatus.Lost;
task.Status = BackgroundTaskStatus.Lost;
changed = true;
continue;
}
@@ -188,32 +188,32 @@ public sealed class SubAgentsProvider : AIContextProvider
}
/// <summary>
/// Finalizes a task by extracting results from the completed Task and updating the SubTaskInfo.
/// Finalizes a task by extracting results from the completed Task and updating the BackgroundTaskInfo.
/// </summary>
private static void FinalizeTask(SubTaskInfo taskInfo, Task<AgentResponse> completedTask, SubAgentRuntimeState runtimeState)
private static void FinalizeTask(BackgroundTaskInfo taskInfo, Task<AgentResponse> completedTask, BackgroundAgentRuntimeState runtimeState)
{
if (completedTask.Status == TaskStatus.RanToCompletion)
{
taskInfo.Status = SubTaskStatus.Completed;
taskInfo.Status = BackgroundTaskStatus.Completed;
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits — task is already completed
taskInfo.ResultText = completedTask.Result.Text;
#pragma warning restore VSTHRD002
}
else if (completedTask.IsFaulted)
{
taskInfo.Status = SubTaskStatus.Failed;
taskInfo.Status = BackgroundTaskStatus.Failed;
taskInfo.ErrorText = completedTask.Exception?.InnerException?.Message ?? completedTask.Exception?.Message ?? "Unknown error";
}
else if (completedTask.IsCanceled)
{
taskInfo.Status = SubTaskStatus.Failed;
taskInfo.Status = BackgroundTaskStatus.Failed;
taskInfo.ErrorText = "Task was canceled.";
}
runtimeState.InFlightTasks.Remove(taskInfo.Id);
}
private AITool[] CreateTools(SubAgentState state, SubAgentRuntimeState runtimeState, AgentSession? session)
private AITool[] CreateTools(BackgroundAgentState state, BackgroundAgentRuntimeState runtimeState, AgentSession? session)
{
var serializerOptions = AgentJsonUtilities.DefaultOptions;
@@ -221,43 +221,43 @@ public sealed class SubAgentsProvider : AIContextProvider
[
AIFunctionFactory.Create(
async (
[Description("The name of the sub agent to delegate the task to.")] string agentName,
[Description("The request to pass to the sub agent.")] string input,
[Description("The name of the background agent to delegate the task to.")] string agentName,
[Description("The request to pass to the background agent.")] string input,
[Description("A description of the task used to identify the task later.")] string description) =>
{
if (!this._agents.TryGetValue(agentName, out AIAgent? agent))
{
return $"Error: No sub-agent found with name '{agentName}'. Available agents: {string.Join(", ", this._agents.Keys)}";
return $"Error: No background agent found with name '{agentName}'. Available agents: {string.Join(", ", this._agents.Keys)}";
}
int taskId = state.NextTaskId++;
var taskInfo = new SubTaskInfo
var taskInfo = new BackgroundTaskInfo
{
Id = taskId,
AgentName = agentName,
Description = description,
Status = SubTaskStatus.Running,
Status = BackgroundTaskStatus.Running,
};
state.Tasks.Add(taskInfo);
// Create a dedicated session for this sub-task so it can be continued later.
// Create a dedicated session for this background task so it can be continued later.
AgentSession subSession = await agent.CreateSessionAsync().ConfigureAwait(false);
// Wrap in Task.Run to fork the ExecutionContext. AIAgent.RunAsync is a non-async
// method that synchronously sets the static AsyncLocal CurrentRunContext. Without
// this isolation, the sub-agent's RunAsync would overwrite the outer (calling)
// this isolation, the background agent's RunAsync would overwrite the outer (calling)
// agent's CurrentRunContext, corrupting all subsequent tool invocations in the
// same FICC batch.
runtimeState.InFlightTasks[taskId] = Task.Run(() => agent.RunAsync(input, subSession));
runtimeState.SubTaskSessions[taskId] = subSession;
runtimeState.BackgroundTaskSessions[taskId] = subSession;
this._sessionState.SaveState(session, state);
return $"Sub-task {taskId} started on agent '{agentName}'.";
return $"Background task {taskId} started on agent '{agentName}'.";
},
new AIFunctionFactoryOptions
{
Name = "SubAgents_StartTask",
Description = "Start a sub-task on a named sub-agent. Returns a confirmation message containing the task ID.",
Name = "BackgroundAgents_StartTask",
Description = "Start a background task on a named background agent. Returns a confirmation message containing the task ID.",
SerializerOptions = serializerOptions,
}),
@@ -287,7 +287,7 @@ public sealed class SubAgentsProvider : AIContextProvider
this._sessionState.SaveState(session, state);
// Check if any of the requested IDs are already complete.
SubTaskInfo? alreadyComplete = state.Tasks.FirstOrDefault(t => taskIds.Contains(t.Id) && t.Status != SubTaskStatus.Running);
BackgroundTaskInfo? alreadyComplete = state.Tasks.FirstOrDefault(t => taskIds.Contains(t.Id) && t.Status != BackgroundTaskStatus.Running);
if (alreadyComplete is not null)
{
return $"Task {alreadyComplete.Id} is not running; current status: {alreadyComplete.Status}.";
@@ -303,7 +303,7 @@ public sealed class SubAgentsProvider : AIContextProvider
var completedEntry = waitableTasks.First(t => t.Task == completedTask);
// Finalize the completed task.
SubTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == completedEntry.Id);
BackgroundTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == completedEntry.Id);
if (taskInfo is not null)
{
FinalizeTask(taskInfo, completedEntry.Task, runtimeState);
@@ -314,8 +314,8 @@ public sealed class SubAgentsProvider : AIContextProvider
},
new AIFunctionFactoryOptions
{
Name = "SubAgents_WaitForFirstCompletion",
Description = "Block until the first of the specified sub-tasks completes. Provide one or more task IDs. Returns a status message containing the ID of the task that completed first.",
Name = "BackgroundAgents_WaitForFirstCompletion",
Description = "Block until the first of the specified background tasks completes. Provide one or more task IDs. Returns a status message containing the ID of the task that completed first.",
SerializerOptions = serializerOptions,
}),
@@ -324,7 +324,7 @@ public sealed class SubAgentsProvider : AIContextProvider
{
this.TryRefreshTaskState(state, runtimeState, session);
SubTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == taskId);
BackgroundTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == taskId);
if (taskInfo is null)
{
return $"Error: No task found with ID {taskId}.";
@@ -332,17 +332,17 @@ public sealed class SubAgentsProvider : AIContextProvider
return taskInfo.Status switch
{
SubTaskStatus.Completed => taskInfo.ResultText ?? "(no output)",
SubTaskStatus.Failed => $"Task failed: {taskInfo.ErrorText ?? "Unknown error"}",
SubTaskStatus.Lost => "Task state was lost (reference unavailable).",
SubTaskStatus.Running => $"Task {taskId} is still running.",
BackgroundTaskStatus.Completed => taskInfo.ResultText ?? "(no output)",
BackgroundTaskStatus.Failed => $"Task failed: {taskInfo.ErrorText ?? "Unknown error"}",
BackgroundTaskStatus.Lost => "Task state was lost (reference unavailable).",
BackgroundTaskStatus.Running => $"Task {taskId} is still running.",
_ => $"Task {taskId} has status: {taskInfo.Status}.",
};
},
new AIFunctionFactoryOptions
{
Name = "SubAgents_GetTaskResults",
Description = "Get the text output of a sub-task by its ID. Returns the result text if complete, or status information if still running or failed.",
Name = "BackgroundAgents_GetTaskResults",
Description = "Get the text output of a background task by its ID. Returns the result text if complete, or status information if still running or failed.",
SerializerOptions = serializerOptions,
}),
@@ -358,7 +358,7 @@ public sealed class SubAgentsProvider : AIContextProvider
var sb = new StringBuilder();
sb.AppendLine("Tasks:");
foreach (SubTaskInfo task in state.Tasks)
foreach (BackgroundTaskInfo task in state.Tasks)
{
sb.Append("- Task ").Append(task.Id).Append(" [").Append(task.Status).Append("] (").Append(task.AgentName).Append("): ").AppendLine(task.Description);
}
@@ -367,8 +367,8 @@ public sealed class SubAgentsProvider : AIContextProvider
},
new AIFunctionFactoryOptions
{
Name = "SubAgents_GetAllTasks",
Description = "List all sub-tasks with their IDs, statuses, agent names, and descriptions.",
Name = "BackgroundAgents_GetAllTasks",
Description = "List all background tasks with their IDs, statuses, agent names, and descriptions.",
SerializerOptions = serializerOptions,
}),
@@ -377,18 +377,18 @@ public sealed class SubAgentsProvider : AIContextProvider
{
this.TryRefreshTaskState(state, runtimeState, session);
SubTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == taskId);
BackgroundTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == taskId);
if (taskInfo is null)
{
return $"Error: No task found with ID {taskId}.";
}
if (taskInfo.Status == SubTaskStatus.Lost)
if (taskInfo.Status == BackgroundTaskStatus.Lost)
{
return $"Error: Task {taskId} cannot be continued because its session was lost (e.g., after a session restore). Start a new task instead.";
}
if (taskInfo.Status == SubTaskStatus.Running)
if (taskInfo.Status == BackgroundTaskStatus.Running)
{
return $"Error: Task {taskId} is still running. Wait for it to complete before continuing.";
}
@@ -398,17 +398,17 @@ public sealed class SubAgentsProvider : AIContextProvider
return $"Error: Agent '{taskInfo.AgentName}' is no longer available.";
}
if (!runtimeState.SubTaskSessions.TryGetValue(taskId, out AgentSession? subSession))
if (!runtimeState.BackgroundTaskSessions.TryGetValue(taskId, out AgentSession? subSession))
{
return $"Error: Session for task {taskId} is no longer available.";
}
// Reset task state and start a new run on the existing session.
taskInfo.Status = SubTaskStatus.Running;
taskInfo.Status = BackgroundTaskStatus.Running;
taskInfo.ResultText = null;
taskInfo.ErrorText = null;
// Wrap in Task.Run to isolate the ExecutionContext (see StartSubTask comment).
// Wrap in Task.Run to isolate the ExecutionContext (see StartBackgroundTask comment).
runtimeState.InFlightTasks[taskId] = Task.Run(() => agent.RunAsync(text, subSession));
this._sessionState.SaveState(session, state);
@@ -416,8 +416,8 @@ public sealed class SubAgentsProvider : AIContextProvider
},
new AIFunctionFactoryOptions
{
Name = "SubAgents_ContinueTask",
Description = "Send follow-up input to a completed or failed sub-task to resume its work. The sub-task's session is preserved, so the agent retains conversational context.",
Name = "BackgroundAgents_ContinueTask",
Description = "Send follow-up input to a completed or failed background task to resume its work. The background task's session is preserved, so the agent retains conversational context.",
SerializerOptions = serializerOptions,
}),
@@ -426,13 +426,13 @@ public sealed class SubAgentsProvider : AIContextProvider
{
this.TryRefreshTaskState(state, runtimeState, session);
SubTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == taskId);
BackgroundTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == taskId);
if (taskInfo is null)
{
return $"Error: No task found with ID {taskId}.";
}
if (taskInfo.Status == SubTaskStatus.Running)
if (taskInfo.Status == BackgroundTaskStatus.Running)
{
return $"Error: Task {taskId} is still running. Wait for it to complete before clearing.";
}
@@ -442,15 +442,15 @@ public sealed class SubAgentsProvider : AIContextProvider
// Clean up runtime references.
runtimeState.InFlightTasks.Remove(taskId);
runtimeState.SubTaskSessions.Remove(taskId);
runtimeState.BackgroundTaskSessions.Remove(taskId);
this._sessionState.SaveState(session, state);
return $"Task {taskId} cleared.";
},
new AIFunctionFactoryOptions
{
Name = "SubAgents_ClearCompletedTask",
Description = "Remove a completed or failed sub-task and release its session to free memory. Use this after retrieving results when you no longer need to continue the task.",
Name = "BackgroundAgents_ClearCompletedTask",
Description = "Remove a completed or failed background task and release its session to free memory. Use this after retrieving results when you no longer need to continue the task.",
SerializerOptions = serializerOptions,
}),
];
@@ -8,21 +8,21 @@ using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI;
/// <summary>
/// Options controlling the behavior of <see cref="SubAgentsProvider"/>.
/// Options controlling the behavior of <see cref="BackgroundAgentsProvider"/>.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class SubAgentsProviderOptions
public sealed class BackgroundAgentsProviderOptions
{
/// <summary>
/// Gets or sets custom instructions provided to the agent for using the sub-agent tools.
/// Gets or sets custom instructions provided to the agent for using the background agent tools.
/// </summary>
/// <remarks>
/// Use the <c>{sub_agents}</c> placeholder to allow the provider to inject
/// the formatted list of available sub agents.
/// Use the <c>{background_agents}</c> placeholder to allow the provider to inject
/// the formatted list of available background agents.
/// </remarks>
/// <value>
/// When <see langword="null"/> (the default), the provider uses built-in instructions
/// that guide the agent on how to use the sub-agent tools.
/// that guide the agent on how to use the background agent tools.
/// The agent list is always appended after the instructions regardless of this setting.
/// </value>
public string? Instructions { get; set; }
@@ -33,7 +33,7 @@ public sealed class SubAgentsProviderOptions
/// <value>
/// When <see langword="null"/> (the default), the provider generates a standard list of agent names and descriptions.
/// When set, this function receives the dictionary of available agents (keyed by name) and should return
/// a formatted string describing the available sub-agents.
/// a formatted string describing the available background agents.
/// </value>
public Func<IReadOnlyDictionary<string, AIAgent>, string>? AgentListBuilder { get; set; }
}
@@ -7,43 +7,43 @@ using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI;
/// <summary>
/// Represents the metadata and result of a sub-task managed by the <see cref="SubAgentsProvider"/>.
/// Represents the metadata and result of a background task managed by the <see cref="BackgroundAgentsProvider"/>.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class SubTaskInfo
public sealed class BackgroundTaskInfo
{
/// <summary>
/// Gets or sets the unique identifier for this sub-task.
/// Gets or sets the unique identifier for this background task.
/// </summary>
[JsonPropertyName("id")]
public int Id { get; set; }
/// <summary>
/// Gets or sets the name of the agent that is executing this sub-task.
/// Gets or sets the name of the agent that is executing this background task.
/// </summary>
[JsonPropertyName("agentName")]
public string AgentName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a description of what this sub-task is doing.
/// Gets or sets a description of what this background task is doing.
/// </summary>
[JsonPropertyName("description")]
public string Description { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the current status of this sub-task.
/// Gets or sets the current status of this background task.
/// </summary>
[JsonPropertyName("status")]
public SubTaskStatus Status { get; set; }
public BackgroundTaskStatus Status { get; set; }
/// <summary>
/// Gets or sets the text result of the sub-task, populated when the task completes successfully.
/// Gets or sets the text result of the background task, populated when the task completes successfully.
/// </summary>
[JsonPropertyName("resultText")]
public string? ResultText { get; set; }
/// <summary>
/// Gets or sets the error message if the sub-task failed.
/// Gets or sets the error message if the background task failed.
/// </summary>
[JsonPropertyName("errorText")]
public string? ErrorText { get; set; }
@@ -6,28 +6,28 @@ using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI;
/// <summary>
/// Represents the status of a sub-task managed by the <see cref="SubAgentsProvider"/>.
/// Represents the status of a background task managed by the <see cref="BackgroundAgentsProvider"/>.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public enum SubTaskStatus
public enum BackgroundTaskStatus
{
/// <summary>
/// The sub-task is currently running.
/// The background task is currently running.
/// </summary>
Running,
/// <summary>
/// The sub-task completed successfully.
/// The background task completed successfully.
/// </summary>
Completed,
/// <summary>
/// The sub-task failed with an error.
/// The background task failed with an error.
/// </summary>
Failed,
/// <summary>
/// The sub-task's in-flight reference was lost (e.g., after a restart),
/// The background task's in-flight reference was lost (e.g., after a restart),
/// and its final state cannot be determined.
/// </summary>
Lost,
@@ -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;
}
@@ -54,7 +54,7 @@ public sealed class TodoProvider : AIContextProvider, IDisposable
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,
}),
@@ -13,9 +13,9 @@ using Moq.Protected;
namespace Microsoft.Agents.AI.UnitTests;
/// <summary>
/// Unit tests for the <see cref="SubAgentsProvider"/> class.
/// Unit tests for the <see cref="BackgroundAgentsProvider"/> class.
/// </summary>
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<ArgumentNullException>(() => new SubAgentsProvider(null!));
Assert.Throws<ArgumentNullException>(() => new BackgroundAgentsProvider(null!));
}
/// <summary>
@@ -36,7 +36,7 @@ public class SubAgentsProviderTests
public void Constructor_EmptyAgents_Throws()
{
// Act & Assert
Assert.Throws<ArgumentException>(() => new SubAgentsProvider(Array.Empty<AIAgent>()));
Assert.Throws<ArgumentException>(() => new BackgroundAgentsProvider(Array.Empty<AIAgent>()));
}
/// <summary>
@@ -49,7 +49,7 @@ public class SubAgentsProviderTests
var agent = CreateMockAgent(null!, "desc");
// Act & Assert
Assert.Throws<ArgumentException>(() => new SubAgentsProvider(new[] { agent }));
Assert.Throws<ArgumentException>(() => new BackgroundAgentsProvider(new[] { agent }));
}
/// <summary>
@@ -62,7 +62,7 @@ public class SubAgentsProviderTests
var agent = CreateMockAgent("", "desc");
// Act & Assert
Assert.Throws<ArgumentException>(() => new SubAgentsProvider(new[] { agent }));
Assert.Throws<ArgumentException>(() => new BackgroundAgentsProvider(new[] { agent }));
}
/// <summary>
@@ -76,7 +76,7 @@ public class SubAgentsProviderTests
var agent2 = CreateMockAgent("research", "Agent 2");
// Act & Assert
Assert.Throws<ArgumentException>(() => new SubAgentsProvider(new[] { agent1, agent2 }));
Assert.Throws<ArgumentException>(() => new BackgroundAgentsProvider(new[] { agent1, agent2 }));
}
/// <summary>
@@ -90,7 +90,7 @@ public class SubAgentsProviderTests
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 class SubAgentsProviderTests
{
// 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 class SubAgentsProviderTests
// 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 class SubAgentsProviderTests
#endregion
#region StartSubTask Tests
#region StartBackgroundTask Tests
/// <summary>
/// Verify that StartSubTask returns a task ID.
/// Verify that StartBackgroundTask returns a task ID.
/// </summary>
[Fact]
public async Task StartSubTask_ReturnsTaskIdAsync()
public async Task StartBackgroundTask_ReturnsTaskIdAsync()
{
// Arrange
var tcs = new TaskCompletionSource<AgentResponse>();
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 class SubAgentsProviderTests
}
/// <summary>
/// Verify that StartSubTask with invalid agent name returns an error.
/// Verify that StartBackgroundTask with invalid agent name returns an error.
/// </summary>
[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 class SubAgentsProviderTests
}
/// <summary>
/// Verify that StartSubTask assigns sequential IDs.
/// Verify that StartBackgroundTask assigns sequential IDs.
/// </summary>
[Fact]
public async Task StartSubTask_AssignsSequentialIdsAsync()
public async Task StartBackgroundTask_AssignsSequentialIdsAsync()
{
// Arrange
var tcs1 = new TaskCompletionSource<AgentResponse>();
@@ -215,16 +215,16 @@ public class SubAgentsProviderTests
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 class SubAgentsProviderTests
var tcs = new TaskCompletionSource<AgentResponse>();
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 class SubAgentsProviderTests
// 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 class SubAgentsProviderTests
#endregion
#region GetSubTaskResults Tests
#region GetBackgroundTaskResults Tests
/// <summary>
/// Verify that GetSubTaskResults returns the result text of a completed task.
/// Verify that GetBackgroundTaskResults returns the result text of a completed task.
/// </summary>
[Fact]
public async Task GetSubTaskResults_CompletedTask_ReturnsResultTextAsync()
public async Task GetBackgroundTaskResults_CompletedTask_ReturnsResultTextAsync()
{
// Arrange
var tcs = new TaskCompletionSource<AgentResponse>();
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 @@ public class SubAgentsProviderTests
}
/// <summary>
/// Verify that GetSubTaskResults for a still-running task returns status info.
/// Verify that GetBackgroundTaskResults for a still-running task returns status info.
/// </summary>
[Fact]
public async Task GetSubTaskResults_RunningTask_ReturnsStatusAsync()
public async Task GetBackgroundTaskResults_RunningTask_ReturnsStatusAsync()
{
// Arrange
var tcs = new TaskCompletionSource<AgentResponse>();
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 @@ public class SubAgentsProviderTests
}
/// <summary>
/// Verify that GetSubTaskResults for a nonexistent task returns an error.
/// Verify that GetBackgroundTaskResults for a nonexistent task returns an error.
/// </summary>
[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 class SubAgentsProviderTests
}
/// <summary>
/// Verify that GetSubTaskResults for a failed task returns the error.
/// Verify that GetBackgroundTaskResults for a failed task returns the error.
/// </summary>
[Fact]
public async Task GetSubTaskResults_FailedTask_ReturnsErrorTextAsync()
public async Task GetBackgroundTaskResults_FailedTask_ReturnsErrorTextAsync()
{
// Arrange
var tcs = new TaskCompletionSource<AgentResponse>();
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 class SubAgentsProviderTests
var tcs = new TaskCompletionSource<AgentResponse>();
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 class SubAgentsProviderTests
var tcs = new TaskCompletionSource<AgentResponse>();
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 class SubAgentsProviderTests
// 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 class SubAgentsProviderTests
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 class SubAgentsProviderTests
var tcs = new TaskCompletionSource<AgentResponse>();
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 class SubAgentsProviderTests
// 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 class SubAgentsProviderTests
var tcs = new TaskCompletionSource<AgentResponse>();
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 class SubAgentsProviderTests
var tcs = new TaskCompletionSource<AgentResponse>();
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 class SubAgentsProviderTests
// 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 class SubAgentsProviderTests
{
// 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 class SubAgentsProviderTests
#region CurrentRunContext Isolation Tests
/// <summary>
/// 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.
/// </summary>
[Fact]
public async Task StartSubTask_DoesNotCorruptCurrentRunContextAsync()
public async Task StartBackgroundTask_DoesNotCorruptCurrentRunContextAsync()
{
// Arrange
var tcs = new TaskCompletionSource<AgentResponse>();
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<string, object?>
{
["agentName"] = "Research",
@@ -826,16 +826,16 @@ public class SubAgentsProviderTests
{
// 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 class SubAgentsProviderTests
{
// 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 class SubAgentsProviderTests
{
// 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 class SubAgentsProviderTests
// 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 @@ public class SubAgentsProviderTests
return mock.Object;
}
private static async Task<(IEnumerable<AITool> Tools, SubAgentsProvider Provider)> CreateToolsWithProviderAsync(AIAgent agent)
private static async Task<(IEnumerable<AITool> 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);
@@ -116,7 +116,7 @@ public class TodoProviderTests
await addTodos.InvokeAsync(new AIFunctionArguments() { ["todos"] = new List<TodoItemInput> { new() { Title = "Test", Description = null } } });
// Act
object? result = await completeTodos.InvokeAsync(new AIFunctionArguments() { ["ids"] = new List<int> { 1 } });
object? result = await completeTodos.InvokeAsync(new AIFunctionArguments() { ["items"] = new List<TodoCompleteInput> { new() { Id = 1, Reason = "Done" } } });
// Assert
Assert.True(state.Items[0].IsComplete);
@@ -139,7 +139,7 @@ public class TodoProviderTests
});
// Act
object? result = await completeTodos.InvokeAsync(new AIFunctionArguments() { ["ids"] = new List<int> { 1, 3 } });
object? result = await completeTodos.InvokeAsync(new AIFunctionArguments() { ["items"] = new List<TodoCompleteInput> { new() { Id = 1, Reason = "Done" }, new() { Id = 3, Reason = "Done" } } });
// Assert
Assert.True(state.Items[0].IsComplete);
@@ -159,12 +159,35 @@ public class TodoProviderTests
AIFunction completeTodos = GetTool(tools, "TodoList_Complete");
// Act
object? result = await completeTodos.InvokeAsync(new AIFunctionArguments() { ["ids"] = new List<int> { 999 } });
object? result = await completeTodos.InvokeAsync(new AIFunctionArguments() { ["items"] = new List<TodoCompleteInput> { new() { Id = 999, Reason = "Done" } } });
// Assert
Assert.Equal(0, GetIntResult(result));
}
/// <summary>
/// Verify that CompleteTodos accepts an optional reason parameter.
/// </summary>
[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<TodoItemInput> { new() { Title = "Research topic" } } });
// Act
object? result = await completeTodos.InvokeAsync(new AIFunctionArguments()
{
["items"] = new List<TodoCompleteInput> { 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 @@ public class TodoProviderTests
{
["todos"] = new List<TodoItemInput> { new() { Title = "Done", Description = null }, new() { Title = "Pending", Description = null } },
});
await completeTodos.InvokeAsync(new AIFunctionArguments() { ["ids"] = new List<int> { 1 } });
await completeTodos.InvokeAsync(new AIFunctionArguments() { ["items"] = new List<TodoCompleteInput> { new() { Id = 1, Reason = "Done" } } });
// Act
object? result = await getRemainingTodos.InvokeAsync(new AIFunctionArguments());
@@ -279,7 +302,7 @@ public class TodoProviderTests
{
["todos"] = new List<TodoItemInput> { new() { Title = "Done", Description = null }, new() { Title = "Pending", Description = null } },
});
await completeTodos.InvokeAsync(new AIFunctionArguments() { ["ids"] = new List<int> { 1 } });
await completeTodos.InvokeAsync(new AIFunctionArguments() { ["items"] = new List<TodoCompleteInput> { new() { Id = 1, Reason = "Done" } } });
// Act
object? result = await getAllTodos.InvokeAsync(new AIFunctionArguments());
@@ -376,7 +399,7 @@ public class TodoProviderTests
{
["todos"] = new List<TodoItemInput> { new() { Title = "Done", Description = null }, new() { Title = "Pending", Description = null } },
});
await completeTodos.InvokeAsync(new AIFunctionArguments() { ["ids"] = new List<int> { 1 } });
await completeTodos.InvokeAsync(new AIFunctionArguments() { ["items"] = new List<TodoCompleteInput> { new() { Id = 1, Reason = "Done" } } });
// Act
var remaining = await provider.GetRemainingTodosAsync(session);
@@ -543,7 +566,7 @@ public class TodoProviderTests
new() { Title = "Second", Description = "Has details" },
},
});
await completeTodos.InvokeAsync(new AIFunctionArguments() { ["ids"] = new List<int> { 1 } });
await completeTodos.InvokeAsync(new AIFunctionArguments() { ["items"] = new List<TodoCompleteInput> { 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 @@ public class TodoProviderTests
{
["todos"] = new List<TodoItemInput> { new() { Title = "New C" } },
}).AsTask(),
completeTodos.InvokeAsync(new AIFunctionArguments() { ["ids"] = new List<int> { 1, 2, 3 } }).AsTask());
completeTodos.InvokeAsync(new AIFunctionArguments() { ["items"] = new List<TodoCompleteInput> { new() { Id = 1, Reason = "Done" }, new() { Id = 2, Reason = "Done" }, new() { Id = 3, Reason = "Done" } } }).AsTask());
// Assert
object? allResult = await getAllTodos.InvokeAsync(new AIFunctionArguments());