mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
[BREAKING] .NET: Workflow Off-Thread Execution Mode (#1233)
* Updates to async run loop. * fix: Workflow Onwership can be release by nonowner * fix: Incorrect handling of blockOnPending in StreamingRun Depending on whether we are running in streaming on non-streaming mode, we may be using the StreamingRun in different ways. Unfortunately, the only place we can really know what is the actual state of execution is in the RunEventStream implementations. This resulted in blocking where blocking was unneeded and occasionally not-blocking when blocking was needed. The fix is to move the logic of handling this blocking into RunEventStream implementations. * fix: Fix cleanup on error and end run This ensures we clean up the background resources correctly. * fix: Ensure we let the run loop proceed when shutting down * fix: Add timeout for Input Waiting * fix: Make the samples properly clean up `Run`s and `StreamingRun`s * fix: Simplify Declarative Workflow Run disposal pattern * Also fixes missing .Disposal() in Integration tests --------- Co-authored-by: Ben Thomas <ben.thomas@microsoft.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
0113e0466d
commit
7ebe00ec3d
@@ -49,7 +49,7 @@ public static class Program
|
||||
.Build();
|
||||
|
||||
// Execute the workflow
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, "Create a slogan for a new electric SUV that is affordable and fun to drive.");
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, "Create a slogan for a new electric SUV that is affordable and fun to drive.");
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is SloganGeneratedEvent or FeedbackEvent)
|
||||
|
||||
@@ -38,7 +38,7 @@ public static class Program
|
||||
.Build();
|
||||
|
||||
// Execute the workflow
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, "Hello World!"));
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, "Hello World!"));
|
||||
// Must send the turn token to trigger the agents.
|
||||
// The agents are wrapped as executors. When they receive messages,
|
||||
// they will cache the messages and only start processing when they receive a TurnToken.
|
||||
|
||||
+2
-2
@@ -32,7 +32,7 @@ public static class Program
|
||||
var checkpoints = new List<CheckpointInfo>();
|
||||
|
||||
// Execute the workflow and save checkpoints
|
||||
Checkpointed<StreamingRun> checkpointedRun = await InProcessExecution
|
||||
await using Checkpointed<StreamingRun> checkpointedRun = await InProcessExecution
|
||||
.StreamAsync(workflow, NumberSignal.Init, checkpointManager)
|
||||
.ConfigureAwait(false);
|
||||
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
|
||||
@@ -72,7 +72,7 @@ public static class Program
|
||||
Console.WriteLine($"\n\nHydrating a new workflow instance from the {CheckpointIndex + 1}th checkpoint.");
|
||||
CheckpointInfo savedCheckpoint = checkpoints[CheckpointIndex];
|
||||
|
||||
Checkpointed<StreamingRun> newCheckpointedRun =
|
||||
await using Checkpointed<StreamingRun> newCheckpointedRun =
|
||||
await InProcessExecution.ResumeStreamAsync(newWorkflow, savedCheckpoint, checkpointManager, checkpointedRun.Run.RunId)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ public static class Program
|
||||
var checkpoints = new List<CheckpointInfo>();
|
||||
|
||||
// Execute the workflow and save checkpoints
|
||||
Checkpointed<StreamingRun> checkpointedRun = await InProcessExecution
|
||||
await using Checkpointed<StreamingRun> checkpointedRun = await InProcessExecution
|
||||
.StreamAsync(workflow, NumberSignal.Init, checkpointManager)
|
||||
.ConfigureAwait(false);
|
||||
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ public static class Program
|
||||
var checkpoints = new List<CheckpointInfo>();
|
||||
|
||||
// Execute the workflow and save checkpoints
|
||||
Checkpointed<StreamingRun> checkpointedRun = await InProcessExecution
|
||||
await using Checkpointed<StreamingRun> checkpointedRun = await InProcessExecution
|
||||
.StreamAsync(workflow, new SignalWithNumber(NumberSignal.Init), checkpointManager)
|
||||
.ConfigureAwait(false);
|
||||
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false))
|
||||
|
||||
@@ -59,7 +59,7 @@ public static class Program
|
||||
.Build();
|
||||
|
||||
// Execute the workflow in streaming mode
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, "What is temperature?");
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, "What is temperature?");
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is WorkflowOutputEvent output)
|
||||
|
||||
@@ -99,7 +99,7 @@ public static class Program
|
||||
|
||||
// Step 2: Run the workflow
|
||||
Console.WriteLine("\n=== RUNNING WORKFLOW ===\n");
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, rawText);
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, rawText);
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
|
||||
{
|
||||
Console.WriteLine($"Event: {evt}");
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ public static class Program
|
||||
string email = Resources.Read("spam.txt");
|
||||
|
||||
// Execute the workflow
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, email));
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, email));
|
||||
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
|
||||
@@ -78,7 +78,7 @@ public static class Program
|
||||
string email = Resources.Read("ambiguous_email.txt");
|
||||
|
||||
// Execute the workflow
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, email));
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, email));
|
||||
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ public static class Program
|
||||
string email = Resources.Read("email.txt");
|
||||
|
||||
// Execute the workflow
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, email));
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, email));
|
||||
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
|
||||
@@ -64,7 +64,7 @@ internal sealed class Program
|
||||
InputResponse? response = null;
|
||||
do
|
||||
{
|
||||
ExternalRequest? inputRequest = await this.MonitorWorkflowRunAsync(run, response);
|
||||
ExternalRequest? inputRequest = await this.MonitorAndDisposeWorkflowRunAsync(run, response);
|
||||
if (inputRequest is not null)
|
||||
{
|
||||
Notify("\nWORKFLOW: Yield");
|
||||
@@ -83,6 +83,7 @@ internal sealed class Program
|
||||
// Restore the latest checkpoint.
|
||||
Debug.WriteLine($"RESTORE #{this.LastCheckpoint.CheckpointId}");
|
||||
Notify("\nWORKFLOW: Restore");
|
||||
|
||||
run = await InProcessExecution.ResumeStreamAsync(workflow, this.LastCheckpoint, checkpointManager, run.Run.RunId);
|
||||
}
|
||||
else
|
||||
@@ -132,8 +133,10 @@ internal sealed class Program
|
||||
this.FoundryClient = new PersistentAgentsClient(this.FoundryEndpoint, new AzureCliCredential());
|
||||
}
|
||||
|
||||
private async Task<ExternalRequest?> MonitorWorkflowRunAsync(Checkpointed<StreamingRun> run, InputResponse? response = null)
|
||||
private async Task<ExternalRequest?> MonitorAndDisposeWorkflowRunAsync(Checkpointed<StreamingRun> run, InputResponse? response = null)
|
||||
{
|
||||
await using IAsyncDisposable disposeRun = run;
|
||||
|
||||
string? messageId = null;
|
||||
|
||||
await foreach (WorkflowEvent workflowEvent in run.Run.WatchStreamAsync().ConfigureAwait(false))
|
||||
@@ -175,7 +178,7 @@ internal sealed class Program
|
||||
}
|
||||
else
|
||||
{
|
||||
await run.Run.EndRunAsync().ConfigureAwait(false);
|
||||
await run.Run.DisposeAsync().ConfigureAwait(false);
|
||||
return requestInfo.Request;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -45,7 +45,7 @@ internal sealed class Program
|
||||
// Run the workflow, just like any other workflow
|
||||
string input = this.GetWorkflowInput();
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, input);
|
||||
await this.MonitorWorkflowRunAsync(run);
|
||||
await this.MonitorAndDisposeWorkflowRunAsync(run);
|
||||
|
||||
Notify("\nWORKFLOW: Done!");
|
||||
}
|
||||
@@ -70,8 +70,10 @@ internal sealed class Program
|
||||
this.FoundryClient = new PersistentAgentsClient(this.FoundryEndpoint, new AzureCliCredential());
|
||||
}
|
||||
|
||||
private async Task MonitorWorkflowRunAsync(StreamingRun run)
|
||||
private async Task MonitorAndDisposeWorkflowRunAsync(StreamingRun run)
|
||||
{
|
||||
await using IAsyncDisposable disposeRun = run;
|
||||
|
||||
string? messageId = null;
|
||||
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ public static class Program
|
||||
var workflow = await WorkflowHelper.GetWorkflowAsync().ConfigureAwait(false);
|
||||
|
||||
// Execute the workflow
|
||||
StreamingRun handle = await InProcessExecution.StreamAsync(workflow, NumberSignal.Init).ConfigureAwait(false);
|
||||
await using StreamingRun handle = await InProcessExecution.StreamAsync(workflow, NumberSignal.Init).ConfigureAwait(false);
|
||||
await foreach (WorkflowEvent evt in handle.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
switch (evt)
|
||||
|
||||
@@ -33,7 +33,7 @@ public static class Program
|
||||
.BuildAsync<NumberSignal>();
|
||||
|
||||
// Execute the workflow
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, NumberSignal.Init).ConfigureAwait(false);
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, NumberSignal.Init).ConfigureAwait(false);
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is WorkflowOutputEvent outputEvent)
|
||||
|
||||
@@ -57,7 +57,7 @@ public static class Program
|
||||
.Build();
|
||||
|
||||
// Execute the workflow with input data
|
||||
Run run = await InProcessExecution.RunAsync(workflow, "Hello, World!");
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "Hello, World!");
|
||||
foreach (WorkflowEvent evt in run.NewEvents)
|
||||
{
|
||||
if (evt is ExecutorCompletedEvent executorComplete)
|
||||
|
||||
@@ -33,7 +33,7 @@ public static class Program
|
||||
.Build();
|
||||
|
||||
// Execute the workflow with input data
|
||||
Run run = await InProcessExecution.RunAsync(workflow, "Lorem_Ipsum.txt");
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "Lorem_Ipsum.txt");
|
||||
foreach (WorkflowEvent evt in run.NewEvents)
|
||||
{
|
||||
if (evt is WorkflowOutputEvent outputEvent)
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ public static class Program
|
||||
var workflow = builder.Build();
|
||||
|
||||
// Execute the workflow with input data
|
||||
Run run = await InProcessExecution.RunAsync(workflow, "Hello, World!");
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "Hello, World!");
|
||||
foreach (WorkflowEvent evt in run.NewEvents)
|
||||
{
|
||||
if (evt is ExecutorCompletedEvent executorComplete)
|
||||
|
||||
@@ -29,7 +29,7 @@ public static class Program
|
||||
var workflow = builder.Build();
|
||||
|
||||
// Execute the workflow in streaming mode
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, "Hello, World!");
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, "Hello, World!");
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is ExecutorCompletedEvent executorCompleted)
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ public static class Program
|
||||
.Build();
|
||||
|
||||
// Execute the workflow
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, "Hello World!"));
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, "Hello World!"));
|
||||
|
||||
// Must send the turn token to trigger the agents.
|
||||
// The agents are wrapped as executors. When they receive messages,
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ public static class Program
|
||||
{
|
||||
string? lastExecutorId = null;
|
||||
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, messages);
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, messages);
|
||||
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -14,7 +15,7 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
/// <typeparam name="TRun">The type of the underlying workflow run handle.</typeparam>
|
||||
/// <seealso cref="Run"/>
|
||||
/// <seealso cref="StreamingRun"/>
|
||||
public class Checkpointed<TRun>
|
||||
public sealed class Checkpointed<TRun> : IAsyncDisposable
|
||||
{
|
||||
private readonly ICheckpointingHandle _runner;
|
||||
|
||||
@@ -46,6 +47,19 @@ public class Checkpointed<TRun>
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (this.Run is IAsyncDisposable asyncDisposable)
|
||||
{
|
||||
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
else if (this.Run is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ICheckpointingHandle.RestoreCheckpointAsync"/>
|
||||
public ValueTask RestoreCheckpointAsync(CheckpointInfo checkpointInfo, CancellationToken cancellationToken = default)
|
||||
=> this._runner.RestoreCheckpointAsync(checkpointInfo, cancellationToken);
|
||||
|
||||
@@ -11,13 +11,12 @@ using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Execution;
|
||||
|
||||
internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable, IInputCoordinator
|
||||
internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable
|
||||
{
|
||||
private readonly AsyncCoordinator _waitForResponseCoordinator = new();
|
||||
private readonly ISuperStepRunner _stepRunner;
|
||||
private readonly ICheckpointingHandle _checkpointingHandle;
|
||||
|
||||
private readonly LockstepRunEventStream _eventStream;
|
||||
private readonly IRunEventStream _eventStream;
|
||||
private readonly CancellationTokenSource _endRunSource = new();
|
||||
private int _isDisposed;
|
||||
private int _isEventStreamTaken;
|
||||
@@ -29,17 +28,26 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable, I
|
||||
|
||||
this._eventStream = mode switch
|
||||
{
|
||||
//ExecutionMode.OffThread => Not supported yet
|
||||
ExecutionMode.OffThread => new StreamingRunEventStream(stepRunner),
|
||||
ExecutionMode.Subworkflow => new StreamingRunEventStream(stepRunner, disableRunLoop: true),
|
||||
ExecutionMode.Lockstep => new LockstepRunEventStream(stepRunner),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mode), $"Unknown execution mode {mode}")
|
||||
};
|
||||
|
||||
this._eventStream.Start();
|
||||
|
||||
// If there are already unprocessed messages (e.g., from a checkpoint restore that happened
|
||||
// before this handle was created), signal the run loop to start processing them
|
||||
if (stepRunner.HasUnprocessedMessages)
|
||||
{
|
||||
this.SignalInputToRunLoop();
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask<bool> WaitForNextInputAsync(CancellationToken cancellation = default)
|
||||
=> this._waitForResponseCoordinator.WaitForCoordinationAsync(cancellation);
|
||||
//private readonly AsyncCoordinator _waitForResponseCoordinator = new();
|
||||
|
||||
public void ReleaseResponseWaiter() => this._waitForResponseCoordinator.MarkCoordinationPoint();
|
||||
//public ValueTask<bool> WaitForNextInputAsync(CancellationToken cancellation = default)
|
||||
// => this._waitForResponseCoordinator.WaitForCoordinationAsync(cancellation);
|
||||
|
||||
public string RunId => this._stepRunner.RunId;
|
||||
|
||||
@@ -48,28 +56,39 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable, I
|
||||
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellation = default)
|
||||
=> this._eventStream.GetStatusAsync(cancellation);
|
||||
|
||||
public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool breakOnHalt, [EnumeratorCancellation] CancellationToken cancellation = default)
|
||||
public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPendingRequest, [EnumeratorCancellation] CancellationToken cancellation = default)
|
||||
{
|
||||
// Create a linked cancellation token that combines the provided token with the end-run token
|
||||
using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(cancellation, this._endRunSource.Token);
|
||||
|
||||
// Only one enumerator of this is allowed at a time
|
||||
//Debug.Assert(breakOnHalt);
|
||||
// Enforce single active enumerator (this runs when enumeration begins)
|
||||
if (Interlocked.CompareExchange(ref this._isEventStreamTaken, 1, 0) != 0)
|
||||
{
|
||||
throw new InvalidOperationException("The event stream has already been taken. Only one enumerator is allowed at a time.");
|
||||
}
|
||||
|
||||
CancellationTokenSource? linked = null;
|
||||
try
|
||||
{
|
||||
await foreach (WorkflowEvent @event in this._eventStream.TakeEventStreamAsync(linkedSource.Token)
|
||||
.ConfigureAwait(false))
|
||||
linked = CancellationTokenSource.CreateLinkedTokenSource(cancellation, this._endRunSource.Token);
|
||||
var token = linked.Token;
|
||||
|
||||
// Build the inner stream before the loop so synchronous exceptions still release the gate
|
||||
var inner = this._eventStream.TakeEventStreamAsync(blockOnPendingRequest, token);
|
||||
|
||||
await foreach (var ev in inner.WithCancellation(token).ConfigureAwait(false))
|
||||
{
|
||||
yield return @event;
|
||||
// Filter out the RequestHaltEvent, since it is an internal signalling event.
|
||||
if (ev is RequestHaltEvent)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return ev;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Volatile.Write(ref this._isEventStreamTaken, 0);
|
||||
linked?.Dispose();
|
||||
Interlocked.Exchange(ref this._isEventStreamTaken, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +99,7 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable, I
|
||||
{
|
||||
if (message is ExternalResponse response)
|
||||
{
|
||||
// EnqueueResponseAsync marks the coordination point itself
|
||||
// EnqueueResponseAsync handles signaling
|
||||
await this.EnqueueResponseAsync(response, cancellation)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -90,7 +109,8 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable, I
|
||||
bool result = await this._stepRunner.EnqueueMessageAsync(message, cancellation)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
this._waitForResponseCoordinator.MarkCoordinationPoint();
|
||||
// Signal the run loop that new input is available
|
||||
this.SignalInputToRunLoop();
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -104,7 +124,7 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable, I
|
||||
|
||||
if (declaredType != null && typeof(ExternalResponse).IsAssignableFrom(declaredType))
|
||||
{
|
||||
// EnqueueResponseAsync marks the coordination point itself
|
||||
// EnqueueResponseAsync handles signaling
|
||||
await this.EnqueueResponseAsync((ExternalResponse)message, cancellation)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -112,7 +132,7 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable, I
|
||||
}
|
||||
else if (declaredType == null && message is ExternalResponse response)
|
||||
{
|
||||
// EnqueueResponseAsync marks the coordination point itself
|
||||
// EnqueueResponseAsync handles signaling
|
||||
await this.EnqueueResponseAsync(response, cancellation)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -122,7 +142,8 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable, I
|
||||
bool result = await this._stepRunner.EnqueueMessageUntypedAsync(message, declaredType ?? message.GetType(), cancellation)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
this._waitForResponseCoordinator.MarkCoordinationPoint();
|
||||
// Signal the run loop that new input is available
|
||||
this.SignalInputToRunLoop();
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -131,13 +152,13 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable, I
|
||||
{
|
||||
await this._stepRunner.EnqueueResponseAsync(response, cancellation).ConfigureAwait(false);
|
||||
|
||||
this._waitForResponseCoordinator.MarkCoordinationPoint();
|
||||
// Signal the run loop that new input is available
|
||||
this.SignalInputToRunLoop();
|
||||
}
|
||||
|
||||
public ValueTask RequestEndRunAsync()
|
||||
private void SignalInputToRunLoop()
|
||||
{
|
||||
this._endRunSource.Cancel();
|
||||
return this._stepRunner.RequestEndRunAsync();
|
||||
this._eventStream.SignalInput();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
@@ -145,13 +166,32 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable, I
|
||||
if (Interlocked.Exchange(ref this._isDisposed, 1) == 0)
|
||||
{
|
||||
this._endRunSource.Cancel();
|
||||
await this.RequestEndRunAsync().ConfigureAwait(false);
|
||||
|
||||
await this._eventStream.StopAsync().ConfigureAwait(false);
|
||||
await this._stepRunner.RequestEndRunAsync().ConfigureAwait(false);
|
||||
|
||||
this._endRunSource.Dispose();
|
||||
|
||||
await this._eventStream.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask RestoreCheckpointAsync(CheckpointInfo checkpointInfo, CancellationToken cancellationToken = default)
|
||||
=> this._checkpointingHandle.RestoreCheckpointAsync(checkpointInfo, cancellationToken);
|
||||
public async ValueTask RestoreCheckpointAsync(CheckpointInfo checkpointInfo, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Clear buffered events from the channel BEFORE restoring to discard stale events from supersteps
|
||||
// that occurred after the checkpoint we're restoring to
|
||||
// This must happen BEFORE the restore so that events republished during restore aren't cleared
|
||||
if (this._eventStream is StreamingRunEventStream streamingEventStream)
|
||||
{
|
||||
streamingEventStream.ClearBufferedEvents();
|
||||
}
|
||||
|
||||
// Restore the workflow state - this will republish unserviced requests as new events
|
||||
await this._checkpointingHandle.RestoreCheckpointAsync(checkpointInfo, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// After restore, signal the run loop to process any restored messages
|
||||
// This is necessary because ClearBufferedEvents() doesn't signal, and the restored
|
||||
// queued messages won't automatically wake up the run loop
|
||||
this.SignalInputToRunLoop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,12 @@ namespace Microsoft.Agents.AI.Workflows.Execution;
|
||||
internal interface IRunEventStream : IAsyncDisposable
|
||||
{
|
||||
void Start();
|
||||
void SignalInput();
|
||||
|
||||
// this cannot be cancelled
|
||||
ValueTask StopAsync();
|
||||
|
||||
ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellation = default);
|
||||
IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(CancellationToken cancellation = default);
|
||||
|
||||
IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPendingRequest, CancellationToken cancellation = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Execution;
|
||||
|
||||
internal sealed class InputWaiter : IDisposable
|
||||
{
|
||||
private readonly SemaphoreSlim _inputSignal = new(initialCount: 0, 1);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._inputSignal.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signals that new input has been provided and the waiter should continue processing.
|
||||
/// Called by AsyncRunHandle when the user enqueues a message or response.
|
||||
/// </summary>
|
||||
public void SignalInput()
|
||||
{
|
||||
// Release the run loop to process more work
|
||||
// Only release if not already signaled (binary semaphore behavior)
|
||||
try
|
||||
{
|
||||
this._inputSignal.Release();
|
||||
}
|
||||
catch (SemaphoreFullException)
|
||||
{
|
||||
// Swallow for now
|
||||
}
|
||||
}
|
||||
|
||||
public Task WaitForInputAsync(CancellationToken cancellation = default) => this.WaitForInputAsync(null, cancellation);
|
||||
|
||||
public async Task WaitForInputAsync(TimeSpan? timeout = null, CancellationToken cancellation = default)
|
||||
{
|
||||
await this._inputSignal.WaitAsync(timeout ?? TimeSpan.FromMilliseconds(-1), cancellation).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -16,29 +16,45 @@ internal sealed class LockstepRunEventStream : IRunEventStream
|
||||
private static readonly string s_namespace = typeof(LockstepRunEventStream).Namespace!;
|
||||
private static readonly ActivitySource s_activitySource = new(s_namespace);
|
||||
|
||||
private readonly CancellationTokenSource _stopCancellation = new();
|
||||
private readonly InputWaiter _inputWaiter = new();
|
||||
private int _isDisposed;
|
||||
|
||||
private readonly ISuperStepRunner _stepRunner;
|
||||
|
||||
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellation = default) => new(this.RunStatus);
|
||||
|
||||
public LockstepRunEventStream(ISuperStepRunner stepRunner)
|
||||
{
|
||||
this.StepRunner = stepRunner;
|
||||
this._stepRunner = stepRunner;
|
||||
}
|
||||
|
||||
private RunStatus RunStatus { get; set; } = RunStatus.NotStarted;
|
||||
private ISuperStepRunner StepRunner { get; }
|
||||
|
||||
public void Start()
|
||||
{
|
||||
// No-op for lockstep execution
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync([EnumeratorCancellation] CancellationToken cancellation = default)
|
||||
public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPendingRequest, [EnumeratorCancellation] CancellationToken cancellation = default)
|
||||
{
|
||||
#if NET
|
||||
ObjectDisposedException.ThrowIf(Volatile.Read(ref this._isDisposed) == 1, this);
|
||||
#else
|
||||
if (Volatile.Read(ref this._isDisposed) == 1)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(LockstepRunEventStream));
|
||||
}
|
||||
#endif
|
||||
|
||||
CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(this._stopCancellation.Token, cancellation);
|
||||
|
||||
ConcurrentQueue<WorkflowEvent> eventSink = [];
|
||||
|
||||
this.StepRunner.OutgoingEvents.EventRaised += OnWorkflowEventAsync;
|
||||
this._stepRunner.OutgoingEvents.EventRaised += OnWorkflowEventAsync;
|
||||
|
||||
using Activity? activity = s_activitySource.StartActivity(ActivityNames.WorkflowRun);
|
||||
activity?.SetTag(Tags.WorkflowId, this.StepRunner.StartExecutorId).SetTag(Tags.RunId, this.StepRunner.RunId);
|
||||
activity?.SetTag(Tags.WorkflowId, this._stepRunner.StartExecutorId).SetTag(Tags.RunId, this._stepRunner.RunId);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -47,63 +63,81 @@ internal sealed class LockstepRunEventStream : IRunEventStream
|
||||
|
||||
do
|
||||
{
|
||||
// Because we may be yielding out of this function, we need to ensure that the Activity.Current
|
||||
// is set to our activity for the duration of this loop iteration.
|
||||
Activity.Current = activity;
|
||||
while (this._stepRunner.HasUnprocessedMessages &&
|
||||
!linkedSource.Token.IsCancellationRequested)
|
||||
{
|
||||
// Because we may be yielding out of this function, we need to ensure that the Activity.Current
|
||||
// is set to our activity for the duration of this loop iteration.
|
||||
Activity.Current = activity;
|
||||
|
||||
// Drain SuperSteps while there are steps to run
|
||||
try
|
||||
{
|
||||
await this.StepRunner.RunSuperStepAsync(cancellation).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (activity is not null)
|
||||
{
|
||||
activity.AddEvent(new ActivityEvent(EventNames.WorkflowError, tags: new() {
|
||||
{ Tags.ErrorType, ex.GetType().FullName },
|
||||
{ Tags.BuildErrorMessage, ex.Message },
|
||||
}));
|
||||
activity.CaptureException(ex);
|
||||
throw;
|
||||
}
|
||||
// Drain SuperSteps while there are steps to run
|
||||
try
|
||||
{
|
||||
await this._stepRunner.RunSuperStepAsync(linkedSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex) when (activity is not null)
|
||||
{
|
||||
activity.AddEvent(new ActivityEvent(EventNames.WorkflowError, tags: new() {
|
||||
{ Tags.ErrorType, ex.GetType().FullName },
|
||||
{ Tags.BuildErrorMessage, ex.Message },
|
||||
}));
|
||||
activity.CaptureException(ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
if (cancellation.IsCancellationRequested)
|
||||
{
|
||||
yield break; // Exit if cancellation is requested
|
||||
}
|
||||
|
||||
bool hadRequestHaltEvent = false;
|
||||
foreach (WorkflowEvent raisedEvent in Interlocked.Exchange(ref eventSink, []))
|
||||
{
|
||||
if (cancellation.IsCancellationRequested)
|
||||
if (linkedSource.Token.IsCancellationRequested)
|
||||
{
|
||||
yield break; // Exit if cancellation is requested
|
||||
}
|
||||
|
||||
// TODO: Do we actually want to interpret this as a termination request?
|
||||
if (raisedEvent is RequestHaltEvent)
|
||||
bool hadRequestHaltEvent = false;
|
||||
foreach (WorkflowEvent raisedEvent in Interlocked.Exchange(ref eventSink, []))
|
||||
{
|
||||
hadRequestHaltEvent = true;
|
||||
if (linkedSource.Token.IsCancellationRequested)
|
||||
{
|
||||
yield break; // Exit if cancellation is requested
|
||||
}
|
||||
|
||||
// TODO: Do we actually want to interpret this as a termination request?
|
||||
if (raisedEvent is RequestHaltEvent)
|
||||
{
|
||||
hadRequestHaltEvent = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return raisedEvent;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
if (hadRequestHaltEvent || linkedSource.Token.IsCancellationRequested)
|
||||
{
|
||||
yield return raisedEvent;
|
||||
// If we had a completion event, we are done.
|
||||
yield break;
|
||||
}
|
||||
|
||||
this.RunStatus = this._stepRunner.HasUnservicedRequests ? RunStatus.PendingRequests : RunStatus.Idle;
|
||||
}
|
||||
|
||||
if (hadRequestHaltEvent)
|
||||
if (blockOnPendingRequest && this.RunStatus == RunStatus.PendingRequests)
|
||||
{
|
||||
// If we had a completion event, we are done.
|
||||
yield break;
|
||||
try
|
||||
{
|
||||
await this._inputWaiter.WaitForInputAsync(TimeSpan.FromSeconds(1), linkedSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{ }
|
||||
}
|
||||
} while (this.StepRunner.HasUnprocessedMessages &&
|
||||
!cancellation.IsCancellationRequested);
|
||||
} while (!ShouldBreak());
|
||||
|
||||
activity?.AddEvent(new ActivityEvent(EventNames.WorkflowCompleted));
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.RunStatus = this.StepRunner.HasUnservicedRequests ? RunStatus.PendingRequests : RunStatus.Idle;
|
||||
this.StepRunner.OutgoingEvents.EventRaised -= OnWorkflowEventAsync;
|
||||
this.RunStatus = this._stepRunner.HasUnservicedRequests ? RunStatus.PendingRequests : RunStatus.Idle;
|
||||
this._stepRunner.OutgoingEvents.EventRaised -= OnWorkflowEventAsync;
|
||||
}
|
||||
|
||||
ValueTask OnWorkflowEventAsync(object? sender, WorkflowEvent e)
|
||||
@@ -111,7 +145,40 @@ internal sealed class LockstepRunEventStream : IRunEventStream
|
||||
eventSink.Enqueue(e);
|
||||
return default;
|
||||
}
|
||||
|
||||
// If we are Idle or Ended, we should break out of the loop
|
||||
// If we are PendingRequests and not blocking on pending requests, we should break out of the loop
|
||||
// If cancellation is requested, we should break out of the loop
|
||||
bool ShouldBreak() => this.RunStatus is RunStatus.Idle or RunStatus.Ended ||
|
||||
(this.RunStatus == RunStatus.PendingRequests && !blockOnPendingRequest) ||
|
||||
linkedSource.Token.IsCancellationRequested;
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync() => default;
|
||||
/// <summary>
|
||||
/// Signals that new input has been provided and the run loop should continue processing.
|
||||
/// Called by AsyncRunHandle when the user enqueues a message or response.
|
||||
/// </summary>
|
||||
public void SignalInput()
|
||||
{
|
||||
this._inputWaiter?.SignalInput();
|
||||
}
|
||||
|
||||
public ValueTask StopAsync()
|
||||
{
|
||||
this._stopCancellation.Cancel();
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
if (Interlocked.Exchange(ref this._isDisposed, 1) == 0)
|
||||
{
|
||||
this._stopCancellation.Cancel();
|
||||
|
||||
this._stopCancellation.Dispose();
|
||||
this._inputWaiter.Dispose();
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ internal sealed class StepContext
|
||||
{
|
||||
public ConcurrentDictionary<string, ConcurrentQueue<MessageEnvelope>> QueuedMessages { get; } = [];
|
||||
|
||||
public bool HasMessages => this.QueuedMessages.Values.Any(messageQueue => !messageQueue.IsEmpty);
|
||||
public bool HasMessages => !this.QueuedMessages.IsEmpty && this.QueuedMessages.Values.Any(messageQueue => !messageQueue.IsEmpty);
|
||||
|
||||
public ConcurrentQueue<MessageEnvelope> MessagesFor(string target)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Execution;
|
||||
|
||||
/// <summary>
|
||||
/// A modern implementation of IRunEventStream that streams events as they are created,
|
||||
/// using System.Threading.Channels for thread-safe coordination.
|
||||
/// </summary>
|
||||
internal sealed class StreamingRunEventStream : IRunEventStream
|
||||
{
|
||||
private readonly Channel<WorkflowEvent> _eventChannel;
|
||||
private readonly ISuperStepRunner _stepRunner;
|
||||
private readonly InputWaiter _inputWaiter;
|
||||
private readonly CancellationTokenSource _runLoopCancellation;
|
||||
private readonly bool _disableRunLoop;
|
||||
private Task? _runLoopTask;
|
||||
private RunStatus _runStatus = RunStatus.NotStarted;
|
||||
private int _completionEpoch; // Tracks which completion signal belongs to which consumer iteration
|
||||
|
||||
public StreamingRunEventStream(ISuperStepRunner stepRunner, bool disableRunLoop = false)
|
||||
{
|
||||
this._stepRunner = stepRunner;
|
||||
this._runLoopCancellation = new CancellationTokenSource();
|
||||
this._inputWaiter = new();
|
||||
this._disableRunLoop = disableRunLoop;
|
||||
|
||||
// Unbounded channel - events never block the producer
|
||||
// This allows events to flow freely during superstep execution
|
||||
this._eventChannel = Channel.CreateUnbounded<WorkflowEvent>(new UnboundedChannelOptions
|
||||
{
|
||||
SingleReader = true, // Only one consumer at a time (enforced by AsyncRunHandle)
|
||||
SingleWriter = false, // Events can come from multiple threads during superstep execution
|
||||
AllowSynchronousContinuations = false // Prevent potential deadlocks
|
||||
});
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
// Start the background run loop that drives superstep execution
|
||||
if (!this._disableRunLoop)
|
||||
{
|
||||
this._runLoopTask = Task.Run(() => this.RunLoopAsync(this._runLoopCancellation.Token));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunLoopAsync(CancellationToken cancellation)
|
||||
{
|
||||
using CancellationTokenSource errorSource = new();
|
||||
CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(errorSource.Token, cancellation);
|
||||
|
||||
// Subscribe to events - they will flow directly to the channel as they're raised
|
||||
this._stepRunner.OutgoingEvents.EventRaised += OnEventRaisedAsync;
|
||||
|
||||
try
|
||||
{
|
||||
// Wait for the first input before starting
|
||||
// The consumer will call EnqueueMessageAsync which signals the run loop
|
||||
await this._inputWaiter.WaitForInputAsync(cancellation: linkedSource.Token).ConfigureAwait(false);
|
||||
|
||||
this._runStatus = RunStatus.Running;
|
||||
|
||||
while (!linkedSource.Token.IsCancellationRequested)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
await this._stepRunner.RunSuperStepAsync(linkedSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Update status based on what's waiting
|
||||
this._runStatus = this._stepRunner.HasUnservicedRequests
|
||||
? RunStatus.PendingRequests
|
||||
: RunStatus.Idle;
|
||||
|
||||
// Signal completion to consumer so they can check status and decide whether to continue
|
||||
// Increment epoch so next consumer iteration gets a new completion signal
|
||||
// Capture the status at this moment to avoid race conditions with event reading
|
||||
int currentEpoch = Interlocked.Increment(ref this._completionEpoch);
|
||||
RunStatus capturedStatus = this._runStatus;
|
||||
await this._eventChannel.Writer.WriteAsync(new InternalHaltSignal(currentEpoch, capturedStatus), linkedSource.Token).ConfigureAwait(false);
|
||||
|
||||
// Wait for next input from the consumer
|
||||
// Works for both Idle (no work) and PendingRequests (waiting for responses)
|
||||
await this._inputWaiter.WaitForInputAsync(TimeSpan.FromSeconds(1), linkedSource.Token).ConfigureAwait(false);
|
||||
|
||||
// When signaled, resume running
|
||||
this._runStatus = RunStatus.Running;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected during shutdown
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await this._eventChannel.Writer.WriteAsync(new WorkflowErrorEvent(e), linkedSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this._stepRunner.OutgoingEvents.EventRaised -= OnEventRaisedAsync;
|
||||
this._eventChannel.Writer.Complete();
|
||||
|
||||
// Mark as ended when run loop exits
|
||||
this._runStatus = RunStatus.Ended;
|
||||
}
|
||||
|
||||
async ValueTask OnEventRaisedAsync(object? sender, WorkflowEvent e)
|
||||
{
|
||||
// Write event directly to channel - it's thread-safe and non-blocking
|
||||
// The channel handles all synchronization internally using lock-free algorithms
|
||||
// Events flow immediately to consumers rather than being batched
|
||||
await this._eventChannel.Writer.WriteAsync(e, linkedSource.Token).ConfigureAwait(false);
|
||||
|
||||
if (e is WorkflowErrorEvent error)
|
||||
{
|
||||
errorSource.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signals that new input has been provided and the run loop should continue processing.
|
||||
/// Called by AsyncRunHandle when the user enqueues a message or response.
|
||||
/// </summary>
|
||||
public void SignalInput() => this._inputWaiter.SignalInput();
|
||||
|
||||
public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(
|
||||
bool blockOnPendingRequest,
|
||||
[EnumeratorCancellation] CancellationToken cancellation = default)
|
||||
{
|
||||
// Get the current epoch - we'll only respond to completion signals from this epoch or later
|
||||
int myEpoch = Volatile.Read(ref this._completionEpoch) + 1;
|
||||
|
||||
// Simply read from channel - all coordination is handled by Channel infrastructure
|
||||
// Note: When cancellation is requested, ReadAllAsync may throw OperationCanceledException
|
||||
// or may complete the enumeration. We check IsCancellationRequested explicitly at superstep
|
||||
// boundaries to ensure clean cancellation.
|
||||
await foreach (WorkflowEvent evt in this._eventChannel.Reader.ReadAllAsync(cancellation).ConfigureAwait(false))
|
||||
{
|
||||
// Filter out internal signals used for run loop coordination
|
||||
if (evt is InternalHaltSignal completionSignal)
|
||||
{
|
||||
// Ignore completion signals from previous iterations
|
||||
if (completionSignal.Epoch < myEpoch)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for cancellation at superstep boundaries (before processing completion signal)
|
||||
// This allows consumers to stop reading events cleanly between supersteps
|
||||
if (cancellation.IsCancellationRequested)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Check if we should stop streaming based on the status captured at completion time
|
||||
// This avoids race conditions where _runStatus changes while events are being read
|
||||
// - Idle: Workflow completed, no pending requests
|
||||
// - Ended: Run loop disposed/cancelled
|
||||
// Note: PendingRequests is handled by WatchStreamAsync's do-while loop
|
||||
if (completionSignal.Status is RunStatus.Idle or RunStatus.Ended)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (!blockOnPendingRequest && completionSignal.Status is RunStatus.PendingRequests)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Otherwise continue reading (more events coming after input provided)
|
||||
continue;
|
||||
}
|
||||
|
||||
// RequestHaltEvent signals the end of the event stream
|
||||
if (evt is RequestHaltEvent)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (cancellation.IsCancellationRequested)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return evt;
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellation = default)
|
||||
{
|
||||
// Thread-safe read of status (enum is read atomically on most platforms)
|
||||
return new ValueTask<RunStatus>(this._runStatus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all buffered events from the channel.
|
||||
/// This should be called when restoring a checkpoint to discard stale events from superseded supersteps.
|
||||
/// </summary>
|
||||
public void ClearBufferedEvents()
|
||||
{
|
||||
// Drain all events currently in the channel buffer
|
||||
// We discard all events since they're from a timeline that's been superseded by the checkpoint restore
|
||||
while (this._eventChannel.Reader.TryRead(out _))
|
||||
{
|
||||
// Discard each event (including InternalCompletionSignals)
|
||||
}
|
||||
|
||||
// After clearing, signal the run loop to continue if needed
|
||||
// The run loop will send a new completion signal when it finishes processing from the restored state
|
||||
this.SignalInput();
|
||||
}
|
||||
|
||||
public async ValueTask StopAsync()
|
||||
{
|
||||
// Cancel the run loop
|
||||
this._runLoopCancellation.Cancel();
|
||||
|
||||
// Release the event waiter, if any
|
||||
this._inputWaiter.SignalInput();
|
||||
|
||||
// Wait for clean shutdown
|
||||
if (this._runLoopTask != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._runLoopTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected during cancellation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await this.StopAsync().ConfigureAwait(false);
|
||||
|
||||
// Dispose resources
|
||||
this._runLoopCancellation.Dispose();
|
||||
this._inputWaiter.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal signal used to mark completion of a work batch and allow status checking.
|
||||
/// This is never exposed to consumers.
|
||||
/// </summary>
|
||||
private sealed class InternalHaltSignal(int epoch, RunStatus status) : WorkflowEvent
|
||||
{
|
||||
public int Epoch => epoch;
|
||||
public RunStatus Status => status;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,21 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
internal enum ExecutionMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Normal streaming mode using the new channel-based implementation.
|
||||
/// Events stream out immediately as they are created.
|
||||
/// </summary>
|
||||
OffThread,
|
||||
Lockstep
|
||||
|
||||
/// <summary>
|
||||
/// Lockstep mode where events are batched per superstep.
|
||||
/// Events are accumulated and emitted after each superstep completes.
|
||||
/// </summary>
|
||||
Lockstep,
|
||||
|
||||
/// <summary>
|
||||
/// A special execution mode for subworkflows - it functions like OffThread, but without the internal task
|
||||
/// running super steps, as they are implemented by being driven directly by the hosting workflow
|
||||
/// </summary>
|
||||
Subworkflow,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Defines an execution environment for running, streaming, and resuming workflows asynchronously, with optional
|
||||
/// checkpointing and run management capabilities.
|
||||
/// </summary>
|
||||
public interface IWorkflowExecutionEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Initiates an asynchronous streaming execution using the specified input.
|
||||
/// </summary>
|
||||
/// <remarks>The returned <see cref="StreamingRun"/> provides methods to observe and control
|
||||
/// the ongoing streaming execution. The operation will continue until the streaming execution is finished or
|
||||
/// cancelled.</remarks>
|
||||
/// <typeparam name="TInput">A type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the streaming run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{StreamingRun}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="StreamingRun"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<StreamingRun> StreamAsync<TInput>(Workflow workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Initiates an asynchronous streaming execution using the specified input.
|
||||
/// </summary>
|
||||
/// <remarks>The returned <see cref="StreamingRun"/> provides methods to observe and control
|
||||
/// the ongoing streaming execution. The operation will continue until the streaming execution is finished or
|
||||
/// cancelled.</remarks>
|
||||
/// <typeparam name="TInput">A type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the streaming run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{StreamingRun}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="StreamingRun"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<StreamingRun> StreamAsync<TInput>(Workflow<TInput> workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Initiates an asynchronous streaming execution using the specified input, with checkpointing.
|
||||
/// </summary>
|
||||
/// <remarks>The returned <see cref="StreamingRun"/> provides methods to observe and control
|
||||
/// the ongoing streaming execution. The operation will continue until the streaming execution is finished or
|
||||
/// cancelled.</remarks>
|
||||
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the streaming run.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{StreamingRun}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="StreamingRun"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<Checkpointed<StreamingRun>> StreamAsync<TInput>(Workflow workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Initiates an asynchronous streaming execution using the specified input, with checkpointing.
|
||||
/// </summary>
|
||||
/// <remarks>The returned <see cref="StreamingRun"/> provides methods to observe and control
|
||||
/// the ongoing streaming execution. The operation will continue until the streaming execution is finished or
|
||||
/// cancelled.</remarks>
|
||||
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the streaming run.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{StreamingRun}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="StreamingRun"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<Checkpointed<StreamingRun>> StreamAsync<TInput>(Workflow<TInput> workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Resumes an asynchronous streaming execution for the specified input from a checkpoint.
|
||||
/// </summary>
|
||||
/// <remarks>If the operation is cancelled via the <paramref name="cancellationToken"/> token, the streaming execution will
|
||||
/// be terminated.</remarks>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="fromCheckpoint">The <see cref="CheckpointInfo"/> corresponding to the checkpoint from which to resume.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="StreamingRun"/> that provides access to the results of the streaming run.</returns>
|
||||
ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync(Workflow workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Resumes an asynchronous streaming execution for the specified input from a checkpoint.
|
||||
/// </summary>
|
||||
/// <remarks>If the operation is cancelled via the <paramref name="cancellationToken"/> token, the streaming execution will
|
||||
/// be terminated.</remarks>
|
||||
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="fromCheckpoint">The <see cref="CheckpointInfo"/> corresponding to the checkpoint from which to resume.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="StreamingRun"/> that provides access to the results of the streaming run.</returns>
|
||||
ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync<TInput>(Workflow<TInput> workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a non-streaming execution of the workflow with the specified input.
|
||||
/// </summary>
|
||||
/// <remarks>The workflow will run until its first halt, and the returned <see cref="Run"/> will capture
|
||||
/// all outgoing events. Use the <c>Run</c> instance to resume execution with responses to outgoing events.</remarks>
|
||||
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<Run> RunAsync<TInput>(Workflow workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a non-streaming execution of the workflow with the specified input.
|
||||
/// </summary>
|
||||
/// <remarks>The workflow will run until its first halt, and the returned <see cref="Run"/> will capture
|
||||
/// all outgoing events. Use the <c>Run</c> instance to resume execution with responses to outgoing events.</remarks>
|
||||
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<Run> RunAsync<TInput>(Workflow<TInput> workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a non-streaming execution of the workflow with the specified input, with checkpointing.
|
||||
/// </summary>
|
||||
/// <remarks>The workflow will run until its first halt, and the returned <see cref="Run"/> will capture
|
||||
/// all outgoing events. Use the <c>Run</c> instance to resume execution with responses to outgoing events.</remarks>
|
||||
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the run.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<Checkpointed<Run>> RunAsync<TInput>(Workflow workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a non-streaming execution of the workflow with the specified input, with checkpointing.
|
||||
/// </summary>
|
||||
/// <remarks>The workflow will run until its first halt, and the returned <see cref="Run"/> will capture
|
||||
/// all outgoing events. Use the <c>Run</c> instance to resume execution with responses to outgoing events.</remarks>
|
||||
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the run.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<Checkpointed<Run>> RunAsync<TInput>(Workflow<TInput> workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Resumes a non-streaming execution of the workflow from a checkpoint.
|
||||
/// </summary>
|
||||
/// <remarks>The workflow will run until its first halt, and the returned <see cref="Run"/> will capture
|
||||
/// all outgoing events. Use the <c>Run</c> instance to resume execution with responses to outgoing events.</remarks>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="fromCheckpoint">The <see cref="CheckpointInfo"/> corresponding to the checkpoint from which to resume.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<Checkpointed<Run>> ResumeAsync(Workflow workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Resumes a non-streaming execution of the workflow from a checkpoint.
|
||||
/// </summary>
|
||||
/// <remarks>The workflow will run until its first halt, and the returned <see cref="Run"/> will capture
|
||||
/// all outgoing events. Use the <c>Run</c> instance to resume execution with responses to outgoing events.</remarks>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="fromCheckpoint">The <see cref="CheckpointInfo"/> corresponding to the checkpoint from which to resume.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
|
||||
ValueTask<Checkpointed<Run>> ResumeAsync<TInput>(Workflow<TInput> workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
using Microsoft.Agents.AI.Workflows.Execution;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.InProc;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an in-process implementation of the workflow execution environment for running, streaming, and
|
||||
/// checkpointing workflows within the current application domain.
|
||||
/// </summary>
|
||||
public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironment
|
||||
{
|
||||
private readonly ExecutionMode _executionMode;
|
||||
internal InProcessExecutionEnvironment(ExecutionMode mode)
|
||||
{
|
||||
this._executionMode = mode;
|
||||
}
|
||||
|
||||
internal ValueTask<AsyncRunHandle> BeginRunAsync(Workflow workflow, ICheckpointManager? checkpointManager, string? runId, IEnumerable<Type> knownValidInputTypes, CancellationToken cancellationToken)
|
||||
{
|
||||
InProcessRunner runner = new(workflow, checkpointManager, runId, knownValidInputTypes: knownValidInputTypes);
|
||||
return runner.BeginStreamAsync(this._executionMode, cancellationToken);
|
||||
}
|
||||
|
||||
internal ValueTask<AsyncRunHandle> ResumeRunAsync(Workflow workflow, ICheckpointManager? checkpointManager, string? runId, CheckpointInfo fromCheckpoint, IEnumerable<Type> knownValidInputTypes, CancellationToken cancellationToken)
|
||||
{
|
||||
InProcessRunner runner = new(workflow, checkpointManager, runId, knownValidInputTypes: knownValidInputTypes);
|
||||
return runner.ResumeStreamAsync(this._executionMode, fromCheckpoint, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<StreamingRun> StreamAsync<TInput>(
|
||||
Workflow workflow,
|
||||
TInput input,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager: null, runId: runId, [], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.EnqueueAndStreamAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<StreamingRun> StreamAsync<TInput>(
|
||||
Workflow<TInput> workflow,
|
||||
TInput input,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager: null, runId: runId, [typeof(TInput)], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.EnqueueAndStreamAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Checkpointed<StreamingRun>> StreamAsync<TInput>(
|
||||
Workflow workflow,
|
||||
TInput input,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager, runId: runId, [], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync(() => runHandle.EnqueueAndStreamAsync(input, cancellationToken))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Checkpointed<StreamingRun>> StreamAsync<TInput>(
|
||||
Workflow<TInput> workflow,
|
||||
TInput input,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager, runId: runId, [typeof(TInput)], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync(() => runHandle.EnqueueAndStreamAsync(input, cancellationToken))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync(
|
||||
Workflow workflow,
|
||||
CheckpointInfo fromCheckpoint,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
AsyncRunHandle runHandle = await this.ResumeRunAsync(workflow, checkpointManager, runId: runId, fromCheckpoint, [], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync<StreamingRun>(() => new(new StreamingRun(runHandle)))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync<TInput>(
|
||||
Workflow<TInput> workflow,
|
||||
CheckpointInfo fromCheckpoint,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await this.ResumeRunAsync(workflow, checkpointManager, runId: runId, fromCheckpoint, [typeof(TInput)], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync<StreamingRun>(() => new(new StreamingRun(runHandle)))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Run> RunAsync<TInput>(
|
||||
Workflow workflow,
|
||||
TInput input,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager: null, runId: runId, [], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.EnqueueAndRunAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Run> RunAsync<TInput>(
|
||||
Workflow<TInput> workflow,
|
||||
TInput input,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager: null, runId: runId, [typeof(TInput)], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.EnqueueAndRunAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Checkpointed<Run>> RunAsync<TInput>(
|
||||
Workflow workflow,
|
||||
TInput input,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager, runId: runId, [], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync(() => runHandle.EnqueueAndRunAsync(input, cancellationToken))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Checkpointed<Run>> RunAsync<TInput>(
|
||||
Workflow<TInput> workflow,
|
||||
TInput input,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager, runId: runId, [typeof(TInput)], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync(() => runHandle.EnqueueAndRunAsync(input, cancellationToken))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Checkpointed<Run>> ResumeAsync(
|
||||
Workflow workflow,
|
||||
CheckpointInfo fromCheckpoint,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
AsyncRunHandle runHandle = await this.ResumeRunAsync(workflow, checkpointManager, runId: runId, fromCheckpoint, [], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync<Run>(() => new(new Run(runHandle)))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Checkpointed<Run>> ResumeAsync<TInput>(
|
||||
Workflow<TInput> workflow,
|
||||
CheckpointInfo fromCheckpoint,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await this.ResumeRunAsync(workflow, checkpointManager, runId: runId, fromCheckpoint, [typeof(TInput)], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync<Run>(() => new(new Run(runHandle)))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -148,7 +148,17 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
|
||||
this.RunContext.HasQueuedExternalDeliveries ||
|
||||
this.RunContext.JoinedRunnersHaveActions)
|
||||
{
|
||||
await this.RunSuperstepAsync(currentStep).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await this.RunSuperstepAsync(currentStep).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{ }
|
||||
catch (Exception e)
|
||||
{
|
||||
await this.RaiseWorkflowEventAsync(new WorkflowErrorEvent(e)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -367,6 +367,19 @@ internal sealed class InProcessRunnerContext : IRunnerContext
|
||||
{
|
||||
if (Interlocked.Exchange(ref this._runEnded, 1) == 0)
|
||||
{
|
||||
foreach (string executorId in this._executors.Keys)
|
||||
{
|
||||
Task<Executor> executor = this._executors[executorId];
|
||||
if (executor is IAsyncDisposable asyncDisposable)
|
||||
{
|
||||
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
else if (executor is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
await this._workflow.ReleaseOwnershipAsync(this).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
using Microsoft.Agents.AI.Workflows.Execution;
|
||||
using Microsoft.Agents.AI.Workflows.InProc;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
@@ -14,338 +10,77 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
/// Provides methods to initiate and manage in-process workflow executions, supporting both streaming and
|
||||
/// non-streaming modes with asynchronous operations.
|
||||
/// </summary>
|
||||
public sealed class InProcessExecution
|
||||
public static class InProcessExecution
|
||||
{
|
||||
private static InProcessExecution DefaultInstance { get; } = new(ExecutionMode.Lockstep);
|
||||
|
||||
private readonly ExecutionMode _executionMode;
|
||||
private InProcessExecution(ExecutionMode mode)
|
||||
{
|
||||
this._executionMode = mode;
|
||||
}
|
||||
|
||||
internal static ExecutionMode DefaultMode => DefaultInstance._executionMode;
|
||||
|
||||
internal ValueTask<AsyncRunHandle> BeginRunAsync(Workflow workflow, ICheckpointManager? checkpointManager, string? runId, IEnumerable<Type> knownValidInputTypes, CancellationToken cancellationToken)
|
||||
{
|
||||
InProcessRunner runner = new(workflow, checkpointManager, runId, knownValidInputTypes: knownValidInputTypes);
|
||||
return runner.BeginStreamAsync(this._executionMode, cancellationToken);
|
||||
}
|
||||
|
||||
internal ValueTask<AsyncRunHandle> ResumeRunAsync(Workflow workflow, ICheckpointManager? checkpointManager, string? runId, CheckpointInfo fromCheckpoint, IEnumerable<Type> knownValidInputTypes, CancellationToken cancellationToken)
|
||||
{
|
||||
InProcessRunner runner = new(workflow, checkpointManager, runId, knownValidInputTypes: knownValidInputTypes);
|
||||
return runner.ResumeStreamAsync(this._executionMode, fromCheckpoint, cancellationToken);
|
||||
}
|
||||
/// <summary>
|
||||
/// The default InProcess execution environment.
|
||||
/// </summary>
|
||||
public static InProcessExecutionEnvironment Default => OffThread;
|
||||
|
||||
/// <summary>
|
||||
/// Initiates an asynchronous streaming execution using the specified input.
|
||||
/// An InProcessExecution environment which will run SuperSteps in a background thread, streaming
|
||||
/// events out as they are raised.
|
||||
/// </summary>
|
||||
/// <remarks>The returned <see cref="StreamingRun"/> provides methods to observe and control
|
||||
/// the ongoing streaming execution. The operation will continue until the streaming execution is finished or
|
||||
/// cancelled.</remarks>
|
||||
/// <typeparam name="TInput">A type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the streaming run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{StreamingRun}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="StreamingRun"/> for managing and interacting with the streaming run.</returns>
|
||||
public static async ValueTask<StreamingRun> StreamAsync<TInput>(
|
||||
Workflow workflow,
|
||||
TInput input,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await DefaultInstance.BeginRunAsync(workflow, checkpointManager: null, runId: runId, [], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.EnqueueAndStreamAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
public static InProcessExecutionEnvironment OffThread { get; } = new(ExecutionMode.OffThread);
|
||||
|
||||
/// <summary>
|
||||
/// Initiates an asynchronous streaming execution using the specified input.
|
||||
/// An InProcesExecution environment which will run SuperSteps in the event watching thread,
|
||||
/// accumulating events during each SuperStep and streaming them out after each SuperStep is
|
||||
/// completed.
|
||||
/// </summary>
|
||||
/// <remarks>The returned <see cref="StreamingRun"/> provides methods to observe and control
|
||||
/// the ongoing streaming execution. The operation will continue until the streaming execution is finished or
|
||||
/// cancelled.</remarks>
|
||||
/// <typeparam name="TInput">A type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the streaming run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{StreamingRun}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="StreamingRun"/> for managing and interacting with the streaming run.</returns>
|
||||
public static async ValueTask<StreamingRun> StreamAsync<TInput>(
|
||||
Workflow<TInput> workflow,
|
||||
TInput input,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await DefaultInstance.BeginRunAsync(workflow, checkpointManager: null, runId: runId, [typeof(TInput)], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.EnqueueAndStreamAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
public static InProcessExecutionEnvironment Lockstep { get; } = new(ExecutionMode.Lockstep);
|
||||
|
||||
/// <summary>
|
||||
/// Initiates an asynchronous streaming execution using the specified input, with checkpointing.
|
||||
/// An InProcessExecution environment which will not run SuperSteps directly, relying instead
|
||||
/// on the hosting workflow to run them directly, while streaming events out as they are raised.
|
||||
/// </summary>
|
||||
/// <remarks>The returned <see cref="StreamingRun"/> provides methods to observe and control
|
||||
/// the ongoing streaming execution. The operation will continue until the streaming execution is finished or
|
||||
/// cancelled.</remarks>
|
||||
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the streaming run.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{StreamingRun}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="StreamingRun"/> for managing and interacting with the streaming run.</returns>
|
||||
public static async ValueTask<Checkpointed<StreamingRun>> StreamAsync<TInput>(
|
||||
Workflow workflow,
|
||||
TInput input,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await DefaultInstance.BeginRunAsync(workflow, checkpointManager, runId: runId, [], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
internal static InProcessExecutionEnvironment Subworkflow { get; } = new(ExecutionMode.Subworkflow);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync(() => runHandle.EnqueueAndStreamAsync(input, cancellationToken))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.StreamAsync{TInput}(Workflow, TInput, string?, CancellationToken)"/>
|
||||
public static ValueTask<StreamingRun> StreamAsync<TInput>(Workflow workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.StreamAsync(workflow, input, runId, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Initiates an asynchronous streaming execution using the specified input, with checkpointing.
|
||||
/// </summary>
|
||||
/// <remarks>The returned <see cref="StreamingRun"/> provides methods to observe and control
|
||||
/// the ongoing streaming execution. The operation will continue until the streaming execution is finished or
|
||||
/// cancelled.</remarks>
|
||||
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the streaming run.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{StreamingRun}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="StreamingRun"/> for managing and interacting with the streaming run.</returns>
|
||||
public static async ValueTask<Checkpointed<StreamingRun>> StreamAsync<TInput>(
|
||||
Workflow<TInput> workflow,
|
||||
TInput input,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await DefaultInstance.BeginRunAsync(workflow, checkpointManager, runId: runId, [typeof(TInput)], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.StreamAsync{TInput}(Workflow{TInput}, TInput, string?, CancellationToken)"/>
|
||||
public static ValueTask<StreamingRun> StreamAsync<TInput>(Workflow<TInput> workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.StreamAsync(workflow, input, runId, cancellationToken);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync(() => runHandle.EnqueueAndStreamAsync(input, cancellationToken))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.StreamAsync{TInput}(Workflow, TInput, CheckpointManager, string?, CancellationToken)"/>
|
||||
public static ValueTask<Checkpointed<StreamingRun>> StreamAsync<TInput>(Workflow workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.StreamAsync(workflow, input, checkpointManager, runId, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Resumes an asynchronous streaming execution for the specified input from a checkpoint.
|
||||
/// </summary>
|
||||
/// <remarks>If the operation is cancelled via the <paramref name="cancellationToken"/> token, the streaming execution will
|
||||
/// be terminated.</remarks>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="fromCheckpoint">The <see cref="CheckpointInfo"/> corresponding to the checkpoint from which to resume.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="StreamingRun"/> that provides access to the results of the streaming run.</returns>
|
||||
public static async ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync(
|
||||
Workflow workflow,
|
||||
CheckpointInfo fromCheckpoint,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
AsyncRunHandle runHandle = await DefaultInstance.ResumeRunAsync(workflow, checkpointManager, runId: runId, fromCheckpoint, [], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.StreamAsync{TInput}(Workflow{TInput}, TInput, CheckpointManager, string?, CancellationToken)"/>
|
||||
public static ValueTask<Checkpointed<StreamingRun>> StreamAsync<TInput>(Workflow<TInput> workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.StreamAsync(workflow, input, checkpointManager, runId, cancellationToken);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync<StreamingRun>(() => new(new StreamingRun(runHandle)))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.ResumeStreamAsync(Workflow, CheckpointInfo, CheckpointManager, string?, CancellationToken)"/>
|
||||
public static ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync(Workflow workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default)
|
||||
=> Default.ResumeStreamAsync(workflow, fromCheckpoint, checkpointManager, runId, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Resumes an asynchronous streaming execution for the specified input from a checkpoint.
|
||||
/// </summary>
|
||||
/// <remarks>If the operation is cancelled via the <paramref name="cancellationToken"/> token, the streaming execution will
|
||||
/// be terminated.</remarks>
|
||||
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="fromCheckpoint">The <see cref="CheckpointInfo"/> corresponding to the checkpoint from which to resume.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="StreamingRun"/> that provides access to the results of the streaming run.</returns>
|
||||
public static async ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync<TInput>(
|
||||
Workflow<TInput> workflow,
|
||||
CheckpointInfo fromCheckpoint,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await DefaultInstance.ResumeRunAsync(workflow, checkpointManager, runId: runId, fromCheckpoint, [typeof(TInput)], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.ResumeStreamAsync{TInput}(Workflow{TInput}, CheckpointInfo, CheckpointManager, string?, CancellationToken)"/>
|
||||
public static ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync<TInput>(Workflow<TInput> workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.ResumeStreamAsync(workflow, fromCheckpoint, checkpointManager, runId, cancellationToken);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync<StreamingRun>(() => new(new StreamingRun(runHandle)))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.RunAsync{TInput}(Workflow, TInput, string?, CancellationToken)"/>
|
||||
public static ValueTask<Run> RunAsync<TInput>(Workflow workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.RunAsync(workflow, input, runId, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a non-streaming execution of the workflow with the specified input.
|
||||
/// </summary>
|
||||
/// <remarks>The workflow will run until its first halt, and the returned <see cref="Run"/> will capture
|
||||
/// all outgoing events. Use the <c>Run</c> instance to resume execution with responses to outgoing events.</remarks>
|
||||
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
|
||||
public static async ValueTask<Run> RunAsync<TInput>(
|
||||
Workflow workflow,
|
||||
TInput input,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await DefaultInstance.BeginRunAsync(workflow, checkpointManager: null, runId: runId, [], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.RunAsync{TInput}(Workflow{TInput}, TInput, string?, CancellationToken)"/>
|
||||
public static ValueTask<Run> RunAsync<TInput>(Workflow<TInput> workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.RunAsync(workflow, input, runId, cancellationToken);
|
||||
|
||||
return await runHandle.EnqueueAndRunAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.RunAsync{TInput}(Workflow, TInput, CheckpointManager, string?, CancellationToken)"/>
|
||||
public static ValueTask<Checkpointed<Run>> RunAsync<TInput>(Workflow workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.RunAsync(workflow, input, checkpointManager, runId, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a non-streaming execution of the workflow with the specified input.
|
||||
/// </summary>
|
||||
/// <remarks>The workflow will run until its first halt, and the returned <see cref="Run"/> will capture
|
||||
/// all outgoing events. Use the <c>Run</c> instance to resume execution with responses to outgoing events.</remarks>
|
||||
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
|
||||
public static async ValueTask<Run> RunAsync<TInput>(
|
||||
Workflow<TInput> workflow,
|
||||
TInput input,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await DefaultInstance.BeginRunAsync(workflow, checkpointManager: null, runId: runId, [typeof(TInput)], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.RunAsync{TInput}(Workflow{TInput}, TInput, CheckpointManager, string?, CancellationToken)"/>
|
||||
public static ValueTask<Checkpointed<Run>> RunAsync<TInput>(Workflow<TInput> workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.RunAsync(workflow, input, checkpointManager, runId, cancellationToken);
|
||||
|
||||
return await runHandle.EnqueueAndRunAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.ResumeAsync(Workflow, CheckpointInfo, CheckpointManager, string?, CancellationToken)"/>
|
||||
public static ValueTask<Checkpointed<Run>> ResumeAsync(Workflow workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default)
|
||||
=> Default.ResumeAsync(workflow, fromCheckpoint, checkpointManager, runId, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a non-streaming execution of the workflow with the specified input, with checkpointing.
|
||||
/// </summary>
|
||||
/// <remarks>The workflow will run until its first halt, and the returned <see cref="Run"/> will capture
|
||||
/// all outgoing events. Use the <c>Run</c> instance to resume execution with responses to outgoing events.</remarks>
|
||||
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the run.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
|
||||
public static async ValueTask<Checkpointed<Run>> RunAsync<TInput>(
|
||||
Workflow workflow,
|
||||
TInput input,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await DefaultInstance.BeginRunAsync(workflow, checkpointManager, runId: runId, [], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync(() => runHandle.EnqueueAndRunAsync(input, cancellationToken))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a non-streaming execution of the workflow with the specified input, with checkpointing.
|
||||
/// </summary>
|
||||
/// <remarks>The workflow will run until its first halt, and the returned <see cref="Run"/> will capture
|
||||
/// all outgoing events. Use the <c>Run</c> instance to resume execution with responses to outgoing events.</remarks>
|
||||
/// <typeparam name="TInput">The type of input accepted by the workflow. Must be non-nullable.</typeparam>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="input">The input message to be processed as part of the run.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
|
||||
public static async ValueTask<Checkpointed<Run>> RunAsync<TInput>(
|
||||
Workflow<TInput> workflow,
|
||||
TInput input,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await DefaultInstance.BeginRunAsync(workflow, checkpointManager, runId: runId, [typeof(TInput)], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync(() => runHandle.EnqueueAndRunAsync(input, cancellationToken))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resumes a non-streaming execution of the workflow from a checkpoint.
|
||||
/// </summary>
|
||||
/// <remarks>The workflow will run until its first halt, and the returned <see cref="Run"/> will capture
|
||||
/// all outgoing events. Use the <c>Run</c> instance to resume execution with responses to outgoing events.</remarks>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="fromCheckpoint">The <see cref="CheckpointInfo"/> corresponding to the checkpoint from which to resume.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
|
||||
public static async ValueTask<Checkpointed<Run>> ResumeAsync(
|
||||
Workflow workflow,
|
||||
CheckpointInfo fromCheckpoint,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
AsyncRunHandle runHandle = await DefaultInstance.ResumeRunAsync(workflow, checkpointManager, runId: runId, fromCheckpoint, [], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync<Run>(() => new(new Run(runHandle)))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resumes a non-streaming execution of the workflow from a checkpoint.
|
||||
/// </summary>
|
||||
/// <remarks>The workflow will run until its first halt, and the returned <see cref="Run"/> will capture
|
||||
/// all outgoing events. Use the <c>Run</c> instance to resume execution with responses to outgoing events.</remarks>
|
||||
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</param>
|
||||
/// <param name="fromCheckpoint">The <see cref="CheckpointInfo"/> corresponding to the checkpoint from which to resume.</param>
|
||||
/// <param name="checkpointManager">The <see cref="CheckpointManager"/> to use with this run.</param>
|
||||
/// <param name="runId">An optional unique identifier for the run. If not provided, a new identifier will be generated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellationToken requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{Run}"/> that represents the asynchronous operation. The result contains a <see
|
||||
/// cref="Run"/> for managing and interacting with the streaming run.</returns>
|
||||
public static async ValueTask<Checkpointed<Run>> ResumeAsync<TInput>(
|
||||
Workflow<TInput> workflow,
|
||||
CheckpointInfo fromCheckpoint,
|
||||
CheckpointManager checkpointManager,
|
||||
string? runId = null,
|
||||
CancellationToken cancellationToken = default) where TInput : notnull
|
||||
{
|
||||
AsyncRunHandle runHandle = await DefaultInstance.ResumeRunAsync(workflow, checkpointManager, runId: runId, fromCheckpoint, [typeof(TInput)], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await runHandle.WithCheckpointingAsync<Run>(() => new(new Run(runHandle)))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
/// <inheritdoc cref="IWorkflowExecutionEnvironment.ResumeAsync{TInput}(Workflow{TInput}, CheckpointInfo, CheckpointManager, string?, CancellationToken)"/>
|
||||
public static ValueTask<Checkpointed<Run>> ResumeAsync<TInput>(Workflow<TInput> workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
|
||||
=> Default.ResumeAsync(workflow, fromCheckpoint, checkpointManager, runId, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -8,44 +10,11 @@ using Microsoft.Agents.AI.Workflows.Execution;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the current operational state of a workflow run.
|
||||
/// </summary>
|
||||
public enum RunStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The run has not yet started. This only occurs when running in "lockstep" mode.
|
||||
/// </summary>
|
||||
NotStarted,
|
||||
|
||||
/// <summary>
|
||||
/// The run has halted, has no outstanding requets, but has not received a <see cref="RequestHaltEvent"/>.
|
||||
/// </summary>
|
||||
Idle,
|
||||
|
||||
/// <summary>
|
||||
/// The run has halted, and has at least one outstanding <see cref="ExternalRequest"/>.
|
||||
/// </summary>
|
||||
PendingRequests,
|
||||
|
||||
/// <summary>
|
||||
/// The user has ended the run. No further events will be emitted, and no messages can be sent to it.
|
||||
/// </summary>
|
||||
/// <seealso cref="StreamingRun.EndRunAsync"/>
|
||||
/// <seealso cref="Run.EndRunAsync"/>
|
||||
Ended,
|
||||
|
||||
/// <summary>
|
||||
/// The workflow is currently running, and may receive events or requests.
|
||||
/// </summary>
|
||||
Running
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a workflow run that tracks execution status and emitted workflow events, supporting resumption
|
||||
/// with responses to <see cref="RequestInfoEvent"/>.
|
||||
/// </summary>
|
||||
public sealed class Run
|
||||
public sealed class Run : IAsyncDisposable
|
||||
{
|
||||
private readonly List<WorkflowEvent> _eventSink = [];
|
||||
private readonly AsyncRunHandle _runHandle;
|
||||
@@ -57,7 +26,7 @@ public sealed class Run
|
||||
internal async ValueTask<bool> RunToNextHaltAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
bool hadEvents = false;
|
||||
await foreach (WorkflowEvent evt in this._runHandle.TakeEventStreamAsync(breakOnHalt: true, cancellationToken).ConfigureAwait(false))
|
||||
await foreach (WorkflowEvent evt in this._runHandle.TakeEventStreamAsync(blockOnPendingRequest: false, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
hadEvents = true;
|
||||
this._eventSink.Add(evt);
|
||||
@@ -84,9 +53,15 @@ public sealed class Run
|
||||
|
||||
private int _lastBookmark;
|
||||
|
||||
/// <summary>
|
||||
/// The number of events emitted by the workflow since the last access to <see cref="NewEvents"/>
|
||||
/// </summary>
|
||||
public int NewEventCount => this._eventSink.Count - this._lastBookmark;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all events emitted by the workflow since the last access to <see cref="NewEvents" />.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("NewEvents[{NewEventCount}]")]
|
||||
public IEnumerable<WorkflowEvent> NewEvents
|
||||
{
|
||||
get
|
||||
@@ -152,6 +127,9 @@ public sealed class Run
|
||||
return await this.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="StreamingRun.EndRunAsync"/>
|
||||
public ValueTask EndRunAsync() => this._runHandle.RequestEndRunAsync();
|
||||
/// <inheritdoc/>
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
return this._runHandle.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the current operational state of a workflow run.
|
||||
/// </summary>
|
||||
public enum RunStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The run has not yet started. This only occurs when running in "lockstep" mode.
|
||||
/// </summary>
|
||||
NotStarted,
|
||||
|
||||
/// <summary>
|
||||
/// The run has halted, has no outstanding requets, but has not received a <see cref="RequestHaltEvent"/>.
|
||||
/// </summary>
|
||||
Idle,
|
||||
|
||||
/// <summary>
|
||||
/// The run has halted, and has at least one outstanding <see cref="ExternalRequest"/>.
|
||||
/// </summary>
|
||||
PendingRequests,
|
||||
|
||||
/// <summary>
|
||||
/// The user has ended the run. No further events will be emitted, and no messages can be sent to it.
|
||||
/// </summary>
|
||||
Ended,
|
||||
|
||||
/// <summary>
|
||||
/// The workflow is currently running, and may receive events or requests.
|
||||
/// </summary>
|
||||
Running
|
||||
}
|
||||
@@ -13,7 +13,7 @@ using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Specialized;
|
||||
|
||||
internal class WorkflowHostExecutor : Executor, IResettableExecutor
|
||||
internal class WorkflowHostExecutor : Executor, IAsyncDisposable
|
||||
{
|
||||
private readonly string _runId;
|
||||
private readonly Workflow _workflow;
|
||||
@@ -114,8 +114,9 @@ internal class WorkflowHostExecutor : Executor, IResettableExecutor
|
||||
throw new InvalidOperationException("No checkpoints available to resume from.");
|
||||
}
|
||||
|
||||
runHandle = await activeRunner.ResumeStreamAsync(InProcessExecution.DefaultMode, lastCheckpoint!, cancellation)
|
||||
.ConfigureAwait(false);
|
||||
runHandle = await activeRunner.ResumeStreamAsync(ExecutionMode.Subworkflow, lastCheckpoint!, cancellation)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (incomingMessage != null)
|
||||
{
|
||||
await runHandle.EnqueueUntypedAndRunAsync(incomingMessage, cancellation).ConfigureAwait(false);
|
||||
@@ -123,8 +124,8 @@ internal class WorkflowHostExecutor : Executor, IResettableExecutor
|
||||
}
|
||||
else if (incomingMessage != null)
|
||||
{
|
||||
runHandle = await activeRunner.BeginStreamAsync(InProcessExecution.DefaultMode, cancellation)
|
||||
.ConfigureAwait(false);
|
||||
runHandle = await activeRunner.BeginStreamAsync(ExecutionMode.Subworkflow, cancellation)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await runHandle.EnqueueUntypedAndRunAsync(incomingMessage, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
@@ -135,7 +136,7 @@ internal class WorkflowHostExecutor : Executor, IResettableExecutor
|
||||
}
|
||||
else
|
||||
{
|
||||
runHandle = await activeRunner.BeginStreamAsync(InProcessExecution.DefaultMode, cancellation).ConfigureAwait(false);
|
||||
runHandle = await activeRunner.BeginStreamAsync(ExecutionMode.Subworkflow, cancellation).ConfigureAwait(false);
|
||||
|
||||
await runHandle.EnqueueMessageUntypedAsync(Throw.IfNull(incomingMessage), cancellation: cancellation).ConfigureAwait(false);
|
||||
}
|
||||
@@ -251,9 +252,13 @@ internal class WorkflowHostExecutor : Executor, IResettableExecutor
|
||||
StreamingRun run = await this.EnsureRunSendMessageAsync(cancellation: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask ResetAsync()
|
||||
private async ValueTask ResetAsync()
|
||||
{
|
||||
this._run = null;
|
||||
if (this._run != null)
|
||||
{
|
||||
await this._run.DisposeAsync().ConfigureAwait(false);
|
||||
this._run = null;
|
||||
}
|
||||
|
||||
if (this._activeRunner != null)
|
||||
{
|
||||
@@ -263,4 +268,6 @@ internal class WorkflowHostExecutor : Executor, IResettableExecutor
|
||||
this._activeRunner = new(this._workflow, this._checkpointManager, this._runId);
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync() => this.ResetAsync();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -15,7 +14,7 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
/// A <see cref="Workflow"/> run instance supporting a streaming form of receiving workflow events, and providing
|
||||
/// a mechanism to send responses back to the workflow.
|
||||
/// </summary>
|
||||
public sealed class StreamingRun
|
||||
public sealed class StreamingRun : IAsyncDisposable
|
||||
{
|
||||
private readonly AsyncRunHandle _runHandle;
|
||||
|
||||
@@ -24,9 +23,6 @@ public sealed class StreamingRun
|
||||
this._runHandle = Throw.IfNull(runHandle);
|
||||
}
|
||||
|
||||
private ValueTask<bool> WaitOnInputAsync(CancellationToken cancellation = default)
|
||||
=> this._runHandle.WaitForNextInputAsync(cancellation);
|
||||
|
||||
/// <summary>
|
||||
/// A unique identifier for the run. Can be provided at the start of the run, or auto-generated.
|
||||
/// </summary>
|
||||
@@ -78,50 +74,15 @@ public sealed class StreamingRun
|
||||
CancellationToken cancellationToken = default)
|
||||
=> this.WatchStreamAsync(blockOnPendingRequest: true, cancellationToken);
|
||||
|
||||
internal async IAsyncEnumerable<WorkflowEvent> WatchStreamAsync(
|
||||
internal IAsyncEnumerable<WorkflowEvent> WatchStreamAsync(
|
||||
bool blockOnPendingRequest,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
CancellationToken cancellationToken = default)
|
||||
=> this._runHandle.TakeEventStreamAsync(blockOnPendingRequest, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
RunStatus runStatus;
|
||||
|
||||
do
|
||||
{
|
||||
await foreach (WorkflowEvent @event in this._runHandle.TakeEventStreamAsync(breakOnHalt: true, cancellationToken)
|
||||
.WithCancellation(cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
yield return @event;
|
||||
}
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
yield break; // We are done.
|
||||
}
|
||||
|
||||
runStatus = await this._runHandle.GetStatusAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (runStatus == RunStatus.Idle)
|
||||
{
|
||||
yield break; // We are done.
|
||||
}
|
||||
|
||||
if (blockOnPendingRequest && runStatus == RunStatus.PendingRequests)
|
||||
{
|
||||
// Although we are only doing this while there are pending requests, any input allows us to continue
|
||||
// running, so we should not wait until the input is specifically an ExternalResponse.
|
||||
await this.WaitOnInputAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
} while (runStatus == RunStatus.Running);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signals the end of the current run and initiates any necessary cleanup operations asynchronously.
|
||||
/// Enables the underlying Workflow instance to be reused in subsequent runs.
|
||||
/// </summary>
|
||||
/// <returns>A ValueTask that represents the asynchronous operation. The task is complete when the run has
|
||||
/// ended and cleanup is finished.</returns>
|
||||
public async ValueTask EndRunAsync()
|
||||
{
|
||||
await this._runHandle.DisposeAsync().ConfigureAwait(false);
|
||||
return this._runHandle.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -193,7 +193,6 @@ public class Workflow
|
||||
await this.TryResetExecutorRegistrationsAsync().ConfigureAwait(false);
|
||||
|
||||
Interlocked.CompareExchange(ref this._ownerToken, null, ownerToken);
|
||||
this._ownerToken = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ internal sealed class WorkflowHostAgent : AIAgent
|
||||
if (!this._runningWorkflows.TryGetValue(runId, out StreamingRun? run))
|
||||
{
|
||||
run = await InProcessExecution.StreamAsync(this._workflow, messages, cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
.ConfigureAwait(false);
|
||||
this._runningWorkflows[runId] = run;
|
||||
}
|
||||
else
|
||||
|
||||
+5
-4
@@ -40,7 +40,7 @@ internal sealed class WorkflowHarness(Workflow workflow, string runId)
|
||||
{
|
||||
Console.WriteLine("RUNNING WORKFLOW...");
|
||||
Checkpointed<StreamingRun> run = await InProcessExecution.StreamAsync(workflow, input, this._checkpointManager, runId);
|
||||
IReadOnlyList<WorkflowEvent> workflowEvents = await MonitorWorkflowRunAsync(run).ToArrayAsync();
|
||||
IReadOnlyList<WorkflowEvent> workflowEvents = await MonitorAndDisposeWorkflowRunAsync(run).ToArrayAsync();
|
||||
this.LastCheckpoint = workflowEvents.OfType<SuperStepCompletedEvent>().LastOrDefault()?.CompletionInfo?.Checkpoint;
|
||||
return new WorkflowEvents(workflowEvents);
|
||||
}
|
||||
@@ -50,7 +50,7 @@ internal sealed class WorkflowHarness(Workflow workflow, string runId)
|
||||
Console.WriteLine("RESUMING WORKFLOW...");
|
||||
Assert.NotNull(this.LastCheckpoint);
|
||||
Checkpointed<StreamingRun> run = await InProcessExecution.ResumeStreamAsync(workflow, this.LastCheckpoint, this._checkpointManager, runId);
|
||||
IReadOnlyList<WorkflowEvent> workflowEvents = await MonitorWorkflowRunAsync(run, response).ToArrayAsync();
|
||||
IReadOnlyList<WorkflowEvent> workflowEvents = await MonitorAndDisposeWorkflowRunAsync(run, response).ToArrayAsync();
|
||||
return new WorkflowEvents(workflowEvents);
|
||||
}
|
||||
|
||||
@@ -75,8 +75,10 @@ internal sealed class WorkflowHarness(Workflow workflow, string runId)
|
||||
return new WorkflowHarness(workflow, runId);
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<WorkflowEvent> MonitorWorkflowRunAsync(Checkpointed<StreamingRun> run, InputResponse? response = null)
|
||||
private static async IAsyncEnumerable<WorkflowEvent> MonitorAndDisposeWorkflowRunAsync(Checkpointed<StreamingRun> run, InputResponse? response = null)
|
||||
{
|
||||
await using IAsyncDisposable disposeRun = run;
|
||||
|
||||
await foreach (WorkflowEvent workflowEvent in run.Run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
bool exitLoop = false;
|
||||
@@ -93,7 +95,6 @@ internal sealed class WorkflowHarness(Workflow workflow, string runId)
|
||||
}
|
||||
else
|
||||
{
|
||||
await run.Run.EndRunAsync().ConfigureAwait(false);
|
||||
exitLoop = true;
|
||||
}
|
||||
break;
|
||||
|
||||
+1
-1
@@ -259,7 +259,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
|
||||
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<TInput>(yamlReader, workflowContext);
|
||||
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, workflowInput);
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, workflowInput);
|
||||
|
||||
this.WorkflowEvents = run.WatchStreamAsync().ToEnumerable().ToList();
|
||||
foreach (WorkflowEvent workflowEvent in this.WorkflowEvents)
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor
|
||||
TestWorkflowExecutor workflowExecutor = new();
|
||||
WorkflowBuilder workflowBuilder = new(workflowExecutor);
|
||||
workflowBuilder.AddEdge(workflowExecutor, executor);
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflowBuilder.Build(), this.State);
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflowBuilder.Build(), this.State);
|
||||
WorkflowEvent[] events = await run.WatchStreamAsync().ToArrayAsync();
|
||||
Assert.Contains(events, e => e is DeclarativeActionInvokedEvent);
|
||||
Assert.Contains(events, e => e is DeclarativeActionCompletedEvent);
|
||||
|
||||
@@ -385,31 +385,24 @@ public class AgentWorkflowBuilderTests
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, input);
|
||||
try
|
||||
{
|
||||
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
|
||||
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, input);
|
||||
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
|
||||
|
||||
WorkflowOutputEvent? output = null;
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
WorkflowOutputEvent? output = null;
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (evt is AgentRunUpdateEvent executorComplete)
|
||||
{
|
||||
if (evt is AgentRunUpdateEvent executorComplete)
|
||||
{
|
||||
sb.Append(executorComplete.Data);
|
||||
}
|
||||
else if (evt is WorkflowOutputEvent e)
|
||||
{
|
||||
output = e;
|
||||
break;
|
||||
}
|
||||
sb.Append(executorComplete.Data);
|
||||
}
|
||||
else if (evt is WorkflowOutputEvent e)
|
||||
{
|
||||
output = e;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (sb.ToString(), output?.As<List<ChatMessage>>());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await run.EndRunAsync();
|
||||
}
|
||||
return (sb.ToString(), output?.As<List<ChatMessage>>());
|
||||
}
|
||||
|
||||
private sealed class DoubleEchoAgentWithBarrier(string name, StrongBox<TaskCompletionSource<bool>> barrier, StrongBox<int> remaining) : DoubleEchoAgent(name)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Agents.AI.Workflows.InProc;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
internal static class ExecutionExtensions
|
||||
{
|
||||
public static InProcessExecutionEnvironment GetEnvironment(this ExecutionMode executionMode)
|
||||
{
|
||||
return executionMode switch
|
||||
{
|
||||
ExecutionMode.OffThread => InProcessExecution.OffThread,
|
||||
ExecutionMode.Lockstep => InProcessExecution.Lockstep,
|
||||
ExecutionMode.Subworkflow => throw new NotSupportedException(),
|
||||
_ => throw new InvalidOperationException($"Unknown execution mode {executionMode}")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -163,9 +163,25 @@ public class InProcessStateTests
|
||||
.AddFanOutEdge(forward, targets: [testExecutor, testExecutor2])
|
||||
.Build();
|
||||
|
||||
var act = async () => await InProcessExecution.RunAsync(workflow, new TurnToken());
|
||||
Run runWithFailure = await InProcessExecution.RunAsync(workflow, new TurnToken());
|
||||
|
||||
var result = await act.Should()
|
||||
.ThrowAsync("multiple writers to the same shared scope key");
|
||||
bool hadFailure = false;
|
||||
foreach (WorkflowEvent evt in runWithFailure.NewEvents)
|
||||
{
|
||||
if (evt is WorkflowErrorEvent errorEvent)
|
||||
{
|
||||
hadFailure.Should().BeFalse("There can be only one!");
|
||||
hadFailure = true;
|
||||
|
||||
errorEvent.Data.Should().BeOfType<InvalidOperationException>()
|
||||
.Subject.Message.Should().Contain("TestKey");
|
||||
}
|
||||
}
|
||||
|
||||
hadFailure.Should().BeTrue();
|
||||
|
||||
//var act = async () => await InProcessExecution.RunAsync(workflow, new TurnToken());
|
||||
//var result = await act.Should()
|
||||
// .ThrowAsync("multiple writers to the same shared scope key");
|
||||
}
|
||||
}
|
||||
|
||||
+7
-2
@@ -1,9 +1,12 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.InProc;
|
||||
using Microsoft.Agents.AI.Workflows.Reflection;
|
||||
using Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Sample;
|
||||
|
||||
@@ -23,9 +26,11 @@ internal static class Step1EntryPoint
|
||||
}
|
||||
}
|
||||
|
||||
public static async ValueTask RunAsync(TextWriter writer)
|
||||
public static async ValueTask RunAsync(TextWriter writer, ExecutionMode executionMode)
|
||||
{
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(WorkflowInstance, "Hello, World!").ConfigureAwait(false);
|
||||
InProcessExecutionEnvironment env = executionMode.GetEnvironment();
|
||||
|
||||
StreamingRun run = await env.StreamAsync(WorkflowInstance, "Hello, World!").ConfigureAwait(false);
|
||||
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
|
||||
+5
-3
@@ -2,16 +2,18 @@
|
||||
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.InProc;
|
||||
using Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
using static Microsoft.Agents.AI.Workflows.Sample.Step1EntryPoint;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Sample;
|
||||
|
||||
internal static class Step1aEntryPoint
|
||||
{
|
||||
public static async ValueTask RunAsync(TextWriter writer)
|
||||
public static async ValueTask RunAsync(TextWriter writer, ExecutionMode executionMode)
|
||||
{
|
||||
Run run = await InProcessExecution.RunAsync(WorkflowInstance, "Hello, World!").ConfigureAwait(false);
|
||||
InProcessExecutionEnvironment env = executionMode.GetEnvironment();
|
||||
Run run = await env.RunAsync(WorkflowInstance, "Hello, World!").ConfigureAwait(false);
|
||||
|
||||
Assert.Equal(RunStatus.Idle, await run.GetStatusAsync());
|
||||
|
||||
|
||||
+5
-2
@@ -4,7 +4,9 @@ using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.InProc;
|
||||
using Microsoft.Agents.AI.Workflows.Reflection;
|
||||
using Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Sample;
|
||||
|
||||
@@ -28,9 +30,10 @@ internal static class Step2EntryPoint
|
||||
}
|
||||
}
|
||||
|
||||
public static async ValueTask<string> RunAsync(TextWriter writer, string input = "This is a spam message.")
|
||||
public static async ValueTask<string> RunAsync(TextWriter writer, ExecutionMode executionMode, string input = "This is a spam message.")
|
||||
{
|
||||
StreamingRun handle = await InProcessExecution.StreamAsync(WorkflowInstance, input).ConfigureAwait(false);
|
||||
InProcessExecutionEnvironment env = executionMode.GetEnvironment();
|
||||
StreamingRun handle = await env.StreamAsync(WorkflowInstance, input).ConfigureAwait(false);
|
||||
await foreach (WorkflowEvent evt in handle.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
switch (evt)
|
||||
|
||||
+5
-2
@@ -4,7 +4,9 @@ using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.InProc;
|
||||
using Microsoft.Agents.AI.Workflows.Reflection;
|
||||
using Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Sample;
|
||||
|
||||
@@ -25,9 +27,10 @@ internal static class Step3EntryPoint
|
||||
}
|
||||
}
|
||||
|
||||
public static async ValueTask<string> RunAsync(TextWriter writer)
|
||||
public static async ValueTask<string> RunAsync(TextWriter writer, ExecutionMode executionMode)
|
||||
{
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(WorkflowInstance, NumberSignal.Init).ConfigureAwait(false);
|
||||
InProcessExecutionEnvironment env = executionMode.GetEnvironment();
|
||||
StreamingRun run = await env.StreamAsync(WorkflowInstance, NumberSignal.Init).ConfigureAwait(false);
|
||||
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
{
|
||||
|
||||
+5
-2
@@ -4,6 +4,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.InProc;
|
||||
using Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Sample;
|
||||
|
||||
@@ -37,13 +39,14 @@ internal static class Step4EntryPoint
|
||||
}
|
||||
}
|
||||
|
||||
public static async ValueTask<string> RunAsync(TextWriter writer, Func<string, int> userGuessCallback)
|
||||
public static async ValueTask<string> RunAsync(TextWriter writer, Func<string, int> userGuessCallback, ExecutionMode executionMode)
|
||||
{
|
||||
NumberSignal signal = NumberSignal.Init;
|
||||
string? prompt = UpdatePrompt(null, signal);
|
||||
|
||||
Workflow workflow = WorkflowInstance;
|
||||
StreamingRun handle = await InProcessExecution.StreamAsync(workflow, NumberSignal.Init).ConfigureAwait(false);
|
||||
InProcessExecutionEnvironment env = executionMode.GetEnvironment();
|
||||
StreamingRun handle = await env.StreamAsync(workflow, NumberSignal.Init).ConfigureAwait(false);
|
||||
|
||||
List<ExternalRequest> requests = [];
|
||||
await foreach (WorkflowEvent evt in handle.WatchStreamAsync().ConfigureAwait(false))
|
||||
|
||||
+22
-17
@@ -6,12 +6,14 @@ using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Agents.AI.Workflows.InProc;
|
||||
using Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Sample;
|
||||
|
||||
internal static class Step5EntryPoint
|
||||
{
|
||||
public static async ValueTask<string> RunAsync(TextWriter writer, Func<string, int> userGuessCallback, bool rehydrateToRestore = false, CheckpointManager? checkpointManager = null)
|
||||
public static async ValueTask<string> RunAsync(TextWriter writer, Func<string, int> userGuessCallback, ExecutionMode executionMode, bool rehydrateToRestore = false, CheckpointManager? checkpointManager = null)
|
||||
{
|
||||
Dictionary<CheckpointInfo, (NumberSignal signal, string? prompt)> checkpointedOutputs = [];
|
||||
|
||||
@@ -21,9 +23,11 @@ internal static class Step5EntryPoint
|
||||
checkpointManager ??= CheckpointManager.Default;
|
||||
|
||||
Workflow workflow = Step4EntryPoint.CreateWorkflowInstance(out JudgeExecutor judge);
|
||||
|
||||
InProcessExecutionEnvironment env = executionMode.GetEnvironment();
|
||||
Checkpointed<StreamingRun> checkpointed =
|
||||
await InProcessExecution.StreamAsync(workflow, NumberSignal.Init, checkpointManager)
|
||||
.ConfigureAwait(false);
|
||||
await env.StreamAsync(workflow, NumberSignal.Init, checkpointManager)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
List<CheckpointInfo> checkpoints = [];
|
||||
CancellationTokenSource cancellationSource = new();
|
||||
@@ -40,10 +44,10 @@ internal static class Step5EntryPoint
|
||||
Console.WriteLine($"Restoring to checkpoint {targetCheckpoint} from run {targetCheckpoint.RunId}");
|
||||
if (rehydrateToRestore)
|
||||
{
|
||||
await handle.EndRunAsync().ConfigureAwait(false);
|
||||
await handle.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
checkpointed = await InProcessExecution.ResumeStreamAsync(workflow, targetCheckpoint, checkpointManager, runId: handle.RunId, cancellationToken: CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
checkpointed = await env.ResumeStreamAsync(workflow, targetCheckpoint, checkpointManager, runId: handle.RunId, cancellationToken: CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
handle = checkpointed.Run;
|
||||
}
|
||||
else
|
||||
@@ -112,20 +116,21 @@ internal static class Step5EntryPoint
|
||||
{
|
||||
Console.WriteLine($"*** Max step {maxStep} reached, cancelling.");
|
||||
cancellationSource.Cancel();
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"*** Processing {requests.Count} queued requests.");
|
||||
foreach (ExternalRequest request in requests)
|
||||
{
|
||||
ExternalResponse response = ExecuteExternalRequest(request, userGuessCallback, prompt);
|
||||
Console.WriteLine($"!!! Sending response: {response}");
|
||||
await handle.SendResponseAsync(response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
requests.Clear();
|
||||
Console.WriteLine("*** Completed processing requests.");
|
||||
Console.WriteLine($"*** Processing {requests.Count} queued requests.");
|
||||
foreach (ExternalRequest request in requests)
|
||||
{
|
||||
ExternalResponse response = ExecuteExternalRequest(request, userGuessCallback, prompt);
|
||||
Console.WriteLine($"!!! Sending response: {response}");
|
||||
await handle.SendResponseAsync(response).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
requests.Clear();
|
||||
|
||||
Console.WriteLine("*** Completed processing requests.");
|
||||
|
||||
break;
|
||||
|
||||
case ExecutorCompletedEvent executorCompleteEvt:
|
||||
|
||||
+6
-3
@@ -10,6 +10,8 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.InProc;
|
||||
using Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Sample;
|
||||
@@ -22,12 +24,13 @@ internal static class Step6EntryPoint
|
||||
.AddParticipants(new HelloAgent(), new EchoAgent())
|
||||
.Build();
|
||||
|
||||
public static async ValueTask RunAsync(TextWriter writer, int maxSteps = 2)
|
||||
public static async ValueTask RunAsync(TextWriter writer, ExecutionMode executionMode, int maxSteps = 2)
|
||||
{
|
||||
Workflow workflow = CreateWorkflow(maxSteps);
|
||||
|
||||
StreamingRun run = await InProcessExecution.StreamAsync(workflow, Array.Empty<ChatMessage>())
|
||||
.ConfigureAwait(false);
|
||||
InProcessExecutionEnvironment env = executionMode.GetEnvironment();
|
||||
StreamingRun run = await env.StreamAsync(workflow, Array.Empty<ChatMessage>())
|
||||
.ConfigureAwait(false);
|
||||
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
|
||||
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
|
||||
|
||||
-2
@@ -1,6 +1,5 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
@@ -26,7 +25,6 @@ internal static class Step7EntryPoint
|
||||
?? update.AgentId
|
||||
?? update.Role.ToString()
|
||||
?? ChatRole.Assistant.ToString()}: {update.Text}";
|
||||
Console.WriteLine(updateText);
|
||||
writer.WriteLine(updateText);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -7,6 +7,8 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Agents.AI.Workflows.InProc;
|
||||
using Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Sample;
|
||||
|
||||
@@ -26,7 +28,7 @@ internal static class Step8EntryPoint
|
||||
" Spaces around text ",
|
||||
];
|
||||
|
||||
public static async ValueTask<List<TextProcessingResult>> RunAsync(TextWriter writer, List<string> textsToProcess)
|
||||
public static async ValueTask<List<TextProcessingResult>> RunAsync(TextWriter writer, ExecutionMode executionMode, List<string> textsToProcess)
|
||||
{
|
||||
Func<TextProcessingRequest, IWorkflowContext, CancellationToken, ValueTask> processTextAsyncFunc = ProcessTextAsync;
|
||||
ExecutorIsh processText = processTextAsyncFunc.AsExecutor("TextProcessor");
|
||||
@@ -41,7 +43,8 @@ internal static class Step8EntryPoint
|
||||
.AddEdge(textProcessor, orchestrator)
|
||||
.Build();
|
||||
|
||||
Run workflowRun = await InProcessExecution.RunAsync(workflow, textsToProcess);
|
||||
InProcessExecutionEnvironment env = executionMode.GetEnvironment();
|
||||
Run workflowRun = await env.RunAsync(workflow, textsToProcess);
|
||||
|
||||
RunStatus status = await workflowRun.GetStatusAsync();
|
||||
status.Should().Be(RunStatus.Idle);
|
||||
|
||||
+5
-2
@@ -7,6 +7,8 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Agents.AI.Workflows.InProc;
|
||||
using Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Sample;
|
||||
|
||||
@@ -170,12 +172,13 @@ internal static class Step9EntryPoint
|
||||
.Select(request => Part2FinishedResponses[request.Id])
|
||||
.OrderBy(request => request.Id)];
|
||||
|
||||
public static async ValueTask<List<RequestFinished>> RunAsync(TextWriter writer)
|
||||
public static async ValueTask<List<RequestFinished>> RunAsync(TextWriter writer, ExecutionMode executionMode)
|
||||
{
|
||||
RunStatus runStatus;
|
||||
List<RequestFinished> results = [];
|
||||
|
||||
Run workflowRun = await InProcessExecution.RunAsync(WorkflowInstance, RequestsToProcess.ToList());
|
||||
InProcessExecutionEnvironment env = executionMode.GetEnvironment();
|
||||
Run workflowRun = await env.RunAsync(WorkflowInstance, RequestsToProcess.ToList());
|
||||
|
||||
RunStatus part1Status = ExpectedResponsesPart2.Length > 0 ? RunStatus.PendingRequests : RunStatus.Idle;
|
||||
runStatus = await workflowRun.GetStatusAsync();
|
||||
|
||||
@@ -13,12 +13,14 @@ namespace Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
public class SampleSmokeTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task Test_RunSample_Step1Async()
|
||||
[Theory]
|
||||
[InlineData(ExecutionMode.Lockstep)]
|
||||
[InlineData(ExecutionMode.OffThread)]
|
||||
internal async Task Test_RunSample_Step1Async(ExecutionMode executionMode)
|
||||
{
|
||||
using StringWriter writer = new();
|
||||
|
||||
await Step1EntryPoint.RunAsync(writer);
|
||||
await Step1EntryPoint.RunAsync(writer, executionMode);
|
||||
|
||||
string result = writer.ToString();
|
||||
string[] lines = result.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
|
||||
@@ -31,12 +33,14 @@ public class SampleSmokeTest
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_RunSample_Step1aAsync()
|
||||
[Theory]
|
||||
[InlineData(ExecutionMode.Lockstep)]
|
||||
[InlineData(ExecutionMode.OffThread)]
|
||||
internal async Task Test_RunSample_Step1aAsync(ExecutionMode executionMode)
|
||||
{
|
||||
using StringWriter writer = new();
|
||||
|
||||
await Step1aEntryPoint.RunAsync(writer);
|
||||
await Step1aEntryPoint.RunAsync(writer, executionMode);
|
||||
|
||||
string result = writer.ToString();
|
||||
string[] lines = result.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
|
||||
@@ -49,32 +53,38 @@ public class SampleSmokeTest
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_RunSample_Step2Async()
|
||||
[Theory]
|
||||
[InlineData(ExecutionMode.Lockstep)]
|
||||
[InlineData(ExecutionMode.OffThread)]
|
||||
internal async Task Test_RunSample_Step2Async(ExecutionMode executionMode)
|
||||
{
|
||||
using StringWriter writer = new();
|
||||
|
||||
string spamResult = await Step2EntryPoint.RunAsync(writer);
|
||||
string spamResult = await Step2EntryPoint.RunAsync(writer, executionMode);
|
||||
|
||||
Assert.Equal(RemoveSpamExecutor.ActionResult, spamResult);
|
||||
|
||||
string nonSpamResult = await Step2EntryPoint.RunAsync(writer, "This is a valid message.");
|
||||
string nonSpamResult = await Step2EntryPoint.RunAsync(writer, executionMode, "This is a valid message.");
|
||||
|
||||
Assert.Equal(RespondToMessageExecutor.ActionResult, nonSpamResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_RunSample_Step3Async()
|
||||
[Theory]
|
||||
[InlineData(ExecutionMode.Lockstep)]
|
||||
[InlineData(ExecutionMode.OffThread)]
|
||||
internal async Task Test_RunSample_Step3Async(ExecutionMode executionMode)
|
||||
{
|
||||
using StringWriter writer = new();
|
||||
|
||||
string guessResult = await Step3EntryPoint.RunAsync(writer);
|
||||
string guessResult = await Step3EntryPoint.RunAsync(writer, executionMode);
|
||||
|
||||
Assert.Equal("Guessed the number: 42", guessResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_RunSample_Step4Async()
|
||||
[Theory]
|
||||
[InlineData(ExecutionMode.Lockstep)]
|
||||
[InlineData(ExecutionMode.OffThread)]
|
||||
internal async Task Test_RunSample_Step4Async(ExecutionMode executionMode)
|
||||
{
|
||||
using StringWriter writer = new();
|
||||
|
||||
@@ -83,12 +93,14 @@ public class SampleSmokeTest
|
||||
("Your guess was too high. Try again.", 23),
|
||||
("Your guess was too low. Try again.", 42));
|
||||
|
||||
string guessResult = await Step4EntryPoint.RunAsync(writer, userGuessCallback: responder.InvokeNext);
|
||||
string guessResult = await Step4EntryPoint.RunAsync(writer, userGuessCallback: responder.InvokeNext, executionMode);
|
||||
Assert.Equal("You guessed correctly! You Win!", guessResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_RunSample_Step5Async()
|
||||
[Theory]
|
||||
[InlineData(ExecutionMode.Lockstep)]
|
||||
[InlineData(ExecutionMode.OffThread)]
|
||||
internal async Task Test_RunSample_Step5Async(ExecutionMode executionMode)
|
||||
{
|
||||
using StringWriter writer = new();
|
||||
|
||||
@@ -102,12 +114,14 @@ public class SampleSmokeTest
|
||||
("Your guess was too low. Try again.", 42)
|
||||
);
|
||||
|
||||
string guessResult = await Step5EntryPoint.RunAsync(writer, userGuessCallback: responder.InvokeNext);
|
||||
string guessResult = await Step5EntryPoint.RunAsync(writer, userGuessCallback: responder.InvokeNext, executionMode);
|
||||
Assert.Equal("You guessed correctly! You Win!", guessResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_RunSample_Step5aAsync()
|
||||
[Theory]
|
||||
[InlineData(ExecutionMode.Lockstep)]
|
||||
[InlineData(ExecutionMode.OffThread)]
|
||||
internal async Task Test_RunSample_Step5aAsync(ExecutionMode executionMode)
|
||||
{
|
||||
using StringWriter writer = new();
|
||||
|
||||
@@ -121,12 +135,14 @@ public class SampleSmokeTest
|
||||
("Your guess was too low. Try again.", 42)
|
||||
);
|
||||
|
||||
string guessResult = await Step5EntryPoint.RunAsync(writer, userGuessCallback: responder.InvokeNext, rehydrateToRestore: true);
|
||||
string guessResult = await Step5EntryPoint.RunAsync(writer, userGuessCallback: responder.InvokeNext, executionMode, rehydrateToRestore: true);
|
||||
Assert.Equal("You guessed correctly! You Win!", guessResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_RunSample_Step5bAsync()
|
||||
[Theory]
|
||||
[InlineData(ExecutionMode.Lockstep)]
|
||||
[InlineData(ExecutionMode.OffThread)]
|
||||
internal async Task Test_RunSample_Step5bAsync(ExecutionMode executionMode)
|
||||
{
|
||||
using StringWriter writer = new();
|
||||
|
||||
@@ -144,16 +160,18 @@ public class SampleSmokeTest
|
||||
options.MakeReadOnly();
|
||||
|
||||
CheckpointManager memoryJsonManager = CheckpointManager.CreateJson(new InMemoryJsonStore(), options);
|
||||
string guessResult = await Step5EntryPoint.RunAsync(writer, userGuessCallback: responder.InvokeNext, rehydrateToRestore: true, checkpointManager: memoryJsonManager);
|
||||
string guessResult = await Step5EntryPoint.RunAsync(writer, userGuessCallback: responder.InvokeNext, executionMode, rehydrateToRestore: true, checkpointManager: memoryJsonManager);
|
||||
Assert.Equal("You guessed correctly! You Win!", guessResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_RunSample_Step6Async()
|
||||
[Theory]
|
||||
[InlineData(ExecutionMode.Lockstep)]
|
||||
[InlineData(ExecutionMode.OffThread)]
|
||||
internal async Task Test_RunSample_Step6Async(ExecutionMode executionMode)
|
||||
{
|
||||
using StringWriter writer = new();
|
||||
|
||||
await Step6EntryPoint.RunAsync(writer);
|
||||
await Step6EntryPoint.RunAsync(writer, executionMode);
|
||||
|
||||
string result = writer.ToString();
|
||||
string[] lines = result.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
|
||||
@@ -180,8 +198,10 @@ public class SampleSmokeTest
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_RunSample_Step8Async()
|
||||
[Theory]
|
||||
[InlineData(ExecutionMode.Lockstep)]
|
||||
[InlineData(ExecutionMode.OffThread)]
|
||||
internal async Task Test_RunSample_Step8Async(ExecutionMode executionMode)
|
||||
{
|
||||
List<string> textsToProcess = [
|
||||
"Hello world! This is a simple test.",
|
||||
@@ -194,7 +214,7 @@ public class SampleSmokeTest
|
||||
|
||||
using StringWriter writer = new();
|
||||
|
||||
List<TextProcessingResult> results = await Step8EntryPoint.RunAsync(writer, textsToProcess);
|
||||
List<TextProcessingResult> results = await Step8EntryPoint.RunAsync(writer, executionMode, textsToProcess);
|
||||
Assert.Equal(textsToProcess.Count, results.Count);
|
||||
|
||||
Assert.Collection(results,
|
||||
@@ -216,11 +236,13 @@ public class SampleSmokeTest
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_RunSample_Step9Async()
|
||||
[Theory]
|
||||
[InlineData(ExecutionMode.Lockstep)]
|
||||
[InlineData(ExecutionMode.OffThread)]
|
||||
internal async Task Test_RunSample_Step9Async(ExecutionMode executionMode)
|
||||
{
|
||||
using StringWriter writer = new();
|
||||
_ = await Step9EntryPoint.RunAsync(writer);
|
||||
_ = await Step9EntryPoint.RunAsync(writer, executionMode);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user