@smithers-orchestrator/driver 0.16.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/LICENSE +21 -0
- package/package.json +80 -0
- package/src/ContinueAsNewHandler.ts +7 -0
- package/src/CreateWorkflowSession.ts +5 -0
- package/src/CreateWorkflowSessionOptions.ts +9 -0
- package/src/OutputAccessor.ts +20 -0
- package/src/OutputKey.ts +1 -0
- package/src/OutputSnapshot.ts +3 -0
- package/src/RunAuthContext.ts +6 -0
- package/src/RunOptions.ts +42 -0
- package/src/RunResult.ts +9 -0
- package/src/RunStatus.ts +9 -0
- package/src/SafeParser.ts +5 -0
- package/src/SchedulerWaitHandler.ts +6 -0
- package/src/SmithersCtx.js +195 -0
- package/src/SmithersCtxOptions.ts +14 -0
- package/src/SmithersRuntimeConfig.ts +3 -0
- package/src/SmithersTaskRuntime.ts +22 -0
- package/src/SpawnCaptureOptions.ts +12 -0
- package/src/SpawnCaptureResult.ts +5 -0
- package/src/TaskCompletedEvent.ts +5 -0
- package/src/TaskExecutor.ts +7 -0
- package/src/TaskExecutorContext.ts +7 -0
- package/src/TaskFailedEvent.ts +5 -0
- package/src/WaitHandler.ts +8 -0
- package/src/WorkflowDefinition.ts +16 -0
- package/src/WorkflowDriver.js +519 -0
- package/src/WorkflowDriverOptions.ts +27 -0
- package/src/WorkflowElement.ts +5 -0
- package/src/WorkflowGraphRenderer.ts +9 -0
- package/src/WorkflowRuntime.ts +3 -0
- package/src/WorkflowSession.ts +11 -0
- package/src/buildCurrentScopes.js +34 -0
- package/src/child-process.js +222 -0
- package/src/defaultTaskExecutor.js +28 -0
- package/src/filterRowsByNodeId.js +19 -0
- package/src/ignoreSyncError.js +16 -0
- package/src/index.d.ts +411 -0
- package/src/index.js +31 -0
- package/src/interop.js +1 -0
- package/src/normalizeInputRow.js +27 -0
- package/src/task-runtime.js +30 -0
- package/src/withAbort.js +43 -0
- package/src/withLogicalIterationShortcuts.js +46 -0
- package/src/workflow-types.ts +20 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import { SmithersCtx } from "./SmithersCtx.js";
|
|
2
|
+
import { defaultTaskExecutor } from "./defaultTaskExecutor.js";
|
|
3
|
+
import { withAbort } from "./withAbort.js";
|
|
4
|
+
/** @typedef {import("./CreateWorkflowSession.ts").CreateWorkflowSession} CreateWorkflowSession */
|
|
5
|
+
/** @typedef {import("./OutputSnapshot.ts").OutputSnapshot} OutputSnapshot */
|
|
6
|
+
/** @typedef {import("./WorkflowSession.ts").WorkflowSession} WorkflowSession */
|
|
7
|
+
/** @typedef {import("./WorkflowRuntime.ts").WorkflowRuntime} WorkflowRuntime */
|
|
8
|
+
/** @typedef {import("./WorkflowGraphRenderer.ts").WorkflowGraphRenderer} WorkflowGraphRenderer */
|
|
9
|
+
/** @typedef {import("./TaskExecutor.ts").TaskExecutor} TaskExecutor */
|
|
10
|
+
/** @typedef {import("./SchedulerWaitHandler.ts").SchedulerWaitHandler} SchedulerWaitHandler */
|
|
11
|
+
/** @typedef {import("./WaitHandler.ts").WaitHandler} WaitHandler */
|
|
12
|
+
/** @typedef {import("./ContinueAsNewHandler.ts").ContinueAsNewHandler} ContinueAsNewHandler */
|
|
13
|
+
|
|
14
|
+
/** @typedef {import("./RunOptions.ts").RunOptions} RunOptions */
|
|
15
|
+
/** @typedef {import("@smithers-orchestrator/scheduler").RunResult} RunResult */
|
|
16
|
+
/** @typedef {import("@smithers-orchestrator/scheduler").EngineDecision} EngineDecision */
|
|
17
|
+
/** @typedef {import("@smithers-orchestrator/scheduler").RenderContext} RenderContext */
|
|
18
|
+
/** @typedef {import("@smithers-orchestrator/scheduler").WaitReason} WaitReason */
|
|
19
|
+
/** @typedef {import("@smithers-orchestrator/graph/types").TaskDescriptor} TaskDescriptor */
|
|
20
|
+
|
|
21
|
+
const SCHEDULER_SPECIFIER = "@smithers-orchestrator/scheduler";
|
|
22
|
+
const LOCAL_SCHEDULER_SPECIFIER = "../../scheduler/src/index.js";
|
|
23
|
+
function createRunId() {
|
|
24
|
+
return `run_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* @param {unknown} value
|
|
28
|
+
* @returns {value is EngineDecision}
|
|
29
|
+
*/
|
|
30
|
+
function isEngineDecision(value) {
|
|
31
|
+
if (!value || typeof value !== "object")
|
|
32
|
+
return false;
|
|
33
|
+
return typeof value._tag === "string";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* @param {unknown} value
|
|
37
|
+
* @returns {value is RunResult}
|
|
38
|
+
*/
|
|
39
|
+
function isRunResult(value) {
|
|
40
|
+
if (!value || typeof value !== "object")
|
|
41
|
+
return false;
|
|
42
|
+
const status = value.status;
|
|
43
|
+
return typeof status === "string";
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* @param {unknown} value
|
|
47
|
+
* @returns {value is WorkflowSession}
|
|
48
|
+
*/
|
|
49
|
+
function isWorkflowSession(value) {
|
|
50
|
+
return Boolean(value &&
|
|
51
|
+
typeof value === "object" &&
|
|
52
|
+
typeof value.submitGraph === "function" &&
|
|
53
|
+
typeof value.taskCompleted === "function" &&
|
|
54
|
+
typeof value.taskFailed === "function");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* @param {Record<string, number> | ReadonlyMap<string, number>} [iterations]
|
|
58
|
+
* @returns {Record<string, number> | undefined}
|
|
59
|
+
*/
|
|
60
|
+
function recordFromIterations(iterations) {
|
|
61
|
+
if (!iterations)
|
|
62
|
+
return undefined;
|
|
63
|
+
if (typeof iterations.entries === "function") {
|
|
64
|
+
return Object.fromEntries(iterations);
|
|
65
|
+
}
|
|
66
|
+
return iterations;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* @param {RenderContext} context
|
|
70
|
+
* @param {ReadonlyMap<string, string>} [knownOutputTables]
|
|
71
|
+
* @returns {OutputSnapshot}
|
|
72
|
+
*/
|
|
73
|
+
function snapshotFromContext(context, knownOutputTables) {
|
|
74
|
+
const outputs = context.outputs;
|
|
75
|
+
if (!outputs)
|
|
76
|
+
return {};
|
|
77
|
+
if (typeof outputs.values !== "function") {
|
|
78
|
+
return normalizeOutputSnapshot(outputs);
|
|
79
|
+
}
|
|
80
|
+
const outputMap = outputs;
|
|
81
|
+
const descriptors = new Map();
|
|
82
|
+
for (const [nodeId, outputTableName] of knownOutputTables ?? []) {
|
|
83
|
+
descriptors.set(nodeId, { outputTableName });
|
|
84
|
+
}
|
|
85
|
+
for (const task of context.graph?.tasks ?? []) {
|
|
86
|
+
descriptors.set(task.nodeId, { outputTableName: task.outputTableName });
|
|
87
|
+
}
|
|
88
|
+
const snapshot = {};
|
|
89
|
+
for (const output of outputMap.values()) {
|
|
90
|
+
const tableName = descriptors.get(output.nodeId)?.outputTableName;
|
|
91
|
+
if (!tableName)
|
|
92
|
+
continue;
|
|
93
|
+
const row = output.output && typeof output.output === "object" && !Array.isArray(output.output)
|
|
94
|
+
? {
|
|
95
|
+
...output.output,
|
|
96
|
+
nodeId: output.nodeId,
|
|
97
|
+
iteration: output.iteration,
|
|
98
|
+
}
|
|
99
|
+
: {
|
|
100
|
+
nodeId: output.nodeId,
|
|
101
|
+
iteration: output.iteration,
|
|
102
|
+
payload: output.output,
|
|
103
|
+
};
|
|
104
|
+
(snapshot[tableName] ??= []).push(row);
|
|
105
|
+
}
|
|
106
|
+
return snapshot;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* @param {unknown} value
|
|
110
|
+
* @returns {OutputSnapshot}
|
|
111
|
+
*/
|
|
112
|
+
function normalizeOutputSnapshot(value) {
|
|
113
|
+
if (!value || typeof value !== "object")
|
|
114
|
+
return {};
|
|
115
|
+
const snapshot = {};
|
|
116
|
+
for (const [key, rows] of Object.entries(value)) {
|
|
117
|
+
snapshot[key] = Array.isArray(rows) ? rows : [];
|
|
118
|
+
}
|
|
119
|
+
return snapshot;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* @param {OutputSnapshot} base
|
|
123
|
+
* @param {OutputSnapshot} live
|
|
124
|
+
* @returns {OutputSnapshot}
|
|
125
|
+
*/
|
|
126
|
+
function mergeOutputSnapshots(base, live) {
|
|
127
|
+
const merged = {};
|
|
128
|
+
for (const [key, rows] of Object.entries(base)) {
|
|
129
|
+
merged[key] = [...rows];
|
|
130
|
+
}
|
|
131
|
+
for (const [key, rows] of Object.entries(live)) {
|
|
132
|
+
merged[key] = [...(merged[key] ?? []), ...rows];
|
|
133
|
+
}
|
|
134
|
+
return merged;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* @returns {Promise<CreateWorkflowSession | null>}
|
|
138
|
+
*/
|
|
139
|
+
async function loadCreateSession() {
|
|
140
|
+
for (const specifier of [SCHEDULER_SPECIFIER, LOCAL_SCHEDULER_SPECIFIER]) {
|
|
141
|
+
let mod;
|
|
142
|
+
try {
|
|
143
|
+
mod = (await import(specifier));
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (typeof mod.createSession === "function")
|
|
149
|
+
return mod.createSession;
|
|
150
|
+
if (typeof mod.makeWorkflowSession === "function") {
|
|
151
|
+
return mod.makeWorkflowSession;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* @param {unknown} error
|
|
158
|
+
* @returns {boolean}
|
|
159
|
+
*/
|
|
160
|
+
function isAbortError(error) {
|
|
161
|
+
return Boolean(error &&
|
|
162
|
+
typeof error === "object" &&
|
|
163
|
+
("name" in error || "message" in error) &&
|
|
164
|
+
(/abort/i.test(String(error.name ?? "")) ||
|
|
165
|
+
/abort/i.test(String(error.message ?? ""))));
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* @param {number} ms
|
|
169
|
+
* @param {AbortSignal} [signal]
|
|
170
|
+
* @returns {Promise<void>}
|
|
171
|
+
*/
|
|
172
|
+
async function sleepWithAbort(ms, signal) {
|
|
173
|
+
if (signal?.aborted) {
|
|
174
|
+
const error = new Error("Task aborted");
|
|
175
|
+
error.name = "AbortError";
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
if (ms <= 0)
|
|
179
|
+
return;
|
|
180
|
+
let timeout;
|
|
181
|
+
const sleep = new Promise((resolve) => {
|
|
182
|
+
timeout = setTimeout(resolve, ms);
|
|
183
|
+
});
|
|
184
|
+
try {
|
|
185
|
+
await withAbort(sleep, signal);
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
if (timeout)
|
|
189
|
+
clearTimeout(timeout);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* @template {unknown} [Schema=unknown]
|
|
194
|
+
*/
|
|
195
|
+
export class WorkflowDriver {
|
|
196
|
+
/** @type {import("./WorkflowDefinition.ts").WorkflowDefinition<Schema>} */
|
|
197
|
+
workflow;
|
|
198
|
+
/** @type {WorkflowRuntime} */
|
|
199
|
+
runtime;
|
|
200
|
+
/** @type {unknown} */
|
|
201
|
+
db;
|
|
202
|
+
/** @type {string | undefined} */
|
|
203
|
+
configuredRunId;
|
|
204
|
+
/** @type {string | undefined} */
|
|
205
|
+
rootDir;
|
|
206
|
+
/** @type {string | null | undefined} */
|
|
207
|
+
workflowPath;
|
|
208
|
+
/** @type {TaskExecutor} */
|
|
209
|
+
executeTask;
|
|
210
|
+
/** @type {SchedulerWaitHandler | undefined} */
|
|
211
|
+
onSchedulerWait;
|
|
212
|
+
/** @type {WaitHandler | undefined} */
|
|
213
|
+
onWait;
|
|
214
|
+
/** @type {ContinueAsNewHandler | undefined} */
|
|
215
|
+
continueAsNewHandler;
|
|
216
|
+
/** @type {CreateWorkflowSession | undefined} */
|
|
217
|
+
createSession;
|
|
218
|
+
/** @type {WorkflowGraphRenderer} */
|
|
219
|
+
renderer;
|
|
220
|
+
/** @type {WorkflowSession | undefined} */
|
|
221
|
+
session;
|
|
222
|
+
/** @type {string} */
|
|
223
|
+
activeRunId = "";
|
|
224
|
+
/** @type {RunOptions | undefined} */
|
|
225
|
+
activeOptions;
|
|
226
|
+
/** @type {import("@smithers-orchestrator/graph").WorkflowGraph | undefined} */
|
|
227
|
+
lastGraph;
|
|
228
|
+
/** @type {Map<string, string>} */
|
|
229
|
+
outputTablesByNodeId = new Map();
|
|
230
|
+
/** @type {OutputSnapshot} */
|
|
231
|
+
baseOutputs = {};
|
|
232
|
+
/**
|
|
233
|
+
* @param {import("./WorkflowDriverOptions.ts").WorkflowDriverOptions<Schema>} options
|
|
234
|
+
*/
|
|
235
|
+
constructor(options) {
|
|
236
|
+
this.workflow = options.workflow;
|
|
237
|
+
this.runtime = options.runtime;
|
|
238
|
+
this.db = options.db ?? options.workflow.db;
|
|
239
|
+
this.configuredRunId = options.runId;
|
|
240
|
+
this.rootDir = options.rootDir;
|
|
241
|
+
this.workflowPath = options.workflowPath;
|
|
242
|
+
this.session = options.session;
|
|
243
|
+
this.createSession = options.createSession;
|
|
244
|
+
this.executeTask = options.executeTask ?? defaultTaskExecutor;
|
|
245
|
+
this.onSchedulerWait = options.onSchedulerWait;
|
|
246
|
+
this.onWait = options.onWait;
|
|
247
|
+
this.continueAsNewHandler = options.continueAsNew;
|
|
248
|
+
this.renderer = options.renderer;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* @param {RunOptions} options
|
|
252
|
+
* @returns {Promise<RunResult>}
|
|
253
|
+
*/
|
|
254
|
+
async run(options) {
|
|
255
|
+
const runId = options.runId ?? this.configuredRunId ?? createRunId();
|
|
256
|
+
this.activeRunId = runId;
|
|
257
|
+
this.activeOptions = options;
|
|
258
|
+
this.baseOutputs = normalizeOutputSnapshot(options.initialOutputs ?? options.outputs);
|
|
259
|
+
this.session = this.session ?? (await this.initializeSession(runId, options));
|
|
260
|
+
if (options.signal?.aborted) {
|
|
261
|
+
return this.cancelRun();
|
|
262
|
+
}
|
|
263
|
+
const initialIterations = recordFromIterations(options.initialIterations ??
|
|
264
|
+
options.iterations ??
|
|
265
|
+
options.ralphIterations);
|
|
266
|
+
let decision = await this.renderAndSubmit({
|
|
267
|
+
runId,
|
|
268
|
+
iteration: typeof options.initialIteration === "number"
|
|
269
|
+
? options.initialIteration
|
|
270
|
+
: typeof options.iteration === "number"
|
|
271
|
+
? options.iteration
|
|
272
|
+
: 0,
|
|
273
|
+
iterations: initialIterations ?? {},
|
|
274
|
+
input: options.input,
|
|
275
|
+
outputs: {},
|
|
276
|
+
auth: options.auth ?? null,
|
|
277
|
+
});
|
|
278
|
+
while (true) {
|
|
279
|
+
if (this.activeOptions?.signal?.aborted) {
|
|
280
|
+
return this.cancelRun();
|
|
281
|
+
}
|
|
282
|
+
switch (decision._tag) {
|
|
283
|
+
case "Execute": {
|
|
284
|
+
const next = await this.executeTasks(decision.tasks);
|
|
285
|
+
if (isRunResult(next))
|
|
286
|
+
return next;
|
|
287
|
+
decision = next;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
case "ReRender":
|
|
291
|
+
decision = await this.renderAndSubmit(decision.context);
|
|
292
|
+
break;
|
|
293
|
+
case "Wait": {
|
|
294
|
+
const next = await this.handleWait(decision.reason);
|
|
295
|
+
if (isRunResult(next))
|
|
296
|
+
return next;
|
|
297
|
+
decision = next;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
case "ContinueAsNew":
|
|
301
|
+
return this.continueAsNew(decision.transition);
|
|
302
|
+
case "Finished":
|
|
303
|
+
return decision.result;
|
|
304
|
+
case "Failed":
|
|
305
|
+
return { runId, status: "failed", error: decision.error };
|
|
306
|
+
default:
|
|
307
|
+
return {
|
|
308
|
+
runId,
|
|
309
|
+
status: "failed",
|
|
310
|
+
error: new Error(`Unknown engine decision: ${String(decision?._tag)}`),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* @param {string} runId
|
|
317
|
+
* @param {RunOptions} options
|
|
318
|
+
* @returns {Promise<WorkflowSession>}
|
|
319
|
+
*/
|
|
320
|
+
async initializeSession(runId, options) {
|
|
321
|
+
const createSession = this.createSession ?? (await loadCreateSession());
|
|
322
|
+
if (!createSession) {
|
|
323
|
+
throw new Error("WorkflowDriver requires a WorkflowSession or createSession from @smithers-orchestrator/scheduler.");
|
|
324
|
+
}
|
|
325
|
+
const created = createSession({
|
|
326
|
+
db: this.db,
|
|
327
|
+
runId,
|
|
328
|
+
rootDir: options.rootDir ?? this.rootDir,
|
|
329
|
+
workflowPath: options.workflowPath ?? this.workflowPath ?? null,
|
|
330
|
+
options,
|
|
331
|
+
});
|
|
332
|
+
if (isWorkflowSession(created)) {
|
|
333
|
+
return created;
|
|
334
|
+
}
|
|
335
|
+
return this.runEffect(created);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* @param {RenderContext} context
|
|
339
|
+
* @returns {Promise<EngineDecision>}
|
|
340
|
+
*/
|
|
341
|
+
async renderAndSubmit(context) {
|
|
342
|
+
if (!this.session) {
|
|
343
|
+
throw new Error("WorkflowSession is not initialized.");
|
|
344
|
+
}
|
|
345
|
+
const iteration = typeof context.iteration === "number" ? context.iteration : 0;
|
|
346
|
+
const iterations = recordFromIterations(context.iterations ?? context.ralphIterations);
|
|
347
|
+
const ctx = new SmithersCtx({
|
|
348
|
+
runId: context.runId,
|
|
349
|
+
iteration,
|
|
350
|
+
iterations,
|
|
351
|
+
input: context.input ?? this.activeOptions?.input ?? {},
|
|
352
|
+
auth: context.auth,
|
|
353
|
+
outputs: mergeOutputSnapshots(this.baseOutputs, snapshotFromContext(context, this.outputTablesByNodeId)),
|
|
354
|
+
zodToKeyName: this.workflow.zodToKeyName,
|
|
355
|
+
runtimeConfig: this.activeOptions?.cliAgentToolsDefault
|
|
356
|
+
? { cliAgentToolsDefault: this.activeOptions.cliAgentToolsDefault }
|
|
357
|
+
: undefined,
|
|
358
|
+
});
|
|
359
|
+
const graph = await this.renderer.render(this.workflow.build(ctx), {
|
|
360
|
+
ralphIterations: context.iterations ?? context.ralphIterations,
|
|
361
|
+
defaultIteration: iteration,
|
|
362
|
+
baseRootDir: this.activeOptions?.rootDir ?? this.rootDir,
|
|
363
|
+
workflowPath: this.activeOptions?.workflowPath ?? this.workflowPath ?? null,
|
|
364
|
+
});
|
|
365
|
+
for (const task of graph.tasks) {
|
|
366
|
+
if (task.outputTableName) {
|
|
367
|
+
this.outputTablesByNodeId.set(task.nodeId, task.outputTableName);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
this.lastGraph = graph;
|
|
371
|
+
return this.runEffect(this.session.submitGraph(graph));
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* @param {readonly TaskDescriptor[]} tasks
|
|
375
|
+
* @returns {Promise<EngineDecision | RunResult>}
|
|
376
|
+
*/
|
|
377
|
+
async executeTasks(tasks) {
|
|
378
|
+
if (!this.session) {
|
|
379
|
+
throw new Error("WorkflowSession is not initialized.");
|
|
380
|
+
}
|
|
381
|
+
const context = {
|
|
382
|
+
runId: this.activeRunId,
|
|
383
|
+
options: this.activeOptions ?? { input: {} },
|
|
384
|
+
signal: this.activeOptions?.signal,
|
|
385
|
+
};
|
|
386
|
+
if (context.signal?.aborted) {
|
|
387
|
+
return this.cancelRun();
|
|
388
|
+
}
|
|
389
|
+
let latestDecision;
|
|
390
|
+
let cancelled = false;
|
|
391
|
+
const waitStart = performance.now();
|
|
392
|
+
try {
|
|
393
|
+
await Promise.all(tasks.map(async (task) => {
|
|
394
|
+
let report;
|
|
395
|
+
try {
|
|
396
|
+
const output = await withAbort(Promise.resolve().then(() => this.executeTask(task, context)), context.signal);
|
|
397
|
+
report = await this.runEffect(this.session.taskCompleted({
|
|
398
|
+
nodeId: task.nodeId,
|
|
399
|
+
iteration: task.iteration,
|
|
400
|
+
output,
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
if (context.signal?.aborted || isAbortError(error)) {
|
|
405
|
+
cancelled = true;
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
report = await this.runEffect(this.session.taskFailed({
|
|
409
|
+
nodeId: task.nodeId,
|
|
410
|
+
iteration: task.iteration,
|
|
411
|
+
error,
|
|
412
|
+
}));
|
|
413
|
+
}
|
|
414
|
+
if (isEngineDecision(report)) {
|
|
415
|
+
latestDecision = report;
|
|
416
|
+
}
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
419
|
+
finally {
|
|
420
|
+
await this.onSchedulerWait?.(performance.now() - waitStart, {
|
|
421
|
+
runId: this.activeRunId,
|
|
422
|
+
tasks,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
if (cancelled || context.signal?.aborted) {
|
|
426
|
+
return this.cancelRun();
|
|
427
|
+
}
|
|
428
|
+
if (latestDecision) {
|
|
429
|
+
return latestDecision;
|
|
430
|
+
}
|
|
431
|
+
if (typeof this.session.getNextDecision === "function") {
|
|
432
|
+
return this.runEffect(this.session.getNextDecision());
|
|
433
|
+
}
|
|
434
|
+
throw new Error("WorkflowSession did not provide the next EngineDecision.");
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* @param {WaitReason} reason
|
|
438
|
+
* @returns {Promise<EngineDecision | RunResult>}
|
|
439
|
+
*/
|
|
440
|
+
async handleWait(reason) {
|
|
441
|
+
if (this.onWait) {
|
|
442
|
+
return this.onWait(reason, {
|
|
443
|
+
runId: this.activeRunId,
|
|
444
|
+
options: this.activeOptions ?? { input: {} },
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
switch (reason._tag) {
|
|
448
|
+
case "Approval":
|
|
449
|
+
return { runId: this.activeRunId, status: "waiting-approval" };
|
|
450
|
+
case "Event":
|
|
451
|
+
case "ExternalTrigger":
|
|
452
|
+
case "HotReload":
|
|
453
|
+
case "OrphanRecovery":
|
|
454
|
+
return { runId: this.activeRunId, status: "waiting-event" };
|
|
455
|
+
case "Timer":
|
|
456
|
+
return { runId: this.activeRunId, status: "waiting-timer" };
|
|
457
|
+
case "RetryBackoff": {
|
|
458
|
+
await sleepWithAbort(reason.waitMs, this.activeOptions?.signal);
|
|
459
|
+
if (this.activeOptions?.signal?.aborted) {
|
|
460
|
+
return this.cancelRun();
|
|
461
|
+
}
|
|
462
|
+
if (this.session && typeof this.session.getNextDecision === "function") {
|
|
463
|
+
return this.runEffect(this.session.getNextDecision());
|
|
464
|
+
}
|
|
465
|
+
if (this.session && this.lastGraph) {
|
|
466
|
+
return this.runEffect(this.session.submitGraph(this.lastGraph));
|
|
467
|
+
}
|
|
468
|
+
return { runId: this.activeRunId, status: "waiting-timer" };
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* @param {unknown} transition
|
|
474
|
+
* @returns {Promise<RunResult>}
|
|
475
|
+
*/
|
|
476
|
+
async continueAsNew(transition) {
|
|
477
|
+
if (this.continueAsNewHandler) {
|
|
478
|
+
return this.continueAsNewHandler(transition, {
|
|
479
|
+
runId: this.activeRunId,
|
|
480
|
+
options: this.activeOptions ?? { input: {} },
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
runId: this.activeRunId,
|
|
485
|
+
status: "continued",
|
|
486
|
+
output: transition,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* @returns {Promise<RunResult>}
|
|
491
|
+
*/
|
|
492
|
+
async cancelRun() {
|
|
493
|
+
if (this.session && typeof this.session.cancelRequested === "function") {
|
|
494
|
+
const result = await this.runEffect(this.session.cancelRequested());
|
|
495
|
+
if (isRunResult(result))
|
|
496
|
+
return result;
|
|
497
|
+
if (isEngineDecision(result)) {
|
|
498
|
+
if (result._tag === "Finished")
|
|
499
|
+
return result.result;
|
|
500
|
+
if (result._tag === "Failed") {
|
|
501
|
+
return {
|
|
502
|
+
runId: this.activeRunId,
|
|
503
|
+
status: "failed",
|
|
504
|
+
error: result.error,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return { runId: this.activeRunId, status: "cancelled" };
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* @template A
|
|
513
|
+
* @param {unknown} effect
|
|
514
|
+
* @returns {Promise<A>}
|
|
515
|
+
*/
|
|
516
|
+
runEffect(effect) {
|
|
517
|
+
return this.runtime.runPromise(effect);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ContinueAsNewHandler,
|
|
3
|
+
CreateWorkflowSession,
|
|
4
|
+
SchedulerWaitHandler,
|
|
5
|
+
TaskExecutor,
|
|
6
|
+
WaitHandler,
|
|
7
|
+
WorkflowRuntime,
|
|
8
|
+
WorkflowSession,
|
|
9
|
+
} from "./workflow-types.ts";
|
|
10
|
+
import type { WorkflowDefinition } from "./WorkflowDefinition.ts";
|
|
11
|
+
import type { WorkflowGraphRenderer } from "./WorkflowGraphRenderer.ts";
|
|
12
|
+
|
|
13
|
+
export type WorkflowDriverOptions<Schema = unknown> = {
|
|
14
|
+
workflow: WorkflowDefinition<Schema>;
|
|
15
|
+
runtime: WorkflowRuntime;
|
|
16
|
+
renderer: WorkflowGraphRenderer;
|
|
17
|
+
session?: WorkflowSession;
|
|
18
|
+
createSession?: CreateWorkflowSession;
|
|
19
|
+
db?: unknown;
|
|
20
|
+
runId?: string;
|
|
21
|
+
rootDir?: string;
|
|
22
|
+
workflowPath?: string | null;
|
|
23
|
+
executeTask?: TaskExecutor;
|
|
24
|
+
onSchedulerWait?: SchedulerWaitHandler;
|
|
25
|
+
onWait?: WaitHandler;
|
|
26
|
+
continueAsNew?: ContinueAsNewHandler;
|
|
27
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ExtractOptions, WorkflowGraph } from "@smithers-orchestrator/graph";
|
|
2
|
+
import type { WorkflowElement } from "./WorkflowElement.ts";
|
|
3
|
+
|
|
4
|
+
export type WorkflowGraphRenderer = {
|
|
5
|
+
render(
|
|
6
|
+
element: WorkflowElement,
|
|
7
|
+
opts?: ExtractOptions,
|
|
8
|
+
): Promise<WorkflowGraph> | WorkflowGraph;
|
|
9
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WorkflowGraph } from "@smithers-orchestrator/graph/types";
|
|
2
|
+
import type { TaskCompletedEvent } from "./TaskCompletedEvent.ts";
|
|
3
|
+
import type { TaskFailedEvent } from "./TaskFailedEvent.ts";
|
|
4
|
+
|
|
5
|
+
export type WorkflowSession = {
|
|
6
|
+
submitGraph(graph: WorkflowGraph): unknown;
|
|
7
|
+
taskCompleted(event: TaskCompletedEvent): unknown;
|
|
8
|
+
taskFailed(event: TaskFailedEvent): unknown;
|
|
9
|
+
getNextDecision?(): unknown;
|
|
10
|
+
cancelRequested?(): unknown;
|
|
11
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Record<string, number>} [iterations]
|
|
3
|
+
* @returns {Set<string>}
|
|
4
|
+
*/
|
|
5
|
+
export function buildCurrentScopes(iterations) {
|
|
6
|
+
const scopes = new Set();
|
|
7
|
+
if (!iterations)
|
|
8
|
+
return scopes;
|
|
9
|
+
const unscopedIters = {};
|
|
10
|
+
for (const [ralphId, iter] of Object.entries(iterations)) {
|
|
11
|
+
if (!ralphId.includes("@@")) {
|
|
12
|
+
unscopedIters[ralphId] = iter;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
for (const ralphId of Object.keys(iterations)) {
|
|
16
|
+
const atIdx = ralphId.indexOf("@@");
|
|
17
|
+
if (atIdx < 0)
|
|
18
|
+
continue;
|
|
19
|
+
const suffix = ralphId.slice(atIdx + 2);
|
|
20
|
+
const rebuiltParts = [];
|
|
21
|
+
for (const part of suffix.split(",")) {
|
|
22
|
+
const eqIdx = part.indexOf("=");
|
|
23
|
+
if (eqIdx < 0)
|
|
24
|
+
continue;
|
|
25
|
+
const ancestorId = part.slice(0, eqIdx);
|
|
26
|
+
const currentIter = unscopedIters[ancestorId];
|
|
27
|
+
rebuiltParts.push(currentIter === undefined ? part : `${ancestorId}=${currentIter}`);
|
|
28
|
+
}
|
|
29
|
+
if (rebuiltParts.length > 0) {
|
|
30
|
+
scopes.add("@@" + rebuiltParts.join(","));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return scopes;
|
|
34
|
+
}
|