Skip to content

Commit c73bd87

Browse files
authored
[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
1 parent fd4e6e8 commit c73bd87

34 files changed

Lines changed: 299 additions & 317 deletions

dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndRehydrate/Program.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ private static async Task Main()
3232
var checkpoints = new List<CheckpointInfo>();
3333

3434
// Execute the workflow and save checkpoints
35-
await using Checkpointed<StreamingRun> checkpointedRun = await InProcessExecution
35+
await using StreamingRun checkpointedRun = await InProcessExecution
3636
.StreamAsync(workflow, NumberSignal.Init, checkpointManager);
3737

38-
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync())
38+
await foreach (WorkflowEvent evt in checkpointedRun.WatchStreamAsync())
3939
{
4040
if (evt is ExecutorCompletedEvent executorCompletedEvt)
4141
{
@@ -72,10 +72,10 @@ private static async Task Main()
7272
Console.WriteLine($"\n\nHydrating a new workflow instance from the {CheckpointIndex + 1}th checkpoint.");
7373
CheckpointInfo savedCheckpoint = checkpoints[CheckpointIndex];
7474

75-
await using Checkpointed<StreamingRun> newCheckpointedRun =
75+
await using StreamingRun newCheckpointedRun =
7676
await InProcessExecution.ResumeStreamAsync(newWorkflow, savedCheckpoint, checkpointManager);
7777

78-
await foreach (WorkflowEvent evt in newCheckpointedRun.Run.WatchStreamAsync())
78+
await foreach (WorkflowEvent evt in newCheckpointedRun.WatchStreamAsync())
7979
{
8080
if (evt is ExecutorCompletedEvent executorCompletedEvt)
8181
{

dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointAndResume/Program.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ private static async Task Main()
3131
var checkpoints = new List<CheckpointInfo>();
3232

3333
// Execute the workflow and save checkpoints
34-
await using Checkpointed<StreamingRun> checkpointedRun = await InProcessExecution
34+
await using StreamingRun checkpointedRun = await InProcessExecution
3535
.StreamAsync(workflow, NumberSignal.Init, checkpointManager)
3636
;
37-
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync())
37+
await foreach (WorkflowEvent evt in checkpointedRun.WatchStreamAsync())
3838
{
3939
if (evt is ExecutorCompletedEvent executorCompletedEvt)
4040
{
@@ -71,7 +71,7 @@ private static async Task Main()
7171
CheckpointInfo savedCheckpoint = checkpoints[CheckpointIndex];
7272
// Note that we are restoring the state directly to the same run instance.
7373
await checkpointedRun.RestoreCheckpointAsync(savedCheckpoint, CancellationToken.None);
74-
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync())
74+
await foreach (WorkflowEvent evt in checkpointedRun.WatchStreamAsync())
7575
{
7676
if (evt is ExecutorCompletedEvent executorCompletedEvt)
7777
{

dotnet/samples/GettingStarted/Workflows/Checkpoint/CheckpointWithHumanInTheLoop/Program.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,17 @@ private static async Task Main()
3434
var checkpoints = new List<CheckpointInfo>();
3535

3636
// Execute the workflow and save checkpoints
37-
await using Checkpointed<StreamingRun> checkpointedRun = await InProcessExecution
37+
await using StreamingRun checkpointedRun = await InProcessExecution
3838
.StreamAsync(workflow, new SignalWithNumber(NumberSignal.Init), checkpointManager)
3939
;
40-
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync())
40+
await foreach (WorkflowEvent evt in checkpointedRun.WatchStreamAsync())
4141
{
4242
switch (evt)
4343
{
4444
case RequestInfoEvent requestInputEvt:
4545
// Handle `RequestInfoEvent` from the workflow
4646
ExternalResponse response = HandleExternalRequest(requestInputEvt.Request);
47-
await checkpointedRun.Run.SendResponseAsync(response);
47+
await checkpointedRun.SendResponseAsync(response);
4848
break;
4949
case ExecutorCompletedEvent executorCompletedEvt:
5050
Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");
@@ -77,14 +77,14 @@ private static async Task Main()
7777
CheckpointInfo savedCheckpoint = checkpoints[CheckpointIndex];
7878
// Note that we are restoring the state directly to the same run instance.
7979
await checkpointedRun.RestoreCheckpointAsync(savedCheckpoint, CancellationToken.None);
80-
await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync())
80+
await foreach (WorkflowEvent evt in checkpointedRun.WatchStreamAsync())
8181
{
8282
switch (evt)
8383
{
8484
case RequestInfoEvent requestInputEvt:
8585
// Handle `RequestInfoEvent` from the workflow
8686
ExternalResponse response = HandleExternalRequest(requestInputEvt.Request);
87-
await checkpointedRun.Run.SendResponseAsync(response);
87+
await checkpointedRun.SendResponseAsync(response);
8888
break;
8989
case ExecutorCompletedEvent executorCompletedEvt:
9090
Console.WriteLine($"* Executor {executorCompletedEvt.ExecutorId} completed.");

dotnet/src/Microsoft.Agents.AI.Workflows/CheckpointManager.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3+
using System.Collections.Generic;
34
using System.Text.Json;
45
using System.Threading.Tasks;
56
using Microsoft.Agents.AI.Workflows.Checkpointing;
@@ -54,4 +55,7 @@ ValueTask<CheckpointInfo> ICheckpointManager.CommitCheckpointAsync(string runId,
5455

5556
ValueTask<Checkpoint> ICheckpointManager.LookupCheckpointAsync(string runId, CheckpointInfo checkpointInfo)
5657
=> this._impl.LookupCheckpointAsync(runId, checkpointInfo);
58+
59+
ValueTask<IEnumerable<CheckpointInfo>> ICheckpointManager.RetrieveIndexAsync(string runId, CheckpointInfo? withParent)
60+
=> this._impl.RetrieveIndexAsync(runId, withParent);
5761
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Collections.Generic;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.Agents.AI.Workflows.Checkpointing;
7+
8+
namespace Microsoft.Agents.AI.Workflows;
9+
10+
/// <summary>
11+
/// Represents a base object for a workflow run that may support checkpointing.
12+
/// </summary>
13+
public abstract class CheckpointableRunBase
14+
{
15+
// TODO: Rename Context?
16+
private readonly ICheckpointingHandle _checkpointingHandle;
17+
18+
internal CheckpointableRunBase(ICheckpointingHandle checkpointingHandle)
19+
{
20+
this._checkpointingHandle = checkpointingHandle;
21+
}
22+
23+
/// <inheritdoc cref="ICheckpointingHandle.IsCheckpointingEnabled"/>
24+
public bool IsCheckpointingEnabled => this._checkpointingHandle.IsCheckpointingEnabled;
25+
26+
/// <inheritdoc cref="ICheckpointingHandle.Checkpoints"/>
27+
public IReadOnlyList<CheckpointInfo> Checkpoints => this._checkpointingHandle.Checkpoints ?? [];
28+
29+
/// <summary>
30+
/// Gets the most recent checkpoint information.
31+
/// </summary>
32+
public CheckpointInfo? LastCheckpoint
33+
{
34+
get
35+
{
36+
if (!this.IsCheckpointingEnabled)
37+
{
38+
return null;
39+
}
40+
41+
var checkpoints = this.Checkpoints;
42+
return checkpoints.Count > 0 ? checkpoints[checkpoints.Count - 1] : null;
43+
}
44+
}
45+
46+
/// <inheritdoc cref="ICheckpointingHandle.RestoreCheckpointAsync"/>
47+
public ValueTask RestoreCheckpointAsync(CheckpointInfo checkpointInfo, CancellationToken cancellationToken = default)
48+
=> this._checkpointingHandle.RestoreCheckpointAsync(checkpointInfo, cancellationToken);
49+
}

dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointed.cs

Lines changed: 0 additions & 66 deletions
This file was deleted.

dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/CheckpointManagerImpl.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3+
using System.Collections.Generic;
34
using System.Threading.Tasks;
45

56
namespace Microsoft.Agents.AI.Workflows.Checkpointing;
@@ -27,4 +28,7 @@ public async ValueTask<Checkpoint> LookupCheckpointAsync(string runId, Checkpoin
2728
TStoreObject result = await this._store.RetrieveCheckpointAsync(runId, checkpointInfo).ConfigureAwait(false);
2829
return this._marshaller.Marshal<Checkpoint>(result);
2930
}
31+
32+
public ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string runId, CheckpointInfo? withParent = null)
33+
=> this._store.RetrieveIndexAsync(runId, withParent);
3034
}

dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/ICheckpointManager.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,16 @@ internal interface ICheckpointManager
2727
/// cref="Checkpoint"/> associated with the specified <paramref name="checkpointInfo"/>.</returns>
2828
/// <exception cref="KeyNotFoundException">Thrown if the checkpoint is not found.</exception>
2929
ValueTask<Checkpoint> LookupCheckpointAsync(string runId, CheckpointInfo checkpointInfo);
30+
31+
/// <summary>
32+
/// Asynchronously retrieves the collection of checkpoint information for the specified run identifier, optionally
33+
/// filtered by a parent checkpoint.
34+
/// </summary>
35+
/// <param name="runId">The unique identifier of the run for which to retrieve checkpoint information. Cannot be null or empty.</param>
36+
/// <param name="withParent">An optional parent checkpoint to filter the results. If specified, only checkpoints with the given parent are
37+
/// returned; otherwise, all checkpoints for the run are included.</param>
38+
/// <returns>A value task representing the asynchronous operation. The result contains a collection of <see
39+
/// cref="CheckpointInfo"/> objects associated with the specified run. The collection is empty if no checkpoints are
40+
/// found.</returns>
41+
ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string runId, CheckpointInfo? withParent = null);
3042
}

dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/ICheckpointingHandle.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,21 @@ namespace Microsoft.Agents.AI.Workflows.Checkpointing;
88

99
internal interface ICheckpointingHandle
1010
{
11-
// TODO: Convert this to a multi-timeline (e.g.: Live timeline + forks for orphaned checkpoints due to timetravel)
11+
/// <summary>
12+
/// Gets a value indicating whether checkpointing is enabled for the current operation or process.
13+
/// </summary>
14+
bool IsCheckpointingEnabled { get; }
15+
16+
/// <summary>
17+
/// Gets a read-only list of checkpoint information associated with the current context.
18+
/// </summary>
1219
IReadOnlyList<CheckpointInfo> Checkpoints { get; }
1320

21+
/// <summary>
22+
/// Restores the system state from the specified checkpoint asynchronously.
23+
/// </summary>
24+
/// <param name="checkpointInfo">The checkpoint information that identifies the state to restore. Cannot be null.</param>
25+
/// <param name="cancellationToken">A cancellation token that can be used to cancel the restore operation.</param>
26+
/// <returns>A <see cref="ValueTask"/> that represents the asynchronous restore operation.</returns>
1427
ValueTask RestoreCheckpointAsync(CheckpointInfo checkpointInfo, CancellationToken cancellationToken = default);
1528
}

dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/InMemoryCheckpointManager.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,7 @@ public ValueTask<Checkpoint> LookupCheckpointAsync(string runId, CheckpointInfo
6060

6161
public bool TryGetLastCheckpoint(string runId, [NotNullWhen(true)] out CheckpointInfo? checkpoint)
6262
=> this.GetRunStore(runId).TryGetLastCheckpointInfo(out checkpoint);
63+
64+
public ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string runId, CheckpointInfo? withParent = null)
65+
=> new(this.GetRunStore(runId).CheckpointIndex.AsReadOnly());
6366
}

0 commit comments

Comments
 (0)