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
1 change: 1 addition & 0 deletions dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,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" />
</Folder>
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,20 @@ public static async Task RunAgentAsync(AIAgent agent, string title, string userP

System.Console.WriteLine($"=== {title} ===");
System.Console.WriteLine(userPrompt);
System.Console.WriteLine("Commands: /todos (show todo list), /mode [plan|execute] (show or switch mode), exit (quit)");

var commands = new List<string>();
if (todoProvider is not null)
{
commands.Add("/todos (show todo list)");
}

if (modeProvider is not null)
{
commands.Add("/mode [plan|execute] (show or switch mode)");
}

commands.Add("exit (quit)");
System.Console.WriteLine($"Commands: {string.Join(", ", commands)}");
System.Console.WriteLine();

AgentSession session = await agent.CreateSessionAsync();
Expand Down Expand Up @@ -76,9 +89,16 @@ private static async Task StreamAgentResponseAsync(AIAgent agent, AgentSession s
private static async Task<List<ToolApprovalRequestContent>> StreamAndCollectApprovalsAsync(IAsyncEnumerable<AgentResponseUpdate> updates, AgentModeProvider? modeProvider, AgentSession session, int? maxContextWindowTokens, int? maxOutputTokens)
{
var approvalRequests = new List<ToolApprovalRequestContent>();
string mode = modeProvider?.GetMode(session) ?? "unknown";
System.Console.ForegroundColor = GetModeColor(mode);
System.Console.Write($"\n[{mode}] Agent: ");
string? mode = modeProvider is not null ? modeProvider.GetMode(session) : null;
if (mode is not null)
{
System.Console.ForegroundColor = GetModeColor(mode);
System.Console.Write($"\n[{mode}] Agent: ");
}
else
{
System.Console.Write("\nAgent: ");
}

var spinner = new Spinner();
spinner.Start();
Expand Down Expand Up @@ -181,11 +201,14 @@ private static async Task<List<ToolApprovalRequestContent>> StreamAndCollectAppr
hasReceivedAnyText = true;
}

string currentMode = modeProvider?.GetMode(session) ?? "unknown";
if (currentMode != mode)
if (modeProvider is not null)
{
mode = currentMode;
System.Console.ForegroundColor = GetModeColor(mode);
string currentMode = modeProvider.GetMode(session);
if (currentMode != mode)
{
mode = currentMode;
System.Console.ForegroundColor = GetModeColor(mode);
}
}

System.Console.Write(update.Text);
Expand Down Expand Up @@ -299,9 +322,14 @@ private static void HandleModeCommand(AgentModeProvider? modeProvider, AgentSess

private static void WritePrompt(AgentModeProvider? modeProvider, AgentSession session)
{
string mode = modeProvider?.GetMode(session) ?? "unknown";
System.Console.ForegroundColor = GetModeColor(mode);
System.Console.Write($"[{mode}] You: ");
if (modeProvider is not null)
{
string mode = modeProvider.GetMode(session);
System.Console.ForegroundColor = GetModeColor(mode);
System.Console.Write($"[{mode}] ");
}

System.Console.Write("You: ");
System.Console.ResetColor();
}

Expand Down Expand Up @@ -371,10 +399,11 @@ private static void WriteTokenCount(long? count, int? budget)
}
}

private static ConsoleColor GetModeColor(string mode) => mode switch
private static ConsoleColor GetModeColor(string? mode) => mode switch
{
"plan" => ConsoleColor.Cyan,
"execute" => ConsoleColor.Green,
null => ConsoleColor.Gray,
_ => ConsoleColor.Gray,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ public static string Format(FunctionCallContent call)
"AgentMode_Get" => null,

// Sub-agent tools
"StartSubTask" => FormatStartSubTask(call),
"WaitForFirstCompletion" => FormatIdList(call, "taskIds", "Wait for"),
"GetSubTaskResults" => FormatSingleId(call, "taskId"),
"GetAllTasks" => null,
"ContinueTask" => FormatContinueTask(call),
"ClearCompletedTask" => FormatSingleId(call, "taskId"),
"SubAgents_StartTask" => FormatStartSubTask(call),
"SubAgents_WaitForFirstCompletion" => FormatIdList(call, "taskIds", "Wait for"),
"SubAgents_GetTaskResults" => FormatSingleId(call, "taskId"),
"SubAgents_GetAllTasks" => null,
"SubAgents_ContinueTask" => FormatContinueTask(call),
"SubAgents_ClearCompletedTask" => FormatSingleId(call, "taskId"),

// File memory tools
"FileMemory_SaveFile" => FormatSaveFile(call),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>

<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\Harness_Shared_Console\Harness_Shared_Console.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) Microsoft. All rights reserved.

// This sample demonstrates how to use the SubAgentsProvider to delegate work to sub-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
// equipped with Foundry's hosted web search tool.
//
// Special commands:
// exit — End the session.

#pragma warning disable OPENAI001 // Suppress experimental API warnings for Responses API usage.
#pragma warning disable MAAI001 // Suppress experimental API warnings for Agents AI experiments.

using System.ClientModel.Primitives;
using Azure.Identity;
using Harness.Shared.Console;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Responses;

var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4";

// --- Sub-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(
new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"),
new OpenAIClientOptions()
{
Endpoint = new Uri(endpoint),
RetryPolicy = new ClientRetryPolicy(3)
})
.GetResponsesClient()
.AsIChatClientWithStoredOutputDisabled(deploymentName)
.AsAIAgent(
new ChatClientAgentOptions
{
Name = "WebSearchAgent",
Description = "An agent that can search the web to find information.",
ChatOptions = new ChatOptions
{
Instructions = "You are a web search assistant. When asked to find information, use the web search tool to look it up and return a concise, factual answer.",
Tools =
[
ResponseTool.CreateWebSearchTool().AsAITool(),
],
},
});

// --- Parent agent: Stock Price Researcher ---
// This agent orchestrates the sub-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.

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.
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.
- Present results in a clean markdown table format.
""";

AIAgent parentAgent =
new OpenAIClient(
new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"),
new OpenAIClientOptions()
{
Endpoint = new Uri(endpoint),
RetryPolicy = new ClientRetryPolicy(3)
})
.GetResponsesClient()
.AsIChatClientWithStoredOutputDisabled(deploymentName)
.AsAIAgent(
new ChatClientAgentOptions
{
Name = "StockPriceResearcher",
Description = "An agent that researches stock prices using sub-agents.",
AIContextProviders =
[
new SubAgentsProvider([webSearchAgent]),
],
ChatOptions = new ChatOptions
{
Instructions = parentInstructions,
MaxOutputTokens = 16_000,
},
});

// Run the interactive console session.
await HarnessConsole.RunAgentAsync(
parentAgent,
title: "Stock Price Researcher (SubAgents Demo)",
userPrompt: "Enter a list of stock tickers (e.g., BAC, MSFT, BA):");
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Harness Step 02 — SubAgents (Stock Price Research)

This sample demonstrates how to use the **SubAgentsProvider** to delegate work from a parent agent to sub-agents.

## 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.

### Architecture

```
┌─────────────────────────────────┐
│ StockPriceResearcher │
│ (Parent Agent) │
│ │
│ SubAgentsProvider │
│ ├─ SubAgents_StartTask │
│ ├─ SubAgents_WaitFor... │
│ ├─ SubAgents_GetTaskResults │
│ └─ ... │
└────────────┬────────────────────┘
│ delegates to
┌─────────────────────────────────┐
│ WebSearchAgent │
│ (Sub-Agent) │
│ │
│ Tools: │
│ └─ web_search (Foundry) │
└─────────────────────────────────┘
```

## Prerequisites

- An Azure AI Foundry endpoint with an OpenAI model deployment
- Set the following environment variables:
- `AZURE_FOUNDRY_OPENAI_ENDPOINT` — Your Foundry OpenAI endpoint URL
- `AZURE_AI_MODEL_DEPLOYMENT_NAME` — Model deployment name (defaults to `gpt-5.4`)

## Running the Sample

```bash
cd dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithSubAgents
dotnet run
```

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.
1 change: 1 addition & 0 deletions dotnet/samples/02-agents/Harness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ 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 |
7 changes: 7 additions & 0 deletions dotnet/src/Microsoft.Agents.AI/AgentJsonUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ 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")]

[ExcludeFromCodeCoverage]
internal sealed partial class JsonContext : JsonSerializerContext;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace Microsoft.Agents.AI;

/// <summary>
/// Holds non-serializable runtime references for in-flight sub-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"/>.
/// </remarks>
internal sealed class SubAgentRuntimeState
{
/// <summary>
/// Gets the mapping of task IDs to their in-flight <see cref="Task{AgentResponse}"/> instances.
/// </summary>
[JsonIgnore]
public Dictionary<int, Task<AgentResponse>> InFlightTasks { get; } = [];

/// <summary>
/// Gets the mapping of task IDs to their sub-agent <see cref="AgentSession"/> instances,
/// needed for <c>ContinueTask</c>.
/// </summary>
[JsonIgnore]
public Dictionary<int, AgentSession> SubTaskSessions { get; } = [];
}
28 changes: 28 additions & 0 deletions dotnet/src/Microsoft.Agents.AI/Harness/SubAgents/SubAgentState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Agents.AI;

/// <summary>
/// Represents the serializable state of sub-tasks managed by the <see cref="SubAgentsProvider"/>,
/// stored in the session's <see cref="AgentSessionStateBag"/>.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
internal sealed class SubAgentState
{
/// <summary>
/// Gets or sets the next ID to assign to a new sub-task.
/// </summary>
[JsonPropertyName("nextTaskId")]
public int NextTaskId { get; set; } = 1;

/// <summary>
/// Gets the list of sub-task metadata entries.
/// </summary>
[JsonPropertyName("tasks")]
public List<SubTaskInfo> Tasks { get; set; } = [];
}
Loading
Loading