From a107a614b45eb9b76d88c94f18e959f9135041de Mon Sep 17 00:00:00 2001 From: atty57 <99388680+atty57@users.noreply.github.com> Date: Thu, 17 Sep 2026 09:43:56 -0400 Subject: [PATCH] .NET: Fix AG-UI "Unknown chat role: reasoning" on follow-up turns (#8462) --- .../AGUIEndpointRouteBuilderExtensions.cs | 17 ++++++++++ ...AGUIEndpointRouteBuilderExtensionsTests.cs | 32 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs index ca900935daf..e7f9be1a7fd 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs @@ -134,6 +134,7 @@ public static IEndpointConventionBuilder MapAGUIServer( var streamOptions = context.GetEndpoint()?.Metadata.GetMetadata() ?? context.RequestServices.GetService>()?.Value; + RemoveReasoningMessages(input); var ctx = input.ToChatRequestContext(jsonSerializerOptions, streamOptions); // AG-UI continuation is keyed by thread id. When the client does not supply one, generate a @@ -179,6 +180,22 @@ private static void MarkFeatureUsed() #pragma warning restore MAAI001 } + /// + /// Removes entries from the request history. Clients such as CopilotKit + /// echo reasoning back on follow-up turns, but the AG-UI SDK cannot map the reasoning role to a + /// and throws. + /// + internal static void RemoveReasoningMessages(RunAgentInput input) + { + for (int i = input.Messages.Count - 1; i >= 0; i--) + { + if (input.Messages[i] is AGUIReasoningMessage) + { + input.Messages.RemoveAt(i); + } + } + } + private static async IAsyncEnumerable SaveSessionAfterStreamingAsync( IAsyncEnumerable events, AIHostAgent hostAgent, diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs index 5c6367ef652..5c48695d908 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs @@ -2,9 +2,12 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using AGUI.Abstractions; +using AGUI.Server; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.AI; @@ -223,6 +226,35 @@ public void MapAGUIServer_WithNullAgentBuilder_ThrowsArgumentNullException() endpointsMock.Object.MapAGUIServer((IHostedAgentBuilder)null!, "/api/agent")); } + [Fact] + public void RemoveReasoningMessages_FollowUpTurnWithReasoning_AllowsChatRequestConversion() + { + // Arrange + Microsoft.AspNetCore.Http.Json.JsonOptions jsonOptions = new(); + new ConfigureAGUIJsonOptions().Configure(jsonOptions); + const string Body = """ + { + "threadId": "t1", "runId": "r2", "state": {}, "tools": [], "context": [], "forwardedProps": {}, + "messages": [ + { "id": "u1", "role": "user", "content": "hi" }, + { "id": "rs1", "role": "reasoning", "content": "thinking..." }, + { "id": "a1", "role": "assistant", "content": "hello" }, + { "id": "u2", "role": "user", "content": "follow up" } + ] + } + """; + RunAgentInput input = JsonSerializer.Deserialize(Body, jsonOptions.SerializerOptions)!; + Assert.Throws(() => input.ToChatRequestContext(jsonOptions.SerializerOptions, null)); + + // Act + AGUIEndpointRouteBuilderExtensions.RemoveReasoningMessages(input); + var ctx = input.ToChatRequestContext(jsonOptions.SerializerOptions, null); + + // Assert + Assert.Equal(["u1", "a1", "u2"], input.Messages.Select(m => m.Id)); + Assert.Equal(3, ctx.Messages.Count); + } + private sealed class TestAgent : AIAgent { protected override Task RunCoreAsync(IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();