@osolmaz/pi-workflows 0.3.0 → 0.5.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 +11 -7
- package/dist/builtins/catalog.d.ts +2 -0
- package/dist/builtins/catalog.js +32 -0
- package/dist/builtins/catalog.js.map +1 -0
- package/dist/builtins/monitor.workflow.d.ts +2 -2
- package/dist/builtins/monitor.workflow.js +25 -25
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +70 -9
- package/dist/controllers/sqlite.js +209 -35
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/workflow-engine-scheduler.d.ts +2 -2
- package/dist/controllers/workflow-engine-scheduler.js +3 -1
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
- package/dist/extension/executor.d.ts +3 -0
- package/dist/extension/executor.js +11 -1
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/index.js +157 -108
- package/dist/extension/index.js.map +1 -1
- package/dist/host/runner.d.ts +1 -0
- package/dist/host/runner.js +68 -20
- package/dist/host/runner.js.map +1 -1
- package/dist/render/graph-render.js +3 -0
- package/dist/render/graph-render.js.map +1 -1
- package/dist/workflows/catalog.d.ts +43 -0
- package/dist/workflows/catalog.js +79 -0
- package/dist/workflows/catalog.js.map +1 -0
- package/dist/workflows/definition.d.ts +2 -1
- package/dist/workflows/definition.js +9 -1
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +6 -6
- package/dist/workflows/engine.js +93 -33
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -3
- package/dist/workflows/index.js +2 -2
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/loader.d.ts +18 -16
- package/dist/workflows/loader.js +58 -23
- package/dist/workflows/loader.js.map +1 -1
- package/dist/workflows/migrate-sources.d.ts +42 -0
- package/dist/workflows/migrate-sources.js +133 -0
- package/dist/workflows/migrate-sources.js.map +1 -0
- package/dist/workflows/schema.d.ts +2 -1
- package/dist/workflows/schema.js +14 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.js +5 -2
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +44 -5
- package/docs/development.md +5 -3
- package/docs/plans/2026-08-12-coordinated-workflow-timeouts-plan.md +74 -0
- package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +97 -0
- package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +95 -0
- package/docs/run-bundles.md +22 -4
- package/docs/workflows.md +57 -14
- package/package.json +1 -1
- package/src/builtins/catalog.ts +32 -0
- package/src/builtins/monitor.workflow.ts +33 -26
- package/src/controllers/index.ts +1 -0
- package/src/controllers/sqlite.ts +353 -43
- package/src/controllers/workflow-engine-scheduler.ts +5 -2
- package/src/extension/executor.ts +12 -1
- package/src/extension/index.ts +181 -140
- package/src/host/runner.ts +78 -20
- package/src/render/graph-render.ts +3 -0
- package/src/workflows/catalog.ts +135 -0
- package/src/workflows/definition.ts +11 -0
- package/src/workflows/engine.ts +128 -53
- package/src/workflows/index.ts +7 -0
- package/src/workflows/loader.ts +70 -26
- package/src/workflows/migrate-sources.ts +174 -0
- package/src/workflows/schema.ts +16 -1
- package/src/workflows/store.ts +5 -2
- package/src/workflows/types.ts +43 -4
package/src/host/runner.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { builtinWorkflowCatalog } from "../builtins/catalog.js";
|
|
4
5
|
import {
|
|
5
6
|
ControllerManager,
|
|
6
7
|
loadDiscoveredControllers,
|
|
@@ -17,8 +18,9 @@ import {
|
|
|
17
18
|
isClaimLostError,
|
|
18
19
|
WorkflowSourceChangedError,
|
|
19
20
|
} from "../workflows/errors.js";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
21
|
+
import { resolveWorkflowRef, resolveWorkflowSource } from "../workflows/loader.js";
|
|
22
|
+
import { migrateLegacyWorkflowSources } from "../workflows/migrate-sources.js";
|
|
23
|
+
import { WorkflowRunStore, readRunBundle } from "../workflows/store.js";
|
|
22
24
|
import type { WorkflowDefinition } from "../workflows/types.js";
|
|
23
25
|
import { HostProcessRegistry } from "./processes.js";
|
|
24
26
|
import { RpcStepExecutor } from "./rpc-executor.js";
|
|
@@ -59,6 +61,7 @@ export class WorkflowHost {
|
|
|
59
61
|
private manager: ControllerManager | null = null;
|
|
60
62
|
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
61
63
|
private readonly activeRuns = new Map<string, Promise<void>>();
|
|
64
|
+
private readonly migrationBlockedRuns = new Set<string>();
|
|
62
65
|
private readonly schedulerExecutors = new Map<WorkflowEngine, RpcStepExecutor>();
|
|
63
66
|
/** Runs whose resume refused (edited source); skipped until a host restart. */
|
|
64
67
|
private readonly skippedRuns = new Set<string>();
|
|
@@ -94,14 +97,27 @@ export class WorkflowHost {
|
|
|
94
97
|
this.log(`reaped ${reaped.length} orphaned headless session(s): ${reaped.join(", ")}`);
|
|
95
98
|
}
|
|
96
99
|
|
|
100
|
+
const migration = await migrateLegacyWorkflowSources({
|
|
101
|
+
catalog: builtinWorkflowCatalog,
|
|
102
|
+
store: this.childRunStore,
|
|
103
|
+
queue: this.store,
|
|
104
|
+
});
|
|
105
|
+
for (const blocked of migration.blocked) {
|
|
106
|
+
this.migrationBlockedRuns.add(blocked.runId);
|
|
107
|
+
this.log(`run ${blocked.runId} parked: ${blocked.reason}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
97
110
|
const definitions = await loadDiscoveredControllers({ cwd: this.options.cwd });
|
|
98
111
|
if (definitions.length > 0) {
|
|
99
112
|
const scheduler = new WorkflowEngineScheduler({
|
|
100
113
|
store: this.childRunStore,
|
|
101
114
|
resolveWorkflow: async (name) => {
|
|
102
|
-
const resolved = await resolveWorkflowRef(
|
|
103
|
-
|
|
104
|
-
|
|
115
|
+
const resolved = await resolveWorkflowRef(
|
|
116
|
+
name,
|
|
117
|
+
{ cwd: this.options.cwd },
|
|
118
|
+
builtinWorkflowCatalog,
|
|
119
|
+
);
|
|
120
|
+
return { workflow: resolved.definition, workflowSource: resolved.source };
|
|
105
121
|
},
|
|
106
122
|
createEngine: () => {
|
|
107
123
|
const executor = new RpcStepExecutor({
|
|
@@ -176,7 +192,7 @@ export class WorkflowHost {
|
|
|
176
192
|
runnerId: this.runnerId,
|
|
177
193
|
claimToken: randomUUID(),
|
|
178
194
|
leaseMs: RUN_CLAIM_LEASE_MS,
|
|
179
|
-
excludeRunIds: [...this.skippedRuns],
|
|
195
|
+
excludeRunIds: [...this.skippedRuns, ...this.migrationBlockedRuns],
|
|
180
196
|
});
|
|
181
197
|
} catch (error) {
|
|
182
198
|
// Store contention or corruption must not kill the host's loop.
|
|
@@ -195,13 +211,36 @@ export class WorkflowHost {
|
|
|
195
211
|
private async runClaimed(record: WorkflowRunQueueRecord): Promise<void> {
|
|
196
212
|
const claimToken = record.claimToken as string;
|
|
197
213
|
const runId = record.runId;
|
|
198
|
-
this.log(`resuming ${record.
|
|
214
|
+
this.log(`resuming ${record.workflowName} run ${runId}`);
|
|
199
215
|
let workflow: WorkflowDefinition;
|
|
200
|
-
let
|
|
216
|
+
let workflowSource: import("../workflows/types.js").WorkflowSource;
|
|
201
217
|
try {
|
|
202
|
-
|
|
203
|
-
|
|
218
|
+
const bundle = await readRunBundle(this.childRunStore.runDirFor(runId));
|
|
219
|
+
if (bundle?.state.workflowSource === undefined) {
|
|
220
|
+
throw new Error(`Workflow run ${runId} has no canonical workflow source`);
|
|
221
|
+
}
|
|
222
|
+
workflow = await resolveWorkflowSource(
|
|
223
|
+
bundle.state.workflowSource,
|
|
224
|
+
builtinWorkflowCatalog,
|
|
225
|
+
runId,
|
|
226
|
+
);
|
|
227
|
+
workflowSource = bundle.state.workflowSource;
|
|
204
228
|
} catch (error) {
|
|
229
|
+
if (error instanceof WorkflowSourceChangedError) {
|
|
230
|
+
try {
|
|
231
|
+
this.store.parkWorkflowRun({ runId, claimToken });
|
|
232
|
+
} catch {
|
|
233
|
+
// Best-effort.
|
|
234
|
+
}
|
|
235
|
+
this.skippedRuns.add(runId);
|
|
236
|
+
this.recordEvent(runId, record.workflowName, "parked", {
|
|
237
|
+
reason: "workflow source changed",
|
|
238
|
+
});
|
|
239
|
+
this.log(
|
|
240
|
+
`run ${runId} skipped: workflow source changed; install the matching package revision, then restart the host`,
|
|
241
|
+
);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
205
244
|
await this.failUnresumable(record, claimToken, errorMessage(error));
|
|
206
245
|
return;
|
|
207
246
|
}
|
|
@@ -230,7 +269,26 @@ export class WorkflowHost {
|
|
|
230
269
|
...(this.options.piArgs !== undefined ? { piArgs: this.options.piArgs } : {}),
|
|
231
270
|
...(this.options.env !== undefined ? { env: this.options.env } : {}),
|
|
232
271
|
});
|
|
233
|
-
const engine = new WorkflowEngine({
|
|
272
|
+
const engine = new WorkflowEngine({
|
|
273
|
+
executor,
|
|
274
|
+
store: fencedStore,
|
|
275
|
+
notificationSink: {
|
|
276
|
+
notify: (request) => {
|
|
277
|
+
fence();
|
|
278
|
+
if (record.originSessionId === null) {
|
|
279
|
+
throw new Error(`Workflow run ${request.runId} has no origin session`);
|
|
280
|
+
}
|
|
281
|
+
const notification = store.enqueueWorkflowNotification({
|
|
282
|
+
...request,
|
|
283
|
+
targetSessionId: record.originSessionId,
|
|
284
|
+
});
|
|
285
|
+
return {
|
|
286
|
+
notificationId: notification.notificationId,
|
|
287
|
+
targetSessionId: notification.targetSessionId,
|
|
288
|
+
};
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
});
|
|
234
292
|
const parkEngine = () => engine.park();
|
|
235
293
|
this.parkedEngines.push(parkEngine);
|
|
236
294
|
|
|
@@ -245,23 +303,23 @@ export class WorkflowHost {
|
|
|
245
303
|
}, RUN_CLAIM_RENEW_MS);
|
|
246
304
|
renewTimer.unref?.();
|
|
247
305
|
|
|
248
|
-
this.recordEvent(runId, record.
|
|
306
|
+
this.recordEvent(runId, record.workflowName, "resumed", { runnerId: this.runnerId });
|
|
249
307
|
try {
|
|
250
|
-
const result = await engine.resumeRun(workflow, runId, {
|
|
308
|
+
const result = await engine.resumeRun(workflow, runId, { workflowSource });
|
|
251
309
|
clearInterval(renewTimer);
|
|
252
310
|
if (result.state.status === "running") {
|
|
253
311
|
// Parked again mid-drain: leave it claimable for the next runner.
|
|
254
312
|
this.store.parkWorkflowRun({ runId, claimToken });
|
|
255
|
-
this.recordEvent(runId, record.
|
|
256
|
-
this.log(`parked ${record.
|
|
313
|
+
this.recordEvent(runId, record.workflowName, "parked", {});
|
|
314
|
+
this.log(`parked ${record.workflowName} run ${runId}`);
|
|
257
315
|
return;
|
|
258
316
|
}
|
|
259
317
|
this.store.completeWorkflowRun({ runId, claimToken });
|
|
260
|
-
this.recordEvent(runId, record.
|
|
318
|
+
this.recordEvent(runId, record.workflowName, result.state.status, {
|
|
261
319
|
...(result.state.error !== undefined ? { error: result.state.error } : {}),
|
|
262
320
|
...(result.state.waitingOn !== undefined ? { waitingOn: result.state.waitingOn } : {}),
|
|
263
321
|
});
|
|
264
|
-
this.log(`${record.
|
|
322
|
+
this.log(`${record.workflowName} run ${runId} ${result.state.status}`);
|
|
265
323
|
} catch (error) {
|
|
266
324
|
clearInterval(renewTimer);
|
|
267
325
|
if (isClaimLostError(error)) {
|
|
@@ -277,7 +335,7 @@ export class WorkflowHost {
|
|
|
277
335
|
// Best-effort.
|
|
278
336
|
}
|
|
279
337
|
this.skippedRuns.add(runId);
|
|
280
|
-
this.recordEvent(runId, record.
|
|
338
|
+
this.recordEvent(runId, record.workflowName, "parked", {
|
|
281
339
|
reason: "workflow source changed",
|
|
282
340
|
});
|
|
283
341
|
this.log(
|
|
@@ -327,9 +385,9 @@ export class WorkflowHost {
|
|
|
327
385
|
// no-op (the bundle was already waiting or completed), so the feed
|
|
328
386
|
// stays truthful for sessions syncing from it.
|
|
329
387
|
if (actualStatus !== undefined && actualStatus !== "failed") {
|
|
330
|
-
this.recordEvent(record.runId, record.
|
|
388
|
+
this.recordEvent(record.runId, record.workflowName, actualStatus, {});
|
|
331
389
|
} else {
|
|
332
|
-
this.recordEvent(record.runId, record.
|
|
390
|
+
this.recordEvent(record.runId, record.workflowName, "failed", { error: message });
|
|
333
391
|
}
|
|
334
392
|
this.log(`run ${record.runId} cannot resume: ${message}`);
|
|
335
393
|
}
|
|
@@ -79,6 +79,7 @@ const CARD_DYNAMIC_RESERVE = "↻ 100 ◷ 9999d 23h 59m 59s";
|
|
|
79
79
|
const NODE_TYPE_GLYPHS: Record<string, string> = {
|
|
80
80
|
agent: "●",
|
|
81
81
|
compute: "ƒ",
|
|
82
|
+
notify: "✉",
|
|
82
83
|
action: "⚙",
|
|
83
84
|
checkpoint: "◆",
|
|
84
85
|
};
|
|
@@ -87,6 +88,8 @@ function nodeTypeStyle(nodeType: string): CanvasStyle {
|
|
|
87
88
|
switch (nodeType) {
|
|
88
89
|
case "agent":
|
|
89
90
|
case "compute":
|
|
91
|
+
case "notify":
|
|
92
|
+
return nodeType === "notify" ? "action" : nodeType;
|
|
90
93
|
case "action":
|
|
91
94
|
case "checkpoint":
|
|
92
95
|
return nodeType;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { isWorkflowDefinition } from "./definition.js";
|
|
2
|
+
import { WorkflowSourceChangedError } from "./errors.js";
|
|
3
|
+
import type { WorkflowDefinition, WorkflowSource } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const BUILTIN_ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
|
|
6
|
+
const REVISION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
7
|
+
|
|
8
|
+
export type LegacyBuiltinSource = {
|
|
9
|
+
workflowHash: string;
|
|
10
|
+
revision: string;
|
|
11
|
+
pathSuffixes: readonly string[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type BuiltinWorkflowRegistration = {
|
|
15
|
+
id: string;
|
|
16
|
+
revision: string;
|
|
17
|
+
definition: WorkflowDefinition;
|
|
18
|
+
legacySources?: LegacyBuiltinSource[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type BuiltinWorkflowEntry = Readonly<{
|
|
22
|
+
id: string;
|
|
23
|
+
ref: string;
|
|
24
|
+
revision: string;
|
|
25
|
+
definition: WorkflowDefinition;
|
|
26
|
+
legacySources: readonly LegacyBuiltinSource[];
|
|
27
|
+
}>;
|
|
28
|
+
|
|
29
|
+
export type LegacyBuiltinMatch = {
|
|
30
|
+
entry: BuiltinWorkflowEntry;
|
|
31
|
+
revision: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Process-local catalog of package-provided workflow definitions. */
|
|
35
|
+
export class BuiltinWorkflowCatalog {
|
|
36
|
+
private readonly byId = new Map<string, BuiltinWorkflowEntry>();
|
|
37
|
+
private readonly byName = new Map<string, BuiltinWorkflowEntry>();
|
|
38
|
+
|
|
39
|
+
constructor(registrations: BuiltinWorkflowRegistration[]) {
|
|
40
|
+
for (const registration of registrations) {
|
|
41
|
+
if (!BUILTIN_ID_PATTERN.test(registration.id)) {
|
|
42
|
+
throw new Error(`Invalid built-in workflow id: ${JSON.stringify(registration.id)}`);
|
|
43
|
+
}
|
|
44
|
+
if (!REVISION_PATTERN.test(registration.revision)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Invalid built-in workflow revision: ${JSON.stringify(registration.revision)}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (!isWorkflowDefinition(registration.definition)) {
|
|
50
|
+
throw new Error(`Built-in workflow ${registration.id} is not defined with defineWorkflow`);
|
|
51
|
+
}
|
|
52
|
+
if (this.byId.has(registration.id)) {
|
|
53
|
+
throw new Error(`Duplicate built-in workflow id: ${registration.id}`);
|
|
54
|
+
}
|
|
55
|
+
if (this.byName.has(registration.definition.name)) {
|
|
56
|
+
throw new Error(`Duplicate built-in workflow name: ${registration.definition.name}`);
|
|
57
|
+
}
|
|
58
|
+
const entry: BuiltinWorkflowEntry = Object.freeze({
|
|
59
|
+
id: registration.id,
|
|
60
|
+
ref: `builtin:${registration.id}`,
|
|
61
|
+
revision: registration.revision,
|
|
62
|
+
definition: registration.definition,
|
|
63
|
+
legacySources: Object.freeze(
|
|
64
|
+
(registration.legacySources ?? []).map((legacy) =>
|
|
65
|
+
Object.freeze({ ...legacy, pathSuffixes: Object.freeze([...legacy.pathSuffixes]) }),
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
});
|
|
69
|
+
this.byId.set(entry.id, entry);
|
|
70
|
+
this.byName.set(entry.definition.name, entry);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
list(): BuiltinWorkflowEntry[] {
|
|
75
|
+
return [...this.byId.values()];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get(id: string): BuiltinWorkflowEntry | undefined {
|
|
79
|
+
return this.byId.get(id);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getByName(name: string): BuiltinWorkflowEntry | undefined {
|
|
83
|
+
return this.byName.get(name);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
resolve(
|
|
87
|
+
source: WorkflowSource,
|
|
88
|
+
runId = `builtin:${source.kind === "builtin" ? source.id : "unknown"}`,
|
|
89
|
+
): WorkflowDefinition {
|
|
90
|
+
if (source.kind !== "builtin") {
|
|
91
|
+
throw new Error("A file workflow source cannot be resolved by the built-in catalog");
|
|
92
|
+
}
|
|
93
|
+
const entry = this.byId.get(source.id);
|
|
94
|
+
if (entry === undefined) {
|
|
95
|
+
throw new Error(`Unknown built-in workflow: ${source.id}`);
|
|
96
|
+
}
|
|
97
|
+
if (entry.revision !== source.revision) {
|
|
98
|
+
throw new WorkflowSourceChangedError(runId);
|
|
99
|
+
}
|
|
100
|
+
return entry.definition;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
matchLegacy(options: {
|
|
104
|
+
workflowName: string;
|
|
105
|
+
workflowPath: string;
|
|
106
|
+
workflowHash: string;
|
|
107
|
+
}): LegacyBuiltinMatch | undefined {
|
|
108
|
+
const entry = this.legacyPathEntry(options);
|
|
109
|
+
if (entry === undefined) return undefined;
|
|
110
|
+
const workflowPath = options.workflowPath.replaceAll("\\", "/");
|
|
111
|
+
const legacy = entry.legacySources.find(
|
|
112
|
+
(candidate) =>
|
|
113
|
+
candidate.workflowHash === options.workflowHash &&
|
|
114
|
+
candidate.pathSuffixes.some((suffix) =>
|
|
115
|
+
workflowPath.endsWith(suffix.replaceAll("\\", "/")),
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
return legacy === undefined ? undefined : { entry, revision: legacy.revision };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Identify a registered old built-in path without accepting its revision. */
|
|
122
|
+
legacyPathEntry(options: {
|
|
123
|
+
workflowName: string;
|
|
124
|
+
workflowPath: string;
|
|
125
|
+
}): BuiltinWorkflowEntry | undefined {
|
|
126
|
+
const entry = this.byName.get(options.workflowName);
|
|
127
|
+
if (entry === undefined) return undefined;
|
|
128
|
+
const workflowPath = options.workflowPath.replaceAll("\\", "/");
|
|
129
|
+
return entry.legacySources.some((legacy) =>
|
|
130
|
+
legacy.pathSuffixes.some((suffix) => workflowPath.endsWith(suffix.replaceAll("\\", "/"))),
|
|
131
|
+
)
|
|
132
|
+
? entry
|
|
133
|
+
: undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
assertValidActionNode,
|
|
4
4
|
assertValidCheckpointNode,
|
|
5
5
|
assertValidComputeNode,
|
|
6
|
+
assertValidNotifyNode,
|
|
6
7
|
assertValidShellActionNode,
|
|
7
8
|
assertValidWorkflowDefinitionShape,
|
|
8
9
|
} from "./schema.js";
|
|
@@ -12,6 +13,7 @@ import type {
|
|
|
12
13
|
CheckpointNodeDefinition,
|
|
13
14
|
ComputeNodeDefinition,
|
|
14
15
|
FunctionActionNodeDefinition,
|
|
16
|
+
NotifyNodeDefinition,
|
|
15
17
|
ShellActionNodeDefinition,
|
|
16
18
|
WorkflowDefinition,
|
|
17
19
|
} from "./types.js";
|
|
@@ -62,6 +64,15 @@ export function compute(
|
|
|
62
64
|
return node;
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
export function notify(definition: Omit<NotifyNodeDefinition, "nodeType">): NotifyNodeDefinition {
|
|
68
|
+
const node: NotifyNodeDefinition = {
|
|
69
|
+
nodeType: "notify",
|
|
70
|
+
...definition,
|
|
71
|
+
};
|
|
72
|
+
assertValidNotifyNode(node);
|
|
73
|
+
return node;
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
export function action(
|
|
66
77
|
definition: Omit<FunctionActionNodeDefinition, "nodeType">,
|
|
67
78
|
): FunctionActionNodeDefinition;
|