From 8f8eb9415bb967da8c6b3df33d7864bb580da878 Mon Sep 17 00:00:00 2001 From: atty57 <99388680+atty57@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:40:50 -0400 Subject: [PATCH] .NET: Fix AG-UI SSE events written with explicit nulls (#7919) ConfigureAGUIJsonOptions appended its resolvers to the ASP.NET Core TypeInfoResolverChain, which already has a reflection-based resolver at its head. Both appended resolvers were therefore unreachable for every type reflection can handle, so the AG-UI wire-format rules never applied. This was invisible with AGUI 0.0.5, whose omit-empty rule rides on attributes the reflection resolver honors. AGUI 0.0.6 moved that rule to a type-info modifier on AGUIJsonUtilities.DefaultTypeInfoResolver, which only applies when that resolver is actually consulted. Bumping the pin without fixing the ordering makes every SSE event go out with explicit nulls for its optional fields ("parentRunId": null and similar), which @ag-ui/client rejects with a ZodError. Insert both resolvers ahead of the reflection resolver and compose AGUIJsonUtilities.DefaultTypeInfoResolver rather than the context directly, as the AGUI docs direct. The AGUI pin moves to 0.0.6 in the same change since DefaultTypeInfoResolver is new in that version. Adds a regression test asserting the serialized event carries no explicit nulls; it fails against the previous ordering. --- dotnet/Directory.Packages.props | 10 +++++----- .../ConfigureAGUIJsonOptions.cs | 19 +++++++++++++------ .../ConfigureAGUIJsonOptionsTests.cs | 13 +++++++++++++ 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 9b95114b167..f7ca336e6fc 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -55,11 +55,11 @@ - - - - - + + + + + diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/ConfigureAGUIJsonOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/ConfigureAGUIJsonOptions.cs index 27a23ed4d62..78eccec92fa 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/ConfigureAGUIJsonOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/ConfigureAGUIJsonOptions.cs @@ -16,11 +16,18 @@ public void Configure(JsonOptions options) { var chain = options.SerializerOptions.TypeInfoResolverChain; - // Agent Framework abstractions first to ensure M.E.AI types are handled via its resolver, - // followed by the AG-UI wire-format resolver for protocol types (the AG-UI context is needed - // on the net10 TypedResults.ServerSentEvents path, which serializes events through the - // configured ASP.NET Core JsonSerializerOptions). - chain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!); - chain.Add(AGUIJsonSerializerContext.Default.Options.TypeInfoResolver!); + // Both resolvers must go in front of the reflection-based resolver ASP.NET Core already + // placed at the head of the chain; appending leaves them unreachable for every type that + // reflection can handle, which silently discards the AG-UI wire-format rules. + // + // AGUIJsonUtilities.DefaultTypeInfoResolver is the AG-UI context plus the modifier that omits + // properties with no value. Without it the SSE events go out with explicit nulls for their + // optional fields ("parentRunId": null and similar), which receiving SDKs reject. The AG-UI + // resolver is needed on the net10 TypedResults.ServerSentEvents path, which serializes events + // through the configured ASP.NET Core JsonSerializerOptions. + // + // Agent Framework abstractions follow so that M.E.AI types are handled via its resolver. + chain.Insert(0, AGUIJsonUtilities.DefaultTypeInfoResolver); + chain.Insert(1, AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/ConfigureAGUIJsonOptionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/ConfigureAGUIJsonOptionsTests.cs index 9ffe491bf63..fd9f6ae742b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/ConfigureAGUIJsonOptionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/ConfigureAGUIJsonOptionsTests.cs @@ -33,6 +33,19 @@ public void AddAGUIServer_ConfiguresJsonOptions_ResolvesAgentAbstractionsTypes() options.Invoking(o => o.GetTypeInfo(typeof(ChatMessage))).Should().NotThrow(); } + [Fact] + public void AddAGUIServer_ConfiguresJsonOptions_OmitsOptionalFieldsWithNoValue() + { + JsonSerializerOptions options = BuildConfiguredSerializerOptions(); + + string json = JsonSerializer.Serialize(new RunStartedEvent { ThreadId = "thread", RunId = "run" }, options); + + // AG-UI receivers declare the optional event fields as optional, not nullable, so writing them + // as explicit nulls fails validation client-side. The configured options must omit them. + json.Should().NotContain("null"); + json.Should().Be("""{"type":"RUN_STARTED","threadId":"thread","runId":"run"}"""); + } + private static JsonSerializerOptions BuildConfiguredSerializerOptions() { ServiceCollection services = new();