@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,490 @@
1
+ import { F as FlowGraph, R as RunEvent, b as RunIdentity, E as ExecutorRegistry, a as FlowNode, N as NodeExecutor, c as RunIdentityJson } from '../types-Jx1TwehV.js';
2
+ import { a as RunResult } from '../run-flow-CQvTDBYk.js';
3
+ import { b as PauseSignal } from '../pause-9iT4tCEV.js';
4
+ import '@xyflow/react';
5
+
6
+ /**
7
+ * What a durable run remembers, and the seam a real database plugs into.
8
+ *
9
+ * A durable run is bookkeeping plus one hard requirement: **the claim is a
10
+ * unique constraint, not a check.** Two workers racing for the same node must
11
+ * produce a no-op, not a double run, and only the storage layer can promise
12
+ * that. So {@link NodeClaimStore} is an interface with exactly the operations a
13
+ * driver needs, and an adapter implements it over `INSERT … ON CONFLICT DO
14
+ * NOTHING` (or its dialect's spelling).
15
+ *
16
+ * {@link InMemoryClaimStore} is the reference implementation and is genuinely
17
+ * useful: it makes the whole per-node driver testable, and it is correct for a
18
+ * single-process durable run.
19
+ *
20
+ * This is a port of `fancy_flow.durable.state`, which is itself the Python twin
21
+ * of `fancy-flow-php`'s `NodeClaims` + `workflow_run_nodes`. Three runtimes,
22
+ * one design — see `.ai/plans/fancy-flow-run-identity-and-steps.md`.
23
+ */
24
+ /** Where one node of one run has got to. */
25
+ declare const NodeRunStatus: {
26
+ readonly CLAIMED: "claimed";
27
+ readonly COMPLETED: "completed";
28
+ readonly SKIPPED: "skipped";
29
+ readonly FAILED: "failed";
30
+ readonly PAUSED: "paused";
31
+ };
32
+ type NodeRunStatusValue = (typeof NodeRunStatus)[keyof typeof NodeRunStatus];
33
+ /**
34
+ * A node the frontier may treat as decided.
35
+ *
36
+ * A FAILED node is settled too — it will never publish, so its successors skip
37
+ * rather than wait forever.
38
+ */
39
+ declare const SETTLED: readonly NodeRunStatusValue[];
40
+ /**
41
+ * One node's row.
42
+ *
43
+ * `ports` are the ports the engine's own `node-output` events reported. They
44
+ * are STORED, never recomputed: a second copy of the routing table would agree
45
+ * for a year and then disagree on one branch.
46
+ */
47
+ type NodeState = {
48
+ status: NodeRunStatusValue;
49
+ ports: readonly string[];
50
+ output?: unknown;
51
+ error?: string | null;
52
+ owner?: string | null;
53
+ /** 1-based, incremented when an owner re-enters its own claim. */
54
+ attempts: number;
55
+ /** ISO-8601 UTC of the FIRST claim. Never updated — it is the retry clock. */
56
+ firstAttemptAt: string;
57
+ };
58
+ /**
59
+ * The persistence a per-node driver needs.
60
+ *
61
+ * Six operations. An adapter over Postgres, SQLite or Redis implements these
62
+ * and nothing else; every rule about WHICH node may run lives in `frontier.ts`,
63
+ * which reads only {@link state}.
64
+ */
65
+ interface NodeClaimStore {
66
+ /**
67
+ * Take exclusive ownership of one node of one run.
68
+ *
69
+ * MUST be atomic against concurrent callers, and MUST return `true` for a
70
+ * caller re-entering its OWN claim — that is what lets a job's retry resume
71
+ * instead of deadlocking against the row it wrote itself.
72
+ */
73
+ claim(runKey: string, nodeId: string, owner: string): boolean | Promise<boolean>;
74
+ state(runKey: string): Record<string, NodeState> | Promise<Record<string, NodeState>>;
75
+ complete(runKey: string, nodeId: string, output: unknown, ports: readonly string[]): void | Promise<void>;
76
+ skip(runKey: string, nodeId: string): void | Promise<void>;
77
+ fail(runKey: string, nodeId: string, error: string): void | Promise<void>;
78
+ pause(runKey: string, nodeId: string, reason: string): void | Promise<void>;
79
+ }
80
+ /**
81
+ * A correct, single-process {@link NodeClaimStore}.
82
+ *
83
+ * JavaScript's single-threaded event loop makes the claim atomic for free —
84
+ * there is no interleaving point inside a synchronous method. It is NOT durable
85
+ * across a restart, which is the honest limit: use it for tests, for a CLI run,
86
+ * and for a worker that genuinely owns the whole run.
87
+ */
88
+ declare class InMemoryClaimStore implements NodeClaimStore {
89
+ private readonly runs;
90
+ claim(runKey: string, nodeId: string, owner: string): boolean;
91
+ state(runKey: string): Record<string, NodeState>;
92
+ complete(runKey: string, nodeId: string, output: unknown, ports: readonly string[]): void;
93
+ skip(runKey: string, nodeId: string): void;
94
+ fail(runKey: string, nodeId: string, error: string): void;
95
+ pause(runKey: string, nodeId: string, reason: string): void;
96
+ /**
97
+ * Drop a paused node's claim so a recorded answer can re-run it.
98
+ *
99
+ * Not part of the interface: resuming a human gate is the host's decision and
100
+ * its storage's business. Provided here because the in-memory store is also
101
+ * what the tests resume through.
102
+ */
103
+ release(runKey: string, nodeId: string): void;
104
+ private run;
105
+ /** The row for one node, created as CLAIMED if a driver never claimed it. */
106
+ private entry;
107
+ }
108
+ declare function isSettled(status: string | undefined): boolean;
109
+
110
+ /**
111
+ * Which nodes can run RIGHT NOW, given what has already settled.
112
+ *
113
+ * ## Why this is not a second engine
114
+ *
115
+ * `runFlow` walks a Kahn topological order and, at each node, runs it when at
116
+ * least one incoming edge is active. That is a total order because one process
117
+ * executes every node. Split the graph across jobs and the same rule has to be
118
+ * asked the other way round — not "what is next" but "what is unblocked" —
119
+ * which is this module.
120
+ *
121
+ * The rule is the engine's, restated:
122
+ *
123
+ * - every direct predecessor has SETTLED (in topological order the engine has
124
+ * already settled all of them by the time it reaches a node);
125
+ * - and either the node has no incoming edges, or at least one incoming edge is
126
+ * ACTIVE — its source completed and published on that edge's source handle.
127
+ *
128
+ * A node whose predecessors have all settled with no active edge is what the
129
+ * engine reports as `idle/skipped`. Skipping SETTLES it, which can in turn
130
+ * unblock — or skip — its own successors, so the pass repeats until nothing
131
+ * changes. That cascade is how a dead branch collapses without leaving the run
132
+ * stuck.
133
+ *
134
+ * ## The one thing it does NOT decide
135
+ *
136
+ * Which ports a result activated. Those rules (`__port`, `branch`, declared
137
+ * outputs, the kind's ports, the `out` fallback) live in the engine and stay
138
+ * there: this reads the ports back off the `node-output` events the engine
139
+ * emitted when the node ran, stored on the claim row.
140
+ */
141
+
142
+ type FrontierResult = {
143
+ ready: string[];
144
+ skipped: string[];
145
+ };
146
+ declare const Frontier: {
147
+ compute(graph: FlowGraph, state: Record<string, NodeState>): FrontierResult;
148
+ /** Has every node settled? The run is finished when it has. */
149
+ isComplete(graph: FlowGraph, state: Record<string, NodeState>): boolean;
150
+ /**
151
+ * Is any node still held by a worker, or parked for a person?
152
+ *
153
+ * An empty frontier means something different depending on this: with work in
154
+ * flight the run is simply waiting, and whichever job finishes will advance
155
+ * it. With nothing in flight and nodes still unsettled, the graph cannot
156
+ * progress at all — which is a stuck run, and must be reported rather than
157
+ * waited on.
158
+ */
159
+ hasWorkInFlight(state: Record<string, NodeState>): boolean;
160
+ /** Persist the skip cascade so the next pass does not recompute it. */
161
+ settleSkips(store: NodeClaimStore, runKey: string, skipped: readonly string[]): Promise<void>;
162
+ };
163
+
164
+ /**
165
+ * Run ONE node of a graph — through the real engine, not around it.
166
+ *
167
+ * ## The problem this solves
168
+ *
169
+ * A per-node driver has to hand a node exactly the inputs it would have
170
+ * received mid-run: the right values, on the right target handles, from the
171
+ * right *active* edges. Those rules are the engine's (`collectInputs`,
172
+ * `activatedPorts`, the merge-after-decision contract, the `out` fallbacks),
173
+ * and they are the reason the three runtimes agree. Re-implementing them here
174
+ * would be a second engine wearing a driver's clothes, and the two would drift.
175
+ *
176
+ * ## What it does instead
177
+ *
178
+ * It replays the graph with `runFlow` untouched:
179
+ *
180
+ * - every node already completed is fed back as `resumeOutputs`, so the engine
181
+ * republishes it on the same ports and routes exactly as it did the first
182
+ * time;
183
+ * - every node EXCEPT the target is bound, by node id, to a boundary executor
184
+ * that aborts — and a node-id binding outranks every kind binding and the
185
+ * `*` fallback, so the fence holds whatever a host registered;
186
+ * - so the engine walks its own topological order, skips its own dead branches,
187
+ * collects the target's inputs its own way, runs the target — and stops at
188
+ * the next thing it would have run.
189
+ *
190
+ * The target's output is `result.outputs[nodeId]`, and the ports it activated
191
+ * arrive as the engine's own `node-output` events. Nothing about routing is
192
+ * recomputed here.
193
+ *
194
+ * ## The cost, stated plainly
195
+ *
196
+ * Replaying the completed prefix is O(nodes) per node, so a run is O(nodes²) in
197
+ * bookkeeping. The republish executes nothing — it re-publishes stored values —
198
+ * so for the graph sizes workflows actually have this is noise next to a single
199
+ * queue round trip. It buys exact fidelity to the engine, which is not
200
+ * negotiable, and one implementation of the routing rules instead of two.
201
+ */
202
+
203
+ /**
204
+ * The abort reason the boundary executor uses.
205
+ *
206
+ * Not a failure: it is the engine telling us it reached a node this job is not
207
+ * responsible for.
208
+ */
209
+ declare const BOUNDARY = "fancy-flow:node-boundary";
210
+ type ReplayResult = {
211
+ result: RunResult;
212
+ /** node id -> the ports its output activated, from the engine's own events. */
213
+ ports: Record<string, string[]>;
214
+ outputOf(nodeId: string): unknown;
215
+ portsOf(nodeId: string): string[];
216
+ };
217
+ type ReplayOptions = {
218
+ resumeOutputs?: Record<string, unknown>;
219
+ initialInputs?: Record<string, Record<string, unknown>>;
220
+ onEvent?: (event: RunEvent) => void;
221
+ depth?: number;
222
+ run?: RunIdentity;
223
+ };
224
+ /**
225
+ * Replay `graph` up to and through `nodeId`.
226
+ *
227
+ * Pass `nodeId = null` to PROBE: every node is a boundary, so nothing executes
228
+ * and the engine reports only what it can determine structurally — a cycle, and
229
+ * the ports each resumed output republishes on.
230
+ */
231
+ declare function replayUpTo(graph: FlowGraph, nodeId: string | null, executors: ExecutorRegistry, options?: ReplayOptions): Promise<ReplayResult>;
232
+ /** True when a replay ended because it reached a node it does not own. */
233
+ declare function isBoundary(error: string | null | undefined): boolean;
234
+
235
+ /**
236
+ * How many times a single node may be attempted.
237
+ *
238
+ * ## Why this cannot be one number
239
+ *
240
+ * A run-wide `tries` setting forces every workflow to pick between two bad
241
+ * answers. At 1, a single flaky LLM or HTTP call takes the whole run down.
242
+ * Above 1, the retry replays from the last checkpoint and everything already
243
+ * done runs again — including the nodes that must not: `git_pr_open` opens a
244
+ * second pull request.
245
+ *
246
+ * Per-node jobs make the question per node, which is where it always belonged.
247
+ * A node declaring `sideEffects: unsafe-to-replay` is pinned to ONE attempt and
248
+ * no backoff. Everything else takes the configured tries, or a per-kind
249
+ * override.
250
+ *
251
+ * Undeclared side effects are treated as the configured default rather than
252
+ * assumed safe: this decides retries, and inventing a safety claim on a node
253
+ * author's behalf is how a retry loop ends up posting the same webhook twice.
254
+ */
255
+
256
+ /** A node that is not safe to run twice. Same vocabulary as the node manifest. */
257
+ declare const UNSAFE_TO_REPLAY = "unsafe-to-replay";
258
+ type RetryPolicyOptions = {
259
+ tries?: number;
260
+ backoffSeconds?: number;
261
+ /** kind id -> tries. Keyed by any spelling; every id is checked. */
262
+ perKind?: Record<string, number>;
263
+ };
264
+ declare class RetryPolicy {
265
+ readonly tries: number;
266
+ readonly backoffSeconds: number;
267
+ readonly perKind: Record<string, number>;
268
+ constructor(options?: RetryPolicyOptions);
269
+ triesFor(node: FlowNode): number;
270
+ backoffFor(node: FlowNode): number;
271
+ static isUnsafeToReplay(node: FlowNode): boolean;
272
+ }
273
+
274
+ /**
275
+ * Human gates that cannot be walked past — and that hold no worker.
276
+ *
277
+ * ## The directive these implement
278
+ *
279
+ * A node that waits on a person must not occupy a worker, a connection or a
280
+ * process while it waits. So the job for a human gate does its work by
281
+ * **emitting the request** and then **finishing**: the run is persisted as
282
+ * parked, the job returns, the worker takes the next thing off the queue. When
283
+ * the person answers, *that* enqueues the continuation.
284
+ *
285
+ * Mechanically the gate aborts with an encoded pause, which is a *return*, not
286
+ * a block. Nothing in this file sleeps, polls or awaits a person.
287
+ *
288
+ * ## Fail closed, and why
289
+ *
290
+ * A gate pauses because it **is** a human node, not because its input port
291
+ * happens to be empty. This is not a preference; it is a fix. Both peer
292
+ * runtimes once decided whether to pause by reading their own input, so a
293
+ * pre-filled `values` or `approved` value ran the flow straight past the person
294
+ * it was waiting for — silently, with the run reporting success.
295
+ *
296
+ * Restoring the old behaviour is possible, explicit, and per node:
297
+ * `autoAnswerFromInput`. Turn it on for a step that is a form when a human is
298
+ * present and a pass-through when an upstream node already produced the answer.
299
+ * On an approval node, weigh it harder: it means the graph, not a person, can
300
+ * approve.
301
+ *
302
+ * ## The other half of the fix
303
+ *
304
+ * Recording an answer for a node the run is not parked on THROWS rather than
305
+ * queueing a write nobody reads. See {@link Submissions.record}.
306
+ */
307
+
308
+ /** An answer was recorded for a node the run is not waiting on. */
309
+ declare class NotAwaitingHuman extends Error {
310
+ constructor(message: string);
311
+ }
312
+ /**
313
+ * Answers recorded for human gates, keyed by node id.
314
+ *
315
+ * Deliberately separate from the run's inputs. Keeping them in one bag is
316
+ * precisely what let a pre-filled input satisfy a gate.
317
+ */
318
+ declare class Submissions {
319
+ private readonly answers;
320
+ /** The node the run is currently parked on, if any. */
321
+ awaiting: string | null;
322
+ /**
323
+ * Record an answer for the node the run is parked on.
324
+ *
325
+ * Throws when the run is not waiting on that node. A queued answer for a node
326
+ * that never paused is a write nobody reads — and it looks, from the outside,
327
+ * exactly like a submission that worked.
328
+ */
329
+ record(nodeId: string, value: unknown): void;
330
+ answered(nodeId: string): boolean;
331
+ answer(nodeId: string): unknown;
332
+ park(nodeId: string): void;
333
+ }
334
+ /** `user_input` — pauses until a submission for THIS node is recorded. */
335
+ declare function durableUserInput(submissions: Submissions): NodeExecutor;
336
+ /** `human_approval` — pauses until a decision for THIS node is recorded. */
337
+ declare function durableApproval(submissions: Submissions): NodeExecutor;
338
+
339
+ /**
340
+ * The per-node driver, with the queue left out.
341
+ *
342
+ * This is the whole of "how a queued run branches", minus the transport. It
343
+ * owns two operations and nothing else:
344
+ *
345
+ * `advance()`
346
+ * Ask the frontier what is unblocked, settle the skip cascade, and report the
347
+ * ready node ids. A queue adapter dispatches one job per id.
348
+ *
349
+ * `runNode()`
350
+ * Claim one node, replay the graph through the real engine fenced to that
351
+ * node, and checkpoint the output plus the ports the ENGINE said it
352
+ * activated.
353
+ *
354
+ * Everything a queue library would add — enqueue, retry scheduling, worker
355
+ * lifecycle — sits outside. That separation is the point: it makes the subtle
356
+ * part (which node may run, and with what inputs) testable in-process, with no
357
+ * broker, and identical under every adapter. **A queue adapter therefore
358
+ * contains no workflow logic at all.** That is the test: if an adapter needs to
359
+ * know what a port is, the seam is in the wrong place.
360
+ *
361
+ * `runToCompletion` drives both in one process. It is a real durable runner,
362
+ * not a toy: with a persistent `NodeClaimStore` it survives a crash exactly as
363
+ * a queued run does, because the crash-resume behaviour lives in the
364
+ * checkpoints rather than in the loop.
365
+ *
366
+ * ## No worker waits on a person
367
+ *
368
+ * A human gate returns `paused`. `runToCompletion` returns immediately when it
369
+ * sees one — it does not spin, sleep or poll. The run is parked in the store,
370
+ * the process is free, and a recorded answer is what starts the next job.
371
+ */
372
+
373
+ /** What happened to one node. */
374
+ type NodeOutcome = {
375
+ nodeId: string;
376
+ status: "not-claimed" | "completed" | "skipped" | "failed" | "paused";
377
+ output?: unknown;
378
+ ports?: string[];
379
+ error?: string;
380
+ pause?: PauseSignal;
381
+ /** False when another worker got there first. A lost race is a NO-OP. */
382
+ claimed: boolean;
383
+ /** 1-based attempt this execution ran as. */
384
+ attempt: number;
385
+ /**
386
+ * `true` when this attempt failed and the policy still allows another.
387
+ *
388
+ * The claim row is left CLAIMED in that case, deliberately: a queue adapter
389
+ * re-dispatches the job with the SAME owner token and the retry re-enters the
390
+ * claim it already holds. Recording FAILED here instead would settle the
391
+ * node, which SKIPS everything downstream — a run reporting a tidy finish
392
+ * having done half its work.
393
+ */
394
+ retryable?: boolean;
395
+ };
396
+ type DurableRunResult = {
397
+ ok: boolean;
398
+ outputs: Record<string, unknown>;
399
+ error?: string;
400
+ pause?: PauseSignal;
401
+ paused: boolean;
402
+ };
403
+ type CoordinatorOptions = {
404
+ graph: FlowGraph;
405
+ executors: ExecutorRegistry;
406
+ /**
407
+ * The run's stable identity. A bare string is taken as the run key.
408
+ *
409
+ * Required — not defaulted. A durable run without a stable key cannot key an
410
+ * idempotent write, and minting one per construction would hand a retrying
411
+ * host a different key each time.
412
+ */
413
+ run: string | RunIdentity | RunIdentityJson;
414
+ store?: NodeClaimStore;
415
+ initialInputs?: Record<string, Record<string, unknown>>;
416
+ retry?: RetryPolicy;
417
+ onEvent?: (event: RunEvent) => void;
418
+ };
419
+ declare class Coordinator {
420
+ readonly graph: FlowGraph;
421
+ readonly executors: ExecutorRegistry;
422
+ readonly run: RunIdentity;
423
+ readonly store: NodeClaimStore;
424
+ readonly initialInputs: Record<string, Record<string, unknown>>;
425
+ readonly retry: RetryPolicy;
426
+ private readonly onEvent?;
427
+ constructor(options: CoordinatorOptions);
428
+ get runKey(): string;
429
+ /**
430
+ * Which nodes may be dispatched right now.
431
+ *
432
+ * Also settles the skip cascade, because a skip is a decision the frontier
433
+ * just made and a second caller must not make it again.
434
+ */
435
+ advance(): Promise<string[]>;
436
+ /**
437
+ * Claim, execute and checkpoint one node.
438
+ *
439
+ * The claim is taken FIRST. Two workers racing for the same node produce one
440
+ * execution and one no-op, and the loser learns that from the store rather
441
+ * than from a duplicate side effect.
442
+ *
443
+ * `owner` is the token that lets a job's own retry re-enter its claim instead
444
+ * of deadlocking against the row it wrote itself — pass the SAME token across
445
+ * a job's attempts.
446
+ */
447
+ runNode(nodeId: string, owner?: string): Promise<NodeOutcome>;
448
+ /**
449
+ * Drive the graph here, in this process, one node at a time.
450
+ *
451
+ * Every checkpoint is written exactly as a queued run writes it, so a crash
452
+ * mid-loop resumes from the same place a crashed worker would.
453
+ *
454
+ * Retries honour {@link RetryPolicy}: a node declaring `unsafe-to-replay`
455
+ * gets one attempt whatever the policy says, and the retry re-enters the same
456
+ * claim with the same owner token — so the step key it derives is unchanged,
457
+ * which is what makes the retry idempotent rather than duplicative.
458
+ */
459
+ runToCompletion(maxPasses?: number): Promise<DurableRunResult>;
460
+ /**
461
+ * Checkpointed outputs, in the graph's own node order.
462
+ *
463
+ * Ordered by the graph rather than by completion so two runs of the same
464
+ * workflow produce comparable output maps even when nodes finished in a
465
+ * different order.
466
+ */
467
+ outputs(): Promise<Record<string, unknown>>;
468
+ /** The checkpointed run, in the shape a single-process run returns. */
469
+ asRunResult(): Promise<RunResult>;
470
+ private runNodeWithRetries;
471
+ /**
472
+ * The identity handed to the node about to execute.
473
+ *
474
+ * `attempt` and `firstAttemptAt` come off the CLAIM ROW, so they describe
475
+ * this step rather than the run — which is what makes the retry window check
476
+ * exact instead of conservative.
477
+ */
478
+ private identityFor;
479
+ private completedOutputs;
480
+ /**
481
+ * Forward only the events the target node produced.
482
+ *
483
+ * A replay re-emits the whole completed prefix. Passing that through would
484
+ * show a consumer every node running again on every job — the run feed would
485
+ * report a 20-node workflow as 200 status changes.
486
+ */
487
+ private forward;
488
+ }
489
+
490
+ export { BOUNDARY, Coordinator, type CoordinatorOptions, type DurableRunResult, Frontier, type FrontierResult, InMemoryClaimStore, type NodeClaimStore, type NodeOutcome, NodeRunStatus, type NodeRunStatusValue, type NodeState, NotAwaitingHuman, type ReplayOptions, type ReplayResult, RetryPolicy, type RetryPolicyOptions, SETTLED, Submissions, UNSAFE_TO_REPLAY, durableApproval, durableUserInput, isBoundary, isSettled, replayUpTo };