Skip to content

Commit cd84898

Browse files
committed
[feat] Implement checkpointing mechanism for task execution
- Introduced CheckpointSaver interface and its implementations (InMemoryCheckpointSaver, TabularCheckpointSaver) for persisting execution state. - Enhanced task runners (GraphAsTaskRunner, IteratorTaskRunner, TaskRunner, WhileTaskRunner) to support checkpointing, allowing for state recovery and iteration-level checkpoints. - Updated TaskGraph to manage checkpointing configuration and resume execution from checkpoints. - Added tests to validate checkpoint saving and retrieval functionality, ensuring robust handling of task execution states. Plan: --- name: Checkpointing & Retry overview: Add execution checkpointing to @workglow/task-graph that saves graph state (task statuses, outputs, dataflow data) after each task completion, with configurable granularity. Build InMemory and Tabular checkpoint savers, add thread_id isolation, enable resume-from-checkpoint. On the builder side, wire checkpoint data into activities and build iteration time-travel UI (builder not a part) todos: - id: checkpoint-types content: Create checkpoint data model types (CheckpointData, TaskCheckpointState, etc.) in packages/task-graph/src/checkpoint/ status: pending - id: checkpoint-saver-interface content: Create abstract CheckpointSaver class with save/get/getLatest/getHistory/delete methods status: pending - id: inmemory-saver content: Implement InMemoryCheckpointSaver using Map with threadId index status: pending - id: tabular-saver content: Implement TabularCheckpointSaver using existing ITabularStorage interface status: pending - id: runner-checkpoint-hooks content: Add captureCheckpoint() to TaskGraphRunner, call after each task completion with configurable granularity status: pending - id: runner-restore content: Add restoreFromCheckpoint() to TaskGraphRunner for resume-on-failure status: pending - id: config-extensions content: Extend TaskGraphRunConfig, IRunConfig, and IExecuteContext with checkpointSaver/threadId/granularity status: pending - id: iteration-checkpoints content: Add iteration checkpointing to WhileTask and IteratorTaskRunner after each subgraph run status: pending - id: checkpoint-events content: Add 'checkpoint' event to TaskGraphEvents and emit from runner status: pending - id: exports content: Export all checkpoint types/classes from common.ts and add checkpoint/index.ts status: pending - id: tests content: Write tests for checkpoint save/restore, resume-from-failure, and iteration checkpoints status: pending isProject: false --- # Checkpointing & Retry ## Architecture Overview ```mermaid flowchart TD subgraph taskGraph ["@workglow/task-graph"] CheckpointSaver["CheckpointSaver (abstract)"] InMemory["InMemoryCheckpointSaver"] Tabular["TabularCheckpointSaver"] CheckpointData["CheckpointData"] TaskGraphRunner_CP["TaskGraphRunner (checkpoint hooks)"] WhileTask_CP["WhileTask / IteratorTask (iteration checkpoints)"] end subgraph builder ["Builder (frontend)"] ActivityRepo["ActivityRepository + checkpoint_id refs"] ActivityViewer["ActivityViewer (per-task drill-in)"] TimeTravelUI["Iteration Time Travel UI"] end TaskGraphRunner_CP -->|"after each task"| CheckpointSaver WhileTask_CP -->|"after each iteration"| CheckpointSaver CheckpointSaver --> InMemory CheckpointSaver --> Tabular ActivityRepo -->|"reads"| CheckpointSaver ActivityViewer -->|"reads"| ActivityRepo TimeTravelUI -->|"navigates"| ActivityViewer ``` ## Part 1: Checkpoint Data Model & Saver Interface **New directory:** `packages/task-graph/src/checkpoint/` ### 1a. Checkpoint Types (`CheckpointTypes.ts`) Define the core data structures: ```typescript export type CheckpointId = string; export type ThreadId = string; export interface TaskCheckpointState { taskId: unknown; taskType: string; status: TaskStatus; inputData: TaskInput; outputData: TaskOutput; progress: number; error?: string; startedAt?: string; completedAt?: string; } export interface DataflowCheckpointState { id: string; sourceTaskId: unknown; targetTaskId: unknown; status: TaskStatus; portData?: TaskOutput; } export interface CheckpointData { checkpointId: CheckpointId; threadId: ThreadId; parentCheckpointId?: CheckpointId; graphJson: TaskGraphJson; // structural definition taskStates: TaskCheckpointState[]; // runtime state per task dataflowStates: DataflowCheckpointState[]; metadata: { createdAt: string; triggerTaskId?: unknown; // task that just completed iterationIndex?: number; // for while/map loops iterationParentTaskId?: unknown; // which iterator task owns this }; } ``` ### 1b. CheckpointSaver Interface (`CheckpointSaver.ts`) ```typescript export abstract class CheckpointSaver { abstract saveCheckpoint(data: CheckpointData): Promise<void>; abstract getCheckpoint(checkpointId: CheckpointId): Promise<CheckpointData | undefined>; abstract getLatestCheckpoint(threadId: ThreadId): Promise<CheckpointData | undefined>; abstract getCheckpointHistory(threadId: ThreadId): Promise<CheckpointData[]>; abstract getCheckpointsForIteration( threadId: ThreadId, iterationParentTaskId: unknown ): Promise<CheckpointData[]>; abstract deleteCheckpoints(threadId: ThreadId): Promise<void>; } ``` Modeled after the existing `TaskOutputRepository` pattern with `EventEmitter` support and a service token (`CHECKPOINT_SAVER`). ### 1c. InMemoryCheckpointSaver (`InMemoryCheckpointSaver.ts`) Simple `Map<CheckpointId, CheckpointData>` with a secondary index on `threadId`. Follows the same pattern as existing in-memory storage implementations. ### 1d. TabularCheckpointSaver (`TabularCheckpointSaver.ts`) Uses the existing `ITabularStorage` interface (same as `TaskOutputTabularRepository`). Schema: - Primary key: `checkpoint_id` - Columns: `thread_id`, `parent_checkpoint_id`, `graph_json` (compressed JSON), `task_states` (compressed JSON), `dataflow_states` (compressed JSON), `metadata` (JSON), `created_at` - Searchable by: `thread_id` This automatically gives us SQLite, Postgres, IndexedDB, Supabase, and File-backed checkpoint storage via the existing tabular storage backends. ### 1e. Exports Add all checkpoint exports to `[packages/task-graph/src/common.ts](packages/task-graph/src/common.ts)`: ```typescript export * from "./checkpoint/CheckpointTypes"; export * from "./checkpoint/CheckpointSaver"; export * from "./checkpoint/InMemoryCheckpointSaver"; export * from "./checkpoint/TabularCheckpointSaver"; ``` ## Part 2: Integrate Checkpointing into Execution ### 2a. Add `CheckpointSaver` to `TaskGraphRunConfig` In `[packages/task-graph/src/task-graph/TaskGraph.ts](packages/task-graph/src/task-graph/TaskGraph.ts)`, extend `TaskGraphRunConfig`: ```typescript export interface TaskGraphRunConfig { outputCache?: TaskOutputRepository | boolean; parentSignal?: AbortSignal; registry?: ServiceRegistry; checkpointSaver?: CheckpointSaver; // NEW threadId?: string; // NEW resumeFromCheckpoint?: CheckpointId; // NEW checkpointGranularity?: "every-task" | "top-level-only" | "none"; // NEW, default 'every-task' } ``` ### 2b. Checkpoint Hook in `TaskGraphRunner` In `[packages/task-graph/src/task-graph/TaskGraphRunner.ts](packages/task-graph/src/task-graph/TaskGraphRunner.ts)`: 1. Store `checkpointSaver`, `threadId`, and `checkpointGranularity` as instance properties (set in `handleStart`). 2. Add a `captureCheckpoint(triggerTaskId)` method that snapshots the full graph state (iterating `graph.getTasks()` and `graph.getDataflows()` to build `TaskCheckpointState[]` and `DataflowCheckpointState[]`). 3. Call `captureCheckpoint` in `runGraph()` after each task completes (inside the `runAsync` function, after `pushOutputFromNodeToEdges` and `pushStatusFromNodeToEdges`), respecting `checkpointGranularity`. 4. Emit a new `checkpoint` event on the graph: `this.graph.emit("checkpoint", checkpointData)`. ### 2c. Resume from Checkpoint Add a `restoreFromCheckpoint(checkpointData: CheckpointData)` method to `TaskGraphRunner` that: 1. For each task in `checkpointData.taskStates` with status `COMPLETED` or `DISABLED`, restore the task's `status`, `runOutputData`, `progress`, `error`. 2. For each dataflow, restore `portData` and `status`. 3. Configure the `DependencyBasedScheduler` to skip already-completed tasks by calling `onTaskCompleted` for each. 4. The subsequent `runGraph` call then only processes `PENDING` tasks. In `TaskGraph.run()`, if `config.resumeFromCheckpoint` is provided, call `restoreFromCheckpoint` instead of `resetGraph` in `handleStart`. ### 2d. Iteration Checkpoints in `WhileTask` and `IteratorTaskRunner` In `[packages/task-graph/src/task/WhileTask.ts](packages/task-graph/src/task/WhileTask.ts)` (line ~380, inside the while loop): - After each iteration's `subGraph.run()` completes, if the execution context has a checkpoint saver, capture a checkpoint with `iterationIndex` and `iterationParentTaskId` metadata. In `[packages/task-graph/src/task/IteratorTaskRunner.ts](packages/task-graph/src/task/IteratorTaskRunner.ts)` (inside `executeSubgraphIteration`): - Same pattern: after each subgraph run, capture an iteration checkpoint. This requires threading the `checkpointSaver` and `threadId` through the execution context (`IExecuteContext` or `IRunConfig`). The cleanest approach is to add optional `checkpointSaver` and `threadId` to the `IRunConfig` interface in `[packages/task-graph/src/task/ITask.ts](packages/task-graph/src/task/ITask.ts)`. ### 2e. Thread ID Concept The `threadId` serves as the isolation key for checkpoint namespacing. When running a graph: - If no `threadId` is provided, generate one via `uuid4()`. - The `threadId` is stored on the runner and propagated to all child graph runs. - Maps directly to `activity_id` in the builder. ## Part 3: Builder - Checkpoint Data in Activities ### 3a. Wire CheckpointSaver into `runWorkflow` In `[builder/src/lib/run-workflow.ts](builder/src/lib/run-workflow.ts)`: 1. Create/get a `CheckpointSaver` (TabularCheckpointSaver backed by the same storage infrastructure used by `ActivityRepository`). 2. Pass it to `taskGraph.run()` via the config: `{ checkpointSaver, threadId: actId }`. 3. On failure, the checkpoint is already saved. The existing `activity_id` serves as the `threadId`. 4. Add a `resumeFromCheckpoint` option to `RunWorkflowOptions` that, when set, passes `resumeFromCheckpoint` to the graph config to skip completed tasks. ### 3b. Checkpoint Repository for the Builder Create `builder/src/components/activities/CheckpointRepository.ts`: - Wraps a `TabularCheckpointSaver` (or an `InMemoryCheckpointSaver` for browser-only mode). - Provides queries: `getCheckpointsForActivity(activityId)`, `getIterationCheckpoints(activityId, taskId)`. - Registered alongside `ActivityRepository` in the builder's storage setup. ### 3c. Activity Detail: Per-Task Run Data Enhance `[builder/src/components/activities/ActivityViewer.tsx](builder/src/components/activities/ActivityViewer.tsx)`: - Fetch checkpoints for the current activity using `CheckpointRepository`. - Display a timeline of task completions derived from checkpoint `metadata.triggerTaskId` and `metadata.createdAt`. - For each task in the graph, show its state (status, inputs, outputs, timing) by reading from the relevant checkpoint's `taskStates`. - For iterative tasks (WhileTask, MapTask), show an expandable list of iteration checkpoints. ## File Summary | Area | Files | Action | | ---- | --------------------------------------------------------------- | ------------------------------------- | | libs | `packages/task-graph/src/checkpoint/CheckpointTypes.ts` | New | | libs | `packages/task-graph/src/checkpoint/CheckpointSaver.ts` | New | | libs | `packages/task-graph/src/checkpoint/InMemoryCheckpointSaver.ts` | New | | libs | `packages/task-graph/src/checkpoint/TabularCheckpointSaver.ts` | New | | libs | `packages/task-graph/src/checkpoint/index.ts` | New | | libs | `packages/task-graph/src/common.ts` | Modify (add checkpoint exports) | | libs | `packages/task-graph/src/task-graph/TaskGraph.ts` | Modify (extend config) | | libs | `packages/task-graph/src/task-graph/TaskGraphRunner.ts` | Modify (checkpoint hooks, restore) | | libs | `packages/task-graph/src/task-graph/TaskGraphEvents.ts` | Modify (add checkpoint event) | | libs | `packages/task-graph/src/task/ITask.ts` | Modify (add checkpoint to IRunConfig) | | libs | `packages/task-graph/src/task/TaskRunner.ts` | Modify (propagate checkpoint config) | | libs | `packages/task-graph/src/task/WhileTask.ts` | Modify (iteration checkpoints) | | libs | `packages/task-graph/src/task/IteratorTaskRunner.ts` | Modify (iteration checkpoints) | ## Testing Tests should be added in `packages/test/src/test/task/`: - `Checkpoint.test.ts` - Test checkpoint save/restore cycle for a simple graph - `CheckpointResume.test.ts` - Test resume from checkpoint after simulated failure - `CheckpointIteration.test.ts` - Test iteration checkpoints for WhileTask and MapTask
1 parent 5c0247f commit cd84898

16 files changed

Lines changed: 1020 additions & 4 deletions
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Steven Roussey <sroussey@gmail.com>
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { createServiceToken } from "@workglow/util";
8+
import type { CheckpointData, CheckpointId, ThreadId } from "./CheckpointTypes";
9+
10+
/**
11+
* Service token for CheckpointSaver
12+
*/
13+
export const CHECKPOINT_SAVER = createServiceToken<CheckpointSaver>("taskgraph.checkpointSaver");
14+
15+
/**
16+
* Abstract class for saving and retrieving execution checkpoints.
17+
* Implementations provide persistence for checkpoint data to enable
18+
* resume-from-failure and execution history features.
19+
*/
20+
export abstract class CheckpointSaver {
21+
abstract saveCheckpoint(data: CheckpointData): Promise<void>;
22+
abstract getCheckpoint(checkpointId: CheckpointId): Promise<CheckpointData | undefined>;
23+
abstract getLatestCheckpoint(threadId: ThreadId): Promise<CheckpointData | undefined>;
24+
abstract getCheckpointHistory(threadId: ThreadId): Promise<CheckpointData[]>;
25+
abstract getCheckpointsForIteration(
26+
threadId: ThreadId,
27+
iterationParentTaskId: unknown
28+
): Promise<CheckpointData[]>;
29+
abstract deleteCheckpoints(threadId: ThreadId): Promise<void>;
30+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Steven Roussey <sroussey@gmail.com>
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import type { TaskGraphJson } from "../task/TaskJSON";
8+
import type { TaskInput, TaskOutput, TaskStatus } from "../task/TaskTypes";
9+
10+
export type CheckpointId = string;
11+
export type ThreadId = string;
12+
13+
export type CheckpointGranularity = "every-task" | "top-level-only" | "none";
14+
15+
export interface TaskCheckpointState {
16+
taskId: unknown;
17+
taskType: string;
18+
status: TaskStatus;
19+
inputData: TaskInput;
20+
outputData: TaskOutput;
21+
progress: number;
22+
error?: string;
23+
startedAt?: string;
24+
completedAt?: string;
25+
}
26+
27+
export interface DataflowCheckpointState {
28+
id: string;
29+
sourceTaskId: unknown;
30+
targetTaskId: unknown;
31+
status: TaskStatus;
32+
portData?: TaskOutput;
33+
}
34+
35+
export interface CheckpointData {
36+
checkpointId: CheckpointId;
37+
threadId: ThreadId;
38+
parentCheckpointId?: CheckpointId;
39+
graphJson: TaskGraphJson;
40+
taskStates: TaskCheckpointState[];
41+
dataflowStates: DataflowCheckpointState[];
42+
metadata: {
43+
createdAt: string;
44+
triggerTaskId?: unknown;
45+
iterationIndex?: number;
46+
iterationParentTaskId?: unknown;
47+
};
48+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Steven Roussey <sroussey@gmail.com>
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { CheckpointSaver } from "./CheckpointSaver";
8+
import type { CheckpointData, CheckpointId, ThreadId } from "./CheckpointTypes";
9+
10+
/**
11+
* In-memory implementation of CheckpointSaver.
12+
* Uses a Map with a secondary index on threadId for efficient lookups.
13+
*/
14+
export class InMemoryCheckpointSaver extends CheckpointSaver {
15+
private checkpoints: Map<CheckpointId, CheckpointData> = new Map();
16+
private threadIndex: Map<ThreadId, CheckpointId[]> = new Map();
17+
18+
async saveCheckpoint(data: CheckpointData): Promise<void> {
19+
this.checkpoints.set(data.checkpointId, data);
20+
21+
const threadCheckpoints = this.threadIndex.get(data.threadId) ?? [];
22+
threadCheckpoints.push(data.checkpointId);
23+
this.threadIndex.set(data.threadId, threadCheckpoints);
24+
}
25+
26+
async getCheckpoint(checkpointId: CheckpointId): Promise<CheckpointData | undefined> {
27+
return this.checkpoints.get(checkpointId);
28+
}
29+
30+
async getLatestCheckpoint(threadId: ThreadId): Promise<CheckpointData | undefined> {
31+
const ids = this.threadIndex.get(threadId);
32+
if (!ids || ids.length === 0) return undefined;
33+
return this.checkpoints.get(ids[ids.length - 1]);
34+
}
35+
36+
async getCheckpointHistory(threadId: ThreadId): Promise<CheckpointData[]> {
37+
const ids = this.threadIndex.get(threadId);
38+
if (!ids) return [];
39+
return ids
40+
.map((id) => this.checkpoints.get(id))
41+
.filter((cp): cp is CheckpointData => cp !== undefined);
42+
}
43+
44+
async getCheckpointsForIteration(
45+
threadId: ThreadId,
46+
iterationParentTaskId: unknown
47+
): Promise<CheckpointData[]> {
48+
const history = await this.getCheckpointHistory(threadId);
49+
return history.filter((cp) => cp.metadata.iterationParentTaskId === iterationParentTaskId);
50+
}
51+
52+
async deleteCheckpoints(threadId: ThreadId): Promise<void> {
53+
const ids = this.threadIndex.get(threadId);
54+
if (ids) {
55+
for (const id of ids) {
56+
this.checkpoints.delete(id);
57+
}
58+
this.threadIndex.delete(threadId);
59+
}
60+
}
61+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Steven Roussey <sroussey@gmail.com>
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import type { BaseTabularStorage } from "@workglow/storage";
8+
import { compress, DataPortSchemaObject, decompress } from "@workglow/util";
9+
import { CheckpointSaver } from "./CheckpointSaver";
10+
import type { CheckpointData, CheckpointId, ThreadId } from "./CheckpointTypes";
11+
12+
export const CheckpointSchema = {
13+
type: "object",
14+
properties: {
15+
checkpoint_id: { type: "string" },
16+
thread_id: { type: "string" },
17+
parent_checkpoint_id: { type: "string" },
18+
graph_json: { type: "string", contentEncoding: "blob" },
19+
task_states: { type: "string", contentEncoding: "blob" },
20+
dataflow_states: { type: "string", contentEncoding: "blob" },
21+
metadata: { type: "string" },
22+
created_at: { type: "string", format: "date-time" },
23+
},
24+
additionalProperties: false,
25+
} satisfies DataPortSchemaObject;
26+
27+
export const CheckpointPrimaryKeyNames = ["checkpoint_id"] as const;
28+
29+
export type CheckpointStorage = BaseTabularStorage<
30+
typeof CheckpointSchema,
31+
typeof CheckpointPrimaryKeyNames
32+
>;
33+
34+
export type TabularCheckpointSaverOptions = {
35+
tabularRepository: CheckpointStorage;
36+
compression?: boolean;
37+
};
38+
39+
/**
40+
* Tabular storage implementation of CheckpointSaver.
41+
* Uses the existing ITabularStorage interface for persistence,
42+
* giving access to SQLite, Postgres, IndexedDB, Supabase, and
43+
* file-backed checkpoint storage via existing backends.
44+
*/
45+
export class TabularCheckpointSaver extends CheckpointSaver {
46+
tabularRepository: CheckpointStorage;
47+
compression: boolean;
48+
49+
constructor({ tabularRepository, compression = true }: TabularCheckpointSaverOptions) {
50+
super();
51+
this.tabularRepository = tabularRepository;
52+
this.compression = compression;
53+
}
54+
55+
async setupDatabase(): Promise<void> {
56+
await this.tabularRepository.setupDatabase?.();
57+
}
58+
59+
private async compressJson(value: string): Promise<unknown> {
60+
if (this.compression) {
61+
return (await compress(value)) as unknown;
62+
}
63+
return Buffer.from(value) as unknown;
64+
}
65+
66+
private async decompressJson(raw: unknown): Promise<string> {
67+
if (this.compression) {
68+
const bytes: Uint8Array =
69+
raw instanceof Uint8Array
70+
? raw
71+
: Array.isArray(raw)
72+
? new Uint8Array(raw as number[])
73+
: raw && typeof raw === "object"
74+
? new Uint8Array(
75+
Object.keys(raw as Record<string, number>)
76+
.filter((k) => /^\d+$/.test(k))
77+
.sort((a, b) => Number(a) - Number(b))
78+
.map((k) => (raw as Record<string, number>)[k])
79+
)
80+
: new Uint8Array();
81+
return await decompress(bytes);
82+
}
83+
return (raw as Buffer).toString();
84+
}
85+
86+
async saveCheckpoint(data: CheckpointData): Promise<void> {
87+
await this.tabularRepository.put({
88+
checkpoint_id: data.checkpointId,
89+
thread_id: data.threadId,
90+
parent_checkpoint_id: data.parentCheckpointId ?? "",
91+
graph_json: (await this.compressJson(JSON.stringify(data.graphJson))) as string,
92+
task_states: (await this.compressJson(JSON.stringify(data.taskStates))) as string,
93+
dataflow_states: (await this.compressJson(JSON.stringify(data.dataflowStates))) as string,
94+
metadata: JSON.stringify(data.metadata),
95+
created_at: data.metadata.createdAt,
96+
});
97+
}
98+
99+
async getCheckpoint(checkpointId: CheckpointId): Promise<CheckpointData | undefined> {
100+
const row = await this.tabularRepository.get({ checkpoint_id: checkpointId });
101+
if (!row) return undefined;
102+
return this.rowToCheckpointData(row);
103+
}
104+
105+
async getLatestCheckpoint(threadId: ThreadId): Promise<CheckpointData | undefined> {
106+
const rows = await this.tabularRepository.search({ thread_id: threadId });
107+
if (!rows || rows.length === 0) return undefined;
108+
109+
// Sort by created_at descending and return the latest
110+
rows.sort((a, b) => {
111+
const aTime = a.created_at ?? "";
112+
const bTime = b.created_at ?? "";
113+
return bTime.localeCompare(aTime);
114+
});
115+
116+
return this.rowToCheckpointData(rows[0]);
117+
}
118+
119+
async getCheckpointHistory(threadId: ThreadId): Promise<CheckpointData[]> {
120+
const rows = await this.tabularRepository.search({ thread_id: threadId });
121+
if (!rows || rows.length === 0) return [];
122+
123+
// Sort by created_at ascending
124+
rows.sort((a, b) => {
125+
const aTime = a.created_at ?? "";
126+
const bTime = b.created_at ?? "";
127+
return aTime.localeCompare(bTime);
128+
});
129+
130+
const results: CheckpointData[] = [];
131+
for (const row of rows) {
132+
results.push(await this.rowToCheckpointData(row));
133+
}
134+
return results;
135+
}
136+
137+
async getCheckpointsForIteration(
138+
threadId: ThreadId,
139+
iterationParentTaskId: unknown
140+
): Promise<CheckpointData[]> {
141+
const history = await this.getCheckpointHistory(threadId);
142+
return history.filter((cp) => cp.metadata.iterationParentTaskId === iterationParentTaskId);
143+
}
144+
145+
async deleteCheckpoints(threadId: ThreadId): Promise<void> {
146+
await this.tabularRepository.deleteSearch({ thread_id: threadId });
147+
}
148+
149+
private async rowToCheckpointData(row: Record<string, unknown>): Promise<CheckpointData> {
150+
const graphJson = JSON.parse(await this.decompressJson(row.graph_json));
151+
const taskStates = JSON.parse(await this.decompressJson(row.task_states));
152+
const dataflowStates = JSON.parse(await this.decompressJson(row.dataflow_states));
153+
const metadata = JSON.parse(row.metadata as string);
154+
155+
return {
156+
checkpointId: row.checkpoint_id as string,
157+
threadId: row.thread_id as string,
158+
parentCheckpointId: (row.parent_checkpoint_id as string) || undefined,
159+
graphJson,
160+
taskStates,
161+
dataflowStates,
162+
metadata,
163+
};
164+
}
165+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Steven Roussey <sroussey@gmail.com>
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
export * from "./CheckpointSaver";
8+
export * from "./CheckpointTypes";
9+
export * from "./InMemoryCheckpointSaver";
10+
export * from "./TabularCheckpointSaver";

packages/task-graph/src/common.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ export * from "./storage/TaskGraphRepository";
2222
export * from "./storage/TaskGraphTabularRepository";
2323
export * from "./storage/TaskOutputRepository";
2424
export * from "./storage/TaskOutputTabularRepository";
25+
26+
export * from "./checkpoint";

packages/task-graph/src/task-graph/TaskGraph.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*/
66

77
import { DirectedAcyclicGraph, EventEmitter, ServiceRegistry, uuid4 } from "@workglow/util";
8+
import type { CheckpointSaver } from "../checkpoint/CheckpointSaver";
9+
import type { CheckpointGranularity, CheckpointId, ThreadId } from "../checkpoint/CheckpointTypes";
810
import { TaskOutputRepository } from "../storage/TaskOutputRepository";
911
import type { ITask } from "../task/ITask";
1012
import { JsonTaskItem, TaskGraphJson } from "../task/TaskJSON";
@@ -39,6 +41,14 @@ export interface TaskGraphRunConfig {
3941
parentSignal?: AbortSignal;
4042
/** Optional service registry to use for this task graph (creates child from global if not provided) */
4143
registry?: ServiceRegistry;
44+
/** Optional checkpoint saver for persisting execution state */
45+
checkpointSaver?: CheckpointSaver;
46+
/** Thread ID for checkpoint isolation; auto-generated if not provided */
47+
threadId?: ThreadId;
48+
/** Resume execution from a specific checkpoint */
49+
resumeFromCheckpoint?: CheckpointId;
50+
/** Controls when checkpoints are captured. Default: "every-task" */
51+
checkpointGranularity?: CheckpointGranularity;
4252
}
4353

4454
class TaskGraphDAG extends DirectedAcyclicGraph<
@@ -103,6 +113,10 @@ export class TaskGraph implements ITaskGraph {
103113
return this.runner.runGraph<ExecuteOutput>(input, {
104114
outputCache: config?.outputCache || this.outputCache,
105115
parentSignal: config?.parentSignal || undefined,
116+
checkpointSaver: config?.checkpointSaver,
117+
threadId: config?.threadId,
118+
resumeFromCheckpoint: config?.resumeFromCheckpoint,
119+
checkpointGranularity: config?.checkpointGranularity,
106120
});
107121
}
108122

packages/task-graph/src/task-graph/TaskGraphEvents.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import { EventParameters } from "@workglow/util";
8+
import type { CheckpointData } from "../checkpoint/CheckpointTypes";
89
import { TaskIdType } from "../task/TaskTypes";
910
import { DataflowIdType } from "./Dataflow";
1011

@@ -19,6 +20,7 @@ export type TaskGraphStatusListeners = {
1920
error: (error: Error) => void;
2021
abort: () => void;
2122
disabled: () => void;
23+
checkpoint: (data: CheckpointData) => void;
2224
};
2325
export type TaskGraphStatusEvents = keyof TaskGraphStatusListeners;
2426
export type TaskGraphStatusListener<Event extends TaskGraphStatusEvents> =

0 commit comments

Comments
 (0)