@particle-academy/fancy-flow 0.65.1 → 0.66.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 (106) hide show
  1. package/README.md +100 -3
  2. package/dist/{FlowViewer-BI_AyyvU.d.ts → FlowViewer-BCc_BvuD.d.ts} +1 -1
  3. package/dist/{FlowViewer-DABx-6A-.d.cts → FlowViewer-CoAJ3o52.d.cts} +1 -1
  4. package/dist/{HumanPrompt-Bpp3JiVH.d.cts → HumanPrompt--pXDOyYC.d.cts} +2 -2
  5. package/dist/{HumanPrompt-BKThiyjB.d.ts → HumanPrompt-DXm_FP9z.d.ts} +2 -2
  6. package/dist/chunk-2QXKDLGT.js +58 -0
  7. package/dist/chunk-2QXKDLGT.js.map +1 -0
  8. package/dist/{chunk-77V4QC6Y.js → chunk-B7HKJJVV.js} +3 -3
  9. package/dist/{chunk-77V4QC6Y.js.map → chunk-B7HKJJVV.js.map} +1 -1
  10. package/dist/{chunk-MBVX4ZRB.js → chunk-FNYLKSFJ.js} +3 -3
  11. package/dist/{chunk-MBVX4ZRB.js.map → chunk-FNYLKSFJ.js.map} +1 -1
  12. package/dist/{chunk-JF6WCRBU.js → chunk-HQGOFGAL.js} +861 -40
  13. package/dist/chunk-HQGOFGAL.js.map +1 -0
  14. package/dist/{chunk-5H54OTKT.js → chunk-IEYCNFXZ.js} +3 -3
  15. package/dist/{chunk-5H54OTKT.js.map → chunk-IEYCNFXZ.js.map} +1 -1
  16. package/dist/{chunk-W5DPKJY4.js → chunk-MTONBM53.js} +4 -4
  17. package/dist/chunk-MTONBM53.js.map +1 -0
  18. package/dist/{chunk-RIAFHQT5.js → chunk-PNHZHEWE.js} +3 -3
  19. package/dist/{chunk-RIAFHQT5.js.map → chunk-PNHZHEWE.js.map} +1 -1
  20. package/dist/{chunk-EO6444T2.js → chunk-UCCTXJXL.js} +4 -4
  21. package/dist/{chunk-EO6444T2.js.map → chunk-UCCTXJXL.js.map} +1 -1
  22. package/dist/connectors.d.cts +2 -2
  23. package/dist/connectors.d.ts +2 -2
  24. package/dist/durable/index.d.cts +2 -2
  25. package/dist/durable/index.d.ts +2 -2
  26. package/dist/durable.cjs +837 -47
  27. package/dist/durable.cjs.map +1 -1
  28. package/dist/durable.js +2 -3
  29. package/dist/durable.js.map +1 -1
  30. package/dist/engine.cjs +843 -132
  31. package/dist/engine.cjs.map +1 -1
  32. package/dist/engine.d.cts +6 -7
  33. package/dist/engine.d.ts +6 -7
  34. package/dist/engine.js +5 -6
  35. package/dist/engine.js.map +1 -1
  36. package/dist/fields/react-fancy.d.cts +3 -3
  37. package/dist/fields/react-fancy.d.ts +3 -3
  38. package/dist/index.cjs +853 -140
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.cts +65 -36
  41. package/dist/index.d.ts +65 -36
  42. package/dist/index.js +19 -18
  43. package/dist/index.js.map +1 -1
  44. package/dist/layout/index.d.cts +1 -1
  45. package/dist/layout/index.d.ts +1 -1
  46. package/dist/llm/prism.cjs.map +1 -1
  47. package/dist/llm/prism.d.cts +1 -2
  48. package/dist/llm/prism.d.ts +1 -2
  49. package/dist/llm/prism.js +1 -1
  50. package/dist/llm/vercel-ai.cjs.map +1 -1
  51. package/dist/llm/vercel-ai.d.cts +1 -2
  52. package/dist/llm/vercel-ai.d.ts +1 -2
  53. package/dist/llm/vercel-ai.js +1 -1
  54. package/dist/registry/index.d.cts +5 -6
  55. package/dist/registry/index.d.ts +5 -6
  56. package/dist/{registry-CntAdUcY.d.cts → registry-BcgllSk2.d.cts} +1 -1
  57. package/dist/{registry-DYoV-MQi.d.ts → registry-h-2QswZ5.d.ts} +1 -1
  58. package/dist/registry.cjs +867 -37
  59. package/dist/registry.cjs.map +1 -1
  60. package/dist/registry.js +3 -3
  61. package/dist/{run-cohort-C1nRh_mi.d.ts → run-cohort-CjUBWg6X.d.ts} +2 -2
  62. package/dist/{run-cohort-CBRAUj-z.d.cts → run-cohort-DL9JzzPU.d.cts} +2 -2
  63. package/dist/{run-flow-2XsHBwpd.d.cts → run-flow-B_8hgO5_.d.cts} +1 -1
  64. package/dist/{run-flow-D7AOCcfE.d.ts → run-flow-CxEGBOxd.d.ts} +1 -1
  65. package/dist/runtime/index.d.cts +5 -5
  66. package/dist/runtime/index.d.ts +5 -5
  67. package/dist/runtime.cjs +837 -36
  68. package/dist/runtime.cjs.map +1 -1
  69. package/dist/runtime.js +4 -4
  70. package/dist/schema/index.d.cts +19 -6
  71. package/dist/schema/index.d.ts +19 -6
  72. package/dist/schema.cjs +837 -36
  73. package/dist/schema.cjs.map +1 -1
  74. package/dist/schema.js +4 -4
  75. package/dist/screens.cjs +837 -36
  76. package/dist/screens.cjs.map +1 -1
  77. package/dist/screens.d.cts +2 -2
  78. package/dist/screens.d.ts +2 -2
  79. package/dist/screens.js +5 -5
  80. package/dist/terminal/fancy-term-host.cjs +99 -0
  81. package/dist/terminal/fancy-term-host.cjs.map +1 -0
  82. package/dist/terminal/fancy-term-host.d.cts +76 -0
  83. package/dist/terminal/fancy-term-host.d.ts +76 -0
  84. package/dist/terminal/fancy-term-host.js +92 -0
  85. package/dist/terminal/fancy-term-host.js.map +1 -0
  86. package/dist/types-B-Syk9-M.d.cts +620 -0
  87. package/dist/types-B-Syk9-M.d.ts +620 -0
  88. package/dist/{types-D71SKA5A.d.cts → types-C2ATTmjN.d.cts} +1 -1
  89. package/dist/{types-Ckhz-YwC.d.ts → types-VunLGqrn.d.ts} +1 -1
  90. package/dist/ux.cjs +845 -39
  91. package/dist/ux.cjs.map +1 -1
  92. package/dist/ux.d.cts +14 -2
  93. package/dist/ux.d.ts +14 -2
  94. package/dist/ux.js +10 -5
  95. package/dist/ux.js.map +1 -1
  96. package/package.json +11 -1
  97. package/dist/capabilities-BYa5p5jw.d.cts +0 -112
  98. package/dist/capabilities-COOXRiNL.d.ts +0 -112
  99. package/dist/chunk-JF6WCRBU.js.map +0 -1
  100. package/dist/chunk-UM4C46AF.js +0 -103
  101. package/dist/chunk-UM4C46AF.js.map +0 -1
  102. package/dist/chunk-USL4FMFU.js +0 -41
  103. package/dist/chunk-USL4FMFU.js.map +0 -1
  104. package/dist/chunk-W5DPKJY4.js.map +0 -1
  105. package/dist/types-JFYjPJAG.d.cts +0 -333
  106. package/dist/types-JFYjPJAG.d.ts +0 -333
@@ -1,103 +0,0 @@
1
- // src/expressions/expr.ts
2
- function wholeExpression(trimmed) {
3
- if (trimmed.length < 4) return null;
4
- if (!trimmed.startsWith("{{") || !trimmed.endsWith("}}")) return null;
5
- return trimmed.slice(2, -2);
6
- }
7
- function interpolate(template, resolve) {
8
- let out = "";
9
- let i = 0;
10
- for (; ; ) {
11
- const open = template.indexOf("{{", i);
12
- if (open === -1) return out + template.slice(i);
13
- const close = template.indexOf("}}", open + 2);
14
- if (close === -1) return out + template.slice(i);
15
- out += template.slice(i, open) + resolve(template.slice(open + 2, close));
16
- i = close + 2;
17
- }
18
- }
19
- var FALSY_STRINGS = /* @__PURE__ */ new Set(["", "0", "false", "no", "off", "null"]);
20
- var UnresolvedPathError = class extends Error {
21
- constructor(path) {
22
- super(
23
- `Expression path "${path}" did not resolve. Under the "throw" policy an unresolvable path is an error rather than an empty string.`
24
- );
25
- this.path = path;
26
- this.name = "UnresolvedPathError";
27
- }
28
- };
29
- function tryResolvePath(path, context) {
30
- const unresolved = { resolved: false, value: null };
31
- const trimmed = path.trim();
32
- if (trimmed === "") return unresolved;
33
- const segments = trimmed.split(".");
34
- let cursor;
35
- const head = segments[0];
36
- if (head === "$json" || head === "$input") {
37
- cursor = context !== null && typeof context === "object" && "in" in context ? context.in : context;
38
- segments.shift();
39
- } else {
40
- cursor = context;
41
- }
42
- for (const segment of segments) {
43
- if (cursor === null || cursor === void 0) return unresolved;
44
- if (typeof cursor !== "object") return unresolved;
45
- const next = cursor[segment];
46
- if (next === void 0) return unresolved;
47
- cursor = next;
48
- }
49
- return { resolved: true, value: cursor === void 0 ? null : cursor };
50
- }
51
- function resolvePath(path, context) {
52
- return tryResolvePath(path, context).value;
53
- }
54
- function evaluateExpression(template, context, options = {}) {
55
- if (typeof template !== "string") return template;
56
- const policy = options.onUnresolved ?? "empty";
57
- const whole = wholeExpression(template.trim());
58
- if (whole !== null) {
59
- const r = tryResolvePath(whole, context);
60
- if (r.resolved) return r.value;
61
- if (policy === "throw") throw new UnresolvedPathError(whole);
62
- return policy === "keep" ? template : null;
63
- }
64
- return interpolate(template, (path) => {
65
- const r = tryResolvePath(path, context);
66
- if (r.resolved) return stringify(r.value);
67
- if (policy === "throw") throw new UnresolvedPathError(path);
68
- return policy === "keep" ? `{{${path}}}` : "";
69
- });
70
- }
71
- function truthy(value) {
72
- if (typeof value === "boolean") return value;
73
- if (value === null || value === void 0) return false;
74
- if (typeof value === "string") return !FALSY_STRINGS.has(value.trim().toLowerCase());
75
- if (Array.isArray(value)) return value.length > 0;
76
- if (typeof value === "number") return value !== 0;
77
- return Boolean(value);
78
- }
79
- function text(value) {
80
- return stringify(value);
81
- }
82
- function stringify(value) {
83
- if (typeof value === "string") return value;
84
- if (typeof value === "boolean") return value ? "true" : "false";
85
- if (value === null || value === void 0) return "";
86
- if (typeof value === "number" || typeof value === "bigint") return String(value);
87
- try {
88
- return JSON.stringify(value) ?? "";
89
- } catch {
90
- return "";
91
- }
92
- }
93
- function evaluateConfig(config, context, options = {}) {
94
- const out = {};
95
- for (const [key, value] of Object.entries(config)) {
96
- out[key] = Array.isArray(value) ? value.map((v) => evaluateExpression(v, context, options)) : value !== null && typeof value === "object" ? evaluateConfig(value, context, options) : evaluateExpression(value, context, options);
97
- }
98
- return out;
99
- }
100
-
101
- export { UnresolvedPathError, evaluateConfig, evaluateExpression, resolvePath, text, truthy, tryResolvePath };
102
- //# sourceMappingURL=chunk-UM4C46AF.js.map
103
- //# sourceMappingURL=chunk-UM4C46AF.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/expressions/expr.ts"],"names":[],"mappings":"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","file":"chunk-UM4C46AF.js","sourcesContent":["/**\n * `{{ }}` resolution — the TypeScript twin of `FancyFlow\\Nodes\\Support\\Expr`.\n *\n * ## Why this is opt-in and not wired into `runFlow`\n *\n * The PHP runtime resolves expressions inside its batteries-included executors.\n * The JS runtime never has: `runFlow` hands `node.data.config` to your executor\n * verbatim, so every host that uses `{{ }}` today already resolves it itself.\n * Turning resolution on inside `runFlow` would interpolate a second time over\n * values those hosts had already substituted — silently, and only for graphs\n * that happen to contain `{{` in their DATA.\n *\n * So this exports the semantics and changes no behaviour. Call it yourself:\n *\n * ```ts\n * const url = evaluateExpression(node.data.config.url, inputs);\n * ```\n *\n * ## The semantics are the PHP file's, deliberately\n *\n * This is not a general expression language and must not grow into one. It\n * resolves a dot-path against a context and nothing else — no arithmetic, no\n * comparisons, no calls. Hosts that want real expressions override the executor\n * (the PHP docblock points at symfony/expression-language for the same reason).\n *\n * Divergence here is a correctness bug rather than a style difference: the same\n * graph is authored once and may run on either runtime. `suites/shared/expr`\n * in `@particle-academy/fancy-conformance` is the fixture table both sides run,\n * so parity is a test result instead of a claim.\n */\n\n/** Anything a context or a resolved value can be. */\nexport type ExprValue = unknown;\n\n/** The context an expression resolves against — the executor's `inputs`. */\nexport type ExprContext = Record<string, ExprValue>;\n\n/**\n * `{{ … }}` is parsed by SCANNING, not by a regular expression.\n *\n * Two CodeQL `js/polynomial-redos` alerts (#2, #3) came out of the regex\n * version, and the second survived the obvious fix — which is the useful part.\n * Dropping the ambiguous `\\s*` around the capture killed one witness\n * (`'{{{{' + ' '.repeat(n)`) and CodeQL immediately produced another\n * (`'{{{{a'` repeated). That is not a pattern bug: a GLOBAL lazy scan for a\n * delimiter that never arrives is quadratic by construction — O(n) starts, each\n * scanning O(n) forward — and no amount of tuning the pattern removes it.\n *\n * `indexOf` has no backtracking at all. Each character is visited a bounded\n * number of times, so this is linear by construction rather than by careful\n * pattern-writing, and there is no next witness to find.\n *\n * The behaviour is deliberately identical to the regexes it replaces, including\n * the odd corner: `{{a}}{{b}}` is a WHOLE expression whose path is `a}}{{b`\n * (which resolves to null), because the old pattern was `$`-anchored and its\n * lazy capture had to grow to reach the end. The PHP twin does the same, and\n * `shared/expr` is what holds the two together.\n */\n\n/** The inner text of a template that is exactly one expression, else `null`. */\nfunction wholeExpression(trimmed: string): string | null {\n if (trimmed.length < 4) return null;\n if (!trimmed.startsWith(\"{{\") || !trimmed.endsWith(\"}}\")) return null;\n return trimmed.slice(2, -2);\n}\n\n/**\n * Replace every `{{ … }}` run, left to right, in a single pass.\n *\n * An unterminated `{{` is literal text — the same thing the regex did by simply\n * not matching, and the case an author hits constantly while typing.\n */\nfunction interpolate(template: string, resolve: (path: string) => string): string {\n let out = \"\";\n let i = 0;\n\n for (;;) {\n const open = template.indexOf(\"{{\", i);\n if (open === -1) return out + template.slice(i);\n\n const close = template.indexOf(\"}}\", open + 2);\n if (close === -1) return out + template.slice(i);\n\n out += template.slice(i, open) + resolve(template.slice(open + 2, close));\n i = close + 2;\n }\n}\n\n/**\n * Strings PHP's `Expr::truthy` treats as false.\n *\n * `\"0\"` and `\"false\"` are the ones that matter: a branch condition arriving as\n * text from a form or a JSON body would otherwise be truthy for every non-empty\n * string, and `\"false\"` taking the true branch is the kind of bug that looks\n * like the engine is broken.\n */\nconst FALSY_STRINGS = new Set([\"\", \"0\", \"false\", \"no\", \"off\", \"null\"]);\n\n/**\n * What a resolution attempt ANSWERS — did the path resolve, and to what.\n *\n * `resolvePath` cannot express this, and that is the defect it exists to fix:\n * it returns `null` both for \"this path does not exist\" and for \"this path\n * exists and holds null\". One value standing for two states.\n *\n * At the interpolation layer the collapse is worse, because `null` stringifies\n * to `\"\"`. A consumer put it exactly:\n *\n * > \"An unresolvable path yields `''`, so a wrong field is indistinguishable\n * > from an empty one at runtime.\"\n *\n * A misspelled field renders as an empty string, which looks precisely like a\n * field that is legitimately empty. The graph runs, the node succeeds, and the\n * output is quietly missing a value nobody is told about — worst on\n * LLM-authored graphs, where the field name was guessed in the first place.\n *\n * Same shape as the four `??` collapses fixed across all four runtimes on\n * 2026-08-26 (absent vs null), one layer up: **presence is the only correct\n * test, and a return value that cannot express presence cannot be tested for\n * it.** Hence a second return channel rather than a cleverer sentinel — every\n * sentinel is a legal value for somebody.\n */\nexport interface Resolution {\n /** Whether the path resolved at all. `false` means it does not exist. */\n resolved: boolean;\n /** The value, when `resolved`. `null` when not — do not read it blind. */\n value: ExprValue;\n}\n\n/** Thrown by the `\"throw\"` policy when a path does not resolve. */\nexport class UnresolvedPathError extends Error {\n constructor(readonly path: string) {\n super(\n `Expression path \"${path}\" did not resolve. ` +\n `Under the \"throw\" policy an unresolvable path is an error rather than an empty string.`,\n );\n this.name = \"UnresolvedPathError\";\n }\n}\n\n/**\n * What evaluation does with a path that does not resolve.\n *\n * - `\"empty\"` — today's behaviour, and the DEFAULT. Interpolates to `\"\"`; a\n * whole expression yields `null`. Unchanged so that widening this API breaks\n * nobody: 40 call sites across the runtimes assume it.\n * - `\"keep\"` — leave the `{{ … }}` text in place. The failure becomes VISIBLE\n * in the output without stopping the run, which is what you want for\n * human-reviewed content: a rendered `{{ in.recipient_naem }}` is self-\n * diagnosing in a way an absence never is.\n * - `\"throw\"` — refuse. For hosts that would rather fail a run than deliver a\n * silently incomplete result.\n *\n * Opt-in before default at the request of the consumer who reported it — the\n * host with the most LLM-authored graphs, and so both the biggest beneficiary\n * and the right place for it to break first if it is going to.\n */\nexport type UnresolvedPolicy = \"empty\" | \"keep\" | \"throw\";\n\n/** Options for {@link evaluateExpression} / {@link evaluateConfig}. */\nexport interface EvaluateOptions {\n /** What to do with a path that does not resolve. Default `\"empty\"`. */\n onUnresolved?: UnresolvedPolicy;\n}\n\n/**\n * Resolve a dot-path, reporting WHETHER it resolved.\n *\n * The same walk as `resolvePath` — deliberately, so the two can never disagree\n * about what resolves; `resolvePath` is defined in terms of this one below.\n *\n * A note on JS having two absent values: a key present with the value\n * `undefined` reports `resolved: false`, matching `resolvePath`'s long-standing\n * behaviour. It cannot arise from graph data (JSON has no `undefined`), and\n * changing it would make the two functions disagree for no reachable gain.\n */\nexport function tryResolvePath(path: string, context: ExprContext): Resolution {\n const unresolved: Resolution = { resolved: false, value: null };\n\n const trimmed = path.trim();\n if (trimmed === \"\") return unresolved;\n\n const segments = trimmed.split(\".\");\n let cursor: ExprValue;\n\n const head = segments[0];\n if (head === \"$json\" || head === \"$input\") {\n cursor = context !== null && typeof context === \"object\" && \"in\" in context ? context.in : context;\n segments.shift();\n } else {\n cursor = context;\n }\n\n for (const segment of segments) {\n if (cursor === null || cursor === undefined) return unresolved;\n if (typeof cursor !== \"object\") return unresolved;\n // Arrays are indexed by their numeric keys, matching PHP's list access.\n const next = (cursor as Record<string, ExprValue>)[segment];\n if (next === undefined) return unresolved;\n cursor = next;\n }\n\n return { resolved: true, value: cursor === undefined ? null : cursor };\n}\n\n/**\n * Resolve a dot-path against the context, honouring the `$json` / `$input`\n * alias.\n *\n * Both aliases point at the `in` port value when the context has one, and at\n * the whole context otherwise — the same fallback the PHP does, which is what\n * makes `{{ $json.x }}` work on a trigger node that has no upstream input.\n *\n * A path that does not resolve returns `null`, never `undefined`: PHP has one\n * absent value and JS has two, and letting the difference leak would make the\n * two runtimes disagree about `{{ missing }}` for no useful reason.\n */\nexport function resolvePath(path: string, context: ExprContext): ExprValue {\n // Defined in terms of `tryResolvePath` rather than repeating the walk. Two\n // copies of a traversal agree right up until someone edits one of them, and\n // nothing anywhere reports that -- the same reason the shared conformance\n // tables exist instead of hand-copied fixture rows.\n return tryResolvePath(path, context).value;\n}\n\n/**\n * Evaluate a template against a context.\n *\n * A string that is EXACTLY one expression returns the resolved value with its\n * type intact — `{{ $json.count }}` gives you a number, not `\"3\"`. Anything\n * else interpolates each run as text. That distinction is load-bearing: it is\n * what lets a config field carry either a value or a sentence.\n *\n * Non-string templates pass through untouched, so this is safe to map over a\n * whole config object.\n */\nexport function evaluateExpression(\n template: ExprValue,\n context: ExprContext,\n options: EvaluateOptions = {},\n): ExprValue {\n if (typeof template !== \"string\") return template;\n\n const policy = options.onUnresolved ?? \"empty\";\n\n const whole = wholeExpression(template.trim());\n if (whole !== null) {\n const r = tryResolvePath(whole, context);\n if (r.resolved) return r.value;\n\n // The whole-expression branch returns `null` under `\"empty\"`, not `\"\"` --\n // that is what it has always done, and the asymmetry is deliberate: this\n // branch preserves TYPE, so its absent value is the typed one.\n if (policy === \"throw\") throw new UnresolvedPathError(whole);\n return policy === \"keep\" ? template : null;\n }\n\n return interpolate(template, (path) => {\n const r = tryResolvePath(path, context);\n if (r.resolved) return stringify(r.value);\n if (policy === \"throw\") throw new UnresolvedPathError(path);\n return policy === \"keep\" ? `{{${path}}}` : \"\";\n });\n}\n\n/**\n * Truthiness for branch / switch decisions.\n *\n * Mirrors PHP's rules rather than JavaScript's, because the graph is authored\n * once and may run on either side. The two disagree in exactly the places a\n * workflow hits: `\"0\"` and `\"false\"` are truthy in JS and falsy here, and an\n * empty array is truthy in JS and falsy here.\n */\nexport function truthy(value: ExprValue): boolean {\n if (typeof value === \"boolean\") return value;\n if (value === null || value === undefined) return false;\n if (typeof value === \"string\") return !FALSY_STRINGS.has(value.trim().toLowerCase());\n if (Array.isArray(value)) return value.length > 0;\n if (typeof value === \"number\") return value !== 0;\n return Boolean(value);\n}\n\n/** Coerce a value to text the way interpolation does. */\nexport function text(value: ExprValue): string {\n return stringify(value);\n}\n\nfunction stringify(value: ExprValue): string {\n if (typeof value === \"string\") return value;\n if (typeof value === \"boolean\") return value ? \"true\" : \"false\";\n if (value === null || value === undefined) return \"\";\n if (typeof value === \"number\" || typeof value === \"bigint\") return String(value);\n try {\n return JSON.stringify(value) ?? \"\";\n } catch {\n return \"\";\n }\n}\n\n/**\n * Resolve every string in a config object, one level of nesting at a time.\n *\n * The convenience most hosts actually want, and the shape their hand-rolled\n * version usually takes. Opt-in like the rest of this module.\n */\nexport function evaluateConfig<T extends Record<string, ExprValue>>(\n config: T,\n context: ExprContext,\n options: EvaluateOptions = {},\n): T {\n const out: Record<string, ExprValue> = {};\n for (const [key, value] of Object.entries(config)) {\n out[key] = Array.isArray(value)\n ? value.map((v) => evaluateExpression(v, context, options))\n : value !== null && typeof value === \"object\"\n ? evaluateConfig(value as Record<string, ExprValue>, context, options)\n : evaluateExpression(value, context, options);\n }\n return out as T;\n}\n"]}
@@ -1,41 +0,0 @@
1
- // src/registry/capabilities.ts
2
- var llmClient = null;
3
- function registerLlmClient(client) {
4
- llmClient = client;
5
- return () => {
6
- if (llmClient === client) llmClient = null;
7
- };
8
- }
9
- function getLlmClient() {
10
- return llmClient;
11
- }
12
- function isResolutionFailure(value) {
13
- return typeof value === "object" && value !== null && "reason" in value && value.reason !== void 0;
14
- }
15
- var workflowResolver = null;
16
- function registerWorkflowResolver(resolver) {
17
- workflowResolver = resolver;
18
- return () => {
19
- if (workflowResolver === resolver) workflowResolver = null;
20
- };
21
- }
22
- function getWorkflowResolver() {
23
- return workflowResolver;
24
- }
25
- function capabilityStatus() {
26
- let documentReady = false;
27
- try {
28
- documentReady = Boolean(globalThis.__fancyFlowDocumentAdapter);
29
- } catch {
30
- documentReady = false;
31
- }
32
- return {
33
- llm: llmClient !== null,
34
- workflow_resolver: workflowResolver !== null,
35
- document: documentReady
36
- };
37
- }
38
-
39
- export { capabilityStatus, getLlmClient, getWorkflowResolver, isResolutionFailure, registerLlmClient, registerWorkflowResolver };
40
- //# sourceMappingURL=chunk-USL4FMFU.js.map
41
- //# sourceMappingURL=chunk-USL4FMFU.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/registry/capabilities.ts"],"names":[],"mappings":";AAwDA,IAAI,SAAA,GAA8B,IAAA;AAG3B,SAAS,kBAAkB,MAAA,EAA+B;AAC/D,EAAA,SAAA,GAAY,MAAA;AACZ,EAAA,OAAO,MAAM;AACX,IAAA,IAAI,SAAA,KAAc,QAAQ,SAAA,GAAY,IAAA;AAAA,EACxC,CAAA;AACF;AAEO,SAAS,YAAA,GAAiC;AAC/C,EAAA,OAAO,SAAA;AACT;AAqDO,SAAS,oBAAoB,KAAA,EAA+D;AACjG,EAAA,OACE,OAAO,UAAU,QAAA,IACjB,KAAA,KAAU,QACV,QAAA,IAAY,KAAA,IACX,MAAoC,MAAA,KAAW,MAAA;AAEpD;AAEA,IAAI,gBAAA,GAA4C,IAAA;AAGzC,SAAS,yBAAyB,QAAA,EAAwC;AAC/E,EAAA,gBAAA,GAAmB,QAAA;AACnB,EAAA,OAAO,MAAM;AACX,IAAA,IAAI,gBAAA,KAAqB,UAAU,gBAAA,GAAmB,IAAA;AAAA,EACxD,CAAA;AACF;AAEO,SAAS,mBAAA,GAA+C;AAC7D,EAAA,OAAO,gBAAA;AACT;AAYO,SAAS,gBAAA,GAAkD;AAGhE,EAAA,IAAI,aAAA,GAAgB,KAAA;AACpB,EAAA,IAAI;AAEF,IAAA,aAAA,GAAgB,OAAA,CAAS,WAAmB,0BAA0B,CAAA;AAAA,EACxE,CAAA,CAAA,MAAQ;AACN,IAAA,aAAA,GAAgB,KAAA;AAAA,EAClB;AACA,EAAA,OAAO;AAAA,IACL,KAAK,SAAA,KAAc,IAAA;AAAA,IACnB,mBAAmB,gBAAA,KAAqB,IAAA;AAAA,IACxC,QAAA,EAAU;AAAA,GACZ;AACF","file":"chunk-USL4FMFU.js","sourcesContent":["import type { FlowGraph } from \"../types\";\n\n/**\n * Host capabilities — the services core nodes need but must never depend on.\n *\n * A node that imports a provider SDK forces every consumer to install it: a\n * workflow app that never calls a model should not inherit an LLM dependency.\n * So core declares the CONTRACT and the host supplies the implementation, the\n * same arrangement `renderDocumentField` already uses for documents.\n *\n * That keeps opinionated nodes in core without their opinions: `llm_branch`\n * ships the routing semantics, port derivation and config UI, while whichever\n * client the host registers — Prism, an OpenAI SDK, a local model, a fake in a\n * test — decides how the question actually gets asked.\n *\n * Registration is deliberately explicit and typed per capability rather than a\n * stringly-keyed bag, so a missing one is a clear error at the seam instead of\n * an undefined somewhere downstream.\n */\n\n// ── LLM ─────────────────────────────────────────────────────────────────────\n\nexport type LlmRoute = { port: string; description?: string };\n\nexport type LlmRouteRequest = {\n /** Optional framing for the decision. */\n system?: string;\n /** What the model is deciding about. */\n prompt: string;\n /** The ports it must choose between. */\n routes: LlmRoute[];\n provider?: string;\n model?: string;\n /** Host-resolved credential reference, never a raw key. */\n credential?: string;\n};\n\nexport type LlmRouteChoice = {\n /** Must be one of the requested route ports. */\n port: string;\n /** Why — carried down the chosen port so a run is explainable afterwards. */\n reason?: string;\n};\n\n/**\n * The only thing core asks of an LLM: given routes, pick one.\n *\n * Deliberately not a general chat interface. A narrow contract is one a host\n * can satisfy in a few lines over any SDK, and it keeps the choice\n * machine-checkable — an implementation should constrain the model to the\n * declared ports (structured output / enum) rather than parsing prose.\n */\nexport type LlmClient = {\n chooseRoute: (request: LlmRouteRequest) => Promise<LlmRouteChoice> | LlmRouteChoice;\n};\n\nlet llmClient: LlmClient | null = null;\n\n/** Install the host's LLM client. Returns an unregister function. */\nexport function registerLlmClient(client: LlmClient): () => void {\n llmClient = client;\n return () => {\n if (llmClient === client) llmClient = null;\n };\n}\n\nexport function getLlmClient(): LlmClient | null {\n return llmClient;\n}\n\n// ── Workflow resolution ─────────────────────────────────────────────────────\n\n/**\n * Why a workflow reference could not be resolved.\n *\n * `missing` and `version-mismatch` are deliberately distinct. Collapsing them\n * into a bare null makes \"no such workflow\" indistinguishable from \"that\n * workflow exists, but it is not the one you pinned\" — and the second wants an\n * error naming both versions, because it is the interesting failure.\n */\nexport type WorkflowResolutionFailure = {\n reason: \"missing\" | \"version-mismatch\";\n /** The version the host actually holds, when it holds one. */\n available?: number;\n message?: string;\n};\n\nexport type WorkflowResolution = FlowGraph | WorkflowResolutionFailure | null;\n\n/**\n * Resolve a workflow reference to a runnable graph.\n *\n * `subflow` names another workflow rather than embedding it, so the host owns\n * where workflows live — a database, a file, an API.\n *\n * ## Why `version` is here\n *\n * A workflow another workflow depends on is an INTERFACE, and interfaces need\n * pins. Without a version, a parent goes on calling `invoice-triage`, someone\n * edits that child, and the parent now runs different logic having reported\n * success the whole time — correct-looking, no error, wrong behaviour. The same\n * failure family as the 0.9.0 routing divergence.\n *\n * The parameter lives on the resolver rather than being encoded into the ref\n * string (`invoice-triage@3`) because a stringly-typed protocol is one every\n * host invents differently — the \"three vocabularies for one node\" problem.\n *\n * Raised by the MOIC Suite consumer, whose `workflow_ref` pins versions and\n * fails loudly on mismatch. Their point: a host COULD NOT implement pinning\n * before this, because the node had no way to ask and the resolver no way to\n * receive.\n *\n * Returning `null` still means \"no such workflow\". Return a\n * {@link WorkflowResolutionFailure} to distinguish a version mismatch.\n */\nexport type WorkflowResolver = (\n ref: string,\n version?: number,\n) => Promise<WorkflowResolution> | WorkflowResolution;\n\n/** Narrow a resolver's return value to an explicit failure. */\nexport function isResolutionFailure(value: WorkflowResolution): value is WorkflowResolutionFailure {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"reason\" in value &&\n (value as WorkflowResolutionFailure).reason !== undefined\n );\n}\n\nlet workflowResolver: WorkflowResolver | null = null;\n\n/** Install the host's workflow resolver. Returns an unregister function. */\nexport function registerWorkflowResolver(resolver: WorkflowResolver): () => void {\n workflowResolver = resolver;\n return () => {\n if (workflowResolver === resolver) workflowResolver = null;\n };\n}\n\nexport function getWorkflowResolver(): WorkflowResolver | null {\n return workflowResolver;\n}\n\n// ── Introspection ───────────────────────────────────────────────────────────\n\nexport type CapabilityId = \"llm\" | \"workflow_resolver\" | \"document\";\n\n/**\n * Which capabilities are currently satisfied.\n *\n * Exists so a host (or the CLI, or an agent over MCP) can answer \"what does\n * this graph need that I haven't wired?\" BEFORE a run fails halfway through.\n */\nexport function capabilityStatus(): Record<CapabilityId, boolean> {\n // Imported lazily to avoid dragging the React-dependent rich-input module\n // into the headless engine.\n let documentReady = false;\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires, no-undef\n documentReady = Boolean((globalThis as any).__fancyFlowDocumentAdapter);\n } catch {\n documentReady = false;\n }\n return {\n llm: llmClient !== null,\n workflow_resolver: workflowResolver !== null,\n document: documentReady,\n };\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/schema/workflow-schema.ts"],"names":[],"mappings":"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","file":"chunk-W5DPKJY4.js","sourcesContent":["import type { FlowEdge, FlowGraph, FlowNode, PortDescriptor, WorkflowInput } from \"../types\";\nimport { checkGraphConnectivity } from \"../analysis/graph-connectivity\";\nimport { defaultConfigFor, getNodeKind, validateConfig } from \"../registry/registry\";\nimport { resolveNodePorts } from \"../registry/ports\";\n\n/** Schema version. Bump on breaking shape changes; add migrations as needed. */\nexport const WORKFLOW_SCHEMA_VERSION = 1 as const;\nexport const WORKFLOW_SCHEMA_URL = \"https://particle.academy/schemas/workflow/v1.json\";\n\nexport type WorkflowSchema = {\n $schema: typeof WORKFLOW_SCHEMA_URL;\n version: typeof WORKFLOW_SCHEMA_VERSION;\n metadata?: WorkflowMetadata;\n /**\n * What this workflow accepts at run start, passed BY NAME.\n *\n * Lives beside `graph` rather than inside it because it describes the\n * workflow's CONTRACT, not its topology: a caller reads this to know what to\n * pass, and reading it should not mean walking nodes looking for a trigger.\n *\n * Omitted entirely when a workflow takes none, so every existing saved\n * document is unchanged byte for byte.\n */\n inputs?: WorkflowInput[];\n graph: {\n nodes: WorkflowSchemaNode[];\n edges: WorkflowSchemaEdge[];\n };\n view?: {\n viewport?: { x: number; y: number; zoom: number };\n };\n};\n\nexport type WorkflowMetadata = {\n id?: string;\n name?: string;\n description?: string;\n createdAt?: number;\n updatedAt?: number;\n author?: string;\n tags?: string[];\n};\n\nexport type WorkflowSchemaNode = {\n id: string;\n /** Registry kind name (e.g. \"memory_store\"). */\n kind: string;\n position: { x: number; y: number };\n label?: string;\n description?: string;\n /**\n * Announced to a person just before / just after this node runs. Omitted\n * entirely when unset, so a graph of ordinary plumbing nodes does not carry\n * a pair of empty keys per node and every diff of a saved graph stays\n * readable.\n */\n startingMsg?: string;\n stoppingMsg?: string;\n config?: Record<string, unknown>;\n /**\n * Resolved ports, written on export.\n *\n * A kind may derive its ports from config (`switch_case` cases,\n * `llm_branch` routes), and that derivation is a JavaScript function — a\n * runtime in another language cannot execute it. Without the resolved ports\n * in the document, the PHP twin sees no declared outputs and falls back to a\n * single `out`, so every branch edge in an exported flow silently stops\n * firing. Serializing them keeps the schema self-describing and preserves\n * the cross-runtime guarantee: same JSON in, same routing out.\n *\n * Optional and additive — a hand-written schema may omit them, and each\n * runtime then falls back to its own kind registry.\n */\n inputs?: PortDescriptor[];\n outputs?: PortDescriptor[];\n /**\n * Visual layout — additive + optional. `parentId`/`extent` carry node grouping\n * (swimlanes / containers); `width`/`height` carry an explicit (resized) size;\n * `style` carries inline presentation. A runtime that only walks edges/ports\n * (e.g. the PHP twin) ignores all of these — they exist purely for the canvas,\n * so an older reader that doesn't know them simply drops them.\n */\n parentId?: string;\n extent?: \"parent\" | [[number, number], [number, number]];\n width?: number;\n height?: number;\n style?: Record<string, unknown>;\n};\n\nexport type WorkflowSchemaEdge = {\n id: string;\n source: string;\n target: string;\n sourceHandle?: string;\n targetHandle?: string;\n label?: string;\n};\n\nexport type ImportIssue = {\n level: \"error\" | \"warning\";\n nodeId?: string;\n edgeId?: string;\n message: string;\n};\n\nexport type ImportResult = {\n graph: FlowGraph;\n issues: ImportIssue[];\n /** True when the import produced a usable graph (errors may have been\n * rewritten to warnings via `lenient: true`). */\n ok: boolean;\n};\n\n/** Snapshot the in-memory graph as a portable WorkflowSchema. */\nexport function exportWorkflow(\n graph: FlowGraph,\n metadata?: WorkflowMetadata,\n view?: WorkflowSchema[\"view\"],\n): WorkflowSchema {\n return {\n $schema: WORKFLOW_SCHEMA_URL,\n version: WORKFLOW_SCHEMA_VERSION,\n metadata: metadata ? { ...metadata, updatedAt: Date.now() } : undefined,\n // Round-trips, or a saved workflow silently forgets its own contract and\n // every caller's props become \"unknown input\" on the next load.\n ...(graph.inputs && graph.inputs.length > 0 ? { inputs: graph.inputs } : {}),\n graph: {\n nodes: graph.nodes.map(toSchemaNode),\n edges: graph.edges.map(toSchemaEdge),\n },\n view,\n };\n}\n\nfunction toSchemaNode(n: FlowNode): WorkflowSchemaNode {\n const data: any = n.data ?? {};\n const kindName = data.kind ?? n.type ?? \"custom\";\n // Resolve through the same helper the canvas and runtime use, so a\n // config-driven kind writes its ACTUAL ports into the document instead of\n // leaving another language's runtime to guess at them.\n const ports = resolveNodePorts(n, getNodeKind(kindName) ?? undefined);\n const node = n as any;\n return {\n id: n.id,\n kind: kindName,\n position: { x: n.position.x, y: n.position.y },\n label: data.label,\n description: data.description,\n ...(typeof data.startingMsg === \"string\" && data.startingMsg !== \"\" ? { startingMsg: data.startingMsg } : {}),\n ...(typeof data.stoppingMsg === \"string\" && data.stoppingMsg !== \"\" ? { stoppingMsg: data.stoppingMsg } : {}),\n config: data.config,\n inputs: ports.inputs,\n outputs: ports.outputs,\n // Visual layout — only when explicitly set (never persist auto-`measured`\n // dimensions, which are derived, not authored).\n ...(node.parentId ? { parentId: node.parentId } : {}),\n ...(node.extent ? { extent: node.extent } : {}),\n ...(typeof node.width === \"number\" ? { width: node.width } : {}),\n ...(typeof node.height === \"number\" ? { height: node.height } : {}),\n ...(node.style ? { style: node.style } : {}),\n };\n}\n\nfunction toSchemaEdge(e: FlowEdge): WorkflowSchemaEdge {\n return {\n id: e.id,\n source: e.source,\n target: e.target,\n sourceHandle: e.sourceHandle ?? undefined,\n targetHandle: e.targetHandle ?? undefined,\n label: typeof e.label === \"string\" ? e.label : undefined,\n };\n}\n\nexport type ImportOptions = {\n /** When true, unknown kinds become warnings + a \"custom\" placeholder\n * instead of errors. Default false. */\n lenient?: boolean;\n};\n\n/** A migration step: takes a version-N document to version N+1. */\nexport type MigrationStep = (schema: Record<string, unknown>) => Record<string, unknown>;\n\n/**\n * Every migration step, keyed by the version it upgrades FROM.\n *\n * Empty today because v1 is current — when a BREAKING bump lands, add the step\n * here and every stored document upgrades on read, in this runtime and its\n * PHP and Python twins.\n */\nexport const MIGRATIONS: Record<number, MigrationStep> = {};\n\n/**\n * Migrate a raw schema object up to the current version, as far as it can go.\n *\n * Additive changes need no migration — an older document simply lacks the newer\n * optional fields. This is the seam for a future BREAKING bump, and\n * `importWorkflow` runs it before validating the version.\n *\n * ## The three rules, each with a reason\n *\n * - **A PAST version migrates forward**, step by step, to the current one.\n * - **A FUTURE version is left ALONE.** We cannot know what a later schema\n * means, and migrating downward would be guessing. Untouched hands it to the\n * version check, which reports it honestly.\n * - **A GAP in the table is left alone too.** A missing step is not a licence\n * to guess.\n *\n * ## Why `steps` is a parameter\n *\n * With only v1 in existence there is no old document to migrate, so a seam\n * tested against the built-in (empty) table is a check that CANNOT fail — it\n * would pass identically against a function that returned its input untouched,\n * which is what this was until the twins needed the same seam.\n *\n * The PHP and Python runtimes carry the identical shape. That mattered more\n * than it sounds: they had no migration at all and errored on any version\n * mismatch, so a v2 bump would have made every stored Op unreadable on exactly\n * the runtimes where durable runs resume.\n */\nexport function migrateSchema(\n schema: unknown,\n steps: Record<number, MigrationStep> = MIGRATIONS,\n): unknown {\n if (!schema || typeof schema !== \"object\") return schema;\n\n const doc = schema as Record<string, unknown>;\n let version = doc.version;\n\n if (typeof version !== \"number\" || !Number.isInteger(version) || version >= WORKFLOW_SCHEMA_VERSION) {\n return schema;\n }\n\n let out = doc;\n while (version < WORKFLOW_SCHEMA_VERSION) {\n const step = steps[version];\n if (!step) return out;\n\n out = step(out);\n version += 1;\n out.version = version;\n }\n\n return out;\n}\n\n/**\n * Hydrate a schema into runtime FlowGraph + validate kinds/configs against\n * the registry. Reports issues for unknown kinds, missing required config,\n * and dangling edges.\n */\nexport function importWorkflow(schema: unknown, options: ImportOptions = {}): ImportResult {\n const issues: ImportIssue[] = [];\n const lenient = options.lenient === true;\n schema = migrateSchema(schema);\n\n if (!schema || typeof schema !== \"object\") {\n return { ok: false, graph: { nodes: [], edges: [] }, issues: [{ level: \"error\", message: \"Schema is not an object.\" }] };\n }\n const s = schema as Partial<WorkflowSchema>;\n if (s.version !== WORKFLOW_SCHEMA_VERSION) {\n issues.push({\n level: lenient ? \"warning\" : \"error\",\n message: `Unsupported workflow schema version: ${s.version} (expected ${WORKFLOW_SCHEMA_VERSION})`,\n });\n if (!lenient) return { ok: false, graph: { nodes: [], edges: [] }, issues };\n }\n\n const rawNodes = s.graph?.nodes ?? [];\n const rawEdges = s.graph?.edges ?? [];\n\n const nodes: FlowNode[] = rawNodes.map((n) => {\n const kind = getNodeKind(n.kind);\n if (!kind) {\n issues.push({\n level: lenient ? \"warning\" : \"error\",\n nodeId: n.id,\n message: `Unknown kind \"${n.kind}\" — register it before importing.`,\n });\n }\n const config = n.config ?? (kind ? defaultConfigFor(kind) : {});\n if (kind) {\n for (const iss of validateConfig(kind, config)) {\n issues.push({ level: \"warning\", nodeId: n.id, message: `${iss.key}: ${iss.message}` });\n }\n }\n // Canonicalise on the way in: a document may carry a pre-namespace id, and\n // rewriting it here means the graph converges on the canonical id the next\n // time it is saved, rather than carrying the ambiguous name forever.\n const kindId = kind?.name ?? n.kind;\n return {\n id: n.id,\n type: kindId,\n position: { x: n.position?.x ?? 0, y: n.position?.y ?? 0 },\n // Rehydrate visual layout onto the node's top level (where xyflow reads it).\n ...(n.parentId ? { parentId: n.parentId } : {}),\n ...(n.extent ? { extent: n.extent } : {}),\n ...(typeof n.width === \"number\" ? { width: n.width } : {}),\n ...(typeof n.height === \"number\" ? { height: n.height } : {}),\n ...(n.style ? { style: n.style } : {}),\n data: {\n kind: kindId,\n label: n.label ?? kind?.label ?? n.kind,\n description: n.description,\n ...(n.startingMsg ? { startingMsg: n.startingMsg } : {}),\n ...(n.stoppingMsg ? { stoppingMsg: n.stoppingMsg } : {}),\n config,\n // Carry serialized ports back onto the node so a round-trip is stable\n // and an unknown kind still routes the way the document described.\n ...(n.inputs ? { inputs: n.inputs } : {}),\n ...(n.outputs ? { outputs: n.outputs } : {}),\n } as any,\n };\n });\n\n const nodeIds = new Set(nodes.map((n) => n.id));\n const edges: FlowEdge[] = rawEdges\n .map((e) => {\n if (!nodeIds.has(e.source)) {\n issues.push({ level: \"warning\", edgeId: e.id, message: `Edge source \"${e.source}\" not found.` });\n return null;\n }\n if (!nodeIds.has(e.target)) {\n issues.push({ level: \"warning\", edgeId: e.id, message: `Edge target \"${e.target}\" not found.` });\n return null;\n }\n return {\n id: e.id,\n source: e.source,\n target: e.target,\n sourceHandle: e.sourceHandle,\n targetHandle: e.targetHandle,\n label: e.label,\n } as FlowEdge;\n })\n .filter((e): e is FlowEdge => e !== null);\n\n // WIRING, not merely dataflow: a node no edge reaches and that reaches no\n // edge, and an edge reading from a node that publishes nothing.\n //\n // Deliberately AFTER the edge loop, so it sees the same edges the engine will\n // -- a dangling edge is dropped with a warning above, and running this first\n // would let a dropped edge count as a connection.\n //\n // Deliberately NOT gated on `lenient`. That flag is about unknown VOCABULARY\n // (a kind this host has not registered), never about wiring.\n issues.push(...checkGraphConnectivity({ nodes, edges }));\n\n const ok = issues.every((i) => i.level !== \"error\");\n // The declaration comes back with the graph. Dropping it on import would be\n // the worse half of losing it on export: the document still says what it\n // accepts, the loaded graph does not, and every prop a caller passes becomes\n // \"unknown input\" — an error pointing at the caller for a bug in the loader.\n const declaredInputs = Array.isArray(s.inputs)\n ? (s.inputs as WorkflowInput[]).filter(\n (input): input is WorkflowInput =>\n input !== null && typeof input === \"object\" && typeof (input as WorkflowInput).name === \"string\",\n )\n : undefined;\n\n return {\n ok,\n graph: {\n nodes,\n edges,\n ...(declaredInputs && declaredInputs.length > 0 ? { inputs: declaredInputs } : {}),\n },\n issues,\n };\n}\n\n/** Convenience: serialize a schema as a downloadable JSON Blob. */\nexport function workflowToBlob(schema: WorkflowSchema): Blob {\n return new Blob([JSON.stringify(schema, null, 2)], { type: \"application/json\" });\n}\n"]}
@@ -1,333 +0,0 @@
1
- import { Node, Edge } from '@xyflow/react';
2
-
3
- /**
4
- * Who is running, which step this is, and how many times it has been tried.
5
- *
6
- * ## Why an engine needs this at all
7
- *
8
- * A node that WRITES to somebody else's system — charge a card, send a message,
9
- * open a pull request — can only survive a retry if the retry carries the same
10
- * idempotency key the first attempt did. Otherwise the provider treats the
11
- * second call as a new request and the customer is charged twice.
12
- *
13
- * Until this existed the executor context was `{ node, inputs, emit, abort }`,
14
- * which is not enough to derive one. Both obvious fallbacks are worse than
15
- * sending no key at all:
16
- *
17
- * - **the node id alone** is stable across retries, and also across RUNS — two
18
- * legitimate payments share a key and the provider silently collapses the
19
- * second into the first. A payment that never happened, reported as success;
20
- * - **a fresh random value** is unique per run, and also per ATTEMPT — a retry
21
- * creates a second charge, which is the thing being avoided.
22
- *
23
- * ## What actually identifies a step
24
- *
25
- * Not `(run, node)`. A node legitimately executes more than once inside one
26
- * run: once per subflow invocation, once per iteration of a loop an executor
27
- * drives itself. `(run, node)` would give every one of those the same key, and
28
- * a provider would honour exactly one of them.
29
- *
30
- * So a step is identified by the **path of invocations that led to it**, plus
31
- * an optional **occurrence** for repetition at the same level:
32
- *
33
- * ```text
34
- * runKey ":" segment ("/" segment)* segment := escape(id) ["#" occurrence]
35
- * ```
36
- *
37
- * And the part that is easy to get backwards: **`attempt` is NOT in the key.**
38
- * It is carried here for logging and for {@link RunIdentity.isReplaySafe}, and
39
- * putting it in the key would restore the exact bug the key exists to prevent.
40
- *
41
- * Pinned cross-runtime by `shared/flow-run-identity` in
42
- * `@particle-academy/fancy-conformance`.
43
- */
44
- /** The wire shape — what a queue job payload carries. */
45
- type RunIdentityJson = {
46
- runKey: string;
47
- path?: string[];
48
- attempt?: number;
49
- firstAttemptAt?: string;
50
- };
51
- /**
52
- * Escape one segment so the composition is injective.
53
- *
54
- * `%` FIRST, or the escaping is not reversible: escaping `/` before `%` turns a
55
- * literal `a%2Fb` into the same text as the escaped form of `a/b`, which is the
56
- * collision this exists to prevent, reintroduced by its own fix.
57
- */
58
- declare function escapeSegment(value: string): string;
59
- /**
60
- * A run, a position inside it, and how many times this position has been tried.
61
- *
62
- * Immutable. {@link descend} returns a new identity rather than mutating, so an
63
- * executor cannot change what its siblings see.
64
- */
65
- declare class RunIdentity {
66
- /** Stable for the whole run: same across retries, resumes, workers and hosts. */
67
- readonly runKey: string;
68
- /**
69
- * Enclosing invocation segments, outermost first, ALREADY RENDERED.
70
- *
71
- * Empty at the top level. A subflow pushes the invoking node's id; an
72
- * executor that loops pushes `id#i`.
73
- */
74
- readonly path: readonly string[];
75
- /**
76
- * 1-based attempt of THIS logical step. Never part of the key.
77
- *
78
- * The durable driver sets it from the node's claim row, which is exact. A
79
- * plain in-process `runFlow` gets whatever the host passed, which is
80
- * run-scoped and therefore conservative — see `isReplaySafe`.
81
- */
82
- readonly attempt: number;
83
- /** ISO-8601 UTC instant of attempt 1 of this step. */
84
- readonly firstAttemptAt: string;
85
- constructor(runKey: string, path?: readonly string[], attempt?: number, firstAttemptAt?: string);
86
- /**
87
- * The identity of one execution of one node — stable across retries of that
88
- * execution, distinct from every other execution of the same node.
89
- *
90
- * Pass `occurrence` when an executor runs the same node more than once at the
91
- * same level (a loop body, one item of a fan-out it drives itself).
92
- */
93
- stepKey(nodeId: string, occurrence?: number | null): string;
94
- /**
95
- * A child identity for work nested inside this step.
96
- *
97
- * `subflow` pushes the invoking node's id, so a node inside the child graph
98
- * cannot collide with a same-named node in the parent. Attempt and
99
- * `firstAttemptAt` are carried down unchanged: the nested work happens inside
100
- * this step's attempt, and shares its clock.
101
- */
102
- descend(segment: string, occurrence?: number | null): RunIdentity;
103
- /** A copy on a different attempt, first-attempt clock preserved. */
104
- withAttempt(attempt: number, firstAttemptAt?: string): RunIdentity;
105
- /**
106
- * May this attempt reuse the step key and still be deduplicated?
107
- *
108
- * Providers forget idempotency keys — Stripe after 24 hours. Past that
109
- * window, resending the key creates a second charge and sending a fresh one
110
- * creates a second charge, so **the caller must refuse rather than pick
111
- * between them**: a loud stuck run beats a silent double write.
112
- *
113
- * `true` on attempt 1 whatever the elapsed time — nothing was sent on an
114
- * earlier attempt, so there is nothing for the provider to have forgotten.
115
- * That is what lets a run park on a human gate for a week and then write.
116
- *
117
- * `windowSeconds: null` means the provider does not expire keys. `0` means
118
- * it does not dedupe at all, so no retry may reuse a key — it is a real
119
- * window, not an absent one, and the two must not be conflated: reading `0`
120
- * as `null` turns "this provider does not dedupe" into "this provider
121
- * dedupes forever", which is the more dangerous of the two by a distance.
122
- */
123
- isReplaySafe(windowSeconds: number | null | undefined, now?: Date | string): boolean;
124
- toJSON(): Required<RunIdentityJson>;
125
- /** Rebuild from a queue payload. */
126
- static from(value: RunIdentity | RunIdentityJson | string): RunIdentity;
127
- }
128
-
129
- /**
130
- * Public domain types for fancy-flow. Built-in nodes are layered on top of
131
- * @xyflow/react's `Node` so consumers can mix custom xyflow nodes alongside
132
- * the kit. Edges remain xyflow's standard `Edge`.
133
- */
134
-
135
- type FlowNodeKind = "trigger" | "action" | "decision" | "output" | "note" | "subgraph";
136
- /** Status surfaced on the node while a run is in progress. */
137
- type NodeRunStatus = "idle" | "queued" | "running" | "done" | "error";
138
- /** Port description on a node. Ports are visual handles xyflow can connect. */
139
- type PortDescriptor = {
140
- id: string;
141
- label?: string;
142
- /** Optional logical type for hosts that want to validate connections. */
143
- type?: string;
144
- };
145
- /** Common shape every kit node carries in its `data` slot. */
146
- type BaseNodeData = {
147
- label: string;
148
- description?: string;
149
- /** Free-form configuration the host owns (form values, code, parameters). */
150
- config?: Record<string, unknown>;
151
- /** Set by the runner; hosts shouldn't edit this directly. */
152
- status?: NodeRunStatus;
153
- /** Optional human-readable status detail (e.g. error message, current step). */
154
- statusText?: string;
155
- /**
156
- * Announced to a person just BEFORE this node runs — "Starting the deep
157
- * analysis". Authored on the node, so a graph narrates itself without the
158
- * host writing any per-node reporting code.
159
- *
160
- * Optional on purpose. Most nodes in a real graph are plumbing, and a run
161
- * that narrates all of them buries the two or three steps anyone follows.
162
- */
163
- startingMsg?: string;
164
- /**
165
- * Announced AFTER this node finishes — "Analysis complete".
166
- *
167
- * Emitted only when the node SUCCEEDS. A completion message printed after a
168
- * failure tells a human the opposite of what happened, in the part of the UI
169
- * they trust most; failures report through `node-status` and `log`.
170
- */
171
- stoppingMsg?: string;
172
- /** Per-node accent override, e.g. for theming a custom subclass. */
173
- color?: string;
174
- /** Input ports rendered on the node. Defaults vary by kind. */
175
- inputs?: PortDescriptor[];
176
- /** Output ports rendered on the node. Defaults vary by kind. */
177
- outputs?: PortDescriptor[];
178
- };
179
- type TriggerNodeData = BaseNodeData & {
180
- kind: "trigger";
181
- };
182
- type ActionNodeData = BaseNodeData & {
183
- kind: "action";
184
- };
185
- type DecisionNodeData = BaseNodeData & {
186
- kind: "decision";
187
- };
188
- type OutputNodeData = BaseNodeData & {
189
- kind: "output";
190
- };
191
- type NoteNodeData = BaseNodeData & {
192
- kind: "note";
193
- body?: string;
194
- };
195
- type SubgraphNodeData = BaseNodeData & {
196
- kind: "subgraph";
197
- /** Ids of the nodes contained in this subgraph. */
198
- childIds?: string[];
199
- /** Whether the subgraph is shown collapsed (default true — children hidden). */
200
- collapsed?: boolean;
201
- };
202
- type FlowNodeData = TriggerNodeData | ActionNodeData | DecisionNodeData | OutputNodeData | NoteNodeData | SubgraphNodeData;
203
- type FlowNode = Node<FlowNodeData>;
204
- type FlowEdge = Edge;
205
- /**
206
- * One value a workflow DECLARES that it accepts at run start.
207
- *
208
- * The declaration is the point. Before this existed, a caller passed
209
- * `initialInputs` keyed BY NODE ID — so they had to know the trigger happened
210
- * to be called `t`, and renaming that node broke every caller while the graph
211
- * itself stayed valid. Nothing reported it.
212
- *
213
- * And nothing said what a workflow accepted at all: no names, no types, no
214
- * defaults. An agent composing a call had nothing to read, and a misspelled key
215
- * did not fail — the value simply sat unused while the run reported success.
216
- */
217
- type WorkflowInput = {
218
- /** What a caller passes it as. */
219
- name: string;
220
- /**
221
- * Optional. Omitting it means "I am not asserting a shape", which must not
222
- * degrade into "nothing is allowed" — an undeclared type accepts anything.
223
- */
224
- type?: "string" | "number" | "boolean" | "object" | "array";
225
- /**
226
- * The run needs a value. Satisfied by a `default`, so `required` means
227
- * "this must resolve to something" rather than "the caller must type it".
228
- */
229
- required?: boolean;
230
- /** Used when the caller omits the key. An explicit value always wins. */
231
- default?: unknown;
232
- description?: string;
233
- };
234
- /** A serializable graph — what hosts persist, what agents read/write. */
235
- type FlowGraph = {
236
- nodes: FlowNode[];
237
- edges: FlowEdge[];
238
- /**
239
- * What this workflow accepts. Callers pass a flat object BY NAME.
240
- *
241
- * Omitted entirely when a graph takes none, so existing saved graphs are
242
- * unchanged byte for byte and every diff stays readable.
243
- */
244
- inputs?: WorkflowInput[];
245
- };
246
- /** Per-node executor signature. Inputs are keyed by input-port id. */
247
- type NodeExecutor<TIn = Record<string, unknown>, TOut = unknown> = (ctx: {
248
- node: FlowNode;
249
- inputs: TIn;
250
- /** Stops the run if called. */
251
- abort: (reason?: string) => never;
252
- /** Lets the executor stream status updates and partial outputs. */
253
- emit: (event: RunEvent) => void;
254
- /**
255
- * The registry THIS run is executing against.
256
- *
257
- * Handed down so an executor that starts a NESTED run gives the child the
258
- * same executors as the parent. `subflow` previously ran its child against
259
- * `config.executors ?? {}` — an empty registry unless the graph happened to
260
- * carry one — so a host kind resolved at top level and vanished one level
261
- * down, and a host that had REPLACED a builtin got the package's version in
262
- * the child. Same graph, different behaviour by nesting depth, reported
263
- * against the PHP twin as fancy-flow-php#7.
264
- *
265
- * Inheriting from the context rather than from a parameter is what makes it
266
- * unforgettable: any future nesting executor gets it without opting in.
267
- */
268
- executors?: ExecutorRegistry;
269
- /**
270
- * How deep this run is nested. 0 for a top-level run; `subflow` passes
271
- * depth + 1 to its child, so runaway recursion can be reported by name
272
- * rather than as a stack overflow.
273
- */
274
- depth?: number;
275
- /**
276
- * Who is running, and which attempt of which step this is.
277
- *
278
- * `ctx.run.stepKey(ctx.node.id)` is the idempotency key for a node that
279
- * writes to somebody else's system — stable across retries of this step,
280
- * distinct for every other execution of the same node.
281
- *
282
- * `undefined` when the host supplied no identity, and that is a real
283
- * answer: a write with no key must decline or accept one attempt, never
284
- * invent a key. See `RunIdentity`.
285
- */
286
- run?: RunIdentity;
287
- }) => Promise<TOut> | TOut;
288
- type ExecutorRegistry = Partial<Record<FlowNodeKind | string, NodeExecutor>>;
289
- type RunEvent = {
290
- type: "node-status";
291
- nodeId: string;
292
- status: NodeRunStatus;
293
- text?: string;
294
- }
295
- /**
296
- * A human-facing announcement a node makes around its own execution, from
297
- * `data.startingMsg` / `data.stoppingMsg`. Opt-in per node: most nodes in a
298
- * real graph are plumbing, and narrating all of them buries the few steps a
299
- * person cares about.
300
- *
301
- * Deliberately NOT folded into `node-status.text`, which already carries
302
- * "skipped", "resumed", "lane", "annotation" and raw error strings. Those are
303
- * diagnostics; these are addressed to a person. A consumer rendering a
304
- * progress feed cannot be asked to guess which is which — that is how an
305
- * error string ends up shown to a user as a status update.
306
- */
307
- | {
308
- type: "node-message";
309
- nodeId: string;
310
- phase: "start" | "end";
311
- message: string;
312
- } | {
313
- type: "node-output";
314
- nodeId: string;
315
- portId: string;
316
- value: unknown;
317
- } | {
318
- type: "log";
319
- nodeId?: string;
320
- level: "info" | "warn" | "error";
321
- message: string;
322
- detail?: unknown;
323
- } | {
324
- type: "run-start";
325
- } | {
326
- type: "run-end";
327
- ok: boolean;
328
- } | {
329
- type: "run-error";
330
- error: string;
331
- };
332
-
333
- export { type ActionNodeData as A, type BaseNodeData as B, type DecisionNodeData as D, type ExecutorRegistry as E, type FlowGraph as F, type NodeExecutor as N, type OutputNodeData as O, type PortDescriptor as P, type RunEvent as R, type SubgraphNodeData as S, type TriggerNodeData as T, type WorkflowInput as W, type FlowNode as a, RunIdentity as b, type RunIdentityJson as c, type FlowEdge as d, type FlowNodeData as e, type FlowNodeKind as f, type NodeRunStatus as g, type NoteNodeData as h, escapeSegment as i };