Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,26 @@
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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -64,6 +64,46 @@ 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())
{
int id = item.TryGetProperty("id", out JsonElement idElement) ? idElement.GetInt32() : 0;
string? reason = item.TryGetProperty("reason", out JsonElement reasonElement)
? reasonElement.GetString()
: null;
Comment on lines +80 to +83
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public static List<ToolCallFormatter> BuildDefaultToolFormatters()
[
new TodoToolFormatter(),
new ModeToolFormatter(),
new SubAgentToolFormatter(),
new BackgroundAgentToolFormatter(),
new FileMemoryToolFormatter(),
new WebSearchToolFormatter(),
new FallbackToolFormatter(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// 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
// equipped with Foundry's hosted web search tool.
//
// Special commands:
Expand All @@ -25,7 +25,7 @@
const int MaxContextWindowTokens = 1_050_000;
const int MaxOutputTokens = 128_000;

// --- Sub-agent: Web Search Agent ---
// --- Background agent: Web Search Agent ---
// This agent can search the web and is used by the parent agent to look up stock prices.
AIAgent webSearchAgent =
new OpenAIClient(
Expand All @@ -52,26 +52,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.
""";

Expand All @@ -88,10 +88,10 @@ 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.",
AIContextProviders =
[
new SubAgentsProvider([webSearchAgent]),
new BackgroundAgentsProvider([webSearchAgent]),
],
ChatOptions = new ChatOptions
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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
┌─────────────────────────────────┐
Expand All @@ -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
```

Expand All @@ -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.
2 changes: 1 addition & 1 deletion dotnet/samples/02-agents/Harness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
14 changes: 8 additions & 6 deletions dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,11 @@ private static JsonSerializerOptions CreateDefaultOptions()
[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))]
Expand All @@ -95,12 +97,12 @@ private static JsonSerializerOptions CreateDefaultOptions()
[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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
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.
Expand All @@ -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; } = [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@
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; } = [];
}
Loading
Loading