mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Add support for Subworkflows and many threading fixes (#1066)
* feat: Add support for Workflow-as-Executor * Fixes routing of 'object' compile-typed variables to properly take in type information * Fixes a concurrency issue in StepTracer * fix: Make Subworkflow ExternalRequests work properly * fix: Threading and Concurrency fixes; prep for OffThread Mode * refactor: Remove dead code around OffStreamRunEventStream Currently not used, and will be replaced with a rewrite when brought back, so having it in the change is not valuable. * ci: Work around issues with dotnet-format not properly analyzing the source * fix: Fix the logic of AsyncCoordinator and AsyncBarrier * Prevent individual wait cancellations from canceling the entire barrier * Propagate information about whether the wait was completed or cancelled, and whether any waiters were present when released * fix: Remove superfluous acces to .Keys in InProcStepTracer * refactor: Clean up AsyncCoordinator's use of AsyncBarrier
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -18,18 +19,20 @@ namespace Microsoft.Agents.AI.Workflows.InProc;
|
||||
/// <remarks><para> <see cref="InProcessRunner"/> enables step-by-step execution of a workflow graph entirely
|
||||
/// within the current process, without distributed coordination. It is primarily intended for testing, debugging, or
|
||||
/// scenarios where workflow execution does not require executor distribution. </para></remarks>
|
||||
internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingRunner
|
||||
internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
|
||||
{
|
||||
public InProcessRunner(Workflow workflow, ICheckpointManager? checkpointManager, string? runId = null, params Type[] knownValidInputTypes)
|
||||
public InProcessRunner(Workflow workflow, ICheckpointManager? checkpointManager, string? runId = null, object? workflowOwnership = null, bool subworkflow = false, IEnumerable<Type>? knownValidInputTypes = null)
|
||||
{
|
||||
this.RunId = runId ?? Guid.NewGuid().ToString("N");
|
||||
this.StartExecutorId = workflow.StartExecutorId;
|
||||
|
||||
this.Workflow = Throw.IfNull(workflow);
|
||||
this.RunContext = new InProcessRunnerContext(workflow, this.RunId, this.StepTracer);
|
||||
this.RunContext = new InProcessRunnerContext(workflow, this.RunId, withCheckpointing: checkpointManager != null, this.OutgoingEvents, this.StepTracer, workflowOwnership, subworkflow);
|
||||
this.CheckpointManager = checkpointManager;
|
||||
|
||||
this._knownValidInputTypes = [.. knownValidInputTypes];
|
||||
this._knownValidInputTypes = knownValidInputTypes != null
|
||||
? [.. knownValidInputTypes]
|
||||
: [];
|
||||
|
||||
// Initialize the runners for each of the edges, along with the state for edges that
|
||||
// need it.
|
||||
@@ -43,7 +46,7 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingRunner
|
||||
public string StartExecutorId { get; }
|
||||
|
||||
private readonly HashSet<Type> _knownValidInputTypes;
|
||||
public async ValueTask<bool> IsValidInputTypeAsync(Type messageType)
|
||||
public async ValueTask<bool> IsValidInputTypeAsync(Type messageType, CancellationToken cancellation = default)
|
||||
{
|
||||
if (this._knownValidInputTypes.Contains(messageType))
|
||||
{
|
||||
@@ -60,7 +63,10 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingRunner
|
||||
return false;
|
||||
}
|
||||
|
||||
private async ValueTask<bool> EnqueueMessageInternalAsync(object message, Type messageType)
|
||||
public ValueTask<bool> IsValidInputTypeAsync<T>(CancellationToken cancellation = default)
|
||||
=> this.IsValidInputTypeAsync(typeof(T), cancellation);
|
||||
|
||||
public async ValueTask<bool> EnqueueMessageUntypedAsync(object message, Type declaredType, CancellationToken cancellation = default)
|
||||
{
|
||||
this.RunContext.CheckEnded();
|
||||
Throw.IfNull(message);
|
||||
@@ -72,22 +78,22 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingRunner
|
||||
|
||||
// Check that the type of the incoming message is compatible with the starting executor's
|
||||
// input type.
|
||||
if (!await this.IsValidInputTypeAsync(messageType).ConfigureAwait(false))
|
||||
if (!await this.IsValidInputTypeAsync(declaredType, cancellation).ConfigureAwait(false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.RunContext.AddExternalMessageAsync(message, messageType).ConfigureAwait(false);
|
||||
await this.RunContext.AddExternalMessageAsync(message, declaredType).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
public ValueTask<bool> EnqueueMessageAsync<T>(T message)
|
||||
=> this.EnqueueMessageInternalAsync(Throw.IfNull(message), typeof(T));
|
||||
public ValueTask<bool> EnqueueMessageAsync<T>(T message, CancellationToken cancellation = default)
|
||||
=> this.EnqueueMessageUntypedAsync(Throw.IfNull(message), typeof(T), cancellation);
|
||||
|
||||
public ValueTask<bool> EnqueueMessageAsync(object message)
|
||||
=> this.EnqueueMessageInternalAsync(Throw.IfNull(message), message.GetType());
|
||||
public ValueTask<bool> EnqueueMessageAsync(object message, CancellationToken cancellation = default)
|
||||
=> this.EnqueueMessageUntypedAsync(Throw.IfNull(message), message.GetType(), cancellation);
|
||||
|
||||
ValueTask ISuperStepRunner.EnqueueResponseAsync(ExternalResponse response)
|
||||
ValueTask ISuperStepRunner.EnqueueResponseAsync(ExternalResponse response, CancellationToken cancellation)
|
||||
{
|
||||
// TODO: Check that there exists a corresponding input port?
|
||||
return this.RunContext.AddExternalResponseAsync(response);
|
||||
@@ -95,78 +101,32 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingRunner
|
||||
|
||||
private InProcStepTracer StepTracer { get; } = new();
|
||||
private Workflow Workflow { get; init; }
|
||||
private InProcessRunnerContext RunContext { get; init; }
|
||||
internal InProcessRunnerContext RunContext { get; init; }
|
||||
private ICheckpointManager? CheckpointManager { get; }
|
||||
private EdgeMap EdgeMap { get; init; }
|
||||
|
||||
event EventHandler<WorkflowEvent>? ISuperStepRunner.WorkflowEvent
|
||||
{
|
||||
add => this.WorkflowEvent += value;
|
||||
remove => this.WorkflowEvent -= value;
|
||||
}
|
||||
public ConcurrentEventSink OutgoingEvents { get; } = new();
|
||||
|
||||
private event EventHandler<WorkflowEvent>? WorkflowEvent;
|
||||
private ValueTask RaiseWorkflowEventAsync(WorkflowEvent workflowEvent)
|
||||
=> this.OutgoingEvents.EnqueueAsync(workflowEvent);
|
||||
|
||||
private void RaiseWorkflowEvent(WorkflowEvent workflowEvent)
|
||||
{
|
||||
this.WorkflowEvent?.Invoke(this, workflowEvent);
|
||||
}
|
||||
|
||||
public async ValueTask<StreamingRun> ResumeStreamAsync(CheckpointInfo checkpoint, CancellationToken cancellationToken = default)
|
||||
public ValueTask<AsyncRunHandle> BeginStreamAsync(ExecutionMode mode, CancellationToken cancellation = default)
|
||||
{
|
||||
this.RunContext.CheckEnded();
|
||||
Throw.IfNull(checkpoint);
|
||||
return new(new AsyncRunHandle(this, this, mode));
|
||||
}
|
||||
|
||||
public async ValueTask<AsyncRunHandle> ResumeStreamAsync(ExecutionMode mode, CheckpointInfo fromCheckpoint, CancellationToken cancellation = default)
|
||||
{
|
||||
this.RunContext.CheckEnded();
|
||||
Throw.IfNull(fromCheckpoint);
|
||||
if (this.CheckpointManager is null)
|
||||
{
|
||||
throw new InvalidOperationException("This runner was not configured with a CheckpointManager, so it cannot restore checkpoints.");
|
||||
}
|
||||
|
||||
await this.RestoreCheckpointAsync(checkpoint, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new StreamingRun(this);
|
||||
}
|
||||
|
||||
public async ValueTask<StreamingRun> StreamAsync(object input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.RunContext.CheckEnded();
|
||||
await this.EnqueueMessageAsync(input).ConfigureAwait(false);
|
||||
|
||||
return new StreamingRun(this);
|
||||
}
|
||||
|
||||
public async ValueTask<StreamingRun> StreamAsync<TInput>(TInput input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.RunContext.CheckEnded();
|
||||
await this.EnqueueMessageAsync(input).ConfigureAwait(false);
|
||||
|
||||
return new StreamingRun(this);
|
||||
}
|
||||
|
||||
internal async ValueTask<Run> ResumeAsync(CheckpointInfo checkpoint, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.RunContext.CheckEnded();
|
||||
StreamingRun streamingRun = await this.ResumeStreamAsync(checkpoint, cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
return await Run.CaptureStreamAsync(streamingRun, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask<Run> RunAsync(object input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.RunContext.CheckEnded();
|
||||
StreamingRun streamingRun = await this.StreamAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
return await Run.CaptureStreamAsync(streamingRun, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask<Run> RunAsync<TInput>(TInput input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.RunContext.CheckEnded();
|
||||
StreamingRun streamingRun = await this.StreamAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
return await Run.CaptureStreamAsync(streamingRun, cancellationToken).ConfigureAwait(false);
|
||||
await this.RestoreCheckpointAsync(fromCheckpoint, cancellation).ConfigureAwait(false);
|
||||
return new AsyncRunHandle(this, this, mode);
|
||||
}
|
||||
|
||||
bool ISuperStepRunner.HasUnservicedRequests => this.RunContext.HasUnservicedRequests;
|
||||
@@ -184,34 +144,23 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingRunner
|
||||
|
||||
StepContext currentStep = await this.RunContext.AdvanceAsync().ConfigureAwait(false);
|
||||
|
||||
if (currentStep.HasMessages)
|
||||
if (currentStep.HasMessages ||
|
||||
this.RunContext.HasQueuedExternalDeliveries ||
|
||||
this.RunContext.JoinedRunnersHaveActions)
|
||||
{
|
||||
await this.RunSuperstepAsync(currentStep).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
this.EmitPendingEvents();
|
||||
return false;
|
||||
}
|
||||
|
||||
private void EmitPendingEvents()
|
||||
{
|
||||
if (this.RunContext.QueuedEvents.Count > 0)
|
||||
{
|
||||
foreach (WorkflowEvent @event in this.RunContext.QueuedEvents)
|
||||
{
|
||||
this.RaiseWorkflowEvent(@event);
|
||||
}
|
||||
this.RunContext.QueuedEvents.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask DeliverMessagesAsync(string receiverId, List<MessageEnvelope> envelopes)
|
||||
private async ValueTask DeliverMessagesAsync(string receiverId, ConcurrentQueue<MessageEnvelope> envelopes)
|
||||
{
|
||||
Executor executor = await this.RunContext.EnsureExecutorAsync(receiverId, this.StepTracer).ConfigureAwait(false);
|
||||
|
||||
this.StepTracer.TraceActivated(receiverId);
|
||||
foreach (MessageEnvelope envelope in envelopes)
|
||||
while (envelopes.TryDequeue(out var envelope))
|
||||
{
|
||||
await executor.ExecuteAsync(
|
||||
envelope.Message,
|
||||
@@ -223,7 +172,7 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingRunner
|
||||
|
||||
private async ValueTask RunSuperstepAsync(StepContext currentStep)
|
||||
{
|
||||
this.RaiseWorkflowEvent(this.StepTracer.Advance(currentStep));
|
||||
await this.RaiseWorkflowEventAsync(this.StepTracer.Advance(currentStep)).ConfigureAwait(false);
|
||||
|
||||
// Deliver the messages and queue the next step
|
||||
List<Task> receiverTasks =
|
||||
@@ -236,12 +185,22 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingRunner
|
||||
// that we would need to avoid firing the tasks when we call InvokeEdgeAsync, or RouteExternalMessageAsync.
|
||||
await Task.WhenAll(receiverTasks).ConfigureAwait(false);
|
||||
|
||||
// After the message handler invocations, we may have some events to deliver
|
||||
this.EmitPendingEvents();
|
||||
// When we have sub-workflows, sending a message to the WorkflowHostExecutor will only queue it into the
|
||||
// subworkflow's input queue. In order to actually process the message and align the supersteps correctly,
|
||||
// we need to drive the superstep of the subworkflow here.
|
||||
// TODO: Investigate if we can fully pull in the subworkflow execution into the WorkflowHostExecutor itself.
|
||||
List<Task> subworkflowTasks = new();
|
||||
foreach (ISuperStepRunner subworkflowRunner in this.RunContext.JoinedSubworkflowRunners)
|
||||
{
|
||||
subworkflowTasks.Add(subworkflowRunner.RunSuperStepAsync(CancellationToken.None).AsTask());
|
||||
}
|
||||
|
||||
await Task.WhenAll(subworkflowTasks).ConfigureAwait(false);
|
||||
|
||||
await this.CheckpointAsync().ConfigureAwait(false);
|
||||
|
||||
this.RaiseWorkflowEvent(this.StepTracer.Complete(this.RunContext.NextStepHasActions, this.RunContext.HasUnservicedRequests));
|
||||
await this.RaiseWorkflowEventAsync(this.StepTracer.Complete(this.RunContext.NextStepHasActions, this.RunContext.HasUnservicedRequests))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private WorkflowInfo? _workflowInfoCache;
|
||||
@@ -310,5 +269,5 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingRunner
|
||||
private bool CheckWorkflowMatch(Checkpoint checkpoint) =>
|
||||
checkpoint.Workflow.IsMatch(this.Workflow);
|
||||
|
||||
ValueTask ISuperStepRunner.RequestEndRunAsync() => this.RunContext.EndRunAsync();
|
||||
public ValueTask RequestEndRunAsync() => this.RunContext.EndRunAsync();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user