@particle-academy/fancy-flow 0.10.1 → 0.12.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 (49) hide show
  1. package/dist/{chunk-2DMBJN2O.js → chunk-D6W5FMCT.js} +6 -5
  2. package/dist/chunk-D6W5FMCT.js.map +1 -0
  3. package/dist/{chunk-NCPQDVUE.js → chunk-IK5DS5JP.js} +262 -29
  4. package/dist/chunk-IK5DS5JP.js.map +1 -0
  5. package/dist/{chunk-HNBO4HP3.js → chunk-L4AX73Q6.js} +12 -5
  6. package/dist/chunk-L4AX73Q6.js.map +1 -0
  7. package/dist/{chunk-VEI743ZX.js → chunk-MFMRTRPO.js} +3 -3
  8. package/dist/{chunk-VEI743ZX.js.map → chunk-MFMRTRPO.js.map} +1 -1
  9. package/dist/{chunk-CPSOC27D.js → chunk-U5F22BHV.js} +18 -4
  10. package/dist/chunk-U5F22BHV.js.map +1 -0
  11. package/dist/engine.cjs +20 -3
  12. package/dist/engine.cjs.map +1 -1
  13. package/dist/engine.d.cts +4 -2
  14. package/dist/engine.d.ts +4 -2
  15. package/dist/engine.js +2 -2
  16. package/dist/index.cjs +417 -189
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +4 -4
  19. package/dist/index.d.ts +4 -4
  20. package/dist/index.js +9 -9
  21. package/dist/registry/index.d.cts +149 -5
  22. package/dist/registry/index.d.ts +149 -5
  23. package/dist/registry.cjs +475 -71
  24. package/dist/registry.cjs.map +1 -1
  25. package/dist/registry.js +3 -2
  26. package/dist/runtime/index.d.cts +1 -1
  27. package/dist/runtime/index.d.ts +1 -1
  28. package/dist/runtime.cjs +20 -3
  29. package/dist/runtime.cjs.map +1 -1
  30. package/dist/runtime.js +3 -3
  31. package/dist/schema/index.d.cts +1 -1
  32. package/dist/schema/index.d.ts +1 -1
  33. package/dist/schema.cjs +11 -3
  34. package/dist/schema.cjs.map +1 -1
  35. package/dist/schema.js +2 -2
  36. package/dist/{types-BS3Gwnkq.d.cts → types-Dg2REAz5.d.cts} +6 -0
  37. package/dist/{types-BS3Gwnkq.d.ts → types-Dg2REAz5.d.ts} +6 -0
  38. package/dist/{types-BocBFh6l.d.ts → types-DnQ4TEHN.d.cts} +16 -2
  39. package/dist/{types-DKqaUjF_.d.cts → types-DtXOwXjA.d.ts} +16 -2
  40. package/dist/ux.cjs +5 -0
  41. package/dist/ux.cjs.map +1 -1
  42. package/dist/ux.d.cts +2 -2
  43. package/dist/ux.d.ts +2 -2
  44. package/dist/ux.js +1 -1
  45. package/package.json +1 -1
  46. package/dist/chunk-2DMBJN2O.js.map +0 -1
  47. package/dist/chunk-CPSOC27D.js.map +0 -1
  48. package/dist/chunk-HNBO4HP3.js.map +0 -1
  49. package/dist/chunk-NCPQDVUE.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -12,19 +12,33 @@ var ReactExports__default = /*#__PURE__*/_interopDefault(ReactExports);
12
12
 
13
13
  // src/registry/registry.ts
14
14
  var kinds = /* @__PURE__ */ new Map();
15
+ var aliases = /* @__PURE__ */ new Map();
15
16
  var listeners = /* @__PURE__ */ new Set();
16
17
  function registerNodeKind(definition) {
17
18
  kinds.set(definition.name, definition);
19
+ for (const alias of definition.aliases ?? []) aliases.set(alias, definition.name);
18
20
  notify();
19
21
  return () => {
20
22
  if (kinds.get(definition.name) === definition) {
21
23
  kinds.delete(definition.name);
24
+ for (const alias of definition.aliases ?? []) {
25
+ if (aliases.get(alias) === definition.name) aliases.delete(alias);
26
+ }
22
27
  notify();
23
28
  }
24
29
  };
25
30
  }
31
+ function resolveKindId(id2) {
32
+ if (kinds.has(id2)) return id2;
33
+ const canonical = aliases.get(id2);
34
+ return canonical && kinds.has(canonical) ? canonical : null;
35
+ }
26
36
  function getNodeKind(name) {
27
- return kinds.get(name) ?? null;
37
+ const canonical = resolveKindId(name);
38
+ return canonical ? kinds.get(canonical) ?? null : null;
39
+ }
40
+ function kindIds(kind) {
41
+ return [kind.name, ...kind.aliases ?? []];
28
42
  }
29
43
  function listNodeKinds(category) {
30
44
  const all = Array.from(kinds.values());
@@ -198,6 +212,299 @@ function RichInputPreview({ config }) {
198
212
  ] });
199
213
  }
200
214
 
215
+ // src/registry/capabilities.ts
216
+ var llmClient = null;
217
+ function getLlmClient() {
218
+ return llmClient;
219
+ }
220
+ var workflowResolver = null;
221
+ function getWorkflowResolver() {
222
+ return workflowResolver;
223
+ }
224
+
225
+ // src/registry/llm-branch.ts
226
+ function declaredRoutes(config) {
227
+ const raw = config.routes;
228
+ if (!Array.isArray(raw)) return [];
229
+ return raw.map((r) => ({ port: String(r?.port ?? "").trim(), description: r?.description })).filter((r) => r.port !== "");
230
+ }
231
+ function resolveFallbackPort(routes, fallbackEnabled) {
232
+ if (fallbackEnabled) return "fallback";
233
+ return routes[0]?.port ?? "out";
234
+ }
235
+ var llmBranchExecutor = async (ctx) => {
236
+ const config = ctx.node.data?.config ?? {};
237
+ const routes = declaredRoutes(config);
238
+ if (routes.length === 0) {
239
+ ctx.abort("llm_branch has no routes configured");
240
+ }
241
+ const client = getLlmClient();
242
+ {
243
+ ctx.abort(
244
+ "No LLM client registered. Call registerLlmClient() with your provider adapter \u2014 fancy-flow ships the routing, not the model call."
245
+ );
246
+ }
247
+ const fallbackEnabled = config.fallback !== false;
248
+ const choice = await client.chooseRoute({
249
+ system: typeof config.system === "string" ? config.system : void 0,
250
+ prompt: String(config.prompt ?? ctx.inputs ?? ""),
251
+ routes,
252
+ provider: typeof config.provider === "string" ? config.provider : void 0,
253
+ model: typeof config.model === "string" ? config.model : void 0,
254
+ credential: typeof config.credential === "string" ? config.credential : void 0
255
+ });
256
+ const offered = new Set(routes.map((r) => r.port));
257
+ let port = choice?.port ?? "";
258
+ let reason = choice?.reason;
259
+ if (!offered.has(port)) {
260
+ const safe = resolveFallbackPort(routes, fallbackEnabled);
261
+ ctx.emit({
262
+ type: "log",
263
+ nodeId: ctx.node.id,
264
+ level: "warn",
265
+ message: `llm_branch: model returned "${port || "(nothing)"}", which is not a declared route. Routing to "${safe}".`
266
+ });
267
+ reason = reason ?? `unrecognised route "${port}"`;
268
+ port = safe;
269
+ }
270
+ return { __port: port, value: { route: port, reason, input: ctx.inputs } };
271
+ };
272
+
273
+ // src/registry/ports.ts
274
+ function resolvePortSpec(spec, config) {
275
+ if (spec === void 0) return void 0;
276
+ if (typeof spec !== "function") return spec;
277
+ try {
278
+ const resolved = spec(config);
279
+ return Array.isArray(resolved) ? resolved : void 0;
280
+ } catch {
281
+ return void 0;
282
+ }
283
+ }
284
+ function nodeConfig(node) {
285
+ return node.data?.config ?? {};
286
+ }
287
+ function resolveNodePorts(node, kind) {
288
+ const config = nodeConfig(node);
289
+ const data = node.data;
290
+ return {
291
+ inputs: data?.inputs ?? resolvePortSpec(kind?.inputs, config),
292
+ outputs: data?.outputs ?? resolvePortSpec(kind?.outputs, config)
293
+ };
294
+ }
295
+
296
+ // src/runtime/run-flow.ts
297
+ async function runFlow(graph, executors, onEvent = () => {
298
+ }, options = {}) {
299
+ const { signal, initialInputs = {}, timeoutMs, depth = 0 } = options;
300
+ const outputs = {};
301
+ const portValues = /* @__PURE__ */ new Map();
302
+ const completed = /* @__PURE__ */ new Set();
303
+ const errors = [];
304
+ const order = topoSort(graph);
305
+ if (order === null) {
306
+ const msg = "Cycle detected in flow graph \u2014 aborting.";
307
+ onEvent({ type: "run-error", error: msg });
308
+ return { ok: false, outputs, error: msg };
309
+ }
310
+ const incomingByNode = indexIncoming(graph.edges);
311
+ const timer2 = timeoutMs ? setTimeout(() => errors.push(`Run timed out after ${timeoutMs}ms`), timeoutMs) : null;
312
+ onEvent({ type: "run-start" });
313
+ try {
314
+ for (const node of order) {
315
+ if (signal?.aborted) throw new Error("aborted");
316
+ if (errors.length) break;
317
+ const incoming = incomingByNode.get(node.id) ?? [];
318
+ if (incoming.length > 0) {
319
+ const anyActive = incoming.some((e) => portValues.has(`${e.source}:${e.sourceHandle ?? "out"}`));
320
+ if (!anyActive) {
321
+ onEvent({ type: "node-status", nodeId: node.id, status: "idle", text: "skipped" });
322
+ continue;
323
+ }
324
+ }
325
+ if (node.type === "note") {
326
+ onEvent({ type: "node-status", nodeId: node.id, status: "idle", text: "annotation" });
327
+ continue;
328
+ }
329
+ onEvent({ type: "node-status", nodeId: node.id, status: "running" });
330
+ const inputs = collectInputs(node, incoming, portValues, initialInputs);
331
+ const exec = pickExecutor(executors, node);
332
+ if (!exec) {
333
+ const msg = `No executor registered for kind=${node.type}`;
334
+ errors.push(msg);
335
+ onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
336
+ onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
337
+ break;
338
+ }
339
+ try {
340
+ const result = await Promise.resolve(
341
+ exec({
342
+ node,
343
+ inputs,
344
+ abort: (reason) => {
345
+ throw new Error(reason ?? "aborted");
346
+ },
347
+ emit: onEvent,
348
+ depth
349
+ })
350
+ );
351
+ outputs[node.id] = result;
352
+ const activated = activatedPorts(node, result);
353
+ for (const portId of activated.ports) {
354
+ portValues.set(`${node.id}:${portId}`, activated.value);
355
+ onEvent({ type: "node-output", nodeId: node.id, portId, value: activated.value });
356
+ }
357
+ completed.add(node.id);
358
+ onEvent({ type: "node-status", nodeId: node.id, status: "done" });
359
+ } catch (e) {
360
+ const msg = e instanceof Error ? e.message : String(e);
361
+ errors.push(msg);
362
+ onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
363
+ onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
364
+ break;
365
+ }
366
+ }
367
+ } finally {
368
+ if (timer2) clearTimeout(timer2);
369
+ }
370
+ const ok = errors.length === 0;
371
+ onEvent({ type: "run-end", ok });
372
+ return ok ? { ok, outputs } : { ok, outputs, error: errors[0] };
373
+ }
374
+ function indexIncoming(edges) {
375
+ const map = /* @__PURE__ */ new Map();
376
+ for (const e of edges) {
377
+ const list = map.get(e.target) ?? [];
378
+ list.push(e);
379
+ map.set(e.target, list);
380
+ }
381
+ return map;
382
+ }
383
+ function topoSort(graph) {
384
+ const inDegree = /* @__PURE__ */ new Map();
385
+ for (const n of graph.nodes) inDegree.set(n.id, 0);
386
+ for (const e of graph.edges) inDegree.set(e.target, (inDegree.get(e.target) ?? 0) + 1);
387
+ const queue = [];
388
+ for (const [id2, d] of inDegree) if (d === 0) queue.push(id2);
389
+ const ordered = [];
390
+ while (queue.length) {
391
+ const id2 = queue.shift();
392
+ ordered.push(id2);
393
+ for (const e of graph.edges) {
394
+ if (e.source !== id2) continue;
395
+ const next = (inDegree.get(e.target) ?? 0) - 1;
396
+ inDegree.set(e.target, next);
397
+ if (next === 0) queue.push(e.target);
398
+ }
399
+ }
400
+ if (ordered.length !== graph.nodes.length) return null;
401
+ const byId = new Map(graph.nodes.map((n) => [n.id, n]));
402
+ return ordered.map((id2) => byId.get(id2)).filter(Boolean);
403
+ }
404
+ function collectInputs(node, incoming, portValues, initial) {
405
+ const inputs = { ...initial[node.id] ?? {} };
406
+ for (const e of incoming) {
407
+ const portId = e.targetHandle ?? "in";
408
+ const val = portValues.get(`${e.source}:${e.sourceHandle ?? "out"}`);
409
+ inputs[portId] = val;
410
+ }
411
+ return inputs;
412
+ }
413
+ function pickExecutor(executors, node) {
414
+ if (executors[node.id]) return executors[node.id];
415
+ if (node.type && executors[node.type]) return executors[node.type];
416
+ const kind = node.type ? getNodeKind(node.type) : null;
417
+ if (kind) {
418
+ for (const id2 of kindIds(kind)) {
419
+ if (executors[id2]) return executors[id2];
420
+ }
421
+ }
422
+ return executors["*"];
423
+ }
424
+ function activatedPorts(node, result) {
425
+ if (result && typeof result === "object") {
426
+ const r = result;
427
+ if (typeof r.__port === "string") {
428
+ return { ports: [r.__port], value: r.value };
429
+ }
430
+ if (typeof r.branch === "string") {
431
+ return { ports: [r.branch], value: r.value ?? r };
432
+ }
433
+ }
434
+ const kind = getNodeKind(node.data?.kind ?? node.type ?? "") ?? void 0;
435
+ const declared = resolveNodePorts(node, kind).outputs?.map((p) => p.id);
436
+ return { ports: declared?.length ? declared : ["out"], value: result };
437
+ }
438
+
439
+ // src/registry/subflow.ts
440
+ var DEFAULT_MAX_DEPTH = 8;
441
+ function subflowMode(config) {
442
+ const mode = config.mode;
443
+ return mode === "stream" || mode === "both" ? mode : "output";
444
+ }
445
+ function subflowPorts(config) {
446
+ const mode = subflowMode(config);
447
+ const ports = [{ id: "out", label: "result" }];
448
+ if (mode === "stream" || mode === "both") ports.unshift({ id: "stream", label: "stream" });
449
+ return ports;
450
+ }
451
+ var subflowExecutor = async (ctx) => {
452
+ const config = ctx.node.data?.config ?? {};
453
+ const ref = String(config.workflow ?? "").trim();
454
+ if (!ref) ctx.abort("subflow has no workflow reference configured");
455
+ const resolver = getWorkflowResolver();
456
+ {
457
+ ctx.abort(
458
+ "No workflow resolver registered. Call registerWorkflowResolver() so subflow can find the workflow it references."
459
+ );
460
+ }
461
+ const maxDepth = Number.isFinite(config.maxDepth) ? Number(config.maxDepth) : DEFAULT_MAX_DEPTH;
462
+ const depth = ctx.depth ?? 0;
463
+ if (depth + 1 > maxDepth) {
464
+ ctx.abort(
465
+ `subflow depth limit reached (${maxDepth}) at "${ref}" \u2014 a workflow is referencing itself, directly or through a chain.`
466
+ );
467
+ }
468
+ const child = await resolver(ref);
469
+ if (!child) ctx.abort(`subflow could not resolve workflow "${ref}"`);
470
+ const mode = subflowMode(config);
471
+ const streaming = mode === "stream" || mode === "both";
472
+ const forward = (event) => {
473
+ if (!streaming) return;
474
+ const detail = event.type === "node-status" ? `${event.nodeId} ${event.status}` : event.type === "run-end" ? `finished (${event.ok ? "ok" : "failed"})` : event.type;
475
+ ctx.emit({
476
+ type: "log",
477
+ nodeId: ctx.node.id,
478
+ level: "info",
479
+ message: `[${ref}] ${detail}`
480
+ });
481
+ };
482
+ const result = await runFlow(
483
+ child,
484
+ config.executors ?? {},
485
+ forward,
486
+ {
487
+ initialInputs: config.inputs ?? {
488
+ // With no explicit mapping, hand the parent's inputs to the child's
489
+ // entry points — the obvious default, and it makes the simple case
490
+ // require no configuration at all.
491
+ __parent: ctx.inputs
492
+ },
493
+ depth: depth + 1
494
+ }
495
+ );
496
+ if (!result.ok) {
497
+ ctx.abort(`subflow "${ref}" failed: ${result.error ?? "unknown error"}`);
498
+ }
499
+ if (mode === "stream") {
500
+ return { __port: "stream", value: result.outputs };
501
+ }
502
+ if (mode === "both") {
503
+ return result.outputs;
504
+ }
505
+ return { __port: "out", value: result.outputs };
506
+ };
507
+
201
508
  // src/registry/builtin.ts
202
509
  function casePorts(cases) {
203
510
  const byPort = /* @__PURE__ */ new Map();
@@ -244,7 +551,8 @@ var HTTP_METHODS = [
244
551
  var KINDS = [
245
552
  // ───────────── Triggers ─────────────
246
553
  {
247
- name: "manual_trigger",
554
+ name: "@particle-academy/manual_trigger",
555
+ aliases: ["manual_trigger", "@fancy/manual_trigger"],
248
556
  category: "trigger",
249
557
  label: "Manual",
250
558
  description: "Entry point fired when the user clicks Run.",
@@ -253,7 +561,8 @@ var KINDS = [
253
561
  outputs: [{ id: "out" }]
254
562
  },
255
563
  {
256
- name: "webhook_trigger",
564
+ name: "@particle-academy/webhook_trigger",
565
+ aliases: ["webhook_trigger", "@fancy/webhook_trigger"],
257
566
  category: "trigger",
258
567
  label: "Webhook",
259
568
  description: "Triggered by an inbound HTTP request to a host-provided URL.",
@@ -270,7 +579,8 @@ var KINDS = [
270
579
  ]
271
580
  },
272
581
  {
273
- name: "schedule_trigger",
582
+ name: "@particle-academy/schedule_trigger",
583
+ aliases: ["schedule_trigger", "@fancy/schedule_trigger"],
274
584
  category: "trigger",
275
585
  label: "Schedule",
276
586
  description: "Fires on a cron schedule (host-implemented).",
@@ -290,7 +600,8 @@ var KINDS = [
290
600
  ]
291
601
  },
292
602
  {
293
- name: "user_input",
603
+ name: "@particle-academy/user_input",
604
+ aliases: ["user_input", "@fancy/user_input"],
294
605
  category: "human",
295
606
  label: "User Input",
296
607
  description: "Pause the flow until the user submits the configured form.",
@@ -330,7 +641,8 @@ var KINDS = [
330
641
  ]
331
642
  },
332
643
  {
333
- name: "rich_user_input",
644
+ name: "@particle-academy/rich_user_input",
645
+ aliases: ["rich_user_input", "@fancy/rich_user_input"],
334
646
  category: "human",
335
647
  label: "Rich User Input",
336
648
  description: "Pause the flow on a fully authored page \u2014 content, required reading, multi-section forms.",
@@ -353,9 +665,61 @@ var KINDS = [
353
665
  // what the person hitting this step will actually see.
354
666
  renderBody: (ctx) => ReactExports.createElement(RichInputPreview, { config: ctx.config ?? {} })
355
667
  },
668
+ {
669
+ name: "@particle-academy/subflow",
670
+ aliases: ["subflow", "@fancy/subflow"],
671
+ category: "logic",
672
+ label: "SubFlow",
673
+ description: "Run another workflow and bring its result \u2014 or its live progress \u2014 back into this one.",
674
+ icon: "\u29C9",
675
+ inputs: [{ id: "in" }],
676
+ // The stream port only exists when something actually streams.
677
+ outputs: (config) => subflowPorts(config ?? {}),
678
+ // Core, not marketplace: it runs a child graph through this same engine and
679
+ // needs nothing from outside except where workflows live.
680
+ executor: subflowExecutor,
681
+ configSchema: [
682
+ {
683
+ type: "text",
684
+ key: "workflow",
685
+ label: "Workflow",
686
+ required: true,
687
+ placeholder: "onboarding-v2",
688
+ description: "Reference resolved by the host's registerWorkflowResolver()."
689
+ },
690
+ {
691
+ type: "select",
692
+ key: "mode",
693
+ label: "Return",
694
+ default: "output",
695
+ options: [
696
+ { value: "output", label: "Output when it finishes" },
697
+ { value: "stream", label: "Stream progress as it runs" },
698
+ { value: "both", label: "Both \u2014 stream, then output" }
699
+ ],
700
+ description: "Streaming adds a second port so a parent can show progress instead of a spinner."
701
+ },
702
+ {
703
+ type: "json",
704
+ key: "inputs",
705
+ label: "Input mapping",
706
+ description: "Entry-point inputs for the child run. Omit to pass this node's inputs straight through."
707
+ },
708
+ {
709
+ type: "number",
710
+ key: "maxDepth",
711
+ label: "Max nesting depth",
712
+ default: DEFAULT_MAX_DEPTH,
713
+ min: 1,
714
+ max: 32,
715
+ description: "Guards against a workflow referencing itself."
716
+ }
717
+ ]
718
+ },
356
719
  // ───────────── Logic ─────────────
357
720
  {
358
- name: "branch",
721
+ name: "@particle-academy/branch",
722
+ aliases: ["branch", "@fancy/branch"],
359
723
  category: "logic",
360
724
  label: "Branch",
361
725
  description: "Multi-way branch on a condition or value.",
@@ -367,7 +731,8 @@ var KINDS = [
367
731
  ]
368
732
  },
369
733
  {
370
- name: "switch_case",
734
+ name: "@particle-academy/switch_case",
735
+ aliases: ["switch_case", "@fancy/switch_case"],
371
736
  category: "logic",
372
737
  label: "Switch",
373
738
  description: "Route to one of N labelled outputs based on a key.",
@@ -394,7 +759,8 @@ var KINDS = [
394
759
  ]
395
760
  },
396
761
  {
397
- name: "for_each",
762
+ name: "@particle-academy/for_each",
763
+ aliases: ["for_each", "@fancy/for_each"],
398
764
  category: "logic",
399
765
  label: "For Each",
400
766
  description: "Iterate over a list, emitting each item on `item`.",
@@ -407,7 +773,8 @@ var KINDS = [
407
773
  ]
408
774
  },
409
775
  {
410
- name: "merge",
776
+ name: "@particle-academy/merge",
777
+ aliases: ["merge", "@fancy/merge"],
411
778
  category: "logic",
412
779
  label: "Merge",
413
780
  description: "Combine multiple inputs into one object or array.",
@@ -425,7 +792,8 @@ var KINDS = [
425
792
  ]
426
793
  },
427
794
  {
428
- name: "wait",
795
+ name: "@particle-academy/wait",
796
+ aliases: ["wait", "@fancy/wait"],
429
797
  category: "logic",
430
798
  label: "Wait",
431
799
  description: "Sleep or wait for an external event.",
@@ -442,7 +810,8 @@ var KINDS = [
442
810
  ]
443
811
  },
444
812
  {
445
- name: "transform",
813
+ name: "@particle-academy/transform",
814
+ aliases: ["transform", "@fancy/transform"],
446
815
  category: "logic",
447
816
  label: "Transform",
448
817
  description: "Reshape data with an expression.",
@@ -459,7 +828,8 @@ var KINDS = [
459
828
  },
460
829
  // ───────────── Data ─────────────
461
830
  {
462
- name: "memory_store",
831
+ name: "@particle-academy/memory_store",
832
+ aliases: ["memory_store", "@fancy/memory_store"],
463
833
  category: "data",
464
834
  label: "Memory Store",
465
835
  description: "Read or write per-conversation memory.",
@@ -479,7 +849,8 @@ var KINDS = [
479
849
  ]
480
850
  },
481
851
  {
482
- name: "data_store",
852
+ name: "@particle-academy/data_store",
853
+ aliases: ["data_store", "@fancy/data_store"],
483
854
  category: "data",
484
855
  label: "Data Store",
485
856
  description: "Key-value or table read/write against a host store.",
@@ -507,7 +878,8 @@ var KINDS = [
507
878
  ]
508
879
  },
509
880
  {
510
- name: "variable",
881
+ name: "@particle-academy/variable",
882
+ aliases: ["variable", "@fancy/variable"],
511
883
  category: "data",
512
884
  label: "Variable",
513
885
  description: "Workflow-scoped value used by other nodes.",
@@ -519,7 +891,8 @@ var KINDS = [
519
891
  },
520
892
  // ───────────── AI ─────────────
521
893
  {
522
- name: "llm_call",
894
+ name: "@particle-academy/llm_call",
895
+ aliases: ["llm_call", "@fancy/llm_call"],
523
896
  category: "ai",
524
897
  label: "LLM Call",
525
898
  description: "Send a prompt + context to a model and receive a response.",
@@ -546,7 +919,8 @@ var KINDS = [
546
919
  ]
547
920
  },
548
921
  {
549
- name: "llm_branch",
922
+ name: "@particle-academy/llm_branch",
923
+ aliases: ["llm_branch", "@fancy/llm_branch"],
550
924
  category: "ai",
551
925
  label: "LLM Router",
552
926
  description: "Let a model choose which route the flow takes.",
@@ -555,6 +929,10 @@ var KINDS = [
555
929
  // Each declared route is a port. The executor returns `{ __port: id }`
556
930
  // (or `Port.only(id)` on the PHP runtime) to pick one.
557
931
  outputs: (config) => routePorts(config?.routes, config?.fallback),
932
+ // A shuttle, not an engine: it carries the routes out to whatever LLM
933
+ // client the host registered and carries the choice back. No provider SDK
934
+ // reaches core, so this stays a builtin without adding a dependency.
935
+ executor: llmBranchExecutor,
558
936
  configSchema: [
559
937
  {
560
938
  type: "textarea",
@@ -616,7 +994,8 @@ var KINDS = [
616
994
  ]
617
995
  },
618
996
  {
619
- name: "tool_use",
997
+ name: "@particle-academy/tool_use",
998
+ aliases: ["tool_use", "@fancy/tool_use"],
620
999
  category: "ai",
621
1000
  label: "Tool Use",
622
1001
  description: "Hand control to a host-registered tool by name.",
@@ -627,7 +1006,8 @@ var KINDS = [
627
1006
  ]
628
1007
  },
629
1008
  {
630
- name: "embed_search",
1009
+ name: "@particle-academy/embed_search",
1010
+ aliases: ["embed_search", "@fancy/embed_search"],
631
1011
  category: "ai",
632
1012
  label: "Embed & Search",
633
1013
  description: "Embed a query and search a vector store.",
@@ -640,7 +1020,8 @@ var KINDS = [
640
1020
  },
641
1021
  // ───────────── IO ─────────────
642
1022
  {
643
- name: "api_request",
1023
+ name: "@particle-academy/api_request",
1024
+ aliases: ["api_request", "@fancy/api_request"],
644
1025
  category: "io",
645
1026
  label: "API Request",
646
1027
  description: "HTTP request to any URL.",
@@ -654,7 +1035,8 @@ var KINDS = [
654
1035
  ]
655
1036
  },
656
1037
  {
657
- name: "webhook_out",
1038
+ name: "@particle-academy/webhook_out",
1039
+ aliases: ["webhook_out", "@fancy/webhook_out"],
658
1040
  category: "io",
659
1041
  label: "Send Webhook",
660
1042
  description: "POST a payload to a configured URL.",
@@ -667,7 +1049,8 @@ var KINDS = [
667
1049
  },
668
1050
  // ───────────── Human ─────────────
669
1051
  {
670
- name: "human_approval",
1052
+ name: "@particle-academy/human_approval",
1053
+ aliases: ["human_approval", "@fancy/human_approval"],
671
1054
  category: "human",
672
1055
  label: "Human Approval",
673
1056
  description: "Pause until a human approves or denies.",
@@ -681,7 +1064,8 @@ var KINDS = [
681
1064
  ]
682
1065
  },
683
1066
  {
684
- name: "notify",
1067
+ name: "@particle-academy/notify",
1068
+ aliases: ["notify", "@fancy/notify"],
685
1069
  category: "human",
686
1070
  label: "Notify",
687
1071
  description: "Send a message via Slack / email / SMS / etc.",
@@ -705,7 +1089,8 @@ var KINDS = [
705
1089
  },
706
1090
  // ───────────── Output ─────────────
707
1091
  {
708
- name: "output",
1092
+ name: "@particle-academy/output",
1093
+ aliases: ["output", "@fancy/output"],
709
1094
  category: "output",
710
1095
  label: "Output",
711
1096
  description: "Terminal node \u2014 captures the workflow's result.",
@@ -714,7 +1099,8 @@ var KINDS = [
714
1099
  outputs: []
715
1100
  },
716
1101
  {
717
- name: "log",
1102
+ name: "@particle-academy/log",
1103
+ aliases: ["log", "@fancy/log"],
718
1104
  category: "output",
719
1105
  label: "Log",
720
1106
  description: "Send to the run feed.",
@@ -10397,167 +10783,6 @@ function useFlowState(initial) {
10397
10783
  const toGraph = ReactExports.useCallback(() => ({ nodes, edges }), [nodes, edges]);
10398
10784
  return { nodes, edges, setNodes, setEdges, onNodesChange, onEdgesChange, onConnect, toGraph };
10399
10785
  }
10400
-
10401
- // src/registry/ports.ts
10402
- function resolvePortSpec(spec, config) {
10403
- if (spec === void 0) return void 0;
10404
- if (typeof spec !== "function") return spec;
10405
- try {
10406
- const resolved = spec(config);
10407
- return Array.isArray(resolved) ? resolved : void 0;
10408
- } catch {
10409
- return void 0;
10410
- }
10411
- }
10412
- function nodeConfig(node) {
10413
- return node.data?.config ?? {};
10414
- }
10415
- function resolveNodePorts(node, kind) {
10416
- const config = nodeConfig(node);
10417
- const data = node.data;
10418
- return {
10419
- inputs: data?.inputs ?? resolvePortSpec(kind?.inputs, config),
10420
- outputs: data?.outputs ?? resolvePortSpec(kind?.outputs, config)
10421
- };
10422
- }
10423
-
10424
- // src/runtime/run-flow.ts
10425
- async function runFlow(graph, executors, onEvent = () => {
10426
- }, options = {}) {
10427
- const { signal, initialInputs = {}, timeoutMs } = options;
10428
- const outputs = {};
10429
- const portValues = /* @__PURE__ */ new Map();
10430
- const completed = /* @__PURE__ */ new Set();
10431
- const errors = [];
10432
- const order = topoSort(graph);
10433
- if (order === null) {
10434
- const msg = "Cycle detected in flow graph \u2014 aborting.";
10435
- onEvent({ type: "run-error", error: msg });
10436
- return { ok: false, outputs, error: msg };
10437
- }
10438
- const incomingByNode = indexIncoming(graph.edges);
10439
- const timer2 = timeoutMs ? setTimeout(() => errors.push(`Run timed out after ${timeoutMs}ms`), timeoutMs) : null;
10440
- onEvent({ type: "run-start" });
10441
- try {
10442
- for (const node of order) {
10443
- if (signal?.aborted) throw new Error("aborted");
10444
- if (errors.length) break;
10445
- const incoming = incomingByNode.get(node.id) ?? [];
10446
- if (incoming.length > 0) {
10447
- const anyActive = incoming.some((e) => portValues.has(`${e.source}:${e.sourceHandle ?? "out"}`));
10448
- if (!anyActive) {
10449
- onEvent({ type: "node-status", nodeId: node.id, status: "idle", text: "skipped" });
10450
- continue;
10451
- }
10452
- }
10453
- if (node.type === "note") {
10454
- onEvent({ type: "node-status", nodeId: node.id, status: "idle", text: "annotation" });
10455
- continue;
10456
- }
10457
- onEvent({ type: "node-status", nodeId: node.id, status: "running" });
10458
- const inputs = collectInputs(node, incoming, portValues, initialInputs);
10459
- const exec = pickExecutor(executors, node);
10460
- if (!exec) {
10461
- const msg = `No executor registered for kind=${node.type}`;
10462
- errors.push(msg);
10463
- onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
10464
- onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
10465
- break;
10466
- }
10467
- try {
10468
- const result = await Promise.resolve(
10469
- exec({
10470
- node,
10471
- inputs,
10472
- abort: (reason) => {
10473
- throw new Error(reason ?? "aborted");
10474
- },
10475
- emit: onEvent
10476
- })
10477
- );
10478
- outputs[node.id] = result;
10479
- const activated = activatedPorts(node, result);
10480
- for (const portId of activated.ports) {
10481
- portValues.set(`${node.id}:${portId}`, activated.value);
10482
- onEvent({ type: "node-output", nodeId: node.id, portId, value: activated.value });
10483
- }
10484
- completed.add(node.id);
10485
- onEvent({ type: "node-status", nodeId: node.id, status: "done" });
10486
- } catch (e) {
10487
- const msg = e instanceof Error ? e.message : String(e);
10488
- errors.push(msg);
10489
- onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
10490
- onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
10491
- break;
10492
- }
10493
- }
10494
- } finally {
10495
- if (timer2) clearTimeout(timer2);
10496
- }
10497
- const ok = errors.length === 0;
10498
- onEvent({ type: "run-end", ok });
10499
- return ok ? { ok, outputs } : { ok, outputs, error: errors[0] };
10500
- }
10501
- function indexIncoming(edges) {
10502
- const map = /* @__PURE__ */ new Map();
10503
- for (const e of edges) {
10504
- const list = map.get(e.target) ?? [];
10505
- list.push(e);
10506
- map.set(e.target, list);
10507
- }
10508
- return map;
10509
- }
10510
- function topoSort(graph) {
10511
- const inDegree = /* @__PURE__ */ new Map();
10512
- for (const n of graph.nodes) inDegree.set(n.id, 0);
10513
- for (const e of graph.edges) inDegree.set(e.target, (inDegree.get(e.target) ?? 0) + 1);
10514
- const queue = [];
10515
- for (const [id2, d] of inDegree) if (d === 0) queue.push(id2);
10516
- const ordered = [];
10517
- while (queue.length) {
10518
- const id2 = queue.shift();
10519
- ordered.push(id2);
10520
- for (const e of graph.edges) {
10521
- if (e.source !== id2) continue;
10522
- const next = (inDegree.get(e.target) ?? 0) - 1;
10523
- inDegree.set(e.target, next);
10524
- if (next === 0) queue.push(e.target);
10525
- }
10526
- }
10527
- if (ordered.length !== graph.nodes.length) return null;
10528
- const byId = new Map(graph.nodes.map((n) => [n.id, n]));
10529
- return ordered.map((id2) => byId.get(id2)).filter(Boolean);
10530
- }
10531
- function collectInputs(node, incoming, portValues, initial) {
10532
- const inputs = { ...initial[node.id] ?? {} };
10533
- for (const e of incoming) {
10534
- const portId = e.targetHandle ?? "in";
10535
- const val = portValues.get(`${e.source}:${e.sourceHandle ?? "out"}`);
10536
- inputs[portId] = val;
10537
- }
10538
- return inputs;
10539
- }
10540
- function pickExecutor(executors, node) {
10541
- if (executors[node.id]) return executors[node.id];
10542
- if (node.type && executors[node.type]) return executors[node.type];
10543
- return executors["*"];
10544
- }
10545
- function activatedPorts(node, result) {
10546
- if (result && typeof result === "object") {
10547
- const r = result;
10548
- if (typeof r.__port === "string") {
10549
- return { ports: [r.__port], value: r.value };
10550
- }
10551
- if (typeof r.branch === "string") {
10552
- return { ports: [r.branch], value: r.value ?? r };
10553
- }
10554
- }
10555
- const kind = getNodeKind(node.data?.kind ?? node.type ?? "") ?? void 0;
10556
- const declared = resolveNodePorts(node, kind).outputs?.map((p) => p.id);
10557
- return { ports: declared?.length ? declared : ["out"], value: result };
10558
- }
10559
-
10560
- // src/runtime/use-flow-run.ts
10561
10786
  function useFlowRun({ maxFeed = 200 } = {}) {
10562
10787
  const [statuses, setStatuses] = ReactExports.useState({});
10563
10788
  const [statusText, setStatusText] = ReactExports.useState({});
@@ -10721,12 +10946,13 @@ function importWorkflow(schema, options = {}) {
10721
10946
  issues.push({ level: "warning", nodeId: n.id, message: `${iss.key}: ${iss.message}` });
10722
10947
  }
10723
10948
  }
10949
+ const kindId = kind?.name ?? n.kind;
10724
10950
  return {
10725
10951
  id: n.id,
10726
- type: n.kind,
10952
+ type: kindId,
10727
10953
  position: { x: n.position?.x ?? 0, y: n.position?.y ?? 0 },
10728
10954
  data: {
10729
- kind: n.kind,
10955
+ kind: kindId,
10730
10956
  label: n.label ?? kind?.label ?? n.kind,
10731
10957
  description: n.description,
10732
10958
  config,
@@ -10879,7 +11105,9 @@ function previewValue(v) {
10879
11105
  // src/registry/index.ts
10880
11106
  function buildNodeTypes() {
10881
11107
  const map = {};
10882
- for (const k of listNodeKinds()) map[k.name] = RegistryNode;
11108
+ for (const k of listNodeKinds()) {
11109
+ for (const id2 of kindIds(k)) map[id2] = RegistryNode;
11110
+ }
10883
11111
  return map;
10884
11112
  }
10885
11113