airaknit 1.1.2-rc.9
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/LICENSE +84 -0
- package/README.md +202 -0
- package/bin/airaknit +9 -0
- package/bin/airaknit-project +14 -0
- package/bin/kanna +9 -0
- package/dist/client/assets/CompactSummaryMessage-Yw0BDWEJ.js +1 -0
- package/dist/client/assets/ExitPlanModeMessage-DIdkQ4uF.js +1 -0
- package/dist/client/assets/LocalFilePreviewDialog-DQx2eiCc.js +3 -0
- package/dist/client/assets/LocalProjectsSection-C4xlWkgS.js +1 -0
- package/dist/client/assets/TextMessage-B5G39DEJ.js +1 -0
- package/dist/client/assets/UserMessage-CIkWk-0L.js +1 -0
- package/dist/client/assets/_basePickBy-CVrAFfnZ.js +1 -0
- package/dist/client/assets/_baseUniq-JL-aaF4P.js +1 -0
- package/dist/client/assets/arc-B07zg7ol.js +1 -0
- package/dist/client/assets/architecture-YZFGNWBL-PSLVJL3p.js +1 -0
- package/dist/client/assets/architectureDiagram-Q4EWVU46-DfWIF1G_.js +36 -0
- package/dist/client/assets/array-BGFCBI0e.js +1 -0
- package/dist/client/assets/blockDiagram-DXYQGD6D-CzwIeo_B.js +132 -0
- package/dist/client/assets/bundle-mjs-BDE2gWbQ.js +1 -0
- package/dist/client/assets/button-DO50qOGv.js +1 -0
- package/dist/client/assets/c4Diagram-AHTNJAMY-CR8DCQRE.js +10 -0
- package/dist/client/assets/channel-Dj-UUfaF.js +1 -0
- package/dist/client/assets/chunk-2KRD3SAO-dznP-cn8.js +1 -0
- package/dist/client/assets/chunk-336JU56O-Dqss5Vu6.js +2 -0
- package/dist/client/assets/chunk-426QAEUC-CEKis_0_.js +1 -0
- package/dist/client/assets/chunk-4BX2VUAB-BYOv0Gm1.js +1 -0
- package/dist/client/assets/chunk-4TB4RGXK-BxzubH5S.js +206 -0
- package/dist/client/assets/chunk-55IACEB6-BSlTj03a.js +1 -0
- package/dist/client/assets/chunk-5FUZZQ4R-9Au93Bi1.js +62 -0
- package/dist/client/assets/chunk-5PVQY5BW-BhksHFEZ.js +2 -0
- package/dist/client/assets/chunk-67CJDMHE-BFwhz-8t.js +1 -0
- package/dist/client/assets/chunk-7N4EOEYR-BDUPds87.js +1 -0
- package/dist/client/assets/chunk-AA7GKIK3-CEWTdyXO.js +1 -0
- package/dist/client/assets/chunk-BO2N2NFS-D0LvxnhU.js +103 -0
- package/dist/client/assets/chunk-BSJP7CBP-BNJnK6sq.js +1 -0
- package/dist/client/assets/chunk-Bj-mKKzh.js +1 -0
- package/dist/client/assets/chunk-CIAEETIT-CYhfoCeN.js +1 -0
- package/dist/client/assets/chunk-EDXVE4YY-C5ovJLc0.js +1 -0
- package/dist/client/assets/chunk-ENJZ2VHE-HOhYaeGr.js +10 -0
- package/dist/client/assets/chunk-FMBD7UC4-BLCiKcAQ.js +15 -0
- package/dist/client/assets/chunk-FOC6F5B3-B6GtY2ek.js +1 -0
- package/dist/client/assets/chunk-ICPOFSXX-DPoIZoC5.js +122 -0
- package/dist/client/assets/chunk-K5T4RW27-BsKN63rv.js +94 -0
- package/dist/client/assets/chunk-KGLVRYIC-BUGn9uuY.js +1 -0
- package/dist/client/assets/chunk-LIHQZDEY-DhaZyo03.js +1 -0
- package/dist/client/assets/chunk-ORNJ4GCN-DlpeeJyi.js +1 -0
- package/dist/client/assets/chunk-OYMX7WX6-Dc9q7aYA.js +231 -0
- package/dist/client/assets/chunk-QZHKN3VN-BEdrPoSb.js +1 -0
- package/dist/client/assets/chunk-U2HBQHQK-CIB3Bjjd.js +70 -0
- package/dist/client/assets/chunk-X2U36JSP-CtB-o8Yp.js +1 -0
- package/dist/client/assets/chunk-XPW4576I-C6iHhX_8.js +32 -0
- package/dist/client/assets/chunk-YZCP3GAM-CTmKr6ZH.js +1 -0
- package/dist/client/assets/chunk-ZZ45TVLE-BgU8A2RF.js +1 -0
- package/dist/client/assets/classDiagram-6PBFFD2Q-Bqk5e679.js +1 -0
- package/dist/client/assets/classDiagram-v2-HSJHXN6E-6pSaZOkC.js +1 -0
- package/dist/client/assets/client-BrKWI4CM.js +1 -0
- package/dist/client/assets/client-CGgNRU9w.js +1 -0
- package/dist/client/assets/client-DMSLRzg9.js +6 -0
- package/dist/client/assets/clone-DWcL7whJ.js +1 -0
- package/dist/client/assets/cose-bilkent-S5V4N54A-CrV5wsV_.js +1 -0
- package/dist/client/assets/cytoscape.esm--aLzKuep.js +321 -0
- package/dist/client/assets/dagre-CuRxWcrj.js +1 -0
- package/dist/client/assets/dagre-KV5264BT-BIDiVnkA.js +4 -0
- package/dist/client/assets/defaultLocale-CRZydyG6.js +1 -0
- package/dist/client/assets/diagram-5BDNPKRD-i1kjKRCB.js +10 -0
- package/dist/client/assets/diagram-G4DWMVQ6-9ZSLuhbl.js +24 -0
- package/dist/client/assets/diagram-MMDJMWI5-B4_CUjgv.js +43 -0
- package/dist/client/assets/diagram-TYMM5635-Ct5eTGS8.js +24 -0
- package/dist/client/assets/dist-CuB4kiSK.js +1 -0
- package/dist/client/assets/erDiagram-SMLLAGMA-Cy38ercc.js +85 -0
- package/dist/client/assets/flowDiagram-DWJPFMVM-CZKuYl0V.js +162 -0
- package/dist/client/assets/ganttDiagram-T4ZO3ILL-DLPjCh7a.js +292 -0
- package/dist/client/assets/gitGraph-7Q5UKJZL-DqbrtEp9.js +1 -0
- package/dist/client/assets/gitGraphDiagram-UUTBAWPF-BoRBkDhQ.js +106 -0
- package/dist/client/assets/graphlib-BcQ6qlQh.js +1 -0
- package/dist/client/assets/highlighted-body-OFNGDK62-BEpBVDTX.js +1 -0
- package/dist/client/assets/index-CetCiuqP.js +105 -0
- package/dist/client/assets/index-N29Mip7A.css +1 -0
- package/dist/client/assets/info-OMHHGYJF-D98DRBJX.js +1 -0
- package/dist/client/assets/infoDiagram-42DDH7IO-BAcdTWbt.js +2 -0
- package/dist/client/assets/init-B8gtcn7T.js +1 -0
- package/dist/client/assets/isArrayLikeObject-D8SJFmkN.js +1 -0
- package/dist/client/assets/isEmpty-BF3YX5Jk.js +1 -0
- package/dist/client/assets/ishikawaDiagram-UXIWVN3A-Ynu2VKdC.js +70 -0
- package/dist/client/assets/journeyDiagram-VCZTEJTY-BjfhQaN3.js +139 -0
- package/dist/client/assets/jsx-runtime-CyI9ICYU.js +1 -0
- package/dist/client/assets/kanban-definition-6JOO6SKY-JLXH9zUJ.js +89 -0
- package/dist/client/assets/katex-B94qP8b6.js +265 -0
- package/dist/client/assets/lib--QVjyxmL.js +29 -0
- package/dist/client/assets/lib-B6rgJiZ9.js +1 -0
- package/dist/client/assets/line-DCrYfLBn.js +1 -0
- package/dist/client/assets/linear-_4upLmeo.js +1 -0
- package/dist/client/assets/mermaid-GHXKKRXX-rwJHYUmW.js +1 -0
- package/dist/client/assets/mermaid-parser.core-KZinfW8o.js +4 -0
- package/dist/client/assets/mermaid.core-QqY9gSNe.js +11 -0
- package/dist/client/assets/mindmap-definition-QFDTVHPH-TWgHDAzp.js +96 -0
- package/dist/client/assets/ordinal-CCj7PWgZ.js +1 -0
- package/dist/client/assets/packet-4T2RLAQJ-DEvfkn3F.js +1 -0
- package/dist/client/assets/path-DZF-JdEe.js +1 -0
- package/dist/client/assets/pie-ZZUOXDRM-72e6WVjb.js +1 -0
- package/dist/client/assets/pieDiagram-DEJITSTG-Cl8PCsoj.js +30 -0
- package/dist/client/assets/preload-helper-rov5CBGT.js +1 -0
- package/dist/client/assets/pty-client-DZ27IS00.js +1 -0
- package/dist/client/assets/ptyInstancesStore-D9ag7SYd.js +1 -0
- package/dist/client/assets/quadrantDiagram-34T5L4WZ-CHyVGp9E.js +7 -0
- package/dist/client/assets/radar-PYXPWWZC-Cp7xd_EY.js +1 -0
- package/dist/client/assets/react-CClhXMB2.js +1 -0
- package/dist/client/assets/react-Dd6D81m0.js +1 -0
- package/dist/client/assets/react-dom--G6_6fQ_.js +1 -0
- package/dist/client/assets/requirementDiagram-MS252O5E-DaanG2iM.js +84 -0
- package/dist/client/assets/rough.esm-BsmKo2S5.js +1 -0
- package/dist/client/assets/sankeyDiagram-XADWPNL6-B_fhLY36.js +10 -0
- package/dist/client/assets/sequenceDiagram-FGHM5R23-C5FNrveI.js +157 -0
- package/dist/client/assets/src-DeTlMJAU.js +1 -0
- package/dist/client/assets/stateDiagram-FHFEXIEX-nTTcdjjQ.js +1 -0
- package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-Dw0632j_.js +1 -0
- package/dist/client/assets/timeline-definition-GMOUNBTQ-DkQV1yP8.js +120 -0
- package/dist/client/assets/treeView-SZITEDCU-ZLIgC7_K.js +1 -0
- package/dist/client/assets/treemap-W4RFUUIX-BqaMbB6N.js +1 -0
- package/dist/client/assets/uiIdentityOverlay-Ba7GNj7m.js +1 -0
- package/dist/client/assets/vennDiagram-DHZGUBPP-DbZ2xgs6.js +34 -0
- package/dist/client/assets/wardley-RL74JXVD-DXQS8zf4.js +1 -0
- package/dist/client/assets/wardleyDiagram-NUSXRM2D-BzCJ6MAu.js +20 -0
- package/dist/client/assets/xychartDiagram-5P7HB3ND-BSlFecop.js +7 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/favicon.svg +15 -0
- package/dist/client/fonts/body-medium.woff2 +0 -0
- package/dist/client/fonts/body-regular-italic.woff2 +0 -0
- package/dist/client/fonts/body-regular.woff2 +0 -0
- package/dist/client/fonts/body-semibold.woff2 +0 -0
- package/dist/client/index.html +31 -0
- package/dist/client/manifest.webmanifest +12 -0
- package/dist/client/sw.js +32 -0
- package/package.json +122 -0
- package/src/nats/auth-callout/callout-config.test.ts +93 -0
- package/src/nats/auth-callout/callout-config.ts +109 -0
- package/src/nats/auth-callout/callout.integration.test.ts +332 -0
- package/src/nats/auth-callout/keys.ts +103 -0
- package/src/nats/auth-callout/responder.ts +241 -0
- package/src/nats/auth-callout/scope-policy.test.ts +159 -0
- package/src/nats/auth-callout/scope-policy.ts +210 -0
- package/src/nats/auth-callout/token.test.ts +163 -0
- package/src/nats/auth-callout/token.ts +157 -0
- package/src/nats/nats-daemon-callout.ts +194 -0
- package/src/nats/nats-daemon.test.ts +77 -0
- package/src/nats/nats-daemon.ts +50 -0
- package/src/nats/nats-token.test.ts +61 -0
- package/src/nats/nats-token.ts +59 -0
- package/src/runner/coordination-mcp-integration.test.ts +134 -0
- package/src/runner/nats-coordination-client.test.ts +49 -0
- package/src/runner/nats-coordination-client.ts +94 -0
- package/src/runner/runner-agent.test.ts +469 -0
- package/src/runner/runner-agent.ts +453 -0
- package/src/runner/runner-credential.test.ts +93 -0
- package/src/runner/runner-credential.ts +82 -0
- package/src/runner/runner-nats.test.ts +495 -0
- package/src/runner/runner-nats.ts +323 -0
- package/src/runner/runner-pair.test.ts +107 -0
- package/src/runner/runner-pair.ts +81 -0
- package/src/runner/runner.test.ts +135 -0
- package/src/runner/runner.ts +212 -0
- package/src/runner/turn-factories.test.ts +97 -0
- package/src/runner/turn-factories.ts +475 -0
- package/src/server/agent-config-journey.test.ts +106 -0
- package/src/server/agent.ts +8 -0
- package/src/server/auto-continue/auth-error-detector.ts +66 -0
- package/src/server/auto-continue/limit-detector.ts +194 -0
- package/src/server/bm25.test.ts +92 -0
- package/src/server/bm25.ts +101 -0
- package/src/server/chat-events-jetstream.test.ts +135 -0
- package/src/server/claude-harness.ts +360 -0
- package/src/server/claude-pty/agent-normalizers.ts +309 -0
- package/src/server/claude-pty/auth.test.ts +38 -0
- package/src/server/claude-pty/auth.ts +32 -0
- package/src/server/claude-pty/claude-session-registry.adapter.ts +81 -0
- package/src/server/claude-pty/claude-session-registry.test.ts +149 -0
- package/src/server/claude-pty/driver.test.ts +902 -0
- package/src/server/claude-pty/driver.ts +807 -0
- package/src/server/claude-pty/jsonl-path.adapter.ts +57 -0
- package/src/server/claude-pty/jsonl-path.test.ts +114 -0
- package/src/server/claude-pty/jsonl-to-event.test.ts +241 -0
- package/src/server/claude-pty/jsonl-to-event.ts +174 -0
- package/src/server/claude-pty/output-ring.test.ts +35 -0
- package/src/server/claude-pty/output-ring.ts +25 -0
- package/src/server/claude-pty/parity-matrix.test.ts +227 -0
- package/src/server/claude-pty/pid-registry.adapter.ts +135 -0
- package/src/server/claude-pty/pid-registry.test.ts +122 -0
- package/src/server/claude-pty/preflight/binary-fingerprint.adapter.ts +20 -0
- package/src/server/claude-pty/preflight/binary-fingerprint.test.ts +32 -0
- package/src/server/claude-pty/pty-instance-registry.test.ts +177 -0
- package/src/server/claude-pty/pty-instance-registry.ts +166 -0
- package/src/server/claude-pty/pty-memory-sampler.adapter.test.ts +103 -0
- package/src/server/claude-pty/pty-memory-sampler.adapter.ts +85 -0
- package/src/server/claude-pty/pty-process.adapter.ts +66 -0
- package/src/server/claude-pty/pty-process.test.ts +49 -0
- package/src/server/claude-pty/resolve-binary.adapter.ts +106 -0
- package/src/server/claude-pty/resolve-binary.test.ts +118 -0
- package/src/server/claude-pty/runtime-dir.adapter.ts +19 -0
- package/src/server/claude-pty/settings-writer.adapter.ts +27 -0
- package/src/server/claude-pty/settings-writer.test.ts +22 -0
- package/src/server/claude-pty/smoke-test-io.adapter.ts +28 -0
- package/src/server/claude-pty/smoke-test.test.ts +191 -0
- package/src/server/claude-pty/smoke-test.ts +185 -0
- package/src/server/claude-pty/subagent-orchestrator.ts +887 -0
- package/src/server/claude-pty/tool-callback.ts +274 -0
- package/src/server/claude-pty/tui-control.test.ts +272 -0
- package/src/server/claude-pty/tui-control.ts +182 -0
- package/src/server/claude-pty/tui-source.adapter.test.ts +360 -0
- package/src/server/claude-pty/tui-source.adapter.ts +343 -0
- package/src/server/claude-pty/tunnel-gateway.ts +12 -0
- package/src/server/claude-pty-mcp/canonical-args.ts +15 -0
- package/src/server/claude-pty-mcp/fs-stat.adapter.ts +8 -0
- package/src/server/claude-pty-mcp/history-primer.ts +90 -0
- package/src/server/claude-pty-mcp/http-server.adapter.ts +33 -0
- package/src/server/claude-pty-mcp/mcp-http.ts +177 -0
- package/src/server/claude-pty-mcp/mcp.ts +412 -0
- package/src/server/claude-pty-mcp/mention-parser.ts +25 -0
- package/src/server/claude-pty-mcp/paths.ts +24 -0
- package/src/server/claude-pty-mcp/permission-gate.ts +243 -0
- package/src/server/claude-pty-mcp/terminal-pid-registry.adapter.ts +107 -0
- package/src/server/claude-pty-mcp/tools/ask-user-question.test.ts +119 -0
- package/src/server/claude-pty-mcp/tools/ask-user-question.ts +61 -0
- package/src/server/claude-pty-mcp/tools/bash.adapter.ts +76 -0
- package/src/server/claude-pty-mcp/tools/bash.test.ts +56 -0
- package/src/server/claude-pty-mcp/tools/delegate-subagent.test.ts +155 -0
- package/src/server/claude-pty-mcp/tools/delegate-subagent.ts +111 -0
- package/src/server/claude-pty-mcp/tools/edit.adapter.ts +95 -0
- package/src/server/claude-pty-mcp/tools/edit.test.ts +93 -0
- package/src/server/claude-pty-mcp/tools/exit-plan-mode.test.ts +61 -0
- package/src/server/claude-pty-mcp/tools/exit-plan-mode.ts +50 -0
- package/src/server/claude-pty-mcp/tools/glob.adapter.ts +86 -0
- package/src/server/claude-pty-mcp/tools/glob.test.ts +61 -0
- package/src/server/claude-pty-mcp/tools/grep.adapter.ts +126 -0
- package/src/server/claude-pty-mcp/tools/grep.test.ts +62 -0
- package/src/server/claude-pty-mcp/tools/read.adapter.ts +58 -0
- package/src/server/claude-pty-mcp/tools/read.test.ts +62 -0
- package/src/server/claude-pty-mcp/tools/tool-callback-shim.ts +42 -0
- package/src/server/claude-pty-mcp/tools/webfetch.test.ts +81 -0
- package/src/server/claude-pty-mcp/tools/webfetch.ts +82 -0
- package/src/server/claude-pty-mcp/tools/websearch.test.ts +40 -0
- package/src/server/claude-pty-mcp/tools/websearch.ts +42 -0
- package/src/server/claude-pty-mcp/tools/write.adapter.ts +60 -0
- package/src/server/claude-pty-mcp/tools/write.test.ts +52 -0
- package/src/server/claude-pty-mcp/uploads.adapter.ts +98 -0
- package/src/server/claude-pty-mcp/uploads.ts +38 -0
- package/src/server/claude-turn.test.ts +176 -0
- package/src/server/cli-runtime.test.ts +456 -0
- package/src/server/cli-runtime.ts +374 -0
- package/src/server/cli-supervisor.ts +81 -0
- package/src/server/cli.ts +78 -0
- package/src/server/client-log-forwarder.test.ts +74 -0
- package/src/server/client-log-forwarder.ts +75 -0
- package/src/server/codex-app-server-protocol.ts +449 -0
- package/src/server/codex-app-server.test.ts +2990 -0
- package/src/server/codex-app-server.ts +1713 -0
- package/src/server/coordination-integration.test.ts +63 -0
- package/src/server/coordination-mcp.test.ts +149 -0
- package/src/server/coordination-mcp.ts +197 -0
- package/src/server/delegation-coordinator.test.ts +675 -0
- package/src/server/delegation-coordinator.ts +454 -0
- package/src/server/discovery.test.ts +211 -0
- package/src/server/discovery.ts +301 -0
- package/src/server/event-store-agent-config.test.ts +124 -0
- package/src/server/event-store-coordination.test.ts +149 -0
- package/src/server/event-store-profile.test.ts +132 -0
- package/src/server/event-store-repo.test.ts +154 -0
- package/src/server/event-store-runner-team.test.ts +104 -0
- package/src/server/event-store.test.ts +342 -0
- package/src/server/event-store.ts +2208 -0
- package/src/server/events.ts +379 -0
- package/src/server/extension-router.test.ts +183 -0
- package/src/server/extension-router.ts +114 -0
- package/src/server/extensions/agents/server.test.ts +191 -0
- package/src/server/extensions/agents/server.ts +108 -0
- package/src/server/extensions/c3/server.test.ts +284 -0
- package/src/server/extensions/c3/server.ts +212 -0
- package/src/server/extensions/code/server.test.ts +200 -0
- package/src/server/extensions/code/server.ts +150 -0
- package/src/server/extensions.config.ts +10 -0
- package/src/server/external-open.ts +69 -0
- package/src/server/generate-fork-context.ts +58 -0
- package/src/server/generate-merge-context.test.ts +290 -0
- package/src/server/generate-merge-context.ts +141 -0
- package/src/server/generate-title.ts +36 -0
- package/src/server/git-clone-policy.test.ts +138 -0
- package/src/server/git-clone-policy.ts +27 -0
- package/src/server/harness-types.ts +1 -0
- package/src/server/journey-verification.test.ts +640 -0
- package/src/server/journey-verification.ts +195 -0
- package/src/server/machine-name.ts +22 -0
- package/src/server/nats-auth.test.ts +92 -0
- package/src/server/nats-auth.ts +6 -0
- package/src/server/nats-bind-guard.test.ts +71 -0
- package/src/server/nats-bind-guard.ts +42 -0
- package/src/server/nats-bridge.test.ts +141 -0
- package/src/server/nats-bridge.ts +111 -0
- package/src/server/nats-connector.test.ts +56 -0
- package/src/server/nats-connector.ts +71 -0
- package/src/server/nats-daemon-manager.test.ts +76 -0
- package/src/server/nats-daemon-manager.ts +174 -0
- package/src/server/nats-publisher.test.ts +356 -0
- package/src/server/nats-publisher.ts +271 -0
- package/src/server/nats-responders.test.ts +1018 -0
- package/src/server/nats-responders.ts +925 -0
- package/src/server/nats-streams.test.ts +112 -0
- package/src/server/nats-streams.ts +129 -0
- package/src/server/oauth-pool/oauth-responders.ts +185 -0
- package/src/server/oauth-pool/oauth-settings-store.ts +173 -0
- package/src/server/oauth-pool/oauth-token-pool.ts +303 -0
- package/src/server/orchestration.test.ts +1063 -0
- package/src/server/orchestration.ts +716 -0
- package/src/server/pairing-endpoints.test.ts +171 -0
- package/src/server/pairing-store.test.ts +154 -0
- package/src/server/pairing-store.ts +124 -0
- package/src/server/paths.ts +27 -0
- package/src/server/pr3-liveness.test.ts +252 -0
- package/src/server/process-utils.ts +10 -0
- package/src/server/project-cli.ts +180 -0
- package/src/server/provider-catalog.test.ts +177 -0
- package/src/server/provider-catalog.ts +146 -0
- package/src/server/pty-responders.ts +345 -0
- package/src/server/push-notifications.test.ts +234 -0
- package/src/server/push-notifications.ts +188 -0
- package/src/server/quick-response.test.ts +196 -0
- package/src/server/quick-response.ts +154 -0
- package/src/server/read-models-agent-config.test.ts +59 -0
- package/src/server/read-models-coordination.test.ts +69 -0
- package/src/server/read-models.test.ts +332 -0
- package/src/server/read-models.ts +258 -0
- package/src/server/repo-journey.test.ts +96 -0
- package/src/server/repo-manager.test.ts +118 -0
- package/src/server/repo-manager.ts +97 -0
- package/src/server/repo-status.test.ts +54 -0
- package/src/server/repo-status.ts +82 -0
- package/src/server/restart.test.ts +27 -0
- package/src/server/restart.ts +30 -0
- package/src/server/runner-incompatible-gate.test.ts +383 -0
- package/src/server/runner-manager.test.ts +72 -0
- package/src/server/runner-manager.ts +312 -0
- package/src/server/runner-pairing-urls.test.ts +59 -0
- package/src/server/runner-pairing-urls.ts +67 -0
- package/src/server/runner-proxy.test.ts +456 -0
- package/src/server/runner-proxy.ts +494 -0
- package/src/server/runner-router.test.ts +429 -0
- package/src/server/runner-router.ts +212 -0
- package/src/server/runner-routing.test.ts +584 -0
- package/src/server/runtime-registry.test.ts +436 -0
- package/src/server/runtime-registry.ts +307 -0
- package/src/server/sandbox-health.test.ts +127 -0
- package/src/server/sandbox-health.ts +94 -0
- package/src/server/sandbox-journey.test.ts +232 -0
- package/src/server/sandbox-manager.test.ts +146 -0
- package/src/server/sandbox-manager.ts +159 -0
- package/src/server/server.test.ts +287 -0
- package/src/server/server.ts +1108 -0
- package/src/server/session-discovery.test.ts +129 -0
- package/src/server/session-discovery.ts +475 -0
- package/src/server/session-index.test.ts +362 -0
- package/src/server/session-index.ts +119 -0
- package/src/server/session-seed.ts +288 -0
- package/src/server/share.test.ts +108 -0
- package/src/server/share.ts +113 -0
- package/src/server/skill-discovery.test.ts +201 -0
- package/src/server/skill-discovery.ts +77 -0
- package/src/server/storage/test-helpers.ts +67 -0
- package/src/server/terminal-manager.test.ts +309 -0
- package/src/server/terminal-manager.ts +354 -0
- package/src/server/transcript-consumer.test.ts +339 -0
- package/src/server/transcript-consumer.ts +162 -0
- package/src/server/transcript-search.test.ts +193 -0
- package/src/server/transcript-search.ts +83 -0
- package/src/server/transcript-utils.ts +52 -0
- package/src/server/update-manager.test.ts +107 -0
- package/src/server/update-manager.ts +230 -0
- package/src/server/workflow-engine.test.ts +251 -0
- package/src/server/workflow-engine.ts +169 -0
- package/src/server/workflow-mcp.ts +49 -0
- package/src/server/workflow-store.test.ts +155 -0
- package/src/server/workflow-store.ts +139 -0
- package/src/server/workspace-agent-integration.test.ts +167 -0
- package/src/server/workspace-agent-routes.test.ts +127 -0
- package/src/server/workspace-agent-routes.ts +89 -0
- package/src/server/workspace-agent.test.ts +103 -0
- package/src/server/workspace-agent.ts +102 -0
- package/src/server/workspace-cli.test.ts +79 -0
- package/src/server/workspace-config-manager.test.ts +109 -0
- package/src/server/workspace-config-manager.ts +83 -0
- package/src/server/workspace-directory-policy.test.ts +109 -0
- package/src/server/workspace-directory-policy.ts +56 -0
- package/src/shared/agent-config-types.ts +25 -0
- package/src/shared/branding.test.ts +42 -0
- package/src/shared/branding.ts +54 -0
- package/src/shared/compression.test.ts +85 -0
- package/src/shared/compression.ts +42 -0
- package/src/shared/coordination-store.test.ts +24 -0
- package/src/shared/coordination-store.ts +26 -0
- package/src/shared/dev-ports.test.ts +84 -0
- package/src/shared/dev-ports.ts +100 -0
- package/src/shared/extension-types.ts +45 -0
- package/src/shared/fork-presets.ts +54 -0
- package/src/shared/harness-types.test.ts +15 -0
- package/src/shared/harness-types.ts +21 -0
- package/src/shared/log-sink.test.ts +112 -0
- package/src/shared/log-sink.ts +185 -0
- package/src/shared/mention-pattern.ts +7 -0
- package/src/shared/merge-presets.ts +41 -0
- package/src/shared/nats-subjects.test.ts +61 -0
- package/src/shared/nats-subjects.ts +131 -0
- package/src/shared/permission-policy.ts +136 -0
- package/src/shared/ports.ts +3 -0
- package/src/shared/preset-types.ts +15 -0
- package/src/shared/profile-types.ts +49 -0
- package/src/shared/projectFileUrl.ts +36 -0
- package/src/shared/protocol.ts +153 -0
- package/src/shared/pty-instance.ts +43 -0
- package/src/shared/puggy/diagnostics/result.ts +18 -0
- package/src/shared/puggy/expressions/evaluate.ts +292 -0
- package/src/shared/puggy/index.test.ts +101 -0
- package/src/shared/puggy/index.ts +69 -0
- package/src/shared/puggy/parser/ast.ts +110 -0
- package/src/shared/puggy/parser/parser.ts +624 -0
- package/src/shared/puggy/renderer/html.ts +447 -0
- package/src/shared/runner-protocol.test.ts +277 -0
- package/src/shared/runner-protocol.ts +210 -0
- package/src/shared/runner-team-types.ts +73 -0
- package/src/shared/runtime-types.ts +48 -0
- package/src/shared/sandbox-types.ts +53 -0
- package/src/shared/tailwind-build.test.ts +12 -0
- package/src/shared/tinkaria-system-prompt.ts +101 -0
- package/src/shared/tools.test.ts +335 -0
- package/src/shared/tools.ts +397 -0
- package/src/shared/transcript-entries.ts +27 -0
- package/src/shared/transcript-render.test.ts +225 -0
- package/src/shared/transcript-render.ts +467 -0
- package/src/shared/types.ts +1031 -0
- package/src/shared/vite-config.test.ts +47 -0
- package/src/shared/web-context.test.ts +110 -0
- package/src/shared/web-context.ts +76 -0
- package/src/shared/workflow-types.ts +51 -0
- package/src/shared/workspace-types.ts +100 -0
|
@@ -0,0 +1,1063 @@
|
|
|
1
|
+
import { describe, expect, test, afterEach } from "bun:test"
|
|
2
|
+
import { createOrchestrationMcpServer, SessionOrchestrator } from "./orchestration"
|
|
3
|
+
import type { TranscriptEntry } from "../shared/types"
|
|
4
|
+
import type { CreateDelegationArgs } from "./delegation-coordinator"
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Test helpers
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
function timestamped<T extends Omit<TranscriptEntry, "_id" | "createdAt">>(entry: T): TranscriptEntry {
|
|
11
|
+
return {
|
|
12
|
+
_id: crypto.randomUUID(),
|
|
13
|
+
createdAt: Date.now(),
|
|
14
|
+
...entry,
|
|
15
|
+
} as TranscriptEntry
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Fake store — supports multiple chats
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
interface FakeChat {
|
|
23
|
+
id: string
|
|
24
|
+
workspaceId: string
|
|
25
|
+
title: string
|
|
26
|
+
provider: "claude" | "codex" | null
|
|
27
|
+
planMode: boolean
|
|
28
|
+
sessionToken: string | null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createFakeStore() {
|
|
32
|
+
const chats = new Map<string, FakeChat>()
|
|
33
|
+
const project = { id: "project-1", localPath: "/tmp/project" }
|
|
34
|
+
const messagesByChatId = new Map<string, TranscriptEntry[]>()
|
|
35
|
+
let chatCounter = 0
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
chats,
|
|
39
|
+
project,
|
|
40
|
+
messagesByChatId,
|
|
41
|
+
async createChat(workspaceId: string) {
|
|
42
|
+
const id = `chat-${++chatCounter}`
|
|
43
|
+
const chat: FakeChat = {
|
|
44
|
+
id,
|
|
45
|
+
workspaceId,
|
|
46
|
+
title: "New Chat",
|
|
47
|
+
provider: null,
|
|
48
|
+
planMode: false,
|
|
49
|
+
sessionToken: null,
|
|
50
|
+
}
|
|
51
|
+
chats.set(id, chat)
|
|
52
|
+
messagesByChatId.set(id, [])
|
|
53
|
+
return chat
|
|
54
|
+
},
|
|
55
|
+
requireChat(chatId: string) {
|
|
56
|
+
const chat = chats.get(chatId)
|
|
57
|
+
if (!chat) throw new Error(`Chat not found: ${chatId}`)
|
|
58
|
+
return chat
|
|
59
|
+
},
|
|
60
|
+
getMessages(chatId: string) {
|
|
61
|
+
return [...(messagesByChatId.get(chatId) ?? [])]
|
|
62
|
+
},
|
|
63
|
+
getProject() {
|
|
64
|
+
return project
|
|
65
|
+
},
|
|
66
|
+
listChatsByProject() {
|
|
67
|
+
return [...chats.values()]
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Fake coordinator — tracks calls, can simulate turn completion
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
function createFakeCoordinator() {
|
|
77
|
+
const startedTurns: Array<{ chatId: string; content: string; delegatedContext?: string; isSpawned?: boolean; provider: string }> = []
|
|
78
|
+
const queuedTurns: Array<{
|
|
79
|
+
type: "chat.queue"
|
|
80
|
+
chatId: string
|
|
81
|
+
content: string
|
|
82
|
+
provider?: string
|
|
83
|
+
model?: string
|
|
84
|
+
planMode?: boolean
|
|
85
|
+
}> = []
|
|
86
|
+
const activeTurns = new Map<string, unknown>()
|
|
87
|
+
const cancelledChats: string[] = []
|
|
88
|
+
const disposedChats: string[] = []
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
startedTurns,
|
|
92
|
+
queuedTurns,
|
|
93
|
+
activeTurns,
|
|
94
|
+
cancelledChats,
|
|
95
|
+
disposedChats,
|
|
96
|
+
getActiveStatuses(): Map<string, "idle" | "starting" | "running" | "waiting_for_user" | "failed"> {
|
|
97
|
+
const statuses = new Map<string, "idle" | "starting" | "running" | "waiting_for_user" | "failed">()
|
|
98
|
+
for (const [chatId] of activeTurns) {
|
|
99
|
+
statuses.set(chatId, "running")
|
|
100
|
+
}
|
|
101
|
+
return statuses
|
|
102
|
+
},
|
|
103
|
+
async startTurnForChat(args: { chatId: string; content: string; delegatedContext?: string; isSpawned?: boolean; provider: string }) {
|
|
104
|
+
startedTurns.push(args)
|
|
105
|
+
activeTurns.set(args.chatId, { chatId: args.chatId })
|
|
106
|
+
},
|
|
107
|
+
async queue(args: {
|
|
108
|
+
type: "chat.queue"
|
|
109
|
+
chatId: string
|
|
110
|
+
content: string
|
|
111
|
+
provider?: string
|
|
112
|
+
model?: string
|
|
113
|
+
planMode?: boolean
|
|
114
|
+
}) {
|
|
115
|
+
queuedTurns.push(args)
|
|
116
|
+
return { chatId: args.chatId, queued: true }
|
|
117
|
+
},
|
|
118
|
+
async cancel(chatId: string) {
|
|
119
|
+
cancelledChats.push(chatId)
|
|
120
|
+
activeTurns.delete(chatId)
|
|
121
|
+
},
|
|
122
|
+
async disposeChat(chatId: string) {
|
|
123
|
+
disposedChats.push(chatId)
|
|
124
|
+
activeTurns.delete(chatId)
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Fake delegation coordinator — tracks createDelegation calls
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
function createFakeDelegationCoordinator() {
|
|
134
|
+
const createdDelegations: CreateDelegationArgs[] = []
|
|
135
|
+
return {
|
|
136
|
+
createdDelegations,
|
|
137
|
+
async createDelegation(args: CreateDelegationArgs) {
|
|
138
|
+
createdDelegations.push(args)
|
|
139
|
+
return { delegationId: `del-${createdDelegations.length}` }
|
|
140
|
+
},
|
|
141
|
+
generateResumeHint(entries: TranscriptEntry[]) {
|
|
142
|
+
return entries.length > 0 ? "generated-hint" : undefined
|
|
143
|
+
},
|
|
144
|
+
async initialize() {},
|
|
145
|
+
async getDelegation() { return null },
|
|
146
|
+
async getDelegationsForChild() { return [] },
|
|
147
|
+
async getBlockingDelegationsForParent() { return [] },
|
|
148
|
+
hasActiveBlockingDelegations() { return false },
|
|
149
|
+
async reconcileChildTerminal() { return null },
|
|
150
|
+
async bootReconciliation() {},
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Factory for orchestrator under test
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
function createOrchestrator(overrides?: {
|
|
159
|
+
maxDepth?: number
|
|
160
|
+
maxConcurrency?: number
|
|
161
|
+
delegationCoordinator?: ReturnType<typeof createFakeDelegationCoordinator>
|
|
162
|
+
}) {
|
|
163
|
+
const store = createFakeStore()
|
|
164
|
+
const coordinator = createFakeCoordinator()
|
|
165
|
+
const delegationCoordinator = overrides?.delegationCoordinator
|
|
166
|
+
const appendedMessages: Array<{ chatId: string; entry: TranscriptEntry }> = []
|
|
167
|
+
|
|
168
|
+
let onMessageAppendedCallback: ((chatId: string, entry: TranscriptEntry) => void) | undefined
|
|
169
|
+
|
|
170
|
+
const orchestrator = new SessionOrchestrator({
|
|
171
|
+
store: store as never,
|
|
172
|
+
coordinator: coordinator as never,
|
|
173
|
+
delegationCoordinator: delegationCoordinator as never,
|
|
174
|
+
onMessageAppended(chatId: string, entry: TranscriptEntry) {
|
|
175
|
+
appendedMessages.push({ chatId, entry })
|
|
176
|
+
onMessageAppendedCallback?.(chatId, entry)
|
|
177
|
+
},
|
|
178
|
+
maxDepth: overrides?.maxDepth,
|
|
179
|
+
maxConcurrency: overrides?.maxConcurrency,
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
orchestrator,
|
|
184
|
+
store,
|
|
185
|
+
coordinator,
|
|
186
|
+
delegationCoordinator,
|
|
187
|
+
appendedMessages,
|
|
188
|
+
/** Allow tests to hook into message delivery for simulating delayed results */
|
|
189
|
+
setOnMessageAppended(cb: (chatId: string, entry: TranscriptEntry) => void) {
|
|
190
|
+
onMessageAppendedCallback = cb
|
|
191
|
+
},
|
|
192
|
+
/** Simulate the target agent emitting a result entry */
|
|
193
|
+
emitResult(chatId: string, result: string, isError = false) {
|
|
194
|
+
const entry = timestamped({
|
|
195
|
+
kind: "result" as const,
|
|
196
|
+
subtype: isError ? ("error" as const) : ("success" as const),
|
|
197
|
+
isError,
|
|
198
|
+
durationMs: 100,
|
|
199
|
+
result,
|
|
200
|
+
})
|
|
201
|
+
orchestrator.onMessageAppended(chatId, entry)
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Seed a caller chat in the fake store so orchestrator calls have a valid origin
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
async function seedCallerChat(store: ReturnType<typeof createFakeStore>, provider: "claude" | "codex" = "claude") {
|
|
211
|
+
const chat = await store.createChat("project-1")
|
|
212
|
+
chat.provider = provider
|
|
213
|
+
return chat
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Tests
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
describe("SessionOrchestrator", () => {
|
|
221
|
+
let ctx: ReturnType<typeof createOrchestrator>
|
|
222
|
+
|
|
223
|
+
afterEach(() => {
|
|
224
|
+
ctx = undefined!
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// =========================================================================
|
|
228
|
+
// spawnAgent
|
|
229
|
+
// =========================================================================
|
|
230
|
+
|
|
231
|
+
describe("spawnAgent", () => {
|
|
232
|
+
test("creates a new chat and records origin chain", async () => {
|
|
233
|
+
ctx = createOrchestrator()
|
|
234
|
+
const caller = await seedCallerChat(ctx.store)
|
|
235
|
+
|
|
236
|
+
const { chatId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
237
|
+
instruction: "Do the thing",
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
expect(chatId).toBeDefined()
|
|
241
|
+
expect(typeof chatId).toBe("string")
|
|
242
|
+
// The spawned chat should exist in the store
|
|
243
|
+
const spawned = ctx.store.requireChat(chatId)
|
|
244
|
+
expect(spawned).toBeDefined()
|
|
245
|
+
expect(spawned.workspaceId).toBe("project-1")
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test("passes instruction to startTurnForChat", async () => {
|
|
249
|
+
ctx = createOrchestrator()
|
|
250
|
+
const caller = await seedCallerChat(ctx.store)
|
|
251
|
+
|
|
252
|
+
await ctx.orchestrator.spawnAgent(caller.id, {
|
|
253
|
+
instruction: "Write unit tests",
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
expect(ctx.coordinator.startedTurns).toHaveLength(1)
|
|
257
|
+
expect(ctx.coordinator.startedTurns[0]!.content).toBe("Write unit tests")
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
test("fork_context seeds delegated context without rewriting the child instruction", async () => {
|
|
261
|
+
ctx = createOrchestrator()
|
|
262
|
+
const caller = await seedCallerChat(ctx.store)
|
|
263
|
+
ctx.store.messagesByChatId.set(caller.id, [
|
|
264
|
+
timestamped({ kind: "user_prompt", content: "Investigate the auth race condition" }),
|
|
265
|
+
timestamped({ kind: "assistant_text", text: "The race likely happens between session restore and token refresh." }),
|
|
266
|
+
timestamped({ kind: "result", subtype: "success", isError: false, durationMs: 1, result: "Auth logs collected." }),
|
|
267
|
+
])
|
|
268
|
+
|
|
269
|
+
await ctx.orchestrator.spawnAgent(caller.id, {
|
|
270
|
+
instruction: "Write the regression test",
|
|
271
|
+
forkContext: true,
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
expect(ctx.coordinator.startedTurns).toHaveLength(1)
|
|
275
|
+
expect(ctx.coordinator.startedTurns[0]!.content).toBe("Write the regression test")
|
|
276
|
+
expect(ctx.coordinator.startedTurns[0]!.delegatedContext).toContain("Forked parent chat context:")
|
|
277
|
+
expect(ctx.coordinator.startedTurns[0]!.delegatedContext).toContain("User: Investigate the auth race condition")
|
|
278
|
+
expect(ctx.coordinator.startedTurns[0]!.delegatedContext).toContain("Assistant: The race likely happens between session restore and token refresh.")
|
|
279
|
+
expect(ctx.coordinator.startedTurns[0]!.delegatedContext).toContain("Result: Auth logs collected.")
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test("uses caller's provider by default", async () => {
|
|
283
|
+
ctx = createOrchestrator()
|
|
284
|
+
const caller = await seedCallerChat(ctx.store, "codex")
|
|
285
|
+
|
|
286
|
+
await ctx.orchestrator.spawnAgent(caller.id, {
|
|
287
|
+
instruction: "Refactor module",
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
expect(ctx.coordinator.startedTurns[0]!.provider).toBe("codex")
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
test("allows explicit provider override", async () => {
|
|
294
|
+
ctx = createOrchestrator()
|
|
295
|
+
const caller = await seedCallerChat(ctx.store, "claude")
|
|
296
|
+
|
|
297
|
+
await ctx.orchestrator.spawnAgent(caller.id, {
|
|
298
|
+
instruction: "Refactor module",
|
|
299
|
+
provider: "codex",
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
expect(ctx.coordinator.startedTurns[0]!.provider).toBe("codex")
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
test("passes isSpawned flag to startTurnForChat", async () => {
|
|
306
|
+
ctx = createOrchestrator()
|
|
307
|
+
const caller = await seedCallerChat(ctx.store)
|
|
308
|
+
|
|
309
|
+
await ctx.orchestrator.spawnAgent(caller.id, {
|
|
310
|
+
instruction: "delegated work",
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
expect(ctx.coordinator.startedTurns).toHaveLength(1)
|
|
314
|
+
expect(ctx.coordinator.startedTurns[0]!.isSpawned).toBe(true)
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
test("rejects when max concurrency (3) reached", async () => {
|
|
318
|
+
ctx = createOrchestrator({ maxConcurrency: 3 })
|
|
319
|
+
const caller = await seedCallerChat(ctx.store)
|
|
320
|
+
|
|
321
|
+
// Spawn 3 agents (the default max)
|
|
322
|
+
await ctx.orchestrator.spawnAgent(caller.id, { instruction: "task-1" })
|
|
323
|
+
await ctx.orchestrator.spawnAgent(caller.id, { instruction: "task-2" })
|
|
324
|
+
await ctx.orchestrator.spawnAgent(caller.id, { instruction: "task-3" })
|
|
325
|
+
|
|
326
|
+
// The 4th should reject
|
|
327
|
+
await expect(
|
|
328
|
+
ctx.orchestrator.spawnAgent(caller.id, { instruction: "task-4" }),
|
|
329
|
+
).rejects.toThrow(/concurrency/i)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
test("completed children no longer count toward concurrency", async () => {
|
|
333
|
+
ctx = createOrchestrator({ maxConcurrency: 1 })
|
|
334
|
+
const caller = await seedCallerChat(ctx.store)
|
|
335
|
+
|
|
336
|
+
const child = await ctx.orchestrator.spawnAgent(caller.id, { instruction: "task-1" })
|
|
337
|
+
ctx.coordinator.activeTurns.delete(child.chatId)
|
|
338
|
+
|
|
339
|
+
await expect(
|
|
340
|
+
ctx.orchestrator.spawnAgent(caller.id, { instruction: "task-2" }),
|
|
341
|
+
).resolves.toEqual({ chatId: expect.any(String) })
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
test("rejects when max depth (3) exceeded", async () => {
|
|
345
|
+
ctx = createOrchestrator({ maxDepth: 3 })
|
|
346
|
+
const caller = await seedCallerChat(ctx.store)
|
|
347
|
+
|
|
348
|
+
// Build a chain: caller -> child1 -> child2 -> child3 (depth 3)
|
|
349
|
+
const child1 = await ctx.orchestrator.spawnAgent(caller.id, { instruction: "depth-1" })
|
|
350
|
+
const child2 = await ctx.orchestrator.spawnAgent(child1.chatId, { instruction: "depth-2" })
|
|
351
|
+
const child3 = await ctx.orchestrator.spawnAgent(child2.chatId, { instruction: "depth-3" })
|
|
352
|
+
|
|
353
|
+
// Depth 4 should reject
|
|
354
|
+
await expect(
|
|
355
|
+
ctx.orchestrator.spawnAgent(child3.chatId, { instruction: "depth-4" }),
|
|
356
|
+
).rejects.toThrow(/depth/i)
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
// =========================================================================
|
|
361
|
+
// sendInput
|
|
362
|
+
// =========================================================================
|
|
363
|
+
|
|
364
|
+
describe("sendInput", () => {
|
|
365
|
+
test("calls startTurnForChat on target with content", async () => {
|
|
366
|
+
ctx = createOrchestrator()
|
|
367
|
+
const caller = await seedCallerChat(ctx.store)
|
|
368
|
+
const { chatId: targetId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
369
|
+
instruction: "initial",
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
// Clear started turns from spawn
|
|
373
|
+
ctx.coordinator.startedTurns.length = 0
|
|
374
|
+
// Remove from active so sendInput doesn't see it as running
|
|
375
|
+
ctx.coordinator.activeTurns.delete(targetId)
|
|
376
|
+
|
|
377
|
+
await ctx.orchestrator.sendInput(caller.id, {
|
|
378
|
+
targetChatId: targetId,
|
|
379
|
+
content: "follow-up message",
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
expect(ctx.coordinator.startedTurns).toHaveLength(1)
|
|
383
|
+
expect(ctx.coordinator.startedTurns[0]!.chatId).toBe(targetId)
|
|
384
|
+
expect(ctx.coordinator.startedTurns[0]!.content).toBe("follow-up message")
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
test("rejects if target chat does not exist", async () => {
|
|
388
|
+
ctx = createOrchestrator()
|
|
389
|
+
const caller = await seedCallerChat(ctx.store)
|
|
390
|
+
|
|
391
|
+
await expect(
|
|
392
|
+
ctx.orchestrator.sendInput(caller.id, {
|
|
393
|
+
targetChatId: "nonexistent-chat",
|
|
394
|
+
content: "hello",
|
|
395
|
+
}),
|
|
396
|
+
).rejects.toThrow(/not found|spawned agent/i)
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
test("queues input if target is already running", async () => {
|
|
400
|
+
ctx = createOrchestrator()
|
|
401
|
+
const caller = await seedCallerChat(ctx.store)
|
|
402
|
+
const { chatId: targetId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
403
|
+
instruction: "initial",
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
// Target is still in activeTurns from spawnAgent
|
|
407
|
+
await expect(ctx.orchestrator.sendInput(caller.id, {
|
|
408
|
+
targetChatId: targetId,
|
|
409
|
+
content: "more input",
|
|
410
|
+
})).resolves.toBeUndefined()
|
|
411
|
+
|
|
412
|
+
expect(ctx.coordinator.startedTurns).toHaveLength(1)
|
|
413
|
+
expect(ctx.coordinator.queuedTurns).toEqual([
|
|
414
|
+
{
|
|
415
|
+
type: "chat.queue",
|
|
416
|
+
chatId: targetId,
|
|
417
|
+
content: "more input",
|
|
418
|
+
provider: "claude",
|
|
419
|
+
model: "opus",
|
|
420
|
+
planMode: false,
|
|
421
|
+
},
|
|
422
|
+
])
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
test("rejects when caller does not own the target child", async () => {
|
|
426
|
+
ctx = createOrchestrator()
|
|
427
|
+
const caller = await seedCallerChat(ctx.store)
|
|
428
|
+
const otherCaller = await seedCallerChat(ctx.store)
|
|
429
|
+
const { chatId: targetId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
430
|
+
instruction: "initial",
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
ctx.coordinator.activeTurns.delete(targetId)
|
|
434
|
+
|
|
435
|
+
await expect(
|
|
436
|
+
ctx.orchestrator.sendInput(otherCaller.id, {
|
|
437
|
+
targetChatId: targetId,
|
|
438
|
+
content: "unauthorized follow-up",
|
|
439
|
+
}),
|
|
440
|
+
).rejects.toThrow(/does not own/i)
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
// =========================================================================
|
|
445
|
+
// waitForResult
|
|
446
|
+
// =========================================================================
|
|
447
|
+
|
|
448
|
+
describe("waitForResult", () => {
|
|
449
|
+
test("resolves when target emits result entry via onMessageAppended", async () => {
|
|
450
|
+
ctx = createOrchestrator()
|
|
451
|
+
const caller = await seedCallerChat(ctx.store)
|
|
452
|
+
const { chatId: targetId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
453
|
+
instruction: "compute something",
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
// Emit a result after a small delay
|
|
457
|
+
setTimeout(() => ctx.emitResult(targetId, "computed value"), 50)
|
|
458
|
+
|
|
459
|
+
const outcome = await ctx.orchestrator.waitForResult(caller.id, {
|
|
460
|
+
targetChatId: targetId,
|
|
461
|
+
timeoutMs: 2000,
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
expect(outcome.result).toBe("computed value")
|
|
465
|
+
expect(outcome.isError).toBe(false)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
test("returns result text and isError flag", async () => {
|
|
469
|
+
ctx = createOrchestrator()
|
|
470
|
+
const caller = await seedCallerChat(ctx.store)
|
|
471
|
+
const { chatId: targetId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
472
|
+
instruction: "do something risky",
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
// Emit an error result
|
|
476
|
+
setTimeout(() => ctx.emitResult(targetId, "something went wrong", true), 50)
|
|
477
|
+
|
|
478
|
+
const outcome = await ctx.orchestrator.waitForResult(caller.id, {
|
|
479
|
+
targetChatId: targetId,
|
|
480
|
+
timeoutMs: 2000,
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
expect(outcome.result).toBe("something went wrong")
|
|
484
|
+
expect(outcome.isError).toBe(true)
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
test("times out after configured duration", async () => {
|
|
488
|
+
ctx = createOrchestrator()
|
|
489
|
+
const caller = await seedCallerChat(ctx.store)
|
|
490
|
+
const { chatId: targetId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
491
|
+
instruction: "slow task",
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
// No result emitted — should time out
|
|
495
|
+
await expect(
|
|
496
|
+
ctx.orchestrator.waitForResult(caller.id, {
|
|
497
|
+
targetChatId: targetId,
|
|
498
|
+
timeoutMs: 50,
|
|
499
|
+
}),
|
|
500
|
+
).rejects.toThrow(/timeout|timed out/i)
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
test("cancels target turn on timeout", async () => {
|
|
504
|
+
ctx = createOrchestrator()
|
|
505
|
+
const caller = await seedCallerChat(ctx.store)
|
|
506
|
+
const { chatId: targetId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
507
|
+
instruction: "slow task",
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
await ctx.orchestrator.waitForResult(caller.id, {
|
|
512
|
+
targetChatId: targetId,
|
|
513
|
+
timeoutMs: 50,
|
|
514
|
+
})
|
|
515
|
+
} catch {
|
|
516
|
+
// Expected to throw on timeout
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
expect(ctx.coordinator.cancelledChats).toContain(targetId)
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
test("rejects when caller does not own the waited child", async () => {
|
|
523
|
+
ctx = createOrchestrator()
|
|
524
|
+
const caller = await seedCallerChat(ctx.store)
|
|
525
|
+
const otherCaller = await seedCallerChat(ctx.store)
|
|
526
|
+
const { chatId: targetId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
527
|
+
instruction: "child work",
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
await expect(
|
|
531
|
+
ctx.orchestrator.waitForResult(otherCaller.id, {
|
|
532
|
+
targetChatId: targetId,
|
|
533
|
+
timeoutMs: 50,
|
|
534
|
+
}),
|
|
535
|
+
).rejects.toThrow(/does not own/i)
|
|
536
|
+
})
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
// =========================================================================
|
|
540
|
+
// closeAgent
|
|
541
|
+
// =========================================================================
|
|
542
|
+
|
|
543
|
+
describe("closeAgent", () => {
|
|
544
|
+
test("disposes the target chat via coordinator", async () => {
|
|
545
|
+
ctx = createOrchestrator()
|
|
546
|
+
const caller = await seedCallerChat(ctx.store)
|
|
547
|
+
const { chatId: targetId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
548
|
+
instruction: "short-lived agent",
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
await ctx.orchestrator.closeAgent(caller.id, { targetChatId: targetId })
|
|
552
|
+
|
|
553
|
+
expect(ctx.coordinator.disposedChats).toContain(targetId)
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
test("cleans up origin chain tracking after prune", async () => {
|
|
557
|
+
ctx = createOrchestrator()
|
|
558
|
+
const caller = await seedCallerChat(ctx.store)
|
|
559
|
+
const { chatId: targetId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
560
|
+
instruction: "temporary agent",
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
await ctx.orchestrator.closeAgent(caller.id, { targetChatId: targetId })
|
|
564
|
+
ctx.orchestrator.pruneTombstones()
|
|
565
|
+
|
|
566
|
+
// After prune, spawning again should not count toward the old concurrency
|
|
567
|
+
const { chatId: newId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
568
|
+
instruction: "replacement agent",
|
|
569
|
+
})
|
|
570
|
+
expect(newId).toBeDefined()
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
test("marks child as closed in hierarchy before disposing", async () => {
|
|
574
|
+
ctx = createOrchestrator()
|
|
575
|
+
const caller = await seedCallerChat(ctx.store)
|
|
576
|
+
const { chatId: targetId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
577
|
+
instruction: "closeable agent",
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
await ctx.orchestrator.closeAgent(caller.id, { targetChatId: targetId })
|
|
581
|
+
|
|
582
|
+
// Hierarchy should show the child as "closed" (tombstone)
|
|
583
|
+
const hierarchy = ctx.orchestrator.getHierarchy(caller.id)
|
|
584
|
+
expect(hierarchy.children).toHaveLength(1)
|
|
585
|
+
expect(hierarchy.children[0]!.status).toBe("closed")
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
test("rejects when caller does not own the child being closed", async () => {
|
|
589
|
+
ctx = createOrchestrator()
|
|
590
|
+
const caller = await seedCallerChat(ctx.store)
|
|
591
|
+
const otherCaller = await seedCallerChat(ctx.store)
|
|
592
|
+
const { chatId: targetId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
593
|
+
instruction: "close me",
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
await expect(
|
|
597
|
+
ctx.orchestrator.closeAgent(otherCaller.id, { targetChatId: targetId }),
|
|
598
|
+
).rejects.toThrow(/does not own/i)
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
test("pruneTombstones removes closed children", async () => {
|
|
602
|
+
ctx = createOrchestrator()
|
|
603
|
+
const caller = await seedCallerChat(ctx.store)
|
|
604
|
+
const { chatId: targetId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
605
|
+
instruction: "closeable agent",
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
await ctx.orchestrator.closeAgent(caller.id, { targetChatId: targetId })
|
|
609
|
+
|
|
610
|
+
// Before prune
|
|
611
|
+
expect(ctx.orchestrator.getHierarchy(caller.id).children).toHaveLength(1)
|
|
612
|
+
|
|
613
|
+
// After prune
|
|
614
|
+
ctx.orchestrator.pruneTombstones()
|
|
615
|
+
expect(ctx.orchestrator.getHierarchy(caller.id).children).toHaveLength(0)
|
|
616
|
+
})
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
// =========================================================================
|
|
620
|
+
// circular detection
|
|
621
|
+
// =========================================================================
|
|
622
|
+
|
|
623
|
+
describe("circular detection", () => {
|
|
624
|
+
test("rejects nested spawn when max depth is explicitly 1", async () => {
|
|
625
|
+
ctx = createOrchestrator({ maxDepth: 1 })
|
|
626
|
+
const chatA = await seedCallerChat(ctx.store)
|
|
627
|
+
|
|
628
|
+
const { chatId: chatBId } = await ctx.orchestrator.spawnAgent(chatA.id, {
|
|
629
|
+
instruction: "B's task",
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
await expect(
|
|
633
|
+
ctx.orchestrator.spawnAgent(chatBId, {
|
|
634
|
+
instruction: "nested child",
|
|
635
|
+
}),
|
|
636
|
+
).rejects.toThrow(/depth|circular/i)
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
test("allows A->B, A->C (fan-out, no cycle)", async () => {
|
|
640
|
+
ctx = createOrchestrator()
|
|
641
|
+
const chatA = await seedCallerChat(ctx.store)
|
|
642
|
+
|
|
643
|
+
const childB = await ctx.orchestrator.spawnAgent(chatA.id, {
|
|
644
|
+
instruction: "B's task",
|
|
645
|
+
})
|
|
646
|
+
expect(childB.chatId).toBeDefined()
|
|
647
|
+
|
|
648
|
+
const childC = await ctx.orchestrator.spawnAgent(chatA.id, {
|
|
649
|
+
instruction: "C's task",
|
|
650
|
+
})
|
|
651
|
+
expect(childC.chatId).toBeDefined()
|
|
652
|
+
|
|
653
|
+
// Both should succeed — fan-out is fine
|
|
654
|
+
expect(childB.chatId).not.toBe(childC.chatId)
|
|
655
|
+
})
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
// =========================================================================
|
|
659
|
+
// cancelWithCascade
|
|
660
|
+
// =========================================================================
|
|
661
|
+
|
|
662
|
+
describe("cancelWithCascade", () => {
|
|
663
|
+
test("cancelling parent cancels all children", async () => {
|
|
664
|
+
ctx = createOrchestrator()
|
|
665
|
+
const parent = await seedCallerChat(ctx.store)
|
|
666
|
+
|
|
667
|
+
const childA = await ctx.orchestrator.spawnAgent(parent.id, {
|
|
668
|
+
instruction: "child A",
|
|
669
|
+
})
|
|
670
|
+
const childB = await ctx.orchestrator.spawnAgent(parent.id, {
|
|
671
|
+
instruction: "child B",
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
await ctx.orchestrator.cancelWithCascade(parent.id)
|
|
675
|
+
|
|
676
|
+
expect(ctx.coordinator.cancelledChats).toContain(childA.chatId)
|
|
677
|
+
expect(ctx.coordinator.cancelledChats).toContain(childB.chatId)
|
|
678
|
+
expect(ctx.coordinator.cancelledChats).toContain(parent.id)
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
test("handles nested chains: A->B->C, cancel A cancels B and C", async () => {
|
|
682
|
+
ctx = createOrchestrator({ maxDepth: 5 })
|
|
683
|
+
const chatA = await seedCallerChat(ctx.store)
|
|
684
|
+
|
|
685
|
+
const { chatId: chatBId } = await ctx.orchestrator.spawnAgent(chatA.id, {
|
|
686
|
+
instruction: "B's task",
|
|
687
|
+
})
|
|
688
|
+
const { chatId: chatCId } = await ctx.orchestrator.spawnAgent(chatBId, {
|
|
689
|
+
instruction: "C's task",
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
await ctx.orchestrator.cancelWithCascade(chatA.id)
|
|
693
|
+
|
|
694
|
+
expect(ctx.coordinator.cancelledChats).toContain(chatA.id)
|
|
695
|
+
expect(ctx.coordinator.cancelledChats).toContain(chatBId)
|
|
696
|
+
expect(ctx.coordinator.cancelledChats).toContain(chatCId)
|
|
697
|
+
})
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
// =========================================================================
|
|
701
|
+
// destroy
|
|
702
|
+
// =========================================================================
|
|
703
|
+
|
|
704
|
+
describe("destroy", () => {
|
|
705
|
+
test("rejects pending waiters and clears timers", async () => {
|
|
706
|
+
ctx = createOrchestrator()
|
|
707
|
+
const caller = await seedCallerChat(ctx.store)
|
|
708
|
+
const { chatId: targetId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
709
|
+
instruction: "long-running task",
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
const waitPromise = ctx.orchestrator.waitForResult(caller.id, {
|
|
713
|
+
targetChatId: targetId,
|
|
714
|
+
timeoutMs: 60_000,
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
ctx.orchestrator.destroy()
|
|
718
|
+
|
|
719
|
+
await expect(waitPromise).rejects.toThrow(/disposed/i)
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
test("allows new spawns after destroy + re-init", async () => {
|
|
723
|
+
ctx = createOrchestrator()
|
|
724
|
+
const caller = await seedCallerChat(ctx.store)
|
|
725
|
+
|
|
726
|
+
await ctx.orchestrator.spawnAgent(caller.id, { instruction: "pre-destroy" })
|
|
727
|
+
ctx.orchestrator.destroy()
|
|
728
|
+
|
|
729
|
+
// After destroy, origin tracking is cleared — new spawns should work
|
|
730
|
+
const { chatId } = await ctx.orchestrator.spawnAgent(caller.id, { instruction: "post-destroy" })
|
|
731
|
+
expect(chatId).toBeDefined()
|
|
732
|
+
})
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
// =========================================================================
|
|
736
|
+
// getHierarchy
|
|
737
|
+
// =========================================================================
|
|
738
|
+
|
|
739
|
+
describe("getHierarchy", () => {
|
|
740
|
+
test("returns empty children when chat has no spawned agents", async () => {
|
|
741
|
+
ctx = createOrchestrator()
|
|
742
|
+
const caller = await seedCallerChat(ctx.store)
|
|
743
|
+
|
|
744
|
+
const hierarchy = ctx.orchestrator.getHierarchy(caller.id)
|
|
745
|
+
|
|
746
|
+
expect(hierarchy).toEqual({ children: [] })
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
test("returns children with status after spawnAgent", async () => {
|
|
750
|
+
ctx = createOrchestrator()
|
|
751
|
+
const caller = await seedCallerChat(ctx.store)
|
|
752
|
+
|
|
753
|
+
const { chatId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
754
|
+
instruction: "Do the thing",
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
const hierarchy = ctx.orchestrator.getHierarchy(caller.id)
|
|
758
|
+
|
|
759
|
+
expect(hierarchy.children).toHaveLength(1)
|
|
760
|
+
expect(hierarchy.children[0]!.chatId).toBe(chatId)
|
|
761
|
+
expect(hierarchy.children[0]!.instruction).toBe("Do the thing")
|
|
762
|
+
expect(hierarchy.children[0]!.spawnedAt).toBeGreaterThan(0)
|
|
763
|
+
expect(hierarchy.children[0]!.children).toEqual([])
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
test("maps activeTurns starting/running to running status", async () => {
|
|
767
|
+
ctx = createOrchestrator()
|
|
768
|
+
const caller = await seedCallerChat(ctx.store)
|
|
769
|
+
|
|
770
|
+
await ctx.orchestrator.spawnAgent(caller.id, {
|
|
771
|
+
instruction: "task",
|
|
772
|
+
})
|
|
773
|
+
// The fake coordinator sets activeTurns on startTurnForChat,
|
|
774
|
+
// so the child should be in activeTurns -> status should be "running"
|
|
775
|
+
const hierarchy = ctx.orchestrator.getHierarchy(caller.id)
|
|
776
|
+
|
|
777
|
+
expect(hierarchy.children[0]!.status).toBe("running")
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
test("shows completed when child finishes (removed from activeTurns)", async () => {
|
|
781
|
+
ctx = createOrchestrator()
|
|
782
|
+
const caller = await seedCallerChat(ctx.store)
|
|
783
|
+
|
|
784
|
+
const { chatId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
785
|
+
instruction: "quick task",
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
// Simulate turn completion: remove from activeTurns
|
|
789
|
+
ctx.coordinator.activeTurns.delete(chatId)
|
|
790
|
+
|
|
791
|
+
const hierarchy = ctx.orchestrator.getHierarchy(caller.id)
|
|
792
|
+
|
|
793
|
+
expect(hierarchy.children[0]!.status).toBe("completed")
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
test("truncates instruction to 120 characters", async () => {
|
|
797
|
+
ctx = createOrchestrator()
|
|
798
|
+
const caller = await seedCallerChat(ctx.store)
|
|
799
|
+
|
|
800
|
+
const longInstruction = "A".repeat(200)
|
|
801
|
+
await ctx.orchestrator.spawnAgent(caller.id, {
|
|
802
|
+
instruction: longInstruction,
|
|
803
|
+
})
|
|
804
|
+
|
|
805
|
+
const hierarchy = ctx.orchestrator.getHierarchy(caller.id)
|
|
806
|
+
|
|
807
|
+
expect(hierarchy.children[0]!.instruction).toHaveLength(120)
|
|
808
|
+
})
|
|
809
|
+
|
|
810
|
+
test("nested children rendered in hierarchy tree", async () => {
|
|
811
|
+
ctx = createOrchestrator({ maxDepth: 3 })
|
|
812
|
+
const caller = await seedCallerChat(ctx.store)
|
|
813
|
+
|
|
814
|
+
const child1 = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
815
|
+
instruction: "parent task",
|
|
816
|
+
})
|
|
817
|
+
const child2 = await ctx.orchestrator.spawnAgent(child1.chatId, {
|
|
818
|
+
instruction: "nested task",
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
const hierarchy = ctx.orchestrator.getHierarchy(caller.id)
|
|
822
|
+
|
|
823
|
+
expect(hierarchy.children).toHaveLength(1)
|
|
824
|
+
expect(hierarchy.children[0]!.chatId).toBe(child1.chatId)
|
|
825
|
+
expect(hierarchy.children[0]!.children).toHaveLength(1)
|
|
826
|
+
expect(hierarchy.children[0]!.children[0]!.chatId).toBe(child2.chatId)
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
test("tracks codex-native subagent task entries in the hierarchy tree", async () => {
|
|
830
|
+
ctx = createOrchestrator()
|
|
831
|
+
const caller = await seedCallerChat(ctx.store, "codex")
|
|
832
|
+
|
|
833
|
+
ctx.orchestrator.onMessageAppended(caller.id, timestamped({
|
|
834
|
+
kind: "tool_call",
|
|
835
|
+
tool: {
|
|
836
|
+
kind: "tool",
|
|
837
|
+
toolKind: "subagent_task",
|
|
838
|
+
toolName: "Task",
|
|
839
|
+
toolId: "agent-1",
|
|
840
|
+
input: { subagentType: "spawnAgent" },
|
|
841
|
+
rawInput: {
|
|
842
|
+
type: "collabAgentToolCall",
|
|
843
|
+
tool: "spawnAgent",
|
|
844
|
+
status: "completed",
|
|
845
|
+
receiverThreadIds: ["thread-2"],
|
|
846
|
+
prompt: "Inspect tests",
|
|
847
|
+
agentsStates: {
|
|
848
|
+
"thread-2": { status: "running", message: "Inspecting" },
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
},
|
|
852
|
+
} as unknown as TranscriptEntry))
|
|
853
|
+
|
|
854
|
+
const hierarchy = ctx.orchestrator.getHierarchy(caller.id)
|
|
855
|
+
|
|
856
|
+
expect(hierarchy.children).toHaveLength(1)
|
|
857
|
+
expect(hierarchy.children[0]!.chatId).toBe("thread-2")
|
|
858
|
+
expect(hierarchy.children[0]!.externalSessionId).toBe("thread-2")
|
|
859
|
+
expect(hierarchy.children[0]!.instruction).toBe("Inspect tests")
|
|
860
|
+
expect(hierarchy.children[0]!.status).toBe("running")
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
test("updates codex-native subagent status from tool results", async () => {
|
|
864
|
+
ctx = createOrchestrator()
|
|
865
|
+
const caller = await seedCallerChat(ctx.store, "codex")
|
|
866
|
+
|
|
867
|
+
ctx.orchestrator.onMessageAppended(caller.id, timestamped({
|
|
868
|
+
kind: "tool_call",
|
|
869
|
+
tool: {
|
|
870
|
+
kind: "tool",
|
|
871
|
+
toolKind: "subagent_task",
|
|
872
|
+
toolName: "Task",
|
|
873
|
+
toolId: "agent-1",
|
|
874
|
+
input: { subagentType: "spawnAgent" },
|
|
875
|
+
rawInput: {
|
|
876
|
+
type: "collabAgentToolCall",
|
|
877
|
+
tool: "spawnAgent",
|
|
878
|
+
status: "completed",
|
|
879
|
+
receiverThreadIds: ["thread-2"],
|
|
880
|
+
prompt: "Inspect tests",
|
|
881
|
+
agentsStates: {
|
|
882
|
+
"thread-2": { status: "running", message: "Inspecting" },
|
|
883
|
+
},
|
|
884
|
+
},
|
|
885
|
+
},
|
|
886
|
+
} as unknown as TranscriptEntry))
|
|
887
|
+
|
|
888
|
+
ctx.orchestrator.onMessageAppended(caller.id, timestamped({
|
|
889
|
+
kind: "tool_result",
|
|
890
|
+
toolId: "agent-1",
|
|
891
|
+
isError: true,
|
|
892
|
+
content: {
|
|
893
|
+
type: "collabAgentToolCall",
|
|
894
|
+
tool: "spawnAgent",
|
|
895
|
+
status: "failed",
|
|
896
|
+
receiverThreadIds: ["thread-2"],
|
|
897
|
+
prompt: "Inspect tests",
|
|
898
|
+
agentsStates: {
|
|
899
|
+
"thread-2": { status: "failed", message: "Crashed" },
|
|
900
|
+
},
|
|
901
|
+
},
|
|
902
|
+
}))
|
|
903
|
+
|
|
904
|
+
const hierarchy = ctx.orchestrator.getHierarchy(caller.id)
|
|
905
|
+
expect(hierarchy.children[0]!.status).toBe("failed")
|
|
906
|
+
})
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
describe("createOrchestrationMcpServer", () => {
|
|
910
|
+
test("registers spawn, list, send, wait, and close tools", async () => {
|
|
911
|
+
ctx = createOrchestrator()
|
|
912
|
+
const caller = await seedCallerChat(ctx.store)
|
|
913
|
+
|
|
914
|
+
const server = createOrchestrationMcpServer(ctx.orchestrator, caller.id)
|
|
915
|
+
const tools = Object.keys((server.instance as unknown as { _registeredTools: Record<string, unknown> })._registeredTools)
|
|
916
|
+
|
|
917
|
+
expect(tools).toEqual([
|
|
918
|
+
"spawn_agent",
|
|
919
|
+
"list_agents",
|
|
920
|
+
"send_input",
|
|
921
|
+
"wait_agent",
|
|
922
|
+
"close_agent",
|
|
923
|
+
])
|
|
924
|
+
})
|
|
925
|
+
})
|
|
926
|
+
|
|
927
|
+
// =========================================================================
|
|
928
|
+
// delegation
|
|
929
|
+
// =========================================================================
|
|
930
|
+
|
|
931
|
+
describe("delegation", () => {
|
|
932
|
+
test("spawnAgent with blocking mode creates delegation via coordinator", async () => {
|
|
933
|
+
const dc = createFakeDelegationCoordinator()
|
|
934
|
+
ctx = createOrchestrator({ delegationCoordinator: dc })
|
|
935
|
+
const caller = await seedCallerChat(ctx.store)
|
|
936
|
+
|
|
937
|
+
const { chatId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
938
|
+
instruction: "Do blocking work",
|
|
939
|
+
mode: "blocking",
|
|
940
|
+
resume: "gate",
|
|
941
|
+
})
|
|
942
|
+
|
|
943
|
+
expect(dc.createdDelegations).toHaveLength(1)
|
|
944
|
+
expect(dc.createdDelegations[0]!.parentChatId).toBe(caller.id)
|
|
945
|
+
expect(dc.createdDelegations[0]!.childChatId).toBe(chatId)
|
|
946
|
+
expect(dc.createdDelegations[0]!.mode).toBe("blocking")
|
|
947
|
+
expect(dc.createdDelegations[0]!.resume).toBe("gate")
|
|
948
|
+
expect(dc.createdDelegations[0]!.workspaceId).toBe("project-1")
|
|
949
|
+
})
|
|
950
|
+
|
|
951
|
+
test("spawnAgent defaults mode=blocking, resume=gate", async () => {
|
|
952
|
+
const dc = createFakeDelegationCoordinator()
|
|
953
|
+
ctx = createOrchestrator({ delegationCoordinator: dc })
|
|
954
|
+
const caller = await seedCallerChat(ctx.store)
|
|
955
|
+
|
|
956
|
+
await ctx.orchestrator.spawnAgent(caller.id, {
|
|
957
|
+
instruction: "Default modes",
|
|
958
|
+
})
|
|
959
|
+
|
|
960
|
+
expect(dc.createdDelegations).toHaveLength(1)
|
|
961
|
+
expect(dc.createdDelegations[0]!.mode).toBe("blocking")
|
|
962
|
+
expect(dc.createdDelegations[0]!.resume).toBe("gate")
|
|
963
|
+
})
|
|
964
|
+
|
|
965
|
+
test("spawnAgent with background creates delegation with mode=background", async () => {
|
|
966
|
+
const dc = createFakeDelegationCoordinator()
|
|
967
|
+
ctx = createOrchestrator({ delegationCoordinator: dc })
|
|
968
|
+
const caller = await seedCallerChat(ctx.store)
|
|
969
|
+
|
|
970
|
+
await ctx.orchestrator.spawnAgent(caller.id, {
|
|
971
|
+
instruction: "Fire and forget",
|
|
972
|
+
mode: "background",
|
|
973
|
+
})
|
|
974
|
+
|
|
975
|
+
expect(dc.createdDelegations).toHaveLength(1)
|
|
976
|
+
expect(dc.createdDelegations[0]!.mode).toBe("background")
|
|
977
|
+
})
|
|
978
|
+
|
|
979
|
+
test("spawnAgent generates resumeHint when not provided", async () => {
|
|
980
|
+
const dc = createFakeDelegationCoordinator()
|
|
981
|
+
ctx = createOrchestrator({ delegationCoordinator: dc })
|
|
982
|
+
const caller = await seedCallerChat(ctx.store)
|
|
983
|
+
// Seed transcript so generateResumeHint returns a value
|
|
984
|
+
ctx.store.messagesByChatId.set(caller.id, [
|
|
985
|
+
timestamped({ kind: "user_prompt", content: "Some parent context" }),
|
|
986
|
+
])
|
|
987
|
+
|
|
988
|
+
await ctx.orchestrator.spawnAgent(caller.id, {
|
|
989
|
+
instruction: "Needs hint",
|
|
990
|
+
})
|
|
991
|
+
|
|
992
|
+
expect(dc.createdDelegations).toHaveLength(1)
|
|
993
|
+
expect(dc.createdDelegations[0]!.resumeHint).toBe("generated-hint")
|
|
994
|
+
})
|
|
995
|
+
|
|
996
|
+
test("spawnAgent passes explicit resumeHint", async () => {
|
|
997
|
+
const dc = createFakeDelegationCoordinator()
|
|
998
|
+
ctx = createOrchestrator({ delegationCoordinator: dc })
|
|
999
|
+
const caller = await seedCallerChat(ctx.store)
|
|
1000
|
+
ctx.store.messagesByChatId.set(caller.id, [
|
|
1001
|
+
timestamped({ kind: "user_prompt", content: "Some parent context" }),
|
|
1002
|
+
])
|
|
1003
|
+
|
|
1004
|
+
await ctx.orchestrator.spawnAgent(caller.id, {
|
|
1005
|
+
instruction: "Has explicit hint",
|
|
1006
|
+
resumeHint: "my-custom-hint",
|
|
1007
|
+
})
|
|
1008
|
+
|
|
1009
|
+
expect(dc.createdDelegations).toHaveLength(1)
|
|
1010
|
+
expect(dc.createdDelegations[0]!.resumeHint).toBe("my-custom-hint")
|
|
1011
|
+
})
|
|
1012
|
+
|
|
1013
|
+
test("spawnAgent without delegationCoordinator skips delegation creation", async () => {
|
|
1014
|
+
ctx = createOrchestrator() // no delegationCoordinator
|
|
1015
|
+
const caller = await seedCallerChat(ctx.store)
|
|
1016
|
+
|
|
1017
|
+
const { chatId } = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
1018
|
+
instruction: "No coordinator",
|
|
1019
|
+
})
|
|
1020
|
+
|
|
1021
|
+
// Should still work fine — just no delegation tracking
|
|
1022
|
+
expect(chatId).toBeDefined()
|
|
1023
|
+
})
|
|
1024
|
+
|
|
1025
|
+
test("spawnAgent passes correct depth to delegation", async () => {
|
|
1026
|
+
const dc = createFakeDelegationCoordinator()
|
|
1027
|
+
ctx = createOrchestrator({ maxDepth: 3, delegationCoordinator: dc })
|
|
1028
|
+
const caller = await seedCallerChat(ctx.store)
|
|
1029
|
+
|
|
1030
|
+
const child1 = await ctx.orchestrator.spawnAgent(caller.id, {
|
|
1031
|
+
instruction: "depth-1",
|
|
1032
|
+
})
|
|
1033
|
+
await ctx.orchestrator.spawnAgent(child1.chatId, {
|
|
1034
|
+
instruction: "depth-2",
|
|
1035
|
+
})
|
|
1036
|
+
|
|
1037
|
+
expect(dc.createdDelegations).toHaveLength(2)
|
|
1038
|
+
expect(dc.createdDelegations[0]!.depth).toBe(1)
|
|
1039
|
+
expect(dc.createdDelegations[1]!.depth).toBe(2)
|
|
1040
|
+
})
|
|
1041
|
+
|
|
1042
|
+
test("spawn_agent MCP tool accepts mode + resume params", async () => {
|
|
1043
|
+
const dc = createFakeDelegationCoordinator()
|
|
1044
|
+
ctx = createOrchestrator({ delegationCoordinator: dc })
|
|
1045
|
+
const caller = await seedCallerChat(ctx.store)
|
|
1046
|
+
|
|
1047
|
+
const server = createOrchestrationMcpServer(ctx.orchestrator, caller.id)
|
|
1048
|
+
// Call spawn_agent through MCP with mode + resume
|
|
1049
|
+
const result = await (server.instance as unknown as {
|
|
1050
|
+
_registeredTools: Record<string, { handler: (args: Record<string, unknown>) => Promise<unknown> }>
|
|
1051
|
+
})._registeredTools["spawn_agent"]!.handler({
|
|
1052
|
+
instruction: "MCP delegation task",
|
|
1053
|
+
mode: "background",
|
|
1054
|
+
resume: "immediate",
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
expect(dc.createdDelegations).toHaveLength(1)
|
|
1058
|
+
expect(dc.createdDelegations[0]!.mode).toBe("background")
|
|
1059
|
+
expect(dc.createdDelegations[0]!.resume).toBe("immediate")
|
|
1060
|
+
expect(result).toBeDefined()
|
|
1061
|
+
})
|
|
1062
|
+
})
|
|
1063
|
+
})
|