From c4da7cef255d6244da4307fdab2c9cd79a83630a Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:37:03 +0900 Subject: [PATCH 1/4] Fix broken Harness Agent bookmarks (#1085) --- .../agents/background-responses.md | 2 +- agent-framework/concepts/harness.md | 28 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/agent-framework/agents/background-responses.md b/agent-framework/agents/background-responses.md index 9982fb19..df3f3a91 100644 --- a/agent-framework/agents/background-responses.md +++ b/agent-framework/agents/background-responses.md @@ -273,7 +273,7 @@ Background responses require an explicit session via `agent.WithSession(session) A Harness Agent remains a standard Agent Framework agent, so provider background responses use the same per-run options documented above. Harness construction doesn't enable provider background responses automatically: set `AllowBackgroundResponses` in .NET or `options={"background": True}` in Python when starting the run, keep the session, and persist continuation tokens when the operation must survive a process restart. -This is separate from [background agents](background-agents.md#use-background-agents-with-harnessed-agent), which delegate work to child agents rather than continuing one provider request. +This is separate from [background agents](background-agents.md#use-background-agents-with-harness-agent), which delegate work to child agents rather than continuing one provider request. ## Best Practices diff --git a/agent-framework/concepts/harness.md b/agent-framework/concepts/harness.md index 4ecc9bad..e613542d 100644 --- a/agent-framework/concepts/harness.md +++ b/agent-framework/concepts/harness.md @@ -36,27 +36,27 @@ The Harness composes existing Agent Framework building blocks rather than defini 1. **Middleware and decorators** — add approval handling, observability, and optional bounded looping. 1. **Application UX** — streams responses, displays progress, and collects input such as tool approvals. -The resulting object remains a normal Agent Framework agent: a `HarnessAgent` that derives from `AIAgent` in .NET, or an `Agent` returned by `create_harness_agent` in Python. Its sessions use the same [session](./agents/conversations/session.md#use-sessions-with-harnessed-agent) and [context provider](./agents/conversations/context-providers.md#use-context-providers-with-harnessed-agent) abstractions as other agents. +The resulting object remains a normal Agent Framework agent: a `HarnessAgent` that derives from `AIAgent` in .NET, or an `Agent` returned by `create_harness_agent` in Python. Its sessions use the same [session](./agents/conversations/session.md#use-sessions-with-harness-agent) and [context provider](./agents/conversations/context-providers.md#use-context-providers-with-harness-agent) abstractions as other agents. ## Harness capability matrix | Capability | Harness behavior | Canonical guidance | |---|---|---| | Function invocation | Enabled with a configurable per-request iteration limit. | [Function tools](../agents/tools/function-tools.md#use-function-tools-with-harnessed-agent) | -| Per-service-call history persistence | Persists history after each model call in a tool-calling run. | [Sessions](./agents/conversations/session.md#use-sessions-with-harnessed-agent) | -| Compaction | Enabled when token limits or a custom strategy are supplied. | [Compaction](./agents/conversations/compaction.md#use-compaction-with-harnessed-agent) | -| Todo tracking | Enabled by default. | [Planning and todos](../agents/planning-and-todos.md#use-planning-and-todos-with-harnessed-agent) | -| Agent modes | Plan and execute modes are enabled by default. | [Planning and todos](../agents/planning-and-todos.md#use-planning-and-todos-with-harnessed-agent) | -| File memory and file access | Session file memory is enabled by default; shared file access is opt-in. | [Context providers](./agents/conversations/context-providers.md#use-context-providers-with-harnessed-agent) | +| Per-service-call history persistence | Persists history after each model call in a tool-calling run. | [Sessions](./agents/conversations/session.md#use-sessions-with-harness-agent) | +| Compaction | Enabled when token limits or a custom strategy are supplied. | [Compaction](./agents/conversations/compaction.md#use-compaction-with-harness-agent) | +| Todo tracking | Enabled by default. | [Planning and todos](../agents/planning-and-todos.md#use-planning-and-todos-with-harness-agent) | +| Agent modes | Plan and execute modes are enabled by default. | [Planning and todos](../agents/planning-and-todos.md#use-planning-and-todos-with-harness-agent) | +| File memory and file access | Session file memory is enabled by default; shared file access is opt-in. | [Context providers](./agents/conversations/context-providers.md#use-context-providers-with-harness-agent) | | Tool approval | Standing approvals and auto-approval rules are enabled by default. | [Tool approval](../agents/tools/tool-approval.md#use-tool-approval-with-harnessed-agent) | | OpenTelemetry | Agent observability is enabled by default. | [Observability](../agents/observability.md#use-observability-with-harnessed-agent) | | Web search | Added by default where the selected chat client supports it. | [Web search](../agents/tools/web-search.md#use-web-search-with-harnessed-agent) | -| Agent Skills | Enabled by default in .NET; opt-in through a provider or paths in Python. | [Agent Skills](../agents/skills.md#use-agent-skills-with-harnessed-agent) | -| Background agents | Optional parallel delegation to named child agents. | [Background agents](../agents/background-agents.md#use-background-agents-with-harnessed-agent) | +| Agent Skills | Enabled by default in .NET; opt-in through a provider or paths in Python. | [Agent Skills](../agents/skills.md#use-agent-skills-with-harness-agent) | +| Background agents | Optional parallel delegation to named child agents. | [Background agents](../agents/background-agents.md#use-background-agents-with-harness-agent) | | Shell execution | Composed from the shell package; the Python factory can wire it automatically. | [Shell tools](../integrations/by-component/tools/shell-tools.md#use-shell-tools-with-harnessed-agent) | -| Looping | Optional bounded re-invocation driven by evaluators or predicates. | [Agent looping](../agents/looping.md#use-looping-with-harnessed-agent) | +| Looping | Optional bounded re-invocation driven by evaluators or predicates. | [Agent looping](../agents/looping.md#use-looping-with-harness-agent) | -Background-agent delegation is separate from provider-managed [background responses](../agents/background-responses.md#use-background-responses-with-harnessed-agent). Background agents run child agents on delegated tasks; background responses poll or resume one provider request by using a continuation token. +Background-agent delegation is separate from provider-managed [background responses](../agents/background-responses.md#use-background-responses-with-harness-agent). Background agents run child agents on delegated tasks; background responses poll or resume one provider request by using a continuation token. ::: zone pivot="programming-language-csharp" @@ -198,11 +198,11 @@ The repository doesn't currently include a packaged Go Harness terminal sample. ## Next steps > [!div class="nextstepaction"] -> [Plan work and track todos](../agents/planning-and-todos.md#use-planning-and-todos-with-harnessed-agent) +> [Plan work and track todos](../agents/planning-and-todos.md#use-planning-and-todos-with-harness-agent) ### Go deeper -- [Looping](../agents/looping.md#use-looping-with-harnessed-agent) -- [Background agents](../agents/background-agents.md#use-background-agents-with-harnessed-agent) -- [Compaction](./agents/conversations/compaction.md#use-compaction-with-harnessed-agent) +- [Looping](../agents/looping.md#use-looping-with-harness-agent) +- [Background agents](../agents/background-agents.md#use-background-agents-with-harness-agent) +- [Compaction](./agents/conversations/compaction.md#use-compaction-with-harness-agent) - [Shell tools](../integrations/by-component/tools/shell-tools.md#use-shell-tools-with-harnessed-agent) From 38a5ebfeeed0b7c3ca8493d67b60b8ee460d6605 Mon Sep 17 00:00:00 2001 From: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:04:11 -0700 Subject: [PATCH 2/4] Document .NET workflow protocol declaration attributes (#1086) --- .../concepts/workflows/executors.md | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/agent-framework/concepts/workflows/executors.md b/agent-framework/concepts/workflows/executors.md index b1c3b52e..acc32299 100644 --- a/agent-framework/concepts/workflows/executors.md +++ b/agent-framework/concepts/workflows/executors.md @@ -5,7 +5,7 @@ zone_pivot_groups: programming-languages author: TaoChenOSU ms.topic: article ms.author: taochen -ms.date: 07/01/2026 +ms.date: 08/11/2026 ms.service: agent-framework --- @@ -20,6 +20,7 @@ ms.service: agent-framework | Function-Based Executors | ✅ | ✅ | ✅ | | | Explicit Type Parameters | ❌ | ✅ | ❌ | Python-specific | | The WorkflowContext Object | ✅ | ✅ | ✅ | | + | Declaring Protocol Types | ✅ | ❌ | ❌ | C#-specific protocol declaration attributes | | Designating Terminal and Intermediate Outputs | ❌ | ✅ | ❌ | Python-specific | | Agent Executors | ❌ | ❌ | ✅ | Go-specific | | Executor Lifecycle | ❌ | ❌ | ✅ | Go-specific | @@ -137,6 +138,47 @@ internal sealed partial class LogExecutor() : Executor("LogExecutor") } ``` +## Declaring Protocol Types + +An executor's protocol declares the message types it may send to connected executors and the output types it may yield. The workflow validates calls to `SendMessageAsync` and `YieldOutputAsync` against these declarations and throws an `InvalidOperationException` when an executor uses an undeclared type. + +Use `[SendsMessage]` to declare sent message types and `[YieldsOutput]` to declare yielded output types. These attributes describe the executor's capabilities; they do not send or yield values themselves. Apply each attribute multiple times when the executor uses multiple types. + +For executors with a single typed handler, derive from `Executor` or `Executor` and override `HandleAsync`: + +```csharp +internal sealed record ProcessRequest(string Text); +internal sealed record ProgressUpdate(string Status); + +[SendsMessage(typeof(ProgressUpdate))] +[YieldsOutput(typeof(string))] +internal sealed partial class ProcessingExecutor() + : Executor("ProcessingExecutor") +{ + public override async ValueTask HandleAsync( + ProcessRequest message, + IWorkflowContext context, + CancellationToken cancellationToken = default) + { + await context.SendMessageAsync( + new ProgressUpdate("Processing started"), + cancellationToken); + + await context.YieldOutputAsync( + message.Text.ToUpperInvariant(), + cancellationToken); + } +} +``` + +When the workflows source generator is referenced, a class with `[SendsMessage]` or `[YieldsOutput]` must be declared `partial` so the generator can add its protocol configuration. + +For source-generated executors with `[MessageHandler]` methods, declare types used by one handler with its `Send` and `Yield` named arguments, such as `[MessageHandler(Send = [typeof(ProgressUpdate)], Yield = [typeof(string)])]`. Use class-level `[SendsMessage]` and `[YieldsOutput]` when the declarations apply to the entire executor. + +Non-void handler return types are automatically added to the sent and yielded protocol types when `ExecutorOptions.AutoSendMessageHandlerResultObject` and `ExecutorOptions.AutoYieldOutputHandlerResultObject` are enabled. Both options are enabled by default. Explicit declarations are therefore primarily needed for additional types emitted directly through `SendMessageAsync` or `YieldOutputAsync`. + +`[YieldsOutput]` permits the executor to yield a type, but it does not designate the executor as a terminal output source. Register the executor with `WorkflowBuilder.WithOutputFrom` for its yielded values to surface to the workflow caller. + ::: zone-end ::: zone pivot="programming-language-python" From c5d33a8ce6bf17148a0bde2c30efcfa3616ba6dc Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 14 Aug 2026 00:33:39 -0700 Subject: [PATCH 3/4] Streamline C# AG-UI documentation (#1088) * Document C# self-hosting sessions Add source-verified guidance for hosted session persistence, durable stores, and multi-user isolation, with a focused cross-link from the core session concept. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c15f5140-beac-4f46-b9bd-55e52f465cf5 * Align C# self-hosting overview flow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c15f5140-beac-4f46-b9bd-55e52f465cf5 * Streamline C# AG-UI documentation Focus the .NET guidance on MAF-owned AG-UI behavior, correct support boundaries, and remove unsupported future-content promises. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c15f5140-beac-4f46-b9bd-55e52f465cf5 * Link AG-UI session hosting guidance Keep thread continuation behavior in the AG-UI docs while linking shared persistence and isolation configuration from the self-hosting overview. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c15f5140-beac-4f46-b9bd-55e52f465cf5 * Clarify AG-UI session guidance placement Keep conversation continuity in Getting Started and reduce the security article to the thread ID authorization warning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c15f5140-beac-4f46-b9bd-55e52f465cf5 * Address AG-UI documentation feedback Replace the support matrix with direct feature guidance, remove parity-table comments, and clarify that MCP Apps behavior is external to MAF. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c15f5140-beac-4f46-b9bd-55e52f465cf5 * Complete AG-UI continuation guidance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c15f5140-beac-4f46-b9bd-55e52f465cf5 --------- Copilot-Session: c15f5140-beac-4f46-b9bd-55e52f465cf5 --- agent-framework/TOC.yml | 2 +- .../concepts/agents/conversations/session.md | 5 +- agent-framework/hosting/self-hosting/index.md | 103 +++- .../ui/ag-ui/backend-tool-rendering.md | 281 ++-------- .../by-component/ui/ag-ui/frontend-tools.md | 157 ++---- .../by-component/ui/ag-ui/getting-started.md | 382 +++---------- .../ui/ag-ui/human-in-the-loop.md | 162 ++---- .../by-component/ui/ag-ui/index.md | 115 ++-- .../by-component/ui/ag-ui/mcp-apps.md | 19 +- .../ui/ag-ui/security-considerations.md | 19 +- .../by-component/ui/ag-ui/state-management.md | 516 +++--------------- .../ui/ag-ui/testing-with-dojo.md | 20 +- .../by-component/ui/ag-ui/workflows.md | 67 +-- 13 files changed, 469 insertions(+), 1379 deletions(-) diff --git a/agent-framework/TOC.yml b/agent-framework/TOC.yml index fd6aa02a..b6057ea4 100644 --- a/agent-framework/TOC.yml +++ b/agent-framework/TOC.yml @@ -307,7 +307,7 @@ items: href: integrations/by-component/ui/ag-ui/backend-tool-rendering.md - name: Frontend Tool Rendering href: integrations/by-component/ui/ag-ui/frontend-tools.md - - name: Security Considerations + - name: Production and Security Considerations href: integrations/by-component/ui/ag-ui/security-considerations.md - name: Workflows href: integrations/by-component/ui/ag-ui/workflows.md diff --git a/agent-framework/concepts/agents/conversations/session.md b/agent-framework/concepts/agents/conversations/session.md index dd3cd631..b63b661d 100644 --- a/agent-framework/concepts/agents/conversations/session.md +++ b/agent-framework/concepts/agents/conversations/session.md @@ -5,7 +5,7 @@ zone_pivot_groups: programming-languages author: eavanvalkenburg ms.topic: article ms.author: edvan -ms.date: 07/29/2026 +ms.date: 08/11/2026 ms.service: agent-framework --- @@ -19,6 +19,7 @@ ms.service: agent-framework | Harness Agent session usage | ✅ | ✅ | ❌ | Harness Agent isn't available in Go | | Existing service conversation ID | ✅ | ✅ | ❌ | | | Serialization and restoration | ✅ | ✅ | ✅ | | + | Hosted session persistence | ✅ | ❌ | ❌ | .NET hosting-specific | --> # Session @@ -205,6 +206,8 @@ var serialized = agent.SerializeSession(session); AgentSession resumed = await agent.DeserializeSessionAsync(serialized); ``` +In a self-hosted application, an `AgentSessionStore` can load and save sessions by a continuation ID as part of request processing. This is distinct from manually persisting a session and from configuring a history provider. See [Self-host Agent Framework applications](../../../hosting/self-hosting/index.md#persist-hosted-sessions). + :::zone-end :::zone pivot="programming-language-python" diff --git a/agent-framework/hosting/self-hosting/index.md b/agent-framework/hosting/self-hosting/index.md index 1afcc9f4..18b820ab 100644 --- a/agent-framework/hosting/self-hosting/index.md +++ b/agent-framework/hosting/self-hosting/index.md @@ -5,16 +5,113 @@ zone_pivot_groups: programming-languages author: eavanvalkenburg ms.topic: article ms.author: edvan -ms.date: 07/22/2026 +ms.date: 08/11/2026 ms.service: agent-framework --- + + # Self-host Agent Framework applications :::zone pivot="programming-language-csharp" -> [!NOTE] -> Self-hosting protocol helpers for .NET are coming soon. The hosting model will let your application own its server, state, and protocol integrations. +Self-hosting lets you run an Agent Framework agent or workflow in your own ASP.NET Core application, container, service, or runtime. Your application controls routing, identity, authorization, request policy, storage, deployment, and scaling. Add protocol integrations to the host based on the clients you need to support. + +Use this option when you need to integrate an agent endpoint with your existing application infrastructure. If you want Microsoft Foundry to run the agent for you, see [Foundry Hosted Agents](../foundry-hosted-agent.md). If you need Azure Functions triggers or durable execution, see [Durable Extension](../azure-functions.md). + +> [!IMPORTANT] +> The .NET hosting packages are prerelease. Install prerelease versions explicitly and review release notes before updating a production deployment. + +```dotnetcli +dotnet add package Microsoft.Agents.AI.Hosting --prerelease +``` + +## What the hosting helpers provide + +The `Microsoft.Agents.AI.Hosting` package integrates agents and workflows with the .NET generic host: + +- `AddAIAgent` registers a named `AIAgent` with dependency injection. +- `AddWorkflow` registers a named workflow. Chain `AddAsAIAgent` to make the workflow available to protocol integrations through the standard agent interface. +- `IHostedAgentBuilder` configures hosting services associated with that agent. +- `AgentSessionStore` optionally loads and saves `AgentSession` instances by an application- or protocol-supplied continuation ID. + +The hosting package isn't an HTTP server or protocol registry. Your application selects the hosted agents and workflows, configures their services, and adds the protocol endpoints it needs. + +## Persist hosted sessions + +Session persistence is opt-in. Without a configured `AgentSessionStore`, protocol integrations can create a new session for each request but can't recover server-owned session state from an earlier request. + +For development or a single-process application, configure the built-in in-memory store: + +```csharp +builder.AddAIAgent("weather-agent", (_, _) => agent) + .WithInMemorySessionStore(withIsolation: false); +``` + +Setting `withIsolation` to `false` is appropriate only when one trusted user or process owns the session namespace. `InMemoryAgentSessionStore` loses all sessions when the process exits and doesn't share state across application instances. + +For durable or distributed hosting, implement `AgentSessionStore` and register it with `WithSessionStore`. A store implements asynchronous save, get, and delete operations. It receives the owning `AIAgent` and an opaque session-store ID, and it must return an independent `AgentSession` instance from each get operation. + +`AgentSessionStore` and [history providers](../../concepts/agents/conversations/storage.md) serve different purposes. A session store persists the `AgentSession` selected by a hosted request. A history provider controls where conversation messages are stored. When history is held in session state, persisting the session also persists that history; an external history provider stores messages separately. + +## Integrate with ASP.NET Core + +The shared hosting package uses the .NET generic host and dependency injection. For an HTTP server, create an ASP.NET Core application and add the protocol-specific packages for the endpoints you want to expose. Those packages resolve named `AIAgent` instances from dependency injection and add ASP.NET Core route mappings. + +Your application remains responsible for its middleware pipeline, authentication, authorization, request validation, allowed model options, and durable storage. A non-HTTP host can use the shared hosting services without adding ASP.NET Core protocol endpoints. + +## Add protocols to your server + +Choose the protocol integrations your application needs: + +| Protocol | Integration | +|---|---| +| [OpenAI-compatible endpoints](openai-endpoints.md) | Chat Completions and Responses-compatible HTTP endpoints | +| [A2A](a2a/server.md) | Agent-to-agent discovery, messaging, and task endpoints | +| [AG-UI](../../integrations/by-component/ui/ag-ui/index.md) | Event-streaming endpoints for web agent applications | + +Each protocol defines its own continuation identifier and endpoint behavior. Keep authentication, authorization, session ownership, and durable storage in shared application infrastructure rather than reimplementing them for each endpoint. + +## Secure session continuation + +A continuation ID identifies a session to resume; it doesn't prove that the caller owns that session. Scope persisted sessions by an authenticated user, tenant, or other authorization boundary before accepting client-supplied IDs. + +For ASP.NET Core applications that use claims-based authentication, install the prerelease `Microsoft.Agents.AI.Hosting.AspNetCore` package, register the claims-based isolation provider, and keep isolation enabled on the session store: + +```csharp +builder.Services.AddHttpContextAccessor(); +builder.Services.UseClaimsBasedAgentIsolation(); + +builder.AddAIAgent("weather-agent", (_, _) => agent) + .WithInMemorySessionStore(); +``` + +By default, `UseClaimsBasedAgentIsolation` uses the `ClaimTypes.NameIdentifier` claim. Configure another claim only when it is stable and unique across every caller served by the store. The isolation provider doesn't authenticate requests; configure ASP.NET Core authentication and authorization separately. With the default strict isolation behavior, session access fails when the current principal doesn't provide the configured claim. + +For a non-HTTP host or another tenancy model, register a custom `AgentIsolationKeyProvider`. The default `WithInMemorySessionStore()` and `WithSessionStore(...)` overloads wrap the configured store in `IsolationKeyScopedAgentSessionStore`. + +## Next steps + +> [!div class="nextstepaction"] +> [Add an OpenAI-compatible endpoint](openai-endpoints.md) + +**Go deeper:** + +- [Host agents with A2A](a2a/server.md) +- [Build web agent applications with AG-UI](../../integrations/by-component/ui/ag-ui/index.md) +- [Foundry Hosted Agents](../foundry-hosted-agent.md) :::zone-end diff --git a/agent-framework/integrations/by-component/ui/ag-ui/backend-tool-rendering.md b/agent-framework/integrations/by-component/ui/ag-ui/backend-tool-rendering.md index a275c67a..45ab16e6 100644 --- a/agent-framework/integrations/by-component/ui/ag-ui/backend-tool-rendering.md +++ b/agent-framework/integrations/by-component/ui/ag-ui/backend-tool-rendering.md @@ -5,286 +5,85 @@ zone_pivot_groups: programming-languages author: moonbox3 ms.topic: tutorial ms.author: evmattso -ms.date: 07/01/2026 +ms.date: 08/11/2026 ms.service: agent-framework --- -# Backend Tool Rendering with AG-UI - -::: zone pivot="programming-language-csharp" + -Before you begin, ensure you have completed the [Getting Started](getting-started.md) tutorial and have: +# Backend Tool Rendering with AG-UI -- .NET 8.0 or later -- `Microsoft.Agents.AI.Hosting.AGUI.AspNetCore` package installed -- Azure OpenAI service configured -- Basic understanding of AG-UI server and client setup +::: zone pivot="programming-language-csharp" -## What is Backend Tool Rendering? +Backend tools use the normal MAF tool pipeline. AG-UI adds transport events so a client can observe the call and result; it doesn't introduce a separate tool abstraction. -Backend tool rendering means: +## Add a backend tool -- Function tools are defined on the server -- The AI agent decides when to call these tools -- Tools execute on the backend (server-side) -- Tool call events and results are streamed to the client in real-time -- The client receives updates about tool execution progress - -## Creating an AG-UI Server with Function Tools - -Here's a complete server implementation demonstrating how to register tools with complex parameter types: +Define and register the tool as you would for any MAF agent: ```csharp -// Copyright (c) Microsoft. All rights reserved. - using System.ComponentModel; -using System.Text.Json.Serialization; -using Azure.AI.OpenAI; -using Azure.Identity; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; using Microsoft.Extensions.AI; -using Microsoft.Extensions.Options; -using OpenAI.Chat; - -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -builder.Services.ConfigureHttpJsonOptions(options => - options.SerializerOptions.TypeInfoResolverChain.Add(SampleJsonSerializerContext.Default)); -builder.Services.AddAGUIServer(); - -WebApplication app = builder.Build(); -string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] - ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] - ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); +[Description("Get the weather for a location.")] +static string GetWeather( + [Description("The city to look up.")] string location) => + $"The weather in {location} is sunny."; -// Define the function tool -[Description("Search for restaurants in a location.")] -static RestaurantSearchResponse SearchRestaurants( - [Description("The restaurant search request")] RestaurantSearchRequest request) -{ - // Simulated restaurant data - string cuisine = request.Cuisine == "any" ? "Italian" : request.Cuisine; - - return new RestaurantSearchResponse - { - Location = request.Location, - Cuisine = request.Cuisine, - Results = - [ - new RestaurantInfo - { - Name = "The Golden Fork", - Cuisine = cuisine, - Rating = 4.5, - Address = $"123 Main St, {request.Location}" - }, - new RestaurantInfo - { - Name = "Spice Haven", - Cuisine = cuisine == "Italian" ? "Indian" : cuisine, - Rating = 4.7, - Address = $"456 Oak Ave, {request.Location}" - }, - new RestaurantInfo - { - Name = "Green Leaf", - Cuisine = "Vegetarian", - Rating = 4.3, - Address = $"789 Elm Rd, {request.Location}" - } - ] - }; -} +AITool getWeather = AIFunctionFactory.Create(GetWeather, name: "get_weather"); +AIAgent agent = chatClient.AsAIAgent(tools: [getWeather]); -// Get JsonSerializerOptions from the configured HTTP JSON options -Microsoft.AspNetCore.Http.Json.JsonOptions jsonOptions = app.Services.GetRequiredService>().Value; - -// Create tool with serializer options -AITool[] tools = -[ - AIFunctionFactory.Create( - SearchRestaurants, - name: "search_restaurants", - serializerOptions: jsonOptions.SerializerOptions) -]; - -// Create the AI agent with tools -AIAgent agent = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) - .GetChatClient(deploymentName) - .AsAIAgent( - name: "AGUIAssistant", - instructions: "You are a helpful assistant with access to restaurant information.", - tools: tools); - -// Map the AG-UI agent endpoint app.MapAGUIServer("/", agent); - -await app.RunAsync(); - -// Define request/response types for the tool -internal sealed class RestaurantSearchRequest -{ - public string Location { get; set; } = string.Empty; - public string Cuisine { get; set; } = "any"; -} - -internal sealed class RestaurantSearchResponse -{ - public string Location { get; set; } = string.Empty; - public string Cuisine { get; set; } = string.Empty; - public RestaurantInfo[] Results { get; set; } = []; -} - -internal sealed class RestaurantInfo -{ - public string Name { get; set; } = string.Empty; - public string Cuisine { get; set; } = string.Empty; - public double Rating { get; set; } - public string Address { get; set; } = string.Empty; -} - -// JSON serialization context for source generation -[JsonSerializable(typeof(RestaurantSearchRequest))] -[JsonSerializable(typeof(RestaurantSearchResponse))] -internal sealed partial class SampleJsonSerializerContext : JsonSerializerContext; ``` -> [!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. - -### Key Concepts +For complex request or response types, configure the same `JsonSerializerOptions` for ASP.NET Core and `AIFunctionFactory.Create`. -- **Server-side execution**: Tools execute in the server process -- **Automatic streaming**: Tool calls and results are streamed to clients in real-time - -> [!IMPORTANT] -> When creating tools with complex parameter types (objects, arrays, etc.), you must provide the `serializerOptions` parameter to `AIFunctionFactory.Create()`. The serializer options should be obtained from the application's configured `JsonOptions` via `IOptions` to ensure consistency with the rest of the application's JSON serialization. - -### Running the Server +> [!TIP] +> See the [.NET backend-tools sample](https://github.com/microsoft/agent-framework/tree/main/dotnet/samples/02-agents/AGUI/Step02_BackendTools) for a complete implementation. -Set environment variables and run: +For tool schemas, dependency injection, error handling, and general tool design, see [Use function tools with an agent](../../../../agents/tools/function-tools.md). -```bash -export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" -export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" -dotnet run --urls http://localhost:8888 -``` +## AG-UI event mapping -## Observing Tool Calls in the Client +When the agent calls the tool: -The basic client from the Getting Started tutorial displays the agent's final text response. However, you can extend it to observe tool calls and results as they're streamed from the server. +- `FunctionCallContent` is emitted as AG-UI `TOOL_CALL_START`, `TOOL_CALL_ARGS`, and `TOOL_CALL_END` events. +- `FunctionResultContent` is emitted as a `TOOL_CALL_RESULT` event. +- Text and other agent content continue to stream normally. -### Displaying Tool Execution Details - -To see tool calls and results in real-time, extend the client's streaming loop to handle `FunctionCallContent` and `FunctionResultContent`: +A .NET client receives the translated content as `FunctionCallContent` and `FunctionResultContent`: ```csharp -// Inside the streaming loop from getting-started.md await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session)) { - ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate(); - - // ... existing run started code ... - - // Display streaming content foreach (AIContent content in update.Contents) { - switch (content) + if (content is FunctionCallContent call) { - case TextContent textContent: - Console.ForegroundColor = ConsoleColor.Cyan; - Console.Write(textContent.Text); - Console.ResetColor(); - break; - - case FunctionCallContent functionCallContent: - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"\n[Function Call - Name: {functionCallContent.Name}]"); - - // Display individual parameters - if (functionCallContent.Arguments != null) - { - foreach (var kvp in functionCallContent.Arguments) - { - Console.WriteLine($" Parameter: {kvp.Key} = {kvp.Value}"); - } - } - Console.ResetColor(); - break; - - case FunctionResultContent functionResultContent: - Console.ForegroundColor = ConsoleColor.Magenta; - Console.WriteLine($"\n[Function Result - CallId: {functionResultContent.CallId}]"); - - if (functionResultContent.Exception != null) - { - Console.WriteLine($" Exception: {functionResultContent.Exception}"); - } - else - { - Console.WriteLine($" Result: {functionResultContent.Result}"); - } - Console.ResetColor(); - break; - - case ErrorContent errorContent: - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"\n[Error: {errorContent.Message}]"); - Console.ResetColor(); - break; + Console.WriteLine($"Calling {call.Name}"); + } + else if (content is FunctionResultContent result) + { + Console.WriteLine($"Result: {result.Result}"); } } } ``` -### Expected Output with Tool Calls - -When the agent calls backend tools, you'll see: - -``` -User (:q or quit to exit): What's the weather like in Amsterdam? - -[Run Started - Run: run_xyz789] - -[Function Call - Name: search_restaurants] - Parameter: Location = Amsterdam - Parameter: Cuisine = any - -[Function Result - CallId: call_def456] - Result: {"Location":"Amsterdam","Cuisine":"any","Results":[...]} +Tool results are model-facing values that AG-UI also exposes to the client. To emit shared UI state in addition to a tool result, use the explicit mappings described in [State management](./state-management.md). -The weather in Amsterdam is sunny with a temperature of 22°C. Here are some -great restaurants in the area: The Golden Fork (Italian, 4.5 stars)... -[Run Finished] -``` - -### Key Concepts +## Next steps -- **`FunctionCallContent`**: Represents a tool being called with its `Name` and `Arguments` (parameter key-value pairs) -- **`FunctionResultContent`**: Contains the tool's `Result` or `Exception`, identified by `CallId` - -## Next Steps - -Now that you can add function tools, you can: - -- **[Frontend tools](frontend-tools.md)**: Add frontend tools. - - -- **[Test with Dojo](testing-with-dojo.md)**: Use AG-UI's Dojo app to test your agents - -## Additional Resources - -- [AG-UI Overview](index.md) -- [Getting Started Tutorial](getting-started.md) -- [Agent Framework Documentation](../../../../overview/index.md) +> [!div class="nextstepaction"] +> [Use frontend tools with AG-UI](./frontend-tools.md) ::: zone-end diff --git a/agent-framework/integrations/by-component/ui/ag-ui/frontend-tools.md b/agent-framework/integrations/by-component/ui/ag-ui/frontend-tools.md index e27cc297..e4da1a66 100644 --- a/agent-framework/integrations/by-component/ui/ag-ui/frontend-tools.md +++ b/agent-framework/integrations/by-component/ui/ag-ui/frontend-tools.md @@ -5,146 +5,67 @@ zone_pivot_groups: programming-languages author: moonbox3 ms.topic: tutorial ms.author: evmattso -ms.date: 07/01/2026 +ms.date: 08/11/2026 ms.service: agent-framework --- -# Frontend Tool Rendering with AG-UI - -::: zone pivot="programming-language-csharp" - -This tutorial shows you how to add frontend function tools to your AG-UI clients. Frontend tools are functions that execute on the client side, allowing the AI agent to interact with the user's local environment, access client-specific data, or perform UI operations. The server orchestrates when to call these tools, but the execution happens entirely on the client. + -- .NET 8.0 or later -- `AGUI.Client` package installed (the AG-UI C# SDK client) -- `Microsoft.Agents.AI` package installed -- Basic understanding of AG-UI client setup - -## What are Frontend Tools? - -Frontend tools are function tools that: +# Frontend Tool Rendering with AG-UI -- Are defined and registered on the client -- Execute in the client's environment (not on the server) -- Allow the AI agent to interact with client-specific resources -- Provide results back to the server for the agent to incorporate into responses -- Enable personalized, context-aware experiences +::: zone pivot="programming-language-csharp" -Common use cases: -- Reading local sensor data (GPS, temperature, etc.) -- Accessing client-side storage or preferences -- Performing UI operations (changing themes, displaying notifications) -- Interacting with device-specific features (camera, microphone) +Frontend tools are declared and executed by the AG-UI client. The server receives their schemas so the model can request them, but it doesn't receive their implementations. -## Registering Frontend Tools on the Client +## Register a frontend tool -The key difference from the Getting Started tutorial is registering tools with the client agent. Here's what changes: +Create the tool and pass it to the agent backed by `AGUIChatClient`: ```csharp -// Define a frontend function tool -[Description("Get the user's current location from GPS.")] -static string GetUserLocation() -{ - // Access client-side GPS - return "Amsterdam, Netherlands (52.37°N, 4.90°E)"; -} - -// Create frontend tools -AITool[] frontendTools = [AIFunctionFactory.Create(GetUserLocation)]; - -// Pass tools when creating the agent -AIAgent agent = chatClient.AsAIAgent( - name: "agui-client", - description: "AG-UI Client Agent", - tools: frontendTools); -``` - -The rest of your client code remains the same as shown in the Getting Started tutorial. - -### How Tools Are Sent to the Server +using System.ComponentModel; +using AGUI.Client; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; -When you register tools with `AsAIAgent()`, the `AGUIChatClient` automatically: +[Description("Get the user's current location from the client device.")] +static string GetUserLocation() => "Amsterdam, Netherlands"; -1. Captures the tool definitions (names, descriptions, parameter schemas) -2. Sends the tools with each request to the server agent, which maps them to `ChatClientAgentRunOptions.ChatOptions.Tools` +AITool locationTool = AIFunctionFactory.Create( + GetUserLocation, + name: "get_user_location"); -The server receives the client tool declarations and the AI model can decide when to call them. - -### Key Concepts - -The following are new concepts for frontend tools: - -- **Client-side registration**: Tools are registered on the client using `AIFunctionFactory.Create()` and passed to `AsAIAgent()` -- **Automatic capture**: Tools are automatically captured and sent via `ChatClientAgentRunOptions.ChatOptions.Tools` - -## How Frontend Tools Work - -### Server-Side Flow - -The server doesn't know the implementation details of frontend tools. It only knows: - -1. Tool names and descriptions (from client registration) -2. Parameter schemas -3. When to request tool execution - -When the AI agent decides to call a frontend tool: - -1. Server sends a tool call request to the client via SSE -2. Server waits for the client to execute the tool and return results -3. Server incorporates the results into the agent's context -4. Agent continues processing with the tool results - -### Client-Side Flow - -The client handles frontend tool execution: - -1. Receives `FunctionCallContent` from server indicating a tool call request -2. Matches the tool name to a locally registered function -3. Deserializes parameters from the request -4. Executes the function locally -5. Serializes the result -6. Sends `FunctionResultContent` back to the server -7. Continues receiving agent responses - -## Expected Output with Frontend Tools - -When the agent calls frontend tools, you'll see the tool call and result in the streaming output: - -``` -User (:q or quit to exit): Where am I located? - -[Client Tool Call - Name: GetUserLocation] -[Client Tool Result: Amsterdam, Netherlands (52.37°N, 4.90°E)] - -You are currently in Amsterdam, Netherlands, at coordinates 52.37°N, 4.90°E. +using HttpClient httpClient = new() { BaseAddress = new Uri("http://localhost:8888") }; +AGUIChatClient chatClient = new(new AGUIChatClientOptions(httpClient, "/")); +AIAgent agent = chatClient.AsAIAgent(tools: [locationTool]); ``` -## Server Setup for Frontend Tools +`AGUIChatClient` handles the continuation flow: -The server doesn't need special configuration to support frontend tools. Use the standard AG-UI server from the Getting Started tutorial - it automatically: -- Receives frontend tool declarations during client connection -- Requests tool execution when the AI agent needs them -- Waits for results from the client -- Incorporates results into the agent's decision-making +1. Sends the frontend tool declaration with the run request. +2. Receives the model's tool call from the server. +3. Executes the matching function locally. +4. Sends the result back to the server. +5. Continues the run and streams the final response. -## Next Steps +> [!TIP] +> See the [.NET frontend-tools sample](https://github.com/microsoft/agent-framework/tree/main/dotnet/samples/02-agents/AGUI/Step03_FrontendTools) for a complete client and server. -Now that you understand frontend tools, you can: +> [!WARNING] +> Tool declarations and results supplied by an untrusted client are untrusted input. Authorize which client tools may influence server-side agent execution, and validate results before using them for privileged operations. - - -- **[Combine with Backend Tools](backend-tool-rendering.md)**: Use both frontend and backend tools together +For general tool-authoring guidance, see [Use function tools with an agent](../../../../agents/tools/function-tools.md). -## Additional Resources +## Next steps -- [AG-UI Overview](index.md) -- [Getting Started Tutorial](getting-started.md) -- [Backend Tool Rendering](backend-tool-rendering.md) -- [Agent Framework Documentation](../../../../overview/index.md) +> [!div class="nextstepaction"] +> [Use human approval with AG-UI](./human-in-the-loop.md) ::: zone-end diff --git a/agent-framework/integrations/by-component/ui/ag-ui/getting-started.md b/agent-framework/integrations/by-component/ui/ag-ui/getting-started.md index 26e8a3c0..2d42724b 100644 --- a/agent-framework/integrations/by-component/ui/ag-ui/getting-started.md +++ b/agent-framework/integrations/by-component/ui/ag-ui/getting-started.md @@ -5,13 +5,23 @@ zone_pivot_groups: programming-languages author: moonbox3 ms.topic: tutorial ms.author: evmattso -ms.date: 07/10/2026 +ms.date: 08/11/2026 ms.service: agent-framework --- + + # Getting Started with AG-UI -This tutorial demonstrates how to build both server and client applications using the AG-UI protocol with .NET or Python and Agent Framework. You'll learn how to create an AG-UI server that hosts an AI agent and a client that connects to it for interactive conversations. +This tutorial demonstrates how to build server and client applications using the AG-UI protocol with Agent Framework. You'll learn how to host an agent behind an AG-UI endpoint and connect a client for interactive conversations. ## What You'll Build @@ -25,364 +35,130 @@ By the end of this tutorial, you'll have: ## Prerequisites -Before you begin, ensure you have the following: +- .NET 8 or later +- An ASP.NET Core project +- A configured MAF `AIAgent` -- .NET 8.0 or later -- [Azure OpenAI service endpoint and deployment configured](/azure/ai-foundry/openai/how-to/create-resource) -- [Azure CLI installed](/cli/azure/install-azure-cli) and [authenticated](/cli/azure/authenticate-azure-cli) -- User has the `Cognitive Services OpenAI Contributor` role for the Azure OpenAI resource +The example uses Azure OpenAI, but `MapAGUIServer` works with any MAF agent. -> [!NOTE] -> These samples use Azure OpenAI models. For more information, see [how to deploy Azure OpenAI models with Microsoft Foundry](/azure/ai-foundry/how-to/deploy-models-openai). - -> [!NOTE] -> These samples use `DefaultAzureCredential` for authentication. Make sure you're authenticated with Azure (e.g., via `az login`). For more information, see the [Azure Identity documentation](/dotnet/api/overview/azure/identity-readme). +## Create an AG-UI server -> [!WARNING] -> The AG-UI protocol is still under development and subject to change. We will keep these samples updated as the protocol evolves. +Install the hosting package: -## Step 1: Creating an AG-UI Server - -The AG-UI server hosts your AI agent and exposes it via HTTP endpoints using ASP.NET Core. - -> [!NOTE] -> The server project requires the `Microsoft.NET.Sdk.Web` SDK. If you're creating a new project from scratch, use `dotnet new web` or ensure your `.csproj` file uses `` instead of `Microsoft.NET.Sdk`. - -### Install Required Packages - -Install the necessary packages for the server: - -```bash +```dotnetcli dotnet add package Microsoft.Agents.AI.Hosting.AGUI.AspNetCore --prerelease -dotnet add package Microsoft.Agents.AI.OpenAI --prerelease -dotnet add package Azure.AI.OpenAI -dotnet add package Azure.Identity ``` -> [!NOTE] -> The `Microsoft.Agents.AI.OpenAI` package provides the `AsAIAgent()` extension method that turns an Azure OpenAI (or OpenAI) chat client into an Agent Framework agent. - -### Server Code - -Create a file named `Program.cs`: +Register AG-UI hosting and map your agent: ```csharp -// Copyright (c) Microsoft. All rights reserved. - -using Azure.AI.OpenAI; -using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; -using OpenAI.Chat; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.Services.AddAGUIServer(); -string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] - ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] - ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); - -// Build an agent directly from the Azure OpenAI chat client. -AIAgent agent = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) - .GetChatClient(deploymentName) - .AsAIAgent( - name: "AGUIAssistant", - instructions: "You are a helpful assistant."); +AIAgent agent = CreateAgent(); WebApplication app = builder.Build(); - -// Map the agent to an AG-UI endpoint (HTTP POST + SSE streaming). app.MapAGUIServer("/", agent); - await app.RunAsync(); ``` -> [!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. - -### Key Concepts - -- **`AddAGUIServer`**: Registers AG-UI server services with the dependency injection container -- **`MapAGUIServer`**: Extension method that maps an agent to an AG-UI endpoint with automatic request/response handling and SSE streaming -- **`AsAIAgent`**: Creates an Agent Framework agent directly from the Azure OpenAI chat client with a name and instructions -- **ASP.NET Core Integration**: Uses ASP.NET Core's native async support for streaming responses -- **Instructions**: The agent is created with default instructions, which can be overridden by client messages -- **Configuration**: `AzureOpenAIClient` with `DefaultAzureCredential` provides secure authentication - -### Configure and Run the Server - -Set the required environment variables: - -```bash -export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" -export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" -``` +`MapAGUIServer` accepts AG-UI `RunAgentInput` requests and streams the agent's response as AG-UI events over server-sent events (SSE). -Run the server: +Run the server on the URL used by the client example: -```bash +```dotnetcli dotnet run --urls http://localhost:8888 ``` -The server will start listening on `http://localhost:8888`. - -> [!NOTE] -> Keep this server running while you set up and run the client in Step 2. Both the server and client need to run simultaneously for the complete system to work. - -## Step 2: Creating an AG-UI Client - -The AG-UI client connects to the remote server and displays streaming responses. +> [!TIP] +> See the [.NET getting-started sample](https://github.com/microsoft/agent-framework/tree/main/dotnet/samples/02-agents/AGUI/Step01_GettingStarted) for a complete server and console client. -> [!IMPORTANT] -> Before running the client, ensure the AG-UI server from Step 1 is running at `http://localhost:8888`. +## Connect with a .NET client -### Install Required Packages +The AG-UI .NET SDK provides `AGUIChatClient`, which implements `IChatClient` and can be adapted to a MAF agent: -Install the AG-UI client library: - -```bash +```dotnetcli dotnet add package AGUI.Client --prerelease dotnet add package Microsoft.Agents.AI --prerelease ``` -> [!NOTE] -> The `Microsoft.Agents.AI` package provides the `AsAIAgent()` extension method. The AG-UI client -> type (`AGUIChatClient`) ships in the AG-UI C# SDK package `AGUI.Client`. - -### Client Code - -Create a file named `Program.cs`: - ```csharp -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI; +using AGUI.Abstractions; using AGUI.Client; +using Microsoft.Agents.AI; using Microsoft.Extensions.AI; -string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:8888"; +using HttpClient httpClient = new() { BaseAddress = new Uri("http://localhost:8888") }; +AGUIChatClient chatClient = new(new AGUIChatClientOptions(httpClient, "/")); +AIAgent remoteAgent = chatClient.AsAIAgent(); +AgentSession session = await remoteAgent.CreateSessionAsync(); -Console.WriteLine($"Connecting to AG-UI server at: {serverUrl}\n"); - -// Create the AG-UI client agent -using HttpClient httpClient = new() +List firstTurnUpdates = []; +await foreach (AgentResponseUpdate update in + remoteAgent.RunStreamingAsync("Hello", session)) { - Timeout = TimeSpan.FromSeconds(60) -}; - -AGUIChatClient chatClient = new(new AGUIChatClientOptions(httpClient, serverUrl)); - -AIAgent agent = chatClient.AsAIAgent( - name: "agui-client", - description: "AG-UI Client Agent"); - -AgentSession session = await agent.CreateSessionAsync(); -List messages = -[ - new(ChatRole.System, "You are a helpful assistant.") -]; + firstTurnUpdates.Add(update); -try -{ - while (true) + foreach (TextContent text in update.Contents.OfType()) { - // Get user input - Console.Write("\nUser (:q or quit to exit): "); - string? message = Console.ReadLine(); - - if (string.IsNullOrWhiteSpace(message)) - { - Console.WriteLine("Request cannot be empty."); - continue; - } - - if (message is ":q" or "quit") - { - break; - } - - messages.Add(new ChatMessage(ChatRole.User, message)); - - // Stream the response - bool isFirstUpdate = true; - - await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session)) - { - ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate(); - - // First update indicates run started - if (isFirstUpdate) - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine($"\n[Run Started - Run: {chatUpdate.ResponseId}]"); - Console.ResetColor(); - isFirstUpdate = false; - } - - // Display streaming text content - foreach (AIContent content in update.Contents) - { - if (content is TextContent textContent) - { - Console.ForegroundColor = ConsoleColor.Cyan; - Console.Write(textContent.Text); - Console.ResetColor(); - } - else if (content is ErrorContent errorContent) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"\n[Error: {errorContent.Message}]"); - Console.ResetColor(); - } - } - } - - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("\n[Run Finished]"); - Console.ResetColor(); + Console.Write(text.Text); } } -catch (Exception ex) -{ - Console.WriteLine($"\nAn error occurred: {ex.Message}"); -} -``` - -### Key Concepts - -- **Server-Sent Events (SSE)**: The protocol uses SSE for streaming responses -- **AGUIChatClient**: Client class that connects to AG-UI servers and implements `IChatClient` -- **AsAIAgent**: Extension method on `AGUIChatClient` to create an agent from the client -- **RunStreamingAsync**: Streams responses as `AgentResponseUpdate` objects -- **AsChatResponseUpdate**: Extension method to access chat-specific properties like `ResponseId` -- **Session Management**: The `AgentSession` maintains conversation context across requests -- **Content Types**: Responses include `TextContent` for messages and `ErrorContent` for errors - -### Configure and Run the Client - -Optionally set a custom server URL: - -```bash -export AGUI_SERVER_URL="http://localhost:8888" -``` - -Run the client in a separate terminal (ensure the server from Step 1 is running): - -```bash -dotnet run -``` - -## Step 3: Testing the Complete System - -With both the server and client running, you can now test the complete system. - -### Expected Output - -``` -$ dotnet run -Connecting to AG-UI server at: http://localhost:8888 - - -User (:q or quit to exit): What is 2 + 2? - -[Run Started - Run: run_OXhljyhFd6LNjtYlQGHaYknF] -2 + 2 equals 4. -[Run Finished] - -User (:q or quit to exit): Tell me a fun fact about space - -[Run Started - Run: run_9fTgYc51ITc5xsGetz1zTKnh] -A fun fact about space is that there are more stars in the observable -universe than grains of sand on all the beaches on Earth. -[Run Finished] - -User (:q or quit to exit): :q -``` - -## Testing with curl (Optional) - -You can exercise the server directly with curl before running the client. The AG-UI endpoint accepts a -`RunAgentInput` JSON body. The only required field is `messages`. If you omit `threadId`, the server -generates one for you: - -```bash -curl -N http://localhost:8888/ \ - -H "Content-Type: application/json" \ - -H "Accept: text/event-stream" \ - -d '{ - "messages": [ - {"role": "user", "content": "What is 2 + 2?"} - ] - }' -``` - -You should see Server-Sent Events streaming back: - ``` -data: {"type":"RUN_STARTED","threadId":"b60869bc...","runId":""} -data: {"type":"TEXT_MESSAGE_START","messageId":"...","role":"assistant","name":"AGUIAssistant"} +You can also connect with any client that implements the AG-UI protocol. -data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"...","delta":"Two"} +## Conversation continuity -data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"...","delta":" plus"} +AG-UI uses `threadId` and `parentRunId` to identify continuation requests. These identifiers are protocol data, not authorization credentials. -... +`AGUIChatClient` is stateless. To continue a server-owned conversation, get the identifiers from the first turn's `RunStartedEvent`, then include the same `threadId` and the previous `runId` as `parentRunId` on the next request: -data: {"type":"TEXT_MESSAGE_END","messageId":"..."} +```csharp +RunStartedEvent started = firstTurnUpdates + .Select(update => update.AsChatResponseUpdate().RawRepresentation) + .OfType() + .FirstOrDefault() + ?? throw new InvalidOperationException("The server didn't return a run-started event."); + +ChatMessage nextMessage = new(ChatRole.User, "What did I just say?"); +ChatClientAgentRunOptions continuationOptions = new() +{ + ChatOptions = new ChatOptions + { + RawRepresentationFactory = _ => new RunAgentInput + { + ThreadId = started.ThreadId, + ParentRunId = started.RunId, + Messages = new[] { nextMessage }.AsAGUIMessages().ToList(), + }, + }, +}; -data: {"type":"RUN_FINISHED","threadId":"b60869bc...","runId":""} +await foreach (AgentResponseUpdate update in + remoteAgent.RunStreamingAsync([nextMessage], session, continuationOptions)) +{ + // Process the continued response. +} ``` -Use the route you mapped with `MapAGUIServer` (here `/`) and the port your server listens on -(`dotnet run --urls http://localhost:8888`). - -## How It Works +Send only the new messages in a continuation request. `MapAGUIServer` uses `threadId` to select the hosted agent session and `parentRunId` to identify the run being continued. Without hosted session persistence, each request receives a new server session; the client can instead resend conversation history. -### Server-Side Flow +To retain server-owned `AgentSession` state across requests, configure [hosted session persistence and isolation](../../../../hosting/self-hosting/index.md#persist-hosted-sessions), then map the named hosted agent with `MapAGUIServer`. For the AG-UI-specific trust boundary, see [Production and security considerations](./security-considerations.md). -1. Client sends HTTP POST request with messages -2. ASP.NET Core endpoint receives the request via `MapAGUIServer` -3. Agent processes the messages using Agent Framework -4. Responses are converted to AG-UI events -5. Events are streamed back as Server-Sent Events (SSE) -6. Connection closes when the run completes +## Next steps -### Client-Side Flow +> [!div class="nextstepaction"] +> [Use backend tools with AG-UI](./backend-tool-rendering.md) -1. `AGUIChatClient` sends HTTP POST request to server endpoint -2. Server responds with SSE stream -3. Client parses incoming events into `AgentResponseUpdate` objects -4. Each update is displayed based on its content type -5. `ConversationId` is captured for conversation continuity -6. Stream completes when run finishes +## Related resources -### Protocol Details - -The AG-UI protocol uses: - -- HTTP POST for sending requests -- Server-Sent Events (SSE) for streaming responses -- JSON for event serialization -- Thread IDs (as `ConversationId`) for maintaining conversation context -- Run IDs (as `ResponseId`) for tracking individual executions - -## Next Steps - -Now that you understand the basics of AG-UI, you can: - -- **[Add Backend Tools](backend-tool-rendering.md)**: Create custom function tools for your domain - - - -## Additional Resources - -- [AG-UI Overview](index.md) -- [Agent Framework Documentation](../../../../overview/index.md) -- [AG-UI Protocol Specification](https://docs.ag-ui.com/) +- [AG-UI overview](./index.md) +- [MAF hosting](../../../../hosting/index.md) +- [AG-UI protocol documentation](https://docs.ag-ui.com/) ::: zone-end diff --git a/agent-framework/integrations/by-component/ui/ag-ui/human-in-the-loop.md b/agent-framework/integrations/by-component/ui/ag-ui/human-in-the-loop.md index 75476fd5..4f2ab41c 100644 --- a/agent-framework/integrations/by-component/ui/ag-ui/human-in-the-loop.md +++ b/agent-framework/integrations/by-component/ui/ag-ui/human-in-the-loop.md @@ -5,170 +5,82 @@ zone_pivot_groups: programming-languages author: moonbox3 ms.topic: tutorial ms.author: evmattso -ms.date: 07/10/2026 +ms.date: 08/11/2026 ms.service: agent-framework --- -# Human-in-the-Loop with AG-UI - -::: zone pivot="programming-language-csharp" - -This tutorial demonstrates how to implement human-in-the-loop approval workflows with AG-UI in .NET. The .NET implementation uses Microsoft.Extensions.AI's `ApprovalRequiredAIFunction` and translates approval requests into AG-UI "client tool calls" that the client handles and responds to. + -The C# AG-UI approval pattern works as follows: +# Human-in-the-Loop with AG-UI -1. **Server**: Wraps a function with `ApprovalRequiredAIFunction` to mark it as requiring approval, and maps the agent with `MapAGUIServer`. -2. **Interrupt**: When the model calls the tool, the run ends with an interrupt instead of executing it. The AG-UI client surfaces this as a `ToolApprovalRequestContent`. -3. **Client**: Presents the request to the user, then creates a decision with `CreateResponse(approved)` and sends it back. -4. **Resume**: `AGUIChatClient` transports the decision over the AG-UI resume mechanism. The agent continues and runs (or skips) the tool. +::: zone pivot="programming-language-csharp" -## Prerequisites +MAF tool approval remains responsible for deciding whether a tool requires approval. AG-UI transports the approval request to the client and the client's decision back to the server. -- Azure OpenAI resource with a deployed model -- Environment variables: - - `AZURE_OPENAI_ENDPOINT` - - `AZURE_OPENAI_DEPLOYMENT_NAME` -- Understanding of [Backend Tool Rendering](backend-tool-rendering.md) +For approval policies, conditional rules, and general safety guidance, see [Use function tools with human-in-the-loop approvals](../../../../agents/tools/tool-approval.md). -## Server Implementation +## Require approval -To require human approval before a tool runs, wrap the tool's `AIFunction` in `ApprovalRequiredAIFunction` and map the agent with `MapAGUIServer`. The AG-UI hosting layer raises an approval interrupt automatically when the model calls the tool. +Wrap the MAF function with `ApprovalRequiredAIFunction` and expose the agent normally: ```csharp -using System.ComponentModel; -using Azure.AI.OpenAI; -using Azure.Identity; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; using Microsoft.Extensions.AI; -using OpenAI.Chat; - -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -builder.Services.AddAGUIServer(); -WebApplication app = builder.Build(); +AIFunction deleteFile = AIFunctionFactory.Create( + (string path) => $"Deleted {path}", + name: "delete_file", + description: "Delete a file."); -string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] - ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] - ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); - -// A tool that must be approved before it runs. -[Description("Send an email to a recipient.")] -static string SendEmail( - [Description("The email address to send to.")] string to, - [Description("The subject line.")] string subject, - [Description("The email body.")] string body) - => $"Email sent to {to} with subject '{subject}'."; - -AITool sendEmail = new ApprovalRequiredAIFunction(AIFunctionFactory.Create(SendEmail, name: "send_email")); - -AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential()) - .GetChatClient(deploymentName) - .AsAIAgent( - name: "AGUIAssistant", - instructions: "You are a helpful assistant. Use the send_email tool when asked to send email.", - tools: [sendEmail]); +AITool approvalRequiredTool = new ApprovalRequiredAIFunction(deleteFile); +AIAgent agent = chatClient.AsAIAgent(tools: [approvalRequiredTool]); app.MapAGUIServer("/", agent); - -await app.RunAsync(); ``` -> [!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. +When the model calls the tool, the AG-UI adapter finishes the run with a tool-call interrupt instead of executing the function. -When the model calls `SendEmail`, the run ends with an **interrupt** outcome instead of executing the -tool. The AG-UI client surfaces this as a `ToolApprovalRequestContent` that carries the tool call and a -response schema of `{ "approved": boolean }`. The tool runs only after the client sends an approval. +## Resolve the interrupt from a .NET client -## Client Implementation - -A client handles the interrupt by reading the `ToolApprovalRequestContent`, creating a decision with -`CreateResponse(approved)`, and sending it back on the next turn. You don't hand-encode the AG-UI -resume message. `AGUIChatClient` converts the decision into the AG-UI resume for you, and reusing the -same `AgentSession` resumes the run. +`AGUIChatClient` surfaces the interrupt as `ToolApprovalRequestContent`. Create and send a response using the normal MAF approval types: ```csharp -using AGUI.Client; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; +ToolApprovalRequestContent? request = null; -string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:8888"; -using HttpClient httpClient = new() { BaseAddress = new Uri(serverUrl) }; - -AGUIChatClient chatClient = new(new AGUIChatClientOptions(httpClient, "/")); -AIAgent agent = chatClient.AsAIAgent(); -AgentSession session = await agent.CreateSessionAsync(); - -List messages = [new(ChatRole.User, "Email alice@example.com to say the report is ready.")]; - -// First turn: run until the agent requests approval. -ToolApprovalRequestContent? approvalRequest = null; -await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session)) +await foreach (AgentResponseUpdate update in + remoteAgent.RunStreamingAsync(messages, session)) { - foreach (AIContent content in update.Contents) - { - if (content is ToolApprovalRequestContent request) - { - approvalRequest = request; - var call = request.ToolCall as FunctionCallContent; - Console.WriteLine($"Approval requested for '{call?.Name}'."); - } - else if (content is TextContent text) - { - Console.Write(text.Text); - } - } + request ??= update.Contents + .OfType() + .FirstOrDefault(); } -// Second turn: send the decision. The agent resumes and runs (or skips) the tool. -if (approvalRequest is not null) +if (request is not null) { - ToolApprovalResponseContent decision = approvalRequest.CreateResponse(approved: true); - List resume = [new(ChatRole.User, [decision])]; + ToolApprovalResponseContent response = request.CreateResponse(approved: true); + ChatMessage resume = new(ChatRole.User, [response]); - // Reusing the same session resumes the run. No thread/run id plumbing is needed. - await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(resume, session)) + await foreach (AgentResponseUpdate update in + remoteAgent.RunStreamingAsync([resume], session)) { - foreach (AIContent content in update.Contents) - { - if (content is TextContent text) - { - Console.Write(text.Text); - } - } + // Process the resumed response. } } ``` -To reject instead, call `approvalRequest.CreateResponse(approved: false)`; the agent continues without running the tool. - -## Approval modes - -Marking a tool for approval, and deciding *when* a call needs it, is a general Agent Framework -capability, not something AG-UI defines. It works the same for an agent exposed over AG-UI: - -- **Always require / never require** approval by wrapping (or not wrapping) the function in - `ApprovalRequiredAIFunction`. -- **Selective approval**: wrap only the sensitive tools so the rest run unattended. -- **Conditional approval**: auto-approve some calls to an approval-required tool based on their - **arguments** using `AIAgentBuilder.UseToolApproval` with `AutoApprovalRules`. - -For the APIs, examples, and guidance, see [Using function tools with human-in-the-loop approvals](../../../../agents/tools/tool-approval.md). - -What AG-UI adds is the transport. A call that needs a human ends the run with a `RUN_FINISHED` -**interrupt** that carries the tool call and a `{ "approved": boolean }` response schema, which the -AG-UI client approves and resumes. The [Server](#server-implementation) and [Client](#client-implementation) -implementations above show this end to end. Calls that run directly, or that a conditional rule -auto-approves, stream their `TOOL_CALL_RESULT` normally and never interrupt. +Reuse the same `AgentSession` when sending the response so the client can continue the interrupted run. Use `approved: false` to reject the call. The adapter converts the MAF response to the canonical AG-UI resume payload. ## Next steps > [!div class="nextstepaction"] -> [MCP Apps Compatibility](./mcp-apps.md) +> [Manage shared state with AG-UI](./state-management.md) ::: zone-end diff --git a/agent-framework/integrations/by-component/ui/ag-ui/index.md b/agent-framework/integrations/by-component/ui/ag-ui/index.md index bd4dfef3..323f38cd 100644 --- a/agent-framework/integrations/by-component/ui/ag-ui/index.md +++ b/agent-framework/integrations/by-component/ui/ag-ui/index.md @@ -5,10 +5,20 @@ zone_pivot_groups: programming-languages author: moonbox3 ms.topic: overview ms.author: evmattso -ms.date: 07/10/2026 +ms.date: 08/11/2026 ms.service: agent-framework --- + + # AG-UI Integration with Agent Framework [AG-UI](https://docs.ag-ui.com/introduction) is a protocol that enables you to build web-based AI agent applications with advanced features like real-time streaming, state management, and interactive UI components. The Agent Framework AG-UI integration provides seamless connectivity between your agents and web clients. @@ -34,9 +44,9 @@ Consider using AG-UI when you need to: - Synchronize state between client and server for interactive experiences - Render custom UI components based on agent tool calls -## Supported Features +## AG-UI scenarios -The Agent Framework AG-UI integration supports all 7 AG-UI protocol features: +AG-UI defines seven showcase scenarios. MAF support varies by SDK; use the language-specific section on this page for the current support level and implementation guidance. 1. **Agentic Chat**: Basic streaming chat with automatic tool calling 2. **Backend Tool Rendering**: Tools executed on backend with results streamed to client @@ -57,96 +67,47 @@ To learn more about getting started with Microsoft Agent Framework and CopilotKi ::: zone pivot="programming-language-csharp" -## AG-UI vs. Direct Agent Usage +## .NET integration -While you can run agents directly in your application using Agent Framework's `Run` and `RunStreamingAsync` methods, AG-UI provides additional capabilities: +The .NET integration exposes a MAF `AIAgent` as an AG-UI HTTP endpoint. The hosting adapter converts the agent's response stream into AG-UI events; core agent behavior such as tool execution and approval remains part of MAF. -| Feature | Direct Agent Usage | AG-UI Integration | -|---------|-------------------|-------------------| -| Deployment | Embedded in application | Remote service via HTTP | -| Client Access | Single application | Multiple clients (web, mobile) | -| Streaming | In-process async iteration | Server-Sent Events (SSE) | -| State Management | Application-managed | Protocol-level state snapshots | -| Session Context | Application-managed | Protocol-managed session IDs | -| Approval Workflows | Custom implementation | Built-in middleware pattern | +Use the .NET integration to: -## Architecture Overview +- Stream agent text over Server-Sent Events (SSE). +- Surface [backend](./backend-tool-rendering.md) and [frontend](./frontend-tools.md) tool calls as AG-UI events. +- Send [MAF tool approval](./human-in-the-loop.md) requests to the client and return the decision. +- Exchange [client state, state snapshots and deltas, and forwarded properties](./state-management.md). +- [Resume persisted hosted sessions](./getting-started.md#conversation-continuity) using the AG-UI `threadId`. +- Expose [workflows converted to agents](./workflows.md) through the same endpoint. -The AG-UI integration uses ASP.NET Core and follows a clean middleware-based architecture: +AG-UI clients decide how to render text, tool, approval, and state events. -``` -┌─────────────────┐ -│ Web Client │ -│ (Browser/App) │ -└────────┬────────┘ - │ HTTP POST + SSE - ▼ -┌────────────────────────────┐ -│ ASP.NET Core │ -│ MapAGUIServer("/", agent) │ -└────────┬───────────────────┘ - │ - ▼ -┌────────────────────────────┐ -│ AIAgent │ -│ (with Middleware) │ -└────────┬───────────────────┘ - │ - ▼ -┌────────────────────────────┐ -│ IChatClient │ -│ (Azure OpenAI, etc.) │ -└────────────────────────────┘ -``` +## Architecture -### Key Components - -- **ASP.NET Core Endpoint**: `MapAGUIServer` extension method handles HTTP requests and SSE streaming -- **AIAgent**: Agent Framework agent created from `IChatClient` or custom implementation -- **Middleware Pipeline**: Optional middleware for approvals, state management, and custom logic -- **Protocol Adapter**: Converts between Agent Framework types and AG-UI protocol events -- **Chat Client**: Microsoft.Extensions.AI chat client (Azure OpenAI, OpenAI, Ollama, etc.) +The C# hosting package adds an ASP.NET Core endpoint around an ordinary MAF agent: -## How Agent Framework Translates to AG-UI - -Understanding how Agent Framework concepts map to AG-UI helps you build effective integrations: +```text +AG-UI client -- HTTP POST / SSE --> MapAGUIServer --> AIAgent +``` -| Agent Framework Concept | AG-UI Equivalent | Description | -|------------------------|------------------|-------------| -| `AIAgent` | Agent Endpoint | Each agent becomes an HTTP endpoint | -| `agent.Run()` | HTTP POST Request | Client sends messages via HTTP | -| `agent.RunStreamingAsync()` | Server-Sent Events | Streaming responses via SSE | -| `AgentResponseUpdate` | AG-UI Events | Converted to protocol events automatically | -| `AIFunctionFactory.Create()` | Backend Tools | Executed on server, results streamed | -| `ApprovalRequiredAIFunction` | Human-in-the-Loop | Middleware converts to approval protocol | -| `AgentSession` | Session Management | `ConversationId` maintains context | -| `ChatResponseFormat.ForJsonSchema()` | State Snapshots | Structured output becomes state events | +`MapAGUIServer` adapts the AG-UI request to MAF messages and run options. It then converts the agent's streaming response to AG-UI events using the AG-UI .NET SDK. ## Installation -The AG-UI integration is included in the ASP.NET Core hosting package: - -```bash -dotnet add package Microsoft.Agents.AI.Hosting.AGUI.AspNetCore +```dotnetcli +dotnet add package Microsoft.Agents.AI.Hosting.AGUI.AspNetCore --prerelease ``` -This package includes all dependencies needed for AG-UI integration including `Microsoft.Extensions.AI`. +## Next steps -## Next Steps - -To get started with AG-UI integration: - -1. **[Getting Started](getting-started.md)**: Build your first AG-UI server and client -2. **[Backend Tool Rendering](backend-tool-rendering.md)**: Add function tools to your agents -3. **[Human-in-the-Loop](human-in-the-loop.md)**: Implement approval workflows -4. **[State Management](state-management.md)**: Synchronize state between client and server +> [!div class="nextstepaction"] +> [Get started with AG-UI](./getting-started.md) -## Additional Resources +## Related resources -- [Agent Framework Documentation](../../../../overview/index.md) -- [AG-UI Protocol Documentation](https://docs.ag-ui.com/introduction) -- [Microsoft.Extensions.AI Documentation](/dotnet/api/microsoft.extensions.ai) -- [Agent Framework GitHub Repository](https://github.com/microsoft/agent-framework) +- [Agent Framework overview](../../../../overview/index.md) +- [AG-UI protocol documentation](https://docs.ag-ui.com/introduction) +- [Microsoft Agent Framework repository](https://github.com/microsoft/agent-framework) ::: zone-end diff --git a/agent-framework/integrations/by-component/ui/ag-ui/mcp-apps.md b/agent-framework/integrations/by-component/ui/ag-ui/mcp-apps.md index 2f716912..685adae2 100644 --- a/agent-framework/integrations/by-component/ui/ag-ui/mcp-apps.md +++ b/agent-framework/integrations/by-component/ui/ag-ui/mcp-apps.md @@ -5,16 +5,26 @@ zone_pivot_groups: programming-languages author: moonbox3 ms.topic: article ms.author: evmattso -ms.date: 04/09/2026 +ms.date: 08/11/2026 ms.service: agent-framework --- + + # MCP Apps Compatibility with AG-UI ::: zone pivot="programming-language-csharp" -> [!NOTE] -> MCP Apps compatibility documentation for the .NET AG-UI integration is coming soon. +MAF doesn't provide MCP Apps-specific configuration or runtime behavior. MCP Apps support is implemented by middleware outside the MAF AG-UI endpoint, which continues to receive standard AG-UI requests. + +For middleware setup and compatibility requirements, use the documentation for the selected AG-UI client or middleware. ::: zone-end @@ -115,7 +125,6 @@ If your application doesn't need the MCP Apps middleware layer, your Agent Frame ::: zone pivot="programming-language-go" -> [!NOTE] -> Go support for this feature is coming soon. See the [Agent Framework Go repository](https://github.com/microsoft/agent-framework-go) for the latest status. +MAF Go doesn't provide MCP Apps-specific configuration or runtime behavior. MCP Apps support is implemented by middleware outside the MAF AG-UI endpoint. ::: zone-end \ No newline at end of file diff --git a/agent-framework/integrations/by-component/ui/ag-ui/security-considerations.md b/agent-framework/integrations/by-component/ui/ag-ui/security-considerations.md index 9303e6ed..77b7fc03 100644 --- a/agent-framework/integrations/by-component/ui/ag-ui/security-considerations.md +++ b/agent-framework/integrations/by-component/ui/ag-ui/security-considerations.md @@ -4,7 +4,7 @@ description: Essential security guidelines for building secure AG-UI application author: moonbox3 ms.topic: reference ms.author: evmattso -ms.date: 07/10/2026 +ms.date: 08/11/2026 ms.service: agent-framework --- @@ -148,7 +148,11 @@ Forwarded properties contain arbitrary JSON that passes through the system. Trea ## Authentication and Authorization -AG-UI does not include built-in authorization mechanism. It is up to your application to prevent unauthorized use of the exposed AG-UI endpoint. +AG-UI does not include a built-in authorization mechanism. Authenticate and authorize the exposed endpoint with your application framework. + +Treat a client-supplied `threadId` as an untrusted continuation identifier, not an authorization credential. When session persistence is enabled, authorize the caller before resuming the selected session. See [Conversation continuity](./getting-started.md#conversation-continuity) for AG-UI behavior and [Self-host Agent Framework applications](../../../../hosting/self-hosting/index.md#isolate-sessions-in-multi-user-hosts) for shared persistence and isolation configuration. + +For ASP.NET Core authentication schemes and policies, see [ASP.NET Core authentication](/aspnet/core/security/authentication/) and [ASP.NET Core authorization](/aspnet/core/security/authorization/introduction). ### Approval State Storage @@ -159,14 +163,13 @@ Approval State is not an authentication, tenant authorization, or distributed du authorize every endpoint request, and choose deployment and storage architecture that matches your availability and worker topology requirements. -### Session ID Management +### Thread ID management -Session IDs identify conversation sessions. Implement proper validation to prevent unauthorized access. +AG-UI thread IDs identify conversation continuations. Clients can provide a thread ID, and an endpoint can generate one when it is omitted. In either case: -**Security considerations:** -- Generate Session IDs server-side using cryptographically secure random values -- Never allow clients to directly access arbitrary Session IDs -- Verify session ownership before processing requests +- Don't treat a thread ID as proof of identity or ownership. +- Verify that the authenticated caller can access persisted data associated with the thread. +- Scope storage by an authenticated user, tenant, workspace, or another application-owned boundary. ### Sensitive Data Filtering diff --git a/agent-framework/integrations/by-component/ui/ag-ui/state-management.md b/agent-framework/integrations/by-component/ui/ag-ui/state-management.md index a6e3ff3d..def187b5 100644 --- a/agent-framework/integrations/by-component/ui/ag-ui/state-management.md +++ b/agent-framework/integrations/by-component/ui/ag-ui/state-management.md @@ -5,13 +5,23 @@ zone_pivot_groups: programming-languages author: moonbox3 ms.topic: tutorial ms.author: evmattso -ms.date: 07/10/2026 +ms.date: 08/11/2026 ms.service: agent-framework --- + + # State Management with AG-UI -This tutorial shows you how to implement state management with AG-UI, enabling bidirectional synchronization of state between the client and server. This is essential for building interactive applications like generative UI, real-time dashboards, or collaborative experiences. +AG-UI defines state events and request fields for sharing application state between a client and an agent endpoint. The implementation and supported state patterns vary by MAF SDK. ## Prerequisites @@ -23,12 +33,12 @@ Before you begin, ensure you understand: ## What is State Management? -State management in AG-UI enables: +AG-UI state can provide: - **Shared State**: Both client and server maintain a synchronized view of application state -- **Bidirectional Sync**: State can be updated from either client or server +- **Client and server updates**: Applications can send state in requests and emit state events - **Real-time Updates**: Changes are streamed immediately using state events -- **Predictive Updates**: State updates stream as the LLM generates tool arguments (optimistic UI) +- **Predictive Updates**: An SDK can map tool-call progress to optimistic UI state - **Structured Data**: State follows a JSON schema for validation ### Use Cases @@ -43,163 +53,16 @@ State management is valuable for: ::: zone pivot="programming-language-csharp" -## Creating State-Aware Agents in C# - -State management in the .NET AG-UI integration is **declarative**: your agent exposes ordinary tools that return your state objects, and you tell the hosting layer which tool results become AG-UI state events by configuring an `AGUIStreamOptions`. You don't write a custom agent or emit protocol content by hand. - -### Define Your State Model - -First, define classes for your state structure: - -```csharp -using System.Text.Json.Serialization; - -namespace RecipeAssistant; - -// State response wrapper returned by the tool. Its shape is what the client renders as state. -internal sealed class RecipeResponse -{ - [JsonPropertyName("recipe")] - public Recipe Recipe { get; set; } = new(); -} - -// Recipe state model. -internal sealed class Recipe -{ - [JsonPropertyName("title")] - public string Title { get; set; } = string.Empty; - - [JsonPropertyName("skill_level")] - public string SkillLevel { get; set; } = string.Empty; - - [JsonPropertyName("cooking_time")] - public string CookingTime { get; set; } = string.Empty; - - [JsonPropertyName("special_preferences")] - public List SpecialPreferences { get; set; } = []; - - [JsonPropertyName("ingredients")] - public List Ingredients { get; set; } = []; - - [JsonPropertyName("instructions")] - public List Instructions { get; set; } = []; -} - -// A single ingredient. -internal sealed class Ingredient -{ - [JsonPropertyName("icon")] - public string Icon { get; set; } = string.Empty; - - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - [JsonPropertyName("amount")] - public string Amount { get; set; } = string.Empty; -} - -// JSON serialization context for the tool payloads. -[JsonSerializable(typeof(RecipeResponse))] -[JsonSerializable(typeof(Recipe))] -[JsonSerializable(typeof(Ingredient))] -internal sealed partial class RecipeSerializerContext : JsonSerializerContext; -``` - -### Emit a State Snapshot from a Tool - -Expose a tool that returns the complete state. The agent calls it whenever the recipe should change. The hosting layer turns the tool result into a `STATE_SNAPSHOT` event when you map it: - -```csharp -using System.ComponentModel; -using Microsoft.Extensions.AI; - -[Description("Generate or update the shared recipe and display it to the user.")] -static RecipeResponse GenerateRecipe( - [Description("The complete recipe to display.")] Recipe recipe) => new() { Recipe = recipe }; - -AITool generateRecipe = AIFunctionFactory.Create( - GenerateRecipe, - name: "generate_recipe", - description: "Generate or update the shared recipe and display it to the user.", - RecipeSerializerContext.Default.Options); -``` - -### Create the Agent - -Build the agent directly from your chat client with . Put the system prompt and tools on : - -```csharp -using Microsoft.Agents.AI; -using Azure.AI.OpenAI; -using Azure.Identity; -using Microsoft.Extensions.AI; -using OpenAI.Chat; - -const string SharedStateSystemPrompt = - """ - You are a helpful recipe assistant that maintains a shared recipe state with the user. - - IMPORTANT: - - When the user asks you to create, change, or improve a recipe, call the `generate_recipe` - tool with a COMPLETE recipe: a title, skill_level, cooking_time, special_preferences, the - full list of ingredients (each with an icon, name and amount) and the step-by-step - instructions. - - Always include every ingredient the recipe needs, keeping any the user already added. - - When the user only asks a question about the recipe, answer in plain text and do NOT call the tool. - """; - -string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") - ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") - ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); - -AIAgent recipeAgent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential()) - .GetChatClient(deploymentName) - .AsAIAgent(new ChatClientAgentOptions - { - Name = "RecipeAgent", - Description = "An agent that maintains a shared recipe state with the user.", - ChatOptions = new ChatOptions - { - Instructions = SharedStateSystemPrompt, - Tools = [generateRecipe], - }, - }); -``` - -> [!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. - -### Map the Tool Result to a State Event - -Create an `AGUIStreamOptions`, register the tool name as a state snapshot, and attach it to the endpoint metadata. `MapAGUIServer` reads the stream options from the endpoint (or from `IOptions` in DI) and emits the state events for you: - -```csharp -using AGUI.Server; -using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; - -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -builder.Services.ConfigureHttpJsonOptions(options => - options.SerializerOptions.TypeInfoResolverChain.Add(RecipeSerializerContext.Default)); -builder.Services.AddAGUIServer(); - -// A `generate_recipe` result becomes a STATE_SNAPSHOT event. -AGUIStreamOptions streamOptions = new AGUIStreamOptions() - .MapResultAsStateSnapshot("generate_recipe"); +AG-UI state is client-visible JSON associated with a run. In .NET, the integration provides two explicit mechanisms: -WebApplication app = builder.Build(); +- Read state supplied by the client from the originating `RunAgentInput`. +- Map selected tool calls or results to AG-UI state events with `AGUIStreamOptions`. -// Attach the stream options to the endpoint. MapAGUIServer emits the state events for you. -app.MapAGUIServer("/", recipeAgent).WithMetadata(streamOptions); +State mapping is opt-in. Arbitrary tool results don't automatically become shared state. -await app.RunAsync(); -``` - -That's the whole server. There is no custom `DelegatingAIAgent` and no protocol content to build: the tool returns your state object, `MapResultAsStateSnapshot` turns each result into a `STATE_SNAPSHOT`, and the framework streams it to the client. - -### Reading Client State +## Read client state -The recipe lives on the client. When the client sends a turn, it includes its current state on the AG-UI `RunAgentInput`. Recover it from the request's with `TryGetRunAgentInput` and read `RunAgentInput.State` (a `JsonElement`): +`MapAGUIServer` stores the originating `RunAgentInput` on `ChatOptions`. A delegating agent or chat-client middleware can recover it with `TryGetRunAgentInput`: ```csharp using System.Text.Json; @@ -207,12 +70,12 @@ using AGUI.Abstractions; using AGUI.Server; using Microsoft.Extensions.AI; -static bool TryGetClientState(ChatOptions chatOptions, out JsonElement state) +static bool TryGetClientState(ChatOptions options, out JsonElement state) { - if (chatOptions.TryGetRunAgentInput(out RunAgentInput? input) && - input.State is { ValueKind: not JsonValueKind.Undefined } clientState) + if (options.TryGetRunAgentInput(out RunAgentInput? input) && + input.State is { ValueKind: not JsonValueKind.Undefined } value) { - state = clientState; + state = value; return true; } @@ -221,332 +84,87 @@ static bool TryGetClientState(ChatOptions chatOptions, out JsonElement state) } ``` -`TryGetRunAgentInput` reads the input that the hosting layer stashed on `ChatOptions.AdditionalProperties`. You never touch that dictionary directly. Give the model the current recipe by prepending it as a system message before the agent runs (for example, from a lightweight that only injects context and delegates the run), so edits build on the existing state instead of starting from scratch. - -### Key Concepts - -- **Tools return state**: A tool returns your state object; you never construct AG-UI events yourself. -- **Declarative mapping**: `AGUIStreamOptions.MapResultAsStateSnapshot(toolName)` / `MapResultAsStateDelta(toolName)` map a tool result to a `STATE_SNAPSHOT` / `STATE_DELTA` event. -- **Endpoint wiring**: Attach the stream options with `.WithMetadata(streamOptions)` on `MapAGUIServer`, or register `IOptions` in DI. -- **Reading state**: `ChatOptions.TryGetRunAgentInput(out var input)` recovers the `RunAgentInput`; `input.State` is the client's current state as a `JsonElement`. - -## State Deltas with Agentic Generative UI - -`MapResultAsStateSnapshot` replaces the entire state on each turn. For incremental changes, map a tool result to a `STATE_DELTA` with `MapResultAsStateDelta`, returning a [JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) document. - -A common scenario is *agentic generative UI*: the agent builds a plan, then updates the status of individual steps as it works. `create_plan` sends the full plan as a snapshot; `update_plan_step` sends only the changed fields as a delta. - -Define the plan model and status enum: - -```csharp -using System.Text.Json.Serialization; - -internal sealed class Plan -{ - [JsonPropertyName("steps")] - public List Steps { get; set; } = []; -} - -internal sealed class Step -{ - [JsonPropertyName("description")] - public required string Description { get; set; } - - [JsonPropertyName("status")] - public StepStatus Status { get; set; } = StepStatus.Pending; -} - -[JsonConverter(typeof(JsonStringEnumConverter))] -internal enum StepStatus -{ - Pending, - Completed -} - -internal sealed class JsonPatchOperation -{ - [JsonPropertyName("op")] - public required string Op { get; set; } - - [JsonPropertyName("path")] - public required string Path { get; set; } - - [JsonPropertyName("value")] - public object? Value { get; set; } -} -``` - -The `create_plan` tool returns the full plan; `update_plan_step` returns a list of JSON Patch operations: - -```csharp -using System.ComponentModel; - -[Description("Create a plan with multiple steps.")] -public static Plan CreatePlan( - [Description("List of step descriptions to create the plan.")] List steps) -{ - return new Plan - { - Steps = [.. steps.Select(s => new Step { Description = s, Status = StepStatus.Pending })] - }; -} - -[Description("Update a step in the plan with new description or status.")] -public static List UpdatePlanStep( - [Description("The index of the step to update.")] int index, - [Description("The new status for the step.")] StepStatus status) -{ - // Status must be lowercase to match AG-UI frontend expectations. - string statusValue = status == StepStatus.Pending ? "pending" : "completed"; +Client state is request input. Validate its shape and values before using it in prompts, routing, or privileged operations. - return - [ - new JsonPatchOperation { Op = "replace", Path = $"/steps/{index}/status", Value = statusValue } - ]; -} -``` +## Emit a state snapshot -Register both tools on the agent (with `AllowMultipleToolCalls = false` so the model updates one step at a time), and map each tool result to the matching state event: `create_plan` to a snapshot, `update_plan_step` to a delta. +Map a tool result to `STATE_SNAPSHOT` when the tool returns the complete state: ```csharp using AGUI.Server; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; - -AITool createPlan = AIFunctionFactory.Create( - CreatePlan, name: "create_plan", description: "Create a plan with multiple steps."); -AITool updatePlanStep = AIFunctionFactory.Create( - UpdatePlanStep, name: "update_plan_step", description: "Update a step in the plan with new description or status."); - -AIAgent planAgent = chatClient.AsAIAgent(new ChatClientAgentOptions -{ - Name = "AgenticUIAgent", - ChatOptions = new ChatOptions - { - Instructions = "Use `create_plan` to set the initial steps, then call `update_plan_step` until every step is completed. Do not describe the plan in text.", - Tools = [createPlan, updatePlanStep], - AllowMultipleToolCalls = false, - }, -}); -AGUIStreamOptions planStreamOptions = new AGUIStreamOptions() - .MapResultAsStateSnapshot("create_plan") // full plan -> STATE_SNAPSHOT - .MapResultAsStateDelta("update_plan_step"); // JSON Patch -> STATE_DELTA +AGUIStreamOptions streamOptions = new AGUIStreamOptions() + .MapResultAsStateSnapshot("generate_recipe"); -app.MapAGUIServer("/agentic_generative_ui", planAgent).WithMetadata(planStreamOptions); +app.MapAGUIServer("/", agent).WithMetadata(streamOptions); ``` -> [!NOTE] -> `STATE_SNAPSHOT` replaces the entire state; `STATE_DELTA` applies a JSON Patch to the existing state. Send a snapshot when you set up or reset state, and deltas for incremental changes. - -For a related pattern that streams a tool's arguments into state as the model generates them, continue with Predictive State Updates below. - -## Predictive State Updates - -Predictive state updates let the UI react to a tool call *while its arguments are still being generated*, instead of waiting for the tool to finish. As the model streams the arguments for a tool, the server converts those partial arguments into state snapshots and sends them to the client. The client renders each snapshot immediately, giving the user an optimistic, live preview. For example, a document editor shows the text appearing in real time, then asks the user to confirm the change once the model is done. - -> [!NOTE] -> This scenario maps the endpoint manually and uses the built-in `TypedResults.ServerSentEvents(...)`, which requires **.NET 10.0 or later**. - -### How It Works +`MapResultAsStateSnapshot` requires the `FunctionResultContent.Result` value to be a `JsonElement`. Serialize a POCO, dictionary, or collection to `JsonElement` in the tool before returning it. The result of `generate_recipe` then becomes the snapshot and replaces the client's current shared state. -Unlike the [shared-state](#emit-a-state-snapshot-from-a-tool) scenario, where a tool *runs* and its result becomes a snapshot, the predictive scenario intercepts the tool **call** before it executes and streams the argument into state: +For other result types, use `MapResult` with a custom mapper that constructs the `StateSnapshotEvent`. -1. The agent declares a `write_document_local` tool. The model calls it with the full document text as its `document` argument. -2. The tool is **not** executed server-side. Instead, an `AGUIStreamOptions` `MapCall` mapping intercepts the call. -3. The mapping emits a series of `STATE_SNAPSHOT` events, each carrying a progressively longer prefix of the document, so the client sees the text stream in. -4. It then completes the tool call with a `TOOL_CALL_RESULT` event and injects a client-side `confirm_changes` tool call so the client can prompt the user to approve. -5. The client renders each snapshot and shows the confirm/reject prompt. +## Emit state deltas -Because the mapping produces the tool's result itself, the document tool is declared but never invoked. The chat client is built **without** function invocation. - -### Define the State Model - -The state model describes the shape the client renders. Use `JsonPropertyName` so the property names match what the client expects: +Map a tool result to `STATE_DELTA` when it returns an [RFC 6902 JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902): ```csharp -using System.Text.Json; -using System.Text.Json.Serialization; - -internal sealed class DocumentState -{ - [JsonPropertyName("document")] - public string Document { get; set; } = string.Empty; -} +AGUIStreamOptions streamOptions = new AGUIStreamOptions() + .MapResultAsStateSnapshot("create_plan") + .MapResultAsStateDelta("update_plan_step"); -[JsonSerializable(typeof(DocumentState))] -[JsonSerializable(typeof(JsonElement))] -internal sealed partial class DocumentSerializerContext : JsonSerializerContext; +app.MapAGUIServer("/", agent).WithMetadata(streamOptions); ``` -### Declare the Document Tool - -The tool signature is what the model fills in. It is declared so the model calls it, but its result is produced by the stream mapping, not by executing the method body: - -```csharp -using System.ComponentModel; -using Microsoft.Extensions.AI; - -[Description("Write a document in markdown format.")] -static string WriteDocument( - [Description("The document content to write.")] string document) => "Document written successfully"; +Use a snapshot to initialize or replace state and deltas for incremental changes. -AITool writeDocument = AIFunctionFactory.Create( - WriteDocument, - name: "write_document_local", - description: "Write a document. Use markdown formatting to format the document."); -``` +`MapResultAsStateDelta` also requires a `JsonElement` result. The element must contain an [RFC 6902 JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) array. Use `MapResult` with a custom mapper if the tool returns another representation. -### Configure the Predictive Stream Mapping +## Map tool calls to state -Register a `MapCall` mapping for the tool. When the model calls `write_document_local`, the mapping reads the streamed `document` argument, emits progressive `StateSnapshotEvent` snapshots, completes the tool call, and injects a `confirm_changes` client-side tool call: +`AGUIStreamOptions.MapCall` maps a selected `FunctionCallContent` to additional AG-UI events emitted after the normal tool-call events. Use it when state derives from tool arguments rather than the tool result: ```csharp -using System.Text.Json; -using AGUI.Abstractions; -using AGUI.Server; -using Microsoft.Extensions.AI; - -static AGUIStreamOptions CreatePredictiveStreamOptions(JsonSerializerOptions jsonSerializerOptions) -{ - string? lastEmittedDocument = null; - - return new AGUIStreamOptions().MapCall("write_document_local", fcc => +AGUIStreamOptions streamOptions = new AGUIStreamOptions() + .MapCall("write_document", call => { - string? document = fcc.Arguments?.TryGetValue("document", out var value) == true - ? value?.ToString() - : null; - - if (document is null || document == lastEmittedDocument) + if (call.Arguments?.TryGetValue("document", out object? document) is not true) { return []; } - var events = new List(); - - // Only stream the newly added portion if the document grew. - int startIndex = lastEmittedDocument is not null && - document.StartsWith(lastEmittedDocument, StringComparison.Ordinal) - ? lastEmittedDocument.Length - : 0; - - const int chunkSize = 10; - for (int i = startIndex; i < document.Length; i += chunkSize) - { - int length = Math.Min(chunkSize, document.Length - i); - var snapshot = new DocumentState { Document = document[..(i + length)] }; - JsonElement snapshotJson = JsonSerializer.SerializeToElement(snapshot, jsonSerializerOptions); - - events.Add(new StateSnapshotEvent { Snapshot = snapshotJson }); - } - - // Complete the write_document_local call (its document is now reflected in state) so the - // only tool call the client sees pending is confirm_changes. - events.Add(new ToolCallResultEvent - { - MessageId = Guid.NewGuid().ToString("N"), - ToolCallId = fcc.CallId, - Content = "Document written.", - Role = "tool", - }); - - // Inject a client-side confirm_changes tool call so the approval modal renders. - string confirmCallId = Guid.NewGuid().ToString("N"); - string confirmMessageId = Guid.NewGuid().ToString("N"); - events.Add(new ToolCallStartEvent { ToolCallId = confirmCallId, ToolCallName = "confirm_changes", ParentMessageId = confirmMessageId }); - events.Add(new ToolCallArgsEvent { ToolCallId = confirmCallId, Delta = "{}" }); - events.Add(new ToolCallEndEvent { ToolCallId = confirmCallId }); - - lastEmittedDocument = document; - return events; + JsonElement snapshot = JsonSerializer.SerializeToElement(new { document }); + return [new StateSnapshotEvent { Snapshot = snapshot }]; }); -} + +app.MapAGUIServer("/", agent).WithMetadata(streamOptions); ``` -> [!NOTE] -> Each snapshot contains the full document up to that point, so the client always renders a consistent view even if it misses an intermediate update. +The application owns the mapping and the state shape. `MapCall` doesn't infer state from arbitrary tool arguments or suppress normal tool execution. Incremental updates require the underlying model client to expose streamed tool-call arguments and the application to configure the corresponding argument extraction. -### Map the Endpoint +## Receive state in a .NET client -Because the mapping produces the tool result itself, build the chat client **without** function invocation and stream through the AG-UI pipeline directly: adapt the incoming `RunAgentInput` with `ToChatRequestContext`, call `GetStreamingResponseAsync`, and convert the updates with `AsAGUIEventStreamAsync`. +The AG-UI .NET client surfaces state protocol events through `ChatResponseUpdate.RawRepresentation`: ```csharp -using AGUI.Abstractions; -using AGUI.Server; -using Azure.AI.OpenAI; -using Azure.Identity; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.Options; -using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions; - -const string PredictiveSystemPrompt = - """ - You are a document editor assistant. When asked to write or edit content: - - Use the `write_document_local` tool with the full document text in Markdown format. - - You MUST write the full document, even when changing only a few words. - - When making edits, keep them minimal. Do not change every word. - After writing the document, briefly summarize the changes you made in at most two sentences. - """; - -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -builder.Services.ConfigureHttpJsonOptions(options => - options.SerializerOptions.TypeInfoResolverChain.Add(DocumentSerializerContext.Default)); -builder.Services.AddAGUIServer(); - -string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] - ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] - ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); - -// No UseFunctionInvocation: the call is intercepted by the stream mapping, not executed. -IChatClient chatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential()) - .GetChatClient(deploymentName) - .AsIChatClient(); - -WebApplication app = builder.Build(); - -JsonSerializerOptions jsonSerializerOptions = app.Services - .GetRequiredService>() - .Value.SerializerOptions; - -app.MapPost("/", ( - [FromBody] RunAgentInput input, - HttpContext httpContext, - CancellationToken cancellationToken) => +await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session)) { - AGUIStreamOptions streamOptions = CreatePredictiveStreamOptions(jsonSerializerOptions); - - ChatRequestContext ctx = input.ToChatRequestContext(jsonSerializerOptions, streamOptions); - ctx.Messages.Insert(0, new ChatMessage(ChatRole.System, PredictiveSystemPrompt)); - (ctx.ChatOptions.Tools ??= []).Add(writeDocument); - - var updates = chatClient.GetStreamingResponseAsync(ctx.Messages, ctx.ChatOptions, cancellationToken); - IAsyncEnumerable events = updates.AsAGUIEventStreamAsync(ctx, cancellationToken); - - return TypedResults.ServerSentEvents(events); -}); - -await app.RunAsync(); + if (update.AsChatResponseUpdate().RawRepresentation is StateSnapshotEvent snapshot) + { + JsonElement state = snapshot.Snapshot; + } + else if (update.AsChatResponseUpdate().RawRepresentation is StateDeltaEvent delta) + { + JsonElement changes = delta.Delta; + } +} ``` -> [!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. - -> [!NOTE] -> `confirm_changes` is a *client-side* tool. The stream mapping requests it, and the client renders the approval prompt. See [Human-in-the-Loop](human-in-the-loop.md) for the client-side tool pattern. - -### Predictive Key Concepts - -- **`AGUIStreamOptions.MapCall`**: Intercepts a tool *call* (before execution) and returns the AG-UI events to emit for it. -- **`FunctionCallContent.Arguments`**: The streamed tool arguments. Read `Arguments["document"]` to get the text as the model produces it. -- **`StateSnapshotEvent`**: Each snapshot holds the full document prefix so far, producing the optimistic streaming effect. -- **`ToChatRequestContext` / `AsAGUIEventStreamAsync`**: The AG-UI streaming pipeline that adapts a `RunAgentInput` to a chat request and converts the response updates back into AG-UI events. -- **`confirm_changes`**: A client-side tool call injected after the document is written, so the user can approve the result. +The client is responsible for retaining and applying shared state, then sending the current state on later requests when the application requires it. -### Rendering on the Client +## Next steps -A UI toolkit such as [CopilotKit](https://copilotkit.ai/) subscribes to the state snapshots and re-renders the document on each one, then shows the confirm or reject prompt when the `confirm_changes` tool call arrives. You can see this scenario running in the [AG-UI Dojo](https://dojo.ag-ui.com/microsoft-agent-framework-dotnet). +> [!div class="nextstepaction"] +> [Review workflow support with AG-UI](./workflows.md) ::: zone-end diff --git a/agent-framework/integrations/by-component/ui/ag-ui/testing-with-dojo.md b/agent-framework/integrations/by-component/ui/ag-ui/testing-with-dojo.md index 20ed22f2..cea7db09 100644 --- a/agent-framework/integrations/by-component/ui/ag-ui/testing-with-dojo.md +++ b/agent-framework/integrations/by-component/ui/ag-ui/testing-with-dojo.md @@ -4,11 +4,20 @@ description: Learn how to test your Microsoft Agent Framework agents with AG-UI' zone_pivot_groups: programming-languages author: moonbox3 ms.topic: tutorial -ms.date: 07/01/2026 +ms.date: 08/11/2026 ms.author: evmattso ms.service: agent-framework --- + + # Testing with AG-UI Dojo The [AG-UI Dojo application](https://dojo.ag-ui.com/) provides an interactive environment to test and explore Microsoft Agent Framework agents that implement the AG-UI protocol. Dojo offers a visual interface to connect to your agents and interact with all 7 AG-UI features. @@ -262,6 +271,13 @@ if err := http.ListenAndServe(":8888", mux); err != nil { :::zone-end ::: zone pivot="programming-language-csharp" -Coming soon. +Dojo is an AG-UI interoperability tool and doesn't require MAF-specific .NET configuration. Expose the scenario through `MapAGUIServer`, then follow the Dojo documentation for connecting an AG-UI endpoint. + +For MAF implementation guidance, use the scenario articles in this section: + +- [Backend tools](./backend-tool-rendering.md) +- [Frontend tools](./frontend-tools.md) +- [Human approval](./human-in-the-loop.md) +- [State management](./state-management.md) ::: zone-end diff --git a/agent-framework/integrations/by-component/ui/ag-ui/workflows.md b/agent-framework/integrations/by-component/ui/ag-ui/workflows.md index 8e3995ec..bebe394e 100644 --- a/agent-framework/integrations/by-component/ui/ag-ui/workflows.md +++ b/agent-framework/integrations/by-component/ui/ag-ui/workflows.md @@ -1,73 +1,48 @@ --- title: Workflows with AG-UI -description: Learn how to expose Agent Framework workflows through AG-UI with step tracking, interrupt/resume, and custom events +description: Review language-specific support for exposing Agent Framework workflows through AG-UI zone_pivot_groups: programming-languages author: moonbox3 ms.topic: tutorial ms.author: evmattso -ms.date: 07/10/2026 +ms.date: 08/11/2026 ms.service: agent-framework --- + + # Workflows with AG-UI ::: zone pivot="programming-language-csharp" -[Workflows](../../../../concepts/workflows/index.md) orchestrate multiple agents in a defined execution graph. In .NET -you expose a workflow over AG-UI exactly the way you expose any agent: convert it to an `AIAgent` with -`AsAIAgent()` and map it with `MapAGUIServer`. There is no workflow-specific server API to learn. +MAF .NET can expose a workflow through AG-UI by converting the workflow to an `AIAgent` and mapping it like any other agent: ```csharp -using Azure.AI.OpenAI; -using Azure.Identity; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; -using Microsoft.Agents.AI.Workflows; -using OpenAI.Chat; - -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -builder.Services.AddAGUIServer(); - -string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] - ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] - ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); - -ChatClient chatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential()) - .GetChatClient(deploymentName); +AIAgent workflowAgent = AgentWorkflowBuilder + .BuildSequential(researcher, reporter) + .AsAIAgent(); -AIAgent researcher = chatClient.AsAIAgent( - name: "researcher", instructions: "Research the user's topic and write a short, factual brief."); -AIAgent reporter = chatClient.AsAIAgent( - name: "reporter", instructions: "Summarize the researcher's brief into a single clear paragraph."); - -// A workflow-as-agent is just an AIAgent. Map it like any other agent. -AIAgent workflowAgent = AgentWorkflowBuilder.BuildSequential(researcher, reporter).AsAIAgent(); - -WebApplication app = builder.Build(); app.MapAGUIServer("/", workflowAgent); - -await app.RunAsync(); ``` -> [!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. +The endpoint streams the constituent agents' standard text and tool-call output. `AuthorName` identifies the agent that produced each update. -The client side is unchanged. Connect with `AGUIChatClient` as in [Getting Started](getting-started.md). -Each agent's output streams as normal AG-UI text and tool-call events, and the `AuthorName` on each update -identifies which agent in the workflow produced it. +MAF .NET doesn't currently map workflow-specific lifecycle behavior to AG-UI. Clients don't receive workflow step events, activity snapshots, workflow interrupts, or workflow resume operations equivalent to the Python integration. Wrapping a workflow as an `AIAgent` doesn't add those mappings. -> [!NOTE] -> The .NET integration streams a workflow's **agent output** (text and tool calls) over AG-UI, but not the -> workflow-specific AG-UI events shown in the Python version of this article: step tracking -> (`STEP_STARTED` / `STEP_FINISHED`), activity snapshots, and workflow-level interrupts. Those events are -> still evolving for .NET (tracked by [agent-framework#2494](https://github.com/microsoft/agent-framework/issues/2494)). +For the current .NET tracking status, see [microsoft/agent-framework#2494](https://github.com/microsoft/agent-framework/issues/2494). For workflow construction and execution independent of AG-UI, see [MAF workflow concepts](../../../../concepts/workflows/index.md). ## Next steps -- [State Management](state-management.md) -- [Human-in-the-Loop](human-in-the-loop.md) -- [Workflows](../../../../concepts/workflows/index.md) +> [!div class="nextstepaction"] +> [Review production and security considerations](./security-considerations.md) ::: zone-end From bfc0958103109ef4354511f55b68653417123266 Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:26:24 +0900 Subject: [PATCH 4/4] Python: Add draft for agent-hooks docs (#1084) * Add draft for agent-hooks docs * Address Agent Hooks review feedback Clarify language parity, enforcement modes, timeout behavior, persistence boundaries, and tool approval interactions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5262f258-0cff-4de3-8cde-508b0e621f54 * Address Agent Hooks technical review Document the complete post-model transform surface, disambiguate AgentContext types, and locate the per-service-call persistence option. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5262f258-0cff-4de3-8cde-508b0e621f54 --------- Copilot-Session: 5262f258-0cff-4de3-8cde-508b0e621f54 --- agent-framework/TOC.yml | 2 + agent-framework/agents/agent-hooks.md | 336 ++++++++++++++++++ agent-framework/agents/index.md | 3 +- .../concepts/agents/agent-pipeline.md | 4 +- .../concepts/agents/middleware/index.md | 5 +- 5 files changed, 347 insertions(+), 3 deletions(-) create mode 100644 agent-framework/agents/agent-hooks.md diff --git a/agent-framework/TOC.yml b/agent-framework/TOC.yml index b6057ea4..ae6adfd8 100644 --- a/agent-framework/TOC.yml +++ b/agent-framework/TOC.yml @@ -121,6 +121,8 @@ items: href: agents/observability.md - name: Evaluation href: agents/evaluation.md + - name: Agent Hooks + href: agents/agent-hooks.md - name: Agent Skills href: agents/skills.md - name: CodeAct diff --git a/agent-framework/agents/agent-hooks.md b/agent-framework/agents/agent-hooks.md new file mode 100644 index 00000000..ad0e65d1 --- /dev/null +++ b/agent-framework/agents/agent-hooks.md @@ -0,0 +1,336 @@ +--- +title: Agent hooks +description: Add fail-closed governance and runtime controls to agents with the Agent Hooks interception contract. +zone_pivot_groups: programming-languages +author: moonbox3 +ms.topic: article +ms.author: evmattso +ms.date: 08/07/2026 +ms.service: agent-framework +--- + + + +# Agent hooks + +Agent Hooks is a first-class Agent Framework capability for applying governance and runtime controls at well-defined points in an agent's execution. It implements the framework-neutral [AGENT-HOOKS-0.1 contract](https://github.com/responsibleai/agent-hooks/blob/main/spec/AGENT-HOOKS-0.1.md), so policy engines, approval gateways, budget guards, content filters, and egress controls can target one common control surface. + +> [!IMPORTANT] +> Agent Hooks is a control plane, not a telemetry plane. Every interceptor returns a verdict. In `enforce` mode, the framework acts on that verdict; in `evaluate_only` mode, it records the verdict without changing execution. Use [observability](./observability.md) for passive tracing, metrics, and logs. + +::: zone pivot="programming-language-csharp" + +Agent Hooks isn't yet available for .NET. Use [agent middleware](../concepts/agents/middleware/index.md), [tool approval](./tools/tool-approval.md), and [agent safety](../concepts/agents/safety.md) to add runtime controls to .NET agents. + +::: zone-end + +::: zone pivot="programming-language-python" + +Agent Hooks is experimental in Python. The factory emits an `ExperimentalWarning` when first used, and its API can change before general availability. + +## When to use Agent Hooks + +Use Agent Hooks when independently developed controls need one shared, enforceable contract across agent input, model calls, tool calls, and final output. + +| Capability | Use it for | +|---|---| +| **Agent Hooks** | Standardized policy decisions, transforms, approvals, budgets, and egress controls across the agent lifecycle. | +| [Agent middleware](../concepts/agents/middleware/index.md) | Application-specific cross-cutting behavior that doesn't need the Agent Hooks contract or its core runtime guarantees. | +| [Agent Security with FIDES](./security.md) | Deterministic information-flow labels and policies for untrusted or confidential content. | +| [Tool approval](./tools/tool-approval.md) | Human confirmation of individual function-tool calls. | +| [Observability](./observability.md) | Passive traces, metrics, and logs that don't control execution. | + +## What Agent Framework enforces + +When you add Agent Hooks to an agent, Agent Framework applies a coordinated enforcement boundary across agent runs, model calls, and tool calls. The runtime provides the following guarantees: + +- **Fail closed:** A deny blocks the guarded action. Invalid contexts, invalid verdicts, interceptor failures, and enforcement failures don't silently bypass controls. +- **Transform write-back:** A transform changes the native messages, tool arguments, tool results, or final response that execution actually uses. If a transform can't be applied, the run fails closed. +- **Buffered streaming:** No response update reaches the caller until the complete model response and final output pass their interception points. +- **Verdict-gated persistence:** Persistence waits for the verdict that covers it. Standard after-run persistence waits for `output`; per-service-call history persistence waits for each `post_model_call`. +- **Complete bundle installation:** The agent, chat, and function parts are installed as one unit, so an incomplete enforcement boundary can't be configured accidentally. + +The contract is cooperative rather than a process isolation boundary. Interceptors run in the host process and receive the content needed to make decisions. Only register interceptors you trust. + +## Install Agent Hooks + +Install the optional `agent-hooks` extra for the core package: + +```bash +pip install "agent-framework-core[agent-hooks]" +``` + +If you use `uv`: + +```bash +uv add "agent-framework-core[agent-hooks]" +``` + +The `agent-hooks-sdk` dependency is lazy-imported. Importing `agent_framework` doesn't load the SDK unless you create an Agent Hooks middleware bundle. + +> [!NOTE] +> The `agent-hooks` extra is intentionally not included in `agent-framework-core[all]`. Install it explicitly when you want to enable this experimental control surface. + +## Add an interceptor + +An interceptor receives an `agent_hooks.AgentContext` (the specification's context mapping, not the `agent_framework.AgentContext` used by agent middleware) and returns a verdict. The following interceptor blocks final output containing the word `secret`. The example assumes `client` is an already configured Agent Framework chat client. + +```python +from agent_framework import Agent, create_agent_hooks_middleware +from agent_hooks import ALLOW, AgentContext, InterceptionBlocked, Verdict + + +class SecretEgressGuard: + def intercept(self, context: AgentContext) -> Verdict: + if ( + context["interception_point"] == "output" + and "secret" in str(context["target"]).lower() + ): + return Verdict.deny( + reason="secret_in_output", + message="The final response contains restricted content.", + ) + return ALLOW + + +hooks = create_agent_hooks_middleware( + {"secret-egress": SecretEgressGuard()}, +) + +agent = Agent( + client=client, + instructions="You are a helpful assistant.", + middleware=[hooks], +) + +try: + response = await agent.run("Summarize the account details.") +except InterceptionBlocked as exc: + print(f"Blocked: {exc.result.verdict.reason}") +``` + +Pass the bundle as one element of the agent's `middleware` list. Install exactly one Agent Hooks bundle on each agent. + +## Interception points + +Agent Framework emits the applicable interception points automatically: + +| Interception point | When it's emitted | Transform target | +|---|---|---| +| `agent_startup` | Before the first input in an Agent Hooks session | Not transformable | +| `input` | When an external request enters the agent | Input content and role | +| `pre_model_call` | Before each model request | Messages sent to the model | +| `post_model_call` | After each complete model response | Response content, framework-executed tool calls, and finish reason | +| `pre_tool_call` | Before each framework-executed tool invocation | Tool arguments | +| `post_tool_call` | After a tool succeeds or fails | Tool result | +| `output` | Before the final response reaches the caller | Final response content | +| `agent_shutdown` | When the Agent Hooks session completes, fails, or is canceled | Not transformable | + +A run that calls a tool typically emits: + +`agent_startup` → `input` → `pre_model_call` → `post_model_call` → `pre_tool_call` → `post_tool_call` → `pre_model_call` → `post_model_call` → `output` → `agent_shutdown` + +## Verdicts + +The contract has three decisions: `allow`, `deny`, and `transform`. The Python SDK also provides helpers for warnings and liftable denies. + +| Result | Python API | Behavior | +|---|---|---| +| Allow | `ALLOW` or `Verdict(decision=Decision.ALLOW)` | Continue with the target unchanged. | +| Allow with warning | `Verdict.warn(...)` | Continue and include the warning in the interception record. | +| Deny | `Verdict.deny(...)` | Block the guarded action. | +| Deny pending approval | `Verdict.escalate(...)` | Block unless the configured approval resolver returns a permit verdict. | +| Transform | `Verdict(decision=Decision.TRANSFORM, transform=Transform(...))` | Rewrite a value under `$target`, then continue with the rewritten value. | + +Run-level and model-level denies raise `InterceptionBlocked` and prevent the guarded result from reaching the caller or the next stage. At a tool seam, a policy deny prevents the tool action or discards its result and returns a control error containing the policy reason, without the denied target payload, to the model. This allows the agent loop to continue. A host or enforcement failure halts the run. + +### Apply a transform + +A transform path must start at `$target`. For example, an interceptor can replace final response content: + +```python +from agent_hooks import ALLOW, AgentContext, Decision, Transform, Verdict + + +class OutputRedactor: + def intercept(self, context: AgentContext) -> Verdict: + if context["interception_point"] != "output": + return ALLOW + + return Verdict( + decision=Decision.TRANSFORM, + reason="redacted_output", + transform=Transform( + path="$target.content", + value="[Response removed by policy]", + ), + ) +``` + +Transforms are applied to Agent Framework `Content` values, preserving supported rich content rather than reducing every value to plain text. A malformed path or incompatible replacement fails closed instead of continuing with the original value. + +### Tool approval and argument transforms + +Agent Framework tool approval and the Agent Hooks approval seam are separate mechanisms. For a function tool with `approval_mode="always_require"`, Agent Framework creates the human approval request before function middleware runs. A `pre_tool_call` transform can therefore change arguments after the user approved the original values. + +> [!WARNING] +> Don't transform arguments at `pre_tool_call` for tools that use `approval_mode="always_require"`. Transform the tool call at `post_model_call` so the framework approval request contains the transformed values, or return `Verdict.escalate(...)` at `pre_tool_call` and resolve approval through the Agent Hooks `resolver`. + +## Streaming and persistence + +Agent Hooks keeps the streaming API but uses buffered-output semantics. Agent Framework assembles the complete model response, emits `post_model_call`, assembles the final agent response, and emits `output` before releasing any updates. If either point denies the response, the caller receives no partial updates. + +This behavior trades token-by-token latency for fail-closed output enforcement. An output transform is also reflected in the updates eventually released to the caller. + +Persistence is gated by the interception point that covers the persistence operation: + +- By default, history and other after-run provider work wait for the `output` verdict. A denied output isn't persisted, and an output transform is persisted after transformation. +- When you set `require_per_service_call_history_persistence=True` on the `Agent` constructor or `client.as_agent(...)`, each model exchange is persisted after its `post_model_call` verdict permits it. A later `output` deny doesn't roll back that already permitted history. +- For default after-run persistence, retry attempts remain behind the final `output` decision. Per-service-call mode instead persists each model response that passes `post_model_call`. + +> [!IMPORTANT] +> If model content must not become durable, enforce that policy at `post_model_call` when `require_per_service_call_history_persistence=True`. An output-only egress policy protects what reaches the caller, but it doesn't retroactively remove model exchanges already permitted and persisted at `post_model_call`. + +## Sessions and audit records + +By default, each agent run creates one Agent Hooks session. `agent_startup` and `agent_shutdown` bracket the run, and records receive one session ID with a monotonically increasing sequence. + +Use `record_sink` to receive each `InterceptionRecord`: + +```python +records = [] + +hooks = create_agent_hooks_middleware( + {"secret-egress": SecretEgressGuard()}, + record_sink=records.append, +) +``` + +Interception records capture the decision, reason, interceptor summary, mode, identity, and sequence without copying the intercepted payload into the audit record. The interceptor itself still receives the full context. + +### Span multiple runs with one session + +Use `create_agent_hooks_middleware_from_emitter()` when the application owns a longer-lived Agent Hooks session, such as a conversation with one approval ledger: + +```python +from agent_framework import Agent, create_agent_hooks_middleware_from_emitter +from agent_hooks import AgentContextBuilder, InterceptionEmitter + + +emitter = InterceptionEmitter().register(SecretEgressGuard()) +builder = AgentContextBuilder( + agent_id="support-agent", + framework="agent-framework", + session_id="conversation-42", +) + +hooks = create_agent_hooks_middleware_from_emitter(emitter, builder) +agent = Agent(client=client, middleware=[hooks]) + +await emitter.emit(builder.agent_startup(tools_registered=[])) +await agent.run("First turn") +await agent.run("Second turn") +await emitter.emit(builder.agent_shutdown(reason="completed")) +``` + +In this form, the application configures the emitter and owns startup, shutdown, and error cleanup. The middleware emits the per-run points from `input` through `output`. + +## Configure enforcement + +`create_agent_hooks_middleware()` accepts the following controls: + +| Parameter | Purpose | +|---|---| +| `interceptors` | A sequence of interceptors or a name-to-interceptor mapping. At least one is required. | +| `resolver` | Resolves liftable denies through an approval channel. Without a resolver, the deny remains in effect. | +| `mode` | `"enforce"` applies verdicts. `"evaluate_only"` records what would happen but allows every action. | +| `composition` | Selects how multiple interceptor verdicts are combined. | +| `identity_provider` | Produces content-bound context identities. The default is `"jcs-sha256"`. | +| `timeout` | Per-interceptor and resolver timeout for awaitable calls. The default is five seconds. A synchronous interceptor or resolver that blocks the event loop can't be preempted by this timeout. | +| `record_sink` | Receives each payload-free interception record. | + +The default composition is sequential `first_deny` with approval configured to stop the fold. Interceptor order therefore matters: put controls that must always run before controls that can request approval. See the Agent Hooks [production checklist](https://github.com/responsibleai/agent-hooks/blob/main/docs/PRODUCTION.md) before selecting another composition profile. + +### Roll out with evaluate-only mode + +Use `evaluate_only` to measure policy behavior before enforcement: + +```python +hooks = create_agent_hooks_middleware( + {"secret-egress": SecretEgressGuard()}, + mode="evaluate_only", + record_sink=records.append, +) +``` + +In this mode, interceptors run and records include their verdicts, but no action is blocked or transformed. Don't describe an `evaluate_only` deployment as enforced governance. + +## Composition rules + +Place the bundle first in the agent's middleware list so it forms the outermost enforcement boundary: + +```python +agent = Agent( + client=client, + middleware=[ + create_agent_hooks_middleware([SecretEgressGuard()]), + application_middleware, + ], +) +``` + +Follow these rules: + +- Install exactly one Agent Hooks bundle per agent. Stacked bundles are rejected. +- Keep the bundle intact. Its agent, chat, and function middleware can't be installed separately. +- Install the bundle on `Agent`, not directly on a chat client or through a context provider. +- Middleware placed before the bundle is outside the enforcement boundary. Treat outer position as outer trust. +- Give each nested agent its own bundle when its internal model and tool activity also needs interception. + +## Current limitations + +- **Python only:** Agent Hooks isn't yet implemented in the .NET or Go SDKs. +- **Experimental API:** Factory signatures and behavior can change before general availability. +- **Buffered streaming:** Updates aren't released token by token because output must be complete before a fail-closed verdict. +- **Hosted tools:** Tools executed by a model provider don't pass through Agent Framework's function-invocation seam. Their calls and outputs are surfaced in `post_model_call`, but `pre_tool_call` and `post_tool_call` can't block the provider's server-side execution. +- **Cooperative boundary:** Agent Hooks doesn't sandbox interceptors or protect against a hostile host. Code paths that bypass the guarded agent pipeline aren't covered. +- **Interceptor availability affects agent availability:** In enforce mode, an interceptor failure or timeout blocks the guarded action by design. + +For production rollout, failure reasons, and alerting guidance, see the Agent Hooks [operations runbook](https://github.com/responsibleai/agent-hooks/blob/main/docs/OPERATIONS.md). + +::: zone-end + +::: zone pivot="programming-language-go" + +Agent Hooks isn't yet available for Go. Use [agent middleware](../concepts/agents/middleware/index.md), [tool approval](./tools/tool-approval.md), and [agent safety](../concepts/agents/safety.md) to add runtime controls to Go agents. + +::: zone-end + +## Next steps + +> [!div class="nextstepaction"] +> [Understand the agent pipeline](../concepts/agents/agent-pipeline.md) + +### Related content + +- [Agent middleware](../concepts/agents/middleware/index.md) +- [Agent safety](../concepts/agents/safety.md) +- [Tool approval](./tools/tool-approval.md) +- [Agent Security with FIDES](./security.md) +- [Observability](./observability.md) +- [AGENT-HOOKS-0.1 specification](https://github.com/responsibleai/agent-hooks/blob/main/spec/AGENT-HOOKS-0.1.md) diff --git a/agent-framework/agents/index.md b/agent-framework/agents/index.md index e9677923..cba8f8ef 100644 --- a/agent-framework/agents/index.md +++ b/agent-framework/agents/index.md @@ -4,7 +4,7 @@ description: Browse built-in Agent Framework capabilities for multimodal input, author: eavanvalkenburg ms.topic: overview ms.author: edvan -ms.date: 07/30/2026 +ms.date: 08/07/2026 ms.service: agent-framework --- @@ -52,6 +52,7 @@ Looking for the agent-type and SDK-selection guidance previously hosted on this |---|---| | [Observability](observability.md) | Export traces, metrics, and logs. | | [Evaluation](evaluation.md) | Measure agent quality, safety, and correctness. | +| [Agent Hooks](agent-hooks.md) | Apply fail-closed governance controls through a shared interception contract. | | [Agent Security with FIDES](security.md) | Enforce information-flow controls across agent data and tools. | The [Harness Agent](../concepts/harness.md) assembles many of these capabilities into an opinionated operational agent. diff --git a/agent-framework/concepts/agents/agent-pipeline.md b/agent-framework/concepts/agents/agent-pipeline.md index fca085fd..0ea4fbdd 100644 --- a/agent-framework/concepts/agents/agent-pipeline.md +++ b/agent-framework/concepts/agents/agent-pipeline.md @@ -5,7 +5,7 @@ zone_pivot_groups: programming-languages author: eavanvalkenburg ms.topic: article ms.author: edvan -ms.date: 07/01/2026 +ms.date: 08/07/2026 ms.service: agent-framework --- @@ -51,6 +51,8 @@ The `Agent` class builds a pipeline through class composition with two main comp When you call `run()`, your request flows through the Agent layers, then into the ChatClient pipeline for LLM communication. +The optional [Agent Hooks](../../agents/agent-hooks.md) capability installs one middleware bundle across the agent, chat, and function layers. Core streaming and persistence gates extend that boundary so output isn't released or stored before the applicable verdict permits it. + ::: zone-end ::: zone pivot="programming-language-go" diff --git a/agent-framework/concepts/agents/middleware/index.md b/agent-framework/concepts/agents/middleware/index.md index 779fdd9c..76fae459 100644 --- a/agent-framework/concepts/agents/middleware/index.md +++ b/agent-framework/concepts/agents/middleware/index.md @@ -5,7 +5,7 @@ zone_pivot_groups: programming-languages author: dmytrostruk ms.topic: reference ms.author: dmytrostruk -ms.date: 07/01/2026 +ms.date: 08/07/2026 ms.service: agent-framework --- @@ -193,6 +193,9 @@ All types support both function-based and class-based implementations. When mult > `A1 -> A2 -> R1 -> R2 -> Agent -> R2 -> R1 -> A2 -> A1`. > - Function/chat middleware follows the same wrapping principle at tool/chat-call time. +> [!TIP] +> For a standardized, fail-closed control boundary spanning agent, chat, and function middleware, see [Agent Hooks](../../../agents/agent-hooks.md). Agent Hooks also coordinates core streaming and persistence behavior that ordinary middleware can't provide by itself. + ## Agent Middleware Agent middleware intercepts and modifies agent run execution. It uses the `AgentContext` which contains: