@cogitator-ai/workflows 0.9.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 +90 -10
- package/dist/builder.d.ts +1 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.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/human-node.d.ts +3 -1
- package/dist/human/human-node.d.ts.map +1 -1
- package/dist/human/human-node.js +41 -20
- package/dist/human/human-node.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/manager/workflow-manager.d.ts +55 -8
- package/dist/manager/workflow-manager.d.ts.map +1 -1
- package/dist/manager/workflow-manager.js +249 -185
- 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/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/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/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 +3 -3
|
@@ -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,95 +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
|
-
...this.trackNodes(runId, options),
|
|
122
|
-
});
|
|
123
|
-
await this.settleNodeUpdates(runId);
|
|
124
|
-
const result = timedOut
|
|
125
|
-
? {
|
|
126
|
-
...executed,
|
|
127
|
-
error: new Error(`Workflow run '${runId}' timed out after ${String(this.defaultTimeout)}ms`),
|
|
128
|
-
}
|
|
129
|
-
: executed;
|
|
130
|
-
if (result.error) {
|
|
131
|
-
await this.runStore.update(runId, {
|
|
132
|
-
status: 'failed',
|
|
133
|
-
state: result.state,
|
|
134
|
-
completedAt: Date.now(),
|
|
135
|
-
checkpointId: result.checkpointId,
|
|
136
|
-
error: {
|
|
137
|
-
name: result.error.name,
|
|
138
|
-
message: result.error.message,
|
|
139
|
-
stack: result.error.stack,
|
|
140
|
-
},
|
|
141
|
-
});
|
|
142
|
-
const updatedRun = await this.runStore.get(runId);
|
|
143
|
-
if (updatedRun)
|
|
144
|
-
this.notifyStateChange(updatedRun);
|
|
145
|
-
return result;
|
|
146
|
-
}
|
|
147
|
-
await this.runStore.update(runId, {
|
|
148
|
-
status: 'completed',
|
|
149
|
-
state: result.state,
|
|
150
|
-
output: result.state,
|
|
151
|
-
completedAt: Date.now(),
|
|
152
|
-
checkpointId: result.checkpointId,
|
|
153
|
-
});
|
|
154
|
-
const updatedRun = await this.runStore.get(runId);
|
|
155
|
-
if (updatedRun)
|
|
156
|
-
this.notifyStateChange(updatedRun);
|
|
157
|
-
return result;
|
|
158
|
-
}
|
|
159
|
-
catch (error) {
|
|
160
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
161
|
-
await this.settleNodeUpdates(runId);
|
|
162
|
-
await this.runStore.update(runId, {
|
|
163
|
-
status: 'failed',
|
|
164
|
-
completedAt: Date.now(),
|
|
165
|
-
error: {
|
|
166
|
-
name: err.name,
|
|
167
|
-
message: err.message,
|
|
168
|
-
stack: err.stack,
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
const updatedRun = await this.runStore.get(runId);
|
|
172
|
-
if (updatedRun)
|
|
173
|
-
this.notifyStateChange(updatedRun);
|
|
174
|
-
throw error;
|
|
175
|
-
}
|
|
176
|
-
finally {
|
|
177
|
-
if (timeoutHandle)
|
|
178
|
-
clearTimeout(timeoutHandle);
|
|
179
|
-
if (runTracer && runTracer !== this.tracer) {
|
|
180
|
-
await runTracer.flush().catch((error) => {
|
|
181
|
-
console.warn('[WorkflowManager] Failed to flush run traces:', error);
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
this.activeRuns.delete(runId);
|
|
185
|
-
this.runLocks.delete(runId);
|
|
186
|
-
this.scheduler.runCompleted(runId);
|
|
187
|
-
}
|
|
147
|
+
return this.runWorkflow({ runId, workflow, input, options, timeout: this.defaultTimeout });
|
|
188
148
|
}
|
|
189
149
|
/**
|
|
190
|
-
* 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.
|
|
191
152
|
*/
|
|
192
153
|
async cancel(runId, reason) {
|
|
193
154
|
const run = await this.runStore.get(runId);
|
|
@@ -201,18 +162,13 @@ export class DefaultWorkflowManager {
|
|
|
201
162
|
return;
|
|
202
163
|
}
|
|
203
164
|
if (run.status === 'running' || run.status === 'paused') {
|
|
204
|
-
|
|
205
|
-
if (active) {
|
|
206
|
-
active.abort();
|
|
207
|
-
}
|
|
208
|
-
await this.runStore.update(runId, {
|
|
165
|
+
await this.stopRun(runId, 'cancelled', {
|
|
209
166
|
status: 'cancelled',
|
|
210
167
|
completedAt: Date.now(),
|
|
211
168
|
error: reason ? { name: 'CancelError', message: reason } : undefined,
|
|
212
169
|
});
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
this.notifyStateChange(updatedRun);
|
|
170
|
+
if (!this.activeRuns.has(runId))
|
|
171
|
+
this.runOptions.delete(runId);
|
|
216
172
|
}
|
|
217
173
|
}
|
|
218
174
|
/**
|
|
@@ -234,7 +190,9 @@ export class DefaultWorkflowManager {
|
|
|
234
190
|
return this.runStore.getStats(workflowName);
|
|
235
191
|
}
|
|
236
192
|
/**
|
|
237
|
-
* 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`.
|
|
238
196
|
*/
|
|
239
197
|
async pause(runId) {
|
|
240
198
|
const run = await this.runStore.get(runId);
|
|
@@ -243,35 +201,62 @@ export class DefaultWorkflowManager {
|
|
|
243
201
|
if (run.status !== 'running') {
|
|
244
202
|
throw new Error(`Cannot pause run in status: ${run.status}`);
|
|
245
203
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
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)`);
|
|
249
206
|
}
|
|
250
|
-
await this.
|
|
251
|
-
status: 'paused',
|
|
252
|
-
pausedAt: Date.now(),
|
|
253
|
-
});
|
|
254
|
-
const updatedRun = await this.runStore.get(runId);
|
|
255
|
-
if (updatedRun)
|
|
256
|
-
this.notifyStateChange(updatedRun);
|
|
207
|
+
await this.stopRun(runId, 'paused', { status: 'paused', pausedAt: Date.now() });
|
|
257
208
|
}
|
|
258
209
|
/**
|
|
259
|
-
* 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.
|
|
260
214
|
*/
|
|
261
|
-
async resume(runId) {
|
|
215
|
+
async resume(runId, options) {
|
|
262
216
|
const run = await this.runStore.get(runId);
|
|
263
217
|
if (!run)
|
|
264
218
|
throw new Error(`Run not found: ${runId}`);
|
|
265
219
|
if (run.status !== 'paused') {
|
|
266
220
|
throw new Error(`Cannot resume run in status: ${run.status}`);
|
|
267
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
|
+
}
|
|
268
235
|
await this.runStore.update(runId, {
|
|
269
236
|
status: 'running',
|
|
270
237
|
pausedAt: undefined,
|
|
238
|
+
currentNodes: [],
|
|
271
239
|
});
|
|
272
240
|
const updatedRun = await this.runStore.get(runId);
|
|
273
241
|
if (updatedRun)
|
|
274
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(() => { });
|
|
275
260
|
}
|
|
276
261
|
/**
|
|
277
262
|
* Retry a failed run
|
|
@@ -306,6 +291,7 @@ export class DefaultWorkflowManager {
|
|
|
306
291
|
...run.metadata,
|
|
307
292
|
retriedFrom: runId,
|
|
308
293
|
retriedAt: now,
|
|
294
|
+
retryAttempt: (metadataNumber(run, 'retryAttempt') ?? 0) + 1,
|
|
309
295
|
},
|
|
310
296
|
};
|
|
311
297
|
await this.runStore.save(newRun);
|
|
@@ -356,51 +342,21 @@ export class DefaultWorkflowManager {
|
|
|
356
342
|
replayedAt: now,
|
|
357
343
|
},
|
|
358
344
|
};
|
|
345
|
+
this.registerWorkflow(workflow);
|
|
359
346
|
await this.runStore.save(newRun);
|
|
360
347
|
this.notifyStateChange(newRun);
|
|
361
|
-
const skipNodes = new Set(newRun.completedNodes);
|
|
362
|
-
const abortController = new AbortController();
|
|
363
|
-
this.activeRuns.set(newRunId, { abort: () => abortController.abort() });
|
|
364
348
|
this.scheduler.runStarted(newRunId);
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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),
|
|
369
357
|
nodeResults: keptResults,
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
await this.runStore.update(newRunId, {
|
|
373
|
-
status: result.error ? 'failed' : 'completed',
|
|
374
|
-
state: result.state,
|
|
375
|
-
output: result.error ? undefined : result.state,
|
|
376
|
-
completedAt: Date.now(),
|
|
377
|
-
checkpointId: result.checkpointId,
|
|
378
|
-
error: result.error
|
|
379
|
-
? { name: result.error.name, message: result.error.message, stack: result.error.stack }
|
|
380
|
-
: undefined,
|
|
381
|
-
});
|
|
382
|
-
const updatedRun = await this.runStore.get(newRunId);
|
|
383
|
-
if (updatedRun)
|
|
384
|
-
this.notifyStateChange(updatedRun);
|
|
385
|
-
return result;
|
|
386
|
-
}
|
|
387
|
-
catch (error) {
|
|
388
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
389
|
-
await this.runStore.update(newRunId, {
|
|
390
|
-
status: 'failed',
|
|
391
|
-
completedAt: Date.now(),
|
|
392
|
-
error: { name: err.name, message: err.message, stack: err.stack },
|
|
393
|
-
});
|
|
394
|
-
const updatedRun = await this.runStore.get(newRunId);
|
|
395
|
-
if (updatedRun)
|
|
396
|
-
this.notifyStateChange(updatedRun);
|
|
397
|
-
throw error;
|
|
398
|
-
}
|
|
399
|
-
finally {
|
|
400
|
-
this.activeRuns.delete(newRunId);
|
|
401
|
-
this.runLocks.delete(newRunId);
|
|
402
|
-
this.scheduler.runCompleted(newRunId);
|
|
403
|
-
}
|
|
358
|
+
},
|
|
359
|
+
});
|
|
404
360
|
}
|
|
405
361
|
/**
|
|
406
362
|
* Get count of active runs
|
|
@@ -431,15 +387,22 @@ export class DefaultWorkflowManager {
|
|
|
431
387
|
dispose() {
|
|
432
388
|
this.stop();
|
|
433
389
|
this.activeRuns.clear();
|
|
390
|
+
this.runOptions.clear();
|
|
434
391
|
this.runLocks.clear();
|
|
435
392
|
this.workflows.clear();
|
|
436
393
|
this.stateChangeCallbacks.clear();
|
|
437
394
|
this.scheduler.dispose();
|
|
438
395
|
}
|
|
396
|
+
/** Scheduling `timeout` stored on the run, else the manager's `defaultTimeout` */
|
|
397
|
+
runTimeout(run) {
|
|
398
|
+
return metadataNumber(run, 'timeout') ?? this.defaultTimeout;
|
|
399
|
+
}
|
|
439
400
|
async handleRunReady(runId) {
|
|
440
401
|
const run = await this.runStore.get(runId);
|
|
441
|
-
if (!run)
|
|
402
|
+
if (!run || (run.status !== 'pending' && run.status !== 'scheduled')) {
|
|
403
|
+
this.scheduler.runCompleted(runId);
|
|
442
404
|
return;
|
|
405
|
+
}
|
|
443
406
|
const workflow = this.workflows.get(run.workflowName);
|
|
444
407
|
if (!workflow) {
|
|
445
408
|
await this.runStore.update(runId, {
|
|
@@ -451,53 +414,149 @@ export class DefaultWorkflowManager {
|
|
|
451
414
|
},
|
|
452
415
|
});
|
|
453
416
|
this.scheduler.runCompleted(runId);
|
|
417
|
+
const failedRun = await this.runStore.get(runId);
|
|
418
|
+
if (failedRun)
|
|
419
|
+
this.notifyStateChange(failedRun);
|
|
454
420
|
return;
|
|
455
421
|
}
|
|
456
422
|
await this.runStore.update(runId, {
|
|
457
423
|
status: 'running',
|
|
458
424
|
startedAt: Date.now(),
|
|
459
425
|
});
|
|
460
|
-
const
|
|
461
|
-
|
|
426
|
+
const startedRun = await this.runStore.get(runId);
|
|
427
|
+
if (startedRun)
|
|
428
|
+
this.notifyStateChange(startedRun);
|
|
462
429
|
try {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
430
|
+
await this.runWorkflow({
|
|
431
|
+
runId,
|
|
432
|
+
workflow,
|
|
433
|
+
input: run.input,
|
|
434
|
+
timeout: this.runTimeout(run),
|
|
466
435
|
});
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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),
|
|
477
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;
|
|
478
529
|
}
|
|
479
530
|
catch (error) {
|
|
480
531
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
481
532
|
await this.settleNodeUpdates(runId);
|
|
482
|
-
await
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
error:
|
|
486
|
-
|
|
487
|
-
message: err.message,
|
|
488
|
-
stack: err.stack,
|
|
489
|
-
},
|
|
490
|
-
});
|
|
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;
|
|
491
538
|
}
|
|
492
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
|
+
}
|
|
493
547
|
this.activeRuns.delete(runId);
|
|
494
548
|
this.runLocks.delete(runId);
|
|
549
|
+
if (active.stopReason !== 'paused')
|
|
550
|
+
this.runOptions.delete(runId);
|
|
495
551
|
this.scheduler.runCompleted(runId);
|
|
496
|
-
const updatedRun = await this.runStore.get(runId);
|
|
497
|
-
if (updatedRun)
|
|
498
|
-
this.notifyStateChange(updatedRun);
|
|
499
552
|
}
|
|
500
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
|
+
}
|
|
501
560
|
/** Node callbacks that record the run's current, completed and failed nodes, then call `options`' own. */
|
|
502
561
|
trackNodes(runId, options) {
|
|
503
562
|
return {
|
|
@@ -526,17 +585,22 @@ export class DefaultWorkflowManager {
|
|
|
526
585
|
if (!run)
|
|
527
586
|
return;
|
|
528
587
|
const updates = {};
|
|
588
|
+
const others = run.currentNodes.filter((n) => n !== nodeId);
|
|
529
589
|
switch (action) {
|
|
530
590
|
case 'start':
|
|
531
|
-
updates.currentNodes = [...
|
|
591
|
+
updates.currentNodes = [...others, nodeId];
|
|
532
592
|
break;
|
|
533
593
|
case 'complete':
|
|
534
|
-
updates.currentNodes =
|
|
535
|
-
updates.completedNodes =
|
|
594
|
+
updates.currentNodes = others;
|
|
595
|
+
updates.completedNodes = run.completedNodes.includes(nodeId)
|
|
596
|
+
? run.completedNodes
|
|
597
|
+
: [...run.completedNodes, nodeId];
|
|
536
598
|
break;
|
|
537
599
|
case 'error':
|
|
538
|
-
updates.currentNodes =
|
|
539
|
-
updates.failedNodes =
|
|
600
|
+
updates.currentNodes = others;
|
|
601
|
+
updates.failedNodes = run.failedNodes.includes(nodeId)
|
|
602
|
+
? run.failedNodes
|
|
603
|
+
: [...run.failedNodes, nodeId];
|
|
540
604
|
break;
|
|
541
605
|
}
|
|
542
606
|
await this.runStore.update(runId, updates);
|