@agent-spaces/server 0.3.65 → 0.4.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 (278) hide show
  1. package/dist/adapters/agent-runtime.js +8 -0
  2. package/dist/adapters/codex-function-tool-bridge.js +152 -0
  3. package/dist/adapters/codex-runtime.js +23 -4
  4. package/dist/adapters/git.js +25 -3
  5. package/dist/adapters/hermes-runtime.js +813 -0
  6. package/dist/adapters/langchain-runtime.js +384 -21
  7. package/dist/adapters/oh-my-pi-runtime.js +858 -0
  8. package/dist/adapters/open-agent-sdk-runtime.js +202 -5
  9. package/dist/adapters/open-agent-sdk-runtime.test.js +35 -0
  10. package/dist/agents/agent-message-parts.js +52 -2
  11. package/dist/agents/issue-task-controller.js +71 -2
  12. package/dist/agents/pull-request-agent.js +115 -0
  13. package/dist/agents/scheduler-agent.js +1 -1
  14. package/dist/agents/title-generator-agent.js +120 -0
  15. package/dist/app.js +61 -4
  16. package/dist/middleware/auth.js +1 -1
  17. package/dist/package.json +2 -1
  18. package/dist/routes/agent-sse.js +70 -6
  19. package/dist/routes/channel.js +23 -2
  20. package/dist/routes/chat-run.js +191 -0
  21. package/dist/routes/chat.js +142 -0
  22. package/dist/routes/command.js +16 -0
  23. package/dist/routes/data.js +189 -0
  24. package/dist/routes/git.js +10 -1
  25. package/dist/routes/import.js +199 -0
  26. package/dist/routes/issue.js +17 -5
  27. package/dist/routes/notification.js +10 -0
  28. package/dist/routes/plugin.js +69 -0
  29. package/dist/routes/version.js +95 -0
  30. package/dist/routes/workflow-hook.js +71 -0
  31. package/dist/routes/workflow.js +282 -7
  32. package/dist/routes/workspace.js +13 -4
  33. package/dist/routes/worktree.js +93 -0
  34. package/dist/services/agent.js +123 -36
  35. package/dist/services/ai-text.js +185 -0
  36. package/dist/services/builtin-tools/index.js +1 -0
  37. package/dist/services/builtin-tools/workflow-editor-tools.js +509 -0
  38. package/dist/services/builtin-tools/workflow-exec-tools.js +320 -0
  39. package/dist/services/chat.js +134 -0
  40. package/dist/services/command-process-manager.js +16 -0
  41. package/dist/services/execution-manager.js +1346 -0
  42. package/dist/services/generated-title.js +59 -0
  43. package/dist/services/gitignore.js +22 -18
  44. package/dist/services/interaction-manager.js +114 -0
  45. package/dist/services/issue-retry.js +25 -0
  46. package/dist/services/notification-center.js +9 -0
  47. package/dist/services/output-style.js +8 -1
  48. package/dist/services/plugin.js +257 -0
  49. package/dist/services/prompt-template.js +8 -1
  50. package/dist/services/pty.js +20 -0
  51. package/dist/services/search.js +16 -6
  52. package/dist/services/skill.js +2 -0
  53. package/dist/services/version.js +69 -0
  54. package/dist/services/workflow-command-runner.js +24 -0
  55. package/dist/services/workflow-trigger-service.js +137 -0
  56. package/dist/services/workflow.js +219 -39
  57. package/dist/services/workspace.js +23 -1
  58. package/dist/services/worktree.js +207 -0
  59. package/dist/storage/agent-store.js +8 -0
  60. package/dist/storage/chat-store.js +151 -0
  61. package/dist/storage/database-store.js +6 -0
  62. package/dist/storage/json-store.js +2 -1
  63. package/dist/storage/kanban-store.js +6 -0
  64. package/dist/storage/workflow-store.js +386 -22
  65. package/dist/storage/worktree-store.js +33 -0
  66. package/dist/web/404.html +1 -1
  67. package/dist/web/__next.__PAGE__.txt +4 -4
  68. package/dist/web/__next._full.txt +21 -21
  69. package/dist/web/__next._head.txt +4 -4
  70. package/dist/web/__next._index.txt +13 -13
  71. package/dist/web/__next._tree.txt +2 -2
  72. package/dist/web/_next/static/chunks/{08x85faoz58i7.js → 0-92u-tiuy40a.js} +4 -4
  73. package/dist/web/_next/static/chunks/{0db14450oqzhg.js → 0-yizd8osmas2.js} +1 -1
  74. package/dist/web/_next/static/chunks/0.ln~3mnv8.69.js +1 -0
  75. package/dist/web/_next/static/chunks/{11cchuwoupmbf.js → 01y3e9pemva7b.js} +10 -10
  76. package/dist/web/_next/static/chunks/02l~tmzrgny.~.js +1 -0
  77. package/dist/web/_next/static/chunks/03f1bwuynjdmk.js +1 -0
  78. package/dist/web/_next/static/chunks/04cb4kc9r2pd1.js +1 -0
  79. package/dist/web/_next/static/chunks/04de6ha5_e3pp.js +1 -0
  80. package/dist/web/_next/static/chunks/09he0p79ov1rn.js +1 -0
  81. package/dist/web/_next/static/chunks/09vpocs4zuc8h.js +1 -0
  82. package/dist/web/_next/static/chunks/0_o2exgr0e_nh.js +1 -0
  83. package/dist/web/_next/static/chunks/{0xbaakj0dfc0..js → 0_y9nskc-xt2k.js} +1 -1
  84. package/dist/web/_next/static/chunks/0clnj-1fi6u8j.js +3 -0
  85. package/dist/web/_next/static/chunks/0gv40by0zzxs_.js +1 -0
  86. package/dist/web/_next/static/chunks/0iusr8y88.kjg.js +1 -0
  87. package/dist/web/_next/static/chunks/{0-a.-hl3wv99k.js → 0j-8418fxvkd1.js} +1 -1
  88. package/dist/web/_next/static/chunks/0j.wxxyoclma5.js +1 -0
  89. package/dist/web/_next/static/chunks/{0.6b64sy0g5.3.js → 0jq.16sozqik..js} +1 -1
  90. package/dist/web/_next/static/chunks/0jw8xdk_fpnp0.css +1 -0
  91. package/dist/web/_next/static/chunks/0l1lkd-wcotma.js +1 -0
  92. package/dist/web/_next/static/chunks/{0u6h-hqc_i52e.js → 0ncxf_zr4n839.js} +1 -1
  93. package/dist/web/_next/static/chunks/0ndo7um~v7vco.js +1 -0
  94. package/dist/web/_next/static/chunks/0ngk.~kan8wq_.js +1 -0
  95. package/dist/web/_next/static/chunks/0ppr271hz0xoq.js +1 -0
  96. package/dist/web/_next/static/chunks/0rt9bofnn6qvn.js +1 -0
  97. package/dist/web/_next/static/chunks/0t64.9jd00hio.js +3 -0
  98. package/dist/web/_next/static/chunks/0taui9--5n6-5.js +1 -0
  99. package/dist/web/_next/static/chunks/0ty39169lh~z3.js +1 -0
  100. package/dist/web/_next/static/chunks/0v8nof~waedlu.js +1 -0
  101. package/dist/web/_next/static/chunks/{0-pi.67w.ymsr.js → 0vcfoagnpf.aw.js} +1 -1
  102. package/dist/web/_next/static/chunks/{0vis~lx0nxgn-.js → 0wwwq.gyyz0vg.js} +3 -3
  103. package/dist/web/_next/static/chunks/0x5lak8yz7pm7.js +1 -0
  104. package/dist/web/_next/static/chunks/0xon_.4-o0_zq.js +7 -0
  105. package/dist/web/_next/static/chunks/0zjnrbt7inime.js +1 -0
  106. package/dist/web/_next/static/chunks/0zwebpitvpuv1.js +1 -0
  107. package/dist/web/_next/static/chunks/0~02o4g-o6n~m.js +1 -0
  108. package/dist/web/_next/static/chunks/{0w7.tb29958zj.js → 0~fjezp.k4qk3.js} +1 -1
  109. package/dist/web/_next/static/chunks/0~sh93re1-_rp.js +1 -0
  110. package/dist/web/_next/static/chunks/10_niiq0k6o-p.js +5 -0
  111. package/dist/web/_next/static/chunks/12tjr0_2ccrtq.js +2 -0
  112. package/dist/web/_next/static/chunks/12x5.go_x9mfk.js +1 -0
  113. package/dist/web/_next/static/chunks/{0l2w0qz1ea7ql.js → 12~5dk9i9d16t.js} +2 -2
  114. package/dist/web/_next/static/chunks/13iye4rv5p31b.js +1 -0
  115. package/dist/web/_next/static/chunks/{01~quun6sefgl.js → 13~y-b.b9rrw4.js} +1 -1
  116. package/dist/web/_next/static/chunks/151wzhb5q0lo5.js +1 -0
  117. package/dist/web/_not-found/__next._full.txt +19 -19
  118. package/dist/web/_not-found/__next._head.txt +4 -4
  119. package/dist/web/_not-found/__next._index.txt +13 -13
  120. package/dist/web/_not-found/__next._not-found.__PAGE__.txt +2 -2
  121. package/dist/web/_not-found/__next._not-found.txt +3 -3
  122. package/dist/web/_not-found/__next._tree.txt +2 -2
  123. package/dist/web/_not-found.html +1 -1
  124. package/dist/web/_not-found.txt +19 -19
  125. package/dist/web/index.html +1 -1
  126. package/dist/web/index.txt +21 -21
  127. package/dist/web/login/__next._full.txt +21 -21
  128. package/dist/web/login/__next._head.txt +4 -4
  129. package/dist/web/login/__next._index.txt +13 -13
  130. package/dist/web/login/__next._tree.txt +2 -2
  131. package/dist/web/login/__next.login.__PAGE__.txt +4 -4
  132. package/dist/web/login/__next.login.txt +3 -3
  133. package/dist/web/login.html +1 -1
  134. package/dist/web/login.txt +21 -21
  135. package/dist/web/settings/__next._full.txt +23 -23
  136. package/dist/web/settings/__next._head.txt +4 -4
  137. package/dist/web/settings/__next._index.txt +13 -13
  138. package/dist/web/settings/__next._tree.txt +2 -2
  139. package/dist/web/settings/__next.settings.__PAGE__.txt +4 -4
  140. package/dist/web/settings/__next.settings.txt +5 -5
  141. package/dist/web/settings/agents/__next._full.txt +23 -23
  142. package/dist/web/settings/agents/__next._head.txt +4 -4
  143. package/dist/web/settings/agents/__next._index.txt +13 -13
  144. package/dist/web/settings/agents/__next._tree.txt +2 -2
  145. package/dist/web/settings/agents/__next.settings.agents.__PAGE__.txt +4 -4
  146. package/dist/web/settings/agents/__next.settings.agents.txt +3 -3
  147. package/dist/web/settings/agents/__next.settings.txt +5 -5
  148. package/dist/web/settings/agents.html +1 -1
  149. package/dist/web/settings/agents.txt +23 -23
  150. package/dist/web/settings/mcps/__next._full.txt +23 -23
  151. package/dist/web/settings/mcps/__next._head.txt +4 -4
  152. package/dist/web/settings/mcps/__next._index.txt +13 -13
  153. package/dist/web/settings/mcps/__next._tree.txt +2 -2
  154. package/dist/web/settings/mcps/__next.settings.mcps.__PAGE__.txt +4 -4
  155. package/dist/web/settings/mcps/__next.settings.mcps.txt +3 -3
  156. package/dist/web/settings/mcps/__next.settings.txt +5 -5
  157. package/dist/web/settings/mcps.html +1 -1
  158. package/dist/web/settings/mcps.txt +23 -23
  159. package/dist/web/settings/models/__next._full.txt +23 -23
  160. package/dist/web/settings/models/__next._head.txt +4 -4
  161. package/dist/web/settings/models/__next._index.txt +13 -13
  162. package/dist/web/settings/models/__next._tree.txt +2 -2
  163. package/dist/web/settings/models/__next.settings.models.__PAGE__.txt +4 -4
  164. package/dist/web/settings/models/__next.settings.models.txt +3 -3
  165. package/dist/web/settings/models/__next.settings.txt +5 -5
  166. package/dist/web/settings/models.html +1 -1
  167. package/dist/web/settings/models.txt +23 -23
  168. package/dist/web/settings/output-styles/__next._full.txt +23 -23
  169. package/dist/web/settings/output-styles/__next._head.txt +4 -4
  170. package/dist/web/settings/output-styles/__next._index.txt +13 -13
  171. package/dist/web/settings/output-styles/__next._tree.txt +2 -2
  172. package/dist/web/settings/output-styles/__next.settings.output-styles.__PAGE__.txt +4 -4
  173. package/dist/web/settings/output-styles/__next.settings.output-styles.txt +3 -3
  174. package/dist/web/settings/output-styles/__next.settings.txt +5 -5
  175. package/dist/web/settings/output-styles.html +1 -1
  176. package/dist/web/settings/output-styles.txt +23 -23
  177. package/dist/web/settings/prompts/__next._full.txt +23 -23
  178. package/dist/web/settings/prompts/__next._head.txt +4 -4
  179. package/dist/web/settings/prompts/__next._index.txt +13 -13
  180. package/dist/web/settings/prompts/__next._tree.txt +2 -2
  181. package/dist/web/settings/prompts/__next.settings.prompts.__PAGE__.txt +4 -4
  182. package/dist/web/settings/prompts/__next.settings.prompts.txt +3 -3
  183. package/dist/web/settings/prompts/__next.settings.txt +5 -5
  184. package/dist/web/settings/prompts.html +1 -1
  185. package/dist/web/settings/prompts.txt +23 -23
  186. package/dist/web/settings/providers/__next._full.txt +23 -23
  187. package/dist/web/settings/providers/__next._head.txt +4 -4
  188. package/dist/web/settings/providers/__next._index.txt +13 -13
  189. package/dist/web/settings/providers/__next._tree.txt +2 -2
  190. package/dist/web/settings/providers/__next.settings.providers.__PAGE__.txt +4 -4
  191. package/dist/web/settings/providers/__next.settings.providers.txt +3 -3
  192. package/dist/web/settings/providers/__next.settings.txt +5 -5
  193. package/dist/web/settings/providers.html +1 -1
  194. package/dist/web/settings/providers.txt +23 -23
  195. package/dist/web/settings/skills/__next._full.txt +23 -23
  196. package/dist/web/settings/skills/__next._head.txt +4 -4
  197. package/dist/web/settings/skills/__next._index.txt +13 -13
  198. package/dist/web/settings/skills/__next._tree.txt +2 -2
  199. package/dist/web/settings/skills/__next.settings.skills.__PAGE__.txt +4 -4
  200. package/dist/web/settings/skills/__next.settings.skills.txt +3 -3
  201. package/dist/web/settings/skills/__next.settings.txt +5 -5
  202. package/dist/web/settings/skills.html +1 -1
  203. package/dist/web/settings/skills.txt +23 -23
  204. package/dist/web/settings/tools/__next._full.txt +23 -23
  205. package/dist/web/settings/tools/__next._head.txt +4 -4
  206. package/dist/web/settings/tools/__next._index.txt +13 -13
  207. package/dist/web/settings/tools/__next._tree.txt +2 -2
  208. package/dist/web/settings/tools/__next.settings.tools.__PAGE__.txt +4 -4
  209. package/dist/web/settings/tools/__next.settings.tools.txt +3 -3
  210. package/dist/web/settings/tools/__next.settings.txt +5 -5
  211. package/dist/web/settings/tools.html +1 -1
  212. package/dist/web/settings/tools.txt +23 -23
  213. package/dist/web/settings.html +1 -1
  214. package/dist/web/settings.txt +23 -23
  215. package/dist/web/workflows/__next._full.txt +22 -22
  216. package/dist/web/workflows/__next._head.txt +4 -4
  217. package/dist/web/workflows/__next._index.txt +13 -13
  218. package/dist/web/workflows/__next._tree.txt +2 -2
  219. package/dist/web/workflows/__next.workflows.__PAGE__.txt +4 -4
  220. package/dist/web/workflows/__next.workflows.txt +3 -3
  221. package/dist/web/workflows.html +1 -1
  222. package/dist/web/workflows.txt +22 -22
  223. package/dist/web/workspace/_/__next._full.txt +20 -20
  224. package/dist/web/workspace/_/__next._head.txt +4 -4
  225. package/dist/web/workspace/_/__next._index.txt +13 -13
  226. package/dist/web/workspace/_/__next._tree.txt +2 -2
  227. package/dist/web/workspace/_/__next.workspace.$d$id.__PAGE__.txt +3 -3
  228. package/dist/web/workspace/_/__next.workspace.$d$id.txt +3 -3
  229. package/dist/web/workspace/_/__next.workspace.txt +3 -3
  230. package/dist/web/workspace/_.html +1 -1
  231. package/dist/web/workspace/_.txt +20 -20
  232. package/dist/web/workspaces/__next._full.txt +21 -21
  233. package/dist/web/workspaces/__next._head.txt +4 -4
  234. package/dist/web/workspaces/__next._index.txt +13 -13
  235. package/dist/web/workspaces/__next._tree.txt +2 -2
  236. package/dist/web/workspaces/__next.workspaces.__PAGE__.txt +4 -4
  237. package/dist/web/workspaces/__next.workspaces.txt +3 -3
  238. package/dist/web/workspaces.html +1 -1
  239. package/dist/web/workspaces.txt +21 -21
  240. package/dist/ws/agent-prompt.js +36 -3
  241. package/dist/ws/agent-runner.js +30 -18
  242. package/dist/ws/chat-handler.js +89 -0
  243. package/dist/ws/connection-manager.js +49 -2
  244. package/dist/ws/execution-channels.js +88 -0
  245. package/dist/ws/handler.js +32 -5
  246. package/dist/ws/message-parts.js +130 -12
  247. package/dist/ws/terminal-handler.js +26 -0
  248. package/package.json +13 -1
  249. package/public/avatars/user.jpg +0 -0
  250. package/dist/web/_next/static/chunks/0-v_p6j5u3_9l.js +0 -1
  251. package/dist/web/_next/static/chunks/0.-x29t-8gk0r.js +0 -1
  252. package/dist/web/_next/static/chunks/0.l5e8~r7_0~p.js +0 -1
  253. package/dist/web/_next/static/chunks/0.n~yfwf4~6w-.js +0 -1
  254. package/dist/web/_next/static/chunks/00-syqzi-6zxr.js +0 -1
  255. package/dist/web/_next/static/chunks/02j4sr9psucf-.js +0 -1
  256. package/dist/web/_next/static/chunks/06si8l0doqe8p.js +0 -1
  257. package/dist/web/_next/static/chunks/08kfx8on9oty~.js +0 -1
  258. package/dist/web/_next/static/chunks/08ugbvzr20drp.js +0 -2
  259. package/dist/web/_next/static/chunks/0_cqojbge48kq.js +0 -5
  260. package/dist/web/_next/static/chunks/0d6~~lv0-6kex.js +0 -3
  261. package/dist/web/_next/static/chunks/0gdwop9cnpmtu.js +0 -1
  262. package/dist/web/_next/static/chunks/0ghx3j_.-gdyd.js +0 -1
  263. package/dist/web/_next/static/chunks/0kbvoxla7f751.js +0 -1
  264. package/dist/web/_next/static/chunks/0paaat8y96ayc.js +0 -7
  265. package/dist/web/_next/static/chunks/0pq318f8xgafg.js +0 -1
  266. package/dist/web/_next/static/chunks/0t6s8of0qr6nc.js +0 -1
  267. package/dist/web/_next/static/chunks/0xt-8wk21gs7g.js +0 -1
  268. package/dist/web/_next/static/chunks/0yd.m1afqg.2t.js +0 -3
  269. package/dist/web/_next/static/chunks/0yhbr8hl09b-5.js +0 -1
  270. package/dist/web/_next/static/chunks/0z~vqzbm_q371.js +0 -1
  271. package/dist/web/_next/static/chunks/0~~_ltu6dx_ch.js +0 -1
  272. package/dist/web/_next/static/chunks/12oxcgyhpaxw~.js +0 -1
  273. package/dist/web/_next/static/chunks/1452eibz62gs6.css +0 -1
  274. package/dist/web/_next/static/chunks/16zlreiems5cn.js +0 -1
  275. package/dist/web/_next/static/chunks/18awqjk9-o5m0.js +0 -1
  276. /package/dist/web/_next/static/{x4-CsKe6qUQyFDWVtfvUg → rGXyAK8rgod1NjF3bLZAY}/_buildManifest.js +0 -0
  277. /package/dist/web/_next/static/{x4-CsKe6qUQyFDWVtfvUg → rGXyAK8rgod1NjF3bLZAY}/_clientMiddlewareManifest.js +0 -0
  278. /package/dist/web/_next/static/{x4-CsKe6qUQyFDWVtfvUg → rGXyAK8rgod1NjF3bLZAY}/_ssgManifest.js +0 -0
@@ -0,0 +1,1346 @@
1
+ // Workflow Execution Manager — core execution engine
2
+ // Ported from work_fox, adapted for agent-spaces:
3
+ // - Removed: pluginRegistry, clientNodeCache, Electron main process bridge
4
+ // - Changed: agent_run uses agent-spaces Agent runtime directly
5
+ // - Kept: DAG traversal, loops, switches, variables, breakpoints, recovery
6
+ import { randomUUID } from 'node:crypto';
7
+ import { AsyncLocalStorage } from 'node:async_hooks';
8
+ import { createErrorShape } from '@agent-spaces/shared';
9
+ import * as workflowStore from '../storage/workflow-store.js';
10
+ import * as pluginService from './plugin.js';
11
+ const MAX_RECENT_EVENTS = 100;
12
+ const FINISHED_RECOVERY_TTL_MS = 2 * 60_000;
13
+ const DELAY_NODE_MIN_MS = 100;
14
+ const DELAY_NODE_MAX_MS = 30_000;
15
+ export class ExecutionManager {
16
+ deps;
17
+ sessions = new Map();
18
+ finishedRecoveries = new Map();
19
+ loopWorkerState = new AsyncLocalStorage();
20
+ constructor(deps) {
21
+ this.deps = deps;
22
+ }
23
+ getRunningSessionCount() {
24
+ let count = 0;
25
+ for (const session of this.sessions.values()) {
26
+ if (session.status === 'running' || session.status === 'paused')
27
+ count++;
28
+ }
29
+ return count;
30
+ }
31
+ async execute(request, ownerClientId, eventSink) {
32
+ const workflow = workflowStore.getWorkflow(request.workflowId);
33
+ if (!workflow) {
34
+ throw createErrorShape('NOT_FOUND', `Workflow not found: ${request.workflowId}`);
35
+ }
36
+ const snapshot = this.resolveExecutionSnapshot(workflow, request);
37
+ const executionId = randomUUID();
38
+ const session = this.createSession(executionId, workflow, ownerClientId, request.input || {}, snapshot, undefined, eventSink);
39
+ session.context.__config__ = this.loadPluginConfigs(session);
40
+ this.sessions.set(executionId, session);
41
+ void this.run(session);
42
+ return { executionId, status: 'running' };
43
+ }
44
+ async debugNode(request, ownerClientId) {
45
+ const startedAt = Date.now();
46
+ const workflow = workflowStore.getWorkflow(request.workflowId);
47
+ if (!workflow) {
48
+ throw createErrorShape('NOT_FOUND', `Workflow not found: ${request.workflowId}`);
49
+ }
50
+ const snapshotNodes = request.snapshot?.nodes ? clone(request.snapshot.nodes) : clone(workflow.nodes);
51
+ const snapshotEdges = request.snapshot?.edges ? clone(request.snapshot.edges) : clone(workflow.edges);
52
+ const snapshotGroups = request.snapshot?.groups ? clone(request.snapshot.groups) : clone(workflow.groups || []);
53
+ const embeddedNode = request.embeddedNode ? clone(request.embeddedNode) : null;
54
+ const nodes = embeddedNode
55
+ ? snapshotNodes.some(n => n.id === request.nodeId)
56
+ ? snapshotNodes.map(n => n.id === request.nodeId ? embeddedNode : n)
57
+ : [...snapshotNodes, embeddedNode]
58
+ : snapshotNodes;
59
+ const targetNode = nodes.find(n => n.id === request.nodeId);
60
+ if (!targetNode) {
61
+ return { status: 'error', error: `Node not found: ${request.nodeId}`, duration: Date.now() - startedAt };
62
+ }
63
+ const session = this.createSession(`debug-${randomUUID()}`, workflow, ownerClientId, request.input || {}, { nodes, edges: snapshotEdges, groups: snapshotGroups }, request.context);
64
+ try {
65
+ session.context.__config__ = this.loadPluginConfigs(session);
66
+ session.status = 'running';
67
+ await this.executeNode(session, targetNode);
68
+ const step = [...session.steps].reverse().find(s => s.nodeId === targetNode.id);
69
+ if (step?.status === 'error') {
70
+ return { status: 'error', error: step.error || 'Debug failed', duration: Date.now() - startedAt };
71
+ }
72
+ return { status: 'completed', output: step?.output, duration: Date.now() - startedAt };
73
+ }
74
+ catch (error) {
75
+ return {
76
+ status: 'error',
77
+ error: error instanceof Error ? error.message : String(error),
78
+ duration: Date.now() - startedAt,
79
+ };
80
+ }
81
+ }
82
+ pause(executionId) {
83
+ const session = this.getSession(executionId);
84
+ if (session.status === 'running')
85
+ session.pauseRequested = true;
86
+ return { executionId, status: session.status };
87
+ }
88
+ async resume(executionId) {
89
+ const session = this.getSession(executionId);
90
+ if (session.status !== 'paused')
91
+ return { executionId, status: session.status };
92
+ const prev = session.pauseReason;
93
+ session.pauseRequested = false;
94
+ session.pauseReason = undefined;
95
+ session.pauseNodeId = undefined;
96
+ session.pauseBreakpoint = undefined;
97
+ session.status = 'running';
98
+ const current = session.executionOrder[session.currentIndex];
99
+ if (prev === 'breakpoint-start' && current?.breakpoint === 'start') {
100
+ session.breakpointBypassKeys.add(`${current.id}:start`);
101
+ }
102
+ this.emitEvent(session, 'workflow:resumed', {
103
+ executionId: session.id, workflowId: session.workflow.id,
104
+ timestamp: Date.now(), status: 'running',
105
+ currentNodeId: session.executionOrder[session.currentIndex]?.id,
106
+ });
107
+ this.emitLog(session);
108
+ void this.runSafe(session, session.currentIndex);
109
+ return { executionId, status: session.status };
110
+ }
111
+ stop(executionId) {
112
+ const session = this.sessions.get(executionId);
113
+ if (!session) {
114
+ const fr = this.finishedRecoveries.get(executionId);
115
+ return { executionId, status: fr?.recovery.status ?? 'error' };
116
+ }
117
+ session.stopRequested = true;
118
+ this.deps.interactionManager.cancelExecution(executionId, 'Execution stopped');
119
+ if (session.status === 'running' || session.status === 'paused') {
120
+ session.status = 'error';
121
+ session.lastErrorMessage = 'Execution stopped';
122
+ session.finishedAt = Date.now();
123
+ this.emitLog(session);
124
+ this.emitWorkflowError(session);
125
+ this.persistAndCleanup(session);
126
+ }
127
+ return { executionId, status: session.status };
128
+ }
129
+ getExecutionRecovery(request, ownerClientId) {
130
+ this.pruneFinishedRecoveries();
131
+ const active = this.findSession(ownerClientId, request.workflowId, request.executionId);
132
+ if (active) {
133
+ return { found: true, execution: this.createRecoveryState(active, true) };
134
+ }
135
+ const finished = this.findFinishedRecovery(ownerClientId, request.workflowId, request.executionId);
136
+ if (finished) {
137
+ return { found: true, execution: clone(finished.recovery) };
138
+ }
139
+ return { found: false };
140
+ }
141
+ // ---- Private: Session lifecycle ----
142
+ resolveExecutionSnapshot(workflow, request) {
143
+ const baseNodes = request.snapshot?.nodes ? clone(request.snapshot.nodes) : clone(workflow.nodes);
144
+ const baseEdges = request.snapshot?.edges ? clone(request.snapshot.edges) : clone(workflow.edges);
145
+ const baseGroups = request.snapshot?.groups ? clone(request.snapshot.groups) : clone(workflow.groups || []);
146
+ const rootNodes = getNodesForExecutionScope(baseNodes, null);
147
+ const startNodes = rootNodes.filter(n => n.type === 'start');
148
+ if (request.startNodeId) {
149
+ const startNode = startNodes.find(n => n.id === request.startNodeId);
150
+ if (!startNode) {
151
+ throw createErrorShape('BAD_REQUEST', `Start node not found: ${request.startNodeId}`);
152
+ }
153
+ return this.buildReachableSnapshot(baseNodes, baseEdges, baseGroups, startNode.id);
154
+ }
155
+ if (startNodes.length > 1) {
156
+ const choices = startNodes.map(n => `${n.label || 'Start'}(${n.id})`).join(', ');
157
+ throw createErrorShape('BAD_REQUEST', `Multiple start nodes, specify startNodeId: ${choices}`);
158
+ }
159
+ return request.snapshot ? { nodes: baseNodes, edges: baseEdges, groups: baseGroups } : undefined;
160
+ }
161
+ buildReachableSnapshot(nodes, edges, groups, firstNodeId) {
162
+ const reachableIds = new Set([firstNodeId]);
163
+ const queue = [firstNodeId];
164
+ while (queue.length > 0) {
165
+ const sourceId = queue.shift();
166
+ for (const edge of edges) {
167
+ if (edge.source !== sourceId || reachableIds.has(edge.target))
168
+ continue;
169
+ reachableIds.add(edge.target);
170
+ queue.push(edge.target);
171
+ }
172
+ }
173
+ const partialNodes = nodes.filter(n => reachableIds.has(n.id));
174
+ const first = partialNodes.find(n => n.id === firstNodeId);
175
+ return {
176
+ nodes: first ? [first, ...partialNodes.filter(n => n.id !== firstNodeId)] : partialNodes,
177
+ edges: edges.filter(e => reachableIds.has(e.source) && reachableIds.has(e.target)),
178
+ groups,
179
+ };
180
+ }
181
+ createSession(executionId, workflow, ownerClientId, input, snapshot, context, eventSink) {
182
+ return {
183
+ id: executionId, workflow, ownerClientId,
184
+ nodes: snapshot?.nodes ? clone(snapshot.nodes) : clone(workflow.nodes),
185
+ edges: snapshot?.edges ? clone(snapshot.edges) : clone(workflow.edges),
186
+ groups: snapshot?.groups ? clone(snapshot.groups) : clone(workflow.groups || []),
187
+ context: {
188
+ ...(context ? clone(context) : {}),
189
+ __data__: context?.__data__ && typeof context.__data__ === 'object' ? clone(context.__data__) : {},
190
+ __input__: input,
191
+ },
192
+ status: 'idle', executionOrder: [], currentIndex: 0,
193
+ pauseRequested: false, stopRequested: false,
194
+ startedAt: Date.now(), steps: [],
195
+ activeBranches: new Map(), persisted: false,
196
+ lastUpdatedAt: Date.now(), eventSequence: 0,
197
+ recentEvents: [], loopStack: [],
198
+ breakpointBypassKeys: new Set(), eventSink,
199
+ };
200
+ }
201
+ // ---- Private: Execution loop ----
202
+ async run(session) {
203
+ try {
204
+ session.executionOrder = this.buildExecutionOrder(session.nodes, session.edges);
205
+ if (session.executionOrder.length === 0) {
206
+ session.status = 'error';
207
+ session.lastErrorMessage = 'Empty workflow or no execution order';
208
+ session.finishedAt = Date.now();
209
+ this.emitWorkflowError(session);
210
+ this.persistAndCleanup(session);
211
+ return;
212
+ }
213
+ session.status = 'running';
214
+ session.startedAt = Date.now();
215
+ this.emitEvent(session, 'workflow:started', {
216
+ executionId: session.id, workflowId: session.workflow.id,
217
+ timestamp: session.startedAt, status: 'running',
218
+ workflowName: session.workflow.name,
219
+ });
220
+ this.emitLog(session);
221
+ this.emitContext(session);
222
+ await this.runSafe(session, 0);
223
+ }
224
+ catch (error) {
225
+ this.handleExecutionError(session, error);
226
+ }
227
+ }
228
+ async runSafe(session, startIndex) {
229
+ try {
230
+ await this.runFromIndex(session, startIndex);
231
+ }
232
+ catch (error) {
233
+ this.handleExecutionError(session, error);
234
+ }
235
+ }
236
+ handleExecutionError(session, error) {
237
+ if (session.status === 'completed' || session.status === 'error')
238
+ return;
239
+ session.status = 'error';
240
+ session.lastErrorMessage = error instanceof Error ? error.message : String(error);
241
+ session.finishedAt = Date.now();
242
+ this.emitWorkflowError(session);
243
+ this.persistAndCleanup(session);
244
+ }
245
+ async runFromIndex(session, startIndex) {
246
+ for (let i = startIndex; i < session.executionOrder.length; i++) {
247
+ if (session.stopRequested) {
248
+ if (session.status === 'error')
249
+ return;
250
+ session.status = 'error';
251
+ session.lastErrorMessage = 'Execution stopped';
252
+ session.finishedAt = Date.now();
253
+ this.emitLog(session);
254
+ this.emitWorkflowError(session);
255
+ this.persistAndCleanup(session);
256
+ return;
257
+ }
258
+ if (session.pauseRequested) {
259
+ session.currentIndex = i;
260
+ session.status = 'paused';
261
+ session.pauseReason = 'manual';
262
+ session.pauseNodeId = session.executionOrder[i]?.id;
263
+ this.emitLog(session);
264
+ this.emitEvent(session, 'workflow:paused', {
265
+ executionId: session.id, workflowId: session.workflow.id,
266
+ timestamp: Date.now(), status: 'paused',
267
+ currentNodeId: session.executionOrder[i]?.id, reason: 'manual',
268
+ });
269
+ return;
270
+ }
271
+ session.currentIndex = i;
272
+ const node = session.executionOrder[i];
273
+ const nodeState = node.nodeState || 'normal';
274
+ if (node.type === 'loop_body' && isGeneratedWorkflowNode(node))
275
+ continue;
276
+ if (getCompositeParentId(node))
277
+ continue;
278
+ if (this.getActiveBranches(session).size > 0 && !this.isNodeReachable(session, node.id)) {
279
+ this.recordSkippedStep(session, node, 'Inactive branch');
280
+ continue;
281
+ }
282
+ if (nodeState === 'disabled') {
283
+ this.recordSkippedStep(session, node, 'Node disabled');
284
+ session.status = 'error';
285
+ session.lastErrorMessage = 'Node disabled, workflow aborted';
286
+ session.finishedAt = Date.now();
287
+ this.emitLog(session);
288
+ this.emitWorkflowError(session);
289
+ this.persistAndCleanup(session);
290
+ return;
291
+ }
292
+ if (nodeState === 'skipped') {
293
+ this.recordSkippedStep(session, node, 'Node skipped');
294
+ continue;
295
+ }
296
+ if (this.shouldPauseAtBreakpoint(session, node, 'start')) {
297
+ this.pauseAtBreakpoint(session, i, node, 'start');
298
+ return;
299
+ }
300
+ const result = await this.executeNode(session, node);
301
+ if (result === 'interrupted') {
302
+ i -= 1;
303
+ continue;
304
+ }
305
+ if (session.status === 'error') {
306
+ session.finishedAt = Date.now();
307
+ this.emitLog(session);
308
+ this.emitWorkflowError(session);
309
+ this.persistAndCleanup(session);
310
+ return;
311
+ }
312
+ if (this.shouldPauseAtBreakpoint(session, node, 'end')) {
313
+ this.pauseAtBreakpoint(session, i + 1, node, 'end');
314
+ return;
315
+ }
316
+ }
317
+ session.status = 'completed';
318
+ session.finishedAt = Date.now();
319
+ this.emitLog(session);
320
+ this.emitContext(session);
321
+ this.emitEvent(session, 'workflow:completed', {
322
+ executionId: session.id, workflowId: session.workflow.id,
323
+ timestamp: Date.now(), status: 'completed',
324
+ log: this.currentLog(session), context: this.currentContext(session),
325
+ });
326
+ this.persistAndCleanup(session);
327
+ }
328
+ // ---- Private: Node execution ----
329
+ async executeNode(session, node) {
330
+ if (session.stopRequested || session.status === 'error')
331
+ return 'interrupted';
332
+ const delay = typeof node.data?._delay === 'number' ? node.data._delay : 0;
333
+ if (delay > 0) {
334
+ await sleep(delay);
335
+ if (session.stopRequested || session.pauseRequested)
336
+ return 'interrupted';
337
+ }
338
+ const resolvedData = this.resolveContextVariables(session, { ...node.data });
339
+ const stepInput = this.getStepInput(node, resolvedData);
340
+ this.setNodeExecutionInput(session, node.id, node.type === 'end' ? {} : this.buildOutputObject(resolvedData.inputFields) ?? {});
341
+ const step = {
342
+ nodeId: node.id, nodeLabel: node.label, startedAt: Date.now(), status: 'running',
343
+ ...(stepInput === undefined ? {} : { input: stepInput }),
344
+ };
345
+ session.steps.push(step);
346
+ this.emitEvent(session, 'node:start', {
347
+ executionId: session.id, workflowId: session.workflow.id,
348
+ timestamp: Date.now(), nodeId: node.id, nodeLabel: node.label, input: stepInput,
349
+ });
350
+ this.emitLog(session);
351
+ const stepLogs = [];
352
+ const appendLog = (level, message) => {
353
+ const entry = { level, message, timestamp: Date.now() };
354
+ stepLogs.push(entry);
355
+ step.logs = [...stepLogs];
356
+ this.emitEvent(session, 'node:progress', {
357
+ executionId: session.id, workflowId: session.workflow.id,
358
+ timestamp: entry.timestamp, nodeId: node.id, message, data: { level },
359
+ });
360
+ this.emitLog(session);
361
+ };
362
+ try {
363
+ const result = await this.dispatchNode(session, node, resolvedData, appendLog);
364
+ if (session.stopRequested)
365
+ return 'interrupted';
366
+ step.finishedAt = Date.now();
367
+ step.status = 'completed';
368
+ step.output = result && Array.isArray(result._logs)
369
+ ? (() => { step.logs = result._logs; const { _logs, ...rest } = result; return rest; })()
370
+ : result;
371
+ session.context[node.id] = step.output;
372
+ this.setNodeExecutionData(session, node.id, result);
373
+ if (node.type === 'switch' && result?.__branch__) {
374
+ this.getActiveBranches(session).set(node.id, result.__branch__);
375
+ }
376
+ this.emitContext(session);
377
+ this.emitEvent(session, 'node:complete', {
378
+ executionId: session.id, workflowId: session.workflow.id,
379
+ timestamp: Date.now(), nodeId: node.id, step: { ...step },
380
+ });
381
+ this.emitLog(session);
382
+ }
383
+ catch (error) {
384
+ if (session.stopRequested)
385
+ return 'interrupted';
386
+ step.finishedAt = Date.now();
387
+ step.status = 'error';
388
+ step.error = error instanceof Error ? error.message : String(error);
389
+ step.logs = stepLogs.length ? [...stepLogs] : undefined;
390
+ session.status = 'error';
391
+ session.lastErrorMessage = step.error;
392
+ this.emitEvent(session, 'node:error', {
393
+ executionId: session.id, workflowId: session.workflow.id,
394
+ timestamp: Date.now(), nodeId: node.id, step: { ...step },
395
+ error: createErrorShape('WORKFLOW_ERROR', step.error),
396
+ });
397
+ this.emitLog(session);
398
+ }
399
+ return 'completed';
400
+ }
401
+ async dispatchNode(session, node, resolvedData, appendLog) {
402
+ switch (node.type) {
403
+ case 'start': {
404
+ const fieldOutput = this.buildOutputObject(resolvedData.inputFields) ?? {};
405
+ const runtimeInput = session.context.__input__ ?? {};
406
+ return { ...fieldOutput, ...runtimeInput };
407
+ }
408
+ case 'loop_body':
409
+ case 'sticky_note':
410
+ return null;
411
+ case 'loop_break':
412
+ return this.executeLoopBreak(session, appendLog);
413
+ case 'end':
414
+ return this.buildOutputObject(resolvedData.outputs);
415
+ case 'gallery_preview':
416
+ return { items: Array.isArray(resolvedData.items) ? resolvedData.items : [] };
417
+ case 'table_display':
418
+ return this.executeTableDisplay(session, node, resolvedData);
419
+ case 'run_code':
420
+ return this.executeCode(this.getRuntimeContext(session), String(resolvedData.code || ''), this.buildOutputObject(resolvedData.inputFields) ?? {});
421
+ case 'toast':
422
+ return { message: String(resolvedData.message || ''), type: String(resolvedData.type || 'info') };
423
+ case 'delay':
424
+ return this.executeDelayNode(resolvedData, appendLog);
425
+ case 'switch':
426
+ return this.executeSwitch(session, resolvedData.conditions || []);
427
+ case 'variable_aggregate':
428
+ return this.executeVariableAggregate(resolvedData.groups || []);
429
+ case 'sub_workflow':
430
+ return this.executeSubWorkflow(session, resolvedData, appendLog);
431
+ case 'loop':
432
+ return this.executeLoopNode(session, node, resolvedData, appendLog);
433
+ case 'agent_run':
434
+ return this.executeAgentRun(session, node, resolvedData, appendLog);
435
+ case 'alert':
436
+ return this.executeAlertDialog(session, node, resolvedData, appendLog);
437
+ case 'prompt':
438
+ return this.executePromptDialog(session, node, resolvedData, appendLog);
439
+ case 'form':
440
+ return this.executeFormDialog(session, node, resolvedData, appendLog);
441
+ default:
442
+ if (pluginService.canExecuteWorkflowNode(node.type)) {
443
+ return pluginService.executeWorkflowNode(node.type, resolvedData, {
444
+ logger: {
445
+ info: (message) => appendLog('info', message),
446
+ warning: (message) => appendLog('warning', message),
447
+ error: (message) => appendLog('error', message),
448
+ },
449
+ });
450
+ }
451
+ throw new Error(`Unsupported node type: ${node.type}`);
452
+ }
453
+ }
454
+ // ---- Private: Node type implementations ----
455
+ async executeDelayNode(resolvedData, appendLog) {
456
+ const rawMilliseconds = Number(resolvedData.milliseconds);
457
+ const milliseconds = Number.isFinite(rawMilliseconds)
458
+ ? Math.min(Math.max(rawMilliseconds, DELAY_NODE_MIN_MS), DELAY_NODE_MAX_MS)
459
+ : 1000;
460
+ const reason = typeof resolvedData.reason === 'string' ? resolvedData.reason.trim() : '';
461
+ appendLog('info', reason ? `Delay ${milliseconds}ms: ${reason}` : `Delay ${milliseconds}ms`);
462
+ await sleep(milliseconds);
463
+ return { milliseconds, reason };
464
+ }
465
+ executeLoopBreak(session, appendLog) {
466
+ const frame = this.getLoopFrame(session);
467
+ if (!frame)
468
+ throw new Error('loop_break can only run inside a loop body');
469
+ frame.breakRequested = true;
470
+ appendLog('info', 'Loop break requested');
471
+ return { break: true };
472
+ }
473
+ async executeAgentRun(session, node, resolvedData, appendLog) {
474
+ const prompt = typeof resolvedData.prompt === 'string' ? resolvedData.prompt : '';
475
+ if (!prompt.trim())
476
+ throw new Error('agent_run node missing prompt');
477
+ appendLog('info', 'Executing agent_run node');
478
+ // If agentConfigId is specified, use agent-spaces Agent runtime
479
+ const agentConfigId = resolvedData.agentConfigId;
480
+ if (agentConfigId) {
481
+ return this.executeAgentWithRuntime(session, node, resolvedData, appendLog);
482
+ }
483
+ // Fallback: use interaction manager (client-side execution)
484
+ const result = await this.deps.interactionManager.request({
485
+ clientId: session.ownerClientId,
486
+ executionId: session.id,
487
+ workflowId: session.workflow.id,
488
+ nodeId: node.id,
489
+ interactionType: 'agent_chat',
490
+ schema: {
491
+ prompt,
492
+ systemPrompt: typeof resolvedData.systemPrompt === 'string' ? resolvedData.systemPrompt : undefined,
493
+ cwd: typeof resolvedData.cwd === 'string' ? resolvedData.cwd : undefined,
494
+ workflowId: session.workflow.id,
495
+ workflowName: session.workflow.name,
496
+ },
497
+ });
498
+ appendLog('info', 'Agent execution completed');
499
+ return result;
500
+ }
501
+ async executeAgentWithRuntime(session, _node, resolvedData, appendLog) {
502
+ const { createAgentRuntime } = await import('../adapters/agent-runtime.js');
503
+ const { listPresets } = await import('./agent.js');
504
+ const agentConfigId = resolvedData.agentConfigId;
505
+ const presets = listPresets(session.workflow.id);
506
+ const preset = presets.find(p => p.id === agentConfigId);
507
+ if (!preset)
508
+ throw new Error(`Agent preset not found: ${agentConfigId}`);
509
+ appendLog('info', `Using agent: ${preset.name || preset.id}`);
510
+ const runtime = createAgentRuntime({
511
+ kind: preset.runtimeKind,
512
+ provider: preset.modelProvider,
513
+ model: preset.modelId,
514
+ apiKey: preset.apiKey,
515
+ baseURL: preset.apiBase,
516
+ });
517
+ const prompt = String(resolvedData.prompt || '');
518
+ const systemPrompt = typeof resolvedData.systemPrompt === 'string' ? resolvedData.systemPrompt : undefined;
519
+ const fullPrompt = systemPrompt ? `${systemPrompt}\n\n${prompt}` : prompt;
520
+ const workingDir = preset.workingDir || resolvedData.cwd || process.cwd();
521
+ const result = await runtime.execute(fullPrompt, workingDir, {
522
+ systemPrompt: preset.systemPrompt,
523
+ outputStyle: preset.outputStyle,
524
+ onEvent: (event) => {
525
+ if (event.type === 'output') {
526
+ appendLog('info', event.line);
527
+ }
528
+ else if (event.type === 'tool_use') {
529
+ appendLog('info', `Tool: ${event.name}`);
530
+ }
531
+ },
532
+ });
533
+ if (!result.success) {
534
+ throw new Error(result.summary || 'Agent execution failed');
535
+ }
536
+ appendLog('info', `Agent completed: ${result.summary || 'done'}`);
537
+ return {
538
+ output: result.output || result.summary,
539
+ summary: result.summary,
540
+ usage: result.usage,
541
+ };
542
+ }
543
+ async executeTableDisplay(session, node, resolvedData) {
544
+ const headers = Array.isArray(resolvedData.headers) ? resolvedData.headers : [];
545
+ const cells = Array.isArray(resolvedData.cells) ? resolvedData.cells : [];
546
+ const selectionMode = ['none', 'single', 'multi'].includes(resolvedData.selectionMode)
547
+ ? resolvedData.selectionMode : 'none';
548
+ if (selectionMode === 'none') {
549
+ return { selectedRows: cells, selectedCount: cells.length };
550
+ }
551
+ const result = await this.deps.interactionManager.request({
552
+ clientId: session.ownerClientId,
553
+ executionId: session.id,
554
+ workflowId: session.workflow.id,
555
+ nodeId: node.id,
556
+ interactionType: 'table_confirm',
557
+ schema: { headers, cells, selectionMode },
558
+ });
559
+ return { ...result, headers, cells };
560
+ }
561
+ async executeAlertDialog(session, node, resolvedData, appendLog) {
562
+ appendLog('info', 'Waiting for alert confirmation');
563
+ await this.deps.interactionManager.request({
564
+ clientId: session.ownerClientId,
565
+ executionId: session.id,
566
+ workflowId: session.workflow.id,
567
+ nodeId: node.id,
568
+ interactionType: 'dialog_alert',
569
+ schema: { title: String(resolvedData.title || 'Alert'), message: String(resolvedData.message || '') },
570
+ });
571
+ appendLog('info', 'Alert confirmed');
572
+ return { confirmed: true };
573
+ }
574
+ async executePromptDialog(session, node, resolvedData, appendLog) {
575
+ appendLog('info', 'Waiting for user input');
576
+ const result = await this.deps.interactionManager.request({
577
+ clientId: session.ownerClientId,
578
+ executionId: session.id,
579
+ workflowId: session.workflow.id,
580
+ nodeId: node.id,
581
+ interactionType: 'dialog_prompt',
582
+ schema: {
583
+ title: String(resolvedData.title || 'Input'),
584
+ message: String(resolvedData.message || ''),
585
+ placeholder: String(resolvedData.placeholder || ''),
586
+ defaultValue: String(resolvedData.defaultValue || ''),
587
+ },
588
+ });
589
+ appendLog('info', 'User input received');
590
+ if (result && typeof result === 'object' && 'value' in result) {
591
+ return { value: result.value, confirmed: true };
592
+ }
593
+ return { value: result, confirmed: result !== null };
594
+ }
595
+ async executeFormDialog(session, node, resolvedData, appendLog) {
596
+ const items = Array.isArray(resolvedData.items) ? resolvedData.items : [];
597
+ appendLog('info', `Waiting for form (${items.length} items)`);
598
+ const result = await this.deps.interactionManager.request({
599
+ clientId: session.ownerClientId,
600
+ executionId: session.id,
601
+ workflowId: session.workflow.id,
602
+ nodeId: node.id,
603
+ interactionType: 'dialog_form',
604
+ schema: { title: String(resolvedData.title || 'Form'), items },
605
+ });
606
+ if (result === null || result === undefined) {
607
+ appendLog('warning', 'Form cancelled');
608
+ throw new Error('Form cancelled, workflow aborted');
609
+ }
610
+ appendLog('info', 'Form completed');
611
+ return { values: result, confirmed: result !== null };
612
+ }
613
+ executeCode(context, code, params) {
614
+ const normalized = code
615
+ .replace(/\basync\s+function\s+main\s*\(\s*\{\s*params\s*\}\s*:\s*Args\s*\)\s*:\s*Promise\s*<\s*Output\s*>/g, 'async function main({ params })')
616
+ .replace(/\bfunction\s+main\s*\(\s*\{\s*params\s*\}\s*:\s*Args\s*\)\s*:\s*Output/g, 'function main({ params })');
617
+ const fn = new Function('context', 'params', `${normalized}\nif (typeof main === 'function') return main({ params, context })`);
618
+ return fn(context, params);
619
+ }
620
+ executeSwitch(session, conditions) {
621
+ for (let i = 0; i < conditions.length; i++) {
622
+ const cond = conditions[i];
623
+ const variable = this.resolveStringValue(session, cond.variable);
624
+ const value = this.resolveStringValue(session, cond.value);
625
+ if (this.evaluateCondition(variable, value, cond.operator)) {
626
+ return { __branch__: `case-${i}`, matchedIndex: i };
627
+ }
628
+ }
629
+ return { __branch__: 'default', matchedIndex: -1 };
630
+ }
631
+ executeVariableAggregate(groups) {
632
+ if (!Array.isArray(groups))
633
+ return {};
634
+ return groups.reduce((result, group) => {
635
+ const key = typeof group?.key === 'string' ? group.key.trim() : '';
636
+ if (!key)
637
+ return result;
638
+ const variables = Array.isArray(group.variables) ? group.variables : [];
639
+ result[key] = this.findFirstNonEmpty(variables);
640
+ return result;
641
+ }, {});
642
+ }
643
+ findFirstNonEmpty(variables) {
644
+ for (const v of variables) {
645
+ const value = v?.value;
646
+ if (value !== null && value !== undefined && value !== '' &&
647
+ !(Array.isArray(value) && value.length === 0) &&
648
+ !(typeof value === 'object' && Object.keys(value).length === 0))
649
+ return value;
650
+ }
651
+ return '';
652
+ }
653
+ // ---- Private: Loop execution ----
654
+ async executeLoopNode(session, node, resolvedData, appendLog) {
655
+ const bodyNode = findCompositeChildByRole(session.nodes, node.id, 'loop_body');
656
+ if (!bodyNode)
657
+ throw new Error('Loop node missing body');
658
+ const loopType = typeof resolvedData.loopType === 'string' ? resolvedData.loopType : 'count';
659
+ const iterations = this.resolveLoopIterations(loopType, resolvedData);
660
+ const concurrency = Math.max(1, Math.floor(Number(resolvedData.concurrency) || 1));
661
+ const sharedVars = this.initLoopSharedVars(resolvedData.sharedVariables);
662
+ const items = [];
663
+ appendLog('info', iterations.infinite
664
+ ? `Starting infinite loop, concurrency ${concurrency}`
665
+ : `Starting loop, ${iterations.count} iterations, concurrency ${concurrency}`);
666
+ let nextIndex = 0;
667
+ let stopScheduling = false;
668
+ const running = new Set();
669
+ const hasNext = () => iterations.infinite || nextIndex < (iterations.count ?? 0);
670
+ const createFrame = (index) => ({
671
+ loopNodeId: node.id,
672
+ parentData: session.context.__data__,
673
+ bodyAnchorId: bodyNode.id,
674
+ variables: sharedVars,
675
+ metadata: { index, count: iterations.count, item: iterations.items[index],
676
+ isFirst: index === 0, isLast: iterations.count !== null && index === iterations.count - 1 },
677
+ });
678
+ const startNext = () => {
679
+ if (stopScheduling || !hasNext())
680
+ return false;
681
+ const index = nextIndex++;
682
+ const frame = createFrame(index);
683
+ const promise = this.executeLoopIteration(session, bodyNode, frame, iterations, appendLog)
684
+ .then(result => {
685
+ items[index] = normalizeLoopResult(result);
686
+ if (session.status === 'error' || frame.breakRequested)
687
+ stopScheduling = true;
688
+ }).finally(() => running.delete(promise));
689
+ running.add(promise);
690
+ return true;
691
+ };
692
+ while (running.size < concurrency && startNext()) { /* fill window */ }
693
+ while (running.size > 0) {
694
+ await Promise.race(running);
695
+ if (session.stopRequested || session.status === 'error')
696
+ throw new Error('Execution stopped');
697
+ while (running.size < concurrency && startNext()) { /* backfill */ }
698
+ }
699
+ appendLog('info', 'Loop completed');
700
+ const output = this.buildOutputObject(resolvedData.outputs) ?? {};
701
+ return { ...output, items };
702
+ }
703
+ async executeLoopIteration(session, bodyNode, frame, iterations, appendLog) {
704
+ if (session.stopRequested)
705
+ throw new Error('Execution stopped');
706
+ if (iterations.infinite && frame.metadata.index > 0)
707
+ await sleep(0);
708
+ session.loopStack.push(frame);
709
+ try {
710
+ this.syncLoopContext(session);
711
+ appendLog('info', iterations.infinite
712
+ ? `Loop iteration ${frame.metadata.index + 1}`
713
+ : `Loop iteration ${frame.metadata.index + 1}/${iterations.count}`);
714
+ return await this.runWithLoopWorkerState(session, frame, () => this.executeLoopBody(session, bodyNode));
715
+ }
716
+ finally {
717
+ const idx = session.loopStack.lastIndexOf(frame);
718
+ if (idx >= 0)
719
+ session.loopStack.splice(idx, 1);
720
+ this.syncLoopContext(session);
721
+ }
722
+ }
723
+ resolveLoopIterations(loopType, data) {
724
+ if (loopType === 'array') {
725
+ const items = Array.isArray(data.arrayPath) ? data.arrayPath : [];
726
+ return { count: items.length, items, infinite: false };
727
+ }
728
+ if (loopType === 'infinite')
729
+ return { count: null, items: [], infinite: true };
730
+ const count = Math.max(0, Math.floor(Number(data.count) || 0));
731
+ return { count, items: Array.from({ length: count }, () => undefined), infinite: false };
732
+ }
733
+ initLoopSharedVars(vars) {
734
+ if (!Array.isArray(vars))
735
+ return {};
736
+ const build = (fields) => {
737
+ const result = {};
738
+ for (const field of fields) {
739
+ if (!field?.key)
740
+ continue;
741
+ if (field.type === 'object') {
742
+ result[field.key] = build(Array.isArray(field.children) ? field.children : []);
743
+ continue;
744
+ }
745
+ result[field.key] = field.value ?? '';
746
+ }
747
+ return result;
748
+ };
749
+ return build(vars);
750
+ }
751
+ async executeLoopBody(session, bodyNode) {
752
+ const scopeNodes = getNodesForExecutionScope(session.nodes, bodyNode.id);
753
+ if (scopeNodes.length > 0)
754
+ return this.executeScopedBody(session, bodyNode, scopeNodes);
755
+ const bodyData = bodyNode.data?.bodyWorkflow;
756
+ if (bodyData && typeof bodyData === 'object') {
757
+ return this.executeEmbeddedWorkflow(session, normalizeEmbeddedWorkflow(bodyData, () => randomUUID()));
758
+ }
759
+ return this.executeScopedBody(session, bodyNode, scopeNodes);
760
+ }
761
+ async executeScopedBody(session, bodyNode, scopeNodes) {
762
+ const scopeIds = new Set(scopeNodes.map(n => n.id));
763
+ const bodyEdges = session.edges.filter(e => {
764
+ if (e.sourceHandle === 'loop_next')
765
+ return false;
766
+ const srcEntry = e.source === bodyNode.id && scopeIds.has(e.target);
767
+ return srcEntry || (scopeIds.has(e.source) && scopeIds.has(e.target));
768
+ });
769
+ const adjacency = new Map();
770
+ for (const edge of bodyEdges) {
771
+ const arr = adjacency.get(edge.source) || [];
772
+ arr.push(edge);
773
+ adjacency.set(edge.source, arr);
774
+ }
775
+ const visited = new Set();
776
+ const execFrom = async (nodeId) => {
777
+ if (shouldInterrupt(session))
778
+ return undefined;
779
+ let lastResult;
780
+ for (const edge of adjacency.get(nodeId) || []) {
781
+ if (shouldInterrupt(session))
782
+ return lastResult;
783
+ const activeHandle = this.getActiveBranches(session).get(edge.source);
784
+ if (activeHandle !== undefined && edge.sourceHandle !== activeHandle)
785
+ continue;
786
+ const nextNode = scopeNodes.find(n => n.id === edge.target);
787
+ if (!nextNode || visited.has(nextNode.id))
788
+ continue;
789
+ visited.add(nextNode.id);
790
+ const result = await this.executeNode(session, nextNode);
791
+ if (result === 'interrupted' || shouldInterrupt(session))
792
+ return lastResult;
793
+ lastResult = this.getNodeExecutionData(session, nextNode.id);
794
+ const downstream = await execFrom(nextNode.id);
795
+ if (downstream !== undefined)
796
+ lastResult = downstream;
797
+ }
798
+ return lastResult;
799
+ };
800
+ return execFrom(bodyNode.id);
801
+ }
802
+ async executeSubWorkflow(session, resolvedData, appendLog) {
803
+ const workflowId = typeof resolvedData.workflowId === 'string' ? resolvedData.workflowId : '';
804
+ if (!workflowId)
805
+ throw new Error('sub_workflow missing workflowId');
806
+ if (workflowId === session.workflow.id)
807
+ throw new Error('sub_workflow cannot call itself');
808
+ const target = workflowStore.getWorkflow(workflowId);
809
+ if (!target)
810
+ throw new Error(`sub_workflow target not found: ${workflowId}`);
811
+ appendLog('info', `Starting sub_workflow: ${target.name}`);
812
+ const result = await this.executeEmbeddedWorkflow(session, {
813
+ nodes: clone(target.nodes), edges: clone(target.edges),
814
+ }, this.buildOutputObject(resolvedData.inputFields) ?? {});
815
+ appendLog('info', `Completed sub_workflow: ${target.name}`);
816
+ return result;
817
+ }
818
+ async executeEmbeddedWorkflow(session, workflow, input) {
819
+ const nodeMap = new Map(workflow.nodes.map(n => [n.id, n]));
820
+ const adjacency = new Map();
821
+ for (const edge of workflow.edges) {
822
+ const arr = adjacency.get(edge.source) || [];
823
+ arr.push(edge);
824
+ adjacency.set(edge.source, arr);
825
+ }
826
+ const startNode = workflow.nodes.find(n => n.type === 'start');
827
+ if (!startNode)
828
+ throw new Error('Embedded workflow missing start node');
829
+ if (input && Object.keys(input).length > 0) {
830
+ this.setNodeExecutionData(session, startNode.id, input);
831
+ this.setNodeExecutionInput(session, startNode.id, input);
832
+ }
833
+ const visited = new Set([startNode.id]);
834
+ const execFrom = async (nodeId) => {
835
+ if (shouldInterrupt(session))
836
+ return undefined;
837
+ let lastResult;
838
+ for (const edge of adjacency.get(nodeId) || []) {
839
+ if (shouldInterrupt(session))
840
+ return lastResult;
841
+ const activeHandle = this.getActiveBranches(session).get(edge.source);
842
+ if (activeHandle !== undefined && edge.sourceHandle !== activeHandle)
843
+ continue;
844
+ const nextNode = nodeMap.get(edge.target);
845
+ if (!nextNode || visited.has(nextNode.id))
846
+ continue;
847
+ visited.add(nextNode.id);
848
+ const result = await this.executeNode(session, nextNode);
849
+ if (result === 'interrupted' || shouldInterrupt(session))
850
+ return lastResult;
851
+ if (nextNode.type !== 'start')
852
+ lastResult = this.getNodeExecutionData(session, nextNode.id);
853
+ const downstream = await execFrom(nextNode.id);
854
+ if (downstream !== undefined)
855
+ lastResult = downstream;
856
+ }
857
+ return lastResult;
858
+ };
859
+ return execFrom(startNode.id);
860
+ }
861
+ // ---- Private: Condition evaluation ----
862
+ evaluateCondition(variable, value, operator) {
863
+ switch (operator) {
864
+ case 'equals': return variable == value;
865
+ case 'not_equals': return variable != value;
866
+ case 'greater_than': return Number(variable) > Number(value);
867
+ case 'less_than': return Number(variable) < Number(value);
868
+ case 'greater_than_or_equal': return Number(variable) >= Number(value);
869
+ case 'less_than_or_equal': return Number(variable) <= Number(value);
870
+ case 'contains': return String(variable).includes(String(value));
871
+ case 'not_contains': return !String(variable).includes(String(value));
872
+ case 'starts_with': return String(variable).startsWith(String(value));
873
+ case 'ends_with': return String(variable).endsWith(String(value));
874
+ case 'is_empty': return variable === '' || variable === null || variable === undefined;
875
+ case 'is_not_empty': return variable !== '' && variable !== null && variable !== undefined;
876
+ case 'is_true': return variable === true || variable === 'true' || variable === 1;
877
+ case 'is_false': return variable === false || variable === 'false' || variable === 0;
878
+ default: return false;
879
+ }
880
+ }
881
+ loadPluginConfigs(session) {
882
+ const pluginIds = this.getReferencedPluginIds(session);
883
+ const schemes = session.workflow.pluginConfigSchemes || {};
884
+ const config = {};
885
+ for (const pluginId of pluginIds) {
886
+ try {
887
+ const schemeName = schemes[pluginId];
888
+ config[pluginId] = schemeName
889
+ ? workflowStore.readPluginScheme(session.workflow.id, pluginId, schemeName)
890
+ : pluginService.getPluginConfig(pluginId);
891
+ }
892
+ catch {
893
+ config[pluginId] = pluginService.getPluginConfig(pluginId);
894
+ }
895
+ }
896
+ return config;
897
+ }
898
+ getReferencedPluginIds(session) {
899
+ const pluginIds = new Set(session.workflow.enabledPlugins || []);
900
+ const collect = (value) => {
901
+ if (typeof value === 'string') {
902
+ const matches = value.matchAll(/__config__\[(["'])([^"']+)\1\]/g);
903
+ for (const match of matches)
904
+ pluginIds.add(match[2]);
905
+ return;
906
+ }
907
+ if (Array.isArray(value)) {
908
+ value.forEach(collect);
909
+ return;
910
+ }
911
+ if (value && typeof value === 'object') {
912
+ Object.values(value).forEach(collect);
913
+ }
914
+ };
915
+ session.nodes.forEach(node => collect(node.data));
916
+ return [...pluginIds];
917
+ }
918
+ // ---- Private: Variable resolution ----
919
+ resolveContextVariables(session, data) {
920
+ return this.resolveValue(session, data);
921
+ }
922
+ resolveValue(session, value) {
923
+ if (typeof value === 'string')
924
+ return this.resolveStringValue(session, value);
925
+ if (Array.isArray(value))
926
+ return value.map(item => this.resolveValue(session, item));
927
+ if (value && typeof value === 'object') {
928
+ const resolved = {};
929
+ for (const [key, nested] of Object.entries(value)) {
930
+ resolved[key] = this.resolveValue(session, nested);
931
+ }
932
+ return resolved;
933
+ }
934
+ return value;
935
+ }
936
+ resolveStringValue(session, value) {
937
+ // Full match patterns (return raw value, not string)
938
+ const loopVarMatch = value.match(/^\s*\{\{\s*__loop__\.vars\.([^}]+?)\s*\}\}\s*$/);
939
+ if (loopVarMatch)
940
+ return this.getLoopVariableValue(session, loopVarMatch[1]) ?? '';
941
+ const loopMetaMatch = value.match(/^\s*\{\{\s*__loop__\.(index|count|item|isFirst|isLast)\s*\}\}\s*$/);
942
+ if (loopMetaMatch)
943
+ return this.getLoopMetaValue(session, loopMetaMatch[1]) ?? '';
944
+ const dataMatch = value.match(/^\s*\{\{\s*__data__\[(["'])([^"']+)\1\]\.([^}]+?)\s*\}\}\s*$/);
945
+ if (dataMatch) {
946
+ const data = this.getNodeExecutionData(session, dataMatch[2]);
947
+ if (data != null) {
948
+ const result = getNestedValue(data, dataMatch[3]);
949
+ if (result !== undefined)
950
+ return result;
951
+ }
952
+ return '';
953
+ }
954
+ const inputMatch = value.match(/^\s*\{\{\s*__inputs__\[(["'])([^"']+)\1\]\.([^}]+?)\s*\}\}\s*$/);
955
+ if (inputMatch) {
956
+ const inputData = this.getNodeExecutionInput(session, inputMatch[2]);
957
+ if (inputData != null) {
958
+ const result = getNestedValue(inputData, inputMatch[3]);
959
+ if (result !== undefined)
960
+ return result;
961
+ }
962
+ return '';
963
+ }
964
+ const configMatch = value.match(/^\s*\{\{\s*__config__\[(["'])([^"']+)\1\]\[(["'])([^"']+)\3\](?:\.(\w+(?:\.\w+)*))?\s*\}\}\s*$/);
965
+ if (configMatch) {
966
+ const pluginConfig = session.context.__config__?.[configMatch[2]];
967
+ if (pluginConfig != null) {
968
+ let raw = pluginConfig[configMatch[4]];
969
+ if (configMatch[5] && typeof raw === 'string') {
970
+ try {
971
+ raw = JSON.parse(raw);
972
+ }
973
+ catch { /* keep raw string */ }
974
+ }
975
+ const result = configMatch[5] ? getNestedValue(raw, configMatch[5]) : raw;
976
+ if (result !== undefined)
977
+ return result;
978
+ }
979
+ return '';
980
+ }
981
+ const ctxMatch = value.match(/^\s*\{\{\s*context\.([^}]+?)\s*\}\}\s*$/);
982
+ if (ctxMatch)
983
+ return getNestedValue(session.context, ctxMatch[1]) ?? '';
984
+ // Inline patterns (string replacement)
985
+ let text = value
986
+ .replace(/\{\{\s*__loop__\.vars\.([^}]+?)\s*\}\}/g, (_m, p) => String(this.getLoopVariableValue(session, p) ?? ''))
987
+ .replace(/\{\{\s*__loop__\.(index|count|item|isFirst|isLast)\s*\}\}/g, (_m, k) => String(this.getLoopMetaValue(session, k) ?? ''))
988
+ .replace(/\{\{\s*__data__\[(["'])([^"']+)\1\]\.([^}]+?)\s*\}\}/g, (_m, _q, nid, fp) => {
989
+ const d = this.getNodeExecutionData(session, nid);
990
+ return d == null ? '' : String(getNestedValue(d, fp) ?? '');
991
+ })
992
+ .replace(/\{\{\s*__inputs__\[(["'])([^"']+)\1\]\.([^}]+?)\s*\}\}/g, (_m, _q, nid, fp) => {
993
+ const d = this.getNodeExecutionInput(session, nid);
994
+ return d == null ? '' : String(getNestedValue(d, fp) ?? '');
995
+ })
996
+ .replace(/\{\{\s*__config__\[(["'])([^"']+)\1\]\[(["'])([^"']+)\3\](?:\.(\w+(?:\.\w+)*))?\s*\}\}/g, (_m, _pq, pluginId, _kq, key, dotPath) => {
997
+ const pluginConfig = session.context.__config__?.[pluginId];
998
+ if (pluginConfig == null)
999
+ return '';
1000
+ let raw = pluginConfig[key];
1001
+ if (dotPath && typeof raw === 'string') {
1002
+ try {
1003
+ raw = JSON.parse(raw);
1004
+ }
1005
+ catch { /* keep raw string */ }
1006
+ }
1007
+ return String((dotPath ? getNestedValue(raw, dotPath) : raw) ?? '');
1008
+ })
1009
+ .replace(/\{\{\s*context\.([^}]+?)\s*\}\}/g, (_m, p) => String(getNestedValue(session.context, p) ?? ''));
1010
+ return text;
1011
+ }
1012
+ // ---- Private: Build execution order ----
1013
+ buildExecutionOrder(nodes, edges) {
1014
+ const nodeMap = new Map(nodes.map(n => [n.id, n]));
1015
+ const inDegree = new Map(nodes.map(n => [n.id, 0]));
1016
+ for (const edge of edges) {
1017
+ inDegree.set(edge.target, (inDegree.get(edge.target) ?? 0) + 1);
1018
+ }
1019
+ const queue = [];
1020
+ for (const [id, deg] of inDegree) {
1021
+ if (deg === 0)
1022
+ queue.push(id);
1023
+ }
1024
+ const order = [];
1025
+ while (queue.length > 0) {
1026
+ const id = queue.shift();
1027
+ const node = nodeMap.get(id);
1028
+ if (node)
1029
+ order.push(node);
1030
+ for (const edge of edges) {
1031
+ if (edge.source !== id)
1032
+ continue;
1033
+ const deg = (inDegree.get(edge.target) ?? 1) - 1;
1034
+ inDegree.set(edge.target, deg);
1035
+ if (deg === 0)
1036
+ queue.push(edge.target);
1037
+ }
1038
+ }
1039
+ return order;
1040
+ }
1041
+ // ---- Private: Breakpoints ----
1042
+ shouldPauseAtBreakpoint(session, node, bp) {
1043
+ if (node.breakpoint !== bp)
1044
+ return false;
1045
+ return !session.breakpointBypassKeys.has(`${node.id}:${bp}`);
1046
+ }
1047
+ pauseAtBreakpoint(session, nextIndex, node, bp) {
1048
+ session.currentIndex = nextIndex;
1049
+ session.status = 'paused';
1050
+ session.pauseReason = bp === 'start' ? 'breakpoint-start' : 'breakpoint-end';
1051
+ session.pauseNodeId = node.id;
1052
+ session.pauseBreakpoint = bp;
1053
+ this.emitLog(session);
1054
+ this.emitEvent(session, 'workflow:paused', {
1055
+ executionId: session.id, workflowId: session.workflow.id,
1056
+ timestamp: Date.now(), status: 'paused',
1057
+ currentNodeId: node.id, reason: session.pauseReason,
1058
+ });
1059
+ }
1060
+ // ---- Private: Branch reachability ----
1061
+ isNodeReachable(session, nodeId, visited) {
1062
+ const seen = visited || new Set();
1063
+ if (seen.has(nodeId))
1064
+ return false;
1065
+ seen.add(nodeId);
1066
+ const incoming = session.edges.filter(e => e.target === nodeId);
1067
+ if (incoming.length === 0)
1068
+ return true;
1069
+ for (const edge of incoming) {
1070
+ const activeHandle = this.getActiveBranches(session).get(edge.source);
1071
+ if (activeHandle !== undefined && edge.sourceHandle !== activeHandle)
1072
+ continue;
1073
+ if (this.isNodeReachable(session, edge.source, seen))
1074
+ return true;
1075
+ }
1076
+ return false;
1077
+ }
1078
+ // ---- Private: Loop context ----
1079
+ getLoopFrame(session) {
1080
+ const workerFrame = this.loopWorkerState.getStore()?.frame;
1081
+ if (workerFrame)
1082
+ return workerFrame;
1083
+ return session.loopStack[session.loopStack.length - 1] || null;
1084
+ }
1085
+ getActiveBranches(session) {
1086
+ return this.loopWorkerState.getStore()?.branch ?? session.activeBranches;
1087
+ }
1088
+ runWithLoopWorkerState(session, frame, callback) {
1089
+ return this.loopWorkerState.run({ branch: new Map(session.activeBranches), data: {}, frame, inputs: {} }, callback);
1090
+ }
1091
+ syncLoopContext(session) {
1092
+ const frame = this.getLoopFrame(session);
1093
+ if (!frame) {
1094
+ delete session.context.__loop__;
1095
+ return;
1096
+ }
1097
+ session.context.__loop__ = {
1098
+ vars: frame.variables, index: frame.metadata.index, count: frame.metadata.count,
1099
+ item: frame.metadata.item, isFirst: frame.metadata.isFirst, isLast: frame.metadata.isLast,
1100
+ };
1101
+ }
1102
+ getRuntimeContext(session) {
1103
+ const frame = this.getLoopFrame(session);
1104
+ const ws = this.loopWorkerState.getStore();
1105
+ if (!frame && !ws)
1106
+ return session.context;
1107
+ return {
1108
+ ...session.context,
1109
+ ...(ws ? { __data__: { ...session.context.__data__, ...ws.data } } : {}),
1110
+ ...(ws ? { __inputs__: { ...session.context.__inputs__, ...ws.inputs } } : {}),
1111
+ ...(frame ? { __loop__: session.context.__loop__ } : {}),
1112
+ };
1113
+ }
1114
+ getLoopVariableValue(session, path) {
1115
+ const frame = this.getLoopFrame(session);
1116
+ if (!frame)
1117
+ return undefined;
1118
+ return getNestedValue(frame.variables, path);
1119
+ }
1120
+ getLoopMetaValue(session, key) {
1121
+ const frame = this.getLoopFrame(session);
1122
+ if (!frame)
1123
+ return undefined;
1124
+ return frame.metadata[key];
1125
+ }
1126
+ // ---- Private: Node execution data ----
1127
+ getNodeExecutionData(session, nodeId) {
1128
+ const frame = this.getLoopFrame(session);
1129
+ const ws = this.loopWorkerState.getStore();
1130
+ if (!frame)
1131
+ return session.context.__data__?.[nodeId];
1132
+ if (nodeId === frame.bodyAnchorId || nodeId === frame.loopNodeId) {
1133
+ return { $index: frame.metadata.index, $count: frame.metadata.count, $item: frame.metadata.item,
1134
+ $isFirst: frame.metadata.isFirst, $isLast: frame.metadata.isLast, ...frame.variables };
1135
+ }
1136
+ return ws?.data[nodeId] ?? session.context.__data__?.[nodeId] ?? frame.parentData?.[nodeId];
1137
+ }
1138
+ setNodeExecutionData(session, nodeId, value) {
1139
+ const ws = this.loopWorkerState.getStore();
1140
+ if (ws) {
1141
+ ws.data[nodeId] = value;
1142
+ return;
1143
+ }
1144
+ if (!session.context.__data__)
1145
+ session.context.__data__ = {};
1146
+ session.context.__data__[nodeId] = value;
1147
+ }
1148
+ getNodeExecutionInput(session, nodeId) {
1149
+ const ws = this.loopWorkerState.getStore();
1150
+ return ws?.inputs[nodeId] ?? session.context.__inputs__?.[nodeId];
1151
+ }
1152
+ setNodeExecutionInput(session, nodeId, value) {
1153
+ const ws = this.loopWorkerState.getStore();
1154
+ if (ws) {
1155
+ ws.inputs[nodeId] = value;
1156
+ return;
1157
+ }
1158
+ if (!session.context.__inputs__)
1159
+ session.context.__inputs__ = {};
1160
+ session.context.__inputs__[nodeId] = value;
1161
+ }
1162
+ // ---- Private: Output building ----
1163
+ getStepInput(node, data) {
1164
+ if (node.type === 'start' || node.type === 'end')
1165
+ return undefined;
1166
+ return data;
1167
+ }
1168
+ buildOutputObject(outputs) {
1169
+ if (!Array.isArray(outputs) || outputs.length === 0)
1170
+ return null;
1171
+ const result = {};
1172
+ for (const field of outputs) {
1173
+ if (!field.key)
1174
+ continue;
1175
+ result[field.key] = field.type === 'object'
1176
+ ? this.buildOutputObject(field.children) ?? {}
1177
+ : field.value ?? '';
1178
+ }
1179
+ return result;
1180
+ }
1181
+ recordSkippedStep(session, node, reason) {
1182
+ session.steps.push({
1183
+ nodeId: node.id, nodeLabel: node.label,
1184
+ startedAt: Date.now(), finishedAt: Date.now(),
1185
+ status: 'skipped', error: reason,
1186
+ });
1187
+ this.emitLog(session);
1188
+ }
1189
+ // ---- Private: Event emission ----
1190
+ currentContext(session) {
1191
+ return clone(session.context);
1192
+ }
1193
+ currentLog(session) {
1194
+ return {
1195
+ id: session.id, workflowId: session.workflow.id,
1196
+ startedAt: session.startedAt, finishedAt: session.finishedAt,
1197
+ status: session.status === 'running' ? 'running' : session.status === 'paused' ? 'paused' : session.status === 'completed' ? 'completed' : 'error',
1198
+ steps: clone(session.steps),
1199
+ snapshot: { nodes: clone(session.nodes), edges: clone(session.edges), groups: clone(session.groups || []) },
1200
+ };
1201
+ }
1202
+ emitEvent(session, channel, payload) {
1203
+ session.lastUpdatedAt = Date.now();
1204
+ session.eventSequence += 1;
1205
+ session.recentEvents.push({
1206
+ sequence: session.eventSequence, channel,
1207
+ payload: clone(payload),
1208
+ });
1209
+ if (session.recentEvents.length > MAX_RECENT_EVENTS) {
1210
+ session.recentEvents.splice(0, session.recentEvents.length - MAX_RECENT_EVENTS);
1211
+ }
1212
+ if (session.eventSink) {
1213
+ session.eventSink(channel, payload);
1214
+ }
1215
+ else {
1216
+ this.deps.emit(channel, payload);
1217
+ }
1218
+ }
1219
+ emitLog(session) {
1220
+ this.emitEvent(session, 'execution:log', {
1221
+ executionId: session.id, workflowId: session.workflow.id,
1222
+ timestamp: Date.now(), log: this.currentLog(session),
1223
+ });
1224
+ }
1225
+ emitContext(session) {
1226
+ this.emitEvent(session, 'execution:context', {
1227
+ executionId: session.id, workflowId: session.workflow.id,
1228
+ timestamp: Date.now(), context: this.currentContext(session),
1229
+ });
1230
+ }
1231
+ emitWorkflowError(session) {
1232
+ this.emitEvent(session, 'workflow:error', {
1233
+ executionId: session.id, workflowId: session.workflow.id,
1234
+ timestamp: Date.now(), status: 'error',
1235
+ error: createErrorShape('WORKFLOW_ERROR', session.lastErrorMessage || 'Workflow execution failed'),
1236
+ log: this.currentLog(session),
1237
+ });
1238
+ }
1239
+ // ---- Private: Persistence & recovery ----
1240
+ persistAndCleanup(session) {
1241
+ if (!session.persisted) {
1242
+ workflowStore.addExecutionLog(session.workflow.id, this.currentLog(session));
1243
+ session.persisted = true;
1244
+ }
1245
+ this.finishedRecoveries.set(session.id, {
1246
+ ownerClientId: session.ownerClientId,
1247
+ workflowId: session.workflow.id,
1248
+ recovery: this.createRecoveryState(session, false),
1249
+ expiresAt: Date.now() + FINISHED_RECOVERY_TTL_MS,
1250
+ });
1251
+ this.sessions.delete(session.id);
1252
+ this.pruneFinishedRecoveries();
1253
+ }
1254
+ createRecoveryState(session, active) {
1255
+ return {
1256
+ executionId: session.id, workflowId: session.workflow.id, status: session.status,
1257
+ currentNodeId: session.pauseNodeId || session.executionOrder[session.currentIndex]?.id,
1258
+ pauseReason: session.pauseReason, updatedAt: session.lastUpdatedAt, active,
1259
+ log: this.currentLog(session), context: this.currentContext(session),
1260
+ recentEvents: clone(session.recentEvents),
1261
+ };
1262
+ }
1263
+ findSession(ownerClientId, workflowId, executionId) {
1264
+ for (const s of this.sessions.values()) {
1265
+ if (s.ownerClientId !== ownerClientId || s.workflow.id !== workflowId)
1266
+ continue;
1267
+ if (executionId && s.id !== executionId)
1268
+ continue;
1269
+ return s;
1270
+ }
1271
+ return undefined;
1272
+ }
1273
+ findFinishedRecovery(ownerClientId, workflowId, executionId) {
1274
+ for (const r of this.finishedRecoveries.values()) {
1275
+ if (r.ownerClientId !== ownerClientId || r.workflowId !== workflowId)
1276
+ continue;
1277
+ if (executionId && r.recovery.executionId !== executionId)
1278
+ continue;
1279
+ return r;
1280
+ }
1281
+ return undefined;
1282
+ }
1283
+ pruneFinishedRecoveries() {
1284
+ const now = Date.now();
1285
+ for (const [id, r] of this.finishedRecoveries) {
1286
+ if (r.expiresAt <= now)
1287
+ this.finishedRecoveries.delete(id);
1288
+ }
1289
+ }
1290
+ getSession(executionId) {
1291
+ const session = this.sessions.get(executionId);
1292
+ if (!session)
1293
+ throw createErrorShape('NOT_FOUND', `Session not found: ${executionId}`);
1294
+ return session;
1295
+ }
1296
+ }
1297
+ // ---- Utility functions ----
1298
+ function clone(value) {
1299
+ return JSON.parse(JSON.stringify(value));
1300
+ }
1301
+ function sleep(ms) {
1302
+ return new Promise(resolve => setTimeout(resolve, ms));
1303
+ }
1304
+ function getNestedValue(obj, path) {
1305
+ const parts = path.split('.');
1306
+ let current = obj;
1307
+ for (const part of parts) {
1308
+ if (current == null)
1309
+ return undefined;
1310
+ current = current[part];
1311
+ }
1312
+ return current;
1313
+ }
1314
+ function shouldInterrupt(session) {
1315
+ return session.stopRequested || session.status === 'error';
1316
+ }
1317
+ // Composite node helpers (from workflow-composite.ts shared types)
1318
+ function getNodesForExecutionScope(nodes, scopeId) {
1319
+ // scopeId null = root nodes (no composite.parentId)
1320
+ // scopeId non-null = nodes whose composite.parentId matches
1321
+ if (scopeId === null) {
1322
+ return nodes.filter(n => !n.composite?.parentId);
1323
+ }
1324
+ return nodes.filter(n => n.composite?.parentId === scopeId);
1325
+ }
1326
+ function findCompositeChildByRole(nodes, parentId, role) {
1327
+ return nodes.find(n => n.composite?.parentId === parentId && n.composite?.role === role);
1328
+ }
1329
+ function getCompositeParentId(node) {
1330
+ return node.composite?.parentId ?? undefined;
1331
+ }
1332
+ function isGeneratedWorkflowNode(node) {
1333
+ return !!node.data?._generated;
1334
+ }
1335
+ function normalizeEmbeddedWorkflow(data, genId) {
1336
+ // Simplified: just return as-is if already normalized
1337
+ if (data.nodes && data.edges)
1338
+ return data;
1339
+ return { nodes: [], edges: [] };
1340
+ }
1341
+ function normalizeLoopResult(result) {
1342
+ if (result && typeof result === 'object' && !Array.isArray(result))
1343
+ return result;
1344
+ return { result };
1345
+ }
1346
+ //# sourceMappingURL=execution-manager.js.map