Pre-flight Checklist
Describe the Bug
AGUI.Abstractions is internally inconsistent about message roles: it defines and deserialises reasoning and activity, then throws when converting them to ChatMessage.
The three pieces, all in sdks/dotnet/src/AGUI.Abstractions:
Messages/AGUIRoles.cs declares all seven protocol roles, including Activity = "activity" and Reasoning = "reasoning".
Messages/AGUIMessageJsonConverter.cs#L42-L45 deserialises them happily into AGUIActivityMessage / AGUIReasoningMessage.
Extensions/AGUIChatMessageExtensions.cs#L271-L277 — MapChatRole handles only system, user, assistant, developer and tool, and otherwise throws:
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 :
string.Equals(role, AGUIRoles.Tool, StringComparison.OrdinalIgnoreCase) ? ChatRole.Tool :
throw new InvalidOperationException($"Unknown chat role: {role}");
AsChatMessages calls it unconditionally at AGUIChatMessageExtensions.cs#L67, before any per-message-type branching, so a single reasoning message anywhere in RunAgentInput.Messages fails the whole run with an unhandled exception — surfacing to the caller as HTTP 500.
Why this breaks conforming clients
This isn't an edge case: the protocol requires the behaviour that triggers it. From docs.ag-ui.com/concepts/messages, under Reasoning Messages:
Unlike Activity messages, Reasoning messages are intended to represent the agent's internal thought process and may be encrypted for privacy and are meant to be sent back to the agent for further processing on subsequent turns.
So any spec-compliant client that echoes reasoning messages back — as it is told to — gets a 500 on the second turn of every conversation in which the model emitted reasoning. Turn 1 succeeds; turn 2 always fails.
activity is the mirror image. The same doc says:
Frontend-only: never forwarded to the agent, so no filtering and no LLM confusion.
So it should never arrive — but if a client sends one anyway, the SDK 500s rather than ignoring it.
Net effect: a .NET AG-UI server cannot be used with a reasoning model and a conforming client.
Steps to Reproduce
Minimal, no server and no client required — the throw is in AGUI.Abstractions alone. This snippet is verified against AGUI.Abstractions 0.0.4 on .NET 10; it prints AGUIReasoningMessage and then throws.
using AGUI.Abstractions;
using System.Text.Json;
// A RunAgentInput exactly as a conforming client re-sends it on turn 2.
const string json = """
{
"threadId": "t-1",
"runId": "r-2",
"messages": [
{ "id": "1", "role": "user", "content": "How many objectives are listed?" },
{ "id": "2", "role": "reasoning", "content": "**Counting objectives** ... 4 items." },
{ "id": "3", "role": "assistant", "content": "There are 4 objectives listed." },
{ "id": "4", "role": "user", "content": "How many agreed actions are listed?" }
]
}
""";
var input = JsonSerializer.Deserialize(json, AGUIJsonSerializerContext.Default.RunAgentInput)!;
// Deserialization succeeds: messages[1] is a well-formed AGUIReasoningMessage.
Console.WriteLine(input.Messages[1].GetType().Name); // AGUIReasoningMessage
// Conversion throws.
var chatMessages = input.Messages.AsChatMessages().ToList();
// System.InvalidOperationException: Unknown chat role: reasoning
End-to-end reproduction
- Host an agent with
AGUI.Server / Microsoft.Agents.AI.Hosting.AGUI.AspNetCore using a reasoning-capable model (e.g. GPT-5 class).
- Connect any conforming client that persists and re-sends the streamed conversation — we hit this with CopilotKit, but the behaviour is mandated by the spec, not specific to that client.
- Send a first message. The agent emits
REASONING_MESSAGE_START / REASONING_MESSAGE_CONTENT / REASONING_MESSAGE_END; the client stores the resulting role: "reasoning" message. ✅ Works.
- Send a second message in the same thread. The client includes the reasoning message in
RunAgentInput.Messages, per spec.
- The request fails with HTTP 500 /
Unknown chat role: reasoning. ❌
Every subsequent turn in that thread fails identically — the thread is permanently unusable, because the offending message is now part of the client's authoritative history.
Expected Behavior
AsChatMessages should handle every role AGUIRoles declares and AGUIMessageJsonConverter accepts. Specifically:
reasoning — mapped to a ChatMessage rather than dropped. Microsoft.Extensions.AI already models this as TextReasoningContent, so the natural mapping is ChatRole.Assistant with a TextReasoningContent payload, preserving encryptedValue so encrypted chain-of-thought keeps its continuity across turns (which is the entire point of the field, and required for store:false / ZDR scenarios).
activity — skipped, matching the spec's "frontend-only, never forwarded to the agent". Silently ignoring an activity message is strictly better than throwing.
More generally: an unrecognised role arriving from a client should not produce an unhandled exception in a request pipeline. Even if some role genuinely cannot be represented, skipping it (or surfacing a validation error) keeps the conversation usable, whereas throwing bricks the thread permanently.
It would also help to have a round-trip test asserting that everything AGUIMessageJsonConverter can deserialise, AsChatMessages can consume — the two lists are currently allowed to drift, which is exactly what happened here.
Environment
AG-UI package(s) & version(s): AGUI.Abstractions 0.0.3 and 0.0.4 (both affected)
AGUI.Server 0.0.3
Also present on: main @ 125befb3e887fc7e2fbf16211f607cd0c0da7d1e (verified by inspection)
Runtime: .NET 10.0 (net10.0), ASP.NET Core
Host packages: Microsoft.Agents.AI.Hosting.AGUI.AspNetCore 1.14.0-preview.260721.1
Microsoft.Extensions.AI 10.6.0
OS: Windows 11
Client: CopilotKit (any client that re-sends reasoning messages per spec)
Screenshots
AG-UI package(s) & version(s): AGUI.Abstractions 0.0.3 and 0.0.4 (both affected)
AGUI.Server 0.0.3
Also present on: main @ 125befb3e887fc7e2fbf16211f607cd0c0da7d1e (verified by inspection)
Runtime: .NET 10.0 (net10.0), ASP.NET Core
Host packages: Microsoft.Agents.AI.Hosting.AGUI.AspNetCore 1.14.0-preview.260721.1
Microsoft.Extensions.AI 10.6.0
OS: Windows 11
Client: CopilotKit (any client that re-sends reasoning messages per spec)
Logs & Errors
AGUIReasoningMessage
Unhandled exception. System.InvalidOperationException: Unknown chat role: reasoning
at AGUI.Abstractions.AGUIChatMessageExtensions.MapChatRole(String role)
at AGUI.Abstractions.AGUIChatMessageExtensions.AsChatMessages(IEnumerable`1 aguiMessages)+MoveNext()
at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
# As returned to the caller when this happens inside a hosted agent endpoint:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
"title": "An error occurred while processing your request.",
"status": 500,
"detail": "Unknown chat role: reasoning"
}
Additional Context
Relevant source
Note that AGUIMessageJsonConverter.Write (L136–L140) serialises both types correctly, so the gap really is isolated to MapChatRole.
Our workaround (and why it isn't a fix)
We strip any message whose role MapChatRole cannot handle from RunAgentInput.Messages in the proxy that fronts our agent, using an allow-list of the five supported roles so a future display-only role degrades to a dropped message instead of a 500.
This unblocks us, but it is a deviation from the spec, not a fix: dropping reasoning messages discards exactly the cross-turn reasoning continuity (including encryptedValue) that the protocol asks clients to preserve. Consumers shouldn't have to choose between a 500 and silently violating the spec.
Possibly related
The same MapChatRole is reached through Microsoft.Agents.AI.Hosting.AGUI.AspNetCore, so this affects the Microsoft Agent Framework AG-UI hosting path too — the exception simply surfaces there as a 500 from the agent endpoint.
Pre-flight Checklist
Describe the Bug
AGUI.Abstractionsis internally inconsistent about message roles: it defines and deserialisesreasoningandactivity, then throws when converting them toChatMessage.The three pieces, all in
sdks/dotnet/src/AGUI.Abstractions:Messages/AGUIRoles.csdeclares all seven protocol roles, includingActivity = "activity"andReasoning = "reasoning".Messages/AGUIMessageJsonConverter.cs#L42-L45deserialises them happily intoAGUIActivityMessage/AGUIReasoningMessage.Extensions/AGUIChatMessageExtensions.cs#L271-L277—MapChatRolehandles onlysystem,user,assistant,developerandtool, and otherwise throws:AsChatMessagescalls it unconditionally atAGUIChatMessageExtensions.cs#L67, before any per-message-type branching, so a single reasoning message anywhere inRunAgentInput.Messagesfails the whole run with an unhandled exception — surfacing to the caller as HTTP 500.Why this breaks conforming clients
This isn't an edge case: the protocol requires the behaviour that triggers it. From docs.ag-ui.com/concepts/messages, under Reasoning Messages:
So any spec-compliant client that echoes reasoning messages back — as it is told to — gets a 500 on the second turn of every conversation in which the model emitted reasoning. Turn 1 succeeds; turn 2 always fails.
activityis the mirror image. The same doc says:So it should never arrive — but if a client sends one anyway, the SDK 500s rather than ignoring it.
Net effect: a .NET AG-UI server cannot be used with a reasoning model and a conforming client.
Steps to Reproduce
Minimal, no server and no client required — the throw is in
AGUI.Abstractionsalone. This snippet is verified againstAGUI.Abstractions0.0.4 on .NET 10; it printsAGUIReasoningMessageand then throws.End-to-end reproduction
AGUI.Server/Microsoft.Agents.AI.Hosting.AGUI.AspNetCoreusing a reasoning-capable model (e.g. GPT-5 class).REASONING_MESSAGE_START/REASONING_MESSAGE_CONTENT/REASONING_MESSAGE_END; the client stores the resultingrole: "reasoning"message. ✅ Works.RunAgentInput.Messages, per spec.Unknown chat role: reasoning. ❌Every subsequent turn in that thread fails identically — the thread is permanently unusable, because the offending message is now part of the client's authoritative history.
Expected Behavior
AsChatMessagesshould handle every roleAGUIRolesdeclares andAGUIMessageJsonConverteraccepts. Specifically:reasoning— mapped to aChatMessagerather than dropped.Microsoft.Extensions.AIalready models this asTextReasoningContent, so the natural mapping isChatRole.Assistantwith aTextReasoningContentpayload, preservingencryptedValueso encrypted chain-of-thought keeps its continuity across turns (which is the entire point of the field, and required forstore:false/ ZDR scenarios).activity— skipped, matching the spec's "frontend-only, never forwarded to the agent". Silently ignoring an activity message is strictly better than throwing.More generally: an unrecognised role arriving from a client should not produce an unhandled exception in a request pipeline. Even if some role genuinely cannot be represented, skipping it (or surfacing a validation error) keeps the conversation usable, whereas throwing bricks the thread permanently.
It would also help to have a round-trip test asserting that everything
AGUIMessageJsonConvertercan deserialise,AsChatMessagescan consume — the two lists are currently allowed to drift, which is exactly what happened here.Environment
Screenshots
Logs & Errors
Additional Context
Relevant source
Messages/AGUIRoles.csActivityandReasoningalongside the other five rolesMessages/AGUIMessageJsonConverter.csactivityandreasoningExtensions/AGUIChatMessageExtensions.csAsChatMessagescallsMapChatRolefor every messageExtensions/AGUIChatMessageExtensions.csMapChatRolethrows forreasoning/activityNote that
AGUIMessageJsonConverter.Write(L136–L140) serialises both types correctly, so the gap really is isolated toMapChatRole.Our workaround (and why it isn't a fix)
We strip any message whose role
MapChatRolecannot handle fromRunAgentInput.Messagesin the proxy that fronts our agent, using an allow-list of the five supported roles so a future display-only role degrades to a dropped message instead of a 500.This unblocks us, but it is a deviation from the spec, not a fix: dropping reasoning messages discards exactly the cross-turn reasoning continuity (including
encryptedValue) that the protocol asks clients to preserve. Consumers shouldn't have to choose between a 500 and silently violating the spec.Possibly related
The same
MapChatRoleis reached throughMicrosoft.Agents.AI.Hosting.AGUI.AspNetCore, so this affects the Microsoft Agent Framework AG-UI hosting path too — the exception simply surfaces there as a 500 from the agent endpoint.