@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.
- package/dist/{ConfigFieldRenderer-Dzy4f6rG.d.cts → ConfigFieldRenderer-Cyir8-B8.d.cts} +2 -2
- package/dist/{ConfigFieldRenderer-WqJr-td_.d.ts → ConfigFieldRenderer-iMkZj0VK.d.ts} +2 -2
- package/dist/{FlowViewer-BbbUJe2b.d.cts → FlowViewer-CY3_vHSg.d.cts} +1 -1
- package/dist/{FlowViewer-csuaTL5t.d.ts → FlowViewer-DGW6t-80.d.ts} +1 -1
- package/dist/{capabilities-Brn74URS.d.ts → capabilities-DMUhnyZD.d.ts} +1 -1
- package/dist/{capabilities-CYCeIx6I.d.cts → capabilities-Dmd26aAs.d.cts} +1 -1
- package/dist/chunk-BHM5HGXH.js +79 -0
- package/dist/chunk-BHM5HGXH.js.map +1 -0
- package/dist/{chunk-DHUFUDEX.js → chunk-HFUBVT7W.js} +3 -3
- package/dist/{chunk-DHUFUDEX.js.map → chunk-HFUBVT7W.js.map} +1 -1
- package/dist/{chunk-VLATLVGH.js → chunk-L43CWD63.js} +129 -5
- package/dist/chunk-L43CWD63.js.map +1 -0
- package/dist/{chunk-A6RFLGWV.js → chunk-MH4ZW4DJ.js} +3 -3
- package/dist/{chunk-A6RFLGWV.js.map → chunk-MH4ZW4DJ.js.map} +1 -1
- package/dist/{chunk-QZTGV3ZL.js → chunk-P5SYH6M6.js} +3 -3
- package/dist/{chunk-QZTGV3ZL.js.map → chunk-P5SYH6M6.js.map} +1 -1
- package/dist/{chunk-WBXGZVLB.js → chunk-RMU6YCIQ.js} +11 -5
- package/dist/chunk-RMU6YCIQ.js.map +1 -0
- package/dist/durable/index.d.cts +490 -0
- package/dist/durable/index.d.ts +490 -0
- package/dist/durable.cjs +927 -0
- package/dist/durable.cjs.map +1 -0
- package/dist/durable.js +552 -0
- package/dist/durable.js.map +1 -0
- package/dist/engine.cjs +128 -2
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.cts +9 -7
- package/dist/engine.d.ts +9 -7
- package/dist/engine.js +4 -4
- package/dist/fields/react-fancy.d.cts +4 -3
- package/dist/fields/react-fancy.d.ts +4 -3
- package/dist/index.cjs +133 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -10
- package/dist/index.d.ts +12 -10
- package/dist/index.js +13 -88
- package/dist/index.js.map +1 -1
- package/dist/layout/index.d.cts +1 -1
- package/dist/layout/index.d.ts +1 -1
- package/dist/llm/prism.d.cts +2 -2
- package/dist/llm/prism.d.ts +2 -2
- package/dist/llm/vercel-ai.d.cts +2 -2
- package/dist/llm/vercel-ai.d.ts +2 -2
- package/dist/pause-9iT4tCEV.d.cts +96 -0
- package/dist/pause-9iT4tCEV.d.ts +96 -0
- package/dist/registry/index.d.cts +7 -6
- package/dist/registry/index.d.ts +7 -6
- package/dist/{registry-Budds73w.d.cts → registry-D_kyUZEf.d.cts} +1 -1
- package/dist/{registry-D0I20HJ3.d.ts → registry-LHB3MlR9.d.ts} +1 -1
- package/dist/registry.cjs +133 -3
- package/dist/registry.cjs.map +1 -1
- package/dist/registry.js +4 -4
- package/dist/{run-cohort-CV7EcwGG.d.ts → run-cohort-Cyeny2Uu.d.ts} +3 -34
- package/dist/{run-cohort-DitsaYgS.d.cts → run-cohort-DdPPowJ3.d.cts} +3 -34
- package/dist/run-flow-1KiXhy59.d.cts +55 -0
- package/dist/run-flow-CQvTDBYk.d.ts +55 -0
- package/dist/runtime/index.d.cts +5 -3
- package/dist/runtime/index.d.ts +5 -3
- package/dist/runtime.cjs +128 -2
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +3 -3
- package/dist/schema/index.d.cts +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/screens.d.cts +2 -2
- package/dist/screens.d.ts +2 -2
- package/dist/screens.js +6 -6
- package/dist/{types-D6ZSKBly.d.ts → types--S2Kzx7w.d.cts} +3 -97
- package/dist/{types-nQOA8jpk.d.cts → types-ChkCRonF.d.ts} +3 -97
- package/dist/types-Jx1TwehV.d.cts +247 -0
- package/dist/types-Jx1TwehV.d.ts +247 -0
- package/dist/ux.d.cts +3 -2
- package/dist/ux.d.ts +3 -2
- package/package.json +12 -2
- package/dist/chunk-VLATLVGH.js.map +0 -1
- package/dist/chunk-WBXGZVLB.js.map +0 -1
- package/dist/types-CMSrWVYM.d.cts +0 -109
- 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 };
|