refactor: [BREAKING] Remove generic Workflow<T> (#1551)

Remove input type checking in favour of explicit `.DescribeProtocolAsync()` flow. Also removes `.AsAgentAsync()` as the validation happens at workflow run time. This makes it easier to use Workflows with DI without resorting to async-over-sync.
This commit is contained in:
Jacob Alber
2025-10-20 20:13:41 -04:00
committed by GitHub
Unverified
parent 7c1e3db846
commit 0ef4e739d5
43 changed files with 284 additions and 461 deletions
@@ -36,6 +36,18 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
return runner.ResumeStreamAsync(this.ExecutionMode, fromCheckpoint, cancellationToken);
}
/// <inheritdoc/>
public async ValueTask<StreamingRun> StreamAsync(
Workflow workflow,
string? runId = null,
CancellationToken cancellationToken = default)
{
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager: null, runId: runId, [], cancellationToken)
.ConfigureAwait(false);
return new(runHandle);
}
/// <inheritdoc/>
public async ValueTask<StreamingRun> StreamAsync<TInput>(
Workflow workflow,
@@ -50,16 +62,17 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
}
/// <inheritdoc/>
public async ValueTask<StreamingRun> StreamAsync<TInput>(
Workflow<TInput> workflow,
TInput input,
public async ValueTask<Checkpointed<StreamingRun>> StreamAsync(
Workflow workflow,
CheckpointManager checkpointManager,
string? runId = null,
CancellationToken cancellationToken = default) where TInput : notnull
CancellationToken cancellationToken = default)
{
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager: null, runId: runId, [typeof(TInput)], cancellationToken)
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager, runId: runId, [], cancellationToken)
.ConfigureAwait(false);
return await runHandle.EnqueueAndStreamAsync(input, cancellationToken).ConfigureAwait(false);
return await runHandle.WithCheckpointingAsync<StreamingRun>(() => new(new StreamingRun(runHandle)))
.ConfigureAwait(false);
}
/// <inheritdoc/>
@@ -77,21 +90,6 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
.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,
@@ -107,19 +105,24 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
.ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync<TInput>(
Workflow<TInput> workflow,
CheckpointInfo fromCheckpoint,
CheckpointManager checkpointManager,
private async ValueTask<AsyncRunHandle> BeginRunHandlingChatProtocolAsync<TInput>(Workflow workflow,
TInput input,
CheckpointManager? checkpointManager,
string? runId = null,
CancellationToken cancellationToken = default) where TInput : notnull
CancellationToken cancellationToken = default)
{
AsyncRunHandle runHandle = await this.ResumeRunAsync(workflow, checkpointManager, runId: runId, fromCheckpoint, [typeof(TInput)], cancellationToken)
ProtocolDescriptor descriptor = await workflow.DescribeProtocolAsync(cancellationToken).ConfigureAwait(false);
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager, runId, descriptor.Accepts, cancellationToken)
.ConfigureAwait(false);
return await runHandle.WithCheckpointingAsync<StreamingRun>(() => new(new StreamingRun(runHandle)))
.ConfigureAwait(false);
await runHandle.EnqueueMessageAsync(input, cancellationToken).ConfigureAwait(false);
if (descriptor.IsChatProtocol() && input is not TurnToken)
{
await runHandle.EnqueueMessageAsync(new TurnToken(emitEvents: true), cancellationToken).ConfigureAwait(false);
}
return runHandle;
}
/// <inheritdoc/>
@@ -129,21 +132,13 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
string? runId = null,
CancellationToken cancellationToken = default) where TInput : notnull
{
var runHandle = await this.GetRunHandleWithTurnTokenAsync(workflow: workflow, input: input, checkpointManager: null, runId: runId, cancellationToken).ConfigureAwait(false);
Run run = new(runHandle);
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
return run;
}
/// <inheritdoc/>
public async ValueTask<Run> RunAsync<TInput>(
Workflow<TInput> workflow,
TInput input,
string? runId = null,
CancellationToken cancellationToken = default) where TInput : notnull
{
var runHandle = await this.GetRunHandleWithTurnTokenAsync(workflow: workflow, input: input, checkpointManager: null, runId: runId, cancellationToken).ConfigureAwait(false);
AsyncRunHandle runHandle = await this.BeginRunHandlingChatProtocolAsync(
workflow,
input,
checkpointManager: null,
runId,
cancellationToken)
.ConfigureAwait(false);
Run run = new(runHandle);
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
@@ -158,23 +153,13 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
string? runId = null,
CancellationToken cancellationToken = default) where TInput : notnull
{
var runHandle = await this.GetRunHandleWithTurnTokenAsync(workflow: workflow, input: input, checkpointManager: checkpointManager, runId: runId, cancellationToken).ConfigureAwait(false);
Run run = new(runHandle);
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
return await runHandle.WithCheckpointingAsync(() => new ValueTask<Run>(run))
.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
{
var runHandle = await this.GetRunHandleWithTurnTokenAsync(workflow: workflow, input: input, checkpointManager: checkpointManager, runId: runId, cancellationToken).ConfigureAwait(false);
AsyncRunHandle runHandle = await this.BeginRunHandlingChatProtocolAsync(
workflow,
input,
checkpointManager,
runId,
cancellationToken)
.ConfigureAwait(false);
Run run = new(runHandle);
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
@@ -196,63 +181,4 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
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);
}
// Helper to construct a RunHandle with the provided input enqueued. If the starting executor supports it, a TurnToken will be enqueued also.
private async ValueTask<AsyncRunHandle> GetRunHandleWithTurnTokenAsync<TInput>(
Workflow workflow,
TInput input,
CheckpointManager? checkpointManager,
string? runId,
CancellationToken cancellationToken)
{
var knownTypes = new List<Type>() { typeof(TInput) };
var needsTurnToken = await StartingExecutorHandlesTurnTokenAsync<TInput>(workflow).ConfigureAwait(false);
if (needsTurnToken)
{
knownTypes.Add(typeof(TurnToken));
}
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager: checkpointManager, runId: runId, knownTypes, cancellationToken)
.ConfigureAwait(false);
await runHandle.EnqueueMessageAsync(input, cancellationToken).ConfigureAwait(false);
if (needsTurnToken)
{
await runHandle.EnqueueMessageAsync(new TurnToken(emitEvents: true), cancellationToken).ConfigureAwait(false);
}
return runHandle;
}
/// <summary>
/// Helper method to detect if the starting executor of a given workflow accepts the provided input type as well as a TurnToken.
/// </summary>
private static async ValueTask<bool> StartingExecutorHandlesTurnTokenAsync<TInput>(Workflow workflow)
{
if (workflow.Registrations.TryGetValue(workflow.StartExecutorId, out var registration))
{
// Create instance to check type
Executor startExecutor = await registration.CreateInstanceAsync(string.Empty)
.ConfigureAwait(false);
return startExecutor.CanHandle(typeof(TInput)) && startExecutor.CanHandle(typeof(TurnToken));
}
return false;
}
}