diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 9b95114b16..f7ca336e6f 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 27a23ed4d6..78eccec92f 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 9ffe491bf6..fd9f6ae742 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();