@desplega.ai/agent-swarm 2.4.5 → 2.5.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 (272) hide show
  1. package/README.md +4 -2
  2. package/dist/{acp-adapter-2wdkktv6.js → acp-adapter-11gp67nr.js} +8 -6
  3. package/dist/{actions-c40ebcm4.js → actions-8p2kze8j.js} +23 -21
  4. package/dist/{amp-adapter-wb67m95z.js → amp-adapter-z4qvhacj.js} +2 -2
  5. package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
  6. package/dist/{app-353vre8g.js → app-0qp0a2gk.js} +3 -3
  7. package/dist/{assistant-sj1njh23.js → assistant-jedd3ktm.js} +24 -22
  8. package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
  9. package/dist/{boot-reembed-qdkvxeem.js → boot-reembed-4xwq5tg0.js} +1 -1
  10. package/dist/{boot-reembed-b9c3az3c.js → boot-reembed-n4wq5dd2.js} +1 -1
  11. package/dist/{boot-scrub-sweep-m3wh8eks.js → boot-scrub-sweep-69h90xcq.js} +4 -4
  12. package/dist/{boot-seeds-j2273s54.js → boot-seeds-3vn0m9sg.js} +4 -4
  13. package/dist/{claude-adapter-m7q0a085.js → claude-adapter-221mvad5.js} +8 -6
  14. package/dist/{claude-managed-adapter-5ptb6tzs.js → claude-managed-adapter-gt6pa35d.js} +2 -2
  15. package/dist/{claude-managed-setup-smrqk5j5.js → claude-managed-setup-c3hp36ph.js} +4 -4
  16. package/dist/{claude-sdk-session-38xnz21e.js → claude-sdk-session-8thjsk5e.js} +9012 -8928
  17. package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
  18. package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
  19. package/dist/{cli-dk44dxec.js → cli-0y6f0ckq.js} +1 -1
  20. package/dist/{cli-kpw04va0.js → cli-0z3vjrpm.js} +1 -1
  21. package/dist/{cli-94gwyg7m.js → cli-179xcyky.js} +9 -9
  22. package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
  23. package/dist/{cli-4zfqgjc5.js → cli-1pr672hz.js} +2 -2
  24. package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
  25. package/dist/{cli-43x64hv3.js → cli-36j4jnk3.js} +4 -4
  26. package/dist/{cli-k8g16gz3.js → cli-3qv8naq8.js} +115 -119
  27. package/dist/cli-3zwye9sn.js +1193 -0
  28. package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
  29. package/dist/{cli-v8fjakwf.js → cli-4w24f7z3.js} +1 -1
  30. package/dist/{cli-n72rg53p.js → cli-5kzk3472.js} +812 -418
  31. package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
  32. package/dist/{cli-j99azg3g.js → cli-5rvg1ddk.js} +5 -5
  33. package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
  34. package/dist/{cli-848hxdwj.js → cli-6rg2jn2y.js} +2 -1
  35. package/dist/{cli-fv5hepyy.js → cli-7bwtc6yx.js} +3 -3
  36. package/dist/{cli-ms85309d.js → cli-7g40g8hq.js} +11 -11
  37. package/dist/{cli-xpa64rek.js → cli-92mqenkc.js} +2362 -159
  38. package/dist/{cli-018r4jq6.js → cli-9ce2xw98.js} +8 -8
  39. package/dist/{cli-1syn2xy1.js → cli-bgdtv62r.js} +32 -18
  40. package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
  41. package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
  42. package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
  43. package/dist/{cli-wt0jf9hm.js → cli-fcc5qe3f.js} +553 -193
  44. package/dist/{cli-ftc1g5tj.js → cli-fes51ya2.js} +3 -3
  45. package/dist/{cli-3a45zcgm.js → cli-ftmq4nvk.js} +2 -2
  46. package/dist/{cli-37j04k05.js → cli-fy1jyyym.js} +13 -13
  47. package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
  48. package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
  49. package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
  50. package/dist/{cli-g35m2xwm.js → cli-j1eghqfm.js} +2 -2
  51. package/dist/{cli-qfxc84gh.js → cli-jpxsrxcb.js} +1 -1
  52. package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
  53. package/dist/{cli-p14m2cnp.js → cli-mbfj11fq.js} +1 -1
  54. package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
  55. package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
  56. package/dist/{cli-f01h25yt.js → cli-n6kmqngw.js} +2 -2
  57. package/dist/{cli-v0yrfmdh.js → cli-nnvjb17s.js} +3 -3
  58. package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
  59. package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
  60. package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
  61. package/dist/cli-pkaqk57w.js +128 -0
  62. package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
  63. package/dist/{cli-9yqgm9nd.js → cli-r25qpaxt.js} +161 -63
  64. package/dist/{cli-eww120g8.js → cli-rh5smt8t.js} +18 -4
  65. package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
  66. package/dist/{cli-a92r1127.js → cli-s4sntwt4.js} +2 -2
  67. package/dist/{cli-be2161h1.js → cli-segs7hx9.js} +6 -6
  68. package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
  69. package/dist/{cli-z7d29rw9.js → cli-sy8f9705.js} +4 -4
  70. package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
  71. package/dist/{cli-st8aw1m6.js → cli-we0ra057.js} +4 -4
  72. package/dist/{cli-7c9qs530.js → cli-x21dqn18.js} +5 -5
  73. package/dist/{cli-1sp861s4.js → cli-x6vf4sad.js} +11 -11
  74. package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
  75. package/dist/{cli-tw4p90jb.js → cli-xawhpgc9.js} +5 -5
  76. package/dist/{cli-f0g3pgvf.js → cli-xd4eqtr2.js} +2 -2
  77. package/dist/{cli-1f3qzw6y.js → cli-xt768r5m.js} +2 -2
  78. package/dist/{cli-cxxe61f6.js → cli-y4x8ww6s.js} +2 -2
  79. package/dist/{cli-bnes4z6b.js → cli-y9jvxfhk.js} +3 -3
  80. package/dist/{cli-qwhy2qma.js → cli-z63a269p.js} +2 -2
  81. package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
  82. package/dist/cli.js +29 -27
  83. package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
  84. package/dist/{codex-adapter-tn6k5mkg.js → codex-adapter-tcg1q3jk.js} +6 -4
  85. package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
  86. package/dist/{codex-session-runner-7vjt7jbw.js → codex-session-runner-xfx4wd9x.js} +7 -5
  87. package/dist/{commands-t7rz9146.js → commands-ew83nj70.js} +2 -2
  88. package/dist/{controller-m2jasma5.js → controller-gnq1k1mf.js} +4 -4
  89. package/dist/{controllers-connect-9ftpex99.js → controllers-connect-tw1dan9r.js} +25 -23
  90. package/dist/{cursor-adapter-mjv79e4z.js → cursor-adapter-yq5xgfcg.js} +1 -1
  91. package/dist/{db-6sfrn9g3.js → db-7gbc53fd.js} +14 -2
  92. package/dist/{db-rn673r06.js → db-t86xrnes.js} +3 -3
  93. package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
  94. package/dist/{dispatcher-c155xz0p.js → dispatcher-64bdyhps.js} +22 -20
  95. package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
  96. package/dist/{dsh-adapter-3mmszc1x.js → dsh-adapter-tvhc4k2d.js} +1 -1
  97. package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
  98. package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
  99. package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
  100. package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
  101. package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
  102. package/dist/{external-channel-35k6tkv8.js → external-channel-e9nw24zv.js} +2 -2
  103. package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
  104. package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
  105. package/dist/{grok-adapter-tfrq7tg1.js → grok-adapter-v82srhrx.js} +9 -7
  106. package/dist/handlers-c9ajzhbn.js +41 -0
  107. package/dist/{heartbeat-dyg4npkp.js → heartbeat-2vqs1319.js} +25 -23
  108. package/dist/{hook-x26b31bg.js → hook-01nk13wx.js} +7 -5
  109. package/dist/{hook-drspbwpe.js → hook-jzpheegg.js} +9 -7
  110. package/dist/{http-pk972xyw.js → http-fc1k6ec8.js} +544 -76
  111. package/dist/index-07ef7nkm.js +42 -0
  112. package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
  113. package/dist/index-7yts8n9s.js +43 -0
  114. package/dist/index-q3t40ryv.js +46 -0
  115. package/dist/{index-arytgj5q.js → index-sepja052.js} +1 -1
  116. package/dist/{index-jwxkkex2.js → index-zdycv3ja.js} +13 -11
  117. package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
  118. package/dist/{keepalive-7yrng7px.js → keepalive-5nh2rhmr.js} +4 -4
  119. package/dist/lead-aayq64jn.js +79 -0
  120. package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
  121. package/dist/{maintenance-e6cemdqt.js → maintenance-hjhzdt3k.js} +1 -1
  122. package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
  123. package/dist/{node-de6akczr.js → node-jzjj7r9x.js} +1 -1
  124. package/dist/{oauth-refresh-sweep-yw3xdbcj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
  125. package/dist/{onboard-0a9y5yrc.js → onboard-1zq3m7db.js} +2 -2
  126. package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
  127. package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
  128. package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
  129. package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
  130. package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
  131. package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
  132. package/dist/{otel-impl-trjpkfb7.js → otel-impl-h9yyza47.js} +2 -2
  133. package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
  134. package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
  135. package/dist/{pricing-refresh-d2b5my0y.js → pricing-refresh-nevhat8h.js} +3 -3
  136. package/dist/{rbac-roles-nxh5ahwk.js → rbac-roles-4y6ngvzq.js} +1 -1
  137. package/dist/{rbac-roles-hey3yv9d.js → rbac-roles-exbg8xb6.js} +2 -2
  138. package/dist/{render-v2-rr3p2atg.js → render-v2-cfsh13z3.js} +3 -3
  139. package/dist/{resume-2efkf1s1.js → resume-5s2t0dbs.js} +10 -8
  140. package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
  141. package/dist/{session-access-s2mttxpw.js → session-access-dnbyp3cn.js} +6 -4
  142. package/dist/{setup-vypma5yt.js → setup-1wj1y77j.js} +1 -1
  143. package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
  144. package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
  145. package/dist/{work-objects-rvhckdkp.js → work-objects-7mg6216g.js} +25 -23
  146. package/dist/worker-nwb789mg.js +79 -0
  147. package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
  148. package/openapi.json +785 -5
  149. package/package.json +9 -9
  150. package/src/assets/key.ts +17 -0
  151. package/src/azure-devops/handlers.ts +1 -1
  152. package/src/be/db/approvals.ts +18 -0
  153. package/src/be/db/session-moves.ts +164 -0
  154. package/src/be/db/session-tree.ts +384 -0
  155. package/src/be/db/tasks/read.ts +40 -4
  156. package/src/be/db/workflow-runs.ts +96 -0
  157. package/src/be/db.ts +80 -236
  158. package/src/be/onboarding.ts +3 -0
  159. package/src/be/skill-sync.ts +43 -2
  160. package/src/be/swarm-config-guard.ts +1 -0
  161. package/src/bitbucket/api.ts +81 -0
  162. package/src/bitbucket/auth.ts +64 -0
  163. package/src/bitbucket/handlers.ts +434 -0
  164. package/src/bitbucket/index.ts +28 -0
  165. package/src/bitbucket/templates.ts +134 -0
  166. package/src/bitbucket/types.ts +65 -0
  167. package/src/commands/runner.ts +55 -3
  168. package/src/e2b/env.ts +1 -0
  169. package/src/extensions/contract-types.generated.ts +1 -1
  170. package/src/extensions/contract.ts +2 -2
  171. package/src/extensions/post-bridge.ts +1 -0
  172. package/src/github/handlers.ts +6 -6
  173. package/src/gitlab/handlers.ts +5 -5
  174. package/src/http/assets.ts +14 -2
  175. package/src/http/core.ts +12 -0
  176. package/src/http/index.ts +4 -0
  177. package/src/http/onboarding.ts +1 -0
  178. package/src/http/sessions.ts +366 -12
  179. package/src/http/skills.ts +48 -1
  180. package/src/http/tasks.ts +1 -1
  181. package/src/http/users.ts +1 -0
  182. package/src/http/webhooks.ts +141 -0
  183. package/src/integrations/chat-sdk/adapters.ts +19 -3
  184. package/src/integrations/chat-sdk/facts.ts +30 -12
  185. package/src/integrations/chat-sdk/outbound.ts +38 -1
  186. package/src/providers/acp-targets.ts +1 -1
  187. package/src/providers/amp-adapter.ts +1 -1
  188. package/src/providers/pi-mono-adapter.ts +5 -53
  189. package/src/scripts-runtime/types/swarm-extension.d.ts +6 -4
  190. package/src/slack/handlers.test.ts +158 -0
  191. package/src/slack/handlers.ts +19 -28
  192. package/src/tasks/context-key.ts +61 -1
  193. package/src/tests/bitbucket-auth.test.ts +156 -0
  194. package/src/tests/bitbucket-handlers.test.ts +425 -0
  195. package/src/tests/bitbucket-webhook.test.ts +220 -0
  196. package/src/tests/chat-sdk-inbound.test.ts +39 -0
  197. package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
  198. package/src/tests/context-key.test.ts +50 -0
  199. package/src/tests/core-auth.test.ts +5 -0
  200. package/src/tests/db-task-read-contract.test.ts +7 -0
  201. package/src/tests/db-task-write-contract.test.ts +6 -0
  202. package/src/tests/docs-index-footprint.test.js +3 -3
  203. package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
  204. package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
  205. package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
  206. package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
  207. package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
  208. package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
  209. package/src/tests/generate-mcp-docs.test.ts +101 -0
  210. package/src/tests/gitlab-handlers.test.ts +3 -3
  211. package/src/tests/gitlab-vcs-db.test.ts +4 -4
  212. package/src/tests/integration-disable-flags.test.ts +36 -0
  213. package/src/tests/mcp-tools.test.ts +1 -1
  214. package/src/tests/onboarding.test.ts +12 -0
  215. package/src/tests/pi-codemode.test.ts +8 -2
  216. package/src/tests/preload-env.test.ts +57 -0
  217. package/src/tests/preload.ts +21 -0
  218. package/src/tests/prompt-template-remaining.test.ts +38 -0
  219. package/src/tests/run-root-coverage.test.ts +39 -0
  220. package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
  221. package/src/tests/runner-skills-refresh.test.ts +58 -0
  222. package/src/tests/secret-scrubber.test.ts +15 -0
  223. package/src/tests/sessions-move.test.ts +576 -0
  224. package/src/tests/sessions-tree.test.ts +493 -0
  225. package/src/tests/skill-files-http.test.ts +50 -0
  226. package/src/tests/skills-bundle.test.ts +99 -0
  227. package/src/tests/swarm-config-export-to-env.test.ts +2 -1
  228. package/src/tests/vcs-bitbucket.test.ts +91 -0
  229. package/src/tests/vcs-provider-isolation.test.ts +123 -0
  230. package/src/tests/vcs-provider.test.ts +16 -1
  231. package/src/tests/vcs-tracking.test.ts +3 -3
  232. package/src/tests/workflow-foreach-bodies.test.ts +637 -0
  233. package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
  234. package/src/tests/workflow-foreach-retry.test.ts +537 -0
  235. package/src/tests/workflow-foreach.test.ts +44 -23
  236. package/src/tests/workflow-step-claims.test.ts +6 -6
  237. package/src/tests/workflow-tla-races.test.ts +2 -2
  238. package/src/tools/script-delete.ts +1 -1
  239. package/src/tools/workflows/create-workflow.ts +5 -3
  240. package/src/types.ts +3 -2
  241. package/src/utils/config-env-export.ts +1 -0
  242. package/src/utils/secret-scrubber.ts +4 -0
  243. package/src/utils/skills-refresh.ts +83 -43
  244. package/src/vcs/bitbucket.ts +58 -0
  245. package/src/vcs/index.ts +2 -0
  246. package/src/vcs/types.ts +2 -2
  247. package/src/workflows/approval-resolution.ts +4 -1
  248. package/src/workflows/checkpoint.ts +9 -1
  249. package/src/workflows/constants.ts +3 -0
  250. package/src/workflows/definition.ts +48 -17
  251. package/src/workflows/engine.ts +25 -15
  252. package/src/workflows/executors/foreach.ts +45 -99
  253. package/src/workflows/foreach-body.ts +146 -0
  254. package/src/workflows/foreach-driver.ts +552 -0
  255. package/src/workflows/foreach-join.ts +206 -7
  256. package/src/workflows/node-timeout.ts +21 -0
  257. package/src/workflows/recovery.ts +68 -3
  258. package/src/workflows/resume.ts +221 -43
  259. package/src/workflows/retry-poller.ts +39 -24
  260. package/src/workflows/step-input.ts +59 -0
  261. package/src/workflows/task-step-routing.ts +114 -56
  262. package/templates/skills/slack-interaction/content.md +5 -0
  263. package/templates/skills/work-on-task/SKILL.md +1 -1
  264. package/templates/skills/work-on-task/content.md +1 -1
  265. package/dist/cli-9hstj09h.js +0 -2657
  266. package/dist/cli-yw91rnmw.js +0 -13
  267. package/dist/handlers-7gw64dxw.js +0 -39
  268. package/dist/index-bgdyvcnv.js +0 -40
  269. package/dist/index-gabbcdn2.js +0 -43
  270. package/dist/index-tfb26zhy.js +0 -41
  271. package/dist/lead-7e02z839.js +0 -77
  272. package/dist/worker-5snytpbd.js +0 -77
@@ -1,19 +1,22 @@
1
1
  import { z } from "zod";
2
+ import { createForeachChildSteps } from "../../be/db/workflow-runs";
2
3
  import type { ExecutorMeta } from "../../types";
3
- import { deepInterpolate } from "../../utils/template";
4
+ import { FOREACH_ITEMS_WAIT_FOR } from "../constants";
4
5
  import { getSuccessors } from "../definition";
6
+ import { foreachBodyTypeError } from "../foreach-body";
5
7
  import {
6
8
  buildForeachAggregate,
7
9
  FOREACH_TERMINAL_STEP_STATUSES,
8
10
  parseSyntheticNodeId,
9
11
  } from "../foreach-join";
10
12
  import { getMaxWorkflowStepsPerRun } from "../limits";
11
- import { AgentTaskExecutor } from "./agent-task";
12
13
  import type { AsyncExecutorResult, ExecutorResult } from "./base";
13
14
  import { BaseExecutor } from "./base";
14
15
 
16
+ // Any registered executor except `foreach` (see foreach-body.ts); the guard in
17
+ // execute() and definition validation enforce the policy.
15
18
  const ForeachBodySchema = z.object({
16
- type: z.literal("agent-task"),
19
+ type: z.string().min(1),
17
20
  config: z.record(z.string(), z.unknown()),
18
21
  });
19
22
 
@@ -21,7 +24,12 @@ export const ForeachConfigSchema = z.object({
21
24
  over: z.array(z.record(z.string(), z.unknown())),
22
25
  itemKey: z.string().min(1),
23
26
  body: ForeachBodySchema,
24
- concurrency: z.never().optional(),
27
+ /** Items in flight at once (`running` or `waiting` children). Unbounded when absent. */
28
+ concurrency: z.number().int().min(1).optional(),
29
+ /** Defaults from the workflow `onNodeFailure` (see effectiveFailurePolicy). */
30
+ // Literal, not FOREACH_FAILURE_POLICIES: foreach-body.ts sits in an import
31
+ // cycle with this module and would be read before it initializes.
32
+ failurePolicy: z.enum(["fail-fast", "collect-outcomes"]).optional(),
25
33
  });
26
34
 
27
35
  const ForeachResultSchema = z.object({
@@ -34,6 +42,8 @@ export const ForeachOutputSchema = z.object({
34
42
  results: z.array(ForeachResultSchema),
35
43
  okCount: z.number().int().nonnegative(),
36
44
  failedCount: z.number().int().nonnegative(),
45
+ /** Items cancelled before they finished (fail-fast queued items, cancelled bodies). Counted in failedCount too. */
46
+ cancelledCount: z.number().int().nonnegative().optional(),
37
47
  });
38
48
 
39
49
  export type ForeachOutput = z.infer<typeof ForeachOutputSchema>;
@@ -49,7 +59,7 @@ export class ForeachExecutor extends BaseExecutor<
49
59
 
50
60
  protected async execute(
51
61
  config: z.infer<typeof ForeachConfigSchema>,
52
- context: Readonly<Record<string, unknown>>,
62
+ _context: Readonly<Record<string, unknown>>,
53
63
  meta: ExecutorMeta,
54
64
  ): Promise<ExecutorResult<ForeachOutput>> {
55
65
  if (config.over.length === 0) {
@@ -77,16 +87,19 @@ export class ForeachExecutor extends BaseExecutor<
77
87
  seenItemKeys.add(itemKey);
78
88
  }
79
89
 
80
- const agentTaskExecutor = new AgentTaskExecutor(this.deps);
90
+ const bodyTypeError = foreachBodyTypeError(meta.nodeId, config.body.type);
91
+ if (bodyTypeError) return { status: "failed", error: bodyTypeError };
92
+
81
93
  const runSteps = await this.deps.db.getWorkflowRunStepsByRunId(meta.runId);
82
- const childStepsByNodeId = new Map(
94
+ const existingChildNodeIds = new Set(
83
95
  runSteps
84
96
  .filter((step) => parseSyntheticNodeId(step.nodeId)?.parentNodeId === meta.nodeId)
85
- .map((step) => [step.nodeId, step]),
97
+ .map((step) => step.nodeId),
98
+ );
99
+ const missingItems = items.filter(
100
+ ({ itemKey }) => !existingChildNodeIds.has(`${meta.nodeId}#${itemKey}`),
86
101
  );
87
- const childCountToCreate = items.filter(
88
- ({ itemKey }) => !childStepsByNodeId.has(`${meta.nodeId}#${itemKey}`),
89
- ).length;
102
+ const childCountToCreate = missingItems.length;
90
103
  const maxSteps = getMaxWorkflowStepsPerRun();
91
104
  // walkGraph's circuit breaker rejects any post-join walk once allSteps.length
92
105
  // >= maxSteps, so a fan-out with a successor that lands exactly on the cap would
@@ -107,79 +120,26 @@ export class ForeachExecutor extends BaseExecutor<
107
120
  };
108
121
  }
109
122
 
110
- // Materialize EVERY child step row before dispatching ANY child task. A task
111
- // dispatched for an early child can complete (and trigger joinForeach) while
112
- // later children are still being set up — the join must always see the full
113
- // child set, with not-yet-dispatched children in a non-terminal state.
114
- for (const { index, itemKey } of items) {
115
- const childNodeId = `${meta.nodeId}#${itemKey}`;
116
- if (childStepsByNodeId.has(childNodeId)) continue;
117
- const childStepId = crypto.randomUUID();
118
- const childStep = await this.deps.db.createWorkflowRunStep({
119
- id: childStepId,
120
- runId: meta.runId,
121
- nodeId: childNodeId,
122
- nodeType: "agent-task",
123
- input: { itemKey, index },
124
- });
125
- await this.deps.db.updateWorkflowRunStep(childStepId, {
126
- idempotencyKey: `${meta.runId}:${childNodeId}:0`,
127
- });
128
- childStepsByNodeId.set(childNodeId, childStep);
129
- }
130
-
131
- for (const { item, index, itemKey } of items) {
132
- const childNodeId = `${meta.nodeId}#${itemKey}`;
133
- const childStep = childStepsByNodeId.get(childNodeId)!;
134
-
135
- // Re-walks must not regress terminal children or disturb their aggregate
136
- // inputs. Retry resets only the failed child to pending before arriving here.
137
- if (FOREACH_TERMINAL_STEP_STATUSES.has(childStep.status)) continue;
138
-
139
- // The deferred per-item pass interpolates against the node's declared-inputs
140
- // context (meta.inputCtx) plus item/index — NOT the full run context — so
141
- // body.config obeys the same explicit-dataflow boundary as normal node
142
- // config instead of silently reading undeclared upstream outputs.
143
- const bodyCtx = { ...(meta.inputCtx ?? context), item, index };
144
- const childInterpolation = deepInterpolate(config.body.config, bodyCtx);
145
- if (childInterpolation.unresolved.length > 0) {
146
- console.warn(
147
- `[workflow] Step ${childNodeId}: unresolved interpolation tokens: ${childInterpolation.unresolved.join(", ")}`,
148
- );
149
- await this.deps.db.updateWorkflowRunStep(childStep.id, {
150
- diagnostics: JSON.stringify({ unresolvedTokens: childInterpolation.unresolved }),
151
- });
152
- }
153
-
154
- const linkedTask = await this.deps.db.getTaskByWorkflowRunStepId(childStep.id);
155
- if (linkedTask?.status === "failed" || linkedTask?.status === "cancelled") {
156
- // A retry needs a fresh task. Detaching the stale terminal task keeps the
157
- // executor's one-task-per-step de-duplication deterministic and scoped.
158
- await this.deps.db.detachTaskFromWorkflowRunStep(linkedTask.id);
159
- }
160
-
161
- const childResult = await agentTaskExecutor.run({
162
- config: childInterpolation.value as Record<string, unknown>,
163
- context,
164
- meta: {
165
- ...meta,
166
- stepId: childStep.id,
123
+ // Materialize EVERY child row, queued (`pending`), in one transaction and
124
+ // before any child starts. An early child can complete (and trigger the
125
+ // join) while later children are still being set up, so the join must
126
+ // always see the full child set. The item value is not persisted: the
127
+ // driver looks it up by key in the re-interpolated `over`, which keeps
128
+ // resolved secrets out of step rows.
129
+ await createForeachChildSteps(
130
+ meta.runId,
131
+ meta.nodeId,
132
+ missingItems.map(({ index, itemKey }) => {
133
+ const childNodeId = `${meta.nodeId}#${itemKey}`;
134
+ return {
135
+ id: crypto.randomUUID(),
167
136
  nodeId: childNodeId,
168
- },
169
- });
170
- if (childResult.status !== "success") {
171
- throw new Error(childResult.error ?? `foreach child "${itemKey}" failed to start`);
172
- }
173
- if ("async" in childResult) {
174
- await this.deps.db.updateWorkflowRunStep(childStep.id, { status: "waiting" });
175
- } else {
176
- await this.deps.db.updateWorkflowRunStep(childStep.id, {
177
- status: "completed",
178
- output: normalizeExistingTaskOutput(childResult.output),
179
- finishedAt: new Date().toISOString(),
180
- });
181
- }
182
- }
137
+ nodeType: config.body.type,
138
+ input: { itemKey, index, attempt: 0 },
139
+ idempotencyKey: `${meta.runId}:${childNodeId}:0`,
140
+ };
141
+ }),
142
+ );
183
143
 
184
144
  const childSteps = (await this.deps.db.getWorkflowRunStepsByRunId(meta.runId)).filter(
185
145
  (step) => parseSyntheticNodeId(step.nodeId)?.parentNodeId === meta.nodeId,
@@ -188,26 +148,12 @@ export class ForeachExecutor extends BaseExecutor<
188
148
  return { status: "success", output: buildForeachAggregate(childSteps) };
189
149
  }
190
150
 
151
+ // The engine parks the parent and the foreach driver starts the children.
191
152
  return {
192
153
  status: "success",
193
154
  async: true,
194
- waitFor: "task.completed",
155
+ waitFor: FOREACH_ITEMS_WAIT_FOR,
195
156
  correlationId: meta.stepId,
196
157
  } as AsyncExecutorResult<ForeachOutput>;
197
158
  }
198
159
  }
199
-
200
- function normalizeExistingTaskOutput(output: unknown): unknown {
201
- if (typeof output !== "object" || output === null) return output;
202
- const record = output as Record<string, unknown>;
203
- if (typeof record.taskOutput !== "string") return output;
204
- try {
205
- const parsed = JSON.parse(record.taskOutput);
206
- if (typeof parsed === "object" && parsed !== null) {
207
- return { ...record, taskOutput: parsed };
208
- }
209
- } catch {
210
- // Non-structured task output remains a string.
211
- }
212
- return output;
213
- }
@@ -0,0 +1,146 @@
1
+ import type { WorkflowDefinition, WorkflowNode } from "../types";
2
+ import type { ExecutorRegistry } from "./executors/registry";
3
+ import { resolveForeachParent } from "./foreach-join";
4
+
5
+ /**
6
+ * Executor types a `foreach` body must not use, with the reason. Every other
7
+ * registered executor is a valid body.
8
+ */
9
+ export const FOREACH_BODY_REJECTED: Readonly<Record<string, string>> = {
10
+ foreach: "nested fan-out is not supported: synthetic ids and the join resolve one level only",
11
+ };
12
+
13
+ /**
14
+ * Built-in executors accepted as a `foreach` body, by how one item completes:
15
+ * `inline` items are run and settled by the foreach driver; `parks` items wait
16
+ * on their own resume path (task, child run, approval, wait), which settles
17
+ * the item through `settleForeachChild`. A `system-one-decision` without
18
+ * `humanReview` returns inline.
19
+ *
20
+ * Every built-in executor must appear here or in FOREACH_BODY_REJECTED; the
21
+ * registry exhaustiveness test enforces that a new executor gets a decision.
22
+ */
23
+ export const FOREACH_BODY_ACCEPTED: Readonly<Record<string, "inline" | "parks">> = {
24
+ script: "inline",
25
+ "swarm-script": "inline",
26
+ "raw-llm": "inline",
27
+ notify: "inline",
28
+ "property-match": "inline",
29
+ "code-match": "inline",
30
+ validate: "inline",
31
+ vcs: "inline",
32
+ "system-one-extraction": "inline",
33
+ "agent-task": "parks",
34
+ "sub-workflow": "parks",
35
+ "human-in-the-loop": "parks",
36
+ wait: "parks",
37
+ "system-one-decision": "parks",
38
+ };
39
+
40
+ /**
41
+ * True when `type` may be a foreach body: not rejected, and registered (or a
42
+ * known built-in when no registry is passed).
43
+ */
44
+ export function isForeachBodyType(type: string, registry?: ExecutorRegistry): boolean {
45
+ if (Object.hasOwn(FOREACH_BODY_REJECTED, type)) return false;
46
+ return registry ? registry.has(type) : Object.hasOwn(FOREACH_BODY_ACCEPTED, type);
47
+ }
48
+
49
+ /**
50
+ * The validation error for a foreach body type, or null when it is allowed.
51
+ * Without a registry only the rejection list applies, so a custom executor
52
+ * registered at run time is not refused by a registry-less validation pass.
53
+ */
54
+ export function foreachBodyTypeError(
55
+ nodeId: string,
56
+ type: string,
57
+ registry?: ExecutorRegistry,
58
+ ): string | null {
59
+ const reason = FOREACH_BODY_REJECTED[type];
60
+ if (reason !== undefined) {
61
+ return `Node "${nodeId}" foreach config.body.type "${type}" is not supported: ${reason} (a ${type} body inside foreach "${nodeId}" is rejected)`;
62
+ }
63
+ if (registry && !registry.has(type)) {
64
+ return `Node "${nodeId}" foreach config.body.type "${type}" is not a registered executor`;
65
+ }
66
+ return null;
67
+ }
68
+
69
+ /**
70
+ * The definition node a step row executes. A real node wins. A synthetic
71
+ * foreach child id (`<foreachId>#<itemKey>`) resolves to a node synthesized
72
+ * from the parent's body: the child's id, the body's type and config, and the
73
+ * parent's declared `inputs` (the body sees the parent's input context, and the
74
+ * script-body allowlist classifies those aliases by their source).
75
+ */
76
+ export function resolveStepNode(
77
+ def: Pick<WorkflowDefinition, "nodes">,
78
+ nodeId: string,
79
+ ): WorkflowNode | null {
80
+ const node = def.nodes.find((candidate) => candidate.id === nodeId);
81
+ if (node) return node;
82
+ const parent = resolveForeachParent(def, nodeId);
83
+ if (!parent) return null;
84
+ const body = parent.config.body;
85
+ if (!isRecord(body) || typeof body.type !== "string") return null;
86
+ // `item` and `index` are bound per item, so a parent alias with either name
87
+ // is dropped: it would otherwise classify item data by the alias's source
88
+ // and let it past the script-body allowlist.
89
+ const inputs = parent.inputs
90
+ ? Object.fromEntries(
91
+ Object.entries(parent.inputs).filter(([alias]) => !FOREACH_ITEM_BINDINGS.has(alias)),
92
+ )
93
+ : undefined;
94
+ return {
95
+ id: nodeId,
96
+ type: body.type,
97
+ config: isRecord(body.config) ? body.config : {},
98
+ ...(inputs ? { inputs } : {}),
99
+ };
100
+ }
101
+
102
+ const FOREACH_ITEM_BINDINGS = new Set(["item", "index"]);
103
+
104
+ export const FOREACH_FAILURE_POLICIES = ["fail-fast", "collect-outcomes"] as const;
105
+ export type ForeachFailurePolicy = (typeof FOREACH_FAILURE_POLICIES)[number];
106
+
107
+ /**
108
+ * How a failed or cancelled item affects its fan-out. `config.failurePolicy`
109
+ * wins; otherwise it follows the workflow's `onNodeFailure`, so a definition
110
+ * without the field behaves as before: `"continue"` collects outcomes, the
111
+ * default `"fail"` is fail-fast.
112
+ */
113
+ export function effectiveFailurePolicy(
114
+ node: WorkflowNode,
115
+ def: Pick<WorkflowDefinition, "onNodeFailure">,
116
+ ): ForeachFailurePolicy {
117
+ const configured = node.config.failurePolicy;
118
+ if (configured === "fail-fast" || configured === "collect-outcomes") return configured;
119
+ return def.onNodeFailure === "continue" ? "collect-outcomes" : "fail-fast";
120
+ }
121
+
122
+ /** How many items of a fan-out may be in flight at once. Unbounded by default. */
123
+ export function foreachConcurrencyCap(node: WorkflowNode): number {
124
+ const cap = node.config.concurrency;
125
+ return typeof cap === "number" && Number.isInteger(cap) && cap >= 1
126
+ ? cap
127
+ : Number.POSITIVE_INFINITY;
128
+ }
129
+
130
+ /**
131
+ * The failure mode a foreach child's task failure takes: the fan-out's
132
+ * effective policy for a synthetic child, the workflow's `onNodeFailure`
133
+ * otherwise.
134
+ */
135
+ export function stepFailureMode(
136
+ def: Pick<WorkflowDefinition, "nodes" | "onNodeFailure">,
137
+ nodeId: string,
138
+ ): "fail" | "continue" {
139
+ const parent = resolveForeachParent(def, nodeId);
140
+ if (parent) return effectiveFailurePolicy(parent, def) === "fail-fast" ? "fail" : "continue";
141
+ return def.onNodeFailure ?? "fail";
142
+ }
143
+
144
+ function isRecord(value: unknown): value is Record<string, unknown> {
145
+ return typeof value === "object" && value !== null && !Array.isArray(value);
146
+ }