Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ public static IEndpointConventionBuilder MapAGUIServer(
var streamOptions = context.GetEndpoint()?.Metadata.GetMetadata<AGUIStreamOptions>()
?? context.RequestServices.GetService<IOptions<AGUIStreamOptions>>()?.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
Expand Down Expand Up @@ -179,6 +180,22 @@ private static void MarkFeatureUsed()
#pragma warning restore MAAI001
}

/// <summary>
/// Removes <see cref="AGUIReasoningMessage"/> entries from the request history. Clients such as CopilotKit
/// echo reasoning back on follow-up turns, but the AG-UI SDK cannot map the <c>reasoning</c> role to a
/// <see cref="ChatMessage"/> and throws.
/// </summary>
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<BaseEvent> SaveSessionAfterStreamingAsync(
IAsyncEnumerable<BaseEvent> events,
AIHostAgent hostAgent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<RunAgentInput>(Body, jsonOptions.SerializerOptions)!;
Assert.Throws<InvalidOperationException>(() => 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<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
Expand Down
Loading