[BREAKING] .NET: Decouple Checkpointing from Run/StreamAsync APIs (#4037)

* [BREAKING] refactor: Decouple Checkpointing and Execution APIs

With this change, Checkpointing becomes an property of an IWorkflowExecutionEnvironment. This lets environments that are tightly-coupled to their CheckpointManager avoid needing to present APIs that would not work (e.g. taking in an InMemory CheckpointManager for Durable Tasks, for example)

* refactor: Normalize IsCheckpointingEnabled naming
This commit is contained in:
Jacob Alber
2026-02-19 11:41:35 -05:00
committed by GitHub
Unverified
parent fd4e6e816c
commit c73bd87503
34 changed files with 299 additions and 317 deletions
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Checkpointing;
@@ -54,4 +55,7 @@ public sealed class CheckpointManager : ICheckpointManager
ValueTask<Checkpoint> ICheckpointManager.LookupCheckpointAsync(string runId, CheckpointInfo checkpointInfo)
=> this._impl.LookupCheckpointAsync(runId, checkpointInfo);
ValueTask<IEnumerable<CheckpointInfo>> ICheckpointManager.RetrieveIndexAsync(string runId, CheckpointInfo? withParent)
=> this._impl.RetrieveIndexAsync(runId, withParent);
}
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Checkpointing;
namespace Microsoft.Agents.AI.Workflows;
/// <summary>
/// Represents a base object for a workflow run that may support checkpointing.
/// </summary>
public abstract class CheckpointableRunBase
{
// TODO: Rename Context?
private readonly ICheckpointingHandle _checkpointingHandle;
internal CheckpointableRunBase(ICheckpointingHandle checkpointingHandle)
{
this._checkpointingHandle = checkpointingHandle;
}
/// <inheritdoc cref="ICheckpointingHandle.IsCheckpointingEnabled"/>
public bool IsCheckpointingEnabled => this._checkpointingHandle.IsCheckpointingEnabled;
/// <inheritdoc cref="ICheckpointingHandle.Checkpoints"/>
public IReadOnlyList<CheckpointInfo> Checkpoints => this._checkpointingHandle.Checkpoints ?? [];
/// <summary>
/// Gets the most recent checkpoint information.
/// </summary>
public CheckpointInfo? LastCheckpoint
{
get
{
if (!this.IsCheckpointingEnabled)
{
return null;
}
var checkpoints = this.Checkpoints;
return checkpoints.Count > 0 ? checkpoints[checkpoints.Count - 1] : null;
}
}
/// <inheritdoc cref="ICheckpointingHandle.RestoreCheckpointAsync"/>
public ValueTask RestoreCheckpointAsync(CheckpointInfo checkpointInfo, CancellationToken cancellationToken = default)
=> this._checkpointingHandle.RestoreCheckpointAsync(checkpointInfo, cancellationToken);
}
@@ -1,66 +0,0 @@
// 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.Shared.Diagnostics;
namespace Microsoft.Agents.AI.Workflows;
/// <summary>
/// Represents a workflow run that supports checkpointing.
/// </summary>
/// <typeparam name="TRun">The type of the underlying workflow run handle.</typeparam>
/// <seealso cref="Run"/>
/// <seealso cref="StreamingRun"/>
public sealed class Checkpointed<TRun> : IAsyncDisposable
{
private readonly ICheckpointingHandle _runner;
internal Checkpointed(TRun run, ICheckpointingHandle runner)
{
this.Run = Throw.IfNull(run);
this._runner = Throw.IfNull(runner);
}
/// <summary>
/// Gets the workflow run associated with this <see cref="Checkpointed{TRun}"/> instance.
/// </summary>
/// <seealso cref="Run"/>
/// <seealso cref="StreamingRun"/>
public TRun Run { get; }
/// <inheritdoc cref="ICheckpointingHandle.Checkpoints"/>
public IReadOnlyList<CheckpointInfo> Checkpoints => this._runner.Checkpoints;
/// <summary>
/// Gets the most recent checkpoint information.
/// </summary>
public CheckpointInfo? LastCheckpoint
{
get
{
var checkpoints = this.Checkpoints;
return checkpoints.Count > 0 ? checkpoints[checkpoints.Count - 1] : null;
}
}
/// <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);
}
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Microsoft.Agents.AI.Workflows.Checkpointing;
@@ -27,4 +28,7 @@ internal sealed class CheckpointManagerImpl<TStoreObject> : ICheckpointManager
TStoreObject result = await this._store.RetrieveCheckpointAsync(runId, checkpointInfo).ConfigureAwait(false);
return this._marshaller.Marshal<Checkpoint>(result);
}
public ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string runId, CheckpointInfo? withParent = null)
=> this._store.RetrieveIndexAsync(runId, withParent);
}
@@ -27,4 +27,16 @@ internal interface ICheckpointManager
/// cref="Checkpoint"/> associated with the specified <paramref name="checkpointInfo"/>.</returns>
/// <exception cref="KeyNotFoundException">Thrown if the checkpoint is not found.</exception>
ValueTask<Checkpoint> LookupCheckpointAsync(string runId, CheckpointInfo checkpointInfo);
/// <summary>
/// Asynchronously retrieves the collection of checkpoint information for the specified run identifier, optionally
/// filtered by a parent checkpoint.
/// </summary>
/// <param name="runId">The unique identifier of the run for which to retrieve checkpoint information. Cannot be null or empty.</param>
/// <param name="withParent">An optional parent checkpoint to filter the results. If specified, only checkpoints with the given parent are
/// returned; otherwise, all checkpoints for the run are included.</param>
/// <returns>A value task representing the asynchronous operation. The result contains a collection of <see
/// cref="CheckpointInfo"/> objects associated with the specified run. The collection is empty if no checkpoints are
/// found.</returns>
ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string runId, CheckpointInfo? withParent = null);
}
@@ -8,8 +8,21 @@ namespace Microsoft.Agents.AI.Workflows.Checkpointing;
internal interface ICheckpointingHandle
{
// TODO: Convert this to a multi-timeline (e.g.: Live timeline + forks for orphaned checkpoints due to timetravel)
/// <summary>
/// Gets a value indicating whether checkpointing is enabled for the current operation or process.
/// </summary>
bool IsCheckpointingEnabled { get; }
/// <summary>
/// Gets a read-only list of checkpoint information associated with the current context.
/// </summary>
IReadOnlyList<CheckpointInfo> Checkpoints { get; }
/// <summary>
/// Restores the system state from the specified checkpoint asynchronously.
/// </summary>
/// <param name="checkpointInfo">The checkpoint information that identifies the state to restore. Cannot be null.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the restore operation.</param>
/// <returns>A <see cref="ValueTask"/> that represents the asynchronous restore operation.</returns>
ValueTask RestoreCheckpointAsync(CheckpointInfo checkpointInfo, CancellationToken cancellationToken = default);
}
@@ -60,4 +60,7 @@ internal sealed class InMemoryCheckpointManager : ICheckpointManager
public bool TryGetLastCheckpoint(string runId, [NotNullWhen(true)] out CheckpointInfo? checkpoint)
=> this.GetRunStore(runId).TryGetLastCheckpointInfo(out checkpoint);
public ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string runId, CheckpointInfo? withParent = null)
=> new(this.GetRunStore(runId).CheckpointIndex.AsReadOnly());
}
@@ -46,6 +46,8 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable
public string RunId => this._stepRunner.RunId;
public bool IsCheckpointingEnabled => this._checkpointingHandle.IsCheckpointingEnabled;
public IReadOnlyList<CheckpointInfo> Checkpoints => this._checkpointingHandle.Checkpoints;
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellationToken = default)
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
@@ -8,12 +7,6 @@ namespace Microsoft.Agents.AI.Workflows.Execution;
internal static class AsyncRunHandleExtensions
{
public static async ValueTask<Checkpointed<TRunType>> WithCheckpointingAsync<TRunType>(this AsyncRunHandle runHandle, Func<ValueTask<TRunType>> prepareFunc)
{
TRunType run = await prepareFunc().ConfigureAwait(false);
return new Checkpointed<TRunType>(run, runHandle);
}
public static async ValueTask<StreamingRun> EnqueueAndStreamAsync<TInput>(this AsyncRunHandle runHandle, TInput input, CancellationToken cancellationToken = default)
{
await runHandle.EnqueueMessageAsync(input, cancellationToken).ConfigureAwait(false);
@@ -8,7 +8,7 @@ namespace Microsoft.Agents.AI.Workflows.Execution;
internal interface ISuperStepJoinContext
{
bool WithCheckpointing { get; }
bool IsCheckpointingEnabled { get; }
bool ConcurrentRunsEnabled { get; }
ValueTask ForwardWorkflowEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default);
@@ -11,6 +11,11 @@ namespace Microsoft.Agents.AI.Workflows;
/// </summary>
public interface IWorkflowExecutionEnvironment
{
/// <summary>
/// Specifies whether Checkpointing is configured for this environment.
/// </summary>
bool IsCheckpointingEnabled { get; }
/// <summary>
/// Initiates a streaming run of the specified workflow without sending any initial input. Note that the starting
/// <see cref="Executor"/> will not be invoked until an input message is received.
@@ -37,36 +42,6 @@ public interface IWorkflowExecutionEnvironment
/// 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 without sending any initial 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>
/// <param name="workflow">The workflow to be executed. Must not be <c>null</c>.</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 cancellation 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(Workflow workflow, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default);
/// <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 cancellation 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>
/// Resumes an asynchronous streaming execution for the specified input from a checkpoint.
/// </summary>
@@ -74,10 +49,9 @@ public interface IWorkflowExecutionEnvironment
/// 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="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation 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, CancellationToken cancellationToken = default);
ValueTask<StreamingRun> ResumeStreamAsync(Workflow workflow, CheckpointInfo fromCheckpoint, CancellationToken cancellationToken = default);
/// <summary>
/// Initiates a non-streaming execution of the workflow with the specified input.
@@ -93,21 +67,6 @@ public interface IWorkflowExecutionEnvironment
/// 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, 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 cancellation 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>
/// Resumes a non-streaming execution of the workflow from a checkpoint.
/// </summary>
@@ -115,9 +74,8 @@ public interface IWorkflowExecutionEnvironment
/// 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="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation 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, CancellationToken cancellationToken = default);
ValueTask<Run> ResumeAsync(Workflow workflow, CheckpointInfo fromCheckpoint, CancellationToken cancellationToken = default);
}
@@ -2,9 +2,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
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;
@@ -15,24 +15,44 @@ namespace Microsoft.Agents.AI.Workflows.InProc;
/// </summary>
public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironment
{
internal InProcessExecutionEnvironment(ExecutionMode mode, bool enableConcurrentRuns = false)
internal InProcessExecutionEnvironment(ExecutionMode mode, bool enableConcurrentRuns = false, CheckpointManager? checkpointManager = null)
{
this.ExecutionMode = mode;
this.EnableConcurrentRuns = enableConcurrentRuns;
this.CheckpointManager = checkpointManager;
}
/// <summary>
/// Configure a new execution environment, inheriting configuration for the current one with the specified <see cref="Workflows.CheckpointManager"/>
/// for use in checkpointing.
/// </summary>
/// <param name="checkpointManager">The CheckpointManager to use for checkpointing.</param>
/// <returns>
/// A new InProcess <see cref="IWorkflowExecutionEnvironment"/> configured for checkpointing, inheriting configuration from the current
/// environment.
/// </returns>
public InProcessExecutionEnvironment WithCheckpointing(CheckpointManager? checkpointManager)
{
return new(this.ExecutionMode, this.EnableConcurrentRuns, checkpointManager);
}
internal ExecutionMode ExecutionMode { get; }
internal bool EnableConcurrentRuns { get; }
internal CheckpointManager? CheckpointManager { get; }
internal ValueTask<AsyncRunHandle> BeginRunAsync(Workflow workflow, ICheckpointManager? checkpointManager, string? runId, IEnumerable<Type> knownValidInputTypes, CancellationToken cancellationToken)
/// <inheritdoc/>
public bool IsCheckpointingEnabled => this.CheckpointManager != null;
internal ValueTask<AsyncRunHandle> BeginRunAsync(Workflow workflow, string? runId, IEnumerable<Type> knownValidInputTypes, CancellationToken cancellationToken)
{
InProcessRunner runner = InProcessRunner.CreateTopLevelRunner(workflow, checkpointManager, runId, this.EnableConcurrentRuns, knownValidInputTypes);
InProcessRunner runner = InProcessRunner.CreateTopLevelRunner(workflow, this.CheckpointManager, runId, this.EnableConcurrentRuns, knownValidInputTypes);
return runner.BeginStreamAsync(this.ExecutionMode, cancellationToken);
}
internal ValueTask<AsyncRunHandle> ResumeRunAsync(Workflow workflow, ICheckpointManager? checkpointManager, CheckpointInfo fromCheckpoint, IEnumerable<Type> knownValidInputTypes, CancellationToken cancellationToken)
internal ValueTask<AsyncRunHandle> ResumeRunAsync(Workflow workflow, CheckpointInfo fromCheckpoint, IEnumerable<Type> knownValidInputTypes, CancellationToken cancellationToken)
{
InProcessRunner runner = InProcessRunner.CreateTopLevelRunner(workflow, checkpointManager, fromCheckpoint.RunId, this.EnableConcurrentRuns, knownValidInputTypes);
InProcessRunner runner = InProcessRunner.CreateTopLevelRunner(workflow, this.CheckpointManager, fromCheckpoint.RunId, this.EnableConcurrentRuns, knownValidInputTypes);
return runner.ResumeStreamAsync(this.ExecutionMode, fromCheckpoint, cancellationToken);
}
@@ -42,7 +62,7 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
string? runId = null,
CancellationToken cancellationToken = default)
{
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager: null, runId: runId, [], cancellationToken)
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, runId, [], cancellationToken)
.ConfigureAwait(false);
return new(runHandle);
@@ -55,63 +75,42 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
string? runId = null,
CancellationToken cancellationToken = default) where TInput : notnull
{
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager: null, runId: runId, [], cancellationToken)
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, runId, [], cancellationToken)
.ConfigureAwait(false);
return await runHandle.EnqueueAndStreamAsync(input, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask<Checkpointed<StreamingRun>> StreamAsync(
Workflow workflow,
CheckpointManager checkpointManager,
string? runId = null,
CancellationToken cancellationToken = default)
[MemberNotNull(nameof(CheckpointManager))]
private void VerifyCheckpointingConfigured()
{
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager, runId: runId, [], cancellationToken)
.ConfigureAwait(false);
return await runHandle.WithCheckpointingAsync<StreamingRun>(() => new(new StreamingRun(runHandle)))
.ConfigureAwait(false);
if (this.CheckpointManager == null)
{
throw new InvalidOperationException("Checkpointing is not configured for this execution environment. Please use the InProcessExecutionEnvironment.WithCheckpointing method to attach a CheckpointManager.");
}
}
/// <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>> ResumeStreamAsync(
public async ValueTask<StreamingRun> ResumeStreamAsync(
Workflow workflow,
CheckpointInfo fromCheckpoint,
CheckpointManager checkpointManager,
CancellationToken cancellationToken = default)
{
AsyncRunHandle runHandle = await this.ResumeRunAsync(workflow, checkpointManager, fromCheckpoint, [], cancellationToken)
this.VerifyCheckpointingConfigured();
AsyncRunHandle runHandle = await this.ResumeRunAsync(workflow, fromCheckpoint, [], cancellationToken)
.ConfigureAwait(false);
return await runHandle.WithCheckpointingAsync<StreamingRun>(() => new(new StreamingRun(runHandle)))
.ConfigureAwait(false);
return new(runHandle);
}
private async ValueTask<AsyncRunHandle> BeginRunHandlingChatProtocolAsync<TInput>(Workflow workflow,
TInput input,
CheckpointManager? checkpointManager,
string? runId = null,
CancellationToken cancellationToken = default)
{
ProtocolDescriptor descriptor = await workflow.DescribeProtocolAsync(cancellationToken).ConfigureAwait(false);
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, checkpointManager, runId, descriptor.Accepts, cancellationToken)
AsyncRunHandle runHandle = await this.BeginRunAsync(workflow, runId, descriptor.Accepts, cancellationToken)
.ConfigureAwait(false);
await runHandle.EnqueueMessageAsync(input, cancellationToken).ConfigureAwait(false);
@@ -134,7 +133,6 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
AsyncRunHandle runHandle = await this.BeginRunHandlingChatProtocolAsync(
workflow,
input,
checkpointManager: null,
runId,
cancellationToken)
.ConfigureAwait(false);
@@ -145,38 +143,16 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
}
/// <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.BeginRunHandlingChatProtocolAsync(
workflow,
input,
checkpointManager,
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>> ResumeAsync(
public async ValueTask<Run> ResumeAsync(
Workflow workflow,
CheckpointInfo fromCheckpoint,
CheckpointManager checkpointManager,
CancellationToken cancellationToken = default)
{
AsyncRunHandle runHandle = await this.ResumeRunAsync(workflow, checkpointManager, fromCheckpoint, [], cancellationToken)
this.VerifyCheckpointingConfigured();
AsyncRunHandle runHandle = await this.ResumeRunAsync(workflow, fromCheckpoint, [], cancellationToken)
.ConfigureAwait(false);
return await runHandle.WithCheckpointingAsync<Run>(() => new(new Run(runHandle)))
.ConfigureAwait(false);
return new(runHandle);
}
}
@@ -54,7 +54,7 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
this.StartExecutorId = workflow.StartExecutorId;
this.Workflow = Throw.IfNull(workflow);
this.RunContext = new InProcessRunnerContext(workflow, this.RunId, withCheckpointing: checkpointManager != null, this.OutgoingEvents, this.StepTracer, existingOwnerSignoff, subworkflow, enableConcurrentRuns);
this.RunContext = new InProcessRunnerContext(workflow, this.RunId, checkpointingEnabled: checkpointManager != null, this.OutgoingEvents, this.StepTracer, existingOwnerSignoff, subworkflow, enableConcurrentRuns);
this.CheckpointManager = checkpointManager;
this._knownValidInputTypes = knownValidInputTypes != null
@@ -161,6 +161,8 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
bool ISuperStepRunner.HasUnservicedRequests => this.RunContext.HasUnservicedRequests;
bool ISuperStepRunner.HasUnprocessedMessages => this.RunContext.NextStepHasActions;
public bool IsCheckpointingEnabled => this.RunContext.IsCheckpointingEnabled;
public IReadOnlyList<CheckpointInfo> Checkpoints => this._checkpoints;
async ValueTask<bool> ISuperStepRunner.RunSuperStepAsync(CancellationToken cancellationToken)
@@ -325,6 +327,8 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
throw new InvalidDataException("The specified checkpoint is not compatible with the workflow associated with this runner.");
}
ValueTask restoreCheckpointIndexTask = UpdateCheckpointIndexAsync();
await this.RunContext.StateManager.ImportStateAsync(checkpoint).ConfigureAwait(false);
await this.RunContext.ImportStateAsync(checkpoint).ConfigureAwait(false);
@@ -332,10 +336,18 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
ValueTask republishRequestsTask = this.RunContext.RepublishUnservicedRequestsAsync(cancellationToken);
await this.EdgeMap.ImportStateAsync(checkpoint).ConfigureAwait(false);
await Task.WhenAll(executorNotifyTask, republishRequestsTask.AsTask()).ConfigureAwait(false);
await Task.WhenAll(executorNotifyTask,
republishRequestsTask.AsTask(),
restoreCheckpointIndexTask.AsTask()).ConfigureAwait(false);
this._lastCheckpointInfo = checkpointInfo;
this.StepTracer.Reload(this.StepTracer.StepNumber);
async ValueTask UpdateCheckpointIndexAsync()
{
this._checkpoints.Clear();
this._checkpoints.AddRange(await this.CheckpointManager!.RetrieveIndexAsync(this.RunId).ConfigureAwait(false));
}
}
private bool CheckWorkflowMatch(Checkpoint checkpoint) =>
@@ -41,7 +41,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
public InProcessRunnerContext(
Workflow workflow,
string runId,
bool withCheckpointing,
bool checkpointingEnabled,
IEventSink outgoingEvents,
IStepTracer? stepTracer,
object? existingOwnershipSignoff = null,
@@ -66,7 +66,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
this._edgeMap = new(this, this._workflow, stepTracer);
this._outputFilter = new(workflow);
this.WithCheckpointing = withCheckpointing;
this.IsCheckpointingEnabled = checkpointingEnabled;
this.ConcurrentRunsEnabled = enableConcurrentRuns;
this.OutgoingEvents = outgoingEvents;
}
@@ -350,7 +350,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
public bool ConcurrentRunsEnabled => RunnerContext.ConcurrentRunsEnabled;
}
public bool WithCheckpointing { get; }
public bool IsCheckpointingEnabled { get; }
public bool ConcurrentRunsEnabled { get; }
internal Task PrepareForCheckpointAsync(CancellationToken cancellationToken = default)
@@ -49,27 +49,27 @@ public static class InProcessExecution
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);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.StreamAsync(Workflow, CheckpointManager, string?, CancellationToken)"/>
public static ValueTask<Checkpointed<StreamingRun>> StreamAsync(Workflow workflow, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default)
=> Default.StreamAsync(workflow, checkpointManager, runId, cancellationToken);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.OpenStreamAsync(Workflow, string?, CancellationToken)"/>
public static ValueTask<StreamingRun> OpenStreamAsync(Workflow workflow, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default)
=> Default.WithCheckpointing(checkpointManager).OpenStreamAsync(workflow, runId, cancellationToken);
/// <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);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.StreamAsync{TInput}(Workflow, TInput, string?, CancellationToken)"/>
public static ValueTask<StreamingRun> StreamAsync<TInput>(Workflow workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
=> Default.WithCheckpointing(checkpointManager).StreamAsync(workflow, input, runId, cancellationToken);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.ResumeStreamAsync(Workflow, CheckpointInfo, CheckpointManager, CancellationToken)"/>
public static ValueTask<Checkpointed<StreamingRun>> ResumeStreamAsync(Workflow workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, CancellationToken cancellationToken = default)
=> Default.ResumeStreamAsync(workflow, fromCheckpoint, checkpointManager, cancellationToken);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.ResumeStreamAsync(Workflow, CheckpointInfo, CancellationToken)"/>
public static ValueTask<StreamingRun> ResumeStreamAsync(Workflow workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, CancellationToken cancellationToken = default)
=> Default.WithCheckpointing(checkpointManager).ResumeStreamAsync(workflow, fromCheckpoint, cancellationToken);
/// <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);
/// <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);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.RunAsync{TInput}(Workflow, TInput, string?, CancellationToken)"/>
public static ValueTask<Run> RunAsync<TInput>(Workflow workflow, TInput input, CheckpointManager checkpointManager, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull
=> Default.WithCheckpointing(checkpointManager).RunAsync(workflow, input, runId, cancellationToken);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.ResumeAsync(Workflow, CheckpointInfo, CheckpointManager, CancellationToken)"/>
public static ValueTask<Checkpointed<Run>> ResumeAsync(Workflow workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, CancellationToken cancellationToken = default)
=> Default.ResumeAsync(workflow, fromCheckpoint, checkpointManager, cancellationToken);
/// <inheritdoc cref="IWorkflowExecutionEnvironment.ResumeAsync(Workflow, CheckpointInfo, CancellationToken)"/>
public static ValueTask<Run> ResumeAsync(Workflow workflow, CheckpointInfo fromCheckpoint, CheckpointManager checkpointManager, CancellationToken cancellationToken = default)
=> Default.WithCheckpointing(checkpointManager).ResumeAsync(workflow, fromCheckpoint, cancellationToken);
}
@@ -14,13 +14,13 @@ namespace Microsoft.Agents.AI.Workflows;
/// 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 : IAsyncDisposable
public sealed class Run : CheckpointableRunBase, IAsyncDisposable
{
private readonly List<WorkflowEvent> _eventSink = [];
private readonly AsyncRunHandle _runHandle;
internal Run(AsyncRunHandle _runHandle)
internal Run(AsyncRunHandle runHandle) : base(runHandle)
{
this._runHandle = _runHandle;
this._runHandle = runHandle;
}
internal async ValueTask<bool> RunToNextHaltAsync(CancellationToken cancellationToken = default)
@@ -81,7 +81,7 @@ internal class WorkflowHostExecutor : Executor, IAsyncDisposable
{
if (this._activeRunner == null)
{
if (this.JoinContext.WithCheckpointing)
if (this.JoinContext.IsCheckpointingEnabled)
{
// Use a seprate in-memory checkpoint manager for scoping purposes. We do not need to worry about
// serialization because we will be relying on the parent workflow's checkpoint manager to do that,
@@ -14,11 +14,11 @@ 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 : IAsyncDisposable
public sealed class StreamingRun : CheckpointableRunBase, IAsyncDisposable
{
private readonly AsyncRunHandle _runHandle;
internal StreamingRun(AsyncRunHandle runHandle)
internal StreamingRun(AsyncRunHandle runHandle) : base(runHandle)
{
this._runHandle = Throw.IfNull(runHandle);
}
@@ -7,6 +7,7 @@ using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.InProc;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
@@ -16,7 +17,6 @@ internal sealed class WorkflowHostAgent : AIAgent
{
private readonly Workflow _workflow;
private readonly string? _id;
private readonly CheckpointManager? _checkpointManager;
private readonly IWorkflowExecutionEnvironment _executionEnvironment;
private readonly bool _includeExceptionDetails;
private readonly bool _includeWorkflowOutputsInResponse;
@@ -24,14 +24,22 @@ internal sealed class WorkflowHostAgent : AIAgent
private readonly ConcurrentDictionary<string, string> _assignedRunIds = [];
public WorkflowHostAgent(Workflow workflow, string? id = null, string? name = null, string? description = null, CheckpointManager? checkpointManager = null, IWorkflowExecutionEnvironment? executionEnvironment = null, bool includeExceptionDetails = false, bool includeWorkflowOutputsInResponse = false)
public WorkflowHostAgent(Workflow workflow, string? id = null, string? name = null, string? description = null, IWorkflowExecutionEnvironment? executionEnvironment = null, bool includeExceptionDetails = false, bool includeWorkflowOutputsInResponse = false)
{
this._workflow = Throw.IfNull(workflow);
this._executionEnvironment = executionEnvironment ?? (workflow.AllowConcurrent
? InProcessExecution.Concurrent
: InProcessExecution.OffThread);
this._checkpointManager = checkpointManager;
if (!this._executionEnvironment.IsCheckpointingEnabled &&
this._executionEnvironment is not InProcessExecutionEnvironment)
{
// Cannot have an implicit CheckpointManager for non-InProcessExecution environments (or others that
// support BYO Checkpointing.
throw new InvalidOperationException("Cannot use a non-checkpointed execution environment. Implicit checkpointing is supported only for InProcess.");
}
this._includeExceptionDetails = includeExceptionDetails;
this._includeWorkflowOutputsInResponse = includeWorkflowOutputsInResponse;
@@ -66,7 +74,7 @@ internal sealed class WorkflowHostAgent : AIAgent
}
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
=> new(new WorkflowSession(this._workflow, this.GenerateNewId(), this._executionEnvironment, this._checkpointManager, this._includeExceptionDetails, this._includeWorkflowOutputsInResponse));
=> new(new WorkflowSession(this._workflow, this.GenerateNewId(), this._executionEnvironment, this._includeExceptionDetails, this._includeWorkflowOutputsInResponse));
protected override ValueTask<JsonElement> SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
@@ -81,7 +89,7 @@ internal sealed class WorkflowHostAgent : AIAgent
}
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> new(new WorkflowSession(this._workflow, serializedState, this._executionEnvironment, this._checkpointManager, this._includeExceptionDetails, this._includeWorkflowOutputsInResponse, jsonSerializerOptions));
=> new(new WorkflowSession(this._workflow, serializedState, this._executionEnvironment, this._includeExceptionDetails, this._includeWorkflowOutputsInResponse, jsonSerializerOptions));
private async ValueTask<WorkflowSession> UpdateSessionAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, CancellationToken cancellationToken = default)
{
@@ -17,7 +17,6 @@ public static class WorkflowHostingExtensions
/// <param name="id">A unique id for the hosting <see cref="AIAgent"/>.</param>
/// <param name="name">A name for the hosting <see cref="AIAgent"/>.</param>
/// <param name="description">A description for the hosting <see cref="AIAgent"/>.</param>
/// <param name="checkpointManager">A <see cref="CheckpointManager"/> to enable persistence of run state.</param>
/// <param name="executionEnvironment">Specify the execution environment to use when running the workflows. See
/// <see cref="InProcessExecution.OffThread"/>, <see cref="InProcessExecution.Concurrent"/> and
/// <see cref="InProcessExecution.Lockstep"/> for the in-process environments.</param>
@@ -31,12 +30,11 @@ public static class WorkflowHostingExtensions
string? id = null,
string? name = null,
string? description = null,
CheckpointManager? checkpointManager = null,
IWorkflowExecutionEnvironment? executionEnvironment = null,
bool includeExceptionDetails = false,
bool includeWorkflowOutputsInResponse = false)
{
return new WorkflowHostAgent(workflow, id, name, description, checkpointManager, executionEnvironment, includeExceptionDetails, includeWorkflowOutputsInResponse);
return new WorkflowHostAgent(workflow, id, name, description, executionEnvironment, includeExceptionDetails, includeWorkflowOutputsInResponse);
}
internal static FunctionCallContent ToFunctionCall(this ExternalRequest request)
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
@@ -9,6 +10,7 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Checkpointing;
using Microsoft.Agents.AI.Workflows.InProc;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
@@ -21,26 +23,48 @@ internal sealed class WorkflowSession : AgentSession
private readonly bool _includeExceptionDetails;
private readonly bool _includeWorkflowOutputsInResponse;
private readonly CheckpointManager _checkpointManager;
private readonly InMemoryCheckpointManager? _inMemoryCheckpointManager;
private InMemoryCheckpointManager? _inMemoryCheckpointManager;
public WorkflowSession(Workflow workflow, string runId, IWorkflowExecutionEnvironment executionEnvironment, CheckpointManager? checkpointManager = null, bool includeExceptionDetails = false, bool includeWorkflowOutputsInResponse = false)
internal static bool VerifyCheckpointingConfiguration(IWorkflowExecutionEnvironment executionEnvironment, [NotNullWhen(true)] out InProcessExecutionEnvironment? inProcEnv)
{
inProcEnv = null;
if (executionEnvironment.IsCheckpointingEnabled)
{
return false;
}
if ((inProcEnv = executionEnvironment as InProcessExecutionEnvironment) == null)
{
throw new InvalidOperationException("Cannot use a non-checkpointed execution environment. Implicit checkpointing is supported only for InProcess.");
}
return true;
}
public WorkflowSession(Workflow workflow, string runId, IWorkflowExecutionEnvironment executionEnvironment, bool includeExceptionDetails = false, bool includeWorkflowOutputsInResponse = false)
{
this._workflow = Throw.IfNull(workflow);
this._executionEnvironment = Throw.IfNull(executionEnvironment);
this._includeExceptionDetails = includeExceptionDetails;
this._includeWorkflowOutputsInResponse = includeWorkflowOutputsInResponse;
// If the user provided an external checkpoint manager, use that, otherwise rely on an in-memory one.
// TODO: Implement persist-only-last functionality for in-memory checkpoint manager, to avoid unbounded
// memory growth.
this._checkpointManager = checkpointManager ?? new(this._inMemoryCheckpointManager = new());
if (VerifyCheckpointingConfiguration(executionEnvironment, out InProcessExecutionEnvironment? inProcEnv))
{
// We have an InProcessExecutionEnvironment which is not configured for checkpointing. Ensure it has an externalizable checkpoint manager,
// since we are responsible for maintaining the state.
this._executionEnvironment = inProcEnv.WithCheckpointing(this.EnsureExternalizedInMemoryCheckpointing());
}
this.RunId = Throw.IfNullOrEmpty(runId);
this.ChatHistoryProvider = new WorkflowChatHistoryProvider();
}
public WorkflowSession(Workflow workflow, JsonElement serializedSession, IWorkflowExecutionEnvironment executionEnvironment, CheckpointManager? checkpointManager = null, bool includeExceptionDetails = false, bool includeWorkflowOutputsInResponse = false, JsonSerializerOptions? jsonSerializerOptions = null)
private CheckpointManager EnsureExternalizedInMemoryCheckpointing()
{
return new(this._inMemoryCheckpointManager ??= new());
}
public WorkflowSession(Workflow workflow, JsonElement serializedSession, IWorkflowExecutionEnvironment executionEnvironment, bool includeExceptionDetails = false, bool includeWorkflowOutputsInResponse = false, JsonSerializerOptions? jsonSerializerOptions = null)
{
this._workflow = Throw.IfNull(workflow);
this._executionEnvironment = Throw.IfNull(executionEnvironment);
@@ -51,26 +75,20 @@ internal sealed class WorkflowSession : AgentSession
SessionState sessionState = marshaller.Marshal<SessionState>(serializedSession);
this._inMemoryCheckpointManager = sessionState.CheckpointManager;
if (this._inMemoryCheckpointManager is not null && checkpointManager is not null)
if (this._inMemoryCheckpointManager != null &&
VerifyCheckpointingConfiguration(executionEnvironment, out InProcessExecutionEnvironment? inProcEnv))
{
// The session was externalized with an in-memory checkpoint manager, but the caller is providing an external one.
throw new ArgumentException("Cannot provide an external checkpoint manager when deserializing a session that " +
"was serialized with an in-memory checkpoint manager.", nameof(checkpointManager));
this._executionEnvironment = inProcEnv.WithCheckpointing(this.EnsureExternalizedInMemoryCheckpointing());
}
else if (this._inMemoryCheckpointManager is null && checkpointManager is null)
else if (this._inMemoryCheckpointManager != null)
{
// The session was externalized without an in-memory checkpoint manager, and the caller is not providing an external one.
throw new ArgumentException("An external checkpoint manager must be provided when deserializing a session that " +
"was serialized without an in-memory checkpoint manager.", nameof(checkpointManager));
}
else
{
this._checkpointManager = checkpointManager ?? new(this._inMemoryCheckpointManager!);
throw new ArgumentException("The session was saved with an externalized checkpoint manager, but the incoming execution environment does not support it.", nameof(executionEnvironment));
}
this.RunId = sessionState.RunId;
this.LastCheckpoint = sessionState.LastCheckpoint;
this.ChatHistoryProvider = new WorkflowChatHistoryProvider();
this.LastCheckpoint = sessionState.LastCheckpoint;
this.StateBag = sessionState.StateBag;
}
@@ -123,28 +141,26 @@ internal sealed class WorkflowSession : AgentSession
return update;
}
private async ValueTask<Checkpointed<StreamingRun>> CreateOrResumeRunAsync(List<ChatMessage> messages, CancellationToken cancellationToken = default)
private async ValueTask<StreamingRun> CreateOrResumeRunAsync(List<ChatMessage> messages, CancellationToken cancellationToken = default)
{
// The workflow is validated to be a ChatProtocol workflow by the WorkflowHostAgent before creating the session,
// and does not need to be checked again here.
if (this.LastCheckpoint is not null)
{
Checkpointed<StreamingRun> checkpointed =
StreamingRun run =
await this._executionEnvironment
.ResumeStreamAsync(this._workflow,
this.LastCheckpoint,
this._checkpointManager,
cancellationToken)
.ConfigureAwait(false);
await checkpointed.Run.TrySendMessageAsync(messages).ConfigureAwait(false);
return checkpointed;
await run.TrySendMessageAsync(messages).ConfigureAwait(false);
return run;
}
return await this._executionEnvironment
.StreamAsync(this._workflow,
messages,
this._checkpointManager,
this.RunId,
cancellationToken)
.ConfigureAwait(false);
@@ -160,11 +176,10 @@ internal sealed class WorkflowSession : AgentSession
List<ChatMessage> messages = this.ChatHistoryProvider.GetFromBookmark(this).ToList();
#pragma warning disable CA2007 // Analyzer misfiring and not seeing .ConfigureAwait(false) below.
await using Checkpointed<StreamingRun> checkpointed =
await using StreamingRun run =
await this.CreateOrResumeRunAsync(messages, cancellationToken).ConfigureAwait(false);
#pragma warning restore CA2007
StreamingRun run = checkpointed.Run;
await run.TrySendMessageAsync(new TurnToken(emitEvents: true)).ConfigureAwait(false);
await foreach (WorkflowEvent evt in run.WatchStreamAsync(blockOnPendingRequest: false, cancellationToken)
.ConfigureAwait(false)
@@ -68,7 +68,7 @@ internal sealed class WorkflowRunner
checkpointManager = CheckpointManager.CreateInMemory();
}
Checkpointed<StreamingRun> run = await InProcessExecution.StreamAsync(workflow, input, checkpointManager).ConfigureAwait(false);
StreamingRun run = await InProcessExecution.StreamAsync(workflow, input, checkpointManager).ConfigureAwait(false);
bool isComplete = false;
ExternalResponse? requestResponse = null;
@@ -107,7 +107,7 @@ internal sealed class WorkflowRunner
Notify("\nWORKFLOW: Done!\n");
}
public async Task<ExternalRequest?> MonitorAndDisposeWorkflowRunAsync(Checkpointed<StreamingRun> run, ExternalResponse? response = null)
public async Task<ExternalRequest?> MonitorAndDisposeWorkflowRunAsync(StreamingRun run, ExternalResponse? response = null)
{
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task
await using IAsyncDisposable disposeRun = run;
@@ -121,10 +121,10 @@ internal sealed class WorkflowRunner
if (response is not null)
{
await run.Run.SendResponseAsync(response).ConfigureAwait(false);
await run.SendResponseAsync(response).ConfigureAwait(false);
}
await foreach (WorkflowEvent workflowEvent in run.Run.WatchStreamAsync().ConfigureAwait(false))
await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync().ConfigureAwait(false))
{
switch (workflowEvent)
{