Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@ namespace Microsoft.Agents.AI.Workflows;
/// <param name="e">
/// Optionally, the <see cref="Exception"/> representing the error.
/// </param>
public class WorkflowErrorEvent(Exception? e) : WorkflowEvent(e);
public class WorkflowErrorEvent(Exception? e) : WorkflowEvent(e)
{
/// <summary>
/// Gets the exception that caused the current operation to fail, if one occurred.
/// </summary>
public Exception? Exception => this.Data as Exception;
}
23 changes: 20 additions & 3 deletions dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand All @@ -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());
Expand Down Expand Up @@ -153,10 +155,25 @@ IAsyncEnumerable<AgentRunResponseUpdate> 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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// 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<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return await this.RunStreamingAsync(messages, thread, options, cancellationToken)
.ToAgentRunResponseAsync(cancellationToken);
}

public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> 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)
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the C# coding guidelines, prefer defining variables using types rather than var to help users understand the types involved.

Copilot generated this review using guidance from repository custom instructions.
{
ExecutorBinding agent = new AlwaysFailsAIAgent(failByThrowing).BindAsExecutor(emitEvents: true);

return new WorkflowBuilder(agent).Build();
}

private async static Task InvokeAsAgentAndProcessResponseAsync(Workflow workflow)
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method signature does not follow the C# coding guideline for async methods. Async methods that return Task should have the 'Async' suffix in their name.

Copilot generated this review using guidance from repository custom instructions.
{
List<AgentRunResponseUpdate> updates = await workflow.AsAgent("WorkflowAgent")
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the C# coding guidelines, prefer defining variables using types rather than var to help users understand the types involved.

Copilot generated this review using guidance from repository custom instructions.
.RunStreamingAsync(new ChatMessage(ChatRole.User, "Hello"))
.ToListAsync();

bool hadErrorContent = false;
foreach (AgentRunResponseUpdate update in updates)
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the C# coding guidelines for unit tests, Arrange, Act, and Assert comments should be added for each test to clearly separate the test phases.

Copilot generated this review using guidance from repository custom instructions.
{
if (update.Contents.Any())
{
// We should expect a single update which contains the error content.
update.Contents.Should().ContainSingle()
.Which.Should().BeOfType<ErrorContent>()
.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));
}
Loading