@particle-academy/fancy-flow 0.45.0 → 0.46.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 (77) hide show
  1. package/dist/{ConfigFieldRenderer-Dzy4f6rG.d.cts → ConfigFieldRenderer-Cyir8-B8.d.cts} +2 -2
  2. package/dist/{ConfigFieldRenderer-WqJr-td_.d.ts → ConfigFieldRenderer-iMkZj0VK.d.ts} +2 -2
  3. package/dist/{FlowViewer-BbbUJe2b.d.cts → FlowViewer-CY3_vHSg.d.cts} +1 -1
  4. package/dist/{FlowViewer-csuaTL5t.d.ts → FlowViewer-DGW6t-80.d.ts} +1 -1
  5. package/dist/{capabilities-Brn74URS.d.ts → capabilities-DMUhnyZD.d.ts} +1 -1
  6. package/dist/{capabilities-CYCeIx6I.d.cts → capabilities-Dmd26aAs.d.cts} +1 -1
  7. package/dist/chunk-BHM5HGXH.js +79 -0
  8. package/dist/chunk-BHM5HGXH.js.map +1 -0
  9. package/dist/{chunk-DHUFUDEX.js → chunk-HFUBVT7W.js} +3 -3
  10. package/dist/{chunk-DHUFUDEX.js.map → chunk-HFUBVT7W.js.map} +1 -1
  11. package/dist/{chunk-VLATLVGH.js → chunk-L43CWD63.js} +129 -5
  12. package/dist/chunk-L43CWD63.js.map +1 -0
  13. package/dist/{chunk-A6RFLGWV.js → chunk-MH4ZW4DJ.js} +3 -3
  14. package/dist/{chunk-A6RFLGWV.js.map → chunk-MH4ZW4DJ.js.map} +1 -1
  15. package/dist/{chunk-QZTGV3ZL.js → chunk-P5SYH6M6.js} +3 -3
  16. package/dist/{chunk-QZTGV3ZL.js.map → chunk-P5SYH6M6.js.map} +1 -1
  17. package/dist/{chunk-WBXGZVLB.js → chunk-RMU6YCIQ.js} +11 -5
  18. package/dist/chunk-RMU6YCIQ.js.map +1 -0
  19. package/dist/durable/index.d.cts +490 -0
  20. package/dist/durable/index.d.ts +490 -0
  21. package/dist/durable.cjs +927 -0
  22. package/dist/durable.cjs.map +1 -0
  23. package/dist/durable.js +552 -0
  24. package/dist/durable.js.map +1 -0
  25. package/dist/engine.cjs +128 -2
  26. package/dist/engine.cjs.map +1 -1
  27. package/dist/engine.d.cts +9 -7
  28. package/dist/engine.d.ts +9 -7
  29. package/dist/engine.js +4 -4
  30. package/dist/fields/react-fancy.d.cts +4 -3
  31. package/dist/fields/react-fancy.d.ts +4 -3
  32. package/dist/index.cjs +133 -3
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +12 -10
  35. package/dist/index.d.ts +12 -10
  36. package/dist/index.js +13 -88
  37. package/dist/index.js.map +1 -1
  38. package/dist/layout/index.d.cts +1 -1
  39. package/dist/layout/index.d.ts +1 -1
  40. package/dist/llm/prism.d.cts +2 -2
  41. package/dist/llm/prism.d.ts +2 -2
  42. package/dist/llm/vercel-ai.d.cts +2 -2
  43. package/dist/llm/vercel-ai.d.ts +2 -2
  44. package/dist/pause-9iT4tCEV.d.cts +96 -0
  45. package/dist/pause-9iT4tCEV.d.ts +96 -0
  46. package/dist/registry/index.d.cts +7 -6
  47. package/dist/registry/index.d.ts +7 -6
  48. package/dist/{registry-Budds73w.d.cts → registry-D_kyUZEf.d.cts} +1 -1
  49. package/dist/{registry-D0I20HJ3.d.ts → registry-LHB3MlR9.d.ts} +1 -1
  50. package/dist/registry.cjs +133 -3
  51. package/dist/registry.cjs.map +1 -1
  52. package/dist/registry.js +4 -4
  53. package/dist/{run-cohort-CV7EcwGG.d.ts → run-cohort-Cyeny2Uu.d.ts} +3 -34
  54. package/dist/{run-cohort-DitsaYgS.d.cts → run-cohort-DdPPowJ3.d.cts} +3 -34
  55. package/dist/run-flow-1KiXhy59.d.cts +55 -0
  56. package/dist/run-flow-CQvTDBYk.d.ts +55 -0
  57. package/dist/runtime/index.d.cts +5 -3
  58. package/dist/runtime/index.d.ts +5 -3
  59. package/dist/runtime.cjs +128 -2
  60. package/dist/runtime.cjs.map +1 -1
  61. package/dist/runtime.js +3 -3
  62. package/dist/schema/index.d.cts +1 -1
  63. package/dist/schema/index.d.ts +1 -1
  64. package/dist/screens.d.cts +2 -2
  65. package/dist/screens.d.ts +2 -2
  66. package/dist/screens.js +6 -6
  67. package/dist/{types-D6ZSKBly.d.ts → types--S2Kzx7w.d.cts} +3 -97
  68. package/dist/{types-nQOA8jpk.d.cts → types-ChkCRonF.d.ts} +3 -97
  69. package/dist/types-Jx1TwehV.d.cts +247 -0
  70. package/dist/types-Jx1TwehV.d.ts +247 -0
  71. package/dist/ux.d.cts +3 -2
  72. package/dist/ux.d.ts +3 -2
  73. package/package.json +12 -2
  74. package/dist/chunk-VLATLVGH.js.map +0 -1
  75. package/dist/chunk-WBXGZVLB.js.map +0 -1
  76. package/dist/types-CMSrWVYM.d.cts +0 -109
  77. package/dist/types-CMSrWVYM.d.ts +0 -109
@@ -0,0 +1,552 @@
1
+ import { truthy } from './chunk-BHM5HGXH.js';
2
+ import { pauseForHuman, decodePause } from './chunk-UEOE6B52.js';
3
+ import { runFlow, RunIdentity } from './chunk-L43CWD63.js';
4
+ import './chunk-TITD5W4Y.js';
5
+ import { getNodeKind, resolveKindId, kindIds } from './chunk-PVROYW7C.js';
6
+
7
+ // src/durable/state.ts
8
+ var NodeRunStatus = {
9
+ CLAIMED: "claimed",
10
+ COMPLETED: "completed",
11
+ SKIPPED: "skipped",
12
+ FAILED: "failed",
13
+ PAUSED: "paused"
14
+ };
15
+ var SETTLED = [
16
+ NodeRunStatus.COMPLETED,
17
+ NodeRunStatus.SKIPPED,
18
+ NodeRunStatus.FAILED
19
+ ];
20
+ var InMemoryClaimStore = class {
21
+ constructor() {
22
+ this.runs = /* @__PURE__ */ new Map();
23
+ }
24
+ claim(runKey, nodeId, owner) {
25
+ const run = this.run(runKey);
26
+ const existing = run.get(nodeId);
27
+ if (!existing) {
28
+ run.set(nodeId, {
29
+ status: NodeRunStatus.CLAIMED,
30
+ ports: [],
31
+ owner,
32
+ attempts: 1,
33
+ firstAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
34
+ });
35
+ return true;
36
+ }
37
+ const ownClaim = (existing.status === NodeRunStatus.CLAIMED || existing.status === NodeRunStatus.PAUSED) && existing.owner === owner;
38
+ if (ownClaim) {
39
+ existing.attempts += 1;
40
+ existing.status = NodeRunStatus.CLAIMED;
41
+ return true;
42
+ }
43
+ return false;
44
+ }
45
+ state(runKey) {
46
+ const out = {};
47
+ for (const [nodeId, entry] of this.run(runKey)) out[nodeId] = { ...entry, ports: [...entry.ports] };
48
+ return out;
49
+ }
50
+ complete(runKey, nodeId, output, ports) {
51
+ const entry = this.entry(runKey, nodeId);
52
+ entry.status = NodeRunStatus.COMPLETED;
53
+ entry.output = output;
54
+ entry.ports = [...ports];
55
+ entry.error = null;
56
+ }
57
+ skip(runKey, nodeId) {
58
+ const entry = this.entry(runKey, nodeId);
59
+ entry.status = NodeRunStatus.SKIPPED;
60
+ entry.ports = [];
61
+ }
62
+ fail(runKey, nodeId, error) {
63
+ const entry = this.entry(runKey, nodeId);
64
+ entry.status = NodeRunStatus.FAILED;
65
+ entry.error = error;
66
+ entry.ports = [];
67
+ }
68
+ pause(runKey, nodeId, reason) {
69
+ const entry = this.entry(runKey, nodeId);
70
+ entry.status = NodeRunStatus.PAUSED;
71
+ entry.error = reason;
72
+ entry.ports = [];
73
+ }
74
+ /**
75
+ * Drop a paused node's claim so a recorded answer can re-run it.
76
+ *
77
+ * Not part of the interface: resuming a human gate is the host's decision and
78
+ * its storage's business. Provided here because the in-memory store is also
79
+ * what the tests resume through.
80
+ */
81
+ release(runKey, nodeId) {
82
+ this.run(runKey).delete(nodeId);
83
+ }
84
+ run(runKey) {
85
+ let run = this.runs.get(runKey);
86
+ if (!run) {
87
+ run = /* @__PURE__ */ new Map();
88
+ this.runs.set(runKey, run);
89
+ }
90
+ return run;
91
+ }
92
+ /** The row for one node, created as CLAIMED if a driver never claimed it. */
93
+ entry(runKey, nodeId) {
94
+ const run = this.run(runKey);
95
+ let entry = run.get(nodeId);
96
+ if (!entry) {
97
+ entry = {
98
+ status: NodeRunStatus.CLAIMED,
99
+ ports: [],
100
+ attempts: 1,
101
+ firstAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
102
+ };
103
+ run.set(nodeId, entry);
104
+ }
105
+ return entry;
106
+ }
107
+ };
108
+ function isSettled(status) {
109
+ return status !== void 0 && SETTLED.includes(status);
110
+ }
111
+
112
+ // src/durable/frontier.ts
113
+ function isNote(type) {
114
+ if (!type) return false;
115
+ if (type === "note") return true;
116
+ const kind = getNodeKind(type);
117
+ return kind ? kindIds(kind).includes("note") || kind.category === "annotation" : false;
118
+ }
119
+ var Frontier = {
120
+ compute(graph, state) {
121
+ const incoming = /* @__PURE__ */ new Map();
122
+ for (const edge of graph.edges) {
123
+ const list = incoming.get(edge.target) ?? [];
124
+ list.push(edge);
125
+ incoming.set(edge.target, list);
126
+ }
127
+ const settled = /* @__PURE__ */ new Map();
128
+ const held = /* @__PURE__ */ new Set();
129
+ for (const [nodeId, entry] of Object.entries(state)) {
130
+ if (entry.status === NodeRunStatus.COMPLETED) settled.set(nodeId, entry.ports);
131
+ else if (isSettled(entry.status)) settled.set(nodeId, []);
132
+ else held.add(nodeId);
133
+ }
134
+ const ready = [];
135
+ const skipped = [];
136
+ let changed = true;
137
+ while (changed) {
138
+ changed = false;
139
+ for (const node of graph.nodes) {
140
+ const nodeId = node.id;
141
+ if (settled.has(nodeId) || held.has(nodeId) || ready.includes(nodeId)) continue;
142
+ const edges = incoming.get(nodeId) ?? [];
143
+ let blocked = false;
144
+ let active = false;
145
+ for (const edge of edges) {
146
+ if (!settled.has(edge.source)) {
147
+ blocked = true;
148
+ break;
149
+ }
150
+ if (settled.get(edge.source).includes(edge.sourceHandle ?? "out")) active = true;
151
+ }
152
+ if (blocked) continue;
153
+ if (edges.length > 0 && !active) {
154
+ settled.set(nodeId, []);
155
+ skipped.push(nodeId);
156
+ changed = true;
157
+ continue;
158
+ }
159
+ if (isNote(node.type) || getNodeKind(node.type ?? "")?.category === "layout") {
160
+ settled.set(nodeId, []);
161
+ skipped.push(nodeId);
162
+ changed = true;
163
+ continue;
164
+ }
165
+ ready.push(nodeId);
166
+ }
167
+ }
168
+ return { ready, skipped };
169
+ },
170
+ /** Has every node settled? The run is finished when it has. */
171
+ isComplete(graph, state) {
172
+ return graph.nodes.every((node) => isSettled(state[node.id]?.status));
173
+ },
174
+ /**
175
+ * Is any node still held by a worker, or parked for a person?
176
+ *
177
+ * An empty frontier means something different depending on this: with work in
178
+ * flight the run is simply waiting, and whichever job finishes will advance
179
+ * it. With nothing in flight and nodes still unsettled, the graph cannot
180
+ * progress at all — which is a stuck run, and must be reported rather than
181
+ * waited on.
182
+ */
183
+ hasWorkInFlight(state) {
184
+ return Object.values(state).some(
185
+ (entry) => entry.status === NodeRunStatus.CLAIMED || entry.status === NodeRunStatus.PAUSED
186
+ );
187
+ },
188
+ /** Persist the skip cascade so the next pass does not recompute it. */
189
+ async settleSkips(store, runKey, skipped) {
190
+ for (const nodeId of skipped) await store.skip(runKey, nodeId);
191
+ }
192
+ };
193
+
194
+ // src/durable/replay.ts
195
+ var BOUNDARY = "fancy-flow:node-boundary";
196
+ async function replayUpTo(graph, nodeId, executors, options = {}) {
197
+ const fenced = { ...executors };
198
+ for (const node of graph.nodes) {
199
+ if (node.id !== nodeId) {
200
+ fenced[node.id] = (ctx) => ctx.abort(BOUNDARY);
201
+ }
202
+ }
203
+ const ports = {};
204
+ const collect = (event) => {
205
+ var _a;
206
+ if (event.type === "node-output") {
207
+ (ports[_a = event.nodeId] ?? (ports[_a] = [])).push(event.portId);
208
+ }
209
+ options.onEvent?.(event);
210
+ };
211
+ const result = await runFlow(graph, fenced, collect, {
212
+ initialInputs: options.initialInputs ?? {},
213
+ resumeOutputs: options.resumeOutputs ?? {},
214
+ depth: options.depth ?? 0,
215
+ run: options.run
216
+ });
217
+ return {
218
+ result,
219
+ ports,
220
+ outputOf: (id) => result.outputs[id],
221
+ portsOf: (id) => ports[id] ?? []
222
+ };
223
+ }
224
+ function isBoundary(error) {
225
+ return error === BOUNDARY;
226
+ }
227
+
228
+ // src/durable/retry.ts
229
+ var UNSAFE_TO_REPLAY = "unsafe-to-replay";
230
+ var RetryPolicy = class _RetryPolicy {
231
+ constructor(options = {}) {
232
+ this.tries = options.tries ?? 1;
233
+ this.backoffSeconds = options.backoffSeconds ?? 0;
234
+ this.perKind = options.perKind ?? {};
235
+ }
236
+ triesFor(node) {
237
+ if (_RetryPolicy.isUnsafeToReplay(node)) return 1;
238
+ for (const id of idsFor(node)) {
239
+ if (id in this.perKind) return Math.max(1, Math.trunc(this.perKind[id]));
240
+ }
241
+ return Math.max(1, Math.trunc(this.tries));
242
+ }
243
+ backoffFor(node) {
244
+ if (_RetryPolicy.isUnsafeToReplay(node)) return 0;
245
+ return Math.max(0, this.backoffSeconds);
246
+ }
247
+ static isUnsafeToReplay(node) {
248
+ if (!node.type) return false;
249
+ return getNodeKind(node.type)?.sideEffects === UNSAFE_TO_REPLAY;
250
+ }
251
+ };
252
+ function idsFor(node) {
253
+ if (!node.type) return [];
254
+ const kind = getNodeKind(node.type);
255
+ const canonical = resolveKindId(node.type);
256
+ const ordered = [node.type, ...canonical ? [canonical] : [], ...kind ? kindIds(kind) : []];
257
+ return [...new Set(ordered)];
258
+ }
259
+
260
+ // src/durable/human.ts
261
+ var NotAwaitingHuman = class extends Error {
262
+ constructor(message) {
263
+ super(message);
264
+ this.name = "NotAwaitingHuman";
265
+ }
266
+ };
267
+ var Submissions = class {
268
+ constructor() {
269
+ this.answers = /* @__PURE__ */ new Map();
270
+ /** The node the run is currently parked on, if any. */
271
+ this.awaiting = null;
272
+ }
273
+ /**
274
+ * Record an answer for the node the run is parked on.
275
+ *
276
+ * Throws when the run is not waiting on that node. A queued answer for a node
277
+ * that never paused is a write nobody reads — and it looks, from the outside,
278
+ * exactly like a submission that worked.
279
+ */
280
+ record(nodeId, value) {
281
+ if (this.awaiting !== null && this.awaiting !== nodeId) {
282
+ throw new NotAwaitingHuman(
283
+ `This run is waiting on "${this.awaiting}", not "${nodeId}". Recording an answer for a node the run is not parked on would be stored and never read.`
284
+ );
285
+ }
286
+ if (this.awaiting === null) {
287
+ throw new NotAwaitingHuman(
288
+ `This run is not waiting for anyone, so an answer for "${nodeId}" has nothing to resume.`
289
+ );
290
+ }
291
+ this.answers.set(nodeId, value);
292
+ this.awaiting = null;
293
+ }
294
+ answered(nodeId) {
295
+ return this.answers.has(nodeId);
296
+ }
297
+ answer(nodeId) {
298
+ return this.answers.get(nodeId);
299
+ }
300
+ park(nodeId) {
301
+ this.awaiting = nodeId;
302
+ }
303
+ };
304
+ function option(ctx, key, fallback) {
305
+ const config = ctx.node.data?.config ?? {};
306
+ return config[key] ?? fallback;
307
+ }
308
+ function durableUserInput(submissions) {
309
+ return (ctx) => {
310
+ if (submissions.answered(ctx.node.id)) return submissions.answer(ctx.node.id);
311
+ if (option(ctx, "autoAnswerFromInput") === true) {
312
+ const values = ctx.inputs.values;
313
+ if (values !== void 0) return values;
314
+ }
315
+ submissions.park(ctx.node.id);
316
+ return pauseForHuman(ctx, "input", {
317
+ title: option(ctx, "title", "Need your input"),
318
+ fields: option(ctx, "fields", [])
319
+ });
320
+ };
321
+ }
322
+ function durableApproval(submissions) {
323
+ return (ctx) => {
324
+ const passthrough = ctx.inputs.in ?? ctx.inputs;
325
+ if (submissions.answered(ctx.node.id)) {
326
+ return { branch: truthy(submissions.answer(ctx.node.id)) ? "approved" : "denied", value: passthrough };
327
+ }
328
+ if (option(ctx, "autoAnswerFromInput") === true) {
329
+ const decision = ctx.inputs.approved;
330
+ if (decision !== void 0) {
331
+ return { branch: truthy(decision) ? "approved" : "denied", value: passthrough };
332
+ }
333
+ }
334
+ submissions.park(ctx.node.id);
335
+ return pauseForHuman(ctx, "approval", {
336
+ title: option(ctx, "title", "Approve action"),
337
+ description: option(ctx, "description")
338
+ });
339
+ };
340
+ }
341
+
342
+ // src/durable/coordinator.ts
343
+ var Coordinator = class {
344
+ constructor(options) {
345
+ this.graph = options.graph;
346
+ this.executors = options.executors;
347
+ this.run = RunIdentity.from(options.run);
348
+ this.store = options.store ?? new InMemoryClaimStore();
349
+ this.initialInputs = options.initialInputs ?? {};
350
+ this.retry = options.retry ?? new RetryPolicy();
351
+ this.onEvent = options.onEvent;
352
+ }
353
+ get runKey() {
354
+ return this.run.runKey;
355
+ }
356
+ // -- the two operations ----------------------------------------------
357
+ /**
358
+ * Which nodes may be dispatched right now.
359
+ *
360
+ * Also settles the skip cascade, because a skip is a decision the frontier
361
+ * just made and a second caller must not make it again.
362
+ */
363
+ async advance() {
364
+ const frontier = Frontier.compute(this.graph, await this.store.state(this.runKey));
365
+ await Frontier.settleSkips(this.store, this.runKey, frontier.skipped);
366
+ return frontier.ready;
367
+ }
368
+ /**
369
+ * Claim, execute and checkpoint one node.
370
+ *
371
+ * The claim is taken FIRST. Two workers racing for the same node produce one
372
+ * execution and one no-op, and the loser learns that from the store rather
373
+ * than from a duplicate side effect.
374
+ *
375
+ * `owner` is the token that lets a job's own retry re-enter its claim instead
376
+ * of deadlocking against the row it wrote itself — pass the SAME token across
377
+ * a job's attempts.
378
+ */
379
+ async runNode(nodeId, owner) {
380
+ const token = owner ?? cryptoRandom();
381
+ if (!await this.store.claim(this.runKey, nodeId, token)) {
382
+ return { nodeId, status: "not-claimed", claimed: false, attempt: 0 };
383
+ }
384
+ const row = (await this.store.state(this.runKey))[nodeId];
385
+ const identity = this.identityFor(row);
386
+ const replay = await replayUpTo(this.graph, nodeId, this.executors, {
387
+ resumeOutputs: await this.completedOutputs(),
388
+ initialInputs: this.initialInputs,
389
+ onEvent: this.forward(nodeId),
390
+ run: identity
391
+ });
392
+ const result = replay.result;
393
+ const attempt = identity.attempt;
394
+ if (Object.prototype.hasOwnProperty.call(result.outputs, nodeId)) {
395
+ const output = result.outputs[nodeId];
396
+ const ports = replay.portsOf(nodeId);
397
+ await this.store.complete(this.runKey, nodeId, output, ports);
398
+ return { nodeId, status: "completed", output, ports, claimed: true, attempt };
399
+ }
400
+ const pause = decodePause(result.error);
401
+ if (pause && pause.nodeId === nodeId) {
402
+ await this.store.pause(this.runKey, nodeId, result.error ?? "");
403
+ return { nodeId, status: "paused", pause, claimed: true, attempt };
404
+ }
405
+ if (isBoundary(result.error)) {
406
+ await this.store.skip(this.runKey, nodeId);
407
+ return {
408
+ nodeId,
409
+ status: "skipped",
410
+ error: "replay stopped before reaching this node",
411
+ claimed: true,
412
+ attempt
413
+ };
414
+ }
415
+ const error = result.error ?? `node ${nodeId} produced no output and no error`;
416
+ const node = this.graph.nodes.find((n) => n.id === nodeId);
417
+ const tries = node ? this.retry.triesFor(node) : 1;
418
+ if (attempt < tries) {
419
+ return { nodeId, status: "failed", error, claimed: true, attempt, retryable: true };
420
+ }
421
+ await this.store.fail(this.runKey, nodeId, error);
422
+ return { nodeId, status: "failed", error, claimed: true, attempt, retryable: false };
423
+ }
424
+ // -- an in-process driver over the two ------------------------------
425
+ /**
426
+ * Drive the graph here, in this process, one node at a time.
427
+ *
428
+ * Every checkpoint is written exactly as a queued run writes it, so a crash
429
+ * mid-loop resumes from the same place a crashed worker would.
430
+ *
431
+ * Retries honour {@link RetryPolicy}: a node declaring `unsafe-to-replay`
432
+ * gets one attempt whatever the policy says, and the retry re-enters the same
433
+ * claim with the same owner token — so the step key it derives is unchanged,
434
+ * which is what makes the retry idempotent rather than duplicative.
435
+ */
436
+ async runToCompletion(maxPasses = 1e4) {
437
+ for (let pass = 0; pass < maxPasses; pass++) {
438
+ const ready = await this.advance();
439
+ if (ready.length === 0) break;
440
+ for (const nodeId of ready) {
441
+ const outcome = await this.runNodeWithRetries(nodeId);
442
+ if (outcome.status === "paused") {
443
+ return {
444
+ ok: false,
445
+ outputs: await this.outputs(),
446
+ pause: outcome.pause,
447
+ paused: true
448
+ };
449
+ }
450
+ if (outcome.status === "failed") {
451
+ return { ok: false, outputs: await this.outputs(), error: outcome.error, paused: false };
452
+ }
453
+ }
454
+ }
455
+ const state = await this.store.state(this.runKey);
456
+ if (!Frontier.isComplete(this.graph, state)) {
457
+ if (Frontier.hasWorkInFlight(state)) {
458
+ return {
459
+ ok: false,
460
+ outputs: await this.outputs(),
461
+ error: "the run is waiting on work held elsewhere",
462
+ paused: false
463
+ };
464
+ }
465
+ const unsettled = this.graph.nodes.filter((n) => !["completed", "skipped", "failed"].includes(state[n.id]?.status ?? "")).map((n) => n.id);
466
+ return {
467
+ ok: false,
468
+ outputs: await this.outputs(),
469
+ error: `the run cannot progress; unsettled nodes: ${unsettled.join(", ")}`,
470
+ paused: false
471
+ };
472
+ }
473
+ const failed = Object.values(state).filter((e) => e.status === NodeRunStatus.FAILED).map((e) => e.error ?? "node failed");
474
+ return {
475
+ ok: failed.length === 0,
476
+ outputs: await this.outputs(),
477
+ error: failed[0],
478
+ paused: false
479
+ };
480
+ }
481
+ /**
482
+ * Checkpointed outputs, in the graph's own node order.
483
+ *
484
+ * Ordered by the graph rather than by completion so two runs of the same
485
+ * workflow produce comparable output maps even when nodes finished in a
486
+ * different order.
487
+ */
488
+ async outputs() {
489
+ const state = await this.store.state(this.runKey);
490
+ const out = {};
491
+ for (const node of this.graph.nodes) {
492
+ if (state[node.id]?.status === NodeRunStatus.COMPLETED) out[node.id] = state[node.id].output;
493
+ }
494
+ return out;
495
+ }
496
+ /** The checkpointed run, in the shape a single-process run returns. */
497
+ async asRunResult() {
498
+ const outcome = await this.runToCompletion();
499
+ return outcome.error === void 0 ? { ok: outcome.ok, outputs: outcome.outputs } : { ok: outcome.ok, outputs: outcome.outputs, error: outcome.error };
500
+ }
501
+ // -- internals -------------------------------------------------------
502
+ async runNodeWithRetries(nodeId) {
503
+ const owner = cryptoRandom();
504
+ let outcome = await this.runNode(nodeId, owner);
505
+ while (outcome.retryable) {
506
+ outcome = await this.runNode(nodeId, owner);
507
+ }
508
+ return outcome;
509
+ }
510
+ /**
511
+ * The identity handed to the node about to execute.
512
+ *
513
+ * `attempt` and `firstAttemptAt` come off the CLAIM ROW, so they describe
514
+ * this step rather than the run — which is what makes the retry window check
515
+ * exact instead of conservative.
516
+ */
517
+ identityFor(row) {
518
+ if (!row) return this.run;
519
+ return this.run.withAttempt(row.attempts, row.firstAttemptAt);
520
+ }
521
+ async completedOutputs() {
522
+ const state = await this.store.state(this.runKey);
523
+ const out = {};
524
+ for (const [nodeId, entry] of Object.entries(state)) {
525
+ if (entry.status === NodeRunStatus.COMPLETED) out[nodeId] = entry.output;
526
+ }
527
+ return out;
528
+ }
529
+ /**
530
+ * Forward only the events the target node produced.
531
+ *
532
+ * A replay re-emits the whole completed prefix. Passing that through would
533
+ * show a consumer every node running again on every job — the run feed would
534
+ * report a 20-node workflow as 200 status changes.
535
+ */
536
+ forward(nodeId) {
537
+ const sink = this.onEvent;
538
+ if (!sink) return void 0;
539
+ return (event) => {
540
+ const id = "nodeId" in event ? event.nodeId : void 0;
541
+ if (id === void 0 || id === nodeId) sink(event);
542
+ };
543
+ }
544
+ };
545
+ function cryptoRandom() {
546
+ const g = globalThis;
547
+ return g.crypto?.randomUUID?.() ?? `own_${Math.random().toString(36).slice(2)}${Date.now()}`;
548
+ }
549
+
550
+ export { BOUNDARY, Coordinator, Frontier, InMemoryClaimStore, NodeRunStatus, NotAwaitingHuman, RetryPolicy, SETTLED, Submissions, UNSAFE_TO_REPLAY, durableApproval, durableUserInput, isBoundary, isSettled, replayUpTo };
551
+ //# sourceMappingURL=durable.js.map
552
+ //# sourceMappingURL=durable.js.map