@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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +80 -0
  3. package/src/ContinueAsNewHandler.ts +7 -0
  4. package/src/CreateWorkflowSession.ts +5 -0
  5. package/src/CreateWorkflowSessionOptions.ts +9 -0
  6. package/src/OutputAccessor.ts +20 -0
  7. package/src/OutputKey.ts +1 -0
  8. package/src/OutputSnapshot.ts +3 -0
  9. package/src/RunAuthContext.ts +6 -0
  10. package/src/RunOptions.ts +42 -0
  11. package/src/RunResult.ts +9 -0
  12. package/src/RunStatus.ts +9 -0
  13. package/src/SafeParser.ts +5 -0
  14. package/src/SchedulerWaitHandler.ts +6 -0
  15. package/src/SmithersCtx.js +195 -0
  16. package/src/SmithersCtxOptions.ts +14 -0
  17. package/src/SmithersRuntimeConfig.ts +3 -0
  18. package/src/SmithersTaskRuntime.ts +22 -0
  19. package/src/SpawnCaptureOptions.ts +12 -0
  20. package/src/SpawnCaptureResult.ts +5 -0
  21. package/src/TaskCompletedEvent.ts +5 -0
  22. package/src/TaskExecutor.ts +7 -0
  23. package/src/TaskExecutorContext.ts +7 -0
  24. package/src/TaskFailedEvent.ts +5 -0
  25. package/src/WaitHandler.ts +8 -0
  26. package/src/WorkflowDefinition.ts +16 -0
  27. package/src/WorkflowDriver.js +519 -0
  28. package/src/WorkflowDriverOptions.ts +27 -0
  29. package/src/WorkflowElement.ts +5 -0
  30. package/src/WorkflowGraphRenderer.ts +9 -0
  31. package/src/WorkflowRuntime.ts +3 -0
  32. package/src/WorkflowSession.ts +11 -0
  33. package/src/buildCurrentScopes.js +34 -0
  34. package/src/child-process.js +222 -0
  35. package/src/defaultTaskExecutor.js +28 -0
  36. package/src/filterRowsByNodeId.js +19 -0
  37. package/src/ignoreSyncError.js +16 -0
  38. package/src/index.d.ts +411 -0
  39. package/src/index.js +31 -0
  40. package/src/interop.js +1 -0
  41. package/src/normalizeInputRow.js +27 -0
  42. package/src/task-runtime.js +30 -0
  43. package/src/withAbort.js +43 -0
  44. package/src/withLogicalIterationShortcuts.js +46 -0
  45. 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,5 @@
1
+ export type WorkflowElement = {
2
+ type: unknown;
3
+ props: unknown;
4
+ key: string | number | null;
5
+ };
@@ -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,3 @@
1
+ export type WorkflowRuntime = {
2
+ runPromise<A>(effect: unknown): Promise<A>;
3
+ };
@@ -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
+ }