diff --git a/.github/.linkspector.yml b/.github/.linkspector.yml
index 24fee6edcef..eb365c2982d 100644
--- a/.github/.linkspector.yml
+++ b/.github/.linkspector.yml
@@ -12,6 +12,8 @@ ignorePatterns:
- pattern: "https:\/\/platform.openai.com"
- pattern: "http:\/\/localhost"
- pattern: "http:\/\/127.0.0.1"
+ - pattern: "https:\/\/localhost"
+ - pattern: "https:\/\/127.0.0.1"
- pattern: "0001-spec.md"
- pattern: "0001-madr-architecture-decisions.md"
- pattern: "https://api.powerplatform.com/.default"
diff --git a/.github/upgrades/prompts/SemanticKernelToAgentFramework.md b/.github/upgrades/prompts/SemanticKernelToAgentFramework.md
index 2f2a6886c7f..a121a5f4469 100644
--- a/.github/upgrades/prompts/SemanticKernelToAgentFramework.md
+++ b/.github/upgrades/prompts/SemanticKernelToAgentFramework.md
@@ -1052,7 +1052,7 @@ AgentThread thread = agent.GetNewThread();
**Add Agent Framework Packages:**
```xml
-
+
```
diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml
index 1a51f941193..5abfe2a879d 100644
--- a/.github/workflows/dotnet-build-and-test.yml
+++ b/.github/workflows/dotnet-build-and-test.yml
@@ -74,6 +74,7 @@ jobs:
.
.github
dotnet
+ python
workflow-samples
- name: Setup dotnet
diff --git a/.github/workflows/python-code-quality.yml b/.github/workflows/python-code-quality.yml
index a39445c6433..871436509c0 100644
--- a/.github/workflows/python-code-quality.yml
+++ b/.github/workflows/python-code-quality.yml
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.10"]
+ python-version: ["3.10", "3.14"]
runs-on: ubuntu-latest
continue-on-error: true
defaults:
diff --git a/.github/workflows/python-lab-tests.yml b/.github/workflows/python-lab-tests.yml
index bae78be27c6..ae526cf962f 100644
--- a/.github/workflows/python-lab-tests.yml
+++ b/.github/workflows/python-lab-tests.yml
@@ -48,7 +48,7 @@ jobs:
strategy:
fail-fast: true
matrix:
- python-version: ["3.10", "3.11", "3.12", "3.13"]
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
# TODO(ekzhu): re-enable macos-latest when this is fixed: https://github.com/actions/runner-images/issues/11881
os: [ubuntu-latest, windows-latest]
env:
diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml
index 7a6badaba42..697a8ff4a72 100644
--- a/.github/workflows/python-tests.yml
+++ b/.github/workflows/python-tests.yml
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: true
matrix:
- python-version: ["3.10", "3.11", "3.12", "3.13"]
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
# todo: add macos-latest when problems are resolved
os: [ubuntu-latest, windows-latest]
env:
diff --git a/docs/decisions/0010-ag-ui-support.md b/docs/decisions/0010-ag-ui-support.md
new file mode 100644
index 00000000000..8d9475bb5af
--- /dev/null
+++ b/docs/decisions/0010-ag-ui-support.md
@@ -0,0 +1,95 @@
+---
+status: accepted
+contact: javiercn
+date: 2025-10-29
+deciders: javiercn, DeagleGross, moonbox3, markwallace-microsoft
+consulted: Agent Framework team
+informed: .NET community
+---
+
+# AG-UI Protocol Support for .NET Agent Framework
+
+## Context and Problem Statement
+
+The .NET Agent Framework needed a standardized way to enable communication between AI agents and user-facing applications with support for streaming, real-time updates, and bidirectional communication. Without AG-UI protocol support, .NET agents could not interoperate with the growing ecosystem of AG-UI-compatible frontends and agent frameworks (LangGraph, CrewAI, Pydantic AI, etc.), limiting the framework's adoption and utility.
+
+The AG-UI (Agent-User Interaction) protocol is an open, lightweight, event-based protocol that addresses key challenges in agentic applications including streaming support for long-running agents, event-driven architecture for nondeterministic behavior, and protocol interoperability that complements MCP (tool/context) and A2A (agent-to-agent) protocols.
+
+## Decision Drivers
+
+- Need for streaming communication between agents and client applications
+- Requirement for protocol interoperability with other AI frameworks
+- Support for long-running, multi-turn conversation sessions
+- Real-time UI updates for nondeterministic agent behavior
+- Standardized approach to agent-to-UI communication
+- Framework abstraction to protect consumers from protocol changes
+
+## Considered Options
+
+1. **Implement AG-UI event types as public API surface** - Expose AG-UI event models directly to consumers
+2. **Use custom AIContent types for lifecycle events** - Create new content types (RunStartedContent, RunFinishedContent, RunErrorContent)
+3. **Current approach** - Internal event types with framework-native abstractions
+
+## Decision Outcome
+
+Chosen option: "Current approach with internal event types and framework-native abstractions", because it:
+
+- Protects consumers from protocol changes by keeping AG-UI events internal
+- Maintains framework abstractions through conversion at boundaries
+- Uses existing framework types (AgentRunResponseUpdate, ChatMessage) for public API
+- Focuses on core text streaming functionality
+- Leverages existing properties (ConversationId, ResponseId, ErrorContent) instead of custom types
+- Provides bidirectional client and server support
+
+### Implementation Details
+
+**In Scope:**
+1. **Client-side AG-UI consumption** (`Microsoft.Agents.AI.AGUI` package)
+ - `AGUIAgent` class for connecting to remote AG-UI servers
+ - `AGUIAgentThread` for managing conversation threads
+ - HTTP/SSE streaming support
+ - Event-to-framework type conversion
+
+2. **Server-side AG-UI hosting** (`Microsoft.Agents.AI.Hosting.AGUI.AspNetCore` package)
+ - `MapAGUIAgent` extension method for ASP.NET Core
+ - Server-Sent Events (SSE) response formatting
+ - Framework-to-event type conversion
+ - Agent factory pattern for per-request instantiation
+
+3. **Text streaming events**
+ - Lifecycle events: `RunStarted`, `RunFinished`, `RunError`
+ - Text message events: `TextMessageStart`, `TextMessageContent`, `TextMessageEnd`
+ - Thread and run ID management via `ConversationId` and `ResponseId`
+
+### Key Design Decisions
+
+1. **Event Models as Internal Types** - AG-UI event types are internal with conversion via extension methods; public API uses the existing types in Microsoft.Extensions.AI as those are the abstractions people are familiar with
+
+2. **No Custom Content Types** - Run lifecycle communicated through existing `ChatResponseUpdate` properties (`ConversationId`, `ResponseId`) and standard `ErrorContent` type
+
+3. **Agent Factory Pattern** - `MapAGUIAgent` uses factory function `(messages) => AIAgent` to allow request-specific agent configuration supporting multi-tenancy
+
+4. **Bidirectional Conversion Architecture** - Symmetric conversion logic in shared namespace compiled into both packages for server (`AgentRunResponseUpdate` → AG-UI events) and client (AG-UI events → `AgentRunResponseUpdate`)
+
+5. **Thread Management** - `AGUIAgentThread` stores only `ThreadId` with thread ID communicated via `ConversationId`; applications manage persistence for parity with other implementations and to be compliant with the protocol. Future extensions will support having the server manage the conversation.
+
+6. **Custom JSON Converter** - Uses custom polymorphic deserialization via `BaseEventJsonConverter` instead of built-in System.Text.Json support to handle AG-UI protocol's flexible discriminator positioning
+
+### Consequences
+
+**Positive:**
+- .NET developers can consume AG-UI servers from any framework
+- .NET agents accessible from any AG-UI-compatible client
+- Standardized streaming communication patterns
+- Protected from protocol changes through internal implementation
+- Symmetric conversion logic between client and server
+- Framework-native public API surface
+
+**Negative:**
+- Custom JSON converter required (internal implementation detail)
+- Shared code uses preprocessor directives (`#if ASPNETCORE`)
+- Additional abstraction layer between protocol and public API
+
+**Neutral:**
+- Initial implementation focused on text streaming
+- Applications responsible for thread persistence
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 69d3e03d319..7fb64599064 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -30,6 +30,7 @@
+
@@ -52,6 +53,7 @@
+
@@ -97,7 +99,7 @@
-
+
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 86eee2fbbe9..7cbe76b6fcb 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -18,6 +18,10 @@
+
+
+
+
@@ -62,6 +66,10 @@
+
+
+
+
@@ -272,10 +280,13 @@
-
+
+
+
+
@@ -289,6 +300,7 @@
+
@@ -297,9 +309,11 @@
+
-
+
+
diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props
index cc1c3b84aca..69476cc7134 100644
--- a/dotnet/nuget/nuget-package.props
+++ b/dotnet/nuget/nuget-package.props
@@ -2,9 +2,9 @@
1.0.0
- $(VersionPrefix)-$(VersionSuffix).251104.1
- $(VersionPrefix)-preview.251104.1
- 1.0.0-preview.251104.1
+ $(VersionPrefix)-$(VersionSuffix).251105.1
+ $(VersionPrefix)-preview.251105.1
+ 1.0.0-preview.251105.1
Debug;Release;Publish
true
diff --git a/dotnet/samples/A2AClientServer/A2AServer/A2AServer.csproj b/dotnet/samples/A2AClientServer/A2AServer/A2AServer.csproj
index a053b9e33b7..8d67180f64f 100644
--- a/dotnet/samples/A2AClientServer/A2AServer/A2AServer.csproj
+++ b/dotnet/samples/A2AClientServer/A2AServer/A2AServer.csproj
@@ -20,7 +20,7 @@
-
+
diff --git a/dotnet/samples/AGUIClientServer/AGUIClient/AGUIClient.csproj b/dotnet/samples/AGUIClientServer/AGUIClient/AGUIClient.csproj
new file mode 100644
index 00000000000..db07df5504a
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIClient/AGUIClient.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+ a8b2e9f0-1ea3-4f18-9d41-42d1a6f8fe10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/AGUIClientServer/AGUIClient/Program.cs b/dotnet/samples/AGUIClientServer/AGUIClient/Program.cs
new file mode 100644
index 00000000000..0c6a6539a84
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIClient/Program.cs
@@ -0,0 +1,137 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates how to use the AG-UI client to connect to a remote AG-UI server
+// and display streaming updates including conversation/response metadata, text content, and errors.
+
+using System.CommandLine;
+using System.Reflection;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.AGUI;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace AGUIClient;
+
+public static class Program
+{
+ public static async Task Main(string[] args)
+ {
+ // Create root command with options
+ RootCommand rootCommand = new("AGUIClient");
+ rootCommand.SetAction((_, ct) => HandleCommandsAsync(ct));
+
+ // Run the command
+ return await rootCommand.Parse(args).InvokeAsync();
+ }
+
+ private static async Task HandleCommandsAsync(CancellationToken cancellationToken)
+ {
+ // Set up the logging
+ using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
+ {
+ builder.AddConsole();
+ builder.SetMinimumLevel(LogLevel.Information);
+ });
+ ILogger logger = loggerFactory.CreateLogger("AGUIClient");
+
+ // Retrieve configuration settings
+ IConfigurationRoot configRoot = new ConfigurationBuilder()
+ .AddEnvironmentVariables()
+ .AddUserSecrets(Assembly.GetExecutingAssembly())
+ .Build();
+
+ string serverUrl = configRoot["AGUI_SERVER_URL"] ?? "http://localhost:5100";
+
+ logger.LogInformation("Connecting to AG-UI server at: {ServerUrl}", serverUrl);
+
+ // Create the AG-UI client agent
+ using HttpClient httpClient = new()
+ {
+ Timeout = TimeSpan.FromSeconds(60)
+ };
+
+ AGUIAgent agent = new(
+ id: "agui-client",
+ description: "AG-UI Client Agent",
+ httpClient: httpClient,
+ endpoint: serverUrl);
+
+ AgentThread thread = agent.GetNewThread();
+ List messages = [new(ChatRole.System, "You are a helpful assistant.")];
+ try
+ {
+ while (true)
+ {
+ // Get user message
+ 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(ChatRole.User, message));
+
+ // Call RunStreamingAsync to get streaming updates
+ bool isFirstUpdate = true;
+ string? threadId = null;
+ await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread, cancellationToken: cancellationToken))
+ {
+ // Use AsChatResponseUpdate to access ChatResponseUpdate properties
+ ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
+ if (chatUpdate.ConversationId != null)
+ {
+ threadId = chatUpdate.ConversationId;
+ }
+
+ // Display run started information from the first update
+ if (isFirstUpdate && threadId != null && update.ResponseId != null)
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {update.ResponseId}]");
+ Console.ResetColor();
+ isFirstUpdate = false;
+ }
+
+ // Display different content types with appropriate formatting
+ foreach (AIContent content in update.Contents)
+ {
+ switch (content)
+ {
+ case TextContent textContent:
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.Write(textContent.Text);
+ Console.ResetColor();
+ break;
+
+ case ErrorContent errorContent:
+ Console.ForegroundColor = ConsoleColor.Red;
+ string code = errorContent.AdditionalProperties?["Code"] as string ?? "Unknown";
+ Console.WriteLine($"\n[Error - Code: {code}, Message: {errorContent.Message}]");
+ Console.ResetColor();
+ break;
+ }
+ }
+ }
+ messages.Clear();
+ Console.WriteLine();
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ logger.LogInformation("AGUIClient operation was canceled.");
+ }
+ catch (Exception ex) when (ex is not OutOfMemoryException and not StackOverflowException and not ThreadAbortException and not AccessViolationException)
+ {
+ logger.LogError(ex, "An error occurred while running the AGUIClient");
+ return;
+ }
+ }
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIClient/README.md b/dotnet/samples/AGUIClientServer/AGUIClient/README.md
new file mode 100644
index 00000000000..f0f60521595
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIClient/README.md
@@ -0,0 +1,34 @@
+# AG-UI Client
+
+This is a console application that demonstrates how to connect to an AG-UI server and interact with remote agents using the AG-UI protocol.
+
+## Features
+
+- Connects to an AG-UI server endpoint
+- Displays streaming updates with color-coded output:
+ - **Yellow**: Run started notifications
+ - **Cyan**: Agent text responses (streamed)
+ - **Green**: Run finished notifications
+ - **Red**: Error messages (if any)
+- Interactive prompt loop for sending messages
+
+## Configuration
+
+Set the following environment variable to specify the AG-UI server URL:
+
+```powershell
+$env:AGUI_SERVER_URL="http://localhost:5100"
+```
+
+If not set, the default is `http://localhost:5100`.
+
+## Running the Client
+
+1. Make sure the AG-UI server is running
+2. Run the client:
+ ```bash
+ cd AGUIClient
+ dotnet run
+ ```
+3. Enter your messages and observe the streaming updates
+4. Type `:q` or `quit` to exit
diff --git a/dotnet/samples/AGUIClientServer/AGUIServer/AGUIServer.csproj b/dotnet/samples/AGUIClientServer/AGUIServer/AGUIServer.csproj
new file mode 100644
index 00000000000..c1bcd511da2
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIServer/AGUIServer.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+ a8b2e9f0-1ea3-4f18-9d41-42d1a6f8fe10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/AGUIClientServer/AGUIServer/AGUIServer.http b/dotnet/samples/AGUIClientServer/AGUIServer/AGUIServer.http
new file mode 100644
index 00000000000..b3f58318936
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIServer/AGUIServer.http
@@ -0,0 +1,17 @@
+@host = http://localhost:5100
+
+### Send a message to the AG-UI agent
+POST {{host}}/
+Content-Type: application/json
+
+{
+ "threadId": "thread_123",
+ "runId": "run_456",
+ "messages": [
+ {
+ "role": "user",
+ "content": "What is the capital of France?"
+ }
+ ],
+ "context": {}
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIServer/Program.cs b/dotnet/samples/AGUIClientServer/AGUIServer/Program.cs
new file mode 100644
index 00000000000..f26ace30a1b
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIServer/Program.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
+using Microsoft.Extensions.AI;
+using OpenAI;
+
+WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
+builder.Services.AddHttpClient().AddLogging();
+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.");
+
+// Create the AI agent
+var agent = new AzureOpenAIClient(
+ new Uri(endpoint),
+ new DefaultAzureCredential())
+ .GetChatClient(deploymentName)
+ .CreateAIAgent(name: "AGUIAssistant");
+
+// Map the AG-UI agent endpoint
+app.MapAGUI("/", agent);
+
+await app.RunAsync();
diff --git a/dotnet/samples/AGUIClientServer/README.md b/dotnet/samples/AGUIClientServer/README.md
new file mode 100644
index 00000000000..dabc841542f
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/README.md
@@ -0,0 +1,202 @@
+# AG-UI Client and Server Sample
+
+This sample demonstrates how to use the AG-UI (Agent UI) protocol to enable communication between a client application and a remote agent server. The AG-UI protocol provides a standardized way for clients to interact with AI agents.
+
+## Overview
+
+The demonstration has two components:
+
+1. **AGUIServer** - An ASP.NET Core web server that hosts an AI agent and exposes it via the AG-UI protocol
+2. **AGUIClient** - A console application that connects to the AG-UI server and displays streaming updates
+
+> **Warning**
+> The AG-UI protocol is still under development and changing.
+> We will try to keep these samples updated as the protocol evolves.
+
+## Configuring Environment Variables
+
+Configure the required Azure OpenAI environment variables:
+
+```powershell
+$env:AZURE_OPENAI_ENDPOINT="<>"
+$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4.1-mini"
+```
+
+> **Note:** This sample uses `DefaultAzureCredential` for authentication. Make sure you're authenticated with Azure (e.g., via `az login`, Visual Studio, or environment variables).
+
+## Running the Sample
+
+### Step 1: Start the AG-UI Server
+
+```bash
+cd AGUIServer
+dotnet build
+dotnet run --urls "http://localhost:5100"
+```
+
+The server will start and listen on `http://localhost:5100`.
+
+### Step 2: Testing with the REST Client (Optional)
+
+Before running the client, you can test the server using the included `.http` file:
+
+1. Open [./AGUIServer/AGUIServer.http](./AGUIServer/AGUIServer.http) in Visual Studio or VS Code with the REST Client extension
+2. Send a test request to verify the server is working
+3. Observe the server-sent events stream in the response
+
+Sample request:
+```http
+POST http://localhost:5100/
+Content-Type: application/json
+
+{
+ "threadId": "thread_123",
+ "runId": "run_456",
+ "messages": [
+ {
+ "role": "user",
+ "content": "What is the capital of France?"
+ }
+ ],
+ "context": {}
+}
+```
+
+### Step 3: Run the AG-UI Client
+
+In a new terminal window:
+
+```bash
+cd AGUIClient
+dotnet run
+```
+
+Optionally, configure a different server URL:
+
+```powershell
+$env:AGUI_SERVER_URL="http://localhost:5100"
+```
+
+### Step 4: Interact with the Agent
+
+1. The client will connect to the AG-UI server
+2. Enter your message at the prompt
+3. Observe the streaming updates with color-coded output:
+ - **Yellow**: Run started notification showing thread and run IDs
+ - **Cyan**: Agent's text response (streamed character by character)
+ - **Green**: Run finished notification
+ - **Red**: Error messages (if any occur)
+4. Type `:q` or `quit` to exit
+
+## Sample Output
+
+```
+AGUIClient> dotnet run
+info: AGUIClient[0]
+ Connecting to AG-UI server at: http://localhost:5100
+
+User (:q or quit to exit): What is the capital of France?
+
+[Run Started - Thread: thread_abc123, Run: run_xyz789]
+The capital of France is Paris. It is known for its rich history, culture, and iconic landmarks such as the Eiffel Tower and the Louvre Museum.
+[Run Finished - Thread: thread_abc123, Run: run_xyz789]
+
+User (:q or quit to exit): Tell me a fun fact about space
+
+[Run Started - Thread: thread_abc123, Run: run_def456]
+Here's a fun fact: A day on Venus is longer than its year! Venus takes about 243 Earth days to rotate once on its axis, but only about 225 Earth days to orbit the Sun.
+[Run Finished - Thread: thread_abc123, Run: run_def456]
+
+User (:q or quit to exit): :q
+```
+
+## How It Works
+
+### Server Side
+
+The `AGUIServer` uses the `MapAGUI` extension method to expose an agent through the AG-UI protocol:
+
+```csharp
+AIAgent agent = new OpenAIClient(apiKey)
+ .GetChatClient(model)
+ .CreateAIAgent(
+ instructions: "You are a helpful assistant.",
+ name: "AGUIAssistant");
+
+app.MapAGUI("/", agent);
+```
+
+This automatically handles:
+- HTTP POST requests with message payloads
+- Converting agent responses to AG-UI event streams
+- Server-sent events (SSE) formatting
+- Thread and run management
+
+### Client Side
+
+The `AGUIClient` uses the `AGUIAgent` class to connect to the remote server:
+
+```csharp
+AGUIAgent agent = new(
+ id: "agui-client",
+ description: "AG-UI Client Agent",
+ messages: [],
+ httpClient: httpClient,
+ endpoint: serverUrl);
+
+bool isFirstUpdate = true;
+AgentRunResponseUpdate? currentUpdate = null;
+
+await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread))
+{
+ // First update indicates run started
+ if (isFirstUpdate)
+ {
+ Console.WriteLine($"[Run Started - Thread: {update.ConversationId}, Run: {update.ResponseId}]");
+ isFirstUpdate = false;
+ }
+
+ currentUpdate = update;
+
+ foreach (AIContent content in update.Contents)
+ {
+ switch (content)
+ {
+ case TextContent textContent:
+ // Display streaming text
+ Console.Write(textContent.Text);
+ break;
+ case ErrorContent errorContent:
+ // Display error notification
+ Console.WriteLine($"[Error: {errorContent.Message}]");
+ break;
+ }
+ }
+}
+
+// Last update indicates run finished
+if (currentUpdate != null)
+{
+ Console.WriteLine($"\n[Run Finished - Thread: {currentUpdate.ConversationId}, Run: {currentUpdate.ResponseId}]");
+}
+```
+
+The `RunStreamingAsync` method:
+1. Sends messages to the server via HTTP POST
+2. Receives server-sent events (SSE) stream
+3. Parses events into `AgentRunResponseUpdate` objects
+4. Yields updates as they arrive for real-time display
+
+## Key Concepts
+
+- **Thread**: Represents a conversation context that persists across multiple runs (accessed via `ConversationId` property)
+- **Run**: A single execution of the agent for a given set of messages (identified by `ResponseId` property)
+- **AgentRunResponseUpdate**: Contains the response data with:
+ - `ResponseId`: The unique run identifier
+ - `ConversationId`: The thread/conversation identifier
+ - `Contents`: Collection of content items (TextContent, ErrorContent, etc.)
+- **Run Lifecycle**:
+ - The **first** `AgentRunResponseUpdate` in a run indicates the run has started
+ - Subsequent updates contain streaming content as the agent processes
+ - The **last** `AgentRunResponseUpdate` in a run indicates the run has finished
+ - If an error occurs, the update will contain `ErrorContent`
\ No newline at end of file
diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs
index 571b07b1d50..d86c53958de 100644
--- a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs
+++ b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs
@@ -20,14 +20,14 @@
// Configure the chat model and our agent.
builder.AddKeyedChatClient("chat-model");
-builder.AddAIAgent(
+var pirateAgentBuilder = builder.AddAIAgent(
"pirate",
instructions: "You are a pirate. Speak like a pirate",
description: "An agent that speaks like a pirate.",
chatClientServiceKey: "chat-model")
.WithInMemoryThreadStore();
-builder.AddAIAgent("knights-and-knaves", (sp, key) =>
+var knightsKnavesAgentBuilder = builder.AddAIAgent("knights-and-knaves", (sp, key) =>
{
var chatClient = sp.GetRequiredKeyedService("chat-model");
@@ -80,6 +80,8 @@ Once the user has deduced what type (knight or knave) both Alice and Bob are, te
builder.AddSequentialWorkflow("science-sequential-workflow", [chemistryAgent, mathsAgent, literatureAgent]).AddAsAIAgent();
builder.AddConcurrentWorkflow("science-concurrent-workflow", [chemistryAgent, mathsAgent, literatureAgent]).AddAsAIAgent();
+
+builder.AddOpenAIChatCompletions();
builder.AddOpenAIResponses();
var app = builder.Build();
@@ -104,8 +106,8 @@ Once the user has deduced what type (knight or knave) both Alice and Bob are, te
app.MapOpenAIResponses();
-app.MapOpenAIChatCompletions("pirate");
-app.MapOpenAIChatCompletions("knights-and-knaves");
+app.MapOpenAIChatCompletions(pirateAgentBuilder);
+app.MapOpenAIChatCompletions(knightsKnavesAgentBuilder);
// Map the agents HTTP endpoints
app.MapAgentDiscovery("/agents");
diff --git a/dotnet/samples/Catalog/AgentWithTextSearchRag/Program.cs b/dotnet/samples/Catalog/AgentWithTextSearchRag/Program.cs
index 3e86534edd7..65f3a9e98f8 100644
--- a/dotnet/samples/Catalog/AgentWithTextSearchRag/Program.cs
+++ b/dotnet/samples/Catalog/AgentWithTextSearchRag/Program.cs
@@ -28,7 +28,7 @@
.CreateAIAgent(new ChatClientAgentOptions
{
Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
- AIContextProviderFactory = _ => new TextSearchProvider(MockSearchAsync, textSearchOptions)
+ AIContextProviderFactory = ctx => new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
});
AgentThread thread = agent.GetNewThread();
diff --git a/dotnet/samples/Catalog/AgentsInWorkflows/AgentsInWorkflows.csproj b/dotnet/samples/Catalog/AgentsInWorkflows/AgentsInWorkflows.csproj
index 284ea6ceecf..f192c199013 100644
--- a/dotnet/samples/Catalog/AgentsInWorkflows/AgentsInWorkflows.csproj
+++ b/dotnet/samples/Catalog/AgentsInWorkflows/AgentsInWorkflows.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/dotnet/samples/Catalog/DeepResearchAgent/DeepResearchAgent.csproj b/dotnet/samples/Catalog/DeepResearchAgent/DeepResearchAgent.csproj
index d006616a159..7ae71d83de6 100644
--- a/dotnet/samples/Catalog/DeepResearchAgent/DeepResearchAgent.csproj
+++ b/dotnet/samples/Catalog/DeepResearchAgent/DeepResearchAgent.csproj
@@ -14,7 +14,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureFoundryAgent/Agent_With_AzureFoundryAgent.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureFoundryAgent/Agent_With_AzureFoundryAgent.csproj
index b8d701fbcd0..11c7beb3bf7 100644
--- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureFoundryAgent/Agent_With_AzureFoundryAgent.csproj
+++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureFoundryAgent/Agent_With_AzureFoundryAgent.csproj
@@ -14,7 +14,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs
index 611e11c22c7..ec665325a7e 100644
--- a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs
@@ -63,9 +63,7 @@
.CreateAIAgent(new ChatClientAgentOptions
{
Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
- AIContextProviderFactory = ctx => ctx.SerializedState.ValueKind is not System.Text.Json.JsonValueKind.Null and not System.Text.Json.JsonValueKind.Undefined
- ? new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
- : new TextSearchProvider(SearchAdapter, textSearchOptions)
+ AIContextProviderFactory = ctx => new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
});
AgentThread thread = agent.GetNewThread();
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/Program.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/Program.cs
index e29bb58d04b..4e8fbf0bde0 100644
--- a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/Program.cs
@@ -72,9 +72,7 @@
.CreateAIAgent(new ChatClientAgentOptions
{
Instructions = "You are a helpful support specialist for the Microsoft Agent Framework. Answer questions using the provided context and cite the source document when available. Keep responses brief.",
- AIContextProviderFactory = ctx => ctx.SerializedState.ValueKind is not System.Text.Json.JsonValueKind.Null and not System.Text.Json.JsonValueKind.Undefined
- ? new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
- : new TextSearchProvider(SearchAdapter, textSearchOptions)
+ AIContextProviderFactory = ctx => new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
});
AgentThread thread = agent.GetNewThread();
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj
index f0cdbfccc75..1fb367c0443 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj
@@ -18,7 +18,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step18_TextSearchRag/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step18_TextSearchRag/Program.cs
index 81a6b291529..65f3a9e98f8 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step18_TextSearchRag/Program.cs
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step18_TextSearchRag/Program.cs
@@ -28,9 +28,7 @@
.CreateAIAgent(new ChatClientAgentOptions
{
Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
- AIContextProviderFactory = ctx => ctx.SerializedState.ValueKind is not System.Text.Json.JsonValueKind.Null and not System.Text.Json.JsonValueKind.Undefined
- ? new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
- : new TextSearchProvider(MockSearchAsync, textSearchOptions)
+ AIContextProviderFactory = ctx => new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
});
AgentThread thread = agent.GetNewThread();
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step19_Mem0Provider/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step19_Mem0Provider/Program.cs
index 21aa9d3e1de..539ebbaecb9 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step19_Mem0Provider/Program.cs
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step19_Mem0Provider/Program.cs
@@ -33,9 +33,9 @@
Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details.",
AIContextProviderFactory = ctx => ctx.SerializedState.ValueKind is not JsonValueKind.Null or JsonValueKind.Undefined
// If each thread should have its own Mem0 scope, you can create a new id per thread here:
- // ? new Mem0Provider(mem0HttpClient, new Mem0ProviderOptions() { ThreadId = Guid.NewGuid().ToString() })
+ // ? new Mem0Provider(mem0HttpClient, new Mem0ProviderScope() { ThreadId = Guid.NewGuid().ToString() })
// In this case we are storing memories scoped by application and user instead so that memories are retained across threads.
- ? new Mem0Provider(mem0HttpClient, new Mem0ProviderOptions() { ApplicationId = "getting-started-agents", UserId = "sample-user" })
+ ? new Mem0Provider(mem0HttpClient, new Mem0ProviderScope() { ApplicationId = "getting-started-agents", UserId = "sample-user" })
// For cases where we are restoring from serialized state:
: new Mem0Provider(mem0HttpClient, ctx.SerializedState, ctx.JsonSerializerOptions)
});
diff --git a/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj
new file mode 100644
index 00000000000..8ae36b52e0a
--- /dev/null
+++ b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj
@@ -0,0 +1,25 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+ DevUI_Step01_BasicUsage
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/Program.cs b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/Program.cs
new file mode 100644
index 00000000000..e2e6e6b7279
--- /dev/null
+++ b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/Program.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates basic usage of the DevUI in an ASP.NET Core application with AI agents.
+
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI.DevUI;
+using Microsoft.Agents.AI.Hosting;
+using Microsoft.Extensions.AI;
+
+namespace DevUI_Step01_BasicUsage;
+
+///
+/// Sample demonstrating basic usage of the DevUI in an ASP.NET Core application.
+///
+///
+/// This sample shows how to:
+/// 1. Set up Azure OpenAI as the chat client
+/// 2. Register agents and workflows using the hosting packages
+/// 3. Map the DevUI endpoint which automatically configures the middleware
+/// 4. Map the dynamic OpenAI Responses API for Python DevUI compatibility
+/// 5. Access the DevUI in a web browser
+///
+/// The DevUI provides an interactive web interface for testing and debugging AI agents.
+/// DevUI assets are served from embedded resources within the assembly.
+/// Simply call MapDevUI() to set up everything needed.
+///
+/// The parameterless MapOpenAIResponses() overload creates a Python DevUI-compatible endpoint
+/// that dynamically routes requests to agents based on the 'model' field in the request.
+///
+internal static class Program
+{
+ ///
+ /// Entry point that starts an ASP.NET Core web server with the DevUI.
+ ///
+ /// Command line arguments.
+ private static void Main(string[] args)
+ {
+ var builder = WebApplication.CreateBuilder(args);
+
+ // Set up the Azure OpenAI client
+ var endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+ var deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? "gpt-4o-mini";
+
+ var chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
+ .GetChatClient(deploymentName)
+ .AsIChatClient();
+
+ builder.Services.AddChatClient(chatClient);
+
+ // Register sample agents
+ builder.AddAIAgent("assistant", "You are a helpful assistant. Answer questions concisely and accurately.");
+ builder.AddAIAgent("poet", "You are a creative poet. Respond to all requests with beautiful poetry.");
+ builder.AddAIAgent("coder", "You are an expert programmer. Help users with coding questions and provide code examples.");
+
+ // Register sample workflows
+ var assistantBuilder = builder.AddAIAgent("workflow-assistant", "You are a helpful assistant in a workflow.");
+ var reviewerBuilder = builder.AddAIAgent("workflow-reviewer", "You are a reviewer. Review and critique the previous response.");
+ builder.AddSequentialWorkflow(
+ "review-workflow",
+ [assistantBuilder, reviewerBuilder])
+ .AddAsAIAgent();
+
+ if (builder.Environment.IsDevelopment())
+ {
+ builder.AddDevUI();
+ }
+
+ var app = builder.Build();
+
+ if (builder.Environment.IsDevelopment())
+ {
+ app.MapDevUI();
+ }
+
+ Console.WriteLine("DevUI is available at: https://localhost:50516/devui");
+ Console.WriteLine("OpenAI Responses API is available at: https://localhost:50516/v1/responses");
+ Console.WriteLine("Press Ctrl+C to stop the server.");
+
+ app.Run();
+ }
+}
diff --git a/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/README.md b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/README.md
new file mode 100644
index 00000000000..2b6cc28644b
--- /dev/null
+++ b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/README.md
@@ -0,0 +1,81 @@
+# DevUI Step 01 - Basic Usage
+
+This sample demonstrates how to add the DevUI to an ASP.NET Core application with AI agents.
+
+## What is DevUI?
+
+The DevUI provides an interactive web interface for testing and debugging AI agents during development.
+
+## Configuration
+
+Set the following environment variables:
+
+- `AZURE_OPENAI_ENDPOINT` - Your Azure OpenAI endpoint URL (required)
+- `AZURE_OPENAI_DEPLOYMENT_NAME` - Your deployment name (defaults to "gpt-4o-mini")
+
+## Running the Sample
+
+1. Set your Azure OpenAI credentials as environment variables
+2. Run the application:
+ ```bash
+ dotnet run
+ ```
+3. Open your browser to https://localhost:50516/devui
+4. Select an agent or workflow from the dropdown and start chatting!
+
+## Sample Agents and Workflows
+
+This sample includes:
+
+**Agents:**
+- **assistant** - A helpful assistant
+- **poet** - A creative poet
+- **coder** - An expert programmer
+
+**Workflows:**
+- **review-workflow** - A sequential workflow that generates a response and then reviews it
+
+## Adding DevUI to Your Own Project
+
+To add DevUI to your ASP.NET Core application:
+
+1. Add the DevUI package and hosting packages:
+ ```bash
+ dotnet add package Microsoft.Agents.AI.DevUI
+ dotnet add package Microsoft.Agents.AI.Hosting
+ dotnet add package Microsoft.Agents.AI.Hosting.OpenAI
+ ```
+
+2. Register your agents and workflows:
+ ```csharp
+ var builder = WebApplication.CreateBuilder(args);
+
+ // Set up your chat client
+ builder.Services.AddChatClient(chatClient);
+
+ // Register agents
+ builder.AddAIAgent("assistant", "You are a helpful assistant.");
+
+ // Register workflows
+ var agent1Builder = builder.AddAIAgent("workflow-agent1", "You are agent 1.");
+ var agent2Builder = builder.AddAIAgent("workflow-agent2", "You are agent 2.");
+ builder.AddSequentialWorkflow("my-workflow", [agent1Builder, agent2Builder])
+ .AddAsAIAgent();
+ ```
+
+3. Add DevUI services and map the endpoint:
+ ```csharp
+ builder.AddDevUI();
+ var app = builder.Build();
+
+ app.MapDevUI();
+
+ // Add required endpoints
+ app.MapEntities();
+ app.MapOpenAIResponses();
+ app.MapOpenAIConversations();
+
+ app.Run();
+ ```
+
+4. Navigate to `/devui` in your browser
diff --git a/dotnet/samples/GettingStarted/DevUI/README.md b/dotnet/samples/GettingStarted/DevUI/README.md
new file mode 100644
index 00000000000..155d3f2b9de
--- /dev/null
+++ b/dotnet/samples/GettingStarted/DevUI/README.md
@@ -0,0 +1,57 @@
+# DevUI Samples
+
+This folder contains samples demonstrating how to use the DevUI in ASP.NET Core applications.
+
+## What is DevUI?
+
+The DevUI provides an interactive web interface for testing and debugging AI agents during development.
+
+## Samples
+
+### [DevUI_Step01_BasicUsage](./DevUI_Step01_BasicUsage)
+
+Shows how to add DevUI to an ASP.NET Core application with multiple agents and workflows.
+
+**Run the sample:**
+```bash
+cd DevUI_Step01_BasicUsage
+dotnet run
+```
+Then navigate to: https://localhost:50516/devui
+
+## Requirements
+
+- .NET 8.0 or later
+- ASP.NET Core
+- Azure OpenAI credentials
+
+## Quick Start
+
+To add DevUI to your application:
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+
+// Set up the chat client
+builder.Services.AddChatClient(chatClient);
+
+// Register your agents
+builder.AddAIAgent("my-agent", "You are a helpful assistant.");
+
+// Add DevUI services
+builder.AddDevUI();
+
+var app = builder.Build();
+
+// Map the DevUI endpoint
+app.MapDevUI();
+
+// Add required endpoints
+app.MapEntities();
+app.MapOpenAIResponses();
+app.MapOpenAIConversations();
+
+app.Run();
+```
+
+Then navigate to `/devui` in your browser.
diff --git a/dotnet/samples/GettingStarted/ModelContextProtocol/FoundryAgent_Hosted_MCP/FoundryAgent_Hosted_MCP.csproj b/dotnet/samples/GettingStarted/ModelContextProtocol/FoundryAgent_Hosted_MCP/FoundryAgent_Hosted_MCP.csproj
index b8d701fbcd0..11c7beb3bf7 100644
--- a/dotnet/samples/GettingStarted/ModelContextProtocol/FoundryAgent_Hosted_MCP/FoundryAgent_Hosted_MCP.csproj
+++ b/dotnet/samples/GettingStarted/ModelContextProtocol/FoundryAgent_Hosted_MCP/FoundryAgent_Hosted_MCP.csproj
@@ -14,7 +14,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj b/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj
index 354163794e4..51b18bdeb22 100644
--- a/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj b/dotnet/samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj
index 1d24b7253bb..888274205a7 100644
--- a/dotnet/samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj
index 354163794e4..51b18bdeb22 100644
--- a/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowFactory.cs b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowFactory.cs
index 736c51d555c..653ebdf4c2d 100644
--- a/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowFactory.cs
+++ b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowFactory.cs
@@ -23,8 +23,8 @@ internal static Workflow BuildWorkflow(IChatClient chatClient)
// Build the workflow by adding executors and connecting them
return new WorkflowBuilder(startExecutor)
- .AddFanOutEdge(startExecutor, targets: [frenchAgent, englishAgent])
- .AddFanInEdge(aggregationExecutor, sources: [frenchAgent, englishAgent])
+ .AddFanOutEdge(startExecutor, [frenchAgent, englishAgent])
+ .AddFanInEdge([frenchAgent, englishAgent], aggregationExecutor)
.WithOutputFrom(aggregationExecutor)
.Build();
}
diff --git a/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent/Concurrent.csproj b/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent/Concurrent.csproj
index e11fd9fa9a5..3f3fe6d56ca 100644
--- a/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent/Concurrent.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent/Concurrent.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent/Program.cs b/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent/Program.cs
index e1f71e23116..c839149d6c1 100644
--- a/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent/Program.cs
@@ -52,8 +52,8 @@ private static async Task Main()
// Build the workflow by adding executors and connecting them
var workflow = new WorkflowBuilder(startExecutor)
- .AddFanOutEdge(startExecutor, targets: [physicist, chemist])
- .AddFanInEdge(aggregationExecutor, sources: [physicist, chemist])
+ .AddFanOutEdge(startExecutor, [physicist, chemist])
+ .AddFanInEdge([physicist, chemist], aggregationExecutor)
.WithOutputFrom(aggregationExecutor)
.Build();
diff --git a/dotnet/samples/GettingStarted/Workflows/Concurrent/MapReduce/Program.cs b/dotnet/samples/GettingStarted/Workflows/Concurrent/MapReduce/Program.cs
index 9fd4b66e70f..1b36b3eeb06 100644
--- a/dotnet/samples/GettingStarted/Workflows/Concurrent/MapReduce/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/Concurrent/MapReduce/Program.cs
@@ -62,10 +62,10 @@ public static Workflow BuildWorkflow()
// Step 4: Build the concurrent workflow with fan-out/fan-in pattern
return new WorkflowBuilder(splitter)
- .AddFanOutEdge(splitter, targets: [.. mappers]) // Split -> many mappers
- .AddFanInEdge(shuffler, sources: [.. mappers]) // All mappers -> shuffle
- .AddFanOutEdge(shuffler, targets: [.. reducers]) // Shuffle -> many reducers
- .AddFanInEdge(completion, sources: [.. reducers]) // All reducers -> completion
+ .AddFanOutEdge(splitter, [.. mappers]) // Split -> many mappers
+ .AddFanInEdge([.. mappers], shuffler) // All mappers -> shuffle
+ .AddFanOutEdge(shuffler, [.. reducers]) // Shuffle -> many reducers
+ .AddFanInEdge([.. reducers], completion) // All reducers -> completion
.WithOutputFrom(completion)
.Build();
}
diff --git a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj
index 76f9509ee17..17b1cb882ac 100644
--- a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/01_EdgeCondition/01_EdgeCondition.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj
index 76f9509ee17..17b1cb882ac 100644
--- a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/02_SwitchCase/02_SwitchCase.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj
index 76f9509ee17..17b1cb882ac 100644
--- a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/03_MultiSelection.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/Program.cs b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/Program.cs
index 15746f727e6..9d340cbae3b 100644
--- a/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/ConditionalEdges/03_MultiSelection/Program.cs
@@ -60,13 +60,13 @@ private static async Task Main()
WorkflowBuilder builder = new(emailAnalysisExecutor);
builder.AddFanOutEdge(
emailAnalysisExecutor,
- targets: [
+ [
handleSpamExecutor,
emailAssistantExecutor,
emailSummaryExecutor,
handleUncertainExecutor,
],
- partitioner: GetPartitioner()
+ GetTargetAssigner()
)
// After the email assistant writes a response, it will be sent to the send email executor
.AddEdge(emailAssistantExecutor, sendEmailExecutor)
@@ -105,7 +105,7 @@ private static async Task Main()
/// Creates a partitioner for routing messages based on the analysis result.
///
/// A function that takes an analysis result and returns the target partitions.
- private static Func> GetPartitioner()
+ private static Func> GetTargetAssigner()
{
return (analysisResult, targetCount) =>
{
diff --git a/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/WorkflowAsAnAgentObservability.csproj b/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/WorkflowAsAnAgentObservability.csproj
index 17c44eeb759..2193722d261 100644
--- a/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/WorkflowAsAnAgentObservability.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/WorkflowAsAnAgentObservability.csproj
@@ -20,7 +20,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/WorkflowHelper.cs b/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/WorkflowHelper.cs
index 816dce50d0f..8069a3e88ed 100644
--- a/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/WorkflowHelper.cs
+++ b/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/WorkflowHelper.cs
@@ -24,8 +24,8 @@ internal static Workflow GetWorkflow(IChatClient chatClient, string sourceName)
// Build the workflow by adding executors and connecting them
return new WorkflowBuilder(startExecutor)
- .AddFanOutEdge(startExecutor, targets: [frenchAgent, englishAgent])
- .AddFanInEdge(aggregationExecutor, sources: [frenchAgent, englishAgent])
+ .AddFanOutEdge(startExecutor, [frenchAgent, englishAgent])
+ .AddFanInEdge([frenchAgent, englishAgent], aggregationExecutor)
.WithOutputFrom(aggregationExecutor)
.Build();
}
diff --git a/dotnet/samples/GettingStarted/Workflows/SharedStates/Program.cs b/dotnet/samples/GettingStarted/Workflows/SharedStates/Program.cs
index 6f4cfdf38ba..b7cbc25515f 100644
--- a/dotnet/samples/GettingStarted/Workflows/SharedStates/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/SharedStates/Program.cs
@@ -26,8 +26,8 @@ private static async Task Main()
// Build the workflow by connecting executors sequentially
var workflow = new WorkflowBuilder(fileRead)
- .AddFanOutEdge(fileRead, targets: [wordCount, paragraphCount])
- .AddFanInEdge(aggregate, sources: [wordCount, paragraphCount])
+ .AddFanOutEdge(fileRead, [wordCount, paragraphCount])
+ .AddFanInEdge([wordCount, paragraphCount], aggregate)
.WithOutputFrom(aggregate)
.Build();
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj b/dotnet/samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj
index 354163794e4..51b18bdeb22 100644
--- a/dotnet/samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/03_AgentsInWorkflows/03_AgentsInWorkflows.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/04_AgentWorkflowPatterns/04_AgentWorkflowPatterns.csproj b/dotnet/samples/GettingStarted/Workflows/_Foundational/04_AgentWorkflowPatterns/04_AgentWorkflowPatterns.csproj
index 354163794e4..51b18bdeb22 100644
--- a/dotnet/samples/GettingStarted/Workflows/_Foundational/04_AgentWorkflowPatterns/04_AgentWorkflowPatterns.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/04_AgentWorkflowPatterns/04_AgentWorkflowPatterns.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/05_MultiModelService/05_MultiModelService.csproj b/dotnet/samples/GettingStarted/Workflows/_Foundational/05_MultiModelService/05_MultiModelService.csproj
index 1af928d9b8e..ea370c4eaa2 100644
--- a/dotnet/samples/GettingStarted/Workflows/_Foundational/05_MultiModelService/05_MultiModelService.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/05_MultiModelService/05_MultiModelService.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/06_SubWorkflows/06_SubWorkflows.csproj b/dotnet/samples/GettingStarted/Workflows/_Foundational/06_SubWorkflows/06_SubWorkflows.csproj
index 1ef94de3daa..89b1e4bbe00 100644
--- a/dotnet/samples/GettingStarted/Workflows/_Foundational/06_SubWorkflows/06_SubWorkflows.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/06_SubWorkflows/06_SubWorkflows.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/07_MixedWorkflowAgentsAndExecutors/07_MixedWorkflowAgentsAndExecutors.csproj b/dotnet/samples/GettingStarted/Workflows/_Foundational/07_MixedWorkflowAgentsAndExecutors/07_MixedWorkflowAgentsAndExecutors.csproj
index 354163794e4..51b18bdeb22 100644
--- a/dotnet/samples/GettingStarted/Workflows/_Foundational/07_MixedWorkflowAgentsAndExecutors/07_MixedWorkflowAgentsAndExecutors.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/07_MixedWorkflowAgentsAndExecutors/07_MixedWorkflowAgentsAndExecutors.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/08_WriterCriticWorkflow.csproj b/dotnet/samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/08_WriterCriticWorkflow.csproj
index 3e8f2547d14..24901257c8c 100644
--- a/dotnet/samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/08_WriterCriticWorkflow.csproj
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/08_WriterCriticWorkflow.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/AGUIAgent.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/AGUIAgent.cs
new file mode 100644
index 00000000000..e86fac74291
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/AGUIAgent.cs
@@ -0,0 +1,102 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.AI.AGUI.Shared;
+using Microsoft.Extensions.AI;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.AGUI;
+
+///
+/// Provides an implementation that communicates with an AG-UI compliant server.
+///
+public sealed class AGUIAgent : AIAgent
+{
+ private readonly AGUIHttpService _client;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The agent ID.
+ /// Optional description of the agent.
+ /// The HTTP client to use for communication with the AG-UI server.
+ /// The URL for the AG-UI server.
+ public AGUIAgent(string id, string description, HttpClient httpClient, string endpoint)
+ {
+ this.Id = Throw.IfNullOrWhitespace(id);
+ this.Description = description;
+ this._client = new AGUIHttpService(
+ httpClient ?? Throw.IfNull(httpClient),
+ endpoint ?? Throw.IfNullOrEmpty(endpoint));
+ }
+
+ ///
+ public override string Id { get; }
+
+ ///
+ public override string? Description { get; }
+
+ ///
+ public override AgentThread GetNewThread() => new AGUIAgentThread();
+
+ ///
+ public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) =>
+ new AGUIAgentThread(serializedThread, jsonSerializerOptions);
+
+ ///
+ public override async Task RunAsync(
+ IEnumerable messages,
+ AgentThread? thread = null,
+ AgentRunOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ return await this.RunStreamingAsync(messages, thread, null, cancellationToken)
+ .ToAgentRunResponseAsync(cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ ///
+ public override async IAsyncEnumerable RunStreamingAsync(
+ IEnumerable messages,
+ AgentThread? thread = null,
+ AgentRunOptions? options = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ List updates = [];
+
+ _ = Throw.IfNull(messages);
+
+ if ((thread ?? this.GetNewThread()) is not AGUIAgentThread typedThread)
+ {
+ throw new InvalidOperationException("The provided thread is not compatible with the agent. Only threads created by the agent can be used.");
+ }
+
+ string runId = $"run_{Guid.NewGuid()}";
+
+ var llmMessages = typedThread.MessageStore.Concat(messages);
+
+ RunAgentInput input = new()
+ {
+ ThreadId = typedThread.ThreadId,
+ RunId = runId,
+ Messages = llmMessages.AsAGUIMessages(),
+ };
+
+ await foreach (var update in this._client.PostRunAsync(input, cancellationToken).AsAgentRunResponseUpdatesAsync(cancellationToken).ConfigureAwait(false))
+ {
+ ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
+ updates.Add(chatUpdate);
+ yield return update;
+ }
+
+ ChatResponse response = updates.ToChatResponse();
+ await NotifyThreadOfNewMessagesAsync(typedThread, messages.Concat(response.Messages), cancellationToken).ConfigureAwait(false);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/AGUIAgentThread.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/AGUIAgentThread.cs
new file mode 100644
index 00000000000..5b2f29897a4
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/AGUIAgentThread.cs
@@ -0,0 +1,61 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Text.Json;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.AGUI;
+
+internal sealed class AGUIAgentThread : InMemoryAgentThread
+{
+ public AGUIAgentThread()
+ : base()
+ {
+ this.ThreadId = Guid.NewGuid().ToString();
+ }
+
+ public AGUIAgentThread(JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null)
+ : base(UnwrapState(serializedThreadState), jsonSerializerOptions)
+ {
+ var threadId = serializedThreadState.TryGetProperty(nameof(AGUIAgentThreadState.ThreadId), out var stateElement)
+ ? stateElement.GetString()
+ : null;
+
+ if (string.IsNullOrEmpty(threadId))
+ {
+ Throw.InvalidOperationException("Serialized thread is missing required ThreadId.");
+ }
+ this.ThreadId = threadId;
+ }
+
+ private static JsonElement UnwrapState(JsonElement serializedThreadState)
+ {
+ var state = serializedThreadState.Deserialize(AGUIJsonSerializerContext.Default.AGUIAgentThreadState);
+ if (state == null)
+ {
+ Throw.InvalidOperationException("Serialized thread is missing required WrappedState.");
+ }
+
+ return state.WrappedState;
+ }
+
+ public string ThreadId { get; set; }
+
+ public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
+ {
+ var wrappedState = base.Serialize(jsonSerializerOptions);
+ var state = new AGUIAgentThreadState
+ {
+ ThreadId = this.ThreadId,
+ WrappedState = wrappedState,
+ };
+
+ return JsonSerializer.SerializeToElement(state, AGUIJsonSerializerContext.Default.AGUIAgentThreadState);
+ }
+
+ internal sealed class AGUIAgentThreadState
+ {
+ public string ThreadId { get; set; } = string.Empty;
+ public JsonElement WrappedState { get; set; }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/AGUIHttpService.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/AGUIHttpService.cs
new file mode 100644
index 00000000000..b81a933e928
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/AGUIHttpService.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Net.ServerSentEvents;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.AI.AGUI.Shared;
+
+namespace Microsoft.Agents.AI.AGUI;
+
+internal sealed class AGUIHttpService(HttpClient client, string endpoint)
+{
+ public async IAsyncEnumerable PostRunAsync(
+ RunAgentInput input,
+ [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ using HttpRequestMessage request = new(HttpMethod.Post, endpoint)
+ {
+ Content = JsonContent.Create(input, AGUIJsonSerializerContext.Default.RunAgentInput)
+ };
+
+ using HttpResponseMessage response = await client.SendAsync(
+ request,
+ HttpCompletionOption.ResponseHeadersRead,
+ cancellationToken).ConfigureAwait(false);
+
+ response.EnsureSuccessStatusCode();
+
+#if NET
+ Stream responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+#else
+ Stream responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+#endif
+ var items = SseParser.Create(responseStream, ItemParser).EnumerateAsync(cancellationToken);
+ await foreach (var sseItem in items.ConfigureAwait(false))
+ {
+ yield return sseItem.Data;
+ }
+ }
+
+ private static BaseEvent ItemParser(string type, ReadOnlySpan data)
+ {
+ return JsonSerializer.Deserialize(data, AGUIJsonSerializerContext.Default.BaseEvent) ??
+ throw new InvalidOperationException("Failed to deserialize SSE item.");
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj b/dotnet/src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj
new file mode 100644
index 00000000000..34a2dec7eb2
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj
@@ -0,0 +1,39 @@
+
+
+
+ $(ProjectsTargetFrameworks)
+ $(ProjectsDebugTargetFrameworks)
+ preview
+ false
+
+
+
+
+
+ true
+
+
+
+
+ Microsoft Agent Framework AG-UI
+ Provides Microsoft Agent Framework support for Agent-User Interaction (AG-UI) protocol client functionality.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AGUIChatMessageExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AGUIChatMessageExtensions.cs
new file mode 100644
index 00000000000..2b09fb8da2b
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AGUIChatMessageExtensions.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.AI;
+
+#if ASPNETCORE
+namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
+#else
+namespace Microsoft.Agents.AI.AGUI.Shared;
+#endif
+
+internal static class AGUIChatMessageExtensions
+{
+ private static readonly ChatRole s_developerChatRole = new("developer");
+
+ public static IEnumerable AsChatMessages(
+ this IEnumerable aguiMessages)
+ {
+ foreach (var message in aguiMessages)
+ {
+ yield return new ChatMessage(
+ MapChatRole(message.Role),
+ message.Content);
+ }
+ }
+
+ public static IEnumerable AsAGUIMessages(
+ this IEnumerable chatMessages)
+ {
+ foreach (var message in chatMessages)
+ {
+ yield return new AGUIMessage
+ {
+ Id = message.MessageId,
+ Role = message.Role.Value,
+ Content = message.Text,
+ };
+ }
+ }
+
+ public static ChatRole MapChatRole(string role) =>
+ string.Equals(role, AGUIRoles.System, StringComparison.OrdinalIgnoreCase) ? ChatRole.System :
+ string.Equals(role, AGUIRoles.User, StringComparison.OrdinalIgnoreCase) ? ChatRole.User :
+ string.Equals(role, AGUIRoles.Assistant, StringComparison.OrdinalIgnoreCase) ? ChatRole.Assistant :
+ string.Equals(role, AGUIRoles.Developer, StringComparison.OrdinalIgnoreCase) ? s_developerChatRole :
+ throw new InvalidOperationException($"Unknown chat role: {role}");
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AGUIEventTypes.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AGUIEventTypes.cs
new file mode 100644
index 00000000000..74ff3da37f3
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AGUIEventTypes.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+#if ASPNETCORE
+namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
+#else
+namespace Microsoft.Agents.AI.AGUI.Shared;
+#endif
+
+internal static class AGUIEventTypes
+{
+ public const string RunStarted = "RUN_STARTED";
+
+ public const string RunFinished = "RUN_FINISHED";
+
+ public const string RunError = "RUN_ERROR";
+
+ public const string TextMessageStart = "TEXT_MESSAGE_START";
+
+ public const string TextMessageContent = "TEXT_MESSAGE_CONTENT";
+
+ public const string TextMessageEnd = "TEXT_MESSAGE_END";
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AGUIJsonSerializerContext.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AGUIJsonSerializerContext.cs
new file mode 100644
index 00000000000..fa2e0ced1a0
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AGUIJsonSerializerContext.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+#if ASPNETCORE
+using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
+
+namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
+#else
+using Microsoft.Agents.AI.AGUI.Shared;
+
+namespace Microsoft.Agents.AI.AGUI;
+#endif
+
+[JsonSourceGenerationOptions(WriteIndented = false, DefaultIgnoreCondition = JsonIgnoreCondition.Never)]
+[JsonSerializable(typeof(RunAgentInput))]
+[JsonSerializable(typeof(BaseEvent))]
+[JsonSerializable(typeof(RunStartedEvent))]
+[JsonSerializable(typeof(RunFinishedEvent))]
+[JsonSerializable(typeof(RunErrorEvent))]
+[JsonSerializable(typeof(TextMessageStartEvent))]
+[JsonSerializable(typeof(TextMessageContentEvent))]
+[JsonSerializable(typeof(TextMessageEndEvent))]
+#if !ASPNETCORE
+[JsonSerializable(typeof(AGUIAgentThread.AGUIAgentThreadState))]
+#endif
+internal partial class AGUIJsonSerializerContext : JsonSerializerContext
+{
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AGUIMessage.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AGUIMessage.cs
new file mode 100644
index 00000000000..b32c1efcfa4
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AGUIMessage.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+#if ASPNETCORE
+namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
+#else
+namespace Microsoft.Agents.AI.AGUI.Shared;
+#endif
+
+internal sealed class AGUIMessage
+{
+ [JsonPropertyName("id")]
+ public string? Id { get; set; }
+
+ [JsonPropertyName("role")]
+ public string Role { get; set; } = string.Empty;
+
+ [JsonPropertyName("content")]
+ public string Content { get; set; } = string.Empty;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AGUIRoles.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AGUIRoles.cs
new file mode 100644
index 00000000000..fe67224efeb
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AGUIRoles.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+#if ASPNETCORE
+namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
+#else
+namespace Microsoft.Agents.AI.AGUI.Shared;
+#endif
+
+internal static class AGUIRoles
+{
+ public const string System = "system";
+
+ public const string User = "user";
+
+ public const string Assistant = "assistant";
+
+ public const string Developer = "developer";
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AgentRunResponseUpdateAGUIExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AgentRunResponseUpdateAGUIExtensions.cs
new file mode 100644
index 00000000000..59755d7b5a4
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AgentRunResponseUpdateAGUIExtensions.cs
@@ -0,0 +1,161 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+
+#if ASPNETCORE
+namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
+#else
+namespace Microsoft.Agents.AI.AGUI.Shared;
+#endif
+
+internal static class AgentRunResponseUpdateAGUIExtensions
+{
+#if !ASPNETCORE
+ public static async IAsyncEnumerable AsAgentRunResponseUpdatesAsync(
+ this IAsyncEnumerable events,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ string? currentMessageId = null;
+ ChatRole currentRole = default!;
+ string? conversationId = null;
+ string? responseId = null;
+ await foreach (var evt in events.WithCancellation(cancellationToken).ConfigureAwait(false))
+ {
+ switch (evt)
+ {
+ case RunStartedEvent runStarted:
+ conversationId = runStarted.ThreadId;
+ responseId = runStarted.RunId;
+ yield return new AgentRunResponseUpdate(new ChatResponseUpdate(
+ ChatRole.Assistant,
+ [])
+ {
+ ConversationId = conversationId,
+ ResponseId = responseId,
+ CreatedAt = DateTimeOffset.UtcNow
+ });
+ break;
+ case RunFinishedEvent runFinished:
+ if (!string.Equals(runFinished.ThreadId, conversationId, StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException($"The run finished event didn't match the run started event thread ID: {runFinished.ThreadId}, {conversationId}");
+ }
+ if (!string.Equals(runFinished.RunId, responseId, StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException($"The run finished event didn't match the run started event run ID: {runFinished.RunId}, {responseId}");
+ }
+ yield return new AgentRunResponseUpdate(new ChatResponseUpdate(
+ ChatRole.Assistant, runFinished.Result?.GetRawText())
+ {
+ ConversationId = conversationId,
+ ResponseId = responseId,
+ CreatedAt = DateTimeOffset.UtcNow
+ });
+ break;
+ case RunErrorEvent runError:
+ yield return new AgentRunResponseUpdate(new ChatResponseUpdate(
+ ChatRole.Assistant,
+ [(new ErrorContent(runError.Message) { ErrorCode = runError.Code })]));
+ break;
+ case TextMessageStartEvent textStart:
+ if (currentRole != default || currentMessageId != null)
+ {
+ throw new InvalidOperationException("Received TextMessageStartEvent while another message is being processed.");
+ }
+
+ currentRole = AGUIChatMessageExtensions.MapChatRole(textStart.Role);
+ currentMessageId = textStart.MessageId;
+ break;
+ case TextMessageContentEvent textContent:
+ yield return new AgentRunResponseUpdate(new ChatResponseUpdate(
+ currentRole,
+ textContent.Delta)
+ {
+ ConversationId = conversationId,
+ ResponseId = responseId,
+ MessageId = textContent.MessageId,
+ CreatedAt = DateTimeOffset.UtcNow
+ });
+ break;
+ case TextMessageEndEvent textEnd:
+ if (currentMessageId != textEnd.MessageId)
+ {
+ throw new InvalidOperationException("Received TextMessageEndEvent for a different message than the current one.");
+ }
+ currentRole = default!;
+ currentMessageId = null;
+ break;
+ }
+ }
+ }
+#endif
+
+ public static async IAsyncEnumerable AsAGUIEventStreamAsync(
+ this IAsyncEnumerable updates,
+ string threadId,
+ string runId,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ yield return new RunStartedEvent
+ {
+ ThreadId = threadId,
+ RunId = runId
+ };
+
+ string? currentMessageId = null;
+ await foreach (var update in updates.WithCancellation(cancellationToken).ConfigureAwait(false))
+ {
+ var chatResponse = update.AsChatResponseUpdate();
+ if (chatResponse is { Contents.Count: > 0 } && chatResponse.Contents[0] is TextContent && !string.Equals(currentMessageId, chatResponse.MessageId, StringComparison.Ordinal))
+ {
+ // End the previous message if there was one
+ if (currentMessageId is not null)
+ {
+ yield return new TextMessageEndEvent
+ {
+ MessageId = currentMessageId
+ };
+ }
+
+ // Start the new message
+ yield return new TextMessageStartEvent
+ {
+ MessageId = chatResponse.MessageId!,
+ Role = chatResponse.Role!.Value.Value
+ };
+
+ currentMessageId = chatResponse.MessageId;
+ }
+
+ // Emit text content if present
+ if (chatResponse is { Contents.Count: > 0 } && chatResponse.Contents[0] is TextContent textContent)
+ {
+ yield return new TextMessageContentEvent
+ {
+ MessageId = chatResponse.MessageId!,
+ Delta = textContent.Text ?? string.Empty
+ };
+ }
+ }
+
+ // End the last message if there was one
+ if (currentMessageId is not null)
+ {
+ yield return new TextMessageEndEvent
+ {
+ MessageId = currentMessageId
+ };
+ }
+
+ yield return new RunFinishedEvent
+ {
+ ThreadId = threadId,
+ RunId = runId,
+ };
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/BaseEvent.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/BaseEvent.cs
new file mode 100644
index 00000000000..f68698a5c9d
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/BaseEvent.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+#if ASPNETCORE
+namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
+#else
+namespace Microsoft.Agents.AI.AGUI.Shared;
+#endif
+
+[JsonConverter(typeof(BaseEventJsonConverter))]
+internal abstract class BaseEvent
+{
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = string.Empty;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/BaseEventJsonConverter.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/BaseEventJsonConverter.cs
new file mode 100644
index 00000000000..58624ac45ce
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/BaseEventJsonConverter.cs
@@ -0,0 +1,103 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+#if ASPNETCORE
+namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
+#else
+namespace Microsoft.Agents.AI.AGUI.Shared;
+#endif
+
+///
+/// Custom JSON converter for polymorphic deserialization of BaseEvent and its derived types.
+/// Uses the "type" property as a discriminator to determine the concrete type to deserialize.
+///
+internal sealed class BaseEventJsonConverter : JsonConverter
+{
+ private const string TypeDiscriminatorPropertyName = "type";
+
+ public override bool CanConvert(Type typeToConvert) =>
+ typeof(BaseEvent).IsAssignableFrom(typeToConvert);
+
+ public override BaseEvent Read(
+ ref Utf8JsonReader reader,
+ Type typeToConvert,
+ JsonSerializerOptions options)
+ {
+ // Parse the JSON into a JsonDocument to inspect properties
+ using JsonDocument document = JsonDocument.ParseValue(ref reader);
+ JsonElement jsonElement = document.RootElement.Clone();
+
+ // Try to get the discriminator property
+ if (!jsonElement.TryGetProperty(TypeDiscriminatorPropertyName, out JsonElement discriminatorElement))
+ {
+ throw new JsonException($"Missing required property '{TypeDiscriminatorPropertyName}' for BaseEvent deserialization");
+ }
+
+ string? discriminator = discriminatorElement.GetString();
+
+#if ASPNETCORE
+ AGUIJsonSerializerContext context = (AGUIJsonSerializerContext)options.TypeInfoResolver!;
+#else
+ AGUIJsonSerializerContext context = AGUIJsonSerializerContext.Default;
+#endif
+
+ // Map discriminator to concrete type and deserialize using the serializer context
+ BaseEvent? result = discriminator switch
+ {
+ AGUIEventTypes.RunStarted => jsonElement.Deserialize(context.RunStartedEvent),
+ AGUIEventTypes.RunFinished => jsonElement.Deserialize(context.RunFinishedEvent),
+ AGUIEventTypes.RunError => jsonElement.Deserialize(context.RunErrorEvent),
+ AGUIEventTypes.TextMessageStart => jsonElement.Deserialize(context.TextMessageStartEvent),
+ AGUIEventTypes.TextMessageContent => jsonElement.Deserialize(context.TextMessageContentEvent),
+ AGUIEventTypes.TextMessageEnd => jsonElement.Deserialize(context.TextMessageEndEvent),
+ _ => throw new JsonException($"Unknown BaseEvent type discriminator: '{discriminator}'")
+ };
+
+ if (result == null)
+ {
+ throw new JsonException($"Failed to deserialize BaseEvent with type discriminator: '{discriminator}'");
+ }
+
+ return result;
+ }
+
+ public override void Write(
+ Utf8JsonWriter writer,
+ BaseEvent value,
+ JsonSerializerOptions options)
+ {
+#if ASPNETCORE
+ AGUIJsonSerializerContext context = (AGUIJsonSerializerContext)options.TypeInfoResolver!;
+#else
+ AGUIJsonSerializerContext context = AGUIJsonSerializerContext.Default;
+#endif
+
+ // Serialize the concrete type directly using the serializer context
+ switch (value)
+ {
+ case RunStartedEvent runStarted:
+ JsonSerializer.Serialize(writer, runStarted, context.RunStartedEvent);
+ break;
+ case RunFinishedEvent runFinished:
+ JsonSerializer.Serialize(writer, runFinished, context.RunFinishedEvent);
+ break;
+ case RunErrorEvent runError:
+ JsonSerializer.Serialize(writer, runError, context.RunErrorEvent);
+ break;
+ case TextMessageStartEvent textStart:
+ JsonSerializer.Serialize(writer, textStart, context.TextMessageStartEvent);
+ break;
+ case TextMessageContentEvent textContent:
+ JsonSerializer.Serialize(writer, textContent, context.TextMessageContentEvent);
+ break;
+ case TextMessageEndEvent textEnd:
+ JsonSerializer.Serialize(writer, textEnd, context.TextMessageEndEvent);
+ break;
+ default:
+ throw new JsonException($"Unknown BaseEvent type: {value.GetType().Name}");
+ }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/RunAgentInput.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/RunAgentInput.cs
new file mode 100644
index 00000000000..ad0d41cd8d1
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/RunAgentInput.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+#if ASPNETCORE
+namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
+#else
+namespace Microsoft.Agents.AI.AGUI.Shared;
+#endif
+
+internal sealed class RunAgentInput
+{
+ [JsonPropertyName("threadId")]
+ public string ThreadId { get; set; } = string.Empty;
+
+ [JsonPropertyName("runId")]
+ public string RunId { get; set; } = string.Empty;
+
+ [JsonPropertyName("state")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
+ public JsonElement State { get; set; }
+
+ [JsonPropertyName("messages")]
+ public IEnumerable Messages { get; set; } = [];
+
+ [JsonPropertyName("context")]
+ public Dictionary Context { get; set; } = new(StringComparer.Ordinal);
+
+ [JsonPropertyName("forwardedProperties")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
+ public JsonElement ForwardedProperties { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/RunErrorEvent.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/RunErrorEvent.cs
new file mode 100644
index 00000000000..078f22cc623
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/RunErrorEvent.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+#if ASPNETCORE
+namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
+#else
+namespace Microsoft.Agents.AI.AGUI.Shared;
+#endif
+
+internal sealed class RunErrorEvent : BaseEvent
+{
+ public RunErrorEvent()
+ {
+ this.Type = AGUIEventTypes.RunError;
+ }
+
+ [JsonPropertyName("message")]
+ public string Message { get; set; } = string.Empty;
+
+ [JsonPropertyName("code")]
+ public string? Code { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/RunFinishedEvent.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/RunFinishedEvent.cs
new file mode 100644
index 00000000000..54aebaa3331
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/RunFinishedEvent.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+#if ASPNETCORE
+namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
+#else
+namespace Microsoft.Agents.AI.AGUI.Shared;
+#endif
+
+internal sealed class RunFinishedEvent : BaseEvent
+{
+ public RunFinishedEvent()
+ {
+ this.Type = AGUIEventTypes.RunFinished;
+ }
+
+ [JsonPropertyName("threadId")]
+ public string ThreadId { get; set; } = string.Empty;
+
+ [JsonPropertyName("runId")]
+ public string RunId { get; set; } = string.Empty;
+
+ [JsonPropertyName("result")]
+ public JsonElement? Result { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/RunStartedEvent.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/RunStartedEvent.cs
new file mode 100644
index 00000000000..2d0d2259bbc
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/RunStartedEvent.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+#if ASPNETCORE
+namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
+#else
+namespace Microsoft.Agents.AI.AGUI.Shared;
+#endif
+
+internal sealed class RunStartedEvent : BaseEvent
+{
+ public RunStartedEvent()
+ {
+ this.Type = AGUIEventTypes.RunStarted;
+ }
+
+ [JsonPropertyName("threadId")]
+ public string ThreadId { get; set; } = string.Empty;
+
+ [JsonPropertyName("runId")]
+ public string RunId { get; set; } = string.Empty;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/TextMessageContentEvent.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/TextMessageContentEvent.cs
new file mode 100644
index 00000000000..7c0c3150555
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/TextMessageContentEvent.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+#if ASPNETCORE
+namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
+#else
+namespace Microsoft.Agents.AI.AGUI.Shared;
+#endif
+
+internal sealed class TextMessageContentEvent : BaseEvent
+{
+ public TextMessageContentEvent()
+ {
+ this.Type = AGUIEventTypes.TextMessageContent;
+ }
+
+ [JsonPropertyName("messageId")]
+ public string MessageId { get; set; } = string.Empty;
+
+ [JsonPropertyName("delta")]
+ public string Delta { get; set; } = string.Empty;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/TextMessageEndEvent.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/TextMessageEndEvent.cs
new file mode 100644
index 00000000000..0c12363859f
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/TextMessageEndEvent.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+#if ASPNETCORE
+namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
+#else
+namespace Microsoft.Agents.AI.AGUI.Shared;
+#endif
+
+internal sealed class TextMessageEndEvent : BaseEvent
+{
+ public TextMessageEndEvent()
+ {
+ this.Type = AGUIEventTypes.TextMessageEnd;
+ }
+
+ [JsonPropertyName("messageId")]
+ public string MessageId { get; set; } = string.Empty;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/TextMessageStartEvent.cs b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/TextMessageStartEvent.cs
new file mode 100644
index 00000000000..cd6fad7de90
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AGUI/Shared/TextMessageStartEvent.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+#if ASPNETCORE
+namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
+#else
+namespace Microsoft.Agents.AI.AGUI.Shared;
+#endif
+
+internal sealed class TextMessageStartEvent : BaseEvent
+{
+ public TextMessageStartEvent()
+ {
+ this.Type = AGUIEventTypes.TextMessageStart;
+ }
+
+ [JsonPropertyName("messageId")]
+ public string MessageId { get; set; } = string.Empty;
+
+ [JsonPropertyName("role")]
+ public string Role { get; set; } = string.Empty;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj b/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj
similarity index 100%
rename from dotnet/src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj
rename to dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj
diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/PersistentAgentsClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/PersistentAgentsClientExtensions.cs
similarity index 100%
rename from dotnet/src/Microsoft.Agents.AI.AzureAI/PersistentAgentsClientExtensions.cs
rename to dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/PersistentAgentsClientExtensions.cs
diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/DevUIExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/DevUIExtensions.cs
new file mode 100644
index 00000000000..4a85de121a9
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DevUI/DevUIExtensions.cs
@@ -0,0 +1,70 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.Agents.AI.DevUI;
+
+///
+/// Provides helper methods for configuring the Microsoft Agents AI DevUI in ASP.NET applications.
+///
+public static class DevUIExtensions
+{
+ ///
+ /// Adds the necessary services for the DevUI to the application builder.
+ ///
+ public static IHostApplicationBuilder AddDevUI(this IHostApplicationBuilder builder)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ builder.Services.AddOpenAIConversations();
+ builder.Services.AddOpenAIResponses();
+
+ return builder;
+ }
+
+ ///
+ /// Maps an endpoint that serves the DevUI from the '/devui' path.
+ ///
+ /// The to add the endpoint to.
+ /// A that can be used to add authorization or other endpoint configuration.
+ /// Thrown when is null.
+ public static IEndpointConventionBuilder MapDevUI(
+ this IEndpointRouteBuilder endpoints)
+ {
+ var group = endpoints.MapGroup("");
+ group.MapDevUI(pattern: "/devui");
+ group.MapEntities();
+ group.MapOpenAIConversations();
+ group.MapOpenAIResponses();
+ return group;
+ }
+
+ ///
+ /// Maps an endpoint that serves the DevUI.
+ ///
+ /// The to add the endpoint to.
+ ///
+ /// The route pattern for the endpoint (e.g., "/devui", "/agent-ui").
+ /// Defaults to "/devui" if not specified. This is the path where DevUI will be accessible.
+ ///
+ /// A that can be used to add authorization or other endpoint configuration.
+ /// Thrown when is null.
+ /// Thrown when is null or whitespace.
+ internal static IEndpointConventionBuilder MapDevUI(
+ this IEndpointRouteBuilder endpoints,
+ [StringSyntax("Route")] string pattern = "/devui")
+ {
+ ArgumentNullException.ThrowIfNull(endpoints);
+ ArgumentException.ThrowIfNullOrWhiteSpace(pattern);
+
+ // Ensure the pattern doesn't end with a slash for consistency
+ var cleanPattern = pattern.TrimEnd('/');
+
+ // Create the DevUI handler
+ var logger = endpoints.ServiceProvider.GetRequiredService>();
+ var devUIHandler = new DevUIMiddleware(logger, cleanPattern);
+
+ return endpoints.MapGet($"{cleanPattern}/{{*path}}", devUIHandler.HandleRequestAsync)
+ .WithName($"DevUI at {cleanPattern}")
+ .WithDescription("Interactive developer interface for Microsoft Agent Framework");
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/DevUIMiddleware.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/DevUIMiddleware.cs
new file mode 100644
index 00000000000..fc6dd512ecd
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DevUI/DevUIMiddleware.cs
@@ -0,0 +1,236 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Frozen;
+using System.IO.Compression;
+using System.Reflection;
+using System.Security.Cryptography;
+using Microsoft.AspNetCore.StaticFiles;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.Agents.AI.DevUI;
+
+///
+/// Handler that serves embedded DevUI resource files from the 'resources' directory.
+///
+internal sealed class DevUIMiddleware
+{
+ private const string GZipEncodingValue = "gzip";
+ private static readonly StringValues s_gzipEncodingHeader = new(GZipEncodingValue);
+ private static readonly Assembly s_assembly = typeof(DevUIMiddleware).Assembly;
+ private static readonly FileExtensionContentTypeProvider s_contentTypeProvider = new();
+ private static readonly StringValues s_cacheControl = new(new CacheControlHeaderValue()
+ {
+ NoCache = true,
+ NoStore = true,
+ }.ToString());
+
+ private readonly ILogger _logger;
+ private readonly FrozenDictionary _resourceCache;
+ private readonly string _basePath;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger instance.
+ /// The base path where DevUI is mounted.
+ public DevUIMiddleware(ILogger logger, string basePath)
+ {
+ ArgumentNullException.ThrowIfNull(logger);
+ ArgumentException.ThrowIfNullOrEmpty(basePath);
+ this._logger = logger;
+ this._basePath = basePath.TrimEnd('/');
+
+ // Build resource cache
+ var resourceNamePrefix = $"{s_assembly.GetName().Name}.resources.";
+ this._resourceCache = s_assembly
+ .GetManifestResourceNames()
+ .Where(p => p.StartsWith(resourceNamePrefix, StringComparison.Ordinal))
+ .ToFrozenDictionary(
+ p => p[resourceNamePrefix.Length..].Replace('.', '/'),
+ CreateResourceEntry,
+ StringComparer.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Handles an HTTP request for DevUI resources.
+ ///
+ /// The HTTP context.
+ public async Task HandleRequestAsync(HttpContext context)
+ {
+ var path = context.Request.Path.Value;
+
+ if (path == null)
+ {
+ context.Response.StatusCode = StatusCodes.Status404NotFound;
+ return;
+ }
+
+ // If requesting the base path without a trailing slash, redirect to include it
+ // This ensures relative URLs in the HTML work correctly
+ if (string.Equals(path, this._basePath, StringComparison.OrdinalIgnoreCase) && !path.EndsWith('/'))
+ {
+ var redirectUrl = $"{path}/";
+ if (context.Request.QueryString.HasValue)
+ {
+ redirectUrl += context.Request.QueryString.Value;
+ }
+
+ context.Response.StatusCode = StatusCodes.Status301MovedPermanently;
+ context.Response.Headers.Location = redirectUrl;
+ this._logger.LogDebug("Redirecting {OriginalPath} to {RedirectUrl}", path, redirectUrl);
+ return;
+ }
+
+ // Remove the base path to get the resource path
+ var resourcePath = path.StartsWith(this._basePath, StringComparison.OrdinalIgnoreCase)
+ ? path.Substring(this._basePath.Length).TrimStart('/')
+ : path.TrimStart('/');
+
+ // If requesting the base path, serve index.html
+ if (string.IsNullOrEmpty(resourcePath))
+ {
+ resourcePath = "index.html";
+ }
+
+ // Try to serve the embedded resource
+ if (await this.TryServeResourceAsync(context, resourcePath).ConfigureAwait(false))
+ {
+ return;
+ }
+
+ // If resource not found, try serving index.html for client-side routing
+ if (!resourcePath.Contains('.', StringComparison.Ordinal) || resourcePath.EndsWith('/'))
+ {
+ if (await this.TryServeResourceAsync(context, "index.html").ConfigureAwait(false))
+ {
+ return;
+ }
+ }
+
+ // Resource not found
+ context.Response.StatusCode = StatusCodes.Status404NotFound;
+ }
+
+ private async Task TryServeResourceAsync(HttpContext context, string resourcePath)
+ {
+ try
+ {
+ if (!this._resourceCache.TryGetValue(resourcePath.Replace('.', '/'), out var cacheEntry))
+ {
+ this._logger.LogDebug("Embedded resource not found: {ResourcePath}", resourcePath);
+ return false;
+ }
+
+ var response = context.Response;
+
+ // Check if client has cached version
+ if (context.Request.Headers.IfNoneMatch == cacheEntry.ETag)
+ {
+ response.StatusCode = StatusCodes.Status304NotModified;
+ this._logger.LogDebug("Resource not modified (304): {ResourcePath}", resourcePath);
+ return true;
+ }
+
+ var responseHeaders = response.Headers;
+
+ byte[] content;
+ bool serveCompressed;
+ if (cacheEntry.CompressedContent is not null && IsGZipAccepted(context.Request))
+ {
+ serveCompressed = true;
+ responseHeaders.ContentEncoding = s_gzipEncodingHeader;
+ responseHeaders.ContentLength = cacheEntry.CompressedContent.Length;
+ content = cacheEntry.CompressedContent;
+ }
+ else
+ {
+ serveCompressed = false;
+ responseHeaders.ContentLength = cacheEntry.DecompressedContent!.Length;
+ content = cacheEntry.DecompressedContent;
+ }
+
+ responseHeaders.CacheControl = s_cacheControl;
+ responseHeaders.ContentType = cacheEntry.ContentType;
+ responseHeaders.ETag = cacheEntry.ETag;
+
+ await response.Body.WriteAsync(content, context.RequestAborted).ConfigureAwait(false);
+
+ this._logger.LogDebug("Served embedded resource: {ResourcePath} (compressed: {Compressed})", resourcePath, serveCompressed);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ this._logger.LogError(ex, "Error serving embedded resource: {ResourcePath}", resourcePath);
+ return false;
+ }
+ }
+
+ private static bool IsGZipAccepted(HttpRequest httpRequest)
+ {
+ if (httpRequest.GetTypedHeaders().AcceptEncoding is not { Count: > 0 } acceptEncoding)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < acceptEncoding.Count; i++)
+ {
+ var encoding = acceptEncoding[i];
+
+ if (encoding.Quality is not 0 &&
+ string.Equals(encoding.Value.Value, GZipEncodingValue, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static ResourceEntry CreateResourceEntry(string resourceName)
+ {
+ using var resourceStream = s_assembly.GetManifestResourceStream(resourceName)!;
+ using var decompressedContent = new MemoryStream();
+
+ // Read and cache the original resource content
+ resourceStream.CopyTo(decompressedContent);
+ var decompressedArray = decompressedContent.ToArray();
+
+ // Compress the content
+ using var compressedContent = new MemoryStream();
+ using (var gzip = new GZipStream(compressedContent, CompressionMode.Compress, leaveOpen: true))
+ {
+ // This is a synchronous write to a memory stream.
+ // There is no benefit to asynchrony here.
+ gzip.Write(decompressedArray);
+ }
+
+ // Only use compression if it actually reduces size
+ byte[]? compressedArray = compressedContent.Length < decompressedArray.Length
+ ? compressedContent.ToArray()
+ : null;
+
+ var hash = SHA256.HashData(compressedArray ?? decompressedArray);
+ var eTag = $"\"{Convert.ToBase64String(hash)}\"";
+
+ // Determine content type from resource name
+ var contentType = s_contentTypeProvider.TryGetContentType(resourceName, out var ct)
+ ? ct
+ : "application/octet-stream";
+
+ return new ResourceEntry(resourceName, decompressedArray, compressedArray, eTag, contentType);
+ }
+
+ private sealed class ResourceEntry(string resourceName, byte[] decompressedContent, byte[]? compressedContent, string eTag, string contentType)
+ {
+ public byte[]? CompressedContent { get; } = compressedContent;
+
+ public string ContentType { get; } = contentType;
+
+ public byte[] DecompressedContent { get; } = decompressedContent;
+
+ public string ETag { get; } = eTag;
+
+ public string ResourceName { get; } = resourceName;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntitiesJsonContext.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntitiesJsonContext.cs
new file mode 100644
index 00000000000..fc8bbe3864c
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntitiesJsonContext.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.DevUI.Entities;
+
+///
+/// JSON serialization context for entity-related types.
+/// Enables AOT-compatible JSON serialization using source generators.
+///
+[JsonSourceGenerationOptions(
+ JsonSerializerDefaults.Web,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
+[JsonSerializable(typeof(EntityInfo))]
+[JsonSerializable(typeof(DiscoveryResponse))]
+[JsonSerializable(typeof(EnvVarRequirement))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(Dictionary))]
+[JsonSerializable(typeof(JsonElement))]
+[ExcludeFromCodeCoverage]
+internal sealed partial class EntitiesJsonContext : JsonSerializerContext;
diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntityInfo.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntityInfo.cs
new file mode 100644
index 00000000000..8b5e4e54929
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntityInfo.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.DevUI.Entities;
+
+///
+/// Information about an environment variable required by an entity.
+///
+internal sealed record EnvVarRequirement(
+ [property: JsonPropertyName("name")]
+ string Name,
+
+ [property: JsonPropertyName("description")]
+ string? Description = null,
+
+ [property: JsonPropertyName("required")]
+ bool Required = true,
+
+ [property: JsonPropertyName("example")]
+ string? Example = null
+);
+
+///
+/// Information about an entity (agent or workflow).
+///
+internal sealed record EntityInfo(
+ [property: JsonPropertyName("id")]
+ string Id,
+
+ [property: JsonPropertyName("type")]
+ string Type,
+
+ [property: JsonPropertyName("name")]
+ string Name,
+
+ [property: JsonPropertyName("description")]
+ string? Description = null,
+
+ [property: JsonPropertyName("framework")]
+ string Framework = "dotnet",
+
+ [property: JsonPropertyName("tools")]
+ List? Tools = null,
+
+ [property: JsonPropertyName("metadata")]
+ Dictionary? Metadata = null
+)
+{
+ [JsonPropertyName("source")]
+ public string? Source { get; init; } = "di";
+
+ [JsonPropertyName("original_url")]
+ public string? OriginalUrl { get; init; }
+
+ // Workflow-specific fields
+ [JsonPropertyName("required_env_vars")]
+ public List? RequiredEnvVars { get; init; }
+
+ [JsonPropertyName("executors")]
+ public List? Executors { get; init; }
+
+ [JsonPropertyName("workflow_dump")]
+ public JsonElement? WorkflowDump { get; init; }
+
+ [JsonPropertyName("input_schema")]
+ public JsonElement? InputSchema { get; init; }
+
+ [JsonPropertyName("input_type_name")]
+ public string? InputTypeName { get; init; }
+
+ [JsonPropertyName("start_executor_id")]
+ public string? StartExecutorId { get; init; }
+};
+
+///
+/// Response containing a list of discovered entities.
+///
+internal sealed record DiscoveryResponse(
+ [property: JsonPropertyName("entities")]
+ List Entities
+);
diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/WorkflowSerializationExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/WorkflowSerializationExtensions.cs
new file mode 100644
index 00000000000..81ce6182d10
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/WorkflowSerializationExtensions.cs
@@ -0,0 +1,193 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.Agents.AI.Workflows.Checkpointing;
+
+namespace Microsoft.Agents.AI.DevUI.Entities;
+
+///
+/// Extension methods for serializing workflows to DevUI-compatible format
+///
+internal static class WorkflowSerializationExtensions
+{
+ // The frontend max iterations default value expected by the DevUI frontend
+ private const int MaxIterationsDefault = 100;
+
+ ///
+ /// Converts a workflow to a dictionary representation compatible with DevUI frontend.
+ /// This matches the Python workflow.to_dict() format expected by the UI.
+ ///
+ public static Dictionary ToDevUIDict(this Workflow workflow)
+ {
+ var result = new Dictionary
+ {
+ ["id"] = workflow.Name ?? Guid.NewGuid().ToString(),
+ ["start_executor_id"] = workflow.StartExecutorId,
+ ["max_iterations"] = MaxIterationsDefault
+ };
+
+ // Add optional fields
+ if (!string.IsNullOrEmpty(workflow.Name))
+ {
+ result["name"] = workflow.Name;
+ }
+
+ if (!string.IsNullOrEmpty(workflow.Description))
+ {
+ result["description"] = workflow.Description;
+ }
+
+ // Convert executors to Python-compatible format
+ result["executors"] = ConvertExecutorsToDict(workflow);
+
+ // Convert edges to edge_groups format
+ result["edge_groups"] = ConvertEdgesToEdgeGroups(workflow);
+
+ return result;
+ }
+
+ ///
+ /// Converts workflow executors to a dictionary format compatible with Python
+ ///
+ private static Dictionary ConvertExecutorsToDict(Workflow workflow)
+ {
+ var executors = new Dictionary();
+
+ // Extract executor IDs from edges and start executor
+ // (Registrations is internal, so we infer executors from the graph structure)
+ var executorIds = new HashSet { workflow.StartExecutorId };
+
+ var reflectedEdges = workflow.ReflectEdges();
+ foreach (var (sourceId, edgeSet) in reflectedEdges)
+ {
+ executorIds.Add(sourceId);
+ foreach (var edge in edgeSet)
+ {
+ foreach (var sinkId in edge.Connection.SinkIds)
+ {
+ executorIds.Add(sinkId);
+ }
+ }
+ }
+
+ // Create executor entries (we can't access internal Registrations for type info)
+ foreach (var executorId in executorIds)
+ {
+ executors[executorId] = new Dictionary
+ {
+ ["id"] = executorId,
+ ["type"] = "Executor"
+ };
+ }
+
+ return executors;
+ }
+
+ ///
+ /// Converts workflow edges to edge_groups format expected by the UI
+ ///
+ private static List