@particle-academy/fancy-flow 0.65.2 → 0.66.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -3
- package/dist/{FlowViewer-BI_AyyvU.d.ts → FlowViewer-BCc_BvuD.d.ts} +1 -1
- package/dist/{FlowViewer-DABx-6A-.d.cts → FlowViewer-CoAJ3o52.d.cts} +1 -1
- package/dist/{HumanPrompt-Bpp3JiVH.d.cts → HumanPrompt--pXDOyYC.d.cts} +2 -2
- package/dist/{HumanPrompt-BKThiyjB.d.ts → HumanPrompt-DXm_FP9z.d.ts} +2 -2
- package/dist/chunk-27RZJAE2.js +2586 -0
- package/dist/chunk-27RZJAE2.js.map +1 -0
- package/dist/{chunk-MBVX4ZRB.js → chunk-2QTEELJE.js} +4 -3
- package/dist/chunk-2QTEELJE.js.map +1 -0
- package/dist/chunk-2QXKDLGT.js +58 -0
- package/dist/chunk-2QXKDLGT.js.map +1 -0
- package/dist/{chunk-W5DPKJY4.js → chunk-6AYC3BWI.js} +4 -4
- package/dist/chunk-6AYC3BWI.js.map +1 -0
- package/dist/{chunk-5H54OTKT.js → chunk-6TH646CS.js} +127 -5
- package/dist/chunk-6TH646CS.js.map +1 -0
- package/dist/{chunk-EO6444T2.js → chunk-BUNFPBTF.js} +5 -4
- package/dist/chunk-BUNFPBTF.js.map +1 -0
- package/dist/{chunk-77V4QC6Y.js → chunk-FV3D366L.js} +3 -3
- package/dist/{chunk-77V4QC6Y.js.map → chunk-FV3D366L.js.map} +1 -1
- package/dist/{chunk-JF6WCRBU.js → chunk-OWENS2H5.js} +36 -1905
- package/dist/chunk-OWENS2H5.js.map +1 -0
- package/dist/{chunk-RIAFHQT5.js → chunk-RE3XNVSB.js} +3 -3
- package/dist/{chunk-RIAFHQT5.js.map → chunk-RE3XNVSB.js.map} +1 -1
- package/dist/connectors.d.cts +2 -2
- package/dist/connectors.d.ts +2 -2
- package/dist/durable/index.d.cts +2 -2
- package/dist/durable/index.d.ts +2 -2
- package/dist/durable.cjs +865 -9135
- package/dist/durable.cjs.map +1 -1
- package/dist/durable.js +2 -4
- package/dist/durable.js.map +1 -1
- package/dist/engine.cjs +927 -9278
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.cts +6 -7
- package/dist/engine.d.ts +6 -7
- package/dist/engine.js +5 -7
- package/dist/engine.js.map +1 -1
- package/dist/fields/react-fancy.d.cts +3 -3
- package/dist/fields/react-fancy.d.ts +3 -3
- package/dist/index.cjs +11009 -10282
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -36
- package/dist/index.d.ts +65 -36
- package/dist/index.js +22 -19
- 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.cjs.map +1 -1
- package/dist/llm/prism.d.cts +1 -2
- package/dist/llm/prism.d.ts +1 -2
- package/dist/llm/prism.js +1 -1
- package/dist/llm/vercel-ai.cjs.map +1 -1
- package/dist/llm/vercel-ai.d.cts +1 -2
- package/dist/llm/vercel-ai.d.ts +1 -2
- package/dist/llm/vercel-ai.js +1 -1
- package/dist/registry/index.d.cts +20 -9
- package/dist/registry/index.d.ts +20 -9
- package/dist/{registry-CntAdUcY.d.cts → registry-BcgllSk2.d.cts} +1 -1
- package/dist/{registry-DYoV-MQi.d.ts → registry-h-2QswZ5.d.ts} +1 -1
- package/dist/registry.cjs +11252 -10409
- package/dist/registry.cjs.map +1 -1
- package/dist/registry.js +5 -4
- package/dist/{run-cohort-C1nRh_mi.d.ts → run-cohort-CjUBWg6X.d.ts} +2 -2
- package/dist/{run-cohort-CBRAUj-z.d.cts → run-cohort-DL9JzzPU.d.cts} +2 -2
- package/dist/{run-flow-2XsHBwpd.d.cts → run-flow-B_8hgO5_.d.cts} +1 -1
- package/dist/{run-flow-D7AOCcfE.d.ts → run-flow-CxEGBOxd.d.ts} +1 -1
- package/dist/runtime/index.d.cts +5 -5
- package/dist/runtime/index.d.ts +5 -5
- package/dist/runtime.cjs +10843 -10194
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +5 -5
- package/dist/schema/index.d.cts +19 -6
- package/dist/schema/index.d.ts +19 -6
- package/dist/schema.cjs +881 -9142
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.js +4 -5
- package/dist/screens.cjs +842 -86
- package/dist/screens.cjs.map +1 -1
- package/dist/screens.d.cts +2 -2
- package/dist/screens.d.ts +2 -2
- package/dist/screens.js +7 -6
- package/dist/screens.js.map +1 -1
- package/dist/terminal/fancy-term-host.cjs +99 -0
- package/dist/terminal/fancy-term-host.cjs.map +1 -0
- package/dist/terminal/fancy-term-host.d.cts +76 -0
- package/dist/terminal/fancy-term-host.d.ts +76 -0
- package/dist/terminal/fancy-term-host.js +92 -0
- package/dist/terminal/fancy-term-host.js.map +1 -0
- package/dist/types-B-Syk9-M.d.cts +620 -0
- package/dist/types-B-Syk9-M.d.ts +620 -0
- package/dist/{types-D71SKA5A.d.cts → types-C2ATTmjN.d.cts} +1 -1
- package/dist/{types-Ckhz-YwC.d.ts → types-VunLGqrn.d.ts} +1 -1
- package/dist/ux.cjs +885 -9142
- package/dist/ux.cjs.map +1 -1
- package/dist/ux.d.cts +2 -2
- package/dist/ux.d.ts +2 -2
- package/dist/ux.js +2 -3
- package/dist/ux.js.map +1 -1
- package/package.json +11 -1
- package/dist/capabilities-BYa5p5jw.d.cts +0 -112
- package/dist/capabilities-COOXRiNL.d.ts +0 -112
- package/dist/chunk-5H54OTKT.js.map +0 -1
- package/dist/chunk-EO6444T2.js.map +0 -1
- package/dist/chunk-JF6WCRBU.js.map +0 -1
- package/dist/chunk-MBVX4ZRB.js.map +0 -1
- package/dist/chunk-UM4C46AF.js +0 -103
- package/dist/chunk-UM4C46AF.js.map +0 -1
- package/dist/chunk-USL4FMFU.js +0 -41
- package/dist/chunk-USL4FMFU.js.map +0 -1
- package/dist/chunk-W5DPKJY4.js.map +0 -1
- package/dist/types-JFYjPJAG.d.cts +0 -333
- 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 };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ReactNode, ComponentType } from 'react';
|
|
2
|
-
import { F as FlowGraph, a as FlowNode, P as PortDescriptor, N as NodeExecutor } from './types-
|
|
2
|
+
import { F as FlowGraph, a as FlowNode, P as PortDescriptor, N as NodeExecutor } from './types-B-Syk9-M.cjs';
|
|
3
3
|
import { NodeProps } from '@xyflow/react';
|
|
4
4
|
import { P as PauseAwaiting } from './pause-9iT4tCEV.cjs';
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ReactNode, ComponentType } from 'react';
|
|
2
|
-
import { F as FlowGraph, a as FlowNode, P as PortDescriptor, N as NodeExecutor } from './types-
|
|
2
|
+
import { F as FlowGraph, a as FlowNode, P as PortDescriptor, N as NodeExecutor } from './types-B-Syk9-M.js';
|
|
3
3
|
import { NodeProps } from '@xyflow/react';
|
|
4
4
|
import { P as PauseAwaiting } from './pause-9iT4tCEV.js';
|
|
5
5
|
|