Merge branch 'main' into test-it-bump-aaip210b2

This commit is contained in:
Roger Barreto
2026-05-18 17:03:59 +01:00
committed by GitHub
Unverified
75 changed files with 3817 additions and 487 deletions
@@ -31,6 +31,10 @@ public sealed class ToolCallDisplayObserver : ConsoleObserver
{
await ux.WriteInfoLineAsync($"🔧 Calling tool: {ToolCallFormatter.Format(this._formatters, functionCall)}...", ConsoleColor.DarkYellow);
}
else if (content is WebSearchToolCallContent)
{
// Handled by OpenAIResponsesWebSearchDisplayObserver when present; skip here to avoid duplication.
}
else if (content is ToolCallContent toolCall)
{
await ux.WriteInfoLineAsync($"🔧 Calling tool: {toolCall}...", ConsoleColor.DarkYellow);
@@ -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(),
@@ -0,0 +1,206 @@
// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable OPENAI001 // Suppress experimental API warnings for Responses API usage.
using System.Text;
using Harness.Shared.Console;
using Harness.Shared.Console.Observers;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Responses;
namespace SampleApp;
/// <summary>
/// Displays web search activity in the scroll area. Shows search queries,
/// page opens, and find-in-page actions as they stream in from the API.
/// </summary>
internal sealed class OpenAIResponsesWebSearchDisplayObserver : ConsoleObserver
{
private const int MaxQueryDisplayLength = 120;
/// <inheritdoc/>
public override async Task OnContentAsync(IUXStateDriver ux, AIContent content, AIAgent agent, AgentSession session)
{
if (content is WebSearchToolResultContent resultContent
&& resultContent.RawRepresentation is WebSearchCallResponseItem wscri)
{
await WriteActionAsync(ux, wscri, resultContent.Outputs);
}
}
private static async Task WriteActionAsync(IUXStateDriver ux, WebSearchCallResponseItem wscri, IList<AIContent>? outputs)
{
WebSearchAction? action = wscri.Action;
if (action is null)
{
await ux.WriteInfoLineAsync("🌐 Web Search Tool (no action details)", ConsoleColor.DarkCyan);
return;
}
switch (action)
{
case WebSearchFindInPageAction findInPage:
await WriteFindInPageAsync(ux, findInPage);
break;
case WebSearchOpenPageAction openPage:
await WriteOpenPageAsync(ux, openPage);
break;
case WebSearchSearchAction search:
await WriteSearchAsync(ux, search, outputs);
break;
default:
await ux.WriteInfoLineAsync("🌐 Web Search Tool (unknown action)", ConsoleColor.DarkCyan);
break;
}
}
private static async Task WriteSearchAsync(IUXStateDriver ux, WebSearchSearchAction search, IList<AIContent>? outputs)
{
// Read queries directly from the typed action.
IList<string> queries = search.Queries;
if (queries.Count == 0)
{
await ux.WriteInfoLineAsync("🌐 Web Search Tool: search", ConsoleColor.DarkCyan);
return;
}
var sb = new StringBuilder();
sb.Append("🌐 Web Search Tool: search");
// Show the search queries.
bool hasResults = outputs is { Count: > 0 };
for (int i = 0; i < queries.Count; i++)
{
string connector = (i < queries.Count - 1 || hasResults) ? "├─" : "└─";
string query = Truncate(queries[i], MaxQueryDisplayLength);
sb.Append($"\n {connector} \"{query}\"");
}
// Show search result sources (URLs + titles) when available.
// Sources come from M.E.AI's Outputs when IncludedResponseProperty.WebSearchCallActionSources is set,
// or directly from the SDK's WebSearchSearchAction.Sources.
if (hasResults)
{
sb.Append("\n │");
for (int i = 0; i < outputs!.Count; i++)
{
string connector = i < outputs.Count - 1 ? "├─" : "└─";
string line = FormatOutput(outputs[i]);
sb.Append($"\n {connector} {line}");
}
}
else if (search.Sources is { Count: > 0 } sources)
{
sb.Append("\n │");
for (int i = 0; i < sources.Count; i++)
{
string connector = i < sources.Count - 1 ? "├─" : "└─";
string line = FormatSource(sources[i]);
sb.Append($"\n {connector} {line}");
}
}
await ux.WriteInfoLineAsync(sb.ToString(), ConsoleColor.DarkCyan);
}
private static async Task WriteOpenPageAsync(IUXStateDriver ux, WebSearchOpenPageAction openPage)
{
string url = openPage.Uri?.AbsoluteUri ?? "(unknown)";
await ux.WriteInfoLineAsync(
$"🌐 Web Search Tool: open page\n └─ {url}",
ConsoleColor.DarkCyan);
}
private static async Task WriteFindInPageAsync(IUXStateDriver ux, WebSearchFindInPageAction findInPage)
{
string url = findInPage.Uri?.AbsoluteUri ?? "(unknown)";
string pattern = findInPage.Pattern ?? "(unknown)";
await ux.WriteInfoLineAsync(
$"🌐 Web Search Tool: find in page\n ├─ \"{Truncate(pattern, MaxQueryDisplayLength)}\"\n └─ {url}",
ConsoleColor.DarkCyan);
}
/// <summary>
/// Formats a single search result source from the SDK's <see cref="WebSearchActionSource"/> for display.
/// </summary>
private static string FormatSource(WebSearchActionSource source)
{
if (source is WebSearchActionUriSource uriSource)
{
string url = uriSource.Uri?.AbsoluteUri ?? "(unknown)";
// WebSearchActionUriSource doesn't expose a title property,
// but the API may include one in the raw response JSON.
string? title = GetTitleFromRawRepresentation(uriSource);
return title is not null
? $"{Truncate(title, MaxQueryDisplayLength)} — {url}"
: url;
}
return source.ToString() ?? "(unknown source)";
}
/// <summary>
/// Formats a single search result output from M.E.AI's <see cref="AIContent"/> for display.
/// </summary>
private static string FormatOutput(AIContent output)
{
if (output is UriContent uriContent)
{
string url = uriContent.Uri?.AbsoluteUri ?? "(unknown)";
// Try to extract a title from the raw JSON of the source.
// The SDK's WebSearchActionUriSource doesn't expose a title property,
// but the API may include one in the raw response.
string? title = GetTitleFromRawRepresentation(uriContent.RawRepresentation)
?? (uriContent.AdditionalProperties?.TryGetValue("title", out var t) is true ? t?.ToString() : null);
return title is not null
? $"{Truncate(title, MaxQueryDisplayLength)} — {url}"
: url;
}
return output.ToString() ?? "(unknown output)";
}
/// <summary>
/// Attempts to extract a "title" field from a raw representation object by serializing it to JSON.
/// The SDK's <see cref="WebSearchActionUriSource"/> doesn't expose a title property,
/// but the API may include one in the raw JSON — this is forward-compatible for when
/// the SDK adds title support.
/// </summary>
private static string? GetTitleFromRawRepresentation(object? rawRepresentation)
{
if (rawRepresentation is null)
{
return null;
}
try
{
var data = System.ClientModel.Primitives.ModelReaderWriter.Write(rawRepresentation);
using var doc = System.Text.Json.JsonDocument.Parse(data);
if (doc.RootElement.TryGetProperty("title", out var titleEl)
&& titleEl.ValueKind == System.Text.Json.JsonValueKind.String)
{
return titleEl.GetString();
}
}
catch
{
// Serialization may not be supported for this object type.
}
return null;
}
private static string Truncate(string text, int maxLength)
=> text.Length <= maxLength ? text : string.Concat(text.AsSpan(0, maxLength - 1), "…");
}
@@ -1,8 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use a HarnessAgent with the Harness AIContextProviders
// (TodoProvider and AgentModeProvider) for interactive research tasks with web search
// capabilities powered by Azure AI Foundry.
// This sample demonstrates how to use a HarnessAgent for interactive research tasks.
// The HarnessAgent comes pre-configured with TodoProvider, AgentModeProvider, FileMemoryProvider,
// ToolApproval, WebSearch, and OpenTelemetry — so this sample only needs custom instructions
// and a WebBrowsingTool.
// The agent plans research tasks, creates a todo list, gets user approval,
// and then executes each step — all within an interactive conversation loop.
//
@@ -34,86 +35,32 @@ const int MaxOutputTokens = 128_000;
// and research-focused instructions including the mandatory planning workflow.
var instructions =
"""
## Research Assistant Instructions
You are a research assistant. When given a research topic, research it thoroughly using web search and web browsing.
Use your knowledge to form good search queries and hypotheses, but always verify claims with the tools available to you rather than relying on memory alone.
## Mandatory planning workflow
For every new substantive user request, including short factual questions, your behavior is determined by the mode you are in.
If you are in plan mode, start with the *Plan Mode* steps, and if you are in execute mode, skip directly to the *Execute Mode* steps below.
*Plan Mode*
1. Analyze the request with the purpose of building a research plan.
2. Create a list of todo items.
3. If needed, use the provided tools to do some exploratory checks to help build a plan and determine what clarifying questions you may need from the user.
4. Ask for clarifications from the user where needed.
1. Ask each clarification one by one.
2. When asking for clarification and you have specific options in mind, present them to the user, so they can choose the option instead of having to retype the entire response.
3. Do not proceed until you have received all the needed clarifications.
4. Do short exploratory research if it helps with being able to ask sensible clarifications from the user.
5. Write the plan to a memory file, so that it is retained even if compaction happens. Make sure to update the plan file if the user requests changes.
6. Present the plan to the user and ask for approval to switch to execute mode and process the plan.
7. When approval is granted, always switch to execute mode (using the `AgentMode_Set` tool), and follow the steps for *Execute mode*.
*Execute Mode*
1. If you don't have a plan or tasks yet, analyse the user request and create tasks and a plan. (**Skip this step if you came from plan mode**)
2. Work autonomously use your best judgement to make decisions and keep progressing without asking the user questions. The goal is to have a complete, useful result ready when the user returns.
3. If you encounter ambiguity or an unexpected situation during execution, choose the most reasonable option, note your choice, and keep going.
4. Mark tasks as completed as you finish them.
5. Continue working, thinking and calling tools until you have the research result for the user.
## General Instructions
- You must check the current mode after any user input, since the user may have changed the mode themselves,
e.g. the user may have switched to 'plan' mode after a previous research task finished in 'execute' mode, meaning they want to review a plan first before execution.
- Explain your reasoning and thought process as you work through tasks.
- Explain what you learned and what you are going to do next between tool calls, so the user can follow along with your thought process.
- Avoid making more than 4 tool calls in a row without explaining what you are doing.
- Do not answer the underlying question before the plan has been presented and approved.
- This rule applies even when the answer seems obvious or the task seems small.
- For short requests, use a brief micro-plan rather than skipping planning. The only exceptions are:
- greetings,
- pure acknowledgments,
- clarification questions needed to form the plan,
- follow-up questions about results you have already presented,
- meta-discussion about the workflow itself.
**Todo management**
Mark each todo complete as you finish it so the list stays current.
If a todo turns out to be unnecessary or is blocked, remove it and briefly explain why.
Once the user finishes with a topic and moves onto a new one, clean up old completed todos by deleting them.
**Research quality**
### Research quality
Consult multiple sources when possible and cross-reference key claims.
When sources disagree, note the discrepancy and explain which source you consider more reliable and why.
If a web page fails to load or a search returns irrelevant results, try alternative search queries or sources before moving on.
Track your sources you will need them when presenting results.
**Presenting results**
### Presenting results
When presenting your final findings:
- Use Markdown formatting for clarity.
- Use clear sections with headings for each major topic or sub-question.
- Cite your sources inline (e.g., "According to [source name](URL), ...").
- End with a brief summary of key takeaways.
- Save the final research report to file memory so it survives compaction and can be referenced later.
**File memory**
Use the FileMemory_* tools to:
- Store downloaded search results or web pages.
- Store plans.
- Read the current plan to make sure tasks were done according to plan.
- Store findings.
- Check for relevant previously downloaded data / findings before starting new research.
- In addition to returning the results to the user, save the final research report to file memory so it survives compaction and can be referenced later.
""";
// Create the agent using AsHarnessAgent, which pre-configures function invocation,
// per-service-call chat history persistence, and in-loop compaction.
// Then wrap with UseToolApproval to allow auto-approving tools once confirmed.
// per-service-call chat history persistence, in-loop compaction, TodoProvider, AgentModeProvider,
// FileMemoryProvider, ToolApproval, WebSearch, AgentSkillsProvider, and OpenTelemetry.
// Only custom instructions, a WebBrowsingTool, and FileAccess opt-out are needed.
AIAgent agent =
// Create an OpenAIClient that communicates with the Foundry responses service.
new OpenAIClient(
@@ -127,35 +74,26 @@ AIAgent agent =
RetryPolicy = new ClientRetryPolicy(3) // Enable retries to improve resiliency.
})
.GetResponsesClient()
.AsIChatClientWithStoredOutputDisabled(deploymentName) // We want to manage chat history locally (not stored in the responses service), so that we can manage compaction ourselves.
.AsIChatClientWithStoredOutputDisabled(deploymentName) // We want to manage chat history locally (not stored in the responses service), so that we can manage compaction ourselves.
.AsHarnessAgent(MaxContextWindowTokens, MaxOutputTokens, new HarnessAgentOptions
{
Name = "ResearchAgent",
Description = "A research assistant that plans and executes research tasks.",
AIContextProviders =
[
new TodoProvider(), // Add an AIContextProvider to allow the agent to create a TODO list, which is stored in the session.
new AgentModeProvider(), // Add an AIContextProvider that tracks the agent mode and allows switching mode. Current mode is stored in the session.
new FileMemoryProvider( // Add an AIContextProvider that can store memories in files under a session specific working folder.
new FileSystemAgentFileStore(Path.Combine(AppContext.BaseDirectory, "agent-files")),
(_) => new FileMemoryState() { WorkingFolder = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss") + "_" + Guid.NewGuid().ToString() })
],
DisableFileMemory = true, // If enabled, this would allow the agent to store memories as files in a directory associated with the current session
FileMemoryStore = new FileSystemAgentFileStore( // Configure the file memory provider to store files in a local folder called "agent-files".
Path.Combine(AppContext.BaseDirectory, "agent-files")),
ChatOptions = new ChatOptions
{
Instructions = instructions,
Tools =
[
ResponseTool.CreateWebSearchTool().AsAITool(), // Add the foundry hosted web search tool that runs in the service.
new WebBrowsingTool( // Add a local web browsing tool that converts html to markdown.
new WebBrowsingTool( // Add a local web browsing tool that converts html to markdown.
new WebBrowsingToolOptions { AllowPublicNetworks = true }),
],
MaxOutputTokens = MaxOutputTokens, // Set a high token limit for long research tasks with many tool calls and long outputs.
MaxOutputTokens = MaxOutputTokens, // Set a high token limit for long research tasks with many tool calls and long outputs.
Reasoning = new() { Effort = ReasoningEffort.Medium },
},
})
.AsBuilder()
.UseToolApproval() // Add the ability to auto approve tools once a user has said they don't want to be asked again. Approval rules are tied to the session.
.Build();
});
// Run the interactive console session using the shared HarnessConsole helper.
await HarnessConsole.RunAgentAsync(
@@ -163,12 +101,14 @@ await HarnessConsole.RunAgentAsync(
userPrompt: "Enter a research topic to get started.",
new HarnessConsoleOptions
{
Observers = HarnessConsoleOptions.BuildObserversWithPlanning(
agent,
planModeName: "plan",
executionModeName: "execute",
maxContextWindowTokens: MaxContextWindowTokens,
maxOutputTokens: MaxOutputTokens,
toolFormatters: [new DownloadUriToolFormatter(), .. ToolCallFormatter.BuildDefaultToolFormatters()]),
Observers = [
new OpenAIResponsesWebSearchDisplayObserver(),
.. HarnessConsoleOptions.BuildObserversWithPlanning(
agent,
planModeName: "plan",
executionModeName: "execute",
maxContextWindowTokens: MaxContextWindowTokens,
maxOutputTokens: MaxOutputTokens,
toolFormatters: [new DownloadUriToolFormatter(), .. ToolCallFormatter.BuildDefaultToolFormatters()])],
CommandHandlers = HarnessConsoleOptions.BuildDefaultCommandHandlers(agent),
});
@@ -1,9 +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
// equipped with Foundry's hosted web search tool.
// 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 background agent.
//
// Special commands:
// /exit — End the session.
@@ -25,8 +26,9 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYME
const int MaxContextWindowTokens = 1_050_000;
const int MaxOutputTokens = 128_000;
// --- Sub-agent: Web Search Agent ---
// This agent can search the web and is used by the parent agent to look up stock prices.
// --- 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 =
new OpenAIClient(
new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"),
@@ -41,40 +43,44 @@ AIAgent webSearchAgent =
{
Name = "WebSearchAgent",
Description = "An agent that can search the web to find information.",
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
DisableFileAccess = true, // If enabled, this would allow the agent to read/write files in a working directory
DisableToolApproval = true, // If enabled, this allows don't-ask-again approval functionality.
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.
// 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.
""";
// --- Parent agent: Stock Price Researcher ---
// This agent orchestrates the sub-agent to look up stock prices in parallel.
// Most features are disabled since the parent only needs SubAgentsProvider.
AIAgent parentAgent =
new OpenAIClient(
new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"),
@@ -88,10 +94,16 @@ 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
DisableFileAccess = true, // If enabled, this would allow the agent to read/write files in a working directory
DisableToolApproval = true, // If enabled, this allows don't-ask-again approval functionality.
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.
@@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<Content Include="data\**\*" CopyToOutputDirectory="PreserveNewest" />
<Content Include="working\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
@@ -1,10 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use a HarnessAgent with the FileAccessProvider
// This sample demonstrates how to use a HarnessAgent with the default FileAccessProvider
// to give an agent access to a folder of CSV data files. The agent can read, analyze,
// and extract information from the data, then write results back as new files.
//
// The sample includes a pre-populated `data/` folder with sales transaction data.
// The sample includes a pre-populated `working/` folder with sales transaction data.
// The HarnessAgent's default FileAccessProvider uses `{cwd}/working` as its working directory,
// which matches this sample's folder layout.
// Ask the agent to analyze the data, produce summaries, or create new output files.
//
// Special commands:
@@ -27,10 +29,6 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYME
const int MaxContextWindowTokens = 1_050_000;
const int MaxOutputTokens = 128_000;
// Point the file store at the data/ folder that ships with the sample.
var dataFolder = Path.Combine(AppContext.BaseDirectory, "data");
var fileStore = new FileSystemAgentFileStore(dataFolder);
var instructions =
"""
You are a data analyst assistant. You have access to a folder of data files via the FileAccess_* tools.
@@ -56,7 +54,9 @@ var instructions =
- Always explain what you learned and what you are going to do next between tool calls, so the user can follow along with your thought process.
""";
// Create the chat client from the OpenAI provider.
// Create the agent using AsHarnessAgent. The FileAccessStore is explicitly set to the
// sample's working/ folder (copied to the output directory) so it works regardless of cwd.
// Unused features are disabled.
AIAgent agent =
new OpenAIClient(
new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"),
@@ -71,10 +71,11 @@ AIAgent agent =
{
Name = "DataAnalyst",
Description = "A data analyst assistant that reads, analyzes, and processes data files.",
AIContextProviders =
[
new FileAccessProvider(fileStore),
],
FileAccessStore = new FileSystemAgentFileStore(Path.Combine(AppContext.BaseDirectory, "working")),
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
DisableWebSearch = true,
ChatOptions = new ChatOptions
{
Instructions = instructions,
@@ -1,11 +1,11 @@
# What this sample demonstrates
This sample demonstrates how to use a `HarnessAgent` with the `FileAccessProvider` to give an agent access to a folder of data files for reading, analyzing, and writing results. The `HarnessAgent` pre-configures function invocation, per-service-call chat history persistence, and in-loop compaction — so the sample only needs to supply the chat client, token limits, and application-specific options.
This sample demonstrates how to use a `HarnessAgent` with the default `FileAccessProvider` to give an agent access to a folder of data files for reading, analyzing, and writing results. The `HarnessAgent` pre-configures function invocation, per-service-call chat history persistence, in-loop compaction, tool approval, and OpenTelemetry — so the sample only needs to supply the chat client, token limits, custom instructions, and opt out of unused features.
Key features showcased:
- **HarnessAgent** — a pre-configured agent that wraps a `ChatClientAgent` with function invocation, per-service-call persistence, and context-window compaction
- **FileAccessProvider** — gives the agent tools to read, write, list, search, and delete files in a shared data folder
- **FileAccessProvider** — the HarnessAgent's default file access provider uses `{cwd}/working` as its working directory, matching this sample's `working/` folder
- **CSV data processing** — the agent reads sales transaction data and performs analysis on demand
- **Output file creation** — the agent can write summaries, filtered data, or reports back to the data folder
- **Streaming output** — responses are streamed token-by-token for a natural experience
@@ -39,7 +39,7 @@ dotnet run --project samples/02-agents/Harness/Harness_Step03_DataProcessing
## What to Expect
The sample starts an interactive conversation with a data analyst agent. The `data/` folder contains a `sales.csv` file with ~50 rows of sales transaction data (date, product, category, quantity, unit price, region, salesperson).
The sample starts an interactive conversation with a data analyst agent. The `working/` folder contains a `sales.csv` file with ~50 rows of sales transaction data (date, product, category, quantity, unit price, region, salesperson).
You can ask the agent to:
@@ -53,7 +53,7 @@ E.g. try the following prompt `Please process the sales.csv file by first filter
## Sample Data
The included `data/sales.csv` contains sales transactions from January to March 2025 with the following columns:
The included `working/sales.csv` contains sales transactions from January to March 2025 with the following columns:
| Column | Description |
| --- | --- |
+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 |