Skip to content

[Bug]: [.NET] AsChatMessages throws Unknown chat role: reasoning for roles the SDK itself defines and parses #2290

Description

@JORDAAAN1495

Pre-flight Checklist

  • I have searched existing issues and this hasn't been reported yet.
  • I am using the latest version AG-UI.

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:

  1. Messages/AGUIRoles.cs declares all seven protocol roles, including Activity = "activity" and Reasoning = "reasoning".
  2. Messages/AGUIMessageJsonConverter.cs#L42-L45 deserialises them happily into AGUIActivityMessage / AGUIReasoningMessage.
  3. Extensions/AGUIChatMessageExtensions.cs#L271-L277MapChatRole 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

  1. Host an agent with AGUI.Server / Microsoft.Agents.AI.Hosting.AGUI.AspNetCore using a reasoning-capable model (e.g. GPT-5 class).
  2. 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.
  3. 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.
  4. Send a second message in the same thread. The client includes the reasoning message in RunAgentInput.Messages, per spec.
  5. 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

File Line Behaviour
Messages/AGUIRoles.cs Declares Activity and Reasoning alongside the other five roles
Messages/AGUIMessageJsonConverter.cs 42–45 Deserialises activity and reasoning
Extensions/AGUIChatMessageExtensions.cs 67 AsChatMessages calls MapChatRole for every message
Extensions/AGUIChatMessageExtensions.cs 271–277 MapChatRole throws for reasoning / activity

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions