@particle-academy/fancy-flow 0.45.0 → 0.47.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 (101) hide show
  1. package/README.md +29 -0
  2. package/dist/{ConfigFieldRenderer-Dzy4f6rG.d.cts → ConfigFieldRenderer-Cyir8-B8.d.cts} +2 -2
  3. package/dist/{ConfigFieldRenderer-WqJr-td_.d.ts → ConfigFieldRenderer-iMkZj0VK.d.ts} +2 -2
  4. package/dist/{FlowViewer-BbbUJe2b.d.cts → FlowViewer-CY3_vHSg.d.cts} +1 -1
  5. package/dist/{FlowViewer-csuaTL5t.d.ts → FlowViewer-DGW6t-80.d.ts} +1 -1
  6. package/dist/{capabilities-Brn74URS.d.ts → capabilities-DMUhnyZD.d.ts} +1 -1
  7. package/dist/{capabilities-CYCeIx6I.d.cts → capabilities-Dmd26aAs.d.cts} +1 -1
  8. package/dist/chunk-BHM5HGXH.js +79 -0
  9. package/dist/chunk-BHM5HGXH.js.map +1 -0
  10. package/dist/chunk-DR3GWVDG.js +196 -0
  11. package/dist/chunk-DR3GWVDG.js.map +1 -0
  12. package/dist/{chunk-3BKU4SQZ.js → chunk-ISXRQELT.js} +3 -4
  13. package/dist/chunk-ISXRQELT.js.map +1 -0
  14. package/dist/{chunk-OWENS2H5.js → chunk-NDEX655E.js} +1603 -36
  15. package/dist/chunk-NDEX655E.js.map +1 -0
  16. package/dist/{chunk-QZTGV3ZL.js → chunk-QVFVL5LQ.js} +3 -3
  17. package/dist/{chunk-QZTGV3ZL.js.map → chunk-QVFVL5LQ.js.map} +1 -1
  18. package/dist/{chunk-DHUFUDEX.js → chunk-Z2GRNYR2.js} +7 -6
  19. package/dist/chunk-Z2GRNYR2.js.map +1 -0
  20. package/dist/{chunk-A6RFLGWV.js → chunk-ZKJJWBDJ.js} +3 -5
  21. package/dist/chunk-ZKJJWBDJ.js.map +1 -0
  22. package/dist/durable/index.d.cts +490 -0
  23. package/dist/durable/index.d.ts +490 -0
  24. package/dist/durable.cjs +10991 -0
  25. package/dist/durable.cjs.map +1 -0
  26. package/dist/durable.js +552 -0
  27. package/dist/durable.js.map +1 -0
  28. package/dist/engine.cjs +10223 -47
  29. package/dist/engine.cjs.map +1 -1
  30. package/dist/engine.d.cts +9 -7
  31. package/dist/engine.d.ts +9 -7
  32. package/dist/engine.js +4 -6
  33. package/dist/engine.js.map +1 -1
  34. package/dist/fields/react-fancy.d.cts +4 -3
  35. package/dist/fields/react-fancy.d.ts +4 -3
  36. package/dist/index.cjs +336 -194
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.cts +12 -10
  39. package/dist/index.d.ts +12 -10
  40. package/dist/index.js +15 -93
  41. package/dist/index.js.map +1 -1
  42. package/dist/layout/index.d.cts +1 -1
  43. package/dist/layout/index.d.ts +1 -1
  44. package/dist/llm/prism.d.cts +2 -2
  45. package/dist/llm/prism.d.ts +2 -2
  46. package/dist/llm/vercel-ai.d.cts +2 -2
  47. package/dist/llm/vercel-ai.d.ts +2 -2
  48. package/dist/pause-9iT4tCEV.d.cts +96 -0
  49. package/dist/pause-9iT4tCEV.d.ts +96 -0
  50. package/dist/registry/index.d.cts +7 -6
  51. package/dist/registry/index.d.ts +7 -6
  52. package/dist/{registry-Budds73w.d.cts → registry-Bt55f63_.d.ts} +21 -2
  53. package/dist/{registry-D0I20HJ3.d.ts → registry-CkcSwmfR.d.cts} +21 -2
  54. package/dist/registry.cjs +3961 -3821
  55. package/dist/registry.cjs.map +1 -1
  56. package/dist/registry.js +3 -6
  57. package/dist/{run-cohort-CV7EcwGG.d.ts → run-cohort-Cyeny2Uu.d.ts} +3 -34
  58. package/dist/{run-cohort-DitsaYgS.d.cts → run-cohort-DdPPowJ3.d.cts} +3 -34
  59. package/dist/run-flow-1KiXhy59.d.cts +55 -0
  60. package/dist/run-flow-CQvTDBYk.d.ts +55 -0
  61. package/dist/runtime/index.d.cts +5 -3
  62. package/dist/runtime/index.d.ts +5 -3
  63. package/dist/runtime.cjs +1712 -430
  64. package/dist/runtime.cjs.map +1 -1
  65. package/dist/runtime.js +5 -6
  66. package/dist/schema/index.d.cts +1 -1
  67. package/dist/schema/index.d.ts +1 -1
  68. package/dist/schema.cjs +10369 -24
  69. package/dist/schema.cjs.map +1 -1
  70. package/dist/schema.js +4 -3
  71. package/dist/screens.cjs +1366 -38
  72. package/dist/screens.cjs.map +1 -1
  73. package/dist/screens.d.cts +2 -2
  74. package/dist/screens.d.ts +2 -2
  75. package/dist/screens.js +5 -8
  76. package/dist/screens.js.map +1 -1
  77. package/dist/{types-D6ZSKBly.d.ts → types--S2Kzx7w.d.cts} +3 -97
  78. package/dist/{types-nQOA8jpk.d.cts → types-ChkCRonF.d.ts} +3 -97
  79. package/dist/types-Jx1TwehV.d.cts +247 -0
  80. package/dist/types-Jx1TwehV.d.ts +247 -0
  81. package/dist/ux.cjs +10363 -2
  82. package/dist/ux.cjs.map +1 -1
  83. package/dist/ux.d.cts +3 -2
  84. package/dist/ux.d.ts +3 -2
  85. package/dist/ux.js +3 -1
  86. package/dist/ux.js.map +1 -1
  87. package/package.json +17 -3
  88. package/dist/chunk-3BKU4SQZ.js.map +0 -1
  89. package/dist/chunk-A6RFLGWV.js.map +0 -1
  90. package/dist/chunk-DHUFUDEX.js.map +0 -1
  91. package/dist/chunk-OWENS2H5.js.map +0 -1
  92. package/dist/chunk-PVROYW7C.js +0 -187
  93. package/dist/chunk-PVROYW7C.js.map +0 -1
  94. package/dist/chunk-TITD5W4Y.js +0 -26
  95. package/dist/chunk-TITD5W4Y.js.map +0 -1
  96. package/dist/chunk-VLATLVGH.js +0 -158
  97. package/dist/chunk-VLATLVGH.js.map +0 -1
  98. package/dist/chunk-WBXGZVLB.js +0 -1268
  99. package/dist/chunk-WBXGZVLB.js.map +0 -1
  100. package/dist/types-CMSrWVYM.d.cts +0 -109
  101. package/dist/types-CMSrWVYM.d.ts +0 -109
package/dist/runtime.cjs CHANGED
@@ -8,437 +8,115 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
8
 
9
9
  var ReactExports__default = /*#__PURE__*/_interopDefault(ReactExports);
10
10
 
11
- // src/registry/registry.ts
12
- var kinds = /* @__PURE__ */ new Map();
13
- var aliases = /* @__PURE__ */ new Map();
14
- var overrides = /* @__PURE__ */ new Map();
15
- function resolveKindId(id2) {
16
- if (kinds.has(id2)) return id2;
17
- const canonical = aliases.get(id2);
18
- return canonical && kinds.has(canonical) ? canonical : null;
19
- }
20
- function withOverride(kind) {
21
- if (!kind) return null;
22
- const patch = overrides.get(kind.name);
23
- return patch ? { ...kind, ...patch } : kind;
24
- }
25
- function getNodeKind(name) {
26
- const canonical = resolveKindId(name);
27
- return canonical ? withOverride(kinds.get(canonical)) : null;
28
- }
29
- function kindIds(kind) {
30
- return [kind.name, ...kind.aliases ?? []];
31
- }
32
-
33
- // src/registry/ports.ts
34
- function resolvePortSpec(spec, config) {
35
- if (spec === void 0) return void 0;
36
- if (typeof spec !== "function") return spec;
37
- try {
38
- const resolved = spec(config);
39
- return Array.isArray(resolved) ? resolved : void 0;
40
- } catch {
41
- return void 0;
11
+ // src/registry/builtin.ts
12
+ function RichInputPreview({ config }) {
13
+ const title = typeof config.title === "string" && config.title.trim() !== "" ? config.title : "Untitled step";
14
+ config.document;
15
+ {
16
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-rich-preview", children: [
17
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-rich-preview__title", children: title }),
18
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-rich-preview__unavailable", children: [
19
+ "Add ",
20
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: "@particle-academy/fancy-cms-ui" }),
21
+ " +",
22
+ " ",
23
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: "@particle-academy/react-fancy" }),
24
+ ", then",
25
+ " ",
26
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: 'import "@particle-academy/fancy-flow/rich-input"' }),
27
+ "."
28
+ ] })
29
+ ] });
42
30
  }
43
31
  }
44
- function nodeConfig(node) {
45
- return node.data?.config ?? {};
46
- }
47
- function resolveNodePorts(node, kind) {
48
- const config = nodeConfig(node);
49
- const data = node.data;
50
- return {
51
- inputs: data?.inputs ?? resolvePortSpec(kind?.inputs, config),
52
- outputs: data?.outputs ?? resolvePortSpec(kind?.outputs, config)
53
- };
54
- }
55
32
 
56
- // src/runtime/run-flow.ts
57
- async function runFlow(graph, executors, onEvent = () => {
58
- }, options = {}) {
59
- const { signal, initialInputs = {}, timeoutMs, depth = 0 } = options;
60
- const outputs = {};
61
- const portValues = /* @__PURE__ */ new Map();
62
- const completed = /* @__PURE__ */ new Set();
63
- const errors = [];
64
- const order = topoSort(graph);
65
- if (order === null) {
66
- const msg = "Cycle detected in flow graph \u2014 aborting.";
67
- onEvent({ type: "run-error", error: msg });
68
- return { ok: false, outputs, error: msg };
69
- }
70
- const incomingByNode = indexIncoming(graph.edges);
71
- const timer2 = timeoutMs ? setTimeout(() => errors.push(`Run timed out after ${timeoutMs}ms`), timeoutMs) : null;
72
- onEvent({ type: "run-start" });
73
- try {
74
- for (const node of order) {
75
- if (signal?.aborted) throw new Error("aborted");
76
- if (errors.length) break;
77
- const incoming = incomingByNode.get(node.id) ?? [];
78
- if (incoming.length > 0) {
79
- const anyActive = incoming.some((e) => portValues.has(`${e.source}:${e.sourceHandle ?? "out"}`));
80
- if (!anyActive) {
81
- onEvent({ type: "node-status", nodeId: node.id, status: "idle", text: "skipped" });
82
- continue;
83
- }
84
- }
85
- const visualKind = getNodeKind(node.type ?? "");
86
- const isLayout = visualKind?.category === "layout";
87
- const isAnnotation = node.type === "note" || visualKind?.category === "annotation";
88
- if (isLayout || isAnnotation) {
89
- onEvent({
90
- type: "node-status",
91
- nodeId: node.id,
92
- status: "idle",
93
- text: isLayout ? "lane" : "annotation"
94
- });
95
- continue;
96
- }
97
- onEvent({ type: "node-status", nodeId: node.id, status: "running" });
98
- const inputs = collectInputs(node, incoming, portValues, initialInputs);
99
- const exec = pickExecutor(executors, node);
100
- if (!exec) {
101
- const msg = `No executor registered for kind=${node.type}`;
102
- errors.push(msg);
103
- onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
104
- onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
105
- break;
106
- }
107
- try {
108
- const result = await Promise.resolve(
109
- exec({
110
- node,
111
- inputs,
112
- abort: (reason) => {
113
- throw new Error(reason ?? "aborted");
114
- },
115
- emit: onEvent,
116
- depth
117
- })
118
- );
119
- outputs[node.id] = result;
120
- const activated = activatedPorts(node, result);
121
- for (const portId of activated.ports) {
122
- portValues.set(`${node.id}:${portId}`, activated.value);
123
- onEvent({ type: "node-output", nodeId: node.id, portId, value: activated.value });
124
- }
125
- completed.add(node.id);
126
- onEvent({ type: "node-status", nodeId: node.id, status: "done" });
127
- } catch (e) {
128
- const msg = e instanceof Error ? e.message : String(e);
129
- errors.push(msg);
130
- onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
131
- onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
132
- break;
33
+ // node_modules/classcat/index.js
34
+ function cc(names) {
35
+ if (typeof names === "string" || typeof names === "number") return "" + names;
36
+ let out = "";
37
+ if (Array.isArray(names)) {
38
+ for (let i = 0, tmp; i < names.length; i++) {
39
+ if ((tmp = cc(names[i])) !== "") {
40
+ out += (out && " ") + tmp;
133
41
  }
134
42
  }
135
- } finally {
136
- if (timer2) clearTimeout(timer2);
43
+ } else {
44
+ for (let k in names) {
45
+ if (names[k]) out += (out && " ") + k;
46
+ }
137
47
  }
138
- const ok = errors.length === 0;
139
- onEvent({ type: "run-end", ok });
140
- return ok ? { ok, outputs } : { ok, outputs, error: errors[0] };
48
+ return out;
141
49
  }
142
- function indexIncoming(edges) {
143
- const map = /* @__PURE__ */ new Map();
144
- for (const e of edges) {
145
- const list = map.get(e.target) ?? [];
146
- list.push(e);
147
- map.set(e.target, list);
50
+
51
+ // node_modules/d3-dispatch/src/dispatch.js
52
+ var noop = { value: () => {
53
+ } };
54
+ function dispatch() {
55
+ for (var i = 0, n = arguments.length, _ = {}, t; i < n; ++i) {
56
+ if (!(t = arguments[i] + "") || t in _ || /[\s.]/.test(t)) throw new Error("illegal type: " + t);
57
+ _[t] = [];
148
58
  }
149
- return map;
59
+ return new Dispatch(_);
150
60
  }
151
- function topoSort(graph) {
152
- const inDegree = /* @__PURE__ */ new Map();
153
- for (const n of graph.nodes) inDegree.set(n.id, 0);
154
- for (const e of graph.edges) inDegree.set(e.target, (inDegree.get(e.target) ?? 0) + 1);
155
- const queue = [];
156
- for (const [id2, d] of inDegree) if (d === 0) queue.push(id2);
157
- const ordered = [];
158
- while (queue.length) {
159
- const id2 = queue.shift();
160
- ordered.push(id2);
161
- for (const e of graph.edges) {
162
- if (e.source !== id2) continue;
163
- const next = (inDegree.get(e.target) ?? 0) - 1;
164
- inDegree.set(e.target, next);
165
- if (next === 0) queue.push(e.target);
166
- }
167
- }
168
- if (ordered.length !== graph.nodes.length) return null;
169
- const byId = new Map(graph.nodes.map((n) => [n.id, n]));
170
- return ordered.map((id2) => byId.get(id2)).filter(Boolean);
61
+ function Dispatch(_) {
62
+ this._ = _;
171
63
  }
172
- function collectInputs(node, incoming, portValues, initial) {
173
- const inputs = { ...initial[node.id] ?? {} };
174
- for (const e of incoming) {
175
- const portId = e.targetHandle ?? "in";
176
- const key = `${e.source}:${e.sourceHandle ?? "out"}`;
177
- if (!portValues.has(key)) continue;
178
- inputs[portId] = portValues.get(key);
179
- }
180
- return inputs;
64
+ function parseTypenames(typenames, types) {
65
+ return typenames.trim().split(/^|\s+/).map(function(t) {
66
+ var name = "", i = t.indexOf(".");
67
+ if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);
68
+ if (t && !types.hasOwnProperty(t)) throw new Error("unknown type: " + t);
69
+ return { type: t, name };
70
+ });
181
71
  }
182
- function pickExecutor(executors, node) {
183
- if (executors[node.id]) return executors[node.id];
184
- if (node.type && executors[node.type]) return executors[node.type];
185
- const kind = node.type ? getNodeKind(node.type) : null;
186
- if (kind) {
187
- for (const id2 of kindIds(kind)) {
188
- if (executors[id2]) return executors[id2];
72
+ Dispatch.prototype = dispatch.prototype = {
73
+ constructor: Dispatch,
74
+ on: function(typename, callback) {
75
+ var _ = this._, T = parseTypenames(typename + "", _), t, i = -1, n = T.length;
76
+ if (arguments.length < 2) {
77
+ while (++i < n) if ((t = (typename = T[i]).type) && (t = get(_[t], typename.name))) return t;
78
+ return;
79
+ }
80
+ if (callback != null && typeof callback !== "function") throw new Error("invalid callback: " + callback);
81
+ while (++i < n) {
82
+ if (t = (typename = T[i]).type) _[t] = set(_[t], typename.name, callback);
83
+ else if (callback == null) for (t in _) _[t] = set(_[t], typename.name, null);
189
84
  }
85
+ return this;
86
+ },
87
+ copy: function() {
88
+ var copy = {}, _ = this._;
89
+ for (var t in _) copy[t] = _[t].slice();
90
+ return new Dispatch(copy);
91
+ },
92
+ call: function(type, that) {
93
+ if ((n = arguments.length - 2) > 0) for (var args = new Array(n), i = 0, n, t; i < n; ++i) args[i] = arguments[i + 2];
94
+ if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
95
+ for (t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
96
+ },
97
+ apply: function(type, that, args) {
98
+ if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
99
+ for (var t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
190
100
  }
191
- return executors["*"];
192
- }
193
- function activatedPorts(node, result) {
194
- if (result && typeof result === "object") {
195
- const r = result;
196
- if (typeof r.__port === "string") {
197
- return { ports: [r.__port], value: r.value };
101
+ };
102
+ function get(type, name) {
103
+ for (var i = 0, n = type.length, c; i < n; ++i) {
104
+ if ((c = type[i]).name === name) {
105
+ return c.value;
198
106
  }
199
- if (typeof r.branch === "string") {
200
- return { ports: [r.branch], value: r.value ?? r };
107
+ }
108
+ }
109
+ function set(type, name, callback) {
110
+ for (var i = 0, n = type.length; i < n; ++i) {
111
+ if (type[i].name === name) {
112
+ type[i] = noop, type = type.slice(0, i).concat(type.slice(i + 1));
113
+ break;
201
114
  }
202
115
  }
203
- const kind = getNodeKind(node.data?.kind ?? node.type ?? "") ?? void 0;
204
- const declared = resolveNodePorts(node, kind).outputs?.map((p) => p.id);
205
- return { ports: declared?.length ? declared : ["out"], value: result };
116
+ if (callback != null) type.push({ name, value: callback });
117
+ return type;
206
118
  }
207
-
208
- // src/runtime/run-cohort.ts
209
- async function runCohort(flows, executors, onEvent = () => {
210
- }, options = {}) {
211
- const { policy = "serial-guarded", guard, reason, ...runOptions } = options;
212
- if (policy === "parallel") {
213
- return Promise.all(
214
- flows.map(async (flow, index2) => ({
215
- ...await runFlow(flow, executors, (e) => onEvent(e, index2), runOptions),
216
- index: index2
217
- }))
218
- );
219
- }
220
- const results = [];
221
- for (const [index2, flow] of flows.entries()) {
222
- if (policy === "serial-guarded" && guard) {
223
- const why = () => reason?.(flow, index2) ?? "trigger precondition no longer holds";
224
- let passes;
225
- try {
226
- passes = await guard(flow, index2);
227
- } catch (error) {
228
- results.push({
229
- ok: false,
230
- outputs: {},
231
- index: index2,
232
- skipped: true,
233
- skippedReason: `guard could not be evaluated: ${errorText(error)}`
234
- });
235
- continue;
236
- }
237
- if (!passes) {
238
- results.push({ ok: false, outputs: {}, index: index2, skipped: true, skippedReason: why() });
239
- continue;
240
- }
241
- }
242
- results.push({
243
- ...await runFlow(flow, executors, (e) => onEvent(e, index2), runOptions),
244
- index: index2
245
- });
246
- }
247
- return results;
248
- }
249
- function errorText(error) {
250
- return error instanceof Error ? error.message : String(error);
251
- }
252
- function useFlowRun({ maxFeed = 200 } = {}) {
253
- const [statuses, setStatuses] = ReactExports.useState({});
254
- const [statusText, setStatusText] = ReactExports.useState({});
255
- const [outputs, setOutputs] = ReactExports.useState({});
256
- const [feed, setFeed] = ReactExports.useState([]);
257
- const [running, setRunning] = ReactExports.useState(false);
258
- const [lastResult, setLastResult] = ReactExports.useState(null);
259
- const abortRef = ReactExports.useRef(null);
260
- const handleEvent = ReactExports.useCallback(
261
- (e) => {
262
- switch (e.type) {
263
- case "node-status":
264
- setStatuses((s) => ({ ...s, [e.nodeId]: e.status }));
265
- setStatusText((t) => ({ ...t, [e.nodeId]: e.text }));
266
- appendFeed({ level: "status", text: `${e.nodeId} \u2192 ${e.status}${e.text ? ` (${e.text})` : ""}`, nodeId: e.nodeId });
267
- break;
268
- case "node-output":
269
- setOutputs((o) => ({ ...o, [e.nodeId]: e.value }));
270
- appendFeed({ level: "info", text: `${e.nodeId}.${e.portId} = ${preview(e.value)}`, nodeId: e.nodeId, detail: e.value });
271
- break;
272
- case "log":
273
- appendFeed({ level: e.level, text: e.message, nodeId: e.nodeId, detail: e.detail });
274
- break;
275
- case "run-start":
276
- appendFeed({ level: "info", text: "\u25B6 run started" });
277
- break;
278
- case "run-end":
279
- appendFeed({ level: e.ok ? "info" : "error", text: e.ok ? "\u2713 run complete" : "\u2717 run failed" });
280
- break;
281
- case "run-error":
282
- appendFeed({ level: "error", text: e.error });
283
- break;
284
- }
285
- function appendFeed(partial) {
286
- setFeed((f) => {
287
- const entry = { id: `${Date.now()}_${f.length}`, at: Date.now(), ...partial };
288
- const next = [...f, entry];
289
- return next.length > maxFeed ? next.slice(next.length - maxFeed) : next;
290
- });
291
- }
292
- },
293
- [maxFeed]
294
- );
295
- const run = ReactExports.useCallback(
296
- async (graph, executors, options = {}) => {
297
- if (running) {
298
- return { ok: false, outputs: {}, error: "another run is already in progress" };
299
- }
300
- const controller = new AbortController();
301
- abortRef.current = controller;
302
- const idleStatuses = {};
303
- for (const n of graph.nodes) idleStatuses[n.id] = "idle";
304
- setStatuses(idleStatuses);
305
- setStatusText({});
306
- setOutputs({});
307
- setRunning(true);
308
- try {
309
- const result = await runFlow(graph, executors, handleEvent, { ...options, signal: controller.signal });
310
- setLastResult(result);
311
- return result;
312
- } finally {
313
- setRunning(false);
314
- abortRef.current = null;
315
- }
316
- },
317
- [handleEvent, running]
318
- );
319
- const cancel = ReactExports.useCallback(() => abortRef.current?.abort(), []);
320
- const reset = ReactExports.useCallback(() => {
321
- setStatuses({});
322
- setStatusText({});
323
- setOutputs({});
324
- setFeed([]);
325
- setLastResult(null);
326
- }, []);
327
- return { statuses, statusText, outputs, feed, running, lastResult, run, cancel, reset };
328
- }
329
- function applyOutputsToNodes(nodes, outputs) {
330
- return nodes.map((n) => {
331
- const kind = getNodeKind(n.data?.kind ?? n.type ?? "");
332
- if (!kind?.reactive || !(n.id in outputs)) return n;
333
- return { ...n, data: { ...n.data, output: outputs[n.id] } };
334
- });
335
- }
336
- function applyStatusesToNodes(nodes, statuses, statusText) {
337
- return nodes.map((n) => ({
338
- ...n,
339
- data: {
340
- ...n.data,
341
- status: statuses[n.id] ?? n.data?.status ?? "idle",
342
- statusText: statusText[n.id] ?? n.data?.statusText
343
- }
344
- }));
345
- }
346
- function preview(v) {
347
- try {
348
- const s = JSON.stringify(v);
349
- return s && s.length > 60 ? s.slice(0, 57) + "\u2026" : s ?? String(v);
350
- } catch {
351
- return String(v);
352
- }
353
- }
354
-
355
- // node_modules/classcat/index.js
356
- function cc(names) {
357
- if (typeof names === "string" || typeof names === "number") return "" + names;
358
- let out = "";
359
- if (Array.isArray(names)) {
360
- for (let i = 0, tmp; i < names.length; i++) {
361
- if ((tmp = cc(names[i])) !== "") {
362
- out += (out && " ") + tmp;
363
- }
364
- }
365
- } else {
366
- for (let k in names) {
367
- if (names[k]) out += (out && " ") + k;
368
- }
369
- }
370
- return out;
371
- }
372
-
373
- // node_modules/d3-dispatch/src/dispatch.js
374
- var noop = { value: () => {
375
- } };
376
- function dispatch() {
377
- for (var i = 0, n = arguments.length, _ = {}, t; i < n; ++i) {
378
- if (!(t = arguments[i] + "") || t in _ || /[\s.]/.test(t)) throw new Error("illegal type: " + t);
379
- _[t] = [];
380
- }
381
- return new Dispatch(_);
382
- }
383
- function Dispatch(_) {
384
- this._ = _;
385
- }
386
- function parseTypenames(typenames, types) {
387
- return typenames.trim().split(/^|\s+/).map(function(t) {
388
- var name = "", i = t.indexOf(".");
389
- if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);
390
- if (t && !types.hasOwnProperty(t)) throw new Error("unknown type: " + t);
391
- return { type: t, name };
392
- });
393
- }
394
- Dispatch.prototype = dispatch.prototype = {
395
- constructor: Dispatch,
396
- on: function(typename, callback) {
397
- var _ = this._, T = parseTypenames(typename + "", _), t, i = -1, n = T.length;
398
- if (arguments.length < 2) {
399
- while (++i < n) if ((t = (typename = T[i]).type) && (t = get(_[t], typename.name))) return t;
400
- return;
401
- }
402
- if (callback != null && typeof callback !== "function") throw new Error("invalid callback: " + callback);
403
- while (++i < n) {
404
- if (t = (typename = T[i]).type) _[t] = set(_[t], typename.name, callback);
405
- else if (callback == null) for (t in _) _[t] = set(_[t], typename.name, null);
406
- }
407
- return this;
408
- },
409
- copy: function() {
410
- var copy = {}, _ = this._;
411
- for (var t in _) copy[t] = _[t].slice();
412
- return new Dispatch(copy);
413
- },
414
- call: function(type, that) {
415
- if ((n = arguments.length - 2) > 0) for (var args = new Array(n), i = 0, n, t; i < n; ++i) args[i] = arguments[i + 2];
416
- if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
417
- for (t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
418
- },
419
- apply: function(type, that, args) {
420
- if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
421
- for (var t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
422
- }
423
- };
424
- function get(type, name) {
425
- for (var i = 0, n = type.length, c; i < n; ++i) {
426
- if ((c = type[i]).name === name) {
427
- return c.value;
428
- }
429
- }
430
- }
431
- function set(type, name, callback) {
432
- for (var i = 0, n = type.length; i < n; ++i) {
433
- if (type[i].name === name) {
434
- type[i] = noop, type = type.slice(0, i).concat(type.slice(i + 1));
435
- break;
436
- }
437
- }
438
- if (callback != null) type.push({ name, value: callback });
439
- return type;
440
- }
441
- var dispatch_default = dispatch;
119
+ var dispatch_default = dispatch;
442
120
 
443
121
  // node_modules/d3-selection/src/namespaces.js
444
122
  var xhtml = "http://www.w3.org/1999/xhtml";
@@ -1341,7 +1019,7 @@ function defaultTouchable() {
1341
1019
  return navigator.maxTouchPoints || "ontouchstart" in this;
1342
1020
  }
1343
1021
  function drag_default() {
1344
- var filter2 = defaultFilter, container = defaultContainer, subject = defaultSubject, touchable = defaultTouchable, gestures = {}, listeners = dispatch_default("start", "drag", "end"), active = 0, mousedownx, mousedowny, mousemoving, touchending, clickDistance2 = 0;
1022
+ var filter2 = defaultFilter, container = defaultContainer, subject = defaultSubject, touchable = defaultTouchable, gestures = {}, listeners2 = dispatch_default("start", "drag", "end"), active = 0, mousedownx, mousedowny, mousemoving, touchending, clickDistance2 = 0;
1345
1023
  function drag(selection2) {
1346
1024
  selection2.on("mousedown.drag", mousedowned).filter(touchable).on("touchstart.drag", touchstarted).on("touchmove.drag", touchmoved, nonpassive).on("touchend.drag touchcancel.drag", touchended).style("touch-action", "none").style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
1347
1025
  }
@@ -1404,7 +1082,7 @@ function drag_default() {
1404
1082
  }
1405
1083
  }
1406
1084
  function beforestart(that, container2, event, d, identifier, touch) {
1407
- var dispatch2 = listeners.copy(), p = pointer_default(touch || event, container2), dx, dy, s;
1085
+ var dispatch2 = listeners2.copy(), p = pointer_default(touch || event, container2), dx, dy, s;
1408
1086
  if ((s = subject.call(that, new DragEvent("beforestart", {
1409
1087
  sourceEvent: event,
1410
1088
  target: drag,
@@ -1463,8 +1141,8 @@ function drag_default() {
1463
1141
  return arguments.length ? (touchable = typeof _ === "function" ? _ : constant_default2(!!_), drag) : touchable;
1464
1142
  };
1465
1143
  drag.on = function() {
1466
- var value = listeners.on.apply(listeners, arguments);
1467
- return value === listeners ? drag : value;
1144
+ var value = listeners2.on.apply(listeners2, arguments);
1145
+ return value === listeners2 ? drag : value;
1468
1146
  };
1469
1147
  drag.clickDistance = function(_) {
1470
1148
  return arguments.length ? (clickDistance2 = (_ = +_) * _, drag) : Math.sqrt(clickDistance2);
@@ -3018,7 +2696,7 @@ function defaultConstrain(transform2, extent, translateExtent) {
3018
2696
  );
3019
2697
  }
3020
2698
  function zoom_default2() {
3021
- var filter2 = defaultFilter2, extent = defaultExtent, constrain = defaultConstrain, wheelDelta2 = defaultWheelDelta, touchable = defaultTouchable2, scaleExtent = [0, Infinity], translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]], duration = 250, interpolate = zoom_default, listeners = dispatch_default("start", "zoom", "end"), touchstarting, touchfirst, touchending, touchDelay = 500, wheelDelay = 150, clickDistance2 = 0, tapDistance = 10;
2699
+ var filter2 = defaultFilter2, extent = defaultExtent, constrain = defaultConstrain, wheelDelta2 = defaultWheelDelta, touchable = defaultTouchable2, scaleExtent = [0, Infinity], translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]], duration = 250, interpolate = zoom_default, listeners2 = dispatch_default("start", "zoom", "end"), touchstarting, touchfirst, touchending, touchDelay = 500, wheelDelay = 150, clickDistance2 = 0, tapDistance = 10;
3022
2700
  function zoom(selection2) {
3023
2701
  selection2.property("__zoom", defaultTransform).on("wheel.zoom", wheeled, { passive: false }).on("mousedown.zoom", mousedowned).on("dblclick.zoom", dblclicked).filter(touchable).on("touchstart.zoom", touchstarted).on("touchmove.zoom", touchmoved).on("touchend.zoom touchcancel.zoom", touchended).style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
3024
2702
  }
@@ -3130,14 +2808,14 @@ function zoom_default2() {
3130
2808
  },
3131
2809
  emit: function(type) {
3132
2810
  var d = select_default2(this.that).datum();
3133
- listeners.call(
2811
+ listeners2.call(
3134
2812
  type,
3135
2813
  this.that,
3136
2814
  new ZoomEvent(type, {
3137
2815
  sourceEvent: this.sourceEvent,
3138
2816
  target: zoom,
3139
2817
  transform: this.that.__zoom,
3140
- dispatch: listeners
2818
+ dispatch: listeners2
3141
2819
  }),
3142
2820
  d
3143
2821
  );
@@ -3287,8 +2965,8 @@ function zoom_default2() {
3287
2965
  return arguments.length ? (interpolate = _, zoom) : interpolate;
3288
2966
  };
3289
2967
  zoom.on = function() {
3290
- var value = listeners.on.apply(listeners, arguments);
3291
- return value === listeners ? zoom : value;
2968
+ var value = listeners2.on.apply(listeners2, arguments);
2969
+ return value === listeners2 ? zoom : value;
3292
2970
  };
3293
2971
  zoom.clickDistance = function(_) {
3294
2972
  return arguments.length ? (clickDistance2 = (_ = +_) * _, zoom) : Math.sqrt(clickDistance2);
@@ -5545,6 +5223,8 @@ var ResizeControlVariant;
5545
5223
  ResizeControlVariant2["Line"] = "line";
5546
5224
  ResizeControlVariant2["Handle"] = "handle";
5547
5225
  })(ResizeControlVariant || (ResizeControlVariant = {}));
5226
+ var XY_RESIZER_HANDLE_POSITIONS = ["top-left", "top-right", "bottom-left", "bottom-right"];
5227
+ var XY_RESIZER_LINE_POSITIONS = ["top", "right", "bottom", "left"];
5548
5228
  function getResizeDirection({ width, prevWidth, height, prevHeight, affectsX, affectsY }) {
5549
5229
  const deltaWidth = width - prevWidth;
5550
5230
  const deltaHeight = height - prevHeight;
@@ -5961,20 +5641,20 @@ var use_sync_external_store_with_selector_default = { useSyncExternalStoreWithSe
5961
5641
  // node_modules/zustand/esm/vanilla.mjs
5962
5642
  var createStoreImpl = (createState) => {
5963
5643
  let state;
5964
- const listeners = /* @__PURE__ */ new Set();
5644
+ const listeners2 = /* @__PURE__ */ new Set();
5965
5645
  const setState = (partial, replace) => {
5966
5646
  const nextState = typeof partial === "function" ? partial(state) : partial;
5967
5647
  if (!Object.is(nextState, state)) {
5968
5648
  const previousState = state;
5969
5649
  state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
5970
- listeners.forEach((listener) => listener(state, previousState));
5650
+ listeners2.forEach((listener) => listener(state, previousState));
5971
5651
  }
5972
5652
  };
5973
5653
  const getState = () => state;
5974
5654
  const getInitialState2 = () => initialState;
5975
5655
  const subscribe = (listener) => {
5976
- listeners.add(listener);
5977
- return () => listeners.delete(listener);
5656
+ listeners2.add(listener);
5657
+ return () => listeners2.delete(listener);
5978
5658
  };
5979
5659
  const destroy = () => {
5980
5660
  if ((undefined ? undefined.MODE : void 0) !== "production") {
@@ -5982,7 +5662,7 @@ var createStoreImpl = (createState) => {
5982
5662
  "[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected."
5983
5663
  );
5984
5664
  }
5985
- listeners.clear();
5665
+ listeners2.clear();
5986
5666
  };
5987
5667
  const api = { setState, getState, getInitialState: getInitialState2, subscribe, destroy };
5988
5668
  const initialState = state = createState(setState, getState, api);
@@ -9250,9 +8930,1609 @@ function ResizeControl({ nodeId, position, variant = ResizeControlVariant.Handle
9250
8930
  ...color2 && { [isHandleControl ? "backgroundColor" : "borderColor"]: color2 }
9251
8931
  }, children: children2 });
9252
8932
  }
9253
- ReactExports.memo(ResizeControl);
8933
+ var NodeResizeControl = ReactExports.memo(ResizeControl);
8934
+ function NodeResizer({ nodeId, isVisible = true, handleClassName, handleStyle, lineClassName, lineStyle, color: color2, minWidth = 10, minHeight = 10, maxWidth = Number.MAX_VALUE, maxHeight = Number.MAX_VALUE, keepAspectRatio = false, autoScale = true, shouldResize, onResizeStart, onResize, onResizeEnd }) {
8935
+ if (!isVisible) {
8936
+ return null;
8937
+ }
8938
+ return jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [XY_RESIZER_LINE_POSITIONS.map((position) => jsxRuntime.jsx(NodeResizeControl, { className: lineClassName, style: lineStyle, nodeId, position, variant: ResizeControlVariant.Line, color: color2, minWidth, minHeight, maxWidth, maxHeight, onResizeStart, keepAspectRatio, autoScale, shouldResize, onResize, onResizeEnd }, position)), XY_RESIZER_HANDLE_POSITIONS.map((position) => jsxRuntime.jsx(NodeResizeControl, { className: handleClassName, style: handleStyle, nodeId, position, color: color2, minWidth, minHeight, maxWidth, maxHeight, onResizeStart, keepAspectRatio, autoScale, shouldResize, onResize, onResizeEnd }, position))] });
8939
+ }
8940
+ function LaneNodeInner(props) {
8941
+ const data = props.data;
8942
+ const config = data.config ?? {};
8943
+ const title = config.title ?? data.label ?? "Lane";
8944
+ const orientation = config.orientation ?? "horizontal";
8945
+ const accent = config.color ?? data.color ?? categoryAccent("layout");
8946
+ return /* @__PURE__ */ jsxRuntime.jsxs(
8947
+ "div",
8948
+ {
8949
+ className: ["ff-lane", `ff-lane--${orientation}`, props.selected ? "ff-lane--selected" : ""].filter(Boolean).join(" "),
8950
+ style: { borderColor: props.selected ? accent : void 0 },
8951
+ children: [
8952
+ /* @__PURE__ */ jsxRuntime.jsx(NodeResizer, { isVisible: props.selected ?? false, color: accent, minWidth: 160, minHeight: 72 }),
8953
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-lane__header", style: { background: accent }, children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-lane__title", children: title }) })
8954
+ ]
8955
+ }
8956
+ );
8957
+ }
8958
+ var LaneNode = ReactExports.memo(LaneNodeInner);
8959
+ var FlowEditorContext = ReactExports.createContext(null);
8960
+ FlowEditorContext.Provider;
8961
+ function useFlowEditorOptional() {
8962
+ return ReactExports.useContext(FlowEditorContext);
8963
+ }
8964
+ var NOTE_COLORS = {
8965
+ amber: { bg: "rgba(234, 179, 8, 0.14)", border: "rgba(234, 179, 8, 0.55)", title: "#a16207" },
8966
+ sky: { bg: "rgba(14, 165, 233, 0.12)", border: "rgba(14, 165, 233, 0.5)", title: "#0369a1" },
8967
+ violet: { bg: "rgba(139, 92, 246, 0.13)", border: "rgba(139, 92, 246, 0.5)", title: "#6d28d9" },
8968
+ emerald: { bg: "rgba(16, 185, 129, 0.13)", border: "rgba(16, 185, 129, 0.5)", title: "#047857" },
8969
+ rose: { bg: "rgba(244, 63, 94, 0.12)", border: "rgba(244, 63, 94, 0.5)", title: "#be123c" },
8970
+ slate: { bg: "rgba(100, 116, 139, 0.12)", border: "rgba(100, 116, 139, 0.5)", title: "#475569" }
8971
+ };
8972
+ function coerceColor(v) {
8973
+ return typeof v === "string" && v in NOTE_COLORS ? v : "amber";
8974
+ }
8975
+ function NoteNodeInner(props) {
8976
+ const data = props.data;
8977
+ const config = data.config ?? {};
8978
+ const api = useFlowEditorOptional();
8979
+ const hasConfigText = typeof config.text === "string" && config.text.length > 0;
8980
+ const text = hasConfigText ? String(config.text) : typeof data.body === "string" ? data.body : typeof data.text === "string" ? data.text : "";
8981
+ const title = typeof config.title === "string" && config.title || (!hasConfigText && typeof data.body === "string" && typeof data.label === "string" ? data.label : "");
8982
+ const skin = NOTE_COLORS[coerceColor(config.color)];
8983
+ const [editing, setEditing] = ReactExports.useState(false);
8984
+ const [draft, setDraft] = ReactExports.useState(text);
8985
+ const areaRef = ReactExports.useRef(null);
8986
+ ReactExports.useEffect(() => {
8987
+ if (editing && areaRef.current) {
8988
+ const el = areaRef.current;
8989
+ el.focus();
8990
+ el.setSelectionRange(el.value.length, el.value.length);
8991
+ }
8992
+ }, [editing]);
8993
+ const commit = () => {
8994
+ setEditing(false);
8995
+ if (!api || draft === text) return;
8996
+ const current = api.nodes.find((n) => n.id === props.id);
8997
+ if (!current) return;
8998
+ api.updateNode({
8999
+ ...current,
9000
+ data: { ...current.data, config: { ...config, text: draft } }
9001
+ });
9002
+ };
9003
+ return /* @__PURE__ */ jsxRuntime.jsxs(
9004
+ "div",
9005
+ {
9006
+ className: ["ff-note", props.selected ? "ff-note--selected" : ""].filter(Boolean).join(" "),
9007
+ style: { background: skin.bg, borderColor: skin.border },
9008
+ onDoubleClick: api ? () => {
9009
+ setDraft(text);
9010
+ setEditing(true);
9011
+ } : void 0,
9012
+ children: [
9013
+ /* @__PURE__ */ jsxRuntime.jsx(NodeResizer, { isVisible: props.selected ?? false, color: skin.border, minWidth: 140, minHeight: 80 }),
9014
+ title && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-note__title", style: { color: skin.title }, children: title }),
9015
+ editing ? /* @__PURE__ */ jsxRuntime.jsx(
9016
+ "textarea",
9017
+ {
9018
+ ref: areaRef,
9019
+ className: "ff-note__input nodrag nowheel",
9020
+ value: draft,
9021
+ onChange: (e) => setDraft(e.target.value),
9022
+ onBlur: commit,
9023
+ onKeyDown: (e) => {
9024
+ if (e.key === "Escape") setEditing(false);
9025
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) commit();
9026
+ }
9027
+ }
9028
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-note__body", children: text || /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-note__placeholder", children: api ? "Double-click to write a note\u2026" : "Note" }) })
9029
+ ]
9030
+ }
9031
+ );
9032
+ }
9033
+ var NoteNode = ReactExports.memo(NoteNodeInner);
9034
+
9035
+ // src/registry/capabilities.ts
9036
+ var llmClient = null;
9037
+ function getLlmClient() {
9038
+ return llmClient;
9039
+ }
9040
+ function isResolutionFailure(value) {
9041
+ return typeof value === "object" && value !== null && "reason" in value && value.reason !== void 0;
9042
+ }
9043
+ var workflowResolver = null;
9044
+ function getWorkflowResolver() {
9045
+ return workflowResolver;
9046
+ }
9254
9047
 
9255
- // src/runtime/use-flow-state.ts
9048
+ // src/registry/llm-router.ts
9049
+ function declaredRoutes(config) {
9050
+ const raw = config.routes;
9051
+ if (!Array.isArray(raw)) return [];
9052
+ return raw.map((r) => ({ port: String(r?.port ?? "").trim(), description: r?.description })).filter((r) => r.port !== "");
9053
+ }
9054
+ function resolveFallbackPort(routes, fallbackEnabled) {
9055
+ if (fallbackEnabled) return "fallback";
9056
+ return routes[0]?.port ?? "out";
9057
+ }
9058
+ var llmRouterExecutor = async (ctx) => {
9059
+ const config = ctx.node.data?.config ?? {};
9060
+ const routes = declaredRoutes(config);
9061
+ if (routes.length === 0) {
9062
+ ctx.abort("llm_router has no routes configured");
9063
+ }
9064
+ const client = getLlmClient();
9065
+ {
9066
+ ctx.abort(
9067
+ "No LLM client registered. Call registerLlmClient() with your provider adapter \u2014 fancy-flow ships the routing, not the model call."
9068
+ );
9069
+ }
9070
+ const fallbackEnabled = config.fallback !== false;
9071
+ const choice = await client.chooseRoute({
9072
+ system: typeof config.system === "string" ? config.system : void 0,
9073
+ prompt: String(config.prompt ?? ctx.inputs ?? ""),
9074
+ routes,
9075
+ provider: typeof config.provider === "string" ? config.provider : void 0,
9076
+ model: typeof config.model === "string" ? config.model : void 0,
9077
+ credential: typeof config.credential === "string" ? config.credential : void 0
9078
+ });
9079
+ const offered = new Set(routes.map((r) => r.port));
9080
+ let port = choice?.port ?? "";
9081
+ let reason = choice?.reason;
9082
+ if (!offered.has(port)) {
9083
+ const safe = resolveFallbackPort(routes, fallbackEnabled);
9084
+ ctx.emit({
9085
+ type: "log",
9086
+ nodeId: ctx.node.id,
9087
+ level: "warn",
9088
+ message: `llm_router: model returned "${port || "(nothing)"}", which is not a declared route. Routing to "${safe}".`
9089
+ });
9090
+ reason = reason ?? `unrecognised route "${port}"`;
9091
+ port = safe;
9092
+ }
9093
+ return { __port: port, value: { route: port, reason, input: ctx.inputs } };
9094
+ };
9095
+
9096
+ // src/registry/subflow.ts
9097
+ var DEFAULT_MAX_DEPTH = 8;
9098
+ function subflowMode(config) {
9099
+ const mode = config.mode;
9100
+ return mode === "stream" || mode === "both" ? mode : "output";
9101
+ }
9102
+ function subflowPorts(config) {
9103
+ const mode = subflowMode(config);
9104
+ const ports = [{ id: "out", label: "result" }];
9105
+ if (mode === "stream" || mode === "both") ports.unshift({ id: "stream", label: "stream" });
9106
+ return ports;
9107
+ }
9108
+ var subflowExecutor = async (ctx) => {
9109
+ const config = ctx.node.data?.config ?? {};
9110
+ const ref = String(config.workflow ?? "").trim();
9111
+ if (!ref) ctx.abort("subflow has no workflow reference configured");
9112
+ const resolver = getWorkflowResolver();
9113
+ {
9114
+ ctx.abort(
9115
+ "No workflow resolver registered. Call registerWorkflowResolver() so subflow can find the workflow it references."
9116
+ );
9117
+ }
9118
+ const maxDepth = Number.isFinite(config.maxDepth) ? Number(config.maxDepth) : DEFAULT_MAX_DEPTH;
9119
+ const depth = ctx.depth ?? 0;
9120
+ if (depth + 1 > maxDepth) {
9121
+ ctx.abort(
9122
+ `subflow depth limit reached (${maxDepth}) at "${ref}" \u2014 a workflow is referencing itself, directly or through a chain.`
9123
+ );
9124
+ }
9125
+ const pinned = config.version === void 0 || config.version === "" ? void 0 : Number(config.version);
9126
+ if (pinned !== void 0 && !Number.isInteger(pinned)) {
9127
+ ctx.abort(`subflow "${ref}" has a non-integer version pin (${String(config.version)}).`);
9128
+ }
9129
+ const resolved = await resolver(ref, pinned);
9130
+ const child = isResolutionFailure(resolved) ? ctx.abort(
9131
+ resolved.reason === "version-mismatch" ? resolved.message ?? `subflow "${ref}" is pinned to version ${pinned}, but the host has ${resolved.available ?? "a different version"}.` : resolved.message ?? `subflow could not resolve workflow "${ref}"`
9132
+ ) : resolved;
9133
+ if (!child) ctx.abort(`subflow could not resolve workflow "${ref}"`);
9134
+ const mode = subflowMode(config);
9135
+ const streaming = mode === "stream" || mode === "both";
9136
+ const forward = (event) => {
9137
+ if (!streaming) return;
9138
+ const detail = event.type === "node-status" ? `${event.nodeId} ${event.status}` : event.type === "run-end" ? `finished (${event.ok ? "ok" : "failed"})` : event.type;
9139
+ ctx.emit({
9140
+ type: "log",
9141
+ nodeId: ctx.node.id,
9142
+ level: "info",
9143
+ message: `[${ref}] ${detail}`
9144
+ });
9145
+ };
9146
+ const result = await runFlow(
9147
+ child,
9148
+ config.executors ?? {},
9149
+ forward,
9150
+ {
9151
+ initialInputs: config.inputs ?? {
9152
+ // With no explicit mapping, hand the parent's inputs to the child's
9153
+ // entry points — the obvious default, and it makes the simple case
9154
+ // require no configuration at all.
9155
+ __parent: ctx.inputs
9156
+ },
9157
+ depth: depth + 1,
9158
+ // Push THIS node onto the identity path, so a node inside the child graph
9159
+ // cannot share an idempotency key with a same-named node in the parent —
9160
+ // or with the same child graph invoked from a different parent node.
9161
+ // Attempt and the first-attempt clock ride down unchanged: the child's
9162
+ // work happens inside this node's attempt.
9163
+ run: ctx.run?.descend(ctx.node.id)
9164
+ }
9165
+ );
9166
+ if (!result.ok) {
9167
+ ctx.abort(`subflow "${ref}" failed: ${result.error ?? "unknown error"}`);
9168
+ }
9169
+ if (mode === "stream") {
9170
+ return { __port: "stream", value: result.outputs };
9171
+ }
9172
+ if (mode === "both") {
9173
+ return result.outputs;
9174
+ }
9175
+ return { __port: "out", value: result.outputs };
9176
+ };
9177
+
9178
+ // src/registry/builtin.ts
9179
+ function casePorts(cases) {
9180
+ const byPort = /* @__PURE__ */ new Map();
9181
+ if (cases && typeof cases === "object" && !Array.isArray(cases)) {
9182
+ for (const [match, port] of Object.entries(cases)) {
9183
+ if (typeof port !== "string" || port === "" || port === "default") continue;
9184
+ const matches = byPort.get(port) ?? [];
9185
+ matches.push(match);
9186
+ byPort.set(port, matches);
9187
+ }
9188
+ }
9189
+ const ports = [...byPort].map(([id2, matches]) => ({
9190
+ id: id2,
9191
+ label: matches.join("|")
9192
+ }));
9193
+ return [...ports, { id: "default", label: "default" }];
9194
+ }
9195
+ function routePorts(routes, fallback) {
9196
+ const ports = [];
9197
+ const seen = /* @__PURE__ */ new Set();
9198
+ if (Array.isArray(routes)) {
9199
+ for (const route of routes) {
9200
+ const id2 = route?.port;
9201
+ if (typeof id2 !== "string" || id2.trim() === "" || seen.has(id2)) continue;
9202
+ seen.add(id2);
9203
+ ports.push({ id: id2, label: id2 });
9204
+ }
9205
+ }
9206
+ if (fallback !== false && !seen.has("fallback")) {
9207
+ ports.push({ id: "fallback", label: "fallback" });
9208
+ }
9209
+ if (ports.length === 0) ports.push({ id: "out" });
9210
+ return ports;
9211
+ }
9212
+ var HTTP_METHODS = [
9213
+ { type: "select", key: "method", label: "Method", options: [
9214
+ { value: "GET", label: "GET" },
9215
+ { value: "POST", label: "POST" },
9216
+ { value: "PUT", label: "PUT" },
9217
+ { value: "PATCH", label: "PATCH" },
9218
+ { value: "DELETE", label: "DELETE" }
9219
+ ], default: "GET", required: true }
9220
+ ];
9221
+ var KINDS = [
9222
+ // ───────────── Triggers ─────────────
9223
+ {
9224
+ name: "@particle-academy/manual_trigger",
9225
+ aliases: ["manual_trigger", "@fancy/manual_trigger"],
9226
+ category: "trigger",
9227
+ label: "Manual",
9228
+ description: "Entry point fired when the user clicks Run.",
9229
+ icon: "\u26A1",
9230
+ inputs: [],
9231
+ outputs: [{ id: "out" }]
9232
+ },
9233
+ {
9234
+ name: "@particle-academy/webhook_trigger",
9235
+ aliases: ["webhook_trigger", "@fancy/webhook_trigger"],
9236
+ category: "trigger",
9237
+ label: "Webhook",
9238
+ description: "Triggered by an inbound HTTP request to a host-provided URL.",
9239
+ icon: "\u{1F4E1}",
9240
+ inputs: [],
9241
+ outputs: [{ id: "out", label: "payload" }],
9242
+ configSchema: [
9243
+ { type: "text", key: "path", label: "Path", placeholder: "/hooks/my-flow", required: true },
9244
+ { type: "select", key: "method", label: "Method", options: [
9245
+ { value: "POST", label: "POST" },
9246
+ { value: "GET", label: "GET" }
9247
+ ], default: "POST" },
9248
+ { type: "credential", key: "secret", label: "Verifying secret", credentialType: "webhook_secret" }
9249
+ ]
9250
+ },
9251
+ {
9252
+ name: "@particle-academy/schedule_trigger",
9253
+ aliases: ["schedule_trigger", "@fancy/schedule_trigger"],
9254
+ category: "trigger",
9255
+ label: "Schedule",
9256
+ description: "Fires on a cron schedule (host-implemented).",
9257
+ icon: "\u23F1",
9258
+ inputs: [],
9259
+ outputs: [{ id: "out" }],
9260
+ configSchema: [
9261
+ {
9262
+ type: "text",
9263
+ key: "cron",
9264
+ label: "Cron",
9265
+ placeholder: "*/5 * * * *",
9266
+ required: true,
9267
+ description: "Standard 5-field cron expression."
9268
+ },
9269
+ { type: "text", key: "timezone", label: "Timezone", placeholder: "UTC", default: "UTC" }
9270
+ ]
9271
+ },
9272
+ {
9273
+ name: "@particle-academy/user_input",
9274
+ // The keys an author declared on THIS node — the case a static list cannot
9275
+ // express, and the one issue #5 named.
9276
+ outputShape: (config) => (config.fields ?? []).filter((f) => typeof f.key === "string" && f.key !== "").map((f) => ({ path: f.key, type: "string", description: f.label })),
9277
+ aliases: ["user_input", "@fancy/user_input"],
9278
+ pausesForHuman: "input",
9279
+ category: "human",
9280
+ label: "User Input",
9281
+ description: "Pause the flow until the user submits the configured form.",
9282
+ icon: "\u270E",
9283
+ inputs: [{ id: "in" }],
9284
+ outputs: [{ id: "out", label: "values" }],
9285
+ configSchema: [
9286
+ { type: "text", key: "title", label: "Form title", default: "Need your input" },
9287
+ {
9288
+ type: "repeater",
9289
+ key: "fields",
9290
+ label: "Fields",
9291
+ description: "The form the run pauses on.",
9292
+ titleKey: "label",
9293
+ addLabel: "Add field",
9294
+ minItems: 1,
9295
+ fields: [
9296
+ { type: "text", key: "key", label: "Key", required: true, placeholder: "answer" },
9297
+ { type: "text", key: "label", label: "Label", required: true, placeholder: "Your answer" },
9298
+ {
9299
+ type: "select",
9300
+ key: "type",
9301
+ label: "Type",
9302
+ default: "text",
9303
+ options: [
9304
+ { value: "text", label: "Text" },
9305
+ { value: "textarea", label: "Long text" },
9306
+ { value: "number", label: "Number" },
9307
+ { value: "select", label: "Select" },
9308
+ { value: "switch", label: "Switch" }
9309
+ ]
9310
+ },
9311
+ { type: "switch", key: "required", label: "Required", default: false }
9312
+ ],
9313
+ default: [{ key: "answer", label: "Your answer", type: "textarea", required: true }]
9314
+ }
9315
+ ]
9316
+ },
9317
+ {
9318
+ name: "@particle-academy/rich_user_input",
9319
+ aliases: ["rich_user_input", "@fancy/rich_user_input"],
9320
+ pausesForHuman: "input",
9321
+ category: "human",
9322
+ label: "Rich User Input",
9323
+ description: "Pause the flow on a fully authored page \u2014 content, required reading, multi-section forms.",
9324
+ icon: "\u25A4",
9325
+ inputs: [{ id: "in" }],
9326
+ outputs: [{ id: "out", label: "values" }],
9327
+ configSchema: [
9328
+ { type: "text", key: "title", label: "Step title", default: "Please review" },
9329
+ {
9330
+ type: "document",
9331
+ key: "document",
9332
+ label: "Page content",
9333
+ documentType: "stages",
9334
+ description: "Authored with the host's document editor (fancy-cms Stages)."
9335
+ },
9336
+ { type: "switch", key: "requireConfirm", label: "Require explicit confirmation", default: true },
9337
+ { type: "text", key: "submitLabel", label: "Submit button", default: "Continue" }
9338
+ ],
9339
+ // Preview the authored page inside a FauxClient frame, so the canvas shows
9340
+ // what the person hitting this step will actually see.
9341
+ renderBody: (ctx) => ReactExports.createElement(RichInputPreview, { config: ctx.config ?? {} })
9342
+ },
9343
+ {
9344
+ name: "@particle-academy/subflow",
9345
+ aliases: ["subflow", "@fancy/subflow"],
9346
+ category: "logic",
9347
+ label: "SubFlow",
9348
+ description: "Run another workflow and bring its result \u2014 or its live progress \u2014 back into this one.",
9349
+ icon: "\u29C9",
9350
+ inputs: [{ id: "in" }],
9351
+ // The stream port only exists when something actually streams.
9352
+ outputs: (config) => subflowPorts(config ?? {}),
9353
+ // Core, not marketplace: it runs a child graph through this same engine and
9354
+ // needs nothing from outside except where workflows live.
9355
+ executor: subflowExecutor,
9356
+ configSchema: [
9357
+ {
9358
+ type: "text",
9359
+ key: "workflow",
9360
+ label: "Workflow",
9361
+ required: true,
9362
+ placeholder: "onboarding-v2",
9363
+ description: "Reference resolved by the host's registerWorkflowResolver()."
9364
+ },
9365
+ {
9366
+ type: "number",
9367
+ key: "version",
9368
+ label: "Pin to version",
9369
+ description: "Optional. Leave blank to always run the child's current version. Pinning fails the run loudly if the child has moved on \u2014 without it, someone edits the child and this flow silently runs different logic."
9370
+ },
9371
+ {
9372
+ type: "select",
9373
+ key: "mode",
9374
+ label: "Return",
9375
+ default: "output",
9376
+ options: [
9377
+ { value: "output", label: "Output when it finishes" },
9378
+ { value: "stream", label: "Stream progress as it runs" },
9379
+ { value: "both", label: "Both \u2014 stream, then output" }
9380
+ ],
9381
+ description: "Streaming adds a second port so a parent can show progress instead of a spinner."
9382
+ },
9383
+ {
9384
+ type: "keyvalue",
9385
+ key: "inputs",
9386
+ label: "Input mapping",
9387
+ description: "Values handed to the child's entry points. Omit to pass this node's inputs straight through.",
9388
+ keyLabel: "Name",
9389
+ valueLabel: "Value",
9390
+ addLabel: "Add input"
9391
+ },
9392
+ {
9393
+ type: "number",
9394
+ key: "maxDepth",
9395
+ label: "Max nesting depth",
9396
+ default: DEFAULT_MAX_DEPTH,
9397
+ min: 1,
9398
+ max: 32,
9399
+ description: "Guards against a workflow referencing itself."
9400
+ }
9401
+ ]
9402
+ },
9403
+ // ───────────── Logic ─────────────
9404
+ {
9405
+ name: "@particle-academy/branch",
9406
+ aliases: ["branch", "@fancy/branch"],
9407
+ category: "logic",
9408
+ label: "Branch",
9409
+ description: "Multi-way branch on a condition or value.",
9410
+ icon: "\u25C7",
9411
+ inputs: [{ id: "in" }],
9412
+ outputs: [{ id: "true", label: "true" }, { id: "false", label: "false" }],
9413
+ configSchema: [
9414
+ {
9415
+ type: "select",
9416
+ key: "match",
9417
+ label: "Match",
9418
+ default: "all",
9419
+ options: [
9420
+ { value: "all", label: "All conditions (AND)" },
9421
+ { value: "any", label: "Any condition (OR)" }
9422
+ ]
9423
+ },
9424
+ {
9425
+ type: "repeater",
9426
+ key: "conditions",
9427
+ label: "Conditions",
9428
+ description: "Routes to `true` when these match, otherwise `false`.",
9429
+ titleKey: "left",
9430
+ addLabel: "Add condition",
9431
+ minItems: 1,
9432
+ fields: [
9433
+ { type: "expression", key: "left", label: "Value", example: "{{ $json.status }}", required: true },
9434
+ {
9435
+ type: "select",
9436
+ key: "operator",
9437
+ label: "Is",
9438
+ default: "eq",
9439
+ options: [
9440
+ { value: "eq", label: "equal to" },
9441
+ { value: "neq", label: "not equal to" },
9442
+ { value: "contains", label: "contains" },
9443
+ { value: "not_contains", label: "does not contain" },
9444
+ { value: "gt", label: "greater than" },
9445
+ { value: "gte", label: "greater than or equal to" },
9446
+ { value: "lt", label: "less than" },
9447
+ { value: "lte", label: "less than or equal to" },
9448
+ { value: "truthy", label: "true" },
9449
+ { value: "falsy", label: "false" },
9450
+ { value: "empty", label: "empty" },
9451
+ { value: "not_empty", label: "not empty" }
9452
+ ]
9453
+ },
9454
+ { type: "text", key: "right", label: "Compared to", placeholder: "active" }
9455
+ ],
9456
+ default: [{ left: "", operator: "eq", right: "" }]
9457
+ },
9458
+ {
9459
+ type: "expression",
9460
+ key: "condition",
9461
+ label: "Raw expression (advanced)",
9462
+ example: "{{ $json.active && $json.score > 10 }}",
9463
+ description: "Escape hatch for logic the builder can't express. Overrides the conditions above when set."
9464
+ }
9465
+ ]
9466
+ },
9467
+ {
9468
+ name: "@particle-academy/switch_case",
9469
+ aliases: ["switch_case", "@fancy/switch_case"],
9470
+ category: "logic",
9471
+ label: "Switch",
9472
+ description: "Route to one of N labelled outputs based on a key.",
9473
+ icon: "\u2933",
9474
+ inputs: [{ id: "in" }],
9475
+ // Ports ARE the config: every distinct port a case routes to becomes an
9476
+ // output handle, plus the always-present `default`. Editing the cases map
9477
+ // moves the ports on the canvas and the ports the runtime activates.
9478
+ outputs: (config) => casePorts(config?.cases),
9479
+ configSchema: [
9480
+ { type: "expression", key: "value", label: "Switch on", example: "{{ $json.kind }}", required: true },
9481
+ {
9482
+ type: "keyvalue",
9483
+ key: "cases",
9484
+ label: "Cases",
9485
+ description: "Match value \u2192 output port. Unmatched input takes `default`.",
9486
+ keyLabel: "When value is",
9487
+ valueLabel: "Route to port",
9488
+ keyPlaceholder: "a",
9489
+ valuePlaceholder: "case_a",
9490
+ addLabel: "Add case",
9491
+ default: { a: "case_a", b: "case_b" }
9492
+ }
9493
+ ]
9494
+ },
9495
+ {
9496
+ name: "@particle-academy/for_each",
9497
+ outputShape: [
9498
+ { path: "items", type: "array" },
9499
+ { path: "count", type: "number" }
9500
+ ],
9501
+ aliases: ["for_each", "@fancy/for_each"],
9502
+ category: "logic",
9503
+ label: "For Each",
9504
+ description: "Iterate over a list, emitting each item on `item`.",
9505
+ icon: "\u21BB",
9506
+ inputs: [{ id: "in" }],
9507
+ outputs: [{ id: "item", label: "item" }, { id: "done", label: "done" }],
9508
+ configSchema: [
9509
+ { type: "expression", key: "source", label: "List", example: "{{ $json.users }}", required: true },
9510
+ { type: "number", key: "concurrency", label: "Concurrency", default: 1, min: 1, max: 50 }
9511
+ ]
9512
+ },
9513
+ {
9514
+ name: "@particle-academy/merge",
9515
+ aliases: ["merge", "@fancy/merge"],
9516
+ category: "logic",
9517
+ label: "Merge",
9518
+ description: "Combine multiple inputs into one object or array.",
9519
+ icon: "\u2295",
9520
+ inputs: [{ id: "a" }, { id: "b" }],
9521
+ outputs: [{ id: "out" }],
9522
+ configSchema: [
9523
+ {
9524
+ type: "select",
9525
+ key: "mode",
9526
+ label: "Mode",
9527
+ default: "merge",
9528
+ options: [{ value: "merge", label: "Object merge" }, { value: "concat", label: "Array concat" }]
9529
+ }
9530
+ ]
9531
+ },
9532
+ {
9533
+ name: "@particle-academy/wait",
9534
+ aliases: ["wait", "@fancy/wait"],
9535
+ category: "logic",
9536
+ label: "Wait",
9537
+ description: "Sleep or wait for an external event.",
9538
+ icon: "\u23F8",
9539
+ configSchema: [
9540
+ {
9541
+ type: "select",
9542
+ key: "mode",
9543
+ label: "Mode",
9544
+ default: "duration",
9545
+ options: [{ value: "duration", label: "Duration" }, { value: "until", label: "Until timestamp" }, { value: "event", label: "External event" }]
9546
+ },
9547
+ { type: "text", key: "duration", label: "Duration", placeholder: "5s, 10m, 1h", description: "Used when mode = duration." }
9548
+ ]
9549
+ },
9550
+ {
9551
+ name: "@particle-academy/transform",
9552
+ aliases: ["transform", "@fancy/transform"],
9553
+ category: "logic",
9554
+ label: "Transform",
9555
+ description: "Reshape data with an expression.",
9556
+ icon: "\u0192",
9557
+ configSchema: [
9558
+ {
9559
+ type: "select",
9560
+ key: "mode",
9561
+ label: "Build the output",
9562
+ default: "fields",
9563
+ options: [
9564
+ { value: "fields", label: "Field by field" },
9565
+ { value: "expression", label: "One expression" }
9566
+ ]
9567
+ },
9568
+ {
9569
+ type: "repeater",
9570
+ key: "fields",
9571
+ label: "Output fields",
9572
+ description: "Each row becomes a key on the result.",
9573
+ titleKey: "key",
9574
+ addLabel: "Add field",
9575
+ fields: [
9576
+ { type: "text", key: "key", label: "Key", required: true, placeholder: "name" },
9577
+ { type: "expression", key: "value", label: "Value", example: "{{ $json.first }}", required: true }
9578
+ ],
9579
+ default: [{ key: "", value: "" }]
9580
+ },
9581
+ {
9582
+ type: "expression",
9583
+ key: "expression",
9584
+ label: "Expression (advanced)",
9585
+ example: "{{ { id: $json.id, name: $json.first + ' ' + $json.last } }}",
9586
+ description: "Used when the mode above is set to one expression."
9587
+ }
9588
+ ]
9589
+ },
9590
+ // ───────────── Data ─────────────
9591
+ {
9592
+ name: "@particle-academy/memory_store",
9593
+ aliases: ["memory_store", "@fancy/memory_store"],
9594
+ category: "data",
9595
+ label: "Memory Store",
9596
+ description: "Read or write per-conversation memory.",
9597
+ icon: "\u{1F9E0}",
9598
+ configSchema: [
9599
+ {
9600
+ type: "select",
9601
+ key: "operation",
9602
+ label: "Operation",
9603
+ required: true,
9604
+ default: "read",
9605
+ options: [{ value: "read", label: "Read" }, { value: "write", label: "Write" }, { value: "append", label: "Append" }]
9606
+ },
9607
+ { type: "text", key: "key", label: "Key", placeholder: "user.preferences", required: true },
9608
+ { type: "expression", key: "value", label: "Value (write/append only)", example: "{{ $json }}" },
9609
+ { type: "credential", key: "store", label: "Memory store", credentialType: "memory_store" }
9610
+ ]
9611
+ },
9612
+ {
9613
+ name: "@particle-academy/data_store",
9614
+ aliases: ["data_store", "@fancy/data_store"],
9615
+ category: "data",
9616
+ label: "Data Store",
9617
+ description: "Key-value or table read/write against a host store.",
9618
+ icon: "\u{1F5C3}",
9619
+ configSchema: [
9620
+ {
9621
+ type: "select",
9622
+ key: "operation",
9623
+ label: "Operation",
9624
+ required: true,
9625
+ default: "get",
9626
+ options: [
9627
+ { value: "get", label: "Get" },
9628
+ { value: "set", label: "Set" },
9629
+ { value: "delete", label: "Delete" },
9630
+ { value: "query", label: "Query" },
9631
+ { value: "list", label: "List" }
9632
+ ]
9633
+ },
9634
+ { type: "text", key: "table", label: "Table / collection", required: true },
9635
+ { type: "text", key: "key", label: "Key" },
9636
+ {
9637
+ type: "keyvalue",
9638
+ key: "where",
9639
+ label: "Where",
9640
+ description: "Field/value pairs to match. For query and list operations.",
9641
+ keyLabel: "Field",
9642
+ valueLabel: "Equals",
9643
+ addLabel: "Add filter"
9644
+ },
9645
+ { type: "expression", key: "value", label: "Value (set only)", example: "{{ $json }}" },
9646
+ { type: "credential", key: "store", label: "Data store", credentialType: "data_store" }
9647
+ ]
9648
+ },
9649
+ {
9650
+ name: "@particle-academy/variable",
9651
+ aliases: ["variable", "@fancy/variable"],
9652
+ category: "data",
9653
+ label: "Variable",
9654
+ description: "Workflow-scoped value used by other nodes.",
9655
+ icon: "\u{1D4CD}",
9656
+ configSchema: [
9657
+ { type: "text", key: "name", label: "Name", required: true },
9658
+ { type: "expression", key: "value", label: "Value", required: true }
9659
+ ]
9660
+ },
9661
+ // ───────────── AI ─────────────
9662
+ {
9663
+ name: "@particle-academy/llm_call",
9664
+ // LlmClient::complete() -> array{text:string,data?:mixed,usage?:array,raw?:mixed}
9665
+ //
9666
+ // A FUNCTION of config, because `data` exists only when this node was asked
9667
+ // for a schema. Declaring it statically would offer `{{ $json.data }}` on
9668
+ // every llm_call in the kit, including the ones that emit nothing of the
9669
+ // sort — and a suggestion that resolves to null at runtime is worse than no
9670
+ // suggestion, since the picker is exactly what gave the author confidence.
9671
+ outputShape: (config) => {
9672
+ const asked = config?.response_schema != null && config.response_schema !== "" && !(Array.isArray(config.response_schema) && config.response_schema.length === 0);
9673
+ return [
9674
+ { path: "text", type: "string", description: "The model's completion." },
9675
+ ...asked ? [{ path: "data", type: "unknown", description: "The parsed, schema-checked result." }] : [],
9676
+ { path: "usage", type: "object", description: "Token counts, when the provider reports them." },
9677
+ { path: "raw", type: "unknown", description: "The provider's untouched response." }
9678
+ ];
9679
+ },
9680
+ aliases: ["llm_call", "@fancy/llm_call"],
9681
+ category: "ai",
9682
+ label: "LLM Call",
9683
+ description: "Send a prompt + context to a model and receive a response.",
9684
+ icon: "\u2726",
9685
+ configSchema: [
9686
+ {
9687
+ type: "select",
9688
+ key: "provider",
9689
+ label: "Provider",
9690
+ default: "anthropic",
9691
+ options: [
9692
+ { value: "anthropic", label: "Anthropic" },
9693
+ { value: "openai", label: "OpenAI" },
9694
+ { value: "custom", label: "Custom" }
9695
+ ]
9696
+ },
9697
+ { type: "text", key: "model", label: "Model", placeholder: "claude-sonnet-4-5", required: true },
9698
+ { type: "textarea", key: "system", label: "System prompt", rows: 4 },
9699
+ { type: "expression", key: "prompt", label: "User prompt", example: "{{ $json.question }}", required: true },
9700
+ { type: "number", key: "temperature", label: "Temperature", min: 0, max: 2, step: 0.1, default: 0.7 },
9701
+ { type: "number", key: "max_tokens", label: "Max tokens", min: 1, max: 8192, default: 1024 },
9702
+ {
9703
+ type: "repeater",
9704
+ key: "tools",
9705
+ label: "Tools",
9706
+ description: "Tools the model may call.",
9707
+ titleKey: "name",
9708
+ addLabel: "Add tool",
9709
+ fields: [
9710
+ { type: "text", key: "name", label: "Name", required: true, placeholder: "search_index" },
9711
+ { type: "text", key: "description", label: "When to use it" },
9712
+ {
9713
+ type: "json",
9714
+ key: "input_schema",
9715
+ label: "Input schema",
9716
+ description: "JSON Schema for the tool's arguments."
9717
+ }
9718
+ ]
9719
+ },
9720
+ {
9721
+ type: "json",
9722
+ key: "response_schema",
9723
+ label: "Response schema",
9724
+ description: "Optional JSON Schema. When set, the runtime asks the provider for schema-valid JSON and the node emits the parsed value as `data` \u2014 reference it as {{ $json.data }}. A response that cannot be parsed or does not match FAILS the node rather than passing an empty value downstream."
9725
+ },
9726
+ { type: "credential", key: "credential", label: "API credential", credentialType: "llm_credential" }
9727
+ ]
9728
+ },
9729
+ {
9730
+ name: "@particle-academy/llm_router",
9731
+ // Every id this node has ever shipped under keeps resolving — MOIC's saved
9732
+ // flows carry the bare `llm_branch`.
9733
+ aliases: ["llm_router", "llm_branch", "@fancy/llm_branch", "@fancy/llm_router"],
9734
+ category: "ai",
9735
+ label: "LLM Router",
9736
+ description: "Let a model choose which route the flow takes.",
9737
+ icon: "\u2727",
9738
+ inputs: [{ id: "in" }],
9739
+ // Each declared route is a port. The executor returns `{ __port: id }`
9740
+ // (or `Port.only(id)` on the PHP runtime) to pick one.
9741
+ outputs: (config) => routePorts(config?.routes, config?.fallback),
9742
+ // A shuttle, not an engine: it carries the routes out to whatever LLM
9743
+ // client the host registered and carries the choice back. No provider SDK
9744
+ // reaches core, so this stays a builtin without adding a dependency.
9745
+ executor: llmRouterExecutor,
9746
+ configSchema: [
9747
+ {
9748
+ type: "textarea",
9749
+ key: "system",
9750
+ label: "System prompt",
9751
+ rows: 3,
9752
+ description: "Optional framing for the routing decision."
9753
+ },
9754
+ {
9755
+ type: "expression",
9756
+ key: "prompt",
9757
+ label: "What to route on",
9758
+ example: "{{ $json.message }}",
9759
+ required: true
9760
+ },
9761
+ {
9762
+ type: "repeater",
9763
+ key: "routes",
9764
+ label: "Routes",
9765
+ description: "The model picks exactly one. Descriptions are what it chooses between \u2014 make them distinct.",
9766
+ titleKey: "port",
9767
+ addLabel: "Add route",
9768
+ minItems: 2,
9769
+ fields: [
9770
+ { type: "text", key: "port", label: "Port", required: true, placeholder: "billing" },
9771
+ {
9772
+ type: "text",
9773
+ key: "description",
9774
+ label: "When to choose it",
9775
+ required: true,
9776
+ placeholder: "The user is asking about an invoice, refund, or payment."
9777
+ }
9778
+ ],
9779
+ default: [
9780
+ { port: "a", description: "Describe when the model should pick this route." },
9781
+ { port: "b", description: "Describe when the model should pick this route." }
9782
+ ]
9783
+ },
9784
+ {
9785
+ type: "select",
9786
+ key: "provider",
9787
+ label: "Provider",
9788
+ default: "anthropic",
9789
+ options: [
9790
+ { value: "anthropic", label: "Anthropic" },
9791
+ { value: "openai", label: "OpenAI" },
9792
+ { value: "custom", label: "Custom" }
9793
+ ]
9794
+ },
9795
+ { type: "text", key: "model", label: "Model", placeholder: "claude-sonnet-4-5" },
9796
+ {
9797
+ type: "switch",
9798
+ key: "fallback",
9799
+ label: "Add a `fallback` port",
9800
+ default: true,
9801
+ description: "Where the flow goes if the model returns no usable route."
9802
+ },
9803
+ { type: "credential", key: "credential", label: "API credential", credentialType: "llm_credential" }
9804
+ ]
9805
+ },
9806
+ {
9807
+ name: "@particle-academy/tool_use",
9808
+ aliases: ["tool_use", "@fancy/tool_use"],
9809
+ category: "ai",
9810
+ label: "Tool Use",
9811
+ description: "Hand control to a host-registered tool by name.",
9812
+ icon: "\u{1F6E0}",
9813
+ configSchema: [
9814
+ { type: "text", key: "tool", label: "Tool name", placeholder: "search_index", required: true },
9815
+ { type: "expression", key: "args", label: "Arguments", example: "{{ { query: $json.q } }}" }
9816
+ ]
9817
+ },
9818
+ {
9819
+ name: "@particle-academy/embed_search",
9820
+ outputShape: [
9821
+ { path: "query", type: "string" },
9822
+ { path: "matches", type: "array", description: "Ranked results from the vector store." }
9823
+ ],
9824
+ aliases: ["embed_search", "@fancy/embed_search"],
9825
+ category: "ai",
9826
+ label: "Embed & Search",
9827
+ description: "Embed a query and search a vector store.",
9828
+ icon: "\u273A",
9829
+ configSchema: [
9830
+ { type: "expression", key: "query", label: "Query", required: true, example: "{{ $json.question }}" },
9831
+ { type: "number", key: "topK", label: "Top K", default: 5, min: 1, max: 50 },
9832
+ { type: "credential", key: "vectorStore", label: "Vector store", credentialType: "vector_store" }
9833
+ ]
9834
+ },
9835
+ // ───────────── IO ─────────────
9836
+ {
9837
+ name: "@particle-academy/api_request",
9838
+ // HttpClient::send() -> array{status:int,headers:array,body:mixed}
9839
+ outputShape: [
9840
+ { path: "status", type: "number", description: "HTTP status code." },
9841
+ { path: "headers", type: "object" },
9842
+ { path: "body", type: "unknown", description: "Decoded JSON when the response is JSON, otherwise the raw body." }
9843
+ ],
9844
+ aliases: ["api_request", "@fancy/api_request"],
9845
+ category: "io",
9846
+ label: "API Request",
9847
+ description: "HTTP request to any URL.",
9848
+ icon: "\u2194",
9849
+ configSchema: [
9850
+ ...HTTP_METHODS,
9851
+ { type: "text", key: "url", label: "URL", placeholder: "https://api.example.com/...", required: true },
9852
+ {
9853
+ type: "keyvalue",
9854
+ key: "headers",
9855
+ label: "Headers",
9856
+ keyLabel: "Header",
9857
+ valueLabel: "Value",
9858
+ keyPlaceholder: "content-type",
9859
+ valuePlaceholder: "application/json",
9860
+ addLabel: "Add header",
9861
+ default: { "content-type": "application/json" }
9862
+ },
9863
+ { type: "json", key: "body", label: "Body" },
9864
+ { type: "credential", key: "auth", label: "Auth", credentialType: "api_credential" }
9865
+ ]
9866
+ },
9867
+ {
9868
+ name: "@particle-academy/webhook_out",
9869
+ aliases: ["webhook_out", "@fancy/webhook_out"],
9870
+ category: "io",
9871
+ label: "Send Webhook",
9872
+ description: "POST a payload to a configured URL.",
9873
+ icon: "\u2197",
9874
+ configSchema: [
9875
+ { type: "text", key: "url", label: "URL", required: true },
9876
+ {
9877
+ type: "keyvalue",
9878
+ key: "headers",
9879
+ label: "Headers",
9880
+ keyLabel: "Header",
9881
+ valueLabel: "Value",
9882
+ addLabel: "Add header"
9883
+ },
9884
+ { type: "expression", key: "payload", label: "Payload", required: true, example: "{{ $json }}" }
9885
+ ]
9886
+ },
9887
+ // ───────────── Human ─────────────
9888
+ {
9889
+ name: "@particle-academy/human_approval",
9890
+ aliases: ["human_approval", "@fancy/human_approval"],
9891
+ pausesForHuman: "approval",
9892
+ category: "human",
9893
+ label: "Human Approval",
9894
+ description: "Pause until a human approves or denies.",
9895
+ icon: "\u2713",
9896
+ inputs: [{ id: "in" }],
9897
+ outputs: [{ id: "approved", label: "approved" }, { id: "denied", label: "denied" }],
9898
+ configSchema: [
9899
+ { type: "text", key: "title", label: "Approval title", default: "Approve action" },
9900
+ { type: "textarea", key: "description", label: "Description for approver", rows: 3 },
9901
+ { type: "credential", key: "channel", label: "Notify channel", credentialType: "notify_channel" }
9902
+ ]
9903
+ },
9904
+ {
9905
+ name: "@particle-academy/notify",
9906
+ aliases: ["notify", "@fancy/notify"],
9907
+ category: "human",
9908
+ label: "Notify",
9909
+ description: "Send a message via Slack / email / SMS / etc.",
9910
+ icon: "\u{1F514}",
9911
+ configSchema: [
9912
+ {
9913
+ type: "select",
9914
+ key: "channel",
9915
+ label: "Channel",
9916
+ default: "slack",
9917
+ options: [
9918
+ { value: "slack", label: "Slack" },
9919
+ { value: "email", label: "Email" },
9920
+ { value: "sms", label: "SMS" },
9921
+ { value: "discord", label: "Discord" }
9922
+ ]
9923
+ },
9924
+ { type: "text", key: "to", label: "To", required: true },
9925
+ { type: "expression", key: "message", label: "Message", required: true, example: "{{ $json.summary }}" }
9926
+ ]
9927
+ },
9928
+ // ───────────── Output ─────────────
9929
+ {
9930
+ name: "@particle-academy/output",
9931
+ aliases: ["output", "@fancy/output"],
9932
+ category: "output",
9933
+ label: "Output",
9934
+ description: "Terminal node \u2014 captures the workflow's result.",
9935
+ icon: "\u25CF",
9936
+ inputs: [{ id: "in" }],
9937
+ outputs: []
9938
+ },
9939
+ {
9940
+ name: "@particle-academy/log",
9941
+ aliases: ["log", "@fancy/log"],
9942
+ category: "output",
9943
+ label: "Log",
9944
+ description: "Send to the run feed.",
9945
+ icon: "\u2261",
9946
+ inputs: [{ id: "in" }],
9947
+ outputs: [],
9948
+ configSchema: [
9949
+ {
9950
+ type: "select",
9951
+ key: "level",
9952
+ label: "Level",
9953
+ default: "info",
9954
+ options: [{ value: "info", label: "info" }, { value: "warn", label: "warn" }, { value: "error", label: "error" }]
9955
+ },
9956
+ { type: "expression", key: "message", label: "Message", required: true, example: "{{ $json }}" }
9957
+ ]
9958
+ },
9959
+ {
9960
+ // Swimlane — a portless, resizable container. Child nodes are parented into
9961
+ // it; it never runs (the runtime skips the `layout` category). Uses its own
9962
+ // renderer (LaneNode) instead of the default card.
9963
+ name: "@particle-academy/lane",
9964
+ aliases: ["lane", "@fancy/lane"],
9965
+ category: "layout",
9966
+ label: "Lane",
9967
+ description: "A resizable swimlane \u2014 drop nodes into it to group them.",
9968
+ icon: "\u25A4",
9969
+ inputs: [],
9970
+ outputs: [],
9971
+ configSchema: [
9972
+ { type: "text", key: "title", label: "Title", default: "Lane" },
9973
+ {
9974
+ type: "select",
9975
+ key: "orientation",
9976
+ label: "Orientation",
9977
+ default: "horizontal",
9978
+ options: [{ value: "horizontal", label: "Row" }, { value: "vertical", label: "Column" }]
9979
+ }
9980
+ ],
9981
+ defaultConfig: { title: "Lane", orientation: "horizontal" },
9982
+ resizable: { minWidth: 160, minHeight: 72 },
9983
+ component: LaneNode
9984
+ },
9985
+ {
9986
+ // Note — a sticky-note annotation. Portless + visual-only: the runtime skips
9987
+ // the `annotation` category, so a note's text NEVER reaches a runner — it
9988
+ // rides in the document purely for people, editors, and MCP tools. Uses its
9989
+ // own renderer (NoteNode); double-click on the canvas to edit in place.
9990
+ name: "@particle-academy/note",
9991
+ aliases: ["note", "@fancy/note"],
9992
+ category: "annotation",
9993
+ label: "Note",
9994
+ description: "A sticky note that documents the canvas. Never runs \u2014 editor + agent only.",
9995
+ icon: "\u{1F5D2}",
9996
+ inputs: [],
9997
+ outputs: [],
9998
+ configSchema: [
9999
+ { type: "text", key: "title", label: "Title", placeholder: "Optional heading" },
10000
+ { type: "textarea", key: "text", label: "Note", rows: 5, placeholder: "What does this part of the flow do?" },
10001
+ { type: "select", key: "color", label: "Color", default: "amber", options: [
10002
+ { value: "amber", label: "Amber" },
10003
+ { value: "sky", label: "Sky" },
10004
+ { value: "violet", label: "Violet" },
10005
+ { value: "emerald", label: "Emerald" },
10006
+ { value: "rose", label: "Rose" },
10007
+ { value: "slate", label: "Slate" }
10008
+ ] }
10009
+ ],
10010
+ defaultConfig: { text: "", color: "amber" },
10011
+ resizable: { minWidth: 140, minHeight: 80 },
10012
+ component: NoteNode
10013
+ }
10014
+ ];
10015
+ function registerBuiltinKinds() {
10016
+ for (const k of KINDS) registerNodeKind(k);
10017
+ }
10018
+
10019
+ // src/registry/registry.ts
10020
+ var kinds = /* @__PURE__ */ new Map();
10021
+ var aliases = /* @__PURE__ */ new Map();
10022
+ var listeners = /* @__PURE__ */ new Set();
10023
+ var overrides = /* @__PURE__ */ new Map();
10024
+ var builtinsEnsured = false;
10025
+ function ensureBuiltinKinds() {
10026
+ if (builtinsEnsured) return;
10027
+ builtinsEnsured = true;
10028
+ registerBuiltinKinds();
10029
+ }
10030
+ function registerNodeKind(definition) {
10031
+ ensureBuiltinKinds();
10032
+ kinds.set(definition.name, definition);
10033
+ for (const alias of definition.aliases ?? []) aliases.set(alias, definition.name);
10034
+ notify();
10035
+ return () => {
10036
+ if (kinds.get(definition.name) === definition) {
10037
+ kinds.delete(definition.name);
10038
+ for (const alias of definition.aliases ?? []) {
10039
+ if (aliases.get(alias) === definition.name) aliases.delete(alias);
10040
+ }
10041
+ notify();
10042
+ }
10043
+ };
10044
+ }
10045
+ function resolveKindId(id2) {
10046
+ ensureBuiltinKinds();
10047
+ if (kinds.has(id2)) return id2;
10048
+ const canonical = aliases.get(id2);
10049
+ return canonical && kinds.has(canonical) ? canonical : null;
10050
+ }
10051
+ function withOverride(kind) {
10052
+ if (!kind) return null;
10053
+ const patch = overrides.get(kind.name);
10054
+ return patch ? { ...kind, ...patch } : kind;
10055
+ }
10056
+ function getNodeKind(name) {
10057
+ const canonical = resolveKindId(name);
10058
+ return canonical ? withOverride(kinds.get(canonical)) : null;
10059
+ }
10060
+ function kindIds(kind) {
10061
+ return [kind.name, ...kind.aliases ?? []];
10062
+ }
10063
+ function notify() {
10064
+ for (const l of listeners) l();
10065
+ }
10066
+ function categoryAccent(category) {
10067
+ switch (category) {
10068
+ case "trigger":
10069
+ return "#10b981";
10070
+ case "logic":
10071
+ return "#f59e0b";
10072
+ case "data":
10073
+ return "#0ea5e9";
10074
+ case "ai":
10075
+ return "#8b5cf6";
10076
+ case "io":
10077
+ return "#3b82f6";
10078
+ case "human":
10079
+ return "#ec4899";
10080
+ case "output":
10081
+ return "#a855f7";
10082
+ case "layout":
10083
+ return "#64748b";
10084
+ case "annotation":
10085
+ return "#eab308";
10086
+ default:
10087
+ return "#71717a";
10088
+ }
10089
+ }
10090
+
10091
+ // src/registry/ports.ts
10092
+ function resolvePortSpec(spec, config) {
10093
+ if (spec === void 0) return void 0;
10094
+ if (typeof spec !== "function") return spec;
10095
+ try {
10096
+ const resolved = spec(config);
10097
+ return Array.isArray(resolved) ? resolved : void 0;
10098
+ } catch {
10099
+ return void 0;
10100
+ }
10101
+ }
10102
+ function nodeConfig(node) {
10103
+ return node.data?.config ?? {};
10104
+ }
10105
+ function resolveNodePorts(node, kind) {
10106
+ const config = nodeConfig(node);
10107
+ const data = node.data;
10108
+ return {
10109
+ inputs: data?.inputs ?? resolvePortSpec(kind?.inputs, config),
10110
+ outputs: data?.outputs ?? resolvePortSpec(kind?.outputs, config)
10111
+ };
10112
+ }
10113
+
10114
+ // src/runtime/run-identity.ts
10115
+ function escapeSegment(value) {
10116
+ return value.replace(/%/g, "%25").replace(/\//g, "%2F").replace(/#/g, "%23");
10117
+ }
10118
+ function renderSegment(value, occurrence) {
10119
+ const escaped = escapeSegment(value);
10120
+ return occurrence === void 0 || occurrence === null ? escaped : `${escaped}#${occurrence}`;
10121
+ }
10122
+ function instant(value) {
10123
+ const ms = Date.parse(value);
10124
+ if (Number.isNaN(ms)) {
10125
+ throw new Error(
10126
+ `RunIdentity: firstAttemptAt is not a parseable timestamp: ${JSON.stringify(value)}`
10127
+ );
10128
+ }
10129
+ return ms;
10130
+ }
10131
+ var RunIdentity = class _RunIdentity {
10132
+ constructor(runKey, path = [], attempt = 1, firstAttemptAt = (/* @__PURE__ */ new Date()).toISOString()) {
10133
+ if (!runKey || runKey.trim() === "") {
10134
+ throw new Error("RunIdentity: runKey must be a non-empty string.");
10135
+ }
10136
+ this.runKey = runKey;
10137
+ this.path = Object.freeze([...path]);
10138
+ this.attempt = Math.max(1, Math.trunc(attempt));
10139
+ this.firstAttemptAt = firstAttemptAt;
10140
+ Object.freeze(this);
10141
+ }
10142
+ /**
10143
+ * The identity of one execution of one node — stable across retries of that
10144
+ * execution, distinct from every other execution of the same node.
10145
+ *
10146
+ * Pass `occurrence` when an executor runs the same node more than once at the
10147
+ * same level (a loop body, one item of a fan-out it drives itself).
10148
+ */
10149
+ stepKey(nodeId, occurrence) {
10150
+ return `${this.runKey}:${[...this.path, renderSegment(nodeId, occurrence)].join("/")}`;
10151
+ }
10152
+ /**
10153
+ * A child identity for work nested inside this step.
10154
+ *
10155
+ * `subflow` pushes the invoking node's id, so a node inside the child graph
10156
+ * cannot collide with a same-named node in the parent. Attempt and
10157
+ * `firstAttemptAt` are carried down unchanged: the nested work happens inside
10158
+ * this step's attempt, and shares its clock.
10159
+ */
10160
+ descend(segment, occurrence) {
10161
+ return new _RunIdentity(
10162
+ this.runKey,
10163
+ [...this.path, renderSegment(segment, occurrence)],
10164
+ this.attempt,
10165
+ this.firstAttemptAt
10166
+ );
10167
+ }
10168
+ /** A copy on a different attempt, first-attempt clock preserved. */
10169
+ withAttempt(attempt, firstAttemptAt) {
10170
+ return new _RunIdentity(
10171
+ this.runKey,
10172
+ this.path,
10173
+ attempt,
10174
+ firstAttemptAt ?? this.firstAttemptAt
10175
+ );
10176
+ }
10177
+ /**
10178
+ * May this attempt reuse the step key and still be deduplicated?
10179
+ *
10180
+ * Providers forget idempotency keys — Stripe after 24 hours. Past that
10181
+ * window, resending the key creates a second charge and sending a fresh one
10182
+ * creates a second charge, so **the caller must refuse rather than pick
10183
+ * between them**: a loud stuck run beats a silent double write.
10184
+ *
10185
+ * `true` on attempt 1 whatever the elapsed time — nothing was sent on an
10186
+ * earlier attempt, so there is nothing for the provider to have forgotten.
10187
+ * That is what lets a run park on a human gate for a week and then write.
10188
+ *
10189
+ * `windowSeconds: null` means the provider does not expire keys. `0` means
10190
+ * it does not dedupe at all, so no retry may reuse a key — it is a real
10191
+ * window, not an absent one, and the two must not be conflated: reading `0`
10192
+ * as `null` turns "this provider does not dedupe" into "this provider
10193
+ * dedupes forever", which is the more dangerous of the two by a distance.
10194
+ */
10195
+ isReplaySafe(windowSeconds, now2 = /* @__PURE__ */ new Date()) {
10196
+ if (this.attempt <= 1) return true;
10197
+ if (windowSeconds === null || windowSeconds === void 0) return true;
10198
+ if (windowSeconds <= 0) return false;
10199
+ const nowMs = typeof now2 === "string" ? instant(now2) : now2.getTime();
10200
+ const elapsedSeconds = Math.max(0, (nowMs - instant(this.firstAttemptAt)) / 1e3);
10201
+ return elapsedSeconds <= windowSeconds;
10202
+ }
10203
+ toJSON() {
10204
+ return {
10205
+ runKey: this.runKey,
10206
+ path: [...this.path],
10207
+ attempt: this.attempt,
10208
+ firstAttemptAt: this.firstAttemptAt
10209
+ };
10210
+ }
10211
+ /** Rebuild from a queue payload. */
10212
+ static from(value) {
10213
+ if (value instanceof _RunIdentity) return value;
10214
+ if (typeof value === "string") return new _RunIdentity(value);
10215
+ return new _RunIdentity(
10216
+ value.runKey,
10217
+ value.path ?? [],
10218
+ value.attempt ?? 1,
10219
+ value.firstAttemptAt ?? (/* @__PURE__ */ new Date()).toISOString()
10220
+ );
10221
+ }
10222
+ };
10223
+
10224
+ // src/runtime/run-flow.ts
10225
+ async function runFlow(graph, executors, onEvent = () => {
10226
+ }, options = {}) {
10227
+ const { signal, initialInputs = {}, timeoutMs, depth = 0, resumeOutputs = {} } = options;
10228
+ const run = options.run === void 0 ? void 0 : RunIdentity.from(options.run);
10229
+ const outputs = {};
10230
+ const portValues = /* @__PURE__ */ new Map();
10231
+ const completed = /* @__PURE__ */ new Set();
10232
+ const errors = [];
10233
+ const order = topoSort(graph);
10234
+ if (order === null) {
10235
+ const msg = "Cycle detected in flow graph \u2014 aborting.";
10236
+ onEvent({ type: "run-error", error: msg });
10237
+ return { ok: false, outputs, error: msg };
10238
+ }
10239
+ const incomingByNode = indexIncoming(graph.edges);
10240
+ const timer2 = timeoutMs ? setTimeout(() => errors.push(`Run timed out after ${timeoutMs}ms`), timeoutMs) : null;
10241
+ onEvent({ type: "run-start" });
10242
+ try {
10243
+ for (const node of order) {
10244
+ if (signal?.aborted) throw new Error("aborted");
10245
+ if (errors.length) break;
10246
+ if (Object.prototype.hasOwnProperty.call(resumeOutputs, node.id)) {
10247
+ const stored = resumeOutputs[node.id];
10248
+ outputs[node.id] = stored;
10249
+ const activated = activatedPorts(node, stored);
10250
+ for (const portId of activated.ports) {
10251
+ portValues.set(`${node.id}:${portId}`, activated.value);
10252
+ onEvent({ type: "node-output", nodeId: node.id, portId, value: activated.value });
10253
+ }
10254
+ completed.add(node.id);
10255
+ onEvent({ type: "node-status", nodeId: node.id, status: "done", text: "resumed" });
10256
+ continue;
10257
+ }
10258
+ const incoming = incomingByNode.get(node.id) ?? [];
10259
+ if (incoming.length > 0) {
10260
+ const anyActive = incoming.some((e) => portValues.has(`${e.source}:${e.sourceHandle ?? "out"}`));
10261
+ if (!anyActive) {
10262
+ onEvent({ type: "node-status", nodeId: node.id, status: "idle", text: "skipped" });
10263
+ continue;
10264
+ }
10265
+ }
10266
+ const visualKind = getNodeKind(node.type ?? "");
10267
+ const isLayout = visualKind?.category === "layout";
10268
+ const isAnnotation = node.type === "note" || visualKind?.category === "annotation";
10269
+ if (isLayout || isAnnotation) {
10270
+ onEvent({
10271
+ type: "node-status",
10272
+ nodeId: node.id,
10273
+ status: "idle",
10274
+ text: isLayout ? "lane" : "annotation"
10275
+ });
10276
+ continue;
10277
+ }
10278
+ onEvent({ type: "node-status", nodeId: node.id, status: "running" });
10279
+ const inputs = collectInputs(node, incoming, portValues, initialInputs);
10280
+ const exec = pickExecutor(executors, node);
10281
+ if (!exec) {
10282
+ const msg = `No executor registered for kind=${node.type}`;
10283
+ errors.push(msg);
10284
+ onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
10285
+ onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
10286
+ break;
10287
+ }
10288
+ try {
10289
+ const result = await Promise.resolve(
10290
+ exec({
10291
+ node,
10292
+ inputs,
10293
+ abort: (reason) => {
10294
+ throw new Error(reason ?? "aborted");
10295
+ },
10296
+ emit: onEvent,
10297
+ depth,
10298
+ run
10299
+ })
10300
+ );
10301
+ outputs[node.id] = result;
10302
+ const activated = activatedPorts(node, result);
10303
+ for (const portId of activated.ports) {
10304
+ portValues.set(`${node.id}:${portId}`, activated.value);
10305
+ onEvent({ type: "node-output", nodeId: node.id, portId, value: activated.value });
10306
+ }
10307
+ completed.add(node.id);
10308
+ onEvent({ type: "node-status", nodeId: node.id, status: "done" });
10309
+ } catch (e) {
10310
+ const msg = e instanceof Error ? e.message : String(e);
10311
+ errors.push(msg);
10312
+ onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
10313
+ onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
10314
+ break;
10315
+ }
10316
+ }
10317
+ } finally {
10318
+ if (timer2) clearTimeout(timer2);
10319
+ }
10320
+ const ok = errors.length === 0;
10321
+ onEvent({ type: "run-end", ok });
10322
+ return ok ? { ok, outputs } : { ok, outputs, error: errors[0] };
10323
+ }
10324
+ function indexIncoming(edges) {
10325
+ const map = /* @__PURE__ */ new Map();
10326
+ for (const e of edges) {
10327
+ const list = map.get(e.target) ?? [];
10328
+ list.push(e);
10329
+ map.set(e.target, list);
10330
+ }
10331
+ return map;
10332
+ }
10333
+ function topoSort(graph) {
10334
+ const inDegree = /* @__PURE__ */ new Map();
10335
+ for (const n of graph.nodes) inDegree.set(n.id, 0);
10336
+ for (const e of graph.edges) inDegree.set(e.target, (inDegree.get(e.target) ?? 0) + 1);
10337
+ const queue = [];
10338
+ for (const [id2, d] of inDegree) if (d === 0) queue.push(id2);
10339
+ const ordered = [];
10340
+ while (queue.length) {
10341
+ const id2 = queue.shift();
10342
+ ordered.push(id2);
10343
+ for (const e of graph.edges) {
10344
+ if (e.source !== id2) continue;
10345
+ const next = (inDegree.get(e.target) ?? 0) - 1;
10346
+ inDegree.set(e.target, next);
10347
+ if (next === 0) queue.push(e.target);
10348
+ }
10349
+ }
10350
+ if (ordered.length !== graph.nodes.length) return null;
10351
+ const byId = new Map(graph.nodes.map((n) => [n.id, n]));
10352
+ return ordered.map((id2) => byId.get(id2)).filter(Boolean);
10353
+ }
10354
+ function collectInputs(node, incoming, portValues, initial) {
10355
+ const inputs = { ...initial[node.id] ?? {} };
10356
+ for (const e of incoming) {
10357
+ const portId = e.targetHandle ?? "in";
10358
+ const key = `${e.source}:${e.sourceHandle ?? "out"}`;
10359
+ if (!portValues.has(key)) continue;
10360
+ inputs[portId] = portValues.get(key);
10361
+ }
10362
+ return inputs;
10363
+ }
10364
+ function pickExecutor(executors, node) {
10365
+ if (executors[node.id]) return executors[node.id];
10366
+ if (node.type && executors[node.type]) return executors[node.type];
10367
+ const kind = node.type ? getNodeKind(node.type) : null;
10368
+ if (kind) {
10369
+ for (const id2 of kindIds(kind)) {
10370
+ if (executors[id2]) return executors[id2];
10371
+ }
10372
+ }
10373
+ return executors["*"];
10374
+ }
10375
+ function activatedPorts(node, result) {
10376
+ if (result && typeof result === "object") {
10377
+ const r = result;
10378
+ if (typeof r.__port === "string") {
10379
+ return { ports: [r.__port], value: r.value };
10380
+ }
10381
+ if (typeof r.branch === "string") {
10382
+ return { ports: [r.branch], value: r.value ?? r };
10383
+ }
10384
+ }
10385
+ const kind = getNodeKind(node.data?.kind ?? node.type ?? "") ?? void 0;
10386
+ const declared = resolveNodePorts(node, kind).outputs?.map((p) => p.id);
10387
+ return { ports: declared?.length ? declared : ["out"], value: result };
10388
+ }
10389
+
10390
+ // src/runtime/run-cohort.ts
10391
+ async function runCohort(flows, executors, onEvent = () => {
10392
+ }, options = {}) {
10393
+ const { policy = "serial-guarded", guard, reason, ...runOptions } = options;
10394
+ if (policy === "parallel") {
10395
+ return Promise.all(
10396
+ flows.map(async (flow, index2) => ({
10397
+ ...await runFlow(flow, executors, (e) => onEvent(e, index2), runOptions),
10398
+ index: index2
10399
+ }))
10400
+ );
10401
+ }
10402
+ const results = [];
10403
+ for (const [index2, flow] of flows.entries()) {
10404
+ if (policy === "serial-guarded" && guard) {
10405
+ const why = () => reason?.(flow, index2) ?? "trigger precondition no longer holds";
10406
+ let passes;
10407
+ try {
10408
+ passes = await guard(flow, index2);
10409
+ } catch (error) {
10410
+ results.push({
10411
+ ok: false,
10412
+ outputs: {},
10413
+ index: index2,
10414
+ skipped: true,
10415
+ skippedReason: `guard could not be evaluated: ${errorText(error)}`
10416
+ });
10417
+ continue;
10418
+ }
10419
+ if (!passes) {
10420
+ results.push({ ok: false, outputs: {}, index: index2, skipped: true, skippedReason: why() });
10421
+ continue;
10422
+ }
10423
+ }
10424
+ results.push({
10425
+ ...await runFlow(flow, executors, (e) => onEvent(e, index2), runOptions),
10426
+ index: index2
10427
+ });
10428
+ }
10429
+ return results;
10430
+ }
10431
+ function errorText(error) {
10432
+ return error instanceof Error ? error.message : String(error);
10433
+ }
10434
+ function useFlowRun({ maxFeed = 200 } = {}) {
10435
+ const [statuses, setStatuses] = ReactExports.useState({});
10436
+ const [statusText, setStatusText] = ReactExports.useState({});
10437
+ const [outputs, setOutputs] = ReactExports.useState({});
10438
+ const [feed, setFeed] = ReactExports.useState([]);
10439
+ const [running, setRunning] = ReactExports.useState(false);
10440
+ const [lastResult, setLastResult] = ReactExports.useState(null);
10441
+ const abortRef = ReactExports.useRef(null);
10442
+ const handleEvent = ReactExports.useCallback(
10443
+ (e) => {
10444
+ switch (e.type) {
10445
+ case "node-status":
10446
+ setStatuses((s) => ({ ...s, [e.nodeId]: e.status }));
10447
+ setStatusText((t) => ({ ...t, [e.nodeId]: e.text }));
10448
+ appendFeed({ level: "status", text: `${e.nodeId} \u2192 ${e.status}${e.text ? ` (${e.text})` : ""}`, nodeId: e.nodeId });
10449
+ break;
10450
+ case "node-output":
10451
+ setOutputs((o) => ({ ...o, [e.nodeId]: e.value }));
10452
+ appendFeed({ level: "info", text: `${e.nodeId}.${e.portId} = ${preview(e.value)}`, nodeId: e.nodeId, detail: e.value });
10453
+ break;
10454
+ case "log":
10455
+ appendFeed({ level: e.level, text: e.message, nodeId: e.nodeId, detail: e.detail });
10456
+ break;
10457
+ case "run-start":
10458
+ appendFeed({ level: "info", text: "\u25B6 run started" });
10459
+ break;
10460
+ case "run-end":
10461
+ appendFeed({ level: e.ok ? "info" : "error", text: e.ok ? "\u2713 run complete" : "\u2717 run failed" });
10462
+ break;
10463
+ case "run-error":
10464
+ appendFeed({ level: "error", text: e.error });
10465
+ break;
10466
+ }
10467
+ function appendFeed(partial) {
10468
+ setFeed((f) => {
10469
+ const entry = { id: `${Date.now()}_${f.length}`, at: Date.now(), ...partial };
10470
+ const next = [...f, entry];
10471
+ return next.length > maxFeed ? next.slice(next.length - maxFeed) : next;
10472
+ });
10473
+ }
10474
+ },
10475
+ [maxFeed]
10476
+ );
10477
+ const run = ReactExports.useCallback(
10478
+ async (graph, executors, options = {}) => {
10479
+ if (running) {
10480
+ return { ok: false, outputs: {}, error: "another run is already in progress" };
10481
+ }
10482
+ const controller = new AbortController();
10483
+ abortRef.current = controller;
10484
+ const idleStatuses = {};
10485
+ for (const n of graph.nodes) idleStatuses[n.id] = "idle";
10486
+ setStatuses(idleStatuses);
10487
+ setStatusText({});
10488
+ setOutputs({});
10489
+ setRunning(true);
10490
+ try {
10491
+ const result = await runFlow(graph, executors, handleEvent, { ...options, signal: controller.signal });
10492
+ setLastResult(result);
10493
+ return result;
10494
+ } finally {
10495
+ setRunning(false);
10496
+ abortRef.current = null;
10497
+ }
10498
+ },
10499
+ [handleEvent, running]
10500
+ );
10501
+ const cancel = ReactExports.useCallback(() => abortRef.current?.abort(), []);
10502
+ const reset = ReactExports.useCallback(() => {
10503
+ setStatuses({});
10504
+ setStatusText({});
10505
+ setOutputs({});
10506
+ setFeed([]);
10507
+ setLastResult(null);
10508
+ }, []);
10509
+ return { statuses, statusText, outputs, feed, running, lastResult, run, cancel, reset };
10510
+ }
10511
+ function applyOutputsToNodes(nodes, outputs) {
10512
+ return nodes.map((n) => {
10513
+ const kind = getNodeKind(n.data?.kind ?? n.type ?? "");
10514
+ if (!kind?.reactive || !(n.id in outputs)) return n;
10515
+ return { ...n, data: { ...n.data, output: outputs[n.id] } };
10516
+ });
10517
+ }
10518
+ function applyStatusesToNodes(nodes, statuses, statusText) {
10519
+ return nodes.map((n) => ({
10520
+ ...n,
10521
+ data: {
10522
+ ...n.data,
10523
+ status: statuses[n.id] ?? n.data?.status ?? "idle",
10524
+ statusText: statusText[n.id] ?? n.data?.statusText
10525
+ }
10526
+ }));
10527
+ }
10528
+ function preview(v) {
10529
+ try {
10530
+ const s = JSON.stringify(v);
10531
+ return s && s.length > 60 ? s.slice(0, 57) + "\u2026" : s ?? String(v);
10532
+ } catch {
10533
+ return String(v);
10534
+ }
10535
+ }
9256
10536
  function useFlowState(initial) {
9257
10537
  const [nodes, setNodes] = ReactExports.useState(initial.nodes);
9258
10538
  const [edges, setEdges] = ReactExports.useState(initial.edges);
@@ -9378,9 +10658,11 @@ function useFlowHistory(flow) {
9378
10658
  };
9379
10659
  }
9380
10660
 
10661
+ exports.RunIdentity = RunIdentity;
9381
10662
  exports.applyOutputsToNodes = applyOutputsToNodes;
9382
10663
  exports.applyStatusesToNodes = applyStatusesToNodes;
9383
10664
  exports.createHistory = createHistory;
10665
+ exports.escapeSegment = escapeSegment;
9384
10666
  exports.runCohort = runCohort;
9385
10667
  exports.runFlow = runFlow;
9386
10668
  exports.useFlowHistory = useFlowHistory;