chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,22 @@
<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.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Harness\Microsoft.Agents.AI.Harness.csproj" />
<ProjectReference Include="..\..\Harness_Shared_Console\Harness_Shared_Console.csproj" />
<ProjectReference Include="..\..\Harness_Shared_Console_OpenAI\Harness_Shared_Console_OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,111 @@
// Copyright (c) Microsoft. All rights reserved.
// "Meet your agent harness and claw" — Post 1 of the "Build your own claw with Microsoft Agent Framework" series.
// See: https://devblogs.microsoft.com/agent-framework/meet-your-agent-harness-and-claw.
//
// This sample builds the foundation of a personal finance / investing assistant on top of a
// HarnessAgent. The harness comes pre-configured with function invocation, per-service-call
// history persistence, and planning (TodoProvider + AgentModeProvider), plus web search — so
// all we add here is:
// 1. Finance-focused instructions.
// 2. A custom get_stock_price function tool.
//
// The agent can plan a multi-step request ("Review my watchlist and recommend some stocks to add"), create a todo list, switch
// between plan and execute modes, search the web for market news, and call our stock-price tool.
//
// Special commands (handled by the shared HarnessConsole):
// /todos — Display the current todo list without invoking the agent.
// /mode — Get or set the current agent mode.
// /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.AI.Projects;
using Azure.Identity;
using ClawSample;
using Harness.Shared.Console;
using Harness.Shared.Console.OpenAI;
using Harness.Shared.Console.ToolFormatters;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4";
// <instructions>
var instructions =
"""
## Personal Finance Assistant Instructions
You are a personal finance and investing assistant. You help the user understand their
watchlist and the markets. When asked about a stock, look up its current price with the
get_stock_price tool, and use web search for recent news, earnings, or analyst commentary.
### Working style
- Always verify numbers with a tool rather than relying on memory. Stock prices change.
- Cite web sources inline when you use them.
- Keep the user's watchlist in a memory file called watchlist.md: read it when reviewing the
watchlist, and update it whenever the user adds or removes a ticker.
### Important
You provide information and analysis only you are not a licensed financial advisor and you
must not present your output as personalized investment advice. Remind the user to do their
own research before making decisions.
""";
// </instructions>
// <create_client>
// Construct an IChatClient. Here we use a Microsoft Foundry project: the endpoint points at the
// project, DefaultAzureCredential handles auth, and the deployment name selects the model.
// The harness works with ANY IChatClient — see the AgentProviders samples for OpenAI, Azure
// OpenAI, Anthropic, Google Gemini, Ollama, ONNX, and more.
IChatClient chatClient =
new AIProjectClient(
new Uri(endpoint),
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
new DefaultAzureCredential(),
new AIProjectClientOptions { RetryPolicy = new ClientRetryPolicy(3) }) // Enable retries to improve resiliency.
.GetProjectOpenAIClient()
.GetResponsesClient()
.AsIChatClient(deploymentName);
// </create_client>
// <create_agent>
// Turn the chat client into a HarnessAgent. AsHarnessAgent pre-configures function invocation,
// per-service-call chat history persistence, TodoProvider, AgentModeProvider, and web search.
// We add finance instructions and our get_stock_price tool.
AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
ChatOptions = new ChatOptions
{
Instructions = instructions,
Tools = [StockTools.CreateGetStockPriceTool()],
Reasoning = new() { Effort = ReasoningEffort.Medium },
},
});
// </create_agent>
// <run>
// Run the interactive console session using the shared HarnessConsole helper.
await HarnessConsole.RunAgentAsync(
agent,
userPrompt: "Ask about a stock or say 'Review my watchlist and recommend some stocks to add' to get started.",
new HarnessConsoleOptions
{
Observers = [
new OpenAIResponsesWebSearchDisplayObserver(),
new OpenAIResponsesErrorObserver(),
.. HarnessConsoleOptions.BuildObserversWithPlanning(
agent,
planModeName: "plan",
executionModeName: "execute",
toolFormatters: ToolCallFormatter.BuildDefaultToolFormatters())],
CommandHandlers = HarnessConsoleOptions.BuildDefaultCommandHandlers(agent),
});
// </run>
@@ -0,0 +1,52 @@
# Meet your claw (Post 1) — .NET
The first runnable sample from the [**"Build your own agent harness and claw with Microsoft Agent Framework"** blog](https://devblogs.microsoft.com/agent-framework/build-your-own-claw-and-agent-harness-with-microsoft-agent-framework)
series. It builds the foundation of a personal finance / investing assistant on top of a
`HarnessAgent`.
## What this sample demonstrates
- **`AsHarnessAgent`** — turns an `IChatClient` into a batteries-included agent: function
invocation, per-service-call history persistence, planning
(`TodoProvider` + `AgentModeProvider`), and web search.
- **A custom function tool** — `get_stock_price` (see `StockTools.cs`), exposing local data to the
agent. Prices are illustrative mock data, not real quotes.
- **Web search** — provided automatically by the harness for market news and commentary.
- **Planning & modes** — the agent breaks a multi-step request ("Review my watchlist and recommend some stocks to add") into a todo
list and switches between *plan* and *execute* modes.
- **Shared harness console** — interactive streaming UI with `/todos`, `/mode`, and `/exit`
commands.
## Prerequisites
1. A Microsoft Foundry project with a deployed model (e.g. `gpt-5.4`).
2. Azure CLI installed and authenticated (`az login`).
## Environment variables
```bash
export FOUNDRY_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/api/projects/your-project"
# Optional (defaults to gpt-5.4)
export FOUNDRY_MODEL="gpt-5.4"
```
## Running
```bash
cd dotnet
dotnet run --project samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step01_MeetYourClaw
```
## What to expect
The sample starts an interactive loop. Try these in order:
1. `/mode execute` — switch out of the default plan mode; quick lookups don't need a plan.
2. `What's the price of MSFT?` — the agent calls the `get_stock_price` tool.
3. `Any recent news on NVDA?` — the agent uses web search.
4. `Add MSFT, NVDA and SPY to my watch list` — saved to `watchlist.md` in the session's memory.
5. `/mode plan` — switch back to plan mode for a bigger, multi-step task.
6. `Review my watchlist and recommend some stocks to add` — the agent plans, then executes. Type
`/todos` to see the list and `/mode` to inspect the current mode.
Output is colored by mode: **cyan** during planning, **green** during execution.
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using Microsoft.Extensions.AI;
namespace ClawSample;
/// <summary>
/// A custom function tool that gives our "claw" access to (illustrative) stock prices.
/// </summary>
/// <remarks>
/// The prices returned here are mock data for demonstration purposes only and are not real
/// market quotes. In a real assistant you would call a market-data API instead.
/// </remarks>
internal static class StockTools
{
// <stock_quote>
/// <summary>A delayed, illustrative stock quote.</summary>
public sealed record StockQuote(string Symbol, decimal Price, string Currency, DateTimeOffset AsOf);
// </stock_quote>
// A tiny in-memory price book so the sample runs without any external dependency.
private static readonly Dictionary<string, decimal> s_priceBook = new(StringComparer.OrdinalIgnoreCase)
{
["MSFT"] = 462.97m,
["AAPL"] = 229.35m,
["GOOGL"] = 178.12m,
["AMZN"] = 201.45m,
["NVDA"] = 134.81m,
};
// <get_stock_price>
/// <summary>
/// Gets the latest (delayed, illustrative) stock price for a ticker symbol.
/// </summary>
/// <param name="symbol">The stock ticker symbol, e.g. <c>MSFT</c> or <c>AAPL</c>.</param>
[Description("Gets the latest (delayed, illustrative) stock price for a ticker symbol.")]
public static StockQuote GetStockPrice(
[Description("The stock ticker symbol, e.g. MSFT or AAPL.")] string symbol)
{
if (!s_priceBook.TryGetValue(symbol, out var price))
{
// Deterministic pseudo-price for unknown symbols so the sample stays self-contained.
// Derive a stable seed from the characters — string.GetHashCode() is randomized per
// process and Math.Abs(int.MinValue) throws, so neither is safe for repeatable output.
var seed = 0;
foreach (var ch in symbol.ToUpperInvariant())
{
seed = (seed * 31 + ch) % 1_000_000;
}
price = 50m + seed % 45000 / 100m;
}
return new StockQuote(symbol.ToUpperInvariant(), price, "USD", DateTimeOffset.UtcNow);
}
// </get_stock_price>
/// <summary>Creates the <see cref="AIFunction"/> wrapper used to expose the tool to the agent.</summary>
public static AIFunction CreateGetStockPriceTool() => AIFunctionFactory.Create(GetStockPrice, "get_stock_price");
}
@@ -0,0 +1,26 @@
<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.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Harness\Microsoft.Agents.AI.Harness.csproj" />
<ProjectReference Include="..\..\Harness_Shared_Console\Harness_Shared_Console.csproj" />
<ProjectReference Include="..\..\Harness_Shared_Console_OpenAI\Harness_Shared_Console_OpenAI.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="working\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
@@ -0,0 +1,180 @@
// Copyright (c) Microsoft. All rights reserved.
// "Working with your data, safely" — Post 2 of the "Build your own claw and agent harness with Microsoft Agent Framework" series.
// See: https://devblogs.microsoft.com/agent-framework/agent-harness-working-with-your-data-safely.
//
// This sample builds on Post 1's personal finance assistant and adds three abilities:
// 1. File access — read the user's portfolio.csv and write report files (file_access_* tools).
// 2. Approvals — the place_trade tool is wrapped so it requires human approval before running.
// 3. Durable memory — two complementary kinds:
// * File memory (coarse-grained, explicit) — the agent reads/writes files like
// watchlist.md. Its files live on disk under {cwd}/agent-file-memory/<session-id>/,
// so they persist across runs on this machine; /session-export and /session-import
// preserve the session id so a relaunched session re-links to its memory files.
// * Foundry memory (fine-grained, automatic) — Microsoft Foundry extracts durable facts
// (e.g. the user's risk tolerance) from the conversation. Opt-in: enabled
// only when FOUNDRY_MEMORY_STORE and FOUNDRY_EMBEDDING_MODEL are set.
//
// Special commands (handled by the shared HarnessConsole):
// /todos — Display the current todo list without invoking the agent.
// /mode — Get or set the current agent mode.
// /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.AI.Projects;
using Azure.Identity;
using ClawSample;
using Harness.Shared.Console;
using Harness.Shared.Console.OpenAI;
using Harness.Shared.Console.ToolFormatters;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
using Microsoft.Extensions.AI;
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4";
// <instructions>
var instructions =
"""
## Personal Finance Assistant Instructions
You are a personal finance and investing assistant. You help the user understand their
portfolio and watchlist, and you can place trades on their behalf.
### Working style
- The user's holdings live in a file called portfolio.csv. Read it with the file_access tools
before answering questions about their portfolio, and never modify it unless asked.
- When asked for a report or analysis, write it to a Markdown file with the file_access tools
(e.g. reports/portfolio-review.md) and tell the user where you saved it.
- Keep the user's watchlist in a memory file called watchlist.md: read it when reviewing the
watchlist, and update it whenever the user adds or removes a ticker.
- To buy or sell, use the place_trade tool. This takes a real action, so the user will be
asked to approve it before it runs explain what you are about to do first.
- Remember durable facts the user tells you about themselves (risk tolerance, goals,
preferences) and take them into account when giving analysis.
### Important
You provide information and analysis only you are not a licensed financial advisor and you
must not present your output as personalized investment advice. Remind the user to do their
own research before making decisions.
""";
// </instructions>
// <create_client>
// Construct an IChatClient backed by a Microsoft Foundry project (see Post 1 for details).
var projectClient = new AIProjectClient(
new Uri(endpoint),
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
new DefaultAzureCredential(),
new AIProjectClientOptions { RetryPolicy = new ClientRetryPolicy(3) });
IChatClient chatClient = projectClient
.GetProjectOpenAIClient()
.GetResponsesClient()
.AsIChatClient(deploymentName);
// </create_client>
// <memory>
// Fine-grained Foundry memory is opt-in: it needs a memory store and an embedding model. When the
// environment is configured we add a FoundryMemoryProvider scoped to a single user, so the facts it
// extracts are recalled across sessions. Otherwise we fall back to file memory only.
var memoryStoreName = Environment.GetEnvironmentVariable("FOUNDRY_MEMORY_STORE");
var embeddingModel = Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_MODEL");
FoundryMemoryProvider? foundryMemory = null;
if (!string.IsNullOrWhiteSpace(memoryStoreName) && !string.IsNullOrWhiteSpace(embeddingModel))
{
foundryMemory = new FoundryMemoryProvider(
projectClient,
memoryStoreName,
// In a real world scenario, "claw-sample-user" should be replaced with a unique identifier
// for the active user.
// To tie memories to the session, replace "claw-sample-user" with Guid.NewGuid().ToString().
// stateInitializer is called once per session to define the scope for the memory provider.
stateInitializer: _ => new(new FoundryMemoryProviderScope("claw-sample-user")),
new FoundryMemoryProviderOptions()
{
// For demo purposes, configure the memory provider to extract facts immediately
// from each message. In a real-world scenario, you may want to set this to a higher value.
UpdateDelay = 0,
});
// Create the memory store on first use (no-op if it already exists).
await foundryMemory.EnsureMemoryStoreCreatedAsync(
deploymentName,
embeddingModel,
"Durable memory for the Build-your-own-claw finance assistant.");
Console.WriteLine($"Foundry memory enabled (store: {memoryStoreName}).");
}
else
{
Console.WriteLine("Foundry memory disabled. Set FOUNDRY_MEMORY_STORE and FOUNDRY_EMBEDDING_MODEL to enable it.");
}
// </memory>
// <create_agent>
// Turn the chat client into a HarnessAgent. On top of Post 1's defaults we point file access at a
// fixed folder, add our approval-gated place_trade tool, and (optionally) wire in the Foundry
// memory provider for automatic, fine-grained fact extraction. File memory keeps its on-disk default
// store (see below), and we don't point it at a custom folder.
AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
// File access: read portfolio.csv and write reports under the sample's working/ folder.
FileAccessStore = new FileSystemAgentFileStore(Path.Combine(AppContext.BaseDirectory, "working")),
// Auto-approve the read-only file tools so reading portfolio.csv is frictionless, while saving,
// deleting, and place_trade still pause for explicit approval.
ToolApprovalAgentOptions = new ToolApprovalAgentOptions
{
AutoApprovalRules = [FileAccessProvider.ReadOnlyToolsAutoApprovalRule],
},
// Start in "execute" mode: this assistant is mostly quick lookups and single actions, so a plan
// would be overkill. (Planning is still available — switch any time with /mode plan.)
AgentModeProviderOptions = new AgentModeProviderOptions { DefaultMode = "execute" },
// File memory is on by default. Its files live on disk under {cwd}/agent-file-memory/<session-id>/,
// so they persist across runs on this machine. A brand-new session gets a new id (and so an empty
// memory); /session-export and /session-import preserve the session's identity so a relaunched
// session re-links to its existing on-disk memory files. The export file holds session state, not
// the memory files themselves.
// Fine-grained, automatic memory (when configured).
AIContextProviders = foundryMemory is null ? null : [foundryMemory],
ChatOptions = new ChatOptions
{
Instructions = instructions,
Tools =
[
StockTools.CreateGetStockPriceTool(),
TradingTools.CreatePlaceTradeTool(),
],
Reasoning = new() { Effort = ReasoningEffort.Medium },
},
});
// </create_agent>
// <run>
// Run the interactive console session. The default planning observers already include a tool
// approval observer, so the place_trade approval prompt is surfaced automatically.
await HarnessConsole.RunAgentAsync(
agent,
userPrompt: "Ask me to review your portfolio, draft a report, update your watchlist, or place a trade.",
new HarnessConsoleOptions
{
Observers = [
new OpenAIResponsesWebSearchDisplayObserver(),
new OpenAIResponsesErrorObserver(),
.. HarnessConsoleOptions.BuildObserversWithPlanning(
agent,
planModeName: "plan",
executionModeName: "execute",
toolFormatters: ToolCallFormatter.BuildDefaultToolFormatters())],
CommandHandlers = HarnessConsoleOptions.BuildDefaultCommandHandlers(agent),
});
// </run>
@@ -0,0 +1,73 @@
# Working with your data, safely (Post 2) — .NET
The second runnable sample from the [**"Build your own claw and agent harness with Microsoft Agent Framework"** blog](https://devblogs.microsoft.com/agent-framework/build-your-own-claw-and-agent-harness-with-microsoft-agent-framework)
series ([Part 2 — Working with your data, safely](https://devblogs.microsoft.com/agent-framework/agent-harness-working-with-your-data-safely)).
It builds on Post 1's personal finance assistant and teaches it to work with *your* data safely.
## What this sample demonstrates
- **File access** — the agent reads a pre-populated `working/portfolio.csv` and writes reports
(e.g. `reports/portfolio-review.md`) with the built-in `file_access_*` tools. A custom
`FileAccessStore` roots those tools at the sample's `working/` folder.
- **Approvals** — file-access tools require approval by default, but the sample wires the built-in
`FileAccessProvider.ReadOnlyToolsAutoApprovalRule` so reads/lists/searches are frictionless while
saving and deleting still pause for approval. The `place_trade` tool is also wrapped in an
`ApprovalRequiredAIFunction` (see `TradingTools.cs`), so the harness surfaces an approval prompt
before any trade runs. The trade itself is simulated — no real order is placed.
- **Durable memory, two ways:**
- **File memory** (coarse-grained, explicit) — the agent reads/writes files such as
`watchlist.md`. File memory is on by default; its files live on disk under
`{cwd}/agent-file-memory/<session-id>/`, so they persist across runs on this machine. A new
session starts empty; use `/session-export` and `/session-import` to preserve the session id so a
relaunch re-links to its memory files (no fixed folder required).
- **Foundry memory** (fine-grained, automatic) — Microsoft Foundry extracts durable facts (e.g.
your risk tolerance) from the conversation. Opt-in; see below.
## Prerequisites
1. A Microsoft Foundry project with a deployed model (e.g. `gpt-5.4`).
2. Azure CLI installed and authenticated (`az login`).
3. *(Optional, for Foundry memory)* a deployed embedding model and a memory store name.
## Environment variables
```bash
export FOUNDRY_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/api/projects/your-project"
# Optional (defaults to gpt-5.4)
export FOUNDRY_MODEL="gpt-5.4"
# Optional — enable fine-grained Foundry memory (both must be set):
export FOUNDRY_MEMORY_STORE="claw-finance-memory"
export FOUNDRY_EMBEDDING_MODEL="text-embedding-3-small"
```
When the Foundry memory variables are not set, the sample runs with file memory only and prints a
note.
## Running
```bash
cd dotnet
dotnet run --project samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData
```
## What to expect
The sample starts an interactive loop in **execute** mode (quick lookups don't need a plan). Try
these in order:
1. `What's in my portfolio?` — the agent reads `portfolio.csv` with the file_access tools.
2. `Write me a short report on my portfolio and save it.` — the agent writes a Markdown file
under `working/`; saving is a write, so **you are prompted to approve** before it lands.
3. `I'm a conservative investor saving for a house in two years.` — a durable fact (recalled later
by Foundry memory when enabled).
4. `Buy 10 shares of MSFT.` — the agent calls `place_trade`; **you are prompted to approve or
deny** before it runs.
5. `Add SPY to my watchlist.` — saved to `watchlist.md` in file memory.
Restart the app to see memory persist across runs:
- **Foundry memory** (when enabled) recalls facts about you in any new session — it's scoped to you,
not the session.
- **File memory** (the watchlist) lives on disk keyed by session id, so `/session-export` before you
quit and `/session-import` after relaunching to re-link the relaunched session to its files.
@@ -0,0 +1,58 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using Microsoft.Extensions.AI;
namespace ClawSample;
/// <summary>
/// A custom function tool that gives our "claw" access to (illustrative) stock prices.
/// </summary>
/// <remarks>
/// The prices returned here are mock data for demonstration purposes only and are not real
/// market quotes. In a real assistant you would call a market-data API instead.
/// </remarks>
internal static class StockTools
{
/// <summary>A delayed, illustrative stock quote.</summary>
public sealed record StockQuote(string Symbol, decimal Price, string Currency, DateTimeOffset AsOf);
// A tiny in-memory price book so the sample runs without any external dependency.
private static readonly Dictionary<string, decimal> s_priceBook = new(StringComparer.OrdinalIgnoreCase)
{
["MSFT"] = 462.97m,
["AAPL"] = 229.35m,
["GOOGL"] = 178.12m,
["AMZN"] = 201.45m,
["NVDA"] = 134.81m,
["SPY"] = 612.40m,
};
/// <summary>
/// Gets the latest (delayed, illustrative) stock price for a ticker symbol.
/// </summary>
/// <param name="symbol">The stock ticker symbol, e.g. <c>MSFT</c> or <c>AAPL</c>.</param>
[Description("Gets the latest (delayed, illustrative) stock price for a ticker symbol.")]
public static StockQuote GetStockPrice(
[Description("The stock ticker symbol, e.g. MSFT or AAPL.")] string symbol)
{
if (!s_priceBook.TryGetValue(symbol, out var price))
{
// Deterministic pseudo-price for unknown symbols so the sample stays self-contained.
// Derive a stable seed from the characters — string.GetHashCode() is randomized per
// process and Math.Abs(int.MinValue) throws, so neither is safe for repeatable output.
var seed = 0;
foreach (var ch in symbol.ToUpperInvariant())
{
seed = (seed * 31 + ch) % 1_000_000;
}
price = 50m + seed % 45000 / 100m;
}
return new StockQuote(symbol.ToUpperInvariant(), price, "USD", DateTimeOffset.UtcNow);
}
/// <summary>Creates the <see cref="AIFunction"/> wrapper used to expose the tool to the agent.</summary>
public static AIFunction CreateGetStockPriceTool() => AIFunctionFactory.Create(GetStockPrice, "get_stock_price");
}
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using Microsoft.Extensions.AI;
namespace ClawSample;
/// <summary>
/// Sensitive "claw" tools that take real-world actions and therefore require human approval.
/// </summary>
/// <remarks>
/// These tools only simulate their effects (no orders are placed, no email is sent). They exist
/// to demonstrate how the harness gates risky actions behind an approval prompt.
/// </remarks>
internal static class TradingTools
{
// <place_trade>
/// <summary>
/// Places a (simulated) buy or sell order for a given symbol and quantity.
/// </summary>
/// <param name="symbol">The stock ticker symbol to trade, e.g. <c>MSFT</c>.</param>
/// <param name="action">Either <c>buy</c> or <c>sell</c>.</param>
/// <param name="quantity">The number of shares to trade.</param>
[Description("Places a buy or sell order for a given symbol and quantity.")]
public static string PlaceTrade(
[Description("The stock ticker symbol to trade, e.g. MSFT.")] string symbol,
[Description("Either 'buy' or 'sell'.")] string action,
[Description("The number of shares to trade.")] int quantity)
{
var isBuy = action.Equals("buy", StringComparison.OrdinalIgnoreCase);
var isSell = action.Equals("sell", StringComparison.OrdinalIgnoreCase);
if (!isBuy && !isSell)
{
return $"Invalid action '{action}'. Use 'buy' or 'sell'.";
}
if (quantity <= 0)
{
return $"Invalid quantity '{quantity}'. Quantity must be a positive whole number of shares.";
}
var verb = isSell ? "Sold" : "Bought";
var confirmation = $"TRADE-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
return $"{verb} {quantity} share(s) of {symbol.ToUpperInvariant()}. Confirmation: {confirmation}.";
}
// </place_trade>
/// <summary>
/// Creates an approval-required <see cref="AIFunction"/> for <see cref="PlaceTrade"/>.
/// Wrapping the function in <see cref="ApprovalRequiredAIFunction"/> tells the harness to
/// surface an approval request before the function ever runs.
/// </summary>
public static AIFunction CreatePlaceTradeTool() =>
new ApprovalRequiredAIFunction(AIFunctionFactory.Create(PlaceTrade, "place_trade"));
}
@@ -0,0 +1,7 @@
symbol,shares,cost_basis,purchase_date
MSFT,40,312.50,2023-02-14
AAPL,75,168.20,2022-11-03
NVDA,120,42.80,2021-06-21
AMZN,30,142.10,2023-09-12
GOOGL,25,128.45,2024-01-30
SPY,60,418.90,2024-05-08
1 symbol shares cost_basis purchase_date
2 MSFT 40 312.50 2023-02-14
3 AAPL 75 168.20 2022-11-03
4 NVDA 120 42.80 2021-06-21
5 AMZN 30 142.10 2023-09-12
6 GOOGL 25 128.45 2024-01-30
7 SPY 60 418.90 2024-05-08
@@ -0,0 +1,31 @@
<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" />
<PackageReference Include="Hyperlight.HyperlightSandbox.Guest.Python" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Harness\Microsoft.Agents.AI.Harness.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hyperlight\Microsoft.Agents.AI.Hyperlight.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Mcp\Microsoft.Agents.AI.Mcp.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Tools.Shell\Microsoft.Agents.AI.Tools.Shell.csproj" />
<ProjectReference Include="..\..\Harness_Shared_Console\Harness_Shared_Console.csproj" />
<ProjectReference Include="..\..\Harness_Shared_Console_OpenAI\Harness_Shared_Console_OpenAI.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="skills\**\*" CopyToOutputDirectory="PreserveNewest" />
<Content Include="working\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
@@ -0,0 +1,69 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net.Http.Headers;
using Azure.Core;
using ModelContextProtocol.Client;
namespace ClawSample;
/// <summary>
/// Helpers for wiring centrally-managed <b>Foundry skills</b> into the claw via a Foundry Toolbox
/// MCP endpoint. These are opt-in: skills published to the toolbox are discovered at runtime, so
/// they can be managed and updated without changing or redeploying the agent.
/// </summary>
internal static class FoundrySkills
{
/// <summary>
/// Connects to a Foundry Toolbox MCP endpoint and returns a connected <see cref="McpClient"/>.
/// The caller owns the returned client and its HTTP client.
/// </summary>
/// <param name="toolboxMcpServerUrl">The Foundry Toolbox MCP server URL.</param>
/// <param name="credential">Credential used to obtain a bearer token for the toolbox.</param>
/// <returns>The connected MCP client and the underlying HTTP client; both must be disposed by the caller.</returns>
public static async Task<(McpClient McpClient, HttpClient HttpClient)> ConnectAsync(
string toolboxMcpServerUrl,
TokenCredential credential)
{
var httpClient = new HttpClient(new BearerTokenHandler(credential, "https://ai.azure.com/.default")
{
InnerHandler = new HttpClientHandler(),
});
try
{
McpClient mcpClient = await McpClient.CreateAsync(
new HttpClientTransport(
new HttpClientTransportOptions
{
Endpoint = new Uri(toolboxMcpServerUrl),
Name = "foundry_toolbox",
TransportMode = HttpTransportMode.StreamableHttp,
AdditionalHeaders = new Dictionary<string, string>
{
["Foundry-Features"] = "Toolboxes=V1Preview",
},
},
httpClient));
return (mcpClient, httpClient);
}
catch
{
// The MCP client never took ownership of the HTTP client, so dispose it here.
httpClient.Dispose();
throw;
}
}
private sealed class BearerTokenHandler(TokenCredential credential, string scope) : DelegatingHandler
{
private readonly TokenRequestContext _tokenContext = new([scope]);
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
AccessToken token = await credential.GetTokenAsync(this._tokenContext, cancellationToken).ConfigureAwait(false);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,228 @@
// Copyright (c) Microsoft. All rights reserved.
// "Scaling its capabilities" — Post 3 of the "Build your own claw and agent harness with Microsoft
// Agent Framework" series.
// See: https://devblogs.microsoft.com/agent-framework/agent-harness-scaling-the-claw-or-harness-capabilities/.
//
// This sample builds on Post 2's personal finance assistant and makes it *more capable* in four ways:
// 1. Skills — package finance know-how (valuation, risk-scoring) as discoverable SKILL.md
// files the agent loads on demand. Optionally fold in centrally-managed Foundry
// skills from a Foundry Toolbox MCP endpoint (opt-in via FOUNDRY_TOOLBOX_MCP_SERVER_URL).
// 2. Shell — a sandboxed shell, confined to the trade-confirmation vault, that the agent
// uses to reorganize the accumulated confirmation files (year/month, rename,
// archive). Guarded by a deny-list policy and a confined working directory.
// 3. CodeAct — the agent writes and runs Python to crunch portfolio numbers, in a sandboxed
// Hyperlight micro-VM (needs hardware virtualization).
// 4. Background agents — fan out a per-ticker research sub-agent so several tickers are researched
// concurrently, then aggregated.
//
// Special commands (handled by the shared HarnessConsole):
// /todos — Display the current todo list without invoking the agent.
// /mode — Get or set the current agent mode.
// /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.AI.Projects;
using Azure.Identity;
using ClawSample;
using Harness.Shared.Console;
using Harness.Shared.Console.OpenAI;
using Harness.Shared.Console.ToolFormatters;
using HyperlightSandbox.Guest.Python;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hyperlight;
using Microsoft.Agents.AI.Tools.Shell;
using Microsoft.Extensions.AI;
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4";
// The two folders the claw works in: the working folder (portfolio.csv, reports) and the
// trade-confirmation "vault" inside it that the shell will reorganize.
var workingDir = Path.Combine(AppContext.BaseDirectory, "working");
var vaultDir = Path.Combine(workingDir, "confirmations");
var skillsDir = Path.Combine(AppContext.BaseDirectory, "skills");
// <instructions>
var instructions =
"""
## Personal Finance Assistant Instructions
You are a personal finance and investing assistant. You help the user understand their
portfolio and watchlist, value individual stocks, gauge portfolio risk, research the market,
and keep their records tidy.
### Working style
- The user's holdings live in a file called portfolio.csv. Read it with the file_access tools
before answering questions about their portfolio, and never modify it unless asked.
- You have skills for valuation and risk-scoring. When a question matches a skill, load it and
follow its instructions (read its references, run its scripts) rather than guessing.
- When asked to research several tickers, delegate each one to the background research agent so
they run concurrently, then summarize the findings together.
- The user's trade confirmations accumulate in the working/confirmations folder. When asked to
tidy or reorganize them, use the run_shell tool: inspect the folder first, then move files into
a year/month layout and rename them to YYYY-MM-DD_TICKER_BUY|SELL.txt. Explain your plan before
running commands that change anything.
- To buy or sell, use the place_trade tool. This takes a real action, so the user will be asked
to approve it before it runs explain what you are about to do first.
### Important
You provide information and analysis only you are not a licensed financial advisor and you
must not present your output as personalized investment advice. Remind the user to do their own
research before making decisions.
""";
// </instructions>
// <create_client>
// Construct an IChatClient backed by a Microsoft Foundry project (see Post 1 for details).
var credential = new DefaultAzureCredential();
var projectClient = new AIProjectClient(
new Uri(endpoint),
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
credential,
new AIProjectClientOptions { RetryPolicy = new ClientRetryPolicy(3) });
IChatClient chatClient = projectClient
.GetProjectOpenAIClient()
.GetResponsesClient()
.AsIChatClient(deploymentName);
// </create_client>
// <skills>
// The harness turns a skills provider on by default (it discovers SKILL.md files from the working
// directory). Here we build our own so we can point it at this sample's skills/ folder and, when
// configured, fold in centrally-managed Foundry skills — all behind one provider.
var skillsBuilder = new AgentSkillsProviderBuilder()
// File-based skills: valuation and risk-scoring. SubprocessScriptRunner runs their Python scripts.
.UseFileSkills([skillsDir], scriptRunner: new SubprocessScriptRunner().RunAsync);
// Foundry skills (opt-in): discovered live from a Foundry Toolbox MCP endpoint, so they can be
// managed and updated centrally without changing or redeploying this agent.
HttpClient? toolboxHttpClient = null;
ModelContextProtocol.Client.McpClient? toolboxMcpClient = null;
var toolboxUrl = Environment.GetEnvironmentVariable("FOUNDRY_TOOLBOX_MCP_SERVER_URL");
if (!string.IsNullOrWhiteSpace(toolboxUrl))
{
(toolboxMcpClient, toolboxHttpClient) = await FoundrySkills.ConnectAsync(toolboxUrl, credential);
skillsBuilder.UseMcpSkills(toolboxMcpClient);
Console.WriteLine("Foundry skills enabled (Toolbox MCP).");
}
else
{
Console.WriteLine("Foundry skills disabled. Set FOUNDRY_TOOLBOX_MCP_SERVER_URL to enable them.");
}
AgentSkillsProvider skillsProvider = skillsBuilder.Build();
// </skills>
// <background>
// Background agents: a lean, web-search-only research sub-agent. Passing it to the harness exposes
// the background_agents_* tools so the claw can start several research tasks concurrently and
// collect the results.
AIAgent researchAgent = ResearchAgent.Create(chatClient);
// </background>
// <shell>
// A sandboxed shell, confined to the trade-confirmation vault. ConfineWorkingDirectory re-anchors
// every command to the vault, and the deny-list policy pre-filters obviously destructive commands.
// (Patterns are a UX guardrail, not a security boundary — for hard isolation use DockerShellExecutor.)
await using var shell = new LocalShellExecutor(new LocalShellExecutorOptions
{
WorkingDirectory = vaultDir,
ConfineWorkingDirectory = true,
Policy = new ShellPolicy(denyList:
[
@"\brm\s+-rf\b",
@"\bsudo\b",
@":\(\)\s*\{", // fork-bomb shape
@"\bmkfs\b",
@">\s*/dev/sd",
]),
Timeout = TimeSpan.FromSeconds(15),
});
// </shell>
// <codeact>
// CodeAct: a sandboxed Python interpreter the model can write and run code in to crunch numbers.
// It runs on Hyperlight (a micro-VM, so it needs hardware virtualization). The guest module path is
// resolved automatically from the Hyperlight.HyperlightSandbox.Guest.Python NuGet package.
using var codeAct = new HyperlightCodeActProvider(HyperlightCodeActProviderOptions.CreateForWasm(PythonGuestModule.GetModulePath()));
// </codeact>
// <create_agent>
// Turn the chat client into a HarnessAgent. On top of Post 2's file access and approvals we add the
// four "scaling" capabilities: skills (our own provider), background agents, a confined shell, and
// CodeAct.
List<AIContextProvider> contextProviders = [skillsProvider, codeAct];
AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
// File access: portfolio.csv, reports, and the confirmations vault all live under working/.
FileAccessStore = new FileSystemAgentFileStore(workingDir),
// We supply our own skills provider (file + optional Foundry), so turn off the default one.
DisableAgentSkillsProvider = true,
// Fan-out research is delegated to this background agent.
BackgroundAgents = [researchAgent],
// The confined shell, exposed as the approval-gated run_shell tool.
ShellExecutor = shell,
// Keep reading the portfolio frictionless while writes, trades, and shell commands still prompt.
ToolApprovalAgentOptions = new ToolApprovalAgentOptions
{
AutoApprovalRules = [FileAccessProvider.ReadOnlyToolsAutoApprovalRule],
},
// Start in "execute" mode for quick lookups and actions; switch any time with /mode plan.
AgentModeProviderOptions = new AgentModeProviderOptions { DefaultMode = "execute" },
// Our skills provider plus CodeAct.
AIContextProviders = contextProviders,
ChatOptions = new ChatOptions
{
Instructions = instructions,
Tools =
[
StockTools.CreateGetStockPriceTool(),
TradingTools.CreatePlaceTradeTool(),
],
Reasoning = new() { Effort = ReasoningEffort.Medium },
},
});
// </create_agent>
try
{
// <run>
// Run the interactive console session. The default planning observers already include a tool
// approval observer, so the place_trade and run_shell approval prompts are surfaced automatically.
await HarnessConsole.RunAgentAsync(
agent,
userPrompt: "Ask me to value a stock, score your portfolio risk, research some tickers, or tidy your trade confirmations.",
new HarnessConsoleOptions
{
Observers = [
new OpenAIResponsesWebSearchDisplayObserver(),
new OpenAIResponsesErrorObserver(),
.. HarnessConsoleOptions.BuildObserversWithPlanning(
agent,
planModeName: "plan",
executionModeName: "execute",
toolFormatters: ToolCallFormatter.BuildDefaultToolFormatters())],
CommandHandlers = HarnessConsoleOptions.BuildDefaultCommandHandlers(agent),
});
// </run>
}
finally
{
codeAct?.Dispose();
if (toolboxMcpClient is not null)
{
await toolboxMcpClient.DisposeAsync().ConfigureAwait(false);
}
toolboxHttpClient?.Dispose();
}
@@ -0,0 +1,80 @@
# Scaling its capabilities (Post 3) — .NET
The third runnable sample from the [**"Build your own claw and agent harness with Microsoft Agent Framework"** blog](https://devblogs.microsoft.com/agent-framework/build-your-own-claw-and-agent-harness-with-microsoft-agent-framework)
series ([Part 3 — Scaling its capabilities](https://devblogs.microsoft.com/agent-framework/agent-harness-scaling-the-claw-or-harness-capabilities/)).
It builds on Post 2's personal finance assistant and makes it *more capable* along four axes.
## What this sample demonstrates
- **Skills** — finance know-how (`valuation`, `risk-scoring`) is packaged as discoverable `SKILL.md`
files under `skills/`, which the agent loads on demand. The sample builds its own provider with
`AgentSkillsProviderBuilder.UseFileSkills([skillsDir], scriptRunner: new SubprocessScriptRunner().RunAsync)`
so the skills' Python scripts can run, and sets `DisableAgentSkillsProvider = true` to replace the
harness default. Optionally folds in centrally-managed **Foundry skills** discovered live from a
Foundry **Toolbox MCP** endpoint via `FoundrySkills.ConnectAsync(...)` + `UseMcpSkills(...)`
(opt-in; see below).
- **Shell** — a `LocalShellExecutor` confined to the trade-confirmation vault
(`working/confirmations/`) lets the agent tidy the accumulated confirmation files (reorganize into
`year/month`, rename to `YYYY-MM-DD_TICKER_BUY|SELL.txt`). `ConfineWorkingDirectory` re-anchors
every command to the vault and a `ShellPolicy` deny-list pre-filters obviously destructive
commands. Exposed as the `run_shell` tool, which prompts for approval before each command runs.
(The deny-list is a UX guardrail, not a security boundary — for hard isolation use a
`DockerShellExecutor`.)
- **CodeAct** — a `HyperlightCodeActProvider` gives the agent a sandboxed Python interpreter to
crunch portfolio numbers by writing and running code. It runs on Hyperlight (a micro-VM), so it
requires hardware virtualization. The guest module path is resolved automatically from the
`Hyperlight.HyperlightSandbox.Guest.Python` NuGet package via `PythonGuestModule.GetModulePath()`.
- **Background agents** — a lean, web-search-only `ResearchAgent` is registered via
`HarnessAgentOptions.BackgroundAgents`, exposing the `background_agents_*` tools so the main agent
can fan out per-ticker research concurrently and aggregate the findings.
## Prerequisites
1. A Microsoft Foundry project with a deployed model (e.g. `gpt-5.4`).
2. Azure CLI installed and authenticated (`az login`).
3. *(For CodeAct)* a host with hardware virtualization enabled (Hyperlight runs the Python
interpreter in a micro-VM).
## Environment variables
```bash
export FOUNDRY_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/api/projects/your-project"
# Optional (defaults to gpt-5.4)
export FOUNDRY_MODEL="gpt-5.4"
# Optional — enable centrally-managed Foundry skills (Foundry Toolbox MCP endpoint URL):
export FOUNDRY_TOOLBOX_MCP_SERVER_URL="https://your-project.services.ai.azure.com/.../toolboxes/your-toolbox/mcp?api-version=v1"
```
When `FOUNDRY_TOOLBOX_MCP_SERVER_URL` is not set, the sample runs with the local file skills only and
prints a note.
## Running
```bash
cd dotnet
dotnet run --project samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step03_ScalingCapabilities
```
## What to expect
The sample starts an interactive loop in **execute** mode (quick lookups don't need a plan). Try
these in order:
1. `Value MSFT for me.` — the agent loads the `valuation` skill and follows its instructions
(reading references and running its script).
2. `Score the risk of my portfolio.` — the agent reads `portfolio.csv` and loads the `risk-scoring`
skill.
3. `/mode plan`, then `Tidy up my trade confirmations.` — switching to plan mode first makes the
agent inspect `working/confirmations/` and propose a reorganization plan before touching anything;
once you approve it switches to execute and uses the shell to reorganize and rename the files,
**prompting you to approve** each command.
4. `Work out the total value of my portfolio.` — the agent writes and runs Python via CodeAct.
5. `Research MSFT, NVDA and SPY and summarize the latest news.` — the agent fans the tickers out to
the background research agent and aggregates the results.
6. `What's the capital of France?` — with a `financial-agent-rules` skill published to your Foundry
toolbox and Foundry skills enabled (`FOUNDRY_TOOLBOX_MCP_SERVER_URL`), the agent loads it,
recognizes the question is off-topic, and politely declines, steering you back to finance.
See the [Part 3 blog post](https://devblogs.microsoft.com/agent-framework/agent-harness-scaling-the-claw-or-harness-capabilities/)
for more on the `financial-agent-rules` skill — including the SKILL.md to publish to your Foundry toolbox.
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace ClawSample;
/// <summary>
/// Builds the background "research" agent that the main claw fans work out to.
/// </summary>
/// <remarks>
/// This sub-agent doesn't need any of the harness machinery, so it's a plain
/// <see cref="ChatClientAgent"/> with a single tool: the hosted web search. The parent claw
/// delegates a per-ticker research task to one of these and they run concurrently.
/// </remarks>
internal static class ResearchAgent
{
/// <summary>Creates a web-search-only background agent for delegated ticker research.</summary>
/// <param name="chatClient">The chat client the background agent should use.</param>
public static AIAgent Create(IChatClient chatClient) =>
chatClient.AsAIAgent(
instructions:
"You research a single stock ticker. Use the web search tool to find the most " +
"recent, relevant news and commentary, then return a short, factual summary " +
"(3-4 bullet points) with no preamble.",
name: "TickerResearchAgent",
description: "Searches the web for recent news and commentary about a single stock ticker.",
// The only tool it needs: the same hosted web search the harness would have added.
tools: [new HostedWebSearchTool()]);
}
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using Microsoft.Extensions.AI;
namespace ClawSample;
/// <summary>
/// A custom function tool that gives our "claw" access to (illustrative) stock prices.
/// </summary>
/// <remarks>
/// The prices and earnings figures returned here are mock data for demonstration purposes only and
/// are not real market quotes. In a real assistant you would call a market-data API instead. The
/// trailing earnings-per-share value is included so the valuation skill has something to work with.
/// </remarks>
internal static class StockTools
{
/// <summary>A delayed, illustrative stock quote, including a trailing earnings-per-share figure.</summary>
public sealed record StockQuote(string Symbol, decimal Price, decimal TrailingEps, string Currency, DateTimeOffset AsOf);
// A tiny in-memory book of (price, trailing EPS) so the sample runs without any external dependency.
private static readonly Dictionary<string, (decimal Price, decimal Eps)> s_priceBook = new(StringComparer.OrdinalIgnoreCase)
{
["MSFT"] = (462.97m, 11.80m),
["AAPL"] = (229.35m, 6.13m),
["GOOGL"] = (178.12m, 7.54m),
["AMZN"] = (201.45m, 4.18m),
["NVDA"] = (134.81m, 2.95m),
["SPY"] = (612.40m, 23.10m),
};
/// <summary>
/// Gets the latest (delayed, illustrative) stock price and trailing EPS for a ticker symbol.
/// </summary>
/// <param name="symbol">The stock ticker symbol, e.g. <c>MSFT</c> or <c>AAPL</c>.</param>
[Description("Gets the latest (delayed, illustrative) stock price and trailing earnings per share for a ticker symbol.")]
public static StockQuote GetStockPrice(
[Description("The stock ticker symbol, e.g. MSFT or AAPL.")] string symbol)
{
if (!s_priceBook.TryGetValue(symbol, out var data))
{
// Deterministic pseudo-values for unknown symbols so the sample stays self-contained.
// Derive a stable seed from the characters — string.GetHashCode() is randomized per
// process and Math.Abs(int.MinValue) throws, so neither is safe for repeatable output.
var seed = 0;
foreach (var ch in symbol.ToUpperInvariant())
{
seed = (seed * 31 + ch) % 1_000_000;
}
var price = 50m + seed % 45000 / 100m;
data = (price, Math.Round(price / 20m, 2));
}
return new StockQuote(symbol.ToUpperInvariant(), data.Price, data.Eps, "USD", DateTimeOffset.UtcNow);
}
/// <summary>Creates the <see cref="AIFunction"/> wrapper used to expose the tool to the agent.</summary>
public static AIFunction CreateGetStockPriceTool() => AIFunctionFactory.Create(GetStockPrice, "get_stock_price");
}
@@ -0,0 +1,191 @@
// Copyright (c) Microsoft. All rights reserved.
// Sample subprocess-based skill script runner.
// Executes file-based skill scripts as local subprocesses.
// This is provided for demonstration purposes only.
using System.Diagnostics;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
/// <summary>
/// Executes file-based skill scripts as local subprocesses.
/// </summary>
/// <remarks>
/// This runner uses the script's absolute path and converts the arguments
/// to CLI arguments. When the LLM sends a JSON array, each element is used
/// as a positional argument. It is intended for demonstration purposes only.
/// </remarks>
internal sealed class SubprocessScriptRunner
{
/// <summary>Maximum time a skill script is allowed to run before it is terminated.</summary>
private static readonly TimeSpan s_scriptTimeout = TimeSpan.FromSeconds(30);
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="SubprocessScriptRunner"/> class.
/// </summary>
/// <param name="loggerFactory">
/// Optional logger factory. When provided, script outcomes (success output, stderr, non-zero
/// exit codes, and failures) are written to the log in addition to being returned to the LLM.
/// </param>
public SubprocessScriptRunner(ILoggerFactory? loggerFactory = null)
{
this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<SubprocessScriptRunner>();
}
/// <summary>
/// Runs a skill script as a local subprocess.
/// </summary>
public async Task<object?> RunAsync(
AgentFileSkill skill,
AgentFileSkillScript script,
JsonElement? arguments,
IServiceProvider? serviceProvider,
CancellationToken cancellationToken)
{
this._logger.LogDebug("Running script '{ScriptName}' from skill '{SkillName}'.", script.Name, skill.Frontmatter.Name);
if (!File.Exists(script.FullPath))
{
this._logger.LogError("Script file not found for skill '{SkillName}': {ScriptPath}", skill.Frontmatter.Name, script.FullPath);
return $"Error: Script file not found: {script.FullPath}";
}
string extension = Path.GetExtension(script.FullPath);
string? interpreter = extension switch
{
// Windows Python installs commonly expose "python" rather than "python3".
".py" => OperatingSystem.IsWindows() ? "python" : "python3",
".js" => "node",
".sh" => "bash",
".ps1" => "pwsh",
_ => null,
};
var startInfo = new ProcessStartInfo
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = Path.GetDirectoryName(script.FullPath) ?? ".",
};
if (interpreter is not null)
{
startInfo.FileName = interpreter;
startInfo.ArgumentList.Add(script.FullPath);
}
else
{
startInfo.FileName = script.FullPath;
}
if (arguments is { ValueKind: JsonValueKind.Array } json)
{
// Positional CLI arguments
foreach (var element in json.EnumerateArray())
{
if (element.ValueKind != JsonValueKind.String)
{
throw new InvalidOperationException(
$"File-based skill scripts only accept string CLI arguments but received a JSON element of kind '{element.ValueKind}'. " +
"All array elements must be JSON strings.");
}
startInfo.ArgumentList.Add(element.GetString()!);
}
}
else if (arguments is not null && arguments.Value.ValueKind != JsonValueKind.Null && arguments.Value.ValueKind != JsonValueKind.Undefined)
{
throw new InvalidOperationException(
$"Expected a JSON array of CLI arguments but received {arguments.Value.ValueKind}. " +
"File-based skill scripts expect positional arguments as a JSON array of strings.");
}
// Bound the script's lifetime: cancel after a timeout, or when the caller cancels.
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(s_scriptTimeout);
CancellationToken runToken = timeoutCts.Token;
Process? process = null;
try
{
process = Process.Start(startInfo);
if (process is null)
{
this._logger.LogError("Failed to start process for script '{ScriptName}' from skill '{SkillName}'.", script.Name, skill.Frontmatter.Name);
return $"Error: Failed to start process for script '{script.Name}'.";
}
Task<string> outputTask = process.StandardOutput.ReadToEndAsync(runToken);
Task<string> errorTask = process.StandardError.ReadToEndAsync(runToken);
await process.WaitForExitAsync(runToken).ConfigureAwait(false);
string output = await outputTask.ConfigureAwait(false);
string error = await errorTask.ConfigureAwait(false);
if (!string.IsNullOrEmpty(error))
{
if (process.ExitCode == 0)
{
this._logger.LogWarning(
"Script '{ScriptName}' from skill '{SkillName}' succeeded but wrote to stderr:\n{Stderr}",
script.Name, skill.Frontmatter.Name, error.Trim());
}
output += $"\nStderr:\n{error}";
}
if (process.ExitCode != 0)
{
this._logger.LogError(
"Script '{ScriptName}' from skill '{SkillName}' exited with code {ExitCode}.{Stderr}",
script.Name, skill.Frontmatter.Name, process.ExitCode,
string.IsNullOrEmpty(error) ? string.Empty : $"\nStderr:\n{error.Trim()}");
output += $"\nScript exited with code {process.ExitCode}";
}
string result = string.IsNullOrEmpty(output) ? "(no output)" : output.Trim();
if (process.ExitCode == 0)
{
this._logger.LogInformation(
"Script '{ScriptName}' from skill '{SkillName}' completed successfully. Output:\n{Output}",
script.Name, skill.Frontmatter.Name, result);
}
return result;
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
// The timeout fired (the caller did not cancel). Kill the process and report a timeout.
process?.Kill(entireProcessTree: true);
this._logger.LogError(
"Script '{ScriptName}' from skill '{SkillName}' timed out after {Timeout} seconds.",
script.Name, skill.Frontmatter.Name, s_scriptTimeout.TotalSeconds);
return $"Error: Script '{script.Name}' timed out after {s_scriptTimeout.TotalSeconds:0} seconds.";
}
catch (OperationCanceledException)
{
// The caller cancelled: kill the process to avoid leaving orphaned subprocesses, then rethrow.
process?.Kill(entireProcessTree: true);
throw;
}
catch (Exception ex)
{
this._logger.LogError(ex, "Failed to execute script '{ScriptName}' from skill '{SkillName}'.", script.Name, skill.Frontmatter.Name);
return $"Error: Failed to execute script '{script.Name}': {ex.Message}";
}
finally
{
process?.Dispose();
}
}
}
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using Microsoft.Extensions.AI;
namespace ClawSample;
/// <summary>
/// Sensitive "claw" tools that take real-world actions and therefore require human approval.
/// </summary>
/// <remarks>
/// These tools only simulate their effects (no orders are placed, no email is sent). They exist
/// to demonstrate how the harness gates risky actions behind an approval prompt.
/// </remarks>
internal static class TradingTools
{
// <place_trade>
/// <summary>
/// Places a (simulated) buy or sell order for a given symbol and quantity.
/// </summary>
/// <param name="symbol">The stock ticker symbol to trade, e.g. <c>MSFT</c>.</param>
/// <param name="action">Either <c>buy</c> or <c>sell</c>.</param>
/// <param name="quantity">The number of shares to trade.</param>
[Description("Places a buy or sell order for a given symbol and quantity.")]
public static string PlaceTrade(
[Description("The stock ticker symbol to trade, e.g. MSFT.")] string symbol,
[Description("Either 'buy' or 'sell'.")] string action,
[Description("The number of shares to trade.")] int quantity)
{
var isBuy = action.Equals("buy", StringComparison.OrdinalIgnoreCase);
var isSell = action.Equals("sell", StringComparison.OrdinalIgnoreCase);
if (!isBuy && !isSell)
{
return $"Invalid action '{action}'. Use 'buy' or 'sell'.";
}
if (quantity <= 0)
{
return $"Invalid quantity '{quantity}'. Quantity must be a positive whole number of shares.";
}
var verb = isSell ? "Sold" : "Bought";
var confirmation = $"TRADE-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
return $"{verb} {quantity} share(s) of {symbol.ToUpperInvariant()}. Confirmation: {confirmation}.";
}
// </place_trade>
/// <summary>
/// Creates an approval-required <see cref="AIFunction"/> for <see cref="PlaceTrade"/>.
/// Wrapping the function in <see cref="ApprovalRequiredAIFunction"/> tells the harness to
/// surface an approval request before the function ever runs.
/// </summary>
public static AIFunction CreatePlaceTradeTool() =>
new ApprovalRequiredAIFunction(AIFunctionFactory.Create(PlaceTrade, "place_trade"));
}
@@ -0,0 +1,18 @@
---
name: risk-scoring
description: Score how concentrated and risky a portfolio is on a 0-100 scale from its position weights. Use when the user asks how risky their portfolio is, whether it is too concentrated, or for a diversification check.
---
## Usage
When the user asks about portfolio risk or concentration:
1. Read `references/risk-bands.md` to understand the score bands and what drives them.
2. Compute each holding's market value (shares × price) — use the `get_stock_price` tool for current
prices if you do not already have them.
3. Run `scripts/risk_score.py` with one `--position VALUE` argument per holding,
e.g. `--position 18518 --position 17201 --position 16177`.
4. Report the 0-100 score, the band it falls in, and the largest single-position weight, then suggest
(in general terms) whether the portfolio looks well diversified or concentrated.
Remind the user this is a crude concentration measure, not a complete risk model, and not advice.
@@ -0,0 +1,27 @@
# Risk-scoring guide (illustrative)
This skill scores **concentration risk** — how much a portfolio depends on its largest positions —
on a 0-100 scale, where higher means riskier.
## How the score is built
1. Convert each position to a weight: `weight = position_value / total_value`.
2. Compute the Herfindahl-Hirschman Index (HHI): `HHI = sum(weight^2)`.
- A perfectly even portfolio of *n* holdings has `HHI = 1/n` (low).
- A single-stock portfolio has `HHI = 1` (maximum concentration).
3. Scale to 0-100: `score = round(HHI * 100)`.
## Score bands
| Score | Band | Interpretation |
|---------|--------------------|-------------------------------------------------|
| 0-20 | Well diversified | No single holding dominates. |
| 21-40 | Moderately diversified | Some tilt, but broadly spread. |
| 41-60 | Concentrated | A few positions carry most of the risk. |
| 61-100 | Highly concentrated| Heavily dependent on one or two positions. |
Also watch the **largest single-position weight**: above ~25% is usually worth flagging regardless
of the overall score.
This measures concentration only — it ignores volatility, correlation, sector exposure, and leverage,
so it is a starting point, not a verdict.
@@ -0,0 +1,58 @@
# Portfolio risk-scoring script
# Scores concentration risk on a 0-100 scale using the Herfindahl-Hirschman Index (HHI).
#
# weight_i = position_i / total
# HHI = sum(weight_i ^ 2)
# score = round(HHI * 100) # higher = more concentrated = riskier
#
# Usage:
# python scripts/risk_score.py --position 18518 --position 17201 --position 16177
import argparse
import json
def main() -> None:
parser = argparse.ArgumentParser(description="Score portfolio concentration risk (0-100).")
parser.add_argument(
"--position",
type=float,
action="append",
required=True,
help="Market value of one holding. Pass once per position.",
)
args = parser.parse_args()
positions = args.position
if any(p <= 0 for p in positions):
print(json.dumps({"error": "Each position value must be a positive market value."}))
return
total = sum(positions)
if total <= 0:
print(json.dumps({"error": "Total portfolio value must be positive."}))
return
weights = [p / total for p in positions]
hhi = sum(w * w for w in weights)
score = round(hhi * 100)
if score <= 20:
band = "Well diversified"
elif score <= 40:
band = "Moderately diversified"
elif score <= 60:
band = "Concentrated"
else:
band = "Highly concentrated"
print(json.dumps({
"positions": len(positions),
"score": score,
"band": band,
"largest_weight_pct": round(max(weights) * 100, 1),
}))
if __name__ == "__main__":
main()
@@ -0,0 +1,17 @@
---
name: valuation
description: Estimate whether a stock looks cheap or expensive using a price-to-earnings (P/E) based fair-value method. Use when the user asks if a stock is over- or under-valued, or for a fair-value / target price.
---
## Usage
When the user asks whether a stock is fairly valued, over-valued, or under-valued:
1. Read `references/valuation-guide.md` to pick a sensible target P/E for the company's sector.
2. Run `scripts/valuation_metrics.py` with the current price, trailing EPS, and the target P/E,
e.g. `--price 462.97 --eps 11.80 --target-pe 32`.
3. Report the computed P/E, the fair-value estimate, and the percentage upside/downside, then state
plainly whether the stock looks cheap or expensive on this measure.
Always remind the user that a single P/E heuristic is not investment advice and ignores growth,
debt, and many other factors.
@@ -0,0 +1,28 @@
# Valuation guide (illustrative)
A quick price-to-earnings (P/E) sanity check:
- **P/E = price ÷ trailing earnings per share (EPS)**
- **Fair value = trailing EPS × target P/E**
- **Upside/downside = (fair value price) ÷ price**
## Typical target P/E by sector
These are rough, illustrative anchors only — not live market multiples.
| Sector | Conservative target P/E | Growth target P/E |
|-----------------------|-------------------------|-------------------|
| Mega-cap technology | 28 | 35 |
| Semiconductors | 25 | 40 |
| Consumer staples | 18 | 22 |
| Financials / banks | 11 | 14 |
| Broad market (index) | 19 | 21 |
## How to read the result
- Fair value **well above** the current price ⇒ the stock looks **cheap** on this measure.
- Fair value **well below** the current price ⇒ the stock looks **expensive** on this measure.
- Within ~5% ⇒ roughly **fairly valued**.
This is one crude lens. It ignores growth rates, balance-sheet strength, and cash flow, so never
present it as a recommendation.
@@ -0,0 +1,57 @@
# Valuation metrics script
# Computes a simple price-to-earnings (P/E) based fair-value estimate.
#
# fair_value = eps * target_pe
# pe = price / eps
# upside = (fair_value - price) / price
#
# Usage:
# python scripts/valuation_metrics.py --price 462.97 --eps 11.80 --target-pe 32
import argparse
import json
def main() -> None:
parser = argparse.ArgumentParser(description="Compute a P/E based fair-value estimate.")
parser.add_argument("--price", type=float, required=True, help="Current share price.")
parser.add_argument("--eps", type=float, required=True, help="Trailing earnings per share.")
parser.add_argument("--target-pe", type=float, required=True, help="Target P/E from the guide.")
args = parser.parse_args()
if args.eps <= 0:
print(json.dumps({"error": "EPS must be positive to compute a P/E ratio."}))
return
if args.price <= 0:
print(json.dumps({"error": "Price must be positive to compute valuation metrics."}))
return
if args.target_pe <= 0:
print(json.dumps({"error": "Target P/E must be positive."}))
return
pe = args.price / args.eps
fair_value = args.eps * args.target_pe
upside = (fair_value - args.price) / args.price
if upside > 0.05:
verdict = "looks cheap"
elif upside < -0.05:
verdict = "looks expensive"
else:
verdict = "roughly fairly valued"
print(json.dumps({
"price": round(args.price, 2),
"eps": round(args.eps, 2),
"target_pe": round(args.target_pe, 2),
"pe": round(pe, 2),
"fair_value": round(fair_value, 2),
"upside_pct": round(upside * 100, 1),
"verdict": verdict,
}))
if __name__ == "__main__":
main()
@@ -0,0 +1,6 @@
TRADE CONFIRMATION
Confirmation: TRADE-55AA44BB
Date: 2025-06-21
Symbol: NVDA
Action: SELL
Quantity: 20
@@ -0,0 +1,6 @@
TRADE CONFIRMATION
Confirmation: TRADE-77CC88DD
Date: 2024-05-08
Symbol: SPY
Action: SELL
Quantity: 15
@@ -0,0 +1,6 @@
TRADE CONFIRMATION
Confirmation: TRADE-9F8E7D6C
Date: 2024-11-03
Symbol: AAPL
Action: BUY
Quantity: 75
@@ -0,0 +1,6 @@
TRADE CONFIRMATION
Confirmation: TRADE-1234ABCD
Date: 2025-09-12
Symbol: AMZN
Action: BUY
Quantity: 30
@@ -0,0 +1,6 @@
TRADE CONFIRMATION
Confirmation: TRADE-EE11FF22
Date: 2025-01-30
Symbol: GOOGL
Action: BUY
Quantity: 25
@@ -0,0 +1,6 @@
TRADE CONFIRMATION
Confirmation: TRADE-A1B2C3D4
Date: 2024-02-14
Symbol: MSFT
Action: BUY
Quantity: 40
@@ -0,0 +1,7 @@
symbol,shares,cost_basis,purchase_date
MSFT,40,312.50,2023-02-14
AAPL,75,168.20,2022-11-03
NVDA,120,42.80,2021-06-21
AMZN,30,142.10,2023-09-12
GOOGL,25,128.45,2024-01-30
SPY,60,418.90,2024-05-08
1 symbol shares cost_basis purchase_date
2 MSFT 40 312.50 2023-02-14
3 AAPL 75 168.20 2022-11-03
4 NVDA 120 42.80 2021-06-21
5 AMZN 30 142.10 2023-09-12
6 GOOGL 25 128.45 2024-01-30
7 SPY 60 418.90 2024-05-08