From a0a363356e4e78cb3613300a73a44ed69c3826b8 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:52:34 +0000 Subject: [PATCH 1/5] Add samples for the harness blog part 2 --- dotnet/agent-framework-dotnet.slnx | 1 + .../Claw_Step02_WorkingWithData.csproj | 26 ++ .../Claw_Step02_WorkingWithData/Program.cs | 175 ++++++++++++ .../Claw_Step02_WorkingWithData/README.md | 71 +++++ .../Claw_Step02_WorkingWithData/StockTools.cs | 51 ++++ .../TradingTools.cs | 43 +++ .../working/portfolio.csv | 7 + python/samples/02-agents/harness/README.md | 1 + .../harness/build_your_own_claw/README.md | 61 +++- .../claw_step02_working_with_data.py | 263 ++++++++++++++++++ .../build_your_own_claw/working/portfolio.csv | 7 + 11 files changed, 704 insertions(+), 2 deletions(-) create mode 100644 dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Claw_Step02_WorkingWithData.csproj create mode 100644 dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Program.cs create mode 100644 dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/README.md create mode 100644 dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/StockTools.cs create mode 100644 dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/TradingTools.cs create mode 100644 dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/working/portfolio.csv create mode 100644 python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_with_data.py create mode 100644 python/samples/02-agents/harness/build_your_own_claw/working/portfolio.csv diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 8d51bdf44b..953165d5be 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -121,6 +121,7 @@ + diff --git a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Claw_Step02_WorkingWithData.csproj b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Claw_Step02_WorkingWithData.csproj new file mode 100644 index 0000000000..b04bcf841a --- /dev/null +++ b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Claw_Step02_WorkingWithData.csproj @@ -0,0 +1,26 @@ + + + + Exe + net10.0 + + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Program.cs b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Program.cs new file mode 100644 index 0000000000..53af019dda --- /dev/null +++ b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Program.cs @@ -0,0 +1,175 @@ +// 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/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. It travels with the session, so it survives as long as you +// reuse the session (use /session-export and /session-import across runs). +// * 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"; + +// +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. + """; +// + +// +// 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); +// + +// +// 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."); +} +// + +// +// 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 stays on its +// session-scoped default folder (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], + }, + // File memory is on by default and travels with the session: the agent records which memory + // folder it is using in the session state. It persists for as long as you reuse the same + // session, so use /session-export and /session-import to carry it across a restart. No fixed + // folder is required. + // 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 }, + }, +}); +// + +// +// 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), + }); +// diff --git a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/README.md b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/README.md new file mode 100644 index 0000000000..9c159bafc2 --- /dev/null +++ b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/README.md @@ -0,0 +1,71 @@ +# 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/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 travels with the session, so use `/session-export` and + `/session-import` to keep it across runs (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. Try these in order: + +1. `/mode execute` — quick lookups don't need a plan. +2. `What's in my portfolio?` — the agent reads `portfolio.csv` with the file_access tools. +3. `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. +4. `I'm a conservative investor saving for a house in two years.` — a durable fact (recalled later + by Foundry memory when enabled). +5. `Buy 10 shares of MSFT.` — the agent calls `place_trade`; **you are prompted to approve or + deny** before it runs. +6. `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) travels with the session, so `/session-export` before you quit and + `/session-import` after relaunching to bring it back. diff --git a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/StockTools.cs b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/StockTools.cs new file mode 100644 index 0000000000..49d95eb63c --- /dev/null +++ b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/StockTools.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel; +using Microsoft.Extensions.AI; + +namespace ClawSample; + +/// +/// A custom function tool that gives our "claw" access to (illustrative) stock prices. +/// +/// +/// 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. +/// +internal static class StockTools +{ + /// A delayed, illustrative stock quote. + 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 s_priceBook = new(StringComparer.OrdinalIgnoreCase) + { + ["MSFT"] = 462.97m, + ["AAPL"] = 229.35m, + ["GOOGL"] = 178.12m, + ["AMZN"] = 201.45m, + ["NVDA"] = 134.81m, + ["SPY"] = 612.40m, + }; + + /// + /// Gets the latest (delayed, illustrative) stock price for a ticker symbol. + /// + /// The stock ticker symbol, e.g. MSFT or AAPL. + [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. + var seed = Math.Abs(symbol.ToUpperInvariant().GetHashCode()); + price = 50m + seed % 45000 / 100m; + } + + return new StockQuote(symbol.ToUpperInvariant(), price, "USD", DateTimeOffset.UtcNow); + } + + /// Creates the wrapper used to expose the tool to the agent. + public static AIFunction CreateGetStockPriceTool() => AIFunctionFactory.Create(GetStockPrice, "get_stock_price"); +} diff --git a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/TradingTools.cs b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/TradingTools.cs new file mode 100644 index 0000000000..6ed4c9bb32 --- /dev/null +++ b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/TradingTools.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel; +using Microsoft.Extensions.AI; + +namespace ClawSample; + +/// +/// Sensitive "claw" tools that take real-world actions and therefore require human approval. +/// +/// +/// 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. +/// +internal static class TradingTools +{ + // + /// + /// Places a (simulated) buy or sell order for a given symbol and quantity. + /// + /// The stock ticker symbol to trade, e.g. MSFT. + /// Either buy or sell. + /// The number of shares to trade. + [Description("Places a buy or sell order for a given symbol and quantity. This executes a real trade.")] + 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 verb = action.Equals("sell", StringComparison.OrdinalIgnoreCase) ? "Sold" : "Bought"; + var confirmation = $"TRADE-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}"; + return $"{verb} {quantity} share(s) of {symbol.ToUpperInvariant()}. Confirmation: {confirmation}."; + } + // + + /// + /// Creates an approval-required for . + /// Wrapping the function in tells the harness to + /// surface an approval request before the function ever runs. + /// + public static AIFunction CreatePlaceTradeTool() => + new ApprovalRequiredAIFunction(AIFunctionFactory.Create(PlaceTrade, "place_trade")); +} diff --git a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/working/portfolio.csv b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/working/portfolio.csv new file mode 100644 index 0000000000..d2e0fbad5e --- /dev/null +++ b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/working/portfolio.csv @@ -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 diff --git a/python/samples/02-agents/harness/README.md b/python/samples/02-agents/harness/README.md index 410bd34759..62f30f2753 100644 --- a/python/samples/02-agents/harness/README.md +++ b/python/samples/02-agents/harness/README.md @@ -30,6 +30,7 @@ Each feature can be disabled or customized via keyword arguments. |------|-------------| | `harness_research.py` | Interactive research assistant with web search, a plan/execute workflow, and an execute-mode loop that re-invokes the agent until every todo is complete | | `harness_data_processing.py` | Data-processing assistant over a folder of CSV files, demonstrating file-access tools and tool approval | +| [`build_your_own_claw/`](./build_your_own_claw/README.md) | *Build your own claw* blog series — a personal finance assistant built step by step | ## Running diff --git a/python/samples/02-agents/harness/build_your_own_claw/README.md b/python/samples/02-agents/harness/build_your_own_claw/README.md index 49413a3b37..38830e9e5e 100644 --- a/python/samples/02-agents/harness/build_your_own_claw/README.md +++ b/python/samples/02-agents/harness/build_your_own_claw/README.md @@ -1,11 +1,12 @@ -# Build your own agent harness and claw — Python samples +# Build your own claw and agent harness — Python samples -Runnable Python samples for 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) +Runnable Python samples for 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. Each step builds a personal finance / investing assistant on top of `create_harness_agent`, reusing the shared harness `console` package in the parent `harness/` directory. - **Part 1 — `claw_step01_meet_your_claw.py`** — the minimal harness. +- **Part 2 — `claw_step02_working_with_data.py`** — file access, approvals, and durable memory. ## Prerequisites @@ -59,3 +60,59 @@ The sample starts an interactive loop. Try these in order: --- +## Part 2 — Working with your data, safely + +Teaches the assistant to work with *your* data safely. + +### What this sample demonstrates + +- **File access** — the agent reads a pre-populated `working/portfolio.csv` and writes reports + with the `file_access_*` tools. File access is on by default; the sample points its store at the + sample's `working/` folder via `create_harness_agent(file_access_store=...)`. +- **Approvals** — file-access tools require approval by default, but the sample wires the built-in + `read_only_tools_auto_approval_rule` so reads/lists/searches are frictionless while saving and + deleting still pause for approval. The `place_trade` tool is marked + `approval_mode="always_require"`, so the harness asks you to approve or deny before any trade + runs. The trade is simulated. +- **Durable memory, two ways:** + - **File memory** (coarse-grained, explicit) — the agent reads/writes files such as + `watchlist.md`. File memory is on by default and is session-scoped: it travels with the + session, so use `/session-export` and `/session-import` to carry it across runs (no fixed + folder or owner id required). + - **Foundry memory** (fine-grained, automatic) — Microsoft Foundry extracts durable facts from + the conversation. Opt-in; see below. + +### Additional environment variables (optional — enable Foundry memory) + +```bash +export FOUNDRY_MEMORY_STORE="claw-finance-memory" +export FOUNDRY_EMBEDDING_MODEL="text-embedding-3-small" +``` + +When these are not set, the sample runs with file memory only and prints a note. + +### Running + +```bash +uv run python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_with_data.py +``` + +### What to expect + +Try these in order: + +1. `/mode execute` — quick lookups don't need a plan. +2. `What's in my portfolio?` — the agent reads `portfolio.csv` with the file_access tools. +3. `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 the file is created. +4. `I'm a conservative investor saving for a house in two years.` — a durable fact (recalled later + by Foundry memory when enabled). +5. `Buy 10 shares of MSFT.` — the agent calls `place_trade`; **you are prompted to approve or + deny** before it runs. +6. `Add SPY to my watchlist.` — saved to `watchlist.md` in file memory. + +Foundry memory (when enabled) recalls facts about you in any new session. File memory (the +watchlist) is session-scoped, so `/session-export` before you quit and `/session-import` after +relaunching to bring it back, then ask *"What's on my watchlist?"* or *"What do you know about +me?"*. + diff --git a/python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_with_data.py b/python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_with_data.py new file mode 100644 index 0000000000..f915d083de --- /dev/null +++ b/python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_with_data.py @@ -0,0 +1,263 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "agent-framework", +# "azure-ai-projects", +# "textual>=6.2.1", +# "rich>=13.7.1", +# "azure-identity", +# "python-dotenv", +# ] +# /// +# Run with any PEP 723 compatible runner, e.g.: +# uv run python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_with_data.py + +# Copyright (c) Microsoft. All rights reserved. + +"""Working with your data, safely (Post 2) — Python. + +The second runnable sample from the "Build your own claw and agent harness with Microsoft Agent +Framework" blog series. See: https://devblogs.microsoft.com/agent-framework/working-with-your-data-safely. +It 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), + on by default in the harness. Read-only file tools are auto-approved; writes + still prompt. +2. Approvals — the ``place_trade`` tool is marked ``approval_mode="always_require"`` so the + harness asks for human approval before it runs. +3. Durable memory, two complementary kinds: + * File memory (coarse-grained, explicit) — the agent reads/writes files like + ``watchlist.md``. On by default and session-scoped, so it survives across runs + when you reuse the session (``/session-export`` and ``/session-import``). + * 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. + +This sample reuses the shared harness ``console`` package in the parent ``harness/`` directory. + +Environment variables: + FOUNDRY_PROJECT_ENDPOINT — Microsoft Foundry project endpoint URL + FOUNDRY_MODEL — Model deployment name (defaults to gpt-5.4) + FOUNDRY_MEMORY_STORE — (optional) Foundry memory store name; enables Foundry memory + FOUNDRY_EMBEDDING_MODEL — (optional) embedding deployment; required for Foundry memory + +Authentication: + Run ``az login`` before running this sample. +""" + +import asyncio +import os +import sys +from contextlib import AsyncExitStack +from datetime import datetime, timezone +from pathlib import Path +from typing import Annotated, Any + +from agent_framework import ( + FileAccessProvider, + FileSystemAgentFileStore, + create_harness_agent, + tool, +) +from agent_framework.foundry import FoundryChatClient, FoundryMemoryProvider +from azure.identity import AzureCliCredential +from dotenv import load_dotenv + +# Reuse the shared harness console that lives in the parent ``harness/`` directory. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from console import build_observers_with_planning, run_agent_async # noqa: E402 + +# Fixed folder so file access (portfolio.csv, reports) lives next to this script. File memory stays +# on its session-scoped default (no fixed folder); it persists across runs when you reuse the +# session (via the console's /session-export and /session-import commands). +_SAMPLE_DIR = Path(__file__).resolve().parent +_WORKING_DIR = _SAMPLE_DIR / "working" +# Foundry memory is scoped to a single logical user here, so its facts are recalled across sessions. +# 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 instead, just don't pass a scope, and the provider will default +# to session scoped. +_MEMORY_SCOPE = "claw-sample-user" + +FINANCE_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. +""" + +# A tiny in-memory price book so the sample runs without any external dependency. +# These are illustrative mock prices, not real market quotes. +_PRICE_BOOK: dict[str, float] = { + "MSFT": 462.97, + "AAPL": 229.35, + "GOOGL": 178.12, + "AMZN": 201.45, + "NVDA": 134.81, + "SPY": 612.40, +} + + +# +def get_stock_price( + symbol: Annotated[str, "The stock ticker symbol, e.g. MSFT or AAPL."], +) -> dict[str, object]: + """Get the latest (delayed, illustrative) stock price for a ticker symbol.""" + ticker = symbol.upper() + price = _PRICE_BOOK.get(ticker) + if price is None: + # Deterministic pseudo-price for unknown symbols so the sample stays self-contained. + # Derive a stable seed from the characters — the built-in hash() is randomized per + # process (PYTHONHASHSEED), so it would give different prices on every run. + seed = 0 + for ch in ticker: + seed = (seed * 31 + ord(ch)) % 1_000_000 + price = 50.0 + (seed % 45000) / 100.0 + + return { + "symbol": ticker, + "price": round(price, 2), + "currency": "USD", + "as_of": datetime.now(timezone.utc).isoformat(), + } +# + + +# +@tool(approval_mode="always_require") +def place_trade( + symbol: Annotated[str, "The stock ticker symbol to trade, e.g. MSFT."], + action: Annotated[str, "Either 'buy' or 'sell'."], + quantity: Annotated[int, "The number of shares to trade."], +) -> str: + """Place a (simulated) buy or sell order. Marked approval-required, so the harness asks the + user to approve before this ever runs. No real order is placed.""" + verb = "Sold" if action.lower() == "sell" else "Bought" + confirmation = f"TRADE-{abs(hash((symbol, action, quantity))) % 100000000:08d}" + return f"{verb} {quantity} share(s) of {symbol.upper()}. Confirmation: {confirmation}." +# + + +# +async def _maybe_enable_foundry_memory(stack: AsyncExitStack) -> FoundryMemoryProvider | None: + """Enable fine-grained Foundry memory when configured, otherwise return None. + + Foundry memory needs a memory store and an embedding model, so it is opt-in. When the required + environment variables are present we (best-effort) create the store and return a provider + scoped to a single user, so extracted facts are recalled across sessions. + """ + endpoint = os.environ.get("FOUNDRY_PROJECT_ENDPOINT") + store_name = os.environ.get("FOUNDRY_MEMORY_STORE") + embedding_model = os.environ.get("FOUNDRY_EMBEDDING_MODEL") + chat_model = os.environ.get("FOUNDRY_MODEL", "gpt-5.4") + + if not (endpoint and store_name and embedding_model): + print("Foundry memory disabled. Set FOUNDRY_MEMORY_STORE and FOUNDRY_EMBEDDING_MODEL to enable it.") + return None + + # Imported lazily so the common (file-memory-only) path has no async-project dependency. + from azure.ai.projects.aio import AIProjectClient + from azure.ai.projects.models import MemoryStoreDefaultDefinition, MemoryStoreDefaultOptions + from azure.identity.aio import AzureCliCredential as AsyncAzureCliCredential + + credential = await stack.enter_async_context(AsyncAzureCliCredential()) + project_client = await stack.enter_async_context(AIProjectClient(endpoint=endpoint, credential=credential)) + + # Create the memory store if it does not already exist (no-op on conflict). + try: + definition = MemoryStoreDefaultDefinition( + chat_model=chat_model, + embedding_model=embedding_model, + options=MemoryStoreDefaultOptions(chat_summary_enabled=False, user_profile_enabled=True), + ) + await project_client.beta.memory_stores.create( + name=store_name, + description="Durable memory for the Build-your-own-claw finance assistant.", + definition=definition, + ) + except Exception as exc: # noqa: BLE001 - store may already exist; that is fine. + print(f"(Using existing memory store '{store_name}': {exc})") + + provider = FoundryMemoryProvider( + project_client=project_client, + memory_store_name=store_name, + scope=_MEMORY_SCOPE, + update_delay=0, # Update memories immediately (demo). In production, batch with a delay. + ) + print(f"Foundry memory enabled (store: {store_name}).") + return provider +# + + +async def main() -> None: + load_dotenv() + _WORKING_DIR.mkdir(exist_ok=True) + + # + # Construct a chat client (see Post 1). FoundryChatClient reads FOUNDRY_PROJECT_ENDPOINT and + # FOUNDRY_MODEL from the environment; AzureCliCredential handles auth (run `az login`). + client = FoundryChatClient(credential=AzureCliCredential()) + # + + async with AsyncExitStack() as stack: + # + # Fine-grained, automatic memory (when configured) is just another context provider. + context_providers: list[Any] = [] + foundry_memory = await _maybe_enable_foundry_memory(stack) + if foundry_memory is not None: + context_providers.append(foundry_memory) + # + + # + # Turn the chat client into a harness agent. On top of Post 1's defaults we point file + # access at a folder next to this script, add our approval-gated place_trade tool, + # auto-approve the read-only file tools (so reading is frictionless while writes and + # trades still prompt), and optionally add the Foundry memory provider. File memory stays + # on its session-scoped default folder, and we don't point it at a custom folder here. + agent = create_harness_agent( + client=client, + agent_instructions=FINANCE_INSTRUCTIONS, + tools=[get_stock_price, place_trade], + file_access_store=FileSystemAgentFileStore(str(_WORKING_DIR)), + auto_approval_rules=[FileAccessProvider.read_only_tools_auto_approval_rule], + context_providers=context_providers or None, + ) + # + + # + session = agent.create_session() + + # 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 run_agent_async( + agent, + session=session, + observers=build_observers_with_planning(agent), + initial_mode="plan", + title="💹 Finance Assistant", + placeholder="Review your portfolio, draft a report, update your watchlist, or place a trade...", + ) + # + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/02-agents/harness/build_your_own_claw/working/portfolio.csv b/python/samples/02-agents/harness/build_your_own_claw/working/portfolio.csv new file mode 100644 index 0000000000..d2e0fbad5e --- /dev/null +++ b/python/samples/02-agents/harness/build_your_own_claw/working/portfolio.csv @@ -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 From 8f97e786e2362e76070f46f922c8ff9dad5e28a0 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:01:08 +0000 Subject: [PATCH 2/5] Address PR comments --- .../Claw_Step02_WorkingWithData/StockTools.cs | 9 ++++++++- .../TradingTools.cs | 16 +++++++++++++-- .../claw_step02_working_with_data.py | 20 ++++++++++++++----- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/StockTools.cs b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/StockTools.cs index 49d95eb63c..229ec73e07 100644 --- a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/StockTools.cs +++ b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/StockTools.cs @@ -39,7 +39,14 @@ public static StockQuote GetStockPrice( if (!s_priceBook.TryGetValue(symbol, out var price)) { // Deterministic pseudo-price for unknown symbols so the sample stays self-contained. - var seed = Math.Abs(symbol.ToUpperInvariant().GetHashCode()); + // 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; } diff --git a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/TradingTools.cs b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/TradingTools.cs index 6ed4c9bb32..b17ba8f6dc 100644 --- a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/TradingTools.cs +++ b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/TradingTools.cs @@ -21,13 +21,25 @@ internal static class TradingTools /// The stock ticker symbol to trade, e.g. MSFT. /// Either buy or sell. /// The number of shares to trade. - [Description("Places a buy or sell order for a given symbol and quantity. This executes a real trade.")] + [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 verb = action.Equals("sell", StringComparison.OrdinalIgnoreCase) ? "Sold" : "Bought"; + 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}."; } diff --git a/python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_with_data.py b/python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_with_data.py index f915d083de..6acf02dca0 100644 --- a/python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_with_data.py +++ b/python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_with_data.py @@ -48,6 +48,7 @@ import asyncio import os import sys +import uuid from contextlib import AsyncExitStack from datetime import datetime, timezone from pathlib import Path @@ -151,8 +152,14 @@ def place_trade( ) -> str: """Place a (simulated) buy or sell order. Marked approval-required, so the harness asks the user to approve before this ever runs. No real order is placed.""" - verb = "Sold" if action.lower() == "sell" else "Bought" - confirmation = f"TRADE-{abs(hash((symbol, action, quantity))) % 100000000:08d}" + normalized_action = action.lower() + if normalized_action not in ("buy", "sell"): + return f"Invalid action '{action}'. Use 'buy' or 'sell'." + if quantity <= 0: + return f"Invalid quantity '{quantity}'. Quantity must be a positive whole number of shares." + + verb = "Sold" if normalized_action == "sell" else "Bought" + confirmation = f"TRADE-{uuid.uuid4().hex[:8].upper()}" return f"{verb} {quantity} share(s) of {symbol.upper()}. Confirmation: {confirmation}." # @@ -177,13 +184,17 @@ async def _maybe_enable_foundry_memory(stack: AsyncExitStack) -> FoundryMemoryPr # Imported lazily so the common (file-memory-only) path has no async-project dependency. from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import MemoryStoreDefaultDefinition, MemoryStoreDefaultOptions + from azure.core.exceptions import ResourceNotFoundError from azure.identity.aio import AzureCliCredential as AsyncAzureCliCredential credential = await stack.enter_async_context(AsyncAzureCliCredential()) project_client = await stack.enter_async_context(AIProjectClient(endpoint=endpoint, credential=credential)) - # Create the memory store if it does not already exist (no-op on conflict). + # Create the memory store only if it does not already exist. try: + await project_client.beta.memory_stores.get(name=store_name) + print(f"Using existing memory store '{store_name}'.") + except ResourceNotFoundError: definition = MemoryStoreDefaultDefinition( chat_model=chat_model, embedding_model=embedding_model, @@ -194,8 +205,7 @@ async def _maybe_enable_foundry_memory(stack: AsyncExitStack) -> FoundryMemoryPr description="Durable memory for the Build-your-own-claw finance assistant.", definition=definition, ) - except Exception as exc: # noqa: BLE001 - store may already exist; that is fine. - print(f"(Using existing memory store '{store_name}': {exc})") + print(f"Created memory store '{store_name}'.") provider = FoundryMemoryProvider( project_client=project_client, From 894846bd61d45a73e96791703e59df598995eee5 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:06:28 +0000 Subject: [PATCH 3/5] Fix blog links. --- .../BuildYourOwnClaw/Claw_Step02_WorkingWithData/Program.cs | 2 +- .../BuildYourOwnClaw/Claw_Step02_WorkingWithData/README.md | 2 +- .../build_your_own_claw/claw_step02_working_with_data.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Program.cs b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Program.cs index 53af019dda..7ac2489702 100644 --- a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Program.cs +++ b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Program.cs @@ -1,7 +1,7 @@ // 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/working-with-your-data-safely. +// 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). diff --git a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/README.md b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/README.md index 9c159bafc2..e1b23ca502 100644 --- a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/README.md +++ b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/README.md @@ -1,7 +1,7 @@ # 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/working-with-your-data-safely)). +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 diff --git a/python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_with_data.py b/python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_with_data.py index 6acf02dca0..370d5fd6d8 100644 --- a/python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_with_data.py +++ b/python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_with_data.py @@ -17,7 +17,7 @@ """Working with your data, safely (Post 2) — Python. The second runnable sample from the "Build your own claw and agent harness with Microsoft Agent -Framework" blog series. See: https://devblogs.microsoft.com/agent-framework/working-with-your-data-safely. +Framework" blog series. See: https://devblogs.microsoft.com/agent-framework/agent-harness-working-with-your-data-safely. It 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), From f42a859e5737238540af8358457b65b0ed849e5d Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:48:26 +0000 Subject: [PATCH 4/5] Address PR comments --- .../Claw_Step02_WorkingWithData/Program.cs | 21 ++++++---- .../Claw_Step02_WorkingWithData/README.md | 24 ++++++----- .../harness/build_your_own_claw/README.md | 26 ++++++------ .../claw_step02_working_with_data.py | 42 ++++++++++--------- 4 files changed, 62 insertions(+), 51 deletions(-) diff --git a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Program.cs b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Program.cs index 7ac2489702..15683834b7 100644 --- a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Program.cs +++ b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Program.cs @@ -8,8 +8,9 @@ // 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. It travels with the session, so it survives as long as you -// reuse the session (use /session-export and /session-import across runs). +// watchlist.md. Its files live on disk under {cwd}/agent-file-memory//, +// 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. @@ -123,8 +124,8 @@ await foundryMemory.EnsureMemoryStoreCreatedAsync( // // 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 stays on its -// session-scoped default folder (see below), and we don't point it at a custom folder. +// 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. @@ -135,10 +136,14 @@ await foundryMemory.EnsureMemoryStoreCreatedAsync( { AutoApprovalRules = [FileAccessProvider.ReadOnlyToolsAutoApprovalRule], }, - // File memory is on by default and travels with the session: the agent records which memory - // folder it is using in the session state. It persists for as long as you reuse the same - // session, so use /session-export and /session-import to carry it across a restart. No fixed - // folder is required. + // 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//, + // 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 diff --git a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/README.md b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/README.md index e1b23ca502..48dc0175eb 100644 --- a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/README.md +++ b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/README.md @@ -16,8 +16,10 @@ It builds on Post 1's personal finance assistant and teaches it to work with *yo 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 travels with the session, so use `/session-export` and - `/session-import` to keep it across runs (no fixed folder required). + `watchlist.md`. File memory is on by default; its files live on disk under + `{cwd}/agent-file-memory//`, 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. @@ -51,21 +53,21 @@ dotnet run --project samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_Work ## What to expect -The sample starts an interactive loop. Try these in order: +The sample starts an interactive loop in **execute** mode (quick lookups don't need a plan). Try +these in order: -1. `/mode execute` — quick lookups don't need a plan. -2. `What's in my portfolio?` — the agent reads `portfolio.csv` with the file_access tools. -3. `Write me a short report on my portfolio and save it.` — the agent writes a Markdown file +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. -4. `I'm a conservative investor saving for a house in two years.` — a durable fact (recalled later +3. `I'm a conservative investor saving for a house in two years.` — a durable fact (recalled later by Foundry memory when enabled). -5. `Buy 10 shares of MSFT.` — the agent calls `place_trade`; **you are prompted to approve or +4. `Buy 10 shares of MSFT.` — the agent calls `place_trade`; **you are prompted to approve or deny** before it runs. -6. `Add SPY to my watchlist.` — saved to `watchlist.md` in file memory. +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) travels with the session, so `/session-export` before you quit and - `/session-import` after relaunching to bring it back. +- **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. diff --git a/python/samples/02-agents/harness/build_your_own_claw/README.md b/python/samples/02-agents/harness/build_your_own_claw/README.md index 38830e9e5e..9d27587ca5 100644 --- a/python/samples/02-agents/harness/build_your_own_claw/README.md +++ b/python/samples/02-agents/harness/build_your_own_claw/README.md @@ -76,9 +76,10 @@ Teaches the assistant to work with *your* data safely. runs. The trade is simulated. - **Durable memory, two ways:** - **File memory** (coarse-grained, explicit) — the agent reads/writes files such as - `watchlist.md`. File memory is on by default and is session-scoped: it travels with the - session, so use `/session-export` and `/session-import` to carry it across runs (no fixed - folder or owner id required). + `watchlist.md`. File memory is on by default; its files live on disk under + `{cwd}/agent-file-memory//`, 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 or owner id required). - **Foundry memory** (fine-grained, automatic) — Microsoft Foundry extracts durable facts from the conversation. Opt-in; see below. @@ -99,20 +100,19 @@ uv run python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_ ### What to expect -Try these in order: +Try these in order (the sample starts in **execute** mode — quick lookups don't need a plan): -1. `/mode execute` — quick lookups don't need a plan. -2. `What's in my portfolio?` — the agent reads `portfolio.csv` with the file_access tools. -3. `Write me a short report on my portfolio and save it.` — the agent writes a Markdown file under +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 the file is created. -4. `I'm a conservative investor saving for a house in two years.` — a durable fact (recalled later +3. `I'm a conservative investor saving for a house in two years.` — a durable fact (recalled later by Foundry memory when enabled). -5. `Buy 10 shares of MSFT.` — the agent calls `place_trade`; **you are prompted to approve or +4. `Buy 10 shares of MSFT.` — the agent calls `place_trade`; **you are prompted to approve or deny** before it runs. -6. `Add SPY to my watchlist.` — saved to `watchlist.md` in file memory. +5. `Add SPY to my watchlist.` — saved to `watchlist.md` in file memory. Foundry memory (when enabled) recalls facts about you in any new session. File memory (the -watchlist) is session-scoped, so `/session-export` before you quit and `/session-import` after -relaunching to bring it back, then ask *"What's on my watchlist?"* or *"What do you know about -me?"*. +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, then ask +*"What's on my watchlist?"* or *"What do you know about me?"*. diff --git a/python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_with_data.py b/python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_with_data.py index 370d5fd6d8..8e1542572f 100644 --- a/python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_with_data.py +++ b/python/samples/02-agents/harness/build_your_own_claw/claw_step02_working_with_data.py @@ -27,8 +27,10 @@ harness asks for human approval before it runs. 3. Durable memory, two complementary kinds: * File memory (coarse-grained, explicit) — the agent reads/writes files like - ``watchlist.md``. On by default and session-scoped, so it survives across runs - when you reuse the session (``/session-export`` and ``/session-import``). + ``watchlist.md``. On by default. Its files live on disk under + ``{cwd}/agent-file-memory//``, so they persist across runs on this + machine. A new session starts empty; ``/session-export`` + ``/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. @@ -52,9 +54,10 @@ from contextlib import AsyncExitStack from datetime import datetime, timezone from pathlib import Path -from typing import Annotated, Any +from typing import Annotated, Any, Literal from agent_framework import ( + AgentModeProvider, FileAccessProvider, FileSystemAgentFileStore, create_harness_agent, @@ -63,14 +66,15 @@ from agent_framework.foundry import FoundryChatClient, FoundryMemoryProvider from azure.identity import AzureCliCredential from dotenv import load_dotenv +from pydantic import Field # Reuse the shared harness console that lives in the parent ``harness/`` directory. sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from console import build_observers_with_planning, run_agent_async # noqa: E402 -# Fixed folder so file access (portfolio.csv, reports) lives next to this script. File memory stays -# on its session-scoped default (no fixed folder); it persists across runs when you reuse the -# session (via the console's /session-export and /session-import commands). +# Fixed folder so file access (portfolio.csv, reports) lives next to this script. File memory uses its +# on-disk default ({cwd}/agent-file-memory//), so memory files persist across runs on this +# machine; /session-export + /session-import preserve the session id so a relaunch re-links to them. _SAMPLE_DIR = Path(__file__).resolve().parent _WORKING_DIR = _SAMPLE_DIR / "working" # Foundry memory is scoped to a single logical user here, so its facts are recalled across sessions. @@ -147,18 +151,16 @@ def get_stock_price( @tool(approval_mode="always_require") def place_trade( symbol: Annotated[str, "The stock ticker symbol to trade, e.g. MSFT."], - action: Annotated[str, "Either 'buy' or 'sell'."], - quantity: Annotated[int, "The number of shares to trade."], + action: Annotated[Literal["buy", "sell"], "Either 'buy' or 'sell'."], + quantity: Annotated[int, Field(gt=0, description="The number of shares to trade.")], ) -> str: """Place a (simulated) buy or sell order. Marked approval-required, so the harness asks the - user to approve before this ever runs. No real order is placed.""" - normalized_action = action.lower() - if normalized_action not in ("buy", "sell"): - return f"Invalid action '{action}'. Use 'buy' or 'sell'." - if quantity <= 0: - return f"Invalid quantity '{quantity}'. Quantity must be a positive whole number of shares." - - verb = "Sold" if normalized_action == "sell" else "Bought" + user to approve before this ever runs. No real order is placed. + + ``action`` and ``quantity`` are validated by the framework (pydantic) from their type hints: + the model can only pass 'buy'/'sell' and a quantity greater than zero. + """ + verb = "Sold" if action == "sell" else "Bought" confirmation = f"TRADE-{uuid.uuid4().hex[:8].upper()}" return f"{verb} {quantity} share(s) of {symbol.upper()}. Confirmation: {confirmation}." # @@ -241,8 +243,9 @@ async def main() -> None: # Turn the chat client into a harness agent. On top of Post 1's defaults we point file # access at a folder next to this script, add our approval-gated place_trade tool, # auto-approve the read-only file tools (so reading is frictionless while writes and - # trades still prompt), and optionally add the Foundry memory provider. File memory stays - # on its session-scoped default folder, and we don't point it at a custom folder here. + # trades still prompt), and optionally add the Foundry memory provider. File memory keeps its + # on-disk default store, and we don't point it at a custom folder here. We default the agent to + # execute mode (autonomous); the user can still switch to plan with the `mode_set` tool. agent = create_harness_agent( client=client, agent_instructions=FINANCE_INSTRUCTIONS, @@ -250,6 +253,7 @@ async def main() -> None: file_access_store=FileSystemAgentFileStore(str(_WORKING_DIR)), auto_approval_rules=[FileAccessProvider.read_only_tools_auto_approval_rule], context_providers=context_providers or None, + mode_provider=AgentModeProvider(default_mode="execute"), ) # @@ -262,7 +266,7 @@ async def main() -> None: agent, session=session, observers=build_observers_with_planning(agent), - initial_mode="plan", + initial_mode="execute", title="💹 Finance Assistant", placeholder="Review your portfolio, draft a report, update your watchlist, or place a trade...", ) From 387a3cd8224458e06b0ba7b93ac1978274119875 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:31:05 +0000 Subject: [PATCH 5/5] Fix bug where mode was incorrectly defaulted when reading the mode before the first run. --- .../core/tests/core/test_harness_mode.py | 44 +++++++++++++++++++ .../harness/console/commands/mode_handler.py | 14 +++++- .../console/observers/planning_output.py | 19 +++++++- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/python/packages/core/tests/core/test_harness_mode.py b/python/packages/core/tests/core/test_harness_mode.py index 12d31b1de2..a6bf6fadda 100644 --- a/python/packages/core/tests/core/test_harness_mode.py +++ b/python/packages/core/tests/core/test_harness_mode.py @@ -79,6 +79,50 @@ def test_agent_mode_context_provider_validates_configuration_and_is_experimental assert ".. warning:: Experimental" in set_agent_mode.__doc__ +async def test_external_read_with_provider_config_preserves_nondefault_mode( + chat_client_base: SupportsChatGetResponse, +) -> None: + """A pre-run external mode read must honor the provider's configured default, not the built-in one. + + Regression test for the harness console bug where ``configure_run_options`` read the mode with a + bare ``get_agent_mode(session)`` before the agent ran. Because ``get_agent_mode`` persists the + resolved default into session state, the built-in ``plan`` default was stored and the provider — + configured with ``default_mode="execute"`` — then read back ``plan``, so the agent ran in plan + mode while the console showed execute. Threading the provider's configuration into the read keeps + the two in sync. + """ + provider = AgentModeProvider(default_mode="execute") + + # A bare read (the original buggy call) would resolve and persist the built-in ``plan`` default, + # which does not match the provider's configured ``execute`` default. + poisoned_session = AgentSession(session_id="poisoned") + assert get_agent_mode(poisoned_session) == "plan" + agent = Agent(client=chat_client_base, context_providers=[provider]) + _, poisoned_options = await agent._prepare_session_and_messages( # pyright: ignore[reportPrivateUsage] + session=poisoned_session, + input_messages=[Message(role="user", contents=["Go"])], + ) + assert "You are currently operating in the plan mode." in poisoned_options["instructions"] + + # Reading with the provider's own configuration resolves and persists ``execute``, so the + # provider injects execute-mode instructions on the run. + session = AgentSession(session_id="configured") + assert ( + get_agent_mode( + session, + source_id=provider.source_id, + default_mode=provider.default_mode, + available_modes=provider.available_modes, + ) + == "execute" + ) + _, options = await agent._prepare_session_and_messages( # pyright: ignore[reportPrivateUsage] + session=session, + input_messages=[Message(role="user", contents=["Go"])], + ) + assert "You are currently operating in the execute mode." in options["instructions"] + + async def test_agent_mode_context_provider_normalizes_custom_modes( chat_client_base: SupportsChatGetResponse, ) -> None: diff --git a/python/samples/02-agents/harness/console/commands/mode_handler.py b/python/samples/02-agents/harness/console/commands/mode_handler.py index b4abc836bb..22270aaefc 100644 --- a/python/samples/02-agents/harness/console/commands/mode_handler.py +++ b/python/samples/02-agents/harness/console/commands/mode_handler.py @@ -59,7 +59,12 @@ async def try_handle( # Show current mode from agent_framework import get_agent_mode - current = get_agent_mode(session) + current = get_agent_mode( + session, + source_id=self._mode_provider.source_id, + default_mode=self._mode_provider.default_mode, + available_modes=self._mode_provider.available_modes, + ) ux.append_info_line(f"Current mode: {current}") return True @@ -68,7 +73,12 @@ async def try_handle( try: from agent_framework import set_agent_mode - normalized = set_agent_mode(session, new_mode) + normalized = set_agent_mode( + session, + new_mode, + source_id=self._mode_provider.source_id, + available_modes=self._mode_provider.available_modes, + ) color = self._mode_colors.get(normalized) ux.set_mode(normalized, color) ux.append_info_line( diff --git a/python/samples/02-agents/harness/console/observers/planning_output.py b/python/samples/02-agents/harness/console/observers/planning_output.py index f47bafcb15..3a91ec76c4 100644 --- a/python/samples/02-agents/harness/console/observers/planning_output.py +++ b/python/samples/02-agents/harness/console/observers/planning_output.py @@ -135,7 +135,17 @@ def _is_planning_mode(self, session: Any) -> bool: from agent_framework import get_agent_mode try: - current_mode = get_agent_mode(session) + # Thread the provider's own configuration (source id, default mode, and the set of + # available modes) so this read matches what the provider resolves in ``before_run``. + # ``get_agent_mode`` persists the resolved default into session state, so reading with + # the built-in default here would wrongly store ``plan`` and override the provider's + # configured default (e.g. ``execute``) before the agent ever runs. + current_mode = get_agent_mode( + session, + source_id=self._mode_provider.source_id, + default_mode=self._mode_provider.default_mode, + available_modes=self._mode_provider.available_modes, + ) except (AttributeError, TypeError): return True # No mode provider → treat as planning return current_mode.lower() == self._plan_mode_name.lower() @@ -218,7 +228,12 @@ async def continuation( if selection == approve_option: from agent_framework import set_agent_mode - set_agent_mode(session, self._execution_mode_name) + set_agent_mode( + session, + self._execution_mode_name, + source_id=self._mode_provider.source_id, + available_modes=self._mode_provider.available_modes, + ) exec_color = self._mode_colors.get(self._execution_mode_name) ux.set_mode(self._execution_mode_name, exec_color) ux.append_info_line(