@particle-academy/fancy-flow 0.11.0 → 0.13.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 (48) hide show
  1. package/dist/capabilities-B6r1Snfj.d.ts +77 -0
  2. package/dist/capabilities-D-V9Rxv1.d.cts +77 -0
  3. package/dist/chunk-BB45F6S3.js +38 -0
  4. package/dist/chunk-BB45F6S3.js.map +1 -0
  5. package/dist/{chunk-2NQT2A3I.js → chunk-L4AX73Q6.js} +5 -4
  6. package/dist/chunk-L4AX73Q6.js.map +1 -0
  7. package/dist/{chunk-3GMBLHTM.js → chunk-MFMRTRPO.js} +3 -3
  8. package/dist/{chunk-3GMBLHTM.js.map → chunk-MFMRTRPO.js.map} +1 -1
  9. package/dist/{chunk-YNOI7ONN.js → chunk-WEZJFMLV.js} +224 -51
  10. package/dist/chunk-WEZJFMLV.js.map +1 -0
  11. package/dist/engine.cjs +3 -2
  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 +1 -1
  16. package/dist/index.cjs +396 -215
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +5 -4
  19. package/dist/index.d.ts +5 -4
  20. package/dist/index.js +6 -5
  21. package/dist/index.js.map +1 -1
  22. package/dist/llm/vercel-ai.cjs +59 -0
  23. package/dist/llm/vercel-ai.cjs.map +1 -0
  24. package/dist/llm/vercel-ai.d.cts +59 -0
  25. package/dist/llm/vercel-ai.d.ts +59 -0
  26. package/dist/llm/vercel-ai.js +49 -0
  27. package/dist/llm/vercel-ai.js.map +1 -0
  28. package/dist/registry/index.d.cts +60 -4
  29. package/dist/registry/index.d.ts +60 -4
  30. package/dist/registry.cjs +455 -93
  31. package/dist/registry.cjs.map +1 -1
  32. package/dist/registry.js +3 -1
  33. package/dist/runtime/index.d.cts +1 -1
  34. package/dist/runtime/index.d.ts +1 -1
  35. package/dist/runtime.cjs +3 -2
  36. package/dist/runtime.cjs.map +1 -1
  37. package/dist/runtime.js +2 -2
  38. package/dist/schema/index.d.cts +1 -1
  39. package/dist/schema/index.d.ts +1 -1
  40. package/dist/{types-CsEe3EQl.d.ts → types-CnnKPnpt.d.ts} +1 -1
  41. package/dist/{types-BwLr5tZV.d.cts → types-H60tQAxr.d.cts} +1 -1
  42. package/dist/{types-BS3Gwnkq.d.cts → types-fc0fFKkf.d.cts} +7 -1
  43. package/dist/{types-BS3Gwnkq.d.ts → types-fc0fFKkf.d.ts} +7 -1
  44. package/dist/ux.d.cts +2 -2
  45. package/dist/ux.d.ts +2 -2
  46. package/package.json +19 -3
  47. package/dist/chunk-2NQT2A3I.js.map +0 -1
  48. package/dist/chunk-YNOI7ONN.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -212,6 +212,299 @@ function RichInputPreview({ config }) {
212
212
  ] });
213
213
  }
214
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
+
215
508
  // src/registry/builtin.ts
216
509
  function casePorts(cases) {
217
510
  const byPort = /* @__PURE__ */ new Map();
@@ -258,8 +551,8 @@ var HTTP_METHODS = [
258
551
  var KINDS = [
259
552
  // ───────────── Triggers ─────────────
260
553
  {
261
- name: "@fancy/manual_trigger",
262
- aliases: ["manual_trigger"],
554
+ name: "@particle-academy/manual_trigger",
555
+ aliases: ["manual_trigger", "@fancy/manual_trigger"],
263
556
  category: "trigger",
264
557
  label: "Manual",
265
558
  description: "Entry point fired when the user clicks Run.",
@@ -268,8 +561,8 @@ var KINDS = [
268
561
  outputs: [{ id: "out" }]
269
562
  },
270
563
  {
271
- name: "@fancy/webhook_trigger",
272
- aliases: ["webhook_trigger"],
564
+ name: "@particle-academy/webhook_trigger",
565
+ aliases: ["webhook_trigger", "@fancy/webhook_trigger"],
273
566
  category: "trigger",
274
567
  label: "Webhook",
275
568
  description: "Triggered by an inbound HTTP request to a host-provided URL.",
@@ -286,8 +579,8 @@ var KINDS = [
286
579
  ]
287
580
  },
288
581
  {
289
- name: "@fancy/schedule_trigger",
290
- aliases: ["schedule_trigger"],
582
+ name: "@particle-academy/schedule_trigger",
583
+ aliases: ["schedule_trigger", "@fancy/schedule_trigger"],
291
584
  category: "trigger",
292
585
  label: "Schedule",
293
586
  description: "Fires on a cron schedule (host-implemented).",
@@ -307,8 +600,8 @@ var KINDS = [
307
600
  ]
308
601
  },
309
602
  {
310
- name: "@fancy/user_input",
311
- aliases: ["user_input"],
603
+ name: "@particle-academy/user_input",
604
+ aliases: ["user_input", "@fancy/user_input"],
312
605
  category: "human",
313
606
  label: "User Input",
314
607
  description: "Pause the flow until the user submits the configured form.",
@@ -348,8 +641,8 @@ var KINDS = [
348
641
  ]
349
642
  },
350
643
  {
351
- name: "@fancy/rich_user_input",
352
- aliases: ["rich_user_input"],
644
+ name: "@particle-academy/rich_user_input",
645
+ aliases: ["rich_user_input", "@fancy/rich_user_input"],
353
646
  category: "human",
354
647
  label: "Rich User Input",
355
648
  description: "Pause the flow on a fully authored page \u2014 content, required reading, multi-section forms.",
@@ -372,10 +665,61 @@ var KINDS = [
372
665
  // what the person hitting this step will actually see.
373
666
  renderBody: (ctx) => ReactExports.createElement(RichInputPreview, { config: ctx.config ?? {} })
374
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
+ },
375
719
  // ───────────── Logic ─────────────
376
720
  {
377
- name: "@fancy/branch",
378
- aliases: ["branch"],
721
+ name: "@particle-academy/branch",
722
+ aliases: ["branch", "@fancy/branch"],
379
723
  category: "logic",
380
724
  label: "Branch",
381
725
  description: "Multi-way branch on a condition or value.",
@@ -387,8 +731,8 @@ var KINDS = [
387
731
  ]
388
732
  },
389
733
  {
390
- name: "@fancy/switch_case",
391
- aliases: ["switch_case"],
734
+ name: "@particle-academy/switch_case",
735
+ aliases: ["switch_case", "@fancy/switch_case"],
392
736
  category: "logic",
393
737
  label: "Switch",
394
738
  description: "Route to one of N labelled outputs based on a key.",
@@ -415,8 +759,8 @@ var KINDS = [
415
759
  ]
416
760
  },
417
761
  {
418
- name: "@fancy/for_each",
419
- aliases: ["for_each"],
762
+ name: "@particle-academy/for_each",
763
+ aliases: ["for_each", "@fancy/for_each"],
420
764
  category: "logic",
421
765
  label: "For Each",
422
766
  description: "Iterate over a list, emitting each item on `item`.",
@@ -429,8 +773,8 @@ var KINDS = [
429
773
  ]
430
774
  },
431
775
  {
432
- name: "@fancy/merge",
433
- aliases: ["merge"],
776
+ name: "@particle-academy/merge",
777
+ aliases: ["merge", "@fancy/merge"],
434
778
  category: "logic",
435
779
  label: "Merge",
436
780
  description: "Combine multiple inputs into one object or array.",
@@ -448,8 +792,8 @@ var KINDS = [
448
792
  ]
449
793
  },
450
794
  {
451
- name: "@fancy/wait",
452
- aliases: ["wait"],
795
+ name: "@particle-academy/wait",
796
+ aliases: ["wait", "@fancy/wait"],
453
797
  category: "logic",
454
798
  label: "Wait",
455
799
  description: "Sleep or wait for an external event.",
@@ -466,8 +810,8 @@ var KINDS = [
466
810
  ]
467
811
  },
468
812
  {
469
- name: "@fancy/transform",
470
- aliases: ["transform"],
813
+ name: "@particle-academy/transform",
814
+ aliases: ["transform", "@fancy/transform"],
471
815
  category: "logic",
472
816
  label: "Transform",
473
817
  description: "Reshape data with an expression.",
@@ -484,8 +828,8 @@ var KINDS = [
484
828
  },
485
829
  // ───────────── Data ─────────────
486
830
  {
487
- name: "@fancy/memory_store",
488
- aliases: ["memory_store"],
831
+ name: "@particle-academy/memory_store",
832
+ aliases: ["memory_store", "@fancy/memory_store"],
489
833
  category: "data",
490
834
  label: "Memory Store",
491
835
  description: "Read or write per-conversation memory.",
@@ -505,8 +849,8 @@ var KINDS = [
505
849
  ]
506
850
  },
507
851
  {
508
- name: "@fancy/data_store",
509
- aliases: ["data_store"],
852
+ name: "@particle-academy/data_store",
853
+ aliases: ["data_store", "@fancy/data_store"],
510
854
  category: "data",
511
855
  label: "Data Store",
512
856
  description: "Key-value or table read/write against a host store.",
@@ -534,8 +878,8 @@ var KINDS = [
534
878
  ]
535
879
  },
536
880
  {
537
- name: "@fancy/variable",
538
- aliases: ["variable"],
881
+ name: "@particle-academy/variable",
882
+ aliases: ["variable", "@fancy/variable"],
539
883
  category: "data",
540
884
  label: "Variable",
541
885
  description: "Workflow-scoped value used by other nodes.",
@@ -547,8 +891,8 @@ var KINDS = [
547
891
  },
548
892
  // ───────────── AI ─────────────
549
893
  {
550
- name: "@fancy/llm_call",
551
- aliases: ["llm_call"],
894
+ name: "@particle-academy/llm_call",
895
+ aliases: ["llm_call", "@fancy/llm_call"],
552
896
  category: "ai",
553
897
  label: "LLM Call",
554
898
  description: "Send a prompt + context to a model and receive a response.",
@@ -575,8 +919,8 @@ var KINDS = [
575
919
  ]
576
920
  },
577
921
  {
578
- name: "@fancy/llm_branch",
579
- aliases: ["llm_branch"],
922
+ name: "@particle-academy/llm_branch",
923
+ aliases: ["llm_branch", "@fancy/llm_branch"],
580
924
  category: "ai",
581
925
  label: "LLM Router",
582
926
  description: "Let a model choose which route the flow takes.",
@@ -585,6 +929,10 @@ var KINDS = [
585
929
  // Each declared route is a port. The executor returns `{ __port: id }`
586
930
  // (or `Port.only(id)` on the PHP runtime) to pick one.
587
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,
588
936
  configSchema: [
589
937
  {
590
938
  type: "textarea",
@@ -646,8 +994,8 @@ var KINDS = [
646
994
  ]
647
995
  },
648
996
  {
649
- name: "@fancy/tool_use",
650
- aliases: ["tool_use"],
997
+ name: "@particle-academy/tool_use",
998
+ aliases: ["tool_use", "@fancy/tool_use"],
651
999
  category: "ai",
652
1000
  label: "Tool Use",
653
1001
  description: "Hand control to a host-registered tool by name.",
@@ -658,8 +1006,8 @@ var KINDS = [
658
1006
  ]
659
1007
  },
660
1008
  {
661
- name: "@fancy/embed_search",
662
- aliases: ["embed_search"],
1009
+ name: "@particle-academy/embed_search",
1010
+ aliases: ["embed_search", "@fancy/embed_search"],
663
1011
  category: "ai",
664
1012
  label: "Embed & Search",
665
1013
  description: "Embed a query and search a vector store.",
@@ -672,8 +1020,8 @@ var KINDS = [
672
1020
  },
673
1021
  // ───────────── IO ─────────────
674
1022
  {
675
- name: "@fancy/api_request",
676
- aliases: ["api_request"],
1023
+ name: "@particle-academy/api_request",
1024
+ aliases: ["api_request", "@fancy/api_request"],
677
1025
  category: "io",
678
1026
  label: "API Request",
679
1027
  description: "HTTP request to any URL.",
@@ -687,8 +1035,8 @@ var KINDS = [
687
1035
  ]
688
1036
  },
689
1037
  {
690
- name: "@fancy/webhook_out",
691
- aliases: ["webhook_out"],
1038
+ name: "@particle-academy/webhook_out",
1039
+ aliases: ["webhook_out", "@fancy/webhook_out"],
692
1040
  category: "io",
693
1041
  label: "Send Webhook",
694
1042
  description: "POST a payload to a configured URL.",
@@ -701,8 +1049,8 @@ var KINDS = [
701
1049
  },
702
1050
  // ───────────── Human ─────────────
703
1051
  {
704
- name: "@fancy/human_approval",
705
- aliases: ["human_approval"],
1052
+ name: "@particle-academy/human_approval",
1053
+ aliases: ["human_approval", "@fancy/human_approval"],
706
1054
  category: "human",
707
1055
  label: "Human Approval",
708
1056
  description: "Pause until a human approves or denies.",
@@ -716,8 +1064,8 @@ var KINDS = [
716
1064
  ]
717
1065
  },
718
1066
  {
719
- name: "@fancy/notify",
720
- aliases: ["notify"],
1067
+ name: "@particle-academy/notify",
1068
+ aliases: ["notify", "@fancy/notify"],
721
1069
  category: "human",
722
1070
  label: "Notify",
723
1071
  description: "Send a message via Slack / email / SMS / etc.",
@@ -741,8 +1089,8 @@ var KINDS = [
741
1089
  },
742
1090
  // ───────────── Output ─────────────
743
1091
  {
744
- name: "@fancy/output",
745
- aliases: ["output"],
1092
+ name: "@particle-academy/output",
1093
+ aliases: ["output", "@fancy/output"],
746
1094
  category: "output",
747
1095
  label: "Output",
748
1096
  description: "Terminal node \u2014 captures the workflow's result.",
@@ -751,8 +1099,8 @@ var KINDS = [
751
1099
  outputs: []
752
1100
  },
753
1101
  {
754
- name: "@fancy/log",
755
- aliases: ["log"],
1102
+ name: "@particle-academy/log",
1103
+ aliases: ["log", "@fancy/log"],
756
1104
  category: "output",
757
1105
  label: "Log",
758
1106
  description: "Send to the run feed.",
@@ -10435,173 +10783,6 @@ function useFlowState(initial) {
10435
10783
  const toGraph = ReactExports.useCallback(() => ({ nodes, edges }), [nodes, edges]);
10436
10784
  return { nodes, edges, setNodes, setEdges, onNodesChange, onEdgesChange, onConnect, toGraph };
10437
10785
  }
10438
-
10439
- // src/registry/ports.ts
10440
- function resolvePortSpec(spec, config) {
10441
- if (spec === void 0) return void 0;
10442
- if (typeof spec !== "function") return spec;
10443
- try {
10444
- const resolved = spec(config);
10445
- return Array.isArray(resolved) ? resolved : void 0;
10446
- } catch {
10447
- return void 0;
10448
- }
10449
- }
10450
- function nodeConfig(node) {
10451
- return node.data?.config ?? {};
10452
- }
10453
- function resolveNodePorts(node, kind) {
10454
- const config = nodeConfig(node);
10455
- const data = node.data;
10456
- return {
10457
- inputs: data?.inputs ?? resolvePortSpec(kind?.inputs, config),
10458
- outputs: data?.outputs ?? resolvePortSpec(kind?.outputs, config)
10459
- };
10460
- }
10461
-
10462
- // src/runtime/run-flow.ts
10463
- async function runFlow(graph, executors, onEvent = () => {
10464
- }, options = {}) {
10465
- const { signal, initialInputs = {}, timeoutMs } = options;
10466
- const outputs = {};
10467
- const portValues = /* @__PURE__ */ new Map();
10468
- const completed = /* @__PURE__ */ new Set();
10469
- const errors = [];
10470
- const order = topoSort(graph);
10471
- if (order === null) {
10472
- const msg = "Cycle detected in flow graph \u2014 aborting.";
10473
- onEvent({ type: "run-error", error: msg });
10474
- return { ok: false, outputs, error: msg };
10475
- }
10476
- const incomingByNode = indexIncoming(graph.edges);
10477
- const timer2 = timeoutMs ? setTimeout(() => errors.push(`Run timed out after ${timeoutMs}ms`), timeoutMs) : null;
10478
- onEvent({ type: "run-start" });
10479
- try {
10480
- for (const node of order) {
10481
- if (signal?.aborted) throw new Error("aborted");
10482
- if (errors.length) break;
10483
- const incoming = incomingByNode.get(node.id) ?? [];
10484
- if (incoming.length > 0) {
10485
- const anyActive = incoming.some((e) => portValues.has(`${e.source}:${e.sourceHandle ?? "out"}`));
10486
- if (!anyActive) {
10487
- onEvent({ type: "node-status", nodeId: node.id, status: "idle", text: "skipped" });
10488
- continue;
10489
- }
10490
- }
10491
- if (node.type === "note") {
10492
- onEvent({ type: "node-status", nodeId: node.id, status: "idle", text: "annotation" });
10493
- continue;
10494
- }
10495
- onEvent({ type: "node-status", nodeId: node.id, status: "running" });
10496
- const inputs = collectInputs(node, incoming, portValues, initialInputs);
10497
- const exec = pickExecutor(executors, node);
10498
- if (!exec) {
10499
- const msg = `No executor registered for kind=${node.type}`;
10500
- errors.push(msg);
10501
- onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
10502
- onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
10503
- break;
10504
- }
10505
- try {
10506
- const result = await Promise.resolve(
10507
- exec({
10508
- node,
10509
- inputs,
10510
- abort: (reason) => {
10511
- throw new Error(reason ?? "aborted");
10512
- },
10513
- emit: onEvent
10514
- })
10515
- );
10516
- outputs[node.id] = result;
10517
- const activated = activatedPorts(node, result);
10518
- for (const portId of activated.ports) {
10519
- portValues.set(`${node.id}:${portId}`, activated.value);
10520
- onEvent({ type: "node-output", nodeId: node.id, portId, value: activated.value });
10521
- }
10522
- completed.add(node.id);
10523
- onEvent({ type: "node-status", nodeId: node.id, status: "done" });
10524
- } catch (e) {
10525
- const msg = e instanceof Error ? e.message : String(e);
10526
- errors.push(msg);
10527
- onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
10528
- onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
10529
- break;
10530
- }
10531
- }
10532
- } finally {
10533
- if (timer2) clearTimeout(timer2);
10534
- }
10535
- const ok = errors.length === 0;
10536
- onEvent({ type: "run-end", ok });
10537
- return ok ? { ok, outputs } : { ok, outputs, error: errors[0] };
10538
- }
10539
- function indexIncoming(edges) {
10540
- const map = /* @__PURE__ */ new Map();
10541
- for (const e of edges) {
10542
- const list = map.get(e.target) ?? [];
10543
- list.push(e);
10544
- map.set(e.target, list);
10545
- }
10546
- return map;
10547
- }
10548
- function topoSort(graph) {
10549
- const inDegree = /* @__PURE__ */ new Map();
10550
- for (const n of graph.nodes) inDegree.set(n.id, 0);
10551
- for (const e of graph.edges) inDegree.set(e.target, (inDegree.get(e.target) ?? 0) + 1);
10552
- const queue = [];
10553
- for (const [id2, d] of inDegree) if (d === 0) queue.push(id2);
10554
- const ordered = [];
10555
- while (queue.length) {
10556
- const id2 = queue.shift();
10557
- ordered.push(id2);
10558
- for (const e of graph.edges) {
10559
- if (e.source !== id2) continue;
10560
- const next = (inDegree.get(e.target) ?? 0) - 1;
10561
- inDegree.set(e.target, next);
10562
- if (next === 0) queue.push(e.target);
10563
- }
10564
- }
10565
- if (ordered.length !== graph.nodes.length) return null;
10566
- const byId = new Map(graph.nodes.map((n) => [n.id, n]));
10567
- return ordered.map((id2) => byId.get(id2)).filter(Boolean);
10568
- }
10569
- function collectInputs(node, incoming, portValues, initial) {
10570
- const inputs = { ...initial[node.id] ?? {} };
10571
- for (const e of incoming) {
10572
- const portId = e.targetHandle ?? "in";
10573
- const val = portValues.get(`${e.source}:${e.sourceHandle ?? "out"}`);
10574
- inputs[portId] = val;
10575
- }
10576
- return inputs;
10577
- }
10578
- function pickExecutor(executors, node) {
10579
- if (executors[node.id]) return executors[node.id];
10580
- if (node.type && executors[node.type]) return executors[node.type];
10581
- const kind = node.type ? getNodeKind(node.type) : null;
10582
- if (kind) {
10583
- for (const id2 of kindIds(kind)) {
10584
- if (executors[id2]) return executors[id2];
10585
- }
10586
- }
10587
- return executors["*"];
10588
- }
10589
- function activatedPorts(node, result) {
10590
- if (result && typeof result === "object") {
10591
- const r = result;
10592
- if (typeof r.__port === "string") {
10593
- return { ports: [r.__port], value: r.value };
10594
- }
10595
- if (typeof r.branch === "string") {
10596
- return { ports: [r.branch], value: r.value ?? r };
10597
- }
10598
- }
10599
- const kind = getNodeKind(node.data?.kind ?? node.type ?? "") ?? void 0;
10600
- const declared = resolveNodePorts(node, kind).outputs?.map((p) => p.id);
10601
- return { ports: declared?.length ? declared : ["out"], value: result };
10602
- }
10603
-
10604
- // src/runtime/use-flow-run.ts
10605
10786
  function useFlowRun({ maxFeed = 200 } = {}) {
10606
10787
  const [statuses, setStatuses] = ReactExports.useState({});
10607
10788
  const [statusText, setStatusText] = ReactExports.useState({});