flowcraft 2.7.1 → 2.8.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 (229) hide show
  1. package/README.md +1 -1
  2. package/dist/index-D3dyjW2G.d.mts +1269 -0
  3. package/dist/index.d.mts +2 -0
  4. package/dist/index.mjs +727 -0
  5. package/dist/index.mjs.map +1 -0
  6. package/dist/replay-BB11M6K1.mjs +107 -0
  7. package/dist/replay-BB11M6K1.mjs.map +1 -0
  8. package/dist/runtime-CmefIAu_.mjs +2216 -0
  9. package/dist/runtime-CmefIAu_.mjs.map +1 -0
  10. package/dist/{sdk.d.ts → sdk.d.mts} +14 -12
  11. package/dist/sdk.mjs +29 -0
  12. package/dist/sdk.mjs.map +1 -0
  13. package/dist/testing/index.d.mts +172 -0
  14. package/dist/testing/index.mjs +277 -0
  15. package/dist/testing/index.mjs.map +1 -0
  16. package/package.json +59 -59
  17. package/LICENSE +0 -21
  18. package/dist/adapters/index.d.ts +0 -4
  19. package/dist/adapters/index.js +0 -4
  20. package/dist/adapters/index.js.map +0 -1
  21. package/dist/adapters/persistent-event-bus.d.ts +0 -69
  22. package/dist/adapters/persistent-event-bus.js +0 -3
  23. package/dist/adapters/persistent-event-bus.js.map +0 -1
  24. package/dist/analysis.d.ts +0 -53
  25. package/dist/analysis.js +0 -3
  26. package/dist/analysis.js.map +0 -1
  27. package/dist/chunk-27STBUGG.js +0 -44
  28. package/dist/chunk-27STBUGG.js.map +0 -1
  29. package/dist/chunk-2TSADFQX.js +0 -46
  30. package/dist/chunk-2TSADFQX.js.map +0 -1
  31. package/dist/chunk-3Y5O5EGB.js +0 -3
  32. package/dist/chunk-3Y5O5EGB.js.map +0 -1
  33. package/dist/chunk-4PELJWF7.js +0 -29
  34. package/dist/chunk-4PELJWF7.js.map +0 -1
  35. package/dist/chunk-55J6XMHW.js +0 -3
  36. package/dist/chunk-55J6XMHW.js.map +0 -1
  37. package/dist/chunk-6RKHCJUU.js +0 -29
  38. package/dist/chunk-6RKHCJUU.js.map +0 -1
  39. package/dist/chunk-7EBKWATZ.js +0 -86
  40. package/dist/chunk-7EBKWATZ.js.map +0 -1
  41. package/dist/chunk-7EMUOH77.js +0 -90
  42. package/dist/chunk-7EMUOH77.js.map +0 -1
  43. package/dist/chunk-7M6FHFHP.js +0 -25
  44. package/dist/chunk-7M6FHFHP.js.map +0 -1
  45. package/dist/chunk-AKDL2ZX7.js +0 -287
  46. package/dist/chunk-AKDL2ZX7.js.map +0 -1
  47. package/dist/chunk-BC4G7OM6.js +0 -42
  48. package/dist/chunk-BC4G7OM6.js.map +0 -1
  49. package/dist/chunk-BCMR7Y4U.js +0 -76
  50. package/dist/chunk-BCMR7Y4U.js.map +0 -1
  51. package/dist/chunk-BCRWXTWX.js +0 -21
  52. package/dist/chunk-BCRWXTWX.js.map +0 -1
  53. package/dist/chunk-BEHVGFIM.js +0 -532
  54. package/dist/chunk-BEHVGFIM.js.map +0 -1
  55. package/dist/chunk-DL7KVYZF.js +0 -39
  56. package/dist/chunk-DL7KVYZF.js.map +0 -1
  57. package/dist/chunk-DV2CXHOY.js +0 -74
  58. package/dist/chunk-DV2CXHOY.js.map +0 -1
  59. package/dist/chunk-H4JTZYIT.js +0 -172
  60. package/dist/chunk-H4JTZYIT.js.map +0 -1
  61. package/dist/chunk-HFJXYY4E.js +0 -3
  62. package/dist/chunk-HFJXYY4E.js.map +0 -1
  63. package/dist/chunk-HNHM3FDK.js +0 -52
  64. package/dist/chunk-HNHM3FDK.js.map +0 -1
  65. package/dist/chunk-HXSK5P2X.js +0 -150
  66. package/dist/chunk-HXSK5P2X.js.map +0 -1
  67. package/dist/chunk-I53JB2KW.js +0 -26
  68. package/dist/chunk-I53JB2KW.js.map +0 -1
  69. package/dist/chunk-IDTYHLDQ.js +0 -16
  70. package/dist/chunk-IDTYHLDQ.js.map +0 -1
  71. package/dist/chunk-IKOTX22J.js +0 -85
  72. package/dist/chunk-IKOTX22J.js.map +0 -1
  73. package/dist/chunk-L3MX5MTA.js +0 -33
  74. package/dist/chunk-L3MX5MTA.js.map +0 -1
  75. package/dist/chunk-L46TQXCV.js +0 -144
  76. package/dist/chunk-L46TQXCV.js.map +0 -1
  77. package/dist/chunk-LM4ACVHL.js +0 -73
  78. package/dist/chunk-LM4ACVHL.js.map +0 -1
  79. package/dist/chunk-LNK7LZER.js +0 -51
  80. package/dist/chunk-LNK7LZER.js.map +0 -1
  81. package/dist/chunk-N63S5NEG.js +0 -107
  82. package/dist/chunk-N63S5NEG.js.map +0 -1
  83. package/dist/chunk-NVLZFLYM.js +0 -3
  84. package/dist/chunk-NVLZFLYM.js.map +0 -1
  85. package/dist/chunk-ONH7PIJZ.js +0 -300
  86. package/dist/chunk-ONH7PIJZ.js.map +0 -1
  87. package/dist/chunk-PH2IYZHV.js +0 -48
  88. package/dist/chunk-PH2IYZHV.js.map +0 -1
  89. package/dist/chunk-RAZWRNAJ.js +0 -54
  90. package/dist/chunk-RAZWRNAJ.js.map +0 -1
  91. package/dist/chunk-RM677CNU.js +0 -52
  92. package/dist/chunk-RM677CNU.js.map +0 -1
  93. package/dist/chunk-TKSSRS5U.js +0 -39
  94. package/dist/chunk-TKSSRS5U.js.map +0 -1
  95. package/dist/chunk-U7DKCIWT.js +0 -340
  96. package/dist/chunk-U7DKCIWT.js.map +0 -1
  97. package/dist/chunk-UNORA7EM.js +0 -103
  98. package/dist/chunk-UNORA7EM.js.map +0 -1
  99. package/dist/chunk-WWGFIYKW.js +0 -47
  100. package/dist/chunk-WWGFIYKW.js.map +0 -1
  101. package/dist/chunk-XZZWIJ4G.js +0 -25
  102. package/dist/chunk-XZZWIJ4G.js.map +0 -1
  103. package/dist/chunk-ZETQCNEF.js +0 -139
  104. package/dist/chunk-ZETQCNEF.js.map +0 -1
  105. package/dist/chunk-ZLW4QOTS.js +0 -192
  106. package/dist/chunk-ZLW4QOTS.js.map +0 -1
  107. package/dist/container-factory.d.ts +0 -17
  108. package/dist/container-factory.js +0 -13
  109. package/dist/container-factory.js.map +0 -1
  110. package/dist/container.d.ts +0 -23
  111. package/dist/container.js +0 -3
  112. package/dist/container.js.map +0 -1
  113. package/dist/context.d.ts +0 -65
  114. package/dist/context.js +0 -3
  115. package/dist/context.js.map +0 -1
  116. package/dist/error-mapper.d.ts +0 -15
  117. package/dist/error-mapper.js +0 -4
  118. package/dist/error-mapper.js.map +0 -1
  119. package/dist/errors.d.ts +0 -20
  120. package/dist/errors.js +0 -3
  121. package/dist/errors.js.map +0 -1
  122. package/dist/evaluator.d.ts +0 -32
  123. package/dist/evaluator.js +0 -3
  124. package/dist/evaluator.js.map +0 -1
  125. package/dist/flow.d.ts +0 -85
  126. package/dist/flow.js +0 -4
  127. package/dist/flow.js.map +0 -1
  128. package/dist/index.d.ts +0 -18
  129. package/dist/index.js +0 -38
  130. package/dist/index.js.map +0 -1
  131. package/dist/linter.d.ts +0 -26
  132. package/dist/linter.js +0 -4
  133. package/dist/linter.js.map +0 -1
  134. package/dist/logger.d.ts +0 -20
  135. package/dist/logger.js +0 -3
  136. package/dist/logger.js.map +0 -1
  137. package/dist/node.d.ts +0 -3
  138. package/dist/node.js +0 -3
  139. package/dist/node.js.map +0 -1
  140. package/dist/nodes/batch-gather.d.ts +0 -9
  141. package/dist/nodes/batch-gather.js +0 -4
  142. package/dist/nodes/batch-gather.js.map +0 -1
  143. package/dist/nodes/batch-scatter.d.ts +0 -9
  144. package/dist/nodes/batch-scatter.js +0 -4
  145. package/dist/nodes/batch-scatter.js.map +0 -1
  146. package/dist/nodes/sleep.d.ts +0 -9
  147. package/dist/nodes/sleep.js +0 -4
  148. package/dist/nodes/sleep.js.map +0 -1
  149. package/dist/nodes/subflow.d.ts +0 -9
  150. package/dist/nodes/subflow.js +0 -10
  151. package/dist/nodes/subflow.js.map +0 -1
  152. package/dist/nodes/wait.d.ts +0 -9
  153. package/dist/nodes/wait.js +0 -4
  154. package/dist/nodes/wait.js.map +0 -1
  155. package/dist/nodes/webhook.d.ts +0 -13
  156. package/dist/nodes/webhook.js +0 -4
  157. package/dist/nodes/webhook.js.map +0 -1
  158. package/dist/runtime/adapter.d.ts +0 -114
  159. package/dist/runtime/adapter.js +0 -28
  160. package/dist/runtime/adapter.js.map +0 -1
  161. package/dist/runtime/builtin-keys.d.ts +0 -38
  162. package/dist/runtime/builtin-keys.js +0 -10
  163. package/dist/runtime/builtin-keys.js.map +0 -1
  164. package/dist/runtime/execution-context.d.ts +0 -3
  165. package/dist/runtime/execution-context.js +0 -6
  166. package/dist/runtime/execution-context.js.map +0 -1
  167. package/dist/runtime/executors.d.ts +0 -3
  168. package/dist/runtime/executors.js +0 -4
  169. package/dist/runtime/executors.js.map +0 -1
  170. package/dist/runtime/index.d.ts +0 -7
  171. package/dist/runtime/index.js +0 -31
  172. package/dist/runtime/index.js.map +0 -1
  173. package/dist/runtime/node-executor-factory.d.ts +0 -12
  174. package/dist/runtime/node-executor-factory.js +0 -6
  175. package/dist/runtime/node-executor-factory.js.map +0 -1
  176. package/dist/runtime/orchestrator.d.ts +0 -9
  177. package/dist/runtime/orchestrator.js +0 -8
  178. package/dist/runtime/orchestrator.js.map +0 -1
  179. package/dist/runtime/orchestrators/replay.d.ts +0 -45
  180. package/dist/runtime/orchestrators/replay.js +0 -3
  181. package/dist/runtime/orchestrators/replay.js.map +0 -1
  182. package/dist/runtime/orchestrators/step-by-step.d.ts +0 -16
  183. package/dist/runtime/orchestrators/step-by-step.js +0 -5
  184. package/dist/runtime/orchestrators/step-by-step.js.map +0 -1
  185. package/dist/runtime/orchestrators/utils.d.ts +0 -35
  186. package/dist/runtime/orchestrators/utils.js +0 -4
  187. package/dist/runtime/orchestrators/utils.js.map +0 -1
  188. package/dist/runtime/runtime.d.ts +0 -3
  189. package/dist/runtime/runtime.js +0 -27
  190. package/dist/runtime/runtime.js.map +0 -1
  191. package/dist/runtime/scheduler.d.ts +0 -3
  192. package/dist/runtime/scheduler.js +0 -3
  193. package/dist/runtime/scheduler.js.map +0 -1
  194. package/dist/runtime/state.d.ts +0 -3
  195. package/dist/runtime/state.js +0 -5
  196. package/dist/runtime/state.js.map +0 -1
  197. package/dist/runtime/traverser.d.ts +0 -3
  198. package/dist/runtime/traverser.js +0 -4
  199. package/dist/runtime/traverser.js.map +0 -1
  200. package/dist/runtime/types.d.ts +0 -3
  201. package/dist/runtime/types.js +0 -3
  202. package/dist/runtime/types.js.map +0 -1
  203. package/dist/runtime/workflow-logic-handler.d.ts +0 -17
  204. package/dist/runtime/workflow-logic-handler.js +0 -5
  205. package/dist/runtime/workflow-logic-handler.js.map +0 -1
  206. package/dist/sanitizer.d.ts +0 -12
  207. package/dist/sanitizer.js +0 -3
  208. package/dist/sanitizer.js.map +0 -1
  209. package/dist/sdk.js +0 -20
  210. package/dist/sdk.js.map +0 -1
  211. package/dist/serializer.d.ts +0 -18
  212. package/dist/serializer.js +0 -3
  213. package/dist/serializer.js.map +0 -1
  214. package/dist/testing/event-logger.d.ts +0 -63
  215. package/dist/testing/event-logger.js +0 -3
  216. package/dist/testing/event-logger.js.map +0 -1
  217. package/dist/testing/index.d.ts +0 -7
  218. package/dist/testing/index.js +0 -37
  219. package/dist/testing/index.js.map +0 -1
  220. package/dist/testing/run-with-trace.d.ts +0 -38
  221. package/dist/testing/run-with-trace.js +0 -33
  222. package/dist/testing/run-with-trace.js.map +0 -1
  223. package/dist/testing/stepper.d.ts +0 -79
  224. package/dist/testing/stepper.js +0 -11
  225. package/dist/testing/stepper.js.map +0 -1
  226. package/dist/types-CKhffqyb.d.ts +0 -666
  227. package/dist/types.d.ts +0 -3
  228. package/dist/types.js +0 -3
  229. package/dist/types.js.map +0 -1
@@ -0,0 +1,2216 @@
1
+ //#region src/adapters/persistent-event-bus.ts
2
+ /**
3
+ * A pluggable event bus adapter that persists all workflow events
4
+ * to a configurable storage backend, enabling time-travel debugging and replay.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * // Using a database-backed store
9
+ * const eventStore = new DatabaseEventStore(dbConnection)
10
+ * const eventBus = new PersistentEventBusAdapter(eventStore)
11
+ * const runtime = new FlowRuntime({ eventBus })
12
+ *
13
+ * // Later, replay the execution
14
+ * const events = await eventStore.retrieve(executionId)
15
+ * const finalState = await runtime.replay(blueprint, events)
16
+ * ```
17
+ */
18
+ var PersistentEventBusAdapter = class {
19
+ constructor(store) {
20
+ this.store = store;
21
+ }
22
+ /**
23
+ * Emit an event by storing it persistently.
24
+ * Also emits to console for debugging (can be made configurable).
25
+ */
26
+ async emit(event) {
27
+ let executionId = "unknown";
28
+ if ("executionId" in event.payload) executionId = event.payload.executionId;
29
+ await this.store.store(event, executionId);
30
+ }
31
+ };
32
+ /**
33
+ * Simple in-memory event store for testing and development.
34
+ * Not suitable for production use.
35
+ */
36
+ var InMemoryEventStore = class {
37
+ events = /* @__PURE__ */ new Map();
38
+ async store(event, executionId) {
39
+ if (!this.events.has(executionId)) this.events.set(executionId, []);
40
+ this.events.get(executionId)?.push(event);
41
+ }
42
+ async retrieve(executionId) {
43
+ return this.events.get(executionId) || [];
44
+ }
45
+ async retrieveMultiple(executionIds) {
46
+ const result = /* @__PURE__ */ new Map();
47
+ for (const id of executionIds) result.set(id, await this.retrieve(id));
48
+ return result;
49
+ }
50
+ /**
51
+ * Clear all stored events (useful for testing).
52
+ */
53
+ clear() {
54
+ this.events.clear();
55
+ }
56
+ };
57
+
58
+ //#endregion
59
+ //#region src/analysis.ts
60
+ /**
61
+ * Analyzes a workflow blueprint to detect cycles using an iterative DFS algorithm.
62
+ * This avoids stack overflow issues for deep graphs compared to the recursive version.
63
+ * @param blueprint The WorkflowBlueprint object containing nodes and edges.
64
+ * @returns An array of cycles found. Each cycle is represented as an array of node IDs.
65
+ */
66
+ function checkForCycles(blueprint) {
67
+ const cycles = [];
68
+ if (!blueprint?.nodes || blueprint.nodes.length === 0) return cycles;
69
+ const allNodeIds = blueprint.nodes.map((node) => node.id);
70
+ const adj = /* @__PURE__ */ new Map();
71
+ for (const id of allNodeIds) adj.set(id, []);
72
+ for (const edge of blueprint.edges) adj.get(edge.source)?.push(edge.target);
73
+ const state = /* @__PURE__ */ new Map();
74
+ for (const id of allNodeIds) state.set(id, 0);
75
+ for (const startNode of allNodeIds) {
76
+ if (state.get(startNode) !== 0) continue;
77
+ const stack = [{
78
+ node: startNode,
79
+ path: []
80
+ }];
81
+ const pathSet = /* @__PURE__ */ new Set();
82
+ while (stack.length > 0) {
83
+ const { node, path } = stack[stack.length - 1];
84
+ if (state.get(node) === 0) {
85
+ state.set(node, 1);
86
+ pathSet.add(node);
87
+ path.push(node);
88
+ }
89
+ const neighbors = adj.get(node) || [];
90
+ let foundUnvisited = false;
91
+ for (const neighbor of neighbors) if (state.get(neighbor) === 1) {
92
+ const cycleStartIndex = path.indexOf(neighbor);
93
+ const cycle = path.slice(cycleStartIndex);
94
+ cycles.push([...cycle, neighbor]);
95
+ } else if (state.get(neighbor) === 0) {
96
+ stack.push({
97
+ node: neighbor,
98
+ path: [...path]
99
+ });
100
+ foundUnvisited = true;
101
+ break;
102
+ }
103
+ if (!foundUnvisited) {
104
+ state.set(node, 2);
105
+ stack.pop();
106
+ pathSet.delete(node);
107
+ }
108
+ }
109
+ }
110
+ return cycles;
111
+ }
112
+ /**
113
+ * Generates Mermaid diagram syntax from a WorkflowBlueprint
114
+ * @param blueprint The WorkflowBlueprint object containing nodes and edges
115
+ * @returns Mermaid syntax string for the flowchart
116
+ */
117
+ function generateMermaid(blueprint) {
118
+ if (!blueprint?.nodes || blueprint.nodes.length === 0) return "flowchart TD\n empty[Empty Blueprint]";
119
+ let mermaid = "flowchart TD\n";
120
+ for (const node of blueprint.nodes) {
121
+ const paramsString = node.params ? `<br/>params: ${JSON.stringify(node.params)}` : "";
122
+ const nodeLabel = `${node.id}${paramsString}`;
123
+ mermaid += ` ${node.id}["${nodeLabel}"]\n`;
124
+ }
125
+ for (const edge of blueprint.edges || []) {
126
+ const labelParts = [];
127
+ if (edge.action) labelParts.push(edge.action);
128
+ if (edge.condition) labelParts.push(edge.condition);
129
+ if (edge.transform) labelParts.push(edge.transform);
130
+ if (labelParts.length > 0) {
131
+ const edgeLabel = labelParts.join(" | ");
132
+ mermaid += ` ${edge.source} -- "${edgeLabel}" --> ${edge.target}\n`;
133
+ } else mermaid += ` ${edge.source} --> ${edge.target}\n`;
134
+ }
135
+ return mermaid;
136
+ }
137
+ /**
138
+ * Generates Mermaid diagram syntax from a WorkflowBlueprint with execution history styling
139
+ * @param blueprint The WorkflowBlueprint object containing nodes and edges
140
+ * @param events Array of FlowcraftEvent objects from the workflow execution
141
+ * @returns Mermaid syntax string for the flowchart with execution path highlighting
142
+ */
143
+ function generateMermaidForRun(blueprint, events) {
144
+ if (!blueprint?.nodes || blueprint.nodes.length === 0) return "flowchart TD\n empty[Empty Blueprint]";
145
+ let mermaid = "flowchart TD\n";
146
+ const successfulNodes = /* @__PURE__ */ new Set();
147
+ const failedNodes = /* @__PURE__ */ new Set();
148
+ const takenEdges = /* @__PURE__ */ new Set();
149
+ for (const event of events) switch (event.type) {
150
+ case "node:finish":
151
+ successfulNodes.add(event.payload.nodeId);
152
+ break;
153
+ case "node:error":
154
+ failedNodes.add(event.payload.nodeId);
155
+ break;
156
+ case "edge:evaluate":
157
+ if (event.payload.result) {
158
+ const edgeKey = `${event.payload.source}->${event.payload.target}`;
159
+ takenEdges.add(edgeKey);
160
+ }
161
+ break;
162
+ }
163
+ for (const node of blueprint.nodes) {
164
+ const paramsString = node.params ? `<br/>params: ${JSON.stringify(node.params)}` : "";
165
+ const nodeLabel = `${node.id}${paramsString}`;
166
+ mermaid += ` ${node.id}["${nodeLabel}"]\n`;
167
+ }
168
+ for (const node of blueprint.nodes) if (successfulNodes.has(node.id)) mermaid += ` style ${node.id} fill:#d4edda,stroke:#c3e6cb\n`;
169
+ else if (failedNodes.has(node.id)) mermaid += ` style ${node.id} fill:#f8d7da,stroke:#f5c6cb\n`;
170
+ let edgeIndex = 0;
171
+ for (const edge of blueprint.edges || []) {
172
+ const labelParts = [];
173
+ if (edge.action) labelParts.push(edge.action);
174
+ if (edge.condition) labelParts.push(edge.condition);
175
+ if (edge.transform) labelParts.push(edge.transform);
176
+ const edgeKey = `${edge.source}->${edge.target}`;
177
+ const isTaken = takenEdges.has(edgeKey);
178
+ let edgeLine;
179
+ if (labelParts.length > 0) {
180
+ const edgeLabel = labelParts.join(" | ");
181
+ edgeLine = ` ${edge.source} -- "${edgeLabel}" --> ${edge.target}\n`;
182
+ } else edgeLine = ` ${edge.source} --> ${edge.target}\n`;
183
+ mermaid += edgeLine;
184
+ if (isTaken) mermaid += ` linkStyle ${edgeIndex} stroke:#007bff,stroke-width:3px\n`;
185
+ edgeIndex++;
186
+ }
187
+ return mermaid;
188
+ }
189
+ /**
190
+ * Analyzes a workflow blueprint and returns comprehensive analysis
191
+ * @param blueprint The WorkflowBlueprint object containing nodes and edges
192
+ * @returns Analysis result with cycles, start nodes, terminal nodes, and other metrics
193
+ */
194
+ function analyzeBlueprint(blueprint) {
195
+ if (!blueprint?.nodes || blueprint.nodes.length === 0) return {
196
+ cycles: [],
197
+ startNodeIds: [],
198
+ terminalNodeIds: [],
199
+ nodeCount: 0,
200
+ edgeCount: 0,
201
+ isDag: true
202
+ };
203
+ const cycles = checkForCycles(blueprint);
204
+ const nodeCount = blueprint.nodes.length;
205
+ const edgeCount = blueprint.edges?.length || 0;
206
+ const nodesWithIncoming = /* @__PURE__ */ new Set();
207
+ for (const edge of blueprint.edges || []) nodesWithIncoming.add(edge.target);
208
+ const startNodeIds = blueprint.nodes.map((node) => node.id).filter((nodeId) => !nodesWithIncoming.has(nodeId));
209
+ const nodesWithOutgoing = /* @__PURE__ */ new Set();
210
+ for (const edge of blueprint.edges || []) nodesWithOutgoing.add(edge.source);
211
+ return {
212
+ cycles,
213
+ startNodeIds,
214
+ terminalNodeIds: blueprint.nodes.map((node) => node.id).filter((nodeId) => !nodesWithOutgoing.has(nodeId)),
215
+ nodeCount,
216
+ edgeCount,
217
+ isDag: cycles.length === 0
218
+ };
219
+ }
220
+
221
+ //#endregion
222
+ //#region src/container.ts
223
+ var DIContainer = class DIContainer {
224
+ services = /* @__PURE__ */ new Map();
225
+ factories = /* @__PURE__ */ new Map();
226
+ register(token, implementation) {
227
+ this.services.set(token, implementation);
228
+ }
229
+ registerFactory(token, factory) {
230
+ this.factories.set(token, factory);
231
+ }
232
+ resolve(token) {
233
+ if (this.services.has(token)) return this.services.get(token);
234
+ if (this.factories.has(token)) {
235
+ const instance = this.factories.get(token)?.(this);
236
+ this.services.set(token, instance);
237
+ return instance;
238
+ }
239
+ throw new Error(`Service not found for token: ${String(token)}`);
240
+ }
241
+ has(token) {
242
+ return this.services.has(token) || this.factories.has(token);
243
+ }
244
+ createChild() {
245
+ const child = new DIContainer();
246
+ child.services = new Map(this.services);
247
+ child.factories = new Map(this.factories);
248
+ return child;
249
+ }
250
+ };
251
+ const ServiceTokens = {
252
+ Logger: Symbol.for("flowcraft:logger"),
253
+ Serializer: Symbol.for("flowcraft:serializer"),
254
+ Evaluator: Symbol.for("flowcraft:evaluator"),
255
+ EventBus: Symbol.for("flowcraft:eventBus"),
256
+ Orchestrator: Symbol.for("flowcraft:orchestrator"),
257
+ Middleware: Symbol.for("flowcraft:middleware"),
258
+ NodeRegistry: Symbol.for("flowcraft:nodeRegistry"),
259
+ BlueprintRegistry: Symbol.for("flowcraft:blueprintRegistry"),
260
+ Dependencies: Symbol.for("flowcraft:dependencies")
261
+ };
262
+
263
+ //#endregion
264
+ //#region src/evaluator.ts
265
+ /**
266
+ * A safe evaluator that only allows simple property access.
267
+ * It cannot execute arbitrary code and is secure for untrusted inputs.
268
+ *
269
+ * Example expressions:
270
+ * - "result.output.status"
271
+ * - "context.user.isAdmin"
272
+ * - "input.value"
273
+ */
274
+ var PropertyEvaluator = class {
275
+ evaluate(expression, context) {
276
+ try {
277
+ if (!/^[a-zA-Z0-9_$.]+$/.test(expression)) {
278
+ console.error(`Error evaluating expression: "${expression}" contains invalid characters.`);
279
+ return;
280
+ }
281
+ const parts = expression.split(".");
282
+ const startKey = parts[0];
283
+ if (!Object.hasOwn(context, startKey)) return;
284
+ let current = context[startKey];
285
+ for (let i = 1; i < parts.length; i++) {
286
+ if (current === null || current === void 0) return;
287
+ current = current[parts[i]];
288
+ }
289
+ return current;
290
+ } catch (error) {
291
+ console.error(`Error evaluating property expression "${expression}":`, error);
292
+ return;
293
+ }
294
+ }
295
+ };
296
+ /**
297
+ * @warning This evaluator uses `new Function()` and can execute arbitrary
298
+ * JavaScript code. It poses a significant security risk if the expressions
299
+ * are not from a trusted source (e.g., user input).
300
+ *
301
+ * It should only be used in controlled environments where all workflow
302
+ * definitions are static and authored by trusted developers.
303
+ *
304
+ * For safer evaluation, use the default `PropertyEvaluator` or install a
305
+ * sandboxed library like `jsep` to create a custom, secure evaluator.
306
+ */
307
+ var UnsafeEvaluator = class {
308
+ evaluate(expression, context) {
309
+ try {
310
+ const validIdentifierRegex = /^[a-z_$][\w$]*$/i;
311
+ const validKeys = Object.keys(context).filter((key) => validIdentifierRegex.test(key));
312
+ const validContext = {};
313
+ for (const key of validKeys) validContext[key] = context[key];
314
+ return new Function(...validKeys, `return ${expression}`)(...validKeys.map((k) => validContext[k]));
315
+ } catch (error) {
316
+ console.error(`Error evaluating expression "${expression}":`, error);
317
+ return;
318
+ }
319
+ }
320
+ };
321
+
322
+ //#endregion
323
+ //#region src/logger.ts
324
+ /** A logger implementation that outputs to the console. */
325
+ var ConsoleLogger = class {
326
+ debug(message, meta) {
327
+ console.debug(`[DEBUG] ${message}`, meta || "");
328
+ }
329
+ info(message, meta) {
330
+ console.info(`[INFO] ${message}`, meta || "");
331
+ }
332
+ warn(message, meta) {
333
+ console.warn(`[WARN] ${message}`, meta || "");
334
+ }
335
+ error(message, meta) {
336
+ console.error(`[ERROR] ${message}`, meta || "");
337
+ }
338
+ };
339
+ /** A logger implementation that does nothing (no-op). */
340
+ var NullLogger = class {
341
+ debug(_message, _meta) {}
342
+ info(_message, _meta) {}
343
+ warn(_message, _meta) {}
344
+ error(_message, _meta) {}
345
+ };
346
+
347
+ //#endregion
348
+ //#region src/errors.ts
349
+ /**
350
+ * A single, comprehensive error class for the framework.
351
+ * Use this for all errors to ensure consistent structure and easy debugging.
352
+ */
353
+ var FlowcraftError = class extends Error {
354
+ message;
355
+ nodeId;
356
+ blueprintId;
357
+ executionId;
358
+ isFatal;
359
+ constructor(message, options = {}) {
360
+ super(message, { cause: options.cause });
361
+ this.name = "FlowcraftError";
362
+ this.message = message;
363
+ this.nodeId = options.nodeId;
364
+ this.blueprintId = options.blueprintId;
365
+ this.executionId = options.executionId;
366
+ this.isFatal = options.isFatal ?? false;
367
+ }
368
+ };
369
+
370
+ //#endregion
371
+ //#region src/context.ts
372
+ /**
373
+ * A default, high-performance, in-memory implementation of ISyncContext using a Map.
374
+ */
375
+ var Context = class {
376
+ type = "sync";
377
+ data;
378
+ constructor(initialData = {}) {
379
+ this.data = new Map(Object.entries(initialData));
380
+ }
381
+ get(key) {
382
+ return this.data.get(key);
383
+ }
384
+ set(key, value) {
385
+ this.data.set(key, value);
386
+ }
387
+ has(key) {
388
+ return this.data.has(key);
389
+ }
390
+ delete(key) {
391
+ return this.data.delete(key);
392
+ }
393
+ toJSON() {
394
+ return Object.fromEntries(this.data);
395
+ }
396
+ };
397
+ /**
398
+ * An adapter that provides a consistent, Promise-based view of a synchronous context.
399
+ * This is created by the runtime and is transparent to the node author.
400
+ */
401
+ var AsyncContextView = class {
402
+ type = "async";
403
+ constructor(syncContext) {
404
+ this.syncContext = syncContext;
405
+ }
406
+ get(key) {
407
+ return Promise.resolve(this.syncContext.get(key));
408
+ }
409
+ set(key, value) {
410
+ this.syncContext.set(key, value);
411
+ return Promise.resolve();
412
+ }
413
+ has(key) {
414
+ return Promise.resolve(this.syncContext.has(key));
415
+ }
416
+ delete(key) {
417
+ return Promise.resolve(this.syncContext.delete(key));
418
+ }
419
+ toJSON() {
420
+ return Promise.resolve(this.syncContext.toJSON());
421
+ }
422
+ async patch(_operations) {
423
+ throw new Error("Patch operations not supported by AsyncContextView");
424
+ }
425
+ };
426
+ /**
427
+ * A proxy wrapper that tracks changes to an async context for delta-based persistence.
428
+ * Records all mutations (set/delete operations) to enable efficient partial updates.
429
+ */
430
+ var TrackedAsyncContext = class {
431
+ type = "async";
432
+ deltas = [];
433
+ innerContext;
434
+ eventBus;
435
+ executionId;
436
+ sourceNode;
437
+ constructor(innerContext, eventBus, executionId, sourceNode) {
438
+ this.innerContext = innerContext;
439
+ this.eventBus = eventBus;
440
+ this.executionId = executionId;
441
+ this.sourceNode = sourceNode;
442
+ }
443
+ async get(key) {
444
+ return this.innerContext.get(key);
445
+ }
446
+ async set(key, value) {
447
+ this.deltas.push({
448
+ op: "set",
449
+ key,
450
+ value
451
+ });
452
+ await this.innerContext.set(key, value);
453
+ if (this.eventBus && this.executionId) await this.eventBus.emit({
454
+ type: "context:change",
455
+ payload: {
456
+ sourceNode: this.sourceNode || "unknown",
457
+ key,
458
+ op: "set",
459
+ value,
460
+ executionId: this.executionId
461
+ }
462
+ });
463
+ }
464
+ async has(key) {
465
+ return this.innerContext.has(key);
466
+ }
467
+ async delete(key) {
468
+ this.deltas.push({
469
+ op: "delete",
470
+ key
471
+ });
472
+ const result = await this.innerContext.delete(key);
473
+ if (this.eventBus && this.executionId && result) await this.eventBus.emit({
474
+ type: "context:change",
475
+ payload: {
476
+ sourceNode: this.sourceNode || "unknown",
477
+ key,
478
+ op: "delete",
479
+ executionId: this.executionId
480
+ }
481
+ });
482
+ return result;
483
+ }
484
+ toJSON() {
485
+ return this.innerContext.toJSON();
486
+ }
487
+ async patch(operations) {
488
+ if (this.innerContext.patch) return this.innerContext.patch(operations);
489
+ for (const op of operations) if (op.op === "set") await this.innerContext.set(op.key, op.value);
490
+ else if (op.op === "delete") await this.innerContext.delete(op.key);
491
+ }
492
+ getDeltas() {
493
+ return [...this.deltas];
494
+ }
495
+ clearDeltas() {
496
+ this.deltas = [];
497
+ }
498
+ /**
499
+ * Configures the event emitter for tracking context changes.
500
+ * This enables the context to emit events when set/delete operations occur,
501
+ * allowing for external monitoring and persistence of context mutations.
502
+ *
503
+ * @param eventBus - The event bus instance to emit context change events
504
+ * @param executionId - The unique identifier for the current workflow execution
505
+ * @param sourceNode - Optional identifier for the node that triggered the context change
506
+ */
507
+ configureEventEmitter(eventBus, executionId, sourceNode) {
508
+ this.eventBus = eventBus;
509
+ this.executionId = executionId;
510
+ this.sourceNode = sourceNode;
511
+ }
512
+ };
513
+
514
+ //#endregion
515
+ //#region src/runtime/state.ts
516
+ var WorkflowState = class {
517
+ _completedNodes = /* @__PURE__ */ new Set();
518
+ errors = [];
519
+ anyFallbackExecuted = false;
520
+ context;
521
+ _isAwaiting = false;
522
+ _awaitingNodeIds = /* @__PURE__ */ new Set();
523
+ _awaitingDetails = /* @__PURE__ */ new Map();
524
+ constructor(initialData, context) {
525
+ if (context) this.context = context instanceof TrackedAsyncContext ? context : new TrackedAsyncContext(context);
526
+ else this.context = new TrackedAsyncContext(new AsyncContextView(new Context(initialData)));
527
+ if (initialData._awaitingNodeIds) {
528
+ this._isAwaiting = true;
529
+ const awaitingIds = initialData._awaitingNodeIds;
530
+ if (Array.isArray(awaitingIds)) for (const id of awaitingIds) this._awaitingNodeIds.add(id);
531
+ }
532
+ if (initialData._awaitingDetails) this._awaitingDetails = new Map(Object.entries(initialData._awaitingDetails));
533
+ for (const key of Object.keys(initialData)) if (key.startsWith("_outputs.")) {
534
+ const nodeId = key.substring(9);
535
+ this._completedNodes.add(nodeId);
536
+ }
537
+ }
538
+ /**
539
+ * Configure the context to emit events when modified.
540
+ * This is called after the ExecutionContext is created.
541
+ */
542
+ setEventEmitter(eventBus, executionId, sourceNode) {
543
+ if (this.context instanceof TrackedAsyncContext) this.context.configureEventEmitter(eventBus, executionId, sourceNode);
544
+ }
545
+ async addCompletedNode(nodeId, output) {
546
+ this._completedNodes.add(nodeId);
547
+ await this.context.set(`_outputs.${nodeId}`, output);
548
+ }
549
+ addError(nodeId, error) {
550
+ const flowcraftError = new FlowcraftError(error.message, {
551
+ cause: error,
552
+ nodeId,
553
+ isFatal: false
554
+ });
555
+ this.errors.push({
556
+ ...flowcraftError,
557
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
558
+ originalError: error
559
+ });
560
+ }
561
+ clearError(nodeId) {
562
+ this.errors = this.errors.filter((err) => err.nodeId !== nodeId);
563
+ }
564
+ markFallbackExecuted() {
565
+ this.anyFallbackExecuted = true;
566
+ }
567
+ getContext() {
568
+ return this.context;
569
+ }
570
+ getCompletedNodes() {
571
+ return new Set(this._completedNodes);
572
+ }
573
+ getErrors() {
574
+ return this.errors;
575
+ }
576
+ getAnyFallbackExecuted() {
577
+ return this.anyFallbackExecuted;
578
+ }
579
+ async markAsAwaiting(nodeId, details) {
580
+ this._isAwaiting = true;
581
+ this._awaitingNodeIds.add(nodeId);
582
+ if (details) this._awaitingDetails.set(nodeId, details);
583
+ await this.context.set("_awaitingNodeIds", Array.from(this._awaitingNodeIds));
584
+ await this.context.set("_awaitingDetails", Object.fromEntries(this._awaitingDetails));
585
+ }
586
+ isAwaiting() {
587
+ return this._isAwaiting && this._awaitingNodeIds.size > 0;
588
+ }
589
+ getAwaitingNodeIds() {
590
+ return Array.from(this._awaitingNodeIds);
591
+ }
592
+ getAwaitingDetails(nodeId) {
593
+ return this._awaitingDetails.get(nodeId);
594
+ }
595
+ clearAwaiting(nodeId) {
596
+ if (nodeId) {
597
+ this._awaitingNodeIds.delete(nodeId);
598
+ this._awaitingDetails.delete(nodeId);
599
+ } else {
600
+ this._awaitingNodeIds.clear();
601
+ this._awaitingDetails.clear();
602
+ }
603
+ this._isAwaiting = this._awaitingNodeIds.size > 0;
604
+ if (this._awaitingNodeIds.size > 0) {
605
+ this.context.set("_awaitingNodeIds", Array.from(this._awaitingNodeIds));
606
+ this.context.set("_awaitingDetails", Object.fromEntries(this._awaitingDetails));
607
+ } else {
608
+ this.context.delete("_awaitingNodeIds");
609
+ this.context.delete("_awaitingDetails");
610
+ }
611
+ }
612
+ getStatus(isTraversalComplete = false) {
613
+ if (this._isAwaiting) return "awaiting";
614
+ if (this.anyFallbackExecuted) return "completed";
615
+ if (this.errors.length > 0) return "failed";
616
+ if (isTraversalComplete) return "completed";
617
+ return "stalled";
618
+ }
619
+ async toResult(serializer, executionId) {
620
+ const contextJSON = await this.context.toJSON();
621
+ if (!this._isAwaiting && contextJSON._awaitingNodeIds) {
622
+ delete contextJSON._awaitingNodeIds;
623
+ delete contextJSON._awaitingDetails;
624
+ }
625
+ if (executionId) contextJSON._executionId = executionId;
626
+ return {
627
+ context: contextJSON,
628
+ serializedContext: serializer.serialize(contextJSON),
629
+ status: this.getStatus(),
630
+ errors: this.errors.length > 0 ? this.errors : void 0
631
+ };
632
+ }
633
+ };
634
+
635
+ //#endregion
636
+ //#region src/runtime/execution-context.ts
637
+ /**
638
+ * A container for all state and dependencies of a single workflow execution.
639
+ * This object is created once per `run` and passed through the execution stack.
640
+ */
641
+ var ExecutionContext = class ExecutionContext {
642
+ constructor(blueprint, state, nodeRegistry, executionId, runtime, services, signal, concurrency) {
643
+ this.blueprint = blueprint;
644
+ this.state = state;
645
+ this.nodeRegistry = nodeRegistry;
646
+ this.executionId = executionId;
647
+ this.runtime = runtime;
648
+ this.services = services;
649
+ this.signal = signal;
650
+ this.concurrency = concurrency;
651
+ this.state.setEventEmitter(this.services.eventBus, this.executionId);
652
+ }
653
+ createForSubflow(subBlueprint, initialSubState) {
654
+ return new ExecutionContext(subBlueprint, new WorkflowState(initialSubState), this.nodeRegistry, this.executionId, this.runtime, this.services, this.signal, this.concurrency);
655
+ }
656
+ };
657
+
658
+ //#endregion
659
+ //#region src/runtime/orchestrators/utils.ts
660
+ async function executeBatch(readyNodes, blueprint, state, executorFactory, runtime, maxConcurrency) {
661
+ const concurrency = maxConcurrency || readyNodes.length;
662
+ const results = [];
663
+ for (let i = 0; i < readyNodes.length; i += concurrency) {
664
+ const batchPromises = readyNodes.slice(i, i + concurrency).map(async ({ nodeId }) => {
665
+ try {
666
+ const executor = executorFactory(nodeId);
667
+ if (!executor) throw new Error(`No executor for node ${nodeId}`);
668
+ const executionResult = await executor.execute(await runtime.resolveNodeInput(nodeId, blueprint, state.getContext()));
669
+ results.push({
670
+ status: "fulfilled",
671
+ value: {
672
+ nodeId,
673
+ executionResult
674
+ }
675
+ });
676
+ } catch (error) {
677
+ results.push({
678
+ status: "rejected",
679
+ reason: {
680
+ nodeId,
681
+ error
682
+ }
683
+ });
684
+ }
685
+ });
686
+ await Promise.all(batchPromises);
687
+ }
688
+ return results;
689
+ }
690
+ async function processResults(settledResults, traverser, state, runtime, _blueprint, executionId) {
691
+ for (const promiseResult of settledResults) {
692
+ if (promiseResult.status === "rejected") {
693
+ const { nodeId, error } = promiseResult.reason;
694
+ if (error instanceof FlowcraftError && error.message.includes("cancelled")) throw error;
695
+ state.addError(nodeId, error);
696
+ continue;
697
+ }
698
+ const { nodeId, executionResult } = promiseResult.value;
699
+ if (executionResult.status === "success") {
700
+ const result = executionResult.result;
701
+ if (result) {
702
+ state.addCompletedNode(nodeId, result.output);
703
+ if (result._fallbackExecuted) state.markFallbackExecuted();
704
+ if (result.dynamicNodes && result.dynamicNodes.length > 0) {
705
+ const gatherNodeId = result.output?.gatherNodeId;
706
+ for (const dynamicNode of result.dynamicNodes) traverser.addDynamicNode(dynamicNode.id, dynamicNode, nodeId, gatherNodeId);
707
+ }
708
+ }
709
+ const matched = await runtime.determineNextNodes(traverser.getDynamicBlueprint(), nodeId, result, state.getContext(), executionId);
710
+ for (const { node, edge } of matched) await runtime.applyEdgeTransform(edge, result, node, state.getContext(), traverser.getAllPredecessors(), executionId);
711
+ traverser.markNodeCompleted(nodeId, result, matched.map((m) => m.node));
712
+ } else if (executionResult.status === "failed_with_fallback") {
713
+ const { fallbackNodeId, error } = executionResult;
714
+ const fallbackNodeDef = traverser.getDynamicBlueprint().nodes.find((n) => n.id === fallbackNodeId);
715
+ if (!fallbackNodeDef) {
716
+ const notFoundError = new FlowcraftError(`Fallback node '${fallbackNodeId}' not found in blueprint.`, {
717
+ nodeId,
718
+ cause: error
719
+ });
720
+ state.addError(nodeId, notFoundError);
721
+ } else {
722
+ state.addCompletedNode(nodeId, null);
723
+ state.markFallbackExecuted();
724
+ traverser.markNodeCompleted(nodeId, {
725
+ action: "fallback",
726
+ output: null,
727
+ _fallbackExecuted: true
728
+ }, [fallbackNodeDef]);
729
+ }
730
+ } else state.addError(nodeId, executionResult.error);
731
+ }
732
+ }
733
+
734
+ //#endregion
735
+ //#region src/runtime/orchestrator.ts
736
+ var DefaultOrchestrator = class {
737
+ async run(context, traverser) {
738
+ const hardwareConcurrency = globalThis.navigator?.hardwareConcurrency || 4;
739
+ const maxConcurrency = context.concurrency != null && context.concurrency > 0 ? context.concurrency : Math.min(hardwareConcurrency, 10);
740
+ try {
741
+ context.signal?.throwIfAborted();
742
+ } catch (error) {
743
+ if (error instanceof DOMException && error.name === "AbortError") throw new FlowcraftError("Workflow cancelled", { isFatal: false });
744
+ throw error;
745
+ }
746
+ let iterations = 0;
747
+ const maxIterations = 1e4;
748
+ while (traverser.hasMoreWork()) {
749
+ if (++iterations > maxIterations) throw new Error("Traversal exceeded maximum iterations, possible infinite loop");
750
+ try {
751
+ context.signal?.throwIfAborted();
752
+ } catch (error) {
753
+ if (error instanceof DOMException && error.name === "AbortError") throw new FlowcraftError("Workflow cancelled", { isFatal: false });
754
+ throw error;
755
+ }
756
+ const readyNodes = traverser.getReadyNodes();
757
+ const dynamicBlueprint = traverser.getDynamicBlueprint();
758
+ const updatedContext = new ExecutionContext(dynamicBlueprint, context.state, context.nodeRegistry, context.executionId, context.runtime, context.services, context.signal, context.concurrency);
759
+ await processResults(await executeBatch(readyNodes, dynamicBlueprint, context.state, (nodeId) => context.runtime.getExecutorForNode(nodeId, updatedContext), context.runtime, maxConcurrency), traverser, context.state, context.runtime, context.blueprint, context.executionId);
760
+ if (context.state.isAwaiting()) break;
761
+ }
762
+ const isTraversalComplete = !traverser.hasMoreWork();
763
+ const status = context.state.getStatus(isTraversalComplete);
764
+ const result = await context.state.toResult(context.services.serializer, context.executionId);
765
+ result.status = status;
766
+ return result;
767
+ }
768
+ };
769
+
770
+ //#endregion
771
+ //#region src/serializer.ts
772
+ /**
773
+ * A default serializer using standard JSON.
774
+ *
775
+ * @warning This implementation is lossy and does not handle complex data types
776
+ * like `Date`, `Map`, `Set`, `undefined`, etc. It is recommended to provide a robust
777
+ * serializer like `superjson` if working with complex data types.
778
+ */
779
+ var JsonSerializer = class {
780
+ hasWarned = false;
781
+ serialize(data) {
782
+ for (const value of Object.values(data)) if (value instanceof Map || value instanceof Set || value instanceof Date) {
783
+ if (!this.hasWarned) {
784
+ console.warn("[Flowcraft] Warning: Default JsonSerializer does not support Map, Set, or Date types. Data may be lost. Consider providing a custom ISerializer (e.g., using superjson).");
785
+ this.hasWarned = true;
786
+ }
787
+ }
788
+ try {
789
+ return JSON.stringify(data);
790
+ } catch {
791
+ console.warn("[Flowcraft] Warning: Circular reference detected in context. Using safe serialization.");
792
+ return JSON.stringify({
793
+ _circularReference: true,
794
+ message: "Context contains circular references"
795
+ });
796
+ }
797
+ }
798
+ deserialize(text) {
799
+ return JSON.parse(text);
800
+ }
801
+ };
802
+
803
+ //#endregion
804
+ //#region src/node.ts
805
+ /** A type guard to reliably distinguish a NodeClass from a NodeFunction. */
806
+ function isNodeClass(impl) {
807
+ return typeof impl === "function" && !!impl.prototype?.exec;
808
+ }
809
+ /**
810
+ * A structured, class-based node for complex logic with a safe, granular lifecycle.
811
+ * This class is generic, allowing implementations to specify the exact context
812
+ * and dependency types they expect.
813
+ */
814
+ var BaseNode = class {
815
+ /**
816
+ * @param params Static parameters for this node instance, passed from the blueprint.
817
+ * @param nodeId The ID of the node in the blueprint.
818
+ */
819
+ constructor(params, nodeId) {
820
+ this.params = params;
821
+ this.nodeId = nodeId;
822
+ }
823
+ /**
824
+ * Phase 1: Gathers and prepares data for execution. This phase is NOT retried on failure.
825
+ * @param context The node's execution context.
826
+ * @returns The data needed for the `exec` phase.
827
+ */
828
+ async prep(context) {
829
+ return context.input;
830
+ }
831
+ /**
832
+ * Phase 3: Processes the result and saves state. This phase is NOT retried.
833
+ * @param execResult The successful result from the `exec` or `fallback` phase.
834
+ * @param _context The node's execution context.
835
+ */
836
+ async post(execResult, _context) {
837
+ return execResult;
838
+ }
839
+ /**
840
+ * An optional safety net that runs if all `exec` retries fail.
841
+ * @param error The final error from the last `exec` attempt.
842
+ * @param _context The node's execution context.
843
+ */
844
+ async fallback(error, _context) {
845
+ throw error;
846
+ }
847
+ /**
848
+ * An optional cleanup phase for non-retriable errors that occur outside the main `exec` method.
849
+ * This method is invoked in a `finally` block or equivalent construct if a fatal, unhandled exception occurs in the `prep`, `exec`, or `post` phases.
850
+ * Allows nodes to perform crucial cleanup, such as closing database connections or releasing locks.
851
+ * @param _error The error that caused the failure.
852
+ * @param _context The node's execution context.
853
+ */
854
+ async recover(_error, _context) {}
855
+ };
856
+
857
+ //#endregion
858
+ //#region src/nodes/batch-gather.ts
859
+ var BatchGatherNode = class extends BaseNode {
860
+ async exec(_prepResult, context) {
861
+ const { gatherNodeId, outputKey } = this.params || {};
862
+ const hasMore = await context.context.get(`${gatherNodeId}_hasMore`) || false;
863
+ const dynamicNodes = [];
864
+ let results = [];
865
+ if (hasMore) {
866
+ const newScatterId = `${gatherNodeId}_scatter_next`;
867
+ dynamicNodes.push({
868
+ id: newScatterId,
869
+ uses: "batch-scatter",
870
+ inputs: context.input,
871
+ params: {
872
+ ...this.params,
873
+ gatherNodeId
874
+ }
875
+ });
876
+ } else {
877
+ const allWorkerIds = await context.context.get(`${gatherNodeId}_allWorkerIds`) || [];
878
+ results = [];
879
+ for (const workerId of allWorkerIds) {
880
+ const result = await context.context.get(`_outputs.${workerId}`);
881
+ if (result !== void 0) results.push(result);
882
+ }
883
+ await context.context.set(outputKey, results);
884
+ const parentBatchId = gatherNodeId.replace("_gather", "");
885
+ await context.dependencies.runtime.services.eventBus.emit({
886
+ type: "batch:finish",
887
+ payload: {
888
+ batchId: parentBatchId,
889
+ gatherNodeId,
890
+ results
891
+ }
892
+ });
893
+ }
894
+ return {
895
+ dynamicNodes,
896
+ output: results
897
+ };
898
+ }
899
+ };
900
+
901
+ //#endregion
902
+ //#region src/nodes/batch-scatter.ts
903
+ var BatchScatterNode = class extends BaseNode {
904
+ async exec(_prepResult, context) {
905
+ const inputArray = context.input || [];
906
+ if (!Array.isArray(inputArray)) throw new Error(`Input for batch-scatter node '${this.nodeId}' must be an array.`);
907
+ const { chunkSize = inputArray.length, workerUsesKey, gatherNodeId } = this.params || {};
908
+ if (!workerUsesKey || !gatherNodeId) throw new Error(`BatchScatterNode requires 'workerUsesKey' and 'gatherNodeId' parameters.`);
909
+ const batchId = globalThis.crypto.randomUUID();
910
+ const currentIndex = await context.context.get(`${this.nodeId}_currentIndex`) || 0;
911
+ const endIndex = Math.min(currentIndex + chunkSize, inputArray.length);
912
+ const dynamicNodes = [];
913
+ const workerIds = [];
914
+ for (let i = currentIndex; i < endIndex; i++) {
915
+ const item = inputArray[i];
916
+ const itemInputKey = `_batch.${this.nodeId}_${batchId}_item_${i}`;
917
+ await context.context.set(itemInputKey, item);
918
+ const workerId = `${workerUsesKey}_${batchId}_${i}`;
919
+ workerIds.push(workerId);
920
+ dynamicNodes.push({
921
+ id: workerId,
922
+ uses: workerUsesKey,
923
+ inputs: itemInputKey
924
+ });
925
+ }
926
+ const parentBatchId = this.nodeId?.replace("_scatter", "") || "";
927
+ await context.dependencies.runtime.services.eventBus.emit({
928
+ type: "batch:start",
929
+ payload: {
930
+ batchId: parentBatchId,
931
+ scatterNodeId: this.nodeId,
932
+ workerNodeIds: workerIds
933
+ }
934
+ });
935
+ await context.context.set(`${this.nodeId}_currentIndex`, endIndex);
936
+ const hasMore = endIndex < inputArray.length;
937
+ await context.context.set(`${gatherNodeId}_hasMore`, hasMore);
938
+ const allWorkerIds = [...await context.context.get(`${gatherNodeId}_allWorkerIds`) || [], ...workerIds];
939
+ await context.context.set(`${gatherNodeId}_allWorkerIds`, allWorkerIds);
940
+ return {
941
+ dynamicNodes,
942
+ output: {
943
+ gatherNodeId,
944
+ hasMore
945
+ }
946
+ };
947
+ }
948
+ };
949
+
950
+ //#endregion
951
+ //#region src/nodes/sleep.ts
952
+ var SleepNode = class extends BaseNode {
953
+ async exec(_prepResult, context) {
954
+ const durationParam = this.params?.duration;
955
+ let durationMs;
956
+ if (typeof durationParam === "string") {
957
+ const match = durationParam.match(/^(\d+)([smhd])$/);
958
+ if (!match) throw new Error(`SleepNode '${this.nodeId}' received an invalid duration string: '${durationParam}'. Expected format: '5m', '10s', '1h', '2d'`);
959
+ const [, numStr, unit] = match;
960
+ const num = parseInt(numStr, 10);
961
+ switch (unit) {
962
+ case "s":
963
+ durationMs = num * 1e3;
964
+ break;
965
+ case "m":
966
+ durationMs = num * 60 * 1e3;
967
+ break;
968
+ case "h":
969
+ durationMs = num * 60 * 60 * 1e3;
970
+ break;
971
+ case "d":
972
+ durationMs = num * 24 * 60 * 60 * 1e3;
973
+ break;
974
+ default: throw new Error(`Invalid duration unit: ${unit}`);
975
+ }
976
+ } else if (typeof durationParam === "number") durationMs = durationParam;
977
+ else throw new Error(`SleepNode '${this.nodeId}' received an invalid duration type: ${typeof durationParam}`);
978
+ if (durationMs < 0) throw new Error(`SleepNode '${this.nodeId}' received a negative duration.`);
979
+ const wakeUpAt = new Date(Date.now() + durationMs).toISOString();
980
+ await context.dependencies.workflowState.markAsAwaiting(this.nodeId ?? "", {
981
+ reason: "timer",
982
+ wakeUpAt
983
+ });
984
+ return { output: void 0 };
985
+ }
986
+ };
987
+
988
+ //#endregion
989
+ //#region src/runtime/traverser.ts
990
+ var GraphTraverser = class GraphTraverser {
991
+ frontier = /* @__PURE__ */ new Set();
992
+ allPredecessors;
993
+ allSuccessors;
994
+ dynamicBlueprint;
995
+ completedNodes = /* @__PURE__ */ new Set();
996
+ nodesInLoops;
997
+ constructor(blueprint, isStrictMode = false) {
998
+ this.dynamicBlueprint = structuredClone(blueprint);
999
+ this.allPredecessors = /* @__PURE__ */ new Map();
1000
+ this.allSuccessors = /* @__PURE__ */ new Map();
1001
+ this.nodesInLoops = /* @__PURE__ */ new Map();
1002
+ for (const node of this.dynamicBlueprint.nodes) {
1003
+ this.allPredecessors.set(node.id, /* @__PURE__ */ new Set());
1004
+ this.allSuccessors.set(node.id, /* @__PURE__ */ new Set());
1005
+ }
1006
+ for (const edge of this.dynamicBlueprint.edges) this.getPredecessors(edge.target).add(edge.source);
1007
+ for (const edge of this.dynamicBlueprint.edges) this.getSuccessors(edge.source).add(edge.target);
1008
+ const analysis = analyzeBlueprint(blueprint);
1009
+ this.filterNodesInLoops(blueprint);
1010
+ this.frontier = new Set(analysis.startNodeIds.filter((id) => !this.isFallbackNode(id)));
1011
+ if (this.frontier.size === 0 && analysis.cycles.length > 0 && !isStrictMode) {
1012
+ const uniqueStartNodes = /* @__PURE__ */ new Set();
1013
+ const cycleEntryPoints = new Set(blueprint.metadata?.cycleEntryPoints || []);
1014
+ for (const cycle of analysis.cycles) if (cycle.length > 0) {
1015
+ const entryPoint = cycle.find((node) => cycleEntryPoints.has(node));
1016
+ uniqueStartNodes.add(entryPoint || cycle[0]);
1017
+ }
1018
+ this.frontier = new Set(uniqueStartNodes);
1019
+ }
1020
+ }
1021
+ /**
1022
+ * Clears all nodes from the execution frontier.
1023
+ */
1024
+ clearFrontier() {
1025
+ this.frontier.clear();
1026
+ }
1027
+ /**
1028
+ * Creates and initializes a GraphTraverser from a saved workflow state.
1029
+ * This is the correct way to prepare a traverser for a `resume` operation.
1030
+ * @param blueprint The workflow blueprint.
1031
+ * @param state The workflow state being resumed.
1032
+ * @returns A configured GraphTraverser instance.
1033
+ */
1034
+ static fromState(blueprint, state) {
1035
+ const traverser = new GraphTraverser(blueprint);
1036
+ traverser.clearFrontier();
1037
+ const completedNodes = state.getCompletedNodes();
1038
+ traverser.completedNodes = new Set(completedNodes);
1039
+ for (const node of traverser.dynamicBlueprint.nodes) {
1040
+ if (traverser.completedNodes.has(node.id)) continue;
1041
+ const requiredPredecessors = traverser.allPredecessors.get(node.id);
1042
+ const joinStrategy = traverser.getJoinStrategy(node.id);
1043
+ if (!requiredPredecessors || requiredPredecessors.size === 0) {
1044
+ traverser.frontier.add(node.id);
1045
+ continue;
1046
+ }
1047
+ const completedPredecessors = [...requiredPredecessors].filter((p) => traverser.completedNodes.has(p));
1048
+ if (joinStrategy === "any" ? completedPredecessors.length > 0 : completedPredecessors.length === requiredPredecessors.size) traverser.frontier.add(node.id);
1049
+ }
1050
+ return traverser;
1051
+ }
1052
+ isFallbackNode(nodeId) {
1053
+ return this.dynamicBlueprint.nodes.some((n) => n.config?.fallback === nodeId);
1054
+ }
1055
+ getJoinStrategy(nodeId) {
1056
+ return this.dynamicBlueprint.nodes.find((n) => n.id === nodeId)?.config?.joinStrategy || "all";
1057
+ }
1058
+ filterNodesInLoops(blueprint) {
1059
+ blueprint.nodes.forEach((node) => {
1060
+ if (node.uses !== "loop-controller") return;
1061
+ const nextInLoopId = blueprint.edges.find((e) => e.source === node.id && e.action === "continue")?.target;
1062
+ if (!nextInLoopId) throw new FlowcraftError(`Loop '${node.id}' has no continue edge to start node. Ensure edges are wired inside the loop and incoming/breaking edges point to the loop controller.`, {
1063
+ nodeId: node.id,
1064
+ blueprintId: blueprint.id
1065
+ });
1066
+ const set = /* @__PURE__ */ new Set();
1067
+ set.add(nextInLoopId);
1068
+ this.nodesInLoops.set(node.id, this.getAllLoopSuccessors(nextInLoopId, blueprint, set));
1069
+ });
1070
+ }
1071
+ getAllLoopSuccessors(nodeId, blueprint, set) {
1072
+ this.getSuccessors(nodeId).forEach((successor) => {
1073
+ if (set.has(successor)) return;
1074
+ const node = this.getNode(successor, blueprint);
1075
+ if (!node || node.uses === "loop-controller") return;
1076
+ set.add(successor);
1077
+ this.getAllLoopSuccessors(successor, blueprint, set);
1078
+ });
1079
+ return set;
1080
+ }
1081
+ getReadyNodes() {
1082
+ const readyNodes = [];
1083
+ for (const nodeId of this.frontier) {
1084
+ const nodeDef = this.dynamicBlueprint.nodes.find((n) => n.id === nodeId);
1085
+ if (nodeDef) readyNodes.push({
1086
+ nodeId,
1087
+ nodeDef
1088
+ });
1089
+ }
1090
+ this.frontier.clear();
1091
+ return readyNodes;
1092
+ }
1093
+ hasMoreWork() {
1094
+ return this.frontier.size > 0;
1095
+ }
1096
+ markNodeCompleted(nodeId, result, nextNodes) {
1097
+ this.completedNodes.add(nodeId);
1098
+ if (result?.dynamicNodes && result.dynamicNodes.length > 0) {
1099
+ const gatherNodeId = result.output?.gatherNodeId;
1100
+ for (const dynamicNode of result.dynamicNodes) {
1101
+ this.dynamicBlueprint.nodes.push(dynamicNode);
1102
+ this.allPredecessors.set(dynamicNode.id, new Set([nodeId]));
1103
+ if (gatherNodeId) this.getPredecessors(gatherNodeId).add(dynamicNode.id);
1104
+ this.frontier.add(dynamicNode.id);
1105
+ }
1106
+ }
1107
+ for (const node of nextNodes) {
1108
+ const joinStrategy = this.getJoinStrategy(node.id);
1109
+ if (joinStrategy !== "any" && this.completedNodes.has(node.id)) continue;
1110
+ const requiredPredecessors = this.getPredecessors(node.id);
1111
+ if (joinStrategy === "any" ? requiredPredecessors.has(nodeId) : [...requiredPredecessors].every((p) => this.completedNodes.has(p))) {
1112
+ this.frontier.add(node.id);
1113
+ if (node.uses === "loop-controller") this.getNodesInLoop(node.id).forEach((id) => {
1114
+ this.resetNodeCompletion(id);
1115
+ });
1116
+ }
1117
+ }
1118
+ if (nextNodes.length === 0) {
1119
+ for (const [potentialNextId, predecessors] of this.allPredecessors) if (predecessors.has(nodeId) && !this.completedNodes.has(potentialNextId)) {
1120
+ if (this.getJoinStrategy(potentialNextId) === "any" ? predecessors.has(nodeId) : [...predecessors].every((p) => this.completedNodes.has(p))) {
1121
+ this.frontier.add(potentialNextId);
1122
+ const node = this.getNode(potentialNextId, this.dynamicBlueprint);
1123
+ if (!node) continue;
1124
+ if (node.uses === "loop-controller") this.getNodesInLoop(node.id).forEach((id) => {
1125
+ this.resetNodeCompletion(id);
1126
+ });
1127
+ }
1128
+ }
1129
+ }
1130
+ }
1131
+ getAllNodeIds() {
1132
+ return new Set(this.dynamicBlueprint.nodes.map((n) => n.id));
1133
+ }
1134
+ getFallbackNodeIds() {
1135
+ const fallbackNodeIds = /* @__PURE__ */ new Set();
1136
+ for (const node of this.dynamicBlueprint.nodes) if (node.config?.fallback) fallbackNodeIds.add(node.config.fallback);
1137
+ return fallbackNodeIds;
1138
+ }
1139
+ getCompletedNodes() {
1140
+ return new Set(this.completedNodes);
1141
+ }
1142
+ getDynamicBlueprint() {
1143
+ return this.dynamicBlueprint;
1144
+ }
1145
+ getAllPredecessors() {
1146
+ return this.allPredecessors;
1147
+ }
1148
+ getAllSuccessors() {
1149
+ return this.allSuccessors;
1150
+ }
1151
+ getPredecessors(nodeId) {
1152
+ const predecessors = this.allPredecessors.get(nodeId);
1153
+ if (!predecessors) return /* @__PURE__ */ new Set();
1154
+ return predecessors;
1155
+ }
1156
+ getSuccessors(nodeId) {
1157
+ const successors = this.allSuccessors.get(nodeId);
1158
+ if (!successors) return /* @__PURE__ */ new Set();
1159
+ return successors;
1160
+ }
1161
+ getNodesInLoop(id) {
1162
+ const loopNodes = this.nodesInLoops.get(id);
1163
+ if (!loopNodes) return /* @__PURE__ */ new Set();
1164
+ return loopNodes;
1165
+ }
1166
+ resetNodeCompletion(nodeId) {
1167
+ this.completedNodes.delete(nodeId);
1168
+ }
1169
+ getNode(nodeId, blueprint) {
1170
+ return blueprint.nodes.find((n) => n.id === nodeId);
1171
+ }
1172
+ addDynamicNode(_nodeId, dynamicNode, predecessorId, gatherNodeId) {
1173
+ this.dynamicBlueprint.nodes.push(dynamicNode);
1174
+ this.allPredecessors.set(dynamicNode.id, new Set([predecessorId]));
1175
+ if (gatherNodeId) this.allPredecessors.get(gatherNodeId)?.add(dynamicNode.id);
1176
+ this.frontier.add(dynamicNode.id);
1177
+ }
1178
+ /**
1179
+ * Manually adds a node ID back to the execution frontier.
1180
+ * Used by orchestrators that need fine-grained control over steps.
1181
+ * @param nodeId The ID of the node to add to the frontier.
1182
+ */
1183
+ addToFrontier(nodeId) {
1184
+ this.frontier.add(nodeId);
1185
+ }
1186
+ };
1187
+
1188
+ //#endregion
1189
+ //#region src/nodes/subflow.ts
1190
+ var SubflowNode = class extends BaseNode {
1191
+ async exec(_prepResult, context) {
1192
+ const { blueprintId, inputs, outputs } = this.params ?? {};
1193
+ const { runtime, workflowState } = context.dependencies;
1194
+ if (!blueprintId) throw new FlowcraftError(`Subflow node '${this.nodeId}' is missing 'blueprintId' parameter.`, { isFatal: true });
1195
+ const subBlueprint = runtime.blueprints?.[blueprintId] || runtime.runtime?.blueprints?.[blueprintId];
1196
+ if (!subBlueprint) throw new FlowcraftError(`Sub-blueprint '${blueprintId}' not found in runtime registry.`, { isFatal: true });
1197
+ const subflowInitialContext = {};
1198
+ if (inputs) for (const [targetKey, sourceKey] of Object.entries(inputs)) {
1199
+ let value = await context.context.get(sourceKey);
1200
+ if (value === void 0) value = await context.context.get(`_outputs.${sourceKey}`);
1201
+ subflowInitialContext[targetKey] = value;
1202
+ }
1203
+ else if (context.input !== void 0) {
1204
+ const subAnalysis = analyzeBlueprint(subBlueprint);
1205
+ for (const startNodeId of subAnalysis.startNodeIds) {
1206
+ const inputKey = `_inputs.${startNodeId}`;
1207
+ subflowInitialContext[inputKey] = context.input;
1208
+ }
1209
+ }
1210
+ const subflowExecContext = new ExecutionContext(subBlueprint, new WorkflowState(subflowInitialContext), runtime.nodeRegistry, runtime.executionId, runtime.runtime, runtime.services, runtime.signal, runtime.concurrency);
1211
+ const subflowTraverser = new GraphTraverser(subBlueprint);
1212
+ const subflowResult = await runtime.runtime.orchestrator.run(subflowExecContext, subflowTraverser);
1213
+ if (subflowResult.status === "awaiting") {
1214
+ await workflowState.markAsAwaiting(this.nodeId ?? "");
1215
+ const subflowStateKey = `_subflowState.${this.nodeId}`;
1216
+ await context.context.set(subflowStateKey, subflowResult.serializedContext);
1217
+ return { output: void 0 };
1218
+ }
1219
+ if (subflowResult.status !== "completed") {
1220
+ const firstError = subflowResult.errors?.[0];
1221
+ const errorMessage = firstError?.message || "Unknown error";
1222
+ throw new FlowcraftError(`Sub-workflow '${blueprintId}' did not complete successfully. Status: ${subflowResult.status}. Error: ${errorMessage}`, {
1223
+ cause: firstError,
1224
+ nodeId: this.nodeId,
1225
+ blueprintId
1226
+ });
1227
+ }
1228
+ const subflowFinalContext = subflowResult.context;
1229
+ if (outputs) {
1230
+ for (const [parentKey, subKey] of Object.entries(outputs)) {
1231
+ const value = subflowFinalContext[`_outputs.${subKey}`] ?? subflowFinalContext[subKey];
1232
+ await context.context.set(parentKey, value);
1233
+ }
1234
+ return { output: subflowFinalContext };
1235
+ }
1236
+ const subAnalysis = analyzeBlueprint(subBlueprint);
1237
+ if (subAnalysis.terminalNodeIds.length === 1) return { output: subflowFinalContext[`_outputs.${subAnalysis.terminalNodeIds[0]}`] };
1238
+ const terminalOutputs = {};
1239
+ for (const terminalId of subAnalysis.terminalNodeIds) terminalOutputs[terminalId] = subflowFinalContext[`_outputs.${terminalId}`];
1240
+ return { output: terminalOutputs };
1241
+ }
1242
+ };
1243
+
1244
+ //#endregion
1245
+ //#region src/nodes/wait.ts
1246
+ var WaitNode = class extends BaseNode {
1247
+ async exec(_prepResult, context) {
1248
+ await context.dependencies.workflowState.markAsAwaiting(this.nodeId ?? "", { reason: "external_event" });
1249
+ return { output: void 0 };
1250
+ }
1251
+ };
1252
+
1253
+ //#endregion
1254
+ //#region src/nodes/webhook.ts
1255
+ var WebhookNode = class extends BaseNode {
1256
+ async prep(context) {
1257
+ const runId = context.dependencies.runtime.executionId;
1258
+ const nodeId = this.nodeId ?? "";
1259
+ const { url, event } = await context.dependencies.adapter.registerWebhookEndpoint(runId, nodeId);
1260
+ return {
1261
+ url,
1262
+ event
1263
+ };
1264
+ }
1265
+ async exec(prepResult, _context) {
1266
+ return { output: {
1267
+ url: prepResult.url,
1268
+ event: prepResult.event,
1269
+ request: new Promise(() => {})
1270
+ } };
1271
+ }
1272
+ };
1273
+
1274
+ //#endregion
1275
+ //#region src/sanitizer.ts
1276
+ /**
1277
+ * Sanitizes a raw workflow blueprint by removing extra properties
1278
+ * added by UI tools (e.g., position, style) and keeping only the
1279
+ * properties defined in NodeDefinition and EdgeDefinition.
1280
+ */
1281
+ function sanitizeBlueprint(raw) {
1282
+ let nodesArray = [];
1283
+ if (Array.isArray(raw.nodes)) nodesArray = raw.nodes;
1284
+ else if (typeof raw.nodes === "object" && raw.nodes !== null) nodesArray = Object.values(raw.nodes);
1285
+ const nodes = nodesArray.map((node) => ({
1286
+ id: node.id,
1287
+ uses: node.uses,
1288
+ params: node.params,
1289
+ inputs: node.inputs,
1290
+ config: node.config
1291
+ }));
1292
+ let edgesArray = [];
1293
+ if (Array.isArray(raw.edges)) edgesArray = raw.edges;
1294
+ else if (typeof raw.edges === "object" && raw.edges !== null) edgesArray = Object.values(raw.edges);
1295
+ const edges = edgesArray.map((edge) => ({
1296
+ source: edge.source,
1297
+ target: edge.target,
1298
+ action: edge.action,
1299
+ condition: edge.condition,
1300
+ transform: edge.transform
1301
+ }));
1302
+ return {
1303
+ id: raw.id,
1304
+ nodes,
1305
+ edges,
1306
+ metadata: raw.metadata
1307
+ };
1308
+ }
1309
+
1310
+ //#endregion
1311
+ //#region src/runtime/executors.ts
1312
+ async function withRetries(executor, maxRetries, nodeDef, context, executionId, signal, eventBus) {
1313
+ let lastError;
1314
+ for (let attempt = 1; attempt <= maxRetries; attempt++) try {
1315
+ signal?.throwIfAborted();
1316
+ const result = await executor();
1317
+ if (attempt > 1) context.dependencies.logger.info(`Node execution succeeded after retry`, {
1318
+ nodeId: nodeDef.id,
1319
+ attempt,
1320
+ executionId
1321
+ });
1322
+ return result;
1323
+ } catch (error) {
1324
+ lastError = error;
1325
+ if (error instanceof DOMException && error.name === "AbortError") throw new FlowcraftError("Workflow cancelled", { isFatal: false });
1326
+ if (error instanceof FlowcraftError && error.isFatal) break;
1327
+ if (attempt < maxRetries) {
1328
+ context.dependencies.logger.warn(`Node execution failed, retrying`, {
1329
+ nodeId: nodeDef.id,
1330
+ attempt,
1331
+ maxRetries,
1332
+ error: error instanceof Error ? error.message : String(error),
1333
+ executionId
1334
+ });
1335
+ if (eventBus) await eventBus.emit({
1336
+ type: "node:retry",
1337
+ payload: {
1338
+ nodeId: nodeDef.id,
1339
+ attempt,
1340
+ executionId: executionId || "",
1341
+ blueprintId: context.dependencies.blueprint?.id || ""
1342
+ }
1343
+ });
1344
+ } else context.dependencies.logger.error(`Node execution failed after all retries`, {
1345
+ nodeId: nodeDef.id,
1346
+ attempts: maxRetries,
1347
+ error: error instanceof Error ? error.message : String(error),
1348
+ executionId
1349
+ });
1350
+ }
1351
+ throw lastError;
1352
+ }
1353
+ var FunctionNodeExecutor = class {
1354
+ constructor(implementation, maxRetries, eventBus) {
1355
+ this.implementation = implementation;
1356
+ this.maxRetries = maxRetries;
1357
+ this.eventBus = eventBus;
1358
+ }
1359
+ async execute(nodeDef, context, executionId, signal) {
1360
+ return withRetries(() => this.implementation(context), this.maxRetries, nodeDef, context, executionId, signal, this.eventBus);
1361
+ }
1362
+ };
1363
+ var ClassNodeExecutor = class {
1364
+ constructor(implementation, maxRetries, eventBus) {
1365
+ this.implementation = implementation;
1366
+ this.maxRetries = maxRetries;
1367
+ this.eventBus = eventBus;
1368
+ }
1369
+ async execute(nodeDef, context, executionId, signal) {
1370
+ const instance = new this.implementation(nodeDef.params || {}, nodeDef.id);
1371
+ let lastError;
1372
+ try {
1373
+ signal?.throwIfAborted();
1374
+ const prepResult = await instance.prep(context);
1375
+ let execResult;
1376
+ try {
1377
+ execResult = await withRetries(() => instance.exec(prepResult, context), this.maxRetries, nodeDef, context, executionId, signal, this.eventBus);
1378
+ } catch (error) {
1379
+ lastError = error instanceof Error ? error : new Error(String(error));
1380
+ if (error instanceof DOMException && error.name === "AbortError") throw new FlowcraftError("Workflow cancelled", { isFatal: false });
1381
+ if (error instanceof FlowcraftError && error.isFatal) throw error;
1382
+ }
1383
+ if (lastError) {
1384
+ signal?.throwIfAborted();
1385
+ execResult = await instance.fallback(lastError, context);
1386
+ }
1387
+ signal?.throwIfAborted();
1388
+ if (!execResult) throw new Error("Execution failed after all retries");
1389
+ return await instance.post(execResult, context);
1390
+ } catch (error) {
1391
+ lastError = error instanceof Error ? error : new Error(String(error));
1392
+ if (error instanceof DOMException && error.name === "AbortError") throw new FlowcraftError("Workflow cancelled", { isFatal: false });
1393
+ throw error;
1394
+ } finally {
1395
+ if (lastError) try {
1396
+ await instance.recover(lastError, context);
1397
+ } catch (recoverError) {
1398
+ context.dependencies.logger.warn(`Recover phase failed`, {
1399
+ nodeId: nodeDef.id,
1400
+ originalError: lastError.message,
1401
+ recoverError: recoverError instanceof Error ? recoverError.message : String(recoverError),
1402
+ executionId
1403
+ });
1404
+ }
1405
+ }
1406
+ }
1407
+ };
1408
+ var NodeExecutor = class {
1409
+ context;
1410
+ nodeDef;
1411
+ strategy;
1412
+ constructor(config) {
1413
+ this.context = config.context;
1414
+ this.nodeDef = config.nodeDef;
1415
+ this.strategy = config.strategy;
1416
+ }
1417
+ async execute(input) {
1418
+ const nodeContext = {
1419
+ context: this.context.state.getContext(),
1420
+ input,
1421
+ params: this.nodeDef.params || {},
1422
+ dependencies: {
1423
+ ...this.context.services.dependencies,
1424
+ logger: this.context.services.logger,
1425
+ runtime: this.context,
1426
+ workflowState: this.context.state
1427
+ },
1428
+ signal: this.context.signal
1429
+ };
1430
+ const beforeHooks = this.context.services.middleware.map((m) => m.beforeNode).filter((hook) => !!hook);
1431
+ const afterHooks = this.context.services.middleware.map((m) => m.afterNode).filter((hook) => !!hook);
1432
+ const aroundHooks = this.context.services.middleware.map((m) => m.aroundNode).filter((hook) => !!hook);
1433
+ const coreExecution = async () => {
1434
+ let result;
1435
+ let error;
1436
+ try {
1437
+ for (const hook of beforeHooks) await hook(nodeContext.context, this.nodeDef.id);
1438
+ result = await this.strategy.execute(this.nodeDef, nodeContext, this.context.executionId, this.context.signal);
1439
+ return {
1440
+ status: "success",
1441
+ result
1442
+ };
1443
+ } catch (e) {
1444
+ error = e instanceof Error ? e : new Error(String(e));
1445
+ const flowcraftError = error instanceof FlowcraftError ? error : new FlowcraftError(`Node '${this.nodeDef.id}' execution failed`, {
1446
+ cause: error,
1447
+ nodeId: this.nodeDef.id,
1448
+ blueprintId: this.context.blueprint.id,
1449
+ executionId: this.context.executionId,
1450
+ isFatal: false
1451
+ });
1452
+ const fallbackNodeId = this.nodeDef.config?.fallback;
1453
+ if (fallbackNodeId && !flowcraftError.isFatal) {
1454
+ this.context.services.logger.warn(`Node failed, fallback required`, {
1455
+ nodeId: this.nodeDef.id,
1456
+ fallbackNodeId,
1457
+ error: error.message,
1458
+ executionId: this.context.executionId
1459
+ });
1460
+ await this.context.services.eventBus.emit({
1461
+ type: "node:fallback",
1462
+ payload: {
1463
+ nodeId: this.nodeDef.id,
1464
+ executionId: this.context.executionId || "",
1465
+ fallback: fallbackNodeId,
1466
+ blueprintId: this.context.blueprint.id
1467
+ }
1468
+ });
1469
+ return {
1470
+ status: "failed_with_fallback",
1471
+ fallbackNodeId,
1472
+ error: flowcraftError
1473
+ };
1474
+ }
1475
+ return {
1476
+ status: "failed",
1477
+ error: flowcraftError
1478
+ };
1479
+ } finally {
1480
+ for (const hook of afterHooks) await hook(nodeContext.context, this.nodeDef.id, result, error);
1481
+ }
1482
+ };
1483
+ let executionChain = coreExecution;
1484
+ for (let i = aroundHooks.length - 1; i >= 0; i--) {
1485
+ const hook = aroundHooks[i];
1486
+ const next = executionChain;
1487
+ executionChain = async () => {
1488
+ let capturedResult;
1489
+ const middlewareResult = await hook(nodeContext.context, this.nodeDef.id, async () => {
1490
+ capturedResult = await next();
1491
+ if (capturedResult.status === "success") return capturedResult.result;
1492
+ throw capturedResult.error;
1493
+ });
1494
+ if (!capturedResult && middlewareResult) return {
1495
+ status: "success",
1496
+ result: middlewareResult
1497
+ };
1498
+ if (!capturedResult) throw new Error("Middleware did not call next() and did not return a result");
1499
+ return capturedResult;
1500
+ };
1501
+ }
1502
+ try {
1503
+ await this.context.services.eventBus.emit({
1504
+ type: "node:start",
1505
+ payload: {
1506
+ nodeId: this.nodeDef.id,
1507
+ executionId: this.context.executionId || "",
1508
+ input: nodeContext.input,
1509
+ blueprintId: this.context.blueprint.id
1510
+ }
1511
+ });
1512
+ const executionResult = await executionChain();
1513
+ if (executionResult.status === "success") await this.context.services.eventBus.emit({
1514
+ type: "node:finish",
1515
+ payload: {
1516
+ nodeId: this.nodeDef.id,
1517
+ result: executionResult.result,
1518
+ executionId: this.context.executionId || "",
1519
+ blueprintId: this.context.blueprint.id
1520
+ }
1521
+ });
1522
+ else await this.context.services.eventBus.emit({
1523
+ type: "node:error",
1524
+ payload: {
1525
+ nodeId: this.nodeDef.id,
1526
+ error: executionResult.error,
1527
+ executionId: this.context.executionId || "",
1528
+ blueprintId: this.context.blueprint.id
1529
+ }
1530
+ });
1531
+ return executionResult;
1532
+ } catch (error) {
1533
+ const err = error instanceof Error ? error : new Error(String(error));
1534
+ const flowcraftError = err instanceof FlowcraftError ? err : new FlowcraftError(`Node '${this.nodeDef.id}' failed execution.`, {
1535
+ cause: err,
1536
+ nodeId: this.nodeDef.id,
1537
+ blueprintId: this.context.blueprint.id,
1538
+ executionId: this.context.executionId,
1539
+ isFatal: false
1540
+ });
1541
+ await this.context.services.eventBus.emit({
1542
+ type: "node:error",
1543
+ payload: {
1544
+ nodeId: this.nodeDef.id,
1545
+ error: flowcraftError,
1546
+ executionId: this.context.executionId || "",
1547
+ blueprintId: this.context.blueprint.id
1548
+ }
1549
+ });
1550
+ if (error instanceof DOMException && error.name === "AbortError") throw new FlowcraftError("Workflow cancelled", {
1551
+ executionId: this.context.executionId,
1552
+ isFatal: false
1553
+ });
1554
+ throw error instanceof FlowcraftError && !error.isFatal ? error : new FlowcraftError(`Node '${this.nodeDef.id}' failed execution.`, {
1555
+ cause: error,
1556
+ nodeId: this.nodeDef.id,
1557
+ blueprintId: this.context.blueprint.id,
1558
+ executionId: this.context.executionId,
1559
+ isFatal: false
1560
+ });
1561
+ }
1562
+ }
1563
+ };
1564
+
1565
+ //#endregion
1566
+ //#region src/runtime/node-executor-factory.ts
1567
+ var NodeExecutorFactory = class {
1568
+ constructor(eventBus) {
1569
+ this.eventBus = eventBus;
1570
+ }
1571
+ createExecutorForNode(nodeId, context) {
1572
+ const nodeDef = context.blueprint.nodes.find((n) => n.id === nodeId);
1573
+ if (!nodeDef) throw new FlowcraftError(`Node '${nodeId}' not found in blueprint.`, {
1574
+ nodeId,
1575
+ blueprintId: context.blueprint.id,
1576
+ executionId: context.executionId,
1577
+ isFatal: false
1578
+ });
1579
+ return new NodeExecutor({
1580
+ context,
1581
+ nodeDef,
1582
+ strategy: this.getExecutionStrategy(nodeDef, context.nodeRegistry)
1583
+ });
1584
+ }
1585
+ getExecutionStrategy(nodeDef, nodeRegistry) {
1586
+ const implementation = nodeRegistry.get(nodeDef.uses);
1587
+ if (!implementation) throw new FlowcraftError(`Implementation for '${nodeDef.uses}' not found.`, {
1588
+ nodeId: nodeDef.id,
1589
+ blueprintId: "",
1590
+ isFatal: true
1591
+ });
1592
+ const maxRetries = nodeDef.config?.maxRetries ?? 1;
1593
+ return isNodeClass(implementation) ? new ClassNodeExecutor(implementation, maxRetries, this.eventBus) : new FunctionNodeExecutor(implementation, maxRetries, this.eventBus);
1594
+ }
1595
+ };
1596
+
1597
+ //#endregion
1598
+ //#region src/runtime/scheduler.ts
1599
+ var WorkflowScheduler = class {
1600
+ runtime;
1601
+ activeWorkflows = /* @__PURE__ */ new Map();
1602
+ intervalId;
1603
+ checkIntervalMs;
1604
+ constructor(runtime, checkIntervalMs = 1e3) {
1605
+ this.runtime = runtime;
1606
+ this.checkIntervalMs = checkIntervalMs;
1607
+ }
1608
+ start() {
1609
+ if (this.intervalId) return;
1610
+ this.intervalId = setInterval(() => {
1611
+ this.checkAndResumeWorkflows();
1612
+ }, this.checkIntervalMs);
1613
+ }
1614
+ stop() {
1615
+ if (this.intervalId) {
1616
+ clearInterval(this.intervalId);
1617
+ this.intervalId = void 0;
1618
+ }
1619
+ }
1620
+ registerAwaitingWorkflow(executionId, blueprintId, serializedContext, awaitingNodeId, wakeUpAt) {
1621
+ this.activeWorkflows.set(executionId, {
1622
+ executionId,
1623
+ blueprintId,
1624
+ serializedContext,
1625
+ awaitingNodeId,
1626
+ wakeUpAt
1627
+ });
1628
+ }
1629
+ unregisterWorkflow(executionId) {
1630
+ this.activeWorkflows.delete(executionId);
1631
+ }
1632
+ async checkAndResumeWorkflows() {
1633
+ const now = /* @__PURE__ */ new Date();
1634
+ const toResume = [];
1635
+ for (const [_executionId, workflow] of this.activeWorkflows) if (new Date(workflow.wakeUpAt) <= now) toResume.push(workflow);
1636
+ for (const workflow of toResume) try {
1637
+ const blueprint = this.runtime.getBlueprint(workflow.blueprintId);
1638
+ if (!blueprint) {
1639
+ console.warn(`Blueprint ${workflow.blueprintId} not found, skipping resumption`);
1640
+ continue;
1641
+ }
1642
+ const result = await this.runtime.resume(blueprint, workflow.serializedContext, { output: void 0 }, workflow.awaitingNodeId);
1643
+ if (result.status === "completed" || result.status === "failed") this.unregisterWorkflow(workflow.executionId);
1644
+ } catch (error) {
1645
+ console.error(`Failed to resume workflow ${workflow.executionId}:`, error);
1646
+ this.unregisterWorkflow(workflow.executionId);
1647
+ }
1648
+ }
1649
+ getActiveWorkflows() {
1650
+ return Array.from(this.activeWorkflows.values());
1651
+ }
1652
+ };
1653
+
1654
+ //#endregion
1655
+ //#region src/runtime/workflow-logic-handler.ts
1656
+ var WorkflowLogicHandler = class {
1657
+ constructor(evaluator, eventBus) {
1658
+ this.evaluator = evaluator;
1659
+ this.eventBus = eventBus;
1660
+ }
1661
+ async determineNextNodes(blueprint, completedNodeId, result, context, executionId) {
1662
+ if (!result) return [];
1663
+ const effectiveSourceNodeId = completedNodeId;
1664
+ const directOutgoingEdges = blueprint.edges.filter((edge) => edge.source === effectiveSourceNodeId);
1665
+ const inheritedOutgoingEdges = blueprint.nodes.filter((n) => n.config?.fallback === completedNodeId).flatMap((originalNode) => blueprint.edges.filter((edge) => edge.source === originalNode.id));
1666
+ const allPossibleEdges = [...directOutgoingEdges, ...inheritedOutgoingEdges];
1667
+ const outgoingEdges = [...new Map(allPossibleEdges.map((edge) => [`${edge.source}-${edge.target}-${edge.action || ""}-${edge.condition || ""}`, edge])).values()];
1668
+ const matched = [];
1669
+ const evaluateEdge = async (edge) => {
1670
+ if (!edge.condition) return true;
1671
+ const contextData = context.type === "sync" ? context.toJSON() : await context.toJSON();
1672
+ const evaluationResult = !!this.evaluator.evaluate(edge.condition, {
1673
+ ...contextData,
1674
+ result
1675
+ });
1676
+ await this.eventBus.emit({
1677
+ type: "edge:evaluate",
1678
+ payload: {
1679
+ source: edge.source,
1680
+ target: edge.target,
1681
+ condition: edge.condition,
1682
+ result: evaluationResult
1683
+ }
1684
+ });
1685
+ return evaluationResult;
1686
+ };
1687
+ if (blueprint.nodes.find((n) => n.id === completedNodeId)?.uses === "loop-controller") {
1688
+ const conditionalEdges = outgoingEdges.filter((edge) => edge.condition);
1689
+ for (const edge of conditionalEdges) if (await evaluateEdge(edge)) {
1690
+ const targetNode = blueprint.nodes.find((n) => n.id === edge.target);
1691
+ if (targetNode) matched.push({
1692
+ node: targetNode,
1693
+ edge
1694
+ });
1695
+ } else await this.eventBus.emit({
1696
+ type: "node:skipped",
1697
+ payload: {
1698
+ nodeId: completedNodeId,
1699
+ edge,
1700
+ executionId: executionId || "",
1701
+ blueprintId: blueprint.id
1702
+ }
1703
+ });
1704
+ if (matched.length > 0) return matched;
1705
+ }
1706
+ if (result.action) {
1707
+ const actionEdges = outgoingEdges.filter((edge) => edge.action === result.action);
1708
+ for (const edge of actionEdges) if (await evaluateEdge(edge)) {
1709
+ const targetNode = blueprint.nodes.find((n) => n.id === edge.target);
1710
+ if (targetNode) matched.push({
1711
+ node: targetNode,
1712
+ edge
1713
+ });
1714
+ } else await this.eventBus.emit({
1715
+ type: "node:skipped",
1716
+ payload: {
1717
+ nodeId: completedNodeId,
1718
+ edge,
1719
+ executionId: executionId || "",
1720
+ blueprintId: blueprint.id
1721
+ }
1722
+ });
1723
+ }
1724
+ if (matched.length === 0) {
1725
+ const defaultEdges = outgoingEdges.filter((edge) => !edge.action);
1726
+ for (const edge of defaultEdges) if (await evaluateEdge(edge)) {
1727
+ const targetNode = blueprint.nodes.find((n) => n.id === edge.target);
1728
+ if (targetNode) matched.push({
1729
+ node: targetNode,
1730
+ edge
1731
+ });
1732
+ } else await this.eventBus.emit({
1733
+ type: "node:skipped",
1734
+ payload: {
1735
+ nodeId: completedNodeId,
1736
+ edge,
1737
+ executionId: executionId || "",
1738
+ blueprintId: blueprint.id
1739
+ }
1740
+ });
1741
+ }
1742
+ return matched;
1743
+ }
1744
+ async applyEdgeTransform(edge, sourceResult, targetNode, context, allPredecessors, executionId) {
1745
+ const asyncContext = context.type === "sync" ? new AsyncContextView(context) : context;
1746
+ const predecessors = allPredecessors?.get(targetNode.id);
1747
+ const hasSinglePredecessor = predecessors && predecessors.size === 1;
1748
+ const hasExplicitInputs = targetNode.inputs !== void 0;
1749
+ const hasEdgeTransform = edge.transform !== void 0;
1750
+ if (!hasExplicitInputs && !hasSinglePredecessor && !hasEdgeTransform) return;
1751
+ const finalInput = edge.transform ? this.evaluator.evaluate(edge.transform, {
1752
+ input: sourceResult.output,
1753
+ context: await asyncContext.toJSON()
1754
+ }) : sourceResult.output;
1755
+ const inputKey = `_inputs.${targetNode.id}`;
1756
+ await asyncContext.set(inputKey, finalInput);
1757
+ await this.eventBus.emit({
1758
+ type: "context:change",
1759
+ payload: {
1760
+ sourceNode: edge.source,
1761
+ key: inputKey,
1762
+ op: "set",
1763
+ value: finalInput,
1764
+ executionId: executionId || "unknown"
1765
+ }
1766
+ });
1767
+ if (!hasExplicitInputs) targetNode.inputs = inputKey;
1768
+ }
1769
+ async resolveNodeInput(nodeId, blueprint, context) {
1770
+ const nodeDef = blueprint.nodes.find((n) => n.id === nodeId);
1771
+ if (!nodeDef) throw new FlowcraftError(`Node '${nodeId}' not found in blueprint.`, {
1772
+ nodeId,
1773
+ blueprintId: blueprint.id,
1774
+ isFatal: false
1775
+ });
1776
+ const asyncContext = context.type === "sync" ? new AsyncContextView(context) : context;
1777
+ if (nodeDef.inputs) {
1778
+ if (typeof nodeDef.inputs === "string") {
1779
+ const key = nodeDef.inputs;
1780
+ if (key.startsWith("_")) return await asyncContext.get(key);
1781
+ const outputKey = `_outputs.${key}`;
1782
+ if (await asyncContext.has(outputKey)) return await asyncContext.get(outputKey);
1783
+ return await asyncContext.get(key);
1784
+ }
1785
+ if (typeof nodeDef.inputs === "object") {
1786
+ const input = {};
1787
+ for (const key in nodeDef.inputs) {
1788
+ const contextKey = nodeDef.inputs[key];
1789
+ if (contextKey.startsWith("_")) input[key] = await asyncContext.get(contextKey);
1790
+ else {
1791
+ const outputKey = `_outputs.${contextKey}`;
1792
+ if (await asyncContext.has(outputKey)) input[key] = await asyncContext.get(outputKey);
1793
+ else input[key] = await asyncContext.get(contextKey);
1794
+ }
1795
+ }
1796
+ return input;
1797
+ }
1798
+ }
1799
+ const inputKey = `_inputs.${nodeDef.id}`;
1800
+ return await asyncContext.get(inputKey);
1801
+ }
1802
+ };
1803
+
1804
+ //#endregion
1805
+ //#region src/runtime/runtime.ts
1806
+ var FlowRuntime = class {
1807
+ container;
1808
+ registry;
1809
+ blueprints;
1810
+ dependencies;
1811
+ logger;
1812
+ eventBus;
1813
+ serializer;
1814
+ middleware;
1815
+ evaluator;
1816
+ analysisCache;
1817
+ orchestrator;
1818
+ options;
1819
+ logicHandler;
1820
+ executorFactory;
1821
+ scheduler;
1822
+ getBlueprint(id) {
1823
+ return this.blueprints[id];
1824
+ }
1825
+ constructor(containerOrOptions, legacyOptions) {
1826
+ let userRegistry;
1827
+ if (containerOrOptions instanceof DIContainer) {
1828
+ this.container = containerOrOptions;
1829
+ this.logger = this.container.resolve(ServiceTokens.Logger);
1830
+ this.serializer = this.container.resolve(ServiceTokens.Serializer);
1831
+ this.evaluator = this.container.resolve(ServiceTokens.Evaluator);
1832
+ this.eventBus = this.container.resolve(ServiceTokens.EventBus) || { emit: async () => {} };
1833
+ this.middleware = this.container.resolve(ServiceTokens.Middleware) || [];
1834
+ userRegistry = this.container.resolve(ServiceTokens.NodeRegistry);
1835
+ this.blueprints = this.container.resolve(ServiceTokens.BlueprintRegistry);
1836
+ this.dependencies = this.container.resolve(ServiceTokens.Dependencies);
1837
+ this.options = legacyOptions || {};
1838
+ this.orchestrator = this.container.resolve(ServiceTokens.Orchestrator);
1839
+ this.scheduler = new WorkflowScheduler(this);
1840
+ } else {
1841
+ const options = containerOrOptions;
1842
+ this.logger = options.logger || new NullLogger();
1843
+ this.serializer = options.serializer || new JsonSerializer();
1844
+ this.evaluator = options.evaluator || new PropertyEvaluator();
1845
+ this.eventBus = options.eventBus || { emit: async () => {} };
1846
+ this.middleware = options.middleware || [];
1847
+ userRegistry = options.registry || {};
1848
+ this.blueprints = options.blueprints || {};
1849
+ this.scheduler = new WorkflowScheduler(this);
1850
+ this.dependencies = options.dependencies || {};
1851
+ this.options = options;
1852
+ this.container = null;
1853
+ }
1854
+ const loopControllerFunction = async (context) => {
1855
+ const condition = context.params.condition;
1856
+ const contextData = await context.context.toJSON();
1857
+ if (this.evaluator.evaluate(condition, contextData)) return { action: "continue" };
1858
+ else return {
1859
+ action: "break",
1860
+ output: null
1861
+ };
1862
+ };
1863
+ this.registry = new Map(Object.entries({
1864
+ wait: WaitNode,
1865
+ sleep: SleepNode,
1866
+ webhook: WebhookNode,
1867
+ subflow: SubflowNode,
1868
+ "batch-scatter": BatchScatterNode,
1869
+ "batch-gather": BatchGatherNode,
1870
+ "loop-controller": loopControllerFunction,
1871
+ ...userRegistry
1872
+ }));
1873
+ this.orchestrator = this.container?.has(ServiceTokens.Orchestrator) ? this.container.resolve(ServiceTokens.Orchestrator) : new DefaultOrchestrator();
1874
+ this.analysisCache = /* @__PURE__ */ new WeakMap();
1875
+ this.logicHandler = new WorkflowLogicHandler(this.evaluator, this.eventBus);
1876
+ this.executorFactory = new NodeExecutorFactory(this.eventBus);
1877
+ }
1878
+ _setupExecutionContext(blueprint, initialState, options) {
1879
+ const executionId = globalThis.crypto?.randomUUID();
1880
+ const contextData = typeof initialState === "string" ? this.serializer.deserialize(initialState) : initialState;
1881
+ blueprint = sanitizeBlueprint(blueprint);
1882
+ const state = new WorkflowState(contextData);
1883
+ const nodeRegistry = this._createExecutionRegistry(options?.functionRegistry);
1884
+ return new ExecutionContext(blueprint, state, nodeRegistry, executionId, this, {
1885
+ logger: this.logger,
1886
+ eventBus: this.eventBus,
1887
+ serializer: this.serializer,
1888
+ evaluator: this.evaluator,
1889
+ middleware: this.middleware,
1890
+ dependencies: this.dependencies
1891
+ }, options?.signal, options?.concurrency);
1892
+ }
1893
+ async run(blueprint, initialState = {}, options) {
1894
+ const startTime = Date.now();
1895
+ const executionContext = this._setupExecutionContext(blueprint, initialState, options);
1896
+ this.logger.info(`Starting workflow execution`, {
1897
+ blueprintId: executionContext.blueprint.id,
1898
+ executionId: executionContext.executionId
1899
+ });
1900
+ try {
1901
+ await this.eventBus.emit({
1902
+ type: "workflow:start",
1903
+ payload: {
1904
+ blueprintId: executionContext.blueprint.id,
1905
+ executionId: executionContext.executionId
1906
+ }
1907
+ });
1908
+ await this.eventBus.emit({
1909
+ type: "workflow:resume",
1910
+ payload: {
1911
+ blueprintId: executionContext.blueprint.id,
1912
+ executionId: executionContext.executionId
1913
+ }
1914
+ });
1915
+ const analysis = this.analysisCache.get(executionContext.blueprint) ?? (() => {
1916
+ const computed = analyzeBlueprint(executionContext.blueprint);
1917
+ this.analysisCache.set(executionContext.blueprint, computed);
1918
+ return computed;
1919
+ })();
1920
+ if (options?.strict && !analysis.isDag) throw new Error(`Workflow '${executionContext.blueprint.id}' failed strictness check: Cycles are not allowed.`);
1921
+ if (!analysis.isDag) this.logger.warn(`Workflow contains cycles`, { blueprintId: executionContext.blueprint.id });
1922
+ const traverser = new GraphTraverser(executionContext.blueprint, options?.strict === true);
1923
+ const result = await this.orchestrator.run(executionContext, traverser);
1924
+ const duration = Date.now() - startTime;
1925
+ if (result.status === "stalled") {
1926
+ await this.eventBus.emit({
1927
+ type: "workflow:stall",
1928
+ payload: {
1929
+ blueprintId: executionContext.blueprint.id,
1930
+ executionId: executionContext.executionId,
1931
+ remainingNodes: traverser.getAllNodeIds().size - executionContext.state.getCompletedNodes().size
1932
+ }
1933
+ });
1934
+ await this.eventBus.emit({
1935
+ type: "workflow:pause",
1936
+ payload: {
1937
+ blueprintId: executionContext.blueprint.id,
1938
+ executionId: executionContext.executionId
1939
+ }
1940
+ });
1941
+ }
1942
+ this.logger.info(`Workflow execution completed`, {
1943
+ blueprintId: executionContext.blueprint.id,
1944
+ executionId: executionContext.executionId,
1945
+ status: result.status,
1946
+ duration,
1947
+ errors: result.errors?.length || 0
1948
+ });
1949
+ await this.eventBus.emit({
1950
+ type: "workflow:finish",
1951
+ payload: {
1952
+ blueprintId: executionContext.blueprint.id,
1953
+ executionId: executionContext.executionId,
1954
+ status: result.status,
1955
+ errors: result.errors
1956
+ }
1957
+ });
1958
+ if (result.status === "awaiting") {
1959
+ const awaitingNodeIds = executionContext.state.getAwaitingNodeIds();
1960
+ for (const nodeId of awaitingNodeIds) {
1961
+ const details = executionContext.state.getAwaitingDetails(nodeId);
1962
+ if (details?.reason === "timer") this.scheduler.registerAwaitingWorkflow(executionContext.executionId, executionContext.blueprint.id, result.serializedContext, nodeId, details.wakeUpAt);
1963
+ }
1964
+ }
1965
+ return result;
1966
+ } catch (error) {
1967
+ const duration = Date.now() - startTime;
1968
+ const workflowError = {
1969
+ message: error instanceof Error ? error.message : String(error),
1970
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1971
+ isFatal: false,
1972
+ name: "WorkflowError"
1973
+ };
1974
+ await this.eventBus.emit({
1975
+ type: "workflow:finish",
1976
+ payload: {
1977
+ blueprintId: executionContext.blueprint.id,
1978
+ executionId: executionContext.executionId,
1979
+ status: "cancelled",
1980
+ errors: [workflowError]
1981
+ }
1982
+ });
1983
+ if (error instanceof DOMException ? error.name === "AbortError" : error instanceof FlowcraftError && error.message.includes("cancelled")) {
1984
+ this.logger.info(`Workflow execution cancelled`, {
1985
+ blueprintId: executionContext.blueprint.id,
1986
+ executionId: executionContext.executionId,
1987
+ duration
1988
+ });
1989
+ await this.eventBus.emit({
1990
+ type: "workflow:pause",
1991
+ payload: {
1992
+ blueprintId: executionContext.blueprint.id,
1993
+ executionId: executionContext.executionId
1994
+ }
1995
+ });
1996
+ await this.eventBus.emit({
1997
+ type: "workflow:finish",
1998
+ payload: {
1999
+ blueprintId: executionContext.blueprint.id,
2000
+ executionId: executionContext.executionId,
2001
+ status: "cancelled",
2002
+ errors: [workflowError]
2003
+ }
2004
+ });
2005
+ return {
2006
+ context: {},
2007
+ serializedContext: "{}",
2008
+ status: "cancelled"
2009
+ };
2010
+ }
2011
+ this.logger.error(`Workflow execution failed`, {
2012
+ blueprintId: executionContext.blueprint.id,
2013
+ executionId: executionContext.executionId,
2014
+ duration,
2015
+ error: error instanceof Error ? error.message : String(error)
2016
+ });
2017
+ throw error;
2018
+ }
2019
+ }
2020
+ startScheduler(checkIntervalMs) {
2021
+ if (checkIntervalMs !== void 0) this.scheduler = new WorkflowScheduler(this, checkIntervalMs);
2022
+ this.scheduler.start();
2023
+ }
2024
+ stopScheduler() {
2025
+ this.scheduler.stop();
2026
+ }
2027
+ _setupResumedExecutionContext(blueprint, workflowState, options) {
2028
+ const executionId = globalThis.crypto?.randomUUID();
2029
+ return new ExecutionContext(blueprint, workflowState, this._createExecutionRegistry(options?.functionRegistry), executionId, this, {
2030
+ logger: this.logger,
2031
+ eventBus: this.eventBus,
2032
+ serializer: this.serializer,
2033
+ evaluator: this.evaluator,
2034
+ middleware: this.middleware,
2035
+ dependencies: this.dependencies
2036
+ }, options?.signal);
2037
+ }
2038
+ async resume(blueprint, serializedContext, resumeData, nodeId, options) {
2039
+ const executionId = globalThis.crypto?.randomUUID();
2040
+ const workflowState = new WorkflowState(this.serializer.deserialize(serializedContext));
2041
+ const awaitingNodeIds = workflowState.getAwaitingNodeIds();
2042
+ if (awaitingNodeIds.length === 0) throw new FlowcraftError("Cannot resume: The provided context is not in an awaiting state.", { isFatal: true });
2043
+ const awaitingNodeId = nodeId || awaitingNodeIds[0];
2044
+ if (!awaitingNodeIds.includes(awaitingNodeId)) throw new FlowcraftError(`Cannot resume: Node '${awaitingNodeId}' is not in an awaiting state.`, { isFatal: true });
2045
+ const awaitingNodeDef = blueprint.nodes.find((n) => n.id === awaitingNodeId);
2046
+ if (!awaitingNodeDef) throw new FlowcraftError(`Awaiting node '${awaitingNodeId}' not found in blueprint.`, {
2047
+ nodeId: awaitingNodeId,
2048
+ blueprintId: blueprint.id,
2049
+ isFatal: true
2050
+ });
2051
+ const contextImpl = workflowState.getContext();
2052
+ if (awaitingNodeDef.uses === "SubflowNode") {
2053
+ const subflowStateKey = `_subflowState.${awaitingNodeId}`;
2054
+ const subflowContext = await contextImpl.get(subflowStateKey);
2055
+ if (!subflowContext) throw new FlowcraftError(`Cannot resume: Subflow state for node '${awaitingNodeId}' not found.`, {
2056
+ nodeId: awaitingNodeId,
2057
+ blueprintId: blueprint.id,
2058
+ isFatal: true
2059
+ });
2060
+ const blueprintId = awaitingNodeDef.params?.blueprintId;
2061
+ if (!blueprintId) throw new FlowcraftError(`Subflow node '${awaitingNodeId}' is missing the 'blueprintId' parameter.`, {
2062
+ nodeId: awaitingNodeId,
2063
+ blueprintId: blueprint.id,
2064
+ isFatal: true
2065
+ });
2066
+ const subBlueprint = this.blueprints[blueprintId];
2067
+ if (!subBlueprint) throw new FlowcraftError(`Sub-blueprint with ID '${blueprintId}' not found in runtime registry.`, {
2068
+ nodeId: awaitingNodeId,
2069
+ blueprintId: blueprint.id,
2070
+ isFatal: true
2071
+ });
2072
+ const subflowResumeResult = await this.resume(subBlueprint, subflowContext, resumeData, void 0, options);
2073
+ if (subflowResumeResult.status !== "completed") throw new FlowcraftError(`Resumed subflow '${subBlueprint.id}' did not complete. Status: ${subflowResumeResult.status}`, {
2074
+ nodeId: awaitingNodeId,
2075
+ blueprintId: blueprint.id,
2076
+ isFatal: false
2077
+ });
2078
+ const subflowFinalContext = subflowResumeResult.context;
2079
+ let finalSubflowOutput;
2080
+ const subAnalysis = analyzeBlueprint(subBlueprint);
2081
+ if (awaitingNodeDef.params?.outputs) finalSubflowOutput = subflowFinalContext;
2082
+ else if (subAnalysis.terminalNodeIds.length === 1) finalSubflowOutput = subflowFinalContext[`_outputs.${subAnalysis.terminalNodeIds[0]}`];
2083
+ else {
2084
+ const terminalOutputs = {};
2085
+ for (const terminalId of subAnalysis.terminalNodeIds) terminalOutputs[terminalId] = subflowFinalContext[`_outputs.${terminalId}`];
2086
+ finalSubflowOutput = terminalOutputs;
2087
+ }
2088
+ resumeData = { output: finalSubflowOutput };
2089
+ await contextImpl.delete(subflowStateKey);
2090
+ }
2091
+ workflowState.addCompletedNode(awaitingNodeId, resumeData.output);
2092
+ const nextSteps = await this.determineNextNodes(blueprint, awaitingNodeId, resumeData, contextImpl, executionId);
2093
+ if (nextSteps.length === 0) {
2094
+ workflowState.clearAwaiting(awaitingNodeId);
2095
+ const result = await workflowState.toResult(this.serializer, executionId);
2096
+ result.status = "completed";
2097
+ return result;
2098
+ }
2099
+ const allPredecessors = new GraphTraverser(blueprint).getAllPredecessors();
2100
+ for (const { node, edge } of nextSteps) await this.applyEdgeTransform(edge, resumeData, node, contextImpl, allPredecessors, executionId);
2101
+ const traverser = GraphTraverser.fromState(blueprint, workflowState);
2102
+ const nextNodeDefs = nextSteps.map((s) => s.node);
2103
+ for (const nodeDef of nextNodeDefs) traverser.addToFrontier(nodeDef.id);
2104
+ workflowState.clearAwaiting(awaitingNodeId);
2105
+ const executionContext = this._setupResumedExecutionContext(blueprint, workflowState, options);
2106
+ return await this.orchestrator.run(executionContext, traverser);
2107
+ }
2108
+ _createExecutionRegistry(dynamicRegistry) {
2109
+ const executionRegistry = new Map(this.registry);
2110
+ if (dynamicRegistry) for (const [key, func] of dynamicRegistry.entries()) executionRegistry.set(key, func);
2111
+ return executionRegistry;
2112
+ }
2113
+ async executeNode(blueprint, nodeId, state, _allPredecessors, functionRegistry, executionId, signal) {
2114
+ const nodeDef = blueprint.nodes.find((n) => n.id === nodeId);
2115
+ if (!nodeDef) throw new FlowcraftError(`Node '${nodeId}' not found in blueprint.`, {
2116
+ nodeId,
2117
+ blueprintId: blueprint.id,
2118
+ executionId,
2119
+ isFatal: false
2120
+ });
2121
+ const asyncContext = state.getContext();
2122
+ const input = await this.resolveNodeInput(nodeDef.id, blueprint, asyncContext);
2123
+ const nodeRegistry = new Map([...this.registry, ...functionRegistry || /* @__PURE__ */ new Map()]);
2124
+ const services = {
2125
+ logger: this.logger,
2126
+ eventBus: this.eventBus,
2127
+ serializer: this.serializer,
2128
+ evaluator: this.evaluator,
2129
+ middleware: this.middleware,
2130
+ dependencies: this.dependencies
2131
+ };
2132
+ const context = new ExecutionContext(blueprint, state, nodeRegistry, executionId || "", this, services, signal);
2133
+ const executionResult = await this.executorFactory.createExecutorForNode(nodeId, context).execute(input);
2134
+ if (executionResult.status === "success") return executionResult.result;
2135
+ if (executionResult.status === "failed_with_fallback") {
2136
+ const fallbackNode = blueprint.nodes.find((n) => n.id === executionResult.fallbackNodeId);
2137
+ if (!fallbackNode) throw new FlowcraftError(`Fallback node '${executionResult.fallbackNodeId}' not found in blueprint.`, {
2138
+ nodeId: nodeDef.id,
2139
+ blueprintId: blueprint.id,
2140
+ executionId,
2141
+ isFatal: false
2142
+ });
2143
+ const fallbackInput = await this.resolveNodeInput(fallbackNode.id, blueprint, asyncContext);
2144
+ const fallbackResult = await this.executorFactory.createExecutorForNode(fallbackNode.id, context).execute(fallbackInput);
2145
+ if (fallbackResult.status === "success") {
2146
+ state.markFallbackExecuted();
2147
+ state.addCompletedNode(executionResult.fallbackNodeId, fallbackResult.result.output);
2148
+ this.logger.info(`Fallback execution completed`, {
2149
+ nodeId: nodeDef.id,
2150
+ fallbackNodeId: executionResult.fallbackNodeId,
2151
+ executionId
2152
+ });
2153
+ return {
2154
+ ...fallbackResult.result,
2155
+ _fallbackExecuted: true
2156
+ };
2157
+ }
2158
+ throw fallbackResult.error;
2159
+ }
2160
+ throw executionResult.error;
2161
+ }
2162
+ getExecutorForNode(nodeId, context) {
2163
+ return this.executorFactory.createExecutorForNode(nodeId, context);
2164
+ }
2165
+ createForSubflow(subBlueprint, initialSubState, executionId, signal) {
2166
+ return new ExecutionContext(subBlueprint, new WorkflowState(initialSubState), this.registry, executionId, this, {
2167
+ logger: this.logger,
2168
+ eventBus: this.eventBus,
2169
+ serializer: this.serializer,
2170
+ evaluator: this.evaluator,
2171
+ middleware: this.middleware,
2172
+ dependencies: this.dependencies
2173
+ }, signal);
2174
+ }
2175
+ async determineNextNodes(blueprint, nodeId, result, context, executionId) {
2176
+ return this.logicHandler.determineNextNodes(blueprint, nodeId, result, context, executionId);
2177
+ }
2178
+ async applyEdgeTransform(edge, sourceResult, targetNode, context, allPredecessors, executionId) {
2179
+ return this.logicHandler.applyEdgeTransform(edge, sourceResult, targetNode, context, allPredecessors, executionId);
2180
+ }
2181
+ async resolveNodeInput(nodeId, blueprint, context) {
2182
+ return this.logicHandler.resolveNodeInput(nodeId, blueprint, context);
2183
+ }
2184
+ /**
2185
+ * Replay a workflow execution from a pre-recorded event history.
2186
+ * This reconstructs the final workflow state without executing any node logic,
2187
+ * enabling time-travel debugging and post-mortem analysis.
2188
+ *
2189
+ * @param blueprint The workflow blueprint
2190
+ * @param events The recorded event history for the execution
2191
+ * @param executionId Optional execution ID to filter events (if events contain multiple executions)
2192
+ * @returns The reconstructed workflow result
2193
+ */
2194
+ async replay(blueprint, events, executionId) {
2195
+ let filteredEvents = events;
2196
+ if (executionId) filteredEvents = events.filter((event) => {
2197
+ if ("executionId" in event.payload) return event.payload.executionId === executionId;
2198
+ return false;
2199
+ });
2200
+ if (!executionId) {
2201
+ const workflowStartEvent = filteredEvents.find((e) => e.type === "workflow:start");
2202
+ if (workflowStartEvent && "executionId" in workflowStartEvent.payload) executionId = workflowStartEvent.payload.executionId;
2203
+ else throw new FlowcraftError("Cannot determine execution ID from events", { isFatal: true });
2204
+ }
2205
+ const tempContext = this._setupExecutionContext(blueprint, {}, { strict: false });
2206
+ const executionContext = new ExecutionContext(blueprint, tempContext.state, tempContext.nodeRegistry, executionId, this, tempContext.services, tempContext.signal, tempContext.concurrency);
2207
+ const { ReplayOrchestrator } = await import("./replay-BB11M6K1.mjs").then((n) => n.n);
2208
+ const replayOrchestrator = new ReplayOrchestrator(filteredEvents);
2209
+ const traverser = new GraphTraverser(blueprint);
2210
+ return await replayOrchestrator.run(executionContext, traverser);
2211
+ }
2212
+ };
2213
+
2214
+ //#endregion
2215
+ export { PersistentEventBusAdapter as A, DIContainer as C, generateMermaid as D, checkForCycles as E, generateMermaidForRun as O, UnsafeEvaluator as S, analyzeBlueprint as T, TrackedAsyncContext as _, sanitizeBlueprint as a, NullLogger as b, isNodeClass as c, executeBatch as d, processResults as f, Context as g, AsyncContextView as h, NodeExecutor as i, InMemoryEventStore as k, JsonSerializer as l, WorkflowState as m, ClassNodeExecutor as n, GraphTraverser as o, ExecutionContext as p, FunctionNodeExecutor as r, BaseNode as s, FlowRuntime as t, DefaultOrchestrator as u, FlowcraftError as v, ServiceTokens as w, PropertyEvaluator as x, ConsoleLogger as y };
2216
+ //# sourceMappingURL=runtime-CmefIAu_.mjs.map