.NET: Fix to emit WorkflowStartedEvent during workflow execution (#4514)

* Fix bug to emit WorkflowStartedEvent during workflow execution

* Updated based on PR comments
This commit is contained in:
Peter Ibekwe
2026-03-12 08:45:17 -07:00
committed by GitHub
Unverified
parent bcb55b4a98
commit aa2ff672fb
3 changed files with 79 additions and 2 deletions
@@ -72,6 +72,9 @@ internal sealed class LockstepRunEventStream : IRunEventStream
this.RunStatus = RunStatus.Running;
runActivity?.AddEvent(new ActivityEvent(EventNames.WorkflowStarted));
// Emit WorkflowStartedEvent to the event stream for consumers
eventSink.Enqueue(new WorkflowStartedEvent());
do
{
while (this._stepRunner.HasUnprocessedMessages &&
@@ -88,9 +88,16 @@ internal sealed class StreamingRunEventStream : IRunEventStream
// Run all available supersteps continuously
// Events are streamed out in real-time as they happen via the event handler
while (this._stepRunner.HasUnprocessedMessages && !linkedSource.Token.IsCancellationRequested)
if (this._stepRunner.HasUnprocessedMessages)
{
await this._stepRunner.RunSuperStepAsync(linkedSource.Token).ConfigureAwait(false);
// Emit WorkflowStartedEvent only when there's actual work to process
// This avoids spurious events on timeout-only loop iterations
await this._eventChannel.Writer.WriteAsync(new WorkflowStartedEvent(), linkedSource.Token).ConfigureAwait(false);
while (this._stepRunner.HasUnprocessedMessages && !linkedSource.Token.IsCancellationRequested)
{
await this._stepRunner.RunSuperStepAsync(linkedSource.Token).ConfigureAwait(false);
}
}
// Update status based on what's waiting
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Workflows.UnitTests;
@@ -88,4 +90,69 @@ public class AgentEventsTests
Assert.Same(response, evt.Response);
Assert.Same(response, evt.Data);
}
/// <summary>
/// Verifies that WorkflowStartedEvent is emitted first before any SuperStepStartedEvent.
/// </summary>
[Fact]
public async Task StreamingRun_WorkflowStartedEvent_ShouldBeEmittedBefore_SuperStepStartedAsync()
{
// Arrange
TestEchoAgent agent = new("test-agent");
Workflow workflow = AgentWorkflowBuilder.BuildSequential(agent);
ChatMessage inputMessage = new(ChatRole.User, "Hello");
// Act
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, new List<ChatMessage> { inputMessage });
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
List<WorkflowEvent> events = [];
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
events.Add(evt);
}
// Assert
events.Should().NotBeEmpty();
List<WorkflowStartedEvent> startedEvents = events.OfType<WorkflowStartedEvent>().ToList();
startedEvents.Should().NotBeEmpty();
WorkflowStartedEvent? firstStartedEvent = startedEvents.FirstOrDefault();
SuperStepStartedEvent? firstSuperStepEvent = events.OfType<SuperStepStartedEvent>().FirstOrDefault();
firstSuperStepEvent.Should().NotBeNull();
int startedIndex = events.IndexOf(firstStartedEvent!);
int superStepIndex = events.IndexOf(firstSuperStepEvent!);
startedIndex.Should().BeLessThan(superStepIndex);
}
/// <summary>
/// Verifies that WorkflowStartedEvent is emitted using Lockstep execution mode.
/// </summary>
[Fact]
public async Task StreamingRun_LockstepExecution_ShouldEmit_WorkflowStartedEventAsync()
{
// Arrange
TestEchoAgent agent = new("test-agent");
Workflow workflow = AgentWorkflowBuilder.BuildSequential(agent);
ChatMessage inputMessage = new(ChatRole.User, "Hello");
// Act: Use Lockstep execution mode
await using StreamingRun run = await InProcessExecution.Lockstep.RunStreamingAsync(workflow, new List<ChatMessage> { inputMessage });
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
List<WorkflowEvent> events = [];
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
events.Add(evt);
}
// Assert
events.Should().NotBeEmpty();
List<WorkflowStartedEvent> startedEvents = events.OfType<WorkflowStartedEvent>().ToList();
startedEvents.Should().NotBeEmpty();
}
}