@particle-academy/fancy-flow 0.12.0 → 0.14.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 (37) 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-IK5DS5JP.js → chunk-7U3EP4Q5.js} +140 -54
  4. package/dist/chunk-7U3EP4Q5.js.map +1 -0
  5. package/dist/chunk-BB45F6S3.js +38 -0
  6. package/dist/chunk-BB45F6S3.js.map +1 -0
  7. package/dist/engine.d.cts +2 -2
  8. package/dist/engine.d.ts +2 -2
  9. package/dist/index.cjs +136 -16
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +5 -4
  12. package/dist/index.d.ts +5 -4
  13. package/dist/index.js +3 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/llm/vercel-ai.cjs +59 -0
  16. package/dist/llm/vercel-ai.cjs.map +1 -0
  17. package/dist/llm/vercel-ai.d.cts +59 -0
  18. package/dist/llm/vercel-ai.d.ts +59 -0
  19. package/dist/llm/vercel-ai.js +49 -0
  20. package/dist/llm/vercel-ai.js.map +1 -0
  21. package/dist/registry/index.d.cts +7 -79
  22. package/dist/registry/index.d.ts +7 -79
  23. package/dist/registry.cjs +138 -17
  24. package/dist/registry.cjs.map +1 -1
  25. package/dist/registry.js +2 -1
  26. package/dist/runtime/index.d.cts +1 -1
  27. package/dist/runtime/index.d.ts +1 -1
  28. package/dist/schema/index.d.cts +1 -1
  29. package/dist/schema/index.d.ts +1 -1
  30. package/dist/{types-DtXOwXjA.d.ts → types-CnnKPnpt.d.ts} +1 -1
  31. package/dist/{types-DnQ4TEHN.d.cts → types-H60tQAxr.d.cts} +1 -1
  32. package/dist/{types-Dg2REAz5.d.cts → types-fc0fFKkf.d.cts} +1 -1
  33. package/dist/{types-Dg2REAz5.d.ts → types-fc0fFKkf.d.ts} +1 -1
  34. package/dist/ux.d.cts +2 -2
  35. package/dist/ux.d.ts +2 -2
  36. package/package.json +19 -3
  37. package/dist/chunk-IK5DS5JP.js.map +0 -1
@@ -0,0 +1,77 @@
1
+ import { F as FlowGraph } from './types-fc0fFKkf.js';
2
+
3
+ /**
4
+ * Host capabilities — the services core nodes need but must never depend on.
5
+ *
6
+ * A node that imports a provider SDK forces every consumer to install it: a
7
+ * workflow app that never calls a model should not inherit an LLM dependency.
8
+ * So core declares the CONTRACT and the host supplies the implementation, the
9
+ * same arrangement `renderDocumentField` already uses for documents.
10
+ *
11
+ * That keeps opinionated nodes in core without their opinions: `llm_branch`
12
+ * ships the routing semantics, port derivation and config UI, while whichever
13
+ * client the host registers — Prism, an OpenAI SDK, a local model, a fake in a
14
+ * test — decides how the question actually gets asked.
15
+ *
16
+ * Registration is deliberately explicit and typed per capability rather than a
17
+ * stringly-keyed bag, so a missing one is a clear error at the seam instead of
18
+ * an undefined somewhere downstream.
19
+ */
20
+ type LlmRoute = {
21
+ port: string;
22
+ description?: string;
23
+ };
24
+ type LlmRouteRequest = {
25
+ /** Optional framing for the decision. */
26
+ system?: string;
27
+ /** What the model is deciding about. */
28
+ prompt: string;
29
+ /** The ports it must choose between. */
30
+ routes: LlmRoute[];
31
+ provider?: string;
32
+ model?: string;
33
+ /** Host-resolved credential reference, never a raw key. */
34
+ credential?: string;
35
+ };
36
+ type LlmRouteChoice = {
37
+ /** Must be one of the requested route ports. */
38
+ port: string;
39
+ /** Why — carried down the chosen port so a run is explainable afterwards. */
40
+ reason?: string;
41
+ };
42
+ /**
43
+ * The only thing core asks of an LLM: given routes, pick one.
44
+ *
45
+ * Deliberately not a general chat interface. A narrow contract is one a host
46
+ * can satisfy in a few lines over any SDK, and it keeps the choice
47
+ * machine-checkable — an implementation should constrain the model to the
48
+ * declared ports (structured output / enum) rather than parsing prose.
49
+ */
50
+ type LlmClient = {
51
+ chooseRoute: (request: LlmRouteRequest) => Promise<LlmRouteChoice> | LlmRouteChoice;
52
+ };
53
+ /** Install the host's LLM client. Returns an unregister function. */
54
+ declare function registerLlmClient(client: LlmClient): () => void;
55
+ declare function getLlmClient(): LlmClient | null;
56
+ /**
57
+ * Resolve a workflow reference to a runnable graph.
58
+ *
59
+ * `subflow` names another workflow rather than embedding it, so the host owns
60
+ * where workflows live — a database, a file, an API. Returning null means "no
61
+ * such workflow", which the node reports as an error rather than silently
62
+ * running nothing.
63
+ */
64
+ type WorkflowResolver = (ref: string) => Promise<FlowGraph | null> | FlowGraph | null;
65
+ /** Install the host's workflow resolver. Returns an unregister function. */
66
+ declare function registerWorkflowResolver(resolver: WorkflowResolver): () => void;
67
+ declare function getWorkflowResolver(): WorkflowResolver | null;
68
+ type CapabilityId = "llm" | "workflow_resolver" | "document";
69
+ /**
70
+ * Which capabilities are currently satisfied.
71
+ *
72
+ * Exists so a host (or the CLI, or an agent over MCP) can answer "what does
73
+ * this graph need that I haven't wired?" BEFORE a run fails halfway through.
74
+ */
75
+ declare function capabilityStatus(): Record<CapabilityId, boolean>;
76
+
77
+ export { type CapabilityId as C, type LlmRouteRequest as L, type WorkflowResolver as W, type LlmClient as a, type LlmRoute as b, type LlmRouteChoice as c, capabilityStatus as d, getWorkflowResolver as e, registerWorkflowResolver as f, getLlmClient as g, registerLlmClient as r };
@@ -0,0 +1,77 @@
1
+ import { F as FlowGraph } from './types-fc0fFKkf.cjs';
2
+
3
+ /**
4
+ * Host capabilities — the services core nodes need but must never depend on.
5
+ *
6
+ * A node that imports a provider SDK forces every consumer to install it: a
7
+ * workflow app that never calls a model should not inherit an LLM dependency.
8
+ * So core declares the CONTRACT and the host supplies the implementation, the
9
+ * same arrangement `renderDocumentField` already uses for documents.
10
+ *
11
+ * That keeps opinionated nodes in core without their opinions: `llm_branch`
12
+ * ships the routing semantics, port derivation and config UI, while whichever
13
+ * client the host registers — Prism, an OpenAI SDK, a local model, a fake in a
14
+ * test — decides how the question actually gets asked.
15
+ *
16
+ * Registration is deliberately explicit and typed per capability rather than a
17
+ * stringly-keyed bag, so a missing one is a clear error at the seam instead of
18
+ * an undefined somewhere downstream.
19
+ */
20
+ type LlmRoute = {
21
+ port: string;
22
+ description?: string;
23
+ };
24
+ type LlmRouteRequest = {
25
+ /** Optional framing for the decision. */
26
+ system?: string;
27
+ /** What the model is deciding about. */
28
+ prompt: string;
29
+ /** The ports it must choose between. */
30
+ routes: LlmRoute[];
31
+ provider?: string;
32
+ model?: string;
33
+ /** Host-resolved credential reference, never a raw key. */
34
+ credential?: string;
35
+ };
36
+ type LlmRouteChoice = {
37
+ /** Must be one of the requested route ports. */
38
+ port: string;
39
+ /** Why — carried down the chosen port so a run is explainable afterwards. */
40
+ reason?: string;
41
+ };
42
+ /**
43
+ * The only thing core asks of an LLM: given routes, pick one.
44
+ *
45
+ * Deliberately not a general chat interface. A narrow contract is one a host
46
+ * can satisfy in a few lines over any SDK, and it keeps the choice
47
+ * machine-checkable — an implementation should constrain the model to the
48
+ * declared ports (structured output / enum) rather than parsing prose.
49
+ */
50
+ type LlmClient = {
51
+ chooseRoute: (request: LlmRouteRequest) => Promise<LlmRouteChoice> | LlmRouteChoice;
52
+ };
53
+ /** Install the host's LLM client. Returns an unregister function. */
54
+ declare function registerLlmClient(client: LlmClient): () => void;
55
+ declare function getLlmClient(): LlmClient | null;
56
+ /**
57
+ * Resolve a workflow reference to a runnable graph.
58
+ *
59
+ * `subflow` names another workflow rather than embedding it, so the host owns
60
+ * where workflows live — a database, a file, an API. Returning null means "no
61
+ * such workflow", which the node reports as an error rather than silently
62
+ * running nothing.
63
+ */
64
+ type WorkflowResolver = (ref: string) => Promise<FlowGraph | null> | FlowGraph | null;
65
+ /** Install the host's workflow resolver. Returns an unregister function. */
66
+ declare function registerWorkflowResolver(resolver: WorkflowResolver): () => void;
67
+ declare function getWorkflowResolver(): WorkflowResolver | null;
68
+ type CapabilityId = "llm" | "workflow_resolver" | "document";
69
+ /**
70
+ * Which capabilities are currently satisfied.
71
+ *
72
+ * Exists so a host (or the CLI, or an agent over MCP) can answer "what does
73
+ * this graph need that I haven't wired?" BEFORE a run fails halfway through.
74
+ */
75
+ declare function capabilityStatus(): Record<CapabilityId, boolean>;
76
+
77
+ export { type CapabilityId as C, type LlmRouteRequest as L, type WorkflowResolver as W, type LlmClient as a, type LlmRoute as b, type LlmRouteChoice as c, capabilityStatus as d, getWorkflowResolver as e, registerWorkflowResolver as f, getLlmClient as g, registerLlmClient as r };
@@ -3,44 +3,10 @@ import { runFlow } from './chunk-L4AX73Q6.js';
3
3
  import { resolveNodePorts, nodeConfig } from './chunk-TITD5W4Y.js';
4
4
  import { getNodeKind, categoryAccent, registerNodeKind, listNodeKinds, kindIds } from './chunk-U5F22BHV.js';
5
5
  import { RichInputPreview } from './chunk-F5RPRB7A.js';
6
+ import { getWorkflowResolver, getLlmClient } from './chunk-BB45F6S3.js';
6
7
  import { memo, useMemo, createElement } from 'react';
7
8
  import { jsxs, jsx } from 'react/jsx-runtime';
8
9
 
9
- // src/registry/capabilities.ts
10
- var llmClient = null;
11
- function registerLlmClient(client) {
12
- llmClient = client;
13
- return () => {
14
- if (llmClient === client) llmClient = null;
15
- };
16
- }
17
- function getLlmClient() {
18
- return llmClient;
19
- }
20
- var workflowResolver = null;
21
- function registerWorkflowResolver(resolver) {
22
- workflowResolver = resolver;
23
- return () => {
24
- if (workflowResolver === resolver) workflowResolver = null;
25
- };
26
- }
27
- function getWorkflowResolver() {
28
- return workflowResolver;
29
- }
30
- function capabilityStatus() {
31
- let documentReady = false;
32
- try {
33
- documentReady = Boolean(globalThis.__fancyFlowDocumentAdapter);
34
- } catch {
35
- documentReady = false;
36
- }
37
- return {
38
- llm: llmClient !== null,
39
- workflow_resolver: workflowResolver !== null,
40
- document: documentReady
41
- };
42
- }
43
-
44
10
  // src/registry/subflow.ts
45
11
  var DEFAULT_MAX_DEPTH = 8;
46
12
  function subflowMode(config) {
@@ -110,7 +76,7 @@ var subflowExecutor = async (ctx) => {
110
76
  return { __port: "out", value: result.outputs };
111
77
  };
112
78
 
113
- // src/registry/llm-branch.ts
79
+ // src/registry/llm-router.ts
114
80
  function declaredRoutes(config) {
115
81
  const raw = config.routes;
116
82
  if (!Array.isArray(raw)) return [];
@@ -120,11 +86,11 @@ function resolveFallbackPort(routes, fallbackEnabled) {
120
86
  if (fallbackEnabled) return "fallback";
121
87
  return routes[0]?.port ?? "out";
122
88
  }
123
- var llmBranchExecutor = async (ctx) => {
89
+ var llmRouterExecutor = async (ctx) => {
124
90
  const config = ctx.node.data?.config ?? {};
125
91
  const routes = declaredRoutes(config);
126
92
  if (routes.length === 0) {
127
- ctx.abort("llm_branch has no routes configured");
93
+ ctx.abort("llm_router has no routes configured");
128
94
  }
129
95
  const client = getLlmClient();
130
96
  if (!client) {
@@ -150,7 +116,7 @@ var llmBranchExecutor = async (ctx) => {
150
116
  type: "log",
151
117
  nodeId: ctx.node.id,
152
118
  level: "warn",
153
- message: `llm_branch: model returned "${port || "(nothing)"}", which is not a declared route. Routing to "${safe}".`
119
+ message: `llm_router: model returned "${port || "(nothing)"}", which is not a declared route. Routing to "${safe}".`
154
120
  });
155
121
  reason = reason ?? `unrecognised route "${port}"`;
156
122
  port = safe;
@@ -464,10 +430,13 @@ var KINDS = [
464
430
  description: "Streaming adds a second port so a parent can show progress instead of a spinner."
465
431
  },
466
432
  {
467
- type: "json",
433
+ type: "keyvalue",
468
434
  key: "inputs",
469
435
  label: "Input mapping",
470
- description: "Entry-point inputs for the child run. Omit to pass this node's inputs straight through."
436
+ description: "Values handed to the child's entry points. Omit to pass this node's inputs straight through.",
437
+ keyLabel: "Name",
438
+ valueLabel: "Value",
439
+ addLabel: "Add input"
471
440
  },
472
441
  {
473
442
  type: "number",
@@ -491,7 +460,57 @@ var KINDS = [
491
460
  inputs: [{ id: "in" }],
492
461
  outputs: [{ id: "true", label: "true" }, { id: "false", label: "false" }],
493
462
  configSchema: [
494
- { type: "expression", key: "condition", label: "Condition", example: "{{ $json.active }}", required: true }
463
+ {
464
+ type: "select",
465
+ key: "match",
466
+ label: "Match",
467
+ default: "all",
468
+ options: [
469
+ { value: "all", label: "All conditions (AND)" },
470
+ { value: "any", label: "Any condition (OR)" }
471
+ ]
472
+ },
473
+ {
474
+ type: "repeater",
475
+ key: "conditions",
476
+ label: "Conditions",
477
+ description: "Routes to `true` when these match, otherwise `false`.",
478
+ titleKey: "left",
479
+ addLabel: "Add condition",
480
+ minItems: 1,
481
+ fields: [
482
+ { type: "expression", key: "left", label: "Value", example: "{{ $json.status }}", required: true },
483
+ {
484
+ type: "select",
485
+ key: "operator",
486
+ label: "Is",
487
+ default: "eq",
488
+ options: [
489
+ { value: "eq", label: "equal to" },
490
+ { value: "neq", label: "not equal to" },
491
+ { value: "contains", label: "contains" },
492
+ { value: "not_contains", label: "does not contain" },
493
+ { value: "gt", label: "greater than" },
494
+ { value: "gte", label: "greater than or equal to" },
495
+ { value: "lt", label: "less than" },
496
+ { value: "lte", label: "less than or equal to" },
497
+ { value: "truthy", label: "true" },
498
+ { value: "falsy", label: "false" },
499
+ { value: "empty", label: "empty" },
500
+ { value: "not_empty", label: "not empty" }
501
+ ]
502
+ },
503
+ { type: "text", key: "right", label: "Compared to", placeholder: "active" }
504
+ ],
505
+ default: [{ left: "", operator: "eq", right: "" }]
506
+ },
507
+ {
508
+ type: "expression",
509
+ key: "condition",
510
+ label: "Raw expression (advanced)",
511
+ example: "{{ $json.active && $json.score > 10 }}",
512
+ description: "Escape hatch for logic the builder can't express. Overrides the conditions above when set."
513
+ }
495
514
  ]
496
515
  },
497
516
  {
@@ -581,12 +600,35 @@ var KINDS = [
581
600
  description: "Reshape data with an expression.",
582
601
  icon: "\u0192",
583
602
  configSchema: [
603
+ {
604
+ type: "select",
605
+ key: "mode",
606
+ label: "Build the output",
607
+ default: "fields",
608
+ options: [
609
+ { value: "fields", label: "Field by field" },
610
+ { value: "expression", label: "One expression" }
611
+ ]
612
+ },
613
+ {
614
+ type: "repeater",
615
+ key: "fields",
616
+ label: "Output fields",
617
+ description: "Each row becomes a key on the result.",
618
+ titleKey: "key",
619
+ addLabel: "Add field",
620
+ fields: [
621
+ { type: "text", key: "key", label: "Key", required: true, placeholder: "name" },
622
+ { type: "expression", key: "value", label: "Value", example: "{{ $json.first }}", required: true }
623
+ ],
624
+ default: [{ key: "", value: "" }]
625
+ },
584
626
  {
585
627
  type: "expression",
586
628
  key: "expression",
587
- label: "Expression",
629
+ label: "Expression (advanced)",
588
630
  example: "{{ { id: $json.id, name: $json.first + ' ' + $json.last } }}",
589
- required: true
631
+ description: "Used when the mode above is set to one expression."
590
632
  }
591
633
  ]
592
634
  },
@@ -636,7 +678,15 @@ var KINDS = [
636
678
  },
637
679
  { type: "text", key: "table", label: "Table / collection", required: true },
638
680
  { type: "text", key: "key", label: "Key" },
639
- { type: "json", key: "where", label: "Where (JSON)", description: "For query/list operations." },
681
+ {
682
+ type: "keyvalue",
683
+ key: "where",
684
+ label: "Where",
685
+ description: "Field/value pairs to match. For query and list operations.",
686
+ keyLabel: "Field",
687
+ valueLabel: "Equals",
688
+ addLabel: "Add filter"
689
+ },
640
690
  { type: "expression", key: "value", label: "Value (set only)", example: "{{ $json }}" },
641
691
  { type: "credential", key: "store", label: "Data store", credentialType: "data_store" }
642
692
  ]
@@ -678,13 +728,32 @@ var KINDS = [
678
728
  { type: "expression", key: "prompt", label: "User prompt", example: "{{ $json.question }}", required: true },
679
729
  { type: "number", key: "temperature", label: "Temperature", min: 0, max: 2, step: 0.1, default: 0.7 },
680
730
  { type: "number", key: "max_tokens", label: "Max tokens", min: 1, max: 8192, default: 1024 },
681
- { type: "json", key: "tools", label: "Tools (JSON)", description: "Optional Anthropic-style tool definitions." },
731
+ {
732
+ type: "repeater",
733
+ key: "tools",
734
+ label: "Tools",
735
+ description: "Tools the model may call.",
736
+ titleKey: "name",
737
+ addLabel: "Add tool",
738
+ fields: [
739
+ { type: "text", key: "name", label: "Name", required: true, placeholder: "search_index" },
740
+ { type: "text", key: "description", label: "When to use it" },
741
+ {
742
+ type: "json",
743
+ key: "input_schema",
744
+ label: "Input schema",
745
+ description: "JSON Schema for the tool's arguments."
746
+ }
747
+ ]
748
+ },
682
749
  { type: "credential", key: "credential", label: "API credential", credentialType: "llm_credential" }
683
750
  ]
684
751
  },
685
752
  {
686
- name: "@particle-academy/llm_branch",
687
- aliases: ["llm_branch", "@fancy/llm_branch"],
753
+ name: "@particle-academy/llm_router",
754
+ // Every id this node has ever shipped under keeps resolving — MOIC's saved
755
+ // flows carry the bare `llm_branch`.
756
+ aliases: ["llm_router", "llm_branch", "@fancy/llm_branch", "@fancy/llm_router"],
688
757
  category: "ai",
689
758
  label: "LLM Router",
690
759
  description: "Let a model choose which route the flow takes.",
@@ -696,7 +765,7 @@ var KINDS = [
696
765
  // A shuttle, not an engine: it carries the routes out to whatever LLM
697
766
  // client the host registered and carries the choice back. No provider SDK
698
767
  // reaches core, so this stays a builtin without adding a dependency.
699
- executor: llmBranchExecutor,
768
+ executor: llmRouterExecutor,
700
769
  configSchema: [
701
770
  {
702
771
  type: "textarea",
@@ -793,7 +862,17 @@ var KINDS = [
793
862
  configSchema: [
794
863
  ...HTTP_METHODS,
795
864
  { type: "text", key: "url", label: "URL", placeholder: "https://api.example.com/...", required: true },
796
- { type: "json", key: "headers", label: "Headers", default: { "content-type": "application/json" } },
865
+ {
866
+ type: "keyvalue",
867
+ key: "headers",
868
+ label: "Headers",
869
+ keyLabel: "Header",
870
+ valueLabel: "Value",
871
+ keyPlaceholder: "content-type",
872
+ valuePlaceholder: "application/json",
873
+ addLabel: "Add header",
874
+ default: { "content-type": "application/json" }
875
+ },
797
876
  { type: "json", key: "body", label: "Body" },
798
877
  { type: "credential", key: "auth", label: "Auth", credentialType: "api_credential" }
799
878
  ]
@@ -807,7 +886,14 @@ var KINDS = [
807
886
  icon: "\u2197",
808
887
  configSchema: [
809
888
  { type: "text", key: "url", label: "URL", required: true },
810
- { type: "json", key: "headers", label: "Headers" },
889
+ {
890
+ type: "keyvalue",
891
+ key: "headers",
892
+ label: "Headers",
893
+ keyLabel: "Header",
894
+ valueLabel: "Value",
895
+ addLabel: "Add header"
896
+ },
811
897
  { type: "expression", key: "payload", label: "Payload", required: true, example: "{{ $json }}" }
812
898
  ]
813
899
  },
@@ -897,6 +983,6 @@ function buildNodeTypes() {
897
983
  return map;
898
984
  }
899
985
 
900
- export { BUILTIN_KINDS, DEFAULT_MAX_DEPTH, RegistryNode, buildNodeTypes, capabilityStatus, declaredRoutes, getLlmClient, getWorkflowResolver, llmBranchExecutor, registerBuiltinKinds, registerLlmClient, registerWorkflowResolver, resolveFallbackPort, subflowExecutor, subflowMode, subflowPorts };
901
- //# sourceMappingURL=chunk-IK5DS5JP.js.map
902
- //# sourceMappingURL=chunk-IK5DS5JP.js.map
986
+ export { BUILTIN_KINDS, DEFAULT_MAX_DEPTH, RegistryNode, buildNodeTypes, declaredRoutes, llmRouterExecutor, registerBuiltinKinds, resolveFallbackPort, subflowExecutor, subflowMode, subflowPorts };
987
+ //# sourceMappingURL=chunk-7U3EP4Q5.js.map
988
+ //# sourceMappingURL=chunk-7U3EP4Q5.js.map