@desplega.ai/agent-swarm 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/{acp-adapter-dgfakfpw.js → acp-adapter-ma1js5n7.js} +6 -6
- package/dist/{actions-fbqma8aa.js → actions-1vhmtvg9.js} +18 -18
- package/dist/{amp-adapter-en0jy6hs.js → amp-adapter-sxqqvmdk.js} +3 -3
- package/dist/{app-k8a5cgpp.js → app-mwx1d5a9.js} +3 -3
- package/dist/{assistant-a16433gr.js → assistant-yx1p6g82.js} +20 -20
- package/dist/{boot-reembed-f4nn561n.js → boot-reembed-5h37a2c2.js} +1 -1
- package/dist/{boot-reembed-6c1sjfnr.js → boot-reembed-7h78vmvv.js} +2 -1
- package/dist/{boot-scrub-sweep-d4w9gajf.js → boot-scrub-sweep-8mawd4gw.js} +3 -3
- package/dist/{boot-seeds-9bmkspyw.js → boot-seeds-ca84dhnn.js} +4 -4
- package/dist/{claude-adapter-nys1kjxa.js → claude-adapter-wqt4hfkq.js} +7 -7
- package/dist/{claude-managed-adapter-55207973.js → claude-managed-adapter-6fjqtr2z.js} +2 -2
- package/dist/{claude-managed-setup-3cm35gey.js → claude-managed-setup-rqv98sea.js} +2 -2
- package/dist/{claude-sdk-session-nw5yd8cp.js → claude-sdk-session-f29f3b2x.js} +7 -7
- package/dist/{cli-sxgtf5fw.js → cli-07q03xm2.js} +1 -1
- package/dist/{cli-k21gx5ht.js → cli-1hhbzkbb.js} +6 -6
- package/dist/{cli-v49fkghp.js → cli-1m9x8dr5.js} +1 -1
- package/dist/{cli-24j1jeeb.js → cli-2pvzmas1.js} +3 -3
- package/dist/{cli-w2656ka1.js → cli-2y7v133v.js} +7 -7
- package/dist/{cli-g8ka5j4j.js → cli-48gxjmcz.js} +94 -36
- package/dist/{cli-2sjhpsns.js → cli-54sj64fc.js} +1 -1
- package/dist/{cli-aj8djyqt.js → cli-60sty27p.js} +2 -2
- package/dist/{cli-4hxrf0p4.js → cli-6a9qfa8b.js} +308 -47
- package/dist/{cli-n7asmce6.js → cli-6mgdba42.js} +3 -3
- package/dist/{cli-gf95q3bm.js → cli-6srv653c.js} +3 -3
- package/dist/{cli-9abdgn5t.js → cli-86ehn4s5.js} +3 -3
- package/dist/{cli-bqztfyve.js → cli-8mbc9za9.js} +1 -1
- package/dist/{cli-a2rr0fmg.js → cli-8wmj18cz.js} +1 -1
- package/dist/{cli-c2habvsf.js → cli-9gsm2rzp.js} +8 -8
- package/dist/{cli-4rf0f5vg.js → cli-9pshx6rd.js} +4 -2
- package/dist/{cli-abkm71vr.js → cli-a6ds2epe.js} +1 -1
- package/dist/{cli-esakngh8.js → cli-a89zdqz9.js} +1 -1
- package/dist/{cli-3ndmpxxv.js → cli-ay40dnj3.js} +2 -2
- package/dist/{cli-kf4jree9.js → cli-aypd09nf.js} +1 -1
- package/dist/{cli-6y51ryva.js → cli-b3yzyc57.js} +1 -1
- package/dist/{cli-ag2yrnt5.js → cli-c668vvcf.js} +2 -2
- package/dist/{cli-5sckpqcx.js → cli-d20qvpr2.js} +1 -1
- package/dist/{cli-a34pr3w1.js → cli-e1595bgx.js} +2 -2
- package/dist/{cli-m70bajzr.js → cli-f12mr20z.js} +1 -1
- package/dist/{cli-dqygeeh5.js → cli-f1e0ptjj.js} +10 -1
- package/dist/{cli-mvjz5an3.js → cli-f4dbh7a5.js} +7 -7
- package/dist/{cli-az25fn5f.js → cli-gsfk58nw.js} +2 -2
- package/dist/{cli-1tem6ns9.js → cli-gt4h8cxw.js} +3 -2
- package/dist/{cli-cz37jqss.js → cli-h5jeg46p.js} +2 -2
- package/dist/{cli-emhjcj2y.js → cli-he9gbdx0.js} +60 -10
- package/dist/{cli-3fwqy71j.js → cli-j0ppywdr.js} +38 -31
- package/dist/{cli-5rj83bam.js → cli-j3y0x2n4.js} +34 -33
- package/dist/{cli-3gfvmzht.js → cli-j9bpmykh.js} +73 -25
- package/dist/{cli-m5a9m0ak.js → cli-kv7sbc2c.js} +3 -3
- package/dist/{cli-g20jjwt4.js → cli-m30k9777.js} +12 -12
- package/dist/{cli-683y0b3a.js → cli-m5e802r7.js} +2 -2
- package/dist/{cli-nn6vx2mq.js → cli-ptfq202x.js} +5 -3
- package/dist/{cli-d2dpf95n.js → cli-qj442fws.js} +1 -1
- package/dist/{cli-03gg9pd1.js → cli-rd5m70gj.js} +4 -4
- package/dist/{cli-gnsb1zxa.js → cli-s09j9vp5.js} +1 -1
- package/dist/{cli-zqne1qcj.js → cli-s0mgx214.js} +2 -2
- package/dist/{cli-sqma6vw1.js → cli-sgqm2582.js} +5 -5
- package/dist/{cli-nq677csy.js → cli-sjctjr48.js} +1 -1
- package/dist/{cli-3aqd4jaa.js → cli-twctnkng.js} +3 -3
- package/dist/{cli-j6rktc91.js → cli-v6shg7na.js} +1 -1
- package/dist/{cli-7enjwya9.js → cli-vz78s5wm.js} +1 -1
- package/dist/{cli-art7c1zh.js → cli-wqr7cea4.js} +4 -4
- package/dist/{cli-hfeb68z4.js → cli-x37r7qwf.js} +11 -3
- package/dist/{cli-ffvsj2qw.js → cli-yd2nreqw.js} +2 -2
- package/dist/{cli-ca6banwz.js → cli-ymjpkxgn.js} +6 -6
- package/dist/{cli-mtnj4fwa.js → cli-yxbsmx56.js} +5 -5
- package/dist/cli.js +18 -16
- package/dist/{codex-adapter-d95q4qsn.js → codex-adapter-zk59x2zh.js} +5 -5
- package/dist/{codex-hook-brpnycn5.js → codex-hook-xyjhvxwd.js} +1 -1
- package/dist/{codex-session-runner-gdwd8h0c.js → codex-session-runner-kh6s4j3n.js} +5 -5
- package/dist/{commands-jqmgk54b.js → commands-68et1qv2.js} +2 -2
- package/dist/{controller-z3jq589f.js → controller-fyrbnzg4.js} +2 -2
- package/dist/{controllers-connect-r2za39ef.js → controllers-connect-nep9h2xv.js} +19 -19
- package/dist/{cursor-adapter-4mr4jgkw.js → cursor-adapter-yss3pg7t.js} +1 -1
- package/dist/{db-w36wwb5g.js → db-28gjyvxj.js} +2 -2
- package/dist/{db-ppv2tze7.js → db-8ftwvzba.js} +3 -3
- package/dist/{dispatcher-pv5h77vf.js → dispatcher-hayexmkr.js} +17 -17
- package/dist/{dsh-adapter-d4pdpb3r.js → dsh-adapter-46wawff9.js} +1 -1
- package/dist/{easter-egg-3d-4sp4dvmc.js → easter-egg-3d-8d1e0h4m.js} +1 -1
- package/dist/{execute-jev839y1.js → execute-cjrfxfnm.js} +1 -1
- package/dist/{external-channel-y6nkzhnn.js → external-channel-xbmxcg09.js} +2 -2
- package/dist/{grok-adapter-t08b9ntm.js → grok-adapter-ag4wks2a.js} +7 -7
- package/dist/handlers-tzmevqb5.js +41 -0
- package/dist/{heartbeat-vn31xwdk.js → heartbeat-psfvcgwp.js} +20 -20
- package/dist/{hook-6d0zbvvt.js → hook-8pz04697.js} +5 -5
- package/dist/{hook-cpjs8q51.js → hook-cyq4mryn.js} +6 -6
- package/dist/{http-9c6medda.js → http-haj1nd6k.js} +109 -114
- package/dist/{index-qhjf2htm.js → index-jwt2bj6f.js} +261 -96
- package/dist/{index-g6k9tctj.js → index-q52f47pe.js} +1 -1
- package/dist/{index-pkma1yf8.js → index-syvhnvtw.js} +1 -1
- package/dist/{index-yqgbfgaj.js → index-y1ajx04a.js} +20 -20
- package/dist/{index-k0yt1tvq.js → index-z5h22pes.js} +17 -17
- package/dist/{index-38t35h0n.js → index-zyq8ct52.js} +17 -17
- package/dist/{keepalive-em0117bp.js → keepalive-xdvsamvj.js} +3 -3
- package/dist/{lead-23e7z59t.js → lead-hqg57kdp.js} +35 -35
- package/dist/{maintenance-kbme3qy9.js → maintenance-4y2xzh3a.js} +1 -1
- package/dist/{oauth-refresh-sweep-xysj4yse.js → oauth-refresh-sweep-jd4r7zbs.js} +8 -3
- package/dist/{onboard-p6q0vz97.js → onboard-18447q9q.js} +5 -5
- package/dist/{opencode-adapter-esv3k9yn.js → opencode-adapter-26b2frdf.js} +5 -5
- package/dist/{otel-impl-54qhqh1z.js → otel-impl-f9tgr8yw.js} +1 -1
- package/dist/{pi-mono-adapter-mymbwhj0.js → pi-mono-adapter-qn6qbga1.js} +33 -12
- package/dist/{pricing-refresh-yb75wv33.js → pricing-refresh-dw2tgb53.js} +3 -3
- package/dist/{rbac-roles-1vp2mw4p.js → rbac-roles-aygm2760.js} +2 -2
- package/dist/{rbac-roles-dexhc9x8.js → rbac-roles-s4nwj2m3.js} +1 -1
- package/dist/{render-v2-tjay5hhf.js → render-v2-yfvdfr0x.js} +3 -3
- package/dist/{resume-8ayf37g5.js → resume-1bmvjbf3.js} +7 -7
- package/dist/{runtime-hqe5cqcd.js → runtime-f7tpzedy.js} +1 -1
- package/dist/{session-access-fdhp987s.js → session-access-r5garej6.js} +4 -4
- package/dist/{session-source-backfill-qgtc86z1.js → session-source-backfill-424wzv0r.js} +2 -2
- package/dist/{setup-4rrmqcq3.js → setup-8tygvxkq.js} +1 -1
- package/dist/{virtual-modules-52km00d6.js → virtual-modules-xsw323b3.js} +2 -2
- package/dist/{work-objects-p28fgd70.js → work-objects-ggnghpgf.js} +20 -20
- package/dist/{worker-e3w7vye1.js → worker-sqjd1nqf.js} +35 -35
- package/openapi.json +69 -1
- package/package.json +5 -3
- package/src/be/automation-preflight.ts +2 -0
- 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/task-learning.ts +155 -0
- package/src/be/migrations/245_script_executor.sql +3 -0
- package/src/be/oauth-refresh-sweep.ts +2 -1
- package/src/be/scripts/boot-reembed.ts +2 -0
- package/src/be/scripts/db.ts +14 -4
- package/src/be/scripts/embeddings.ts +2 -0
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +9 -3
- package/src/be/seed-skills/index.ts +7 -0
- package/src/commands/onboard/compose-generator.ts +5 -3
- package/src/commands/provider-credentials.ts +2 -1
- package/src/heartbeat/heartbeat.ts +14 -1
- package/src/heartbeat/script-runner-probe.ts +47 -0
- package/src/http/api-keys.ts +24 -9
- package/src/http/idempotency-key.ts +1 -1
- package/src/http/index.ts +3 -3
- package/src/http/pages-public.ts +38 -5
- package/src/http/scripts.ts +27 -0
- package/src/http/workflows.ts +10 -0
- package/src/http/x.ts +5 -0
- package/src/prompts/base-prompt.ts +21 -1
- package/src/prompts/session-templates.ts +17 -3
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +41 -11
- 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/api-key-status-by-suffix.test.ts +242 -0
- package/src/tests/base-prompt.test.ts +70 -2
- 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.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/heartbeat-checklist.test.ts +89 -0
- package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
- package/src/tests/oauth-refresh-sweep.test.ts +34 -1
- package/src/tests/onboard-compose.test.ts +20 -0
- 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-native-mcp.test.ts +13 -1
- package/src/tests/pi-tool-exposure.test.ts +8 -8
- package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
- package/src/tests/prompt-template-session.test.ts +4 -3
- package/src/tests/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/store-progress-learning.test.ts +288 -0
- package/src/tests/ui-logs-parser.test.ts +180 -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/script-common.ts +1 -0
- package/src/tools/script-run.ts +7 -0
- package/src/tools/script-upsert.ts +7 -0
- package/src/tools/store-progress.ts +67 -1
- package/src/tools/workflows/trigger-workflow.ts +104 -10
- package/src/types.ts +12 -1
- package/src/utils/credentials.ts +5 -2
- 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
|
@@ -1859,3 +1859,57 @@ describe("PiMonoSession context-bound note", () => {
|
|
|
1859
1859
|
expect(await lateSteers([assistantTurn(usage(5000))])).toEqual([HELD_NOTE_TEXT]);
|
|
1860
1860
|
});
|
|
1861
1861
|
});
|
|
1862
|
+
|
|
1863
|
+
describe("PiMonoSession — per-call usage line", () => {
|
|
1864
|
+
function assistantEnd(content: unknown[], usage?: unknown): AgentSessionEvent {
|
|
1865
|
+
return {
|
|
1866
|
+
type: "message_end",
|
|
1867
|
+
message: { role: "assistant", content, stopReason: "stop", ...(usage ? { usage } : {}) },
|
|
1868
|
+
} as unknown as AgentSessionEvent;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
test("one call_usage raw_log line per assistant message_end with usage", async () => {
|
|
1872
|
+
const { events } = await runWithEvents([
|
|
1873
|
+
assistantEnd([{ type: "toolCall", id: "call-1", name: "bash", arguments: {} }], {
|
|
1874
|
+
input: 120,
|
|
1875
|
+
cacheRead: 9_000,
|
|
1876
|
+
cacheWrite: 400,
|
|
1877
|
+
output: 60,
|
|
1878
|
+
reasoning: 12,
|
|
1879
|
+
totalTokens: 9_580,
|
|
1880
|
+
}),
|
|
1881
|
+
{
|
|
1882
|
+
type: "message_end",
|
|
1883
|
+
message: { role: "user", content: [{ type: "text", text: "tool output" }] },
|
|
1884
|
+
} as unknown as AgentSessionEvent,
|
|
1885
|
+
// No usage (some providers omit it): no line.
|
|
1886
|
+
assistantEnd([{ type: "text", text: "thinking out loud" }]),
|
|
1887
|
+
assistantEnd([{ type: "text", text: "Done" }], { input: 5, cacheRead: 9_500, output: 3 }),
|
|
1888
|
+
]);
|
|
1889
|
+
|
|
1890
|
+
const raw = events.flatMap((e) =>
|
|
1891
|
+
e.type === "raw_log" ? [JSON.parse(e.content) as Record<string, unknown>] : [],
|
|
1892
|
+
);
|
|
1893
|
+
const usageLines = raw.filter((line) => line.type === "call_usage");
|
|
1894
|
+
expect(usageLines).toEqual([
|
|
1895
|
+
{
|
|
1896
|
+
type: "call_usage",
|
|
1897
|
+
harness: "pi",
|
|
1898
|
+
usage: { input: 120, cacheRead: 9_000, cacheWrite: 400, output: 60, reasoning: 12 },
|
|
1899
|
+
},
|
|
1900
|
+
{
|
|
1901
|
+
type: "call_usage",
|
|
1902
|
+
harness: "pi",
|
|
1903
|
+
usage: { input: 5, cacheRead: 9_500, cacheWrite: 0, output: 3, reasoning: 0 },
|
|
1904
|
+
},
|
|
1905
|
+
]);
|
|
1906
|
+
// The usage line precedes the call's own text line.
|
|
1907
|
+
const done = raw.findIndex(
|
|
1908
|
+
(line) => line.type === "assistant" && JSON.stringify(line).includes('"Done"'),
|
|
1909
|
+
);
|
|
1910
|
+
expect(done).toBeGreaterThan(0);
|
|
1911
|
+
expect(raw[done - 1]).toEqual(usageLines[1]);
|
|
1912
|
+
// Token counts only: no message content rides on the line.
|
|
1913
|
+
expect(JSON.stringify(usageLines)).not.toContain("tool output");
|
|
1914
|
+
});
|
|
1915
|
+
});
|
|
@@ -30,6 +30,17 @@ describe("toPiMcpServers", () => {
|
|
|
30
30
|
});
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
test("deferred maps every installed server to deferred exposure", () => {
|
|
34
|
+
const servers = toPiMcpServers(
|
|
35
|
+
{
|
|
36
|
+
local: { command: "context-mode" },
|
|
37
|
+
remote: { url: "https://mcp.example.com/mcp" },
|
|
38
|
+
},
|
|
39
|
+
{ deferred: true },
|
|
40
|
+
);
|
|
41
|
+
expect(Object.values(servers).map((s) => s.exposure)).toEqual(["deferred", "deferred"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
33
44
|
test("no installed servers maps to an empty set", () => {
|
|
34
45
|
expect(toPiMcpServers(null)).toEqual({});
|
|
35
46
|
});
|
|
@@ -70,7 +81,8 @@ describe("PiMonoAdapter.createSession — native MCP for installed servers", ()
|
|
|
70
81
|
process.env.OPENROUTER_API_KEY = "example-test-key";
|
|
71
82
|
// The session prompts on creation; keep that request off the network.
|
|
72
83
|
process.env.OPENROUTER_BASE_URL = "http://127.0.0.1:9/api/v1";
|
|
73
|
-
|
|
84
|
+
// Direct exposure, so installed tools show up as active tool names.
|
|
85
|
+
process.env.PI_TOOL_DEFERRAL = "false";
|
|
74
86
|
mkdirSync(join(dir, "agent"), { recursive: true });
|
|
75
87
|
});
|
|
76
88
|
afterEach(() => {
|
|
@@ -176,8 +176,8 @@ describe("pi resource loader trust", () => {
|
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
describe("PI_TOOL_DEFERRAL flag", () => {
|
|
179
|
-
test("defaults
|
|
180
|
-
expect(isPiToolDeferralEnabled({})).toBe(
|
|
179
|
+
test("defaults on and parses boolean literals", () => {
|
|
180
|
+
expect(isPiToolDeferralEnabled({})).toBe(true);
|
|
181
181
|
expect(isPiToolDeferralEnabled({ PI_TOOL_DEFERRAL: "true" })).toBe(true);
|
|
182
182
|
expect(isPiToolDeferralEnabled({ PI_TOOL_DEFERRAL: "1" })).toBe(true);
|
|
183
183
|
expect(isPiToolDeferralEnabled({ PI_TOOL_DEFERRAL: "false" })).toBe(false);
|
|
@@ -187,9 +187,9 @@ describe("PI_TOOL_DEFERRAL flag", () => {
|
|
|
187
187
|
"traits.hasToolSearch follows the flag live",
|
|
188
188
|
withEnv("PI_TOOL_DEFERRAL", undefined, () => {
|
|
189
189
|
const adapter = new PiMonoAdapter();
|
|
190
|
-
expect(adapter.traits.hasToolSearch).toBe(false);
|
|
191
|
-
process.env.PI_TOOL_DEFERRAL = "true";
|
|
192
190
|
expect(adapter.traits.hasToolSearch).toBe(true);
|
|
191
|
+
process.env.PI_TOOL_DEFERRAL = "false";
|
|
192
|
+
expect(adapter.traits.hasToolSearch).toBe(false);
|
|
193
193
|
}),
|
|
194
194
|
);
|
|
195
195
|
});
|
|
@@ -259,8 +259,8 @@ describe("PiMonoAdapter.createSession — tool deferral wiring", () => {
|
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
test(
|
|
262
|
-
"flag
|
|
263
|
-
withEnv("PI_TOOL_DEFERRAL",
|
|
262
|
+
"flag unset: non-core tools deferred and tool_search enabled by default",
|
|
263
|
+
withEnv("PI_TOOL_DEFERRAL", undefined, async () => {
|
|
264
264
|
const opts = await captureSessionOptions();
|
|
265
265
|
const tools = opts.customTools as ToolDefinition[];
|
|
266
266
|
expect(Object.fromEntries(tools.map((t) => [t.name, t.exposure]))).toEqual({
|
|
@@ -274,8 +274,8 @@ describe("PiMonoAdapter.createSession — tool deferral wiring", () => {
|
|
|
274
274
|
);
|
|
275
275
|
|
|
276
276
|
test(
|
|
277
|
-
"flag
|
|
278
|
-
withEnv("PI_TOOL_DEFERRAL",
|
|
277
|
+
"flag false: every tool declared and no tool_search",
|
|
278
|
+
withEnv("PI_TOOL_DEFERRAL", "false", async () => {
|
|
279
279
|
const opts = await captureSessionOptions();
|
|
280
280
|
const tools = opts.customTools as ToolDefinition[];
|
|
281
281
|
expect(tools.map((t) => t.exposure)).toEqual([undefined, undefined, undefined]);
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
bareToolName,
|
|
7
|
+
controlReached,
|
|
8
|
+
countSwarmTools,
|
|
9
|
+
hasControlResult,
|
|
10
|
+
inputTokens,
|
|
11
|
+
toolNames,
|
|
12
|
+
turnOne,
|
|
13
|
+
withSessionDirs,
|
|
14
|
+
} from "../../scripts/probe-tool-schema-tokens";
|
|
15
|
+
|
|
16
|
+
describe("probe-tool-schema-tokens", () => {
|
|
17
|
+
let dir = "";
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
if (dir) rmSync(dir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("matches swarm tools under every harness naming", () => {
|
|
23
|
+
const swarm = new Set(["get_swarm", "store_progress"]);
|
|
24
|
+
expect(bareToolName("mcp__agent_swarm__get_swarm")).toBe("get_swarm");
|
|
25
|
+
expect(bareToolName("agent-swarm_store-progress")).toBe("store_progress");
|
|
26
|
+
expect(
|
|
27
|
+
countSwarmTools(["get-swarm", "mcp__agent_swarm__store_progress", "bash", "read"], swarm),
|
|
28
|
+
).toBe(2);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("lists tools in chat-completions and namespaced responses bodies", () => {
|
|
32
|
+
expect(toolNames({ tools: [{ type: "function", function: { name: "get-swarm" } }] })).toEqual([
|
|
33
|
+
"get-swarm",
|
|
34
|
+
]);
|
|
35
|
+
const responses = {
|
|
36
|
+
input: [
|
|
37
|
+
{
|
|
38
|
+
type: "additional_tools",
|
|
39
|
+
tools: [{ type: "namespace", name: "functions", tools: [{ name: "exec" }] }],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
tools: [{ name: "wait" }],
|
|
43
|
+
};
|
|
44
|
+
expect(toolNames(responses).sort()).toEqual(["exec", "wait"]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("reads the last reported input token count from a stream", () => {
|
|
48
|
+
const sse = 'data: {"usage":null}\n\ndata: {"usage":{"prompt_tokens":90776}}\n';
|
|
49
|
+
expect(inputTokens(sse)).toBe(90776);
|
|
50
|
+
expect(inputTokens('{"response":{"usage":{"input_tokens":16629}}}')).toBe(16629);
|
|
51
|
+
expect(inputTokens("data: [DONE]")).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("turn one skips side requests for other models", () => {
|
|
55
|
+
dir = mkdtempSync(join(tmpdir(), "probe-tool-schema-"));
|
|
56
|
+
writeFileSync(join(dir, "req-1.json"), JSON.stringify({ model: "openai/gpt-6-luna" }));
|
|
57
|
+
writeFileSync(join(dir, "res-1.txt"), '{"usage":{"prompt_tokens":453}}');
|
|
58
|
+
writeFileSync(
|
|
59
|
+
join(dir, "req-2.json"),
|
|
60
|
+
JSON.stringify({ model: "deepseek/deepseek-v4.1-flash", tools: [{ name: "bash" }] }),
|
|
61
|
+
);
|
|
62
|
+
writeFileSync(join(dir, "res-2.txt"), '{"usage":{"prompt_tokens":33115}}');
|
|
63
|
+
expect(turnOne(dir, "openrouter/deepseek/deepseek-v4.1-flash")).toEqual({
|
|
64
|
+
tokens: 33115,
|
|
65
|
+
names: ["bash"],
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const found = "Found 5 agent(s) in the swarm. Requested by session: s1";
|
|
70
|
+
|
|
71
|
+
test("control passes only on a get-swarm tool output", () => {
|
|
72
|
+
dir = mkdtempSync(join(tmpdir(), "probe-tool-schema-"));
|
|
73
|
+
// The schema alone carries "in the swarm" in get-swarm's description.
|
|
74
|
+
const schemaOnly = {
|
|
75
|
+
model: "m",
|
|
76
|
+
tools: [
|
|
77
|
+
{
|
|
78
|
+
type: "function",
|
|
79
|
+
function: { name: "agent-swarm_get-swarm", description: "Lists agents in the swarm." },
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
messages: [{ role: "user", content: "Call get-swarm. Found 5 agent(s) in the swarm." }],
|
|
83
|
+
};
|
|
84
|
+
expect(hasControlResult(schemaOnly)).toBe(false);
|
|
85
|
+
writeFileSync(join(dir, "req-1.json"), JSON.stringify(schemaOnly));
|
|
86
|
+
expect(controlReached(dir)).toBe(false);
|
|
87
|
+
|
|
88
|
+
const chat = {
|
|
89
|
+
...schemaOnly,
|
|
90
|
+
messages: [
|
|
91
|
+
{
|
|
92
|
+
role: "assistant",
|
|
93
|
+
tool_calls: [{ id: "c1", type: "function", function: { name: "agent-swarm_get-swarm" } }],
|
|
94
|
+
},
|
|
95
|
+
{ role: "tool", tool_call_id: "c1", content: [{ type: "text", text: found }] },
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
expect(hasControlResult(chat)).toBe(true);
|
|
99
|
+
writeFileSync(join(dir, "req-2.json"), JSON.stringify(chat));
|
|
100
|
+
expect(controlReached(dir)).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("control reads Responses items and rejects other tools and failed calls", () => {
|
|
104
|
+
const responses = (name: string, output: string) => ({
|
|
105
|
+
input: [
|
|
106
|
+
{ type: "function_call", name, call_id: "call_1", arguments: "{}" },
|
|
107
|
+
{ type: "function_call_output", call_id: "call_1", output },
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
expect(hasControlResult(responses("mcp__agent_swarm__get_swarm", found))).toBe(true);
|
|
111
|
+
expect(hasControlResult(responses("mcp__agent_swarm__get_tasks", found))).toBe(false);
|
|
112
|
+
expect(hasControlResult(responses("mcp__agent_swarm__get_swarm", "Agent ID required."))).toBe(
|
|
113
|
+
false,
|
|
114
|
+
);
|
|
115
|
+
// An output for a different call id does not count.
|
|
116
|
+
const mismatched = responses("mcp__agent_swarm__get_swarm", found);
|
|
117
|
+
mismatched.input[1] = { type: "function_call_output", call_id: "call_2", output: found };
|
|
118
|
+
expect(hasControlResult(mismatched)).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("session dirs: home is removed on success and on error, captures stay", async () => {
|
|
122
|
+
dir = mkdtempSync(join(tmpdir(), "probe-tool-schema-"));
|
|
123
|
+
let home = "";
|
|
124
|
+
const capDir = await withSessionDirs(dir, "ok", async (cap, h) => {
|
|
125
|
+
home = h;
|
|
126
|
+
writeFileSync(join(h, "config.toml"), "x");
|
|
127
|
+
writeFileSync(join(cap, "req-1.json"), "{}");
|
|
128
|
+
return cap;
|
|
129
|
+
});
|
|
130
|
+
expect(existsSync(home)).toBe(false);
|
|
131
|
+
expect(existsSync(join(capDir, "req-1.json"))).toBe(true);
|
|
132
|
+
|
|
133
|
+
await expect(
|
|
134
|
+
withSessionDirs(dir, "boom", async (cap, h) => {
|
|
135
|
+
home = h;
|
|
136
|
+
writeFileSync(join(h, "config.toml"), "x");
|
|
137
|
+
writeFileSync(join(cap, "req-1.json"), "{}");
|
|
138
|
+
throw new Error("codex exec exited 1");
|
|
139
|
+
}),
|
|
140
|
+
).rejects.toThrow("codex exec exited 1");
|
|
141
|
+
expect(existsSync(home)).toBe(false);
|
|
142
|
+
expect(existsSync(join(dir, "boom-cap", "req-1.json"))).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -25,6 +25,7 @@ const SYSTEM_TEMPLATES = [
|
|
|
25
25
|
"system.agent.context.bound_note",
|
|
26
26
|
"system.agent.cursor.first_message",
|
|
27
27
|
"system.agent.lead",
|
|
28
|
+
"system.agent.lead.docs",
|
|
28
29
|
"system.agent.memory",
|
|
29
30
|
"system.agent.memory.remote",
|
|
30
31
|
"system.agent.outputs",
|
|
@@ -118,11 +119,11 @@ describe("Session templates: registration", () => {
|
|
|
118
119
|
expect(sessionSystemEventTypes("session")).toEqual([...SESSION_TEMPLATES].sort());
|
|
119
120
|
});
|
|
120
121
|
|
|
121
|
-
test("registers
|
|
122
|
+
test("registers 33 system and session templates in total", () => {
|
|
122
123
|
const all = getAllTemplateDefinitions();
|
|
123
124
|
const sessionSystem = all.filter((d) => d.category === "system" || d.category === "session");
|
|
124
|
-
//
|
|
125
|
-
expect(sessionSystem.length).toBe(
|
|
125
|
+
// 28 system blocks + 5 session composites.
|
|
126
|
+
expect(sessionSystem.length).toBe(33);
|
|
126
127
|
});
|
|
127
128
|
|
|
128
129
|
test("drops the v1 blocks that prompt v2 deleted", () => {
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
type CompositionCall,
|
|
4
|
+
type CompositionManifest,
|
|
5
|
+
compositionCallsOf,
|
|
6
|
+
deriveCallCompositions,
|
|
7
|
+
staticTokens,
|
|
8
|
+
} from "../../apps/ui/src/lib/request-composition";
|
|
9
|
+
import type { CallUsage, NormalizedItem } from "../../apps/ui/src/logs-parser/types";
|
|
10
|
+
|
|
11
|
+
const T0 = Date.parse("2026-10-26T10:00:00.000Z");
|
|
12
|
+
|
|
13
|
+
function manifest(overrides: Partial<CompositionManifest> = {}): CompositionManifest {
|
|
14
|
+
return {
|
|
15
|
+
id: "m-0",
|
|
16
|
+
taskId: "task-1",
|
|
17
|
+
agentId: "agent-1",
|
|
18
|
+
sessionId: "s-1",
|
|
19
|
+
seq: 0,
|
|
20
|
+
provider: "claude",
|
|
21
|
+
model: null,
|
|
22
|
+
source: "worker",
|
|
23
|
+
parts: [
|
|
24
|
+
{
|
|
25
|
+
kind: "system_section",
|
|
26
|
+
id: "system.agent.role",
|
|
27
|
+
label: "role",
|
|
28
|
+
chars: 4000,
|
|
29
|
+
estTokens: 1000,
|
|
30
|
+
fidelity: "exact",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
kind: "persona",
|
|
34
|
+
id: "persona",
|
|
35
|
+
label: "persona",
|
|
36
|
+
chars: 8000,
|
|
37
|
+
estTokens: 2000,
|
|
38
|
+
fidelity: "exact",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
kind: "tool_schemas",
|
|
42
|
+
id: "mcp:agent-swarm",
|
|
43
|
+
label: "tools",
|
|
44
|
+
chars: 40000,
|
|
45
|
+
estTokens: 10000,
|
|
46
|
+
fidelity: "estimate",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
kind: "skills_listing",
|
|
50
|
+
id: "skills_listing",
|
|
51
|
+
label: "skills",
|
|
52
|
+
chars: 4000,
|
|
53
|
+
estTokens: 1000,
|
|
54
|
+
fidelity: "estimate",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
kind: "task_prompt",
|
|
58
|
+
id: "task_prompt",
|
|
59
|
+
label: "task",
|
|
60
|
+
chars: 2000,
|
|
61
|
+
estTokens: 500,
|
|
62
|
+
fidelity: "exact",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
kind: "memories",
|
|
66
|
+
id: "memories",
|
|
67
|
+
label: "memories",
|
|
68
|
+
chars: 2000,
|
|
69
|
+
estTokens: 500,
|
|
70
|
+
fidelity: "exact",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
staticEstTokens: 15000,
|
|
74
|
+
tokenRatio: 1,
|
|
75
|
+
createdAt: new Date(T0 - 1000).toISOString(),
|
|
76
|
+
...overrides,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function call(
|
|
81
|
+
callId: string,
|
|
82
|
+
promptTokens: number,
|
|
83
|
+
offsetMs: number,
|
|
84
|
+
extra: Partial<CallUsage> = {},
|
|
85
|
+
sessionId = "s-1",
|
|
86
|
+
): CompositionCall {
|
|
87
|
+
return {
|
|
88
|
+
sessionId,
|
|
89
|
+
t: T0 + offsetMs,
|
|
90
|
+
usage: {
|
|
91
|
+
callId,
|
|
92
|
+
promptTokens,
|
|
93
|
+
cacheReadTokens: 0,
|
|
94
|
+
cacheWriteTokens: 0,
|
|
95
|
+
uncachedInputTokens: promptTokens,
|
|
96
|
+
outputTokens: 10,
|
|
97
|
+
sidechain: false,
|
|
98
|
+
harness: "claude",
|
|
99
|
+
reportedAt: "start",
|
|
100
|
+
...extra,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function sum(tokens: { tokens: number }[]): number {
|
|
106
|
+
return tokens.reduce((acc, s) => acc + s.tokens, 0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function tokensOf(segments: { group: string; tokens: number }[], group: string): number {
|
|
110
|
+
return segments.find((s) => s.group === group)?.tokens ?? 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
describe("deriveCallCompositions", () => {
|
|
114
|
+
test("segments sum to each call's prompt tokens", () => {
|
|
115
|
+
const { byCall } = deriveCallCompositions(
|
|
116
|
+
[call("c1", 40_000, 0), call("c2", 52_000, 1000), call("c3", 90_000, 2000)],
|
|
117
|
+
[manifest()],
|
|
118
|
+
);
|
|
119
|
+
for (const [id, prompt] of [
|
|
120
|
+
["c1", 40_000],
|
|
121
|
+
["c2", 52_000],
|
|
122
|
+
["c3", 90_000],
|
|
123
|
+
] as const) {
|
|
124
|
+
const comp = byCall.get(id);
|
|
125
|
+
expect(comp).toBeDefined();
|
|
126
|
+
expect(sum(comp!.segments)).toBeCloseTo(prompt, 6);
|
|
127
|
+
expect(comp!.scaled).toBe(false);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("the first main call anchors the harness base; later calls add history", () => {
|
|
132
|
+
const { byCall, anchors } = deriveCallCompositions(
|
|
133
|
+
[call("c1", 40_000, 0), call("c2", 52_000, 1000)],
|
|
134
|
+
[manifest()],
|
|
135
|
+
);
|
|
136
|
+
const first = byCall.get("c1")!;
|
|
137
|
+
expect(tokensOf(first.segments, "harness")).toBe(25_000);
|
|
138
|
+
expect(tokensOf(first.segments, "history")).toBe(0);
|
|
139
|
+
expect(tokensOf(first.segments, "task")).toBe(1000);
|
|
140
|
+
const second = byCall.get("c2")!;
|
|
141
|
+
expect(tokensOf(second.segments, "harness")).toBe(25_000);
|
|
142
|
+
expect(tokensOf(second.segments, "history")).toBe(12_000);
|
|
143
|
+
expect(anchors.get("m-0")).toEqual({
|
|
144
|
+
callId: "c1",
|
|
145
|
+
promptTokens: 40_000,
|
|
146
|
+
residualTokens: 25_000,
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("sub-agent calls are never the anchor and get no composition", () => {
|
|
151
|
+
const { byCall, anchors } = deriveCallCompositions(
|
|
152
|
+
[call("sub", 5_000, 0, { sidechain: true }), call("c1", 40_000, 1000)],
|
|
153
|
+
[manifest()],
|
|
154
|
+
);
|
|
155
|
+
expect(byCall.has("sub")).toBe(false);
|
|
156
|
+
expect(anchors.get("m-0")?.callId).toBe("c1");
|
|
157
|
+
expect(tokensOf(byCall.get("c1")!.segments, "harness")).toBe(25_000);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("a compaction starts a new anchor whose harness base never grows", () => {
|
|
161
|
+
const compactions = [{ sessionId: "s-1", t: T0 + 1500 }];
|
|
162
|
+
// Post-compaction prompt carries a 6k summary: it is history, not harness.
|
|
163
|
+
const grown = deriveCallCompositions(
|
|
164
|
+
[call("c1", 40_000, 0), call("c2", 120_000, 1000), call("c3", 46_000, 2000)],
|
|
165
|
+
[manifest()],
|
|
166
|
+
compactions,
|
|
167
|
+
);
|
|
168
|
+
const post = grown.byCall.get("c3")!;
|
|
169
|
+
expect(tokensOf(post.segments, "harness")).toBe(25_000);
|
|
170
|
+
expect(tokensOf(post.segments, "history")).toBe(6_000);
|
|
171
|
+
// The rail's "first call" stays the session's first call.
|
|
172
|
+
expect(grown.anchors.get("m-0")?.callId).toBe("c1");
|
|
173
|
+
|
|
174
|
+
// The harness dropped part of its base after compacting: the base shrinks.
|
|
175
|
+
const shrunk = deriveCallCompositions(
|
|
176
|
+
[call("c1", 40_000, 0), call("c3", 30_000, 2000), call("c4", 33_000, 3000)],
|
|
177
|
+
[manifest()],
|
|
178
|
+
compactions,
|
|
179
|
+
);
|
|
180
|
+
expect(tokensOf(shrunk.byCall.get("c3")!.segments, "harness")).toBe(15_000);
|
|
181
|
+
expect(tokensOf(shrunk.byCall.get("c4")!.segments, "history")).toBe(3_000);
|
|
182
|
+
// Control: without the compaction, the base stays at the first call's.
|
|
183
|
+
const control = deriveCallCompositions(
|
|
184
|
+
[call("c1", 40_000, 0), call("c3", 30_000, 2000)],
|
|
185
|
+
[manifest()],
|
|
186
|
+
);
|
|
187
|
+
expect(tokensOf(control.byCall.get("c3")!.segments, "harness")).toBe(
|
|
188
|
+
25_000 * (30_000 / 40_000),
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("nothing goes negative when the estimates overshoot the prompt", () => {
|
|
193
|
+
const { byCall } = deriveCallCompositions(
|
|
194
|
+
[call("c1", 9_000, 0), call("c2", 7_500, 1000)],
|
|
195
|
+
[manifest()],
|
|
196
|
+
);
|
|
197
|
+
for (const id of ["c1", "c2"]) {
|
|
198
|
+
const comp = byCall.get(id)!;
|
|
199
|
+
expect(comp.scaled).toBe(true);
|
|
200
|
+
for (const seg of comp.segments) expect(seg.tokens).toBeGreaterThan(0);
|
|
201
|
+
expect(tokensOf(comp.segments, "harness")).toBe(0);
|
|
202
|
+
expect(tokensOf(comp.segments, "history")).toBe(0);
|
|
203
|
+
}
|
|
204
|
+
expect(sum(byCall.get("c1")!.segments)).toBeCloseTo(9_000, 6);
|
|
205
|
+
expect(sum(byCall.get("c2")!.segments)).toBeCloseTo(7_500, 6);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("the token ratio scales the static parts", () => {
|
|
209
|
+
const m = manifest({ tokenRatio: 1.2 });
|
|
210
|
+
expect(staticTokens(m)).toBeCloseTo(18_000, 6);
|
|
211
|
+
const { byCall } = deriveCallCompositions([call("c1", 40_000, 0)], [m]);
|
|
212
|
+
expect(tokensOf(byCall.get("c1")!.segments, "tools")).toBeCloseTo(12_000, 6);
|
|
213
|
+
expect(tokensOf(byCall.get("c1")!.segments, "harness")).toBeCloseTo(22_000, 6);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("each session uses its own manifest; a later seq applies from its write time", () => {
|
|
217
|
+
const s2 = manifest({ id: "m-s2", sessionId: "s-2", parts: manifest().parts.slice(0, 1) });
|
|
218
|
+
const seq1 = manifest({
|
|
219
|
+
id: "m-1",
|
|
220
|
+
seq: 1,
|
|
221
|
+
createdAt: new Date(T0 + 1500).toISOString(),
|
|
222
|
+
parts: manifest().parts.slice(0, 2),
|
|
223
|
+
});
|
|
224
|
+
const { byCall } = deriveCallCompositions(
|
|
225
|
+
[
|
|
226
|
+
call("c1", 40_000, 0),
|
|
227
|
+
call("c2", 41_000, 2000),
|
|
228
|
+
call("d1", 20_000, 0, {}, "s-2"),
|
|
229
|
+
call("x1", 20_000, 0, {}, "s-unknown"),
|
|
230
|
+
],
|
|
231
|
+
[manifest(), seq1, s2],
|
|
232
|
+
);
|
|
233
|
+
expect(byCall.get("c1")!.manifest.id).toBe("m-0");
|
|
234
|
+
expect(byCall.get("c2")!.manifest.id).toBe("m-1");
|
|
235
|
+
expect(byCall.get("d1")!.manifest.id).toBe("m-s2");
|
|
236
|
+
expect(tokensOf(byCall.get("d1")!.segments, "harness")).toBe(19_000);
|
|
237
|
+
expect(byCall.has("x1")).toBe(false);
|
|
238
|
+
for (const comp of byCall.values())
|
|
239
|
+
expect(sum(comp.segments)).toBeCloseTo(comp.promptTokens, 6);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("a hook manifest after the first call keeps later growth as history", () => {
|
|
243
|
+
// The worker manifest (15k static) anchors c1. The harness hook then posts
|
|
244
|
+
// exact parts worth 40k, the whole first prompt. c2's extra 12k is history.
|
|
245
|
+
const hook = manifest({
|
|
246
|
+
id: "m-hook",
|
|
247
|
+
seq: 1,
|
|
248
|
+
source: "harness-hook",
|
|
249
|
+
createdAt: new Date(T0 + 500).toISOString(),
|
|
250
|
+
parts: [{ ...manifest().parts[0]!, estTokens: 40_000, fidelity: "exact" }],
|
|
251
|
+
});
|
|
252
|
+
const { byCall, anchors } = deriveCallCompositions(
|
|
253
|
+
[call("c1", 40_000, 0), call("c2", 52_000, 1000)],
|
|
254
|
+
[manifest(), hook],
|
|
255
|
+
);
|
|
256
|
+
const second = byCall.get("c2")!;
|
|
257
|
+
expect(second.manifest.id).toBe("m-hook");
|
|
258
|
+
expect(tokensOf(second.segments, "harness")).toBe(0);
|
|
259
|
+
expect(tokensOf(second.segments, "history")).toBe(12_000);
|
|
260
|
+
expect(sum(second.segments)).toBeCloseTo(52_000, 6);
|
|
261
|
+
expect(anchors.get("m-hook")).toEqual({
|
|
262
|
+
callId: "c1",
|
|
263
|
+
promptTokens: 40_000,
|
|
264
|
+
residualTokens: 0,
|
|
265
|
+
});
|
|
266
|
+
// After a compaction the re-split base still caps the harness base: the
|
|
267
|
+
// 6k summary is history.
|
|
268
|
+
const compacted = deriveCallCompositions(
|
|
269
|
+
[call("c1", 40_000, 0), call("c2", 52_000, 1000), call("c3", 46_000, 2000)],
|
|
270
|
+
[manifest(), hook],
|
|
271
|
+
[{ sessionId: "s-1", t: T0 + 1500 }],
|
|
272
|
+
);
|
|
273
|
+
expect(tokensOf(compacted.byCall.get("c3")!.segments, "harness")).toBe(0);
|
|
274
|
+
expect(tokensOf(compacted.byCall.get("c3")!.segments, "history")).toBe(6_000);
|
|
275
|
+
// A hook manifest smaller than the first prompt leaves the rest as harness.
|
|
276
|
+
const partial = deriveCallCompositions(
|
|
277
|
+
[call("c1", 40_000, 0), call("c2", 52_000, 1000)],
|
|
278
|
+
[manifest(), { ...hook, parts: [{ ...hook.parts[0]!, estTokens: 30_000 }] }],
|
|
279
|
+
);
|
|
280
|
+
expect(tokensOf(partial.byCall.get("c2")!.segments, "harness")).toBe(10_000);
|
|
281
|
+
expect(tokensOf(partial.byCall.get("c2")!.segments, "history")).toBe(12_000);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("a later worker manifest keeps the harness base and swaps only the static parts", () => {
|
|
285
|
+
// m-0 has 15k static, so c1 (40k) measures a 25k harness base. A worker
|
|
286
|
+
// manifest then re-estimates the static parts as 16k. c2's history is
|
|
287
|
+
// 55k − 16k − 25k = 14k.
|
|
288
|
+
const part = manifest().parts[0]!;
|
|
289
|
+
const first = manifest({ parts: [{ ...part, estTokens: 15_000 }] });
|
|
290
|
+
const second = manifest({
|
|
291
|
+
id: "m-1",
|
|
292
|
+
seq: 1,
|
|
293
|
+
createdAt: new Date(T0 + 500).toISOString(),
|
|
294
|
+
parts: [{ ...part, estTokens: 16_000 }],
|
|
295
|
+
});
|
|
296
|
+
const { byCall, anchors } = deriveCallCompositions(
|
|
297
|
+
[call("c1", 40_000, 0), call("c2", 55_000, 1000)],
|
|
298
|
+
[first, second],
|
|
299
|
+
);
|
|
300
|
+
const c2 = byCall.get("c2")!;
|
|
301
|
+
expect(c2.manifest.id).toBe("m-1");
|
|
302
|
+
expect(tokensOf(c2.segments, "harness")).toBe(25_000);
|
|
303
|
+
expect(tokensOf(c2.segments, "history")).toBe(14_000);
|
|
304
|
+
expect(sum(c2.segments)).toBeCloseTo(55_000, 6);
|
|
305
|
+
expect(anchors.get("m-1")).toEqual({
|
|
306
|
+
callId: "c1",
|
|
307
|
+
promptTokens: 40_000,
|
|
308
|
+
residualTokens: 25_000,
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("no manifests means no compositions", () => {
|
|
313
|
+
expect(deriveCallCompositions([call("c1", 40_000, 0)], []).byCall.size).toBe(0);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("compositionCallsOf", () => {
|
|
318
|
+
test("keeps call_usage items whose record has a session", () => {
|
|
319
|
+
const base = { fileIndex: 0, iteration: 1, lineNumber: 1, createdAt: "", t: T0, cli: "claude" };
|
|
320
|
+
const usage = call("c1", 100, 0).usage;
|
|
321
|
+
const items: NormalizedItem[] = [
|
|
322
|
+
{ ...base, recId: "r1", kind: "call_usage", usage },
|
|
323
|
+
{ ...base, recId: "r2", kind: "call_usage", usage: { ...usage, callId: "c2" } },
|
|
324
|
+
{ ...base, recId: "r1", kind: "text", text: "hi" },
|
|
325
|
+
];
|
|
326
|
+
const calls = compositionCallsOf(items, (recId) => (recId === "r1" ? "s-1" : undefined));
|
|
327
|
+
expect(calls).toEqual([{ usage, sessionId: "s-1", t: T0 }]);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
@@ -653,6 +653,8 @@ describe("script connections", () => {
|
|
|
653
653
|
markMigrationApplied(database, "241_skills_meta_fts.sql");
|
|
654
654
|
// 244 alters api_key_status, which this migration-112-only fixture does not create.
|
|
655
655
|
markMigrationApplied(database, "244_api_key_validation.sql");
|
|
656
|
+
// 245 alters scripts, which this migration-112-only fixture does not create.
|
|
657
|
+
markMigrationApplied(database, "245_script_executor.sql");
|
|
656
658
|
|
|
657
659
|
const id = crypto.randomUUID();
|
|
658
660
|
const now = new Date().toISOString();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterAll, describe, expect, test } from "bun:test";
|
|
1
|
+
import { afterAll, describe, expect, spyOn, test } from "bun:test";
|
|
2
2
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -402,6 +402,41 @@ describe("native-only executor behavior", () => {
|
|
|
402
402
|
expect(output.error).toBe("killed");
|
|
403
403
|
});
|
|
404
404
|
|
|
405
|
+
// Before the fix the abort listener was added after the file writes, so an
|
|
406
|
+
// abort that landed during them was missed and the script ran to the end.
|
|
407
|
+
test.skipIf(SKIP_SANDBOX_SPAWN_TESTS)(
|
|
408
|
+
"an abort while args.json is written kills the run before the script starts",
|
|
409
|
+
async () => {
|
|
410
|
+
const dir = await mkdtemp(join(tmpdir(), "native-abort-"));
|
|
411
|
+
const marker = join(dir, "side-effect.marker");
|
|
412
|
+
const controller = new AbortController();
|
|
413
|
+
const write = Bun.write;
|
|
414
|
+
const spy = spyOn(Bun, "write").mockImplementation((async (
|
|
415
|
+
...writeArgs: Parameters<typeof Bun.write>
|
|
416
|
+
) => {
|
|
417
|
+
const written = await write(...writeArgs);
|
|
418
|
+
if (String(writeArgs[0]).endsWith("/args.json")) controller.abort();
|
|
419
|
+
return written;
|
|
420
|
+
}) as typeof Bun.write);
|
|
421
|
+
try {
|
|
422
|
+
const output = await new NativeScriptExecutor().run(
|
|
423
|
+
input({
|
|
424
|
+
source: `export default async () => { await Bun.write(${JSON.stringify(marker)}, "ran"); return "ran"; };`,
|
|
425
|
+
signal: controller.signal,
|
|
426
|
+
resources: { ...DEFAULT_SCRIPT_RESOURCES, memoryMb: 2048, wallClockMs: 10_000 },
|
|
427
|
+
}),
|
|
428
|
+
);
|
|
429
|
+
expect(output.error).toBe("killed");
|
|
430
|
+
expect(output.result).toBeUndefined();
|
|
431
|
+
expect(await Bun.file(marker).exists()).toBe(false);
|
|
432
|
+
} finally {
|
|
433
|
+
spy.mockRestore();
|
|
434
|
+
await rm(dir, { recursive: true, force: true });
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
438
|
+
);
|
|
439
|
+
|
|
405
440
|
// PR #1326 review finding: exit 134 (SIGABRT) was classified `capacity_exceeded`
|
|
406
441
|
// — and therefore retried by `runScript` — purely from the exit code, with no
|
|
407
442
|
// evidence about *when* the abort happened. A script that causes a side effect
|