@particle-academy/fancy-flow 0.71.0 → 0.72.1

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 (53) hide show
  1. package/dist/{chunk-3L47KJ6Q.js → chunk-3RXQGYGH.js} +3 -3
  2. package/dist/{chunk-3L47KJ6Q.js.map → chunk-3RXQGYGH.js.map} +1 -1
  3. package/dist/{chunk-PNTDTSMR.js → chunk-7DJ56PUA.js} +4 -4
  4. package/dist/{chunk-PNTDTSMR.js.map → chunk-7DJ56PUA.js.map} +1 -1
  5. package/dist/{chunk-EVGSL2EM.js → chunk-EX34GCKK.js} +162 -151
  6. package/dist/chunk-EX34GCKK.js.map +1 -0
  7. package/dist/{chunk-XI4AZD2S.js → chunk-GQ5LEA7Q.js} +3 -3
  8. package/dist/{chunk-XI4AZD2S.js.map → chunk-GQ5LEA7Q.js.map} +1 -1
  9. package/dist/{chunk-D2OBU3CE.js → chunk-K7KO3UJ3.js} +3 -3
  10. package/dist/{chunk-D2OBU3CE.js.map → chunk-K7KO3UJ3.js.map} +1 -1
  11. package/dist/{chunk-FPWJYSXX.js → chunk-LYYQO7EY.js} +4 -4
  12. package/dist/{chunk-FPWJYSXX.js.map → chunk-LYYQO7EY.js.map} +1 -1
  13. package/dist/{chunk-KQCX6KN7.js → chunk-VAF2SWTC.js} +3 -3
  14. package/dist/{chunk-KQCX6KN7.js.map → chunk-VAF2SWTC.js.map} +1 -1
  15. package/dist/durable/index.d.cts +90 -16
  16. package/dist/durable/index.d.ts +90 -16
  17. package/dist/durable.cjs +160 -83
  18. package/dist/durable.cjs.map +1 -1
  19. package/dist/durable.js +79 -14
  20. package/dist/durable.js.map +1 -1
  21. package/dist/engine.cjs +82 -71
  22. package/dist/engine.cjs.map +1 -1
  23. package/dist/engine.d.cts +2 -2
  24. package/dist/engine.d.ts +2 -2
  25. package/dist/engine.js +4 -4
  26. package/dist/index.cjs +128 -117
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +2 -2
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.js +12 -12
  31. package/dist/registry.cjs +82 -71
  32. package/dist/registry.cjs.map +1 -1
  33. package/dist/registry.js +2 -2
  34. package/dist/{run-cohort-DL9JzzPU.d.cts → run-cohort-DNpPa0mq.d.cts} +1 -1
  35. package/dist/{run-cohort-CjUBWg6X.d.ts → run-cohort-rgT_FX3V.d.ts} +1 -1
  36. package/dist/{run-flow-B_8hgO5_.d.cts → run-flow-BLP9rXfO.d.ts} +22 -1
  37. package/dist/{run-flow-CxEGBOxd.d.ts → run-flow-pW0PllaZ.d.cts} +22 -1
  38. package/dist/runtime/index.d.cts +3 -3
  39. package/dist/runtime/index.d.ts +3 -3
  40. package/dist/runtime.cjs +82 -71
  41. package/dist/runtime.cjs.map +1 -1
  42. package/dist/runtime.js +3 -3
  43. package/dist/schema.cjs +82 -71
  44. package/dist/schema.cjs.map +1 -1
  45. package/dist/schema.js +3 -3
  46. package/dist/screens.cjs +82 -71
  47. package/dist/screens.cjs.map +1 -1
  48. package/dist/screens.js +4 -4
  49. package/dist/ux.cjs +82 -71
  50. package/dist/ux.cjs.map +1 -1
  51. package/dist/ux.js +1 -1
  52. package/package.json +1 -1
  53. package/dist/chunk-EVGSL2EM.js.map +0 -1
package/dist/durable.cjs CHANGED
@@ -52,8 +52,10 @@ var InMemoryClaimStore = class {
52
52
  }
53
53
  skip(runKey, nodeId) {
54
54
  const entry = this.entry(runKey, nodeId);
55
+ if (entry.status === NodeRunStatus.SKIPPED) return false;
55
56
  entry.status = NodeRunStatus.SKIPPED;
56
57
  entry.ports = [];
58
+ return true;
57
59
  }
58
60
  fail(runKey, nodeId, error) {
59
61
  const entry = this.entry(runKey, nodeId);
@@ -242,24 +244,6 @@ function resolveNodePorts(node, kind) {
242
244
  };
243
245
  }
244
246
 
245
- // src/expressions/variables.ts
246
- function outputFieldsFor(node) {
247
- const kindName = node.data?.kind ?? node.type;
248
- if (!kindName) return [];
249
- const kind = getNodeKind(kindName);
250
- const shape = kind?.outputShape;
251
- if (!shape) return [];
252
- if (typeof shape === "function") {
253
- try {
254
- const config = node.data?.config ?? {};
255
- return shape(config) ?? [];
256
- } catch {
257
- return [];
258
- }
259
- }
260
- return shape;
261
- }
262
-
263
247
  // src/runtime/run-identity.ts
264
248
  function escapeSegment(value) {
265
249
  return value.replace(/%/g, "%25").replace(/\//g, "%2F").replace(/#/g, "%23");
@@ -603,6 +587,81 @@ function specForLane(lane) {
603
587
  };
604
588
  }
605
589
 
590
+ // src/expressions/variables.ts
591
+ function outputFieldsFor(node) {
592
+ const kindName = node.data?.kind ?? node.type;
593
+ if (!kindName) return [];
594
+ const kind = getNodeKind(kindName);
595
+ const shape = kind?.outputShape;
596
+ if (!shape) return [];
597
+ if (typeof shape === "function") {
598
+ try {
599
+ const config = node.data?.config ?? {};
600
+ return shape(config) ?? [];
601
+ } catch {
602
+ return [];
603
+ }
604
+ }
605
+ return shape;
606
+ }
607
+
608
+ // src/runtime/undelivered-edges.ts
609
+ function undeliveredEdgeWarnings(target, incoming, portValues, completed, nodesById) {
610
+ const events = [];
611
+ for (const e of incoming) {
612
+ const handle = e.sourceHandle ?? "out";
613
+ if (portValues.has(`${e.source}:${handle}`) || !completed.has(e.source)) continue;
614
+ if (possiblePortIds(nodesById.get(e.source)).includes(handle)) continue;
615
+ events.push({
616
+ type: "log",
617
+ nodeId: target.id,
618
+ level: "warn",
619
+ message: undeliveredEdgeMessage(e, target, portValues, nodesById),
620
+ detail: { edge: e.id, source: e.source, sourceHandle: handle }
621
+ });
622
+ }
623
+ return events;
624
+ }
625
+ function possiblePortIds(node) {
626
+ if (!node) return ["out"];
627
+ const own = node.data?.outputs;
628
+ if (Array.isArray(own)) return own.map((p) => p.id);
629
+ const kind = getNodeKind(node.data?.kind ?? node.type ?? "");
630
+ if (!kind) return ["out"];
631
+ const config = nodeConfig(node);
632
+ const representative = typeof kind.outputs === "function" && !configDeclaresPorts(kind.name, config);
633
+ const ids = (resolvePortSpec(kind.outputs, representative ? defaultConfigFor(kind) : config) ?? []).map((p) => p.id);
634
+ return ids.length > 0 ? ids : ["out"];
635
+ }
636
+ function configDeclaresPorts(kindName, config) {
637
+ const bare = kindName.startsWith("@") ? kindName.slice(kindName.lastIndexOf("/") + 1) : kindName;
638
+ if (bare === "switch_case") {
639
+ const cases = config.cases;
640
+ return !!cases && typeof cases === "object" && Object.values(cases).some((port) => typeof port === "string" && port !== "");
641
+ }
642
+ if (bare === "llm_router") {
643
+ const routes = config.routes;
644
+ return Array.isArray(routes) && routes.some((route) => route?.port != null && route.port !== "");
645
+ }
646
+ return true;
647
+ }
648
+ function undeliveredEdgeMessage(edge, target, portValues, nodesById) {
649
+ const handle = edge.sourceHandle ?? "out";
650
+ const prefix = `${edge.source}:`;
651
+ const available = [];
652
+ for (const key of portValues.keys()) {
653
+ if (key.startsWith(prefix)) available.push(key.slice(prefix.length));
654
+ }
655
+ let message = `Edge ${edge.id} reads port "${handle}" from node ${edge.source}, which never publishes it \u2014 nothing would reach ${target.id} at run time.`;
656
+ if (available.length > 0) message += ` Available: ${available.join(", ")}.`;
657
+ const source = nodesById.get(edge.source);
658
+ if (source && outputFieldsFor(source).some((field) => field?.path === handle)) {
659
+ message += ` Note: "${handle}" is a FIELD this node emits, not a port \u2014 read it downstream as {{ in.${handle} }} rather than naming it as a source handle.`;
660
+ }
661
+ if (edge.sourceHandle != null) message += " Leave sourceHandle off to read the node's output.";
662
+ return message;
663
+ }
664
+
606
665
  // src/runtime/workflow-props.ts
607
666
  function typeOf(value) {
608
667
  if (value === null) return "null";
@@ -661,7 +720,7 @@ function resolveWorkflowProps(declared, passed) {
661
720
  // src/runtime/run-flow.ts
662
721
  async function runFlow(graph, executors, onEvent = () => {
663
722
  }, options = {}) {
664
- const { signal, initialInputs = {}, timeoutMs, depth = 0, resumeOutputs = {}, entryNodes } = options;
723
+ const { signal, initialInputs = {}, timeoutMs, depth = 0, resumeOutputs = {}, entryNodes, nodeExecutors } = options;
665
724
  const run = options.run === void 0 ? void 0 : RunIdentity.from(options.run);
666
725
  const outputs = {};
667
726
  const portValues = /* @__PURE__ */ new Map();
@@ -724,17 +783,8 @@ async function runFlow(graph, executors, onEvent = () => {
724
783
  onEvent({ type: "node-status", nodeId: node.id, status: "idle", text: "skipped" });
725
784
  continue;
726
785
  }
727
- for (const e of incoming) {
728
- const handle = e.sourceHandle ?? "out";
729
- if (portValues.has(`${e.source}:${handle}`) || !completed.has(e.source)) continue;
730
- if (possiblePortIds(nodesById.get(e.source)).includes(handle)) continue;
731
- onEvent({
732
- type: "log",
733
- nodeId: node.id,
734
- level: "warn",
735
- message: undeliveredEdgeMessage(e, node, portValues, nodesById),
736
- detail: { edge: e.id, source: e.source, sourceHandle: handle }
737
- });
786
+ for (const warning of undeliveredEdgeWarnings(node, incoming, portValues, completed, nodesById)) {
787
+ onEvent(warning);
738
788
  }
739
789
  if (incoming.length > 0) {
740
790
  const anyActive = incoming.some((e) => portValues.has(`${e.source}:${e.sourceHandle ?? "out"}`));
@@ -758,7 +808,7 @@ async function runFlow(graph, executors, onEvent = () => {
758
808
  onEvent({ type: "node-status", nodeId: node.id, status: "running" });
759
809
  announce(onEvent, node, "start");
760
810
  const inputs = collectInputs(node, incoming, portValues, initialInputs, props, declaresProps);
761
- const exec = pickExecutor(executors, node);
811
+ const exec = pickExecutor(executors, node, nodeExecutors);
762
812
  if (!exec) {
763
813
  const tried = executorLookupIds(node);
764
814
  const msg = `No executor registered for kind=${node.type} \u2014 tried ${tried.map((id) => `"${id}"`).join(", ")}. Key your registry by one of those.`;
@@ -878,7 +928,9 @@ function executorLookupIds(node) {
878
928
  ids.push("*");
879
929
  return [...new Set(ids)];
880
930
  }
881
- function pickExecutor(executors, node) {
931
+ function pickExecutor(executors, node, nodeExecutors) {
932
+ const pinned = nodeExecutors && Object.prototype.hasOwnProperty.call(nodeExecutors, node.id) ? nodeExecutors[node.id] : void 0;
933
+ if (pinned) return pinned;
882
934
  for (const id of executorLookupIds(node)) {
883
935
  if (executors[id]) return executors[id];
884
936
  }
@@ -898,45 +950,6 @@ function activatedPorts(node, result) {
898
950
  const declared = resolveNodePorts(node, kind).outputs?.map((p) => p.id);
899
951
  return { ports: declared?.length ? declared : ["out"], value: result };
900
952
  }
901
- function possiblePortIds(node) {
902
- if (!node) return ["out"];
903
- const own = node.data?.outputs;
904
- if (Array.isArray(own)) return own.map((p) => p.id);
905
- const kind = getNodeKind(node.data?.kind ?? node.type ?? "");
906
- if (!kind) return ["out"];
907
- const config = nodeConfig(node);
908
- const representative = typeof kind.outputs === "function" && !configDeclaresPorts(kind.name, config);
909
- const ids = (resolvePortSpec(kind.outputs, representative ? defaultConfigFor(kind) : config) ?? []).map((p) => p.id);
910
- return ids.length > 0 ? ids : ["out"];
911
- }
912
- function configDeclaresPorts(kindName, config) {
913
- const bare = kindName.startsWith("@") ? kindName.slice(kindName.lastIndexOf("/") + 1) : kindName;
914
- if (bare === "switch_case") {
915
- const cases = config.cases;
916
- return !!cases && typeof cases === "object" && Object.values(cases).some((port) => typeof port === "string" && port !== "");
917
- }
918
- if (bare === "llm_router") {
919
- const routes = config.routes;
920
- return Array.isArray(routes) && routes.some((route) => route?.port != null && route.port !== "");
921
- }
922
- return true;
923
- }
924
- function undeliveredEdgeMessage(edge, target, portValues, nodesById) {
925
- const handle = edge.sourceHandle ?? "out";
926
- const prefix = `${edge.source}:`;
927
- const available = [];
928
- for (const key of portValues.keys()) {
929
- if (key.startsWith(prefix)) available.push(key.slice(prefix.length));
930
- }
931
- let message = `Edge ${edge.id} reads port "${handle}" from node ${edge.source}, which never publishes it \u2014 nothing would reach ${target.id} at run time.`;
932
- if (available.length > 0) message += ` Available: ${available.join(", ")}.`;
933
- const source = nodesById.get(edge.source);
934
- if (source && outputFieldsFor(source).some((field) => field?.path === handle)) {
935
- message += ` Note: "${handle}" is a FIELD this node emits, not a port \u2014 read it downstream as {{ in.${handle} }} rather than naming it as a source handle.`;
936
- }
937
- if (edge.sourceHandle != null) message += " Leave sourceHandle off to read the node's output.";
938
- return message;
939
- }
940
953
  function announce(onEvent, node, phase) {
941
954
  const data = node.data;
942
955
  const raw = phase === "start" ? data?.startingMsg : data?.stoppingMsg;
@@ -2704,20 +2717,35 @@ var Frontier = {
2704
2717
  (entry) => entry.status === NodeRunStatus.CLAIMED || entry.status === NodeRunStatus.PAUSED
2705
2718
  );
2706
2719
  },
2707
- /** Persist the skip cascade so the next pass does not recompute it. */
2720
+ /**
2721
+ * Persist the skip cascade so the next pass does not recompute it.
2722
+ *
2723
+ * Resolves to the ids THIS call settled, in cascade order. A node another
2724
+ * caller had already settled is left out — its store reported `false` — so a
2725
+ * caller acting on "I skipped this" acts once per node, not once per racer. A
2726
+ * store whose `skip` returns nothing counts every id as settled here.
2727
+ */
2708
2728
  async settleSkips(store, runKey, skipped) {
2709
- for (const nodeId of skipped) await store.skip(runKey, nodeId);
2729
+ const settled = [];
2730
+ for (const nodeId of skipped) {
2731
+ if (await store.skip(runKey, nodeId) !== false) settled.push(nodeId);
2732
+ }
2733
+ return settled;
2710
2734
  }
2711
2735
  };
2712
2736
 
2713
2737
  // src/durable/replay.ts
2714
2738
  var BOUNDARY = "fancy-flow:node-boundary";
2739
+ var FENCE_PORT = "fancy-flow:fenced";
2715
2740
  async function replayUpTo(graph, nodeId, executors, options = {}) {
2716
- const fenced = { ...executors };
2741
+ const fencedOff = /* @__PURE__ */ new Set();
2742
+ const fence = (ctx) => {
2743
+ fencedOff.add(ctx.node.id);
2744
+ return { __port: FENCE_PORT, value: null };
2745
+ };
2746
+ const fences = {};
2717
2747
  for (const node of graph.nodes) {
2718
- if (node.id !== nodeId) {
2719
- fenced[node.id] = (ctx) => ctx.abort(BOUNDARY);
2720
- }
2748
+ if (node.id !== nodeId) fences[node.id] = fence;
2721
2749
  }
2722
2750
  const ports = {};
2723
2751
  const collect = (event) => {
@@ -2727,12 +2755,17 @@ async function replayUpTo(graph, nodeId, executors, options = {}) {
2727
2755
  }
2728
2756
  options.onEvent?.(event);
2729
2757
  };
2730
- const result = await runFlow(graph, fenced, collect, {
2758
+ const result = await runFlow(graph, executors, collect, {
2731
2759
  initialInputs: options.initialInputs ?? {},
2732
2760
  resumeOutputs: options.resumeOutputs ?? {},
2733
2761
  depth: options.depth ?? 0,
2734
- run: options.run
2762
+ run: options.run,
2763
+ nodeExecutors: fences
2735
2764
  });
2765
+ for (const id of fencedOff) {
2766
+ delete result.outputs[id];
2767
+ delete ports[id];
2768
+ }
2736
2769
  return {
2737
2770
  result,
2738
2771
  ports,
@@ -2911,10 +2944,19 @@ var Coordinator = class {
2911
2944
  *
2912
2945
  * Also settles the skip cascade, because a skip is a decision the frontier
2913
2946
  * just made and a second caller must not make it again.
2947
+ *
2948
+ * And it is where a skipped node's undelivered-edge warnings are emitted. A
2949
+ * skipped node never gets a job, so it never replays to warn for itself: its
2950
+ * warning used to be raised inside OTHER jobs' replays, carrying its node id,
2951
+ * and {@link forward} filtered it out. Asking at the skip decision — the one
2952
+ * place that knows the node will never run — delivers it, and asking only for
2953
+ * the nodes this call actually settled delivers it once.
2914
2954
  */
2915
2955
  async advance() {
2916
- const frontier = Frontier.compute(this.graph, await this.store.state(this.runKey));
2917
- await Frontier.settleSkips(this.store, this.runKey, frontier.skipped);
2956
+ const state = await this.store.state(this.runKey);
2957
+ const frontier = Frontier.compute(this.graph, state);
2958
+ const settled = await Frontier.settleSkips(this.store, this.runKey, frontier.skipped);
2959
+ this.warnForSkipped(settled, state);
2918
2960
  return frontier.ready;
2919
2961
  }
2920
2962
  /**
@@ -2954,12 +2996,12 @@ var Coordinator = class {
2954
2996
  await this.store.pause(this.runKey, nodeId, result.error ?? "");
2955
2997
  return { nodeId, status: "paused", pause, claimed: true, attempt };
2956
2998
  }
2957
- if (isBoundary(result.error)) {
2999
+ if (isBoundary(result.error) || result.ok) {
2958
3000
  await this.store.skip(this.runKey, nodeId);
2959
3001
  return {
2960
3002
  nodeId,
2961
3003
  status: "skipped",
2962
- error: "replay stopped before reaching this node",
3004
+ error: "the engine found no live inbound edge for this node",
2963
3005
  claimed: true,
2964
3006
  attempt
2965
3007
  };
@@ -3070,6 +3112,40 @@ var Coordinator = class {
3070
3112
  if (!row) return this.run;
3071
3113
  return this.run.withAttempt(row.attempts, row.firstAttemptAt);
3072
3114
  }
3115
+ /**
3116
+ * Send `runFlow`'s undelivered-edge warnings for nodes the frontier skipped.
3117
+ *
3118
+ * The check is `undeliveredEdgeWarnings` — the SAME function `runFlow` calls —
3119
+ * fed the durable equivalent of the engine's own bookkeeping: a port key for
3120
+ * every port a COMPLETED node's claim row stored, in stored order, and the set
3121
+ * of COMPLETED ids. `state` is the snapshot the frontier decided from, and
3122
+ * every predecessor of a skipped node had settled in it, so it holds all a
3123
+ * target's sources.
3124
+ *
3125
+ * Only skipped nodes. A target that RUNS (it had another live inbound edge)
3126
+ * already gets its warning from its own job's replay, whose events carry its
3127
+ * node id and are forwarded — asking here too would send it twice.
3128
+ */
3129
+ warnForSkipped(skipped, state) {
3130
+ const sink = this.onEvent;
3131
+ if (!sink || skipped.length === 0) return;
3132
+ const portValues = /* @__PURE__ */ new Map();
3133
+ const completed = /* @__PURE__ */ new Set();
3134
+ for (const [nodeId, entry] of Object.entries(state)) {
3135
+ if (entry.status !== NodeRunStatus.COMPLETED) continue;
3136
+ completed.add(nodeId);
3137
+ for (const portId of entry.ports) portValues.set(`${nodeId}:${portId}`, entry.output);
3138
+ }
3139
+ const nodesById = new Map(this.graph.nodes.map((n) => [n.id, n]));
3140
+ for (const nodeId of skipped) {
3141
+ const target = nodesById.get(nodeId);
3142
+ if (!target) continue;
3143
+ const incoming = this.graph.edges.filter((e) => e.target === nodeId);
3144
+ for (const warning of undeliveredEdgeWarnings(target, incoming, portValues, completed, nodesById)) {
3145
+ sink(warning);
3146
+ }
3147
+ }
3148
+ }
3073
3149
  async completedOutputs() {
3074
3150
  const state = await this.store.state(this.runKey);
3075
3151
  const out = {};
@@ -3101,6 +3177,7 @@ function cryptoRandom() {
3101
3177
 
3102
3178
  exports.BOUNDARY = BOUNDARY;
3103
3179
  exports.Coordinator = Coordinator;
3180
+ exports.FENCE_PORT = FENCE_PORT;
3104
3181
  exports.Frontier = Frontier;
3105
3182
  exports.InMemoryClaimStore = InMemoryClaimStore;
3106
3183
  exports.NodeRunStatus = NodeRunStatus;