@ai-setting/roy-agent-core 1.0.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 (378) hide show
  1. package/dist/index.js +99145 -0
  2. package/package.json +114 -0
  3. package/src/config/config-component.test.ts +627 -0
  4. package/src/config/config-component.ts +906 -0
  5. package/src/config/config-parser.test.ts +319 -0
  6. package/src/config/config-parser.ts +203 -0
  7. package/src/config/decentralized-config.test.ts +740 -0
  8. package/src/config/env-key.ts +210 -0
  9. package/src/config/env-source.test.ts +252 -0
  10. package/src/config/env-source.ts +301 -0
  11. package/src/config/file-source.test.ts +357 -0
  12. package/src/config/file-source.ts +421 -0
  13. package/src/config/index.ts +24 -0
  14. package/src/config/protocol-resolver.test.ts +217 -0
  15. package/src/config/protocol-resolver.ts +228 -0
  16. package/src/env/agent/agent-component.abort.test.ts +511 -0
  17. package/src/env/agent/agent-component.record-session.test.ts +349 -0
  18. package/src/env/agent/agent-component.test.ts +1389 -0
  19. package/src/env/agent/agent-component.tool-error.test.ts +327 -0
  20. package/src/env/agent/agent-component.ts +1711 -0
  21. package/src/env/agent/agent-config-registration.test.ts +226 -0
  22. package/src/env/agent/agent-config-registration.ts +46 -0
  23. package/src/env/agent/agent-reminder-plugin.integration.test.ts +243 -0
  24. package/src/env/agent/index.ts +10 -0
  25. package/src/env/agent/summary-agent.parse-hint.test.ts +360 -0
  26. package/src/env/agent/summary-agent.ts +508 -0
  27. package/src/env/agent/types.ts +536 -0
  28. package/src/env/commands/commands-component.test.ts +364 -0
  29. package/src/env/commands/commands-component.ts +604 -0
  30. package/src/env/commands/commands-config-registration.test.ts +198 -0
  31. package/src/env/commands/commands-config-registration.ts +38 -0
  32. package/src/env/commands/index.ts +21 -0
  33. package/src/env/commands/parser.test.ts +203 -0
  34. package/src/env/commands/parser.ts +115 -0
  35. package/src/env/commands/types.ts +184 -0
  36. package/src/env/commands-prompt-integration.test.ts +243 -0
  37. package/src/env/component-env.test.ts +119 -0
  38. package/src/env/component.ts +335 -0
  39. package/src/env/constants.test.ts +72 -0
  40. package/src/env/constants.ts +123 -0
  41. package/src/env/debug/debug-component.test.ts +114 -0
  42. package/src/env/debug/debug-component.ts +547 -0
  43. package/src/env/debug/formatters/index.ts +9 -0
  44. package/src/env/debug/formatters/repl-formatter.test.ts +139 -0
  45. package/src/env/debug/formatters/repl-formatter.ts +358 -0
  46. package/src/env/debug/formatters/trace-formatter.test.ts +119 -0
  47. package/src/env/debug/formatters/trace-formatter.ts +191 -0
  48. package/src/env/debug/formatters/tree-formatter.test.ts +107 -0
  49. package/src/env/debug/formatters/tree-formatter.ts +325 -0
  50. package/src/env/debug/index.ts +38 -0
  51. package/src/env/debug/parser/regex-parser.test.ts +201 -0
  52. package/src/env/debug/parser/regex-parser.ts +196 -0
  53. package/src/env/debug/parser/span-builder.test.ts +241 -0
  54. package/src/env/debug/parser/span-builder.ts +386 -0
  55. package/src/env/debug/reader/log-reader.test.ts +170 -0
  56. package/src/env/debug/reader/log-reader.ts +186 -0
  57. package/src/env/debug/reader/span-db-reader.test.ts +118 -0
  58. package/src/env/debug/reader/span-db-reader.ts +201 -0
  59. package/src/env/debug/types.test.ts +187 -0
  60. package/src/env/debug/types.ts +171 -0
  61. package/src/env/environment-init.test.ts +183 -0
  62. package/src/env/environment-lifecycle.test.ts +516 -0
  63. package/src/env/environment-service.test.ts +332 -0
  64. package/src/env/environment.handle-query.test.ts +96 -0
  65. package/src/env/environment.test.ts +232 -0
  66. package/src/env/environment.ts +708 -0
  67. package/src/env/errors.test.ts +165 -0
  68. package/src/env/errors.ts +157 -0
  69. package/src/env/event-source/event-source-agent-handler.test.ts +193 -0
  70. package/src/env/event-source/event-source-agent-handler.ts +111 -0
  71. package/src/env/event-source/event-source-component.process-cleanup.test.ts +236 -0
  72. package/src/env/event-source/event-source-component.stop.test.ts +346 -0
  73. package/src/env/event-source/event-source-component.test.ts +1207 -0
  74. package/src/env/event-source/event-source-component.ts +1379 -0
  75. package/src/env/event-source/event-source-config-registration.test.ts +242 -0
  76. package/src/env/event-source/event-source-config-registration.ts +37 -0
  77. package/src/env/event-source/event-source-integration.test.ts +320 -0
  78. package/src/env/event-source/event-source-platform.test.ts +630 -0
  79. package/src/env/event-source/types.ts +298 -0
  80. package/src/env/hook/global-hook-manager.ts +162 -0
  81. package/src/env/hook/hook-manager.test.ts +374 -0
  82. package/src/env/hook/hook-manager.ts +309 -0
  83. package/src/env/hook/index.ts +38 -0
  84. package/src/env/hook/types.ts +138 -0
  85. package/src/env/index.ts +144 -0
  86. package/src/env/interface.ts +203 -0
  87. package/src/env/llm/hooks.test.ts +293 -0
  88. package/src/env/llm/hooks.ts +316 -0
  89. package/src/env/llm/index.ts +61 -0
  90. package/src/env/llm/invoke-threshold-check.test.ts +88 -0
  91. package/src/env/llm/invoke-timeout.test.ts +54 -0
  92. package/src/env/llm/invoke.test.ts +71 -0
  93. package/src/env/llm/invoke.ts +1039 -0
  94. package/src/env/llm/llm-config.test.ts +523 -0
  95. package/src/env/llm/llm.test.ts +233 -0
  96. package/src/env/llm/llm.ts +568 -0
  97. package/src/env/llm/provider.test.ts +182 -0
  98. package/src/env/llm/provider.ts +108 -0
  99. package/src/env/llm/transform.test.ts +251 -0
  100. package/src/env/llm/transform.ts +286 -0
  101. package/src/env/llm/types.test.ts +580 -0
  102. package/src/env/llm/types.ts +424 -0
  103. package/src/env/log-trace/decorator-otel.test.ts +182 -0
  104. package/src/env/log-trace/decorator.ts +230 -0
  105. package/src/env/log-trace/index.ts +79 -0
  106. package/src/env/log-trace/log-trace-component.test.ts +242 -0
  107. package/src/env/log-trace/log-trace-component.ts +497 -0
  108. package/src/env/log-trace/log-trace-config-registration.test.ts +348 -0
  109. package/src/env/log-trace/log-trace-config-registration.ts +45 -0
  110. package/src/env/log-trace/logger.test.ts +149 -0
  111. package/src/env/log-trace/logger.ts +522 -0
  112. package/src/env/log-trace/opentelemetry/cli-propagation.test.ts +147 -0
  113. package/src/env/log-trace/opentelemetry/cli-propagation.ts +194 -0
  114. package/src/env/log-trace/opentelemetry/integration.test.ts +668 -0
  115. package/src/env/log-trace/opentelemetry/mod.ts +25 -0
  116. package/src/env/log-trace/opentelemetry/propagation-env.test.ts +181 -0
  117. package/src/env/log-trace/opentelemetry/propagation-env.ts +136 -0
  118. package/src/env/log-trace/opentelemetry/propagation.test.ts +259 -0
  119. package/src/env/log-trace/opentelemetry/propagation.ts +215 -0
  120. package/src/env/log-trace/opentelemetry/tracer-provider-context.test.ts +166 -0
  121. package/src/env/log-trace/opentelemetry/tracer-provider.test.ts +379 -0
  122. package/src/env/log-trace/opentelemetry/tracer-provider.ts +612 -0
  123. package/src/env/log-trace/span-storage.test.ts +145 -0
  124. package/src/env/log-trace/span-storage.ts +230 -0
  125. package/src/env/log-trace/trace-context.test.ts +187 -0
  126. package/src/env/log-trace/trace-context.ts +162 -0
  127. package/src/env/log-trace/types.test.ts +63 -0
  128. package/src/env/log-trace/types.ts +172 -0
  129. package/src/env/mcp/README.md +244 -0
  130. package/src/env/mcp/__integration__/mcp-component.integration.test.ts +373 -0
  131. package/src/env/mcp/config.test.ts +74 -0
  132. package/src/env/mcp/config.ts +116 -0
  133. package/src/env/mcp/index.ts +41 -0
  134. package/src/env/mcp/loader.test.ts +161 -0
  135. package/src/env/mcp/loader.ts +209 -0
  136. package/src/env/mcp/mcp-component.test.ts +111 -0
  137. package/src/env/mcp/mcp-component.ts +358 -0
  138. package/src/env/mcp/mcp-config-registration.test.ts +304 -0
  139. package/src/env/mcp/mcp-config-registration.ts +50 -0
  140. package/src/env/mcp/scanner.test.ts +170 -0
  141. package/src/env/mcp/scanner.ts +246 -0
  142. package/src/env/mcp/tool/adapter.test.ts +520 -0
  143. package/src/env/mcp/tool/adapter.ts +521 -0
  144. package/src/env/mcp/tool/index.ts +5 -0
  145. package/src/env/mcp/types.test.ts +171 -0
  146. package/src/env/mcp/types.ts +79 -0
  147. package/src/env/memory/README.md +177 -0
  148. package/src/env/memory/built-in/index.ts +59 -0
  149. package/src/env/memory/built-in/recall-memory.ts +103 -0
  150. package/src/env/memory/built-in/record-memory.ts +148 -0
  151. package/src/env/memory/index.ts +20 -0
  152. package/src/env/memory/memory-component.test.ts +239 -0
  153. package/src/env/memory/memory-component.ts +503 -0
  154. package/src/env/memory/memory-config-registration.test.ts +67 -0
  155. package/src/env/memory/memory-config-registration.ts +48 -0
  156. package/src/env/memory/memory-config.ts +45 -0
  157. package/src/env/memory/memory-file.test.ts +268 -0
  158. package/src/env/memory/plugin/index.ts +48 -0
  159. package/src/env/memory/plugin/memory-agent.test.ts +249 -0
  160. package/src/env/memory/plugin/memory-agent.ts +365 -0
  161. package/src/env/memory/plugin/memory-manager.ts +198 -0
  162. package/src/env/memory/plugin/memory-plugin-agent.test.ts +145 -0
  163. package/src/env/memory/plugin/memory-plugin.ts +210 -0
  164. package/src/env/memory/plugin/plugin-simplified.test.ts +51 -0
  165. package/src/env/memory/plugin/recall-memory.test.ts +106 -0
  166. package/src/env/memory/plugin/recall-memory.ts +53 -0
  167. package/src/env/memory/plugin/types.ts +101 -0
  168. package/src/env/memory/tools/memory-agent-tools.ts +228 -0
  169. package/src/env/memory/types.ts +85 -0
  170. package/src/env/paths.ts +118 -0
  171. package/src/env/prompt/index.ts +18 -0
  172. package/src/env/prompt/memory-prompts.test.ts +91 -0
  173. package/src/env/prompt/prompt-component.test.ts +491 -0
  174. package/src/env/prompt/prompt-component.ts +619 -0
  175. package/src/env/prompt/prompt-config-registration.test.ts +213 -0
  176. package/src/env/prompt/prompt-config-registration.ts +39 -0
  177. package/src/env/prompt/prompts-index.ts +504 -0
  178. package/src/env/prompt/renderer.ts +67 -0
  179. package/src/env/prompt/types.ts +136 -0
  180. package/src/env/session/hooks.ts +18 -0
  181. package/src/env/session/index.ts +37 -0
  182. package/src/env/session/search-query-parser.test.ts +425 -0
  183. package/src/env/session/search-query-parser.ts +171 -0
  184. package/src/env/session/session-checkpoint.test.ts +523 -0
  185. package/src/env/session/session-component.extract-recent-messages.test.ts +209 -0
  186. package/src/env/session/session-component.test.ts +132 -0
  187. package/src/env/session/session-component.ts +1249 -0
  188. package/src/env/session/session-config-registration.test.ts +138 -0
  189. package/src/env/session/session-config-registration.ts +52 -0
  190. package/src/env/session/session-message-converter.test.ts +763 -0
  191. package/src/env/session/session-message-converter.ts +415 -0
  192. package/src/env/session/session-message-e2e.test.ts +448 -0
  193. package/src/env/session/session-search.test.ts +391 -0
  194. package/src/env/session/session-store.test.ts +362 -0
  195. package/src/env/session/session-store.ts +141 -0
  196. package/src/env/session/storage/index.ts +6 -0
  197. package/src/env/session/storage/memory.ts +502 -0
  198. package/src/env/session/storage/sqlite.ts +794 -0
  199. package/src/env/session/types.ts +742 -0
  200. package/src/env/skill/config.ts +39 -0
  201. package/src/env/skill/index.ts +6 -0
  202. package/src/env/skill/parser.test.ts +116 -0
  203. package/src/env/skill/parser.ts +77 -0
  204. package/src/env/skill/scanner.test.ts +211 -0
  205. package/src/env/skill/scanner.ts +119 -0
  206. package/src/env/skill/skill-component.test.ts +234 -0
  207. package/src/env/skill/skill-component.ts +352 -0
  208. package/src/env/skill/skill-config-registration.test.ts +60 -0
  209. package/src/env/skill/skill-config-registration.ts +43 -0
  210. package/src/env/skill/tool/index.ts +1 -0
  211. package/src/env/skill/tool/skill-tool.test.ts +100 -0
  212. package/src/env/skill/tool/skill-tool.ts +72 -0
  213. package/src/env/skill/types.ts +64 -0
  214. package/src/env/task/delegate/delegate-tool.test.ts +498 -0
  215. package/src/env/task/delegate/delegate-tool.ts +1014 -0
  216. package/src/env/task/delegate/index.ts +18 -0
  217. package/src/env/task/delegate/stop-tool.test.ts +140 -0
  218. package/src/env/task/delegate/stop-tool.ts +119 -0
  219. package/src/env/task/delegate/task-events.test.ts +178 -0
  220. package/src/env/task/delegate/task-events.ts +143 -0
  221. package/src/env/task/hooks/contexts.test.ts +92 -0
  222. package/src/env/task/hooks/contexts.ts +192 -0
  223. package/src/env/task/hooks/index.ts +23 -0
  224. package/src/env/task/hooks/task-hook-points.test.ts +32 -0
  225. package/src/env/task/hooks/task-hook-points.ts +54 -0
  226. package/src/env/task/index.ts +7 -0
  227. package/src/env/task/plugins/index.ts +13 -0
  228. package/src/env/task/plugins/task-plugin.test.ts +74 -0
  229. package/src/env/task/plugins/task-plugin.ts +89 -0
  230. package/src/env/task/plugins/task-tag-plugin.test.ts +377 -0
  231. package/src/env/task/plugins/task-tag-plugin.ts +319 -0
  232. package/src/env/task/plugins/task-workflow-extractor.integration.test.ts +226 -0
  233. package/src/env/task/plugins/workflow-extractor-agent.test.ts +107 -0
  234. package/src/env/task/plugins/workflow-extractor-agent.ts +225 -0
  235. package/src/env/task/storage/index.ts +6 -0
  236. package/src/env/task/storage/sqlite-task-store.test.ts +283 -0
  237. package/src/env/task/storage/sqlite-task-store.ts +903 -0
  238. package/src/env/task/storage/task-search.test.ts +291 -0
  239. package/src/env/task/tag-service.test.ts +198 -0
  240. package/src/env/task/tag-service.ts +264 -0
  241. package/src/env/task/task-component.test.ts +193 -0
  242. package/src/env/task/task-component.ts +658 -0
  243. package/src/env/task/task-config-registration.test.ts +57 -0
  244. package/src/env/task/task-config-registration.ts +37 -0
  245. package/src/env/task/task-types.test.ts +137 -0
  246. package/src/env/task/tools/complete-tool.ts +44 -0
  247. package/src/env/task/tools/create-tool.ts +49 -0
  248. package/src/env/task/tools/delete-tool.ts +43 -0
  249. package/src/env/task/tools/get-tool.ts +59 -0
  250. package/src/env/task/tools/index.ts +10 -0
  251. package/src/env/task/tools/list-tool.ts +40 -0
  252. package/src/env/task/tools/operation/create-tool.ts +48 -0
  253. package/src/env/task/tools/operation/delete-tool.ts +43 -0
  254. package/src/env/task/tools/operation/get-tool.ts +43 -0
  255. package/src/env/task/tools/operation/index.ts +9 -0
  256. package/src/env/task/tools/operation/list-tool.ts +40 -0
  257. package/src/env/task/tools/operation/operation-tools.test.ts +274 -0
  258. package/src/env/task/tools/operation/operation-types.ts +75 -0
  259. package/src/env/task/tools/operation/update-tool.ts +47 -0
  260. package/src/env/task/tools/task-tools.test.ts +203 -0
  261. package/src/env/task/tools/task-types.test.ts +75 -0
  262. package/src/env/task/tools/task-types.ts +68 -0
  263. package/src/env/task/tools/update-tool.ts +70 -0
  264. package/src/env/task/types.ts +160 -0
  265. package/src/env/tool/built-in/bash.ts +201 -0
  266. package/src/env/tool/built-in/echo.ts +29 -0
  267. package/src/env/tool/built-in/edit-file.test.ts +136 -0
  268. package/src/env/tool/built-in/edit-file.ts +92 -0
  269. package/src/env/tool/built-in/glob.test.ts +94 -0
  270. package/src/env/tool/built-in/glob.ts +65 -0
  271. package/src/env/tool/built-in/grep.test.ts +122 -0
  272. package/src/env/tool/built-in/grep.ts +108 -0
  273. package/src/env/tool/built-in/index.ts +44 -0
  274. package/src/env/tool/built-in/read-file.test.ts +84 -0
  275. package/src/env/tool/built-in/read-file.ts +75 -0
  276. package/src/env/tool/built-in/write-file.test.ts +119 -0
  277. package/src/env/tool/built-in/write-file.ts +68 -0
  278. package/src/env/tool/index.ts +24 -0
  279. package/src/env/tool/registry.test.ts +257 -0
  280. package/src/env/tool/registry.ts +167 -0
  281. package/src/env/tool/tool-component.test.ts +559 -0
  282. package/src/env/tool/tool-component.ts +563 -0
  283. package/src/env/tool/tool-config-registration.test.ts +249 -0
  284. package/src/env/tool/tool-config-registration.ts +46 -0
  285. package/src/env/tool/types.ts +267 -0
  286. package/src/env/tool/validator.test.ts +143 -0
  287. package/src/env/tool/validator.ts +44 -0
  288. package/src/env/types.ts +180 -0
  289. package/src/env/workflow/ask-user-tool-registration.test.ts +216 -0
  290. package/src/env/workflow/complex-workflow.integration.test.ts +1900 -0
  291. package/src/env/workflow/decorators/decorator-node.ts +229 -0
  292. package/src/env/workflow/decorators/decorator.test.ts +196 -0
  293. package/src/env/workflow/decorators/edge.ts +82 -0
  294. package/src/env/workflow/decorators/index.ts +31 -0
  295. package/src/env/workflow/decorators/node-as.ts +98 -0
  296. package/src/env/workflow/decorators/workflow.ts +54 -0
  297. package/src/env/workflow/engine/dag-manager.test.ts +570 -0
  298. package/src/env/workflow/engine/dag-manager.ts +594 -0
  299. package/src/env/workflow/engine/engine.ts +1422 -0
  300. package/src/env/workflow/engine/event-bus.test.ts +359 -0
  301. package/src/env/workflow/engine/event-bus.ts +156 -0
  302. package/src/env/workflow/engine/executor-agent-session.test.ts +84 -0
  303. package/src/env/workflow/engine/executor.test.ts +619 -0
  304. package/src/env/workflow/engine/executor.ts +593 -0
  305. package/src/env/workflow/engine/index.ts +24 -0
  306. package/src/env/workflow/engine/node-registry.test.ts +560 -0
  307. package/src/env/workflow/engine/node-registry.ts +289 -0
  308. package/src/env/workflow/engine/resume-removed.test.ts +22 -0
  309. package/src/env/workflow/engine/scheduler.test.ts +715 -0
  310. package/src/env/workflow/engine/scheduler.ts +318 -0
  311. package/src/env/workflow/engine/workflow-engine.test.ts +815 -0
  312. package/src/env/workflow/extractor/workflow-converter.ts +306 -0
  313. package/src/env/workflow/fixtures.ts +380 -0
  314. package/src/env/workflow/index.ts +38 -0
  315. package/src/env/workflow/integration/run-resume-unified.test.ts +186 -0
  316. package/src/env/workflow/integration/service-integration.test.ts +267 -0
  317. package/src/env/workflow/metadata/keys.ts +12 -0
  318. package/src/env/workflow/nodes/agent-component-adapter.test.ts +318 -0
  319. package/src/env/workflow/nodes/agent-component-adapter.ts +448 -0
  320. package/src/env/workflow/nodes/agent-node.test.ts +371 -0
  321. package/src/env/workflow/nodes/agent-node.ts +598 -0
  322. package/src/env/workflow/nodes/ask-user-node.ts +113 -0
  323. package/src/env/workflow/nodes/condition-node.ts +200 -0
  324. package/src/env/workflow/nodes/index.ts +9 -0
  325. package/src/env/workflow/nodes/merge-node.ts +141 -0
  326. package/src/env/workflow/nodes/skill-node.test.ts +253 -0
  327. package/src/env/workflow/nodes/skill-node.ts +393 -0
  328. package/src/env/workflow/nodes/tool-node.test.ts +251 -0
  329. package/src/env/workflow/nodes/tool-node.ts +493 -0
  330. package/src/env/workflow/nodes/workflow-llm-history.test.ts +455 -0
  331. package/src/env/workflow/nodes/workflow-node.test.ts +315 -0
  332. package/src/env/workflow/nodes/workflow-node.ts +311 -0
  333. package/src/env/workflow/service/index.ts +27 -0
  334. package/src/env/workflow/service/registry.test.ts +133 -0
  335. package/src/env/workflow/service/registry.ts +71 -0
  336. package/src/env/workflow/service/workflow-service.test.ts +310 -0
  337. package/src/env/workflow/service/workflow-service.ts +393 -0
  338. package/src/env/workflow/storage/index.ts +28 -0
  339. package/src/env/workflow/storage/mock-repositories.ts +385 -0
  340. package/src/env/workflow/storage/sqlite.test.ts +179 -0
  341. package/src/env/workflow/storage/sqlite.ts +163 -0
  342. package/src/env/workflow/storage/workflow-repo.test.ts +780 -0
  343. package/src/env/workflow/storage/workflow-repo.ts +342 -0
  344. package/src/env/workflow/tools/ask-user-tool.ts +82 -0
  345. package/src/env/workflow/tools/index.ts +26 -0
  346. package/src/env/workflow/tools/run-workflow.test.ts +352 -0
  347. package/src/env/workflow/tools/run-workflow.ts +214 -0
  348. package/src/env/workflow/types/context.ts +18 -0
  349. package/src/env/workflow/types/decorators-types.ts +198 -0
  350. package/src/env/workflow/types/event.test.ts +515 -0
  351. package/src/env/workflow/types/event.ts +193 -0
  352. package/src/env/workflow/types/index.ts +49 -0
  353. package/src/env/workflow/types/run.test.ts +437 -0
  354. package/src/env/workflow/types/run.ts +173 -0
  355. package/src/env/workflow/types/workflow-hil.ts +114 -0
  356. package/src/env/workflow/types/workflow-message.test.ts +138 -0
  357. package/src/env/workflow/types/workflow-message.ts +196 -0
  358. package/src/env/workflow/types/workflow-session.test.ts +95 -0
  359. package/src/env/workflow/types/workflow-session.ts +59 -0
  360. package/src/env/workflow/types/workflow.test.ts +495 -0
  361. package/src/env/workflow/types/workflow.ts +195 -0
  362. package/src/env/workflow/types_compat.ts +51 -0
  363. package/src/env/workflow/utils/create-workflow.ts +47 -0
  364. package/src/env/workflow/utils/execution-state.ts +245 -0
  365. package/src/env/workflow/utils/index.ts +18 -0
  366. package/src/env/workflow/utils/node-registry-helper.ts +58 -0
  367. package/src/env/workflow/utils/recovery-validator.test.ts +460 -0
  368. package/src/env/workflow/utils/recovery-validator.ts +377 -0
  369. package/src/env/workflow/utils/session-parser.test.ts +111 -0
  370. package/src/env/workflow/utils/session-parser.ts +94 -0
  371. package/src/env/workflow/utils/session-recovery.test.ts +334 -0
  372. package/src/env/workflow/utils/session-recovery.ts +188 -0
  373. package/src/env/workflow/utils/template-resolver.test.ts +258 -0
  374. package/src/env/workflow/utils/template-resolver.ts +436 -0
  375. package/src/env/workflow/utils/validation-rules.ts +149 -0
  376. package/src/env/workflow/workflow-component.ts +544 -0
  377. package/src/index.ts +422 -0
  378. package/src/utils/id.ts +21 -0
@@ -0,0 +1,1422 @@
1
+ /**
2
+ * @fileoverview WorkflowEngine - Session-based workflow execution engine
3
+ *
4
+ * 基于 Session 实现 run/resume 机制:
5
+ * - Session 直接作为 workflow run 的实例
6
+ * - 使用 workflow.node.* 消息记录节点执行状态
7
+ * - resume 时从 Session 消息恢复状态
8
+ *
9
+ * @version v2.0
10
+ */
11
+
12
+ import { EventEmitter } from 'events';
13
+ import type {
14
+ WorkflowDefinition,
15
+ Workflow,
16
+ RunOptions,
17
+ RunResult,
18
+ RunStatus,
19
+ NodeExecutionContext,
20
+ } from '../types/index';
21
+ import { createWorkflowEvent, WorkflowEvent } from '../types/event';
22
+ import { createNodeInterruptEvent, AskUserError } from '../types/workflow-hil';
23
+ import { DAGManager } from './dag-manager';
24
+ import { EventBus } from './event-bus';
25
+ import { Scheduler } from './scheduler';
26
+ import { Executor, ExecutorOptions } from './executor';
27
+ import { NodeRegistry } from './node-registry';
28
+ import type { SessionComponent } from '../../session/session-component';
29
+ import type { WorkflowSessionMetadata, AgentSessionRef } from '../types/workflow-message';
30
+ import type { Environment } from '../../interface';
31
+ import { createLogger } from '../../log-trace/logger';
32
+ import { TracedAs } from '../../log-trace/decorator';
33
+
34
+ // Logger
35
+ const logger = createLogger("workflow:engine");
36
+
37
+ // ============================================================================
38
+ // Type Definitions
39
+ // ============================================================================
40
+
41
+ /**
42
+ * Agent Session Recovery Info
43
+ */
44
+ export interface AgentSessionRecoveryInfo {
45
+ nodeId: string;
46
+ agentSessionId: string;
47
+ }
48
+
49
+ /**
50
+ * Options for creating WorkflowEngine via static factory
51
+ */
52
+ export interface WorkflowEngineCreateOptions {
53
+ env?: {
54
+ getComponent<T>(name: string): T | undefined;
55
+ };
56
+ toolRegistry?: any;
57
+ skillRegistry?: any;
58
+ workflowRunner?: any;
59
+ sessionComponent?: SessionComponent;
60
+ /** Repository for loading workflow definitions during resume */
61
+ workflowRepository?: WorkflowRepository;
62
+ }
63
+
64
+ /**
65
+ * Workflow Repository interface (abstraction)
66
+ */
67
+ export interface WorkflowRepository {
68
+ getById(id: string): Workflow | null;
69
+ getByName(name: string): Workflow | null;
70
+ }
71
+
72
+ /**
73
+ * Active Session State
74
+ * 内存中追踪正在运行的 workflow session
75
+ */
76
+ interface ActiveSessionState {
77
+ sessionId: string;
78
+ workflowId: string;
79
+ workflowName: string;
80
+ status: RunStatus;
81
+ startedAt: Date;
82
+ eventBus: EventBus;
83
+ dagManager: DAGManager;
84
+ scheduler: Scheduler;
85
+ executor: Executor;
86
+ nodeRegistry: NodeRegistry;
87
+ nodeOutputs: Map<string, any>;
88
+ config: WorkflowEngineConfig;
89
+ abortController: AbortController;
90
+ completedPromise: Promise<RunResult>;
91
+ resolveCompleted: (result: RunResult) => void;
92
+ rejectCompleted: (error: Error) => void;
93
+ // LLM history for agent nodes
94
+ workflowHistory: Array<{
95
+ role: 'user' | 'assistant' | 'tool';
96
+ content: string;
97
+ toolCallId?: string;
98
+ toolName?: string;
99
+ nodeId?: string;
100
+ }>;
101
+ // Agent sessions reference
102
+ agentSessions: Map<string, string>; // nodeId -> agentSessionId
103
+ }
104
+
105
+ /**
106
+ * WorkflowEngine Configuration
107
+ */
108
+ export interface WorkflowEngineConfig {
109
+ parallelLimit: number | null;
110
+ timeout: number | null;
111
+ debug: boolean;
112
+ }
113
+
114
+ /**
115
+ * Runtime State from Session Messages
116
+ * 从 Session 消息恢复的状态
117
+ */
118
+ interface RuntimeState {
119
+ nodeOutputs: Map<string, unknown>;
120
+ pendingNodeId: string | null;
121
+ waitingForUser: boolean;
122
+ agentSessionId?: string;
123
+ lastInterruptMessage?: any;
124
+ }
125
+
126
+ // ============================================================================
127
+ // WorkflowEngine Class
128
+ // ============================================================================
129
+
130
+ /**
131
+ * WorkflowEngine
132
+ *
133
+ * Session-based workflow execution engine.
134
+ * Uses Session to manage workflow run state and node execution history.
135
+ */
136
+ export class WorkflowEngine extends EventEmitter {
137
+ /** 活跃的 session 状态,key 为 sessionId */
138
+ private activeSessions: Map<string, ActiveSessionState> = new Map();
139
+
140
+ /** Session ID 计数器 */
141
+ private sessionIdCounter = 0;
142
+
143
+ /** Session component */
144
+ private sessionComponent?: SessionComponent;
145
+
146
+ /** Node registry */
147
+ private nodeRegistry: NodeRegistry;
148
+
149
+ /** Workflow repository for loading definitions during resume */
150
+ private workflowRepository?: WorkflowRepository;
151
+
152
+ /**
153
+ * Create a new WorkflowEngine
154
+ */
155
+ constructor(
156
+ nodeRegistry: NodeRegistry,
157
+ sessionComponent?: SessionComponent,
158
+ workflowRepository?: WorkflowRepository
159
+ ) {
160
+ super();
161
+ this.nodeRegistry = nodeRegistry;
162
+ this.sessionComponent = sessionComponent;
163
+ this.workflowRepository = workflowRepository;
164
+ }
165
+
166
+ /**
167
+ * Create a WorkflowEngine with automatic AgentComponent integration
168
+ */
169
+ static async create(options: WorkflowEngineCreateOptions): Promise<WorkflowEngine> {
170
+ const agentComponent = options.env?.getComponent<any>('agent');
171
+ const sessionComponent = options.sessionComponent;
172
+
173
+ const nodeRegistry = new NodeRegistry({
174
+ toolRegistry: options.toolRegistry,
175
+ skillRegistry: options.skillRegistry,
176
+ agentComponent,
177
+ workflowRunner: options.workflowRunner,
178
+ sessionComponent,
179
+ });
180
+
181
+ return new WorkflowEngine(nodeRegistry, sessionComponent, options.workflowRepository);
182
+ }
183
+
184
+ // =========================================================================
185
+ // Session ID Utilities
186
+ // =========================================================================
187
+
188
+ /**
189
+ * Generate a unique run ID
190
+ */
191
+ private generateRunId(): string {
192
+ const random = Math.random().toString(36).substring(2, 8);
193
+ return `run_${Date.now()}_${++this.sessionIdCounter}_${random}`;
194
+ }
195
+
196
+ /**
197
+ * Extract runId from sessionId
198
+ * sessionId format: workflow_{runId}
199
+ */
200
+ private getRunIdFromSessionId(sessionId: string): string {
201
+ return sessionId.replace(/^workflow_/, '');
202
+ }
203
+
204
+ /**
205
+ * Get session ID for a run
206
+ */
207
+ getSessionId(runId: string): string {
208
+ return `workflow_${runId}`;
209
+ }
210
+
211
+ // =========================================================================
212
+ // Public API: Session Management
213
+ // =========================================================================
214
+
215
+ /**
216
+ * Create a new workflow session
217
+ *
218
+ * @param workflow - Workflow definition or entity
219
+ * @param options - Optional configuration
220
+ * @returns Promise resolving to sessionId (format: workflow_{runId})
221
+ */
222
+ @TracedAs("workflow.engine.createSession", { recordParams: true, recordResult: true, log: true })
223
+ async createSession(
224
+ workflow: Workflow | WorkflowDefinition,
225
+ options?: RunOptions
226
+ ): Promise<string> {
227
+ const definition = 'definition' in workflow
228
+ ? workflow.definition
229
+ : workflow;
230
+
231
+ const workflowId = 'id' in workflow
232
+ ? workflow.id
233
+ : `inline_${definition.name}`;
234
+
235
+ const workflowName = definition.name;
236
+
237
+ // Generate run ID and session ID
238
+ const runId = this.generateRunId();
239
+ const sessionId = this.getSessionId(runId);
240
+
241
+ // Create Session
242
+ const metadata: WorkflowSessionMetadata = {
243
+ type: 'workflow',
244
+ workflowId,
245
+ workflowName,
246
+ workflowVersion: definition.version,
247
+ status: 'running',
248
+ input: options?.input,
249
+ agentSessions: [],
250
+ };
251
+
252
+ if (this.sessionComponent) {
253
+ await this.sessionComponent.create({
254
+ id: sessionId,
255
+ title: `Workflow: ${workflowName} (${runId})`,
256
+ metadata,
257
+ });
258
+ }
259
+
260
+ logger.info(`[WorkflowEngine] Created session: ${sessionId}`);
261
+
262
+ return sessionId;
263
+ }
264
+
265
+ /**
266
+ * Run a workflow by sessionId (unified entry point)
267
+ *
268
+ * This method supports both:
269
+ * 1. First run: session doesn't exist → create session, start from entry node
270
+ * 2. Resume: session exists with messages → infer next node from messages
271
+ *
272
+ * @param sessionId - Session ID (format: workflow_{runId} or any string)
273
+ * @param options - Optional configuration
274
+ * @returns Promise resolving to RunResult
275
+ */
276
+ @TracedAs("workflow.engine.run", { recordParams: true, recordResult: true, log: true })
277
+ async run(
278
+ sessionId: string,
279
+ options?: RunOptions & { workflowId?: string }
280
+ ): Promise<RunResult> {
281
+ // 1. Try to get existing session
282
+ let session = this.sessionComponent
283
+ ? await this.sessionComponent.get(sessionId)
284
+ : null;
285
+
286
+ let workflowDef: WorkflowDefinition | null = null;
287
+ let workflowId: string = '';
288
+ let workflowName: string = '';
289
+
290
+ if (session) {
291
+ // Session exists - load workflow definition
292
+ const metadata = session.metadata as WorkflowSessionMetadata;
293
+ if (metadata.type === 'workflow' && metadata.workflowId) {
294
+ workflowDef = this.findDefinitionForWorkflow(metadata.workflowId);
295
+ workflowId = metadata.workflowId;
296
+ workflowName = metadata.workflowName;
297
+ }
298
+ if (!workflowDef && metadata.workflowName) {
299
+ workflowDef = this.findDefinitionForWorkflow(metadata.workflowName);
300
+ workflowId = metadata.workflowName;
301
+ workflowName = metadata.workflowName;
302
+ }
303
+ } else {
304
+ // No session - this is a new run
305
+ if (!options?.workflowId) {
306
+ throw new Error('workflowId required for new session');
307
+ }
308
+
309
+ workflowDef = this.findDefinitionForWorkflow(options.workflowId);
310
+ if (!workflowDef) {
311
+ throw new Error(`Workflow not found: ${options.workflowId}`);
312
+ }
313
+
314
+ workflowId = options.workflowId;
315
+ workflowName = workflowDef.name;
316
+
317
+ // Create session
318
+ if (this.sessionComponent) {
319
+ const sessionMetadata: WorkflowSessionMetadata = {
320
+ type: 'workflow',
321
+ workflowId,
322
+ workflowName,
323
+ workflowVersion: workflowDef.version,
324
+ status: 'running',
325
+ };
326
+
327
+ await this.sessionComponent.create({
328
+ id: sessionId,
329
+ title: `Workflow: ${workflowName}`,
330
+ metadata: sessionMetadata,
331
+ });
332
+
333
+ session = await this.sessionComponent.get(sessionId);
334
+ }
335
+ }
336
+
337
+ if (!session || !workflowDef) {
338
+ throw new Error(`Cannot start workflow: session or workflow not found`);
339
+ }
340
+
341
+ // 2. Check session status
342
+ const metadata = session.metadata as WorkflowSessionMetadata;
343
+ if (metadata.status === 'completed') {
344
+ return { runId: this.getRunIdFromSessionId(sessionId), status: 'completed' };
345
+ }
346
+ if (metadata.status === 'failed') {
347
+ return { runId: this.getRunIdFromSessionId(sessionId), status: 'failed' };
348
+ }
349
+
350
+ // 3. Load messages and infer next node
351
+ const messages = this.sessionComponent
352
+ ? await this.sessionComponent.getMessages(sessionId)
353
+ : [];
354
+
355
+ // 4. Import and use session-recovery utilities
356
+ const { inferNextNode } = await import('../utils/session-recovery');
357
+
358
+ // Get entry node from workflow definition
359
+ const entry = workflowDef.entry;
360
+ const entryNode = Array.isArray(entry) ? entry[0] : entry;
361
+
362
+ const resumePoint = inferNextNode(messages, {
363
+ entryNode: entryNode !== '__default_entry__' ? entryNode : undefined,
364
+ edges: undefined, // edges not available in WorkflowDefinition, derived at runtime
365
+ });
366
+
367
+ // 5. Initialize session state and execute
368
+ const sessionState = await this.initializeSessionState(
369
+ sessionId,
370
+ workflowDef,
371
+ workflowId,
372
+ workflowName,
373
+ options
374
+ );
375
+
376
+ this.activeSessions.set(sessionId, sessionState);
377
+
378
+ // 6. Run with resume point
379
+ return this.runWithResume(sessionId, resumePoint, options);
380
+ }
381
+
382
+ /**
383
+ * Initialize session state
384
+ */
385
+ private async initializeSessionState(
386
+ sessionId: string,
387
+ definition: WorkflowDefinition,
388
+ workflowId: string,
389
+ workflowName: string,
390
+ options?: RunOptions
391
+ ): Promise<ActiveSessionState> {
392
+ // Create DAG manager
393
+ const dagManager = new DAGManager(definition);
394
+
395
+ // Validate DAG
396
+ const validation = dagManager.validate();
397
+ if (!validation.valid) {
398
+ throw new Error(`Invalid workflow: ${validation.errors.join(', ')}`);
399
+ }
400
+
401
+ // Get parallel limit
402
+ const parallelLimit = options?.parallelLimit
403
+ ?? definition.config?.parallel_limit
404
+ ?? null;
405
+
406
+ // Create scheduler
407
+ const scheduler = new Scheduler(dagManager, { parallelLimit });
408
+
409
+ // Create event bus
410
+ const eventBus = new EventBus();
411
+
412
+ // Create executor
413
+ const executorOptions: ExecutorOptions = {
414
+ globalTimeout: options?.timeout ?? definition.config?.timeout ?? null,
415
+ globalRetry: definition.config?.retry ?? null,
416
+ debug: options?.debug ?? definition.config?.debug ?? false,
417
+ };
418
+ const executor = new Executor(this.nodeRegistry, eventBus, executorOptions, this.sessionComponent);
419
+
420
+ // Create engine config
421
+ const config: WorkflowEngineConfig = {
422
+ parallelLimit,
423
+ timeout: options?.timeout ?? definition.config?.timeout ?? null,
424
+ debug: options?.debug ?? definition.config?.debug ?? false,
425
+ };
426
+
427
+ // Create abort controller
428
+ const abortController = new AbortController();
429
+
430
+ // Create completion promise
431
+ let resolveCompleted!: (result: RunResult) => void;
432
+ let rejectCompleted!: (error: Error) => void;
433
+ const completedPromise = new Promise<RunResult>((resolve, reject) => {
434
+ resolveCompleted = resolve;
435
+ rejectCompleted = reject;
436
+ });
437
+
438
+ // Create session state
439
+ const sessionState: ActiveSessionState = {
440
+ sessionId,
441
+ workflowId,
442
+ workflowName,
443
+ status: 'running',
444
+ startedAt: new Date(),
445
+ eventBus,
446
+ dagManager,
447
+ scheduler,
448
+ executor,
449
+ nodeRegistry: this.nodeRegistry,
450
+ nodeOutputs: new Map(),
451
+ config,
452
+ abortController,
453
+ completedPromise,
454
+ resolveCompleted,
455
+ rejectCompleted,
456
+ workflowHistory: [],
457
+ agentSessions: new Map(),
458
+ };
459
+
460
+ // Setup event handlers
461
+ this.setupEventHandlers(sessionState);
462
+
463
+ return sessionState;
464
+ }
465
+
466
+ /**
467
+ * Run with resume point
468
+ */
469
+ private async runWithResume(
470
+ sessionId: string,
471
+ resumePoint: { type: string; nodeId?: string; nodeIds?: string[]; agentSessionId?: string },
472
+ options?: RunOptions & { input?: unknown }
473
+ ): Promise<RunResult> {
474
+ const sessionState = this.activeSessions.get(sessionId);
475
+ if (!sessionState) {
476
+ throw new Error(`Session not found: ${sessionId}`);
477
+ }
478
+
479
+ const runId = this.getRunIdFromSessionId(sessionId);
480
+
481
+ // Publish workflow started event
482
+ await sessionState.eventBus.publish(
483
+ createWorkflowEvent('workflow.started', runId, {
484
+ workflow_name: sessionState.workflowName,
485
+ input: options?.input,
486
+ })
487
+ );
488
+
489
+ // Determine what to execute based on resume point
490
+ let pendingNodeId: string | undefined;
491
+ let agentSessionId: string | undefined;
492
+
493
+ switch (resumePoint.type) {
494
+ case 'entry_node':
495
+ case 'next_nodes':
496
+ // Normal execution - scheduler will handle it
497
+ pendingNodeId = undefined;
498
+ break;
499
+ case 'resume_node':
500
+ case 'ask_user':
501
+ // Resume specific node
502
+ pendingNodeId = resumePoint.nodeId;
503
+ agentSessionId = resumePoint.agentSessionId;
504
+ break;
505
+ }
506
+
507
+ // Start scheduling
508
+ // Note: userResponse is extracted from input if it's a string (for resume)
509
+ const userResponse = typeof options?.input === 'string' ? options.input as string : undefined;
510
+ this.scheduleAndExecute(sessionState, {
511
+ input: options?.input as Record<string, any>,
512
+ pendingNodeId,
513
+ agentSessionId,
514
+ userResponse,
515
+ }).catch((error) => {
516
+ logger.error(`Workflow ${sessionId} scheduling error:`, error);
517
+ this.failWorkflow(sessionState, error);
518
+ });
519
+
520
+ // If sync mode, wait for completion
521
+ if (options?.sync !== false) {
522
+ return this.waitForCompletion(sessionId, options?.timeout);
523
+ }
524
+
525
+ return { runId, status: 'running' };
526
+ }
527
+
528
+ /**
529
+ * Convenience method to run a workflow directly
530
+ */
531
+ @TracedAs("workflow.engine.runWorkflow", { recordParams: true, recordResult: true, log: true })
532
+ async runWorkflow(
533
+ workflow: Workflow | WorkflowDefinition,
534
+ options?: RunOptions
535
+ ): Promise<RunResult> {
536
+ const definition = 'definition' in workflow
537
+ ? workflow.definition
538
+ : workflow;
539
+
540
+ const workflowId = 'id' in workflow
541
+ ? workflow.id
542
+ : `inline_${definition.name}`;
543
+
544
+ const workflowName = definition.name;
545
+
546
+ // Create session
547
+ const sessionId = await this.createSession(workflow, options);
548
+
549
+ // Initialize session state
550
+ const sessionState = await this.initializeSessionState(
551
+ sessionId,
552
+ definition,
553
+ workflowId,
554
+ workflowName,
555
+ options
556
+ );
557
+
558
+ this.activeSessions.set(sessionId, sessionState);
559
+
560
+ // Run with default entry node
561
+ return this.runWithResume(sessionId, { type: 'entry_node' }, options);
562
+ }
563
+
564
+ /**
565
+ * Pause a running workflow
566
+ */
567
+ @TracedAs("workflow.engine.pause", { recordParams: true, recordResult: true, log: true })
568
+ async pause(sessionId: string): Promise<void> {
569
+ const sessionState = this.activeSessions.get(sessionId);
570
+
571
+ if (!sessionState) {
572
+ throw new Error(`Session not found: ${sessionId}`);
573
+ }
574
+
575
+ if (sessionState.status !== 'running') {
576
+ throw new Error(`Session ${sessionId} is not running (current: ${sessionState.status})`);
577
+ }
578
+
579
+ sessionState.status = 'paused';
580
+ sessionState.abortController.abort();
581
+ sessionState.executor.cancelAll();
582
+
583
+ // Update session metadata
584
+ if (this.sessionComponent) {
585
+ const metadata = await this.getSessionMetadata(sessionId);
586
+ await this.sessionComponent.update(sessionId, {
587
+ metadata: { ...metadata, status: 'paused' },
588
+ });
589
+ }
590
+
591
+ // Publish pause event
592
+ await sessionState.eventBus.publish(
593
+ createWorkflowEvent('workflow.paused', this.getRunIdFromSessionId(sessionId), {})
594
+ );
595
+
596
+ logger.info(`[WorkflowEngine] Workflow paused: ${sessionId}`);
597
+ }
598
+
599
+ /**
600
+ * Resume a paused workflow
601
+ */
602
+ @TracedAs("workflow.engine.resume", { recordParams: true, recordResult: true, log: true })
603
+ async resume(
604
+ sessionId: string,
605
+ options?: { response?: string }
606
+ ): Promise<RunResult> {
607
+ const sessionState = this.activeSessions.get(sessionId);
608
+
609
+ if (!sessionState) {
610
+ // Session not in memory, try to restore from database
611
+ logger.info(`[WorkflowEngine] Session not in memory, attempting to restore from DB: ${sessionId}`);
612
+ return this.resumeFromDatabase(sessionId, options);
613
+ }
614
+
615
+ // Write resume message
616
+ if (options?.response) {
617
+ await this.writeNodeResume(sessionId, options.response);
618
+ }
619
+
620
+ if (sessionState.status === 'running') {
621
+ // Already running
622
+ return { runId: this.getRunIdFromSessionId(sessionId), status: 'running' };
623
+ }
624
+
625
+ if (sessionState.status !== 'paused') {
626
+ throw new Error(`Session ${sessionId} is not paused (current: ${sessionState.status})`);
627
+ }
628
+
629
+ // Resume state
630
+ sessionState.status = 'running';
631
+ sessionState.abortController = new AbortController();
632
+
633
+ // Update session metadata
634
+ if (this.sessionComponent) {
635
+ const metadata = await this.getSessionMetadata(sessionId);
636
+ await this.sessionComponent.update(sessionId, {
637
+ metadata: { ...metadata, status: 'running' },
638
+ });
639
+ }
640
+
641
+ // Publish resume event
642
+ await sessionState.eventBus.publish(
643
+ createWorkflowEvent('workflow.resumed', this.getRunIdFromSessionId(sessionId), {
644
+ userResponse: options?.response,
645
+ })
646
+ );
647
+
648
+ // Restore runtime state from messages and continue execution
649
+ const runtimeState = await this.restoreRuntimeState(sessionId);
650
+
651
+ this.scheduleAndExecute(sessionState, {
652
+ pendingNodeId: runtimeState.pendingNodeId || undefined,
653
+ agentSessionId: runtimeState.agentSessionId,
654
+ userResponse: options?.response,
655
+ restoredOutputs: runtimeState.nodeOutputs,
656
+ });
657
+
658
+ return {
659
+ runId: this.getRunIdFromSessionId(sessionId),
660
+ status: 'running',
661
+ };
662
+ }
663
+
664
+ /**
665
+ * Resume from database when CLI restarts
666
+ */
667
+ @TracedAs("workflow.engine.resumeFromDatabase", { recordParams: true, recordResult: true, log: true })
668
+ private async resumeFromDatabase(
669
+ sessionId: string,
670
+ options?: { response?: string }
671
+ ): Promise<RunResult> {
672
+ // Get session
673
+ const session = await this.sessionComponent?.get(sessionId);
674
+ if (!session) {
675
+ throw new Error(`Session not found: ${sessionId}`);
676
+ }
677
+
678
+ const metadata = session.metadata as WorkflowSessionMetadata;
679
+ if (metadata.type !== 'workflow') {
680
+ throw new Error(`Session is not a workflow session: ${sessionId}`);
681
+ }
682
+
683
+ if (metadata.status !== 'paused') {
684
+ throw new Error(`Workflow is not paused: ${sessionId}, status: ${metadata.status}`);
685
+ }
686
+
687
+ // Restore runtime state from messages
688
+ const runtimeState = await this.restoreRuntimeState(sessionId);
689
+
690
+ if (!runtimeState.pendingNodeId) {
691
+ throw new Error(`No pending node found in session: ${sessionId}`);
692
+ }
693
+
694
+ // Write resume message
695
+ if (options?.response) {
696
+ await this.writeNodeResume(sessionId, options.response);
697
+ }
698
+
699
+ // Update session metadata to running
700
+ if (this.sessionComponent) {
701
+ await this.sessionComponent.update(sessionId, {
702
+ metadata: { ...metadata, status: 'running' },
703
+ });
704
+ }
705
+
706
+ logger.info(`[WorkflowEngine] Resumed from database: ${sessionId}, pendingNode: ${runtimeState.pendingNodeId}`);
707
+
708
+ // Note: Cannot fully restore without workflow definition
709
+ // This is a current design limitation
710
+ throw new Error(
711
+ `Cannot resume from database without workflow definition. ` +
712
+ `Session ${sessionId} is paused at node "${runtimeState.pendingNodeId}". ` +
713
+ `Please ensure the workflow is registered before resuming.`
714
+ );
715
+ }
716
+
717
+ /**
718
+ * Restore runtime state from Session messages
719
+ */
720
+ @TracedAs("workflow.engine.restoreRuntimeState", { recordParams: true, recordResult: true, log: true })
721
+ private async restoreRuntimeState(sessionId: string): Promise<RuntimeState> {
722
+ const messages = this.sessionComponent
723
+ ? await this.sessionComponent.getMessages(sessionId)
724
+ : [];
725
+
726
+ const nodeOutputs = new Map<string, unknown>();
727
+ let lastCallNodeId: string | null = null;
728
+ let lastInterruptMessage: any = null;
729
+ let agentSessionId: string | undefined;
730
+
731
+ for (const msg of messages) {
732
+ const msgMetadata = msg.metadata as any;
733
+
734
+ if (msgMetadata?.type === 'workflow.node.call') {
735
+ lastCallNodeId = msgMetadata.workflowNodeId;
736
+ } else if (msgMetadata?.type === 'workflow.node.result') {
737
+ if (lastCallNodeId) {
738
+ try {
739
+ nodeOutputs.set(lastCallNodeId, JSON.parse(msg.content));
740
+ } catch {
741
+ nodeOutputs.set(lastCallNodeId, msg.content);
742
+ }
743
+ }
744
+ lastCallNodeId = null;
745
+ } else if (msgMetadata?.type === 'workflow.node.interrupt') {
746
+ lastInterruptMessage = msg;
747
+ lastCallNodeId = msgMetadata.workflowNodeId;
748
+ agentSessionId = msgMetadata.agentSessionId;
749
+ }
750
+ }
751
+
752
+ return {
753
+ nodeOutputs,
754
+ pendingNodeId: lastCallNodeId,
755
+ waitingForUser: !!lastInterruptMessage,
756
+ agentSessionId,
757
+ lastInterruptMessage,
758
+ };
759
+ }
760
+
761
+ /**
762
+ * Write node resume message
763
+ */
764
+ @TracedAs("workflow.engine.writeNodeResume", { recordParams: true, recordResult: true, log: true })
765
+ private async writeNodeResume(sessionId: string, response: string): Promise<void> {
766
+ if (this.sessionComponent) {
767
+ await this.sessionComponent.addMessage(sessionId, {
768
+ role: 'workflow.node.resume',
769
+ content: response,
770
+ metadata: {
771
+ type: 'workflow.node.resume',
772
+ response,
773
+ timestamp: Date.now(),
774
+ },
775
+ });
776
+ }
777
+ }
778
+
779
+ /**
780
+ * Stop a running or paused workflow
781
+ */
782
+ @TracedAs("workflow.engine.stop", { recordParams: true, recordResult: true, log: true })
783
+ async stop(sessionId: string, reason?: string): Promise<void> {
784
+ const sessionState = this.activeSessions.get(sessionId);
785
+
786
+ if (!sessionState) {
787
+ // Not in memory, try to update database
788
+ if (this.sessionComponent) {
789
+ await this.sessionComponent.update(sessionId, {
790
+ metadata: { status: 'stopped' } as any,
791
+ });
792
+ }
793
+ return;
794
+ }
795
+
796
+ sessionState.status = 'stopped';
797
+ sessionState.abortController.abort();
798
+ sessionState.executor.cancelAll();
799
+
800
+ // Update session metadata
801
+ if (this.sessionComponent) {
802
+ const metadata = await this.getSessionMetadata(sessionId);
803
+ await this.sessionComponent.update(sessionId, {
804
+ metadata: { ...metadata, status: 'stopped' },
805
+ });
806
+ }
807
+
808
+ // Publish stop event
809
+ await sessionState.eventBus.publish(
810
+ createWorkflowEvent('workflow.stopped', this.getRunIdFromSessionId(sessionId), {
811
+ reason: reason ?? 'User requested stop',
812
+ })
813
+ );
814
+
815
+ // Cleanup
816
+ this.cleanupSession(sessionState);
817
+ }
818
+
819
+ /**
820
+ * Get session status
821
+ */
822
+ getSessionStatus(sessionId: string): RunStatus | null {
823
+ const sessionState = this.activeSessions.get(sessionId);
824
+ return sessionState?.status ?? null;
825
+ }
826
+
827
+ /**
828
+ * Check if session is active
829
+ */
830
+ isSessionActive(sessionId: string): boolean {
831
+ return this.activeSessions.has(sessionId);
832
+ }
833
+
834
+ // =========================================================================
835
+ // Private Methods
836
+ // =========================================================================
837
+
838
+ /**
839
+ * Setup event handlers for a run
840
+ */
841
+ private setupEventHandlers(sessionState: ActiveSessionState): void {
842
+ const { sessionId, eventBus, scheduler } = sessionState;
843
+ const runId = this.getRunIdFromSessionId(sessionId);
844
+
845
+ // Handle node completed
846
+ eventBus.on('node.completed', async (event: WorkflowEvent) => {
847
+ if (event.type !== 'node.completed') return;
848
+
849
+ sessionState.nodeOutputs.set(event.node_id, event.output);
850
+ scheduler.markCompleted(event.node_id);
851
+
852
+ // Accumulate workflow history
853
+ if (event.output && typeof event.output === 'object' && 'workflowHistory' in event.output) {
854
+ const messages = (event.output as any).workflowHistory;
855
+ if (Array.isArray(messages)) {
856
+ sessionState.workflowHistory.push(...messages);
857
+ }
858
+ }
859
+
860
+ // Check if workflow is complete
861
+ this.checkAndFinalize(sessionState);
862
+ });
863
+
864
+ // Handle node failed
865
+ eventBus.on('node.failed', async (event: WorkflowEvent) => {
866
+ if (event.type !== 'node.failed') return;
867
+
868
+ scheduler.markFailed(event.node_id);
869
+ await this.failWorkflow(sessionState, new Error(event.error.message));
870
+ });
871
+
872
+ // Handle node interrupt (ask_user pause)
873
+ eventBus.on('node.interrupt', async (event: WorkflowEvent) => {
874
+ if (event.type !== 'node.interrupt') return;
875
+
876
+ logger.info(`Workflow paused at node "${event.node_id}" - ask_user pending`);
877
+
878
+ sessionState.status = 'paused';
879
+ sessionState.abortController.abort();
880
+
881
+ // Update session metadata
882
+ if (this.sessionComponent) {
883
+ const metadata = await this.getSessionMetadata(sessionId);
884
+ await this.sessionComponent.update(sessionId, {
885
+ metadata: { ...metadata, status: 'paused' },
886
+ });
887
+ }
888
+
889
+ // Publish workflow.paused event
890
+ await eventBus.publish(
891
+ createWorkflowEvent('workflow.paused', runId, {
892
+ pendingNodeId: event.node_id,
893
+ query: (event as any).query,
894
+ })
895
+ );
896
+
897
+ // Write interrupt message to session
898
+ await this.writeNodeInterrupt(sessionId, event.node_id, event.node_type, (event as any).query, (event as any).agent_session_id);
899
+
900
+ // Resolve with paused status
901
+ sessionState.resolveCompleted({
902
+ runId,
903
+ status: 'paused',
904
+ pendingNodeId: event.node_id,
905
+ query: (event as any).query,
906
+ agentSessionId: (event as any).agent_session_id,
907
+ });
908
+ });
909
+ }
910
+
911
+ /**
912
+ * Write node interrupt message
913
+ */
914
+ @TracedAs("workflow.engine.writeNodeInterrupt", { recordParams: true, recordResult: true, log: true })
915
+ @TracedAs("workflow.engine.writeNodeInterrupt", { recordParams: true, recordResult: true, log: true })
916
+ private async writeNodeInterrupt(
917
+ sessionId: string,
918
+ nodeId: string,
919
+ nodeType: string,
920
+ query: string,
921
+ agentSessionId?: string
922
+ ): Promise<void> {
923
+ if (this.sessionComponent) {
924
+ await this.sessionComponent.addMessage(sessionId, {
925
+ role: 'workflow.node.interrupt',
926
+ content: query,
927
+ metadata: {
928
+ type: 'workflow.node.interrupt',
929
+ workflowNodeId: nodeId,
930
+ workflowNodeType: nodeType,
931
+ query,
932
+ agentSessionId,
933
+ timestamp: Date.now(),
934
+ },
935
+ });
936
+ }
937
+ }
938
+
939
+ /**
940
+ * Main scheduling loop
941
+ */
942
+ @TracedAs("workflow.engine.scheduleAndExecute", { recordParams: true, recordResult: true, log: true })
943
+ @TracedAs("workflow.engine.scheduleAndExecute", { recordParams: true, recordResult: true, log: true })
944
+ private async scheduleAndExecute(
945
+ sessionState: ActiveSessionState,
946
+ options?: {
947
+ input?: Record<string, any>;
948
+ pendingNodeId?: string;
949
+ agentSessionId?: string;
950
+ userResponse?: string;
951
+ restoredOutputs?: Map<string, unknown>;
952
+ }
953
+ ): Promise<void> {
954
+ const { scheduler, executor, eventBus, abortController, nodeOutputs } = sessionState;
955
+ const { input: globalInput, pendingNodeId, agentSessionId, userResponse, restoredOutputs } = options || {};
956
+
957
+ // Merge restored outputs if any
958
+ if (restoredOutputs) {
959
+ for (const [nodeId, output] of restoredOutputs) {
960
+ nodeOutputs.set(nodeId, output);
961
+ }
962
+ }
963
+
964
+ // Helper to schedule available nodes
965
+ const scheduleAvailableNodes = async (): Promise<boolean> => {
966
+ const state = scheduler.getState();
967
+ const completedNodes = new Set(state.completed);
968
+
969
+ const readyNodes = scheduler.getReadyNodes(completedNodes);
970
+ const canStart = scheduler.canStartMore();
971
+
972
+ if (readyNodes.length === 0 || !canStart) {
973
+ return false;
974
+ }
975
+
976
+ let scheduled = 0;
977
+ for (const nodeId of readyNodes) {
978
+ if (!scheduler.canStartMore()) break;
979
+ if (scheduled >= (sessionState.config.parallelLimit ?? Infinity)) break;
980
+
981
+ await this.startNode(sessionState, nodeId, globalInput);
982
+ scheduled++;
983
+ }
984
+
985
+ return true;
986
+ };
987
+
988
+ // Initial scheduling
989
+ if (pendingNodeId) {
990
+ // Resume mode: only schedule the pending node
991
+ await this.resumeNode(sessionState, pendingNodeId, {
992
+ agentSessionId,
993
+ userResponse,
994
+ });
995
+ } else {
996
+ await scheduleAvailableNodes();
997
+ }
998
+
999
+ // Event-driven loop
1000
+ while (sessionState.status === 'running' && !abortController.signal.aborted) {
1001
+ const state = scheduler.getState();
1002
+ if (state.pending.length === 0 && state.running.length === 0) {
1003
+ break;
1004
+ }
1005
+
1006
+ try {
1007
+ await this.waitForNextNodeEvent(sessionState, abortController.signal);
1008
+ } catch {
1009
+ break;
1010
+ }
1011
+
1012
+ await scheduleAvailableNodes();
1013
+ }
1014
+ }
1015
+
1016
+ /**
1017
+ * Wait for next node event
1018
+ */
1019
+ @TracedAs("workflow.engine.waitForNextNodeEvent", { recordParams: true, recordResult: true, log: true })
1020
+ private waitForNextNodeEvent(
1021
+ sessionState: ActiveSessionState,
1022
+ signal: AbortSignal
1023
+ ): Promise<void> {
1024
+ return new Promise((resolve) => {
1025
+ const timeoutId = setTimeout(() => {
1026
+ cleanup();
1027
+ resolve();
1028
+ }, 100);
1029
+
1030
+ const cleanup = () => {
1031
+ clearTimeout(timeoutId);
1032
+ sessionState.eventBus.off('node.completed', onCompleted);
1033
+ sessionState.eventBus.off('node.failed', onFailed);
1034
+ };
1035
+
1036
+ const onCompleted = (event: WorkflowEvent) => {
1037
+ if (event.type === 'node.completed') {
1038
+ cleanup();
1039
+ resolve();
1040
+ }
1041
+ };
1042
+
1043
+ const onFailed = (event: WorkflowEvent) => {
1044
+ if (event.type === 'node.failed') {
1045
+ cleanup();
1046
+ resolve();
1047
+ }
1048
+ };
1049
+
1050
+ sessionState.eventBus.on('node.completed', onCompleted);
1051
+ sessionState.eventBus.on('node.failed', onFailed);
1052
+
1053
+ if (signal.aborted) {
1054
+ cleanup();
1055
+ resolve();
1056
+ }
1057
+ });
1058
+ }
1059
+
1060
+ /**
1061
+ * Start execution of a single node
1062
+ */
1063
+ @TracedAs("workflow.engine.startNode", { recordParams: true, recordResult: true, log: true })
1064
+ @TracedAs("workflow.engine.startNode", { recordParams: true, recordResult: true, log: true })
1065
+ private async startNode(
1066
+ sessionState: ActiveSessionState,
1067
+ nodeId: string,
1068
+ input?: Record<string, any>
1069
+ ): Promise<void> {
1070
+ const { scheduler, executor, eventBus, dagManager } = sessionState;
1071
+ const sessionId = sessionState.sessionId;
1072
+ const runId = this.getRunIdFromSessionId(sessionId);
1073
+
1074
+ scheduler.markStarted(nodeId);
1075
+
1076
+ // Publish scheduled event
1077
+ await eventBus.publish(
1078
+ createWorkflowEvent('node.scheduled', runId, {
1079
+ node_id: nodeId,
1080
+ })
1081
+ );
1082
+
1083
+ // Get node definition
1084
+ const nodeDef = dagManager.getNode(nodeId);
1085
+ if (!nodeDef) {
1086
+ scheduler.markFailed(nodeId);
1087
+ return;
1088
+ }
1089
+
1090
+ // Write workflow.node.call message
1091
+ await this.writeNodeCall(sessionId, nodeId, nodeDef.type, input);
1092
+
1093
+ // Create execution context
1094
+ const context = this.createExecutionContext(sessionState, nodeId, input);
1095
+
1096
+ // Execute node
1097
+ executor.executeNode(nodeDef, context).catch(async (error) => {
1098
+ if (error instanceof AskUserError) {
1099
+ await eventBus.publish(createNodeInterruptEvent(
1100
+ runId,
1101
+ nodeId,
1102
+ nodeDef.type,
1103
+ error.query,
1104
+ error.agentSessionId
1105
+ ));
1106
+ return;
1107
+ }
1108
+
1109
+ scheduler.markFailed(nodeId);
1110
+ });
1111
+ }
1112
+
1113
+ /**
1114
+ * Write node call message
1115
+ */
1116
+ @TracedAs("workflow.engine.writeNodeCall", { recordParams: true, recordResult: true, log: true })
1117
+ @TracedAs("workflow.engine.writeNodeCall", { recordParams: true, recordResult: true, log: true })
1118
+ private async writeNodeCall(
1119
+ sessionId: string,
1120
+ nodeId: string,
1121
+ nodeType: string,
1122
+ input?: any
1123
+ ): Promise<void> {
1124
+ if (this.sessionComponent) {
1125
+ await this.sessionComponent.addMessage(sessionId, {
1126
+ role: 'workflow.node.call',
1127
+ content: JSON.stringify({ input }),
1128
+ metadata: {
1129
+ type: 'workflow.node.call',
1130
+ workflowNodeId: nodeId,
1131
+ workflowNodeType: nodeType,
1132
+ timestamp: Date.now(),
1133
+ },
1134
+ });
1135
+ }
1136
+ }
1137
+
1138
+ /**
1139
+ * Resume execution of a paused node
1140
+ */
1141
+ @TracedAs("workflow.engine.resumeNode", { recordParams: true, recordResult: true, log: true })
1142
+ @TracedAs("workflow.engine.resumeNode", { recordParams: true, recordResult: true, log: true })
1143
+ private async resumeNode(
1144
+ sessionState: ActiveSessionState,
1145
+ nodeId: string,
1146
+ options?: {
1147
+ agentSessionId?: string;
1148
+ userResponse?: string;
1149
+ }
1150
+ ): Promise<void> {
1151
+ const { scheduler, executor, eventBus, dagManager } = sessionState;
1152
+ const sessionId = sessionState.sessionId;
1153
+ const runId = this.getRunIdFromSessionId(sessionId);
1154
+
1155
+ scheduler.markStarted(nodeId);
1156
+
1157
+ // Publish resume event
1158
+ await eventBus.publish(
1159
+ createWorkflowEvent('node.started', runId, {
1160
+ node_id: nodeId,
1161
+ agentSessionId: options?.agentSessionId,
1162
+ userResponse: options?.userResponse,
1163
+ })
1164
+ );
1165
+
1166
+ // Get node definition
1167
+ const nodeDef = dagManager.getNode(nodeId);
1168
+ if (!nodeDef) {
1169
+ scheduler.markFailed(nodeId);
1170
+ return;
1171
+ }
1172
+
1173
+ // Create execution context
1174
+ const context = this.createExecutionContext(sessionState, nodeId, undefined);
1175
+
1176
+ // Add resume options to context
1177
+ if (options?.agentSessionId || options?.userResponse) {
1178
+ (context as any).agentSessionId = options.agentSessionId;
1179
+ (context as any).userResponse = options.userResponse;
1180
+ }
1181
+
1182
+ // Execute node
1183
+ executor.executeNode(nodeDef, context).catch((error) => {
1184
+ if (error instanceof AskUserError) {
1185
+ const event = createNodeInterruptEvent(
1186
+ runId,
1187
+ nodeId,
1188
+ nodeDef.type,
1189
+ error.query,
1190
+ error.agentSessionId
1191
+ );
1192
+ eventBus.publish(event);
1193
+ return;
1194
+ }
1195
+ scheduler.markFailed(nodeId);
1196
+ });
1197
+ }
1198
+
1199
+ /**
1200
+ * Create execution context for a node
1201
+ */
1202
+ private createExecutionContext(
1203
+ sessionState: ActiveSessionState,
1204
+ nodeId: string,
1205
+ globalInput?: Record<string, any>
1206
+ ): NodeExecutionContext {
1207
+ const nodeDef = sessionState.dagManager.getNode(nodeId)!;
1208
+ const runId = this.getRunIdFromSessionId(sessionState.sessionId);
1209
+
1210
+ // Collect inputs from dependencies
1211
+ const deps = nodeDef.depends_on || [];
1212
+ const input: Record<string, any> = {};
1213
+
1214
+ for (const depId of deps) {
1215
+ const depOutput = sessionState.nodeOutputs.get(depId);
1216
+ if (depOutput !== undefined) {
1217
+ input[depId] = depOutput;
1218
+ }
1219
+ }
1220
+
1221
+ // Merge global input if this is an entry node
1222
+ const analysis = sessionState.dagManager.analyze();
1223
+ if (analysis.entryNodes.includes(nodeId) && globalInput) {
1224
+ Object.assign(input, globalInput);
1225
+ }
1226
+
1227
+ // Ask user function
1228
+ const askUser = (query: string): never => {
1229
+ throw new AskUserError(
1230
+ runId,
1231
+ sessionState.sessionId,
1232
+ nodeId,
1233
+ nodeDef.type,
1234
+ query
1235
+ );
1236
+ };
1237
+
1238
+ return {
1239
+ runId,
1240
+ sessionId: sessionState.sessionId,
1241
+ workflowName: sessionState.workflowName,
1242
+ nodeId,
1243
+ input,
1244
+ previousOutputs: sessionState.nodeOutputs,
1245
+ config: nodeDef.config ?? {},
1246
+ debug: sessionState.config.debug,
1247
+ eventBus: sessionState.eventBus,
1248
+ nodeOutputs: sessionState.nodeOutputs,
1249
+ workflowHistory: sessionState.workflowHistory,
1250
+ sessionComponent: this.sessionComponent,
1251
+ askUser,
1252
+ };
1253
+ }
1254
+
1255
+ /**
1256
+ * Check if workflow is complete and finalize
1257
+ */
1258
+ @TracedAs("workflow.engine.checkAndFinalize", { recordParams: true, recordResult: true, log: true })
1259
+ private checkAndFinalize(sessionState: ActiveSessionState): void {
1260
+ const state = sessionState.scheduler.getState();
1261
+
1262
+ if (state.pending.length === 0 &&
1263
+ state.running.length === 0 &&
1264
+ state.ready.length === 0) {
1265
+
1266
+ if (state.failed.length > 0) {
1267
+ this.failWorkflow(sessionState, new Error(`Nodes failed: ${state.failed.join(', ')}`));
1268
+ } else {
1269
+ this.completeWorkflow(sessionState);
1270
+ }
1271
+ }
1272
+ }
1273
+
1274
+ /**
1275
+ * Complete a workflow successfully
1276
+ */
1277
+ @TracedAs("workflow.engine.completeWorkflow", { recordParams: true, recordResult: true, log: true })
1278
+ private async completeWorkflow(sessionState: ActiveSessionState): Promise<void> {
1279
+ const durationMs = Date.now() - sessionState.startedAt.getTime();
1280
+ const sessionId = sessionState.sessionId;
1281
+ const runId = this.getRunIdFromSessionId(sessionId);
1282
+
1283
+ // Collect output
1284
+ const output: Record<string, any> = {};
1285
+ for (const [nodeId, nodeOutput] of sessionState.nodeOutputs) {
1286
+ output[nodeId] = nodeOutput;
1287
+ }
1288
+
1289
+ // Update session metadata
1290
+ if (this.sessionComponent) {
1291
+ const metadata = await this.getSessionMetadata(sessionId);
1292
+ await this.sessionComponent.update(sessionId, {
1293
+ metadata: { ...metadata, status: 'completed' },
1294
+ });
1295
+ }
1296
+
1297
+ // Publish completed event
1298
+ await sessionState.eventBus.publish(
1299
+ createWorkflowEvent('workflow.completed', runId, {
1300
+ result: output,
1301
+ duration_ms: durationMs,
1302
+ }),
1303
+ true
1304
+ );
1305
+
1306
+ // Resolve completion
1307
+ sessionState.resolveCompleted({
1308
+ runId,
1309
+ status: 'completed',
1310
+ output,
1311
+ durationMs,
1312
+ });
1313
+
1314
+ // Cleanup
1315
+ this.cleanupSession(sessionState);
1316
+ }
1317
+
1318
+ /**
1319
+ * Fail a workflow
1320
+ */
1321
+ @TracedAs("workflow.engine.failWorkflow", { recordParams: true, recordResult: true, log: true })
1322
+ private async failWorkflow(sessionState: ActiveSessionState, error: Error): Promise<void> {
1323
+ const durationMs = Date.now() - sessionState.startedAt.getTime();
1324
+ const sessionId = sessionState.sessionId;
1325
+ const runId = this.getRunIdFromSessionId(sessionId);
1326
+
1327
+ sessionState.executor.cancelAll();
1328
+
1329
+ // Update session metadata
1330
+ if (this.sessionComponent) {
1331
+ const metadata = await this.getSessionMetadata(sessionId);
1332
+ await this.sessionComponent.update(sessionId, {
1333
+ metadata: { ...metadata, status: 'failed' },
1334
+ });
1335
+ }
1336
+
1337
+ // Publish failed event
1338
+ await sessionState.eventBus.publish(
1339
+ createWorkflowEvent('workflow.failed', runId, {
1340
+ error: { message: error.message, stack: error.stack },
1341
+ failed_at: new Date().toISOString(),
1342
+ }),
1343
+ true
1344
+ );
1345
+
1346
+ // Resolve with error
1347
+ sessionState.resolveCompleted({
1348
+ runId,
1349
+ status: 'failed',
1350
+ error: error.message,
1351
+ durationMs,
1352
+ });
1353
+
1354
+ // Cleanup
1355
+ this.cleanupSession(sessionState);
1356
+ }
1357
+
1358
+ /**
1359
+ * Cleanup session resources
1360
+ */
1361
+ private cleanupSession(sessionState: ActiveSessionState): void {
1362
+ sessionState.eventBus.clear();
1363
+ this.activeSessions.delete(sessionState.sessionId);
1364
+ }
1365
+
1366
+ /**
1367
+ * Wait for a run to complete
1368
+ */
1369
+ private async waitForCompletion(
1370
+ sessionId: string,
1371
+ timeout?: number | null
1372
+ ): Promise<RunResult> {
1373
+ const sessionState = this.activeSessions.get(sessionId);
1374
+
1375
+ if (!sessionState) {
1376
+ throw new Error(`Session not found: ${sessionId}`);
1377
+ }
1378
+
1379
+ const timeoutMs = timeout ?? 300000;
1380
+
1381
+ const timeoutPromise = new Promise<RunResult>((_, reject) => {
1382
+ setTimeout(() => {
1383
+ reject(new Error(`Workflow execution timeout: ${timeoutMs}ms`));
1384
+ }, timeoutMs);
1385
+ });
1386
+
1387
+ return Promise.race([
1388
+ sessionState.completedPromise,
1389
+ timeoutPromise,
1390
+ ]);
1391
+ }
1392
+
1393
+ /**
1394
+ * Get session metadata
1395
+ */
1396
+ private async getSessionMetadata(sessionId: string): Promise<WorkflowSessionMetadata> {
1397
+ if (!this.sessionComponent) {
1398
+ return { type: 'workflow', workflowId: '', workflowName: '', status: 'running' };
1399
+ }
1400
+ const session = await this.sessionComponent.get(sessionId);
1401
+ return (session?.metadata as WorkflowSessionMetadata) || { type: 'workflow', workflowId: '', workflowName: '', status: 'running' };
1402
+ }
1403
+
1404
+ /**
1405
+ * Find workflow definition from repository
1406
+ */
1407
+ private findDefinitionForWorkflow(workflowIdOrName: string): WorkflowDefinition | null {
1408
+ if (!this.workflowRepository) {
1409
+ return null;
1410
+ }
1411
+
1412
+ // Try by ID first
1413
+ let workflow = this.workflowRepository.getById(workflowIdOrName);
1414
+ if (workflow) return workflow.definition;
1415
+
1416
+ // Try by name
1417
+ workflow = this.workflowRepository.getByName(workflowIdOrName);
1418
+ if (workflow) return workflow.definition;
1419
+
1420
+ return null;
1421
+ }
1422
+ }