@desplega.ai/agent-swarm 2.7.0 → 2.7.1

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 (236) hide show
  1. package/dist/{acp-adapter-ma1js5n7.js → acp-adapter-8dn5tq56.js} +6 -6
  2. package/dist/{actions-1vhmtvg9.js → actions-3r6gw7dy.js} +18 -18
  3. package/dist/{amp-adapter-sxqqvmdk.js → amp-adapter-wjtq413p.js} +3 -3
  4. package/dist/{anthropic-messages-txs190dd.js → anthropic-messages-n4cby1rz.js} +4 -4
  5. package/dist/{app-mwx1d5a9.js → app-d53yhykk.js} +3 -3
  6. package/dist/{assistant-yx1p6g82.js → assistant-ksrawj3k.js} +20 -20
  7. package/dist/{boot-reembed-7h78vmvv.js → boot-reembed-fxfw17m2.js} +10 -3
  8. package/dist/{boot-reembed-5h37a2c2.js → boot-reembed-k5y5xbj0.js} +15 -8
  9. package/dist/{boot-scrub-sweep-8mawd4gw.js → boot-scrub-sweep-se41j9fd.js} +3 -3
  10. package/dist/{boot-seeds-ca84dhnn.js → boot-seeds-r3kwwv8b.js} +4 -4
  11. package/dist/{claude-adapter-wqt4hfkq.js → claude-adapter-ahmyf231.js} +7 -7
  12. package/dist/{claude-managed-adapter-6fjqtr2z.js → claude-managed-adapter-5knamegc.js} +3 -3
  13. package/dist/{claude-managed-setup-rqv98sea.js → claude-managed-setup-mdc7dy6b.js} +2 -2
  14. package/dist/{claude-sdk-session-f29f3b2x.js → claude-sdk-session-n5wen9zk.js} +8728 -8599
  15. package/dist/{cli-m5e802r7.js → cli-0dqtv2jg.js} +2 -2
  16. package/dist/{cli-e1595bgx.js → cli-0tww7dnz.js} +2 -2
  17. package/dist/{cli-2y7v133v.js → cli-0w4e4v58.js} +7 -7
  18. package/dist/{cli-d20qvpr2.js → cli-2sjm3t4r.js} +1 -1
  19. package/dist/{cli-gf2850q7.js → cli-3cywb44q.js} +18 -1
  20. package/dist/{cli-8wkb09q5.js → cli-3k9jej0c.js} +2 -2
  21. package/dist/{cli-twctnkng.js → cli-3v2cyqb8.js} +3 -3
  22. package/dist/{cli-he9gbdx0.js → cli-5qt9vzmq.js} +39 -20
  23. package/dist/{cli-a6ds2epe.js → cli-5rp1w4cr.js} +1 -1
  24. package/dist/{cli-6srv653c.js → cli-6vwjsetx.js} +4 -4
  25. package/dist/{cli-1hhbzkbb.js → cli-6w2yrrc0.js} +8 -8
  26. package/dist/{cli-f12mr20z.js → cli-75tdydgv.js} +1 -1
  27. package/dist/{cli-j3y0x2n4.js → cli-8feswf2v.js} +43 -33
  28. package/dist/{cli-07q03xm2.js → cli-8wd3mbyj.js} +1 -1
  29. package/dist/{cli-ymjpkxgn.js → cli-9csfthbw.js} +8 -6
  30. package/dist/{cli-f1e0ptjj.js → cli-b41p764a.js} +21 -2
  31. package/dist/{cli-kv7sbc2c.js → cli-c1t533hh.js} +3 -3
  32. package/dist/{cli-54sj64fc.js → cli-c28jxpcv.js} +1 -1
  33. package/dist/{cli-8mbc9za9.js → cli-cb50g9cd.js} +1 -1
  34. package/dist/{cli-c668vvcf.js → cli-d8gaphtr.js} +1 -1
  35. package/dist/{cli-86ehn4s5.js → cli-dgtfysne.js} +2 -2
  36. package/dist/{cli-sjctjr48.js → cli-dscnhn18.js} +1 -1
  37. package/dist/{cli-j0ppywdr.js → cli-dv11pgpv.js} +12 -12
  38. package/dist/{cli-1m9x8dr5.js → cli-ekptgk2m.js} +5 -5
  39. package/dist/{cli-9gsm2rzp.js → cli-f3mzy662.js} +17 -17
  40. package/dist/{cli-s0mgx214.js → cli-fk7k8s9x.js} +2 -2
  41. package/dist/{cli-vbhgkw9h.js → cli-h3e7bhvn.js} +3 -3
  42. package/dist/{cli-yd2nreqw.js → cli-hkxqdmnb.js} +2 -2
  43. package/dist/{cli-8wmj18cz.js → cli-hpkvgdad.js} +7 -4
  44. package/dist/{cli-wqr7cea4.js → cli-hq2v4rr5.js} +37 -9
  45. package/dist/{cli-sgqm2582.js → cli-k1zb7raw.js} +5 -5
  46. package/dist/{cli-4rvr7zfx.js → cli-k4awgvzm.js} +5 -0
  47. package/dist/{cli-ay40dnj3.js → cli-k7d3hvya.js} +2 -2
  48. package/dist/{cli-j9bpmykh.js → cli-kbtdftbw.js} +5 -5
  49. package/dist/{cli-x37r7qwf.js → cli-kj69b9tw.js} +25 -5
  50. package/dist/{cli-gsfk58nw.js → cli-kk9jy4nm.js} +1 -1
  51. package/dist/{cli-39aet655.js → cli-m52t4cta.js} +1 -1
  52. package/dist/{cli-h5jeg46p.js → cli-mdadewqp.js} +5 -5
  53. package/dist/{cli-a89zdqz9.js → cli-nf6k60sn.js} +1 -1
  54. package/dist/{cli-v6shg7na.js → cli-nzdf09js.js} +1 -1
  55. package/dist/{cli-rr7zx19j.js → cli-prcyzecg.js} +12 -12
  56. package/dist/{cli-6a9qfa8b.js → cli-pzafyqtk.js} +118 -48
  57. package/dist/{cli-48gxjmcz.js → cli-qvaa1gjp.js} +87 -24
  58. package/dist/{cli-qj442fws.js → cli-rg4b2g6j.js} +1 -1
  59. package/dist/{cli-aypd09nf.js → cli-rm66vqdz.js} +1 -1
  60. package/dist/{cli-9pshx6rd.js → cli-s504c3w7.js} +1 -1
  61. package/dist/{cli-ptfq202x.js → cli-swkx9jm2.js} +3 -3
  62. package/dist/{cli-m30k9777.js → cli-thsaq4pd.js} +16 -16
  63. package/dist/{cli-60sty27p.js → cli-twjnyvs8.js} +2 -2
  64. package/dist/{cli-rd5m70gj.js → cli-typdpe8g.js} +4 -4
  65. package/dist/{cli-yxbsmx56.js → cli-vg8pwmsd.js} +6 -6
  66. package/dist/{cli-s09j9vp5.js → cli-vhpckzvp.js} +1 -1
  67. package/dist/{cli-dv97kdzv.js → cli-wdxgsdz5.js} +2 -2
  68. package/dist/{cli-7a6p4e65.js → cli-wtjg6je3.js} +1 -1
  69. package/dist/{cli-b3yzyc57.js → cli-x3cngb1h.js} +44 -12
  70. package/dist/{cli-6mgdba42.js → cli-xbz6mcg4.js} +2 -2
  71. package/dist/{cli-2pvzmas1.js → cli-y4tdxsqb.js} +3 -3
  72. package/dist/{cli-f4dbh7a5.js → cli-yqrsp3ny.js} +7 -7
  73. package/dist/cli.js +27 -19
  74. package/dist/{codex-adapter-zk59x2zh.js → codex-adapter-7wepzq15.js} +5 -5
  75. package/dist/{codex-hook-xyjhvxwd.js → codex-hook-pbdh4xhx.js} +2 -2
  76. package/dist/{codex-session-runner-kh6s4j3n.js → codex-session-runner-02sb49e4.js} +5 -5
  77. package/dist/{commands-68et1qv2.js → commands-3rcspxct.js} +2 -2
  78. package/dist/{controller-fyrbnzg4.js → controller-8ar4avd6.js} +3 -3
  79. package/dist/{controllers-connect-nep9h2xv.js → controllers-connect-ajvh9h0v.js} +19 -19
  80. package/dist/{cursor-adapter-yss3pg7t.js → cursor-adapter-gd0sa7bh.js} +1 -1
  81. package/dist/{db-28gjyvxj.js → db-dv9j7ts5.js} +2 -2
  82. package/dist/{db-8ftwvzba.js → db-z26vgdvj.js} +3 -3
  83. package/dist/{dispatcher-hayexmkr.js → dispatcher-8m0htjq8.js} +17 -17
  84. package/dist/{dsh-adapter-46wawff9.js → dsh-adapter-y8gyk130.js} +1 -1
  85. package/dist/{e2b-fn3zacvt.js → e2b-74vg9b7e.js} +1 -1
  86. package/dist/{e2b-222hsajc.js → e2b-jyay8wzn.js} +1 -1
  87. package/dist/{easter-egg-3d-8d1e0h4m.js → easter-egg-3d-28f880xx.js} +1 -1
  88. package/dist/{execute-cjrfxfnm.js → execute-9seb79vh.js} +1 -1
  89. package/dist/{external-channel-xbmxcg09.js → external-channel-smz7jyth.js} +2 -2
  90. package/dist/{getMachineId-bsd-brn0xym3.js → getMachineId-bsd-whfre6gr.js} +3 -3
  91. package/dist/{getMachineId-darwin-t339sfxm.js → getMachineId-darwin-5zjynb19.js} +3 -3
  92. package/dist/{getMachineId-linux-5eq95d8b.js → getMachineId-linux-8mksrrsq.js} +3 -3
  93. package/dist/{getMachineId-unsupported-qavrtc7j.js → getMachineId-unsupported-fxtqsdt2.js} +3 -3
  94. package/dist/{getMachineId-win-q7fe1wx7.js → getMachineId-win-nzfmz857.js} +3 -3
  95. package/dist/{grok-adapter-ag4wks2a.js → grok-adapter-z0nnvtbn.js} +7 -7
  96. package/dist/handlers-pspryh6g.js +41 -0
  97. package/dist/{heartbeat-psfvcgwp.js → heartbeat-f928aqyp.js} +20 -20
  98. package/dist/{hook-cyq4mryn.js → hook-d7c605r4.js} +7 -7
  99. package/dist/{hook-8pz04697.js → hook-q9hv5xf9.js} +5 -5
  100. package/dist/{http-haj1nd6k.js → http-q6j4k989.js} +934 -180
  101. package/dist/{index-z5h22pes.js → index-5nb266jr.js} +17 -17
  102. package/dist/{index-ysd45kgc.js → index-8fxt5eah.js} +10 -10
  103. package/dist/{index-zgvqzjsw.js → index-cekzwhez.js} +5 -5
  104. package/dist/{index-zyq8ct52.js → index-ch0c1xff.js} +17 -17
  105. package/dist/{index-syvhnvtw.js → index-ebnjhxh1.js} +1 -1
  106. package/dist/{index-y1ajx04a.js → index-ed8dy8rn.js} +20 -20
  107. package/dist/{index-jwt2bj6f.js → index-ejqdkkqx.js} +11 -10
  108. package/dist/{index-dr94zbnh.js → index-farm1zsn.js} +32661 -29443
  109. package/dist/{index-q52f47pe.js → index-yn1yxg85.js} +1 -1
  110. package/dist/{keepalive-xdvsamvj.js → keepalive-zzc3hks1.js} +3 -3
  111. package/dist/{lead-hqg57kdp.js → lead-c0pjcaw4.js} +36 -36
  112. package/dist/{maintenance-4y2xzh3a.js → maintenance-kqsxzpzx.js} +1 -1
  113. package/dist/{oauth-refresh-sweep-jd4r7zbs.js → oauth-refresh-sweep-7qt0g55z.js} +2 -2
  114. package/dist/{onboard-18447q9q.js → onboard-k7x13fqg.js} +118 -5
  115. package/dist/{opencode-adapter-26b2frdf.js → opencode-adapter-tqvrtter.js} +5 -5
  116. package/dist/{otel-impl-f9tgr8yw.js → otel-impl-sqkf6k3b.js} +97 -97
  117. package/dist/{pi-mono-adapter-qn6qbga1.js → pi-mono-adapter-m2qnafxm.js} +63 -9
  118. package/dist/{pricing-refresh-dw2tgb53.js → pricing-refresh-wkpwhgvs.js} +3 -3
  119. package/dist/{rbac-roles-s4nwj2m3.js → rbac-roles-dpn0c0wj.js} +7 -4
  120. package/dist/{rbac-roles-aygm2760.js → rbac-roles-wk7xd6a6.js} +2 -2
  121. package/dist/{render-v2-yfvdfr0x.js → render-v2-ex2m4g2k.js} +3 -3
  122. package/dist/{resume-1bmvjbf3.js → resume-725g9b0b.js} +7 -7
  123. package/dist/{runtime-f7tpzedy.js → runtime-kajas15g.js} +1 -1
  124. package/dist/{session-access-r5garej6.js → session-access-0j2ezjje.js} +8 -4
  125. package/dist/{session-source-backfill-424wzv0r.js → session-source-backfill-62202awe.js} +2 -2
  126. package/dist/{setup-8tygvxkq.js → setup-9sj11ax6.js} +1 -1
  127. package/dist/{virtual-modules-xsw323b3.js → virtual-modules-c6fcq6zr.js} +2 -2
  128. package/dist/{work-objects-ggnghpgf.js → work-objects-0y07495y.js} +20 -20
  129. package/dist/{worker-sqjd1nqf.js → worker-d4q9sqbc.js} +36 -36
  130. package/openapi.json +1070 -9
  131. package/package.json +3 -3
  132. package/src/be/connector-codes.ts +65 -22
  133. package/src/be/memory/boot-reembed.ts +4 -1
  134. package/src/be/memory/constants.ts +6 -0
  135. package/src/be/memory/embedding-recovery.ts +48 -0
  136. package/src/be/memory/index-content.ts +5 -2
  137. package/src/be/memory/providers/openai-embedding.ts +65 -14
  138. package/src/be/memory/task-learning.ts +110 -8
  139. package/src/be/memory/types.ts +13 -2
  140. package/src/be/migrations/246_connector_code_kind.sql +11 -0
  141. package/src/be/onboarding.ts +125 -15
  142. package/src/be/rbac-roles.ts +4 -1
  143. package/src/be/scripts/embeddings.ts +2 -2
  144. package/src/be/seed/agent-fs-provision.ts +75 -14
  145. package/src/be/swarm-config-guard.ts +3 -0
  146. package/src/cli.tsx +9 -0
  147. package/src/comb/config.ts +6 -1
  148. package/src/commands/onboard/agent-fs-version.ts +5 -0
  149. package/src/commands/onboard/compose-generator.ts +112 -1
  150. package/src/commands/onboard/env-generator.ts +9 -0
  151. package/src/commands/onboard/tei-image.ts +12 -0
  152. package/src/commands/onboard/types.ts +7 -0
  153. package/src/commands/onboard.tsx +11 -1
  154. package/src/commands/runner.ts +14 -1
  155. package/src/http/agentfs-proxy.ts +369 -0
  156. package/src/http/all-routes.ts +1 -0
  157. package/src/http/codex-oauth-device.ts +8 -1
  158. package/src/http/fs.ts +64 -10
  159. package/src/http/inbox-state.ts +27 -1
  160. package/src/http/index.ts +4 -1
  161. package/src/http/memory.ts +7 -4
  162. package/src/http/onboarding.ts +139 -11
  163. package/src/http/session-access.ts +24 -0
  164. package/src/http/sessions.ts +10 -6
  165. package/src/http/tasks.ts +85 -11
  166. package/src/http/telemetry-ui.ts +1 -1
  167. package/src/http/user-roles.ts +4 -1
  168. package/src/http/users.ts +272 -3
  169. package/src/http/utils.ts +22 -3
  170. package/src/prompts/base-prompt.ts +24 -9
  171. package/src/prompts/session-templates.ts +26 -2
  172. package/src/providers/amp-adapter.ts +1 -1
  173. package/src/providers/codex-adapter.ts +6 -1
  174. package/src/providers/cursor-adapter.ts +1 -1
  175. package/src/providers/pi-mono-adapter.ts +58 -3
  176. package/src/rbac/legacy-policy.ts +1 -0
  177. package/src/rbac/permissions.ts +4 -0
  178. package/src/tests/agent-fs-provision-seeder.test.ts +261 -92
  179. package/src/tests/agentfs-proxy.test.ts +1106 -0
  180. package/src/tests/base-prompt.test.ts +69 -2
  181. package/src/tests/codex-adapter.test.ts +46 -8
  182. package/src/tests/codex-oauth-device.test.ts +12 -1
  183. package/src/tests/comb-config-guard.test.ts +20 -0
  184. package/src/tests/comb-links.test.ts +79 -1
  185. package/src/tests/comb-status.test.ts +44 -0
  186. package/src/tests/docs-index-footprint.test.js +3 -3
  187. package/src/tests/fixtures/onboard/baseline-states.ts +68 -0
  188. package/src/tests/fixtures/onboard/content.compose.yml.txt +190 -0
  189. package/src/tests/fixtures/onboard/content.env.txt +29 -0
  190. package/src/tests/fixtures/onboard/dev.compose.yml.txt +155 -0
  191. package/src/tests/fixtures/onboard/dev.env.txt +28 -0
  192. package/src/tests/fixtures/onboard/full-all.compose.yml.txt +582 -0
  193. package/src/tests/fixtures/onboard/full-all.env.txt +59 -0
  194. package/src/tests/fixtures/onboard/full.compose.yml.txt +435 -0
  195. package/src/tests/fixtures/onboard/full.env.txt +36 -0
  196. package/src/tests/fixtures/onboard/research.compose.yml.txt +155 -0
  197. package/src/tests/fixtures/onboard/research.env.txt +28 -0
  198. package/src/tests/fixtures/onboard/solo.compose.yml.txt +85 -0
  199. package/src/tests/fixtures/onboard/solo.env.txt +26 -0
  200. package/src/tests/http-log-scrubbing.test.ts +34 -1
  201. package/src/tests/http-task-write-ownership.test.ts +5 -1
  202. package/src/tests/invites-http.test.ts +464 -0
  203. package/src/tests/memory-embedding-input-cap.test.ts +122 -1
  204. package/src/tests/memory-embedding-recovery.test.ts +137 -0
  205. package/src/tests/onboard-compose.test.ts +142 -0
  206. package/src/tests/onboard-env.test.ts +24 -0
  207. package/src/tests/onboard-non-interactive.test.ts +29 -0
  208. package/src/tests/onboard-tei-image.test.ts +36 -0
  209. package/src/tests/onboarding.test.ts +451 -4
  210. package/src/tests/openai-embedding-provider.test.ts +105 -2
  211. package/src/tests/pi-mono-session-cost-on-abort.test.ts +270 -0
  212. package/src/tests/preload-env.test.ts +4 -0
  213. package/src/tests/preload.ts +4 -0
  214. package/src/tests/prompt-template-session.test.ts +19 -3
  215. package/src/tests/rbac-admission-e2e.test.ts +22 -6
  216. package/src/tests/rbac-admission.test.ts +342 -4
  217. package/src/tests/rbac-engine.test.ts +1 -0
  218. package/src/tests/rbac-roles.test.ts +9 -1
  219. package/src/tests/send-task-requester-gate.test.ts +64 -3
  220. package/src/tests/sessions-move.test.ts +39 -1
  221. package/src/tests/store-progress-learning.test.ts +95 -2
  222. package/src/tests/whoami-route.test.ts +41 -0
  223. package/src/tools/inject-learning.ts +1 -1
  224. package/src/tools/memory-edit.ts +1 -1
  225. package/src/tools/memory-search.ts +1 -1
  226. package/src/tools/store-progress.ts +5 -1
  227. package/src/tools/utils.ts +12 -1
  228. package/src/utils/constants.ts +39 -0
  229. package/src/utils/task-attachment-links.ts +15 -3
  230. package/templates/skills/swarm-docs/content.md +1 -1
  231. package/dist/handlers-tzmevqb5.js +0 -41
  232. package/dist/{azure-openai-responses-9kfwhmn7.js → azure-openai-responses-43asa23s.js} +0 -0
  233. package/dist/{node-vxx896fa.js → node-t28cc56b.js} +0 -0
  234. package/dist/{openai-codex-responses-79s8xnbh.js → openai-codex-responses-jhr3sjtm.js} +3 -3
  235. package/dist/{openai-completions-tdbyts0x.js → openai-completions-gs6yn37k.js} +3 -3
  236. /package/dist/{openai-responses-dm3t5vqp.js → openai-responses-jqm9vwwk.js} +0 -0
@@ -1,4 +1,13 @@
1
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
1
+ import {
2
+ afterAll,
3
+ afterEach,
4
+ beforeAll,
5
+ beforeEach,
6
+ describe,
7
+ expect,
8
+ spyOn,
9
+ test,
10
+ } from "bun:test";
2
11
  import {
3
12
  MAX_EMBEDDING_INPUT_CHARS,
4
13
  OpenAIEmbeddingProvider,
@@ -112,3 +121,115 @@ describe("embedding input cap", () => {
112
121
  expect(requests).toEqual([["one", "three"]]);
113
122
  });
114
123
  });
124
+
125
+ // A TEI server started with the default --max-client-batch-size 32. TEI answers
126
+ // a bigger request with a 422 and this body. TEI also answers 422 for other
127
+ // validation errors, such as a `dimensions` value the model cannot serve.
128
+ const TEI_MAX_BATCH = 32;
129
+ let teiServer: ReturnType<typeof Bun.serve>;
130
+ let teiBaseUrl: string;
131
+ let teiOtherError: string | null = null;
132
+ let teiPayloadTooLarge = false;
133
+ const teiRequests: string[][] = [];
134
+
135
+ function teiProvider(): OpenAIEmbeddingProvider {
136
+ return new OpenAIEmbeddingProvider({
137
+ apiKey: "local-tei",
138
+ model: "nomic-ai/nomic-embed-text-v1.5",
139
+ dimensions: 4,
140
+ });
141
+ }
142
+
143
+ describe("batch-size rejections", () => {
144
+ let savedBaseUrl: string | undefined;
145
+
146
+ beforeAll(() => {
147
+ teiServer = Bun.serve({
148
+ hostname: "127.0.0.1",
149
+ port: 0,
150
+ async fetch(req) {
151
+ const body = (await req.json()) as { input: string | string[] };
152
+ const inputs = Array.isArray(body.input) ? body.input : [body.input];
153
+ teiRequests.push(inputs);
154
+ if (teiPayloadTooLarge && inputs.length > 1) {
155
+ return new Response("payload too large", { status: 413 });
156
+ }
157
+ if (teiOtherError) {
158
+ return Response.json({ error: teiOtherError, error_type: "Validation" }, { status: 422 });
159
+ }
160
+ if (inputs.length > TEI_MAX_BATCH) {
161
+ return Response.json(
162
+ {
163
+ error: `batch size ${inputs.length} > maximum allowed batch size ${TEI_MAX_BATCH}`,
164
+ error_type: "Validation",
165
+ },
166
+ { status: 422 },
167
+ );
168
+ }
169
+ return Response.json({
170
+ object: "list",
171
+ data: inputs.map((input, index) => ({
172
+ object: "embedding",
173
+ index,
174
+ embedding: [input.length, 0, 0, 1],
175
+ })),
176
+ model: "nomic-ai/nomic-embed-text-v1.5",
177
+ usage: { prompt_tokens: 1, total_tokens: 1 },
178
+ });
179
+ },
180
+ });
181
+ teiBaseUrl = `http://127.0.0.1:${teiServer.port}/v1`;
182
+ });
183
+
184
+ afterAll(() => {
185
+ teiServer.stop(true);
186
+ });
187
+
188
+ beforeEach(() => {
189
+ teiRequests.length = 0;
190
+ teiOtherError = null;
191
+ teiPayloadTooLarge = false;
192
+ savedBaseUrl = process.env.EMBEDDING_API_BASE_URL;
193
+ process.env.EMBEDDING_API_BASE_URL = teiBaseUrl;
194
+ });
195
+
196
+ afterEach(() => {
197
+ process.env.EMBEDDING_API_BASE_URL = savedBaseUrl;
198
+ });
199
+
200
+ test("a 422 for the batch size embeds every input one by one", async () => {
201
+ const texts = Array.from({ length: 70 }, (_, i) => `chunk ${i}`);
202
+ const vectors = await teiProvider().embedBatch(texts);
203
+
204
+ expect(vectors.map(embeddedLength)).toEqual(texts.map((text) => text.length));
205
+ expect(teiRequests[0]).toHaveLength(70);
206
+ expect(teiRequests.slice(1)).toHaveLength(70);
207
+ expect(teiRequests.slice(1).every((r) => r.length === 1)).toBe(true);
208
+ });
209
+
210
+ test("a 413 also embeds every input one by one", async () => {
211
+ teiPayloadTooLarge = true;
212
+
213
+ const vectors = await teiProvider().embedBatch(["one", "three"]);
214
+ expect(vectors.map(embeddedLength)).toEqual([3, 5]);
215
+ expect(teiRequests).toEqual([["one", "three"], ["one"], ["three"]]);
216
+ });
217
+
218
+ test("a 422 with another message is one logged failure and no per-input calls", async () => {
219
+ teiOtherError = "`dimensions` set to 4 but the model only supports 768, 512, 256, 128, 64";
220
+ const errors = spyOn(console, "error").mockImplementation(() => {});
221
+ try {
222
+ const texts = Array.from({ length: 70 }, (_, i) => `chunk ${i}`);
223
+ const vectors = await teiProvider().embedBatch(texts);
224
+
225
+ expect(vectors.every((vector) => vector === null)).toBe(true);
226
+ expect(teiRequests).toHaveLength(1);
227
+ const failures = errors.mock.calls.filter((call) =>
228
+ String(call[0]).includes("Batch embedding failed"),
229
+ );
230
+ expect(failures).toHaveLength(1);
231
+ } finally {
232
+ errors.mockRestore();
233
+ }
234
+ });
235
+ });
@@ -0,0 +1,137 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { randomUUID } from "node:crypto";
3
+ import { closeDb, createAgent, getDbClient, initDb } from "../be/db";
4
+ import { getEmbeddingProvider, getMemoryStore, resetEmbeddingProvider } from "../be/memory";
5
+ import { EMBEDDING_DIMENSIONS } from "../be/memory/constants";
6
+ import {
7
+ pendingRecoveryBackfill,
8
+ resetEmbeddingRecoveryForTests,
9
+ } from "../be/memory/embedding-recovery";
10
+
11
+ // A fake OpenAI-compatible endpoint that is down (503), rejects inputs (400)
12
+ // or works, like a local TEI that restarts.
13
+ let mode: "down" | "reject" | "up" = "up";
14
+ let server: ReturnType<typeof Bun.serve>;
15
+ const agentId = randomUUID();
16
+ const saved = {
17
+ baseUrl: process.env.EMBEDDING_API_BASE_URL,
18
+ key: process.env.EMBEDDING_API_KEY,
19
+ };
20
+
21
+ beforeAll(async () => {
22
+ server = Bun.serve({
23
+ hostname: "127.0.0.1",
24
+ port: 0,
25
+ async fetch(req) {
26
+ const body = (await req.json()) as { input: string | string[] };
27
+ const inputs = Array.isArray(body.input) ? body.input : [body.input];
28
+ if (mode === "down") return new Response("model loading", { status: 503 });
29
+ if (mode === "reject") {
30
+ return Response.json({ error: { message: "bad input" } }, { status: 400 });
31
+ }
32
+ return Response.json({
33
+ object: "list",
34
+ data: inputs.map((_, index) => ({
35
+ object: "embedding",
36
+ index,
37
+ embedding: new Array(EMBEDDING_DIMENSIONS).fill(0.1),
38
+ })),
39
+ model: "nomic-ai/nomic-embed-text-v1.5",
40
+ usage: { prompt_tokens: 1, total_tokens: 1 },
41
+ });
42
+ },
43
+ });
44
+ process.env.EMBEDDING_API_BASE_URL = `http://127.0.0.1:${server.port}/v1`;
45
+ process.env.EMBEDDING_API_KEY = "local-tei";
46
+ resetEmbeddingProvider();
47
+ initDb(":memory:");
48
+ await createAgent({ id: agentId, name: "recovery", isLead: false, status: "idle" });
49
+ });
50
+
51
+ afterAll(() => {
52
+ server.stop(true);
53
+ closeDb();
54
+ for (const [env, value] of [
55
+ ["EMBEDDING_API_BASE_URL", saved.baseUrl],
56
+ ["EMBEDDING_API_KEY", saved.key],
57
+ ] as const) {
58
+ if (value === undefined) delete process.env[env];
59
+ else process.env[env] = value;
60
+ }
61
+ resetEmbeddingProvider();
62
+ });
63
+
64
+ beforeEach(() => {
65
+ resetEmbeddingRecoveryForTests();
66
+ mode = "up";
67
+ });
68
+
69
+ async function memoryWithoutVector(): Promise<string> {
70
+ const memory = await getMemoryStore().store({
71
+ agentId,
72
+ scope: "agent",
73
+ name: `written while down ${randomUUID()}`,
74
+ content: "violet umbrella 37",
75
+ source: "manual",
76
+ });
77
+ return memory.id;
78
+ }
79
+
80
+ /** A stored script with no vector, like one seeded while the endpoint was down. */
81
+ async function scriptWithoutVector(): Promise<string> {
82
+ const id = randomUUID();
83
+ await getDbClient().run(
84
+ `INSERT INTO scripts (id, name, scope, source, description, intent, signatureJson, contentHash)
85
+ VALUES (?, ?, 'global', 'export default async () => 1', 'umbrella counter', 'count', '{}', ?)`,
86
+ [id, `recovery-${id}`, id],
87
+ );
88
+ return id;
89
+ }
90
+
91
+ async function scriptVectorBytes(id: string): Promise<number | null> {
92
+ const row = await getDbClient().get<{ bytes: number | null }>(
93
+ "SELECT length(embedding) AS bytes FROM script_embeddings WHERE scriptId = ?",
94
+ [id],
95
+ );
96
+ return row?.bytes ?? null;
97
+ }
98
+
99
+ async function vectorBytes(id: string): Promise<number | null> {
100
+ const row = await getDbClient().get<{ bytes: number | null }>(
101
+ "SELECT length(embedding) AS bytes FROM agent_memory WHERE id = ?",
102
+ [id],
103
+ );
104
+ return row?.bytes ?? null;
105
+ }
106
+
107
+ describe("backfill after the embeddings endpoint recovers", () => {
108
+ test("a memory or script written while the endpoint was down gets its vector on the next working call", async () => {
109
+ mode = "down";
110
+ expect(await getEmbeddingProvider().embed("first try", { kind: "document" })).toBeNull();
111
+ const id = await memoryWithoutVector();
112
+ const scriptId = await scriptWithoutVector();
113
+ expect(await vectorBytes(id)).toBeNull();
114
+ expect(await scriptVectorBytes(scriptId)).toBeNull();
115
+
116
+ mode = "up";
117
+ expect(await getEmbeddingProvider().embed("a search", { kind: "query" })).not.toBeNull();
118
+ const backfill = pendingRecoveryBackfill();
119
+ expect(backfill).not.toBeNull();
120
+ await backfill;
121
+ expect(await vectorBytes(id)).toBe(EMBEDDING_DIMENSIONS * 4);
122
+ expect(await scriptVectorBytes(scriptId)).toBe(EMBEDDING_DIMENSIONS * 4);
123
+ }, 30_000);
124
+
125
+ test("a rejected input does not start a backfill", async () => {
126
+ mode = "reject";
127
+ expect(await getEmbeddingProvider().embed("bad input")).toBeNull();
128
+ mode = "up";
129
+ expect(await getEmbeddingProvider().embed("a search", { kind: "query" })).not.toBeNull();
130
+ expect(pendingRecoveryBackfill()).toBeNull();
131
+ });
132
+
133
+ test("working calls without an earlier failure start no backfill", async () => {
134
+ expect(await getEmbeddingProvider().embed("a search", { kind: "query" })).not.toBeNull();
135
+ expect(pendingRecoveryBackfill()).toBeNull();
136
+ });
137
+ });
@@ -1,8 +1,15 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
2
5
  import { generateCompose as generateTemplateCompose } from "../../apps/templates-ui/src/lib/compose-generator.ts";
6
+ import { readAgentFsImageTag } from "../../scripts/agent-fs-version-utils.ts";
7
+ import { applyPins, PINS } from "../../scripts/bump-agent-fs.ts";
8
+ import { AGENT_FS_VERSION } from "../commands/onboard/agent-fs-version.ts";
3
9
  import { generateCompose } from "../commands/onboard/compose-generator.ts";
4
10
  import { getPresetById, PRESETS } from "../commands/onboard/presets.ts";
5
11
  import { INITIAL_STATE, type OnboardState } from "../commands/onboard/types.ts";
12
+ import { BASELINE_CASES, baselineState } from "./fixtures/onboard/baseline-states.ts";
6
13
 
7
14
  function makeState(overrides: Partial<OnboardState>): OnboardState {
8
15
  return { ...INITIAL_STATE, ...overrides };
@@ -379,3 +386,138 @@ describe("generateCompose", () => {
379
386
  expect(yaml).toContain("condition: service_healthy");
380
387
  });
381
388
  });
389
+
390
+ describe("generateCompose with the agent-fs bundle flag", () => {
391
+ const serviceHeaders = (yaml: string) =>
392
+ yaml.split("\nvolumes:\n")[0]?.match(/^ {2}[a-z][\w-]+:$/gm) ?? [];
393
+ const serviceBlock = (yaml: string, name: string) =>
394
+ yaml.split(`\n ${name}:\n`)[1]?.split(/\n {2}[a-z][\w-]+:\n|\nvolumes:\n/)[0] ?? "";
395
+
396
+ test("INITIAL_STATE keeps the bundle off", () => {
397
+ expect(INITIAL_STATE.bundleAgentFs).toBe(false);
398
+ expect(INITIAL_STATE.teiImage).toBeNull();
399
+ });
400
+
401
+ for (const { name, state } of BASELINE_CASES) {
402
+ test(`flag off: ${name} compose equals the pre-bundle output`, async () => {
403
+ const expected = await Bun.file(
404
+ new URL(`./fixtures/onboard/${name}.compose.yml.txt`, import.meta.url),
405
+ ).text();
406
+ const yaml = generateCompose(state);
407
+ expect(yaml).toBe(expected);
408
+ for (const s of ["agent-fs", "tei", "COMB_ENABLED", "EMBEDDING_", "AGENT_FS_"]) {
409
+ expect(yaml).not.toContain(s);
410
+ }
411
+ });
412
+ }
413
+
414
+ for (const { name, state } of BASELINE_CASES) {
415
+ test(`flag on: ${name} adds agent-fs and tei without changing the rest`, () => {
416
+ const off = generateCompose(state);
417
+ const yaml = generateCompose({ ...state, bundleAgentFs: true });
418
+ const agents = serviceHeaders(off).filter((h) => h !== " swarm-api:");
419
+
420
+ expect(serviceHeaders(yaml)).toEqual([...serviceHeaders(off), " agent-fs:", " tei:"]);
421
+ expect(yaml).toMatch(/\n {2}agent_fs_data:\n {2}tei_data:\n$/);
422
+ expect(yaml.match(/^ {4}pull_policy: /gm)).toHaveLength(agents.length + 3);
423
+ expect(yaml.match(new RegExp(`pull_policy: ${state.pullPolicy}`, "g"))).toHaveLength(
424
+ agents.length + 3,
425
+ );
426
+
427
+ // Every line of the flag-off output survives, in order.
428
+ const onLines = yaml.split("\n");
429
+ let cursor = 0;
430
+ for (const line of off.split("\n")) {
431
+ const found = onLines.indexOf(line, cursor);
432
+ expect(found).toBeGreaterThanOrEqual(cursor);
433
+ cursor = found + 1;
434
+ }
435
+
436
+ // API: proxy, Comb, dashboard links, TEI memory with quoted prefixes.
437
+ const api = serviceBlock(yaml, "swarm-api");
438
+ for (const line of [
439
+ " - AGENT_FS_API_URL=http://agent-fs:7433",
440
+ " - AGENT_FS_PROXY=true",
441
+ " - COMB_ENABLED=true",
442
+ " - AGENT_FS_LINK_TARGET=dashboard",
443
+ " - EMBEDDING_API_BASE_URL=http://tei:80/v1",
444
+ " - EMBEDDING_MODEL=nomic-ai/nomic-embed-text-v1.5",
445
+ " - EMBEDDING_API_KEY=local-tei",
446
+ ' - "EMBEDDING_QUERY_PREFIX=search_query: "',
447
+ ' - "EMBEDDING_DOCUMENT_PREFIX=search_document: "',
448
+ ]) {
449
+ expect(api.split("\n")).toContain(line);
450
+ }
451
+ expect(api).toContain(
452
+ " depends_on:\n agent-fs:\n condition: service_healthy\n tei:\n condition: service_started\n",
453
+ );
454
+
455
+ // Every agent reaches agent-fs and builds dashboard links.
456
+ expect(yaml.match(/^ {6}- AGENT_FS_API_URL=http:\/\/agent-fs:7433$/gm)).toHaveLength(
457
+ agents.length + 1,
458
+ );
459
+ for (const header of agents) {
460
+ const agent = serviceBlock(yaml, header.trim().slice(0, -1));
461
+ expect(agent).toContain(" - AGENT_FS_API_URL=http://agent-fs:7433\n");
462
+ expect(agent).toContain(" - APP_URL=https://app.agent-swarm.dev\n");
463
+ expect(agent).toContain(" - AGENT_FS_LINK_TARGET=dashboard\n");
464
+ }
465
+
466
+ // agent-fs: local disk, no MinIO, no host port.
467
+ const afs = serviceBlock(yaml, "agent-fs");
468
+ expect(afs).toContain("AGENT_FS_STORAGE_PROVIDER=local");
469
+ expect(afs).toContain("AGENT_FS_LOCAL_ROOT=/data/storage");
470
+ expect(afs).toContain(" - agent_fs_data:/data");
471
+ expect(afs).toContain(
472
+ `AGENT_FS_PUBLIC_URL=\${PUBLIC_MCP_BASE_URL:-http://localhost:${state.apiPort || 3013}}/agentfs`,
473
+ );
474
+ expect(afs).toContain("node -e");
475
+ expect(afs).not.toContain("ports:");
476
+
477
+ // tei: no host port, the OOM and batch flags, a 2 GB cap.
478
+ const tei = serviceBlock(yaml, "tei");
479
+ expect(tei).toContain(
480
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Docker Compose env var syntax
481
+ 'image: "${TEI_IMAGE:-ghcr.io/huggingface/text-embeddings-inference:cpu-1.9.4}"',
482
+ );
483
+ expect(tei).toContain('"--max-batch-tokens", "2048"');
484
+ expect(tei).toContain('"--max-client-batch-size", "256"');
485
+ expect(tei).toContain("mem_limit: 2g");
486
+ expect(tei).not.toContain("ports:");
487
+
488
+ expect(yaml).not.toContain("minio");
489
+ expect(yaml).not.toContain("platform:");
490
+ expect(yaml).not.toContain("EMBEDDING_DIMENSIONS");
491
+ });
492
+ }
493
+
494
+ test("flag on: the agent-fs pin equals the Helm chart tag", async () => {
495
+ const values = new URL("../../charts/agent-swarm/values.yaml", import.meta.url);
496
+ const tag = readAgentFsImageTag(await Bun.file(values).text(), "values.yaml");
497
+ expect(AGENT_FS_VERSION).toBe(tag);
498
+ const yaml = generateCompose({ ...baselineState("solo"), bundleAgentFs: true });
499
+ expect(yaml).toContain(` image: "ghcr.io/desplega-ai/agent-fs:${tag}"\n`);
500
+ });
501
+
502
+ test("bump-agent-fs rewrites the onboard pin and keeps the closing quote", async () => {
503
+ const root = await mkdtemp(join(tmpdir(), "bump-agent-fs-"));
504
+ try {
505
+ const repo = new URL("../../", import.meta.url);
506
+ const onboardPath = "src/commands/onboard/agent-fs-version.ts";
507
+ // Copy the onboard file even if PINS drops it, so the failure is the stale version.
508
+ for (const path of new Set([onboardPath, ...PINS.map((pin) => pin.path)])) {
509
+ await Bun.write(join(root, path), Bun.file(new URL(path, repo)));
510
+ }
511
+ const onboard = join(root, onboardPath);
512
+ expect(await Bun.file(onboard).text()).toContain(
513
+ `export const AGENT_FS_VERSION = "${AGENT_FS_VERSION}";`,
514
+ );
515
+ await applyPins("9.9.9", root);
516
+ const bumped = await Bun.file(onboard).text();
517
+ expect(bumped).toContain('export const AGENT_FS_VERSION = "9.9.9";');
518
+ expect(bumped).not.toContain(`"${AGENT_FS_VERSION}"`);
519
+ } finally {
520
+ await rm(root, { recursive: true, force: true });
521
+ }
522
+ });
523
+ });
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { generateEnv } from "../commands/onboard/env-generator.ts";
3
3
  import { INITIAL_STATE, type OnboardState } from "../commands/onboard/types.ts";
4
+ import { BASELINE_CASES, baselineState } from "./fixtures/onboard/baseline-states.ts";
4
5
 
5
6
  function makeState(overrides: Partial<OnboardState>): OnboardState {
6
7
  return { ...INITIAL_STATE, ...overrides };
@@ -232,3 +233,26 @@ describe("generateEnv", () => {
232
233
  expect(env).toContain("SENTRY_ORG=sentry-org");
233
234
  });
234
235
  });
236
+
237
+ describe("generateEnv with the agent-fs bundle flag", () => {
238
+ for (const { name, state } of BASELINE_CASES) {
239
+ test(`flag off: ${name} .env equals the pre-bundle output`, async () => {
240
+ expect(state.bundleAgentFs).toBe(false);
241
+ const expected = await Bun.file(
242
+ new URL(`./fixtures/onboard/${name}.env.txt`, import.meta.url),
243
+ ).text();
244
+ expect(generateEnv(state)).toBe(expected);
245
+ });
246
+ }
247
+
248
+ test("flag on: adds only a comment block; every bundle value lives in the compose", () => {
249
+ const off = generateEnv(baselineState("solo"));
250
+ const on = generateEnv({ ...baselineState("solo"), bundleAgentFs: true });
251
+ const added = on.split("\n").filter((line) => !off.split("\n").includes(line));
252
+ expect(added.filter((line) => line && !line.startsWith("#"))).toEqual([]);
253
+ expect(on).toContain("--no-agent-fs");
254
+ for (const s of ["COMB_ENABLED", "EMBEDDING_", "AGENT_FS_", "EMBEDDING_DIMENSIONS"]) {
255
+ expect(on).not.toContain(s);
256
+ }
257
+ });
258
+ });
@@ -94,6 +94,34 @@ describe("non-interactive onboarding CLI", () => {
94
94
  CHILD_PROCESS_TEST_BUDGET_MS,
95
95
  );
96
96
 
97
+ test(
98
+ "--yes bundles agent-fs by default and --no-agent-fs turns it off (dry run writes nothing)",
99
+ async () => {
100
+ const outputDir = await mkdtemp(join(tmpdir(), "onboard-agent-fs-"));
101
+ const env = { ...process.env, CLAUDE_CODE_OAUTH_TOKEN: "example-test-oauth" };
102
+ try {
103
+ const bundled = await runChild(
104
+ ["bun", CLI_PATH, "onboard", "--yes", "--preset=solo", "--dry-run"],
105
+ { cwd: outputDir, env },
106
+ );
107
+ expect(bundled.exitCode).toBe(0);
108
+ expect(bundled.stdout).toContain("Bundling agent-fs and TEI");
109
+
110
+ const skipped = await runChild(
111
+ ["bun", CLI_PATH, "onboard", "--yes", "--preset=solo", "--dry-run", "--no-agent-fs"],
112
+ { cwd: outputDir, env },
113
+ );
114
+ expect(skipped.exitCode).toBe(0);
115
+ expect(skipped.stdout).toContain("Write docker-compose.yml");
116
+ expect(skipped.stdout).not.toContain("Bundling agent-fs");
117
+ expect(await readdir(outputDir)).toEqual([]);
118
+ } finally {
119
+ await rm(outputDir, { recursive: true, force: true });
120
+ }
121
+ },
122
+ CHILD_PROCESS_TEST_BUDGET_MS,
123
+ );
124
+
97
125
  test(
98
126
  "documents explicit unattended presets and conservative concurrency",
99
127
  async () => {
@@ -104,6 +132,7 @@ describe("non-interactive onboarding CLI", () => {
104
132
  expect(result.stdout).toContain("onboard --yes --preset=full");
105
133
  expect(result.stdout).toContain("default: lead 2, worker 1");
106
134
  expect(result.stdout).not.toContain("default: full");
135
+ expect(result.stdout).toContain("--no-agent-fs");
107
136
  },
108
137
  CHILD_PROCESS_TEST_BUDGET_MS,
109
138
  );
@@ -0,0 +1,36 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { generateEnv } from "../commands/onboard/env-generator.ts";
3
+ import { TEI_ARM64_IMAGE, teiImageForArch } from "../commands/onboard/tei-image.ts";
4
+ import { baselineState } from "./fixtures/onboard/baseline-states.ts";
5
+
6
+ describe("teiImageForArch", () => {
7
+ test("arm64 and aarch64 get the arm64 TEI tag", () => {
8
+ expect(TEI_ARM64_IMAGE).toBe("ghcr.io/huggingface/text-embeddings-inference:cpu-arm64-1.9.4");
9
+ expect(teiImageForArch("arm64")).toBe(TEI_ARM64_IMAGE);
10
+ expect(teiImageForArch("aarch64")).toBe(TEI_ARM64_IMAGE);
11
+ });
12
+
13
+ test("x64 and other arches keep the compose default", () => {
14
+ expect(teiImageForArch("x64")).toBeNull();
15
+ expect(teiImageForArch("ia32")).toBeNull();
16
+ expect(teiImageForArch("")).toBeNull();
17
+ });
18
+ });
19
+
20
+ describe("TEI_IMAGE in .env", () => {
21
+ test("is written only with the arm64 tag", () => {
22
+ const on = { ...baselineState("solo"), bundleAgentFs: true };
23
+ const arm = generateEnv({ ...on, teiImage: teiImageForArch("arm64") });
24
+ expect(arm).toContain(`\nTEI_IMAGE=${TEI_ARM64_IMAGE}\n`);
25
+ expect(arm.match(/^TEI_IMAGE=/gm)).toHaveLength(1);
26
+
27
+ const x64 = generateEnv({ ...on, teiImage: teiImageForArch("x64") });
28
+ expect(x64).not.toContain("TEI_IMAGE");
29
+ expect(x64).toContain("--no-agent-fs");
30
+ });
31
+
32
+ test("is never written with the bundle off", () => {
33
+ const env = generateEnv({ ...baselineState("solo"), teiImage: TEI_ARM64_IMAGE });
34
+ expect(env).not.toContain("TEI_IMAGE");
35
+ });
36
+ });