Skip to content

fix(dotnet): map reasoning and skip activity in AsChatMessages - #2295

Open
1aifanatic wants to merge 1 commit into
ag-ui-protocol:mainfrom
1aifanatic:fix/dotnet-reasoning-activity-chat-roles
Open

fix(dotnet): map reasoning and skip activity in AsChatMessages#2295
1aifanatic wants to merge 1 commit into
ag-ui-protocol:mainfrom
1aifanatic:fix/dotnet-reasoning-activity-chat-roles

Conversation

@1aifanatic

Copy link
Copy Markdown

Fixes #2290

Problem

AGUI.Abstractions is internally inconsistent about message roles across three lists:

Piece Handles
AGUIRoles all 7 roles, including activity and reasoning
AGUIMessageJsonConverter deserializes all 7
MapChatRole 5 — throws InvalidOperationException on the other two

AsChatMessages calls MapChatRole unconditionally before any per-message-type branching, so one reasoning message anywhere in RunAgentInput.Messages fails the whole run — an HTTP 500 from a hosted agent endpoint.

This isn't an edge case. The spec says reasoning messages "are meant to be sent back to the agent for further processing on subsequent turns", so a conforming client that echoes them gets a 500 on turn 2 of every thread in which the model reasoned — and every turn after, because the offending message is now part of the client's authoritative history. The thread is permanently unusable.

Fix

reasoning → an assistant ChatMessage carrying TextReasoningContent, which is how Microsoft.Extensions.AI models the same concept. encryptedValue maps to ProtectedData — MEAI's documented home for "an opaque blob of data... that should be roundtripped back to the provider... often encrypted". That is what keeps encrypted chain-of-thought continuous across turns, which is the entire point of the field (and required for store:false / ZDR).

activity → dropped. It's frontend-only and "never forwarded to the agent", so a client that sends one anyway should be ignored rather than 500. It's dropped before the parallel-tool-call bookkeeping so the message is fully transparent — an interleaved activity must not split a tool-call run that AsChatMessages is mid-coalesce (there's a test for exactly that).

MapChatRole now maps reasoningChatRole.Assistant, so the public helper no longer throws for a role the SDK itself defines. activity genuinely has no ChatRole equivalent, and AsChatMessages no longer routes it there; I documented that on the method rather than inventing a bogus mapping. Since AGUIMessageJsonConverter already rejects unknown role discriminators with a JsonException, the remaining throw is now unreachable from the deserialization path — it only guards programmatically-constructed messages.

Also removed the AGUIReasoningMessage reasoning => reasoning.Content arm from the plain-text switch: reasoning now returns before it, so it was dead code that read as if reasoning were already handled. (That phantom handler is arguably how this drifted in the first place.)

Tests

13 new tests in AGUIChatMessageExtensionsTest, including the exact turn-2 RunAgentInput payload from the issue.

The issue asked for a guard so the three lists can't drift again — that's AsChatMessages_EveryDeserializableRole_DoesNotThrow, a [Theory] over all seven roles asserting that anything AGUIMessageJsonConverter deserializes, AsChatMessages consumes without throwing. 8 of the 13 fail on main.

Full .NET suite (net10.0), all green with this change:

Project Tests
AGUI.Abstractions.UnitTests 198 (was 185)
AGUI.Server.UnitTests 114
AGUI.Client.UnitTests 114
AGUI.Hosting.AspNetCore.IntegrationTests 117
AGUI.Protobuf.UnitTests 53
AGUI.Formatting.UnitTests 8

AGUI.Abstractions builds clean (0 warnings, TreatWarningsAsErrors) across all five TFMs: net10.0, net9.0, net8.0, netstandard2.0, net472. No public API surface added, so no PublicAPI.Unshipped.txt change.

Aside: the non-Abstractions test projects needed -p:SignAssembly=false to build locally, which is #2165, not this change.

Scope note

The outbound direction is still asymmetric: AsAGUIMessages never produces an AGUIReasoningMessage, so a TextReasoningContent coming back from a provider maps to a plain assistant message. That's pre-existing and outside this issue, but this change makes it more visible — happy to send a follow-up if you'd like the reverse mapping too.

AGUI.Abstractions declares seven roles in AGUIRoles and deserializes all
seven in AGUIMessageJsonConverter, but AsChatMessages called MapChatRole
unconditionally and MapChatRole handled only five. A single reasoning
message anywhere in RunAgentInput.Messages threw
"Unknown chat role: reasoning", surfacing as an HTTP 500.

The spec has clients send reasoning messages back to the agent on
subsequent turns, so a conforming client that echoes them fails on
turn 2 of every thread where the model reasoned — and keeps failing,
because the message is now part of the client's authoritative history.

- reasoning maps to an assistant ChatMessage carrying
  TextReasoningContent, MEAI's model for the same concept.
  encryptedValue becomes ProtectedData, the opaque provider blob MEAI
  round-trips untouched, preserving encrypted chain-of-thought
  continuity across turns (store:false / ZDR).
- activity is dropped. It is frontend-only and never forwarded to the
  agent, so a client that sends one anyway should be ignored, not 500.
  Dropped before the parallel-tool-call bookkeeping so it stays fully
  transparent and cannot split a run mid-coalesce.
- MapChatRole maps reasoning to Assistant so the public helper no
  longer throws for a role the SDK itself defines. Activity has no
  ChatRole equivalent and AsChatMessages no longer routes it there;
  that is now documented on the method.

The now-unreachable AGUIReasoningMessage arm in the plain-text switch
is removed, since reasoning returns before it.

Adds a role-coverage theory asserting that everything
AGUIMessageJsonConverter can deserialize, AsChatMessages can consume
without throwing, so the three lists cannot drift apart again.

Fixes ag-ui-protocol#2290
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant