@desplega.ai/agent-swarm 1.165.0 → 1.167.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. package/dist/acp-adapter-zpvy6321.js +34 -0
  2. package/dist/{actions-jna0b3wm.js → actions-fq2zrwtf.js} +30 -34
  3. package/dist/{amp-adapter-0eve81pz.js → amp-adapter-5gbk7501.js} +7 -7
  4. package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
  5. package/dist/{app-75hm5cfa.js → app-nctetn1e.js} +11 -11
  6. package/dist/{assistant-yzd51j33.js → assistant-0aq8gjvj.js} +31 -35
  7. package/dist/{azure-openai-responses-mh8tvf78.js → azure-openai-responses-9st34pfz.js} +5 -5
  8. package/dist/{boot-reembed-z158xq32.js → boot-reembed-1ze5853n.js} +14 -16
  9. package/dist/{boot-reembed-2ex2mcsh.js → boot-reembed-fmp5ww69.js} +15 -16
  10. package/dist/boot-scrub-sweep-cbp8xd4e.js +212 -0
  11. package/dist/{boot-seeds-2bmnh18p.js → boot-seeds-22tx8a53.js} +13 -12
  12. package/dist/{claude-adapter-jbj1mycz.js → claude-adapter-0jhgeh1d.js} +25 -25
  13. package/dist/{claude-managed-adapter-17ggz09t.js → claude-managed-adapter-tzwxy803.js} +5 -5
  14. package/dist/{claude-managed-setup-ptm1nzm2.js → claude-managed-setup-nrhfx5b0.js} +5 -5
  15. package/dist/{claude-sdk-session-9re1g7r2.js → claude-sdk-session-wh77rh83.js} +2632 -2614
  16. package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
  17. package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
  18. package/dist/{cli-8gpbwy09.js → cli-1yrcfa5s.js} +5 -5
  19. package/dist/{cli-xtgtyafv.js → cli-577bxkf6.js} +212 -451
  20. package/dist/{cli-ma3zvh39.js → cli-5qkcdv8c.js} +6118 -5874
  21. package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
  22. package/dist/{cli-zvckv750.js → cli-69egj654.js} +2 -2
  23. package/dist/{cli-9xb89xca.js → cli-6bpvv8h4.js} +9 -9
  24. package/dist/{cli-90v942q1.js → cli-6qxmjpem.js} +8 -8
  25. package/dist/{cli-9866bd07.js → cli-6vfz44sr.js} +1 -1
  26. package/dist/{cli-32bca7nr.js → cli-7avy4v8w.js} +1 -1
  27. package/dist/{cli-xb3c6aat.js → cli-7mhy3ths.js} +21 -18
  28. package/dist/{cli-q38n4bq0.js → cli-95zs84qw.js} +3 -3
  29. package/dist/cli-a35hzdve.js +33 -0
  30. package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
  31. package/dist/{cli-prt9emct.js → cli-c2tpw9ck.js} +3 -3
  32. package/dist/{cli-0n4fasz0.js → cli-c8r99akr.js} +16 -16
  33. package/dist/{cli-5anb0ev3.js → cli-cexctgn5.js} +89 -26
  34. package/dist/{cli-bns5c8k9.js → cli-cf2vgn9d.js} +105 -91
  35. package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
  36. package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
  37. package/dist/{cli-mmzwwptd.js → cli-hf5s9pgf.js} +1 -1
  38. package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
  39. package/dist/{cli-zn2v3t8v.js → cli-jvmtxqtc.js} +27 -15
  40. package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
  41. package/dist/cli-kw0jcfje.js +516 -0
  42. package/dist/{cli-28smfvbn.js → cli-m7vmeepg.js} +32 -19
  43. package/dist/{cli-hpryr6em.js → cli-mb75wpxf.js} +1 -1
  44. package/dist/{cli-qdv8kgz7.js → cli-mkdsy1nq.js} +14018 -3333
  45. package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
  46. package/dist/{cli-420bdgw0.js → cli-mv2520nm.js} +1 -1
  47. package/dist/{cli-n9ehc28t.js → cli-mxry91sa.js} +31 -10
  48. package/dist/{cli-wfajtmf7.js → cli-mxvnnv5q.js} +596 -13
  49. package/dist/cli-nkqjj42q.js +104 -0
  50. package/dist/{cli-e7t4snpa.js → cli-p8rt8spp.js} +5 -5
  51. package/dist/{cli-gh5yqzpe.js → cli-pkj1w46r.js} +12 -7
  52. package/dist/cli-q1vnz8wv.js +96 -0
  53. package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
  54. package/dist/{cli-ba66wyhy.js → cli-qzmzwegq.js} +60 -20
  55. package/dist/{cli-5b2fjjzh.js → cli-r7eeddkq.js} +14 -9
  56. package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
  57. package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
  58. package/dist/{cli-59jn5twa.js → cli-tc5zec7v.js} +7 -7
  59. package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
  60. package/dist/{cli-k6xx3pve.js → cli-tx2zvn23.js} +4 -4
  61. package/dist/{cli-pjxnr1jn.js → cli-ty4bzkqr.js} +5 -5
  62. package/dist/{cli-nfygw434.js → cli-v1pwxreq.js} +55 -52
  63. package/dist/{cli-ftry7975.js → cli-v1w7pbd8.js} +3 -2
  64. package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
  65. package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
  66. package/dist/cli-wt7ccy4r.js +2654 -0
  67. package/dist/{cli-50eg13rj.js → cli-wydz3feg.js} +549 -321
  68. package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
  69. package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
  70. package/dist/{cli-x7afk5wm.js → cli-xvc3q9br.js} +1 -1
  71. package/dist/{cli-9s3h836g.js → cli-xvdgasgy.js} +1 -1
  72. package/dist/{cli-j0pdrmwg.js → cli-y0kjpfm9.js} +3 -3
  73. package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
  74. package/dist/{cli-amwhs8h2.js → cli-ykt8f7va.js} +22 -9
  75. package/dist/{cli-1kzfvd1z.js → cli-ymmc1zxg.js} +15 -11
  76. package/dist/{cli-xnzrzets.js → cli-ypgba1a8.js} +2 -2
  77. package/dist/{cli-bdv39kgj.js → cli-z1dqhc9y.js} +31 -20
  78. package/dist/{cli-fw6vdcq5.js → cli-zp67qg5x.js} +34 -1
  79. package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
  80. package/dist/cli.js +36 -32
  81. package/dist/{cloudflare-workers-ai-system-one-tt484t61.js → cloudflare-workers-ai-system-one-sta45jf9.js} +7 -5
  82. package/dist/{codex-adapter-3qnxshje.js → codex-adapter-cgesnjkr.js} +20 -20
  83. package/dist/{codex-login-brkxhhdg.js → codex-login-g1cyt5zm.js} +2 -2
  84. package/dist/{codex-session-runner-612wnapp.js → codex-session-runner-8kd7ddhr.js} +20 -20
  85. package/dist/{commands-2ybfcxkj.js → commands-h6zaaxym.js} +11 -10
  86. package/dist/{cursor-adapter-p3jgdehv.js → cursor-adapter-v8mm6bp5.js} +7 -7
  87. package/dist/{db-f4ans0jj.js → db-qg3myn53.js} +13 -10
  88. package/dist/{devin-adapter-dhzt01ff.js → devin-adapter-bffjj5n4.js} +2 -2
  89. package/dist/{dispatcher-pk182apd.js → dispatcher-7vnwj9qw.js} +28 -32
  90. package/dist/{dsh-adapter-7twaeps1.js → dsh-adapter-tz6hfp5r.js} +7 -7
  91. package/dist/{e2b-y8t3jzyz.js → e2b-nd50eccg.js} +1 -1
  92. package/dist/{easter-egg-3d-mtpnw06s.js → easter-egg-3d-66zxervv.js} +2 -2
  93. package/dist/{event-bus-w7ac4n3c.js → event-bus-vnfthafg.js} +2 -2
  94. package/dist/{execute-s4ccjq3n.js → execute-h4jgevyw.js} +61 -11
  95. package/dist/{google-generative-ai-d6nphg3q.js → google-generative-ai-b8zxrpax.js} +5 -5
  96. package/dist/{google-vertex-cpshkref.js → google-vertex-nrdd697t.js} +5 -5
  97. package/dist/grok-adapter-jdt2dz2s.js +39 -0
  98. package/dist/{handlers-g2wnr4t9.js → handlers-2qtkpncr.js} +31 -35
  99. package/dist/{hook-f7jt9vnm.js → hook-51yfcgsv.js} +48 -39
  100. package/dist/{hook-bbdpnkb4.js → hook-qnbdgwvd.js} +15 -17
  101. package/dist/{http-w40q4w94.js → http-jsmtag08.js} +536 -231
  102. package/dist/{index-056j5kzn.js → index-2t9kesn1.js} +13 -12
  103. package/dist/{index-cj1r9q83.js → index-bt8wkv1w.js} +29 -33
  104. package/dist/{index-7n70jwbb.js → index-cx1sb9bq.js} +42 -35
  105. package/dist/{index-9bfssf54.js → index-ekywyv2v.js} +28 -32
  106. package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
  107. package/dist/{index-k8xc083e.js → index-t4g06w93.js} +28 -32
  108. package/dist/{keepalive-1tj10tf4.js → keepalive-4aezx4bj.js} +16 -12
  109. package/dist/{lead-st0bg7v4.js → lead-r44bkpm5.js} +52 -56
  110. package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
  111. package/dist/{maintenance-nz71zats.js → maintenance-grz5jrzh.js} +12 -13
  112. package/dist/{mistral-conversations-8445ddjn.js → mistral-conversations-059y51zt.js} +4 -4
  113. package/dist/{node-f0ykebbv.js → node-qm9eb6z0.js} +25 -7
  114. package/dist/{oauth-refresh-sweep-aefzwvvq.js → oauth-refresh-sweep-wm4ctjfj.js} +15 -11
  115. package/dist/{onboard-kgpr9mbn.js → onboard-v60zgqf0.js} +2 -2
  116. package/dist/{openai-codex-responses-9ynm88zy.js → openai-codex-responses-jzxkta9b.js} +8 -7
  117. package/dist/{openai-completions-nhqwjmaw.js → openai-completions-663yncfq.js} +4 -4
  118. package/dist/openai-decisions-vj9nkpf8.js +162 -0
  119. package/dist/{openai-responses-xx1c4kgm.js → openai-responses-m3g43rrt.js} +5 -5
  120. package/dist/{opencode-adapter-0c8e70dk.js → opencode-adapter-yjyqynzf.js} +8 -8
  121. package/dist/{openrouter-images-8yx3487g.js → openrouter-images-kr60n6b9.js} +1 -1
  122. package/dist/{otel-impl-x8zp4y5a.js → otel-impl-ghh4dyf8.js} +2 -2
  123. package/dist/{pi-messages-vxdt8v7j.js → pi-messages-xszk613n.js} +1 -1
  124. package/dist/{pi-mono-adapter-t280ah5c.js → pi-mono-adapter-0eg8kw2c.js} +69 -52
  125. package/dist/{pricing-refresh-9e0q7cwk.js → pricing-refresh-mjrqvnvw.js} +12 -11
  126. package/dist/{rbac-roles-790j3ga8.js → rbac-roles-axv4d74x.js} +11 -10
  127. package/dist/{rbac-roles-kcp6zge2.js → rbac-roles-fd5tskj9.js} +11 -10
  128. package/dist/{render-v2-tch4nefj.js → render-v2-k50j33qd.js} +11 -11
  129. package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
  130. package/dist/{setup-rcm303ns.js → setup-4syh0pd2.js} +2 -2
  131. package/dist/{typesafe-system-one-datfr82p.js → typesafe-system-one-e988vty7.js} +7 -5
  132. package/dist/{virtual-modules-wm8mxdxx.js → virtual-modules-ftr9kwe8.js} +11 -11
  133. package/dist/{work-objects-ctg7t70w.js → work-objects-jcdbg8rr.js} +32 -36
  134. package/dist/{worker-5dp0xm55.js → worker-x8j8c9sx.js} +52 -56
  135. package/dist/{x-v3yw66zs.js → x-ma4gnfgy.js} +2 -2
  136. package/openapi.json +5 -2
  137. package/package.json +15 -11
  138. package/src/be/automation-preflight.ts +9 -4
  139. package/src/be/boot-scrub-sweep.ts +302 -0
  140. package/src/be/crypto/key-bootstrap.ts +1 -1
  141. package/src/be/db/approvals.ts +15 -1
  142. package/src/be/db/codex-oauth-device-flows.ts +40 -0
  143. package/src/be/db/config-secrets.ts +31 -0
  144. package/src/be/db/runtime.ts +4 -0
  145. package/src/be/db/script-runs.ts +15 -0
  146. package/src/be/db/tasks/write.ts +1 -1
  147. package/src/be/db-queries/mcp-oauth.ts +5 -0
  148. package/src/be/db-queries/oauth.ts +77 -0
  149. package/src/be/db.ts +162 -59
  150. package/src/be/events.ts +17 -1
  151. package/src/be/memory/index-content.ts +2 -2
  152. package/src/be/memory/providers/openai-embedding.ts +7 -2
  153. package/src/be/memory/providers/sqlite-store.ts +63 -11
  154. package/src/be/memory/raters/llm-summarizer.ts +7 -2
  155. package/src/be/memory/types.ts +14 -0
  156. package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
  157. package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
  158. package/src/be/modelsdev-cache.json +61 -0
  159. package/src/be/scripts/db.ts +28 -4
  160. package/src/be/scripts/source-secrets.ts +37 -0
  161. package/src/be/scrub-json.ts +32 -0
  162. package/src/be/sealed-json.ts +59 -0
  163. package/src/be/secret-registry.ts +131 -0
  164. package/src/be/seed-pricing.ts +84 -53
  165. package/src/be/swarm-config-guard.ts +13 -0
  166. package/src/be/workflow-replay.ts +77 -0
  167. package/src/commands/provider-credentials.ts +28 -5
  168. package/src/commands/runner.ts +7 -12
  169. package/src/heartbeat/heartbeat.ts +91 -19
  170. package/src/hooks/hook.ts +72 -27
  171. package/src/http/codex-oauth-device.ts +8 -10
  172. package/src/http/db-query-bounded.ts +3 -0
  173. package/src/http/db-query-shared.ts +107 -15
  174. package/src/http/db-query.ts +10 -2
  175. package/src/http/index.ts +23 -6
  176. package/src/http/mcp.ts +5 -0
  177. package/src/http/onboarding.ts +2 -0
  178. package/src/http/script-runs.ts +9 -1
  179. package/src/http/scripts.ts +15 -0
  180. package/src/http/workflows.ts +3 -2
  181. package/src/oauth/mcp-wrapper.ts +1 -1
  182. package/src/providers/acp-adapter.ts +25 -11
  183. package/src/providers/acp-target-catalog.ts +35 -1
  184. package/src/providers/acp-targets.ts +78 -7
  185. package/src/providers/amp-adapter.ts +1 -1
  186. package/src/providers/claude-adapter.ts +26 -0
  187. package/src/providers/codex-adapter.ts +30 -3
  188. package/src/providers/codex-oauth/auth-json.ts +21 -4
  189. package/src/providers/codex-oauth/types.ts +2 -0
  190. package/src/providers/cursor-adapter.ts +1 -1
  191. package/src/providers/dsh-adapter.ts +10 -1
  192. package/src/providers/grok-adapter.ts +18 -2
  193. package/src/providers/opencode-adapter.ts +21 -5
  194. package/src/providers/pi-mono-extension.ts +56 -25
  195. package/src/providers/pricing-sources.md +2 -0
  196. package/src/script-workflows/supervisor.ts +11 -2
  197. package/src/scripts-runtime/eval-harness.ts +2 -12
  198. package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
  199. package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
  200. package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
  201. package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
  202. package/src/scripts-runtime/executors/quickjs.ts +196 -0
  203. package/src/scripts-runtime/executors/registry.ts +2 -0
  204. package/src/scripts-runtime/executors/sourcemap.ts +79 -0
  205. package/src/scripts-runtime/script-error-hints.ts +14 -0
  206. package/src/server.ts +66 -4
  207. package/src/tests/acp-adapter.test.ts +277 -12
  208. package/src/tests/boot-scrub-sweep.test.ts +390 -0
  209. package/src/tests/check-text-columns.test.ts +145 -0
  210. package/src/tests/ci-unit-test-report.test.ts +73 -0
  211. package/src/tests/claude-default-route.test.ts +75 -0
  212. package/src/tests/claude-hook-output.test.ts +233 -0
  213. package/src/tests/claude-sdk-process.fixture.ts +13 -2
  214. package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
  215. package/src/tests/codex-adapter.test.ts +166 -26
  216. package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
  217. package/src/tests/codex-oauth-device.test.ts +23 -37
  218. package/src/tests/codex-oauth.test.ts +4 -1
  219. package/src/tests/context-window.test.ts +2 -2
  220. package/src/tests/credential-check.test.ts +51 -1
  221. package/src/tests/db-query-denylist.test.ts +202 -0
  222. package/src/tests/db-task-read-contract.test.ts +1 -0
  223. package/src/tests/db-task-write-contract.test.ts +1 -0
  224. package/src/tests/dsh-adapter.test.ts +23 -1
  225. package/src/tests/enabled-tools.test.ts +269 -0
  226. package/src/tests/entrypoint-acp-target-binary.test.ts +31 -1
  227. package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
  228. package/src/tests/events-scrub.test.ts +88 -0
  229. package/src/tests/fixtures/secret-corpus.ts +320 -0
  230. package/src/tests/grok-adapter.test.ts +13 -0
  231. package/src/tests/haiku-5-5-pricing.test.ts +38 -0
  232. package/src/tests/heartbeat-checklist.test.ts +158 -1
  233. package/src/tests/onboarding.test.ts +44 -0
  234. package/src/tests/opencode-adapter.test.ts +66 -3
  235. package/src/tests/opencode-plugin.test.ts +37 -0
  236. package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
  237. package/src/tests/openrouter-base-url.test.ts +30 -39
  238. package/src/tests/pi-mono-extension.test.ts +161 -2
  239. package/src/tests/pricing-refresh.test.ts +70 -1
  240. package/src/tests/reasoning-effort.test.ts +3 -1
  241. package/src/tests/script-connections.test.ts +5 -0
  242. package/src/tests/script-executor-conformance.test.ts +3 -0
  243. package/src/tests/script-executor-quickjs.test.ts +442 -0
  244. package/src/tests/script-executor-registry.test.ts +6 -1
  245. package/src/tests/script-runs-http.test.ts +88 -1
  246. package/src/tests/script-runs-scrub.test.ts +275 -0
  247. package/src/tests/scripts-http.test.ts +82 -1
  248. package/src/tests/scrubbed-text-12e.test.ts +289 -0
  249. package/src/tests/sealed-key-guard.test.ts +166 -0
  250. package/src/tests/secret-canary-sweep.test.ts +958 -0
  251. package/src/tests/secret-registry.test.ts +355 -0
  252. package/src/tests/secret-rules.test.ts +230 -0
  253. package/src/tests/secret-scrubber.test.ts +3 -2
  254. package/src/tests/telemetry-trigger.test.ts +12 -1
  255. package/src/tests/workflow-llm.test.ts +14 -0
  256. package/src/tests/workflow-replay.test.ts +518 -0
  257. package/src/tests/workflow-scrub.test.ts +242 -0
  258. package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
  259. package/src/tools/db-query.ts +1 -1
  260. package/src/tools/inject-learning.ts +1 -1
  261. package/src/tools/request-human-input.ts +7 -2
  262. package/src/tools/workflows/get-workflow-run.ts +3 -2
  263. package/src/types.ts +1 -1
  264. package/src/utils/enabled-tools.ts +27 -0
  265. package/src/utils/internal-ai/complete-structured.ts +11 -3
  266. package/src/utils/openrouter-base-url.ts +23 -0
  267. package/src/utils/secret-rules.generated.ts +2055 -0
  268. package/src/utils/secret-scrubber.ts +363 -29
  269. package/src/workflows/engine.ts +39 -4
  270. package/src/workflows/executors/raw-llm.ts +1 -0
  271. package/src/workflows/executors/system-one-decision.ts +2 -0
  272. package/src/workflows/executors/validate.ts +1 -0
  273. package/src/workflows/executors/workflow-llm.ts +9 -2
  274. package/src/workflows/recovery.ts +10 -1
  275. package/src/workflows/resume.ts +62 -3
  276. package/src/workflows/retry-poller.ts +3 -0
  277. package/templates/skills/agent-browser/SKILL.md +2 -0
  278. package/templates/skills/agent-browser/content.md +2 -0
  279. package/templates/skills/code-quality/content.md +1 -0
  280. package/templates/skills/heartbeat-runbook/content.md +1 -1
  281. package/templates/skills/work-on-task/SKILL.md +4 -0
  282. package/templates/skills/work-on-task/content.md +4 -0
  283. package/templates/skills/workflow-iterate/content.md +9 -0
  284. package/dist/acp-adapter-w6zws6me.js +0 -23
  285. package/dist/boot-scrub-logs-246yn076.js +0 -99
  286. package/dist/cli-3a4m1aws.js +0 -572
  287. package/dist/cli-3dpj7atn.js +0 -11
  288. package/dist/cli-a87b04k8.js +0 -951
  289. package/dist/cli-cvk6yzw5.js +0 -40
  290. package/dist/cli-gvcgmx69.js +0 -183
  291. package/dist/cli-h1spv8v4.js +0 -427
  292. package/dist/cli-jqt9hrnz.js +0 -168
  293. package/dist/cli-yhmwmv0g.js +0 -9281
  294. package/dist/grok-adapter-yt9pab3k.js +0 -29
  295. package/src/be/boot-scrub-logs.ts +0 -132
  296. package/dist/{codex-hook-d2rkvf39.js → codex-hook-15dgc4cb.js} +1 -1
@@ -1,4 +1,4 @@
1
- export const ACP_TARGET_IDS = ["opencode", "gemini", "custom"] as const;
1
+ export const ACP_TARGET_IDS = ["opencode", "gemini", "copilot", "custom"] as const;
2
2
 
3
3
  export type AcpTarget = (typeof ACP_TARGET_IDS)[number];
4
4
 
@@ -61,6 +61,40 @@ export const ACP_TARGET_CATALOG: readonly AcpTargetCatalogEntry[] = [
61
61
  ],
62
62
  knobs: [{ id: "model", label: "Model", category: "model" }],
63
63
  },
64
+ {
65
+ id: "copilot",
66
+ label: "GitHub Copilot CLI",
67
+ description:
68
+ "GitHub Copilot CLI's ACP server, launched with `copilot --acp`. Uses a Copilot-entitled COPILOT_GITHUB_TOKEN, or a BYOK provider via COPILOT_PROVIDER_*.",
69
+ command: "copilot",
70
+ args: ["--acp"],
71
+ envKeys: [
72
+ "COPILOT_GITHUB_TOKEN",
73
+ "COPILOT_GH_HOST",
74
+ "GH_HOST",
75
+ "COPILOT_HOME",
76
+ "COPILOT_MODEL",
77
+ "COPILOT_AUTO_TIER",
78
+ "COPILOT_OFFLINE",
79
+ "COPILOT_PROVIDER_BASE_URL",
80
+ "COPILOT_PROVIDER_TYPE",
81
+ "COPILOT_PROVIDER_API_KEY",
82
+ "COPILOT_PROVIDER_API_KEY_COMMAND",
83
+ "COPILOT_PROVIDER_BEARER_TOKEN",
84
+ "COPILOT_PROVIDER_WIRE_API",
85
+ "COPILOT_PROVIDER_TRANSPORT",
86
+ "COPILOT_PROVIDER_AZURE_API_VERSION",
87
+ "COPILOT_PROVIDER_MODEL_ID",
88
+ "COPILOT_PROVIDER_WIRE_MODEL",
89
+ "COPILOT_PROVIDER_MAX_PROMPT_TOKENS",
90
+ "COPILOT_PROVIDER_MAX_OUTPUT_TOKENS",
91
+ "COPILOT_PROVIDER_HEADERS",
92
+ "HTTP_PROXY",
93
+ "HTTPS_PROXY",
94
+ "NO_PROXY",
95
+ ],
96
+ knobs: [{ id: "model", label: "Model", category: "model" }],
97
+ },
64
98
  {
65
99
  id: "custom",
66
100
  label: "Custom",
@@ -1,9 +1,16 @@
1
- import { mkdtemp } from "node:fs/promises";
1
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { normalizeGrokModel } from "@desplega/model-catalog";
5
+ import type { Config } from "@opencode-ai/sdk";
5
6
  import { clampContextPercent, getContextWindowSize } from "../utils/context-window";
6
- import { type AcpTarget, getAcpTargetCatalogEntry, isAcpTarget } from "./acp-target-catalog";
7
+ import {
8
+ ACP_TARGET_IDS,
9
+ type AcpTarget,
10
+ getAcpTargetCatalogEntry,
11
+ isAcpTarget,
12
+ } from "./acp-target-catalog";
13
+ import { applyOpenRouterBaseUrlOverride } from "./opencode-adapter";
7
14
  import { applyReasoningEffort } from "./reasoning-effort";
8
15
  import type { CostData, CostModelUsage, ProviderEvent, ProviderSessionConfig } from "./types";
9
16
 
@@ -14,6 +21,8 @@ export interface AcpTargetProfile {
14
21
  env(config: ProviderSessionConfig): Record<string, string>;
15
22
  configuredOptions(config: ProviderSessionConfig): Record<string, string | boolean>;
16
23
  writeSystemPromptArtifact(config: ProviderSessionConfig): Promise<void>;
24
+ /** Removes what writeSystemPromptArtifact wrote. Called once the session ends or fails to start. */
25
+ cleanupSystemPromptArtifact?(config: ProviderSessionConfig): Promise<void>;
17
26
  /** Target-specific `session/new` `_meta`. Targets without one send none. */
18
27
  sessionMeta?(config: ProviderSessionConfig): Record<string, unknown> | undefined;
19
28
  /** A clearer message for a target error whose own text misleads, else undefined. */
@@ -117,8 +126,8 @@ function configuredOptions(config: ProviderSessionConfig): Record<string, string
117
126
  return options;
118
127
  }
119
128
 
120
- function withModelInJsonEnv(env: Record<string, string>, key: string, model: string): void {
121
- if (!model.trim()) return;
129
+ function withOpencodeConfig(env: Record<string, string>, model: string): void {
130
+ const key = "OPENCODE_CONFIG_CONTENT";
122
131
  let value: Record<string, unknown> = {};
123
132
  const existing = env[key];
124
133
  if (existing) {
@@ -131,7 +140,9 @@ function withModelInJsonEnv(env: Record<string, string>, key: string, model: str
131
140
  console.warn(`\x1b[33m[acp]\x1b[0m Replacing invalid ${key} JSON for model fallback`);
132
141
  }
133
142
  }
134
- env[key] = JSON.stringify({ ...value, model: model.trim() });
143
+ if (model.trim()) value.model = model.trim();
144
+ applyOpenRouterBaseUrlOverride(value as Config, env);
145
+ env[key] = JSON.stringify(value);
135
146
  }
136
147
 
137
148
  function parseCommand(command: string, args: string | undefined): string[] {
@@ -196,7 +207,8 @@ const opencodeTargetProfile: AcpTargetProfile = {
196
207
  const env = baseTargetEnv(config);
197
208
  const entry = getAcpTargetCatalogEntry("opencode");
198
209
  copyEnvKeys(config, env, entry.envKeys);
199
- withModelInJsonEnv(env, "OPENCODE_CONFIG_CONTENT", config.model);
210
+ copyEnvKeys(config, env, ["OPENROUTER_APP_ATTRIBUTION"]);
211
+ withOpencodeConfig(env, config.model);
200
212
  return env;
201
213
  },
202
214
  configuredOptions,
@@ -238,6 +250,63 @@ const geminiTargetProfile: AcpTargetProfile = {
238
250
  },
239
251
  };
240
252
 
253
+ /**
254
+ * Copilot CLI has no system-prompt flag. Under `--acp` it reads custom
255
+ * instructions from the git root, the cwd, and every directory listed in
256
+ * COPILOT_CUSTOM_INSTRUCTIONS_DIRS. Verified against 1.0.93: from those extra
257
+ * directories it loads `.github/instructions/*.instructions.md`, but not
258
+ * `AGENTS.md`. So the swarm prompt goes into a per-task directory outside the
259
+ * repo, and the repo's own AGENTS.md is never touched.
260
+ */
261
+ export function copilotInstructionsDir(config: ProviderSessionConfig): string {
262
+ const key = new Bun.CryptoHasher("sha256")
263
+ .update(`${config.agentId}:${config.taskId}:${config.cwd}`)
264
+ .digest("hex")
265
+ .slice(0, 16);
266
+ return join(tmpdir(), "agent-swarm-copilot-instructions", key);
267
+ }
268
+
269
+ export const COPILOT_INSTRUCTIONS_FILE = join(
270
+ ".github",
271
+ "instructions",
272
+ "agent-swarm.instructions.md",
273
+ );
274
+
275
+ const copilotTargetProfile: AcpTargetProfile = {
276
+ target: "copilot",
277
+ command() {
278
+ const entry = getAcpTargetCatalogEntry("copilot");
279
+ return [entry.command!, ...(entry.args ?? [])];
280
+ },
281
+ env(config) {
282
+ const env = baseTargetEnv(config);
283
+ const entry = getAcpTargetCatalogEntry("copilot");
284
+ copyEnvKeys(config, env, entry.envKeys);
285
+ if (config.model.trim()) env.COPILOT_MODEL = config.model.trim();
286
+ // The image pins the CLI; a self-update would run an unpinned binary.
287
+ env.COPILOT_AUTO_UPDATE = "false";
288
+ const operatorDirs = readEnv(config, "COPILOT_CUSTOM_INSTRUCTIONS_DIRS")?.trim();
289
+ env.COPILOT_CUSTOM_INSTRUCTIONS_DIRS = [copilotInstructionsDir(config), operatorDirs]
290
+ .filter(Boolean)
291
+ .join(",");
292
+ return env;
293
+ },
294
+ configuredOptions,
295
+ async writeSystemPromptArtifact(config) {
296
+ const dir = copilotInstructionsDir(config);
297
+ await rm(dir, { recursive: true, force: true });
298
+ if (!config.systemPrompt) return;
299
+ await mkdir(join(dir, ".github", "instructions"), { recursive: true });
300
+ await Bun.write(
301
+ join(dir, COPILOT_INSTRUCTIONS_FILE),
302
+ `---\napplyTo: "**"\n---\n\n${config.systemPrompt}\n`,
303
+ );
304
+ },
305
+ async cleanupSystemPromptArtifact(config) {
306
+ await rm(copilotInstructionsDir(config), { recursive: true, force: true });
307
+ },
308
+ };
309
+
241
310
  /**
242
311
  * Grok CLI env keys passed through to `grok agent stdio`. `GROK_HOME` is the
243
312
  * per-session state dir the grok adapter creates; the rest switch off the
@@ -471,7 +540,7 @@ export function resolveAcpTarget(config: ProviderSessionConfig): AcpTargetProfil
471
540
  const target = readEnv(config, "ACP_TARGET") ?? "custom";
472
541
  if (!isAcpTarget(target)) {
473
542
  throw new AcpTargetResolutionError(
474
- `Unsupported ACP target "${target}". Supported targets: opencode, gemini, custom.`,
543
+ `Unsupported ACP target "${target}". Supported targets: ${ACP_TARGET_IDS.join(", ")}.`,
475
544
  );
476
545
  }
477
546
  switch (target) {
@@ -479,6 +548,8 @@ export function resolveAcpTarget(config: ProviderSessionConfig): AcpTargetProfil
479
548
  return opencodeTargetProfile;
480
549
  case "gemini":
481
550
  return geminiTargetProfile;
551
+ case "copilot":
552
+ return copilotTargetProfile;
482
553
  case "custom":
483
554
  return customTargetProfile;
484
555
  }
@@ -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.1791360091-gfb32ce";
47
+ export const AMP_PACKAGE = "@ampcode/cli@0.0.1791460855-g1f688c";
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";
@@ -34,6 +34,7 @@ import {
34
34
  import { withFileLock } from "../utils/file-lock";
35
35
  import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
36
36
  import { swarmRuntimeInstanceId } from "../utils/multi-runtime";
37
+ import { getOpenRouterAttributionHeaders } from "../utils/openrouter-base-url";
37
38
  import {
38
39
  detachedProcessGroup,
39
40
  registerProcessGroup,
@@ -738,6 +739,30 @@ export function buildClaudeCodeRuntimeEnv(
738
739
  };
739
740
  }
740
741
 
742
+ /**
743
+ * `ANTHROPIC_CUSTOM_HEADERS` ("Name: value" per line) with OpenRouter app
744
+ * attribution appended, when `ANTHROPIC_BASE_URL` points at openrouter.ai.
745
+ * The operator's lines are kept in order (they may carry gateway
746
+ * credentials); only lines naming one of our headers are replaced. Returns
747
+ * `{}` when nothing changes, so the inherited value stays byte-identical.
748
+ *
749
+ * Exported for unit testing.
750
+ */
751
+ export function claudeOpenRouterAttributionEnv(env: Record<string, string | undefined>): {
752
+ ANTHROPIC_CUSTOM_HEADERS?: string;
753
+ } {
754
+ const headers = getOpenRouterAttributionHeaders(env.ANTHROPIC_BASE_URL ?? "", env);
755
+ const names = new Set(Object.keys(headers).map((name) => name.toLowerCase()));
756
+ if (names.size === 0) return {};
757
+ const kept = (env.ANTHROPIC_CUSTOM_HEADERS ?? "").split("\n").filter((line) => {
758
+ const colon = line.indexOf(":");
759
+ const name = colon === -1 ? "" : line.slice(0, colon).trim().toLowerCase();
760
+ return line.trim() !== "" && !names.has(name);
761
+ });
762
+ const ours = Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
763
+ return { ANTHROPIC_CUSTOM_HEADERS: [...kept, ...ours].join("\n") };
764
+ }
765
+
741
766
  export function buildClaudeSessionEnvironment(
742
767
  config: ProviderSessionConfig,
743
768
  model: string,
@@ -751,6 +776,7 @@ export function buildClaudeSessionEnvironment(
751
776
  env: {
752
777
  ENABLE_PROMPT_CACHING_1H: "1",
753
778
  ...sourceEnv,
779
+ ...claudeOpenRouterAttributionEnv(sourceEnv),
754
780
  ...buildClaudeCodeRuntimeEnv(sourceEnv),
755
781
  ...buildClaudeCodeOtelEnv(sourceEnv),
756
782
  ...(reasoningApplication.kind === "claude-env" ? reasoningApplication.env : {}),
@@ -147,6 +147,8 @@ export type ThreadEvent =
147
147
  usage?: Usage;
148
148
  }
149
149
  | { type: "turn.interrupted"; usage?: Usage }
150
+ /** Latest single-request usage: a context-window snapshot, not billable usage. */
151
+ | { type: "context.updated"; usage: Usage; contextWindow?: number }
150
152
  | { type: "error"; message: string; codexErrorInfo?: string | null; willRetry?: boolean };
151
153
 
152
154
  interface CodexThread {
@@ -833,8 +835,25 @@ class AppServerCodexThread implements CodexThread {
833
835
  }
834
836
  break;
835
837
  case "thread/tokenUsage/updated": {
836
- const tokenUsage = params.tokenUsage as { total?: unknown } | undefined;
838
+ const tokenUsage = params.tokenUsage as
839
+ | { total?: unknown; last?: unknown; modelContextWindow?: unknown }
840
+ | undefined;
841
+ // `total` is cumulative for the thread and drives cost accounting at
842
+ // turn end. `last` is the most recent model request, which is the
843
+ // only figure that reflects what currently sits in the context window.
837
844
  this.latestTotalUsage = normalizeUsage(tokenUsage?.total);
845
+ const last = normalizeUsage(tokenUsage?.last);
846
+ if (last) {
847
+ push({
848
+ type: "context.updated",
849
+ usage: last,
850
+ contextWindow:
851
+ typeof tokenUsage?.modelContextWindow === "number" &&
852
+ tokenUsage.modelContextWindow > 0
853
+ ? tokenUsage.modelContextWindow
854
+ : undefined,
855
+ });
856
+ }
838
857
  break;
839
858
  }
840
859
  case "error": {
@@ -1394,6 +1413,9 @@ export class CodexSession implements ProviderSession {
1394
1413
  case "turn.interrupted":
1395
1414
  this.accountUsage(event.usage);
1396
1415
  break;
1416
+ case "context.updated":
1417
+ this.emitContextUsage(event.usage, event.contextWindow ?? this.contextWindow);
1418
+ break;
1397
1419
  case "error": {
1398
1420
  const { message } = this.formatTerminalError(event.message, event.codexErrorInfo);
1399
1421
  if (event.willRetry === true) {
@@ -1420,7 +1442,12 @@ export class CodexSession implements ProviderSession {
1420
1442
  this.accumulatedUsage.reasoning_output_tokens + usage.reasoning_output_tokens,
1421
1443
  }
1422
1444
  : usage;
1445
+ // No context_usage here: a turn's usage sums every model request in the
1446
+ // turn, so it measures consumption, not window occupancy. Context comes
1447
+ // from the per-request `context.updated` snapshots instead.
1448
+ }
1423
1449
 
1450
+ private emitContextUsage(usage: Usage, contextWindow: number): void {
1424
1451
  // Codex input tokens already include the cached read and write subsets.
1425
1452
  // Passing either subset again would inflate context usage.
1426
1453
  const contextUsed = computeContextUsedUnified({
@@ -1432,8 +1459,8 @@ export class CodexSession implements ProviderSession {
1432
1459
  this.emit({
1433
1460
  type: "context_usage",
1434
1461
  contextUsedTokens: contextUsed,
1435
- contextTotalTokens: this.contextWindow,
1436
- contextPercent: clampContextPercent(contextUsed, this.contextWindow) ?? 0,
1462
+ contextTotalTokens: contextWindow,
1463
+ contextPercent: clampContextPercent(contextUsed, contextWindow) ?? 0,
1437
1464
  outputTokens: usage.output_tokens,
1438
1465
  contextFormula: CONTEXT_FORMULA,
1439
1466
  });
@@ -2,7 +2,7 @@
2
2
  * Conversion utilities between CodexOAuthCredentials (our internal type)
3
3
  * and ~/.codex/auth.json (the format the Codex CLI reads natively).
4
4
  *
5
- * The Codex CLI expects auth.json in this exact format:
5
+ * The Codex CLI expects these native auth.json fields:
6
6
  * {
7
7
  * "auth_mode": "chatgpt",
8
8
  * "OPENAI_API_KEY": null,
@@ -14,6 +14,7 @@
14
14
  * },
15
15
  * "last_refresh": "<ISO 8601>"
16
16
  * }
17
+ * Swarm also writes optional `expires` metadata for lossless conversion.
17
18
  *
18
19
  * Note: `id_token` is set to the `access_token` value because the token
19
20
  * exchange endpoint doesn't return a separate `id_token`. This matches
@@ -23,7 +24,7 @@
23
24
  */
24
25
 
25
26
  import { codexPlanFromClaim } from "../../utils/subscription-plans.js";
26
- import { extractChatgptPlanType, extractChatgptUserId } from "./flow.js";
27
+ import { decodeJwt, extractChatgptPlanType, extractChatgptUserId } from "./flow.js";
27
28
  import type { CodexAuthJson, CodexOAuthCredentials } from "./types.js";
28
29
 
29
30
  /**
@@ -96,15 +97,31 @@ export function credentialsToAuthJson(
96
97
  refresh_token: includeRefreshToken ? creds.refresh : "",
97
98
  account_id: creds.accountId,
98
99
  },
99
- last_refresh: new Date(creds.expires).toISOString(),
100
+ last_refresh: new Date().toISOString(),
101
+ // Swarm-only metadata keeps opaque-token round trips lossless. Codex
102
+ // ignores this field and may drop it when it rewrites auth.json.
103
+ expires: creds.expires,
100
104
  };
101
105
  }
102
106
 
103
107
  export function authJsonToCredentials(auth: CodexAuthJson): CodexOAuthCredentials {
108
+ // A CLI refresh may replace the access token and discard our metadata.
109
+ // Prefer its JWT expiry, which describes the token actually in the file.
110
+ const exp = decodeJwt(auth.tokens.access_token)?.exp;
111
+ const jwtExpires = typeof exp === "number" ? exp * 1000 : NaN;
112
+ const expires =
113
+ Number.isFinite(jwtExpires) && jwtExpires >= 0
114
+ ? jwtExpires
115
+ : typeof auth.expires === "number" && Number.isFinite(auth.expires)
116
+ ? auth.expires
117
+ : new Date(auth.last_refresh).getTime();
118
+
104
119
  return {
105
120
  access: auth.tokens.access_token,
106
121
  refresh: auth.tokens.refresh_token,
107
- expires: new Date(auth.last_refresh).getTime(),
122
+ // Legacy swarm files put expiry in last_refresh; keep that fallback
123
+ // only when neither the token nor the explicit metadata carries it.
124
+ expires,
108
125
  accountId: auth.tokens.account_id,
109
126
  };
110
127
  }
@@ -28,6 +28,8 @@ export type CodexAuthJson = {
28
28
  account_id: string;
29
29
  };
30
30
  last_refresh: string;
31
+ /** Swarm-only access-token expiry (milliseconds); native Codex files omit it. */
32
+ expires?: number;
31
33
  };
32
34
 
33
35
  type TokenResult =
@@ -35,7 +35,7 @@ import type {
35
35
  } from "./types";
36
36
 
37
37
  /** Pinned in package.json and Dockerfile.worker; bump all three together. */
38
- export const CURSOR_SDK_VERSION = "1.0.36";
38
+ export const CURSOR_SDK_VERSION = "1.0.37";
39
39
 
40
40
  /** MCP server name; Cursor reports its tools as `mcp` calls with this `providerIdentifier`. */
41
41
  const CURSOR_MCP_SERVER_NAME = "agent-swarm";
@@ -9,7 +9,10 @@ import {
9
9
  } from "../utils/context-window";
10
10
  import { addDshStepUsage, type DshStepUsage, normalizeDshStepUsage } from "../utils/dsh-usage";
11
11
  import { swarmRuntimeInstanceId } from "../utils/multi-runtime";
12
- import { getOpenRouterBaseUrl } from "../utils/openrouter-base-url";
12
+ import {
13
+ getOpenRouterAttributionHeaders,
14
+ getOpenRouterBaseUrl,
15
+ } from "../utils/openrouter-base-url";
13
16
  import {
14
17
  detachedProcessGroup,
15
18
  registerProcessGroup,
@@ -342,6 +345,9 @@ export class DshAdapter implements ProviderAdapter {
342
345
  const directory = await mkdtemp(join(tmpdir(), "swarm-dsh-"));
343
346
  try {
344
347
  const patchPath = join(directory, "patch.json");
348
+ const openRouterHeaders = openrouter
349
+ ? getOpenRouterAttributionHeaders(getOpenRouterBaseUrl(env), env)
350
+ : {};
345
351
  const runtimeInstanceId = swarmRuntimeInstanceId();
346
352
  // JSON is valid YAML. Values stay data, including arbitrary system prompts.
347
353
  await writeFile(
@@ -364,6 +370,9 @@ export class DshAdapter implements ProviderAdapter {
364
370
  openrouter: {
365
371
  apiKeyEnv: keyEnv,
366
372
  baseURL: getOpenRouterBaseUrl(env),
373
+ ...(Object.keys(openRouterHeaders).length > 0
374
+ ? { headers: openRouterHeaders }
375
+ : {}),
367
376
  api: "openai-completions",
368
377
  // Declare the selected ID even if the bundled catalog predates it.
369
378
  models: [
@@ -3,7 +3,10 @@ import { mkdtemp, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { normalizeGrokModel } from "@desplega/model-catalog";
6
- import { getOpenRouterBaseUrl } from "../utils/openrouter-base-url";
6
+ import {
7
+ getOpenRouterAttributionHeaders,
8
+ getOpenRouterBaseUrl,
9
+ } from "../utils/openrouter-base-url";
7
10
  import { registerVolatileSecret } from "../utils/secret-scrubber";
8
11
  import { ACPAdapter } from "./acp-adapter";
9
12
  import { grokTargetProfile, isGrokOpenRouterModel } from "./acp-targets";
@@ -64,6 +67,8 @@ export interface GrokCustomModel {
64
67
  baseUrl: string;
65
68
  /** Env var holding the endpoint's API key. */
66
69
  envKey: string;
70
+ /** Sent verbatim on every request to the endpoint (`extra_headers`). */
71
+ headers?: Record<string, string>;
67
72
  }
68
73
 
69
74
  /** `openrouter/<vendor>/<id>` -> a `[model."openrouter/<vendor>/<id>"]` block on OpenRouter. */
@@ -73,14 +78,24 @@ export function grokOpenRouterModel(
73
78
  ): GrokCustomModel | null {
74
79
  const id = model.trim();
75
80
  if (!isGrokOpenRouterModel(id)) return null;
81
+ const baseUrl = getOpenRouterBaseUrl(env);
76
82
  return {
77
83
  id,
78
84
  model: id.slice("openrouter/".length),
79
- baseUrl: getOpenRouterBaseUrl(env),
85
+ baseUrl,
80
86
  envKey: "OPENROUTER_API_KEY",
87
+ headers: getOpenRouterAttributionHeaders(baseUrl, env),
81
88
  };
82
89
  }
83
90
 
91
+ /** `key = { "k" = "v", ... }`, or nothing for an empty map. JSON strings are valid TOML basic strings. */
92
+ function tomlInlineTable(key: string, entries: Record<string, string> | undefined): string[] {
93
+ const pairs = Object.entries(entries ?? {});
94
+ if (pairs.length === 0) return [];
95
+ const body = pairs.map(([k, v]) => `${JSON.stringify(k)} = ${JSON.stringify(v)}`).join(", ");
96
+ return [`${key} = { ${body} }`];
97
+ }
98
+
84
99
  /** `$GROK_HOME/config.toml` for one swarm session. */
85
100
  export function buildGrokConfigToml(
86
101
  disabledPlugins: readonly string[],
@@ -95,6 +110,7 @@ export function buildGrokConfigToml(
95
110
  `base_url = ${JSON.stringify(customModel.baseUrl)}`,
96
111
  `env_key = ${JSON.stringify(customModel.envKey)}`,
97
112
  `api_backend = "chat_completions"`,
113
+ ...tomlInlineTable("extra_headers", customModel.headers),
98
114
  ]
99
115
  : [];
100
116
  return [
@@ -20,7 +20,11 @@ import {
20
20
  import { validateOpencodeCredentials } from "../utils/credentials";
21
21
  import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
22
22
  import { swarmRuntimeInstanceId } from "../utils/multi-runtime";
23
- import { DEFAULT_OPENROUTER_BASE_URL, getOpenRouterBaseUrl } from "../utils/openrouter-base-url";
23
+ import {
24
+ DEFAULT_OPENROUTER_BASE_URL,
25
+ getOpenRouterAttributionHeaders,
26
+ getOpenRouterBaseUrl,
27
+ } from "../utils/openrouter-base-url";
24
28
  import { scrubSecrets } from "../utils/secret-scrubber";
25
29
  import { resolveSlashSkillPrompt } from "./codex-skill-resolver";
26
30
  import { CTX_MODE_NUDGE_EVERY } from "./ctx-mode-env";
@@ -177,22 +181,34 @@ function formatUnknownError(err: unknown): string {
177
181
  * Route OpenRouter traffic through the configured gateway (see
178
182
  * src/utils/openrouter-base-url.ts). opencode's bundled openrouter provider
179
183
  * honors `provider.openrouter.options.baseURL`. Mutates `opencodeConfig` in
180
- * place; no-op when `OPENROUTER_BASE_URL` is unset/blank/default, so default
181
- * openrouter.ai behavior is preserved. Exported for tests.
184
+ * place, and attribute direct OpenRouter requests to Agent Swarm.
182
185
  */
183
186
  export function applyOpenRouterBaseUrlOverride(
184
187
  opencodeConfig: Config,
185
188
  env: Record<string, string | undefined> = process.env,
186
189
  ): void {
187
190
  const openRouterBaseUrl = getOpenRouterBaseUrl(env as NodeJS.ProcessEnv);
188
- if (openRouterBaseUrl === DEFAULT_OPENROUTER_BASE_URL) return;
191
+ const headers = getOpenRouterAttributionHeaders(openRouterBaseUrl, env);
192
+ const overrideBaseUrl = openRouterBaseUrl !== DEFAULT_OPENROUTER_BASE_URL;
193
+ if (!overrideBaseUrl && Object.keys(headers).length === 0) return;
189
194
  opencodeConfig.provider = {
190
195
  ...opencodeConfig.provider,
191
196
  openrouter: {
192
197
  ...opencodeConfig.provider?.openrouter,
193
198
  options: {
194
199
  ...opencodeConfig.provider?.openrouter?.options,
195
- baseURL: openRouterBaseUrl,
200
+ ...(overrideBaseUrl ? { baseURL: openRouterBaseUrl } : {}),
201
+ ...(Object.keys(headers).length > 0
202
+ ? {
203
+ headers: {
204
+ ...(opencodeConfig.provider?.openrouter?.options?.headers as
205
+ | Record<string, string>
206
+ | undefined),
207
+ ...headers,
208
+ "X-Title": "Agent Swarm",
209
+ },
210
+ }
211
+ : {}),
196
212
  },
197
213
  },
198
214
  };
@@ -11,6 +11,7 @@ import { buildRatingsFromLlm, fetchRetrievalsForTask, postRatings } from "../be/
11
11
  import { checkToolLoop, clearToolHistory } from "../hooks/tool-loop-detection";
12
12
  import { summarizeSession as runSummarize } from "../utils/internal-ai";
13
13
  import { getMemoryRaterNames } from "../utils/memory-raters";
14
+ import { getOpenRouterAttributionHeaders } from "../utils/openrouter-base-url";
14
15
  import { scrubSecrets } from "../utils/secret-scrubber";
15
16
 
16
17
  export interface SwarmHooksConfig {
@@ -421,6 +422,24 @@ export async function summarizeSessionForPi(
421
422
  }
422
423
  }
423
424
 
425
+ /** `customType` of the advisory messages this extension sends to the model. */
426
+ export const SWARM_NUDGE_CUSTOM_TYPE = "swarm-nudge";
427
+
428
+ /**
429
+ * Deliver an advisory nudge to the model. pi runs extensions in-process and
430
+ * never feeds their stdout to the model, so a console.log here only reaches
431
+ * the worker log. `sendMessage` steers the text in after the current tool
432
+ * batch while the agent is streaming, and otherwise appends it to the session
433
+ * so the next prompt carries it.
434
+ */
435
+ function sendNudge(pi: Parameters<ExtensionFactory>[0], text: string): void {
436
+ pi.sendMessage({
437
+ customType: SWARM_NUDGE_CUSTOM_TYPE,
438
+ content: [{ type: "text", text }],
439
+ display: false,
440
+ });
441
+ }
442
+
424
443
  /**
425
444
  * Create the swarm hooks extension factory for pi-mono.
426
445
  *
@@ -431,6 +450,13 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
431
450
  return (pi) => {
432
451
  let lastContextPostTime = 0;
433
452
 
453
+ pi.on("before_provider_headers", (event, ctx) => {
454
+ Object.assign(
455
+ event.headers,
456
+ getOpenRouterAttributionHeaders(ctx.model?.baseUrl ?? "", config.env),
457
+ );
458
+ });
459
+
434
460
  // === session_start → SessionStart ===
435
461
  pi.on("session_start", async (_event, _ctx) => {
436
462
  // Installed MCP servers: pi's MCP extension connects them in the
@@ -458,7 +484,7 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
458
484
  if (config.isLead) {
459
485
  const ctx = await fetchConcurrentContext(config);
460
486
  if (ctx) {
461
- console.log(ctx);
487
+ sendNudge(pi, ctx);
462
488
  }
463
489
  }
464
490
  });
@@ -498,7 +524,7 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
498
524
  }
499
525
 
500
526
  if (loopResult.severity === "warning" && loopResult.reason) {
501
- console.log(`Warning: ${loopResult.reason}`);
527
+ sendNudge(pi, `Warning: ${loopResult.reason}`);
502
528
  }
503
529
  }
504
530
 
@@ -586,7 +612,8 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
586
612
 
587
613
  // Reminders
588
614
  if (config.isLead && event.toolName?.endsWith("send-task")) {
589
- console.log(
615
+ sendNudge(
616
+ pi,
590
617
  "Task sent successfully. Monitor progress using the get-task-details tool periodically.",
591
618
  );
592
619
  }
@@ -594,29 +621,30 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
594
621
  return undefined;
595
622
  });
596
623
 
597
- // === context → PreCompact ===
598
- // The context event allows injecting messages before compaction.
599
- // We log the goal reminder to console (it gets captured in context).
600
- pi.on("context", async (_event, ctx) => {
601
- if (!config.taskId) return undefined;
602
-
603
- try {
604
- const taskDetails = await fetchTaskDetails(config);
605
- if (taskDetails) {
606
- const reminder = [
607
- "=== GOAL REMINDER (injected before context compaction) ===",
608
- `Task ID: ${taskDetails.id}`,
609
- `Task: ${taskDetails.task}`,
610
- ];
611
- if (taskDetails.progress) {
612
- reminder.push(`Current Progress: ${taskDetails.progress}`);
613
- }
614
- reminder.push("=== Continue working on this task after compaction ===");
615
- console.log(reminder.join("\n"));
624
+ // === session_compact → PreCompact ===
625
+ // Re-anchor the model on its task once compaction has summarized the
626
+ // history. `context` fires before every LLM call, so it cannot carry this.
627
+ pi.on("session_compact", async () => {
628
+ if (!config.taskId) return;
629
+
630
+ const taskDetails = await fetchTaskDetails(config);
631
+ if (taskDetails) {
632
+ const reminder = [
633
+ "=== GOAL REMINDER (injected before context compaction) ===",
634
+ `Task ID: ${taskDetails.id}`,
635
+ `Task: ${taskDetails.task}`,
636
+ ];
637
+ if (taskDetails.progress) {
638
+ reminder.push(`Current Progress: ${taskDetails.progress}`);
616
639
  }
617
- } catch {
618
- /* don't block compaction */
640
+ reminder.push("=== Continue working on this task after compaction ===");
641
+ sendNudge(pi, reminder.join("\n"));
619
642
  }
643
+ });
644
+
645
+ // === context (before every LLM call) ===
646
+ pi.on("context", async (_event, ctx) => {
647
+ if (!config.taskId) return undefined;
620
648
 
621
649
  // Report context usage as a compaction event
622
650
  const usage = ctx.getContextUsage?.();
@@ -644,7 +672,10 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
644
672
  const { cancelled, reason } = await isTaskCancelled(config);
645
673
  if (cancelled) {
646
674
  const cancelReason = reason || "Task cancelled by lead or creator";
647
- console.log(
675
+ // "handled" skips the turn; the nudge lands in the session for the
676
+ // next one instead of paying for a turn on a cancelled task.
677
+ sendNudge(
678
+ pi,
648
679
  `🛑 TASK CANCELLED: ${cancelReason}. Stop working and use store-progress to acknowledge.`,
649
680
  );
650
681
  return { action: "handled" as const };