@desplega.ai/agent-swarm 2.5.4 → 2.7.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 +4 -2
- package/dist/{acp-adapter-ek38cwsv.js → acp-adapter-ma1js5n7.js} +9 -9
- package/dist/{actions-92mepq03.js → actions-1vhmtvg9.js} +21 -21
- package/dist/{amp-adapter-9gaxg015.js → amp-adapter-sxqqvmdk.js} +4 -4
- package/dist/{app-ctn8gsgx.js → app-mwx1d5a9.js} +3 -3
- package/dist/{assistant-zyr5kz47.js → assistant-yx1p6g82.js} +30 -26
- package/dist/{boot-reembed-w75maga2.js → boot-reembed-5h37a2c2.js} +1 -1
- package/dist/{boot-reembed-dwzfttdr.js → boot-reembed-7h78vmvv.js} +2 -1
- package/dist/{boot-scrub-sweep-e9kq43kf.js → boot-scrub-sweep-8mawd4gw.js} +4 -4
- package/dist/{boot-seeds-x28v3fhb.js → boot-seeds-ca84dhnn.js} +4 -4
- package/dist/{claude-adapter-f0afqnne.js → claude-adapter-wqt4hfkq.js} +9 -9
- package/dist/{claude-managed-adapter-x81a7r65.js → claude-managed-adapter-6fjqtr2z.js} +2 -2
- package/dist/{claude-managed-setup-bqzwjd9x.js → claude-managed-setup-rqv98sea.js} +10 -3
- package/dist/{claude-sdk-session-7b7whkaq.js → claude-sdk-session-f29f3b2x.js} +11 -11
- package/dist/{cli-sxgtf5fw.js → cli-07q03xm2.js} +1 -1
- package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
- package/dist/{cli-g9vd9ece.js → cli-1hhbzkbb.js} +14 -16
- package/dist/{cli-qqzmjxkf.js → cli-1m9x8dr5.js} +11 -6
- package/dist/{cli-jbnn4vxb.js → cli-2pvzmas1.js} +3 -3
- package/dist/{cli-x77gen2p.js → cli-2y7v133v.js} +7 -7
- package/dist/{cli-dew6x255.js → cli-39aet655.js} +2 -2
- package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
- package/dist/{cli-jb30dzvk.js → cli-48gxjmcz.js} +530 -190
- package/dist/{cli-pzatvw94.js → cli-54sj64fc.js} +2 -2
- package/dist/{cli-bvpgr9ny.js → cli-60sty27p.js} +5 -5
- package/dist/{cli-asq5ydm4.js → cli-6a9qfa8b.js} +411 -69
- package/dist/{cli-277c31dw.js → cli-6mgdba42.js} +225 -3
- package/dist/{cli-19202kew.js → cli-6srv653c.js} +5 -5
- package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
- package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
- package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
- package/dist/{cli-hh5we352.js → cli-86ehn4s5.js} +7 -7
- package/dist/{cli-9s05422w.js → cli-8mbc9za9.js} +1 -1
- package/dist/{cli-ff1tm9n9.js → cli-8wmj18cz.js} +1 -1
- package/dist/{cli-1r5ad7j5.js → cli-9gsm2rzp.js} +12 -12
- package/dist/{cli-mz9afxxf.js → cli-9pshx6rd.js} +17 -3
- package/dist/{cli-s682zgqz.js → cli-a6ds2epe.js} +1 -1
- package/dist/{cli-6a1xg1p7.js → cli-a89zdqz9.js} +2 -2
- package/dist/{cli-03vpq45j.js → cli-ay40dnj3.js} +2 -2
- package/dist/{cli-tsma7p8v.js → cli-aypd09nf.js} +2 -2
- package/dist/{cli-ethgy7ha.js → cli-b3yzyc57.js} +2 -2
- package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
- package/dist/{cli-mmhj8vd7.js → cli-c668vvcf.js} +11 -2
- package/dist/{cli-sbwsxvtd.js → cli-d20qvpr2.js} +2 -2
- package/dist/{cli-c952c7ac.js → cli-e1595bgx.js} +3 -3
- package/dist/{cli-621ce7kw.js → cli-f12mr20z.js} +3 -3
- package/dist/{cli-dqygeeh5.js → cli-f1e0ptjj.js} +10 -1
- package/dist/{cli-xzfq4at3.js → cli-f4dbh7a5.js} +16 -12
- package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
- package/dist/{cli-5bnsc0e1.js → cli-gsfk58nw.js} +5 -5
- package/dist/cli-gt4h8cxw.js +895 -0
- package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
- package/dist/{cli-25n3mv5d.js → cli-h5jeg46p.js} +6 -6
- package/dist/{cli-gn5af0x5.js → cli-he9gbdx0.js} +62 -12
- package/dist/{cli-gbfxp48k.js → cli-j0ppywdr.js} +42 -35
- package/dist/{cli-k4tjsdx9.js → cli-j3y0x2n4.js} +277 -199
- package/dist/{cli-7p4jxgtt.js → cli-j9bpmykh.js} +75 -27
- package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
- package/dist/{cli-wx2nj19m.js → cli-kv7sbc2c.js} +6 -6
- package/dist/{cli-abs8ndtr.js → cli-m30k9777.js} +26 -26
- package/dist/{cli-cz5nmdxt.js → cli-m5e802r7.js} +2 -2
- package/dist/{cli-0shx9j93.js → cli-pfk92eqz.js} +2 -2
- package/dist/{cli-xtrz6bzs.js → cli-ptfq202x.js} +5 -3
- package/dist/{cli-6n8axjv1.js → cli-qj442fws.js} +2 -2
- package/dist/{cli-c61mbdds.js → cli-rd5m70gj.js} +4 -4
- package/dist/{cli-eftzyh9y.js → cli-s09j9vp5.js} +2 -2
- package/dist/{cli-m4ecdxvz.js → cli-s0mgx214.js} +7 -7
- package/dist/{cli-wghjkr0e.js → cli-sgqm2582.js} +10 -7
- package/dist/{cli-2655k16q.js → cli-sjctjr48.js} +2 -2
- package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
- package/dist/{cli-x0f2v69c.js → cli-twctnkng.js} +12 -7
- package/dist/{cli-j6rktc91.js → cli-v6shg7na.js} +1 -1
- package/dist/{cli-gx4rm0qx.js → cli-vz78s5wm.js} +1 -1
- package/dist/{cli-gwhjmh8g.js → cli-wqr7cea4.js} +5 -5
- package/dist/{cli-e42vss7b.js → cli-x37r7qwf.js} +16 -8
- package/dist/{cli-4b2z6n0m.js → cli-yd2nreqw.js} +17 -8
- package/dist/{cli-64mh9mf3.js → cli-ymjpkxgn.js} +69 -17
- package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
- package/dist/{cli-9j129brq.js → cli-yxbsmx56.js} +6 -6
- package/dist/cli.js +22 -20
- package/dist/{codex-adapter-wch12q99.js → codex-adapter-zk59x2zh.js} +7 -7
- package/dist/{codex-hook-brpnycn5.js → codex-hook-xyjhvxwd.js} +1 -1
- package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
- package/dist/{codex-session-runner-41pfcr95.js → codex-session-runner-kh6s4j3n.js} +8 -8
- package/dist/{commands-9849dkj6.js → commands-68et1qv2.js} +2 -2
- package/dist/{controller-ja0sd95p.js → controller-fyrbnzg4.js} +4 -4
- package/dist/{controllers-connect-xp05dj6x.js → controllers-connect-nep9h2xv.js} +23 -23
- package/dist/{cursor-adapter-623qxqew.js → cursor-adapter-yss3pg7t.js} +2 -2
- package/dist/{db-w759qmx0.js → db-28gjyvxj.js} +4 -2
- package/dist/{db-47vj40dy.js → db-8ftwvzba.js} +3 -3
- package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
- package/dist/{dispatcher-7b8e4z2j.js → dispatcher-hayexmkr.js} +20 -20
- package/dist/{docker-s84hjs91.js → docker-fdzyewpz.js} +2 -2
- package/dist/{dsh-adapter-2ezw3xea.js → dsh-adapter-46wawff9.js} +2 -2
- 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-8d1e0h4m.js} +1 -1
- package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
- package/dist/{execute-0xpz8950.js → execute-cjrfxfnm.js} +1 -1
- package/dist/{external-channel-e2s028da.js → external-channel-xbmxcg09.js} +2 -2
- package/dist/{grok-adapter-ys7gaqee.js → grok-adapter-ag4wks2a.js} +10 -10
- package/dist/handlers-tzmevqb5.js +41 -0
- package/dist/{heartbeat-w3mntghj.js → heartbeat-psfvcgwp.js} +23 -23
- package/dist/{hook-g7kvv6x9.js → hook-8pz04697.js} +6 -6
- package/dist/{hook-3jjywnpb.js → hook-cyq4mryn.js} +8 -8
- package/dist/{http-tkp7d4kb.js → http-haj1nd6k.js} +921 -169
- package/dist/{index-q7m26kvs.js → index-jwt2bj6f.js} +265 -97
- package/dist/{index-3n8m9ea2.js → index-q52f47pe.js} +1 -1
- package/dist/{index-31ceyf42.js → index-syvhnvtw.js} +1 -1
- package/dist/index-y1ajx04a.js +43 -0
- package/dist/{index-1ef1myq2.js → index-z5h22pes.js} +20 -20
- package/dist/{index-xdaahthb.js → index-zyq8ct52.js} +20 -20
- package/dist/{keepalive-v4ta8wgg.js → keepalive-xdvsamvj.js} +4 -4
- package/dist/lead-hqg57kdp.js +79 -0
- package/dist/{maintenance-84n1ex21.js → maintenance-4y2xzh3a.js} +1 -1
- package/dist/{oauth-refresh-sweep-vjnnandw.js → oauth-refresh-sweep-jd4r7zbs.js} +9 -4
- package/dist/{onboard-sfmpe4np.js → onboard-18447q9q.js} +6 -6
- package/dist/{opencode-adapter-jmpst4q4.js → opencode-adapter-26b2frdf.js} +7 -7
- package/dist/{otel-impl-v6cc0zve.js → otel-impl-f9tgr8yw.js} +2 -2
- package/dist/{pi-mono-adapter-fk8fkcf6.js → pi-mono-adapter-qn6qbga1.js} +38 -17
- package/dist/{pricing-refresh-fsk1e1zc.js → pricing-refresh-dw2tgb53.js} +3 -3
- package/dist/{rbac-roles-prmkdnkw.js → rbac-roles-aygm2760.js} +2 -2
- package/dist/{rbac-roles-zxmxqa39.js → rbac-roles-s4nwj2m3.js} +1 -1
- package/dist/render-v2-yfvdfr0x.js +16 -0
- package/dist/{resume-frdtgfa0.js → resume-1bmvjbf3.js} +9 -9
- package/dist/{runtime-bt5pkpm1.js → runtime-f7tpzedy.js} +1 -1
- package/dist/{session-access-fpez4y5q.js → session-access-r5garej6.js} +4 -4
- package/dist/session-source-backfill-424wzv0r.js +110 -0
- package/dist/{setup-mq6m3m8q.js → setup-8tygvxkq.js} +1 -1
- package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-xsw323b3.js} +2 -2
- package/dist/{work-objects-kfahhjsq.js → work-objects-ggnghpgf.js} +23 -23
- package/dist/worker-sqjd1nqf.js +79 -0
- package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
- package/openapi.json +1324 -14
- package/package.json +5 -3
- package/src/assets/source-key.ts +95 -0
- package/src/be/automation-preflight.ts +2 -0
- package/src/be/db/api-keys.ts +89 -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 +6 -1
- package/src/be/db/workflow-runs.ts +219 -6
- package/src/be/db-retention.ts +61 -6
- package/src/be/db.ts +135 -132
- package/src/be/memory/providers/openai-embedding.ts +5 -1
- package/src/be/memory/task-learning.ts +155 -0
- package/src/be/migrations/244_api_key_validation.sql +4 -0
- package/src/be/migrations/245_script_executor.sql +3 -0
- package/src/be/oauth-refresh-sweep.ts +2 -1
- package/src/be/onboarding.ts +4 -0
- package/src/be/scripts/boot-reembed.ts +2 -0
- package/src/be/scripts/db.ts +14 -4
- package/src/be/scripts/embeddings.ts +2 -0
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +10 -4
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/session-source-backfill.ts +163 -0
- package/src/be/swarm-config-guard.ts +1 -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/credential-refresh.ts +76 -1
- package/src/commands/credential-wait.ts +2 -2
- package/src/commands/onboard/compose-generator.ts +5 -3
- package/src/commands/provider-credentials.ts +212 -171
- package/src/commands/runner.ts +25 -4
- package/src/extensions/apply-task-create.ts +8 -1
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +5 -0
- package/src/heartbeat/heartbeat.ts +14 -1
- package/src/heartbeat/script-runner-probe.ts +47 -0
- package/src/http/all-routes.ts +2 -0
- package/src/http/api-keys.ts +77 -9
- 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 +1 -1
- package/src/http/core.ts +6 -0
- package/src/http/fleet-benchmark.ts +5 -2
- package/src/http/idempotency-key.ts +1 -1
- package/src/http/index.ts +14 -3
- package/src/http/integrations.ts +5 -1
- package/src/http/kv.ts +8 -3
- package/src/http/onboarding.ts +6 -1
- package/src/http/pages-public.ts +38 -5
- package/src/http/pool-keys.ts +354 -0
- package/src/http/scripts.ts +27 -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 +88 -9
- package/src/http/x.ts +5 -0
- package/src/integrations/chat-sdk/inbound.ts +27 -0
- package/src/kv-reserved-namespaces.ts +19 -0
- package/src/prompts/base-prompt.ts +21 -1
- package/src/prompts/session-templates.ts +17 -3
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +5 -1
- package/src/providers/claude-managed-adapter.ts +16 -9
- package/src/providers/codex-adapter.ts +71 -11
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +41 -11
- package/src/scheduler/scheduler.ts +5 -3
- package/src/scripts-runtime/eval-harness.ts +8 -1
- package/src/scripts-runtime/executors/native.ts +22 -16
- package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
- package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
- package/src/scripts-runtime/executors/quickjs.ts +7 -0
- package/src/scripts-runtime/executors/registry.ts +32 -0
- package/src/scripts-runtime/executors/types.ts +7 -0
- package/src/scripts-runtime/host-validated-args.ts +133 -0
- package/src/scripts-runtime/loader.ts +30 -3
- package/src/scripts-runtime/swarm-sdk.ts +16 -3
- package/src/scripts-runtime/types/stdlib.d.ts +10 -2
- package/src/scripts-runtime/types/swarm-extension.d.ts +17 -8
- package/src/scripts-runtime/types/swarm-sdk.d.ts +10 -2
- 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/api-key-status-by-suffix.test.ts +242 -0
- package/src/tests/api-keys-validation.test.ts +170 -0
- package/src/tests/base-prompt.test.ts +70 -2
- package/src/tests/claude-managed-adapter.test.ts +46 -0
- package/src/tests/claude-routes-e2e.test.ts +7 -2
- 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/community-linear-delivery-loop.test.ts +2099 -0
- package/src/tests/config-pool-keys.test.ts +533 -0
- package/src/tests/credential-check.test.ts +294 -4
- 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/docs-index.test.ts +135 -0
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/extensions-pre-task.test.ts +53 -1
- package/src/tests/fetch-resolved-env.test.ts +16 -2
- 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/fleet-benchmark.test.ts +8 -0
- package/src/tests/get-workflow-run-tool.test.ts +98 -0
- package/src/tests/heartbeat-checklist.test.ts +89 -0
- package/src/tests/heartbeat-script-runner-probe.test.ts +83 -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/oauth-refresh-sweep.test.ts +34 -1
- package/src/tests/onboard-compose.test.ts +20 -0
- package/src/tests/onboarding.test.ts +108 -1
- package/src/tests/openai-embedding-credentials.test.ts +35 -0
- package/src/tests/opencode-adapter.test.ts +2 -1
- package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
- package/src/tests/org-website.test.ts +36 -0
- package/src/tests/pages-frame-ancestors.test.ts +162 -0
- package/src/tests/pi-codemode.test.ts +2 -1
- package/src/tests/pi-mono-adapter.test.ts +54 -0
- package/src/tests/pi-native-mcp.test.ts +13 -1
- package/src/tests/pi-tool-exposure.test.ts +8 -8
- package/src/tests/probe-tool-schema-tokens.test.ts +144 -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/request-composition-derive.test.ts +329 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/script-executor-conformance.test.ts +36 -1
- package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
- package/src/tests/script-executor-quickjs.test.ts +140 -0
- package/src/tests/script-host-validated-args-native.test.ts +196 -0
- package/src/tests/script-per-script-executor.test.ts +415 -0
- package/src/tests/script-runs-retention.test.ts +212 -0
- package/src/tests/scripts-external-api.test.ts +45 -0
- package/src/tests/sdk-allowlist.test.ts +42 -2
- package/src/tests/self-improvement.test.ts +4 -4
- 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 +299 -2
- package/src/tests/slack-inbound-files.test.ts +103 -23
- package/src/tests/status.test.ts +101 -18
- package/src/tests/store-progress-learning.test.ts +288 -0
- 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/ui-logs-parser.test.ts +180 -0
- package/src/tests/used-by-route.test.ts +11 -0
- package/src/tests/workflow-cascade-cancel.test.ts +172 -0
- package/src/tests/workflow-engine-v2.test.ts +116 -1
- package/src/tests/workflow-http-v2.test.ts +131 -0
- package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
- package/src/tests/workflow-run-finished-event.test.ts +263 -0
- package/src/tests/workflow-step-order.test.ts +92 -0
- package/src/tests/workflow-sub-workflow.test.ts +30 -0
- package/src/tests/workflow-swarm-script.test.ts +34 -0
- package/src/tools/get-tasks.ts +8 -0
- package/src/tools/kv/kv-read-auth.ts +4 -2
- package/src/tools/script-common.ts +1 -0
- package/src/tools/script-run.ts +7 -0
- package/src/tools/script-upsert.ts +7 -0
- package/src/tools/store-progress.ts +67 -1
- package/src/tools/task-action.ts +1 -1
- package/src/tools/workflows/get-workflow-run.ts +14 -4
- package/src/tools/workflows/trigger-workflow.ts +104 -10
- package/src/types.ts +36 -1
- package/src/utils/company-profile.ts +51 -1
- package/src/utils/credentials.ts +10 -4
- 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/engine.ts +18 -16
- package/src/workflows/executors/sub-workflow.ts +8 -1
- package/src/workflows/executors/swarm-script.ts +5 -0
- package/src/workflows/foreach-driver.ts +5 -11
- package/src/workflows/node-timeout.ts +30 -0
- package/templates/ai-toolbox.manifest.json +2 -2
- package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
- package/templates/community/linear-delivery-loop/README.md +38 -0
- package/templates/community/linear-delivery-loop/config.example.json +31 -0
- package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
- package/templates/community/linear-delivery-loop/schedule.json +10 -0
- package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
- package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
- package/templates/skills/researching/content.md +2 -0
- package/templates/skills/swarm-docs/config.json +14 -0
- package/templates/skills/swarm-docs/content.md +144 -0
- package/templates/skills/swarm-scripts/SKILL.md +1 -0
- package/templates/skills/swarm-scripts/content.md +1 -0
- package/dist/cli-7421jzsw.js +0 -84
- package/dist/cli-e45e4m6b.js +0 -413
- package/dist/cli-zw88zjba.js +0 -307
- package/dist/handlers-wy1dr6ja.js +0 -41
- package/dist/index-nmm79qgr.js +0 -43
- package/dist/lead-sawnyqxk.js +0 -79
- package/dist/render-v2-pjwxvnkw.js +0 -12
- package/dist/worker-v5e5rxj3.js +0 -79
package/src/tests/status.test.ts
CHANGED
|
@@ -864,30 +864,30 @@ describe("validateProviderCredentials — error scrubbing", () => {
|
|
|
864
864
|
expect(result.error).toContain("ANTHROPIC_API_KEY");
|
|
865
865
|
});
|
|
866
866
|
|
|
867
|
-
test("claude with CLAUDE_CODE_OAUTH_TOKEN
|
|
867
|
+
test("claude with CLAUDE_CODE_OAUTH_TOKEN checks the token against the OAuth profile", async () => {
|
|
868
868
|
delete process.env.ANTHROPIC_API_KEY;
|
|
869
869
|
process.env.CLAUDE_CODE_OAUTH_TOKEN = "sk-ant-oat01-fake-oauth-token";
|
|
870
|
-
|
|
871
|
-
globalThis.fetch = (async () => {
|
|
872
|
-
|
|
870
|
+
const urls: string[] = [];
|
|
871
|
+
globalThis.fetch = (async (url: string | URL | Request) => {
|
|
872
|
+
urls.push(String(url));
|
|
873
873
|
return new Response("{}", { status: 200 });
|
|
874
874
|
}) as typeof fetch;
|
|
875
875
|
const result = await validateProviderCredentials("claude");
|
|
876
876
|
expect(result.ok).toBe(true);
|
|
877
|
-
expect(
|
|
877
|
+
expect(urls).toEqual(["https://api.anthropic.com/api/oauth/profile"]);
|
|
878
878
|
});
|
|
879
879
|
|
|
880
|
-
test("claude prefers OAuth
|
|
880
|
+
test("claude prefers the OAuth check over the API-key check when both are set", async () => {
|
|
881
881
|
process.env.ANTHROPIC_API_KEY = "sk-ant-api-fake-1234";
|
|
882
882
|
process.env.CLAUDE_CODE_OAUTH_TOKEN = "sk-ant-oat01-fake-oauth";
|
|
883
|
-
|
|
884
|
-
globalThis.fetch = (async () => {
|
|
885
|
-
|
|
883
|
+
const urls: string[] = [];
|
|
884
|
+
globalThis.fetch = (async (url: string | URL | Request) => {
|
|
885
|
+
urls.push(String(url));
|
|
886
886
|
return new Response("{}", { status: 200 });
|
|
887
887
|
}) as typeof fetch;
|
|
888
888
|
const result = await validateProviderCredentials("claude");
|
|
889
889
|
expect(result.ok).toBe(true);
|
|
890
|
-
expect(
|
|
890
|
+
expect(urls).toEqual(["https://api.anthropic.com/api/oauth/profile"]);
|
|
891
891
|
});
|
|
892
892
|
|
|
893
893
|
test("codex with valid CODEX_OAUTH JSON passes via presence check (no upstream call)", async () => {
|
|
@@ -1212,10 +1212,10 @@ describe("computeHealth (Phase 2)", () => {
|
|
|
1212
1212
|
expect((harness.providers ?? []).map((p) => p.provider)).toEqual(["claude"]);
|
|
1213
1213
|
});
|
|
1214
1214
|
|
|
1215
|
-
test("`ok` when workers are `configured`
|
|
1215
|
+
test("`ok` when workers are `configured` and the seeded lead is `idle`", async () => {
|
|
1216
1216
|
// Workers in `configured` state means agents exist but haven't posted a
|
|
1217
|
-
// heartbeat in the last 5 minutes.
|
|
1218
|
-
//
|
|
1217
|
+
// heartbeat in the last 5 minutes. Health reads the lead's status, not
|
|
1218
|
+
// its heartbeat: this lead is `idle`, so it can take work.
|
|
1219
1219
|
const lead = await createAgent({
|
|
1220
1220
|
name: "lead-d",
|
|
1221
1221
|
isLead: true,
|
|
@@ -1229,9 +1229,92 @@ describe("computeHealth (Phase 2)", () => {
|
|
|
1229
1229
|
});
|
|
1230
1230
|
const payload = await buildStatusPayload();
|
|
1231
1231
|
expect(getMilestone(payload, "workers").state).toBe("configured");
|
|
1232
|
+
expect(payload.activity.leads_ready).toBe(1);
|
|
1233
|
+
expect(payload.health).toBe("ok");
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
test("`ok` for an idle lead with a stale heartbeat and no worker heartbeat", async () => {
|
|
1237
|
+
const lead = await createAgent({
|
|
1238
|
+
name: "lead-idle",
|
|
1239
|
+
isLead: true,
|
|
1240
|
+
status: "idle",
|
|
1241
|
+
capabilities: [],
|
|
1242
|
+
});
|
|
1243
|
+
const worker = await createAgent({
|
|
1244
|
+
name: "worker-quiet",
|
|
1245
|
+
isLead: false,
|
|
1246
|
+
status: "idle",
|
|
1247
|
+
capabilities: [],
|
|
1248
|
+
});
|
|
1249
|
+
// The lead last ran a session an hour ago. The worker never heartbeated.
|
|
1250
|
+
const hourAgo = new Date(Date.now() - 3_600_000).toISOString();
|
|
1251
|
+
await getDbClient().run(`UPDATE agents SET lastActivityAt = ? WHERE id = ?`, [
|
|
1252
|
+
hourAgo,
|
|
1253
|
+
lead.id,
|
|
1254
|
+
]);
|
|
1255
|
+
const pass = { ok: true, error: null, latency_ms: 12, testedAt: Date.now() };
|
|
1256
|
+
await seedCredStatus(lead.id, "claude", { ready: true, satisfiedBy: "env", liveTest: pass });
|
|
1257
|
+
await seedCredStatus(worker.id, "claude", { ready: true, satisfiedBy: "env", liveTest: pass });
|
|
1258
|
+
|
|
1259
|
+
const payload = await buildStatusPayload();
|
|
1260
|
+
expect(payload.activity.leads_online).toBe(0);
|
|
1261
|
+
expect(payload.activity.leads_ready).toBe(1);
|
|
1232
1262
|
expect(payload.health).toBe("ok");
|
|
1233
1263
|
});
|
|
1234
1264
|
|
|
1265
|
+
test("`degraded` when no lead row exists", async () => {
|
|
1266
|
+
const worker = await createAgent({
|
|
1267
|
+
name: "worker-alone",
|
|
1268
|
+
isLead: false,
|
|
1269
|
+
status: "idle",
|
|
1270
|
+
capabilities: [],
|
|
1271
|
+
});
|
|
1272
|
+
await updateAgentActivity(worker.id);
|
|
1273
|
+
await seedCredStatus(worker.id, "claude", {
|
|
1274
|
+
ready: true,
|
|
1275
|
+
satisfiedBy: "env",
|
|
1276
|
+
liveTest: { ok: true, error: null, latency_ms: 12, testedAt: Date.now() },
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
const payload = await buildStatusPayload();
|
|
1280
|
+
expect(getMilestone(payload, "harness").state).toBe("verified");
|
|
1281
|
+
expect(payload.activity.leads_ready).toBe(0);
|
|
1282
|
+
expect(payload.health).toBe("degraded");
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
test("`degraded` when the only lead is offline", async () => {
|
|
1286
|
+
const lead = await createAgent({
|
|
1287
|
+
name: "lead-gone",
|
|
1288
|
+
isLead: true,
|
|
1289
|
+
status: "offline",
|
|
1290
|
+
capabilities: [],
|
|
1291
|
+
});
|
|
1292
|
+
const worker = await createAgent({
|
|
1293
|
+
name: "worker-left",
|
|
1294
|
+
isLead: false,
|
|
1295
|
+
status: "idle",
|
|
1296
|
+
capabilities: [],
|
|
1297
|
+
});
|
|
1298
|
+
await updateAgentActivity(worker.id);
|
|
1299
|
+
const pass = { ok: true, error: null, latency_ms: 12, testedAt: Date.now() };
|
|
1300
|
+
await seedCredStatus(lead.id, "claude", { ready: true, satisfiedBy: "env", liveTest: pass });
|
|
1301
|
+
await seedCredStatus(worker.id, "claude", { ready: true, satisfiedBy: "env", liveTest: pass });
|
|
1302
|
+
|
|
1303
|
+
const payload = await buildStatusPayload();
|
|
1304
|
+
expect(getMilestone(payload, "harness").state).toBe("verified");
|
|
1305
|
+
expect(payload.activity.leads_ready).toBe(0);
|
|
1306
|
+
expect(payload.health).toBe("degraded");
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
test("no ready lead degrades an otherwise `ok` rollup", () => {
|
|
1310
|
+
const synthetic: SetupMilestone[] = [
|
|
1311
|
+
{ id: "harness", label: "Harness", state: "verified" },
|
|
1312
|
+
{ id: "workers", label: "Workers", state: "verified" },
|
|
1313
|
+
];
|
|
1314
|
+
expect(computeHealth(synthetic, 1)).toBe("ok");
|
|
1315
|
+
expect(computeHealth(synthetic, 0)).toBe("degraded");
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1235
1318
|
test("`ok` when harness verified and workers verified (no other integration is `configured`)", () => {
|
|
1236
1319
|
// We can't reach the in-memory cache from here, so simulate by directly
|
|
1237
1320
|
// checking the helper: build a synthetic milestone array.
|
|
@@ -1245,7 +1328,7 @@ describe("computeHealth (Phase 2)", () => {
|
|
|
1245
1328
|
{ id: "workers", label: "Workers", state: "verified" },
|
|
1246
1329
|
{ id: "first_task", label: "First task", state: "verified" },
|
|
1247
1330
|
];
|
|
1248
|
-
expect(computeHealth(synthetic)).toBe("ok");
|
|
1331
|
+
expect(computeHealth(synthetic, 1)).toBe("ok");
|
|
1249
1332
|
});
|
|
1250
1333
|
|
|
1251
1334
|
test("`degraded` when an integration is `configured`", () => {
|
|
@@ -1258,7 +1341,7 @@ describe("computeHealth (Phase 2)", () => {
|
|
|
1258
1341
|
{ id: "workers", label: "Workers", state: "verified" },
|
|
1259
1342
|
{ id: "first_task", label: "First task", state: "verified" },
|
|
1260
1343
|
];
|
|
1261
|
-
expect(computeHealth(synthetic)).toBe("degraded");
|
|
1344
|
+
expect(computeHealth(synthetic, 1)).toBe("degraded");
|
|
1262
1345
|
});
|
|
1263
1346
|
|
|
1264
1347
|
test("`ok` when a connected Slack reports `verified` next to other connected integrations", () => {
|
|
@@ -1271,7 +1354,7 @@ describe("computeHealth (Phase 2)", () => {
|
|
|
1271
1354
|
{ id: "workers", label: "Workers", state: "verified" },
|
|
1272
1355
|
{ id: "first_task", label: "First task", state: "verified" },
|
|
1273
1356
|
];
|
|
1274
|
-
expect(computeHealth(synthetic)).toBe("ok");
|
|
1357
|
+
expect(computeHealth(synthetic, 1)).toBe("ok");
|
|
1275
1358
|
});
|
|
1276
1359
|
|
|
1277
1360
|
test("integrations in `unverified` alone do NOT degrade health", () => {
|
|
@@ -1286,7 +1369,7 @@ describe("computeHealth (Phase 2)", () => {
|
|
|
1286
1369
|
{ id: "workers", label: "Workers", state: "verified" },
|
|
1287
1370
|
{ id: "first_task", label: "First task", state: "unverified" },
|
|
1288
1371
|
];
|
|
1289
|
-
expect(computeHealth(synthetic)).toBe("ok");
|
|
1372
|
+
expect(computeHealth(synthetic, 1)).toBe("ok");
|
|
1290
1373
|
});
|
|
1291
1374
|
|
|
1292
1375
|
test("an unverified embedding key never blocks boot or degrades health", () => {
|
|
@@ -1295,7 +1378,7 @@ describe("computeHealth (Phase 2)", () => {
|
|
|
1295
1378
|
{ id: "embeddings", label: "Memory search", state: "unverified" },
|
|
1296
1379
|
{ id: "workers", label: "Workers", state: "verified" },
|
|
1297
1380
|
];
|
|
1298
|
-
expect(computeHealth(synthetic)).toBe("ok");
|
|
1381
|
+
expect(computeHealth(synthetic, 1)).toBe("ok");
|
|
1299
1382
|
});
|
|
1300
1383
|
});
|
|
1301
1384
|
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import type { ZodType } from "zod";
|
|
4
|
+
import {
|
|
5
|
+
closeDb,
|
|
6
|
+
createAgent,
|
|
7
|
+
createTaskExtended,
|
|
8
|
+
getDb,
|
|
9
|
+
getTaskById,
|
|
10
|
+
initDb,
|
|
11
|
+
startTask,
|
|
12
|
+
} from "../be/db";
|
|
13
|
+
import { registerStoreProgressTool } from "../tools/store-progress";
|
|
14
|
+
|
|
15
|
+
interface ToolResult {
|
|
16
|
+
isError?: boolean;
|
|
17
|
+
structuredContent?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const agentId = "dddd0000-0000-4000-8000-000000000b01";
|
|
21
|
+
|
|
22
|
+
const server = new McpServer({ name: "store-progress-learning", version: "1.0.0" });
|
|
23
|
+
registerStoreProgressTool(server);
|
|
24
|
+
const tool = (
|
|
25
|
+
server as unknown as {
|
|
26
|
+
_registeredTools: Record<
|
|
27
|
+
string,
|
|
28
|
+
{
|
|
29
|
+
inputSchema: ZodType;
|
|
30
|
+
handler: (args: unknown, meta: unknown) => Promise<ToolResult>;
|
|
31
|
+
}
|
|
32
|
+
>;
|
|
33
|
+
}
|
|
34
|
+
)._registeredTools["store-progress"]!;
|
|
35
|
+
|
|
36
|
+
async function call(args: Record<string, unknown>) {
|
|
37
|
+
return tool.handler(tool.inputSchema.parse(args), {
|
|
38
|
+
sessionId: "store-progress-learning-session",
|
|
39
|
+
requestInfo: { headers: { "x-agent-id": agentId } },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function task(vcsRepo?: string) {
|
|
44
|
+
const row = await createTaskExtended("learning capture", {
|
|
45
|
+
agentId,
|
|
46
|
+
source: "system",
|
|
47
|
+
followUpConfig: { disabled: true },
|
|
48
|
+
...(vcsRepo ? { vcsRepo } : {}),
|
|
49
|
+
});
|
|
50
|
+
await startTask(row.id);
|
|
51
|
+
return row;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface MemoryRow {
|
|
55
|
+
id: string;
|
|
56
|
+
agentId: string | null;
|
|
57
|
+
key: string;
|
|
58
|
+
name: string;
|
|
59
|
+
scope: string;
|
|
60
|
+
source: string;
|
|
61
|
+
sourceTaskId: string;
|
|
62
|
+
tags: string;
|
|
63
|
+
content: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function learningRows(taskId: string): MemoryRow[] {
|
|
67
|
+
return getDb()
|
|
68
|
+
.query<MemoryRow, [string]>(
|
|
69
|
+
"SELECT id, agentId, key, name, scope, source, sourceTaskId, tags, content FROM agent_memory WHERE sourceTaskId = ? AND key LIKE '/learnings/%' ORDER BY createdAt",
|
|
70
|
+
)
|
|
71
|
+
.all(taskId);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function learningData(result: ToolResult) {
|
|
75
|
+
return result.structuredContent?.learning as
|
|
76
|
+
| { status: string; key?: string; error?: string }
|
|
77
|
+
| undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function message(result: ToolResult) {
|
|
81
|
+
return String(result.structuredContent?.message);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const originalEmbeddingKey = process.env.EMBEDDING_API_KEY;
|
|
85
|
+
const originalOpenAiKey = process.env.OPENAI_API_KEY;
|
|
86
|
+
beforeAll(async () => {
|
|
87
|
+
process.env.EMBEDDING_API_KEY = "";
|
|
88
|
+
process.env.OPENAI_API_KEY = "";
|
|
89
|
+
initDb(":memory:");
|
|
90
|
+
await createAgent({ id: agentId, name: "Learner", isLead: false, status: "idle" });
|
|
91
|
+
});
|
|
92
|
+
afterAll(() => {
|
|
93
|
+
closeDb();
|
|
94
|
+
if (originalEmbeddingKey === undefined) delete process.env.EMBEDDING_API_KEY;
|
|
95
|
+
else process.env.EMBEDDING_API_KEY = originalEmbeddingKey;
|
|
96
|
+
if (originalOpenAiKey === undefined) delete process.env.OPENAI_API_KEY;
|
|
97
|
+
else process.env.OPENAI_API_KEY = originalOpenAiKey;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("store-progress learning", () => {
|
|
101
|
+
test("stores a swarm manual memory under the explicit workstream", async () => {
|
|
102
|
+
const t = await task("desplega-ai/agent-swarm");
|
|
103
|
+
const result = await call({
|
|
104
|
+
taskId: t.id,
|
|
105
|
+
progress: "found the cause",
|
|
106
|
+
learning: {
|
|
107
|
+
text: "SQLite BUSY needs BEGIN IMMEDIATE\nA deferred BEGIN that reads first fails with BUSY_SNAPSHOT.",
|
|
108
|
+
workstream: "DB Runtime",
|
|
109
|
+
tags: ["SQLite", "gotcha", "learning"],
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(result.isError).toBeFalsy();
|
|
114
|
+
expect(learningData(result)?.status).toBe("stored");
|
|
115
|
+
expect(message(result)).toContain(
|
|
116
|
+
"Learning stored at /learnings/db-runtime/sqlite-busy-needs-begin-immediate.",
|
|
117
|
+
);
|
|
118
|
+
const [row, ...rest] = learningRows(t.id);
|
|
119
|
+
expect(rest).toHaveLength(0);
|
|
120
|
+
expect(row).toMatchObject({
|
|
121
|
+
agentId,
|
|
122
|
+
key: "/learnings/db-runtime/sqlite-busy-needs-begin-immediate",
|
|
123
|
+
name: "SQLite BUSY needs BEGIN IMMEDIATE",
|
|
124
|
+
scope: "swarm",
|
|
125
|
+
source: "manual",
|
|
126
|
+
sourceTaskId: t.id,
|
|
127
|
+
});
|
|
128
|
+
expect(JSON.parse(row!.tags)).toEqual(["learning", "sqlite", "gotcha"]);
|
|
129
|
+
expect((await getTaskById(t.id))?.progress).toBe("found the cause");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("string form falls back to the repo name, then to general", async () => {
|
|
133
|
+
const withRepo = await task("desplega-ai/agent-swarm");
|
|
134
|
+
await call({ taskId: withRepo.id, learning: "Run lint before tsc" });
|
|
135
|
+
expect(learningRows(withRepo.id)[0]?.key).toBe("/learnings/agent-swarm/run-lint-before-tsc");
|
|
136
|
+
|
|
137
|
+
const withoutRepo = await task();
|
|
138
|
+
const result = await call({ taskId: withoutRepo.id, learning: { text: "Prefer scripts" } });
|
|
139
|
+
expect(learningData(result)).toMatchObject({ status: "stored", workstream: "general" });
|
|
140
|
+
expect(learningRows(withoutRepo.id)[0]?.key).toBe("/learnings/general/prefer-scripts");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("a taken key gets a short suffix instead of failing", async () => {
|
|
144
|
+
const first = await task();
|
|
145
|
+
const second = await task();
|
|
146
|
+
await call({ taskId: first.id, learning: "Same title\nfirst body" });
|
|
147
|
+
const result = await call({ taskId: second.id, learning: "Same title\nsecond body" });
|
|
148
|
+
|
|
149
|
+
expect(learningData(result)?.status).toBe("stored");
|
|
150
|
+
expect(learningRows(first.id)[0]?.key).toBe("/learnings/general/same-title");
|
|
151
|
+
expect(learningRows(second.id)[0]?.key).toMatch(
|
|
152
|
+
/^\/learnings\/general\/same-title-[0-9a-f]{6}$/,
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("an exact replay on the same task is skipped", async () => {
|
|
157
|
+
const t = await task();
|
|
158
|
+
await call({ taskId: t.id, learning: "Replay me" });
|
|
159
|
+
const replay = await call({ taskId: t.id, progress: "again", learning: "Replay me" });
|
|
160
|
+
|
|
161
|
+
expect(learningData(replay)).toMatchObject({
|
|
162
|
+
status: "replayed",
|
|
163
|
+
key: "/learnings/general/replay-me",
|
|
164
|
+
});
|
|
165
|
+
expect(learningRows(t.id)).toHaveLength(1);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("overlapping calls with the same learning store it once", async () => {
|
|
169
|
+
const t = await task();
|
|
170
|
+
const learning = "Concurrent replay\nKeep exactly one copy of this lesson.";
|
|
171
|
+
const results = await Promise.all([
|
|
172
|
+
call({ taskId: t.id, learning }),
|
|
173
|
+
call({ taskId: t.id, learning }),
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
expect(learningRows(t.id)).toHaveLength(1);
|
|
177
|
+
expect(results.map((result) => learningData(result)?.status).sort()).toEqual([
|
|
178
|
+
"replayed",
|
|
179
|
+
"stored",
|
|
180
|
+
]);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Synthetic token, built at runtime so no literal credential sits in the repo.
|
|
184
|
+
const token = `ghp_${"abcd1234".repeat(5)}`;
|
|
185
|
+
const kebabToken = token.replace("_", "-");
|
|
186
|
+
|
|
187
|
+
test("a secret in the title, workstream or tags never reaches the key or tags", async () => {
|
|
188
|
+
const t = await task();
|
|
189
|
+
const result = await call({
|
|
190
|
+
taskId: t.id,
|
|
191
|
+
learning: {
|
|
192
|
+
text: `${token}\nKeep credentials out of memory.`,
|
|
193
|
+
workstream: token,
|
|
194
|
+
tags: [token],
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(learningData(result)?.status).toBe("stored");
|
|
199
|
+
const [row] = learningRows(t.id);
|
|
200
|
+
for (const field of [row!.key, row!.name, row!.tags, row!.content, learningData(result)?.key]) {
|
|
201
|
+
expect(field).not.toContain(token);
|
|
202
|
+
expect(field).not.toContain(kebabToken);
|
|
203
|
+
}
|
|
204
|
+
expect(row!.content).toContain("[REDACTED:");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("a replay of a secret-bearing learning is still detected", async () => {
|
|
208
|
+
const t = await task();
|
|
209
|
+
const learning = `Redact diagnostic tokens\nExample token: ${token}`;
|
|
210
|
+
await call({ taskId: t.id, learning });
|
|
211
|
+
const replay = await call({ taskId: t.id, learning });
|
|
212
|
+
|
|
213
|
+
expect(learningData(replay)?.status).toBe("replayed");
|
|
214
|
+
expect(learningRows(t.id)).toHaveLength(1);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("a failed learning write leaves the progress write intact", async () => {
|
|
218
|
+
const t = await task();
|
|
219
|
+
getDb().run(
|
|
220
|
+
"CREATE TEMP TRIGGER fail_learning BEFORE INSERT ON agent_memory WHEN NEW.key LIKE '/learnings/%' BEGIN SELECT RAISE(ABORT, 'learning insert refused'); END",
|
|
221
|
+
);
|
|
222
|
+
try {
|
|
223
|
+
const result = await call({
|
|
224
|
+
taskId: t.id,
|
|
225
|
+
progress: "progress survives",
|
|
226
|
+
status: "completed",
|
|
227
|
+
output: "done",
|
|
228
|
+
learning: "This write fails",
|
|
229
|
+
});
|
|
230
|
+
expect(result.isError).toBeFalsy();
|
|
231
|
+
expect(learningData(result)).toMatchObject({ status: "failed" });
|
|
232
|
+
expect(learningData(result)?.error).toContain("learning insert refused");
|
|
233
|
+
expect(message(result)).toContain("Learning not stored:");
|
|
234
|
+
} finally {
|
|
235
|
+
getDb().run("DROP TRIGGER fail_learning");
|
|
236
|
+
}
|
|
237
|
+
const stored = await getTaskById(t.id);
|
|
238
|
+
expect(stored?.progress).toBe("progress survives");
|
|
239
|
+
expect(stored?.status).toBe("completed");
|
|
240
|
+
expect(learningRows(t.id)).toHaveLength(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("invalid learnings are reported without blocking progress", async () => {
|
|
244
|
+
const t = await task();
|
|
245
|
+
const tooManyTags = await call({
|
|
246
|
+
taskId: t.id,
|
|
247
|
+
progress: "still stored",
|
|
248
|
+
learning: { text: "Tagged", tags: ["a", "b", "c", "d", "e", "f"] },
|
|
249
|
+
});
|
|
250
|
+
expect(learningData(tooManyTags)).toMatchObject({
|
|
251
|
+
status: "failed",
|
|
252
|
+
error: "learning takes at most 5 tags",
|
|
253
|
+
});
|
|
254
|
+
const tooLong = await call({ taskId: t.id, learning: "x".repeat(2001) });
|
|
255
|
+
expect(learningData(tooLong)?.status).toBe("failed");
|
|
256
|
+
expect((await getTaskById(t.id))?.progress).toBe("still stored");
|
|
257
|
+
expect(learningRows(t.id)).toHaveLength(0);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("a learning on the completing call is stored", async () => {
|
|
261
|
+
const t = await task();
|
|
262
|
+
const result = await call({
|
|
263
|
+
taskId: t.id,
|
|
264
|
+
status: "completed",
|
|
265
|
+
output: "done",
|
|
266
|
+
learning: "Ship on the completing call",
|
|
267
|
+
});
|
|
268
|
+
expect(learningData(result)?.status).toBe("stored");
|
|
269
|
+
expect((await getTaskById(t.id))?.status).toBe("completed");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("a call on a finished task stores nothing", async () => {
|
|
273
|
+
const t = await task();
|
|
274
|
+
await call({ taskId: t.id, status: "completed", output: "done" });
|
|
275
|
+
const late = await call({
|
|
276
|
+
taskId: t.id,
|
|
277
|
+
status: "completed",
|
|
278
|
+
output: "done",
|
|
279
|
+
learning: "Too late",
|
|
280
|
+
});
|
|
281
|
+
expect(late.structuredContent).toMatchObject({ wasNoOp: true });
|
|
282
|
+
expect(learningData(late)?.status).toBe("failed");
|
|
283
|
+
|
|
284
|
+
const progressOnly = await call({ taskId: t.id, learning: "Still too late" });
|
|
285
|
+
expect(learningData(progressOnly)?.status).toBe("failed");
|
|
286
|
+
expect(learningRows(t.id)).toHaveLength(0);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
createServer as createHttpServer,
|
|
5
|
+
type IncomingMessage,
|
|
6
|
+
type Server,
|
|
7
|
+
type ServerResponse,
|
|
8
|
+
} from "node:http";
|
|
9
|
+
import {
|
|
10
|
+
closeDb,
|
|
11
|
+
createAgent,
|
|
12
|
+
createTaskExtended,
|
|
13
|
+
createWorkflow,
|
|
14
|
+
createWorkflowRun,
|
|
15
|
+
getAllTasks,
|
|
16
|
+
getTasksCount,
|
|
17
|
+
initDb,
|
|
18
|
+
} from "../be/db";
|
|
19
|
+
import { handleCore } from "../http/core";
|
|
20
|
+
import { handleTasks } from "../http/tasks";
|
|
21
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
22
|
+
import { listenOnFreePort } from "./test-net";
|
|
23
|
+
|
|
24
|
+
const TEST_DB_PATH = "./test-tasks-workflow-run-filter.sqlite";
|
|
25
|
+
const API_KEY = "test-tasks-workflow-run-filter";
|
|
26
|
+
|
|
27
|
+
async function removeDbFiles(path: string): Promise<void> {
|
|
28
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
29
|
+
try {
|
|
30
|
+
await unlink(path + suffix);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createTestServer(apiKey: string): Server {
|
|
38
|
+
return createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
39
|
+
const myAgentId = req.headers["x-agent-id"] as string | undefined;
|
|
40
|
+
const handled = await handleCore(req, res, myAgentId, apiKey);
|
|
41
|
+
if (handled) return;
|
|
42
|
+
const pathSegments = getPathSegments(req.url || "");
|
|
43
|
+
const queryParams = parseQueryParams(req.url || "");
|
|
44
|
+
const ok = await handleTasks(req, res, pathSegments, queryParams, myAgentId);
|
|
45
|
+
if (!ok) {
|
|
46
|
+
res.writeHead(404);
|
|
47
|
+
res.end("Not Found");
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("workflowRunId task filter", () => {
|
|
53
|
+
let server: Server;
|
|
54
|
+
let port: number;
|
|
55
|
+
let agentId: string;
|
|
56
|
+
let runAId: string;
|
|
57
|
+
let runATaskIds: string[];
|
|
58
|
+
let runBTaskId: string;
|
|
59
|
+
let noRunTaskId: string;
|
|
60
|
+
|
|
61
|
+
beforeAll(async () => {
|
|
62
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
63
|
+
initDb(TEST_DB_PATH);
|
|
64
|
+
agentId = (
|
|
65
|
+
await createAgent({ name: "workflow-run-filter-agent", isLead: false, status: "idle" })
|
|
66
|
+
).id;
|
|
67
|
+
const workflow = await createWorkflow({
|
|
68
|
+
name: `workflow-run-filter-${crypto.randomUUID()}`,
|
|
69
|
+
definition: { nodes: [] },
|
|
70
|
+
});
|
|
71
|
+
const runA = await createWorkflowRun({ id: crypto.randomUUID(), workflowId: workflow.id });
|
|
72
|
+
const runB = await createWorkflowRun({ id: crypto.randomUUID(), workflowId: workflow.id });
|
|
73
|
+
runAId = runA.id;
|
|
74
|
+
runATaskIds = [
|
|
75
|
+
(
|
|
76
|
+
await createTaskExtended("run A — step 1", {
|
|
77
|
+
agentId,
|
|
78
|
+
source: "workflow",
|
|
79
|
+
workflowRunId: runA.id,
|
|
80
|
+
})
|
|
81
|
+
).id,
|
|
82
|
+
(
|
|
83
|
+
await createTaskExtended("run A — step 2", {
|
|
84
|
+
agentId,
|
|
85
|
+
source: "workflow",
|
|
86
|
+
workflowRunId: runA.id,
|
|
87
|
+
})
|
|
88
|
+
).id,
|
|
89
|
+
];
|
|
90
|
+
runBTaskId = (
|
|
91
|
+
await createTaskExtended("run B — step 1", {
|
|
92
|
+
agentId,
|
|
93
|
+
source: "workflow",
|
|
94
|
+
workflowRunId: runB.id,
|
|
95
|
+
})
|
|
96
|
+
).id;
|
|
97
|
+
noRunTaskId = (await createTaskExtended("task outside any run", { agentId })).id;
|
|
98
|
+
|
|
99
|
+
server = createTestServer(API_KEY);
|
|
100
|
+
port = await listenOnFreePort(server);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
afterAll(async () => {
|
|
104
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
105
|
+
closeDb();
|
|
106
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
function listTasks(query: string): Promise<Response> {
|
|
110
|
+
return fetch(`http://localhost:${port}/api/tasks?${query}`, {
|
|
111
|
+
headers: { Authorization: `Bearer ${API_KEY}`, "X-Agent-ID": agentId },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
test("getAllTasks and getTasksCount return only the run's tasks", async () => {
|
|
116
|
+
const tasks = await getAllTasks({ workflowRunId: runAId });
|
|
117
|
+
expect(tasks.map((t) => t.id).sort()).toEqual([...runATaskIds].sort());
|
|
118
|
+
expect(await getTasksCount({ workflowRunId: runAId })).toBe(2);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
for (const fields of ["slim", "full", "timeline"] as const) {
|
|
122
|
+
test(`GET /api/tasks?workflowRunId with fields=${fields} returns only the run's tasks`, async () => {
|
|
123
|
+
const res = await listTasks(`workflowRunId=${runAId}&fields=${fields}&includeTotal=true`);
|
|
124
|
+
expect(res.status).toBe(200);
|
|
125
|
+
const body = (await res.json()) as { tasks: { id: string }[]; total: number };
|
|
126
|
+
const ids = body.tasks.map((t) => t.id);
|
|
127
|
+
expect(ids.sort()).toEqual([...runATaskIds].sort());
|
|
128
|
+
expect(ids).not.toContain(runBTaskId);
|
|
129
|
+
expect(ids).not.toContain(noRunTaskId);
|
|
130
|
+
expect(body.total).toBe(2);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
test("an unknown run returns an empty list and zero total", async () => {
|
|
135
|
+
const res = await listTasks(`workflowRunId=${crypto.randomUUID()}&includeTotal=true`);
|
|
136
|
+
expect(res.status).toBe(200);
|
|
137
|
+
const body = (await res.json()) as { tasks: unknown[]; total: number };
|
|
138
|
+
expect(body.tasks).toEqual([]);
|
|
139
|
+
expect(body.total).toBe(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("a non-UUID workflowRunId is rejected", async () => {
|
|
143
|
+
const res = await listTasks("workflowRunId=not-a-uuid");
|
|
144
|
+
expect(res.status).toBe(400);
|
|
145
|
+
});
|
|
146
|
+
});
|
package/src/tests/test-net.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* Always bind an OS-assigned port: `listenOnFreePort(server)` for an
|
|
7
7
|
* in-process `node:http` server, `getFreePort()` for a port that must be known
|
|
8
8
|
* before the server exists (spawned `src/http.ts` children take it via `PORT`).
|
|
9
|
+
* A spawned child can lose that port before it binds; `startOnFreePort` retries
|
|
10
|
+
* it on a new one.
|
|
9
11
|
*/
|
|
10
12
|
import { createServer, type Server } from "node:net";
|
|
11
13
|
|
|
@@ -27,6 +29,35 @@ export function getFreePort(): Promise<number> {
|
|
|
27
29
|
});
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
/** Matches the boot error a server prints when its port is already bound. */
|
|
33
|
+
export const PORT_IN_USE_LOG = /EADDRINUSE|Is port \d+ in use\?/;
|
|
34
|
+
|
|
35
|
+
/** Thrown by a `startOnFreePort` callback when the server lost its port to another process. */
|
|
36
|
+
export class PortInUseError extends Error {}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Start a server on a getFreePort() port, and on a new port when `start`
|
|
40
|
+
* throws PortInUseError. getFreePort() releases the port before the spawned
|
|
41
|
+
* server binds it, and under `bun test --parallel` another process can take
|
|
42
|
+
* it during the server's boot. Any other error, or the last attempt's
|
|
43
|
+
* PortInUseError, propagates unchanged.
|
|
44
|
+
*/
|
|
45
|
+
export async function startOnFreePort<T>(
|
|
46
|
+
start: (port: number) => Promise<T>,
|
|
47
|
+
opts: { attempts?: number; getPort?: () => Promise<number> } = {},
|
|
48
|
+
): Promise<T> {
|
|
49
|
+
const attempts = opts.attempts ?? 3;
|
|
50
|
+
const getPort = opts.getPort ?? getFreePort;
|
|
51
|
+
for (let attempt = 1; ; attempt++) {
|
|
52
|
+
const port = await getPort();
|
|
53
|
+
try {
|
|
54
|
+
return await start(port);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (!(error instanceof PortInUseError) || attempt >= attempts) throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
30
61
|
/**
|
|
31
62
|
* Bind `server` to port 0 and return the assigned port. Binds all interfaces
|
|
32
63
|
* unless `host` is given, matching a bare `server.listen(0)`, so
|
|
@@ -207,6 +207,14 @@ describe("provider key fallback", () => {
|
|
|
207
207
|
});
|
|
208
208
|
});
|
|
209
209
|
|
|
210
|
+
test("a pooled OPENAI_API_KEY sends one member", async () => {
|
|
211
|
+
process.env.OPENAI_API_KEY = `${OPENAI_FAKE}-a,${OPENAI_FAKE}-b`;
|
|
212
|
+
const apiKey = resolveTranscriptionConfig()?.apiKey;
|
|
213
|
+
expect([`${OPENAI_FAKE}-a`, `${OPENAI_FAKE}-b`]).toContain(apiKey);
|
|
214
|
+
await transcribeAudio(audio);
|
|
215
|
+
expect(captured[0]?.authorization).toMatch(new RegExp(`^Bearer ${OPENAI_FAKE}-[ab]$`));
|
|
216
|
+
});
|
|
217
|
+
|
|
210
218
|
test("GROQ_API_KEY alone uses Groq's OpenAI-compatible endpoint and whisper model", async () => {
|
|
211
219
|
process.env.GROQ_API_KEY = GROQ_FAKE;
|
|
212
220
|
expect(resolveTranscriptionConfig()).toMatchObject({
|