@desplega.ai/agent-swarm 2.6.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/{acp-adapter-dgfakfpw.js → acp-adapter-8dn5tq56.js} +6 -6
- package/dist/{actions-fbqma8aa.js → actions-3r6gw7dy.js} +18 -18
- package/dist/{amp-adapter-en0jy6hs.js → amp-adapter-wjtq413p.js} +3 -3
- package/dist/{anthropic-messages-txs190dd.js → anthropic-messages-n4cby1rz.js} +4 -4
- package/dist/{app-k8a5cgpp.js → app-d53yhykk.js} +3 -3
- package/dist/{assistant-a16433gr.js → assistant-ksrawj3k.js} +20 -20
- package/dist/{boot-reembed-6c1sjfnr.js → boot-reembed-fxfw17m2.js} +11 -3
- package/dist/{boot-reembed-f4nn561n.js → boot-reembed-k5y5xbj0.js} +15 -8
- package/dist/{boot-scrub-sweep-d4w9gajf.js → boot-scrub-sweep-se41j9fd.js} +3 -3
- package/dist/{boot-seeds-9bmkspyw.js → boot-seeds-r3kwwv8b.js} +4 -4
- package/dist/{claude-adapter-nys1kjxa.js → claude-adapter-ahmyf231.js} +8 -8
- package/dist/{claude-managed-adapter-55207973.js → claude-managed-adapter-5knamegc.js} +3 -3
- package/dist/{claude-managed-setup-3cm35gey.js → claude-managed-setup-mdc7dy6b.js} +3 -3
- package/dist/{claude-sdk-session-nw5yd8cp.js → claude-sdk-session-n5wen9zk.js} +8729 -8600
- package/dist/{cli-683y0b3a.js → cli-0dqtv2jg.js} +2 -2
- package/dist/{cli-a34pr3w1.js → cli-0tww7dnz.js} +2 -2
- package/dist/{cli-w2656ka1.js → cli-0w4e4v58.js} +7 -7
- package/dist/{cli-5sckpqcx.js → cli-2sjm3t4r.js} +1 -1
- package/dist/{cli-gf2850q7.js → cli-3cywb44q.js} +18 -1
- package/dist/{cli-8wkb09q5.js → cli-3k9jej0c.js} +2 -2
- package/dist/{cli-3aqd4jaa.js → cli-3v2cyqb8.js} +3 -3
- package/dist/{cli-emhjcj2y.js → cli-5qt9vzmq.js} +89 -20
- package/dist/{cli-abkm71vr.js → cli-5rp1w4cr.js} +1 -1
- package/dist/{cli-gf95q3bm.js → cli-6vwjsetx.js} +4 -4
- package/dist/{cli-k21gx5ht.js → cli-6w2yrrc0.js} +10 -10
- package/dist/{cli-m70bajzr.js → cli-75tdydgv.js} +1 -1
- package/dist/{cli-5rj83bam.js → cli-8feswf2v.js} +47 -36
- package/dist/{cli-sxgtf5fw.js → cli-8wd3mbyj.js} +1 -1
- package/dist/{cli-ca6banwz.js → cli-9csfthbw.js} +10 -8
- package/dist/{cli-dqygeeh5.js → cli-b41p764a.js} +31 -3
- package/dist/{cli-m5a9m0ak.js → cli-c1t533hh.js} +3 -3
- package/dist/{cli-2sjhpsns.js → cli-c28jxpcv.js} +1 -1
- package/dist/{cli-bqztfyve.js → cli-cb50g9cd.js} +1 -1
- package/dist/{cli-ag2yrnt5.js → cli-d8gaphtr.js} +2 -2
- package/dist/{cli-9abdgn5t.js → cli-dgtfysne.js} +3 -3
- package/dist/{cli-nq677csy.js → cli-dscnhn18.js} +1 -1
- package/dist/{cli-3fwqy71j.js → cli-dv11pgpv.js} +39 -32
- package/dist/{cli-v49fkghp.js → cli-ekptgk2m.js} +6 -6
- package/dist/{cli-c2habvsf.js → cli-f3mzy662.js} +17 -17
- package/dist/{cli-zqne1qcj.js → cli-fk7k8s9x.js} +2 -2
- package/dist/{cli-1tem6ns9.js → cli-gt4h8cxw.js} +3 -2
- package/dist/{cli-vbhgkw9h.js → cli-h3e7bhvn.js} +3 -3
- package/dist/{cli-ffvsj2qw.js → cli-hkxqdmnb.js} +3 -3
- package/dist/{cli-a2rr0fmg.js → cli-hpkvgdad.js} +7 -4
- package/dist/{cli-art7c1zh.js → cli-hq2v4rr5.js} +37 -9
- package/dist/{cli-sqma6vw1.js → cli-k1zb7raw.js} +5 -5
- package/dist/{cli-4rvr7zfx.js → cli-k4awgvzm.js} +5 -0
- package/dist/{cli-3ndmpxxv.js → cli-k7d3hvya.js} +2 -2
- package/dist/{cli-3gfvmzht.js → cli-kbtdftbw.js} +74 -26
- package/dist/{cli-hfeb68z4.js → cli-kj69b9tw.js} +34 -6
- package/dist/{cli-az25fn5f.js → cli-kk9jy4nm.js} +2 -2
- package/dist/{cli-39aet655.js → cli-m52t4cta.js} +1 -1
- package/dist/{cli-cz37jqss.js → cli-mdadewqp.js} +5 -5
- package/dist/{cli-esakngh8.js → cli-nf6k60sn.js} +1 -1
- package/dist/{cli-j6rktc91.js → cli-nzdf09js.js} +1 -1
- package/dist/{cli-rr7zx19j.js → cli-prcyzecg.js} +12 -12
- package/dist/{cli-4hxrf0p4.js → cli-pzafyqtk.js} +393 -62
- package/dist/{cli-g8ka5j4j.js → cli-qvaa1gjp.js} +173 -52
- package/dist/{cli-d2dpf95n.js → cli-rg4b2g6j.js} +1 -1
- package/dist/{cli-kf4jree9.js → cli-rm66vqdz.js} +1 -1
- package/dist/{cli-4rf0f5vg.js → cli-s504c3w7.js} +5 -3
- package/dist/{cli-nn6vx2mq.js → cli-swkx9jm2.js} +7 -5
- package/dist/{cli-g20jjwt4.js → cli-thsaq4pd.js} +16 -16
- package/dist/{cli-aj8djyqt.js → cli-twjnyvs8.js} +2 -2
- package/dist/{cli-03gg9pd1.js → cli-typdpe8g.js} +4 -4
- package/dist/{cli-mtnj4fwa.js → cli-vg8pwmsd.js} +6 -6
- package/dist/{cli-gnsb1zxa.js → cli-vhpckzvp.js} +1 -1
- package/dist/{cli-7enjwya9.js → cli-vz78s5wm.js} +1 -1
- package/dist/{cli-dv97kdzv.js → cli-wdxgsdz5.js} +2 -2
- package/dist/{cli-7a6p4e65.js → cli-wtjg6je3.js} +1 -1
- package/dist/{cli-6y51ryva.js → cli-x3cngb1h.js} +44 -12
- package/dist/{cli-n7asmce6.js → cli-xbz6mcg4.js} +3 -3
- package/dist/{cli-24j1jeeb.js → cli-y4tdxsqb.js} +3 -3
- package/dist/{cli-mvjz5an3.js → cli-yqrsp3ny.js} +7 -7
- package/dist/cli.js +31 -21
- package/dist/{codex-adapter-d95q4qsn.js → codex-adapter-7wepzq15.js} +6 -6
- package/dist/{codex-hook-brpnycn5.js → codex-hook-pbdh4xhx.js} +2 -2
- package/dist/{codex-session-runner-gdwd8h0c.js → codex-session-runner-02sb49e4.js} +6 -6
- package/dist/{commands-jqmgk54b.js → commands-3rcspxct.js} +2 -2
- package/dist/{controller-z3jq589f.js → controller-8ar4avd6.js} +3 -3
- package/dist/{controllers-connect-r2za39ef.js → controllers-connect-ajvh9h0v.js} +19 -19
- package/dist/{cursor-adapter-4mr4jgkw.js → cursor-adapter-gd0sa7bh.js} +1 -1
- package/dist/{db-w36wwb5g.js → db-dv9j7ts5.js} +2 -2
- package/dist/{db-ppv2tze7.js → db-z26vgdvj.js} +3 -3
- package/dist/{dispatcher-pv5h77vf.js → dispatcher-8m0htjq8.js} +17 -17
- package/dist/{dsh-adapter-d4pdpb3r.js → dsh-adapter-y8gyk130.js} +1 -1
- package/dist/{e2b-fn3zacvt.js → e2b-74vg9b7e.js} +1 -1
- package/dist/{e2b-222hsajc.js → e2b-jyay8wzn.js} +1 -1
- package/dist/{easter-egg-3d-4sp4dvmc.js → easter-egg-3d-28f880xx.js} +1 -1
- package/dist/{execute-jev839y1.js → execute-9seb79vh.js} +1 -1
- package/dist/{external-channel-y6nkzhnn.js → external-channel-smz7jyth.js} +2 -2
- package/dist/{getMachineId-bsd-brn0xym3.js → getMachineId-bsd-whfre6gr.js} +3 -3
- package/dist/{getMachineId-darwin-t339sfxm.js → getMachineId-darwin-5zjynb19.js} +3 -3
- package/dist/{getMachineId-linux-5eq95d8b.js → getMachineId-linux-8mksrrsq.js} +3 -3
- package/dist/{getMachineId-unsupported-qavrtc7j.js → getMachineId-unsupported-fxtqsdt2.js} +3 -3
- package/dist/{getMachineId-win-q7fe1wx7.js → getMachineId-win-nzfmz857.js} +3 -3
- package/dist/{grok-adapter-t08b9ntm.js → grok-adapter-z0nnvtbn.js} +7 -7
- package/dist/handlers-pspryh6g.js +41 -0
- package/dist/{heartbeat-vn31xwdk.js → heartbeat-f928aqyp.js} +20 -20
- package/dist/{hook-cpjs8q51.js → hook-d7c605r4.js} +7 -7
- package/dist/{hook-6d0zbvvt.js → hook-q9hv5xf9.js} +5 -5
- package/dist/{http-9c6medda.js → http-q6j4k989.js} +1000 -251
- package/dist/{index-k0yt1tvq.js → index-5nb266jr.js} +17 -17
- package/dist/{index-ysd45kgc.js → index-8fxt5eah.js} +10 -10
- package/dist/{index-zgvqzjsw.js → index-cekzwhez.js} +5 -5
- package/dist/{index-38t35h0n.js → index-ch0c1xff.js} +17 -17
- package/dist/{index-pkma1yf8.js → index-ebnjhxh1.js} +1 -1
- package/dist/{index-yqgbfgaj.js → index-ed8dy8rn.js} +20 -20
- package/dist/{index-qhjf2htm.js → index-ejqdkkqx.js} +262 -96
- package/dist/{index-dr94zbnh.js → index-farm1zsn.js} +32661 -29443
- package/dist/{index-g6k9tctj.js → index-yn1yxg85.js} +1 -1
- package/dist/{keepalive-em0117bp.js → keepalive-zzc3hks1.js} +3 -3
- package/dist/lead-c0pjcaw4.js +79 -0
- package/dist/{maintenance-kbme3qy9.js → maintenance-kqsxzpzx.js} +1 -1
- package/dist/{oauth-refresh-sweep-xysj4yse.js → oauth-refresh-sweep-7qt0g55z.js} +8 -3
- package/dist/{onboard-p6q0vz97.js → onboard-k7x13fqg.js} +121 -8
- package/dist/{opencode-adapter-esv3k9yn.js → opencode-adapter-tqvrtter.js} +5 -5
- package/dist/{otel-impl-54qhqh1z.js → otel-impl-sqkf6k3b.js} +97 -97
- package/dist/{pi-mono-adapter-mymbwhj0.js → pi-mono-adapter-m2qnafxm.js} +89 -14
- package/dist/{pricing-refresh-yb75wv33.js → pricing-refresh-wkpwhgvs.js} +3 -3
- package/dist/{rbac-roles-dexhc9x8.js → rbac-roles-dpn0c0wj.js} +7 -4
- package/dist/{rbac-roles-1vp2mw4p.js → rbac-roles-wk7xd6a6.js} +2 -2
- package/dist/{render-v2-tjay5hhf.js → render-v2-ex2m4g2k.js} +3 -3
- package/dist/{resume-8ayf37g5.js → resume-725g9b0b.js} +7 -7
- package/dist/{runtime-hqe5cqcd.js → runtime-kajas15g.js} +1 -1
- package/dist/{session-access-fdhp987s.js → session-access-0j2ezjje.js} +8 -4
- package/dist/{session-source-backfill-qgtc86z1.js → session-source-backfill-62202awe.js} +2 -2
- package/dist/{setup-4rrmqcq3.js → setup-9sj11ax6.js} +1 -1
- package/dist/{virtual-modules-52km00d6.js → virtual-modules-c6fcq6zr.js} +2 -2
- package/dist/{work-objects-p28fgd70.js → work-objects-0y07495y.js} +20 -20
- package/dist/worker-d4q9sqbc.js +79 -0
- package/openapi.json +1536 -407
- package/package.json +7 -5
- package/src/be/automation-preflight.ts +2 -0
- package/src/be/connector-codes.ts +65 -22
- package/src/be/db/api-keys.ts +9 -0
- package/src/be/db/workflow-runs.ts +36 -5
- package/src/be/db.ts +80 -22
- package/src/be/memory/boot-reembed.ts +4 -1
- package/src/be/memory/constants.ts +6 -0
- package/src/be/memory/embedding-recovery.ts +48 -0
- package/src/be/memory/index-content.ts +5 -2
- package/src/be/memory/providers/openai-embedding.ts +65 -14
- package/src/be/memory/task-learning.ts +257 -0
- package/src/be/memory/types.ts +13 -2
- package/src/be/migrations/245_script_executor.sql +3 -0
- package/src/be/migrations/246_connector_code_kind.sql +11 -0
- package/src/be/oauth-refresh-sweep.ts +2 -1
- package/src/be/onboarding.ts +125 -15
- package/src/be/rbac-roles.ts +4 -1
- package/src/be/scripts/boot-reembed.ts +2 -0
- package/src/be/scripts/db.ts +14 -4
- package/src/be/scripts/embeddings.ts +4 -2
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +9 -3
- package/src/be/seed/agent-fs-provision.ts +75 -14
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/swarm-config-guard.ts +3 -0
- package/src/cli.tsx +9 -0
- package/src/comb/config.ts +6 -1
- package/src/commands/onboard/agent-fs-version.ts +5 -0
- package/src/commands/onboard/compose-generator.ts +117 -4
- package/src/commands/onboard/env-generator.ts +9 -0
- package/src/commands/onboard/tei-image.ts +12 -0
- package/src/commands/onboard/types.ts +7 -0
- package/src/commands/onboard.tsx +11 -1
- package/src/commands/provider-credentials.ts +2 -1
- package/src/commands/runner.ts +14 -1
- package/src/heartbeat/heartbeat.ts +14 -1
- package/src/heartbeat/script-runner-probe.ts +47 -0
- package/src/http/agentfs-proxy.ts +369 -0
- package/src/http/all-routes.ts +1 -0
- package/src/http/api-keys.ts +24 -9
- package/src/http/codex-oauth-device.ts +8 -1
- package/src/http/fs.ts +64 -10
- package/src/http/idempotency-key.ts +1 -1
- package/src/http/inbox-state.ts +27 -1
- package/src/http/index.ts +7 -4
- package/src/http/memory.ts +7 -4
- package/src/http/onboarding.ts +139 -11
- package/src/http/pages-public.ts +38 -5
- package/src/http/scripts.ts +27 -0
- package/src/http/session-access.ts +24 -0
- package/src/http/sessions.ts +10 -6
- package/src/http/tasks.ts +85 -11
- package/src/http/telemetry-ui.ts +1 -1
- package/src/http/user-roles.ts +4 -1
- package/src/http/users.ts +272 -3
- package/src/http/utils.ts +22 -3
- package/src/http/workflows.ts +10 -0
- package/src/http/x.ts +5 -0
- package/src/prompts/base-prompt.ts +45 -10
- package/src/prompts/session-templates.ts +43 -5
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +6 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +99 -14
- package/src/rbac/legacy-policy.ts +1 -0
- package/src/rbac/permissions.ts +4 -0
- package/src/scheduler/scheduler.ts +2 -1
- 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 +9 -2
- package/src/scripts-runtime/types/stdlib.d.ts +9 -1
- package/src/scripts-runtime/types/swarm-sdk.d.ts +9 -1
- package/src/tests/agent-fs-provision-seeder.test.ts +261 -92
- package/src/tests/agentfs-proxy.test.ts +1106 -0
- package/src/tests/api-key-status-by-suffix.test.ts +242 -0
- package/src/tests/base-prompt.test.ts +139 -4
- package/src/tests/codex-adapter.test.ts +46 -8
- package/src/tests/codex-oauth-device.test.ts +12 -1
- package/src/tests/comb-config-guard.test.ts +20 -0
- package/src/tests/comb-links.test.ts +79 -1
- package/src/tests/comb-status.test.ts +44 -0
- package/src/tests/community-linear-delivery-loop.test.ts +2099 -0
- package/src/tests/credential-check.test.ts +4 -2
- 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/fetch-resolved-env.test.ts +16 -2
- package/src/tests/fixtures/onboard/baseline-states.ts +68 -0
- package/src/tests/fixtures/onboard/content.compose.yml.txt +190 -0
- package/src/tests/fixtures/onboard/content.env.txt +29 -0
- package/src/tests/fixtures/onboard/dev.compose.yml.txt +155 -0
- package/src/tests/fixtures/onboard/dev.env.txt +28 -0
- package/src/tests/fixtures/onboard/full-all.compose.yml.txt +582 -0
- package/src/tests/fixtures/onboard/full-all.env.txt +59 -0
- package/src/tests/fixtures/onboard/full.compose.yml.txt +435 -0
- package/src/tests/fixtures/onboard/full.env.txt +36 -0
- package/src/tests/fixtures/onboard/research.compose.yml.txt +155 -0
- package/src/tests/fixtures/onboard/research.env.txt +28 -0
- package/src/tests/fixtures/onboard/solo.compose.yml.txt +85 -0
- package/src/tests/fixtures/onboard/solo.env.txt +26 -0
- package/src/tests/heartbeat-checklist.test.ts +89 -0
- package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
- package/src/tests/http-log-scrubbing.test.ts +34 -1
- package/src/tests/http-task-write-ownership.test.ts +5 -1
- package/src/tests/invites-http.test.ts +464 -0
- package/src/tests/memory-embedding-input-cap.test.ts +122 -1
- package/src/tests/memory-embedding-recovery.test.ts +137 -0
- package/src/tests/oauth-refresh-sweep.test.ts +34 -1
- package/src/tests/onboard-compose.test.ts +162 -0
- package/src/tests/onboard-env.test.ts +24 -0
- package/src/tests/onboard-non-interactive.test.ts +29 -0
- package/src/tests/onboard-tei-image.test.ts +36 -0
- package/src/tests/onboarding.test.ts +451 -4
- package/src/tests/openai-embedding-provider.test.ts +105 -2
- package/src/tests/opencode-adapter.test.ts +2 -1
- 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-mono-session-cost-on-abort.test.ts +270 -0
- package/src/tests/pi-native-mcp.test.ts +13 -1
- package/src/tests/pi-tool-exposure.test.ts +8 -8
- package/src/tests/preload-env.test.ts +4 -0
- package/src/tests/preload.ts +4 -0
- package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
- package/src/tests/prompt-template-session.test.ts +20 -3
- package/src/tests/rbac-admission-e2e.test.ts +22 -6
- package/src/tests/rbac-admission.test.ts +342 -4
- package/src/tests/rbac-engine.test.ts +1 -0
- package/src/tests/rbac-roles.test.ts +9 -1
- package/src/tests/request-composition-derive.test.ts +329 -0
- package/src/tests/script-connections.test.ts +2 -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/scripts-external-api.test.ts +45 -0
- package/src/tests/sdk-allowlist.test.ts +3 -2
- package/src/tests/self-improvement.test.ts +4 -4
- package/src/tests/send-task-requester-gate.test.ts +64 -3
- package/src/tests/sessions-move.test.ts +39 -1
- package/src/tests/store-progress-learning.test.ts +381 -0
- package/src/tests/ui-logs-parser.test.ts +180 -0
- package/src/tests/whoami-route.test.ts +41 -0
- package/src/tests/workflow-engine-v2.test.ts +116 -1
- 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-swarm-script.test.ts +34 -0
- package/src/tools/inject-learning.ts +1 -1
- package/src/tools/memory-edit.ts +1 -1
- package/src/tools/memory-search.ts +1 -1
- 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 +71 -1
- package/src/tools/utils.ts +12 -1
- package/src/tools/workflows/trigger-workflow.ts +104 -10
- package/src/types.ts +12 -1
- package/src/utils/constants.ts +39 -0
- package/src/utils/credentials.ts +5 -2
- package/src/utils/task-attachment-links.ts +15 -3
- package/src/workflows/engine.ts +18 -16
- 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/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/swarm-docs/config.json +14 -0
- package/templates/skills/swarm-docs/content.md +144 -0
- package/dist/handlers-eqsnmv95.js +0 -41
- package/dist/lead-23e7z59t.js +0 -79
- package/dist/worker-e3w7vye1.js +0 -79
- package/dist/{azure-openai-responses-9kfwhmn7.js → azure-openai-responses-43asa23s.js} +0 -0
- package/dist/{node-vxx896fa.js → node-t28cc56b.js} +0 -0
- package/dist/{openai-codex-responses-79s8xnbh.js → openai-codex-responses-jhr3sjtm.js} +3 -3
- package/dist/{openai-completions-tdbyts0x.js → openai-completions-gs6yn37k.js} +3 -3
- /package/dist/{openai-responses-dm3t5vqp.js → openai-responses-jqm9vwwk.js} +0 -0
|
@@ -503,17 +503,17 @@ describe("Self-Improvement Mechanisms", () => {
|
|
|
503
503
|
const MEMORY_SKILL_POINTER =
|
|
504
504
|
"You MUST use the `memory` skill before you store, edit, or delete a memory.";
|
|
505
505
|
|
|
506
|
-
test("a worker prompt
|
|
506
|
+
test("a worker prompt points learnings at store-progress and names the memory skill", async () => {
|
|
507
507
|
const prompt = await getBasePrompt({ role: "worker", agentId: workerId });
|
|
508
508
|
|
|
509
|
-
expect(prompt).toContain("`
|
|
509
|
+
expect(prompt).toContain("as `learning` on a `store-progress` call");
|
|
510
510
|
expect(prompt).toContain(MEMORY_SKILL_POINTER);
|
|
511
511
|
});
|
|
512
512
|
|
|
513
|
-
test("a lead prompt
|
|
513
|
+
test("a lead prompt points learnings at store-progress and names the memory skill", async () => {
|
|
514
514
|
const prompt = await getBasePrompt({ role: "lead", agentId: leadId });
|
|
515
515
|
|
|
516
|
-
expect(prompt).toContain("`
|
|
516
|
+
expect(prompt).toContain("as `learning` on a `store-progress` call");
|
|
517
517
|
expect(prompt).toContain(MEMORY_SKILL_POINTER);
|
|
518
518
|
});
|
|
519
519
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* A task's requester drives attribution, cost reporting and admission. A non-lead agent may omit
|
|
3
3
|
* `requestedByUserId` or pass the requester of its own current task. Only a lead, the operator or
|
|
4
4
|
* a user may name any other registered user. Covers the send-task tool and POST /api/tasks.
|
|
5
|
-
* POST /api/tasks also refuses an agent a foreign parent or a chat thread it does not own
|
|
5
|
+
* POST /api/tasks also refuses an agent a foreign parent or a chat thread it does not own, and
|
|
6
|
+
* refuses a non-admin user a chat thread.
|
|
6
7
|
*/
|
|
7
8
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
8
9
|
import { unlink } from "node:fs/promises";
|
|
@@ -16,8 +17,10 @@ import {
|
|
|
16
17
|
createUser,
|
|
17
18
|
getDbClient,
|
|
18
19
|
getTaskById,
|
|
20
|
+
getUserById,
|
|
19
21
|
initDb,
|
|
20
22
|
} from "../be/db";
|
|
23
|
+
import { attachRole, detachRole } from "../be/rbac-roles";
|
|
21
24
|
import { handleTasks } from "../http/tasks";
|
|
22
25
|
import { chatSdkContextKey } from "../tasks/context-key";
|
|
23
26
|
import { registerSendTaskTool } from "../tools/send-task";
|
|
@@ -70,7 +73,7 @@ const taskCount = async () =>
|
|
|
70
73
|
const KEYED_REQUEST: HttpRequestAuth = { kind: "operator", fingerprint: "test-key" };
|
|
71
74
|
|
|
72
75
|
async function postTask(
|
|
73
|
-
caller: { agentId?: string; sourceTaskId?: string },
|
|
76
|
+
caller: { agentId?: string; sourceTaskId?: string; userId?: string },
|
|
74
77
|
body: Record<string, unknown>,
|
|
75
78
|
): Promise<{ status: number; body: { id?: string; requestedByUserId?: string } }> {
|
|
76
79
|
const response = await fetch(`${routeBaseUrl}/api/tasks`, {
|
|
@@ -79,6 +82,7 @@ async function postTask(
|
|
|
79
82
|
"Content-Type": "application/json",
|
|
80
83
|
...(caller.agentId ? { "X-Agent-ID": caller.agentId } : {}),
|
|
81
84
|
...(caller.sourceTaskId ? { "X-Source-Task-Id": caller.sourceTaskId } : {}),
|
|
85
|
+
...(caller.userId ? { "X-Test-User-Id": caller.userId } : {}),
|
|
82
86
|
},
|
|
83
87
|
body: JSON.stringify({ task: "created over HTTP", ...body }),
|
|
84
88
|
});
|
|
@@ -114,7 +118,10 @@ beforeAll(async () => {
|
|
|
114
118
|
registerSendTaskTool(mcp);
|
|
115
119
|
|
|
116
120
|
routeServer = createServer(async (req, res) => {
|
|
117
|
-
|
|
121
|
+
// A test user id header stands in for an aswt_ user token.
|
|
122
|
+
const testUserId = req.headers["x-test-user-id"] as string | undefined;
|
|
123
|
+
const user = testUserId ? await getUserById(testUserId) : null;
|
|
124
|
+
setRequestAuth(req, user ? { kind: "user", userId: user.id, user } : KEYED_REQUEST);
|
|
118
125
|
const url = req.url ?? "/";
|
|
119
126
|
const handled = await handleTasks(
|
|
120
127
|
req,
|
|
@@ -316,3 +323,57 @@ describe("POST /api/tasks parent and chat-thread binding for an agent caller", (
|
|
|
316
323
|
).toBe(201);
|
|
317
324
|
});
|
|
318
325
|
});
|
|
326
|
+
|
|
327
|
+
describe("POST /api/tasks chat-thread and context binding for a user caller", () => {
|
|
328
|
+
test("a Requester cannot bind a new task to a chat thread by contextKey", async () => {
|
|
329
|
+
const requester = await createUser({ name: "Chat Requester", email: "chat-req@example.com" });
|
|
330
|
+
// A new user starts as admin. Make this one a Requester only.
|
|
331
|
+
await attachRole(requester.id, "requester");
|
|
332
|
+
await detachRole(requester.id, "admin");
|
|
333
|
+
const before = await taskCount();
|
|
334
|
+
const result = await postTask(
|
|
335
|
+
{ userId: requester.id },
|
|
336
|
+
{ source: "chat", contextKey: chatSdkContextKey("telegram:556666") },
|
|
337
|
+
);
|
|
338
|
+
expect(result.status).toBe(403);
|
|
339
|
+
expect(await taskCount()).toBe(before);
|
|
340
|
+
|
|
341
|
+
// A page session key is not a chat thread: the Requester keeps it.
|
|
342
|
+
const page = await postTask({ userId: requester.id }, { contextKey: "home:abc123" });
|
|
343
|
+
expect(page.status).toBe(201);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("a Requester cannot name the context of another user's open task", async () => {
|
|
347
|
+
const requester = await createUser({ name: "Context Requester", email: "ctx-req@example.com" });
|
|
348
|
+
await attachRole(requester.id, "requester");
|
|
349
|
+
await detachRole(requester.id, "admin");
|
|
350
|
+
await createTaskExtended("private case notes", {
|
|
351
|
+
agentId: LEAD_ID,
|
|
352
|
+
contextKey: "case:private-1",
|
|
353
|
+
requestedByUserId: userAId,
|
|
354
|
+
});
|
|
355
|
+
const before = await taskCount();
|
|
356
|
+
const foreign = await postTask({ userId: requester.id }, { contextKey: "case:private-1" });
|
|
357
|
+
expect(foreign.status).toBe(403);
|
|
358
|
+
expect(await taskCount()).toBe(before);
|
|
359
|
+
|
|
360
|
+
// The context of the Requester's own open task stays usable.
|
|
361
|
+
await createTaskExtended("my case notes", {
|
|
362
|
+
agentId: LEAD_ID,
|
|
363
|
+
contextKey: "case:mine-1",
|
|
364
|
+
requestedByUserId: requester.id,
|
|
365
|
+
});
|
|
366
|
+
const own = await postTask({ userId: requester.id }, { contextKey: "case:mine-1" });
|
|
367
|
+
expect(own.status).toBe(201);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("an admin user may bind a task to a chat thread", async () => {
|
|
371
|
+
const admin = await createUser({ name: "Chat Admin", email: "chat-admin@example.com" });
|
|
372
|
+
await attachRole(admin.id, "admin");
|
|
373
|
+
const result = await postTask(
|
|
374
|
+
{ userId: admin.id },
|
|
375
|
+
{ source: "chat", contextKey: chatSdkContextKey("telegram:557777") },
|
|
376
|
+
);
|
|
377
|
+
expect(result.status).toBe(201);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
@@ -16,10 +16,11 @@ import {
|
|
|
16
16
|
upsertInboxState,
|
|
17
17
|
} from "../be/db";
|
|
18
18
|
import * as sessionMoves from "../be/db/session-moves";
|
|
19
|
+
import { attachRole, detachRole, ensureRbacSeedsSynced } from "../be/rbac-roles";
|
|
19
20
|
import { setSessionShares } from "../be/session-shares";
|
|
20
21
|
import * as assets from "../http/assets";
|
|
21
22
|
import { handleAssets } from "../http/assets";
|
|
22
|
-
import { SESSION_READ_DENIED } from "../http/session-access";
|
|
23
|
+
import { NOT_YOUR_TASK_DENIED, SESSION_READ_DENIED } from "../http/session-access";
|
|
23
24
|
import { handleSessions } from "../http/sessions";
|
|
24
25
|
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
25
26
|
import { chatSdkContextKey } from "../tasks/context-key";
|
|
@@ -705,3 +706,40 @@ test("old route denies read-only sharees with and without cascade and admits wri
|
|
|
705
706
|
200,
|
|
706
707
|
);
|
|
707
708
|
});
|
|
709
|
+
|
|
710
|
+
// Regression: the session wall reads an ownerless root as everyone's, so with
|
|
711
|
+
// RBAC on a requester could re-key Slack, schedule and operator sessions.
|
|
712
|
+
test("with RBAC on, a requester cannot move an ownerless session; an admin user still can", async () => {
|
|
713
|
+
const previous = process.env.RBAC_ENABLED;
|
|
714
|
+
process.env.RBAC_ENABLED = "true";
|
|
715
|
+
try {
|
|
716
|
+
await ensureRbacSeedsSynced();
|
|
717
|
+
const requester = (await createUser({ name: "Move Requester" })).id;
|
|
718
|
+
await detachRole(requester, "admin");
|
|
719
|
+
await attachRole(requester, "requester");
|
|
720
|
+
const ownerless = await createTaskExtended("ownerless root", { key: "shared/slack/", agentId });
|
|
721
|
+
const own = await session("shared/requester-own/", requester);
|
|
722
|
+
|
|
723
|
+
const denied = await call(
|
|
724
|
+
"session-moves",
|
|
725
|
+
{ rootTaskIds: [ownerless.id, own.root.id], groupKey: "shared/requester-moved/" },
|
|
726
|
+
requester,
|
|
727
|
+
);
|
|
728
|
+
expect(denied.status).toBe(200);
|
|
729
|
+
expect(denied.body.moved).toEqual([own.root.id]);
|
|
730
|
+
expect(denied.body.skipped).toEqual([
|
|
731
|
+
{ rootTaskId: ownerless.id, reason: NOT_YOUR_TASK_DENIED },
|
|
732
|
+
]);
|
|
733
|
+
expect((await getTaskById(ownerless.id))?.key).toBe("shared/slack/");
|
|
734
|
+
|
|
735
|
+
const admin = await call(
|
|
736
|
+
"session-moves",
|
|
737
|
+
{ rootTaskIds: [ownerless.id], groupKey: "shared/admin-moved/" },
|
|
738
|
+
owner,
|
|
739
|
+
);
|
|
740
|
+
expect(admin.body.moved).toEqual([ownerless.id]);
|
|
741
|
+
} finally {
|
|
742
|
+
if (previous === undefined) delete process.env.RBAC_ENABLED;
|
|
743
|
+
else process.env.RBAC_ENABLED = previous;
|
|
744
|
+
}
|
|
745
|
+
});
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, type Mock, spyOn, 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 { getEmbeddingProvider, getMemoryStore } from "../be/memory";
|
|
14
|
+
import { EMBEDDING_DIMENSIONS } from "../be/memory/constants";
|
|
15
|
+
import { registerStoreProgressTool } from "../tools/store-progress";
|
|
16
|
+
|
|
17
|
+
interface ToolResult {
|
|
18
|
+
isError?: boolean;
|
|
19
|
+
structuredContent?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const agentId = "dddd0000-0000-4000-8000-000000000b01";
|
|
23
|
+
|
|
24
|
+
const server = new McpServer({ name: "store-progress-learning", version: "1.0.0" });
|
|
25
|
+
registerStoreProgressTool(server);
|
|
26
|
+
const tool = (
|
|
27
|
+
server as unknown as {
|
|
28
|
+
_registeredTools: Record<
|
|
29
|
+
string,
|
|
30
|
+
{
|
|
31
|
+
inputSchema: ZodType;
|
|
32
|
+
handler: (args: unknown, meta: unknown) => Promise<ToolResult>;
|
|
33
|
+
}
|
|
34
|
+
>;
|
|
35
|
+
}
|
|
36
|
+
)._registeredTools["store-progress"]!;
|
|
37
|
+
|
|
38
|
+
async function call(args: Record<string, unknown>) {
|
|
39
|
+
return tool.handler(tool.inputSchema.parse(args), {
|
|
40
|
+
sessionId: "store-progress-learning-session",
|
|
41
|
+
requestInfo: { headers: { "x-agent-id": agentId } },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function task(vcsRepo?: string) {
|
|
46
|
+
const row = await createTaskExtended("learning capture", {
|
|
47
|
+
agentId,
|
|
48
|
+
source: "system",
|
|
49
|
+
followUpConfig: { disabled: true },
|
|
50
|
+
...(vcsRepo ? { vcsRepo } : {}),
|
|
51
|
+
});
|
|
52
|
+
await startTask(row.id);
|
|
53
|
+
return row;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface MemoryRow {
|
|
57
|
+
id: string;
|
|
58
|
+
agentId: string | null;
|
|
59
|
+
key: string;
|
|
60
|
+
name: string;
|
|
61
|
+
scope: string;
|
|
62
|
+
source: string;
|
|
63
|
+
sourceTaskId: string;
|
|
64
|
+
tags: string;
|
|
65
|
+
content: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function learningRows(taskId: string): MemoryRow[] {
|
|
69
|
+
return getDb()
|
|
70
|
+
.query<MemoryRow, [string]>(
|
|
71
|
+
"SELECT id, agentId, key, name, scope, source, sourceTaskId, tags, content FROM agent_memory WHERE sourceTaskId = ? AND key LIKE '/learnings/%' ORDER BY createdAt",
|
|
72
|
+
)
|
|
73
|
+
.all(taskId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function learningData(result: ToolResult) {
|
|
77
|
+
return result.structuredContent?.learning as
|
|
78
|
+
| { status: string; key?: string; memoryId?: string; similarity?: number; error?: string }
|
|
79
|
+
| undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function message(result: ToolResult) {
|
|
83
|
+
return String(result.structuredContent?.message);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const originalEmbeddingKey = process.env.EMBEDDING_API_KEY;
|
|
87
|
+
const originalOpenAiKey = process.env.OPENAI_API_KEY;
|
|
88
|
+
beforeAll(async () => {
|
|
89
|
+
process.env.EMBEDDING_API_KEY = "";
|
|
90
|
+
process.env.OPENAI_API_KEY = "";
|
|
91
|
+
initDb(":memory:");
|
|
92
|
+
await createAgent({ id: agentId, name: "Learner", isLead: false, status: "idle" });
|
|
93
|
+
});
|
|
94
|
+
afterAll(() => {
|
|
95
|
+
closeDb();
|
|
96
|
+
if (originalEmbeddingKey === undefined) delete process.env.EMBEDDING_API_KEY;
|
|
97
|
+
else process.env.EMBEDDING_API_KEY = originalEmbeddingKey;
|
|
98
|
+
if (originalOpenAiKey === undefined) delete process.env.OPENAI_API_KEY;
|
|
99
|
+
else process.env.OPENAI_API_KEY = originalOpenAiKey;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("store-progress learning", () => {
|
|
103
|
+
test("stores a swarm manual memory under the explicit workstream", async () => {
|
|
104
|
+
const t = await task("desplega-ai/agent-swarm");
|
|
105
|
+
const result = await call({
|
|
106
|
+
taskId: t.id,
|
|
107
|
+
progress: "found the cause",
|
|
108
|
+
learning: {
|
|
109
|
+
text: "SQLite BUSY needs BEGIN IMMEDIATE\nA deferred BEGIN that reads first fails with BUSY_SNAPSHOT.",
|
|
110
|
+
workstream: "DB Runtime",
|
|
111
|
+
tags: ["SQLite", "gotcha", "learning"],
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(result.isError).toBeFalsy();
|
|
116
|
+
expect(learningData(result)?.status).toBe("stored");
|
|
117
|
+
expect(message(result)).toContain(
|
|
118
|
+
"Learning stored at /learnings/db-runtime/sqlite-busy-needs-begin-immediate.",
|
|
119
|
+
);
|
|
120
|
+
const [row, ...rest] = learningRows(t.id);
|
|
121
|
+
expect(rest).toHaveLength(0);
|
|
122
|
+
expect(row).toMatchObject({
|
|
123
|
+
agentId,
|
|
124
|
+
key: "/learnings/db-runtime/sqlite-busy-needs-begin-immediate",
|
|
125
|
+
name: "SQLite BUSY needs BEGIN IMMEDIATE",
|
|
126
|
+
scope: "swarm",
|
|
127
|
+
source: "manual",
|
|
128
|
+
sourceTaskId: t.id,
|
|
129
|
+
});
|
|
130
|
+
expect(JSON.parse(row!.tags)).toEqual(["learning", "sqlite", "gotcha"]);
|
|
131
|
+
expect((await getTaskById(t.id))?.progress).toBe("found the cause");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("string form falls back to the repo name, then to general", async () => {
|
|
135
|
+
const withRepo = await task("desplega-ai/agent-swarm");
|
|
136
|
+
await call({ taskId: withRepo.id, learning: "Run lint before tsc" });
|
|
137
|
+
expect(learningRows(withRepo.id)[0]?.key).toBe("/learnings/agent-swarm/run-lint-before-tsc");
|
|
138
|
+
|
|
139
|
+
const withoutRepo = await task();
|
|
140
|
+
const result = await call({ taskId: withoutRepo.id, learning: { text: "Prefer scripts" } });
|
|
141
|
+
expect(learningData(result)).toMatchObject({ status: "stored", workstream: "general" });
|
|
142
|
+
expect(learningRows(withoutRepo.id)[0]?.key).toBe("/learnings/general/prefer-scripts");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("a taken key gets a short suffix instead of failing", async () => {
|
|
146
|
+
const first = await task();
|
|
147
|
+
const second = await task();
|
|
148
|
+
await call({ taskId: first.id, learning: "Same title\nfirst body" });
|
|
149
|
+
const result = await call({ taskId: second.id, learning: "Same title\nsecond body" });
|
|
150
|
+
|
|
151
|
+
expect(learningData(result)?.status).toBe("stored");
|
|
152
|
+
expect(learningRows(first.id)[0]?.key).toBe("/learnings/general/same-title");
|
|
153
|
+
expect(learningRows(second.id)[0]?.key).toMatch(
|
|
154
|
+
/^\/learnings\/general\/same-title-[0-9a-f]{6}$/,
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("an exact replay on the same task is skipped", async () => {
|
|
159
|
+
const t = await task();
|
|
160
|
+
await call({ taskId: t.id, learning: "Replay me" });
|
|
161
|
+
const replay = await call({ taskId: t.id, progress: "again", learning: "Replay me" });
|
|
162
|
+
|
|
163
|
+
expect(learningData(replay)).toMatchObject({
|
|
164
|
+
status: "replayed",
|
|
165
|
+
key: "/learnings/general/replay-me",
|
|
166
|
+
});
|
|
167
|
+
expect(learningRows(t.id)).toHaveLength(1);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("overlapping calls with the same learning store it once", async () => {
|
|
171
|
+
const t = await task();
|
|
172
|
+
const learning = "Concurrent replay\nKeep exactly one copy of this lesson.";
|
|
173
|
+
const results = await Promise.all([
|
|
174
|
+
call({ taskId: t.id, learning }),
|
|
175
|
+
call({ taskId: t.id, learning }),
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
expect(learningRows(t.id)).toHaveLength(1);
|
|
179
|
+
expect(results.map((result) => learningData(result)?.status).sort()).toEqual([
|
|
180
|
+
"replayed",
|
|
181
|
+
"stored",
|
|
182
|
+
]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Synthetic token, built at runtime so no literal credential sits in the repo.
|
|
186
|
+
const token = `ghp_${"abcd1234".repeat(5)}`;
|
|
187
|
+
const kebabToken = token.replace("_", "-");
|
|
188
|
+
|
|
189
|
+
test("a secret in the title, workstream or tags never reaches the key or tags", async () => {
|
|
190
|
+
const t = await task();
|
|
191
|
+
const result = await call({
|
|
192
|
+
taskId: t.id,
|
|
193
|
+
learning: {
|
|
194
|
+
text: `${token}\nKeep credentials out of memory.`,
|
|
195
|
+
workstream: token,
|
|
196
|
+
tags: [token],
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(learningData(result)?.status).toBe("stored");
|
|
201
|
+
const [row] = learningRows(t.id);
|
|
202
|
+
for (const field of [row!.key, row!.name, row!.tags, row!.content, learningData(result)?.key]) {
|
|
203
|
+
expect(field).not.toContain(token);
|
|
204
|
+
expect(field).not.toContain(kebabToken);
|
|
205
|
+
}
|
|
206
|
+
expect(row!.content).toContain("[REDACTED:");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("a replay of a secret-bearing learning is still detected", async () => {
|
|
210
|
+
const t = await task();
|
|
211
|
+
const learning = `Redact diagnostic tokens\nExample token: ${token}`;
|
|
212
|
+
await call({ taskId: t.id, learning });
|
|
213
|
+
const replay = await call({ taskId: t.id, learning });
|
|
214
|
+
|
|
215
|
+
expect(learningData(replay)?.status).toBe("replayed");
|
|
216
|
+
expect(learningRows(t.id)).toHaveLength(1);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("a failed learning write leaves the progress write intact", async () => {
|
|
220
|
+
const t = await task();
|
|
221
|
+
getDb().run(
|
|
222
|
+
"CREATE TEMP TRIGGER fail_learning BEFORE INSERT ON agent_memory WHEN NEW.key LIKE '/learnings/%' BEGIN SELECT RAISE(ABORT, 'learning insert refused'); END",
|
|
223
|
+
);
|
|
224
|
+
try {
|
|
225
|
+
const result = await call({
|
|
226
|
+
taskId: t.id,
|
|
227
|
+
progress: "progress survives",
|
|
228
|
+
status: "completed",
|
|
229
|
+
output: "done",
|
|
230
|
+
learning: "This write fails",
|
|
231
|
+
});
|
|
232
|
+
expect(result.isError).toBeFalsy();
|
|
233
|
+
expect(learningData(result)).toMatchObject({ status: "failed" });
|
|
234
|
+
expect(learningData(result)?.error).toContain("learning insert refused");
|
|
235
|
+
expect(message(result)).toContain("Learning not stored:");
|
|
236
|
+
} finally {
|
|
237
|
+
getDb().run("DROP TRIGGER fail_learning");
|
|
238
|
+
}
|
|
239
|
+
const stored = await getTaskById(t.id);
|
|
240
|
+
expect(stored?.progress).toBe("progress survives");
|
|
241
|
+
expect(stored?.status).toBe("completed");
|
|
242
|
+
expect(learningRows(t.id)).toHaveLength(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("invalid learnings are reported without blocking progress", async () => {
|
|
246
|
+
const t = await task();
|
|
247
|
+
const tooManyTags = await call({
|
|
248
|
+
taskId: t.id,
|
|
249
|
+
progress: "still stored",
|
|
250
|
+
learning: { text: "Tagged", tags: ["a", "b", "c", "d", "e", "f"] },
|
|
251
|
+
});
|
|
252
|
+
expect(learningData(tooManyTags)).toMatchObject({
|
|
253
|
+
status: "failed",
|
|
254
|
+
error: "learning takes at most 5 tags",
|
|
255
|
+
});
|
|
256
|
+
const tooLong = await call({ taskId: t.id, learning: "x".repeat(2001) });
|
|
257
|
+
expect(learningData(tooLong)?.status).toBe("failed");
|
|
258
|
+
expect((await getTaskById(t.id))?.progress).toBe("still stored");
|
|
259
|
+
expect(learningRows(t.id)).toHaveLength(0);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("a learning on the completing call is stored", async () => {
|
|
263
|
+
const t = await task();
|
|
264
|
+
const result = await call({
|
|
265
|
+
taskId: t.id,
|
|
266
|
+
status: "completed",
|
|
267
|
+
output: "done",
|
|
268
|
+
learning: "Ship on the completing call",
|
|
269
|
+
});
|
|
270
|
+
expect(learningData(result)?.status).toBe("stored");
|
|
271
|
+
expect((await getTaskById(t.id))?.status).toBe("completed");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("a call on a finished task stores nothing", async () => {
|
|
275
|
+
const t = await task();
|
|
276
|
+
await call({ taskId: t.id, status: "completed", output: "done" });
|
|
277
|
+
const late = await call({
|
|
278
|
+
taskId: t.id,
|
|
279
|
+
status: "completed",
|
|
280
|
+
output: "done",
|
|
281
|
+
learning: "Too late",
|
|
282
|
+
});
|
|
283
|
+
expect(late.structuredContent).toMatchObject({ wasNoOp: true });
|
|
284
|
+
expect(learningData(late)?.status).toBe("failed");
|
|
285
|
+
|
|
286
|
+
const progressOnly = await call({ taskId: t.id, learning: "Still too late" });
|
|
287
|
+
expect(learningData(progressOnly)?.status).toBe("failed");
|
|
288
|
+
expect(learningRows(t.id)).toHaveLength(0);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("store-progress learning near-duplicate guard", () => {
|
|
293
|
+
const vectors = new Map<string, Float32Array>();
|
|
294
|
+
let embedSpy: Mock<(text: string) => Promise<Float32Array | null>>;
|
|
295
|
+
|
|
296
|
+
/** Unit vector on `axis`, tilted toward `axis + 1` so its cosine to the bare axis is `cos`. */
|
|
297
|
+
function vector(axis: number, cos = 1): Float32Array {
|
|
298
|
+
const v = new Float32Array(EMBEDDING_DIMENSIONS);
|
|
299
|
+
v[axis] = cos;
|
|
300
|
+
v[axis + 1] = Math.sqrt(1 - cos ** 2);
|
|
301
|
+
return v;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Store a learning and give it its embedding now, not in the background. */
|
|
305
|
+
async function seed(text: string, embedding: Float32Array, workstream = "seeded") {
|
|
306
|
+
vectors.set(text, embedding);
|
|
307
|
+
const t = await task();
|
|
308
|
+
const result = await call({ taskId: t.id, learning: { text, workstream } });
|
|
309
|
+
const memoryId = learningData(result)!.memoryId!;
|
|
310
|
+
await getMemoryStore().updateEmbedding(memoryId, embedding, "test-embedding");
|
|
311
|
+
return { task: t, memoryId, key: learningData(result)!.key! };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
beforeAll(() => {
|
|
315
|
+
embedSpy = spyOn(getEmbeddingProvider(), "embed").mockImplementation(
|
|
316
|
+
async (text: string) => vectors.get(text) ?? null,
|
|
317
|
+
);
|
|
318
|
+
});
|
|
319
|
+
afterAll(() => embedSpy.mockRestore());
|
|
320
|
+
|
|
321
|
+
test("a near duplicate in another workstream is not stored and rates the existing row", async () => {
|
|
322
|
+
const existing = await seed("Upsert scripts from file bytes\nPOST the raw file.", vector(10));
|
|
323
|
+
const text = "Byte-exact script upsert from a local file\nPOST it to the API.";
|
|
324
|
+
vectors.set(text, vector(10, 0.9));
|
|
325
|
+
const t = await task();
|
|
326
|
+
const result = await call({ taskId: t.id, learning: { text, workstream: "other" } });
|
|
327
|
+
|
|
328
|
+
expect(learningData(result)).toMatchObject({
|
|
329
|
+
status: "duplicate",
|
|
330
|
+
memoryId: existing.memoryId,
|
|
331
|
+
key: existing.key,
|
|
332
|
+
});
|
|
333
|
+
expect(learningData(result)?.similarity).toBeCloseTo(0.9, 2);
|
|
334
|
+
expect(message(result)).toContain(`Learning not stored: it matches ${existing.key}`);
|
|
335
|
+
expect(String(result.structuredContent?.nudge)).toContain(`memory-edit that memory instead`);
|
|
336
|
+
expect(String(result.structuredContent?.nudge)).toContain(existing.memoryId);
|
|
337
|
+
expect(learningRows(t.id)).toHaveLength(0);
|
|
338
|
+
const ratings = getDb()
|
|
339
|
+
.query<{ source: string; signal: number; taskId: string }, [string]>(
|
|
340
|
+
"SELECT source, signal, taskId FROM memory_rating WHERE memoryId = ?",
|
|
341
|
+
)
|
|
342
|
+
.all(existing.memoryId);
|
|
343
|
+
expect(ratings).toEqual([{ source: "learning-duplicate", signal: 1, taskId: t.id }]);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("a related learning below the threshold is stored", async () => {
|
|
347
|
+
await seed("Retry flaky pushes once\nThe pre-push hook is slow.", vector(20));
|
|
348
|
+
const text = "Pre-push hooks take five minutes\nLet them finish.";
|
|
349
|
+
vectors.set(text, vector(20, 0.7));
|
|
350
|
+
const t = await task();
|
|
351
|
+
const result = await call({ taskId: t.id, learning: text });
|
|
352
|
+
|
|
353
|
+
expect(learningData(result)?.status).toBe("stored");
|
|
354
|
+
expect(learningRows(t.id)).toHaveLength(1);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("a failed search falls through to the normal insert", async () => {
|
|
358
|
+
await seed("Kebab keys only\nSlugs are lowercase.", vector(30));
|
|
359
|
+
const text = "Keys are kebab case\nSlugs stay lowercase.";
|
|
360
|
+
vectors.set(text, vector(30, 0.95));
|
|
361
|
+
const searchSpy = spyOn(getMemoryStore(), "search").mockRejectedValue(new Error("vec down"));
|
|
362
|
+
try {
|
|
363
|
+
const t = await task();
|
|
364
|
+
const result = await call({ taskId: t.id, progress: "kept", learning: text });
|
|
365
|
+
expect(learningData(result)?.status).toBe("stored");
|
|
366
|
+
expect(learningRows(t.id)).toHaveLength(1);
|
|
367
|
+
expect((await getTaskById(t.id))?.progress).toBe("kept");
|
|
368
|
+
} finally {
|
|
369
|
+
searchSpy.mockRestore();
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("an exact replay on the same task is replayed, not a duplicate", async () => {
|
|
374
|
+
const text = "Replay before similarity\nThe same task retrying is not a duplicate.";
|
|
375
|
+
const seeded = await seed(text, vector(40));
|
|
376
|
+
const replay = await call({ taskId: seeded.task.id, learning: { text, workstream: "seeded" } });
|
|
377
|
+
|
|
378
|
+
expect(learningData(replay)).toMatchObject({ status: "replayed", memoryId: seeded.memoryId });
|
|
379
|
+
expect(learningRows(seeded.task.id)).toHaveLength(1);
|
|
380
|
+
});
|
|
381
|
+
});
|