@desplega.ai/agent-swarm 2.7.4 → 2.7.5

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 (253) hide show
  1. package/README.md +1 -1
  2. package/dist/acp-adapter-g3ads55s.js +15 -0
  3. package/dist/{actions-gfrr9vb7.js → actions-zwfykqqk.js} +18 -18
  4. package/dist/{amp-adapter-0s58gm46.js → amp-adapter-z3s0ssdt.js} +3 -3
  5. package/dist/{app-stvq692g.js → app-vmp4fgep.js} +3 -3
  6. package/dist/{assistant-5s37fr9n.js → assistant-vwde653f.js} +28 -21
  7. package/dist/{boot-reembed-yzy1841n.js → boot-reembed-375t7tbk.js} +1 -1
  8. package/dist/{boot-reembed-cv82gz7y.js → boot-reembed-gq9rcpnj.js} +1 -1
  9. package/dist/{boot-scrub-sweep-8k4q95ap.js → boot-scrub-sweep-nd938kyf.js} +4 -4
  10. package/dist/{boot-seeds-ftt9htah.js → boot-seeds-aykx51v3.js} +4 -4
  11. package/dist/{boxd-515v1t8z.js → boxd-k1j26g3h.js} +1 -1
  12. package/dist/{claude-adapter-t00wrzeh.js → claude-adapter-8j4hm099.js} +7 -7
  13. package/dist/{claude-managed-adapter-g6vmjccb.js → claude-managed-adapter-dvs2xw5n.js} +2 -2
  14. package/dist/{claude-managed-setup-afpa82f7.js → claude-managed-setup-0wyynpn1.js} +3 -3
  15. package/dist/{claude-sdk-session-vj0c2azx.js → claude-sdk-session-q6wj5hmy.js} +9 -9
  16. package/dist/{cli-wndv9p7g.js → cli-0731hvbw.js} +6 -6
  17. package/dist/{cli-d2pzqej1.js → cli-0bxqnvq9.js} +719 -7
  18. package/dist/{cli-jkt52mgt.js → cli-0gey5fzy.js} +2 -2
  19. package/dist/{cli-1yekbyqm.js → cli-1338796d.js} +205 -48
  20. package/dist/{cli-1s1dqn1j.js → cli-1ayk1eas.js} +171 -123
  21. package/dist/{cli-a4btqqft.js → cli-1b7btd38.js} +1 -1
  22. package/dist/{cli-a2jw6ee5.js → cli-1p89xny0.js} +1 -1
  23. package/dist/{cli-v726afdm.js → cli-2pnswgh8.js} +1 -1
  24. package/dist/{cli-kvtc7aez.js → cli-3h4nhjn8.js} +42 -14
  25. package/dist/{cli-655xm9ms.js → cli-583frbyr.js} +1 -1
  26. package/dist/{cli-4r12t6w6.js → cli-5c4hjzne.js} +101 -4
  27. package/dist/{cli-az0bpf8p.js → cli-5pag6rmn.js} +3 -3
  28. package/dist/{cli-36g23z21.js → cli-6s7vdcta.js} +4 -4
  29. package/dist/{cli-rtk9g7av.js → cli-73jzwksr.js} +6 -2
  30. package/dist/{cli-jg0nfda6.js → cli-75gkpjvw.js} +52 -21
  31. package/dist/{cli-a81qevyg.js → cli-7a42nfy0.js} +20 -36
  32. package/dist/{cli-0sbkss7h.js → cli-7wcyk2s5.js} +2 -2
  33. package/dist/{cli-41z8xwj0.js → cli-7wspm5gm.js} +14 -4
  34. package/dist/{cli-byqfbnjw.js → cli-82ae98pz.js} +9 -9
  35. package/dist/{cli-bq2pcphn.js → cli-8hs41zy2.js} +1 -1
  36. package/dist/{cli-vqgnyh76.js → cli-98rw5dt5.js} +15 -3
  37. package/dist/{cli-9cctx1tn.js → cli-a22h291q.js} +1 -1
  38. package/dist/{cli-j4kt1mpb.js → cli-a47xy8eq.js} +3 -3
  39. package/dist/{cli-ycm6180s.js → cli-a7v854rf.js} +5 -3
  40. package/dist/{cli-98pke4vk.js → cli-ae7mrdf9.js} +2 -2
  41. package/dist/{cli-ahd90bwt.js → cli-afrrppsw.js} +6 -6
  42. package/dist/{cli-8yy525ad.js → cli-bhjt02p8.js} +2 -2
  43. package/dist/{cli-v1yw3wha.js → cli-bnz7a7qf.js} +4 -4
  44. package/dist/{cli-j4kw3rsx.js → cli-cm5yqn35.js} +18 -0
  45. package/dist/{cli-h55y2bjc.js → cli-dhar9qay.js} +2 -2
  46. package/dist/{cli-5gr1g711.js → cli-fqzx7gwa.js} +2 -2
  47. package/dist/{cli-tzrpv5sc.js → cli-ftyxyspd.js} +1 -1
  48. package/dist/cli-fw2w0kva.js +47 -0
  49. package/dist/{cli-2dfbv8qr.js → cli-fy6x31ps.js} +1 -0
  50. package/dist/{cli-mvckssyq.js → cli-g61qxmge.js} +5 -5
  51. package/dist/{cli-aq58fk5m.js → cli-g83s9ewh.js} +7 -1
  52. package/dist/{cli-224epwwf.js → cli-hn9hxmz9.js} +3 -3
  53. package/dist/{cli-5hjced2v.js → cli-j3jvnpe8.js} +6 -6
  54. package/dist/{cli-dae4sbgy.js → cli-jm2vfpfd.js} +2 -2
  55. package/dist/{cli-zn15c46z.js → cli-mjzynrse.js} +1 -1
  56. package/dist/{cli-a1fhj4n9.js → cli-mmwm7zbf.js} +2 -2
  57. package/dist/{cli-cq1pf336.js → cli-mry0qzym.js} +5 -5
  58. package/dist/{cli-bwty35gx.js → cli-mthwvjre.js} +1 -1
  59. package/dist/{cli-s0exq74r.js → cli-nmbmy64z.js} +3 -3
  60. package/dist/{cli-wcmnan2g.js → cli-nn4d8dne.js} +5 -5
  61. package/dist/{cli-jg4p5t36.js → cli-nzxr556s.js} +121 -51
  62. package/dist/{cli-bcz074zx.js → cli-p599f1qm.js} +1 -1
  63. package/dist/{cli-k76tgq03.js → cli-pywf3eh5.js} +2 -2
  64. package/dist/{cli-wk9md5px.js → cli-qxnf8z2z.js} +2 -2
  65. package/dist/{cli-je8yeyc5.js → cli-rhw40jgb.js} +5 -5
  66. package/dist/{cli-avgcz4ra.js → cli-rm6hqj3z.js} +406 -35
  67. package/dist/{cli-0fgr1w9b.js → cli-rpm2sgv0.js} +2 -2
  68. package/dist/{cli-t282jgab.js → cli-rqkxrmgw.js} +12 -12
  69. package/dist/{cli-2qpvmmkw.js → cli-s0qnd4wv.js} +1 -1
  70. package/dist/{cli-pwyevs29.js → cli-spknvxbq.js} +22 -21
  71. package/dist/{cli-9y354g3j.js → cli-srye3vdt.js} +1 -1
  72. package/dist/{cli-pe09p6dv.js → cli-tqgg40ek.js} +1 -1
  73. package/dist/{cli-f255yaqn.js → cli-vbyfwh92.js} +3 -3
  74. package/dist/{cli-gcbdjt47.js → cli-vfym7wmz.js} +6 -6
  75. package/dist/{cli-edmm9d9y.js → cli-vqypxzh4.js} +7 -7
  76. package/dist/{cli-1gm8jcyr.js → cli-wvaawacd.js} +13 -13
  77. package/dist/{cli-eny54typ.js → cli-wx3rhnm0.js} +1 -1
  78. package/dist/{cli-b1afgtj3.js → cli-xbkdm710.js} +3 -3
  79. package/dist/{cli-mc97xy2s.js → cli-xtjhfmpc.js} +16 -4
  80. package/dist/{cli-eqx6k993.js → cli-y24sc29k.js} +25 -13
  81. package/dist/{cli-ktmvb20s.js → cli-z6jkx23q.js} +12 -12
  82. package/dist/{cli-7n5sbw0z.js → cli-z8eh3ex8.js} +5 -5
  83. package/dist/cli-zh3fp8v2.js +151 -0
  84. package/dist/{cli-13jvmew3.js → cli-zppwg9p9.js} +7 -7
  85. package/dist/cli.js +18 -18
  86. package/dist/{codex-adapter-g0gctw29.js → codex-adapter-q6k3gnyg.js} +5 -5
  87. package/dist/{codex-hook-vrzz2xhr.js → codex-hook-9efm2x1m.js} +1 -1
  88. package/dist/{codex-login-0gg46ygn.js → codex-login-y0pmff1c.js} +1 -1
  89. package/dist/{codex-session-runner-dr2cxe1c.js → codex-session-runner-3mwmk3d9.js} +6 -6
  90. package/dist/{commands-1p07cvff.js → commands-evrenjn9.js} +2 -2
  91. package/dist/{controller-z0s7sv6r.js → controller-xjp8vswh.js} +4 -4
  92. package/dist/{controllers-connect-x9kvvhyk.js → controllers-connect-pp5jnxvc.js} +20 -20
  93. package/dist/{cursor-adapter-m5b98pvk.js → cursor-adapter-yf47w4t0.js} +2 -2
  94. package/dist/{db-rsh04kh3.js → db-rgfsh91f.js} +2 -2
  95. package/dist/{db-capk2raj.js → db-zxpw6aht.js} +3 -3
  96. package/dist/{devin-adapter-yxm87fe9.js → devin-adapter-r9phbnc9.js} +1 -1
  97. package/dist/{dispatcher-304q6fwa.js → dispatcher-smasgws7.js} +17 -17
  98. package/dist/{docker-7rxt0rry.js → docker-rrjhaqt9.js} +2 -2
  99. package/dist/{dsh-adapter-btewem0y.js → dsh-adapter-ftyy8dnp.js} +2 -2
  100. package/dist/{e2b-renc0k8q.js → e2b-qcaj675y.js} +2 -2
  101. package/dist/{e2b-7fc9b9bd.js → e2b-w1p0bnpm.js} +2 -2
  102. package/dist/{easter-egg-3d-fm1p3n4d.js → easter-egg-3d-yyts7p16.js} +1 -1
  103. package/dist/{embedding-space-s68v49ct.js → embedding-space-1z0mbg95.js} +6 -6
  104. package/dist/{embeddings-4ms3604w.js → embeddings-ygeg7tm7.js} +1 -1
  105. package/dist/{event-bus-dfjx2qdh.js → event-bus-g0ka3pbw.js} +1 -1
  106. package/dist/{execute-mcdgccjs.js → execute-14qhqp4d.js} +1 -1
  107. package/dist/{external-channel-9dxwfztn.js → external-channel-ttgrs3sx.js} +2 -2
  108. package/dist/fx-adapter-gt6pncj2.js +18 -0
  109. package/dist/{grok-adapter-05ecv4h0.js → grok-adapter-sm4m5k4g.js} +8 -8
  110. package/dist/handlers-vwmba1m2.js +31 -0
  111. package/dist/{heartbeat-3n94heh3.js → heartbeat-qahz4p62.js} +20 -20
  112. package/dist/{hook-zxwje86c.js → hook-4fd5wq5w.js} +4 -4
  113. package/dist/{hook-aadrn69x.js → hook-kj3d398m.js} +6 -6
  114. package/dist/{http-xzxk9mst.js → http-1ted0j1k.js} +100 -64
  115. package/dist/index-5r71rf98.js +32 -0
  116. package/dist/{index-d34f6r87.js → index-6rq3r5zb.js} +1 -1
  117. package/dist/{index-9j4zwcht.js → index-7v51awbn.js} +158 -672
  118. package/dist/index-anmwxs87.js +29 -0
  119. package/dist/{index-qeb82z37.js → index-q4b917zs.js} +1 -1
  120. package/dist/index-spp0ra5c.js +36 -0
  121. package/dist/{keepalive-w4aakbyk.js → keepalive-6cf311t5.js} +4 -4
  122. package/dist/lead-fjy418cw.js +69 -0
  123. package/dist/{maintenance-hbptpk6p.js → maintenance-a4bm1bmt.js} +1 -1
  124. package/dist/{oauth-refresh-sweep-217semrv.js → oauth-refresh-sweep-3h0dfw1k.js} +3 -3
  125. package/dist/{onboard-vz2s5df3.js → onboard-myn5hraa.js} +3 -3
  126. package/dist/{opencode-adapter-gtfd7d7v.js → opencode-adapter-p2nkb5am.js} +5 -5
  127. package/dist/{otel-impl-aefave37.js → otel-impl-a1a0ef6b.js} +2 -2
  128. package/dist/{pi-mono-adapter-f8h5yseh.js → pi-mono-adapter-rzmpb72a.js} +117 -23
  129. package/dist/{pricing-refresh-m1yhh9ya.js → pricing-refresh-83drrnxj.js} +3 -3
  130. package/dist/{rbac-roles-svj72cdf.js → rbac-roles-0fb6e7pj.js} +2 -2
  131. package/dist/{rbac-roles-rrw8twdz.js → rbac-roles-s8y6ecb5.js} +1 -1
  132. package/dist/{render-v2-amwbnfxh.js → render-v2-twmbgrz4.js} +3 -3
  133. package/dist/{resume-9cd6wf6z.js → resume-zfqc36xj.js} +7 -7
  134. package/dist/{runtime-fp4vrx7m.js → runtime-qwggdpqr.js} +1 -1
  135. package/dist/{session-access-21t6yjyg.js → session-access-sc9pn9da.js} +4 -4
  136. package/dist/{session-source-backfill-pxbzcsc6.js → session-source-backfill-qh07xr8g.js} +3 -3
  137. package/dist/{setup-4592tqz4.js → setup-4fr0m7a1.js} +1 -1
  138. package/dist/{virtual-modules-gzfnpmnz.js → virtual-modules-fh4ha15b.js} +2 -2
  139. package/dist/{work-objects-t9tcf174.js → work-objects-0wr8zxct.js} +20 -20
  140. package/dist/worker-0c5tyzqe.js +69 -0
  141. package/dist/{worker-follow-up-q32jh4yk.js → worker-follow-up-ktyyazyp.js} +17 -17
  142. package/dist/{x-b6wxntdt.js → x-vmmye1kb.js} +2 -2
  143. package/openapi.json +97 -32
  144. package/package.json +1 -1
  145. package/src/be/db/session-summaries.ts +8 -2
  146. package/src/be/db/session-tree.ts +174 -61
  147. package/src/be/db/session-visibility.ts +5 -4
  148. package/src/be/db.ts +8 -4
  149. package/src/be/migrations/259_session_summaries_open_tasks.sql +111 -0
  150. package/src/be/model-validation.ts +5 -1
  151. package/src/be/pricing-normalize.ts +7 -2
  152. package/src/be/scripts/typecheck.ts +1 -0
  153. package/src/be/seed/schedules-seeder.ts +135 -9
  154. package/src/be/seed/workflows-seeder.ts +65 -1
  155. package/src/be/seed-pricing.ts +23 -0
  156. package/src/be/steering.ts +50 -3
  157. package/src/be/swarm-config-guard.ts +1 -0
  158. package/src/commands/provider-credentials.ts +29 -2
  159. package/src/commands/runner.ts +86 -5
  160. package/src/extensions/contract-types.generated.ts +1 -1
  161. package/src/extensions/contract.ts +27 -1
  162. package/src/extensions/dispatcher.ts +4 -0
  163. package/src/http/agents.ts +1 -0
  164. package/src/http/mcp-bridge.ts +4 -0
  165. package/src/integrations/chat-sdk/inbound.ts +15 -5
  166. package/src/integrations/chat-sdk/steering.ts +3 -1
  167. package/src/kv-reserved-namespaces.ts +2 -0
  168. package/src/prompts/base-prompt.ts +4 -1
  169. package/src/prompts/session-templates.ts +26 -0
  170. package/src/providers/acp-adapter.ts +190 -26
  171. package/src/providers/acp-targets.ts +306 -6
  172. package/src/providers/amp-adapter.ts +1 -1
  173. package/src/providers/codex-adapter.ts +9 -44
  174. package/src/providers/codex-oauth/flow.ts +24 -3
  175. package/src/providers/codex-oauth/storage.ts +56 -17
  176. package/src/providers/codex-oauth/subscription.ts +67 -0
  177. package/src/providers/fx-adapter.ts +211 -0
  178. package/src/providers/index.ts +5 -1
  179. package/src/providers/mcp-loopback-proxy.ts +94 -0
  180. package/src/providers/pi-mono-adapter.ts +98 -13
  181. package/src/providers/pricing-sources.md +8 -0
  182. package/src/providers/reasoning-effort.ts +17 -1
  183. package/src/providers/types.ts +2 -1
  184. package/src/scheduler/scheduler.ts +36 -3
  185. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  186. package/src/scripts-runtime/types/stdlib.d.ts +5 -0
  187. package/src/scripts-runtime/types/swarm-extension.d.ts +33 -0
  188. package/src/scripts-runtime/types/swarm-sdk.d.ts +5 -0
  189. package/src/server.ts +7 -0
  190. package/src/slack/assistant.ts +17 -1
  191. package/src/slack/handlers.ts +18 -5
  192. package/src/slack/left-threads.ts +65 -0
  193. package/src/slack/steering.ts +3 -0
  194. package/src/slack/thread-buffer.ts +40 -16
  195. package/src/tests/acp-adapter.test.ts +79 -0
  196. package/src/tests/base-prompt.test.ts +12 -0
  197. package/src/tests/chat-sdk-inbound.test.ts +30 -0
  198. package/src/tests/chatgpt-subscription-model-groups.test.ts +62 -0
  199. package/src/tests/chatgpt-subscription-pi.test.ts +170 -0
  200. package/src/tests/chatgpt-subscription-session-bench.test.ts +198 -0
  201. package/src/tests/codex-oauth-credential-info.test.ts +40 -0
  202. package/src/tests/codex-oauth-refresh-egress.test.ts +194 -0
  203. package/src/tests/context-window.test.ts +7 -0
  204. package/src/tests/explicit-model-validation.test.ts +99 -0
  205. package/src/tests/extensions-run-context.test.ts +11 -0
  206. package/src/tests/extensions-steering.test.ts +353 -0
  207. package/src/tests/extensions-validate.test.ts +5 -0
  208. package/src/tests/fixtures/extensions/steering-policy/hooks.ts +39 -0
  209. package/src/tests/fixtures/extensions/steering-policy/manifest.json +7 -0
  210. package/src/tests/fixtures/fake-fx.ts +167 -0
  211. package/src/tests/fixtures/fx/acp-responses-recorded.json +157 -0
  212. package/src/tests/fixtures/fx/acp-updates-recorded.jsonl +15 -0
  213. package/src/tests/fx-adapter.test.ts +771 -0
  214. package/src/tests/grok-adapter.test.ts +36 -0
  215. package/src/tests/mcp-loopback-proxy.test.ts +193 -0
  216. package/src/tests/model-tier-resolution.test.ts +3 -3
  217. package/src/tests/pricing-refresh.test.ts +5 -5
  218. package/src/tests/prompt-template-session.test.ts +5 -3
  219. package/src/tests/provider-registration.test.ts +2 -1
  220. package/src/tests/query-audit-plans.test.ts +3 -2
  221. package/src/tests/script-connections.test.ts +2 -0
  222. package/src/tests/seed-model-profiles.test.ts +2 -2
  223. package/src/tests/seed-schedules.test.ts +138 -0
  224. package/src/tests/session-cost-recompute-unit.test.ts +39 -0
  225. package/src/tests/session-costs-golden.test.ts +26 -0
  226. package/src/tests/session-summaries-plans.test.ts +120 -0
  227. package/src/tests/session-summaries.test.ts +72 -12
  228. package/src/tests/sessions-tree.test.ts +11 -3
  229. package/src/tests/slack-leave-thread.test.ts +431 -0
  230. package/src/tests/tool-annotations.test.ts +2 -1
  231. package/src/tests/workflow-engine-v2.test.ts +42 -0
  232. package/src/tests/workflow-script-principal.test.ts +207 -7
  233. package/src/tools/slack-leave-thread.ts +104 -0
  234. package/src/tools/steer-task.ts +3 -2
  235. package/src/tools/tool-config.ts +2 -1
  236. package/src/types.ts +19 -0
  237. package/src/utils/codex-auth-failure.ts +30 -0
  238. package/src/utils/context-window.ts +4 -1
  239. package/src/utils/credentials.ts +7 -0
  240. package/src/utils/fx-models.ts +43 -0
  241. package/src/workflows/cooldown.ts +3 -2
  242. package/src/workflows/engine.ts +24 -9
  243. package/templates/schedules/dreaming-daily/config.json +1 -1
  244. package/templates/schedules/dreaming-daily/content.md +6 -14
  245. package/templates/skills/swarm-docs/content.md +1 -0
  246. package/templates/workflows/dreaming/content.md +5 -1
  247. package/dist/acp-adapter-96dc64m2.js +0 -15
  248. package/dist/handlers-jg7v74ty.js +0 -31
  249. package/dist/index-8c1zbz59.js +0 -29
  250. package/dist/index-8k8tmfxd.js +0 -32
  251. package/dist/index-hq617xdj.js +0 -36
  252. package/dist/lead-567kw7x3.js +0 -68
  253. package/dist/worker-fjdqra6q.js +0 -68
@@ -14,9 +14,11 @@
14
14
  */
15
15
 
16
16
  import {
17
+ CHATGPT_SUBSCRIPTION_PROVIDER,
17
18
  claudeCatalogModelId,
18
19
  cursorCatalogRef,
19
20
  dshCatalogRef,
21
+ fxCatalogRef,
20
22
  grokCatalogRef,
21
23
  REASONING_EFFORT_LEVELS,
22
24
  type ReasoningEffortLevel,
@@ -37,7 +39,8 @@ export type ReasoningHarness =
37
39
  | "dsh"
38
40
  | "cursor"
39
41
  | "amp"
40
- | "grok";
42
+ | "grok"
43
+ | "fx";
41
44
 
42
45
  export interface ReasoningCapability {
43
46
  supported: boolean;
@@ -65,6 +68,7 @@ export type ReasoningEffortApplication =
65
68
  | { kind: "amp-effort"; reasoningEffort: ReasoningEffort }
66
69
  | { kind: "cursor-effort"; reasoningEffort: ReasoningEffort }
67
70
  | { kind: "grok-effort"; reasoningEffort: ReasoningEffort }
71
+ | { kind: "fx-effort"; reasoningEffort: ReasoningEffort }
68
72
  | { kind: "noop" };
69
73
 
70
74
  // --- Capability lookup --------------------------------------------------------
@@ -102,9 +106,16 @@ function lookupModel(
102
106
  ({ providerId, modelId } = cursorCatalogRef(model));
103
107
  } else if (harness === "grok") {
104
108
  ({ providerId, modelId } = grokCatalogRef(model));
109
+ } else if (harness === "fx") {
110
+ // Only the AI Gateway route (`vercel/…`) has an effort control.
111
+ const ref = fxCatalogRef(model);
112
+ if (!ref) return undefined;
113
+ ({ providerId, modelId } = ref);
105
114
  } else {
106
115
  ({ providerId, modelId } = splitProviderModel(model));
107
116
  if (!providerId) return undefined;
117
+ // A ChatGPT subscription id names an OpenAI catalog model.
118
+ if (providerId.toLowerCase() === CHATGPT_SUBSCRIPTION_PROVIDER) providerId = "openai";
108
119
  }
109
120
  const facts = runtimeCatalogModel(providerId, modelId);
110
121
  return facts ? { id: modelId, facts } : undefined;
@@ -240,6 +251,10 @@ export function applyReasoningEffort(
240
251
  // Grok's `--reasoning-effort` takes the normalized names as-is; see
241
252
  // `src/providers/grok-adapter.ts`.
242
253
  return { kind: "grok-effort", reasoningEffort: level };
254
+ case "fx":
255
+ // fx's `effort` config option takes the normalized names; the adapter
256
+ // sets it on the AI Gateway route only (see `fxTargetProfile`).
257
+ return { kind: "fx-effort", reasoningEffort: level };
243
258
  default: {
244
259
  const _exhaustive: never = harness;
245
260
  return _exhaustive;
@@ -256,6 +271,7 @@ const REASONING_HARNESSES: ReadonlySet<string> = new Set<ReasoningHarness>([
256
271
  "cursor",
257
272
  "amp",
258
273
  "grok",
274
+ "fx",
259
275
  ]);
260
276
 
261
277
  /**
@@ -66,7 +66,8 @@ export interface CostData {
66
66
  | "dsh"
67
67
  | "cursor"
68
68
  | "amp"
69
- | "grok";
69
+ | "grok"
70
+ | "fx";
70
71
  }
71
72
 
72
73
  import type { ProviderName, SteerMode } from "../types";
@@ -11,6 +11,7 @@ import {
11
11
  import {
12
12
  getDbClient,
13
13
  getDueScheduledTasks,
14
+ getLeadAgent,
14
15
  getScheduledTaskById,
15
16
  getUserById,
16
17
  getWorkflow,
@@ -26,8 +27,9 @@ import {
26
27
  import { buildScriptCredentialBindingsWithFailures } from "@/be/script-credential-broker";
27
28
  import { getScript } from "@/be/scripts/db";
28
29
  import { runScriptWithRunCredential } from "@/be/scripts/run-credential";
30
+ import { seededWorkflowState } from "@/be/seed/workflows-seeder";
29
31
  import { telemetry } from "@/telemetry";
30
- import type { AgentTask, ScheduledTask } from "@/types";
32
+ import type { AgentTask, ScheduledTask, Workflow } from "@/types";
31
33
  import { getExecutorRegistry as getWorkflowExecutorRegistry } from "@/workflows";
32
34
  import { startWorkflowExecution } from "@/workflows/engine";
33
35
  import type { ExecutorRegistry } from "@/workflows/executors/registry";
@@ -252,8 +254,7 @@ export async function dispatchScheduleTarget(
252
254
  };
253
255
  const runId = await startWorkflowExecution(workflow, triggerData, registry, {
254
256
  triggerType: "schedule",
255
- // The creator, as for script schedules: `targetAgentId` is caller-chosen.
256
- triggeredByAgentId: schedule.createdByAgentId,
257
+ triggeredByAgentId: await scheduledWorkflowPrincipal(schedule, workflow),
257
258
  requestedByUserId: schedule.createdBy,
258
259
  });
259
260
  await throwIfWorkflowNeedsSetup(schedule.id, [runId]);
@@ -295,6 +296,38 @@ export async function dispatchScheduleTarget(
295
296
  }
296
297
  }
297
298
 
299
+ /**
300
+ * The agent a scheduled workflow run acts as: the schedule's creator, as for
301
+ * script schedules (`targetAgentId` and trigger data are caller-chosen, so
302
+ * neither is ever the principal). A creator-less (seeded) schedule that fires
303
+ * a seeded workflow runs as the lead, resolved at fire time; with no lead the
304
+ * fire fails instead of running agentless. Operator settings such as inputs
305
+ * or on/off keep that. Any agent can edit a workflow, so one whose nodes or
306
+ * params changed never inherits the lead and its fire fails. Workflows the
307
+ * seeder never wrote keep their previous principal.
308
+ */
309
+ async function scheduledWorkflowPrincipal(
310
+ schedule: ScheduledTask,
311
+ workflow: Workflow,
312
+ ): Promise<string | undefined> {
313
+ if (schedule.createdByAgentId) return schedule.createdByAgentId;
314
+ if (workflow.createdByAgentId) return undefined;
315
+ const seeded = await seededWorkflowState(workflow);
316
+ if (seeded === "unseeded") return undefined;
317
+ if (seeded === "edited") {
318
+ throw new Error(
319
+ `Schedule "${schedule.name}" has no creator and the nodes or params of seeded workflow "${workflow.name}" were edited, so it will not run as the lead. Re-create the schedule as the agent it should run as.`,
320
+ );
321
+ }
322
+ const lead = await getLeadAgent();
323
+ if (!lead) {
324
+ throw new Error(
325
+ `Schedule "${schedule.name}" has no creator and the swarm has no lead to run its workflow as`,
326
+ );
327
+ }
328
+ return lead.id;
329
+ }
330
+
298
331
  async function throwIfWorkflowNeedsSetup(scheduleId: string, runIds: string[]): Promise<void> {
299
332
  for (const runId of runIds) {
300
333
  const run = await getWorkflowRun(runId);
@@ -104,6 +104,7 @@ export const SDK_TOOL_NAME_MAP = {
104
104
  slack_downloadFile: "slack-download-file",
105
105
  slack_delete: "slack-delete", // external: mutates Slack, destructive
106
106
  slack_update: "slack-update", // external: mutates Slack
107
+ slack_leaveThread: "slack-leave-thread",
107
108
 
108
109
  // ── chat sdk ──
109
110
  chat_reply: "chat-reply", // external: sends to the task's chat thread
@@ -493,6 +493,11 @@ declare module "swarm-sdk" {
493
493
  slack_downloadFile(args: { url: string }): Promise<unknown>;
494
494
  slack_delete(args: { channelId: string; messageTs: string }): Promise<unknown>;
495
495
  slack_update(args: { channelId: string; messageTs: string; message: string }): Promise<unknown>;
496
+ slack_leaveThread(args: {
497
+ taskId?: string;
498
+ channelId?: string;
499
+ threadTs?: string;
500
+ }): Promise<unknown>;
496
501
 
497
502
  // --- write: chat sdk ---
498
503
  chat_reply(args: { text: string }): Promise<unknown>;
@@ -98,6 +98,7 @@ declare module "swarm-extension" {
98
98
  | "amp"
99
99
  | "cursor"
100
100
  | "grok"
101
+ | "fx"
101
102
  | undefined;
102
103
  leadOnly?: boolean | undefined;
103
104
  }
@@ -233,6 +234,7 @@ declare module "swarm-extension" {
233
234
  | "amp"
234
235
  | "cursor"
235
236
  | "grok"
237
+ | "fx"
236
238
  | undefined;
237
239
  providerMeta?: Record<string, unknown> | undefined;
238
240
  harnessVariant?: string | undefined;
@@ -255,6 +257,7 @@ declare module "swarm-extension" {
255
257
  | "amp"
256
258
  | "cursor"
257
259
  | "grok"
260
+ | "fx"
258
261
  | undefined;
259
262
  leadOnly?: boolean | undefined;
260
263
  }
@@ -276,6 +279,10 @@ declare module "swarm-extension" {
276
279
  runtimeInstanceId?: string | null | undefined;
277
280
  };
278
281
 
282
+ export type SteerMode = "steer" | "queue";
283
+
284
+ export type SteeringSource = "script" | "slack" | "mcp" | "api" | "ui" | "chat";
285
+
279
286
  export type RequestInfo = {
280
287
  sessionId: string | undefined;
281
288
  agentId: string | undefined;
@@ -486,6 +493,27 @@ declare module "swarm-extension" {
486
493
  leadAgentId: string;
487
494
  summary: string;
488
495
  }
496
+ export interface TaskSteerEvent {
497
+ task: AgentTask;
498
+ message: string;
499
+ mode: SteerMode;
500
+ source: SteeringSource;
501
+ sender: {
502
+ kind: "user" | "agent" | "system";
503
+ userId?: string;
504
+ agentId?: string;
505
+ };
506
+ messageTimestamps?: string[];
507
+ /** Slack ingress metadata; not persisted or writable through modify. */
508
+ slack?: {
509
+ channelId: string;
510
+ threadTs: string;
511
+ userId?: string;
512
+ };
513
+ }
514
+ export interface TaskSteerModify {
515
+ message: string;
516
+ }
489
517
  export type TaskFollowUpModify = Partial<
490
518
  Pick<CreateTaskOptions, "agentId" | "priority" | "followUpConfig">
491
519
  > & {
@@ -718,6 +746,11 @@ declare module "swarm-extension" {
718
746
  modify: TaskFollowUpModify;
719
747
  result: PreResult<TaskFollowUpModify>;
720
748
  };
749
+ "pre.task.steer": {
750
+ event: TaskSteerEvent;
751
+ modify: TaskSteerModify;
752
+ result: PreResult<TaskSteerModify>;
753
+ };
721
754
  "pre.slack.route": {
722
755
  event: SlackRouteEvent;
723
756
  modify: SlackRouteModify;
@@ -475,6 +475,11 @@ declare module "swarm-sdk" {
475
475
  slack_downloadFile(args: { url: string }): Promise<unknown>;
476
476
  slack_delete(args: { channelId: string; messageTs: string }): Promise<unknown>;
477
477
  slack_update(args: { channelId: string; messageTs: string; message: string }): Promise<unknown>;
478
+ slack_leaveThread(args: {
479
+ taskId?: string;
480
+ channelId?: string;
481
+ threadTs?: string;
482
+ }): Promise<unknown>;
478
483
 
479
484
  // --- write: chat sdk ---
480
485
  chat_reply(args: { text: string }): Promise<unknown>;
package/src/server.ts CHANGED
@@ -11,6 +11,7 @@ import { MCP_MAX_TOOL_INPUT_ELEMENTS } from "./mcp-limits";
11
11
  import { resolveTemplate } from "./prompts/resolver";
12
12
  import { isRbacEnabled } from "./rbac";
13
13
  import { getSlackConfiguration } from "./slack/config";
14
+ import { isSlackLeaveThreadDisabled } from "./slack/left-threads";
14
15
  import { registerAcceptSteerTool } from "./tools/accept-steer";
15
16
  import { registerAppActionRunTool } from "./tools/app-action-run";
16
17
  import { registerAppDiffTool } from "./tools/app-diff";
@@ -162,6 +163,7 @@ import { registerSlackCreateChannelTool } from "./tools/slack-create-channel";
162
163
  import { registerSlackDeleteTool } from "./tools/slack-delete";
163
164
  import { registerSlackDownloadFileTool } from "./tools/slack-download-file";
164
165
  import { registerSlackInviteToChannelTool } from "./tools/slack-invite-to-channel";
166
+ import { registerSlackLeaveThreadTool } from "./tools/slack-leave-thread";
165
167
  import { registerSlackListChannelsTool } from "./tools/slack-list-channels";
166
168
  import { registerSlackPostTool } from "./tools/slack-post";
167
169
  import { registerSlackReadTool } from "./tools/slack-read";
@@ -693,6 +695,11 @@ export async function createServer(
693
695
  registerSlackDownloadFileTool(server);
694
696
  registerSlackDeleteTool(server);
695
697
  registerSlackUpdateTool(server);
698
+ // The scripts bridge builds its full-surface server once and caches it, so
699
+ // it always registers the tool; the handler refuses while the flag is set.
700
+ if (opts.fullSurface === true || !isSlackLeaveThreadDisabled()) {
701
+ registerSlackLeaveThreadTool(server);
702
+ }
696
703
  }
697
704
 
698
705
  // End of default-enabled capabilities
@@ -20,6 +20,7 @@ import {
20
20
  fetchSlackFiles,
21
21
  notifySlackFileFailures,
22
22
  } from "./inbound-files";
23
+ import { shouldSkipLeftSlackThread } from "./left-threads";
23
24
  import {
24
25
  collectSlackMessageFiles,
25
26
  type SlackMessageLike,
@@ -194,11 +195,26 @@ export function createAssistant(
194
195
  }
195
196
  }
196
197
 
198
+ const botMentioned = !!cachedBotUserId && rawText.includes(`<@${cachedBotUserId}>`);
199
+ // A thread the swarm left (slack-leave-thread) stays silent until someone
200
+ // @mentions the bot.
201
+ if (
202
+ await shouldSkipLeftSlackThread(
203
+ channelId,
204
+ msg.thread_ts as string | undefined,
205
+ botMentioned,
206
+ )
207
+ ) {
208
+ console.log(
209
+ `[Slack] assistant: skipping message in left thread ${channelId}/${threadTs}`,
210
+ );
211
+ return ignoreSlackInbound("thread_left");
212
+ }
213
+
197
214
  // If the message @-mentions someone OTHER than our bot and does NOT mention
198
215
  // our bot, it is addressed to a different agent/user — do not spawn a task
199
216
  // (unless SLACK_THREAD_WAKE_ON_ANY_MESSAGE=true).
200
217
  if (cachedBotUserId) {
201
- const botMentioned = rawText.includes(`<@${cachedBotUserId}>`);
202
218
  if (!botMentioned && isAddressedToOtherUser(rawText, cachedBotUserId)) {
203
219
  console.log(
204
220
  `[Slack] assistant: skipping message in ${channelId}/${threadTs} — mentions another user, not us`,
@@ -10,6 +10,7 @@ import {
10
10
  getTasksByAgentId,
11
11
  isExtensionAgent,
12
12
  } from "../be/db";
13
+ import { SteeringBlockedError } from "../be/steering";
13
14
  import { dispatchPre } from "../extensions/dispatcher";
14
15
  import { resolveTemplate } from "../prompts/resolver";
15
16
  import { slackContextKey } from "../tasks/context-key";
@@ -51,6 +52,7 @@ import {
51
52
  leaveExternalChannel,
52
53
  shouldBlockChannel,
53
54
  } from "./external-channel";
55
+ import { shouldSkipLeftSlackThread } from "./left-threads";
54
56
  import {
55
57
  collectSlackMessageFiles,
56
58
  extractSlackMessageText,
@@ -486,6 +488,17 @@ export function registerMessageHandler(app: App): void {
486
488
  return ignoreSlackInbound("unauthorized_user");
487
489
  }
488
490
 
491
+ // Check if bot was mentioned (in original text only)
492
+ const botMentioned = !!cachedBotUserId && !!msg.text?.includes(`<@${cachedBotUserId}>`);
493
+
494
+ // A thread the swarm left (slack-leave-thread) stays silent until someone
495
+ // @mentions the bot. Checked before the workflow event, buffering, routing,
496
+ // or any AI call.
497
+ if (await shouldSkipLeftSlackThread(msg.channel, msg.thread_ts, botMentioned)) {
498
+ console.log(`[Slack] Skipping message in left thread ${msg.channel}/${msg.thread_ts}`);
499
+ return ignoreSlackInbound("thread_left");
500
+ }
501
+
489
502
  // Resolve canonical user identity via the three-step cascade:
490
503
  // fast-path alias lookup → enrich+auto-link by email → unmapped tracker.
491
504
  // Returns undefined when no email is recoverable; task creation proceeds
@@ -519,9 +532,6 @@ export function registerMessageHandler(app: App): void {
519
532
  }
520
533
  const botUserId = cachedBotUserId;
521
534
 
522
- // Check if bot was mentioned (in original text only)
523
- const botMentioned = !!msg.text?.includes(`<@${botUserId}>`);
524
-
525
535
  // Detect assistant thread context. Bolt's Assistant middleware claims DM thread
526
536
  // messages with no subtype or `file_share` (`isAssistantMessage` in @slack/bolt), so
527
537
  // those — file uploads included — are handled in assistant.ts. An assistant-thread
@@ -862,6 +872,7 @@ export function registerMessageHandler(app: App): void {
862
872
  message: taskDescription,
863
873
  messageTimestamps: [msg.ts],
864
874
  requestedByUserId,
875
+ slackUserId: msg.user,
865
876
  })
866
877
  : null;
867
878
  if (steering) {
@@ -937,12 +948,14 @@ export function registerMessageHandler(app: App): void {
937
948
  } catch (error) {
938
949
  // A policy block is an answered outcome; anything else is a failure the
939
950
  // user sees as "could not assign" and the dispatcher must not call success.
940
- if (!(error instanceof TaskCreationBlockedError)) {
951
+ const blocked =
952
+ error instanceof TaskCreationBlockedError || error instanceof SteeringBlockedError;
953
+ if (!blocked) {
941
954
  reportSlackInboundFailure("task_creation_error");
942
955
  }
943
956
  results.failed.push({
944
957
  agentName: agent.name,
945
- reason: error instanceof TaskCreationBlockedError ? error.reason : "error",
958
+ reason: blocked ? error.reason : "error",
946
959
  });
947
960
  }
948
961
  }
@@ -0,0 +1,65 @@
1
+ import { deleteKv, getKv, upsertKv } from "../be/db";
2
+ import { isEnvFlagEnabled } from "../utils/env-flag";
3
+
4
+ /**
5
+ * Slack threads the swarm left via `slack-leave-thread`.
6
+ *
7
+ * A left thread drops every inbound message that does not @mention the bot,
8
+ * in code, before buffering, routing, or any AI call. A mention clears the
9
+ * state and the message is handled as usual. Stored in kv_entries so a
10
+ * restart keeps it.
11
+ */
12
+ export const SLACK_LEFT_THREAD_KV_NAMESPACE = "slack-left-thread";
13
+
14
+ /** Operator flag that turns the `slack-leave-thread` tool off. */
15
+ export const SLACK_LEAVE_THREAD_DISABLE = "SLACK_LEAVE_THREAD_DISABLE";
16
+
17
+ /**
18
+ * Read at call time, not cached: a global swarm_config row is hydrated into
19
+ * process.env on every config reload, so a flip applies without a restart.
20
+ */
21
+ export function isSlackLeaveThreadDisabled(): boolean {
22
+ return isEnvFlagEnabled(SLACK_LEAVE_THREAD_DISABLE, false);
23
+ }
24
+
25
+ function threadKey(channelId: string, threadTs: string): string {
26
+ return `${channelId}:${threadTs}`;
27
+ }
28
+
29
+ export async function leaveSlackThread(
30
+ channelId: string,
31
+ threadTs: string,
32
+ agentId: string,
33
+ ): Promise<void> {
34
+ await upsertKv({
35
+ namespace: SLACK_LEFT_THREAD_KV_NAMESPACE,
36
+ key: threadKey(channelId, threadTs),
37
+ value: { agentId, leftAt: new Date().toISOString() },
38
+ valueType: "json",
39
+ });
40
+ }
41
+
42
+ export async function isSlackThreadLeft(channelId: string, threadTs: string): Promise<boolean> {
43
+ return (await getKv(SLACK_LEFT_THREAD_KV_NAMESPACE, threadKey(channelId, threadTs))) !== null;
44
+ }
45
+
46
+ /**
47
+ * Inbound gate for left threads. Returns true when the message must be dropped:
48
+ * the thread is left and the message does not @mention the bot. A mention
49
+ * clears the left state and returns false.
50
+ */
51
+ export async function shouldSkipLeftSlackThread(
52
+ channelId: string,
53
+ threadTs: string | undefined,
54
+ botMentioned: boolean,
55
+ ): Promise<boolean> {
56
+ if (!threadTs) return false;
57
+ const key = threadKey(channelId, threadTs);
58
+ if (botMentioned) {
59
+ if (await deleteKv(SLACK_LEFT_THREAD_KV_NAMESPACE, key)) {
60
+ console.log(`[Slack] Rejoined left thread ${key}: bot mentioned`);
61
+ }
62
+ return false;
63
+ }
64
+ return isSlackThreadLeft(channelId, threadTs);
65
+ }
@@ -9,6 +9,7 @@ export interface SlackThreadSteeringRequest {
9
9
  message: string;
10
10
  messageTimestamps?: string[];
11
11
  requestedByUserId?: string;
12
+ slackUserId?: string;
12
13
  }
13
14
 
14
15
  export interface SlackThreadSteeringResult {
@@ -52,6 +53,8 @@ export async function requestSlackThreadSteering(
52
53
  source: "slack",
53
54
  createdByKind: "user",
54
55
  createdByUserId: args.requestedByUserId,
56
+ messageTimestamps: args.messageTimestamps,
57
+ slack: { channelId: args.channelId, threadTs: args.threadTs, userId: args.slackUserId },
55
58
  });
56
59
  for (const messageTs of args.messageTimestamps ?? []) {
57
60
  await createLogEntry({
@@ -4,6 +4,7 @@ import {
4
4
  getLeadAgent,
5
5
  getMostRecentTaskInThread,
6
6
  } from "../be/db";
7
+ import { SteeringBlockedError } from "../be/steering";
7
8
  import { findUserByExternalId } from "../be/users";
8
9
  import { createAdditiveBuffer } from "../tasks/additive-buffer";
9
10
  import { slackContextKey } from "../tasks/context-key";
@@ -21,6 +22,7 @@ import {
21
22
  fetchSlackFiles,
22
23
  notifySlackFileFailures,
23
24
  } from "./inbound-files";
25
+ import { isSlackThreadLeft } from "./left-threads";
24
26
  import { extractSlackMessageText } from "./message-text";
25
27
  import { ensureSlackThreadTree, isSlackRenderV2Enabled } from "./render-v2";
26
28
  import { getAgentDisplayName, getAgentSlackIcon } from "./responses";
@@ -164,6 +166,12 @@ async function slackFlush(
164
166
  return;
165
167
  }
166
168
  const { channelId, threadTs } = split;
169
+ // The thread was left (slack-leave-thread) while these replies waited. Only
170
+ // non-mentions are buffered, so none of them rejoins it.
171
+ if (await isSlackThreadLeft(channelId, threadTs)) {
172
+ console.log(`[Slack] Buffer flush: dropping ${items.length} message(s) in left thread ${key}`);
173
+ return;
174
+ }
167
175
  // Buffer is guaranteed to have at least one item — the first carries the
168
176
  // original requester's userId (same semantics as the pre-refactor version).
169
177
  const originalRequesterId = items[0]!.userId;
@@ -226,22 +234,38 @@ async function slackFlush(
226
234
 
227
235
  // Steering carries text only. As on the mention path, files need a follow-up
228
236
  // task so its attachments exist before a worker can claim it.
229
- const steering =
230
- files.length === 0
231
- ? await requestSlackThreadSteering({
232
- channelId,
233
- threadTs,
234
- message:
235
- nextSenderIndex === -1
236
- ? combinedText
237
- : await rewriteSlackMentions(
238
- steeringItems.map((item) => item.text).join("\n---\n"),
239
- botUserId,
240
- ),
241
- messageTimestamps: steeringItems.map((item) => item.ts),
242
- requestedByUserId,
243
- })
244
- : null;
237
+ let steering: Awaited<ReturnType<typeof requestSlackThreadSteering>>;
238
+ try {
239
+ steering =
240
+ files.length === 0
241
+ ? await requestSlackThreadSteering({
242
+ channelId,
243
+ threadTs,
244
+ message:
245
+ nextSenderIndex === -1
246
+ ? combinedText
247
+ : await rewriteSlackMentions(
248
+ steeringItems.map((item) => item.text).join("\n---\n"),
249
+ botUserId,
250
+ ),
251
+ messageTimestamps: steeringItems.map((item) => item.ts),
252
+ requestedByUserId,
253
+ slackUserId: originalRequesterId,
254
+ })
255
+ : null;
256
+ } catch (error) {
257
+ if (!(error instanceof SteeringBlockedError)) throw error;
258
+ console.info("[Slack] pre.task.steer blocked:", error.extension.name, error.reason);
259
+ if (app) {
260
+ for (const item of steeringItems) {
261
+ await ackSlackMessage(app.client, channelId, item.ts, "no_entry");
262
+ }
263
+ }
264
+ if (nextSenderIndex !== -1) {
265
+ await slackFlush(items.slice(nextSenderIndex), key, immediate);
266
+ }
267
+ return;
268
+ }
245
269
  if (steering) {
246
270
  console.log(
247
271
  `[Slack] Buffer flushed → steering ${steering.result.outcome} for task ${steering.task.id}`,
@@ -1179,6 +1179,85 @@ ${serveFakeAcpAgent("FakeCopilot")}
1179
1179
  expect(toAcpMcpServers(null)).toEqual([]);
1180
1180
  });
1181
1181
 
1182
+ test("a target without the opt-in hooks keeps its session/new payload, MCP URL and no steering", async () => {
1183
+ const cwd = makeTempDir();
1184
+ const agentPath = join(cwd, "fake-acp-hooks-off.ts");
1185
+ const captureFile = join(cwd, "session-new.json");
1186
+ await Bun.write(
1187
+ agentPath,
1188
+ `
1189
+ import { Readable, Writable } from "node:stream";
1190
+ import { PROTOCOL_VERSION, agent, methods, ndJsonStream } from "${ACP_SDK_PATH}";
1191
+ class FakeAgent {
1192
+ constructor(connection) { this.connection = connection; }
1193
+ async initialize() {
1194
+ // Advertises every capability the opt-in hooks key off; a target without
1195
+ // the hooks must ignore them.
1196
+ return {
1197
+ protocolVersion: PROTOCOL_VERSION,
1198
+ agentCapabilities: {
1199
+ loadSession: true,
1200
+ mcpCapabilities: { http: true, sse: false },
1201
+ sessionCapabilities: { resume: {}, systemPrompt: {} },
1202
+ _meta: { fx: { steering: true } },
1203
+ },
1204
+ agentInfo: { name: "fake", version: "9.9.9" },
1205
+ };
1206
+ }
1207
+ async newSession(params) {
1208
+ await Bun.write(${JSON.stringify(captureFile)}, JSON.stringify(params));
1209
+ return { sessionId: "hooks-off-1" };
1210
+ }
1211
+ async prompt() { return { stopReason: "end_turn" }; }
1212
+ async cancel() {}
1213
+ }
1214
+ ${serveFakeAcpAgent("FakeAgent")}
1215
+ `,
1216
+ );
1217
+ const { close: closeTokenStub, apiUrl } = await startTokenStubServer(
1218
+ "hooks-off-token",
1219
+ "aseph_hooksofftoken1234567890",
1220
+ );
1221
+ // A reachable, plain-HTTP, non-loopback URL: only requiresSecureMcpUrl rewrites it.
1222
+ const publicStyleUrl = apiUrl.replace("127.0.0.1", "0.0.0.0");
1223
+ try {
1224
+ const adapter = new ACPAdapter();
1225
+ const session = await adapter.createSession(
1226
+ baseConfig({
1227
+ cwd,
1228
+ apiUrl: publicStyleUrl,
1229
+ resumeSessionId: "ignored-without-a-resume-hook",
1230
+ env: {
1231
+ PATH: process.env.PATH ?? "",
1232
+ HOME: process.env.HOME ?? "",
1233
+ ACP_TARGET_COMMAND: "bun",
1234
+ ACP_TARGET_ARGS: JSON.stringify([agentPath]),
1235
+ },
1236
+ }),
1237
+ );
1238
+ const events: ProviderEvent[] = [];
1239
+ session.onEvent((event) => events.push(event));
1240
+ await session.waitForCompletion();
1241
+
1242
+ expect(session.sessionId).toBe("hooks-off-1");
1243
+ expect(session.deliverSteering).toBeUndefined();
1244
+ expect(await adapter.canResume("hooks-off-1")).toBe(false);
1245
+ const params = JSON.parse(await Bun.file(captureFile).text());
1246
+ expect(Object.keys(params).sort()).toEqual(["cwd", "mcpServers"]);
1247
+ expect(params.mcpServers.map((server: { url: string }) => server.url)).toEqual([
1248
+ `${publicStyleUrl}/mcp`,
1249
+ ]);
1250
+ // Every ACP target records the agent's own version on session_init.
1251
+ expect(events.find((event) => event.type === "session_init")).toMatchObject({
1252
+ sessionId: "hooks-off-1",
1253
+ provider: "acp",
1254
+ harnessVariantMeta: { version: "9.9.9" },
1255
+ });
1256
+ } finally {
1257
+ await closeTokenStub();
1258
+ }
1259
+ });
1260
+
1182
1261
  test("completes before the revoke response and logs a failed revocation", async () => {
1183
1262
  const cwd = makeTempDir();
1184
1263
  const agentPath = join(cwd, "fake-acp-ephem-agent.ts");
@@ -1290,6 +1290,18 @@ describe("getBasePrompt: harness tool discovery", () => {
1290
1290
  expect(result).toContain('import { store_progress } from "agent-swarm"');
1291
1291
  });
1292
1292
 
1293
+ test("fx gets capability_search guidance with mcp_swarm_ tool names", async () => {
1294
+ const result = await getBasePrompt({
1295
+ ...minimalArgs,
1296
+ provider: "fx",
1297
+ traits: { ...localTraits, hasToolSearch: false },
1298
+ });
1299
+ expect(result).not.toContain(deferred);
1300
+ expect(result).not.toContain("already in your tool list");
1301
+ expect(result).toContain("`capability_search`");
1302
+ expect(result).toContain("`store-progress` is `mcp_swarm_store-progress`");
1303
+ });
1304
+
1293
1305
  test("codex gets exec guidance with the normalized tool name", async () => {
1294
1306
  const result = await getBasePrompt({
1295
1307
  ...minimalArgs,