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/WorkflowHostAgent.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs
index 8e8012f5bb..4e5ee86070 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs
@@ -18,11 +18,12 @@ internal sealed class WorkflowHostAgent : AIAgent
private readonly string? _id;
private readonly CheckpointManager? _checkpointManager;
private readonly IWorkflowExecutionEnvironment _executionEnvironment;
+ private readonly bool _includeExceptionDetails;
private readonly Task _describeTask;
private readonly ConcurrentDictionary _assignedRunIds = [];
- public WorkflowHostAgent(Workflow workflow, string? id = null, string? name = null, string? description = null, CheckpointManager? checkpointManager = null, IWorkflowExecutionEnvironment? executionEnvironment = null)
+ public WorkflowHostAgent(Workflow workflow, string? id = null, string? name = null, string? description = null, CheckpointManager? checkpointManager = null, IWorkflowExecutionEnvironment? executionEnvironment = null, bool includeExceptionDetails = false)
{
this._workflow = Throw.IfNull(workflow);
@@ -30,6 +31,7 @@ public WorkflowHostAgent(Workflow workflow, string? id = null, string? name = nu
? InProcessExecution.Concurrent
: InProcessExecution.OffThread);
this._checkpointManager = checkpointManager;
+ this._includeExceptionDetails = includeExceptionDetails;
this._id = id;
this.Name = name;
@@ -61,10 +63,10 @@ private async ValueTask ValidateWorkflowAsync()
protocol.ThrowIfNotChatProtocol();
}
- public override AgentThread GetNewThread() => new WorkflowThread(this._workflow, this.GenerateNewId(), this._executionEnvironment, this._checkpointManager);
+ public override AgentThread GetNewThread() => new WorkflowThread(this._workflow, this.GenerateNewId(), this._executionEnvironment, this._checkpointManager, this._includeExceptionDetails);
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
- => new WorkflowThread(this._workflow, serializedThread, this._executionEnvironment, this._checkpointManager, jsonSerializerOptions);
+ => new WorkflowThread(this._workflow, serializedThread, this._executionEnvironment, this._checkpointManager, this._includeExceptionDetails, jsonSerializerOptions);
private ValueTask UpdateThreadAsync(IEnumerable messages, AgentThread? thread = null, CancellationToken cancellationToken = default)
{
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostingExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostingExtensions.cs
index d48e99bf6e..c217039a35 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostingExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostingExtensions.cs
@@ -21,6 +21,8 @@ public static class WorkflowHostingExtensions
/// Specify the execution environment to use when running the workflows. See
/// , and
/// for the in-process environments.
+ /// If , will include
+ /// in the representing the workflow error.
///
public static AIAgent AsAgent(
this Workflow workflow,
@@ -28,9 +30,10 @@ public static AIAgent AsAgent(
string? name = null,
string? description = null,
CheckpointManager? checkpointManager = null,
- IWorkflowExecutionEnvironment? executionEnvironment = null)
+ IWorkflowExecutionEnvironment? executionEnvironment = null,
+ bool includeExceptionDetails = false)
{
- return new WorkflowHostAgent(workflow, id, name, description, checkpointManager, executionEnvironment);
+ return new WorkflowHostAgent(workflow, id, name, description, checkpointManager, executionEnvironment, includeExceptionDetails);
}
internal static FunctionCallContent ToFunctionCall(this ExternalRequest request)
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs
index d27de6bd5c..94144831e0 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;
@@ -17,14 +18,16 @@ internal sealed class WorkflowThread : AgentThread
{
private readonly Workflow _workflow;
private readonly IWorkflowExecutionEnvironment _executionEnvironment;
+ private readonly bool _includeExceptionDetails;
private readonly CheckpointManager _checkpointManager;
private readonly InMemoryCheckpointManager? _inMemoryCheckpointManager;
- public WorkflowThread(Workflow workflow, string runId, IWorkflowExecutionEnvironment executionEnvironment, CheckpointManager? checkpointManager = null)
+ public WorkflowThread(Workflow workflow, string runId, IWorkflowExecutionEnvironment executionEnvironment, CheckpointManager? checkpointManager = null, bool includeExceptionDetails = false)
{
this._workflow = Throw.IfNull(workflow);
this._executionEnvironment = Throw.IfNull(executionEnvironment);
+ this._includeExceptionDetails = includeExceptionDetails;
// If the user provided an external checkpoint manager, use that, otherwise rely on an in-memory one.
// TODO: Implement persist-only-last functionality for in-memory checkpoint manager, to avoid unbounded
@@ -35,7 +38,7 @@ public WorkflowThread(Workflow workflow, string runId, IWorkflowExecutionEnviron
this.MessageStore = new WorkflowMessageStore();
}
- public WorkflowThread(Workflow workflow, JsonElement serializedThread, IWorkflowExecutionEnvironment executionEnvironment, CheckpointManager? checkpointManager = null, JsonSerializerOptions? jsonSerializerOptions = null)
+ public WorkflowThread(Workflow workflow, JsonElement serializedThread, IWorkflowExecutionEnvironment executionEnvironment, CheckpointManager? checkpointManager = null, bool includeExceptionDetails = false, JsonSerializerOptions? jsonSerializerOptions = null)
{
this._workflow = Throw.IfNull(workflow);
this._executionEnvironment = Throw.IfNull(executionEnvironment);
@@ -80,7 +83,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 +92,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 +157,29 @@ 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)
+ {
+ string message = this._includeExceptionDetails
+ ? exception.Message
+ : "An error occurred while executing the workflow.";
+
+ ErrorContent errorContent = new(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..be3c96d9f5
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowHostSmokeTests.cs
@@ -0,0 +1,114 @@
+// 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();
+ }
+
+ protected override async Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ return await this.RunStreamingAsync(messages, thread, options, cancellationToken)
+ .ToAgentRunResponseAsync(cancellationToken);
+ }
+
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(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();
+ }
+
+ [Theory]
+ [InlineData(true, true)]
+ [InlineData(true, false)]
+ [InlineData(false, true)]
+ [InlineData(false, false)]
+ public async Task Test_AsAgent_ErrorContentStreamedOutAsync(bool includeExceptionDetails, bool failByThrowing)
+ {
+ string expectedMessage = !failByThrowing || includeExceptionDetails
+ ? "Simulated agent failure."
+ : "An error occurred while executing the workflow.";
+
+ // Arrange is done by the caller.
+ Workflow workflow = CreateWorkflow(failByThrowing);
+
+ // Act
+ List updates = await workflow.AsAgent("WorkflowAgent", includeExceptionDetails: includeExceptionDetails)
+ .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(expectedMessage);
+ hadErrorContent = true;
+ }
+ }
+
+ hadErrorContent.Should().BeTrue();
+ }
+}