@particle-academy/fancy-flow 0.65.1 → 0.66.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 (106) hide show
  1. package/README.md +100 -3
  2. package/dist/{FlowViewer-BI_AyyvU.d.ts → FlowViewer-BCc_BvuD.d.ts} +1 -1
  3. package/dist/{FlowViewer-DABx-6A-.d.cts → FlowViewer-CoAJ3o52.d.cts} +1 -1
  4. package/dist/{HumanPrompt-Bpp3JiVH.d.cts → HumanPrompt--pXDOyYC.d.cts} +2 -2
  5. package/dist/{HumanPrompt-BKThiyjB.d.ts → HumanPrompt-DXm_FP9z.d.ts} +2 -2
  6. package/dist/chunk-2QXKDLGT.js +58 -0
  7. package/dist/chunk-2QXKDLGT.js.map +1 -0
  8. package/dist/{chunk-77V4QC6Y.js → chunk-B7HKJJVV.js} +3 -3
  9. package/dist/{chunk-77V4QC6Y.js.map → chunk-B7HKJJVV.js.map} +1 -1
  10. package/dist/{chunk-MBVX4ZRB.js → chunk-FNYLKSFJ.js} +3 -3
  11. package/dist/{chunk-MBVX4ZRB.js.map → chunk-FNYLKSFJ.js.map} +1 -1
  12. package/dist/{chunk-JF6WCRBU.js → chunk-HQGOFGAL.js} +861 -40
  13. package/dist/chunk-HQGOFGAL.js.map +1 -0
  14. package/dist/{chunk-5H54OTKT.js → chunk-IEYCNFXZ.js} +3 -3
  15. package/dist/{chunk-5H54OTKT.js.map → chunk-IEYCNFXZ.js.map} +1 -1
  16. package/dist/{chunk-W5DPKJY4.js → chunk-MTONBM53.js} +4 -4
  17. package/dist/chunk-MTONBM53.js.map +1 -0
  18. package/dist/{chunk-RIAFHQT5.js → chunk-PNHZHEWE.js} +3 -3
  19. package/dist/{chunk-RIAFHQT5.js.map → chunk-PNHZHEWE.js.map} +1 -1
  20. package/dist/{chunk-EO6444T2.js → chunk-UCCTXJXL.js} +4 -4
  21. package/dist/{chunk-EO6444T2.js.map → chunk-UCCTXJXL.js.map} +1 -1
  22. package/dist/connectors.d.cts +2 -2
  23. package/dist/connectors.d.ts +2 -2
  24. package/dist/durable/index.d.cts +2 -2
  25. package/dist/durable/index.d.ts +2 -2
  26. package/dist/durable.cjs +837 -47
  27. package/dist/durable.cjs.map +1 -1
  28. package/dist/durable.js +2 -3
  29. package/dist/durable.js.map +1 -1
  30. package/dist/engine.cjs +843 -132
  31. package/dist/engine.cjs.map +1 -1
  32. package/dist/engine.d.cts +6 -7
  33. package/dist/engine.d.ts +6 -7
  34. package/dist/engine.js +5 -6
  35. package/dist/engine.js.map +1 -1
  36. package/dist/fields/react-fancy.d.cts +3 -3
  37. package/dist/fields/react-fancy.d.ts +3 -3
  38. package/dist/index.cjs +853 -140
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.cts +65 -36
  41. package/dist/index.d.ts +65 -36
  42. package/dist/index.js +19 -18
  43. package/dist/index.js.map +1 -1
  44. package/dist/layout/index.d.cts +1 -1
  45. package/dist/layout/index.d.ts +1 -1
  46. package/dist/llm/prism.cjs.map +1 -1
  47. package/dist/llm/prism.d.cts +1 -2
  48. package/dist/llm/prism.d.ts +1 -2
  49. package/dist/llm/prism.js +1 -1
  50. package/dist/llm/vercel-ai.cjs.map +1 -1
  51. package/dist/llm/vercel-ai.d.cts +1 -2
  52. package/dist/llm/vercel-ai.d.ts +1 -2
  53. package/dist/llm/vercel-ai.js +1 -1
  54. package/dist/registry/index.d.cts +5 -6
  55. package/dist/registry/index.d.ts +5 -6
  56. package/dist/{registry-CntAdUcY.d.cts → registry-BcgllSk2.d.cts} +1 -1
  57. package/dist/{registry-DYoV-MQi.d.ts → registry-h-2QswZ5.d.ts} +1 -1
  58. package/dist/registry.cjs +867 -37
  59. package/dist/registry.cjs.map +1 -1
  60. package/dist/registry.js +3 -3
  61. package/dist/{run-cohort-C1nRh_mi.d.ts → run-cohort-CjUBWg6X.d.ts} +2 -2
  62. package/dist/{run-cohort-CBRAUj-z.d.cts → run-cohort-DL9JzzPU.d.cts} +2 -2
  63. package/dist/{run-flow-2XsHBwpd.d.cts → run-flow-B_8hgO5_.d.cts} +1 -1
  64. package/dist/{run-flow-D7AOCcfE.d.ts → run-flow-CxEGBOxd.d.ts} +1 -1
  65. package/dist/runtime/index.d.cts +5 -5
  66. package/dist/runtime/index.d.ts +5 -5
  67. package/dist/runtime.cjs +837 -36
  68. package/dist/runtime.cjs.map +1 -1
  69. package/dist/runtime.js +4 -4
  70. package/dist/schema/index.d.cts +19 -6
  71. package/dist/schema/index.d.ts +19 -6
  72. package/dist/schema.cjs +837 -36
  73. package/dist/schema.cjs.map +1 -1
  74. package/dist/schema.js +4 -4
  75. package/dist/screens.cjs +837 -36
  76. package/dist/screens.cjs.map +1 -1
  77. package/dist/screens.d.cts +2 -2
  78. package/dist/screens.d.ts +2 -2
  79. package/dist/screens.js +5 -5
  80. package/dist/terminal/fancy-term-host.cjs +99 -0
  81. package/dist/terminal/fancy-term-host.cjs.map +1 -0
  82. package/dist/terminal/fancy-term-host.d.cts +76 -0
  83. package/dist/terminal/fancy-term-host.d.ts +76 -0
  84. package/dist/terminal/fancy-term-host.js +92 -0
  85. package/dist/terminal/fancy-term-host.js.map +1 -0
  86. package/dist/types-B-Syk9-M.d.cts +620 -0
  87. package/dist/types-B-Syk9-M.d.ts +620 -0
  88. package/dist/{types-D71SKA5A.d.cts → types-C2ATTmjN.d.cts} +1 -1
  89. package/dist/{types-Ckhz-YwC.d.ts → types-VunLGqrn.d.ts} +1 -1
  90. package/dist/ux.cjs +845 -39
  91. package/dist/ux.cjs.map +1 -1
  92. package/dist/ux.d.cts +14 -2
  93. package/dist/ux.d.ts +14 -2
  94. package/dist/ux.js +10 -5
  95. package/dist/ux.js.map +1 -1
  96. package/package.json +11 -1
  97. package/dist/capabilities-BYa5p5jw.d.cts +0 -112
  98. package/dist/capabilities-COOXRiNL.d.ts +0 -112
  99. package/dist/chunk-JF6WCRBU.js.map +0 -1
  100. package/dist/chunk-UM4C46AF.js +0 -103
  101. package/dist/chunk-UM4C46AF.js.map +0 -1
  102. package/dist/chunk-USL4FMFU.js +0 -41
  103. package/dist/chunk-USL4FMFU.js.map +0 -1
  104. package/dist/chunk-W5DPKJY4.js.map +0 -1
  105. package/dist/types-JFYjPJAG.d.cts +0 -333
  106. package/dist/types-JFYjPJAG.d.ts +0 -333
@@ -0,0 +1,620 @@
1
+ import { Node, Edge } from '@xyflow/react';
2
+
3
+ /**
4
+ * Host capabilities — the services core nodes need but must never depend on.
5
+ *
6
+ * A node that imports a provider SDK forces every consumer to install it: a
7
+ * workflow app that never calls a model should not inherit an LLM dependency.
8
+ * So core declares the CONTRACT and the host supplies the implementation, the
9
+ * same arrangement `renderDocumentField` already uses for documents.
10
+ *
11
+ * That keeps opinionated nodes in core without their opinions: `llm_branch`
12
+ * ships the routing semantics, port derivation and config UI, while whichever
13
+ * client the host registers — Prism, an OpenAI SDK, a local model, a fake in a
14
+ * test — decides how the question actually gets asked.
15
+ *
16
+ * Registration is deliberately explicit and typed per capability rather than a
17
+ * stringly-keyed bag, so a missing one is a clear error at the seam instead of
18
+ * an undefined somewhere downstream.
19
+ */
20
+ type LlmRoute = {
21
+ port: string;
22
+ description?: string;
23
+ };
24
+ type LlmRouteRequest = {
25
+ /** Optional framing for the decision. */
26
+ system?: string;
27
+ /** What the model is deciding about. */
28
+ prompt: string;
29
+ /** The ports it must choose between. */
30
+ routes: LlmRoute[];
31
+ provider?: string;
32
+ model?: string;
33
+ /** Host-resolved credential reference, never a raw key. */
34
+ credential?: string;
35
+ };
36
+ type LlmRouteChoice = {
37
+ /** Must be one of the requested route ports. */
38
+ port: string;
39
+ /** Why — carried down the chosen port so a run is explainable afterwards. */
40
+ reason?: string;
41
+ };
42
+ /**
43
+ * The only thing core asks of an LLM: given routes, pick one.
44
+ *
45
+ * Deliberately not a general chat interface. A narrow contract is one a host
46
+ * can satisfy in a few lines over any SDK, and it keeps the choice
47
+ * machine-checkable — an implementation should constrain the model to the
48
+ * declared ports (structured output / enum) rather than parsing prose.
49
+ */
50
+ type LlmClient = {
51
+ chooseRoute: (request: LlmRouteRequest) => Promise<LlmRouteChoice> | LlmRouteChoice;
52
+ };
53
+ /** Install the host's LLM client. Returns an unregister function. */
54
+ declare function registerLlmClient(client: LlmClient): () => void;
55
+ declare function getLlmClient(): LlmClient | null;
56
+ /** What to launch. Omit `command` for the host's default shell. */
57
+ type TerminalSessionSpec = {
58
+ command?: string;
59
+ args?: string[];
60
+ cwd?: string;
61
+ env?: Record<string, string>;
62
+ cols?: number;
63
+ rows?: number;
64
+ };
65
+ type TerminalExit = {
66
+ exitCode: number;
67
+ signal?: string;
68
+ };
69
+ /**
70
+ * One live terminal, owned by the host for as long as the run needs it.
71
+ *
72
+ * Output arrives by SUBSCRIPTION rather than a `read()` the engine calls. A TUI
73
+ * emits continuously and on its own schedule — repainting, streaming a reply,
74
+ * redrawing a spinner — so a polled read either misses bytes between calls or
75
+ * has to buffer them anyway, and every host would invent that buffer
76
+ * differently.
77
+ */
78
+ type TerminalSession = {
79
+ id: string;
80
+ write: (data: string) => void | Promise<void>;
81
+ /** Subscribe to output. Returns an unsubscribe. */
82
+ onData: (listener: (chunk: string) => void) => () => void;
83
+ /**
84
+ * Resolves when the process exits.
85
+ *
86
+ * A promise rather than an `exited` flag the engine polls, so "the agent
87
+ * quit" and "the agent has not answered yet" are distinguishable while
88
+ * waiting. Without it a node awaiting output cannot tell a slow reply from a
89
+ * dead process, and waits out its timeout either way.
90
+ */
91
+ exited: Promise<TerminalExit>;
92
+ close: () => void | Promise<void>;
93
+ };
94
+ /**
95
+ * The only thing core asks of a terminal: open one.
96
+ *
97
+ * ## Why the contract stops at the PTY
98
+ *
99
+ * There is deliberately no `waitForOutput(pattern)` here, though every terminal
100
+ * node needs one. Matching is derivable from `onData`, so putting it in the
101
+ * contract would mean every host implements it — and two implementations of one
102
+ * agreed rule is precisely how a week of registry defects happened: both sides
103
+ * agreed output matching should work, and would have disagreed on whether a
104
+ * pattern spans chunk boundaries, whether ANSI escapes are stripped first, and
105
+ * what a timeout returns.
106
+ *
107
+ * So core owns the matching and the host owns the process. A host satisfies
108
+ * this in a few lines over `node-pty`; nothing about how a run interprets the
109
+ * bytes is up to it.
110
+ *
111
+ * ## Why this exists at all
112
+ *
113
+ * A terminal cannot live in the engine. `node-pty` is a native addon, so
114
+ * importing it would break every browser build of this package and force the
115
+ * dependency on consumers who never run a terminal node — the same reason the
116
+ * LLM client is a contract rather than an SDK import. The desktop app that CAN
117
+ * spawn a PTY registers one; everyone else never notices the capability exists.
118
+ */
119
+ type TerminalHost = {
120
+ open: (spec: TerminalSessionSpec) => Promise<TerminalSession> | TerminalSession;
121
+ };
122
+ /** Install the host's terminal. Returns an unregister function. */
123
+ declare function registerTerminalHost(host: TerminalHost): () => void;
124
+ declare function getTerminalHost(): TerminalHost | null;
125
+ /**
126
+ * Why a workflow reference could not be resolved.
127
+ *
128
+ * `missing` and `version-mismatch` are deliberately distinct. Collapsing them
129
+ * into a bare null makes "no such workflow" indistinguishable from "that
130
+ * workflow exists, but it is not the one you pinned" — and the second wants an
131
+ * error naming both versions, because it is the interesting failure.
132
+ */
133
+ type WorkflowResolutionFailure = {
134
+ reason: "missing" | "version-mismatch";
135
+ /** The version the host actually holds, when it holds one. */
136
+ available?: number;
137
+ message?: string;
138
+ };
139
+ type WorkflowResolution = FlowGraph | WorkflowResolutionFailure | null;
140
+ /**
141
+ * Resolve a workflow reference to a runnable graph.
142
+ *
143
+ * `subflow` names another workflow rather than embedding it, so the host owns
144
+ * where workflows live — a database, a file, an API.
145
+ *
146
+ * ## Why `version` is here
147
+ *
148
+ * A workflow another workflow depends on is an INTERFACE, and interfaces need
149
+ * pins. Without a version, a parent goes on calling `invoice-triage`, someone
150
+ * edits that child, and the parent now runs different logic having reported
151
+ * success the whole time — correct-looking, no error, wrong behaviour. The same
152
+ * failure family as the 0.9.0 routing divergence.
153
+ *
154
+ * The parameter lives on the resolver rather than being encoded into the ref
155
+ * string (`invoice-triage@3`) because a stringly-typed protocol is one every
156
+ * host invents differently — the "three vocabularies for one node" problem.
157
+ *
158
+ * Raised by the MOIC Suite consumer, whose `workflow_ref` pins versions and
159
+ * fails loudly on mismatch. Their point: a host COULD NOT implement pinning
160
+ * before this, because the node had no way to ask and the resolver no way to
161
+ * receive.
162
+ *
163
+ * Returning `null` still means "no such workflow". Return a
164
+ * {@link WorkflowResolutionFailure} to distinguish a version mismatch.
165
+ */
166
+ type WorkflowResolver = (ref: string, version?: number) => Promise<WorkflowResolution> | WorkflowResolution;
167
+ /** Narrow a resolver's return value to an explicit failure. */
168
+ declare function isResolutionFailure(value: WorkflowResolution): value is WorkflowResolutionFailure;
169
+ /** Install the host's workflow resolver. Returns an unregister function. */
170
+ declare function registerWorkflowResolver(resolver: WorkflowResolver): () => void;
171
+ declare function getWorkflowResolver(): WorkflowResolver | null;
172
+ type CapabilityId = "llm" | "workflow_resolver" | "document" | "terminal";
173
+ /**
174
+ * Which capabilities are currently satisfied.
175
+ *
176
+ * Exists so a host (or the CLI, or an agent over MCP) can answer "what does
177
+ * this graph need that I haven't wired?" BEFORE a run fails halfway through.
178
+ */
179
+ declare function capabilityStatus(): Record<CapabilityId, boolean>;
180
+
181
+ /**
182
+ * What a terminal has said, in a form a node can wait on.
183
+ *
184
+ * A raw `onData` stream is not something a workflow can match against, for
185
+ * three reasons that all fail the same way — intermittently, and usually only
186
+ * once there is real output moving:
187
+ *
188
+ * 1. **Chunks are arbitrary.** A PTY splits wherever it splits, so `Ready >`
189
+ * can arrive as `Rea` + `dy >`. Testing each chunk on its own finds the
190
+ * pattern when output is small enough to land in one write and misses it
191
+ * when it is not. So matching runs against an ACCUMULATED buffer, never a
192
+ * chunk.
193
+ *
194
+ * 2. **Output arrives before anyone is listening.** A node that types at a
195
+ * process and a node that waits for its reply are two steps, and anything
196
+ * printed between them is gone if the buffer starts when the wait does.
197
+ * So the transcript is attached when the SESSION opens, and a wait reads
198
+ * what has already accumulated before it subscribes to anything new.
199
+ *
200
+ * 3. **Escape sequences are everywhere and they also straddle.** An agent TUI
201
+ * writes `Ready` as `ESC[32m Ready ESC[0m`, so matching raw bytes fails on
202
+ * text a person can plainly read. Stripping per chunk reintroduces problem
203
+ * 1 in a second guise — `ESC[3` + `2mReady` strips to `ESC[32mReady` with
204
+ * the escape now INSIDE the text. So an incomplete trailing sequence is
205
+ * held back until the rest of it arrives.
206
+ *
207
+ * Text is consumed through a match, so two waits for the same pattern do not
208
+ * both resolve on the first occurrence — which would let a loop appear to make
209
+ * progress while reading one old line forever.
210
+ */
211
+ type TranscriptWaitResult = {
212
+ status: "matched";
213
+ text: string;
214
+ match: RegExpMatchArray;
215
+ } | {
216
+ status: "timeout";
217
+ text: string;
218
+ } | {
219
+ status: "exited";
220
+ text: string;
221
+ exitCode: number;
222
+ signal?: string;
223
+ };
224
+ declare class TerminalTranscript {
225
+ /** Stripped, complete text not yet consumed by a wait. */
226
+ private text;
227
+ /** Raw bytes held back because they may be the start of an escape sequence. */
228
+ private tail;
229
+ private readonly waiters;
230
+ /** Feed raw terminal output in. Safe to call with any chunking. */
231
+ append(chunk: string): void;
232
+ /**
233
+ * How much of `tail` can be stripped now.
234
+ *
235
+ * Everything, unless the final `ESC` has not yet been terminated — in which
236
+ * case processing stops there and resumes when the rest arrives.
237
+ */
238
+ private safeLength;
239
+ /** Unconsumed output, escape sequences removed. */
240
+ peek(): string;
241
+ /** Drop everything currently buffered — used before typing a new command. */
242
+ clear(): void;
243
+ /**
244
+ * Wait until `pattern` matches the unconsumed text.
245
+ *
246
+ * Checks what has ALREADY arrived before subscribing, because the common case
247
+ * is that the process answered while the previous node was still finishing.
248
+ *
249
+ * `exited` is raced deliberately. Without it, a shell that dies reports as
250
+ * "timed out waiting for X" — sending whoever reads it to lengthen a timeout
251
+ * for a process that is not running. Naming the exit is the difference
252
+ * between a diagnosis and a wrong lead.
253
+ */
254
+ waitFor(pattern: RegExp, options: {
255
+ timeoutMs: number;
256
+ exited?: Promise<{
257
+ exitCode: number;
258
+ signal?: string;
259
+ }>;
260
+ }): Promise<TranscriptWaitResult>;
261
+ }
262
+
263
+ /**
264
+ * Who is running, which step this is, and how many times it has been tried.
265
+ *
266
+ * ## Why an engine needs this at all
267
+ *
268
+ * A node that WRITES to somebody else's system — charge a card, send a message,
269
+ * open a pull request — can only survive a retry if the retry carries the same
270
+ * idempotency key the first attempt did. Otherwise the provider treats the
271
+ * second call as a new request and the customer is charged twice.
272
+ *
273
+ * Until this existed the executor context was `{ node, inputs, emit, abort }`,
274
+ * which is not enough to derive one. Both obvious fallbacks are worse than
275
+ * sending no key at all:
276
+ *
277
+ * - **the node id alone** is stable across retries, and also across RUNS — two
278
+ * legitimate payments share a key and the provider silently collapses the
279
+ * second into the first. A payment that never happened, reported as success;
280
+ * - **a fresh random value** is unique per run, and also per ATTEMPT — a retry
281
+ * creates a second charge, which is the thing being avoided.
282
+ *
283
+ * ## What actually identifies a step
284
+ *
285
+ * Not `(run, node)`. A node legitimately executes more than once inside one
286
+ * run: once per subflow invocation, once per iteration of a loop an executor
287
+ * drives itself. `(run, node)` would give every one of those the same key, and
288
+ * a provider would honour exactly one of them.
289
+ *
290
+ * So a step is identified by the **path of invocations that led to it**, plus
291
+ * an optional **occurrence** for repetition at the same level:
292
+ *
293
+ * ```text
294
+ * runKey ":" segment ("/" segment)* segment := escape(id) ["#" occurrence]
295
+ * ```
296
+ *
297
+ * And the part that is easy to get backwards: **`attempt` is NOT in the key.**
298
+ * It is carried here for logging and for {@link RunIdentity.isReplaySafe}, and
299
+ * putting it in the key would restore the exact bug the key exists to prevent.
300
+ *
301
+ * Pinned cross-runtime by `shared/flow-run-identity` in
302
+ * `@particle-academy/fancy-conformance`.
303
+ */
304
+ /** The wire shape — what a queue job payload carries. */
305
+ type RunIdentityJson = {
306
+ runKey: string;
307
+ path?: string[];
308
+ attempt?: number;
309
+ firstAttemptAt?: string;
310
+ };
311
+ /**
312
+ * Escape one segment so the composition is injective.
313
+ *
314
+ * `%` FIRST, or the escaping is not reversible: escaping `/` before `%` turns a
315
+ * literal `a%2Fb` into the same text as the escaped form of `a/b`, which is the
316
+ * collision this exists to prevent, reintroduced by its own fix.
317
+ */
318
+ declare function escapeSegment(value: string): string;
319
+ /**
320
+ * A run, a position inside it, and how many times this position has been tried.
321
+ *
322
+ * Immutable. {@link descend} returns a new identity rather than mutating, so an
323
+ * executor cannot change what its siblings see.
324
+ */
325
+ declare class RunIdentity {
326
+ /** Stable for the whole run: same across retries, resumes, workers and hosts. */
327
+ readonly runKey: string;
328
+ /**
329
+ * Enclosing invocation segments, outermost first, ALREADY RENDERED.
330
+ *
331
+ * Empty at the top level. A subflow pushes the invoking node's id; an
332
+ * executor that loops pushes `id#i`.
333
+ */
334
+ readonly path: readonly string[];
335
+ /**
336
+ * 1-based attempt of THIS logical step. Never part of the key.
337
+ *
338
+ * The durable driver sets it from the node's claim row, which is exact. A
339
+ * plain in-process `runFlow` gets whatever the host passed, which is
340
+ * run-scoped and therefore conservative — see `isReplaySafe`.
341
+ */
342
+ readonly attempt: number;
343
+ /** ISO-8601 UTC instant of attempt 1 of this step. */
344
+ readonly firstAttemptAt: string;
345
+ constructor(runKey: string, path?: readonly string[], attempt?: number, firstAttemptAt?: string);
346
+ /**
347
+ * The identity of one execution of one node — stable across retries of that
348
+ * execution, distinct from every other execution of the same node.
349
+ *
350
+ * Pass `occurrence` when an executor runs the same node more than once at the
351
+ * same level (a loop body, one item of a fan-out it drives itself).
352
+ */
353
+ stepKey(nodeId: string, occurrence?: number | null): string;
354
+ /**
355
+ * A child identity for work nested inside this step.
356
+ *
357
+ * `subflow` pushes the invoking node's id, so a node inside the child graph
358
+ * cannot collide with a same-named node in the parent. Attempt and
359
+ * `firstAttemptAt` are carried down unchanged: the nested work happens inside
360
+ * this step's attempt, and shares its clock.
361
+ */
362
+ descend(segment: string, occurrence?: number | null): RunIdentity;
363
+ /** A copy on a different attempt, first-attempt clock preserved. */
364
+ withAttempt(attempt: number, firstAttemptAt?: string): RunIdentity;
365
+ /**
366
+ * May this attempt reuse the step key and still be deduplicated?
367
+ *
368
+ * Providers forget idempotency keys — Stripe after 24 hours. Past that
369
+ * window, resending the key creates a second charge and sending a fresh one
370
+ * creates a second charge, so **the caller must refuse rather than pick
371
+ * between them**: a loud stuck run beats a silent double write.
372
+ *
373
+ * `true` on attempt 1 whatever the elapsed time — nothing was sent on an
374
+ * earlier attempt, so there is nothing for the provider to have forgotten.
375
+ * That is what lets a run park on a human gate for a week and then write.
376
+ *
377
+ * `windowSeconds: null` means the provider does not expire keys. `0` means
378
+ * it does not dedupe at all, so no retry may reuse a key — it is a real
379
+ * window, not an absent one, and the two must not be conflated: reading `0`
380
+ * as `null` turns "this provider does not dedupe" into "this provider
381
+ * dedupes forever", which is the more dangerous of the two by a distance.
382
+ */
383
+ isReplaySafe(windowSeconds: number | null | undefined, now?: Date | string): boolean;
384
+ toJSON(): Required<RunIdentityJson>;
385
+ /** Rebuild from a queue payload. */
386
+ static from(value: RunIdentity | RunIdentityJson | string): RunIdentity;
387
+ }
388
+
389
+ type FlowNodeKind = "trigger" | "action" | "decision" | "output" | "note" | "subgraph";
390
+ /** Status surfaced on the node while a run is in progress. */
391
+ type NodeRunStatus = "idle" | "queued" | "running" | "done" | "error";
392
+ /** Port description on a node. Ports are visual handles xyflow can connect. */
393
+ type PortDescriptor = {
394
+ id: string;
395
+ label?: string;
396
+ /** Optional logical type for hosts that want to validate connections. */
397
+ type?: string;
398
+ };
399
+ /** Common shape every kit node carries in its `data` slot. */
400
+ type BaseNodeData = {
401
+ label: string;
402
+ description?: string;
403
+ /** Free-form configuration the host owns (form values, code, parameters). */
404
+ config?: Record<string, unknown>;
405
+ /** Set by the runner; hosts shouldn't edit this directly. */
406
+ status?: NodeRunStatus;
407
+ /** Optional human-readable status detail (e.g. error message, current step). */
408
+ statusText?: string;
409
+ /**
410
+ * Announced to a person just BEFORE this node runs — "Starting the deep
411
+ * analysis". Authored on the node, so a graph narrates itself without the
412
+ * host writing any per-node reporting code.
413
+ *
414
+ * Optional on purpose. Most nodes in a real graph are plumbing, and a run
415
+ * that narrates all of them buries the two or three steps anyone follows.
416
+ */
417
+ startingMsg?: string;
418
+ /**
419
+ * Announced AFTER this node finishes — "Analysis complete".
420
+ *
421
+ * Emitted only when the node SUCCEEDS. A completion message printed after a
422
+ * failure tells a human the opposite of what happened, in the part of the UI
423
+ * they trust most; failures report through `node-status` and `log`.
424
+ */
425
+ stoppingMsg?: string;
426
+ /** Per-node accent override, e.g. for theming a custom subclass. */
427
+ color?: string;
428
+ /** Input ports rendered on the node. Defaults vary by kind. */
429
+ inputs?: PortDescriptor[];
430
+ /** Output ports rendered on the node. Defaults vary by kind. */
431
+ outputs?: PortDescriptor[];
432
+ };
433
+ type TriggerNodeData = BaseNodeData & {
434
+ kind: "trigger";
435
+ };
436
+ type ActionNodeData = BaseNodeData & {
437
+ kind: "action";
438
+ };
439
+ type DecisionNodeData = BaseNodeData & {
440
+ kind: "decision";
441
+ };
442
+ type OutputNodeData = BaseNodeData & {
443
+ kind: "output";
444
+ };
445
+ type NoteNodeData = BaseNodeData & {
446
+ kind: "note";
447
+ body?: string;
448
+ };
449
+ type SubgraphNodeData = BaseNodeData & {
450
+ kind: "subgraph";
451
+ /** Ids of the nodes contained in this subgraph. */
452
+ childIds?: string[];
453
+ /** Whether the subgraph is shown collapsed (default true — children hidden). */
454
+ collapsed?: boolean;
455
+ };
456
+ type FlowNodeData = TriggerNodeData | ActionNodeData | DecisionNodeData | OutputNodeData | NoteNodeData | SubgraphNodeData;
457
+ type FlowNode = Node<FlowNodeData>;
458
+ type FlowEdge = Edge;
459
+ /**
460
+ * One value a workflow DECLARES that it accepts at run start.
461
+ *
462
+ * The declaration is the point. Before this existed, a caller passed
463
+ * `initialInputs` keyed BY NODE ID — so they had to know the trigger happened
464
+ * to be called `t`, and renaming that node broke every caller while the graph
465
+ * itself stayed valid. Nothing reported it.
466
+ *
467
+ * And nothing said what a workflow accepted at all: no names, no types, no
468
+ * defaults. An agent composing a call had nothing to read, and a misspelled key
469
+ * did not fail — the value simply sat unused while the run reported success.
470
+ */
471
+ type WorkflowInput = {
472
+ /** What a caller passes it as. */
473
+ name: string;
474
+ /**
475
+ * Optional. Omitting it means "I am not asserting a shape", which must not
476
+ * degrade into "nothing is allowed" — an undeclared type accepts anything.
477
+ */
478
+ type?: "string" | "number" | "boolean" | "object" | "array";
479
+ /**
480
+ * The run needs a value. Satisfied by a `default`, so `required` means
481
+ * "this must resolve to something" rather than "the caller must type it".
482
+ */
483
+ required?: boolean;
484
+ /** Used when the caller omits the key. An explicit value always wins. */
485
+ default?: unknown;
486
+ description?: string;
487
+ };
488
+ /** A serializable graph — what hosts persist, what agents read/write. */
489
+ type FlowGraph = {
490
+ nodes: FlowNode[];
491
+ edges: FlowEdge[];
492
+ /**
493
+ * What this workflow accepts. Callers pass a flat object BY NAME.
494
+ *
495
+ * Omitted entirely when a graph takes none, so existing saved graphs are
496
+ * unchanged byte for byte and every diff stays readable.
497
+ */
498
+ inputs?: WorkflowInput[];
499
+ };
500
+ /** Per-node executor signature. Inputs are keyed by input-port id. */
501
+ type NodeExecutor<TIn = Record<string, unknown>, TOut = unknown> = (ctx: {
502
+ node: FlowNode;
503
+ inputs: TIn;
504
+ /** Stops the run if called. */
505
+ abort: (reason?: string) => never;
506
+ /** Lets the executor stream status updates and partial outputs. */
507
+ emit: (event: RunEvent) => void;
508
+ /**
509
+ * The registry THIS run is executing against.
510
+ *
511
+ * Handed down so an executor that starts a NESTED run gives the child the
512
+ * same executors as the parent. `subflow` previously ran its child against
513
+ * `config.executors ?? {}` — an empty registry unless the graph happened to
514
+ * carry one — so a host kind resolved at top level and vanished one level
515
+ * down, and a host that had REPLACED a builtin got the package's version in
516
+ * the child. Same graph, different behaviour by nesting depth, reported
517
+ * against the PHP twin as fancy-flow-php#7.
518
+ *
519
+ * Inheriting from the context rather than from a parameter is what makes it
520
+ * unforgettable: any future nesting executor gets it without opting in.
521
+ */
522
+ executors?: ExecutorRegistry;
523
+ /**
524
+ * How deep this run is nested. 0 for a top-level run; `subflow` passes
525
+ * depth + 1 to its child, so runaway recursion can be reported by name
526
+ * rather than as a stack overflow.
527
+ */
528
+ depth?: number;
529
+ /**
530
+ * Who is running, and which attempt of which step this is.
531
+ *
532
+ * `ctx.run.stepKey(ctx.node.id)` is the idempotency key for a node that
533
+ * writes to somebody else's system — stable across retries of this step,
534
+ * distinct for every other execution of the same node.
535
+ *
536
+ * `undefined` when the host supplied no identity, and that is a real
537
+ * answer: a write with no key must decline or accept one attempt, never
538
+ * invent a key. See `RunIdentity`.
539
+ */
540
+ run?: RunIdentity;
541
+ /**
542
+ * The terminal this node's lane owns, if it is inside a terminal lane.
543
+ *
544
+ * `session()` is a FUNCTION, not an open session, and that is the whole
545
+ * lifetime rule in one shape: the terminal opens on first USE. A node that
546
+ * sits inside a terminal lane and never calls this never spawns a process,
547
+ * so drawing a lane around nodes that mostly do other things costs nothing.
548
+ * Every node in the lane gets the SAME session, which is what makes `cd`
549
+ * persist and an agent TUI still be running with its conversation intact.
550
+ *
551
+ * `undefined` when the node is not inside a terminal lane, and that is a
552
+ * real answer rather than a missing one — a terminal node outside a lane
553
+ * must say so rather than quietly opening a shell of its own, because one
554
+ * unmanaged process per node is exactly what the lane exists to prevent.
555
+ */
556
+ terminal?: {
557
+ session: () => Promise<TerminalSession>;
558
+ /**
559
+ * What the terminal has said, accumulated and matchable.
560
+ *
561
+ * Separate from `session()` because reading and writing are genuinely
562
+ * different jobs with different hazards. `session()` hands over the raw
563
+ * process; this hands over a buffer that already survived arbitrary
564
+ * chunking, escape sequences that straddle chunks, and output that
565
+ * arrived before any node was listening — none of which a node should be
566
+ * asked to get right on its own, and all of which fail intermittently
567
+ * when it doesn't.
568
+ *
569
+ * Opening the session is implied: a transcript with nothing feeding it
570
+ * would silently never match.
571
+ */
572
+ transcript: () => Promise<TerminalTranscript>;
573
+ };
574
+ }) => Promise<TOut> | TOut;
575
+ type ExecutorRegistry = Partial<Record<FlowNodeKind | string, NodeExecutor>>;
576
+ type RunEvent = {
577
+ type: "node-status";
578
+ nodeId: string;
579
+ status: NodeRunStatus;
580
+ text?: string;
581
+ }
582
+ /**
583
+ * A human-facing announcement a node makes around its own execution, from
584
+ * `data.startingMsg` / `data.stoppingMsg`. Opt-in per node: most nodes in a
585
+ * real graph are plumbing, and narrating all of them buries the few steps a
586
+ * person cares about.
587
+ *
588
+ * Deliberately NOT folded into `node-status.text`, which already carries
589
+ * "skipped", "resumed", "lane", "annotation" and raw error strings. Those are
590
+ * diagnostics; these are addressed to a person. A consumer rendering a
591
+ * progress feed cannot be asked to guess which is which — that is how an
592
+ * error string ends up shown to a user as a status update.
593
+ */
594
+ | {
595
+ type: "node-message";
596
+ nodeId: string;
597
+ phase: "start" | "end";
598
+ message: string;
599
+ } | {
600
+ type: "node-output";
601
+ nodeId: string;
602
+ portId: string;
603
+ value: unknown;
604
+ } | {
605
+ type: "log";
606
+ nodeId?: string;
607
+ level: "info" | "warn" | "error";
608
+ message: string;
609
+ detail?: unknown;
610
+ } | {
611
+ type: "run-start";
612
+ } | {
613
+ type: "run-end";
614
+ ok: boolean;
615
+ } | {
616
+ type: "run-error";
617
+ error: string;
618
+ };
619
+
620
+ export { type ActionNodeData as A, type BaseNodeData as B, type CapabilityId as C, type DecisionNodeData as D, type ExecutorRegistry as E, type FlowGraph as F, type WorkflowInput as G, type LlmClient as L, type NodeExecutor as N, type OutputNodeData as O, type PortDescriptor as P, type RunEvent as R, type SubgraphNodeData as S, type TerminalHost as T, type WorkflowResolver as W, type FlowNode as a, type LlmRouteRequest as b, RunIdentity as c, type RunIdentityJson as d, type FlowEdge as e, type FlowNodeData as f, type FlowNodeKind as g, type NodeRunStatus as h, type NoteNodeData as i, type TriggerNodeData as j, escapeSegment as k, type LlmRoute as l, type LlmRouteChoice as m, type TerminalExit as n, type TerminalSession as o, type TerminalSessionSpec as p, type WorkflowResolution as q, type WorkflowResolutionFailure as r, capabilityStatus as s, getLlmClient as t, getTerminalHost as u, getWorkflowResolver as v, isResolutionFailure as w, registerLlmClient as x, registerTerminalHost as y, registerWorkflowResolver as z };