@desplega.ai/agent-swarm 2.5.3 → 2.6.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.
- package/README.md +2 -2
- package/dist/{acp-adapter-gr2fcyyj.js → acp-adapter-dgfakfpw.js} +9 -9
- package/dist/{actions-wv766ghf.js → actions-fbqma8aa.js} +21 -21
- package/dist/{amp-adapter-speb906t.js → amp-adapter-en0jy6hs.js} +4 -4
- package/dist/{app-ag29n14f.js → app-k8a5cgpp.js} +3 -3
- package/dist/{assistant-3x8bz3eg.js → assistant-a16433gr.js} +30 -26
- package/dist/{boot-reembed-jsmfrpzf.js → boot-reembed-6c1sjfnr.js} +1 -1
- package/dist/{boot-reembed-5b3d2eea.js → boot-reembed-f4nn561n.js} +1 -1
- package/dist/{boot-scrub-sweep-s73gerxy.js → boot-scrub-sweep-d4w9gajf.js} +4 -4
- package/dist/{boot-seeds-5zmax2wr.js → boot-seeds-9bmkspyw.js} +4 -4
- package/dist/{claude-adapter-hswdnm3t.js → claude-adapter-nys1kjxa.js} +9 -9
- package/dist/{claude-managed-adapter-cvbzj1h6.js → claude-managed-adapter-55207973.js} +3 -3
- package/dist/{claude-managed-setup-3k8m3hhc.js → claude-managed-setup-3cm35gey.js} +11 -4
- package/dist/{claude-sdk-session-fkbyk66x.js → claude-sdk-session-nw5yd8cp.js} +9313 -9260
- package/dist/{cli-210xd1f4.js → cli-03gg9pd1.js} +4 -4
- package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
- package/dist/cli-1tem6ns9.js +894 -0
- package/dist/{cli-25j01dpj.js → cli-24j1jeeb.js} +3 -3
- package/dist/{cli-ta8bd7th.js → cli-2sjhpsns.js} +2 -2
- package/dist/{cli-p859hcc8.js → cli-39aet655.js} +2 -2
- package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
- package/dist/{cli-e2ar4bp1.js → cli-3aqd4jaa.js} +121 -47
- package/dist/{cli-jjznk3f7.js → cli-3fwqy71j.js} +69 -35
- package/dist/{cli-p9rafdrb.js → cli-3gfvmzht.js} +7 -7
- package/dist/{cli-03vpq45j.js → cli-3ndmpxxv.js} +2 -2
- package/dist/{cli-aecdcr77.js → cli-4hxrf0p4.js} +228 -74
- package/dist/{cli-mz9afxxf.js → cli-4rf0f5vg.js} +15 -3
- package/dist/{cli-qzme5949.js → cli-5rj83bam.js} +284 -202
- package/dist/{cli-fsdprwrg.js → cli-5sckpqcx.js} +2 -2
- package/dist/{cli-cz5nmdxt.js → cli-683y0b3a.js} +2 -2
- package/dist/{cli-3ngdk5te.js → cli-6y51ryva.js} +2 -2
- package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
- package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
- package/dist/{cli-gx4rm0qx.js → cli-7enjwya9.js} +1 -1
- package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
- package/dist/{cli-avdvgcay.js → cli-9abdgn5t.js} +28 -9
- package/dist/{cli-99xgw26r.js → cli-a2rr0fmg.js} +1 -1
- package/dist/{cli-e5bxnjps.js → cli-a34pr3w1.js} +7 -7
- package/dist/{cli-502eqv93.js → cli-abkm71vr.js} +1 -1
- package/dist/{cli-mmhj8vd7.js → cli-ag2yrnt5.js} +11 -2
- package/dist/{cli-n45mr7zh.js → cli-aj8djyqt.js} +5 -5
- package/dist/{cli-1fc85mvt.js → cli-art7c1zh.js} +5 -5
- package/dist/{cli-40jsnrjm.js → cli-az25fn5f.js} +10 -8
- package/dist/{cli-9s05422w.js → cli-bqztfyve.js} +1 -1
- package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
- package/dist/{cli-svrj71pg.js → cli-c2habvsf.js} +12 -12
- package/dist/{cli-s49hpn37.js → cli-ca6banwz.js} +85 -19
- package/dist/{cli-hhxjnjzs.js → cli-cz37jqss.js} +12 -10
- package/dist/{cli-6n8axjv1.js → cli-d2dpf95n.js} +2 -2
- package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
- package/dist/{cli-z21whrrg.js → cli-emhjcj2y.js} +17 -17
- package/dist/{cli-e8jyq7d8.js → cli-esakngh8.js} +7 -3
- package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
- package/dist/{cli-35gtk1p6.js → cli-ffvsj2qw.js} +44 -13
- package/dist/{cli-ce0ak9ms.js → cli-g20jjwt4.js} +26 -26
- package/dist/{cli-kxawjrxg.js → cli-g8ka5j4j.js} +772 -194
- package/dist/{cli-vwn81jbs.js → cli-gf95q3bm.js} +5 -5
- package/dist/{cli-s53vp709.js → cli-gnsb1zxa.js} +2 -2
- package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
- package/dist/{cli-gpv60278.js → cli-hfeb68z4.js} +7 -7
- package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
- package/dist/{cli-ewm3pwbq.js → cli-k21gx5ht.js} +46 -23
- package/dist/{cli-egjggehm.js → cli-kf4jree9.js} +2 -2
- package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
- package/dist/{cli-p0rzjkya.js → cli-m5a9m0ak.js} +32 -13
- package/dist/{cli-fzcn3d87.js → cli-m70bajzr.js} +3 -3
- package/dist/{cli-w1zwctj2.js → cli-mtnj4fwa.js} +11 -5
- package/dist/{cli-gpsz7g88.js → cli-mvjz5an3.js} +16 -12
- package/dist/{cli-hcpj2xqz.js → cli-n7asmce6.js} +225 -3
- package/dist/{cli-y561h5a9.js → cli-nn6vx2mq.js} +5 -5
- package/dist/cli-nq677csy.js +140 -0
- package/dist/{cli-a5ebc95n.js → cli-pfk92eqz.js} +18 -15
- package/dist/{cli-p6zs9h4y.js → cli-rr7zx19j.js} +767 -249
- package/dist/{cli-qgf8emrn.js → cli-sqma6vw1.js} +10 -7
- package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
- package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
- package/dist/{cli-qqzmjxkf.js → cli-v49fkghp.js} +11 -6
- package/dist/{cli-x77gen2p.js → cli-w2656ka1.js} +7 -7
- package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
- package/dist/{cli-8s5wx3nz.js → cli-zqne1qcj.js} +44 -17
- package/dist/cli.js +22 -22
- package/dist/{codex-adapter-n8ka66vf.js → codex-adapter-d95q4qsn.js} +7 -7
- package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
- package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
- package/dist/{codex-session-runner-fsebts37.js → codex-session-runner-gdwd8h0c.js} +8 -8
- package/dist/{commands-t04rt2pc.js → commands-jqmgk54b.js} +2 -2
- package/dist/{controller-rgna04r2.js → controller-z3jq589f.js} +30 -4
- package/dist/{controllers-connect-bqm1rtft.js → controllers-connect-r2za39ef.js} +23 -23
- package/dist/{cursor-adapter-swr2k95y.js → cursor-adapter-4mr4jgkw.js} +2 -3
- package/dist/{db-a4m7haw0.js → db-ppv2tze7.js} +3 -3
- package/dist/{db-24ck09xy.js → db-w36wwb5g.js} +4 -2
- package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
- package/dist/{dispatcher-nwf9ss0q.js → dispatcher-pv5h77vf.js} +20 -20
- package/dist/{docker-nfy2gc8t.js → docker-fdzyewpz.js} +4 -4
- package/dist/{dsh-adapter-kpdnhjst.js → dsh-adapter-d4pdpb3r.js} +2 -3
- package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
- package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
- package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-4sp4dvmc.js} +1 -1
- package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
- package/dist/{execute-0xpz8950.js → execute-jev839y1.js} +1 -1
- package/dist/{external-channel-j1e2p34p.js → external-channel-y6nkzhnn.js} +2 -2
- package/dist/{grok-adapter-qtjz0whk.js → grok-adapter-t08b9ntm.js} +10 -10
- package/dist/handlers-eqsnmv95.js +41 -0
- package/dist/{heartbeat-vtesda5d.js → heartbeat-vn31xwdk.js} +23 -23
- package/dist/{hook-b2hhj6rd.js → hook-6d0zbvvt.js} +6 -5
- package/dist/{hook-8wqaszda.js → hook-cpjs8q51.js} +13 -7
- package/dist/{http-mxcqhhcs.js → http-9c6medda.js} +1085 -136
- package/dist/{index-spwxjgay.js → index-38t35h0n.js} +20 -20
- package/dist/{index-1r8wfpzs.js → index-g6k9tctj.js} +1 -1
- package/dist/{index-srg522m6.js → index-k0yt1tvq.js} +20 -20
- package/dist/{index-31ceyf42.js → index-pkma1yf8.js} +1 -1
- package/dist/{index-n1vbvta9.js → index-qhjf2htm.js} +29 -24
- package/dist/index-yqgbfgaj.js +43 -0
- package/dist/{keepalive-dsd8rtgx.js → keepalive-em0117bp.js} +4 -4
- package/dist/lead-23e7z59t.js +79 -0
- package/dist/{maintenance-jqmqwnet.js → maintenance-kbme3qy9.js} +1 -1
- package/dist/{oauth-refresh-sweep-q2c3bnvm.js → oauth-refresh-sweep-xysj4yse.js} +3 -3
- package/dist/{onboard-w4yd7s8t.js → onboard-p6q0vz97.js} +3 -3
- package/dist/{opencode-adapter-9tppvxar.js → opencode-adapter-esv3k9yn.js} +7 -7
- package/dist/{otel-impl-f36byzbd.js → otel-impl-54qhqh1z.js} +2 -2
- package/dist/{pi-mono-adapter-tqsge1ay.js → pi-mono-adapter-mymbwhj0.js} +41 -14
- package/dist/{pricing-refresh-xvak5p4v.js → pricing-refresh-yb75wv33.js} +3 -3
- package/dist/{rbac-roles-gfn30fb8.js → rbac-roles-1vp2mw4p.js} +2 -2
- package/dist/{rbac-roles-682k2mt5.js → rbac-roles-dexhc9x8.js} +1 -1
- package/dist/render-v2-tjay5hhf.js +16 -0
- package/dist/{resume-2dvq348g.js → resume-8ayf37g5.js} +11 -9
- package/dist/{runtime-bt5pkpm1.js → runtime-hqe5cqcd.js} +1 -1
- package/dist/{session-access-7f21ekq9.js → session-access-fdhp987s.js} +4 -4
- package/dist/session-source-backfill-qgtc86z1.js +110 -0
- package/dist/{setup-epffxtbk.js → setup-4rrmqcq3.js} +1 -1
- package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-52km00d6.js} +2 -2
- package/dist/{work-objects-xz1a5rqk.js → work-objects-p28fgd70.js} +23 -23
- package/dist/worker-e3w7vye1.js +79 -0
- package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
- package/openapi.json +1323 -16
- package/package.json +5 -5
- package/plugin/commands/create-pr.md +2 -2
- package/plugin/pi-skills/create-pr/SKILL.md +2 -2
- package/src/assets/source-key.ts +95 -0
- package/src/be/db/api-keys.ts +80 -0
- package/src/be/db/approvals.ts +48 -0
- package/src/be/db/session-labels.ts +28 -0
- package/src/be/db/session-moves.ts +5 -6
- package/src/be/db/session-tree.ts +153 -14
- package/src/be/db/tasks/read.ts +11 -0
- package/src/be/db/tasks/write.ts +21 -6
- package/src/be/db/workflow-idempotency.ts +183 -0
- package/src/be/db/workflow-runs.ts +500 -7
- package/src/be/db-retention.ts +61 -6
- package/src/be/db.ts +126 -131
- package/src/be/memory/providers/openai-embedding.ts +5 -1
- package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
- package/src/be/migrations/244_api_key_validation.sql +4 -0
- package/src/be/onboarding.ts +4 -0
- package/src/be/scripts/typecheck.ts +1 -1
- package/src/be/sealed-json.ts +1 -0
- package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
- package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
- package/src/be/seed-skills/bundled-files.generated.json +1 -1
- package/src/be/session-source-backfill.ts +163 -0
- package/src/be/sql-connections.ts +7 -1
- package/src/be/swarm-config-guard.ts +3 -0
- package/src/be/users.ts +9 -0
- package/src/comb/review-batch.ts +28 -1
- package/src/comb/templates.ts +7 -2
- package/src/commands/claude-managed-setup.ts +7 -1
- package/src/commands/controller.ts +33 -0
- package/src/commands/credential-refresh.ts +76 -1
- package/src/commands/credential-wait.ts +2 -2
- package/src/commands/provider-credentials.ts +210 -170
- package/src/commands/runner.ts +31 -6
- package/src/extensions/apply-task-create.ts +64 -11
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +30 -0
- package/src/extensions/dispatcher.ts +4 -0
- package/src/extensions/post-bridge.ts +8 -0
- package/src/hooks/context-bound-note.ts +196 -0
- package/src/hooks/hook.ts +12 -0
- package/src/http/all-routes.ts +2 -0
- package/src/http/api-keys.ts +53 -0
- package/src/http/assets.ts +15 -0
- package/src/http/codex-oauth-accounts.ts +191 -0
- package/src/http/comb.ts +15 -2
- package/src/http/config.ts +49 -17
- package/src/http/core.ts +6 -0
- package/src/http/fleet-benchmark.ts +5 -2
- package/src/http/idempotency-key.ts +117 -0
- package/src/http/index.ts +11 -0
- package/src/http/integrations.ts +5 -1
- package/src/http/kv.ts +8 -3
- package/src/http/onboarding.ts +6 -1
- package/src/http/pool-keys.ts +354 -0
- package/src/http/sessions.ts +37 -12
- package/src/http/stats.ts +1 -0
- package/src/http/status.ts +26 -12
- package/src/http/tasks.ts +3 -0
- package/src/http/used-by.ts +7 -2
- package/src/http/workflows.ts +207 -33
- package/src/integrations/chat-sdk/inbound.ts +27 -0
- package/src/kv-reserved-namespaces.ts +19 -0
- package/src/prompts/context-bound-note.ts +48 -0
- package/src/prompts/session-templates.ts +25 -1
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +30 -8
- package/src/providers/claude-adapter.ts +34 -5
- package/src/providers/claude-managed-adapter.ts +42 -9
- package/src/providers/claude-sdk-session.ts +10 -0
- package/src/providers/claude-session-events.ts +13 -0
- package/src/providers/codex-adapter.ts +83 -11
- package/src/providers/opencode-adapter.ts +18 -0
- package/src/providers/pi-mono-adapter.ts +35 -0
- package/src/runtime-providers/docker.ts +6 -2
- package/src/scheduler/scheduler.ts +3 -2
- package/src/scripts-runtime/swarm-sdk.ts +7 -1
- package/src/scripts-runtime/types/stdlib.d.ts +1 -1
- package/src/scripts-runtime/types/swarm-extension.d.ts +52 -8
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -1
- package/src/slack/assistant.ts +7 -0
- package/src/slack/channel-labels.ts +74 -0
- package/src/slack/handlers.ts +8 -0
- package/src/slack/inbound-files.ts +5 -2
- package/src/slack/thread-buffer.ts +4 -0
- package/src/tasks/sibling-awareness.ts +12 -3
- package/src/telemetry-context.ts +5 -6
- package/src/telemetry-identity.ts +3 -3
- package/src/tests/amp-adapter.test.ts +97 -0
- package/src/tests/api-keys-validation.test.ts +170 -0
- package/src/tests/approval-requests.test.ts +10 -7
- package/src/tests/approval-sweeps.test.ts +12 -6
- package/src/tests/base-prompt.test.ts +1 -1
- package/src/tests/claude-hook-output.test.ts +79 -1
- package/src/tests/claude-managed-adapter.test.ts +321 -0
- package/src/tests/claude-routes-e2e.test.ts +7 -2
- package/src/tests/codex-adapter.test.ts +163 -0
- package/src/tests/codex-credential-pools.test.ts +120 -0
- package/src/tests/codex-gateway-route.test.ts +165 -0
- package/src/tests/codex-oauth-accounts.test.ts +357 -0
- package/src/tests/comb-review-batch.test.ts +63 -2
- package/src/tests/config-pool-keys.test.ts +533 -0
- package/src/tests/context-bound-note.test.ts +269 -0
- package/src/tests/credential-check.test.ts +290 -2
- package/src/tests/credential-refresh.test.ts +172 -1
- package/src/tests/db-retention.test.ts +9 -3
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +1 -3
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/extensions-pre-task.test.ts +325 -0
- package/src/tests/fetch-resolved-env.test.ts +17 -0
- package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
- package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
- package/src/tests/fixtures/extensions/record-task-create/hooks.ts +9 -0
- package/src/tests/fixtures/extensions/record-task-create/manifest.json +7 -0
- package/src/tests/fixtures/fake-amp.ts +22 -8
- package/src/tests/fixtures/held-template-render.ts +42 -0
- package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
- package/src/tests/fleet-benchmark.test.ts +8 -0
- package/src/tests/get-workflow-run-tool.test.ts +98 -0
- package/src/tests/integrations-http.test.ts +32 -2
- package/src/tests/internal-ai/complete-structured.test.ts +37 -1
- package/src/tests/internal-ai/credentials.test.ts +23 -0
- package/src/tests/kv-http.test.ts +32 -0
- package/src/tests/kv-tool.test.ts +25 -0
- package/src/tests/multi-runtime-registration.test.ts +12 -12
- package/src/tests/onboarding.test.ts +108 -1
- package/src/tests/openai-embedding-credentials.test.ts +35 -0
- package/src/tests/openapi-route-headers.test.ts +2 -0
- package/src/tests/opencode-adapter.test.ts +134 -0
- package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
- package/src/tests/org-website.test.ts +36 -0
- package/src/tests/pi-mono-adapter.test.ts +105 -0
- package/src/tests/prompt-template-session.test.ts +4 -3
- package/src/tests/provider-key-probes.test.ts +181 -0
- package/src/tests/rbac-e2e-helpers.test.ts +88 -1
- package/src/tests/rbac-e2e-helpers.ts +36 -11
- package/src/tests/runner-push-delivery.test.ts +25 -0
- package/src/tests/runtime-controller-liveness.test.ts +133 -0
- package/src/tests/runtime-providers-docker.test.ts +2 -0
- package/src/tests/script-connections.test.ts +5 -0
- package/src/tests/script-run-config-scope.test.ts +91 -3
- package/src/tests/script-runs-retention.test.ts +212 -0
- package/src/tests/sdk-allowlist.test.ts +39 -0
- package/src/tests/session-logs.test.ts +70 -0
- package/src/tests/session-source-backfill.test.ts +429 -0
- package/src/tests/session-source-keys.test.ts +236 -0
- package/src/tests/session-source-labels.test.ts +419 -0
- package/src/tests/sessions-detail-batch.test.ts +2 -0
- package/src/tests/sessions-move.test.ts +131 -0
- package/src/tests/sessions-tree.test.ts +336 -2
- package/src/tests/slack-inbound-files.test.ts +103 -23
- package/src/tests/status.test.ts +101 -18
- package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
- package/src/tests/test-net.ts +31 -0
- package/src/tests/transcription.test.ts +8 -0
- package/src/tests/used-by-route.test.ts +11 -0
- package/src/tests/workflow-cascade-cancel.test.ts +1341 -0
- package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
- package/src/tests/workflow-http-v2.test.ts +131 -0
- package/src/tests/workflow-idempotency.test.ts +915 -0
- package/src/tests/workflow-step-order.test.ts +92 -0
- package/src/tests/workflow-sub-workflow.test.ts +30 -0
- package/src/tools/get-tasks.ts +8 -0
- package/src/tools/kv/kv-read-auth.ts +4 -2
- package/src/tools/task-action.ts +1 -1
- package/src/tools/workflows/cancel-workflow-run.ts +1 -1
- package/src/tools/workflows/get-workflow-run.ts +14 -4
- package/src/types.ts +24 -0
- package/src/utils/company-profile.ts +51 -1
- package/src/utils/context-window.ts +8 -6
- package/src/utils/credentials.ts +5 -2
- package/src/utils/internal-ai/complete-structured.ts +5 -0
- package/src/utils/internal-ai/credentials.ts +4 -3
- package/src/utils/provider-key-probes.ts +328 -0
- package/src/utils/transcription.ts +4 -1
- package/src/workflows/approval-cancel.ts +15 -13
- package/src/workflows/approval-notifications.ts +5 -1
- package/src/workflows/checkpoint.ts +6 -3
- package/src/workflows/engine.ts +30 -8
- package/src/workflows/executors/foreach.ts +2 -2
- package/src/workflows/executors/sub-workflow.ts +8 -1
- package/src/workflows/foreach-driver.ts +10 -3
- package/src/workflows/foreach-join.ts +29 -6
- package/src/workflows/recovery.ts +6 -1
- package/src/workflows/resume.ts +168 -56
- package/src/workflows/retry-poller.ts +49 -3
- package/src/workflows/task-step-routing.ts +29 -7
- package/templates/ai-toolbox.manifest.json +8 -8
- package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
- package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
- package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
- package/templates/skills/agent-browser/SKILL.md +4 -4
- package/templates/skills/agent-browser/content.md +4 -4
- package/templates/skills/code-quality/config.json +1 -1
- package/templates/skills/code-quality/content.md +2 -2
- package/templates/skills/delegate-work/content.md +29 -25
- package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
- package/templates/skills/qa/content.md +2 -2
- package/templates/skills/researching/content.md +4 -0
- package/templates/skills/swarm-scripts/SKILL.md +1 -0
- package/templates/skills/swarm-scripts/content.md +1 -0
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/dist/cli-7421jzsw.js +0 -84
- package/dist/cli-e45e4m6b.js +0 -413
- package/dist/cli-zw88zjba.js +0 -307
- package/dist/handlers-19xr6dnz.js +0 -41
- package/dist/index-kw2mmat7.js +0 -43
- package/dist/lead-zyerk1x2.js +0 -79
- package/dist/render-v2-hgytz8nn.js +0 -12
- package/dist/worker-42t8emjt.js +0 -79
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { mkdirSync, rmSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { renderContextBoundNote } from "../prompts/context-bound-note";
|
|
4
5
|
import { createProviderAdapter } from "../providers";
|
|
5
6
|
import {
|
|
6
7
|
ClaudeManagedAdapter,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
getClaudeManagedModelPricing,
|
|
14
15
|
} from "../providers/claude-managed-models";
|
|
15
16
|
import type { ProviderEvent, ProviderSessionConfig } from "../providers/types";
|
|
17
|
+
import { HELD_NOTE_TEXT, holdTemplateRender } from "./fixtures/held-template-render";
|
|
16
18
|
|
|
17
19
|
// Stash + restore env vars so this file plays nicely with the rest of the
|
|
18
20
|
// suite (other tests don't expect MANAGED_AGENT_ID / MANAGED_ENVIRONMENT_ID
|
|
@@ -57,6 +59,52 @@ describe("ClaudeManagedAdapter (Phase 1 skeleton)", () => {
|
|
|
57
59
|
expect(adapter.formatCommand("plan")).toBe("/plan");
|
|
58
60
|
});
|
|
59
61
|
|
|
62
|
+
for (const source of ["container", "task", "task-pool"] as const) {
|
|
63
|
+
test(`uses one Anthropic key from ${source}`, async () => {
|
|
64
|
+
const originalFetch = globalThis.fetch;
|
|
65
|
+
const originalKey = process.env.ANTHROPIC_API_KEY;
|
|
66
|
+
let apiKey: string | null = null;
|
|
67
|
+
process.env.ANTHROPIC_API_KEY = "example-container-first,example-container-second";
|
|
68
|
+
globalThis.fetch = (async (input, init) => {
|
|
69
|
+
apiKey =
|
|
70
|
+
new Request(input, init).headers.get("authorization")?.slice("Bearer ".length) ?? null;
|
|
71
|
+
return Response.json(
|
|
72
|
+
{ error: { type: "authentication_error", message: "test rejection" } },
|
|
73
|
+
{ status: 401 },
|
|
74
|
+
);
|
|
75
|
+
}) as typeof fetch;
|
|
76
|
+
try {
|
|
77
|
+
const adapter = new ClaudeManagedAdapter();
|
|
78
|
+
await expect(
|
|
79
|
+
adapter.createSession(
|
|
80
|
+
tConfig({
|
|
81
|
+
env:
|
|
82
|
+
source === "container"
|
|
83
|
+
? {}
|
|
84
|
+
: {
|
|
85
|
+
ANTHROPIC_API_KEY:
|
|
86
|
+
source === "task"
|
|
87
|
+
? "example-task"
|
|
88
|
+
: "example-task-first,example-task-second",
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
).rejects.toThrow("test rejection");
|
|
93
|
+
expect(
|
|
94
|
+
source === "container"
|
|
95
|
+
? ["example-container-first", "example-container-second"]
|
|
96
|
+
: source === "task"
|
|
97
|
+
? ["example-task"]
|
|
98
|
+
: ["example-task-first", "example-task-second"],
|
|
99
|
+
).toContain(apiKey);
|
|
100
|
+
} finally {
|
|
101
|
+
globalThis.fetch = originalFetch;
|
|
102
|
+
if (originalKey === undefined) delete process.env.ANTHROPIC_API_KEY;
|
|
103
|
+
else process.env.ANTHROPIC_API_KEY = originalKey;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
60
108
|
test("ctor throws when MANAGED_AGENT_ID is missing", () => {
|
|
61
109
|
const saved = process.env.MANAGED_AGENT_ID;
|
|
62
110
|
delete process.env.MANAGED_AGENT_ID;
|
|
@@ -1503,3 +1551,276 @@ describe("ClaudeManagedAdapter (Phase 6) — full happy-path integration", () =>
|
|
|
1503
1551
|
expect(result.output).toBe("Read the file and here's what I found.");
|
|
1504
1552
|
});
|
|
1505
1553
|
});
|
|
1554
|
+
|
|
1555
|
+
describe("ClaudeManagedAdapter — one-time context-bound note", () => {
|
|
1556
|
+
const tmpLogDir = `/tmp/claude-managed-adapter-bound-note-${Date.now()}`;
|
|
1557
|
+
let seq = 0;
|
|
1558
|
+
|
|
1559
|
+
beforeAll(() => {
|
|
1560
|
+
mkdirSync(tmpLogDir, { recursive: true });
|
|
1561
|
+
process.env.ANTHROPIC_API_KEY = "sk-test";
|
|
1562
|
+
process.env.MANAGED_AGENT_ID = "agent_x";
|
|
1563
|
+
process.env.MANAGED_ENVIRONMENT_ID = "env_x";
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
afterAll(() => {
|
|
1567
|
+
rmSync(tmpLogDir, { recursive: true, force: true });
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
/** A main-thread model request whose prompt is `input + cacheRead + cacheWrite`. */
|
|
1571
|
+
function requestEnd(input: unknown, cacheRead: unknown = 0, cacheWrite: unknown = 0) {
|
|
1572
|
+
seq += 1;
|
|
1573
|
+
return {
|
|
1574
|
+
type: "span.model_request_end",
|
|
1575
|
+
id: `req-${seq}`,
|
|
1576
|
+
processed_at: "2026-01-01T00:00:00Z",
|
|
1577
|
+
is_error: false,
|
|
1578
|
+
model_request_start_id: `start-${seq}`,
|
|
1579
|
+
model_usage: {
|
|
1580
|
+
input_tokens: input,
|
|
1581
|
+
output_tokens: 10,
|
|
1582
|
+
cache_read_input_tokens: cacheRead,
|
|
1583
|
+
cache_creation_input_tokens: cacheWrite,
|
|
1584
|
+
},
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function toolUse(kind: "agent.tool_use" | "agent.mcp_tool_use" = "agent.tool_use") {
|
|
1589
|
+
seq += 1;
|
|
1590
|
+
return {
|
|
1591
|
+
type: kind,
|
|
1592
|
+
id: `tu-${seq}`,
|
|
1593
|
+
processed_at: "2026-01-01T00:00:00Z",
|
|
1594
|
+
name: "read_file",
|
|
1595
|
+
...(kind === "agent.mcp_tool_use" ? { mcp_server_name: "agent-swarm" } : {}),
|
|
1596
|
+
input: { path: `/tmp/${seq}` },
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
const idle = () => ({
|
|
1601
|
+
type: "session.status_idle",
|
|
1602
|
+
id: `idle-${++seq}`,
|
|
1603
|
+
processed_at: "2026-01-01T00:00:00Z",
|
|
1604
|
+
stop_reason: { type: "end_turn" },
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
/** Run a session over `events` and return the texts of every message sent after the prompt. */
|
|
1608
|
+
async function runNotes(
|
|
1609
|
+
events: Array<Record<string, unknown>>,
|
|
1610
|
+
threshold: string,
|
|
1611
|
+
onSend?: Parameters<typeof makeFakeClient>[0]["onSend"],
|
|
1612
|
+
model?: string,
|
|
1613
|
+
) {
|
|
1614
|
+
const spy = makeFakeClient({
|
|
1615
|
+
streamEvents: async function* () {
|
|
1616
|
+
for (const e of events) yield e;
|
|
1617
|
+
},
|
|
1618
|
+
onSend,
|
|
1619
|
+
});
|
|
1620
|
+
const adapter = new ClaudeManagedAdapter({ client: spy.client });
|
|
1621
|
+
const session = await adapter.createSession(
|
|
1622
|
+
tConfig({
|
|
1623
|
+
logFile: join(tmpLogDir, `note-${++seq}.log`),
|
|
1624
|
+
env: { CONTEXT_BOUND_NOTE_TOKENS: threshold },
|
|
1625
|
+
...(model ? { model } : {}),
|
|
1626
|
+
}),
|
|
1627
|
+
);
|
|
1628
|
+
const result = await session.waitForCompletion();
|
|
1629
|
+
// The note is rendered and sent off the event loop; let it land.
|
|
1630
|
+
await Bun.sleep(150);
|
|
1631
|
+
const notes = spy.sent.slice(1).map((sent) => {
|
|
1632
|
+
const content = sent.events[0]?.content as Array<{ text?: string }> | undefined;
|
|
1633
|
+
return { type: sent.events[0]?.type, text: content?.[0]?.text };
|
|
1634
|
+
});
|
|
1635
|
+
return { result, notes };
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
test("fires once when a tool call follows a request at the threshold, not again", async () => {
|
|
1639
|
+
const { result, notes } = await runNotes(
|
|
1640
|
+
[
|
|
1641
|
+
requestEnd(150_000, 40_000, 10_000),
|
|
1642
|
+
toolUse(),
|
|
1643
|
+
requestEnd(240_000, 10_000, 0),
|
|
1644
|
+
toolUse("agent.mcp_tool_use"),
|
|
1645
|
+
requestEnd(260_000),
|
|
1646
|
+
toolUse(),
|
|
1647
|
+
idle(),
|
|
1648
|
+
],
|
|
1649
|
+
"200000",
|
|
1650
|
+
);
|
|
1651
|
+
expect(result.isError).toBe(false);
|
|
1652
|
+
expect(notes).toEqual([
|
|
1653
|
+
{ type: "user.message", text: await renderContextBoundNote(200_000, "claude-sonnet-4-6") },
|
|
1654
|
+
]);
|
|
1655
|
+
});
|
|
1656
|
+
|
|
1657
|
+
test("the note shows the window share for a known model and omits it for an unknown one", async () => {
|
|
1658
|
+
const noteFor = async (model: string) =>
|
|
1659
|
+
(await runNotes([requestEnd(250_000), toolUse(), idle()], "200000", undefined, model)).notes;
|
|
1660
|
+
const rest =
|
|
1661
|
+
"Keep this task bounded: finish the current step, store-progress, and complete or defer instead of starting new exploration.";
|
|
1662
|
+
expect(await noteFor("claude-sonnet-4-6")).toEqual([
|
|
1663
|
+
{
|
|
1664
|
+
type: "user.message",
|
|
1665
|
+
text: `Context is 250,000 tokens (25% of the 1,000,000-token window). ${rest}`,
|
|
1666
|
+
},
|
|
1667
|
+
]);
|
|
1668
|
+
expect(await noteFor("made-up-model")).toEqual([
|
|
1669
|
+
{ type: "user.message", text: `Context is 250,000 tokens. ${rest}` },
|
|
1670
|
+
]);
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
test("no-op below the threshold, then fires once it is crossed", async () => {
|
|
1674
|
+
const { notes } = await runNotes(
|
|
1675
|
+
[requestEnd(199_999), toolUse(), requestEnd(205_000), toolUse(), idle()],
|
|
1676
|
+
"200000",
|
|
1677
|
+
);
|
|
1678
|
+
expect(notes).toEqual([
|
|
1679
|
+
{ type: "user.message", text: await renderContextBoundNote(205_000, "claude-sonnet-4-6") },
|
|
1680
|
+
]);
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
test("off at 0 and when unset", async () => {
|
|
1684
|
+
const events = () => [requestEnd(500_000), toolUse(), idle()];
|
|
1685
|
+
expect((await runNotes(events(), "0")).notes).toEqual([]);
|
|
1686
|
+
expect((await runNotes(events(), "")).notes).toEqual([]);
|
|
1687
|
+
// Control: the same session with a threshold gets the note.
|
|
1688
|
+
expect((await runNotes(events(), "200000")).notes).toHaveLength(1);
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
test("no note when the request ends the turn without a tool call", async () => {
|
|
1692
|
+
expect((await runNotes([requestEnd(300_000), idle()], "200000")).notes).toEqual([]);
|
|
1693
|
+
// Control: a tool call after the same request carries the note.
|
|
1694
|
+
expect((await runNotes([requestEnd(300_000), toolUse(), idle()], "200000")).notes).toHaveLength(
|
|
1695
|
+
1,
|
|
1696
|
+
);
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
test("fails open on missing or corrupt usage", async () => {
|
|
1700
|
+
const { result, notes } = await runNotes(
|
|
1701
|
+
[
|
|
1702
|
+
// Missing: a tool call before any request reported usage.
|
|
1703
|
+
toolUse(),
|
|
1704
|
+
// Corrupt: non-numeric usage fields.
|
|
1705
|
+
requestEnd("lots", null, {}),
|
|
1706
|
+
toolUse(),
|
|
1707
|
+
requestEnd(Number.NaN),
|
|
1708
|
+
toolUse(),
|
|
1709
|
+
// Control: a valid request afterwards still gets the note.
|
|
1710
|
+
requestEnd(210_000),
|
|
1711
|
+
toolUse(),
|
|
1712
|
+
idle(),
|
|
1713
|
+
],
|
|
1714
|
+
"200000",
|
|
1715
|
+
);
|
|
1716
|
+
expect(result.isError).toBe(false);
|
|
1717
|
+
expect(notes).toEqual([
|
|
1718
|
+
{ type: "user.message", text: await renderContextBoundNote(210_000, "claude-sonnet-4-6") },
|
|
1719
|
+
]);
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
test("fails open when sending the note rejects", async () => {
|
|
1723
|
+
let sends = 0;
|
|
1724
|
+
const { result, notes } = await runNotes(
|
|
1725
|
+
[requestEnd(300_000), toolUse(), requestEnd(310_000), toolUse(), idle()],
|
|
1726
|
+
"200000",
|
|
1727
|
+
() => {
|
|
1728
|
+
sends += 1;
|
|
1729
|
+
if (sends > 1) throw new Error("send failed");
|
|
1730
|
+
},
|
|
1731
|
+
);
|
|
1732
|
+
expect(result.isError).toBe(false);
|
|
1733
|
+
// One attempt only: a failed send does not re-arm the note.
|
|
1734
|
+
expect(notes).toHaveLength(1);
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
describe("note rendered late", () => {
|
|
1738
|
+
const toolResult = (toolUseId: string) => ({
|
|
1739
|
+
type: "agent.tool_result",
|
|
1740
|
+
id: `tr-${++seq}`,
|
|
1741
|
+
processed_at: "2026-01-01T00:00:00Z",
|
|
1742
|
+
tool_use_id: toolUseId,
|
|
1743
|
+
content: [],
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
/**
|
|
1747
|
+
* Holds the render until the stream reaches `releaseAt`. Returns the
|
|
1748
|
+
* `user.message` events sent after the prompt, read once the late render
|
|
1749
|
+
* had time to land.
|
|
1750
|
+
*/
|
|
1751
|
+
async function lateNotes(
|
|
1752
|
+
releaseAt: "tool-open" | "tool-returned" | "idle" | "abort",
|
|
1753
|
+
): Promise<{ notes: unknown[]; isError: boolean }> {
|
|
1754
|
+
const held = holdTemplateRender();
|
|
1755
|
+
const tool = toolUse();
|
|
1756
|
+
let unblock!: () => void;
|
|
1757
|
+
const blocked = new Promise<void>((resolve) => {
|
|
1758
|
+
unblock = resolve;
|
|
1759
|
+
});
|
|
1760
|
+
const spy = makeFakeClient({
|
|
1761
|
+
streamEvents: async function* () {
|
|
1762
|
+
yield requestEnd(300_000);
|
|
1763
|
+
yield tool;
|
|
1764
|
+
await held.requested;
|
|
1765
|
+
if (releaseAt === "tool-open") {
|
|
1766
|
+
held.release();
|
|
1767
|
+
await Bun.sleep(50);
|
|
1768
|
+
}
|
|
1769
|
+
yield toolResult(tool.id);
|
|
1770
|
+
if (releaseAt === "tool-returned") {
|
|
1771
|
+
held.release();
|
|
1772
|
+
await Bun.sleep(50);
|
|
1773
|
+
}
|
|
1774
|
+
if (releaseAt === "abort") await blocked;
|
|
1775
|
+
yield idle();
|
|
1776
|
+
},
|
|
1777
|
+
});
|
|
1778
|
+
try {
|
|
1779
|
+
const session = await new ClaudeManagedAdapter({ client: spy.client }).createSession(
|
|
1780
|
+
tConfig({
|
|
1781
|
+
logFile: join(tmpLogDir, `late-${++seq}.log`),
|
|
1782
|
+
env: { CONTEXT_BOUND_NOTE_TOKENS: "200000" },
|
|
1783
|
+
}),
|
|
1784
|
+
);
|
|
1785
|
+
if (releaseAt === "abort") {
|
|
1786
|
+
await held.requested;
|
|
1787
|
+
await session.abort();
|
|
1788
|
+
unblock();
|
|
1789
|
+
}
|
|
1790
|
+
const result = await session.waitForCompletion();
|
|
1791
|
+
held.release();
|
|
1792
|
+
await Bun.sleep(100);
|
|
1793
|
+
const notes = spy.sent
|
|
1794
|
+
.slice(1)
|
|
1795
|
+
.filter((sent) => sent.events[0]?.type === "user.message")
|
|
1796
|
+
.map((sent) => sent.events[0]);
|
|
1797
|
+
return { notes, isError: result.isError };
|
|
1798
|
+
} finally {
|
|
1799
|
+
unblock();
|
|
1800
|
+
held.stop();
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
test("control: a note ready while its tool call is open is sent", async () => {
|
|
1805
|
+
const { notes, isError } = await lateNotes("tool-open");
|
|
1806
|
+
expect(isError).toBe(false);
|
|
1807
|
+
expect(notes).toEqual([
|
|
1808
|
+
{ type: "user.message", content: [{ type: "text", text: HELD_NOTE_TEXT }] },
|
|
1809
|
+
]);
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
test("dropped once the tool result is back and the final reply is pending", async () => {
|
|
1813
|
+
expect((await lateNotes("tool-returned")).notes).toEqual([]);
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
test("dropped after the session went idle", async () => {
|
|
1817
|
+
const { notes, isError } = await lateNotes("idle");
|
|
1818
|
+
expect(isError).toBe(false);
|
|
1819
|
+
expect(notes).toEqual([]);
|
|
1820
|
+
});
|
|
1821
|
+
|
|
1822
|
+
test("dropped after abort", async () => {
|
|
1823
|
+
expect((await lateNotes("abort")).notes).toEqual([]);
|
|
1824
|
+
});
|
|
1825
|
+
});
|
|
1826
|
+
});
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
* auth redacted.
|
|
11
11
|
*/
|
|
12
12
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
13
|
-
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
13
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
14
14
|
import { createRequire } from "node:module";
|
|
15
15
|
import { tmpdir } from "node:os";
|
|
16
|
-
import { dirname, join } from "node:path";
|
|
16
|
+
import { basename, dirname, join } from "node:path";
|
|
17
17
|
import { deriveDefaultRoute } from "@desplega/model-routing";
|
|
18
18
|
import { closeDb, createAgent, getAgentById, initDb, updateAgentCredentialState } from "../be/db";
|
|
19
19
|
import { awaitCredentials } from "../commands/credential-wait";
|
|
@@ -240,6 +240,11 @@ describe.skipIf(!CLAUDE_BINARY)("claude routes e2e (real CLI, fake endpoints)",
|
|
|
240
240
|
|
|
241
241
|
expect(result).toMatchObject({ isError: false, exitCode: 0 });
|
|
242
242
|
expect(result.output).toContain(REPLY);
|
|
243
|
+
// Trust lands in the session's HOME, never in the host's ~/.claude.json.
|
|
244
|
+
const claudeJson = JSON.parse(readFileSync(join(env.HOME ?? "", ".claude.json"), "utf8"));
|
|
245
|
+
expect(
|
|
246
|
+
Object.keys(claudeJson.projects ?? {}).some((dir) => dir.endsWith(basename(cwd))),
|
|
247
|
+
).toBe(true);
|
|
243
248
|
|
|
244
249
|
const turn = fake.requests.find(
|
|
245
250
|
(req) => req.method === "POST" && route.turnPath.test(req.path),
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "node:fs";
|
|
19
19
|
import { homedir, tmpdir } from "node:os";
|
|
20
20
|
import { join } from "node:path";
|
|
21
|
+
import { renderContextBoundNote } from "../prompts/context-bound-note";
|
|
21
22
|
import type {
|
|
22
23
|
AgentMessageItem,
|
|
23
24
|
CommandExecutionItem,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
resolveCodexModel,
|
|
34
35
|
} from "../providers/codex-models";
|
|
35
36
|
import type { ProviderEvent, ProviderResult, ProviderSessionConfig } from "../providers/types";
|
|
37
|
+
import { HELD_NOTE_TEXT, holdTemplateRender } from "./fixtures/held-template-render";
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
40
|
* Build a tiny fake `Thread` whose `runStreamed` returns a fixed sequence of
|
|
@@ -2611,3 +2613,164 @@ describe("CodexSession — rate-limit error preservation", () => {
|
|
|
2611
2613
|
expect(result.rateLimitResetAt).toBeUndefined();
|
|
2612
2614
|
});
|
|
2613
2615
|
});
|
|
2616
|
+
|
|
2617
|
+
describe("CodexSession context-bound note", () => {
|
|
2618
|
+
let prevSkipEnv: string | undefined;
|
|
2619
|
+
|
|
2620
|
+
beforeAll(() => {
|
|
2621
|
+
prevSkipEnv = process.env.SKIP_SESSION_SUMMARY;
|
|
2622
|
+
process.env.SKIP_SESSION_SUMMARY = "1";
|
|
2623
|
+
});
|
|
2624
|
+
|
|
2625
|
+
afterAll(() => {
|
|
2626
|
+
if (prevSkipEnv === undefined) delete process.env.SKIP_SESSION_SUMMARY;
|
|
2627
|
+
else process.env.SKIP_SESSION_SUMMARY = prevSkipEnv;
|
|
2628
|
+
});
|
|
2629
|
+
|
|
2630
|
+
/** One `context.updated` per request; each request's input tokens in order. */
|
|
2631
|
+
function requestEvents(inputs: unknown[]): ThreadEvent[] {
|
|
2632
|
+
return inputs.map(
|
|
2633
|
+
(input_tokens) =>
|
|
2634
|
+
({
|
|
2635
|
+
type: "context.updated",
|
|
2636
|
+
usage: { input_tokens, cached_input_tokens: 0, output_tokens: 1 },
|
|
2637
|
+
}) as ThreadEvent,
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
/**
|
|
2642
|
+
* Runs the session over `events` and records every `thread.steer` call. The
|
|
2643
|
+
* turn stays open briefly after the requests so the async note can land.
|
|
2644
|
+
*/
|
|
2645
|
+
async function runWithSteers(
|
|
2646
|
+
events: ThreadEvent[],
|
|
2647
|
+
env: Record<string, string>,
|
|
2648
|
+
steer: (input: string) => Promise<void> = async () => {},
|
|
2649
|
+
beforeTurnCompleted: () => Promise<unknown> = () => Bun.sleep(50),
|
|
2650
|
+
model?: string,
|
|
2651
|
+
): Promise<{ steers: string[]; result: ProviderResult }> {
|
|
2652
|
+
const steers: string[] = [];
|
|
2653
|
+
const thread = {
|
|
2654
|
+
id: null,
|
|
2655
|
+
async steer(input: string) {
|
|
2656
|
+
steers.push(input);
|
|
2657
|
+
await steer(input);
|
|
2658
|
+
},
|
|
2659
|
+
async interrupt() {},
|
|
2660
|
+
async close() {},
|
|
2661
|
+
async runStreamed() {
|
|
2662
|
+
async function* generate(): AsyncGenerator<ThreadEvent> {
|
|
2663
|
+
yield { type: "thread.started", thread_id: "thread-bound-note" };
|
|
2664
|
+
yield { type: "turn.started" };
|
|
2665
|
+
for (const event of events) yield event;
|
|
2666
|
+
await beforeTurnCompleted();
|
|
2667
|
+
yield {
|
|
2668
|
+
type: "turn.completed",
|
|
2669
|
+
usage: { input_tokens: 1, cached_input_tokens: 0, output_tokens: 1 },
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
return { events: generate() };
|
|
2673
|
+
},
|
|
2674
|
+
};
|
|
2675
|
+
const adapter = new CodexAdapter({
|
|
2676
|
+
bypassSubprocess: true,
|
|
2677
|
+
threadFactory: () => thread as never,
|
|
2678
|
+
});
|
|
2679
|
+
const session = await adapter.createSession(testConfig({ env, ...(model ? { model } : {}) }));
|
|
2680
|
+
const result = await session.waitForCompletion();
|
|
2681
|
+
return { steers, result };
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
test("steers the note once at the threshold, not again on a larger request", async () => {
|
|
2685
|
+
const { steers, result } = await runWithSteers(requestEvents([400, 1_200, 5_000]), {
|
|
2686
|
+
CONTEXT_BOUND_NOTE_TOKENS: "1000",
|
|
2687
|
+
});
|
|
2688
|
+
expect(result.isError).toBe(false);
|
|
2689
|
+
expect(steers).toEqual([await renderContextBoundNote(1_200, "gpt-5.4")]);
|
|
2690
|
+
expect(steers[0]).toContain("1,200");
|
|
2691
|
+
});
|
|
2692
|
+
|
|
2693
|
+
test("the note shows the window share for a known model and omits it for an unknown one", async () => {
|
|
2694
|
+
const noteFor = async (model: string) =>
|
|
2695
|
+
(
|
|
2696
|
+
await runWithSteers(
|
|
2697
|
+
requestEvents([525_000]),
|
|
2698
|
+
{ CONTEXT_BOUND_NOTE_TOKENS: "1000" },
|
|
2699
|
+
undefined,
|
|
2700
|
+
undefined,
|
|
2701
|
+
model,
|
|
2702
|
+
)
|
|
2703
|
+
).steers;
|
|
2704
|
+
const rest =
|
|
2705
|
+
"Keep this task bounded: finish the current step, store-progress, and complete or defer instead of starting new exploration.";
|
|
2706
|
+
expect(await noteFor("gpt-5.4")).toEqual([
|
|
2707
|
+
`Context is 525,000 tokens (50% of the 1,050,000-token window). ${rest}`,
|
|
2708
|
+
]);
|
|
2709
|
+
expect(await noteFor("made-up-codex-model")).toEqual([`Context is 525,000 tokens. ${rest}`]);
|
|
2710
|
+
});
|
|
2711
|
+
|
|
2712
|
+
test("off when CONTEXT_BOUND_NOTE_TOKENS is unset", async () => {
|
|
2713
|
+
const { steers } = await runWithSteers(requestEvents([250_000, 900_000]), {});
|
|
2714
|
+
expect(steers).toEqual([]);
|
|
2715
|
+
});
|
|
2716
|
+
|
|
2717
|
+
test("no-op below the threshold", async () => {
|
|
2718
|
+
const { steers } = await runWithSteers(requestEvents([400, 999]), {
|
|
2719
|
+
CONTEXT_BOUND_NOTE_TOKENS: "1000",
|
|
2720
|
+
});
|
|
2721
|
+
expect(steers).toEqual([]);
|
|
2722
|
+
});
|
|
2723
|
+
|
|
2724
|
+
test("off at 0", async () => {
|
|
2725
|
+
const { steers } = await runWithSteers(requestEvents([250_000, 500_000]), {
|
|
2726
|
+
CONTEXT_BOUND_NOTE_TOKENS: "0",
|
|
2727
|
+
});
|
|
2728
|
+
expect(steers).toEqual([]);
|
|
2729
|
+
});
|
|
2730
|
+
|
|
2731
|
+
test("fails open on missing or corrupt usage", async () => {
|
|
2732
|
+
const { steers, result } = await runWithSteers(requestEvents([undefined, "lots", null]), {
|
|
2733
|
+
CONTEXT_BOUND_NOTE_TOKENS: "1000",
|
|
2734
|
+
});
|
|
2735
|
+
expect(steers).toEqual([]);
|
|
2736
|
+
expect(result.isError).toBe(false);
|
|
2737
|
+
});
|
|
2738
|
+
|
|
2739
|
+
test("a rejected steer still lets the session complete", async () => {
|
|
2740
|
+
const { steers, result } = await runWithSteers(
|
|
2741
|
+
requestEvents([5_000]),
|
|
2742
|
+
{ CONTEXT_BOUND_NOTE_TOKENS: "1000" },
|
|
2743
|
+
async () => {
|
|
2744
|
+
throw new Error("steer refused");
|
|
2745
|
+
},
|
|
2746
|
+
);
|
|
2747
|
+
expect(steers).toHaveLength(1);
|
|
2748
|
+
expect(result.isError).toBe(false);
|
|
2749
|
+
});
|
|
2750
|
+
|
|
2751
|
+
test("a note rendered after the turn completed is dropped", async () => {
|
|
2752
|
+
const lateSteers = async (releaseInTurn: boolean) => {
|
|
2753
|
+
const held = holdTemplateRender();
|
|
2754
|
+
try {
|
|
2755
|
+
const run = await runWithSteers(
|
|
2756
|
+
requestEvents([5_000]),
|
|
2757
|
+
{ CONTEXT_BOUND_NOTE_TOKENS: "1000" },
|
|
2758
|
+
undefined,
|
|
2759
|
+
async () => {
|
|
2760
|
+
await held.requested;
|
|
2761
|
+
if (releaseInTurn) held.release();
|
|
2762
|
+
await Bun.sleep(50);
|
|
2763
|
+
},
|
|
2764
|
+
);
|
|
2765
|
+
held.release();
|
|
2766
|
+
await Bun.sleep(50);
|
|
2767
|
+
return run.steers;
|
|
2768
|
+
} finally {
|
|
2769
|
+
held.stop();
|
|
2770
|
+
}
|
|
2771
|
+
};
|
|
2772
|
+
expect(await lateSteers(false)).toEqual([]);
|
|
2773
|
+
// Control: ready while the turn is still open.
|
|
2774
|
+
expect(await lateSteers(true)).toEqual([HELD_NOTE_TEXT]);
|
|
2775
|
+
});
|
|
2776
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { CodexAdapter } from "../providers/codex-adapter";
|
|
6
|
+
import type { ProviderSessionConfig } from "../providers/types";
|
|
7
|
+
|
|
8
|
+
const keyVars = ["OPENAI_API_KEY", "OPENROUTER_API_KEY", "ANTHROPIC_API_KEY"];
|
|
9
|
+
let savedEnv: Record<string, string | undefined>;
|
|
10
|
+
let home: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
savedEnv = Object.fromEntries(["HOME", ...keyVars].map((key) => [key, process.env[key]]));
|
|
14
|
+
home = await mkdtemp(join(tmpdir(), "credential-validation-codex-"));
|
|
15
|
+
process.env.HOME = home;
|
|
16
|
+
for (const key of keyVars) process.env[key] = "example-first, example-second";
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
for (const [key, value] of Object.entries(savedEnv)) {
|
|
21
|
+
if (value === undefined) delete process.env[key];
|
|
22
|
+
else process.env[key] = value;
|
|
23
|
+
}
|
|
24
|
+
await rm(home, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function config(env?: Record<string, string>): ProviderSessionConfig {
|
|
28
|
+
return {
|
|
29
|
+
agentId: crypto.randomUUID(),
|
|
30
|
+
prompt: "test",
|
|
31
|
+
systemPrompt: "",
|
|
32
|
+
cwd: home,
|
|
33
|
+
logFile: "/dev/null",
|
|
34
|
+
env,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("Codex credential pools", () => {
|
|
39
|
+
for (const source of ["container", "task", "task-pool"] as const) {
|
|
40
|
+
test(`app-server uses one OpenAI key from ${source}`, async () => {
|
|
41
|
+
let capturedEnv: Record<string, string> | undefined;
|
|
42
|
+
const adapter = new CodexAdapter({
|
|
43
|
+
bypassSubprocess: true,
|
|
44
|
+
appServerFactory: ({ env }) => {
|
|
45
|
+
capturedEnv = env;
|
|
46
|
+
throw new Error("test stops before app-server spawn");
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
const env =
|
|
50
|
+
source === "container"
|
|
51
|
+
? {}
|
|
52
|
+
: {
|
|
53
|
+
OPENAI_API_KEY:
|
|
54
|
+
source === "task" ? "example-task" : "example-task-first, example-task-second",
|
|
55
|
+
};
|
|
56
|
+
await expect(adapter.createSession(config(env))).rejects.toThrow(
|
|
57
|
+
"test stops before app-server spawn",
|
|
58
|
+
);
|
|
59
|
+
expect(
|
|
60
|
+
source === "container"
|
|
61
|
+
? ["example-first", "example-second"]
|
|
62
|
+
: source === "task"
|
|
63
|
+
? ["example-task"]
|
|
64
|
+
: ["example-task-first", "example-task-second"],
|
|
65
|
+
).toContain(capturedEnv?.OPENAI_API_KEY);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test(`subprocess forwards one key per provider from ${source}`, async () => {
|
|
69
|
+
let capturedEnv: Record<string, string | undefined> | undefined;
|
|
70
|
+
const spawn = spyOn(Bun, "spawn").mockImplementation(((
|
|
71
|
+
...args: Parameters<typeof Bun.spawn>
|
|
72
|
+
) => {
|
|
73
|
+
capturedEnv = args[1]?.env;
|
|
74
|
+
throw new Error("test stops before subprocess spawn");
|
|
75
|
+
}) as typeof Bun.spawn);
|
|
76
|
+
try {
|
|
77
|
+
const env =
|
|
78
|
+
source === "container"
|
|
79
|
+
? {}
|
|
80
|
+
: Object.fromEntries(
|
|
81
|
+
keyVars.map((key) => [
|
|
82
|
+
key,
|
|
83
|
+
source === "task" ? "example-task" : "example-task-first, example-task-second",
|
|
84
|
+
]),
|
|
85
|
+
);
|
|
86
|
+
await expect(new CodexAdapter().createSession(config(env))).rejects.toThrow(
|
|
87
|
+
"test stops before subprocess spawn",
|
|
88
|
+
);
|
|
89
|
+
for (const key of keyVars) {
|
|
90
|
+
expect(
|
|
91
|
+
source === "container"
|
|
92
|
+
? ["example-first", "example-second"]
|
|
93
|
+
: source === "task"
|
|
94
|
+
? ["example-task"]
|
|
95
|
+
: ["example-task-first", "example-task-second"],
|
|
96
|
+
).toContain(capturedEnv?.[key]);
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
spawn.mockRestore();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
test("chatgpt mode still removes the OpenAI API key", async () => {
|
|
105
|
+
await Bun.write(join(home, ".codex", "auth.json"), JSON.stringify({ auth_mode: "chatgpt" }));
|
|
106
|
+
let capturedEnv: Record<string, string> | undefined;
|
|
107
|
+
const adapter = new CodexAdapter({
|
|
108
|
+
bypassSubprocess: true,
|
|
109
|
+
appServerFactory: ({ env }) => {
|
|
110
|
+
capturedEnv = env;
|
|
111
|
+
throw new Error("test stops before app-server spawn");
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
await expect(adapter.createSession(config({ OPENAI_API_KEY: "example-task" }))).rejects.toThrow(
|
|
115
|
+
"test stops before app-server spawn",
|
|
116
|
+
);
|
|
117
|
+
expect(capturedEnv).toBeDefined();
|
|
118
|
+
expect(capturedEnv?.OPENAI_API_KEY).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
});
|