diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentResponseUpdateExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentResponseUpdateExtensions.cs index 7930cba8380..f6c0cb1ac19 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentResponseUpdateExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentResponseUpdateExtensions.cs @@ -305,7 +305,14 @@ private static StreamingWorkflowEventComplete CreateWorkflowEventResponse(Workfl if (JsonSerializer.IsReflectionEnabledByDefault) { JsonElement? dataElement = null; - if (workflowEvent.Data is not null) + if (workflowEvent.Data is Exception exception) + { + // Exceptions cannot go through System.Text.Json: Exception.TargetSite is a MethodBase, + // which throws NotSupportedException and would surface as a serialization error instead + // of the actual failure. Report the message only; the stack trace is not for clients. + dataElement = JsonSerializer.SerializeToElement(exception.Message, OpenAIHostingJsonContext.Default.String); + } + else if (workflowEvent.Data is not null) { dataElement = JsonSerializer.SerializeToElement(workflowEvent.Data, OpenAIHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/WorkflowErrorEventStreamingTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/WorkflowErrorEventStreamingTests.cs new file mode 100644 index 00000000000..6ac22ecf3a8 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/WorkflowErrorEventStreamingTests.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses; +using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; + +/// +/// Regression tests for a failing workflow used as an agent: the emitted update carries a +/// as its raw representation, whose data is the original +/// . Streaming that update must not try to JSON-serialize the exception, +/// which fails on Exception.TargetSite and hides the actual workflow failure. +/// +public sealed class WorkflowErrorEventStreamingTests +{ + [Fact] + public async Task WorkflowErrorEvent_IsStreamedWithoutSerializationFailureAsync() + { + // Arrange: the update a failing workflow-as-agent produces. + const string ErrorMessage = "Executor 'AggregatorExecutor' cannot send messages of type 'RawAggregate'."; + var update = new AgentResponseUpdate(ChatRole.Assistant, [new ErrorContent(ErrorMessage)]) + { + RawRepresentation = new WorkflowErrorEvent(Thrown(ErrorMessage)) + }; + + var request = new CreateResponse { Input = "Hello", Stream = true }; + var context = new AgentInvocationContext(new IdGenerator("resp_1", "conv_1")); + + // Act + List events = []; + await foreach (var evt in ToAsyncEnumerableAsync(update).ToStreamingResponseAsync(request, context)) + { + events.Add(evt); + } + + // Assert: the workflow event is streamed and carries the real failure message. + var workflowEvent = Assert.Single(events.OfType()); + Assert.NotNull(workflowEvent.Data); + JsonElement data = workflowEvent.Data.Value; + Assert.Equal(nameof(WorkflowErrorEvent), data.GetProperty("event_type").GetString()); + Assert.Equal(ErrorMessage, data.GetProperty("data").GetString()); + } + + /// Returns an actually-thrown exception, so TargetSite is populated. + private static InvalidOperationException Thrown(string message) + { + try + { + throw new InvalidOperationException(message); + } + catch (InvalidOperationException ex) + { + return ex; + } + } + + private static async IAsyncEnumerable ToAsyncEnumerableAsync(params AgentResponseUpdate[] updates) + { + foreach (var update in updates) + { + yield return update; + } + + await Task.CompletedTask; + } +}