diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/AgentResponseEvent.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/AgentResponseEvent.cs
index a6c0b22525..21e45679eb 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/AgentResponseEvent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/AgentResponseEvent.cs
@@ -7,14 +7,14 @@ namespace Microsoft.Agents.AI.Workflows;
///
/// Represents an event triggered when an agent produces a response.
///
-public class AgentResponseEvent : ExecutorEvent
+public class AgentResponseEvent : WorkflowOutputEvent
{
///
/// Initializes a new instance of the class.
///
/// The identifier of the executor that generated this event.
/// The agent response.
- public AgentResponseEvent(string executorId, AgentResponse response) : base(executorId, data: response)
+ public AgentResponseEvent(string executorId, AgentResponse response) : base(response, executorId)
{
this.Response = Throw.IfNull(response);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/AgentResponseUpdateEvent.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/AgentResponseUpdateEvent.cs
index 939e7a67e8..bfe7cfb447 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/AgentResponseUpdateEvent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/AgentResponseUpdateEvent.cs
@@ -8,14 +8,14 @@ namespace Microsoft.Agents.AI.Workflows;
///
/// Represents an event triggered when an agent run produces an update.
///
-public class AgentResponseUpdateEvent : ExecutorEvent
+public class AgentResponseUpdateEvent : WorkflowOutputEvent
{
///
/// Initializes a new instance of the class.
///
/// The identifier of the executor that generated this event.
/// The agent run response update.
- public AgentResponseUpdateEvent(string executorId, AgentResponseUpdate update) : base(executorId, data: update)
+ public AgentResponseUpdateEvent(string executorId, AgentResponseUpdate update) : base(update, executorId)
{
this.Update = Throw.IfNull(update);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/InProc/InProcessRunnerContext.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/InProc/InProcessRunnerContext.cs
index ac0baf157f..e75ae88649 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/InProc/InProcessRunnerContext.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/InProc/InProcessRunnerContext.cs
@@ -233,6 +233,20 @@ private async ValueTask YieldOutputAsync(string sourceId, object output, Cancell
this.CheckEnded();
Throw.IfNull(output);
+ // Special-case AgentResponse and AgentResponseUpdate to create their specific event types
+ // and bypass the output filter (for backwards compatibility - these events were previously
+ // emitted directly via AddEventAsync without filtering)
+ if (output is AgentResponseUpdate update)
+ {
+ await this.AddEventAsync(new AgentResponseUpdateEvent(sourceId, update), cancellationToken).ConfigureAwait(false);
+ return;
+ }
+ else if (output is AgentResponse response)
+ {
+ await this.AddEventAsync(new AgentResponseEvent(sourceId, response), cancellationToken).ConfigureAwait(false);
+ return;
+ }
+
Executor sourceExecutor = await this.EnsureExecutorAsync(sourceId, tracer: null, cancellationToken).ConfigureAwait(false);
if (!sourceExecutor.CanOutput(output.GetType()))
{
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs
index 97c493d045..d9bd6001cf 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs
@@ -181,7 +181,7 @@ await this.EnsureSessionAsync(context, cancellationToken).ConfigureAwait(false),
List updates = [];
await foreach (AgentResponseUpdate update in agentStream.ConfigureAwait(false))
{
- await context.AddEventAsync(new AgentResponseUpdateEvent(this.Id, update), cancellationToken).ConfigureAwait(false);
+ await context.YieldOutputAsync(update, cancellationToken).ConfigureAwait(false);
ExtractUnservicedRequests(update.Contents);
updates.Add(update);
}
@@ -201,7 +201,7 @@ await this.EnsureSessionAsync(context, cancellationToken).ConfigureAwait(false),
if (this._options.EmitAgentResponseEvents == true)
{
- await context.AddEventAsync(new AgentResponseEvent(this.Id, response), cancellationToken).ConfigureAwait(false);
+ await context.YieldOutputAsync(response, cancellationToken).ConfigureAwait(false);
}
if (userInputRequests.Count > 0 || functionCalls.Count > 0)
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs
index 8c608090f3..1fab6a4984 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs
@@ -109,7 +109,7 @@ async Task AddUpdateAsync(AgentResponseUpdate update, CancellationToken cancella
updates.Add(update);
if (handoffState.TurnToken.EmitEvents is true)
{
- await context.AddEventAsync(new AgentResponseUpdateEvent(this.Id, update), cancellationToken).ConfigureAwait(false);
+ await context.YieldOutputAsync(update, cancellationToken).ConfigureAwait(false);
}
}
});
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowOutputEvent.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowOutputEvent.cs
index 760f2ae029..f0fe884f6d 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowOutputEvent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowOutputEvent.cs
@@ -2,23 +2,37 @@
using System;
using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI.Workflows;
///
/// Event triggered when a workflow executor yields output.
///
-public sealed class WorkflowOutputEvent : WorkflowEvent
+[JsonDerivedType(typeof(AgentResponseEvent))]
+[JsonDerivedType(typeof(AgentResponseUpdateEvent))]
+public class WorkflowOutputEvent : WorkflowEvent
{
- internal WorkflowOutputEvent(object data, string sourceId) : base(data)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The output data.
+ /// The identifier of the executor that yielded this output.
+ public WorkflowOutputEvent(object data, string executorId) : base(data)
{
- this.SourceId = sourceId;
+ this.ExecutorId = executorId;
}
///
/// The unique identifier of the executor that yielded this output.
///
- public string SourceId { get; }
+ public string ExecutorId { get; }
+
+ ///
+ /// The unique identifier of the executor that yielded this output.
+ ///
+ [Obsolete("Use ExecutorId instead.")]
+ public string SourceId => this.ExecutorId;
///
/// Determines whether the underlying data is of the specified type or a derived type.
diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentEventsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentEventsTests.cs
new file mode 100644
index 0000000000..3e1ecfe9ba
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentEventsTests.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Workflows.UnitTests;
+
+public class AgentEventsTests
+{
+ ///
+ /// Regression test for https://github.com/microsoft/agent-framework/issues/2938
+ /// Verifies that WorkflowOutputEvent is triggered for agent workflows built with
+ /// WorkflowBuilder directly (without using AgentWorkflowBuilder helpers).
+ ///
+ [Fact]
+ public async Task WorkflowBuilder_WithAgents_EmitsWorkflowOutputEventAsync()
+ {
+ // Arrange - Build workflow using WorkflowBuilder directly (not AgentWorkflowBuilder.BuildSequential)
+ AIAgent agent1 = new TestEchoAgent("agent1");
+ AIAgent agent2 = new TestEchoAgent("agent2");
+
+ Workflow workflow = new WorkflowBuilder(agent1)
+ .AddEdge(agent1, agent2)
+ .Build();
+
+ // Act
+ await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, new List { new(ChatRole.User, "Hello") });
+ await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
+
+ List outputEvents = new();
+ List updateEvents = new();
+
+ await foreach (WorkflowEvent evt in run.WatchStreamAsync())
+ {
+ if (evt is AgentResponseUpdateEvent updateEvt)
+ {
+ updateEvents.Add(updateEvt);
+ }
+
+ if (evt is WorkflowOutputEvent outputEvt)
+ {
+ outputEvents.Add(outputEvt);
+ }
+ }
+
+ // Assert - AgentResponseUpdateEvent should now be a WorkflowOutputEvent
+ Assert.NotEmpty(updateEvents);
+ Assert.NotEmpty(outputEvents);
+ // All update events should also be output events (since AgentResponseUpdateEvent now inherits from WorkflowOutputEvent)
+ Assert.All(updateEvents, updateEvt => Assert.Contains(updateEvt, outputEvents));
+ }
+
+ ///
+ /// Verifies that AgentResponseUpdateEvent inherits from WorkflowOutputEvent.
+ ///
+ [Fact]
+ public void AgentResponseUpdateEvent_IsWorkflowOutputEvent()
+ {
+ // Arrange
+ AgentResponseUpdate update = new(ChatRole.Assistant, "test");
+
+ // Act
+ AgentResponseUpdateEvent evt = new("executor1", update);
+
+ // Assert
+ Assert.IsAssignableFrom(evt);
+ Assert.Equal("executor1", evt.ExecutorId);
+ Assert.Same(update, evt.Update);
+ Assert.Same(update, evt.Data);
+ }
+
+ ///
+ /// Verifies that AgentResponseEvent inherits from WorkflowOutputEvent.
+ ///
+ [Fact]
+ public void AgentResponseEvent_IsWorkflowOutputEvent()
+ {
+ // Arrange
+ AgentResponse response = new(new List { new(ChatRole.Assistant, "test") });
+
+ // Act
+ AgentResponseEvent evt = new("executor1", response);
+
+ // Assert
+ Assert.IsAssignableFrom(evt);
+ Assert.Equal("executor1", evt.ExecutorId);
+ Assert.Same(response, evt.Response);
+ Assert.Same(response, evt.Data);
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/04_Simple_Workflow_ExternalRequest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/04_Simple_Workflow_ExternalRequest.cs
index cc417e727a..1bda1e7a77 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/04_Simple_Workflow_ExternalRequest.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/04_Simple_Workflow_ExternalRequest.cs
@@ -45,7 +45,7 @@ public static async ValueTask RunAsync(TextWriter writer, Func RunAsync(TextWriter writer, string
{
foreach (ChatMessage message in messages)
{
- writer.WriteLine($"{output.SourceId}: {message.Text}");
+ writer.WriteLine($"{output.ExecutorId}: {message.Text}");
}
}
else
diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestRunContext.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestRunContext.cs
index 390f99ae05..1a66b59648 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestRunContext.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestRunContext.cs
@@ -50,7 +50,20 @@ public ValueTask AddEventAsync(WorkflowEvent workflowEvent, CancellationToken ca
=> runnerContext.AddEventAsync(workflowEvent, cancellationToken);
public ValueTask YieldOutputAsync(object output, CancellationToken cancellationToken = default)
- => this.AddEventAsync(new WorkflowOutputEvent(output, executorId), cancellationToken);
+ {
+ // Special-case AgentResponse and AgentResponseUpdate to create their specific event types
+ // (consistent with InProcessRunnerContext.YieldOutputAsync)
+ if (output is AgentResponseUpdate update)
+ {
+ return this.AddEventAsync(new AgentResponseUpdateEvent(executorId, update), cancellationToken);
+ }
+ else if (output is AgentResponse response)
+ {
+ return this.AddEventAsync(new AgentResponseEvent(executorId, response), cancellationToken);
+ }
+
+ return this.AddEventAsync(new WorkflowOutputEvent(output, executorId), cancellationToken);
+ }
public ValueTask RequestHaltAsync()
=> this.AddEventAsync(new RequestHaltEvent());
diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestWorkflowContext.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestWorkflowContext.cs
index 61fb4e1970..d92a96e037 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestWorkflowContext.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestWorkflowContext.cs
@@ -41,6 +41,18 @@ public ValueTask AddEventAsync(WorkflowEvent workflowEvent, CancellationToken ca
public ValueTask YieldOutputAsync(object output, CancellationToken cancellationToken = default)
{
this.YieldedOutputs.Enqueue(output);
+
+ // Special-case AgentResponse and AgentResponseUpdate to create their specific event types
+ // (consistent with InProcessRunnerContext.YieldOutputAsync)
+ if (output is AgentResponseUpdate update)
+ {
+ return this.AddEventAsync(new AgentResponseUpdateEvent(this._executorId, update), cancellationToken);
+ }
+ else if (output is AgentResponse response)
+ {
+ return this.AddEventAsync(new AgentResponseEvent(this._executorId, response), cancellationToken);
+ }
+
return this.AddEventAsync(new WorkflowOutputEvent(output, this._executorId), cancellationToken);
}