-
Notifications
You must be signed in to change notification settings - Fork 646
.NET: AG-UI support for .NET #1776
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR introduces AG-UI (Agent-User Interaction) protocol support for the Microsoft Agent Framework, enabling standardized client-server communication for AI agents. The implementation includes both server-side ASP.NET Core hosting capabilities and client-side consumption functionality.
Key changes:
- Added AG-UI protocol implementation with server-sent events (SSE) streaming
- Created ASP.NET Core hosting extensions for exposing agents via AG-UI endpoints
- Implemented AGUIAgent client for consuming remote AG-UI services
- Added comprehensive unit and integration tests covering protocol behavior
Reviewed Changes
Copilot reviewed 47 out of 47 changed files in this pull request and generated 29 comments.
Show a summary per file
| File | Description |
|---|---|
Microsoft.Agents.AI.AGUI/AGUIAgent.cs |
Core client implementation for connecting to AG-UI servers |
Microsoft.Agents.AI.AGUI/AGUIHttpService.cs |
HTTP service for SSE-based communication with AG-UI endpoints |
Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs |
Extension methods for mapping AG-UI agents to ASP.NET endpoints |
Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIServerSentEventsResult.cs |
IResult implementation for streaming AG-UI events via SSE |
Shared/*.cs |
Protocol event types, serialization contexts, and message conversion utilities |
| Test files | Comprehensive unit and integration tests for AG-UI functionality |
samples/AGUIClientServer/* |
Sample client and server demonstrating AG-UI protocol usage |
dotnet/tests/Microsoft.Agents.AI.AGUI.UnitTests/AgentRunResponseUpdateAGUIExtensionsTests.cs
Outdated
Show resolved
Hide resolved
dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AgentRunResponseUpdateAGUIExtensions.cs
Outdated
Show resolved
Hide resolved
...osoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs
Outdated
Show resolved
Hide resolved
dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIServerSentEventsResult.cs
Outdated
Show resolved
Hide resolved
| ThreadId = typedThread.ThreadId, | ||
| RunId = runId, | ||
| Messages = messages.AsAGUIMessages(), | ||
| Context = new Dictionary<string, string>(StringComparer.Ordinal) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is Context? It needs to be set to a new dictionary even if it's never used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've taken this out from the current PR. Context is data that the client can pass to the agent. I'll deal with it in a separate PR so that we can better discuss how to map it.
| /// <summary> | ||
| /// Gets the ID of the conversation thread. | ||
| /// </summary> | ||
| public string ThreadId { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets the ID of the agent run. | ||
| /// </summary> | ||
| public string RunId { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this actually need an AIContent-derived type to communicate this, or could it be modeled as ChatResponse{Update}.ConversationId and ResponseId?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got rid of this in favor of an empty update
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This info is captured in ConversationId, and ResponseId
| /// <summary> | ||
| /// Gets optional result data from the run. | ||
| /// </summary> | ||
| public string? Result { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can't just be TextContent?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://docs.ag-ui.com/sdk/js/core/events#runfinishedevent
Result can be any structured data. I think it is fair to map it to a separate message + some TextContent, but what's the best way to represent this when the data is structured? Ideally we would want some JsonContent or similar, wouldn't we? For now I'll just map it to a string
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally we would want some JsonContent or similar, wouldn't we? For now I'll just map it to a string
Mapping to a string sounds fine for now.
But you could also map it to a DataContent with a MediaType of "application/json".
| /// Gets the ID of the agent run. | ||
| /// </summary> | ||
| public string RunId { get; } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm surprised you're not running into serialization issues with these AIContent-derived types, e.g. I'd expect doing serialization on a ChatMessage containing these will fail, because they're not connected to the base type's polymorphism infrastructure. Whatever JsonSerializerOptions is used would likely need to use the AddAIContentType method to augment the resolver in that options. It's doable, but it also adds complication, and is one of the reason it'd be nice to avoid needing custom AIContent types, if possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got rid of all the custom content types, things are mapped to response updates. I think this is fine for now, but I want to align later on in what the best ways are for us representing some of this information
| /// 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. | ||
| /// </summary> | ||
| internal sealed class BaseEventJsonConverter : JsonConverter<BaseEvent> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can't use the built-in polymorphism support in STJ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The built-in polymorphism support requires the discriminator to be the first element in the payload, which we can't guarantee for clients that we don't own.
I started with that approach and switched to this approach because the copilotkit typescript client was not working that way.
| namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared; | ||
| #else | ||
| namespace Microsoft.Agents.AI.AGUI.Shared; | ||
| #endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to change this internal namespace based on what project it's being built into?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is because the test projects have IVT to different assemblies, and otherwise it creates conflicts when you import the namespace.
dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIServerSentEventsResult.cs
Outdated
Show resolved
Hide resolved
|
@javiercn can you copy the information in the PR description to an ADR and add to this folder: https://github.com/microsoft/agent-framework/tree/main/docs/decisions It will be much easier to find the information later and it will be with all of the other design decisions. |
0f9d19d to
f58312e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed feedback:
- I've gotten rid of the custom contents and mapped the info from the events as ResponseUpdate.
- I switched to use the SseFormatter instead of the manual SSE Event serialization.
- I've gotten rid of Context/Tools/ForwardedProps for now and will bring them in a separate PR.
- I've mapped additional properties to response updates.
- I removed some buffering when dealing with messages.
- I've fixed the build dependencies and cleaned up some copilot leftover.
dotnet/src/Microsoft.Agents.AI.AGUI/Shared/AgentRunResponseUpdateAGUIExtensions.cs
Outdated
Show resolved
Hide resolved
| switch (evt) | ||
| { | ||
| case RunStartedEvent runStarted: | ||
| yield return new AgentRunResponseUpdate(new ChatResponseUpdate( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've populated the response ID and conversation ID as well as the message ID for the text updates. For the RunStarted/Finished events there isn't a message ID.
| return result ?? []; | ||
| } | ||
|
|
||
| public static ChatRole MapChatRole(string role) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we care about the string being in the "canonical" form? https://docs.ag-ui.com/sdk/js/core/types#role
dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIServerSentEventsResult.cs
Outdated
Show resolved
Hide resolved
Yep, I saw the ADRs, and was planning to do that, but wanted to check with you first. I'll include the ADRs as a last step as we work out through the details |
…and update the sample client
b3a1d95 to
a1f8977
Compare
Summary
This PR implements Phase 1 of AG-UI protocol support in the .NET Agent Framework, enabling text streaming capabilities for both consuming AG-UI-compliant servers and exposing .NET agents via the AG-UI protocol.
Motivation and goals
The AG-UI (Agent-User Interaction) protocol is an open, lightweight, event-based protocol that standardizes how AI agents connect to user-facing applications. It addresses key challenges in agentic applications:
Without AG-UI support, .NET agents cannot interoperate with the growing ecosystem of AG-UI-compatible frontends and agent frameworks (LangGraph, CrewAI, Pydantic AI, etc.).
This implementation enables:
In scope
Client-side AG-UI consumption (
Microsoft.Agents.AI.AGUIpackage)AGUIAgentclass for connecting to remote AG-UI serversAGUIAgentThreadfor managing conversation threadsServer-side AG-UI hosting (
Microsoft.Agents.AI.Hosting.AGUI.AspNetCorepackage)MapAGUIAgentextension method for ASP.NET CoreSseFormatterText streaming events (Phase 1)
RunStarted,RunFinished,RunErrorTextMessageStart,TextMessageContent,TextMessageEndConversationIdandResponseIdpropertiesTesting and samples
Out of scope
The following AG-UI features are intentionally deferred to future PRs:
ToolCallStart,ToolCallArgs,ToolCallEnd,ToolCallResult)StateSnapshot,StateDelta,MessagesSnapshot)StepStarted,StepFinished)Examples
Server: Exposing a .NET agent via AG-UI
Client: Consuming an AG-UI server
SSE Event Stream Format
When a client sends a request:
The server streams back SSE events:
Detailed design
Architecture
Package Structure
Key Design Decisions
1. Event Models as Internal Types
RunStartedEvent,TextMessageContentEvent) are internalAgentRunResponseUpdate,ChatMessage)2. No Custom Content Types
ChatResponseUpdate.ConversationIdandResponseIdpropertiesErrorContenttypeConversationIdandResponseIdindicating run startedFinishReasonindicates run finished3. Agent Factory Pattern in MapAGUIAgent
(messages) => AIAgent4. Bidirectional Conversion Architecture
AgentRunResponseUpdate→ AG-UI eventsAgentRunResponseUpdateShared/namespace (compiled in both packages)5. Thread Management
AGUIAgentThreadstores onlyThreadId(string)ConversationIdproperty on updates6. SSE Formatting
SseFormatterfor Server-Sent Eventsdata:prefix and double newline delimiterEvent Conversion Logic
Text Message Streaming (Server → Client):
Text Message Streaming (Client → Framework):
JSON Serialization
System.Text.Json.Serialization.JsonConverterfor polymorphic eventsBaseEventJsonConverterdispatches based on"type"fieldAGUIJsonSerializerContext) for AOT compatibilityError Handling
HttpRequestExceptionRunErrorevents convert toErrorContentin update streamJsonExceptionorInvalidOperationExceptionOperationCanceledExceptionwhen cancelledDrawbacks
Custom JSON Converter: Required custom polymorphic deserialization instead of built-in STJ support
Shared Code via Preprocessor Directives: Files in
Shared/use#if ASPNETCOREto compile into both packagesConsidered alternatives
Alternative 1: Expose AG-UI Event Types Publicly
Rejected: We already have common abstractions for AI concepts. We can break AG-UI specific payloads into a separate assembly in the future and take a dependency on that if needed.
Alternative 2: Implement All Event Types Upfront
Rejected: Keeping the PR manageable and focused on text streaming. Tool calls and state management can be added incrementally.
Alternative 3: Custom AIContent Types for Lifecycle
Rejected after review: Initially proposed
RunStartedContent,RunFinishedContent, andRunErrorContent. Feedback indicated these should use existing framework properties (ConversationId,ResponseId,ErrorContent) instead.#Fixes #1775