diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowErrorEvent.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowErrorEvent.cs
index 7af7efd0b9..aec9e8130c 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowErrorEvent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowErrorEvent.cs
@@ -10,4 +10,10 @@ namespace Microsoft.Agents.AI.Workflows;
///
/// Optionally, the representing the error.
///
-public class WorkflowErrorEvent(Exception? e) : WorkflowEvent(e);
+public class WorkflowErrorEvent(Exception? e) : WorkflowEvent(e)
+{
+ ///
+ /// Gets the exception that caused the current operation to fail, if one occurred.
+ ///
+ public Exception? Exception => this.Data as Exception;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs
index d27de6bd5c..10d82e484f 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
@@ -80,7 +81,7 @@ public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptio
return marshaller.Marshal(info);
}
- public AgentRunResponseUpdate CreateUpdate(string responseId, params AIContent[] parts)
+ public AgentRunResponseUpdate CreateUpdate(string responseId, object raw, params AIContent[] parts)
{
Throw.IfNullOrEmpty(parts);
@@ -89,7 +90,8 @@ public AgentRunResponseUpdate CreateUpdate(string responseId, params AIContent[]
CreatedAt = DateTimeOffset.UtcNow,
MessageId = Guid.NewGuid().ToString("N"),
Role = ChatRole.Assistant,
- ResponseId = responseId
+ ResponseId = responseId,
+ RawRepresentation = raw
};
this.MessageStore.AddMessages(update.ToChatMessage());
@@ -153,10 +155,25 @@ IAsyncEnumerable InvokeStageAsync(
case RequestInfoEvent requestInfo:
FunctionCallContent fcContent = requestInfo.Request.ToFunctionCall();
- AgentRunResponseUpdate update = this.CreateUpdate(this.LastResponseId, fcContent);
+ AgentRunResponseUpdate update = this.CreateUpdate(this.LastResponseId, evt, fcContent);
yield return update;
break;
+ case WorkflowErrorEvent workflowError:
+ Exception? exception = workflowError.Exception;
+ if (exception is TargetInvocationException tie && tie.InnerException != null)
+ {
+ exception = tie.InnerException;
+ }
+
+ if (exception != null)
+ {
+ ErrorContent errorContent = new(exception.Message);
+ yield return this.CreateUpdate(this.LastResponseId, evt, errorContent);
+ }
+
+ break;
+
case SuperStepCompletedEvent stepCompleted:
this.LastCheckpoint = stepCompleted.CompletionInfo?.Checkpoint;
goto default;
diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowHostSmokeTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowHostSmokeTests.cs
new file mode 100644
index 0000000000..133fc78e47
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowHostSmokeTests.cs
@@ -0,0 +1,112 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Workflows.UnitTests;
+
+public sealed class ExpectedException : Exception
+{
+ public ExpectedException(string message)
+ : base(message)
+ {
+ }
+
+ public ExpectedException() : base()
+ {
+ }
+
+ public ExpectedException(string? message, Exception? innerException) : base(message, innerException)
+ {
+ }
+}
+
+public class WorkflowHostSmokeTests
+{
+ private sealed class AlwaysFailsAIAgent(bool failByThrowing) : AIAgent
+ {
+ private sealed class Thread : InMemoryAgentThread
+ {
+ public Thread() { }
+
+ public Thread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
+ : base(serializedThread, jsonSerializerOptions)
+ { }
+ }
+
+ public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
+ {
+ return new Thread(serializedThread, jsonSerializerOptions);
+ }
+
+ public override AgentThread GetNewThread()
+ {
+ return new Thread();
+ }
+
+ public override async Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ return await this.RunStreamingAsync(messages, thread, options, cancellationToken)
+ .ToAgentRunResponseAsync(cancellationToken);
+ }
+
+ public override async IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ const string ErrorMessage = "Simulated agent failure.";
+ if (failByThrowing)
+ {
+ throw new ExpectedException(ErrorMessage);
+ }
+
+ yield return new AgentRunResponseUpdate(ChatRole.Assistant, [new ErrorContent(ErrorMessage)]);
+ }
+ }
+
+ private static Workflow CreateWorkflow(bool failByThrowing)
+ {
+ ExecutorBinding agent = new AlwaysFailsAIAgent(failByThrowing).BindAsExecutor(emitEvents: true);
+
+ return new WorkflowBuilder(agent).Build();
+ }
+
+ private async static Task InvokeAsAgentAndProcessResponseAsync(Workflow workflow)
+ {
+ // Arrange is done by the caller.
+
+ // Act
+ List updates = await workflow.AsAgent("WorkflowAgent")
+ .RunStreamingAsync(new ChatMessage(ChatRole.User, "Hello"))
+ .ToListAsync();
+
+ // Assert
+ bool hadErrorContent = false;
+ foreach (AgentRunResponseUpdate update in updates)
+ {
+ if (update.Contents.Any())
+ {
+ // We should expect a single update which contains the error content.
+ update.Contents.Should().ContainSingle()
+ .Which.Should().BeOfType()
+ .Which.Message.Should().Be("Simulated agent failure.");
+ hadErrorContent = true;
+ }
+ }
+
+ hadErrorContent.Should().BeTrue();
+ }
+
+ [Fact]
+ public Task Test_AsAgent_ErrorContentStreamedOutAsync()
+ => InvokeAsAgentAndProcessResponseAsync(CreateWorkflow(failByThrowing: false));
+
+ [Fact]
+ public Task Test_AsAgent_ExceptionToErrorContentAsync()
+ => InvokeAsAgentAndProcessResponseAsync(CreateWorkflow(failByThrowing: true));
+}