diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 2121be9207..efa9a70227 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -112,6 +112,7 @@ + diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index d494768139..d694c10cbc 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -124,6 +124,7 @@ + diff --git a/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/AnsiEscapes.cs b/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/AnsiEscapes.cs index b13c2ddc82..cf916938e7 100644 --- a/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/AnsiEscapes.cs +++ b/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/AnsiEscapes.cs @@ -24,6 +24,11 @@ public static class AnsiEscapes /// public static string MoveCursor(int row, int column) => $"\x1b[{row};{column}H"; + /// + /// Erases the current line from the cursor position to the end of the line (EL 0). + /// + public static string EraseToEndOfLine => "\x1b[0K"; + /// /// Erases the entire current line (EL 2). /// diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Components/AgentStatus.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Components/AgentStatus.cs index f07035d27a..725e1ffaa6 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Components/AgentStatus.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Components/AgentStatus.cs @@ -35,6 +35,7 @@ public class AgentStatus : ConsoleReactiveComponent /// Initializes a new instance of the class. @@ -85,7 +86,12 @@ public override void RenderCore(AgentStatusProps props, AgentStatusState state) } System.Console.Write(AnsiEscapes.SaveCursor); - System.Console.Write(AnsiEscapes.MoveAndEraseLine(this.Y)); + System.Console.Write(AnsiEscapes.MoveCursor(this.Y, this.X)); + if (props != this._previousProps) + { + System.Console.Write(AnsiEscapes.EraseToEndOfLine); + this._previousProps = props; + } if (props.ShowSpinner) { diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessConsole.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessConsole.cs index 1f313d1008..f65d220882 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessConsole.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessConsole.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Text; using Harness.ConsoleReactiveComponents; using Microsoft.Agents.AI; @@ -24,6 +25,8 @@ public static async Task RunAgentAsync(AIAgent agent, string userPrompt, Harness { options ??= new(); + System.Console.OutputEncoding = Encoding.UTF8; + // Null means use defaults; an explicit (possibly empty) list means use exactly what was provided. var observers = options.Observers ?? HarnessConsoleOptions.BuildDefaultObservers(); @@ -63,6 +66,7 @@ public static async Task RunAgentAsync(AIAgent agent, string userPrompt, Harness System.Console.ResetColor(); System.Console.Write(AnsiEscapes.ResetScrollRegion); + System.Console.Write(AnsiEscapes.EraseScrollbackBuffer); System.Console.Write(AnsiEscapes.EraseEntireScreen); System.Console.Write(AnsiEscapes.MoveCursor(1, 1)); System.Console.WriteLine("Goodbye!"); diff --git a/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Harness_Step04_CodeExecution.csproj b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Harness_Step04_CodeExecution.csproj new file mode 100644 index 0000000000..729ba2dd88 --- /dev/null +++ b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Harness_Step04_CodeExecution.csproj @@ -0,0 +1,29 @@ + + + + Exe + net10.0 + + enable + enable + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs new file mode 100644 index 0000000000..af53443c63 --- /dev/null +++ b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample demonstrates a HarnessAgent with ALL features enabled, plus: +// - Hyperlight CodeAct (HyperlightCodeActProvider) for sandboxed Python code execution +// - Skills (AgentSkillsProvider) discovering a local "regex-tester" skill +// +// The agent can plan tasks with todos, manage modes, store memories, read/write files, +// search the web, approve sensitive tools, discover and use skills, and execute arbitrary +// Python code in a Hyperlight sandbox — all pre-configured by the HarnessAgent. +// +// Try asking: "Help me write a regex that matches valid email addresses, then test it." +// +// Special commands: +// /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 Harness.Shared.Console; +using HyperlightSandbox.Guest.Python; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Hyperlight; +using Microsoft.Extensions.AI; + +var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4"; + +const int MaxContextWindowTokens = 1_050_000; +const int MaxOutputTokens = 128_000; +const string TracingSourceName = "Harness.CodeExecution"; + +// Set up OpenTelemetry tracing that writes spans to a text file. +using var tracerProvider = HarnessTracing.CreateFileTracerProvider(TracingSourceName); + +// Create the HyperlightCodeActProvider with the Python/Wasm backend. +// The guest module path is resolved automatically from the Hyperlight.HyperlightSandbox.Guest.Python NuGet package. +using var codeAct = new HyperlightCodeActProvider( + HyperlightCodeActProviderOptions.CreateForWasm(PythonGuestModule.GetModulePath())); + +var instructions = + """ + ## Technical Assistant Instructions + + You are a code-powered technical assistant. You can execute Python code in a sandboxed environment + to solve problems precisely rather than guessing. You also have access to skills that provide + structured workflows for specific technical tasks. + + ### Code Execution + + When a problem requires computation, validation, or testing: + - Write Python code and use `execute_code` to run it in the sandbox. + - Always verify results by running the code rather than reasoning about what would happen. + - If code fails, read the error message carefully, fix the issue, and retry. + + ### Skills + + You have access to discoverable skills. When a task matches a skill's description: + - Follow the skill's instructions carefully. + - Use the skill's reference materials for context. + - Combine the skill's workflow with code execution when appropriate. + + ### Planning and Research + + For complex tasks: + - Break the problem into steps using your todo list. + - Research background information using web search when needed. + - Save important findings to file memory for later reference. + + ### Presenting Results + + - Show your work: include the code you ran and its output. + - Explain what each part of your solution does. + - If applicable, save final results to file memory. + """; + +// Create the agent with ALL HarnessAgent features enabled plus Hyperlight CodeAct. +// No Disable* flags are set — TodoProvider, AgentModeProvider, FileMemory, FileAccess, +// ToolApproval, WebSearch, and AgentSkillsProvider are all active. +AIAgent agent = + new AIProjectClient( + new Uri(endpoint), + new DefaultAzureCredential(), + new AIProjectClientOptions { RetryPolicy = new ClientRetryPolicy(3) }) + .GetProjectOpenAIClient() + .GetResponsesClient() + .AsIChatClient(deploymentName) + .AsHarnessAgent(MaxContextWindowTokens, MaxOutputTokens, new HarnessAgentOptions + { + Name = "CodeExecutionAgent", + Description = "A technical assistant with sandboxed code execution and skill-based workflows.", + OpenTelemetrySourceName = TracingSourceName, + // Point the file memory at a local folder for persistent memory across sessions. + FileMemoryStore = new FileSystemAgentFileStore(Path.Combine(AppContext.BaseDirectory, "agent-files")), + // Add the HyperlightCodeActProvider so the agent can execute Python code in a sandbox. + AIContextProviders = [codeAct], + ChatOptions = new ChatOptions + { + Instructions = instructions, + MaxOutputTokens = MaxOutputTokens, + Reasoning = new() { Effort = ReasoningEffort.Medium }, + }, + }); + +// Run the interactive console session using the shared HarnessConsole helper. +await HarnessConsole.RunAgentAsync( + agent, + userPrompt: "Ask me a technical question, or try: \"Help me write a regex that matches valid email addresses.\"", + new HarnessConsoleOptions + { + Observers = HarnessConsoleOptions.BuildObserversWithPlanning( + agent, + planModeName: "plan", + executionModeName: "execute", + maxContextWindowTokens: MaxContextWindowTokens, + maxOutputTokens: MaxOutputTokens), + CommandHandlers = HarnessConsoleOptions.BuildDefaultCommandHandlers(agent), + }); diff --git a/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/README.md b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/README.md new file mode 100644 index 0000000000..0d1b109bee --- /dev/null +++ b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/README.md @@ -0,0 +1,51 @@ +# Harness Step 04 — Code Execution (Hyperlight + Skills) + +This sample demonstrates a HarnessAgent with **all features enabled**, plus: + +- **Hyperlight CodeAct** — sandboxed Python code execution via `execute_code` (requires KVM) +- **Skills** — file-based skill discovery (a `regex-tester` skill is included) + +The agent can plan tasks, manage modes, store memories, read/write files, search the web, approve sensitive operations, discover and use skills, and execute arbitrary Python code — all pre-configured by the HarnessAgent. + +## Prerequisites + +- .NET 10 SDK +- An Azure AI Foundry project endpoint +- KVM-capable host (the Hyperlight sandbox runs code in micro-VMs) + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `AZURE_AI_PROJECT_ENDPOINT` | Your Azure AI Foundry project endpoint | +| `AZURE_AI_MODEL_DEPLOYMENT_NAME` | Model deployment name (default: `gpt-5.4`) | + +## Running + +```bash +dotnet run +``` + +## What to Try + +- **Regex testing**: "Help me write a regex that matches valid email addresses, then test it against some examples." +- **Code execution**: "Calculate the first 20 prime numbers using the Sieve of Eratosthenes." +- **Skill + code combo**: "I need a regex for ISO 8601 dates — test it thoroughly with edge cases." + +## Included Skill + +The `skills/regex-tester/` skill instructs the agent to validate regex patterns by executing Python test code in the Hyperlight sandbox. It includes a regex cheatsheet as reference material. + +## Features Enabled + +| Feature | Description | +|---------|-------------| +| TodoProvider | Task planning and tracking (`/todos` command) | +| AgentModeProvider | Mode switching (`/mode` command) | +| FileMemoryProvider | Persistent memory stored as files | +| FileAccessProvider | Read/write files in a working directory | +| ToolApproval | Don't-ask-again approval for sensitive tools | +| WebSearch | Built-in hosted web search | +| AgentSkillsProvider | Discovers and uses skills from the `skills/` folder | +| HyperlightCodeActProvider | Sandboxed Python execution via `execute_code` | +| OpenTelemetry | Trace logging to a text file | diff --git a/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/skills/regex-tester/SKILL.md b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/skills/regex-tester/SKILL.md new file mode 100644 index 0000000000..7d1c9c49e3 --- /dev/null +++ b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/skills/regex-tester/SKILL.md @@ -0,0 +1,36 @@ +--- +name: regex-tester +description: Validate, test, and debug regular expressions by executing them against sample inputs. Use when asked to build, verify, or explain a regex pattern. +--- + +## Usage + +When the user asks you to create, validate, or debug a regular expression: + +1. **Understand the requirement** — clarify what the pattern should match and what it should reject. +2. **Consult the cheatsheet** — review `references/regex-cheatsheet.md` for syntax reminders if needed. +3. **Write and execute test code** — use the `execute_code` tool to run Python code that: + - Compiles the regex with `re.compile()` + - Tests it against a set of positive examples (should match) and negative examples (should not match) + - Extracts and displays any capturing groups + - Reports pass/fail for each test case +4. **Iterate** — if any test fails, refine the pattern and re-run until all cases pass. +5. **Present the result** — give the user the final pattern, explain what each part does, and show the test results. + +## Example Test Script + +```python +import re + +pattern = re.compile(r'^[\w.+-]+@[\w-]+\.[\w.-]+$') + +positives = ["user@example.com", "first.last+tag@sub.domain.org"] +negatives = ["@missing.com", "no-at-sign", "spaces in@address.com"] + +for s in positives: + assert pattern.match(s), f"FAIL: expected match for '{s}'" +for s in negatives: + assert not pattern.match(s), f"FAIL: expected no match for '{s}'" + +print("All tests passed!") +``` diff --git a/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/skills/regex-tester/references/regex-cheatsheet.md b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/skills/regex-tester/references/regex-cheatsheet.md new file mode 100644 index 0000000000..342719673a --- /dev/null +++ b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/skills/regex-tester/references/regex-cheatsheet.md @@ -0,0 +1,97 @@ +# Regex Quick Reference (Python `re` module) + +## Character Classes + +| Pattern | Matches | +|---------|---------| +| `.` | Any character except newline | +| `\d` | Digit `[0-9]` | +| `\D` | Non-digit | +| `\w` | Word character `[a-zA-Z0-9_]` | +| `\W` | Non-word character | +| `\s` | Whitespace `[ \t\n\r\f\v]` | +| `\S` | Non-whitespace | +| `[abc]` | Any of a, b, or c | +| `[^abc]`| Any character except a, b, c | +| `[a-z]` | Range: a through z | + +## Quantifiers + +| Pattern | Meaning | +|---------|---------| +| `*` | 0 or more (greedy) | +| `+` | 1 or more (greedy) | +| `?` | 0 or 1 (greedy) | +| `{n}` | Exactly n | +| `{n,}` | n or more | +| `{n,m}` | Between n and m | +| `*?`, `+?`, `??` | Non-greedy versions | + +## Anchors + +| Pattern | Meaning | +|---------|---------| +| `^` | Start of string (or line with `re.MULTILINE`) | +| `$` | End of string (or line with `re.MULTILINE`) | +| `\b` | Word boundary | +| `\B` | Non-word boundary | + +## Groups and Backreferences + +| Pattern | Meaning | +|---------|---------| +| `(...)` | Capturing group | +| `(?:...)`| Non-capturing group | +| `(?P...)` | Named group | +| `\1` | Backreference to group 1 | +| `(?=...)` | Positive lookahead | +| `(?!...)` | Negative lookahead | +| `(?<=...)` | Positive lookbehind | +| `(?\d{4})-(?P\d{2})-(?P\d{2})', "2025-01-15") +m.group('year') # '2025' + +# Replace +re.sub(r'\d+', 'X', "abc 123 def") # 'abc X def' + +# Split +re.split(r',+', "a,b,,c") # ['a', 'b', 'c'] + +# Compile for reuse +pattern = re.compile(r'^\d{4}-\d{2}-\d{2}$') +pattern.match("2025-01-15") # Match object +```