@cogitator-ai/workflows 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -9
- package/dist/builder.d.ts +2 -2
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js.map +1 -1
- package/dist/checkpoint-stores.d.ts.map +1 -1
- package/dist/checkpoint-stores.js +4 -5
- package/dist/checkpoint-stores.js.map +1 -1
- package/dist/executor.d.ts +8 -8
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +83 -8
- package/dist/executor.js.map +1 -1
- package/dist/human/approval-outcomes.d.ts +26 -0
- package/dist/human/approval-outcomes.d.ts.map +1 -0
- package/dist/human/approval-outcomes.js +61 -0
- package/dist/human/approval-outcomes.js.map +1 -0
- package/dist/human/approval-store.d.ts +12 -0
- package/dist/human/approval-store.d.ts.map +1 -1
- package/dist/human/approval-store.js +52 -9
- package/dist/human/approval-store.js.map +1 -1
- package/dist/human/durable-approval-stores.d.ts +118 -0
- package/dist/human/durable-approval-stores.d.ts.map +1 -0
- package/dist/human/durable-approval-stores.js +383 -0
- package/dist/human/durable-approval-stores.js.map +1 -0
- package/dist/human/human-node.d.ts +16 -8
- package/dist/human/human-node.d.ts.map +1 -1
- package/dist/human/human-node.js +48 -26
- package/dist/human/human-node.js.map +1 -1
- package/dist/human/index.d.ts +2 -1
- package/dist/human/index.d.ts.map +1 -1
- package/dist/human/index.js +1 -0
- package/dist/human/index.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/manager/durable-run-stores.d.ts +88 -0
- package/dist/manager/durable-run-stores.d.ts.map +1 -0
- package/dist/manager/durable-run-stores.js +492 -0
- package/dist/manager/durable-run-stores.js.map +1 -0
- package/dist/manager/run-store.d.ts +1 -0
- package/dist/manager/run-store.d.ts.map +1 -1
- package/dist/manager/run-store.js +21 -10
- package/dist/manager/run-store.js.map +1 -1
- package/dist/manager/workflow-manager.d.ts +59 -8
- package/dist/manager/workflow-manager.d.ts.map +1 -1
- package/dist/manager/workflow-manager.js +272 -192
- package/dist/manager/workflow-manager.js.map +1 -1
- package/dist/nodes/adapters.d.ts +3 -2
- package/dist/nodes/adapters.d.ts.map +1 -1
- package/dist/nodes/adapters.js +15 -3
- package/dist/nodes/adapters.js.map +1 -1
- package/dist/nodes/base.d.ts +4 -1
- package/dist/nodes/base.d.ts.map +1 -1
- package/dist/observability/exporters.js +1 -1
- package/dist/observability/exporters.js.map +1 -1
- package/dist/observability/metrics.d.ts +2 -0
- package/dist/observability/metrics.d.ts.map +1 -1
- package/dist/observability/metrics.js +4 -0
- package/dist/observability/metrics.js.map +1 -1
- package/dist/observability/tracer.d.ts +1 -0
- package/dist/observability/tracer.d.ts.map +1 -1
- package/dist/observability/tracer.js +2 -1
- package/dist/observability/tracer.js.map +1 -1
- package/dist/postgres-schema.d.ts +18 -0
- package/dist/postgres-schema.d.ts.map +1 -0
- package/dist/postgres-schema.js +35 -0
- package/dist/postgres-schema.js.map +1 -0
- package/dist/saga/compensation.d.ts +8 -1
- package/dist/saga/compensation.d.ts.map +1 -1
- package/dist/saga/compensation.js +6 -3
- package/dist/saga/compensation.js.map +1 -1
- package/dist/saga/index.d.ts +1 -1
- package/dist/saga/index.d.ts.map +1 -1
- package/dist/saga/index.js.map +1 -1
- package/dist/subworkflows/parallel-subworkflows.d.ts +15 -11
- package/dist/subworkflows/parallel-subworkflows.d.ts.map +1 -1
- package/dist/subworkflows/parallel-subworkflows.js +21 -31
- package/dist/subworkflows/parallel-subworkflows.js.map +1 -1
- package/dist/timers/durable-timer-stores.d.ts +174 -0
- package/dist/timers/durable-timer-stores.d.ts.map +1 -0
- package/dist/timers/durable-timer-stores.js +501 -0
- package/dist/timers/durable-timer-stores.js.map +1 -0
- package/dist/timers/timer-manager.d.ts +28 -1
- package/dist/timers/timer-manager.d.ts.map +1 -1
- package/dist/timers/timer-manager.js +87 -3
- package/dist/timers/timer-manager.js.map +1 -1
- package/dist/timers/timer-node.d.ts +2 -0
- package/dist/timers/timer-node.d.ts.map +1 -1
- package/dist/timers/timer-node.js +6 -0
- package/dist/timers/timer-node.js.map +1 -1
- package/dist/triggers/cron-trigger.d.ts +6 -0
- package/dist/triggers/cron-trigger.d.ts.map +1 -1
- package/dist/triggers/cron-trigger.js +23 -2
- package/dist/triggers/cron-trigger.js.map +1 -1
- package/package.json +4 -4
|
@@ -15,6 +15,15 @@ import { WorkflowScheduler } from '../scheduler.js';
|
|
|
15
15
|
import { createJobScheduler } from './scheduler.js';
|
|
16
16
|
import { InMemoryRunStore } from './run-store.js';
|
|
17
17
|
import { createTracer } from '../observability/tracer.js';
|
|
18
|
+
import { createMetricsCollector } from '../observability/metrics.js';
|
|
19
|
+
/** A finite number stored under `key` in the run's metadata (scheduling options live there) */
|
|
20
|
+
function metadataNumber(run, key) {
|
|
21
|
+
const value = run.metadata?.[key];
|
|
22
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
23
|
+
}
|
|
24
|
+
function errorRecord(error) {
|
|
25
|
+
return { name: error.name, message: error.message, stack: error.stack };
|
|
26
|
+
}
|
|
18
27
|
/**
|
|
19
28
|
* Workflow manager implementation
|
|
20
29
|
*/
|
|
@@ -26,11 +35,14 @@ export class DefaultWorkflowManager {
|
|
|
26
35
|
executor;
|
|
27
36
|
workflows = new Map();
|
|
28
37
|
activeRuns = new Map();
|
|
38
|
+
/** Execute options of runs that are active or paused, reused when a paused run resumes */
|
|
39
|
+
runOptions = new Map();
|
|
29
40
|
stateChangeCallbacks = new Set();
|
|
30
41
|
runLocks = new Map();
|
|
31
42
|
defaultTimeout;
|
|
32
43
|
tracer;
|
|
33
44
|
metrics;
|
|
45
|
+
disabledMetrics;
|
|
34
46
|
constructor(config) {
|
|
35
47
|
this.cogitator = config.cogitator;
|
|
36
48
|
this.defaultTimeout = config.defaultTimeout;
|
|
@@ -42,14 +54,18 @@ export class DefaultWorkflowManager {
|
|
|
42
54
|
this.scheduler = createJobScheduler({
|
|
43
55
|
runStore: this.runStore,
|
|
44
56
|
maxConcurrency: config.maxConcurrency,
|
|
45
|
-
onRunReady: (runId) =>
|
|
57
|
+
onRunReady: (runId) => {
|
|
58
|
+
this.handleRunReady(runId).catch((error) => {
|
|
59
|
+
console.warn(`[WorkflowManager] Scheduled run '${runId}' could not be started:`, error);
|
|
60
|
+
});
|
|
61
|
+
},
|
|
46
62
|
});
|
|
47
63
|
if (config.onRunStateChange) {
|
|
48
64
|
this.stateChangeCallbacks.add(config.onRunStateChange);
|
|
49
65
|
}
|
|
50
66
|
}
|
|
51
67
|
/**
|
|
52
|
-
* Start the manager (begins processing scheduled runs)
|
|
68
|
+
* Start the manager (begins processing scheduled runs and cron jobs)
|
|
53
69
|
*/
|
|
54
70
|
start() {
|
|
55
71
|
this.scheduler.start();
|
|
@@ -61,18 +77,46 @@ export class DefaultWorkflowManager {
|
|
|
61
77
|
this.scheduler.stop();
|
|
62
78
|
}
|
|
63
79
|
/**
|
|
64
|
-
* Register a workflow for scheduling
|
|
80
|
+
* Register a workflow for scheduling, retries and resuming
|
|
65
81
|
*/
|
|
66
82
|
registerWorkflow(workflow) {
|
|
67
83
|
this.workflows.set(workflow.name, workflow);
|
|
68
84
|
}
|
|
69
85
|
/**
|
|
70
|
-
* Schedule a workflow for later execution
|
|
86
|
+
* Schedule a workflow for later execution. `cron` queues one run at the next
|
|
87
|
+
* occurrence; use `registerCronJob` for a recurring schedule.
|
|
71
88
|
*/
|
|
72
89
|
async schedule(workflow, options) {
|
|
73
90
|
this.registerWorkflow(workflow);
|
|
74
91
|
return this.scheduler.scheduleRun(workflow, options);
|
|
75
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Run `workflow` on every occurrence of the cron `expression` while the manager is
|
|
95
|
+
* started. Each occurrence queues a run with `jobOptions` and `triggerId: 'cron:<jobId>'`.
|
|
96
|
+
* Returns the job id.
|
|
97
|
+
*/
|
|
98
|
+
registerCronJob(workflow, expression, options) {
|
|
99
|
+
this.registerWorkflow(workflow);
|
|
100
|
+
return this.scheduler.registerCronJob(workflow, expression, options);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Remove a cron job; runs it already queued are kept
|
|
104
|
+
*/
|
|
105
|
+
unregisterCronJob(jobId) {
|
|
106
|
+
return this.scheduler.unregisterCronJob(jobId);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Pause or continue a cron job
|
|
110
|
+
*/
|
|
111
|
+
setCronJobEnabled(jobId, enabled) {
|
|
112
|
+
return this.scheduler.setCronJobEnabled(jobId, enabled);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* List the registered cron jobs
|
|
116
|
+
*/
|
|
117
|
+
getCronJobs() {
|
|
118
|
+
return this.scheduler.getCronJobs();
|
|
119
|
+
}
|
|
76
120
|
/**
|
|
77
121
|
* Execute a workflow immediately
|
|
78
122
|
*/
|
|
@@ -99,104 +143,12 @@ export class DefaultWorkflowManager {
|
|
|
99
143
|
};
|
|
100
144
|
await this.runStore.save(run);
|
|
101
145
|
this.notifyStateChange(run);
|
|
102
|
-
const abortController = new AbortController();
|
|
103
|
-
this.activeRuns.set(runId, { abort: () => abortController.abort() });
|
|
104
146
|
this.scheduler.runStarted(runId);
|
|
105
|
-
|
|
106
|
-
const timeoutHandle = this.defaultTimeout !== undefined && this.defaultTimeout > 0
|
|
107
|
-
? setTimeout(() => {
|
|
108
|
-
timedOut = true;
|
|
109
|
-
abortController.abort();
|
|
110
|
-
}, this.defaultTimeout)
|
|
111
|
-
: undefined;
|
|
112
|
-
const runTracer = options?.tracing ? createTracer(options.tracing) : this.tracer;
|
|
113
|
-
const runMetrics = options?.metrics?.enabled === false ? undefined : this.metrics;
|
|
114
|
-
try {
|
|
115
|
-
const executed = await this.executor.execute(workflow, input, {
|
|
116
|
-
checkpoint: !!this.checkpointStore,
|
|
117
|
-
...options,
|
|
118
|
-
signal: abortController.signal,
|
|
119
|
-
tracer: runTracer,
|
|
120
|
-
metricsCollector: runMetrics,
|
|
121
|
-
onNodeStart: (node) => {
|
|
122
|
-
void this.updateRunNodes(runId, node, 'start');
|
|
123
|
-
options?.onNodeStart?.(node);
|
|
124
|
-
},
|
|
125
|
-
onNodeComplete: (node, result, duration) => {
|
|
126
|
-
void this.updateRunNodes(runId, node, 'complete');
|
|
127
|
-
options?.onNodeComplete?.(node, result, duration);
|
|
128
|
-
},
|
|
129
|
-
onNodeError: (node, error) => {
|
|
130
|
-
void this.updateRunNodes(runId, node, 'error');
|
|
131
|
-
options?.onNodeError?.(node, error);
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
const result = timedOut
|
|
135
|
-
? {
|
|
136
|
-
...executed,
|
|
137
|
-
error: new Error(`Workflow run '${runId}' timed out after ${String(this.defaultTimeout)}ms`),
|
|
138
|
-
}
|
|
139
|
-
: executed;
|
|
140
|
-
if (result.error) {
|
|
141
|
-
await this.runStore.update(runId, {
|
|
142
|
-
status: 'failed',
|
|
143
|
-
state: result.state,
|
|
144
|
-
completedAt: Date.now(),
|
|
145
|
-
checkpointId: result.checkpointId,
|
|
146
|
-
error: {
|
|
147
|
-
name: result.error.name,
|
|
148
|
-
message: result.error.message,
|
|
149
|
-
stack: result.error.stack,
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
const updatedRun = await this.runStore.get(runId);
|
|
153
|
-
if (updatedRun)
|
|
154
|
-
this.notifyStateChange(updatedRun);
|
|
155
|
-
return result;
|
|
156
|
-
}
|
|
157
|
-
await this.runStore.update(runId, {
|
|
158
|
-
status: 'completed',
|
|
159
|
-
state: result.state,
|
|
160
|
-
output: result.state,
|
|
161
|
-
completedAt: Date.now(),
|
|
162
|
-
checkpointId: result.checkpointId,
|
|
163
|
-
});
|
|
164
|
-
const updatedRun = await this.runStore.get(runId);
|
|
165
|
-
if (updatedRun)
|
|
166
|
-
this.notifyStateChange(updatedRun);
|
|
167
|
-
return result;
|
|
168
|
-
}
|
|
169
|
-
catch (error) {
|
|
170
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
171
|
-
await this.runStore.update(runId, {
|
|
172
|
-
status: 'failed',
|
|
173
|
-
completedAt: Date.now(),
|
|
174
|
-
error: {
|
|
175
|
-
name: err.name,
|
|
176
|
-
message: err.message,
|
|
177
|
-
stack: err.stack,
|
|
178
|
-
},
|
|
179
|
-
});
|
|
180
|
-
const updatedRun = await this.runStore.get(runId);
|
|
181
|
-
if (updatedRun)
|
|
182
|
-
this.notifyStateChange(updatedRun);
|
|
183
|
-
throw error;
|
|
184
|
-
}
|
|
185
|
-
finally {
|
|
186
|
-
if (timeoutHandle)
|
|
187
|
-
clearTimeout(timeoutHandle);
|
|
188
|
-
if (runTracer && runTracer !== this.tracer) {
|
|
189
|
-
await runTracer.flush().catch((error) => {
|
|
190
|
-
console.warn('[WorkflowManager] Failed to flush run traces:', error);
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
this.activeRuns.delete(runId);
|
|
194
|
-
this.runLocks.delete(runId);
|
|
195
|
-
this.scheduler.runCompleted(runId);
|
|
196
|
-
}
|
|
147
|
+
return this.runWorkflow({ runId, workflow, input, options, timeout: this.defaultTimeout });
|
|
197
148
|
}
|
|
198
149
|
/**
|
|
199
|
-
* Cancel a run
|
|
150
|
+
* Cancel a run. A running run is aborted: the nodes in flight get the abort signal and
|
|
151
|
+
* no further node starts.
|
|
200
152
|
*/
|
|
201
153
|
async cancel(runId, reason) {
|
|
202
154
|
const run = await this.runStore.get(runId);
|
|
@@ -210,18 +162,13 @@ export class DefaultWorkflowManager {
|
|
|
210
162
|
return;
|
|
211
163
|
}
|
|
212
164
|
if (run.status === 'running' || run.status === 'paused') {
|
|
213
|
-
|
|
214
|
-
if (active) {
|
|
215
|
-
active.abort();
|
|
216
|
-
}
|
|
217
|
-
await this.runStore.update(runId, {
|
|
165
|
+
await this.stopRun(runId, 'cancelled', {
|
|
218
166
|
status: 'cancelled',
|
|
219
167
|
completedAt: Date.now(),
|
|
220
168
|
error: reason ? { name: 'CancelError', message: reason } : undefined,
|
|
221
169
|
});
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
this.notifyStateChange(updatedRun);
|
|
170
|
+
if (!this.activeRuns.has(runId))
|
|
171
|
+
this.runOptions.delete(runId);
|
|
225
172
|
}
|
|
226
173
|
}
|
|
227
174
|
/**
|
|
@@ -243,7 +190,9 @@ export class DefaultWorkflowManager {
|
|
|
243
190
|
return this.runStore.getStats(workflowName);
|
|
244
191
|
}
|
|
245
192
|
/**
|
|
246
|
-
* Pause a running workflow (
|
|
193
|
+
* Pause a running workflow. Execution is aborted (nodes in flight get the abort signal)
|
|
194
|
+
* and `resume()` continues from the run's last checkpoint, so the manager needs a
|
|
195
|
+
* `checkpointStore`.
|
|
247
196
|
*/
|
|
248
197
|
async pause(runId) {
|
|
249
198
|
const run = await this.runStore.get(runId);
|
|
@@ -252,35 +201,62 @@ export class DefaultWorkflowManager {
|
|
|
252
201
|
if (run.status !== 'running') {
|
|
253
202
|
throw new Error(`Cannot pause run in status: ${run.status}`);
|
|
254
203
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
active.abort();
|
|
204
|
+
if (!this.checkpointStore) {
|
|
205
|
+
throw new Error(`Cannot pause run '${runId}': the manager has no checkpointStore to resume it from (use cancel() to stop it)`);
|
|
258
206
|
}
|
|
259
|
-
await this.
|
|
260
|
-
status: 'paused',
|
|
261
|
-
pausedAt: Date.now(),
|
|
262
|
-
});
|
|
263
|
-
const updatedRun = await this.runStore.get(runId);
|
|
264
|
-
if (updatedRun)
|
|
265
|
-
this.notifyStateChange(updatedRun);
|
|
207
|
+
await this.stopRun(runId, 'paused', { status: 'paused', pausedAt: Date.now() });
|
|
266
208
|
}
|
|
267
209
|
/**
|
|
268
|
-
* Resume a paused workflow
|
|
210
|
+
* Resume a paused workflow from its last checkpoint (from the start when it was paused
|
|
211
|
+
* before the first checkpoint). Nodes that completed before the checkpoint are not run
|
|
212
|
+
* again. Resolves once the run is running again; follow it with `onRunStateChange` or
|
|
213
|
+
* `getStatus`. `options` default to the ones the run was started with in this process.
|
|
269
214
|
*/
|
|
270
|
-
async resume(runId) {
|
|
215
|
+
async resume(runId, options) {
|
|
271
216
|
const run = await this.runStore.get(runId);
|
|
272
217
|
if (!run)
|
|
273
218
|
throw new Error(`Run not found: ${runId}`);
|
|
274
219
|
if (run.status !== 'paused') {
|
|
275
220
|
throw new Error(`Cannot resume run in status: ${run.status}`);
|
|
276
221
|
}
|
|
222
|
+
if (this.activeRuns.has(runId)) {
|
|
223
|
+
throw new Error(`Cannot resume run '${runId}' yet: the nodes it was running when paused have not finished`);
|
|
224
|
+
}
|
|
225
|
+
const workflow = this.workflows.get(run.workflowName);
|
|
226
|
+
if (!workflow) {
|
|
227
|
+
throw new Error(`Workflow not found: ${run.workflowName}`);
|
|
228
|
+
}
|
|
229
|
+
const checkpoint = run.checkpointId
|
|
230
|
+
? await this.checkpointStore?.load(run.checkpointId)
|
|
231
|
+
: undefined;
|
|
232
|
+
if (run.checkpointId && !checkpoint) {
|
|
233
|
+
throw new Error(`Checkpoint '${run.checkpointId}' of run '${runId}' not found`);
|
|
234
|
+
}
|
|
277
235
|
await this.runStore.update(runId, {
|
|
278
236
|
status: 'running',
|
|
279
237
|
pausedAt: undefined,
|
|
238
|
+
currentNodes: [],
|
|
280
239
|
});
|
|
281
240
|
const updatedRun = await this.runStore.get(runId);
|
|
282
241
|
if (updatedRun)
|
|
283
242
|
this.notifyStateChange(updatedRun);
|
|
243
|
+
this.scheduler.runStarted(runId);
|
|
244
|
+
void this.runWorkflow({
|
|
245
|
+
runId,
|
|
246
|
+
workflow,
|
|
247
|
+
options: options ?? this.runOptions.get(runId),
|
|
248
|
+
timeout: this.runTimeout(run),
|
|
249
|
+
...(checkpoint
|
|
250
|
+
? {
|
|
251
|
+
input: checkpoint.state,
|
|
252
|
+
resumeFrom: {
|
|
253
|
+
workflowId: checkpoint.workflowId,
|
|
254
|
+
skipNodes: new Set(checkpoint.completedNodes),
|
|
255
|
+
nodeResults: checkpoint.nodeResults,
|
|
256
|
+
},
|
|
257
|
+
}
|
|
258
|
+
: { input: run.input }),
|
|
259
|
+
}).catch(() => { });
|
|
284
260
|
}
|
|
285
261
|
/**
|
|
286
262
|
* Retry a failed run
|
|
@@ -315,6 +291,7 @@ export class DefaultWorkflowManager {
|
|
|
315
291
|
...run.metadata,
|
|
316
292
|
retriedFrom: runId,
|
|
317
293
|
retriedAt: now,
|
|
294
|
+
retryAttempt: (metadataNumber(run, 'retryAttempt') ?? 0) + 1,
|
|
318
295
|
},
|
|
319
296
|
};
|
|
320
297
|
await this.runStore.save(newRun);
|
|
@@ -365,51 +342,21 @@ export class DefaultWorkflowManager {
|
|
|
365
342
|
replayedAt: now,
|
|
366
343
|
},
|
|
367
344
|
};
|
|
345
|
+
this.registerWorkflow(workflow);
|
|
368
346
|
await this.runStore.save(newRun);
|
|
369
347
|
this.notifyStateChange(newRun);
|
|
370
|
-
const skipNodes = new Set(newRun.completedNodes);
|
|
371
|
-
const abortController = new AbortController();
|
|
372
|
-
this.activeRuns.set(newRunId, { abort: () => abortController.abort() });
|
|
373
348
|
this.scheduler.runStarted(newRunId);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
349
|
+
return this.runWorkflow({
|
|
350
|
+
runId: newRunId,
|
|
351
|
+
workflow,
|
|
352
|
+
input: run.state,
|
|
353
|
+
timeout: this.defaultTimeout,
|
|
354
|
+
resumeFrom: {
|
|
355
|
+
workflowId: newRunId,
|
|
356
|
+
skipNodes: new Set(kept),
|
|
378
357
|
nodeResults: keptResults,
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
await this.runStore.update(newRunId, {
|
|
382
|
-
status: result.error ? 'failed' : 'completed',
|
|
383
|
-
state: result.state,
|
|
384
|
-
output: result.error ? undefined : result.state,
|
|
385
|
-
completedAt: Date.now(),
|
|
386
|
-
checkpointId: result.checkpointId,
|
|
387
|
-
error: result.error
|
|
388
|
-
? { name: result.error.name, message: result.error.message, stack: result.error.stack }
|
|
389
|
-
: undefined,
|
|
390
|
-
});
|
|
391
|
-
const updatedRun = await this.runStore.get(newRunId);
|
|
392
|
-
if (updatedRun)
|
|
393
|
-
this.notifyStateChange(updatedRun);
|
|
394
|
-
return result;
|
|
395
|
-
}
|
|
396
|
-
catch (error) {
|
|
397
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
398
|
-
await this.runStore.update(newRunId, {
|
|
399
|
-
status: 'failed',
|
|
400
|
-
completedAt: Date.now(),
|
|
401
|
-
error: { name: err.name, message: err.message, stack: err.stack },
|
|
402
|
-
});
|
|
403
|
-
const updatedRun = await this.runStore.get(newRunId);
|
|
404
|
-
if (updatedRun)
|
|
405
|
-
this.notifyStateChange(updatedRun);
|
|
406
|
-
throw error;
|
|
407
|
-
}
|
|
408
|
-
finally {
|
|
409
|
-
this.activeRuns.delete(newRunId);
|
|
410
|
-
this.runLocks.delete(newRunId);
|
|
411
|
-
this.scheduler.runCompleted(newRunId);
|
|
412
|
-
}
|
|
358
|
+
},
|
|
359
|
+
});
|
|
413
360
|
}
|
|
414
361
|
/**
|
|
415
362
|
* Get count of active runs
|
|
@@ -440,15 +387,22 @@ export class DefaultWorkflowManager {
|
|
|
440
387
|
dispose() {
|
|
441
388
|
this.stop();
|
|
442
389
|
this.activeRuns.clear();
|
|
390
|
+
this.runOptions.clear();
|
|
443
391
|
this.runLocks.clear();
|
|
444
392
|
this.workflows.clear();
|
|
445
393
|
this.stateChangeCallbacks.clear();
|
|
446
394
|
this.scheduler.dispose();
|
|
447
395
|
}
|
|
396
|
+
/** Scheduling `timeout` stored on the run, else the manager's `defaultTimeout` */
|
|
397
|
+
runTimeout(run) {
|
|
398
|
+
return metadataNumber(run, 'timeout') ?? this.defaultTimeout;
|
|
399
|
+
}
|
|
448
400
|
async handleRunReady(runId) {
|
|
449
401
|
const run = await this.runStore.get(runId);
|
|
450
|
-
if (!run)
|
|
402
|
+
if (!run || (run.status !== 'pending' && run.status !== 'scheduled')) {
|
|
403
|
+
this.scheduler.runCompleted(runId);
|
|
451
404
|
return;
|
|
405
|
+
}
|
|
452
406
|
const workflow = this.workflows.get(run.workflowName);
|
|
453
407
|
if (!workflow) {
|
|
454
408
|
await this.runStore.update(runId, {
|
|
@@ -460,49 +414,170 @@ export class DefaultWorkflowManager {
|
|
|
460
414
|
},
|
|
461
415
|
});
|
|
462
416
|
this.scheduler.runCompleted(runId);
|
|
417
|
+
const failedRun = await this.runStore.get(runId);
|
|
418
|
+
if (failedRun)
|
|
419
|
+
this.notifyStateChange(failedRun);
|
|
463
420
|
return;
|
|
464
421
|
}
|
|
465
422
|
await this.runStore.update(runId, {
|
|
466
423
|
status: 'running',
|
|
467
424
|
startedAt: Date.now(),
|
|
468
425
|
});
|
|
469
|
-
const
|
|
470
|
-
|
|
426
|
+
const startedRun = await this.runStore.get(runId);
|
|
427
|
+
if (startedRun)
|
|
428
|
+
this.notifyStateChange(startedRun);
|
|
471
429
|
try {
|
|
472
|
-
|
|
473
|
-
|
|
430
|
+
await this.runWorkflow({
|
|
431
|
+
runId,
|
|
432
|
+
workflow,
|
|
433
|
+
input: run.input,
|
|
434
|
+
timeout: this.runTimeout(run),
|
|
474
435
|
});
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
436
|
+
}
|
|
437
|
+
catch { }
|
|
438
|
+
const finished = await this.runStore.get(runId);
|
|
439
|
+
const maxRetries = finished ? metadataNumber(finished, 'maxRetries') : undefined;
|
|
440
|
+
if (finished?.status === 'failed' &&
|
|
441
|
+
maxRetries !== undefined &&
|
|
442
|
+
(metadataNumber(finished, 'retryAttempt') ?? 0) < maxRetries) {
|
|
443
|
+
await this.retry(runId);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Abort an active run for `reason` and record `update`. The run's own final write waits
|
|
448
|
+
* for this one, so the stop status is never overwritten by a stale 'running' outcome.
|
|
449
|
+
*/
|
|
450
|
+
async stopRun(runId, reason, update) {
|
|
451
|
+
const write = this.runStore.update(runId, update);
|
|
452
|
+
const active = this.activeRuns.get(runId);
|
|
453
|
+
if (active) {
|
|
454
|
+
active.stopReason = reason;
|
|
455
|
+
active.stopWrite = write.catch(() => { });
|
|
456
|
+
active.controller.abort();
|
|
457
|
+
}
|
|
458
|
+
await write;
|
|
459
|
+
const updatedRun = await this.runStore.get(runId);
|
|
460
|
+
if (updatedRun)
|
|
461
|
+
this.notifyStateChange(updatedRun);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Execute a run that is already stored as running and counted by the scheduler, then
|
|
465
|
+
* record how it ended: completed, failed, or paused/cancelled when it was stopped.
|
|
466
|
+
*/
|
|
467
|
+
async runWorkflow(launch) {
|
|
468
|
+
const { runId, workflow, input, options, timeout, resumeFrom } = launch;
|
|
469
|
+
const active = { controller: new AbortController() };
|
|
470
|
+
this.activeRuns.set(runId, active);
|
|
471
|
+
if (options)
|
|
472
|
+
this.runOptions.set(runId, options);
|
|
473
|
+
let timedOut = false;
|
|
474
|
+
const timeoutHandle = timeout !== undefined && timeout > 0
|
|
475
|
+
? setTimeout(() => {
|
|
476
|
+
timedOut = true;
|
|
477
|
+
active.controller.abort();
|
|
478
|
+
}, timeout)
|
|
479
|
+
: undefined;
|
|
480
|
+
const runTracer = options?.tracing ? createTracer(options.tracing) : this.tracer;
|
|
481
|
+
const runMetrics = options?.metrics?.enabled === false
|
|
482
|
+
? (this.disabledMetrics ??= createMetricsCollector({ enabled: false }))
|
|
483
|
+
: this.metrics;
|
|
484
|
+
try {
|
|
485
|
+
const executed = await this.executor.execute(workflow, input, {
|
|
486
|
+
checkpoint: !!this.checkpointStore,
|
|
487
|
+
workflowId: runId,
|
|
488
|
+
...options,
|
|
489
|
+
...resumeFrom,
|
|
490
|
+
signal: active.controller.signal,
|
|
491
|
+
tracer: runTracer,
|
|
492
|
+
metricsCollector: runMetrics,
|
|
493
|
+
...this.trackNodes(runId, options),
|
|
484
494
|
});
|
|
495
|
+
await this.settleNodeUpdates(runId);
|
|
496
|
+
await active.stopWrite;
|
|
497
|
+
const stopped = executed.error ? active.stopReason : undefined;
|
|
498
|
+
const error = stopped
|
|
499
|
+
? new Error(`Workflow run '${runId}' was ${stopped}`)
|
|
500
|
+
: timedOut
|
|
501
|
+
? new Error(`Workflow run '${runId}' timed out after ${String(timeout)}ms`)
|
|
502
|
+
: executed.error;
|
|
503
|
+
const result = error === executed.error ? executed : { ...executed, error };
|
|
504
|
+
const checkpoint = result.checkpointId ? { checkpointId: result.checkpointId } : {};
|
|
505
|
+
if (stopped) {
|
|
506
|
+
await this.finishRun(runId, { status: stopped, state: result.state, ...checkpoint });
|
|
507
|
+
}
|
|
508
|
+
else if (result.error) {
|
|
509
|
+
await this.finishRun(runId, {
|
|
510
|
+
status: 'failed',
|
|
511
|
+
state: result.state,
|
|
512
|
+
completedAt: Date.now(),
|
|
513
|
+
...checkpoint,
|
|
514
|
+
error: errorRecord(result.error),
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
await this.finishRun(runId, {
|
|
519
|
+
status: 'completed',
|
|
520
|
+
state: result.state,
|
|
521
|
+
output: result.state,
|
|
522
|
+
completedAt: Date.now(),
|
|
523
|
+
pausedAt: undefined,
|
|
524
|
+
error: undefined,
|
|
525
|
+
...checkpoint,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
return result;
|
|
485
529
|
}
|
|
486
530
|
catch (error) {
|
|
487
531
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
488
|
-
await this.
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
stack: err.stack,
|
|
495
|
-
},
|
|
496
|
-
});
|
|
532
|
+
await this.settleNodeUpdates(runId);
|
|
533
|
+
await active.stopWrite;
|
|
534
|
+
await this.finishRun(runId, active.stopReason
|
|
535
|
+
? { status: active.stopReason }
|
|
536
|
+
: { status: 'failed', completedAt: Date.now(), error: errorRecord(err) });
|
|
537
|
+
throw error;
|
|
497
538
|
}
|
|
498
539
|
finally {
|
|
540
|
+
if (timeoutHandle)
|
|
541
|
+
clearTimeout(timeoutHandle);
|
|
542
|
+
if (runTracer && runTracer !== this.tracer) {
|
|
543
|
+
await runTracer.flush().catch((error) => {
|
|
544
|
+
console.warn('[WorkflowManager] Failed to flush run traces:', error);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
499
547
|
this.activeRuns.delete(runId);
|
|
548
|
+
this.runLocks.delete(runId);
|
|
549
|
+
if (active.stopReason !== 'paused')
|
|
550
|
+
this.runOptions.delete(runId);
|
|
500
551
|
this.scheduler.runCompleted(runId);
|
|
501
|
-
const updatedRun = await this.runStore.get(runId);
|
|
502
|
-
if (updatedRun)
|
|
503
|
-
this.notifyStateChange(updatedRun);
|
|
504
552
|
}
|
|
505
553
|
}
|
|
554
|
+
async finishRun(runId, update) {
|
|
555
|
+
await this.runStore.update(runId, update);
|
|
556
|
+
const updatedRun = await this.runStore.get(runId);
|
|
557
|
+
if (updatedRun)
|
|
558
|
+
this.notifyStateChange(updatedRun);
|
|
559
|
+
}
|
|
560
|
+
/** Node callbacks that record the run's current, completed and failed nodes, then call `options`' own. */
|
|
561
|
+
trackNodes(runId, options) {
|
|
562
|
+
return {
|
|
563
|
+
onNodeStart: (node) => {
|
|
564
|
+
void this.updateRunNodes(runId, node, 'start');
|
|
565
|
+
options?.onNodeStart?.(node);
|
|
566
|
+
},
|
|
567
|
+
onNodeComplete: (node, result, duration) => {
|
|
568
|
+
void this.updateRunNodes(runId, node, 'complete');
|
|
569
|
+
options?.onNodeComplete?.(node, result, duration);
|
|
570
|
+
},
|
|
571
|
+
onNodeError: (node, error) => {
|
|
572
|
+
void this.updateRunNodes(runId, node, 'error');
|
|
573
|
+
options?.onNodeError?.(node, error);
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
/** Waits for the node updates still being written, so the final run record has them all. */
|
|
578
|
+
async settleNodeUpdates(runId) {
|
|
579
|
+
await this.runLocks.get(runId);
|
|
580
|
+
}
|
|
506
581
|
async updateRunNodes(runId, nodeId, action) {
|
|
507
582
|
const prev = this.runLocks.get(runId) ?? Promise.resolve();
|
|
508
583
|
const next = prev.then(async () => {
|
|
@@ -510,17 +585,22 @@ export class DefaultWorkflowManager {
|
|
|
510
585
|
if (!run)
|
|
511
586
|
return;
|
|
512
587
|
const updates = {};
|
|
588
|
+
const others = run.currentNodes.filter((n) => n !== nodeId);
|
|
513
589
|
switch (action) {
|
|
514
590
|
case 'start':
|
|
515
|
-
updates.currentNodes = [...
|
|
591
|
+
updates.currentNodes = [...others, nodeId];
|
|
516
592
|
break;
|
|
517
593
|
case 'complete':
|
|
518
|
-
updates.currentNodes =
|
|
519
|
-
updates.completedNodes =
|
|
594
|
+
updates.currentNodes = others;
|
|
595
|
+
updates.completedNodes = run.completedNodes.includes(nodeId)
|
|
596
|
+
? run.completedNodes
|
|
597
|
+
: [...run.completedNodes, nodeId];
|
|
520
598
|
break;
|
|
521
599
|
case 'error':
|
|
522
|
-
updates.currentNodes =
|
|
523
|
-
updates.failedNodes =
|
|
600
|
+
updates.currentNodes = others;
|
|
601
|
+
updates.failedNodes = run.failedNodes.includes(nodeId)
|
|
602
|
+
? run.failedNodes
|
|
603
|
+
: [...run.failedNodes, nodeId];
|
|
524
604
|
break;
|
|
525
605
|
}
|
|
526
606
|
await this.runStore.update(runId, updates);
|