onbuzz 4.9.13 → 4.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (451) hide show
  1. package/node_modules/glob/README.md +31 -5
  2. package/node_modules/glob/dist/commonjs/glob.d.ts +8 -0
  3. package/node_modules/glob/dist/commonjs/glob.d.ts.map +1 -1
  4. package/node_modules/glob/dist/commonjs/glob.js +2 -1
  5. package/node_modules/glob/dist/commonjs/glob.js.map +1 -1
  6. package/node_modules/glob/dist/commonjs/index.min.js +3 -3
  7. package/node_modules/glob/dist/commonjs/index.min.js.map +4 -4
  8. package/node_modules/glob/dist/commonjs/pattern.d.ts +3 -0
  9. package/node_modules/glob/dist/commonjs/pattern.d.ts.map +1 -1
  10. package/node_modules/glob/dist/commonjs/pattern.js +4 -0
  11. package/node_modules/glob/dist/commonjs/pattern.js.map +1 -1
  12. package/node_modules/glob/dist/esm/glob.d.ts +8 -0
  13. package/node_modules/glob/dist/esm/glob.d.ts.map +1 -1
  14. package/node_modules/glob/dist/esm/glob.js +2 -1
  15. package/node_modules/glob/dist/esm/glob.js.map +1 -1
  16. package/node_modules/glob/dist/esm/index.min.js +3 -3
  17. package/node_modules/glob/dist/esm/index.min.js.map +4 -4
  18. package/node_modules/glob/dist/esm/pattern.d.ts +3 -0
  19. package/node_modules/glob/dist/esm/pattern.d.ts.map +1 -1
  20. package/node_modules/glob/dist/esm/pattern.js +4 -0
  21. package/node_modules/glob/dist/esm/pattern.js.map +1 -1
  22. package/node_modules/{@isaacs → glob/node_modules}/balanced-match/README.md +7 -10
  23. package/node_modules/{@isaacs → glob/node_modules}/balanced-match/package.json +7 -18
  24. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/README.md +3 -6
  25. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.js +6 -4
  26. package/node_modules/glob/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -0
  27. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.js +6 -4
  28. package/node_modules/glob/node_modules/brace-expansion/dist/esm/index.js.map +1 -0
  29. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/package.json +11 -7
  30. package/node_modules/glob/node_modules/minimatch/README.md +76 -1
  31. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts +1 -1
  32. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts.map +1 -1
  33. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js.map +1 -1
  34. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts +4 -2
  35. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts.map +1 -1
  36. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js +309 -55
  37. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js.map +1 -1
  38. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.d.ts.map +1 -1
  39. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js +2 -4
  40. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js.map +1 -1
  41. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts +1 -1
  42. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts.map +1 -1
  43. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js +4 -4
  44. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js.map +1 -1
  45. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts +81 -1
  46. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts.map +1 -1
  47. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js +232 -134
  48. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js.map +1 -1
  49. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts +1 -1
  50. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts.map +1 -1
  51. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js +8 -8
  52. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js.map +1 -1
  53. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts +1 -1
  54. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts.map +1 -1
  55. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -1
  56. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts +4 -2
  57. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts.map +1 -1
  58. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js +309 -55
  59. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js.map +1 -1
  60. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.d.ts.map +1 -1
  61. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js +2 -4
  62. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -1
  63. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts +1 -1
  64. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts.map +1 -1
  65. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js +4 -4
  66. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js.map +1 -1
  67. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts +81 -1
  68. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts.map +1 -1
  69. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js +232 -134
  70. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js.map +1 -1
  71. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts +1 -1
  72. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts.map +1 -1
  73. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js +8 -8
  74. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js.map +1 -1
  75. package/node_modules/glob/node_modules/minimatch/package.json +17 -11
  76. package/node_modules/glob/package.json +10 -13
  77. package/node_modules/minipass/LICENSE.md +55 -0
  78. package/node_modules/minipass/dist/commonjs/index.d.ts +12 -16
  79. package/node_modules/minipass/dist/commonjs/index.d.ts.map +1 -1
  80. package/node_modules/minipass/dist/commonjs/index.js +13 -3
  81. package/node_modules/minipass/dist/commonjs/index.js.map +1 -1
  82. package/node_modules/minipass/dist/esm/index.d.ts +12 -16
  83. package/node_modules/minipass/dist/esm/index.d.ts.map +1 -1
  84. package/node_modules/minipass/dist/esm/index.js +3 -1
  85. package/node_modules/minipass/dist/esm/index.js.map +1 -1
  86. package/node_modules/minipass/package.json +9 -14
  87. package/node_modules/path-scurry/node_modules/lru-cache/README.md +96 -10
  88. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.d.ts.map +1 -0
  89. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.js.map +1 -0
  90. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.d.ts +5 -0
  91. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.js +7 -0
  92. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts +1400 -0
  93. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts.map +1 -0
  94. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js +1726 -0
  95. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js.map +1 -0
  96. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js +2 -0
  97. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js.map +7 -0
  98. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts +12 -0
  99. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts.map +1 -0
  100. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js +10 -0
  101. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js.map +1 -0
  102. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.cjs.map +1 -0
  103. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.d.cts.map +1 -0
  104. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.d.ts +5 -0
  105. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js +7 -0
  106. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts +109 -32
  107. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts.map +1 -1
  108. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js +334 -197
  109. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js.map +1 -1
  110. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js +1 -1
  111. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js.map +4 -4
  112. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.d.ts.map +1 -0
  113. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.js.map +1 -0
  114. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.d.ts +5 -0
  115. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.js +9 -0
  116. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts +1400 -0
  117. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts.map +1 -0
  118. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js +1726 -0
  119. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js.map +1 -0
  120. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js +2 -0
  121. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js.map +7 -0
  122. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts +12 -0
  123. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts.map +1 -0
  124. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js +10 -0
  125. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js.map +1 -0
  126. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts +12 -0
  127. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts.map +1 -0
  128. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js +10 -0
  129. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js.map +1 -0
  130. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.d.ts.map +1 -0
  131. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.js.map +1 -0
  132. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.d.ts +5 -0
  133. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js +4 -0
  134. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts +1400 -0
  135. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts.map +1 -0
  136. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js +1722 -0
  137. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js.map +1 -0
  138. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js +2 -0
  139. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js.map +7 -0
  140. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts +12 -0
  141. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts.map +1 -0
  142. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js +7 -0
  143. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js.map +1 -0
  144. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.d.mts.map +1 -0
  145. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.mjs.map +1 -0
  146. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.d.ts +5 -0
  147. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.js +19 -0
  148. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts +109 -32
  149. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts.map +1 -1
  150. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js +333 -196
  151. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js.map +1 -1
  152. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js +1 -1
  153. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js.map +4 -4
  154. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.d.ts.map +1 -0
  155. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.js.map +1 -0
  156. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.d.ts +5 -0
  157. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js +6 -0
  158. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts +1400 -0
  159. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts.map +1 -0
  160. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js +1722 -0
  161. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js.map +1 -0
  162. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js +2 -0
  163. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js.map +7 -0
  164. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts +12 -0
  165. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts.map +1 -0
  166. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js +7 -0
  167. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js.map +1 -0
  168. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts +12 -0
  169. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts.map +1 -0
  170. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js +7 -0
  171. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js.map +1 -0
  172. package/node_modules/path-scurry/node_modules/lru-cache/package.json +71 -18
  173. package/node_modules/path-scurry/package.json +8 -24
  174. package/package.json +1 -1
  175. package/scripts/debug-balance-probe.mjs +35 -35
  176. package/scripts/push-image.sh +43 -43
  177. package/scripts/setup-acr.sh +65 -65
  178. package/scripts/verify-optional-deps.js +96 -1
  179. package/src/__tests__/composioCliFlags.test.js +239 -239
  180. package/src/analyzers/CSSAnalyzer.js +298 -297
  181. package/src/analyzers/ConfigValidator.js +691 -690
  182. package/src/analyzers/ESLintAnalyzer.js +320 -320
  183. package/src/analyzers/JavaScriptAnalyzer.js +260 -261
  184. package/src/analyzers/PrettierFormatter.js +246 -247
  185. package/src/analyzers/PythonAnalyzer.js +283 -283
  186. package/src/analyzers/SecurityAnalyzer.js +729 -729
  187. package/src/analyzers/SparrowAnalyzer.js +341 -341
  188. package/src/analyzers/TypeScriptAnalyzer.js +247 -247
  189. package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -41
  190. package/src/analyzers/__tests__/ConfigValidator.test.js +362 -362
  191. package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -40
  192. package/src/analyzers/__tests__/PythonAnalyzer.test.js +205 -208
  193. package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -303
  194. package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -187
  195. package/src/analyzers/codeCloneDetector/analyzer.js +344 -344
  196. package/src/analyzers/codeCloneDetector/detector.js +250 -250
  197. package/src/analyzers/codeCloneDetector/index.js +194 -192
  198. package/src/analyzers/codeCloneDetector/parser.js +199 -199
  199. package/src/core/__tests__/agentPool.test.js +866 -866
  200. package/src/core/__tests__/agentPoolAutoResume.test.js +209 -209
  201. package/src/core/__tests__/agentPoolWakeOnMessage.test.js +315 -315
  202. package/src/core/__tests__/agentScheduler.emptyResponseChatStall.test.js +213 -213
  203. package/src/core/__tests__/agentScheduler.errorCategorisation.test.js +246 -246
  204. package/src/core/__tests__/agentScheduler.firstChunkTimeout.test.js +138 -138
  205. package/src/core/__tests__/agentScheduler.modeTransitions.test.js +233 -233
  206. package/src/core/__tests__/agentScheduler.nativePromptPick.test.js +319 -319
  207. package/src/core/__tests__/agentScheduler.taskLifecycleInstruction.test.js +78 -78
  208. package/src/core/__tests__/agentScheduler.visualizer.test.js +258 -258
  209. package/src/core/__tests__/flowCheckpointStore.test.js +140 -140
  210. package/src/core/__tests__/flowEndToEnd.test.js +565 -565
  211. package/src/core/__tests__/flowFieldMapping.test.js +188 -189
  212. package/src/core/__tests__/flowLintClientMirror.test.js +96 -98
  213. package/src/core/__tests__/flowSavePayload.test.js +170 -169
  214. package/src/core/__tests__/flowTemplates.test.js +311 -311
  215. package/src/core/__tests__/flowVersionStore.test.js +123 -123
  216. package/src/core/__tests__/messageProcessor.test.js +669 -669
  217. package/src/core/__tests__/stateManager.test.js +0 -1
  218. package/src/core/agentPool.js +2474 -2475
  219. package/src/core/agentScheduler.js +1 -4
  220. package/src/core/contextManager.js +708 -708
  221. package/src/core/flowExecutor.js +1510 -1510
  222. package/src/core/flowFieldMapping.js +136 -138
  223. package/src/core/messageProcessor.js +953 -954
  224. package/src/core/orchestrator.js +593 -595
  225. package/src/core/stateManager.js +1765 -1752
  226. package/src/index.js +1221 -1221
  227. package/src/interfaces/__tests__/archivedAgentDelete.test.js +207 -207
  228. package/src/interfaces/__tests__/bulkAgentRoute.test.js +361 -361
  229. package/src/interfaces/__tests__/imageServing.test.js +228 -228
  230. package/src/interfaces/__tests__/remoteSessionAuth.test.js +308 -308
  231. package/src/interfaces/__tests__/videoJobsRoutes.test.js +178 -179
  232. package/src/interfaces/__tests__/webServer.marketplace.test.js +629 -629
  233. package/src/interfaces/schedulerRoutes.js +50 -50
  234. package/src/interfaces/terminal/__tests__/smoke/connection.test.js +341 -350
  235. package/src/interfaces/terminal/__tests__/smoke/enhancements.test.js +156 -156
  236. package/src/interfaces/terminal/__tests__/smoke/imports.test.js +325 -330
  237. package/src/interfaces/terminal/__tests__/smoke/tools.test.js +385 -388
  238. package/src/interfaces/terminal/api/session.js +265 -266
  239. package/src/interfaces/terminal/api/websocket.js +496 -497
  240. package/src/interfaces/terminal/components/AgentCreator.js +691 -705
  241. package/src/interfaces/terminal/components/AgentEditor.js +676 -678
  242. package/src/interfaces/terminal/components/AgentSwitcher.js +331 -330
  243. package/src/interfaces/terminal/components/ErrorPanel.js +263 -264
  244. package/src/interfaces/terminal/components/Header.js +28 -28
  245. package/src/interfaces/terminal/components/Layout.js +598 -603
  246. package/src/interfaces/terminal/components/MessageList.js +280 -281
  247. package/src/interfaces/terminal/components/SettingsPanel.js +410 -415
  248. package/src/interfaces/terminal/components/StatusBar.js +2 -0
  249. package/src/interfaces/terminal/index.js +168 -168
  250. package/src/interfaces/terminal/state/useAgentControl.js +496 -496
  251. package/src/interfaces/terminal/state/useAgents.js +537 -537
  252. package/src/interfaces/terminal/state/useMessages.js +629 -630
  253. package/src/interfaces/terminal/state/useTools.js +554 -554
  254. package/src/interfaces/terminal/utils/debugLogger.js +44 -44
  255. package/src/interfaces/terminal/utils/settingsStorage.js +232 -232
  256. package/src/interfaces/webServer.js +7578 -7579
  257. package/src/interfaces/webServer.js.bak +7046 -7046
  258. package/src/modules/fileExplorer/__tests__/zipDownload.test.js +237 -237
  259. package/src/modules/fileExplorer/controller.js +470 -469
  260. package/src/modules/fileExplorer/routes.js +285 -286
  261. package/src/modules/widget/__tests__/isDisabled.test.js +41 -41
  262. package/src/modules/widget/__tests__/routes.test.js +677 -678
  263. package/src/modules/widget/__tests__/runtime.test.js +401 -401
  264. package/src/modules/widget/__tests__/versioning.test.js +309 -309
  265. package/src/modules/widget/__tests__/webComponentRuntime.test.js +565 -565
  266. package/src/modules/widget/__tests__/widgetTool.test.js +316 -316
  267. package/src/modules/widget/routes.js +435 -435
  268. package/src/modules/widget/runtime/bundle.js +640 -640
  269. package/src/modules/widget/runtime/webComponentBundle.js +470 -470
  270. package/src/modules/widget/schema.js +182 -181
  271. package/src/modules/widget/widgetTool.js +1389 -1389
  272. package/src/services/__tests__/agentActivityService.test.js +401 -402
  273. package/src/services/__tests__/benchmarkService.test.js +184 -184
  274. package/src/services/__tests__/contextInjectionService.test.js +246 -246
  275. package/src/services/__tests__/conversationQuery.test.js +721 -723
  276. package/src/services/__tests__/credentialVault.test.js +469 -469
  277. package/src/services/__tests__/discordService.integration.test.js +638 -639
  278. package/src/services/__tests__/flowContextService.test.js +590 -590
  279. package/src/services/__tests__/memoryService.test.js +1 -1
  280. package/src/services/__tests__/messageSource.test.js +380 -380
  281. package/src/services/__tests__/modelRouterNaming.test.js +111 -111
  282. package/src/services/__tests__/projectDetector.test.js +34 -34
  283. package/src/services/__tests__/promptService.test.js +242 -242
  284. package/src/services/__tests__/telegramService.test.js +941 -941
  285. package/src/services/__tests__/tokenCountingService.test.js +48 -48
  286. package/src/services/agentActivityService.js +419 -420
  287. package/src/services/aiService.js +2997 -3001
  288. package/src/services/apiKeyManager.js +359 -359
  289. package/src/services/benchmarkService.js +196 -196
  290. package/src/services/codebaseKnowledgeService.js +2 -2
  291. package/src/services/composioService.js +738 -738
  292. package/src/services/conversationCompactionService.js +1258 -1257
  293. package/src/services/credentialVault.js +685 -685
  294. package/src/services/discordService.js +792 -793
  295. package/src/services/embeddings/__tests__/azureCustomProvider.test.js +232 -232
  296. package/src/services/embeddings/__tests__/embeddingService.test.js +417 -417
  297. package/src/services/embeddings/__tests__/localProvider.test.js +263 -263
  298. package/src/services/embeddings/autoRecall.js +218 -219
  299. package/src/services/embeddings/indexers/__tests__/agentIndexer.test.js +232 -232
  300. package/src/services/embeddings/indexers/__tests__/memoryIndexer.test.js +418 -418
  301. package/src/services/embeddings/indexers/__tests__/reminisceIndexer.test.js +356 -357
  302. package/src/services/embeddings/indexers/__tests__/skillsIndexer.test.js +145 -145
  303. package/src/services/embeddings/indexers/__tests__/taskIndexer.test.js +146 -146
  304. package/src/services/embeddings/indexers/composioIndexer.js +279 -279
  305. package/src/services/embeddings/providerInterface.js +206 -206
  306. package/src/services/embeddings/providers/localProvider.js +11 -7
  307. package/src/services/embeddings/providers/openaiProvider.js +101 -101
  308. package/src/services/embeddings/vectorStore/inMemoryJsonStore.js +356 -356
  309. package/src/services/errorHandler.js +809 -809
  310. package/src/services/flowContextService.js +586 -586
  311. package/src/services/grounding/MockAdapter.js +125 -125
  312. package/src/services/modelRouterService.js +26 -31
  313. package/src/services/modelsService.js +322 -322
  314. package/src/services/ollamaService.js +452 -452
  315. package/src/services/projectDetector.js +403 -404
  316. package/src/services/promptService.js +418 -418
  317. package/src/services/qualityInspector.js +795 -795
  318. package/src/services/scheduleService.js +726 -726
  319. package/src/services/serviceRegistry.js +386 -386
  320. package/src/services/telegrafBot.js +174 -174
  321. package/src/services/telegramService.js +1972 -1972
  322. package/src/services/visualEditorBridge.js +1033 -1033
  323. package/src/services/visualEditorServer.js +1769 -1774
  324. package/src/services/whatsappService.js +667 -668
  325. package/src/tools/__tests__/agentCommunicationTool.findAgent.test.js +226 -226
  326. package/src/tools/__tests__/agentCommunicationTool.test.js +3 -3
  327. package/src/tools/__tests__/agentDelayTool.test.js +342 -342
  328. package/src/tools/__tests__/baseTool.test.js +3 -3
  329. package/src/tools/__tests__/codeMapTool.test.js +915 -915
  330. package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -309
  331. package/src/tools/__tests__/fileTreeTool.test.js +274 -274
  332. package/src/tools/__tests__/filesystemTool.test.js +815 -815
  333. package/src/tools/__tests__/foundryWebSearchTool.test.js +252 -252
  334. package/src/tools/__tests__/imageTool.validator.test.js +194 -194
  335. package/src/tools/__tests__/jobDoneTool.test.js +580 -581
  336. package/src/tools/__tests__/memoryTool.forgetStale.test.js +272 -272
  337. package/src/tools/__tests__/memoryTool.reminisce.test.js +2 -2
  338. package/src/tools/__tests__/memoryTool.reminisceSemanticSearch.test.js +301 -301
  339. package/src/tools/__tests__/memoryTool.semanticSearch.test.js +405 -405
  340. package/src/tools/__tests__/memoryTool.teamPool.test.js +293 -293
  341. package/src/tools/__tests__/memoryTool.test.js +1 -1
  342. package/src/tools/__tests__/seekTool.test.js +282 -282
  343. package/src/tools/__tests__/skillsTool.search.test.js +164 -164
  344. package/src/tools/__tests__/skillsTool.test.js +226 -226
  345. package/src/tools/__tests__/staticAnalysisTool.test.js +509 -509
  346. package/src/tools/__tests__/taskManagerTool.discipline.test.js +137 -137
  347. package/src/tools/__tests__/taskManagerTool.search.test.js +143 -143
  348. package/src/tools/__tests__/taskManagerTool.test.js +866 -866
  349. package/src/tools/__tests__/terminalTool.test.js +448 -448
  350. package/src/tools/__tests__/toolShapeForgiveness.test.js +259 -260
  351. package/src/tools/__tests__/userPromptTool.test.js +297 -297
  352. package/src/tools/__tests__/videoTool.jobs.test.js +147 -147
  353. package/src/tools/__tests__/webTool.e2e.test.js +609 -603
  354. package/src/tools/__tests__/webTool.unit.test.js +195 -195
  355. package/src/tools/__tests__/webTool.visionModel.test.js +75 -75
  356. package/src/tools/agentCommunicationTool.js +8 -10
  357. package/src/tools/agentDelayTool.js +496 -497
  358. package/src/tools/asyncToolManager.js +602 -603
  359. package/src/tools/baseTool.js +12 -11
  360. package/src/tools/cloneDetectionTool.js +576 -581
  361. package/src/tools/codeMapTool.js +0 -6
  362. package/src/tools/composioTool.js +617 -617
  363. package/src/tools/dependencyResolverTool.js +1211 -1212
  364. package/src/tools/desktop/DesktopTool.js +629 -638
  365. package/src/tools/desktop/__tests__/DesktopTool.e2e.test.js +306 -306
  366. package/src/tools/desktop/__tests__/DesktopTool.test.js +507 -507
  367. package/src/tools/desktop/__tests__/osController.test.js +364 -364
  368. package/src/tools/desktop/osController.js +491 -491
  369. package/src/tools/docxTool.js +623 -623
  370. package/src/tools/excelTool.js +636 -636
  371. package/src/tools/fileContentReplaceTool.js +5 -7
  372. package/src/tools/fileSystemTool.js +12 -19
  373. package/src/tools/fileTreeTool.js +840 -840
  374. package/src/tools/foundryWebSearchTool.js +273 -273
  375. package/src/tools/helpTool.js +198 -198
  376. package/src/tools/imageTool.js +1397 -1397
  377. package/src/tools/importAnalyzerTool.js +1056 -1056
  378. package/src/tools/jobDoneTool.js +495 -495
  379. package/src/tools/memoryTool.js +1 -1
  380. package/src/tools/office/pres/__tests__/presSystem.test.js +365 -365
  381. package/src/tools/office/pres/archetypes/agenda.js +61 -61
  382. package/src/tools/office/pres/archetypes/bentoGrid.js +218 -219
  383. package/src/tools/office/pres/archetypes/bigStat.js +140 -142
  384. package/src/tools/office/pres/archetypes/closing.js +70 -70
  385. package/src/tools/office/pres/archetypes/hero.js +70 -70
  386. package/src/tools/office/pres/archetypes/productHero.js +93 -94
  387. package/src/tools/office/pres/archetypes/table.js +73 -74
  388. package/src/tools/office/pres/backgrounds/orb.js +66 -66
  389. package/src/tools/office/pres/components.js +422 -423
  390. package/src/tools/officeTool.js +441 -441
  391. package/src/tools/pdfTool.js +625 -627
  392. package/src/tools/platformControlTool.js +1081 -1081
  393. package/src/tools/seekTool.js +917 -918
  394. package/src/tools/skillsTool.js +1 -1
  395. package/src/tools/staticAnalysisTool.js +2143 -2146
  396. package/src/tools/taskManagerTool.js +3324 -3324
  397. package/src/tools/terminalTool.js +2615 -2618
  398. package/src/tools/videoTool.js +1303 -1303
  399. package/src/tools/visionTool.js +508 -508
  400. package/src/tools/visualEditorTool.js +1289 -1290
  401. package/src/tools/webTool.js +3368 -3368
  402. package/src/tools/whatsappTool.js +464 -464
  403. package/src/types/__tests__/agent.test.js +499 -499
  404. package/src/types/__tests__/contextReference.test.js +606 -606
  405. package/src/types/__tests__/conversation.test.js +555 -555
  406. package/src/types/__tests__/toolCommand.test.js +584 -584
  407. package/src/types/contextReference.js +974 -971
  408. package/src/types/conversation.js +729 -729
  409. package/src/types/toolCommand.js +746 -746
  410. package/src/utilities/__tests__/attachmentValidator.test.js +80 -80
  411. package/src/utilities/__tests__/auditReport.test.js +328 -328
  412. package/src/utilities/__tests__/directoryAccessManager.test.js +388 -388
  413. package/src/utilities/__tests__/jsonRepair.test.js +103 -104
  414. package/src/utilities/__tests__/modeTransitionReasons.test.js +105 -105
  415. package/src/utilities/__tests__/platformUtils.test.js +80 -87
  416. package/src/utilities/__tests__/structuredFileValidator.test.js +261 -263
  417. package/src/utilities/__tests__/toolConstants.test.js +92 -94
  418. package/src/utilities/__tests__/useIsTouchDevice.detect.test.js +114 -114
  419. package/src/utilities/__tests__/webUiUtilSync.test.js +117 -117
  420. package/src/utilities/attachmentValidator.js +284 -288
  421. package/src/utilities/authCache.js.backup-1779570472481 +121 -121
  422. package/src/utilities/browserStealth.js +631 -630
  423. package/src/utilities/configManager.js +616 -617
  424. package/src/utilities/directoryAccessManager.js +564 -565
  425. package/src/utilities/fileProcessor.js +308 -307
  426. package/src/utilities/humanBehavior.js +454 -453
  427. package/src/utilities/logger.js +479 -479
  428. package/src/utilities/structuredFileValidator.js +696 -699
  429. package/src/utilities/tagParser.js +5 -10
  430. package/src/utilities/userDataDir.js +308 -308
  431. package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.js.map +0 -1
  432. package/node_modules/@isaacs/brace-expansion/dist/esm/index.js.map +0 -1
  433. package/node_modules/minipass/LICENSE +0 -15
  434. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/LICENSE.md +0 -0
  435. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts +0 -0
  436. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts.map +0 -0
  437. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js +0 -0
  438. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js.map +0 -0
  439. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/package.json +0 -0
  440. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts +0 -0
  441. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts.map +0 -0
  442. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js +0 -0
  443. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js.map +0 -0
  444. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/package.json +0 -0
  445. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/LICENSE +0 -0
  446. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts +0 -0
  447. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts.map +0 -0
  448. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/package.json +0 -0
  449. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts +0 -0
  450. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts.map +0 -0
  451. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/package.json +0 -0
@@ -1,1510 +1,1510 @@
1
- /**
2
- * FlowExecutor - Executes visual flow pipelines
3
- *
4
- * Purpose:
5
- * - Execute flow nodes in topologically sorted order
6
- * - Queue messages to agents via MessageProcessor
7
- * - Detect completion via jobdone tool results
8
- * - Track node states and outputs
9
- * - Broadcast execution progress via WebSocket
10
- */
11
-
12
- import { AGENT_MODES } from '../utilities/constants.js';
13
- import { validateFlowDefinition } from './flowSchema.js';
14
- import { assembleNodeInputs } from './flowFieldMapping.js';
15
- import { runWithRetry, FlowRetryError } from './flowRetry.js';
16
-
17
- class FlowExecutor {
18
- constructor(config, logger, stateManager, agentPool, messageProcessor) {
19
- this.config = config;
20
- this.logger = logger;
21
- this.stateManager = stateManager;
22
- this.agentPool = agentPool;
23
- this.messageProcessor = messageProcessor;
24
-
25
- // WebSocket manager for broadcasting progress
26
- this.webSocketManager = null;
27
-
28
- // Active flow executions
29
- this.activeExecutions = new Map();
30
-
31
- // Completion listeners per run
32
- this.completionListeners = new Map();
33
-
34
- // Phase 8: agentId → currently-awaited node contract.
35
- // Set when an agent is invoked inside a flow node; cleared when
36
- // the await resolves. The jobdone tool reads this to validate
37
- // the agent's job-done call AT TOOL TIME — rejecting partial
38
- // calls before they propagate into the executor.
39
- this.activeContracts = new Map();
40
-
41
- // Optional disk checkpointing — when set, every successful node
42
- // persists its result so the run can resume after a crash. Off by
43
- // default (preserves pre-Phase-4 behavior; opt in via setCheckpointStore).
44
- this.checkpointStore = null;
45
- }
46
-
47
- /**
48
- * Phase 8: get the currently-awaited node contract for an agent (if any).
49
- * Returns the contract object { inputs, outputs, instructions, ... } so
50
- * the jobdone tool can validate the agent's call against it AT TOOL TIME
51
- * — much faster recovery than waiting for executor-level re-prompts.
52
- *
53
- * @param {string} agentId
54
- * @returns {object|null} the contract, or null when the agent isn't in
55
- * a flow step right now.
56
- */
57
- getActiveContract(agentId) {
58
- return this.activeContracts.get(agentId) || null;
59
- }
60
-
61
- /**
62
- * Phase 4: enable disk checkpoints + resume by injecting a store.
63
- * Calling with null disables checkpointing.
64
- */
65
- setCheckpointStore(store) {
66
- this.checkpointStore = store || null;
67
- }
68
-
69
- /**
70
- * Set WebSocket manager for real-time updates
71
- * @param {Object} webSocketManager - WebSocket manager instance
72
- */
73
- setWebSocketManager(webSocketManager) {
74
- this.webSocketManager = webSocketManager;
75
- }
76
-
77
- /**
78
- * Execute a flow
79
- * @param {string} flowId - Flow identifier
80
- * @param {Object} initialInput - Initial input data
81
- * @param {string} sessionId - WebSocket session ID for broadcasts
82
- * @returns {Promise<Object>} Flow run result
83
- */
84
- async executeFlow(flowId, initialInput = {}, sessionId = null) {
85
- // Load flow definition
86
- const flow = await this.stateManager.getFlow(flowId);
87
- if (!flow) {
88
- throw new Error(`Flow not found: ${flowId}`);
89
- }
90
-
91
- // Belt-and-suspenders schema check. The POST/PUT routes already gate
92
- // on this, but flows can also be authored by importing JSON files
93
- // directly into the index, or by older clients that bypassed the
94
- // route. Catching here means we never queue agents on a flow that
95
- // we can't actually execute. Errors include path so the caller can
96
- // surface them in the run-detail view.
97
- const validation = validateFlowDefinition(flow);
98
- if (!validation.ok) {
99
- const detail = validation.errors.map(e => e.message).join('; ');
100
- this.logger.warn(`Flow ${flowId} failed schema validation; refusing to execute`, {
101
- errors: validation.errors,
102
- });
103
- throw new Error(`Flow definition is invalid: ${detail}`);
104
- }
105
-
106
- // Create flow run record
107
- const run = await this.stateManager.createFlowRun(flowId, initialInput);
108
- const runId = run.id;
109
-
110
- this.logger.info(`Starting flow execution: ${flowId}`, {
111
- runId,
112
- nodeCount: flow.nodes?.length || 0,
113
- edgeCount: flow.edges?.length || 0
114
- });
115
-
116
- // Track this execution
117
- this.activeExecutions.set(runId, {
118
- flowId,
119
- flow,
120
- runId,
121
- sessionId,
122
- status: 'running',
123
- startedAt: new Date()
124
- });
125
-
126
- // Update run status to running. Phase 6.3: stamp the flow's
127
- // current version onto the run so we can later answer "which
128
- // definition produced this output?" — important after rollbacks.
129
- await this.stateManager.updateFlowRun(runId, {
130
- status: 'running',
131
- flowVersion: flow.version ?? null,
132
- });
133
- const startedAt = new Date().toISOString();
134
- this.broadcastFlowUpdate(sessionId, {
135
- type: 'flow_run_started',
136
- runId,
137
- flowId,
138
- status: 'running',
139
- startedAt
140
- });
141
-
142
- try {
143
- // Ensure all referenced agents are loaded before execution
144
- await this.ensureAgentsLoaded(flow.nodes);
145
-
146
- // Topologically sort nodes
147
- const sortedNodes = this.topologicalSort(flow.nodes, flow.edges);
148
-
149
- if (sortedNodes.length === 0) {
150
- throw new Error('Flow has no executable nodes');
151
- }
152
-
153
- // Initialize execution context
154
- const context = {
155
- input: initialInput.userInput || initialInput.input || '',
156
- nodeOutputs: {},
157
- variables: { ...flow.variables },
158
- sortedNodes, // Store for position tracking
159
- flow // Store flow reference
160
- };
161
-
162
- // Execute nodes in order
163
- for (const node of sortedNodes) {
164
- // Check if execution was stopped
165
- const execution = this.activeExecutions.get(runId);
166
- if (!execution || execution.status === 'stopped') {
167
- this.logger.info(`Flow execution stopped: ${runId}`);
168
- await this.stateManager.updateFlowRun(runId, {
169
- status: 'stopped',
170
- completedAt: new Date().toISOString()
171
- });
172
- this.broadcastFlowUpdate(sessionId, {
173
- type: 'flow_run_stopped',
174
- runId,
175
- flowId
176
- });
177
- return { runId, status: 'stopped' };
178
- }
179
-
180
- // Phase 4: skip nodes already completed in a previous run that
181
- // we're resuming from. Their outputs were rehydrated into
182
- // context.nodeOutputs by resumeFlow before the loop started.
183
- if (context.skipCompletedNodeIds?.has?.(node.id)) {
184
- this.logger.info(`Flow ${flowId}: skipping already-completed node ${node.id} on resume`);
185
- continue;
186
- }
187
-
188
- // Execute node
189
- const nodeResult = await this.executeNode(node, context, runId, sessionId, flow);
190
-
191
- // Store node output by node ID
192
- context.nodeOutputs[node.id] = nodeResult;
193
-
194
- // Also store by outputKey if specified (for named references like {{result}})
195
- if (node.data?.outputKey) {
196
- context.nodeOutputs[node.data.outputKey] = nodeResult;
197
- }
198
-
199
- // Update node state in run
200
- await this.updateNodeState(runId, node.id, 'completed', nodeResult);
201
-
202
- // Phase 4: persist a checkpoint after each successful node so a
203
- // resume can skip back here. Best-effort — checkpoint failures
204
- // don't fail the run (we'd rather complete without resumability
205
- // than blow up because disk is full).
206
- if (this.checkpointStore) {
207
- try {
208
- await this.checkpointStore.saveNodeResult(runId, node.id, nodeResult);
209
- } catch (err) {
210
- this.logger.warn(`Flow ${flowId}: failed to checkpoint node ${node.id}`, { error: err.message });
211
- }
212
- }
213
- }
214
-
215
- // Flow completed successfully
216
- const finalOutput = this.collectFinalOutput(sortedNodes, context);
217
-
218
- await this.stateManager.updateFlowRun(runId, {
219
- status: 'completed',
220
- output: finalOutput,
221
- completedAt: new Date().toISOString()
222
- });
223
-
224
- this.broadcastFlowUpdate(sessionId, {
225
- type: 'flow_run_completed',
226
- runId,
227
- flowId,
228
- output: finalOutput
229
- });
230
-
231
- this.activeExecutions.delete(runId);
232
-
233
- this.logger.info(`Flow execution completed: ${flowId}`, { runId });
234
-
235
- return { runId, status: 'completed', output: finalOutput };
236
-
237
- } catch (error) {
238
- this.logger.error(`Flow execution failed: ${flowId}`, {
239
- runId,
240
- error: error.message
241
- });
242
-
243
- await this.stateManager.updateFlowRun(runId, {
244
- status: 'failed',
245
- error: error.message,
246
- completedAt: new Date().toISOString()
247
- });
248
-
249
- this.broadcastFlowUpdate(sessionId, {
250
- type: 'flow_run_failed',
251
- runId,
252
- flowId,
253
- error: error.message
254
- });
255
-
256
- this.activeExecutions.delete(runId);
257
-
258
- return { runId, status: 'failed', error: error.message };
259
- }
260
- }
261
-
262
- /**
263
- * Phase 4: resume a previously-failed or interrupted run.
264
- *
265
- * Loads the run record + per-node checkpoints, rehydrates the
266
- * execution context with all completed nodes' outputs, and re-runs
267
- * the loop. The standard executeFlow loop skips any node whose ID is
268
- * present in `context.skipCompletedNodeIds`, so completed work isn't
269
- * repeated.
270
- *
271
- * Requires `setCheckpointStore` to have been called — without it,
272
- * there are no per-node checkpoints to read and resume is a no-op
273
- * fall-through.
274
- *
275
- * @param {string} runId
276
- * @param {string|null} sessionId
277
- * @returns {Promise<{ runId, status, output? }>}
278
- */
279
- async resumeFlow(runId, sessionId = null) {
280
- if (!this.checkpointStore) {
281
- throw new Error('Cannot resume: no checkpoint store configured (call setCheckpointStore first)');
282
- }
283
-
284
- const run = await this.stateManager.getFlowRun(runId);
285
- if (!run) throw new Error(`Cannot resume: flow run not found: ${runId}`);
286
- if (run.status === 'completed') {
287
- return { runId, status: 'completed', output: run.output };
288
- }
289
-
290
- const flow = await this.stateManager.getFlow(run.flowId);
291
- if (!flow) throw new Error(`Cannot resume: flow definition not found: ${run.flowId}`);
292
-
293
- // Same schema gate as executeFlow — the flow definition might have
294
- // been edited between runs into an invalid shape.
295
- const validation = validateFlowDefinition(flow);
296
- if (!validation.ok) {
297
- const detail = validation.errors.map(e => e.message).join('; ');
298
- throw new Error(`Cannot resume: flow definition is invalid: ${detail}`);
299
- }
300
-
301
- // Rehydrate completed-node outputs from disk.
302
- const persistedOutputs = await this.checkpointStore.loadAllNodeResults(runId);
303
- const completedIds = new Set(Object.keys(persistedOutputs));
304
- this.logger.info(`Resuming flow ${run.flowId} run ${runId}`, {
305
- completedNodes: completedIds.size,
306
- totalNodes: flow.nodes.length,
307
- });
308
-
309
- this.activeExecutions.set(runId, {
310
- flowId: run.flowId, flow, runId, sessionId,
311
- status: 'running', startedAt: new Date(),
312
- });
313
- await this.stateManager.updateFlowRun(runId, { status: 'running', resumedAt: new Date().toISOString() });
314
- this.broadcastFlowUpdate(sessionId, { type: 'flow_run_resumed', runId, flowId: run.flowId });
315
-
316
- try {
317
- await this.ensureAgentsLoaded(flow.nodes);
318
- const sortedNodes = this.topologicalSort(flow.nodes, flow.edges);
319
- if (sortedNodes.length === 0) throw new Error('Flow has no executable nodes');
320
-
321
- const context = {
322
- input: run.initialInput?.userInput || run.initialInput?.input || '',
323
- nodeOutputs: { ...persistedOutputs },
324
- variables: { ...flow.variables },
325
- sortedNodes,
326
- flow,
327
- skipCompletedNodeIds: completedIds,
328
- };
329
-
330
- for (const node of sortedNodes) {
331
- const execution = this.activeExecutions.get(runId);
332
- if (!execution || execution.status === 'stopped') {
333
- await this.stateManager.updateFlowRun(runId, { status: 'stopped', completedAt: new Date().toISOString() });
334
- this.broadcastFlowUpdate(sessionId, { type: 'flow_run_stopped', runId, flowId: run.flowId });
335
- return { runId, status: 'stopped' };
336
- }
337
- if (completedIds.has(node.id)) {
338
- this.logger.info(`Resume: skipping already-completed node ${node.id}`);
339
- continue;
340
- }
341
- const nodeResult = await this.executeNode(node, context, runId, sessionId, flow);
342
- context.nodeOutputs[node.id] = nodeResult;
343
- if (node.data?.outputKey) context.nodeOutputs[node.data.outputKey] = nodeResult;
344
- await this.updateNodeState(runId, node.id, 'completed', nodeResult);
345
- try { await this.checkpointStore.saveNodeResult(runId, node.id, nodeResult); }
346
- catch (err) { this.logger.warn(`Resume: checkpoint save failed for ${node.id}`, { error: err.message }); }
347
- }
348
-
349
- const finalOutput = this.collectFinalOutput(sortedNodes, context);
350
- await this.stateManager.updateFlowRun(runId, {
351
- status: 'completed', output: finalOutput, completedAt: new Date().toISOString(),
352
- });
353
- this.broadcastFlowUpdate(sessionId, { type: 'flow_run_completed', runId, flowId: run.flowId, output: finalOutput });
354
- this.activeExecutions.delete(runId);
355
- return { runId, status: 'completed', output: finalOutput };
356
- } catch (error) {
357
- this.logger.error(`Flow resume failed: ${run.flowId}`, { runId, error: error.message });
358
- await this.stateManager.updateFlowRun(runId, {
359
- status: 'failed', error: error.message, completedAt: new Date().toISOString(),
360
- });
361
- this.broadcastFlowUpdate(sessionId, { type: 'flow_run_failed', runId, flowId: run.flowId, error: error.message });
362
- this.activeExecutions.delete(runId);
363
- return { runId, status: 'failed', error: error.message };
364
- }
365
- }
366
-
367
- /**
368
- * Stop a flow execution
369
- * @param {string} runId - Run identifier
370
- * @returns {Promise<boolean>} True if stopped
371
- */
372
- async stopExecution(runId) {
373
- const execution = this.activeExecutions.get(runId);
374
- if (!execution) {
375
- return false;
376
- }
377
-
378
- execution.status = 'stopped';
379
-
380
- // Clean up any completion listeners
381
- this.completionListeners.delete(runId);
382
-
383
- return true;
384
- }
385
-
386
- /**
387
- * Ensure all agents referenced in the flow are loaded
388
- * Automatically loads unloaded agents from disk
389
- * @param {Array} nodes - Flow nodes
390
- * @returns {Promise<void>}
391
- */
392
- async ensureAgentsLoaded(nodes) {
393
- if (!nodes || nodes.length === 0) return;
394
-
395
- // First: surface ALL agent nodes that have no agentId at all (e.g.
396
- // a template was loaded but the user hasn't picked agents yet).
397
- // Better one clear "you haven't filled in N agents yet" than a
398
- // confusing chain of partial loads followed by a "name === ''" error.
399
- const unbound = nodes.filter(n =>
400
- n.type === 'agent' &&
401
- (typeof n.data?.agentId !== 'string' || n.data.agentId.trim().length === 0)
402
- );
403
- if (unbound.length > 0) {
404
- const labels = unbound.map(n => n.data?.label || n.id).join(', ');
405
- throw new Error(
406
- `Cannot run flow: ${unbound.length} agent node(s) have no agent assigned (${labels}). Open each node and pick an agent in the properties panel.`
407
- );
408
- }
409
-
410
- // Extract agent IDs from agent nodes
411
- const agentNodes = nodes.filter(n => n.type === 'agent' && n.data?.agentId);
412
- const agentIds = [...new Set(agentNodes.map(n => n.data.agentId))];
413
-
414
- if (agentIds.length === 0) return;
415
-
416
- this.logger.info(`Checking ${agentIds.length} agent(s) for flow execution`);
417
-
418
- // Get project directory from config
419
- const projectDir = this.config.projectDir || process.cwd();
420
-
421
- for (const agentId of agentIds) {
422
- // Check if already loaded
423
- const existingAgent = await this.agentPool.getAgent(agentId);
424
- if (existingAgent) {
425
- this.logger.debug(`Agent ${agentId} already loaded`);
426
- continue;
427
- }
428
-
429
- // Agent not loaded - try to load from disk
430
- this.logger.info(`Loading agent from disk: ${agentId}`);
431
- try {
432
- const agent = await this.stateManager.importArchivedAgent(agentId, projectDir, this.agentPool);
433
- this.logger.info(`Successfully loaded agent: ${agentId}`);
434
-
435
- // Broadcast agent-loaded event so UI sidebar updates.
436
- // Defensive: this is pure UX — it must NEVER kill the load.
437
- // The WebServer instance exposes broadcastToSession(null, msg)
438
- // for "broadcast to all"; some other manager shapes use
439
- // broadcast(msg). Use whichever is available, swallow errors.
440
- if (this.webSocketManager && agent) {
441
- try {
442
- const msg = {
443
- type: 'agent-loaded',
444
- agent: {
445
- id: agent.id,
446
- name: agent.name,
447
- status: agent.status,
448
- model: agent.currentModel || agent.preferredModel,
449
- capabilities: agent.capabilities,
450
- },
451
- };
452
- if (typeof this.webSocketManager.broadcast === 'function') {
453
- this.webSocketManager.broadcast(msg);
454
- } else if (typeof this.webSocketManager.broadcastToSession === 'function') {
455
- this.webSocketManager.broadcastToSession(null, msg);
456
- }
457
- } catch (broadcastErr) {
458
- this.logger.warn(`Failed to broadcast agent-loaded event (non-fatal)`, {
459
- agentId, error: broadcastErr.message,
460
- });
461
- }
462
- }
463
- } catch (error) {
464
- throw new Error(`Failed to load agent ${agentId}: ${error.message}. Make sure the agent exists.`);
465
- }
466
- }
467
- }
468
-
469
- /**
470
- * Get list of unloaded agents referenced in a flow
471
- * Useful for UI to show which agents need to be loaded
472
- * @param {Object} flow - Flow definition
473
- * @returns {Promise<Array>} Array of { agentId, isLoaded, agentInfo }
474
- */
475
- async getFlowAgentStatus(flow) {
476
- if (!flow?.nodes) return [];
477
-
478
- const agentNodes = flow.nodes.filter(n => n.type === 'agent' && n.data?.agentId);
479
- const agentIds = [...new Set(agentNodes.map(n => n.data.agentId))];
480
- const projectDir = this.config.projectDir || process.cwd();
481
-
482
- const results = [];
483
- for (const agentId of agentIds) {
484
- const existingAgent = await this.agentPool.getAgent(agentId);
485
- if (existingAgent) {
486
- results.push({
487
- agentId,
488
- isLoaded: true,
489
- name: existingAgent.name,
490
- model: existingAgent.currentModel
491
- });
492
- } else {
493
- // Try to get info from disk
494
- try {
495
- const metadata = await this.stateManager.getAgentMetadata(agentId, projectDir);
496
- results.push({
497
- agentId,
498
- isLoaded: false,
499
- name: metadata?.name || agentId,
500
- model: metadata?.model
501
- });
502
- } catch {
503
- results.push({
504
- agentId,
505
- isLoaded: false,
506
- name: agentId,
507
- notFound: true
508
- });
509
- }
510
- }
511
- }
512
-
513
- return results;
514
- }
515
-
516
- /**
517
- * Topologically sort nodes based on edges
518
- * @param {Array} nodes - Array of nodes
519
- * @param {Array} edges - Array of edges
520
- * @returns {Array} Sorted nodes array
521
- */
522
- topologicalSort(nodes, edges) {
523
- if (!nodes || nodes.length === 0) {
524
- return [];
525
- }
526
-
527
- const nodeMap = new Map(nodes.map(n => [n.id, n]));
528
- const inDegree = new Map(nodes.map(n => [n.id, 0]));
529
- const adjacency = new Map(nodes.map(n => [n.id, []]));
530
-
531
- // Build adjacency list and calculate in-degrees
532
- for (const edge of (edges || [])) {
533
- const source = edge.source;
534
- const target = edge.target;
535
-
536
- if (adjacency.has(source) && inDegree.has(target)) {
537
- adjacency.get(source).push(target);
538
- inDegree.set(target, inDegree.get(target) + 1);
539
- }
540
- }
541
-
542
- // Find all nodes with in-degree 0 (starting nodes)
543
- const queue = [];
544
- for (const [nodeId, degree] of inDegree) {
545
- if (degree === 0) {
546
- queue.push(nodeId);
547
- }
548
- }
549
-
550
- // Process queue
551
- const sorted = [];
552
- while (queue.length > 0) {
553
- const nodeId = queue.shift();
554
- const node = nodeMap.get(nodeId);
555
- if (node) {
556
- sorted.push(node);
557
- }
558
-
559
- for (const neighbor of adjacency.get(nodeId) || []) {
560
- const newDegree = inDegree.get(neighbor) - 1;
561
- inDegree.set(neighbor, newDegree);
562
- if (newDegree === 0) {
563
- queue.push(neighbor);
564
- }
565
- }
566
- }
567
-
568
- // Check for cycles
569
- if (sorted.length !== nodes.length) {
570
- this.logger.warn('Flow contains cycles, some nodes may not execute');
571
- }
572
-
573
- return sorted;
574
- }
575
-
576
- /**
577
- * Execute a single node
578
- * @param {Object} node - Node to execute
579
- * @param {Object} context - Execution context
580
- * @param {string} runId - Run identifier
581
- * @param {string} sessionId - Session ID for broadcasts
582
- * @param {Object} flow - Flow definition
583
- * @returns {Promise<Object>} Node execution result
584
- */
585
- async executeNode(node, context, runId, sessionId, flow) {
586
- this.logger.info(`Executing node: ${node.id}`, {
587
- type: node.type,
588
- label: node.data?.label
589
- });
590
-
591
- // Update node state to running
592
- await this.updateNodeState(runId, node.id, 'running', null);
593
- this.broadcastFlowUpdate(sessionId, {
594
- type: 'flow_node_started',
595
- runId,
596
- nodeId: node.id,
597
- nodeType: node.type
598
- });
599
-
600
- let result;
601
-
602
- try {
603
- switch (node.type) {
604
- case 'input':
605
- result = await this.executeInputNode(node, context);
606
- break;
607
- case 'agent':
608
- result = await this.executeAgentNode(node, context, runId, sessionId, flow);
609
- break;
610
- case 'output':
611
- result = await this.executeOutputNode(node, context, flow);
612
- break;
613
- default:
614
- throw new Error(`Unknown node type: ${node.type}`);
615
- }
616
-
617
- this.broadcastFlowUpdate(sessionId, {
618
- type: 'flow_node_completed',
619
- runId,
620
- nodeId: node.id,
621
- nodeType: node.type,
622
- output: this.truncateOutput(result)
623
- });
624
-
625
- return result;
626
-
627
- } catch (error) {
628
- // Phase 6.1: capture structured error info — kind classification
629
- // (timeout / agent-error / agent-failure) + per-attempt history
630
- // (from runWithRetry). Persisted on the node state so the run-
631
- // detail UI can show "node-B timed out after 3 attempts" without
632
- // re-parsing log files.
633
- const errorInfo = {
634
- kind: error?.kind || 'agent-error',
635
- message: error?.message || String(error),
636
- attempts: error?.attempts || null,
637
- lastAt: new Date().toISOString(),
638
- };
639
- await this.updateNodeState(runId, node.id, 'failed', { error: error.message }, errorInfo);
640
- this.broadcastFlowUpdate(sessionId, {
641
- type: 'flow_node_failed',
642
- runId,
643
- nodeId: node.id,
644
- nodeType: node.type,
645
- error: error.message,
646
- errorInfo,
647
- });
648
- throw error;
649
- }
650
- }
651
-
652
- /**
653
- * Execute an input node
654
- * @param {Object} node - Input node
655
- * @param {Object} context - Execution context
656
- * @returns {Promise<Object>} Input result
657
- */
658
- async executeInputNode(node, context) {
659
- // Apply prompt template if provided
660
- const template = node.data?.promptTemplate || '{{userInput}}';
661
- const output = this.applyTemplate(template, {
662
- userInput: context.input,
663
- ...context.variables
664
- });
665
-
666
- return {
667
- type: 'input',
668
- output,
669
- raw: context.input
670
- };
671
- }
672
-
673
- /**
674
- * Execute an agent node
675
- * @param {Object} node - Agent node
676
- * @param {Object} context - Execution context
677
- * @param {string} runId - Run identifier
678
- * @param {string} sessionId - Session ID
679
- * @param {Object} flow - Flow definition
680
- * @returns {Promise<Object>} Agent response
681
- */
682
- async executeAgentNode(node, context, runId, sessionId, flow) {
683
- const agentId = node.data?.agentId;
684
- if (!agentId) {
685
- throw new Error(`Agent node ${node.id} has no agent assigned`);
686
- }
687
-
688
- // Get agent
689
- const agent = await this.agentPool.getAgent(agentId);
690
- if (!agent) {
691
- throw new Error(`Agent not found: ${agentId}`);
692
- }
693
-
694
- // ---- Phase 1: typed-input assembly ---------------------------------
695
- // For v2 nodes (with declared inputs[]), assemble typed values from
696
- // upstream outputs by following the edge field mappings. For legacy
697
- // nodes (no inputs[]), this falls back to {input, previousOutput}
698
- // built by concatenating upstream outputs — identical to v1 behavior.
699
- const assembled = assembleNodeInputs(node, flow.edges, context.nodeOutputs);
700
-
701
- if (!assembled.legacy && assembled.missing.length > 0) {
702
- // v2 fail-fast: a required input has no upstream value. Better to
703
- // bail here with a clear error than send the agent half its data.
704
- throw new Error(
705
- `Agent node ${node.id} (${agentId}) is missing required input(s): ${assembled.missing.join(', ')}`
706
- );
707
- }
708
-
709
- // Legacy fallback also exposes inputNodeIds + previousOutput for the
710
- // existing buildPreviousAgentData / context-injection code paths.
711
- const inputNodeIds = this.getInputNodeIds(node.id, flow.edges);
712
- const previousOutput = assembled.legacy
713
- ? assembled.values.previousOutput
714
- : this.collectPreviousOutput(inputNodeIds, context.nodeOutputs);
715
-
716
- // Apply prompt template. Typed input values are exposed BY NAME so
717
- // templates can write {{topic}}, {{research}}, etc. Legacy keys
718
- // {{input}} / {{previousOutput}} stay available for backwards compat.
719
- const template = node.data?.promptTemplate || '{{input}}';
720
- const prompt = this.applyTemplate(template, {
721
- input: previousOutput,
722
- previousOutput,
723
- ...context.variables,
724
- ...context.nodeOutputs,
725
- ...assembled.values, // typed inputs win over collisions
726
- });
727
-
728
- this.logger.info(`Sending message to agent: ${agentId}`, {
729
- promptLength: prompt.length,
730
- nodeId: node.id
731
- });
732
-
733
- // Build flow metadata + previous-agent data ONCE — reused per retry
734
- const sortedNodes = context.sortedNodes || [];
735
- const nodePosition = sortedNodes.findIndex(n => n.id === node.id) + 1;
736
- const flowMetadata = {
737
- flowId: flow.id,
738
- flowName: flow.name || 'Unnamed Flow',
739
- // Phase 7: forward flow.description so system prompt can render
740
- // a "FLOW GOAL" section orienting each agent to the bigger picture.
741
- flowDescription: flow.description,
742
- nodeName: node.data?.label || 'Agent',
743
- nodePosition: nodePosition || 1,
744
- totalNodes: sortedNodes.length
745
- };
746
- const previousAgentData = this.buildPreviousAgentData(node, context, flow);
747
-
748
- // v2: derive the node's typed I/O contract for system-prompt
749
- // advertisement (handled downstream in agentScheduler). v1 nodes
750
- // don't have inputs/outputs declared → contract is undefined and
751
- // the scheduler skips the contract section.
752
- // Phase 7: also forward node.data.instructions (the per-node role
753
- // + success-criteria description) so the prompt can render a
754
- // NODE INSTRUCTIONS section.
755
- const hasTypedIO = Array.isArray(node.inputs) || Array.isArray(node.outputs);
756
- const hasInstructions = typeof node.data?.instructions === 'string' && node.data.instructions.trim().length > 0;
757
- const nodeContract = (hasTypedIO || hasInstructions)
758
- ? {
759
- inputs: node.inputs || [],
760
- outputs: node.outputs || [],
761
- instructions: node.data?.instructions,
762
- // Phase 8: opt-out flag — when true, scheduler keeps the
763
- // agent's native system prompt and APPENDS flow context
764
- // (legacy behavior). When false/absent, scheduler REPLACES
765
- // the system prompt with a flow-worker version, eliminating
766
- // identity conflicts.
767
- useNativeSystemPrompt: node.data?.useNativeSystemPrompt === true,
768
- }
769
- : undefined;
770
-
771
- // ---- Phase 3: per-node retry + per-node timeout -------------------
772
- // The closure below is "one full agent invocation" — clear conv,
773
- // queue message, wait for jobdone, validate outputs. Each retry is
774
- // a brand-new invocation with the agent's conversation reset.
775
- // Precedence: node.execution > flow.execution > config.flows.execution > defaults.
776
- const exec = this._resolveExecutionConfig(node, flow);
777
-
778
- // Store original mode for potential restoration on hard fail (not
779
- // currently restored — same as pre-Phase 3 behavior — but useful
780
- // to capture if we add cleanup).
781
- const originalMode = agent.mode; // eslint-disable-line no-unused-vars
782
-
783
- // Compute the list of missing required outputs given a job-done result.
784
- // Returns [] when contract is satisfied (or no contract). Used both
785
- // inside attemptOnce (to drive in-conversation re-prompts) and once
786
- // more after the retry loop as a defensive belt-and-suspenders check.
787
- const detectMissingOutputs = (result) => {
788
- if (!nodeContract || !Array.isArray(nodeContract.outputs) || nodeContract.outputs.length === 0) return [];
789
- const provided = (result?.outputs && typeof result.outputs === 'object') ? result.outputs : {};
790
- const missing = [];
791
- for (const decl of nodeContract.outputs) {
792
- if (!decl || typeof decl.name !== 'string') continue;
793
- if (!(decl.name in provided) || provided[decl.name] === null || provided[decl.name] === undefined) {
794
- missing.push(decl.name);
795
- }
796
- }
797
- return missing;
798
- };
799
-
800
- // Build a corrective re-prompt that lists EXACTLY which fields are
801
- // missing and shows the JSON shape the agent must emit on the next
802
- // job-done. This is sent in the SAME conversation — the agent keeps
803
- // its working memory and just patches the handoff.
804
- const MAX_REPROMPTS_PER_ATTEMPT = 2;
805
- const buildRepromptMessage = (missing) => {
806
- // Phase 7: enrich the corrective message with each declared
807
- // output's description + example so the agent has the FULL
808
- // contract to satisfy on the retry, not just a list of names.
809
- const required = nodeContract.outputs.map(o => {
810
- const lines = [` • ${o.name}: ${o.type}`];
811
- if (typeof o.description === 'string' && o.description.trim()) {
812
- lines.push(` ${o.description.trim()}`);
813
- }
814
- if (o.example !== undefined && o.example !== null) {
815
- try {
816
- const ex = (typeof o.example === 'string')
817
- ? JSON.stringify(o.example)
818
- : JSON.stringify(o.example, null, 2);
819
- const oneLine = !ex.includes('\n');
820
- lines.push(oneLine ? ` Example: ${ex}` : ` Example: ${ex.split('\n').join('\n ')}`);
821
- } catch { /* ignore unstringifiable examples */ }
822
- }
823
- return lines.join('\n');
824
- }).join('\n');
825
- const example = nodeContract.outputs
826
- .map(o => ` "${o.name}": <${o.type} value>`).join(',\n');
827
- return [
828
- `⚠ Your previous job-done was incomplete. The flow node declares REQUIRED OUTPUTS that must all be present in the "outputs" field of job-done.`,
829
- ``,
830
- `Missing field(s): ${missing.join(', ')}`,
831
- ``,
832
- `All required outputs:`,
833
- required,
834
- ``,
835
- `Please call job-done AGAIN with the complete payload. The "outputs" object must include EVERY field above. Example structure:`,
836
- ``,
837
- `{`,
838
- ` "toolId": "jobdone",`,
839
- ` "actions": [{`,
840
- ` "action": "complete",`,
841
- ` "summary": "<your summary>",`,
842
- ` "outputs": {`,
843
- example,
844
- ` }`,
845
- ` }]`,
846
- `}`,
847
- ``,
848
- `Use the work you've already done — do not redo the task. Just emit the structured outputs.`,
849
- ].join('\n');
850
- };
851
-
852
- const attemptOnce = async (attemptIndex) => {
853
- // Phase 8: register the active contract so the jobdone tool can
854
- // validate the agent's job-done call AT TOOL TIME — rejecting
855
- // partial calls before they propagate. Cleared in the finally
856
- // block of the outer try (see below) when the attempt completes
857
- // (success OR failure).
858
- if (nodeContract) {
859
- this.activeContracts.set(agentId, nodeContract);
860
- }
861
-
862
- // Reset completion state + conversation for a clean slate every
863
- // OUTER retry. In-conversation re-prompts (below) keep history.
864
- agent.autonomousWorkComplete = false;
865
- agent.lastCompletionSummary = null;
866
- agent.lastCompletionDetails = null;
867
- agent.mode = AGENT_MODES.AGENT;
868
- await this.agentPool.persistAgentState(agentId);
869
- try {
870
- await this.agentPool.clearConversation(agentId);
871
- } catch (error) {
872
- this.logger.warn(`Failed to clear conversation for agent ${agentId}`, { error: error.message });
873
- }
874
-
875
- // Register the listener BEFORE queuing the message so a fast jobdone
876
- // can resolve us immediately.
877
- let completionPromise = this.waitForAgentCompletion(agentId, runId);
878
-
879
- await this.messageProcessor.processMessage(agentId, prompt, {
880
- sessionId,
881
- isFlowExecution: true,
882
- flowRunId: runId,
883
- flowNodeId: node.id,
884
- flowMetadata,
885
- previousAgentData,
886
- nodeContract,
887
- attemptIndex,
888
- });
889
-
890
- let result = await completionPromise;
891
-
892
- // v2 in-conversation re-prompt loop: when outputs are missing, ask
893
- // the agent to fix the LAST job-done call WITHOUT clearing memory.
894
- // Cheap (~1 model call vs full re-run) and almost always succeeds
895
- // because the agent already produced the data — they just forgot
896
- // to put it in the outputs field. After MAX_REPROMPTS, we fall
897
- // through to the outer retry which DOES clear conversation.
898
- let reprompts = 0;
899
- while (reprompts < MAX_REPROMPTS_PER_ATTEMPT) {
900
- const missing = detectMissingOutputs(result);
901
- if (missing.length === 0) break;
902
-
903
- this.logger.warn(`Flow node ${node.id}: re-prompting agent for missing outputs`, {
904
- missing, reprompts, attemptIndex,
905
- });
906
-
907
- // Re-arm the listener BEFORE sending — the agent's response will
908
- // resolve it.
909
- agent.autonomousWorkComplete = false;
910
- await this.agentPool.persistAgentState(agentId);
911
- completionPromise = this.waitForAgentCompletion(agentId, runId);
912
-
913
- await this.messageProcessor.processMessage(agentId, buildRepromptMessage(missing), {
914
- sessionId,
915
- isFlowExecution: true,
916
- flowRunId: runId,
917
- flowNodeId: node.id,
918
- flowMetadata,
919
- previousAgentData,
920
- nodeContract,
921
- attemptIndex,
922
- isReprompt: true,
923
- });
924
-
925
- result = await completionPromise;
926
- reprompts++;
927
- }
928
-
929
- // Final check: if still missing after re-prompts, throw a retryable
930
- // error so the OUTER loop (runWithRetry) starts a fresh agent
931
- // invocation with cleared conversation.
932
- const stillMissing = detectMissingOutputs(result);
933
- if (stillMissing.length > 0) {
934
- throw new FlowRetryError(
935
- `Agent ${agentId} (node ${node.id}) job-done is missing required output(s) after ${reprompts} re-prompt(s): ${stillMissing.join(', ')}. ` +
936
- `Declared outputs: ${nodeContract.outputs.map(o => `${o.name}:${o.type}`).join(', ')}`,
937
- 'agent-error'
938
- );
939
- }
940
- return result;
941
- };
942
-
943
- let attemptHistory = [];
944
- let completionResult;
945
- try {
946
- const ran = await runWithRetry(attemptOnce, {
947
- timeoutMs: exec.timeoutMs,
948
- maxRetries: exec.maxRetries,
949
- retryOn: exec.retryOn,
950
- backoffBaseMs: exec.backoffBaseMs,
951
- backoffMultiplier: exec.backoffMultiplier,
952
- onAttempt: (meta) => {
953
- this.logger.warn(`Flow node ${node.id} attempt ${meta.attempt} failed`, {
954
- kind: meta.error?.kind, message: meta.error?.message, willRetry: meta.willRetry,
955
- });
956
- },
957
- });
958
- completionResult = ran.result;
959
- attemptHistory = ran.attempts;
960
- } catch (e) {
961
- // Surface the kind on the thrown error so the caller (executeFlow)
962
- // and observability can distinguish timeout vs agent-error vs
963
- // explicit failure.
964
- const kind = e?.kind || 'agent-error';
965
- const friendly = kind === 'timeout'
966
- ? `Agent ${agentId} timed out (after ${exec.maxRetries + 1} attempt(s))`
967
- : (e?.message || `Agent ${agentId} failed`);
968
- const wrapped = new Error(friendly);
969
- wrapped.kind = kind;
970
- wrapped.attempts = e?.attempts;
971
- // Phase 8: clear active contract on failure so the jobdone tool
972
- // doesn't validate against a stale node when the agent gets used
973
- // outside the flow later.
974
- this.activeContracts.delete(agentId);
975
- throw wrapped;
976
- }
977
- // Phase 8: clear active contract on success too.
978
- this.activeContracts.delete(agentId);
979
-
980
- // Get the agent's response (last assistant message)
981
- const updatedAgent = await this.agentPool.getAgent(agentId);
982
- const lastResponse = this.getLastAssistantMessage(updatedAgent);
983
-
984
- // Extract files created during this agent's execution (from completion result or messages)
985
- const filesCreated = completionResult.filesCreated || this.extractFilesFromCompletion(completionResult);
986
-
987
- // v2: outputs validation already happened inside attemptOnce; this
988
- // block stays only as a defensive belt-and-suspenders for the case
989
- // where attemptOnce somehow returns without outputs. Same logic.
990
- if (nodeContract && Array.isArray(nodeContract.outputs) && nodeContract.outputs.length > 0) {
991
- const provided = (completionResult.outputs && typeof completionResult.outputs === 'object')
992
- ? completionResult.outputs : {};
993
- const missing = [];
994
- for (const decl of nodeContract.outputs) {
995
- if (!decl || typeof decl.name !== 'string') continue;
996
- if (!(decl.name in provided) || provided[decl.name] === null || provided[decl.name] === undefined) {
997
- missing.push(decl.name);
998
- }
999
- }
1000
- if (missing.length > 0) {
1001
- throw new Error(
1002
- `Agent ${agentId} (node ${node.id}) job-done is missing required output(s): ${missing.join(', ')}. ` +
1003
- `Declared outputs: ${nodeContract.outputs.map(o => `${o.name}:${o.type}`).join(', ')}`
1004
- );
1005
- }
1006
- }
1007
-
1008
- // Choose what becomes the legacy `output` field used by v1 handoff
1009
- // and the prose preview in the next agent's system prompt.
1010
- //
1011
- // Preference order:
1012
- // 1. summary + details (if both present, glue them)
1013
- // 2. lastResponse (the agent's full final assistant message)
1014
- // 3. summary alone
1015
- // 4. ''
1016
- //
1017
- // Why: tiny "Done." summaries used to flow through as the next
1018
- // agent's entire context — agents had nothing to work with. Falling
1019
- // back to the assistant's last message gives the next agent a real
1020
- // payload even when the upstream agent skipped `details`.
1021
- const summary = (completionResult.summary || '').trim();
1022
- const details = (completionResult.details || '').trim();
1023
- let prose;
1024
- if (summary && details) prose = `${summary}\n\n${details}`;
1025
- else if (summary && summary.length < 40 && lastResponse) prose = lastResponse;
1026
- else prose = summary || lastResponse || '';
1027
-
1028
- return {
1029
- type: 'agent',
1030
- agentId,
1031
- agentName: agent.name,
1032
- output: prose,
1033
- // v2: structured outputs bag — keyed by declared field name. This
1034
- // is what assembleNodeInputs reads when wiring the next node.
1035
- outputs: completionResult.outputs,
1036
- details: completionResult.details,
1037
- filesCreated,
1038
- // Phase 3: attempt history (one entry per try; failed entries
1039
- // include { kind, message }). Surfaces in run-detail UI later.
1040
- attempts: attemptHistory,
1041
- success: completionResult.success !== false
1042
- };
1043
- }
1044
-
1045
- /**
1046
- * Resolve effective per-node execution config — timeout, retries,
1047
- * backoff, retryOn classes. Precedence (highest wins):
1048
- * node.execution > flow.execution > config.flows.execution > defaults
1049
- *
1050
- * Defaults:
1051
- * timeoutMs 300000 (5min, matches pre-Phase-3 behavior; legacy
1052
- * config.flows.nodeTimeout still honored as a global
1053
- * timeout for back-compat)
1054
- * maxRetries 1 (one fresh-conversation retry after re-prompts
1055
- * exhaust — without this, structured-output
1056
- * misses by some models cause flow failure
1057
- * after only 2 in-conversation re-prompts).
1058
- * Override per node to 0 to disable retries.
1059
- * retryOn ['timeout', 'agent-error']
1060
- * backoffBaseMs 1000
1061
- * backoffMultiplier 2
1062
- */
1063
- _resolveExecutionConfig(node, flow) {
1064
- const globalCfg = this.config?.flows?.execution || {};
1065
- const flowCfg = flow?.execution || {};
1066
- const nodeCfg = node?.execution || {};
1067
- // Legacy: config.flows.nodeTimeout was the only knob in v1. Keep
1068
- // it as the default timeout if more-specific configs aren't set.
1069
- const legacyTimeout = this.config?.flows?.nodeTimeout;
1070
-
1071
- const pick = (k, fallback) =>
1072
- nodeCfg[k] !== undefined ? nodeCfg[k] :
1073
- flowCfg[k] !== undefined ? flowCfg[k] :
1074
- globalCfg[k] !== undefined ? globalCfg[k] :
1075
- fallback;
1076
-
1077
- return {
1078
- timeoutMs: pick('timeoutMs', legacyTimeout ?? 300000),
1079
- maxRetries: pick('maxRetries', 1),
1080
- retryOn: pick('retryOn', ['timeout', 'agent-error']),
1081
- backoffBaseMs: pick('backoffBaseMs', 1000),
1082
- backoffMultiplier: pick('backoffMultiplier', 2),
1083
- };
1084
- }
1085
-
1086
- /**
1087
- * Build previous agent data for context passing to next agent
1088
- * @param {Object} currentNode - Current agent node
1089
- * @param {Object} context - Execution context with nodeOutputs
1090
- * @param {Object} flow - Flow definition
1091
- * @returns {Object|null} Previous agent data or null if first agent
1092
- */
1093
- buildPreviousAgentData(currentNode, context, flow) {
1094
- // Find the previous agent node that feeds into this one
1095
- const inputNodeIds = this.getInputNodeIds(currentNode.id, flow.edges);
1096
-
1097
- // Look for the most recent agent node in the inputs.
1098
- // We collect ALL upstream agent contributions (not just one) so a
1099
- // node fed by multiple agents gets a merged outputs bag and a
1100
- // labeled list of contributors. v1 fallback: pick the first.
1101
- const contributors = [];
1102
- for (const inputId of inputNodeIds) {
1103
- const nodeOutput = context.nodeOutputs[inputId];
1104
- if (nodeOutput && nodeOutput.type === 'agent') contributors.push(nodeOutput);
1105
- }
1106
- if (contributors.length === 0) return null;
1107
-
1108
- // Merge structured outputs from all contributors. When the same
1109
- // field name appears on multiple upstream agents, the LATER one
1110
- // wins (sortedNodes order in context.sortedNodes is topological).
1111
- const mergedOutputs = {};
1112
- for (const c of contributors) {
1113
- if (c.outputs && typeof c.outputs === 'object') {
1114
- Object.assign(mergedOutputs, c.outputs);
1115
- }
1116
- }
1117
-
1118
- // Primary "previous agent" used for the legacy fields stays the
1119
- // most recent contributor (last one in topo order).
1120
- const primary = contributors[contributors.length - 1];
1121
-
1122
- return {
1123
- agentId: primary.agentId,
1124
- agentName: primary.agentName || primary.agentId,
1125
- summary: primary.output,
1126
- details: primary.details,
1127
- filesCreated: primary.filesCreated || [],
1128
- output: primary.output,
1129
- // Phase 5/6 fix: forward the structured outputs bag end-to-end.
1130
- // Without this the next agent never sees the typed handoff —
1131
- // only the free-text summary, defeating the whole v2 contract.
1132
- outputs: Object.keys(mergedOutputs).length > 0 ? mergedOutputs : undefined,
1133
- // When multiple agents fed this node, list them so the system
1134
- // prompt can show "you received outputs from agents A, B".
1135
- contributors: contributors.length > 1
1136
- ? contributors.map(c => ({
1137
- agentId: c.agentId,
1138
- agentName: c.agentName || c.agentId,
1139
- outputs: c.outputs || null,
1140
- }))
1141
- : undefined,
1142
- };
1143
- }
1144
-
1145
- /**
1146
- * Extract file paths from completion result
1147
- * Parses the summary/details for file path mentions
1148
- * @param {Object} completionResult - Job-done completion result
1149
- * @returns {Array<string>} Array of file paths
1150
- */
1151
- extractFilesFromCompletion(completionResult) {
1152
- const files = new Set();
1153
-
1154
- const textToSearch = [
1155
- completionResult.summary || '',
1156
- completionResult.details || '',
1157
- typeof completionResult.output === 'string' ? completionResult.output : ''
1158
- ].join(' ');
1159
-
1160
- // Common patterns for file paths
1161
- const patterns = [
1162
- /(?:created|wrote|saved|generated|modified|updated)\s+(?:file\s+)?["']?([\/\\][\w\-\.\/\\]+\.\w+)["']?/gi,
1163
- /(?:at|to|in)\s+["']?([\/\\][\w\-\.\/\\]+\.\w+)["']?/gi,
1164
- /File\s+(?:created|written|saved):\s*([\/\\][\w\-\.\/\\]+\.\w+)/gi
1165
- ];
1166
-
1167
- for (const pattern of patterns) {
1168
- let match;
1169
- while ((match = pattern.exec(textToSearch)) !== null) {
1170
- const path = match[1];
1171
- if (path && !path.includes('http') && path.length > 3) {
1172
- files.add(path);
1173
- }
1174
- }
1175
- }
1176
-
1177
- return Array.from(files);
1178
- }
1179
-
1180
- /**
1181
- * Execute an output node
1182
- * @param {Object} node - Output node
1183
- * @param {Object} context - Execution context
1184
- * @param {Object} flow - Flow definition
1185
- * @returns {Promise<Object>} Output result
1186
- */
1187
- async executeOutputNode(node, context, flow) {
1188
- // Collect all previous outputs
1189
- const inputNodeIds = this.getInputNodeIds(node.id, flow.edges);
1190
- const output = this.collectPreviousOutput(inputNodeIds, context.nodeOutputs);
1191
-
1192
- // Apply output format
1193
- const format = node.data?.outputFormat || 'text';
1194
- let formattedOutput;
1195
-
1196
- switch (format) {
1197
- case 'json':
1198
- formattedOutput = typeof output === 'object' ? output : { result: output };
1199
- break;
1200
- case 'markdown':
1201
- formattedOutput = typeof output === 'string' ? output : JSON.stringify(output, null, 2);
1202
- break;
1203
- case 'text':
1204
- default:
1205
- formattedOutput = typeof output === 'string' ? output : JSON.stringify(output);
1206
- break;
1207
- }
1208
-
1209
- return {
1210
- type: 'output',
1211
- format,
1212
- output: formattedOutput
1213
- };
1214
- }
1215
-
1216
- /**
1217
- * Wait for an agent to complete via jobdone tool.
1218
- * Completion is detected two ways:
1219
- * 1. Direct signal: notifyAgentCompletion() called from jobDoneTool
1220
- * 2. Polling fallback: checks autonomousWorkComplete flag every 2s
1221
- * @param {string} agentId - Agent ID
1222
- * @param {string} runId - Run ID
1223
- * @returns {Promise<Object>} Completion result
1224
- */
1225
- waitForAgentCompletion(agentId, runId) {
1226
- return new Promise((resolve) => {
1227
- const key = `${runId}-${agentId}`;
1228
- let checkInterval = null;
1229
-
1230
- const onComplete = (result) => {
1231
- if (checkInterval) clearInterval(checkInterval);
1232
- this.completionListeners.delete(key);
1233
- resolve(result);
1234
- };
1235
-
1236
- // Store listener so notifyAgentCompletion() can resolve directly
1237
- this.completionListeners.set(key, {
1238
- resolve: onComplete,
1239
- agentId,
1240
- runId
1241
- });
1242
-
1243
- // Polling fallback: check autonomousWorkComplete flag
1244
- checkInterval = setInterval(async () => {
1245
- try {
1246
- const agent = await this.agentPool.getAgent(agentId);
1247
- if (agent && agent.autonomousWorkComplete) {
1248
- onComplete({
1249
- completed: true,
1250
- summary: agent.lastCompletionSummary || '',
1251
- details: agent.lastCompletionDetails || null,
1252
- success: true
1253
- });
1254
- }
1255
- } catch (error) {
1256
- // Ignore errors during polling
1257
- }
1258
- }, 2000);
1259
-
1260
- // Clean up interval on timeout
1261
- setTimeout(() => {
1262
- if (checkInterval) clearInterval(checkInterval);
1263
- }, this.config.flows?.nodeTimeout || 300000);
1264
- });
1265
- }
1266
-
1267
- /**
1268
- * Notify that an agent has completed its work (called from jobDoneTool).
1269
- * Directly resolves the completion promise instead of waiting for next poll.
1270
- * @param {string} agentId - Agent ID
1271
- * @param {Object} completionData - { summary, success, details }
1272
- */
1273
- notifyAgentCompletion(agentId, completionData = {}) {
1274
- // Find any listener waiting for this agent
1275
- for (const [key, listener] of this.completionListeners.entries()) {
1276
- if (listener.agentId === agentId) {
1277
- this.logger.info(`Flow: Agent ${agentId} completed via direct signal`, {
1278
- runId: listener.runId,
1279
- success: completionData.success
1280
- });
1281
- listener.resolve({
1282
- completed: true,
1283
- summary: completionData.summary || '',
1284
- details: completionData.details || null,
1285
- // v2: structured outputs bag — forwarded through to the
1286
- // executor's contract validator. v1 callers don't set this.
1287
- outputs: completionData.outputs,
1288
- success: completionData.success !== false
1289
- });
1290
- return true;
1291
- }
1292
- }
1293
- return false; // No listener found (agent not in a flow)
1294
- }
1295
-
1296
- /**
1297
- * Create a timeout promise
1298
- * @param {number} ms - Timeout in milliseconds
1299
- * @returns {Promise<Object>} Timeout result
1300
- */
1301
- createTimeoutPromise(ms) {
1302
- return new Promise((resolve) => {
1303
- setTimeout(() => {
1304
- resolve({ timeout: true });
1305
- }, ms);
1306
- });
1307
- }
1308
-
1309
- /**
1310
- * Get IDs of nodes that connect to a target node
1311
- * @param {string} targetNodeId - Target node ID
1312
- * @param {Array} edges - Flow edges
1313
- * @returns {Array} Source node IDs
1314
- */
1315
- getInputNodeIds(targetNodeId, edges) {
1316
- return (edges || [])
1317
- .filter(e => e.target === targetNodeId)
1318
- .map(e => e.source);
1319
- }
1320
-
1321
- /**
1322
- * Collect output from previous nodes
1323
- * @param {Array} nodeIds - Node IDs to collect from
1324
- * @param {Object} nodeOutputs - Map of node outputs
1325
- * @returns {string} Combined output
1326
- */
1327
- collectPreviousOutput(nodeIds, nodeOutputs) {
1328
- const outputs = nodeIds
1329
- .map(id => nodeOutputs[id])
1330
- .filter(o => o)
1331
- .map(o => o.output || o)
1332
- .filter(o => o);
1333
-
1334
- if (outputs.length === 0) return '';
1335
- if (outputs.length === 1) return outputs[0];
1336
-
1337
- // Combine multiple outputs
1338
- return outputs.join('\n\n---\n\n');
1339
- }
1340
-
1341
- /**
1342
- * Collect final output from output nodes
1343
- * @param {Array} sortedNodes - Sorted nodes
1344
- * @param {Object} context - Execution context
1345
- * @returns {Object} Final output
1346
- */
1347
- collectFinalOutput(sortedNodes, context) {
1348
- const outputNodes = sortedNodes.filter(n => n.type === 'output');
1349
-
1350
- if (outputNodes.length === 0) {
1351
- // No output node, return last node's output
1352
- const lastNode = sortedNodes[sortedNodes.length - 1];
1353
- return context.nodeOutputs[lastNode?.id]?.output || null;
1354
- }
1355
-
1356
- if (outputNodes.length === 1) {
1357
- return context.nodeOutputs[outputNodes[0].id]?.output;
1358
- }
1359
-
1360
- // Multiple output nodes
1361
- const outputs = {};
1362
- for (const node of outputNodes) {
1363
- const key = node.data?.label || node.id;
1364
- outputs[key] = context.nodeOutputs[node.id]?.output;
1365
- }
1366
- return outputs;
1367
- }
1368
-
1369
- /**
1370
- * Apply template with variable substitution
1371
- * @param {string} template - Template string
1372
- * @param {Object} variables - Variables to substitute
1373
- * @returns {string} Processed template
1374
- */
1375
- applyTemplate(template, variables) {
1376
- let result = template;
1377
-
1378
- for (const [key, value] of Object.entries(variables)) {
1379
- const placeholder = `{{${key}}}`;
1380
- const valueStr = typeof value === 'object'
1381
- ? JSON.stringify(value)
1382
- : String(value || '');
1383
- result = result.split(placeholder).join(valueStr);
1384
- }
1385
-
1386
- return result;
1387
- }
1388
-
1389
- /**
1390
- * Update node state in the run record.
1391
- *
1392
- * Phase 6.1: optional `errorInfo` captures classified failure detail
1393
- * (kind, message, attempts) for run-detail diagnostics. Stored as
1394
- * `nodeStates[id].error` alongside the truncated result.
1395
- *
1396
- * @param {string} runId
1397
- * @param {string} nodeId
1398
- * @param {string} status 'running' | 'completed' | 'failed'
1399
- * @param {Object} result
1400
- * @param {Object} [errorInfo] { kind, message, attempts, lastAt }
1401
- */
1402
- async updateNodeState(runId, nodeId, status, result, errorInfo) {
1403
- const run = await this.stateManager.getFlowRun(runId);
1404
- if (!run) return;
1405
-
1406
- const nodeStates = run.nodeStates || {};
1407
- const entry = {
1408
- status,
1409
- result: result ? this.truncateOutput(result) : null,
1410
- updatedAt: new Date().toISOString()
1411
- };
1412
- if (errorInfo && typeof errorInfo === 'object') {
1413
- entry.error = errorInfo;
1414
- }
1415
- nodeStates[nodeId] = entry;
1416
-
1417
- await this.stateManager.updateFlowRun(runId, { nodeStates });
1418
- }
1419
-
1420
- /**
1421
- * Get the last assistant message from an agent
1422
- * @param {Object} agent - Agent object
1423
- * @returns {string|null} Last assistant message content
1424
- */
1425
- getLastAssistantMessage(agent) {
1426
- const messages = agent?.conversations?.full?.messages || [];
1427
- for (let i = messages.length - 1; i >= 0; i--) {
1428
- if (messages[i].role === 'assistant') {
1429
- return messages[i].content;
1430
- }
1431
- }
1432
- return null;
1433
- }
1434
-
1435
- /**
1436
- * Truncate output for storage/transmission.
1437
- *
1438
- * Special-case: when the output is an agent completion result with a
1439
- * structured `outputs` field, preserve the `outputs` object verbatim.
1440
- * Those are the typed contract values that downstream nodes consume —
1441
- * truncating them silently breaks edge field-mapping (`writer.bullets
1442
- * → critic.bullets` would deliver a string-truncation marker instead
1443
- * of the real list). Long prose lives in `output` / `summary` /
1444
- * `details`; those get the legacy string truncation.
1445
- *
1446
- * @param {Object} output - Output to truncate
1447
- * @returns {Object} Truncated output
1448
- */
1449
- truncateOutput(output) {
1450
- if (typeof output === 'string' && output.length > 1000) {
1451
- return output.substring(0, 1000) + '... (truncated)';
1452
- }
1453
- if (output && typeof output === 'object') {
1454
- // Agent-completion shape: preserve structured `outputs`, truncate
1455
- // only the long prose fields. Keeps the run dump useful for
1456
- // post-mortem inspection AND keeps downstream edges working.
1457
- const hasStructuredOutputs = output.outputs && typeof output.outputs === 'object';
1458
- if (hasStructuredOutputs) {
1459
- const trunc = { ...output };
1460
- for (const k of ['output', 'summary', 'details']) {
1461
- if (typeof trunc[k] === 'string' && trunc[k].length > 1000) {
1462
- trunc[k] = trunc[k].substring(0, 1000) + '... (truncated)';
1463
- }
1464
- }
1465
- // outputs object preserved as-is; it's structured contract data.
1466
- return trunc;
1467
- }
1468
- const str = JSON.stringify(output);
1469
- if (str.length > 1000) {
1470
- return { truncated: true, preview: str.substring(0, 1000) };
1471
- }
1472
- }
1473
- return output;
1474
- }
1475
-
1476
- /**
1477
- * Broadcast flow update via WebSocket
1478
- * @param {string} sessionId - Session ID (null broadcasts to all)
1479
- * @param {Object} data - Update data
1480
- */
1481
- broadcastFlowUpdate(sessionId, data) {
1482
- if (!this.webSocketManager) return;
1483
-
1484
- try {
1485
- // broadcastToSession with null sessionId broadcasts to all connections
1486
- this.webSocketManager.broadcastToSession(sessionId, {
1487
- type: 'flow_update',
1488
- data,
1489
- timestamp: new Date().toISOString()
1490
- });
1491
- } catch (error) {
1492
- this.logger.warn('Failed to broadcast flow update', { error: error.message });
1493
- }
1494
- }
1495
-
1496
- /**
1497
- * Get active executions
1498
- * @returns {Array} Active execution info
1499
- */
1500
- getActiveExecutions() {
1501
- return Array.from(this.activeExecutions.entries()).map(([runId, exec]) => ({
1502
- runId,
1503
- flowId: exec.flowId,
1504
- status: exec.status,
1505
- startedAt: exec.startedAt
1506
- }));
1507
- }
1508
- }
1509
-
1510
- export default FlowExecutor;
1
+ /**
2
+ * FlowExecutor - Executes visual flow pipelines
3
+ *
4
+ * Purpose:
5
+ * - Execute flow nodes in topologically sorted order
6
+ * - Queue messages to agents via MessageProcessor
7
+ * - Detect completion via jobdone tool results
8
+ * - Track node states and outputs
9
+ * - Broadcast execution progress via WebSocket
10
+ */
11
+
12
+ import { AGENT_MODES } from '../utilities/constants.js';
13
+ import { validateFlowDefinition } from './flowSchema.js';
14
+ import { assembleNodeInputs } from './flowFieldMapping.js';
15
+ import { runWithRetry, FlowRetryError } from './flowRetry.js';
16
+
17
+ class FlowExecutor {
18
+ constructor(config, logger, stateManager, agentPool, messageProcessor) {
19
+ this.config = config;
20
+ this.logger = logger;
21
+ this.stateManager = stateManager;
22
+ this.agentPool = agentPool;
23
+ this.messageProcessor = messageProcessor;
24
+
25
+ // WebSocket manager for broadcasting progress
26
+ this.webSocketManager = null;
27
+
28
+ // Active flow executions
29
+ this.activeExecutions = new Map();
30
+
31
+ // Completion listeners per run
32
+ this.completionListeners = new Map();
33
+
34
+ // Phase 8: agentId → currently-awaited node contract.
35
+ // Set when an agent is invoked inside a flow node; cleared when
36
+ // the await resolves. The jobdone tool reads this to validate
37
+ // the agent's job-done call AT TOOL TIME — rejecting partial
38
+ // calls before they propagate into the executor.
39
+ this.activeContracts = new Map();
40
+
41
+ // Optional disk checkpointing — when set, every successful node
42
+ // persists its result so the run can resume after a crash. Off by
43
+ // default (preserves pre-Phase-4 behavior; opt in via setCheckpointStore).
44
+ this.checkpointStore = null;
45
+ }
46
+
47
+ /**
48
+ * Phase 8: get the currently-awaited node contract for an agent (if any).
49
+ * Returns the contract object { inputs, outputs, instructions, ... } so
50
+ * the jobdone tool can validate the agent's call against it AT TOOL TIME
51
+ * — much faster recovery than waiting for executor-level re-prompts.
52
+ *
53
+ * @param {string} agentId
54
+ * @returns {object|null} the contract, or null when the agent isn't in
55
+ * a flow step right now.
56
+ */
57
+ getActiveContract(agentId) {
58
+ return this.activeContracts.get(agentId) || null;
59
+ }
60
+
61
+ /**
62
+ * Phase 4: enable disk checkpoints + resume by injecting a store.
63
+ * Calling with null disables checkpointing.
64
+ */
65
+ setCheckpointStore(store) {
66
+ this.checkpointStore = store || null;
67
+ }
68
+
69
+ /**
70
+ * Set WebSocket manager for real-time updates
71
+ * @param {Object} webSocketManager - WebSocket manager instance
72
+ */
73
+ setWebSocketManager(webSocketManager) {
74
+ this.webSocketManager = webSocketManager;
75
+ }
76
+
77
+ /**
78
+ * Execute a flow
79
+ * @param {string} flowId - Flow identifier
80
+ * @param {Object} initialInput - Initial input data
81
+ * @param {string} sessionId - WebSocket session ID for broadcasts
82
+ * @returns {Promise<Object>} Flow run result
83
+ */
84
+ async executeFlow(flowId, initialInput = {}, sessionId = null) {
85
+ // Load flow definition
86
+ const flow = await this.stateManager.getFlow(flowId);
87
+ if (!flow) {
88
+ throw new Error(`Flow not found: ${flowId}`);
89
+ }
90
+
91
+ // Belt-and-suspenders schema check. The POST/PUT routes already gate
92
+ // on this, but flows can also be authored by importing JSON files
93
+ // directly into the index, or by older clients that bypassed the
94
+ // route. Catching here means we never queue agents on a flow that
95
+ // we can't actually execute. Errors include path so the caller can
96
+ // surface them in the run-detail view.
97
+ const validation = validateFlowDefinition(flow);
98
+ if (!validation.ok) {
99
+ const detail = validation.errors.map(e => e.message).join('; ');
100
+ this.logger.warn(`Flow ${flowId} failed schema validation; refusing to execute`, {
101
+ errors: validation.errors,
102
+ });
103
+ throw new Error(`Flow definition is invalid: ${detail}`);
104
+ }
105
+
106
+ // Create flow run record
107
+ const run = await this.stateManager.createFlowRun(flowId, initialInput);
108
+ const runId = run.id;
109
+
110
+ this.logger.info(`Starting flow execution: ${flowId}`, {
111
+ runId,
112
+ nodeCount: flow.nodes?.length || 0,
113
+ edgeCount: flow.edges?.length || 0
114
+ });
115
+
116
+ // Track this execution
117
+ this.activeExecutions.set(runId, {
118
+ flowId,
119
+ flow,
120
+ runId,
121
+ sessionId,
122
+ status: 'running',
123
+ startedAt: new Date()
124
+ });
125
+
126
+ // Update run status to running. Phase 6.3: stamp the flow's
127
+ // current version onto the run so we can later answer "which
128
+ // definition produced this output?" — important after rollbacks.
129
+ await this.stateManager.updateFlowRun(runId, {
130
+ status: 'running',
131
+ flowVersion: flow.version ?? null,
132
+ });
133
+ const startedAt = new Date().toISOString();
134
+ this.broadcastFlowUpdate(sessionId, {
135
+ type: 'flow_run_started',
136
+ runId,
137
+ flowId,
138
+ status: 'running',
139
+ startedAt
140
+ });
141
+
142
+ try {
143
+ // Ensure all referenced agents are loaded before execution
144
+ await this.ensureAgentsLoaded(flow.nodes);
145
+
146
+ // Topologically sort nodes
147
+ const sortedNodes = this.topologicalSort(flow.nodes, flow.edges);
148
+
149
+ if (sortedNodes.length === 0) {
150
+ throw new Error('Flow has no executable nodes');
151
+ }
152
+
153
+ // Initialize execution context
154
+ const context = {
155
+ input: initialInput.userInput || initialInput.input || '',
156
+ nodeOutputs: {},
157
+ variables: { ...flow.variables },
158
+ sortedNodes, // Store for position tracking
159
+ flow // Store flow reference
160
+ };
161
+
162
+ // Execute nodes in order
163
+ for (const node of sortedNodes) {
164
+ // Check if execution was stopped
165
+ const execution = this.activeExecutions.get(runId);
166
+ if (!execution || execution.status === 'stopped') {
167
+ this.logger.info(`Flow execution stopped: ${runId}`);
168
+ await this.stateManager.updateFlowRun(runId, {
169
+ status: 'stopped',
170
+ completedAt: new Date().toISOString()
171
+ });
172
+ this.broadcastFlowUpdate(sessionId, {
173
+ type: 'flow_run_stopped',
174
+ runId,
175
+ flowId
176
+ });
177
+ return { runId, status: 'stopped' };
178
+ }
179
+
180
+ // Phase 4: skip nodes already completed in a previous run that
181
+ // we're resuming from. Their outputs were rehydrated into
182
+ // context.nodeOutputs by resumeFlow before the loop started.
183
+ if (context.skipCompletedNodeIds?.has?.(node.id)) {
184
+ this.logger.info(`Flow ${flowId}: skipping already-completed node ${node.id} on resume`);
185
+ continue;
186
+ }
187
+
188
+ // Execute node
189
+ const nodeResult = await this.executeNode(node, context, runId, sessionId, flow);
190
+
191
+ // Store node output by node ID
192
+ context.nodeOutputs[node.id] = nodeResult;
193
+
194
+ // Also store by outputKey if specified (for named references like {{result}})
195
+ if (node.data?.outputKey) {
196
+ context.nodeOutputs[node.data.outputKey] = nodeResult;
197
+ }
198
+
199
+ // Update node state in run
200
+ await this.updateNodeState(runId, node.id, 'completed', nodeResult);
201
+
202
+ // Phase 4: persist a checkpoint after each successful node so a
203
+ // resume can skip back here. Best-effort — checkpoint failures
204
+ // don't fail the run (we'd rather complete without resumability
205
+ // than blow up because disk is full).
206
+ if (this.checkpointStore) {
207
+ try {
208
+ await this.checkpointStore.saveNodeResult(runId, node.id, nodeResult);
209
+ } catch (err) {
210
+ this.logger.warn(`Flow ${flowId}: failed to checkpoint node ${node.id}`, { error: err.message });
211
+ }
212
+ }
213
+ }
214
+
215
+ // Flow completed successfully
216
+ const finalOutput = this.collectFinalOutput(sortedNodes, context);
217
+
218
+ await this.stateManager.updateFlowRun(runId, {
219
+ status: 'completed',
220
+ output: finalOutput,
221
+ completedAt: new Date().toISOString()
222
+ });
223
+
224
+ this.broadcastFlowUpdate(sessionId, {
225
+ type: 'flow_run_completed',
226
+ runId,
227
+ flowId,
228
+ output: finalOutput
229
+ });
230
+
231
+ this.activeExecutions.delete(runId);
232
+
233
+ this.logger.info(`Flow execution completed: ${flowId}`, { runId });
234
+
235
+ return { runId, status: 'completed', output: finalOutput };
236
+
237
+ } catch (error) {
238
+ this.logger.error(`Flow execution failed: ${flowId}`, {
239
+ runId,
240
+ error: error.message
241
+ });
242
+
243
+ await this.stateManager.updateFlowRun(runId, {
244
+ status: 'failed',
245
+ error: error.message,
246
+ completedAt: new Date().toISOString()
247
+ });
248
+
249
+ this.broadcastFlowUpdate(sessionId, {
250
+ type: 'flow_run_failed',
251
+ runId,
252
+ flowId,
253
+ error: error.message
254
+ });
255
+
256
+ this.activeExecutions.delete(runId);
257
+
258
+ return { runId, status: 'failed', error: error.message };
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Phase 4: resume a previously-failed or interrupted run.
264
+ *
265
+ * Loads the run record + per-node checkpoints, rehydrates the
266
+ * execution context with all completed nodes' outputs, and re-runs
267
+ * the loop. The standard executeFlow loop skips any node whose ID is
268
+ * present in `context.skipCompletedNodeIds`, so completed work isn't
269
+ * repeated.
270
+ *
271
+ * Requires `setCheckpointStore` to have been called — without it,
272
+ * there are no per-node checkpoints to read and resume is a no-op
273
+ * fall-through.
274
+ *
275
+ * @param {string} runId
276
+ * @param {string|null} sessionId
277
+ * @returns {Promise<{ runId, status, output? }>}
278
+ */
279
+ async resumeFlow(runId, sessionId = null) {
280
+ if (!this.checkpointStore) {
281
+ throw new Error('Cannot resume: no checkpoint store configured (call setCheckpointStore first)');
282
+ }
283
+
284
+ const run = await this.stateManager.getFlowRun(runId);
285
+ if (!run) throw new Error(`Cannot resume: flow run not found: ${runId}`);
286
+ if (run.status === 'completed') {
287
+ return { runId, status: 'completed', output: run.output };
288
+ }
289
+
290
+ const flow = await this.stateManager.getFlow(run.flowId);
291
+ if (!flow) throw new Error(`Cannot resume: flow definition not found: ${run.flowId}`);
292
+
293
+ // Same schema gate as executeFlow — the flow definition might have
294
+ // been edited between runs into an invalid shape.
295
+ const validation = validateFlowDefinition(flow);
296
+ if (!validation.ok) {
297
+ const detail = validation.errors.map(e => e.message).join('; ');
298
+ throw new Error(`Cannot resume: flow definition is invalid: ${detail}`);
299
+ }
300
+
301
+ // Rehydrate completed-node outputs from disk.
302
+ const persistedOutputs = await this.checkpointStore.loadAllNodeResults(runId);
303
+ const completedIds = new Set(Object.keys(persistedOutputs));
304
+ this.logger.info(`Resuming flow ${run.flowId} run ${runId}`, {
305
+ completedNodes: completedIds.size,
306
+ totalNodes: flow.nodes.length,
307
+ });
308
+
309
+ this.activeExecutions.set(runId, {
310
+ flowId: run.flowId, flow, runId, sessionId,
311
+ status: 'running', startedAt: new Date(),
312
+ });
313
+ await this.stateManager.updateFlowRun(runId, { status: 'running', resumedAt: new Date().toISOString() });
314
+ this.broadcastFlowUpdate(sessionId, { type: 'flow_run_resumed', runId, flowId: run.flowId });
315
+
316
+ try {
317
+ await this.ensureAgentsLoaded(flow.nodes);
318
+ const sortedNodes = this.topologicalSort(flow.nodes, flow.edges);
319
+ if (sortedNodes.length === 0) throw new Error('Flow has no executable nodes');
320
+
321
+ const context = {
322
+ input: run.initialInput?.userInput || run.initialInput?.input || '',
323
+ nodeOutputs: { ...persistedOutputs },
324
+ variables: { ...flow.variables },
325
+ sortedNodes,
326
+ flow,
327
+ skipCompletedNodeIds: completedIds,
328
+ };
329
+
330
+ for (const node of sortedNodes) {
331
+ const execution = this.activeExecutions.get(runId);
332
+ if (!execution || execution.status === 'stopped') {
333
+ await this.stateManager.updateFlowRun(runId, { status: 'stopped', completedAt: new Date().toISOString() });
334
+ this.broadcastFlowUpdate(sessionId, { type: 'flow_run_stopped', runId, flowId: run.flowId });
335
+ return { runId, status: 'stopped' };
336
+ }
337
+ if (completedIds.has(node.id)) {
338
+ this.logger.info(`Resume: skipping already-completed node ${node.id}`);
339
+ continue;
340
+ }
341
+ const nodeResult = await this.executeNode(node, context, runId, sessionId, flow);
342
+ context.nodeOutputs[node.id] = nodeResult;
343
+ if (node.data?.outputKey) context.nodeOutputs[node.data.outputKey] = nodeResult;
344
+ await this.updateNodeState(runId, node.id, 'completed', nodeResult);
345
+ try { await this.checkpointStore.saveNodeResult(runId, node.id, nodeResult); }
346
+ catch (err) { this.logger.warn(`Resume: checkpoint save failed for ${node.id}`, { error: err.message }); }
347
+ }
348
+
349
+ const finalOutput = this.collectFinalOutput(sortedNodes, context);
350
+ await this.stateManager.updateFlowRun(runId, {
351
+ status: 'completed', output: finalOutput, completedAt: new Date().toISOString(),
352
+ });
353
+ this.broadcastFlowUpdate(sessionId, { type: 'flow_run_completed', runId, flowId: run.flowId, output: finalOutput });
354
+ this.activeExecutions.delete(runId);
355
+ return { runId, status: 'completed', output: finalOutput };
356
+ } catch (error) {
357
+ this.logger.error(`Flow resume failed: ${run.flowId}`, { runId, error: error.message });
358
+ await this.stateManager.updateFlowRun(runId, {
359
+ status: 'failed', error: error.message, completedAt: new Date().toISOString(),
360
+ });
361
+ this.broadcastFlowUpdate(sessionId, { type: 'flow_run_failed', runId, flowId: run.flowId, error: error.message });
362
+ this.activeExecutions.delete(runId);
363
+ return { runId, status: 'failed', error: error.message };
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Stop a flow execution
369
+ * @param {string} runId - Run identifier
370
+ * @returns {Promise<boolean>} True if stopped
371
+ */
372
+ async stopExecution(runId) {
373
+ const execution = this.activeExecutions.get(runId);
374
+ if (!execution) {
375
+ return false;
376
+ }
377
+
378
+ execution.status = 'stopped';
379
+
380
+ // Clean up any completion listeners
381
+ this.completionListeners.delete(runId);
382
+
383
+ return true;
384
+ }
385
+
386
+ /**
387
+ * Ensure all agents referenced in the flow are loaded
388
+ * Automatically loads unloaded agents from disk
389
+ * @param {Array} nodes - Flow nodes
390
+ * @returns {Promise<void>}
391
+ */
392
+ async ensureAgentsLoaded(nodes) {
393
+ if (!nodes || nodes.length === 0) return;
394
+
395
+ // First: surface ALL agent nodes that have no agentId at all (e.g.
396
+ // a template was loaded but the user hasn't picked agents yet).
397
+ // Better one clear "you haven't filled in N agents yet" than a
398
+ // confusing chain of partial loads followed by a "name === ''" error.
399
+ const unbound = nodes.filter(n =>
400
+ n.type === 'agent' &&
401
+ (typeof n.data?.agentId !== 'string' || n.data.agentId.trim().length === 0)
402
+ );
403
+ if (unbound.length > 0) {
404
+ const labels = unbound.map(n => n.data?.label || n.id).join(', ');
405
+ throw new Error(
406
+ `Cannot run flow: ${unbound.length} agent node(s) have no agent assigned (${labels}). Open each node and pick an agent in the properties panel.`
407
+ );
408
+ }
409
+
410
+ // Extract agent IDs from agent nodes
411
+ const agentNodes = nodes.filter(n => n.type === 'agent' && n.data?.agentId);
412
+ const agentIds = [...new Set(agentNodes.map(n => n.data.agentId))];
413
+
414
+ if (agentIds.length === 0) return;
415
+
416
+ this.logger.info(`Checking ${agentIds.length} agent(s) for flow execution`);
417
+
418
+ // Get project directory from config
419
+ const projectDir = this.config.projectDir || process.cwd();
420
+
421
+ for (const agentId of agentIds) {
422
+ // Check if already loaded
423
+ const existingAgent = await this.agentPool.getAgent(agentId);
424
+ if (existingAgent) {
425
+ this.logger.debug(`Agent ${agentId} already loaded`);
426
+ continue;
427
+ }
428
+
429
+ // Agent not loaded - try to load from disk
430
+ this.logger.info(`Loading agent from disk: ${agentId}`);
431
+ try {
432
+ const agent = await this.stateManager.importArchivedAgent(agentId, projectDir, this.agentPool);
433
+ this.logger.info(`Successfully loaded agent: ${agentId}`);
434
+
435
+ // Broadcast agent-loaded event so UI sidebar updates.
436
+ // Defensive: this is pure UX — it must NEVER kill the load.
437
+ // The WebServer instance exposes broadcastToSession(null, msg)
438
+ // for "broadcast to all"; some other manager shapes use
439
+ // broadcast(msg). Use whichever is available, swallow errors.
440
+ if (this.webSocketManager && agent) {
441
+ try {
442
+ const msg = {
443
+ type: 'agent-loaded',
444
+ agent: {
445
+ id: agent.id,
446
+ name: agent.name,
447
+ status: agent.status,
448
+ model: agent.currentModel || agent.preferredModel,
449
+ capabilities: agent.capabilities,
450
+ },
451
+ };
452
+ if (typeof this.webSocketManager.broadcast === 'function') {
453
+ this.webSocketManager.broadcast(msg);
454
+ } else if (typeof this.webSocketManager.broadcastToSession === 'function') {
455
+ this.webSocketManager.broadcastToSession(null, msg);
456
+ }
457
+ } catch (broadcastErr) {
458
+ this.logger.warn(`Failed to broadcast agent-loaded event (non-fatal)`, {
459
+ agentId, error: broadcastErr.message,
460
+ });
461
+ }
462
+ }
463
+ } catch (error) {
464
+ throw new Error(`Failed to load agent ${agentId}: ${error.message}. Make sure the agent exists.`, { cause: error });
465
+ }
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Get list of unloaded agents referenced in a flow
471
+ * Useful for UI to show which agents need to be loaded
472
+ * @param {Object} flow - Flow definition
473
+ * @returns {Promise<Array>} Array of { agentId, isLoaded, agentInfo }
474
+ */
475
+ async getFlowAgentStatus(flow) {
476
+ if (!flow?.nodes) return [];
477
+
478
+ const agentNodes = flow.nodes.filter(n => n.type === 'agent' && n.data?.agentId);
479
+ const agentIds = [...new Set(agentNodes.map(n => n.data.agentId))];
480
+ const projectDir = this.config.projectDir || process.cwd();
481
+
482
+ const results = [];
483
+ for (const agentId of agentIds) {
484
+ const existingAgent = await this.agentPool.getAgent(agentId);
485
+ if (existingAgent) {
486
+ results.push({
487
+ agentId,
488
+ isLoaded: true,
489
+ name: existingAgent.name,
490
+ model: existingAgent.currentModel
491
+ });
492
+ } else {
493
+ // Try to get info from disk
494
+ try {
495
+ const metadata = await this.stateManager.getAgentMetadata(agentId, projectDir);
496
+ results.push({
497
+ agentId,
498
+ isLoaded: false,
499
+ name: metadata?.name || agentId,
500
+ model: metadata?.model
501
+ });
502
+ } catch {
503
+ results.push({
504
+ agentId,
505
+ isLoaded: false,
506
+ name: agentId,
507
+ notFound: true
508
+ });
509
+ }
510
+ }
511
+ }
512
+
513
+ return results;
514
+ }
515
+
516
+ /**
517
+ * Topologically sort nodes based on edges
518
+ * @param {Array} nodes - Array of nodes
519
+ * @param {Array} edges - Array of edges
520
+ * @returns {Array} Sorted nodes array
521
+ */
522
+ topologicalSort(nodes, edges) {
523
+ if (!nodes || nodes.length === 0) {
524
+ return [];
525
+ }
526
+
527
+ const nodeMap = new Map(nodes.map(n => [n.id, n]));
528
+ const inDegree = new Map(nodes.map(n => [n.id, 0]));
529
+ const adjacency = new Map(nodes.map(n => [n.id, []]));
530
+
531
+ // Build adjacency list and calculate in-degrees
532
+ for (const edge of (edges || [])) {
533
+ const source = edge.source;
534
+ const target = edge.target;
535
+
536
+ if (adjacency.has(source) && inDegree.has(target)) {
537
+ adjacency.get(source).push(target);
538
+ inDegree.set(target, inDegree.get(target) + 1);
539
+ }
540
+ }
541
+
542
+ // Find all nodes with in-degree 0 (starting nodes)
543
+ const queue = [];
544
+ for (const [nodeId, degree] of inDegree) {
545
+ if (degree === 0) {
546
+ queue.push(nodeId);
547
+ }
548
+ }
549
+
550
+ // Process queue
551
+ const sorted = [];
552
+ while (queue.length > 0) {
553
+ const nodeId = queue.shift();
554
+ const node = nodeMap.get(nodeId);
555
+ if (node) {
556
+ sorted.push(node);
557
+ }
558
+
559
+ for (const neighbor of adjacency.get(nodeId) || []) {
560
+ const newDegree = inDegree.get(neighbor) - 1;
561
+ inDegree.set(neighbor, newDegree);
562
+ if (newDegree === 0) {
563
+ queue.push(neighbor);
564
+ }
565
+ }
566
+ }
567
+
568
+ // Check for cycles
569
+ if (sorted.length !== nodes.length) {
570
+ this.logger.warn('Flow contains cycles, some nodes may not execute');
571
+ }
572
+
573
+ return sorted;
574
+ }
575
+
576
+ /**
577
+ * Execute a single node
578
+ * @param {Object} node - Node to execute
579
+ * @param {Object} context - Execution context
580
+ * @param {string} runId - Run identifier
581
+ * @param {string} sessionId - Session ID for broadcasts
582
+ * @param {Object} flow - Flow definition
583
+ * @returns {Promise<Object>} Node execution result
584
+ */
585
+ async executeNode(node, context, runId, sessionId, flow) {
586
+ this.logger.info(`Executing node: ${node.id}`, {
587
+ type: node.type,
588
+ label: node.data?.label
589
+ });
590
+
591
+ // Update node state to running
592
+ await this.updateNodeState(runId, node.id, 'running', null);
593
+ this.broadcastFlowUpdate(sessionId, {
594
+ type: 'flow_node_started',
595
+ runId,
596
+ nodeId: node.id,
597
+ nodeType: node.type
598
+ });
599
+
600
+ let result;
601
+
602
+ try {
603
+ switch (node.type) {
604
+ case 'input':
605
+ result = await this.executeInputNode(node, context);
606
+ break;
607
+ case 'agent':
608
+ result = await this.executeAgentNode(node, context, runId, sessionId, flow);
609
+ break;
610
+ case 'output':
611
+ result = await this.executeOutputNode(node, context, flow);
612
+ break;
613
+ default:
614
+ throw new Error(`Unknown node type: ${node.type}`);
615
+ }
616
+
617
+ this.broadcastFlowUpdate(sessionId, {
618
+ type: 'flow_node_completed',
619
+ runId,
620
+ nodeId: node.id,
621
+ nodeType: node.type,
622
+ output: this.truncateOutput(result)
623
+ });
624
+
625
+ return result;
626
+
627
+ } catch (error) {
628
+ // Phase 6.1: capture structured error info — kind classification
629
+ // (timeout / agent-error / agent-failure) + per-attempt history
630
+ // (from runWithRetry). Persisted on the node state so the run-
631
+ // detail UI can show "node-B timed out after 3 attempts" without
632
+ // re-parsing log files.
633
+ const errorInfo = {
634
+ kind: error?.kind || 'agent-error',
635
+ message: error?.message || String(error),
636
+ attempts: error?.attempts || null,
637
+ lastAt: new Date().toISOString(),
638
+ };
639
+ await this.updateNodeState(runId, node.id, 'failed', { error: error.message }, errorInfo);
640
+ this.broadcastFlowUpdate(sessionId, {
641
+ type: 'flow_node_failed',
642
+ runId,
643
+ nodeId: node.id,
644
+ nodeType: node.type,
645
+ error: error.message,
646
+ errorInfo,
647
+ });
648
+ throw error;
649
+ }
650
+ }
651
+
652
+ /**
653
+ * Execute an input node
654
+ * @param {Object} node - Input node
655
+ * @param {Object} context - Execution context
656
+ * @returns {Promise<Object>} Input result
657
+ */
658
+ async executeInputNode(node, context) {
659
+ // Apply prompt template if provided
660
+ const template = node.data?.promptTemplate || '{{userInput}}';
661
+ const output = this.applyTemplate(template, {
662
+ userInput: context.input,
663
+ ...context.variables
664
+ });
665
+
666
+ return {
667
+ type: 'input',
668
+ output,
669
+ raw: context.input
670
+ };
671
+ }
672
+
673
+ /**
674
+ * Execute an agent node
675
+ * @param {Object} node - Agent node
676
+ * @param {Object} context - Execution context
677
+ * @param {string} runId - Run identifier
678
+ * @param {string} sessionId - Session ID
679
+ * @param {Object} flow - Flow definition
680
+ * @returns {Promise<Object>} Agent response
681
+ */
682
+ async executeAgentNode(node, context, runId, sessionId, flow) {
683
+ const agentId = node.data?.agentId;
684
+ if (!agentId) {
685
+ throw new Error(`Agent node ${node.id} has no agent assigned`);
686
+ }
687
+
688
+ // Get agent
689
+ const agent = await this.agentPool.getAgent(agentId);
690
+ if (!agent) {
691
+ throw new Error(`Agent not found: ${agentId}`);
692
+ }
693
+
694
+ // ---- Phase 1: typed-input assembly ---------------------------------
695
+ // For v2 nodes (with declared inputs[]), assemble typed values from
696
+ // upstream outputs by following the edge field mappings. For legacy
697
+ // nodes (no inputs[]), this falls back to {input, previousOutput}
698
+ // built by concatenating upstream outputs — identical to v1 behavior.
699
+ const assembled = assembleNodeInputs(node, flow.edges, context.nodeOutputs);
700
+
701
+ if (!assembled.legacy && assembled.missing.length > 0) {
702
+ // v2 fail-fast: a required input has no upstream value. Better to
703
+ // bail here with a clear error than send the agent half its data.
704
+ throw new Error(
705
+ `Agent node ${node.id} (${agentId}) is missing required input(s): ${assembled.missing.join(', ')}`
706
+ );
707
+ }
708
+
709
+ // Legacy fallback also exposes inputNodeIds + previousOutput for the
710
+ // existing buildPreviousAgentData / context-injection code paths.
711
+ const inputNodeIds = this.getInputNodeIds(node.id, flow.edges);
712
+ const previousOutput = assembled.legacy
713
+ ? assembled.values.previousOutput
714
+ : this.collectPreviousOutput(inputNodeIds, context.nodeOutputs);
715
+
716
+ // Apply prompt template. Typed input values are exposed BY NAME so
717
+ // templates can write {{topic}}, {{research}}, etc. Legacy keys
718
+ // {{input}} / {{previousOutput}} stay available for backwards compat.
719
+ const template = node.data?.promptTemplate || '{{input}}';
720
+ const prompt = this.applyTemplate(template, {
721
+ input: previousOutput,
722
+ previousOutput,
723
+ ...context.variables,
724
+ ...context.nodeOutputs,
725
+ ...assembled.values, // typed inputs win over collisions
726
+ });
727
+
728
+ this.logger.info(`Sending message to agent: ${agentId}`, {
729
+ promptLength: prompt.length,
730
+ nodeId: node.id
731
+ });
732
+
733
+ // Build flow metadata + previous-agent data ONCE — reused per retry
734
+ const sortedNodes = context.sortedNodes || [];
735
+ const nodePosition = sortedNodes.findIndex(n => n.id === node.id) + 1;
736
+ const flowMetadata = {
737
+ flowId: flow.id,
738
+ flowName: flow.name || 'Unnamed Flow',
739
+ // Phase 7: forward flow.description so system prompt can render
740
+ // a "FLOW GOAL" section orienting each agent to the bigger picture.
741
+ flowDescription: flow.description,
742
+ nodeName: node.data?.label || 'Agent',
743
+ nodePosition: nodePosition || 1,
744
+ totalNodes: sortedNodes.length
745
+ };
746
+ const previousAgentData = this.buildPreviousAgentData(node, context, flow);
747
+
748
+ // v2: derive the node's typed I/O contract for system-prompt
749
+ // advertisement (handled downstream in agentScheduler). v1 nodes
750
+ // don't have inputs/outputs declared → contract is undefined and
751
+ // the scheduler skips the contract section.
752
+ // Phase 7: also forward node.data.instructions (the per-node role
753
+ // + success-criteria description) so the prompt can render a
754
+ // NODE INSTRUCTIONS section.
755
+ const hasTypedIO = Array.isArray(node.inputs) || Array.isArray(node.outputs);
756
+ const hasInstructions = typeof node.data?.instructions === 'string' && node.data.instructions.trim().length > 0;
757
+ const nodeContract = (hasTypedIO || hasInstructions)
758
+ ? {
759
+ inputs: node.inputs || [],
760
+ outputs: node.outputs || [],
761
+ instructions: node.data?.instructions,
762
+ // Phase 8: opt-out flag — when true, scheduler keeps the
763
+ // agent's native system prompt and APPENDS flow context
764
+ // (legacy behavior). When false/absent, scheduler REPLACES
765
+ // the system prompt with a flow-worker version, eliminating
766
+ // identity conflicts.
767
+ useNativeSystemPrompt: node.data?.useNativeSystemPrompt === true,
768
+ }
769
+ : undefined;
770
+
771
+ // ---- Phase 3: per-node retry + per-node timeout -------------------
772
+ // The closure below is "one full agent invocation" — clear conv,
773
+ // queue message, wait for jobdone, validate outputs. Each retry is
774
+ // a brand-new invocation with the agent's conversation reset.
775
+ // Precedence: node.execution > flow.execution > config.flows.execution > defaults.
776
+ const exec = this._resolveExecutionConfig(node, flow);
777
+
778
+ // Store original mode for potential restoration on hard fail (not
779
+ // currently restored — same as pre-Phase 3 behavior — but useful
780
+ // to capture if we add cleanup).
781
+ const originalMode = agent.mode; // eslint-disable-line no-unused-vars
782
+
783
+ // Compute the list of missing required outputs given a job-done result.
784
+ // Returns [] when contract is satisfied (or no contract). Used both
785
+ // inside attemptOnce (to drive in-conversation re-prompts) and once
786
+ // more after the retry loop as a defensive belt-and-suspenders check.
787
+ const detectMissingOutputs = (result) => {
788
+ if (!nodeContract || !Array.isArray(nodeContract.outputs) || nodeContract.outputs.length === 0) return [];
789
+ const provided = (result?.outputs && typeof result.outputs === 'object') ? result.outputs : {};
790
+ const missing = [];
791
+ for (const decl of nodeContract.outputs) {
792
+ if (!decl || typeof decl.name !== 'string') continue;
793
+ if (!(decl.name in provided) || provided[decl.name] === null || provided[decl.name] === undefined) {
794
+ missing.push(decl.name);
795
+ }
796
+ }
797
+ return missing;
798
+ };
799
+
800
+ // Build a corrective re-prompt that lists EXACTLY which fields are
801
+ // missing and shows the JSON shape the agent must emit on the next
802
+ // job-done. This is sent in the SAME conversation — the agent keeps
803
+ // its working memory and just patches the handoff.
804
+ const MAX_REPROMPTS_PER_ATTEMPT = 2;
805
+ const buildRepromptMessage = (missing) => {
806
+ // Phase 7: enrich the corrective message with each declared
807
+ // output's description + example so the agent has the FULL
808
+ // contract to satisfy on the retry, not just a list of names.
809
+ const required = nodeContract.outputs.map(o => {
810
+ const lines = [` • ${o.name}: ${o.type}`];
811
+ if (typeof o.description === 'string' && o.description.trim()) {
812
+ lines.push(` ${o.description.trim()}`);
813
+ }
814
+ if (o.example !== undefined && o.example !== null) {
815
+ try {
816
+ const ex = (typeof o.example === 'string')
817
+ ? JSON.stringify(o.example)
818
+ : JSON.stringify(o.example, null, 2);
819
+ const oneLine = !ex.includes('\n');
820
+ lines.push(oneLine ? ` Example: ${ex}` : ` Example: ${ex.split('\n').join('\n ')}`);
821
+ } catch { /* ignore unstringifiable examples */ }
822
+ }
823
+ return lines.join('\n');
824
+ }).join('\n');
825
+ const example = nodeContract.outputs
826
+ .map(o => ` "${o.name}": <${o.type} value>`).join(',\n');
827
+ return [
828
+ `⚠ Your previous job-done was incomplete. The flow node declares REQUIRED OUTPUTS that must all be present in the "outputs" field of job-done.`,
829
+ ``,
830
+ `Missing field(s): ${missing.join(', ')}`,
831
+ ``,
832
+ `All required outputs:`,
833
+ required,
834
+ ``,
835
+ `Please call job-done AGAIN with the complete payload. The "outputs" object must include EVERY field above. Example structure:`,
836
+ ``,
837
+ `{`,
838
+ ` "toolId": "jobdone",`,
839
+ ` "actions": [{`,
840
+ ` "action": "complete",`,
841
+ ` "summary": "<your summary>",`,
842
+ ` "outputs": {`,
843
+ example,
844
+ ` }`,
845
+ ` }]`,
846
+ `}`,
847
+ ``,
848
+ `Use the work you've already done — do not redo the task. Just emit the structured outputs.`,
849
+ ].join('\n');
850
+ };
851
+
852
+ const attemptOnce = async (attemptIndex) => {
853
+ // Phase 8: register the active contract so the jobdone tool can
854
+ // validate the agent's job-done call AT TOOL TIME — rejecting
855
+ // partial calls before they propagate. Cleared in the finally
856
+ // block of the outer try (see below) when the attempt completes
857
+ // (success OR failure).
858
+ if (nodeContract) {
859
+ this.activeContracts.set(agentId, nodeContract);
860
+ }
861
+
862
+ // Reset completion state + conversation for a clean slate every
863
+ // OUTER retry. In-conversation re-prompts (below) keep history.
864
+ agent.autonomousWorkComplete = false;
865
+ agent.lastCompletionSummary = null;
866
+ agent.lastCompletionDetails = null;
867
+ agent.mode = AGENT_MODES.AGENT;
868
+ await this.agentPool.persistAgentState(agentId);
869
+ try {
870
+ await this.agentPool.clearConversation(agentId);
871
+ } catch (error) {
872
+ this.logger.warn(`Failed to clear conversation for agent ${agentId}`, { error: error.message });
873
+ }
874
+
875
+ // Register the listener BEFORE queuing the message so a fast jobdone
876
+ // can resolve us immediately.
877
+ let completionPromise = this.waitForAgentCompletion(agentId, runId);
878
+
879
+ await this.messageProcessor.processMessage(agentId, prompt, {
880
+ sessionId,
881
+ isFlowExecution: true,
882
+ flowRunId: runId,
883
+ flowNodeId: node.id,
884
+ flowMetadata,
885
+ previousAgentData,
886
+ nodeContract,
887
+ attemptIndex,
888
+ });
889
+
890
+ let result = await completionPromise;
891
+
892
+ // v2 in-conversation re-prompt loop: when outputs are missing, ask
893
+ // the agent to fix the LAST job-done call WITHOUT clearing memory.
894
+ // Cheap (~1 model call vs full re-run) and almost always succeeds
895
+ // because the agent already produced the data — they just forgot
896
+ // to put it in the outputs field. After MAX_REPROMPTS, we fall
897
+ // through to the outer retry which DOES clear conversation.
898
+ let reprompts = 0;
899
+ while (reprompts < MAX_REPROMPTS_PER_ATTEMPT) {
900
+ const missing = detectMissingOutputs(result);
901
+ if (missing.length === 0) break;
902
+
903
+ this.logger.warn(`Flow node ${node.id}: re-prompting agent for missing outputs`, {
904
+ missing, reprompts, attemptIndex,
905
+ });
906
+
907
+ // Re-arm the listener BEFORE sending — the agent's response will
908
+ // resolve it.
909
+ agent.autonomousWorkComplete = false;
910
+ await this.agentPool.persistAgentState(agentId);
911
+ completionPromise = this.waitForAgentCompletion(agentId, runId);
912
+
913
+ await this.messageProcessor.processMessage(agentId, buildRepromptMessage(missing), {
914
+ sessionId,
915
+ isFlowExecution: true,
916
+ flowRunId: runId,
917
+ flowNodeId: node.id,
918
+ flowMetadata,
919
+ previousAgentData,
920
+ nodeContract,
921
+ attemptIndex,
922
+ isReprompt: true,
923
+ });
924
+
925
+ result = await completionPromise;
926
+ reprompts++;
927
+ }
928
+
929
+ // Final check: if still missing after re-prompts, throw a retryable
930
+ // error so the OUTER loop (runWithRetry) starts a fresh agent
931
+ // invocation with cleared conversation.
932
+ const stillMissing = detectMissingOutputs(result);
933
+ if (stillMissing.length > 0) {
934
+ throw new FlowRetryError(
935
+ `Agent ${agentId} (node ${node.id}) job-done is missing required output(s) after ${reprompts} re-prompt(s): ${stillMissing.join(', ')}. ` +
936
+ `Declared outputs: ${nodeContract.outputs.map(o => `${o.name}:${o.type}`).join(', ')}`,
937
+ 'agent-error'
938
+ );
939
+ }
940
+ return result;
941
+ };
942
+
943
+ let completionResult;
944
+ let attemptHistory;
945
+ try {
946
+ const ran = await runWithRetry(attemptOnce, {
947
+ timeoutMs: exec.timeoutMs,
948
+ maxRetries: exec.maxRetries,
949
+ retryOn: exec.retryOn,
950
+ backoffBaseMs: exec.backoffBaseMs,
951
+ backoffMultiplier: exec.backoffMultiplier,
952
+ onAttempt: (meta) => {
953
+ this.logger.warn(`Flow node ${node.id} attempt ${meta.attempt} failed`, {
954
+ kind: meta.error?.kind, message: meta.error?.message, willRetry: meta.willRetry,
955
+ });
956
+ },
957
+ });
958
+ completionResult = ran.result;
959
+ attemptHistory = ran.attempts;
960
+ } catch (e) {
961
+ // Surface the kind on the thrown error so the caller (executeFlow)
962
+ // and observability can distinguish timeout vs agent-error vs
963
+ // explicit failure.
964
+ const kind = e?.kind || 'agent-error';
965
+ const friendly = kind === 'timeout'
966
+ ? `Agent ${agentId} timed out (after ${exec.maxRetries + 1} attempt(s))`
967
+ : (e?.message || `Agent ${agentId} failed`);
968
+ const wrapped = new Error(friendly);
969
+ wrapped.kind = kind;
970
+ wrapped.attempts = e?.attempts;
971
+ // Phase 8: clear active contract on failure so the jobdone tool
972
+ // doesn't validate against a stale node when the agent gets used
973
+ // outside the flow later.
974
+ this.activeContracts.delete(agentId);
975
+ throw wrapped;
976
+ }
977
+ // Phase 8: clear active contract on success too.
978
+ this.activeContracts.delete(agentId);
979
+
980
+ // Get the agent's response (last assistant message)
981
+ const updatedAgent = await this.agentPool.getAgent(agentId);
982
+ const lastResponse = this.getLastAssistantMessage(updatedAgent);
983
+
984
+ // Extract files created during this agent's execution (from completion result or messages)
985
+ const filesCreated = completionResult.filesCreated || this.extractFilesFromCompletion(completionResult);
986
+
987
+ // v2: outputs validation already happened inside attemptOnce; this
988
+ // block stays only as a defensive belt-and-suspenders for the case
989
+ // where attemptOnce somehow returns without outputs. Same logic.
990
+ if (nodeContract && Array.isArray(nodeContract.outputs) && nodeContract.outputs.length > 0) {
991
+ const provided = (completionResult.outputs && typeof completionResult.outputs === 'object')
992
+ ? completionResult.outputs : {};
993
+ const missing = [];
994
+ for (const decl of nodeContract.outputs) {
995
+ if (!decl || typeof decl.name !== 'string') continue;
996
+ if (!(decl.name in provided) || provided[decl.name] === null || provided[decl.name] === undefined) {
997
+ missing.push(decl.name);
998
+ }
999
+ }
1000
+ if (missing.length > 0) {
1001
+ throw new Error(
1002
+ `Agent ${agentId} (node ${node.id}) job-done is missing required output(s): ${missing.join(', ')}. ` +
1003
+ `Declared outputs: ${nodeContract.outputs.map(o => `${o.name}:${o.type}`).join(', ')}`
1004
+ );
1005
+ }
1006
+ }
1007
+
1008
+ // Choose what becomes the legacy `output` field used by v1 handoff
1009
+ // and the prose preview in the next agent's system prompt.
1010
+ //
1011
+ // Preference order:
1012
+ // 1. summary + details (if both present, glue them)
1013
+ // 2. lastResponse (the agent's full final assistant message)
1014
+ // 3. summary alone
1015
+ // 4. ''
1016
+ //
1017
+ // Why: tiny "Done." summaries used to flow through as the next
1018
+ // agent's entire context — agents had nothing to work with. Falling
1019
+ // back to the assistant's last message gives the next agent a real
1020
+ // payload even when the upstream agent skipped `details`.
1021
+ const summary = (completionResult.summary || '').trim();
1022
+ const details = (completionResult.details || '').trim();
1023
+ let prose;
1024
+ if (summary && details) prose = `${summary}\n\n${details}`;
1025
+ else if (summary && summary.length < 40 && lastResponse) prose = lastResponse;
1026
+ else prose = summary || lastResponse || '';
1027
+
1028
+ return {
1029
+ type: 'agent',
1030
+ agentId,
1031
+ agentName: agent.name,
1032
+ output: prose,
1033
+ // v2: structured outputs bag — keyed by declared field name. This
1034
+ // is what assembleNodeInputs reads when wiring the next node.
1035
+ outputs: completionResult.outputs,
1036
+ details: completionResult.details,
1037
+ filesCreated,
1038
+ // Phase 3: attempt history (one entry per try; failed entries
1039
+ // include { kind, message }). Surfaces in run-detail UI later.
1040
+ attempts: attemptHistory,
1041
+ success: completionResult.success !== false
1042
+ };
1043
+ }
1044
+
1045
+ /**
1046
+ * Resolve effective per-node execution config — timeout, retries,
1047
+ * backoff, retryOn classes. Precedence (highest wins):
1048
+ * node.execution > flow.execution > config.flows.execution > defaults
1049
+ *
1050
+ * Defaults:
1051
+ * timeoutMs 300000 (5min, matches pre-Phase-3 behavior; legacy
1052
+ * config.flows.nodeTimeout still honored as a global
1053
+ * timeout for back-compat)
1054
+ * maxRetries 1 (one fresh-conversation retry after re-prompts
1055
+ * exhaust — without this, structured-output
1056
+ * misses by some models cause flow failure
1057
+ * after only 2 in-conversation re-prompts).
1058
+ * Override per node to 0 to disable retries.
1059
+ * retryOn ['timeout', 'agent-error']
1060
+ * backoffBaseMs 1000
1061
+ * backoffMultiplier 2
1062
+ */
1063
+ _resolveExecutionConfig(node, flow) {
1064
+ const globalCfg = this.config?.flows?.execution || {};
1065
+ const flowCfg = flow?.execution || {};
1066
+ const nodeCfg = node?.execution || {};
1067
+ // Legacy: config.flows.nodeTimeout was the only knob in v1. Keep
1068
+ // it as the default timeout if more-specific configs aren't set.
1069
+ const legacyTimeout = this.config?.flows?.nodeTimeout;
1070
+
1071
+ const pick = (k, fallback) =>
1072
+ nodeCfg[k] !== undefined ? nodeCfg[k] :
1073
+ flowCfg[k] !== undefined ? flowCfg[k] :
1074
+ globalCfg[k] !== undefined ? globalCfg[k] :
1075
+ fallback;
1076
+
1077
+ return {
1078
+ timeoutMs: pick('timeoutMs', legacyTimeout ?? 300000),
1079
+ maxRetries: pick('maxRetries', 1),
1080
+ retryOn: pick('retryOn', ['timeout', 'agent-error']),
1081
+ backoffBaseMs: pick('backoffBaseMs', 1000),
1082
+ backoffMultiplier: pick('backoffMultiplier', 2),
1083
+ };
1084
+ }
1085
+
1086
+ /**
1087
+ * Build previous agent data for context passing to next agent
1088
+ * @param {Object} currentNode - Current agent node
1089
+ * @param {Object} context - Execution context with nodeOutputs
1090
+ * @param {Object} flow - Flow definition
1091
+ * @returns {Object|null} Previous agent data or null if first agent
1092
+ */
1093
+ buildPreviousAgentData(currentNode, context, flow) {
1094
+ // Find the previous agent node that feeds into this one
1095
+ const inputNodeIds = this.getInputNodeIds(currentNode.id, flow.edges);
1096
+
1097
+ // Look for the most recent agent node in the inputs.
1098
+ // We collect ALL upstream agent contributions (not just one) so a
1099
+ // node fed by multiple agents gets a merged outputs bag and a
1100
+ // labeled list of contributors. v1 fallback: pick the first.
1101
+ const contributors = [];
1102
+ for (const inputId of inputNodeIds) {
1103
+ const nodeOutput = context.nodeOutputs[inputId];
1104
+ if (nodeOutput && nodeOutput.type === 'agent') contributors.push(nodeOutput);
1105
+ }
1106
+ if (contributors.length === 0) return null;
1107
+
1108
+ // Merge structured outputs from all contributors. When the same
1109
+ // field name appears on multiple upstream agents, the LATER one
1110
+ // wins (sortedNodes order in context.sortedNodes is topological).
1111
+ const mergedOutputs = {};
1112
+ for (const c of contributors) {
1113
+ if (c.outputs && typeof c.outputs === 'object') {
1114
+ Object.assign(mergedOutputs, c.outputs);
1115
+ }
1116
+ }
1117
+
1118
+ // Primary "previous agent" used for the legacy fields stays the
1119
+ // most recent contributor (last one in topo order).
1120
+ const primary = contributors[contributors.length - 1];
1121
+
1122
+ return {
1123
+ agentId: primary.agentId,
1124
+ agentName: primary.agentName || primary.agentId,
1125
+ summary: primary.output,
1126
+ details: primary.details,
1127
+ filesCreated: primary.filesCreated || [],
1128
+ output: primary.output,
1129
+ // Phase 5/6 fix: forward the structured outputs bag end-to-end.
1130
+ // Without this the next agent never sees the typed handoff —
1131
+ // only the free-text summary, defeating the whole v2 contract.
1132
+ outputs: Object.keys(mergedOutputs).length > 0 ? mergedOutputs : undefined,
1133
+ // When multiple agents fed this node, list them so the system
1134
+ // prompt can show "you received outputs from agents A, B".
1135
+ contributors: contributors.length > 1
1136
+ ? contributors.map(c => ({
1137
+ agentId: c.agentId,
1138
+ agentName: c.agentName || c.agentId,
1139
+ outputs: c.outputs || null,
1140
+ }))
1141
+ : undefined,
1142
+ };
1143
+ }
1144
+
1145
+ /**
1146
+ * Extract file paths from completion result
1147
+ * Parses the summary/details for file path mentions
1148
+ * @param {Object} completionResult - Job-done completion result
1149
+ * @returns {Array<string>} Array of file paths
1150
+ */
1151
+ extractFilesFromCompletion(completionResult) {
1152
+ const files = new Set();
1153
+
1154
+ const textToSearch = [
1155
+ completionResult.summary || '',
1156
+ completionResult.details || '',
1157
+ typeof completionResult.output === 'string' ? completionResult.output : ''
1158
+ ].join(' ');
1159
+
1160
+ // Common patterns for file paths
1161
+ const patterns = [
1162
+ /(?:created|wrote|saved|generated|modified|updated)\s+(?:file\s+)?["']?([/\\][\w\-./\\]+\.\w+)["']?/gi,
1163
+ /(?:at|to|in)\s+["']?([/\\][\w\-./\\]+\.\w+)["']?/gi,
1164
+ /File\s+(?:created|written|saved):\s*([/\\][\w\-./\\]+\.\w+)/gi
1165
+ ];
1166
+
1167
+ for (const pattern of patterns) {
1168
+ let match;
1169
+ while ((match = pattern.exec(textToSearch)) !== null) {
1170
+ const path = match[1];
1171
+ if (path && !path.includes('http') && path.length > 3) {
1172
+ files.add(path);
1173
+ }
1174
+ }
1175
+ }
1176
+
1177
+ return Array.from(files);
1178
+ }
1179
+
1180
+ /**
1181
+ * Execute an output node
1182
+ * @param {Object} node - Output node
1183
+ * @param {Object} context - Execution context
1184
+ * @param {Object} flow - Flow definition
1185
+ * @returns {Promise<Object>} Output result
1186
+ */
1187
+ async executeOutputNode(node, context, flow) {
1188
+ // Collect all previous outputs
1189
+ const inputNodeIds = this.getInputNodeIds(node.id, flow.edges);
1190
+ const output = this.collectPreviousOutput(inputNodeIds, context.nodeOutputs);
1191
+
1192
+ // Apply output format
1193
+ const format = node.data?.outputFormat || 'text';
1194
+ let formattedOutput;
1195
+
1196
+ switch (format) {
1197
+ case 'json':
1198
+ formattedOutput = typeof output === 'object' ? output : { result: output };
1199
+ break;
1200
+ case 'markdown':
1201
+ formattedOutput = typeof output === 'string' ? output : JSON.stringify(output, null, 2);
1202
+ break;
1203
+ case 'text':
1204
+ default:
1205
+ formattedOutput = typeof output === 'string' ? output : JSON.stringify(output);
1206
+ break;
1207
+ }
1208
+
1209
+ return {
1210
+ type: 'output',
1211
+ format,
1212
+ output: formattedOutput
1213
+ };
1214
+ }
1215
+
1216
+ /**
1217
+ * Wait for an agent to complete via jobdone tool.
1218
+ * Completion is detected two ways:
1219
+ * 1. Direct signal: notifyAgentCompletion() called from jobDoneTool
1220
+ * 2. Polling fallback: checks autonomousWorkComplete flag every 2s
1221
+ * @param {string} agentId - Agent ID
1222
+ * @param {string} runId - Run ID
1223
+ * @returns {Promise<Object>} Completion result
1224
+ */
1225
+ waitForAgentCompletion(agentId, runId) {
1226
+ return new Promise((resolve) => {
1227
+ const key = `${runId}-${agentId}`;
1228
+ let checkInterval = null;
1229
+
1230
+ const onComplete = (result) => {
1231
+ if (checkInterval) clearInterval(checkInterval);
1232
+ this.completionListeners.delete(key);
1233
+ resolve(result);
1234
+ };
1235
+
1236
+ // Store listener so notifyAgentCompletion() can resolve directly
1237
+ this.completionListeners.set(key, {
1238
+ resolve: onComplete,
1239
+ agentId,
1240
+ runId
1241
+ });
1242
+
1243
+ // Polling fallback: check autonomousWorkComplete flag
1244
+ checkInterval = setInterval(async () => {
1245
+ try {
1246
+ const agent = await this.agentPool.getAgent(agentId);
1247
+ if (agent && agent.autonomousWorkComplete) {
1248
+ onComplete({
1249
+ completed: true,
1250
+ summary: agent.lastCompletionSummary || '',
1251
+ details: agent.lastCompletionDetails || null,
1252
+ success: true
1253
+ });
1254
+ }
1255
+ } catch {
1256
+ // Ignore errors during polling
1257
+ }
1258
+ }, 2000);
1259
+
1260
+ // Clean up interval on timeout
1261
+ setTimeout(() => {
1262
+ if (checkInterval) clearInterval(checkInterval);
1263
+ }, this.config.flows?.nodeTimeout || 300000);
1264
+ });
1265
+ }
1266
+
1267
+ /**
1268
+ * Notify that an agent has completed its work (called from jobDoneTool).
1269
+ * Directly resolves the completion promise instead of waiting for next poll.
1270
+ * @param {string} agentId - Agent ID
1271
+ * @param {Object} completionData - { summary, success, details }
1272
+ */
1273
+ notifyAgentCompletion(agentId, completionData = {}) {
1274
+ // Find any listener waiting for this agent
1275
+ for (const [, listener] of this.completionListeners.entries()) {
1276
+ if (listener.agentId === agentId) {
1277
+ this.logger.info(`Flow: Agent ${agentId} completed via direct signal`, {
1278
+ runId: listener.runId,
1279
+ success: completionData.success
1280
+ });
1281
+ listener.resolve({
1282
+ completed: true,
1283
+ summary: completionData.summary || '',
1284
+ details: completionData.details || null,
1285
+ // v2: structured outputs bag — forwarded through to the
1286
+ // executor's contract validator. v1 callers don't set this.
1287
+ outputs: completionData.outputs,
1288
+ success: completionData.success !== false
1289
+ });
1290
+ return true;
1291
+ }
1292
+ }
1293
+ return false; // No listener found (agent not in a flow)
1294
+ }
1295
+
1296
+ /**
1297
+ * Create a timeout promise
1298
+ * @param {number} ms - Timeout in milliseconds
1299
+ * @returns {Promise<Object>} Timeout result
1300
+ */
1301
+ createTimeoutPromise(ms) {
1302
+ return new Promise((resolve) => {
1303
+ setTimeout(() => {
1304
+ resolve({ timeout: true });
1305
+ }, ms);
1306
+ });
1307
+ }
1308
+
1309
+ /**
1310
+ * Get IDs of nodes that connect to a target node
1311
+ * @param {string} targetNodeId - Target node ID
1312
+ * @param {Array} edges - Flow edges
1313
+ * @returns {Array} Source node IDs
1314
+ */
1315
+ getInputNodeIds(targetNodeId, edges) {
1316
+ return (edges || [])
1317
+ .filter(e => e.target === targetNodeId)
1318
+ .map(e => e.source);
1319
+ }
1320
+
1321
+ /**
1322
+ * Collect output from previous nodes
1323
+ * @param {Array} nodeIds - Node IDs to collect from
1324
+ * @param {Object} nodeOutputs - Map of node outputs
1325
+ * @returns {string} Combined output
1326
+ */
1327
+ collectPreviousOutput(nodeIds, nodeOutputs) {
1328
+ const outputs = nodeIds
1329
+ .map(id => nodeOutputs[id])
1330
+ .filter(o => o)
1331
+ .map(o => o.output || o)
1332
+ .filter(o => o);
1333
+
1334
+ if (outputs.length === 0) return '';
1335
+ if (outputs.length === 1) return outputs[0];
1336
+
1337
+ // Combine multiple outputs
1338
+ return outputs.join('\n\n---\n\n');
1339
+ }
1340
+
1341
+ /**
1342
+ * Collect final output from output nodes
1343
+ * @param {Array} sortedNodes - Sorted nodes
1344
+ * @param {Object} context - Execution context
1345
+ * @returns {Object} Final output
1346
+ */
1347
+ collectFinalOutput(sortedNodes, context) {
1348
+ const outputNodes = sortedNodes.filter(n => n.type === 'output');
1349
+
1350
+ if (outputNodes.length === 0) {
1351
+ // No output node, return last node's output
1352
+ const lastNode = sortedNodes[sortedNodes.length - 1];
1353
+ return context.nodeOutputs[lastNode?.id]?.output || null;
1354
+ }
1355
+
1356
+ if (outputNodes.length === 1) {
1357
+ return context.nodeOutputs[outputNodes[0].id]?.output;
1358
+ }
1359
+
1360
+ // Multiple output nodes
1361
+ const outputs = {};
1362
+ for (const node of outputNodes) {
1363
+ const key = node.data?.label || node.id;
1364
+ outputs[key] = context.nodeOutputs[node.id]?.output;
1365
+ }
1366
+ return outputs;
1367
+ }
1368
+
1369
+ /**
1370
+ * Apply template with variable substitution
1371
+ * @param {string} template - Template string
1372
+ * @param {Object} variables - Variables to substitute
1373
+ * @returns {string} Processed template
1374
+ */
1375
+ applyTemplate(template, variables) {
1376
+ let result = template;
1377
+
1378
+ for (const [key, value] of Object.entries(variables)) {
1379
+ const placeholder = `{{${key}}}`;
1380
+ const valueStr = typeof value === 'object'
1381
+ ? JSON.stringify(value)
1382
+ : String(value || '');
1383
+ result = result.split(placeholder).join(valueStr);
1384
+ }
1385
+
1386
+ return result;
1387
+ }
1388
+
1389
+ /**
1390
+ * Update node state in the run record.
1391
+ *
1392
+ * Phase 6.1: optional `errorInfo` captures classified failure detail
1393
+ * (kind, message, attempts) for run-detail diagnostics. Stored as
1394
+ * `nodeStates[id].error` alongside the truncated result.
1395
+ *
1396
+ * @param {string} runId
1397
+ * @param {string} nodeId
1398
+ * @param {string} status 'running' | 'completed' | 'failed'
1399
+ * @param {Object} result
1400
+ * @param {Object} [errorInfo] { kind, message, attempts, lastAt }
1401
+ */
1402
+ async updateNodeState(runId, nodeId, status, result, errorInfo) {
1403
+ const run = await this.stateManager.getFlowRun(runId);
1404
+ if (!run) return;
1405
+
1406
+ const nodeStates = run.nodeStates || {};
1407
+ const entry = {
1408
+ status,
1409
+ result: result ? this.truncateOutput(result) : null,
1410
+ updatedAt: new Date().toISOString()
1411
+ };
1412
+ if (errorInfo && typeof errorInfo === 'object') {
1413
+ entry.error = errorInfo;
1414
+ }
1415
+ nodeStates[nodeId] = entry;
1416
+
1417
+ await this.stateManager.updateFlowRun(runId, { nodeStates });
1418
+ }
1419
+
1420
+ /**
1421
+ * Get the last assistant message from an agent
1422
+ * @param {Object} agent - Agent object
1423
+ * @returns {string|null} Last assistant message content
1424
+ */
1425
+ getLastAssistantMessage(agent) {
1426
+ const messages = agent?.conversations?.full?.messages || [];
1427
+ for (let i = messages.length - 1; i >= 0; i--) {
1428
+ if (messages[i].role === 'assistant') {
1429
+ return messages[i].content;
1430
+ }
1431
+ }
1432
+ return null;
1433
+ }
1434
+
1435
+ /**
1436
+ * Truncate output for storage/transmission.
1437
+ *
1438
+ * Special-case: when the output is an agent completion result with a
1439
+ * structured `outputs` field, preserve the `outputs` object verbatim.
1440
+ * Those are the typed contract values that downstream nodes consume —
1441
+ * truncating them silently breaks edge field-mapping (`writer.bullets
1442
+ * → critic.bullets` would deliver a string-truncation marker instead
1443
+ * of the real list). Long prose lives in `output` / `summary` /
1444
+ * `details`; those get the legacy string truncation.
1445
+ *
1446
+ * @param {Object} output - Output to truncate
1447
+ * @returns {Object} Truncated output
1448
+ */
1449
+ truncateOutput(output) {
1450
+ if (typeof output === 'string' && output.length > 1000) {
1451
+ return output.substring(0, 1000) + '... (truncated)';
1452
+ }
1453
+ if (output && typeof output === 'object') {
1454
+ // Agent-completion shape: preserve structured `outputs`, truncate
1455
+ // only the long prose fields. Keeps the run dump useful for
1456
+ // post-mortem inspection AND keeps downstream edges working.
1457
+ const hasStructuredOutputs = output.outputs && typeof output.outputs === 'object';
1458
+ if (hasStructuredOutputs) {
1459
+ const trunc = { ...output };
1460
+ for (const k of ['output', 'summary', 'details']) {
1461
+ if (typeof trunc[k] === 'string' && trunc[k].length > 1000) {
1462
+ trunc[k] = trunc[k].substring(0, 1000) + '... (truncated)';
1463
+ }
1464
+ }
1465
+ // outputs object preserved as-is; it's structured contract data.
1466
+ return trunc;
1467
+ }
1468
+ const str = JSON.stringify(output);
1469
+ if (str.length > 1000) {
1470
+ return { truncated: true, preview: str.substring(0, 1000) };
1471
+ }
1472
+ }
1473
+ return output;
1474
+ }
1475
+
1476
+ /**
1477
+ * Broadcast flow update via WebSocket
1478
+ * @param {string} sessionId - Session ID (null broadcasts to all)
1479
+ * @param {Object} data - Update data
1480
+ */
1481
+ broadcastFlowUpdate(sessionId, data) {
1482
+ if (!this.webSocketManager) return;
1483
+
1484
+ try {
1485
+ // broadcastToSession with null sessionId broadcasts to all connections
1486
+ this.webSocketManager.broadcastToSession(sessionId, {
1487
+ type: 'flow_update',
1488
+ data,
1489
+ timestamp: new Date().toISOString()
1490
+ });
1491
+ } catch (error) {
1492
+ this.logger.warn('Failed to broadcast flow update', { error: error.message });
1493
+ }
1494
+ }
1495
+
1496
+ /**
1497
+ * Get active executions
1498
+ * @returns {Array} Active execution info
1499
+ */
1500
+ getActiveExecutions() {
1501
+ return Array.from(this.activeExecutions.entries()).map(([runId, exec]) => ({
1502
+ runId,
1503
+ flowId: exec.flowId,
1504
+ status: exec.status,
1505
+ startedAt: exec.startedAt
1506
+ }));
1507
+ }
1508
+ }
1509
+
1510
+ export default FlowExecutor;