@desplega.ai/agent-swarm 2.4.3 → 2.4.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 (270) hide show
  1. package/dist/{acp-adapter-1885dpr7.js → acp-adapter-2wdkktv6.js} +1 -2
  2. package/dist/{actions-pn76n32q.js → actions-c40ebcm4.js} +18 -18
  3. package/dist/{amp-adapter-af84rx1d.js → amp-adapter-wb67m95z.js} +2 -2
  4. package/dist/{app-6sv8mhzm.js → app-353vre8g.js} +3 -3
  5. package/dist/{assistant-sc2tf825.js → assistant-sj1njh23.js} +21 -21
  6. package/dist/{boot-reembed-y3m0ydzj.js → boot-reembed-b9c3az3c.js} +1 -1
  7. package/dist/{boot-reembed-gvxjs2py.js → boot-reembed-qdkvxeem.js} +1 -1
  8. package/dist/{boot-scrub-sweep-1j9mk05f.js → boot-scrub-sweep-m3wh8eks.js} +2 -2
  9. package/dist/{boot-seeds-37sp626b.js → boot-seeds-j2273s54.js} +4 -4
  10. package/dist/{claude-adapter-rp0jtnwr.js → claude-adapter-m7q0a085.js} +2 -2
  11. package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-5ptb6tzs.js} +2 -2
  12. package/dist/{claude-managed-setup-rdt248k7.js → claude-managed-setup-smrqk5j5.js} +3 -3
  13. package/dist/{claude-sdk-session-53rcw5we.js → claude-sdk-session-38xnz21e.js} +8284 -8267
  14. package/dist/{cli-6eyncg0y.js → cli-018r4jq6.js} +34 -6
  15. package/dist/{cli-sds5zzs8.js → cli-1f3qzw6y.js} +78 -12
  16. package/dist/{cli-f0p0f6ja.js → cli-1sp861s4.js} +7 -7
  17. package/dist/{cli-dm8p2dpf.js → cli-1syn2xy1.js} +10 -10
  18. package/dist/{cli-tkf4x6ha.js → cli-37j04k05.js} +11 -11
  19. package/dist/{cli-v87dr6e3.js → cli-3a45zcgm.js} +1 -1
  20. package/dist/{cli-1n9gdg56.js → cli-43x64hv3.js} +4 -4
  21. package/dist/{cli-p0qerd2r.js → cli-4zfqgjc5.js} +2 -2
  22. package/dist/{cli-aa3tb29r.js → cli-7c9qs530.js} +1 -1
  23. package/dist/{cli-9qtjfd60.js → cli-848hxdwj.js} +1 -1
  24. package/dist/{cli-1xsraagq.js → cli-94gwyg7m.js} +8 -8
  25. package/dist/{cli-0dxxwfkt.js → cli-9hstj09h.js} +17 -14
  26. package/dist/{cli-1012xb7j.js → cli-9yqgm9nd.js} +22 -22
  27. package/dist/{cli-pygdsgh8.js → cli-a92r1127.js} +1 -1
  28. package/dist/{cli-8vverzf7.js → cli-ba8ne1es.js} +5 -0
  29. package/dist/{cli-bxrqvpg5.js → cli-be2161h1.js} +7 -7
  30. package/dist/{cli-npfe9t1s.js → cli-bnes4z6b.js} +2 -2
  31. package/dist/{cli-aypbg9r2.js → cli-cxxe61f6.js} +1 -1
  32. package/dist/{cli-vq9gqarq.js → cli-dk44dxec.js} +1 -1
  33. package/dist/{cli-836ehxmt.js → cli-eww120g8.js} +3 -2
  34. package/dist/{cli-a0kcty51.js → cli-f01h25yt.js} +1 -1
  35. package/dist/{cli-mzkwkbdy.js → cli-f0g3pgvf.js} +1 -1
  36. package/dist/{cli-1ma335fz.js → cli-ftc1g5tj.js} +2 -2
  37. package/dist/{cli-4yef8ghv.js → cli-fv5hepyy.js} +3 -3
  38. package/dist/{cli-58vwn100.js → cli-g35m2xwm.js} +4 -4
  39. package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
  40. package/dist/{cli-gx0j2khs.js → cli-j99azg3g.js} +3 -3
  41. package/dist/{cli-er83cjbn.js → cli-k8g16gz3.js} +416 -116
  42. package/dist/{cli-qp2m4btv.js → cli-kpw04va0.js} +2 -2
  43. package/dist/{cli-8y99kfey.js → cli-ms85309d.js} +33 -15
  44. package/dist/{cli-kgnkyabk.js → cli-n72rg53p.js} +1555 -645
  45. package/dist/{cli-e4d78xsv.js → cli-p14m2cnp.js} +1 -1
  46. package/dist/{cli-1eszr343.js → cli-qfxc84gh.js} +1 -1
  47. package/dist/{cli-wk28hzqn.js → cli-qwhy2qma.js} +1 -1
  48. package/dist/{cli-73ezfhha.js → cli-st8aw1m6.js} +228 -11
  49. package/dist/{cli-zvbb4n0e.js → cli-tw4p90jb.js} +92 -192
  50. package/dist/{cli-038fga5k.js → cli-v0yrfmdh.js} +148 -183
  51. package/dist/{cli-6aen3ezb.js → cli-v8fjakwf.js} +1 -1
  52. package/dist/{cli-99jez72r.js → cli-w9etnnxk.js} +6 -1
  53. package/dist/{cli-vcgscaan.js → cli-wt0jf9hm.js} +217 -80
  54. package/dist/{cli-s9rr4gr2.js → cli-xpa64rek.js} +1065 -305
  55. package/dist/{cli-h0mgxann.js → cli-z7d29rw9.js} +2 -2
  56. package/dist/cli.js +45 -20
  57. package/dist/{codex-adapter-2zrmab9b.js → codex-adapter-tn6k5mkg.js} +1 -1
  58. package/dist/{codex-session-runner-kbkz62vc.js → codex-session-runner-7vjt7jbw.js} +1 -1
  59. package/dist/{commands-hd574th6.js → commands-t7rz9146.js} +2 -2
  60. package/dist/{controller-pz5mcfg4.js → controller-m2jasma5.js} +2 -2
  61. package/dist/{controllers-connect-5rgq2hfk.js → controllers-connect-9ftpex99.js} +19 -19
  62. package/dist/{cursor-adapter-mgaprq0j.js → cursor-adapter-mjv79e4z.js} +1 -1
  63. package/dist/{db-6nf1207q.js → db-6sfrn9g3.js} +1 -1
  64. package/dist/{db-h06dxp05.js → db-rn673r06.js} +3 -3
  65. package/dist/{dispatcher-zthpy4m1.js → dispatcher-c155xz0p.js} +17 -17
  66. package/dist/{dsh-adapter-64dhw0j7.js → dsh-adapter-3mmszc1x.js} +1 -1
  67. package/dist/{external-channel-x01t7ke6.js → external-channel-35k6tkv8.js} +2 -2
  68. package/dist/{grok-adapter-h21jv594.js → grok-adapter-tfrq7tg1.js} +2 -3
  69. package/dist/handlers-7gw64dxw.js +39 -0
  70. package/dist/{heartbeat-zzfksckz.js → heartbeat-dyg4npkp.js} +20 -20
  71. package/dist/{hook-z4zhzj1f.js → hook-drspbwpe.js} +1 -1
  72. package/dist/{hook-3xfqhh2s.js → hook-x26b31bg.js} +1 -1
  73. package/dist/{http-q813tmya.js → http-pk972xyw.js} +571 -294
  74. package/dist/{index-va3y63es.js → index-6srwvd5b.js} +1 -2
  75. package/dist/{index-x8xbyn1t.js → index-arytgj5q.js} +1 -1
  76. package/dist/{index-wbbs14c5.js → index-bgdyvcnv.js} +17 -17
  77. package/dist/{index-s98037v0.js → index-cbxkmfc5.js} +1 -2
  78. package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
  79. package/dist/{index-m0a4ydqj.js → index-gabbcdn2.js} +17 -17
  80. package/dist/{index-220tk9bw.js → index-jwxkkex2.js} +139 -76
  81. package/dist/index-tfb26zhy.js +41 -0
  82. package/dist/{keepalive-y5t1my7m.js → keepalive-7yrng7px.js} +3 -3
  83. package/dist/{lead-2xwtq66b.js → lead-7e02z839.js} +29 -30
  84. package/dist/{maintenance-ybfsaedv.js → maintenance-e6cemdqt.js} +1 -1
  85. package/dist/{node-3v3f08f7.js → node-de6akczr.js} +10 -9
  86. package/dist/{oauth-refresh-sweep-909jmn12.js → oauth-refresh-sweep-yw3xdbcj.js} +2 -2
  87. package/dist/{onboard-xn6fkkys.js → onboard-0a9y5yrc.js} +2 -2
  88. package/dist/{otel-impl-e6mn0z13.js → otel-impl-trjpkfb7.js} +1 -1
  89. package/dist/{pricing-refresh-ygd362ep.js → pricing-refresh-d2b5my0y.js} +3 -3
  90. package/dist/{rbac-roles-mb470p4e.js → rbac-roles-hey3yv9d.js} +2 -2
  91. package/dist/{rbac-roles-c9axvfnz.js → rbac-roles-nxh5ahwk.js} +2 -2
  92. package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
  93. package/dist/{render-v2-g0306nh1.js → render-v2-rr3p2atg.js} +3 -3
  94. package/dist/{resume-v315hnq6.js → resume-2efkf1s1.js} +5 -5
  95. package/dist/{session-access-2zg285a1.js → session-access-s2mttxpw.js} +6 -4
  96. package/dist/{setup-a4mtf02k.js → setup-vypma5yt.js} +1 -1
  97. package/dist/{work-objects-9fr0fgtn.js → work-objects-rvhckdkp.js} +20 -20
  98. package/dist/{worker-qae22jeb.js → worker-5snytpbd.js} +29 -30
  99. package/dist/worker-token-xf11yyds.js +136 -0
  100. package/openapi.json +303 -13
  101. package/package.json +8 -8
  102. package/src/apps/action-runner.ts +4 -0
  103. package/src/apps/catalog.generated.json +209 -4
  104. package/src/apps/page-validator.ts +21 -3
  105. package/src/be/chunking.ts +42 -9
  106. package/src/be/db/tasks/draft-text.ts +15 -0
  107. package/src/be/db/tasks/read.ts +14 -1
  108. package/src/be/db.ts +5 -1
  109. package/src/be/memory/index-content.ts +36 -17
  110. package/src/be/memory/providers/openai-embedding.ts +115 -61
  111. package/src/be/memory/providers/sqlite-store.ts +69 -23
  112. package/src/be/memory/reranker.ts +31 -1
  113. package/src/be/memory/search-candidates.ts +47 -0
  114. package/src/be/memory/types.ts +14 -0
  115. package/src/be/migrations/232_agent_session_tokens.sql +41 -0
  116. package/src/be/scripts/typecheck.ts +2 -0
  117. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +116 -55
  118. package/src/be/seed-scripts/index.ts +2 -2
  119. package/src/be/swarm-config-guard.ts +40 -1
  120. package/src/be/task-attachment-store.ts +30 -0
  121. package/src/be/users.ts +169 -12
  122. package/src/cli.tsx +24 -0
  123. package/src/commands/worker-token.ts +164 -0
  124. package/src/extensions/catalog.generated.json +4 -4
  125. package/src/extensions/contract-types.generated.ts +1 -1
  126. package/src/extensions/contract.ts +7 -0
  127. package/src/fs/provider.ts +13 -1
  128. package/src/heartbeat/heartbeat.ts +31 -11
  129. package/src/http/apps.ts +43 -5
  130. package/src/http/codex-oauth-keep-warm.ts +27 -9
  131. package/src/http/config.ts +5 -3
  132. package/src/http/core.ts +2 -0
  133. package/src/http/fs.ts +54 -34
  134. package/src/http/mcp-body.ts +37 -0
  135. package/src/http/mcp-user.ts +3 -5
  136. package/src/http/mcp.ts +6 -7
  137. package/src/http/memory.ts +26 -15
  138. package/src/http/page-proxy.ts +61 -5
  139. package/src/http/pages-public.ts +27 -44
  140. package/src/http/pages.ts +4 -4
  141. package/src/http/script-runs.ts +2 -0
  142. package/src/http/session-access.ts +51 -2
  143. package/src/http/sessions.ts +249 -10
  144. package/src/http/tasks.ts +4 -32
  145. package/src/http/users.ts +12 -1
  146. package/src/integrations/chat-sdk/adapters.ts +7 -3
  147. package/src/integrations/chat-sdk/attachments.ts +90 -12
  148. package/src/integrations/chat-sdk/facts.ts +120 -0
  149. package/src/integrations/chat-sdk/inbound.ts +156 -8
  150. package/src/integrations/chat-sdk/limits.ts +24 -0
  151. package/src/integrations/chat-sdk/outbound-files.ts +186 -0
  152. package/src/integrations/chat-sdk/outbound.ts +234 -4
  153. package/src/integrations/chat-sdk/relay.ts +4 -1
  154. package/src/integrations/chat-sdk/router.ts +28 -2
  155. package/src/integrations/chat-sdk/steering.ts +9 -0
  156. package/src/integrations/chat-sdk/templates.ts +14 -0
  157. package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
  158. package/src/mcp-limits.ts +20 -0
  159. package/src/pages/browser-sdk.ts +8 -3
  160. package/src/pages/feedback-overlay.ts +1 -1
  161. package/src/providers/acp-adapter.ts +33 -13
  162. package/src/providers/acp-swarm-events.ts +1 -1
  163. package/src/providers/amp-adapter.ts +1 -1
  164. package/src/providers/codex-adapter.ts +106 -119
  165. package/src/providers/codex-agents-md.ts +35 -229
  166. package/src/providers/codex-oauth/storage.ts +1 -1
  167. package/src/providers/cursor-adapter.ts +1 -1
  168. package/src/rbac/legacy-policy.ts +3 -1
  169. package/src/rbac/permissions.ts +4 -0
  170. package/src/realtime/transport.ts +1 -2
  171. package/src/script-workflows/workflow-ctx.ts +1 -0
  172. package/src/scripts-runtime/types/stdlib.d.ts +2 -0
  173. package/src/scripts-runtime/types/swarm-extension.d.ts +8 -0
  174. package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
  175. package/src/server-user.ts +2 -0
  176. package/src/server.ts +2 -0
  177. package/src/slack/assistant.ts +2 -2
  178. package/src/slack/files.ts +2 -0
  179. package/src/slack/handlers.ts +4 -4
  180. package/src/slack/inbound-files.ts +39 -6
  181. package/src/slack/thread-buffer.ts +2 -1
  182. package/src/tasks/task-terminal-effects.ts +21 -17
  183. package/src/tests/acp-adapter.test.ts +10 -23
  184. package/src/tests/acp-fake-agent.ts +36 -0
  185. package/src/tests/acp-swarm-events.test.ts +1 -1
  186. package/src/tests/agent-session-tokens.test.ts +452 -0
  187. package/src/tests/apps-action-parent-task.test.ts +295 -0
  188. package/src/tests/apps-card-grid.test.ts +197 -0
  189. package/src/tests/apps-catalog-additions.test.ts +120 -0
  190. package/src/tests/apps-tree.test.ts +129 -0
  191. package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
  192. package/src/tests/chat-sdk-config.test.ts +11 -0
  193. package/src/tests/chat-sdk-facts.test.ts +23 -0
  194. package/src/tests/chat-sdk-inbound.test.ts +558 -4
  195. package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
  196. package/src/tests/codex-adapter.test.ts +167 -242
  197. package/src/tests/codex-oauth-keep-warm.test.ts +85 -1
  198. package/src/tests/config-api-only-keys.test.ts +9 -0
  199. package/src/tests/config-session-auth.test.ts +14 -0
  200. package/src/tests/cors-allowlist.test.ts +1 -1
  201. package/src/tests/docs-index-footprint.test.js +2 -2
  202. package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
  203. package/src/tests/grok-adapter.test.ts +3 -3
  204. package/src/tests/heartbeat-checklist.test.ts +33 -0
  205. package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
  206. package/src/tests/heartbeat-jev-gate.test.ts +265 -1
  207. package/src/tests/kv-page-proxy.test.ts +91 -5
  208. package/src/tests/launch-password-rejection.test.ts +2 -2
  209. package/src/tests/mcp-input-caps.test.ts +294 -0
  210. package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
  211. package/src/tests/memory-embedding-input-cap.test.ts +114 -0
  212. package/src/tests/memory.test.ts +14 -0
  213. package/src/tests/migration-232-agent-session-tokens.test.ts +67 -0
  214. package/src/tests/page-guest-session.test.ts +19 -8
  215. package/src/tests/page-proxy-authed.test.ts +1 -1
  216. package/src/tests/page-proxy.test.ts +17 -10
  217. package/src/tests/page-session.test.ts +102 -1
  218. package/src/tests/pages-authed-mode.test.ts +82 -12
  219. package/src/tests/pages-password-mode.test.ts +5 -5
  220. package/src/tests/rbac-engine.test.ts +1 -0
  221. package/src/tests/realtime-transport.test.ts +13 -7
  222. package/src/tests/runtime-child-token-cleanup.test.ts +3 -3
  223. package/src/tests/script-connections.test.ts +2 -0
  224. package/src/tests/script-runs-http.test.ts +84 -0
  225. package/src/tests/scripts-http.test.ts +10 -1
  226. package/src/tests/session-route-posture.test.ts +2 -0
  227. package/src/tests/swarm-config-export-to-env.test.ts +22 -3
  228. package/src/tests/task-audio-transcription.test.ts +299 -0
  229. package/src/tests/task-completion-memory-chunking.test.ts +434 -0
  230. package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
  231. package/src/tests/transcription.test.ts +299 -0
  232. package/src/tests/worker-token-cli.test.ts +76 -0
  233. package/src/tests/workflow-executors.test.ts +4 -2
  234. package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
  235. package/src/tests/workflow-system-one-decision.test.ts +2 -1
  236. package/src/tests/workflow-system-one-extraction.test.ts +805 -0
  237. package/src/tools/chat-access.ts +10 -0
  238. package/src/tools/chat-post.ts +31 -11
  239. package/src/tools/chat-reply.ts +32 -10
  240. package/src/tools/memory-search.ts +3 -1
  241. package/src/tools/system-one-decide.ts +17 -4
  242. package/src/types.ts +8 -1
  243. package/src/utils/config-env-export.ts +3 -1
  244. package/src/utils/page-session.ts +133 -20
  245. package/src/utils/request-auth-context.ts +2 -1
  246. package/src/utils/transcription.ts +246 -0
  247. package/src/workflows/definition.ts +7 -1
  248. package/src/workflows/engine.ts +18 -12
  249. package/src/workflows/executors/index.ts +1 -0
  250. package/src/workflows/executors/registry.ts +2 -0
  251. package/src/workflows/executors/system-one-decision.ts +84 -262
  252. package/src/workflows/executors/system-one-extract-wire.ts +57 -0
  253. package/src/workflows/executors/system-one-extraction.ts +830 -0
  254. package/src/workflows/executors/system-one-providers.ts +316 -114
  255. package/src/workflows/executors/system-one-transport.ts +264 -0
  256. package/templates/extensions/slack-reply-gate/README.md +24 -22
  257. package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
  258. package/templates/extensions/slack-reply-gate/manifest.json +2 -2
  259. package/templates/official/lead/HEARTBEAT.md +1 -1
  260. package/templates/skills/apps/SKILL.md +31 -7
  261. package/templates/skills/apps/content.md +31 -7
  262. package/templates/skills/chat-sdk/content.md +6 -2
  263. package/templates/skills/code-quality/content.md +2 -1
  264. package/templates/skills/heartbeat-runbook/content.md +8 -8
  265. package/templates/skills/pages/content.md +6 -4
  266. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  267. package/templates/skills/swarm-scripts/content.md +2 -2
  268. package/dist/cli-v1sjhn52.js +0 -7
  269. package/dist/handlers-2qjw434w.js +0 -39
  270. package/dist/index-d0g18bax.js +0 -41
@@ -1,6 +1,15 @@
1
+ import type { Attachment, FileUpload } from "chat";
1
2
  import { scrubSecrets } from "@/utils/secret-scrubber";
2
3
  import { workflowEventBus } from "@/workflows/event-bus";
3
- import { chatMessageLimit, splitChatMessage } from "./limits";
4
+ import { formatFileSize } from "./attachments";
5
+ import {
6
+ CHAT_FILES_PER_MESSAGE,
7
+ chatMessageLimit,
8
+ chatUploadLimit,
9
+ splitChatMessage,
10
+ TELEGRAM_PHOTO_LIMIT,
11
+ } from "./limits";
12
+ import { errorMessage } from "./logger";
4
13
  import { getChatInstance } from "./runtime";
5
14
  import { assertTrustedChatTarget } from "./teams-origin";
6
15
 
@@ -39,12 +48,40 @@ export interface ChatPostResult {
39
48
  adapter: string;
40
49
  threadId: string;
41
50
  channelId: string;
42
- /** One id per chunk, in send order. */
51
+ /** One id per chunk and per file message, in send order. */
43
52
  messageIds: string[];
53
+ /** Files that did not reach the thread. The thread got a note naming each one. */
54
+ unsentFiles: UnsentChatFile[];
55
+ }
56
+
57
+ /** A file to send after the text. `load` reads its bytes. */
58
+ export interface OutboundChatFile {
59
+ name: string;
60
+ mimeType?: string;
61
+ /** Size known before loading, so an oversized file is never read. */
62
+ sizeBytes?: number;
63
+ /** Throws `ChatFileTooLargeError` when the file is larger than `maxBytes`. */
64
+ load(maxBytes: number): Promise<{ data: Uint8Array; mimeType?: string }>;
65
+ }
66
+
67
+ export interface UnsentChatFile {
68
+ name: string;
69
+ reason: string;
70
+ }
71
+
72
+ export class ChatFileTooLargeError extends Error {
73
+ constructor(readonly sizeBytes: number) {
74
+ super(`file is ${formatFileSize(sizeBytes)}`);
75
+ this.name = "ChatFileTooLargeError";
76
+ }
44
77
  }
45
78
 
46
79
  interface PostTarget {
47
- post(message: { markdown: string }): Promise<{ id: string; threadId?: string }>;
80
+ post(message: {
81
+ markdown: string;
82
+ files?: FileUpload[];
83
+ attachments?: Attachment[];
84
+ }): Promise<{ id: string; threadId?: string }>;
48
85
  }
49
86
 
50
87
  async function postChunks(
@@ -62,10 +99,186 @@ async function postChunks(
62
99
  return { messageIds, threadId };
63
100
  }
64
101
 
102
+ type LoadedFile = { name: string; mimeType: string; data: Buffer };
103
+
104
+ /** First usable MIME type, else a guess from the file extension. */
105
+ function resolveMimeType(name: string, ...candidates: (string | null | undefined)[]): string {
106
+ for (const candidate of candidates) {
107
+ const type = candidate?.split(";")[0]?.trim().toLowerCase();
108
+ if (type && type !== "application/octet-stream") return type;
109
+ }
110
+ return Bun.file(name).type.split(";")[0] ?? "application/octet-stream";
111
+ }
112
+
113
+ /**
114
+ * How Telegram shows a file. Only types its media methods take are sent as
115
+ * photo, video or audio; everything else, and a photo over 10 MB, goes as a
116
+ * document.
117
+ */
118
+ export function telegramAttachmentType(mimeType: string, sizeBytes: number): Attachment["type"] {
119
+ if (
120
+ (mimeType === "image/jpeg" || mimeType === "image/png") &&
121
+ sizeBytes <= TELEGRAM_PHOTO_LIMIT
122
+ ) {
123
+ return "image";
124
+ }
125
+ if (mimeType === "video/mp4") return "video";
126
+ if (mimeType === "audio/mpeg" || mimeType === "audio/mp4" || mimeType === "audio/x-m4a") {
127
+ return "audio";
128
+ }
129
+ return "file";
130
+ }
131
+
132
+ /**
133
+ * Telegram media groups mix photos and videos only; documents and audio group
134
+ * with their own kind. Other adapters put any files in one message.
135
+ */
136
+ function fileGroupKey(adapter: string, file: LoadedFile): string {
137
+ if (adapter !== "telegram") return "files";
138
+ const type = telegramAttachmentType(file.mimeType, file.data.byteLength);
139
+ return type === "image" || type === "video" ? "visual" : type;
140
+ }
141
+
142
+ function fileMessage(adapter: string, files: LoadedFile[]) {
143
+ // Telegram uploads `files` only as documents; typed `attachments` become
144
+ // photos, videos and audio. A list of 2 to 10 is sent as one media group.
145
+ if (adapter === "telegram") {
146
+ return {
147
+ markdown: "",
148
+ attachments: files.map(
149
+ (file): Attachment => ({
150
+ type: telegramAttachmentType(file.mimeType, file.data.byteLength),
151
+ name: file.name,
152
+ mimeType: file.mimeType,
153
+ size: file.data.byteLength,
154
+ data: file.data,
155
+ }),
156
+ ),
157
+ };
158
+ }
159
+ return {
160
+ markdown: "",
161
+ files: files.map((file) => ({ data: file.data, filename: file.name, mimeType: file.mimeType })),
162
+ };
163
+ }
164
+
165
+ function platformName(adapter: string): string {
166
+ return adapter.charAt(0).toUpperCase() + adapter.slice(1);
167
+ }
168
+
169
+ /**
170
+ * Send files as file messages, up to 10 per message, grouped the way the
171
+ * platform allows. Files are read one by one and a group is sent once full, so
172
+ * at most one group is held in memory. Never throws: a file that is too large,
173
+ * cannot be read, or is refused by the platform lands in `unsent`.
174
+ */
175
+ async function postFiles(
176
+ adapter: string,
177
+ target: PostTarget,
178
+ files: OutboundChatFile[],
179
+ ): Promise<{ messageIds: string[]; unsent: UnsentChatFile[] }> {
180
+ const limit = chatUploadLimit(adapter);
181
+ const tooLarge = (size: number) =>
182
+ `${formatFileSize(size)}, over the ${formatFileSize(limit)} upload limit of ${platformName(adapter)}`;
183
+ const messageIds: string[] = [];
184
+ const unsent: UnsentChatFile[] = [];
185
+ const groups = new Map<string, LoadedFile[]>();
186
+
187
+ const send = async (group: LoadedFile[]) => {
188
+ try {
189
+ const sent = await target.post(fileMessage(adapter, group));
190
+ messageIds.push(sent.id);
191
+ } catch (error) {
192
+ const reason = `refused by ${platformName(adapter)} (${scrubSecrets(errorMessage(error))})`;
193
+ for (const file of group) unsent.push({ name: file.name, reason });
194
+ }
195
+ };
196
+
197
+ for (const file of files) {
198
+ if (file.sizeBytes !== undefined && file.sizeBytes > limit) {
199
+ unsent.push({ name: file.name, reason: tooLarge(file.sizeBytes) });
200
+ continue;
201
+ }
202
+ let loaded: LoadedFile;
203
+ try {
204
+ const { data, mimeType } = await file.load(limit);
205
+ if (data.byteLength > limit) throw new ChatFileTooLargeError(data.byteLength);
206
+ loaded = {
207
+ name: file.name,
208
+ mimeType: resolveMimeType(file.name, file.mimeType, mimeType),
209
+ data: Buffer.from(data.buffer, data.byteOffset, data.byteLength),
210
+ };
211
+ } catch (error) {
212
+ unsent.push({
213
+ name: file.name,
214
+ reason:
215
+ error instanceof ChatFileTooLargeError
216
+ ? tooLarge(error.sizeBytes)
217
+ : `could not be read (${scrubSecrets(errorMessage(error))})`,
218
+ });
219
+ continue;
220
+ }
221
+ const key = fileGroupKey(adapter, loaded);
222
+ const group = [...(groups.get(key) ?? []), loaded];
223
+ if (group.length === CHAT_FILES_PER_MESSAGE) {
224
+ groups.delete(key);
225
+ await send(group);
226
+ } else {
227
+ groups.set(key, group);
228
+ }
229
+ }
230
+ for (const group of groups.values()) await send(group);
231
+ return { messageIds, unsent };
232
+ }
233
+
234
+ /** Tool-result suffix naming the files that did not go out; empty when all did. */
235
+ export function unsentFilesSummary(unsent: UnsentChatFile[]): string {
236
+ return unsent.length === 0 ? "" : ` ${formatUnsentFilesNote(unsent)}`;
237
+ }
238
+
239
+ /** One line of untrusted text: control characters and line breaks become spaces. */
240
+ function singleLine(text: string): string {
241
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping them is the point
242
+ return text.replace(/[\u0000-\u001f\u007f\u2028\u2029]+/g, " ").trim();
243
+ }
244
+
245
+ /** A zero-width space after `@` and `<!` keeps adapters from turning text into mentions. */
246
+ function breakMentions(text: string): string {
247
+ return text.replace(/@/g, "@\u200b").replace(/<!/g, "<\u200b!");
248
+ }
249
+
250
+ /**
251
+ * A file name as a Markdown code span, so it renders literally: no emphasis,
252
+ * links, or mentions. The fence is one backtick longer than the longest run
253
+ * inside, and the padding keeps an edge backtick from joining the fence.
254
+ */
255
+ function codeSpan(text: string): string {
256
+ const value = breakMentions(singleLine(text)) || "(unnamed)";
257
+ const longestRun = Math.max(0, ...(value.match(/`+/g) ?? []).map((run) => run.length));
258
+ const fence = "`".repeat(longestRun + 1);
259
+ const pad = value.startsWith("`") || value.endsWith("`") ? " " : "";
260
+ return `${fence}${pad}${value}${pad}${fence}`;
261
+ }
262
+
263
+ /**
264
+ * The note posted after the files when some did not go out. File names come
265
+ * from senders and agents, so they render as code spans and cannot add
266
+ * formatting or mentions to the bot's message.
267
+ */
268
+ export function formatUnsentFilesNote(unsent: UnsentChatFile[]): string {
269
+ const lines = unsent.map(
270
+ (file) => `- ${codeSpan(file.name)}: ${breakMentions(singleLine(file.reason))}`,
271
+ );
272
+ const count = unsent.length === 1 ? "1 file" : `${unsent.length} files`;
273
+ return [`Could not send ${count}:`, ...lines].join("\n");
274
+ }
275
+
65
276
  /**
66
277
  * Post markdown to a Chat SDK thread or channel. Scrubs the text, splits it by
67
278
  * the adapter limit, and emits one `chat.message.sent` for the whole message
68
- * once every chunk was accepted. Throws `ChatAdapterUnavailableError` when the
279
+ * once every chunk was accepted. `files` go out after the text; a file that
280
+ * does not reach the thread is named in a note there and in `unsentFiles`,
281
+ * and does not fail the call. Throws `ChatAdapterUnavailableError` when the
69
282
  * adapter is not running, `UntrustedTeamsServiceUrlError` before any send
70
283
  * when a Teams id points at a host that is not Bot Framework, and the adapter
71
284
  * error when a send fails.
@@ -80,6 +293,7 @@ export async function postChatMessage(args: {
80
293
  channelId?: string;
81
294
  text: string;
82
295
  taskId?: string;
296
+ files?: OutboundChatFile[];
83
297
  }): Promise<ChatPostResult> {
84
298
  // Ids come from tools and stored context keys. Check the Teams origin
85
299
  // before the adapter sends its token.
@@ -89,28 +303,44 @@ export async function postChatMessage(args: {
89
303
  const text = scrubSecrets(args.text);
90
304
 
91
305
  let result: ChatPostResult;
306
+ let target: PostTarget;
92
307
  if (args.threadId) {
93
308
  const thread = chat.thread(args.threadId);
309
+ target = thread;
94
310
  const sent = await postChunks(args.adapter, thread, text);
95
311
  result = {
96
312
  adapter: args.adapter,
97
313
  threadId: thread.id,
98
314
  channelId: thread.channelId,
99
315
  messageIds: sent.messageIds,
316
+ unsentFiles: [],
100
317
  };
101
318
  } else if (args.channelId) {
102
319
  const channel = chat.channel(args.channelId);
320
+ target = channel;
103
321
  const sent = await postChunks(args.adapter, channel, text);
104
322
  result = {
105
323
  adapter: args.adapter,
106
324
  threadId: sent.threadId ?? channel.id,
107
325
  channelId: channel.id,
108
326
  messageIds: sent.messageIds,
327
+ unsentFiles: [],
109
328
  };
110
329
  } else {
111
330
  throw new Error("postChatMessage needs a threadId or a channelId");
112
331
  }
113
332
 
333
+ if (args.files?.length) {
334
+ const sent = await postFiles(args.adapter, target, args.files);
335
+ result.messageIds.push(...sent.messageIds);
336
+ result.unsentFiles = sent.unsent;
337
+ if (sent.unsent.length > 0) {
338
+ const note = formatUnsentFilesNote(sent.unsent);
339
+ const noted = await postChunks(args.adapter, target, scrubSecrets(note));
340
+ result.messageIds.push(...noted.messageIds);
341
+ }
342
+ }
343
+
114
344
  emitChatMessageSent({
115
345
  adapter: result.adapter,
116
346
  channelId: result.channelId,
@@ -12,6 +12,7 @@ import { scrubSecrets } from "@/utils/secret-scrubber";
12
12
  import { workflowEventBus } from "@/workflows/event-bus";
13
13
  import { errorMessage } from "./logger";
14
14
  import { postChatMessage } from "./outbound";
15
+ import { taskOutputChatFiles } from "./outbound-files";
15
16
  import { getChatInstance, getChatSdkHealth } from "./runtime";
16
17
  import { sweepExpired } from "./state";
17
18
  import { stopTypingKeepalive } from "./typing";
@@ -49,7 +50,8 @@ export function formatChatRelayText(task: AgentTask): string {
49
50
  *
50
51
  * Delivery is at-least-once: a chunk failure re-posts the earlier chunks on
51
52
  * retry, and a crash between the send and `markChatRelayDelivered` re-sends the
52
- * message.
53
+ * message. The task's output attachments go after the text; a file that cannot
54
+ * be sent gets a note in the thread and does not fail the delivery.
53
55
  *
54
56
  * Once `signal` aborts, no further row is claimed. A send already in flight is
55
57
  * not cancelled (`postChatMessage` takes no signal); its row keeps the claim
@@ -99,6 +101,7 @@ export async function processPendingChatRelays(
99
101
  threadId: target.threadId,
100
102
  text: formatChatRelayText(task),
101
103
  taskId: task.id,
104
+ files: await taskOutputChatFiles(task.id),
102
105
  });
103
106
  stopTypingKeepalive(target.threadId);
104
107
  await markChatRelayDelivered(task.id, now);
@@ -83,6 +83,11 @@ export async function deleteChatChannelMapping(
83
83
  return deleteKv(CHAT_CHANNELS_NAMESPACE, chatChannelKey(adapter, channelId));
84
84
  }
85
85
 
86
+ /** True when the text carries a `swarm#<uuid>` or `swarm#all` routing token. */
87
+ export function hasRoutingToken(text: string): boolean {
88
+ return /swarm#(?:[a-f0-9-]{36}|all)/i.test(text);
89
+ }
90
+
86
91
  /** Remove `swarm#<uuid>` and `swarm#all` routing tokens from task text. */
87
92
  export function stripRoutingTokens(text: string): string {
88
93
  return text
@@ -91,6 +96,20 @@ export function stripRoutingTokens(text: string): string {
91
96
  .trim();
92
97
  }
93
98
 
99
+ const DEFAULT_OWNER_WINDOW_MINUTES = 15;
100
+
101
+ /**
102
+ * Start of the thread-owner window: a thread keeps its owner while the
103
+ * owner's task is open and for `CHAT_SDK_OWNER_WINDOW_MIN` (default 15,
104
+ * 0 = only while open) after it ends. Read per call, so a change applies at once.
105
+ */
106
+ export function chatOwnerWindowStart(now: number = Date.now()): string {
107
+ const raw = process.env.CHAT_SDK_OWNER_WINDOW_MIN?.trim();
108
+ const value = raw ? Number(raw) : Number.NaN;
109
+ const minutes = Number.isFinite(value) && value >= 0 ? value : DEFAULT_OWNER_WINDOW_MINUTES;
110
+ return new Date(now - minutes * 60_000).toISOString();
111
+ }
112
+
94
113
  async function leadMatch(reachable: Agent[], matchedText: string): Promise<ChatAgentMatch> {
95
114
  const lead = reachable.find((agent) => agent.isLead) ?? (await getLeadAgent());
96
115
  return { agent: lead, reason: "skill", matchedText };
@@ -99,7 +118,8 @@ async function leadMatch(reachable: Agent[], matchedText: string): Promise<ChatA
99
118
  /**
100
119
  * Port of `src/slack/router.ts` for Chat SDK threads. Order: `swarm#<uuid>`,
101
120
  * `swarm#all`, KV channel mapping (agent, or workflow for a mention or DM), the
102
- * thread owner (lead when the owner is offline), the lead for a mention or DM.
121
+ * thread owner (inside its window in a Telegram group; lead when the owner is
122
+ * offline), the lead for a mention or DM.
103
123
  * Returns null when the message is not addressed to the swarm.
104
124
  */
105
125
  export async function routeChatMessage(
@@ -148,7 +168,13 @@ export async function routeChatMessage(
148
168
  );
149
169
  }
150
170
 
151
- const owner = await getAgentWorkingOnContextKey(chatSdkContextKey(ctx.threadId), "chat");
171
+ // Only a Telegram group expires its owner: one shared conversation, where a
172
+ // stale owner would catch the next topic. DMs and threads keep theirs.
173
+ const owner = await getAgentWorkingOnContextKey(
174
+ chatSdkContextKey(ctx.threadId),
175
+ "chat",
176
+ ctx.adapter === "telegram" && !ctx.isDirectMessage ? chatOwnerWindowStart() : undefined,
177
+ );
152
178
  if (owner && (await isAgentReachable(owner))) {
153
179
  return {
154
180
  kind: "agents",
@@ -14,15 +14,23 @@ export interface ChatThreadSteeringResult {
14
14
  * rule as the Slack default (`SLACK_THREAD_STEERING=lead`, queue mode) as
15
15
  * constants: `isSteeringEnabled()` is the only switch. Returns null when the
16
16
  * caller should buffer the message into a follow-up task instead.
17
+ *
18
+ * Only the task's own requester steers it: a steering row carries the task's
19
+ * attribution, and an undeliverable one is promoted to a follow-up of this
20
+ * task. `resolveSender` returns the sender's canonical user, undefined when
21
+ * unknown; it runs only when there is a task to steer.
17
22
  */
18
23
  export async function requestChatThreadSteering(args: {
19
24
  contextKey: string;
20
25
  message: string;
26
+ resolveSender: () => Promise<string | undefined>;
21
27
  }): Promise<ChatThreadSteeringResult | null> {
22
28
  if (!isSteeringEnabled()) return null;
23
29
 
24
30
  const task = await getLatestLeadTaskByContextKey(args.contextKey, "chat");
25
31
  if (!task || task.status !== "in_progress") return null;
32
+ const senderUserId = await args.resolveSender();
33
+ if (senderUserId !== task.requestedByUserId) return null;
26
34
 
27
35
  try {
28
36
  const result = await requestSteering({
@@ -32,6 +40,7 @@ export async function requestChatThreadSteering(args: {
32
40
  onUnsupported: "degrade",
33
41
  source: "chat",
34
42
  createdByKind: "user",
43
+ createdByUserId: senderUserId,
35
44
  });
36
45
  return { task, result };
37
46
  } catch (error) {
@@ -14,3 +14,17 @@ registerTemplate({
14
14
  ],
15
15
  category: "system",
16
16
  });
17
+
18
+ registerTemplate({
19
+ eventType: "chat.message.header",
20
+ header: "",
21
+ defaultBody: "[chat] {{chat_facts}}",
22
+ variables: [
23
+ {
24
+ name: "chat_facts",
25
+ description:
26
+ "Adapter, chat type and title, forum topic, sender, why the message was taken (addressed) and the replied-to snippet, joined with ' · '",
27
+ },
28
+ ],
29
+ category: "system",
30
+ });
@@ -1,6 +1,7 @@
1
1
  import type { Attachment } from "chat";
2
2
  import { type AdditiveBuffer, createAdditiveBuffer } from "@/tasks/additive-buffer";
3
3
  import { trackBackground } from "./background";
4
+ import type { ChatAddressedReason, ChatMessageFacts } from "./facts";
4
5
  import { flushChatThread } from "./inbound";
5
6
  import { stopTypingKeepalive } from "./typing";
6
7
 
@@ -14,6 +15,9 @@ export interface ChatBufferedMessage {
14
15
  userName?: string;
15
16
  botMentioned: boolean;
16
17
  isDirectMessage: boolean;
18
+ /** Why the message was taken; `none` when it is unaddressed. */
19
+ addressed: ChatAddressedReason;
20
+ facts: ChatMessageFacts;
17
21
  attachments: Attachment[];
18
22
  }
19
23
 
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Input caps shared by both MCP surfaces (`/mcp` and `/mcp-user`).
3
+ *
4
+ * The SDK leaves both bounds off when you build the server yourself
5
+ * (GHSA-46jr-4wx8-v453): `McpServer` only counts `tools/call` arguments when
6
+ * `maxToolInputElements` is set, and our HTTP handlers read the request body
7
+ * themselves, so the transport's own body cap never runs.
8
+ */
9
+
10
+ /**
11
+ * Largest combined number of array elements and object members a single
12
+ * `tools/call` `arguments` payload may contain. Sized for the largest
13
+ * legitimate call: `app-rows-create` takes up to 500 rows, which is about
14
+ * 24,000 elements at 48 fields per row. Script SDK calls go through
15
+ * `/api/mcp-bridge`, which does not apply it.
16
+ */
17
+ export const MCP_MAX_TOOL_INPUT_ELEMENTS = 25_000;
18
+
19
+ /** Largest MCP HTTP request body we read, in bytes. Matches the SDK default (4 MiB). */
20
+ export const MCP_MAX_REQUEST_BODY_BYTES = 4 * 1024 * 1024;
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Exposes a domain-grouped API on `window.SwarmSDK` (class) and a ready-to-use
4
4
  // singleton `window.swarmSdk`. All calls route through the `/@swarm/api/*`
5
- // proxy, which strips the page-session cookie and forwards to `/api/*` with
5
+ // proxy, which strips the page's own session cookie and forwards to `/api/*` with
6
6
  // a server-side bearer and signed page session. Memory operations use the
7
7
  // page owner's agent scope. Viewer identity and permissions remain separate.
8
8
  // The browser never handles the server bearer.
@@ -30,10 +30,15 @@ class SwarmSDK {
30
30
  this.channel = async (name) => (await loadRealtime()).channel(name);
31
31
 
32
32
  const base = '/@swarm/api';
33
+ // Each page has its own session cookie. Naming the page tells the proxy
34
+ // which one to read; the cookie's signed payload must still match it.
35
+ const pathParts = typeof location === 'undefined' ? [] : location.pathname.split('/');
36
+ const pageId = pathParts[1] === 'p' && pathParts[2] ? pathParts[2].split('.')[0] : '';
33
37
  const call = async (method, path, body) => {
34
- const init = { method };
38
+ const init = { method, headers: {} };
39
+ if (pageId) init.headers['X-Swarm-Page-Id'] = pageId;
35
40
  if (body !== undefined) {
36
- init.headers = { 'Content-Type': 'application/json' };
41
+ init.headers['Content-Type'] = 'application/json';
37
42
  init.body = JSON.stringify(body);
38
43
  }
39
44
  const res = await fetch(base + path, init);
@@ -415,7 +415,7 @@ const FEEDBACK_OVERLAY_JS = `
415
415
  return fetch('/@swarm/api/pages/' + encodeURIComponent(pageId) + '/feedback', {
416
416
  method: 'POST',
417
417
  credentials: 'same-origin',
418
- headers: { 'Content-Type': 'application/json' },
418
+ headers: { 'Content-Type': 'application/json', 'X-Swarm-Page-Id': pageId },
419
419
  body: JSON.stringify(payload),
420
420
  }).then(function (res) {
421
421
  return res.text().then(function (text) {
@@ -1,15 +1,21 @@
1
+ // `/unstable` keeps the full ACP v1 surface the 1.x default import carried:
2
+ // turn `usage` on the prompt response is unstable, and the stable entrypoint
3
+ // does not type it. Both entrypoints speak ACP v1 on the wire.
1
4
  import {
2
- type Client,
3
- ClientSideConnection,
5
+ client as acpClient,
6
+ type ClientContext,
4
7
  type McpServer,
8
+ methods,
5
9
  ndJsonStream,
6
10
  PROTOCOL_VERSION,
7
11
  type RequestPermissionRequest,
8
12
  type RequestPermissionResponse,
9
13
  type SessionConfigOption,
10
14
  type SessionNotification,
15
+ type SetSessionConfigOptionRequest,
16
+ type SetSessionConfigOptionResponse,
11
17
  type Usage,
12
- } from "@agentclientprotocol/sdk";
18
+ } from "@agentclientprotocol/sdk/unstable";
13
19
  import pkg from "../../package.json";
14
20
  import type { AcpSessionConfigOption } from "../types";
15
21
  import { mintAcpSessionToken, revokeAcpSessionToken } from "../utils/acp-session-token";
@@ -154,7 +160,7 @@ function boundedAcpLogScalar(value: unknown): unknown {
154
160
  return typeof value === "string" ? value.slice(0, 500) : value;
155
161
  }
156
162
 
157
- class SwarmAcpClient implements Client {
163
+ class SwarmAcpClient {
158
164
  /** Latest cumulative USD cost the target reported for the session. */
159
165
  reportedCostUsd: number | null = null;
160
166
 
@@ -199,7 +205,7 @@ class ACPSession implements ProviderSession {
199
205
  private ephemeralTokenId: string | null = null;
200
206
 
201
207
  constructor(
202
- private readonly connection: ClientSideConnection,
208
+ private readonly agent: ClientContext,
203
209
  private readonly client: SwarmAcpClient,
204
210
  private readonly process: Bun.Subprocess<"pipe", "pipe", "pipe">,
205
211
  private readonly config: ProviderSessionConfig,
@@ -234,7 +240,7 @@ class ACPSession implements ProviderSession {
234
240
  if (this.aborted) return;
235
241
  this.aborted = true;
236
242
  try {
237
- await this.connection.cancel({ sessionId: this.sessionId });
243
+ await this.agent.notify(methods.agent.session.cancel, { sessionId: this.sessionId });
238
244
  await Promise.race([this.completionPromise, Bun.sleep(ACP_CANCEL_SETTLE_MS)]);
239
245
  } catch (err) {
240
246
  this.emit({
@@ -282,7 +288,7 @@ class ACPSession implements ProviderSession {
282
288
  private async runPrompt(): Promise<void> {
283
289
  let result: ProviderResult;
284
290
  try {
285
- const response = await this.connection.prompt({
291
+ const response = await this.agent.request(methods.agent.session.prompt, {
286
292
  sessionId: this.sessionId,
287
293
  prompt: [{ type: "text", text: this.config.prompt }],
288
294
  });
@@ -441,11 +447,16 @@ export class ACPAdapter implements ProviderAdapter {
441
447
  else preSessionEvents.push(event);
442
448
  }, target);
443
449
  const stream = ndJsonStream(fileSinkWritableStream(proc.stdin), proc.stdout);
444
- const connection = new ClientSideConnection(() => client, stream);
450
+ const agent = acpClient()
451
+ .onRequest(methods.client.session.requestPermission, ({ params }) =>
452
+ client.requestPermission(params),
453
+ )
454
+ .onNotification(methods.client.session.update, ({ params }) => client.sessionUpdate(params))
455
+ .connect(stream).agent;
445
456
 
446
457
  let ephemeralToken: { tokenId: string; plaintext: string } | null = null;
447
458
  try {
448
- await connection.initialize({
459
+ await agent.request(methods.agent.initialize, {
449
460
  protocolVersion: PROTOCOL_VERSION,
450
461
  clientInfo: { name: "agent-swarm", version: pkg.version },
451
462
  clientCapabilities: { session: { configOptions: { boolean: {} } } },
@@ -473,7 +484,7 @@ export class ACPAdapter implements ProviderAdapter {
473
484
  // shape for an ACP agent with no network route to the swarm API. Gated on
474
485
  // `mcpCapabilities.acp` and UNSTABLE; not adopted here.
475
486
  const sessionMeta = target.sessionMeta?.(config);
476
- const newSession = await connection.newSession({
487
+ const newSession = await agent.request(methods.agent.session.new, {
477
488
  ...(sessionMeta ? { _meta: sessionMeta } : {}),
478
489
  cwd: config.cwd,
479
490
  mcpServers: [
@@ -491,13 +502,16 @@ export class ACPAdapter implements ProviderAdapter {
491
502
  ],
492
503
  });
493
504
  const configOptions = await applyConfiguredOptions(
494
- connection,
505
+ {
506
+ setSessionConfigOption: (params) =>
507
+ agent.request(methods.agent.session.setConfigOption, params),
508
+ },
495
509
  newSession.sessionId,
496
510
  newSession.configOptions ?? [],
497
511
  target.configuredOptions(config),
498
512
  );
499
513
  session = new ACPSession(
500
- connection,
514
+ agent,
501
515
  client,
502
516
  proc,
503
517
  config,
@@ -537,8 +551,14 @@ export class ACPAdapter implements ProviderAdapter {
537
551
  }
538
552
  }
539
553
 
554
+ export interface AcpConfigOptionSetter {
555
+ setSessionConfigOption(
556
+ params: SetSessionConfigOptionRequest,
557
+ ): Promise<SetSessionConfigOptionResponse>;
558
+ }
559
+
540
560
  export async function applyConfiguredOptions(
541
- connection: Pick<ClientSideConnection, "setSessionConfigOption">,
561
+ connection: AcpConfigOptionSetter,
542
562
  sessionId: string,
543
563
  advertised: SessionConfigOption[],
544
564
  configured: Record<string, string | boolean>,
@@ -3,7 +3,7 @@ import type {
3
3
  SessionNotification,
4
4
  SessionUpdate,
5
5
  ToolCallUpdate,
6
- } from "@agentclientprotocol/sdk";
6
+ } from "@agentclientprotocol/sdk/unstable";
7
7
  import type { ProviderEvent } from "./types";
8
8
 
9
9
  function contentToText(content: ContentBlock): string {
@@ -44,7 +44,7 @@ import type {
44
44
  * default; the image installs exactly this version and the per-task settings
45
45
  * set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
46
46
  */
47
- export const AMP_PACKAGE = "@ampcode/cli@0.0.1792470027-g8c393d";
47
+ export const AMP_PACKAGE = "@ampcode/cli@0.0.1792584044-g15483c";
48
48
 
49
49
  /** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
50
50
  const AMP_MCP_SERVER_NAME = "agent-swarm";