@desplega.ai/agent-swarm 2.5.2 → 2.5.4
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/dist/{acp-adapter-csh0zz3b.js → acp-adapter-ek38cwsv.js} +5 -5
- package/dist/{actions-6xx079d1.js → actions-92mepq03.js} +19 -19
- package/dist/{amp-adapter-8n6f0hz8.js → amp-adapter-9gaxg015.js} +4 -4
- package/dist/{app-ft6r6v23.js → app-ctn8gsgx.js} +3 -3
- package/dist/{assistant-vt2cyfzd.js → assistant-zyr5kz47.js} +21 -21
- package/dist/{boot-reembed-tgrfy27t.js → boot-reembed-dwzfttdr.js} +1 -1
- package/dist/{boot-reembed-r3e808ky.js → boot-reembed-w75maga2.js} +1 -1
- package/dist/{boot-scrub-sweep-masdwxkm.js → boot-scrub-sweep-e9kq43kf.js} +3 -3
- package/dist/{boot-seeds-0n82qc7q.js → boot-seeds-x28v3fhb.js} +4 -4
- package/dist/{claude-adapter-s7j47k06.js → claude-adapter-f0afqnne.js} +5 -5
- package/dist/{claude-managed-adapter-gt6pa35d.js → claude-managed-adapter-x81a7r65.js} +3 -3
- package/dist/{claude-managed-setup-3hzrgrnk.js → claude-managed-setup-bqzwjd9x.js} +2 -2
- package/dist/{claude-sdk-session-0zrdzyx2.js → claude-sdk-session-7b7whkaq.js} +9307 -9254
- package/dist/{cli-a5ebc95n.js → cli-0shx9j93.js} +16 -13
- package/dist/{cli-4qkg9968.js → cli-19202kew.js} +4 -4
- package/dist/{cli-cyfgxnvb.js → cli-1r5ad7j5.js} +19 -17
- package/dist/{cli-jmcjbjkt.js → cli-25n3mv5d.js} +9 -7
- package/dist/cli-2655k16q.js +140 -0
- package/dist/{cli-wadzapwb.js → cli-277c31dw.js} +1 -1
- package/dist/{cli-n6kmqngw.js → cli-4b2z6n0m.js} +29 -7
- package/dist/{cli-7ccf7akh.js → cli-5bnsc0e1.js} +9 -7
- package/dist/{cli-qrjv7t18.js → cli-621ce7kw.js} +1 -1
- package/dist/{cli-xzge46sj.js → cli-64mh9mf3.js} +20 -6
- package/dist/{cli-b6tnjdvx.js → cli-6a1xg1p7.js} +6 -2
- package/dist/{cli-ydg6ep51.js → cli-6n8axjv1.js} +1 -1
- package/dist/{cli-e91vshb1.js → cli-7p4jxgtt.js} +5 -5
- package/dist/{cli-f3pfn4x3.js → cli-9j129brq.js} +9 -3
- package/dist/{cli-xw9q173h.js → cli-9s05422w.js} +1 -1
- package/dist/{cli-15xd0j7w.js → cli-abs8ndtr.js} +12 -12
- package/dist/{cli-h5d22pfs.js → cli-asq5ydm4.js} +219 -309
- package/dist/{cli-w9etnnxk.js → cli-bf8ma3f1.js} +120 -50
- package/dist/{cli-0vgy5wfw.js → cli-bvpgr9ny.js} +2 -2
- package/dist/{cli-3e9vs93g.js → cli-c61mbdds.js} +4 -4
- package/dist/{cli-e5bxnjps.js → cli-c952c7ac.js} +5 -5
- package/dist/{cli-p859hcc8.js → cli-dew6x255.js} +1 -1
- package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
- package/dist/{cli-gpv60278.js → cli-e42vss7b.js} +2 -2
- package/dist/{cli-b63d08rs.js → cli-eftzyh9y.js} +1 -1
- package/dist/{cli-fgn7yg5r.js → cli-ethgy7ha.js} +1 -1
- package/dist/{cli-86zpfje5.js → cli-ff1tm9n9.js} +1 -1
- package/dist/{cli-cf63dnxj.js → cli-g9vd9ece.js} +37 -12
- package/dist/{cli-j3dwd4kz.js → cli-gbfxp48k.js} +770 -126
- package/dist/{cli-n2918sed.js → cli-gn5af0x5.js} +21 -21
- package/dist/{cli-d172svcw.js → cli-gwhjmh8g.js} +4 -4
- package/dist/{cli-stxdrqs2.js → cli-hh5we352.js} +24 -5
- package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
- package/dist/{cli-csh8fnbg.js → cli-jb30dzvk.js} +473 -58
- package/dist/{cli-vaxw3bg0.js → cli-jbnn4vxb.js} +3 -3
- package/dist/{cli-ha22h0eg.js → cli-k4tjsdx9.js} +45 -40
- package/dist/{cli-f55b6ky0.js → cli-m4ecdxvz.js} +40 -13
- package/dist/{cli-mntxtkhn.js → cli-mz9afxxf.js} +2 -1
- package/dist/{cli-ta8bd7th.js → cli-pzatvw94.js} +1 -1
- package/dist/{cli-3qv8naq8.js → cli-rr7zx19j.js} +1591 -353
- package/dist/{cli-b1tv22wr.js → cli-s682zgqz.js} +1 -1
- package/dist/{cli-bpbcpd8b.js → cli-sbwsxvtd.js} +1 -1
- package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
- package/dist/{cli-6y43v79h.js → cli-tsma7p8v.js} +1 -1
- package/dist/{cli-edtm741g.js → cli-wghjkr0e.js} +5 -5
- package/dist/{cli-aaqnqpyw.js → cli-wx2nj19m.js} +30 -11
- package/dist/{cli-y3dzwyy3.js → cli-x0f2v69c.js} +180 -60
- package/dist/{cli-kcnwn85g.js → cli-xtrz6bzs.js} +7 -7
- package/dist/{cli-14v1sxxy.js → cli-xzfq4at3.js} +7 -7
- package/dist/cli.js +20 -20
- package/dist/{codex-adapter-brh2tcxr.js → codex-adapter-wch12q99.js} +3 -3
- package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
- package/dist/{codex-session-runner-0dnj21qs.js → codex-session-runner-41pfcr95.js} +2 -2
- package/dist/{commands-1cnvseqx.js → commands-9849dkj6.js} +2 -2
- package/dist/{controller-kc7s9xhn.js → controller-ja0sd95p.js} +29 -3
- package/dist/{controllers-connect-jzx7t4bn.js → controllers-connect-xp05dj6x.js} +20 -20
- package/dist/{cursor-adapter-qpwp5sfg.js → cursor-adapter-623qxqew.js} +1 -2
- package/dist/{db-4vq88jkc.js → db-47vj40dy.js} +3 -3
- package/dist/{db-v5ytg5pw.js → db-w759qmx0.js} +2 -2
- package/dist/{dispatcher-trtcrqae.js → dispatcher-7b8e4z2j.js} +17 -17
- package/dist/{docker-nfy2gc8t.js → docker-s84hjs91.js} +2 -2
- package/dist/{dsh-adapter-f20xwn1z.js → dsh-adapter-2ezw3xea.js} +2 -3
- package/dist/{external-channel-n9bymc2n.js → external-channel-e2s028da.js} +2 -2
- package/dist/{grok-adapter-4kpmmnr6.js → grok-adapter-ys7gaqee.js} +6 -6
- package/dist/{handlers-ptsraqa8.js → handlers-wy1dr6ja.js} +20 -20
- package/dist/{heartbeat-60qtxtjf.js → heartbeat-w3mntghj.js} +20 -20
- package/dist/{hook-rbktcstg.js → hook-3jjywnpb.js} +9 -3
- package/dist/{hook-ngtvh729.js → hook-g7kvv6x9.js} +3 -2
- package/dist/{http-z8xy72gp.js → http-tkp7d4kb.js} +2984 -2483
- package/dist/{index-r70q0xjx.js → index-1ef1myq2.js} +17 -17
- package/dist/{index-a9nsavhx.js → index-3n8m9ea2.js} +1 -1
- package/dist/{index-cbxkmfc5.js → index-9pn1pfkf.js} +1 -1
- package/dist/{index-s193j5fh.js → index-nmm79qgr.js} +20 -20
- package/dist/{index-6srwvd5b.js → index-p4xfz37z.js} +1 -1
- package/dist/{index-06qkmcx6.js → index-q7m26kvs.js} +33 -23
- package/dist/{index-1cscynkb.js → index-xdaahthb.js} +17 -17
- package/dist/{keepalive-a9srcphj.js → keepalive-v4ta8wgg.js} +3 -3
- package/dist/{lead-3devneqy.js → lead-sawnyqxk.js} +35 -35
- package/dist/{maintenance-0er16h2y.js → maintenance-84n1ex21.js} +1 -1
- package/dist/{oauth-refresh-sweep-b65jcyw2.js → oauth-refresh-sweep-vjnnandw.js} +2 -2
- package/dist/{onboard-wqywz5py.js → onboard-sfmpe4np.js} +2 -2
- package/dist/{opencode-adapter-e56086m1.js → opencode-adapter-jmpst4q4.js} +4 -4
- package/dist/{otel-impl-vk32s884.js → otel-impl-v6cc0zve.js} +1 -1
- package/dist/{pi-mono-adapter-tzps8f38.js → pi-mono-adapter-fk8fkcf6.js} +33 -6
- package/dist/{pricing-refresh-t27anv4z.js → pricing-refresh-fsk1e1zc.js} +3 -3
- package/dist/{rbac-roles-452665y0.js → rbac-roles-prmkdnkw.js} +2 -2
- package/dist/{rbac-roles-bmcgsxjn.js → rbac-roles-zxmxqa39.js} +1 -1
- package/dist/{render-v2-0ttsk2j4.js → render-v2-pjwxvnkw.js} +3 -3
- package/dist/{resume-a26kvr41.js → resume-frdtgfa0.js} +9 -5
- package/dist/{session-access-dgh9nkwk.js → session-access-fpez4y5q.js} +4 -4
- package/dist/{setup-9vc9qahm.js → setup-mq6m3m8q.js} +1 -1
- package/dist/{work-objects-vn8nkxwn.js → work-objects-kfahhjsq.js} +20 -20
- package/dist/{worker-1x1h8vnm.js → worker-v5e5rxj3.js} +35 -35
- package/openapi.json +3078 -2424
- package/package.json +7 -7
- package/plugin/commands/create-pr.md +2 -2
- package/plugin/pi-skills/create-pr/SKILL.md +2 -2
- package/src/be/approval-config-question.ts +389 -0
- package/src/be/automation-preflight.ts +37 -3
- package/src/be/db/approvals.ts +48 -0
- package/src/be/db/session-tree.ts +73 -15
- package/src/be/db/skill-search.ts +87 -0
- package/src/be/db/tasks/read.ts +29 -2
- package/src/be/db/tasks/write.ts +15 -5
- package/src/be/db/workflow-idempotency.ts +183 -0
- package/src/be/db/workflow-runs.ts +366 -8
- package/src/be/db.ts +92 -22
- package/src/be/migrations/239_skills_fts.sql +40 -0
- package/src/be/migrations/240_workflow_run_parent_step_attempt.sql +37 -0
- package/src/be/migrations/241_skills_meta_fts.sql +39 -0
- package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
- package/src/be/sealed-json.ts +1 -0
- package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
- package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
- package/src/be/seed-skills/bundled-files.generated.json +1 -1
- package/src/be/sql-connections.ts +7 -1
- package/src/be/swarm-config-guard.ts +25 -0
- package/src/be/users.ts +9 -0
- package/src/commands/controller.ts +33 -0
- package/src/commands/runner.ts +6 -2
- package/src/extensions/apply-task-create.ts +57 -11
- package/src/extensions/catalog.generated.json +4 -4
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +25 -0
- package/src/extensions/dispatcher.ts +4 -0
- package/src/extensions/post-bridge.ts +8 -0
- package/src/hooks/context-bound-note.ts +196 -0
- package/src/hooks/hook.ts +12 -0
- package/src/http/approval-requests.ts +209 -38
- package/src/http/config.ts +70 -53
- package/src/http/idempotency-key.ts +117 -0
- package/src/http/sessions.ts +27 -2
- package/src/http/workflows.ts +129 -24
- package/src/integrations/chat-sdk/inbound.ts +5 -4
- package/src/prompts/context-bound-note.ts +48 -0
- package/src/prompts/session-templates.ts +25 -1
- package/src/providers/amp-adapter.ts +30 -8
- package/src/providers/claude-adapter.ts +29 -4
- package/src/providers/claude-managed-adapter.ts +26 -0
- package/src/providers/claude-sdk-session.ts +10 -0
- package/src/providers/claude-session-events.ts +13 -0
- package/src/providers/codex-adapter.ts +12 -0
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/opencode-adapter.ts +18 -0
- package/src/providers/pi-mono-adapter.ts +35 -0
- package/src/runtime-providers/docker.ts +6 -2
- package/src/scripts-runtime/types/swarm-extension.d.ts +35 -0
- package/src/tests/amp-adapter.test.ts +97 -0
- package/src/tests/approval-config-question.test.ts +702 -0
- package/src/tests/approval-requests.test.ts +10 -7
- package/src/tests/approval-sweeps.test.ts +12 -6
- package/src/tests/base-prompt.test.ts +1 -1
- package/src/tests/chat-sdk-inbound.test.ts +20 -0
- package/src/tests/claude-hook-output.test.ts +79 -1
- package/src/tests/claude-managed-adapter.test.ts +275 -0
- package/src/tests/codex-adapter.test.ts +163 -0
- package/src/tests/context-bound-note.test.ts +269 -0
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/entrypoint-gitlab-credential-helper.test.ts +273 -0
- package/src/tests/extensions-example-comms-adherence-gate.test.ts +321 -12
- package/src/tests/extensions-pre-task.test.ts +273 -0
- package/src/tests/fetch-resolved-env.test.ts +17 -0
- package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
- package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
- package/src/tests/fixtures/fake-amp.ts +22 -8
- package/src/tests/fixtures/held-template-render.ts +42 -0
- package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
- package/src/tests/list-endpoint-slimming.test.ts +33 -0
- package/src/tests/migration-240-workflow-run-parent-step-attempt.test.ts +55 -0
- package/src/tests/openapi-route-headers.test.ts +2 -0
- package/src/tests/opencode-adapter.test.ts +134 -0
- package/src/tests/pi-mono-adapter.test.ts +105 -0
- package/src/tests/prompt-template-session.test.ts +4 -3
- package/src/tests/runner-push-delivery.test.ts +25 -0
- package/src/tests/runtime-controller-liveness.test.ts +133 -0
- package/src/tests/runtime-providers-docker.test.ts +2 -0
- package/src/tests/script-connections.test.ts +9 -0
- package/src/tests/script-run-config-scope.test.ts +91 -3
- package/src/tests/session-logs.test.ts +70 -0
- package/src/tests/sessions-tree.test.ts +205 -1
- package/src/tests/skill-search-long-content.test.ts +193 -0
- package/src/tests/skill-search.test.ts +173 -22
- package/src/tests/workflow-cascade-cancel.test.ts +1169 -0
- package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
- package/src/tests/workflow-idempotency.test.ts +915 -0
- package/src/tests/workflow-sub-workflow.test.ts +538 -1
- package/src/tools/request-human-input.ts +27 -5
- package/src/tools/workflows/cancel-workflow-run.ts +1 -1
- package/src/types.ts +7 -0
- package/src/utils/context-window.ts +8 -6
- package/src/workflows/approval-cancel.ts +15 -13
- package/src/workflows/approval-notifications.ts +15 -4
- package/src/workflows/checkpoint.ts +72 -35
- package/src/workflows/engine.ts +262 -93
- package/src/workflows/executors/base.ts +6 -0
- package/src/workflows/executors/foreach.ts +2 -2
- package/src/workflows/executors/human-in-the-loop.ts +25 -1
- package/src/workflows/executors/sub-workflow.ts +45 -16
- package/src/workflows/foreach-driver.ts +10 -3
- package/src/workflows/foreach-join.ts +29 -6
- package/src/workflows/recovery.ts +68 -1
- package/src/workflows/resume.ts +228 -77
- package/src/workflows/retry-poller.ts +74 -7
- package/src/workflows/task-step-routing.ts +29 -7
- package/templates/ai-toolbox.manifest.json +8 -8
- package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
- package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
- package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
- package/templates/extensions/comms-adherence-gate/README.md +38 -13
- package/templates/extensions/comms-adherence-gate/hooks.ts +272 -69
- package/templates/extensions/comms-adherence-gate/manifest.json +2 -2
- package/templates/skills/agent-browser/SKILL.md +4 -4
- package/templates/skills/agent-browser/content.md +4 -4
- package/templates/skills/code-quality/config.json +1 -1
- package/templates/skills/code-quality/content.md +2 -2
- package/templates/skills/delegate-work/content.md +29 -25
- package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
- package/templates/skills/qa/content.md +2 -2
- package/templates/skills/researching/content.md +2 -0
- package/templates/skills/turso-interaction/content.md +1 -1
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/templates/skills/workflow-iterate/content.md +8 -1
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "node:fs";
|
|
19
19
|
import { homedir, tmpdir } from "node:os";
|
|
20
20
|
import { join } from "node:path";
|
|
21
|
+
import { renderContextBoundNote } from "../prompts/context-bound-note";
|
|
21
22
|
import type {
|
|
22
23
|
AgentMessageItem,
|
|
23
24
|
CommandExecutionItem,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
resolveCodexModel,
|
|
34
35
|
} from "../providers/codex-models";
|
|
35
36
|
import type { ProviderEvent, ProviderResult, ProviderSessionConfig } from "../providers/types";
|
|
37
|
+
import { HELD_NOTE_TEXT, holdTemplateRender } from "./fixtures/held-template-render";
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
40
|
* Build a tiny fake `Thread` whose `runStreamed` returns a fixed sequence of
|
|
@@ -2611,3 +2613,164 @@ describe("CodexSession — rate-limit error preservation", () => {
|
|
|
2611
2613
|
expect(result.rateLimitResetAt).toBeUndefined();
|
|
2612
2614
|
});
|
|
2613
2615
|
});
|
|
2616
|
+
|
|
2617
|
+
describe("CodexSession context-bound note", () => {
|
|
2618
|
+
let prevSkipEnv: string | undefined;
|
|
2619
|
+
|
|
2620
|
+
beforeAll(() => {
|
|
2621
|
+
prevSkipEnv = process.env.SKIP_SESSION_SUMMARY;
|
|
2622
|
+
process.env.SKIP_SESSION_SUMMARY = "1";
|
|
2623
|
+
});
|
|
2624
|
+
|
|
2625
|
+
afterAll(() => {
|
|
2626
|
+
if (prevSkipEnv === undefined) delete process.env.SKIP_SESSION_SUMMARY;
|
|
2627
|
+
else process.env.SKIP_SESSION_SUMMARY = prevSkipEnv;
|
|
2628
|
+
});
|
|
2629
|
+
|
|
2630
|
+
/** One `context.updated` per request; each request's input tokens in order. */
|
|
2631
|
+
function requestEvents(inputs: unknown[]): ThreadEvent[] {
|
|
2632
|
+
return inputs.map(
|
|
2633
|
+
(input_tokens) =>
|
|
2634
|
+
({
|
|
2635
|
+
type: "context.updated",
|
|
2636
|
+
usage: { input_tokens, cached_input_tokens: 0, output_tokens: 1 },
|
|
2637
|
+
}) as ThreadEvent,
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
/**
|
|
2642
|
+
* Runs the session over `events` and records every `thread.steer` call. The
|
|
2643
|
+
* turn stays open briefly after the requests so the async note can land.
|
|
2644
|
+
*/
|
|
2645
|
+
async function runWithSteers(
|
|
2646
|
+
events: ThreadEvent[],
|
|
2647
|
+
env: Record<string, string>,
|
|
2648
|
+
steer: (input: string) => Promise<void> = async () => {},
|
|
2649
|
+
beforeTurnCompleted: () => Promise<unknown> = () => Bun.sleep(50),
|
|
2650
|
+
model?: string,
|
|
2651
|
+
): Promise<{ steers: string[]; result: ProviderResult }> {
|
|
2652
|
+
const steers: string[] = [];
|
|
2653
|
+
const thread = {
|
|
2654
|
+
id: null,
|
|
2655
|
+
async steer(input: string) {
|
|
2656
|
+
steers.push(input);
|
|
2657
|
+
await steer(input);
|
|
2658
|
+
},
|
|
2659
|
+
async interrupt() {},
|
|
2660
|
+
async close() {},
|
|
2661
|
+
async runStreamed() {
|
|
2662
|
+
async function* generate(): AsyncGenerator<ThreadEvent> {
|
|
2663
|
+
yield { type: "thread.started", thread_id: "thread-bound-note" };
|
|
2664
|
+
yield { type: "turn.started" };
|
|
2665
|
+
for (const event of events) yield event;
|
|
2666
|
+
await beforeTurnCompleted();
|
|
2667
|
+
yield {
|
|
2668
|
+
type: "turn.completed",
|
|
2669
|
+
usage: { input_tokens: 1, cached_input_tokens: 0, output_tokens: 1 },
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
return { events: generate() };
|
|
2673
|
+
},
|
|
2674
|
+
};
|
|
2675
|
+
const adapter = new CodexAdapter({
|
|
2676
|
+
bypassSubprocess: true,
|
|
2677
|
+
threadFactory: () => thread as never,
|
|
2678
|
+
});
|
|
2679
|
+
const session = await adapter.createSession(testConfig({ env, ...(model ? { model } : {}) }));
|
|
2680
|
+
const result = await session.waitForCompletion();
|
|
2681
|
+
return { steers, result };
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
test("steers the note once at the threshold, not again on a larger request", async () => {
|
|
2685
|
+
const { steers, result } = await runWithSteers(requestEvents([400, 1_200, 5_000]), {
|
|
2686
|
+
CONTEXT_BOUND_NOTE_TOKENS: "1000",
|
|
2687
|
+
});
|
|
2688
|
+
expect(result.isError).toBe(false);
|
|
2689
|
+
expect(steers).toEqual([await renderContextBoundNote(1_200, "gpt-5.4")]);
|
|
2690
|
+
expect(steers[0]).toContain("1,200");
|
|
2691
|
+
});
|
|
2692
|
+
|
|
2693
|
+
test("the note shows the window share for a known model and omits it for an unknown one", async () => {
|
|
2694
|
+
const noteFor = async (model: string) =>
|
|
2695
|
+
(
|
|
2696
|
+
await runWithSteers(
|
|
2697
|
+
requestEvents([525_000]),
|
|
2698
|
+
{ CONTEXT_BOUND_NOTE_TOKENS: "1000" },
|
|
2699
|
+
undefined,
|
|
2700
|
+
undefined,
|
|
2701
|
+
model,
|
|
2702
|
+
)
|
|
2703
|
+
).steers;
|
|
2704
|
+
const rest =
|
|
2705
|
+
"Keep this task bounded: finish the current step, store-progress, and complete or defer instead of starting new exploration.";
|
|
2706
|
+
expect(await noteFor("gpt-5.4")).toEqual([
|
|
2707
|
+
`Context is 525,000 tokens (50% of the 1,050,000-token window). ${rest}`,
|
|
2708
|
+
]);
|
|
2709
|
+
expect(await noteFor("made-up-codex-model")).toEqual([`Context is 525,000 tokens. ${rest}`]);
|
|
2710
|
+
});
|
|
2711
|
+
|
|
2712
|
+
test("off when CONTEXT_BOUND_NOTE_TOKENS is unset", async () => {
|
|
2713
|
+
const { steers } = await runWithSteers(requestEvents([250_000, 900_000]), {});
|
|
2714
|
+
expect(steers).toEqual([]);
|
|
2715
|
+
});
|
|
2716
|
+
|
|
2717
|
+
test("no-op below the threshold", async () => {
|
|
2718
|
+
const { steers } = await runWithSteers(requestEvents([400, 999]), {
|
|
2719
|
+
CONTEXT_BOUND_NOTE_TOKENS: "1000",
|
|
2720
|
+
});
|
|
2721
|
+
expect(steers).toEqual([]);
|
|
2722
|
+
});
|
|
2723
|
+
|
|
2724
|
+
test("off at 0", async () => {
|
|
2725
|
+
const { steers } = await runWithSteers(requestEvents([250_000, 500_000]), {
|
|
2726
|
+
CONTEXT_BOUND_NOTE_TOKENS: "0",
|
|
2727
|
+
});
|
|
2728
|
+
expect(steers).toEqual([]);
|
|
2729
|
+
});
|
|
2730
|
+
|
|
2731
|
+
test("fails open on missing or corrupt usage", async () => {
|
|
2732
|
+
const { steers, result } = await runWithSteers(requestEvents([undefined, "lots", null]), {
|
|
2733
|
+
CONTEXT_BOUND_NOTE_TOKENS: "1000",
|
|
2734
|
+
});
|
|
2735
|
+
expect(steers).toEqual([]);
|
|
2736
|
+
expect(result.isError).toBe(false);
|
|
2737
|
+
});
|
|
2738
|
+
|
|
2739
|
+
test("a rejected steer still lets the session complete", async () => {
|
|
2740
|
+
const { steers, result } = await runWithSteers(
|
|
2741
|
+
requestEvents([5_000]),
|
|
2742
|
+
{ CONTEXT_BOUND_NOTE_TOKENS: "1000" },
|
|
2743
|
+
async () => {
|
|
2744
|
+
throw new Error("steer refused");
|
|
2745
|
+
},
|
|
2746
|
+
);
|
|
2747
|
+
expect(steers).toHaveLength(1);
|
|
2748
|
+
expect(result.isError).toBe(false);
|
|
2749
|
+
});
|
|
2750
|
+
|
|
2751
|
+
test("a note rendered after the turn completed is dropped", async () => {
|
|
2752
|
+
const lateSteers = async (releaseInTurn: boolean) => {
|
|
2753
|
+
const held = holdTemplateRender();
|
|
2754
|
+
try {
|
|
2755
|
+
const run = await runWithSteers(
|
|
2756
|
+
requestEvents([5_000]),
|
|
2757
|
+
{ CONTEXT_BOUND_NOTE_TOKENS: "1000" },
|
|
2758
|
+
undefined,
|
|
2759
|
+
async () => {
|
|
2760
|
+
await held.requested;
|
|
2761
|
+
if (releaseInTurn) held.release();
|
|
2762
|
+
await Bun.sleep(50);
|
|
2763
|
+
},
|
|
2764
|
+
);
|
|
2765
|
+
held.release();
|
|
2766
|
+
await Bun.sleep(50);
|
|
2767
|
+
return run.steers;
|
|
2768
|
+
} finally {
|
|
2769
|
+
held.stop();
|
|
2770
|
+
}
|
|
2771
|
+
};
|
|
2772
|
+
expect(await lateSteers(false)).toEqual([]);
|
|
2773
|
+
// Control: ready while the turn is still open.
|
|
2774
|
+
expect(await lateSteers(true)).toEqual([HELD_NOTE_TEXT]);
|
|
2775
|
+
});
|
|
2776
|
+
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import * as fsPromises from "node:fs/promises";
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
claimContextBoundNote,
|
|
8
|
+
contextBoundNoteThreshold,
|
|
9
|
+
contextStatePath,
|
|
10
|
+
createContextBoundNote,
|
|
11
|
+
recordContextState,
|
|
12
|
+
} from "../hooks/context-bound-note";
|
|
13
|
+
import { renderContextBoundNote } from "../prompts/context-bound-note";
|
|
14
|
+
import { normalizeClaudeMessage } from "../providers/claude-session-events";
|
|
15
|
+
|
|
16
|
+
let dir: string;
|
|
17
|
+
let n = 0;
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
dir = await mkdtemp(join(tmpdir(), "context-bound-note-"));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterAll(async () => {
|
|
24
|
+
await rm(dir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** A fresh task file path with `contextTokens` recorded next to it. */
|
|
28
|
+
function taskFileWith(contextTokens?: number): string {
|
|
29
|
+
const taskFile = join(dir, `task-${++n}.json`);
|
|
30
|
+
if (contextTokens !== undefined) recordContextState(taskFile, contextTokens);
|
|
31
|
+
return taskFile;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("contextBoundNoteThreshold", () => {
|
|
35
|
+
test("off when unset, blank, invalid or 0", () => {
|
|
36
|
+
expect(contextBoundNoteThreshold({})).toBe(0);
|
|
37
|
+
expect(contextBoundNoteThreshold({ CONTEXT_BOUND_NOTE_TOKENS: "" })).toBe(0);
|
|
38
|
+
expect(contextBoundNoteThreshold({ CONTEXT_BOUND_NOTE_TOKENS: " " })).toBe(0);
|
|
39
|
+
expect(contextBoundNoteThreshold({ CONTEXT_BOUND_NOTE_TOKENS: "-5" })).toBe(0);
|
|
40
|
+
expect(contextBoundNoteThreshold({ CONTEXT_BOUND_NOTE_TOKENS: "lots" })).toBe(0);
|
|
41
|
+
expect(contextBoundNoteThreshold({ CONTEXT_BOUND_NOTE_TOKENS: "1e5" })).toBe(0);
|
|
42
|
+
expect(contextBoundNoteThreshold({ CONTEXT_BOUND_NOTE_TOKENS: "99999999999999999999" })).toBe(
|
|
43
|
+
0,
|
|
44
|
+
);
|
|
45
|
+
expect(contextBoundNoteThreshold({ CONTEXT_BOUND_NOTE_TOKENS: "0" })).toBe(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a positive integer turns the note on at that threshold", () => {
|
|
49
|
+
expect(contextBoundNoteThreshold({ CONTEXT_BOUND_NOTE_TOKENS: "150000" })).toBe(150_000);
|
|
50
|
+
expect(contextBoundNoteThreshold({ CONTEXT_BOUND_NOTE_TOKENS: " 200000 " })).toBe(200_000);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("claimContextBoundNote", () => {
|
|
55
|
+
test("fires once at the threshold, not again on later calls", async () => {
|
|
56
|
+
const taskFile = taskFileWith(200_000);
|
|
57
|
+
expect(await claimContextBoundNote(taskFile, 200_000)).toEqual({ contextTokens: 200_000 });
|
|
58
|
+
expect(await claimContextBoundNote(taskFile, 200_000)).toBeNull();
|
|
59
|
+
recordContextState(taskFile, 400_000);
|
|
60
|
+
expect(await claimContextBoundNote(taskFile, 200_000)).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("concurrent hooks fire the note once", async () => {
|
|
64
|
+
const taskFile = taskFileWith(300_000);
|
|
65
|
+
const results = await Promise.all(
|
|
66
|
+
Array.from({ length: 8 }, () => claimContextBoundNote(taskFile, 200_000)),
|
|
67
|
+
);
|
|
68
|
+
expect(results.filter((r) => r !== null)).toEqual([{ contextTokens: 300_000 }]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("no-op below the threshold, then fires once it is crossed", async () => {
|
|
72
|
+
const taskFile = taskFileWith(199_999);
|
|
73
|
+
expect(await claimContextBoundNote(taskFile, 200_000)).toBeNull();
|
|
74
|
+
recordContextState(taskFile, 210_000);
|
|
75
|
+
expect(await claimContextBoundNote(taskFile, 200_000)).toEqual({ contextTokens: 210_000 });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("returns the model the session recorded", async () => {
|
|
79
|
+
const taskFile = join(dir, `task-${++n}.json`);
|
|
80
|
+
recordContextState(taskFile, 250_000, "claude-opus-5-5");
|
|
81
|
+
expect(await claimContextBoundNote(taskFile, 200_000)).toEqual({
|
|
82
|
+
contextTokens: 250_000,
|
|
83
|
+
model: "claude-opus-5-5",
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("disabled at 0", async () => {
|
|
88
|
+
expect(await claimContextBoundNote(taskFileWith(900_000), 0)).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("fails open on missing task file, missing state or corrupt state", async () => {
|
|
92
|
+
expect(await claimContextBoundNote(undefined, 200_000)).toBeNull();
|
|
93
|
+
expect(await claimContextBoundNote(taskFileWith(), 200_000)).toBeNull();
|
|
94
|
+
|
|
95
|
+
const corrupt = taskFileWith();
|
|
96
|
+
await Bun.write(contextStatePath(corrupt), "{not json");
|
|
97
|
+
expect(await claimContextBoundNote(corrupt, 200_000)).toBeNull();
|
|
98
|
+
|
|
99
|
+
const wrongShape = taskFileWith();
|
|
100
|
+
await Bun.write(contextStatePath(wrongShape), JSON.stringify({ contextTokens: "300000" }));
|
|
101
|
+
expect(await claimContextBoundNote(wrongShape, 200_000)).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("fails open when the marker cannot be written", async () => {
|
|
105
|
+
const taskFile = taskFileWith(300_000);
|
|
106
|
+
const writeFile = spyOn(fsPromises, "writeFile").mockRejectedValue(
|
|
107
|
+
Object.assign(new Error("EIO: i/o error"), { code: "EIO" }),
|
|
108
|
+
);
|
|
109
|
+
try {
|
|
110
|
+
expect(await claimContextBoundNote(taskFile, 200_000)).toBeNull();
|
|
111
|
+
expect(writeFile).toHaveBeenCalledTimes(1);
|
|
112
|
+
} finally {
|
|
113
|
+
writeFile.mockRestore();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("createContextBoundNote", () => {
|
|
119
|
+
function observer(
|
|
120
|
+
threshold: number,
|
|
121
|
+
inject: (note: string) => unknown = () => {},
|
|
122
|
+
canDeliver: () => boolean = () => true,
|
|
123
|
+
model?: () => string | undefined,
|
|
124
|
+
) {
|
|
125
|
+
const notes: string[] = [];
|
|
126
|
+
const observe = createContextBoundNote({
|
|
127
|
+
threshold,
|
|
128
|
+
canDeliver,
|
|
129
|
+
model,
|
|
130
|
+
inject: (note) => {
|
|
131
|
+
notes.push(note);
|
|
132
|
+
return inject(note);
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
return { observe, notes };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
test("fires once at the threshold, not again on later requests", async () => {
|
|
139
|
+
const { observe, notes } = observer(200_000);
|
|
140
|
+
await observe(200_000);
|
|
141
|
+
await observe(400_000);
|
|
142
|
+
expect(notes).toEqual([await renderContextBoundNote(200_000)]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("no-op below the threshold, then fires once it is crossed", async () => {
|
|
146
|
+
const { observe, notes } = observer(200_000);
|
|
147
|
+
await observe(199_999);
|
|
148
|
+
expect(notes).toEqual([]);
|
|
149
|
+
await observe(210_000);
|
|
150
|
+
expect(notes).toEqual([await renderContextBoundNote(210_000)]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("disabled at 0", async () => {
|
|
154
|
+
const { observe, notes } = observer(0);
|
|
155
|
+
await observe(900_000);
|
|
156
|
+
expect(notes).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("fails open on missing or corrupt sizes", async () => {
|
|
160
|
+
const { observe, notes } = observer(200_000);
|
|
161
|
+
await observe(undefined);
|
|
162
|
+
await observe(Number.NaN);
|
|
163
|
+
await observe(Number.POSITIVE_INFINITY);
|
|
164
|
+
await observe("300000" as unknown as number);
|
|
165
|
+
expect(notes).toEqual([]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("never rejects when injection fails, and does not retry", async () => {
|
|
169
|
+
const { observe, notes } = observer(200_000, () => Promise.reject(new Error("closed")));
|
|
170
|
+
await expect(observe(300_000)).resolves.toBeUndefined();
|
|
171
|
+
await observe(300_000);
|
|
172
|
+
expect(notes).toHaveLength(1);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("holds a note rendered after its window closed, then delivers it on the next call", async () => {
|
|
176
|
+
let open = false;
|
|
177
|
+
const { observe, notes } = observer(200_000, undefined, () => open);
|
|
178
|
+
await observe(300_000);
|
|
179
|
+
expect(notes).toEqual([]);
|
|
180
|
+
open = true;
|
|
181
|
+
await observe(500_000);
|
|
182
|
+
// The held note, not a re-render at the later size.
|
|
183
|
+
expect(notes).toEqual([await renderContextBoundNote(300_000)]);
|
|
184
|
+
await observe(600_000);
|
|
185
|
+
expect(notes).toHaveLength(1);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("never delivers when the window stays closed", async () => {
|
|
189
|
+
const { observe, notes } = observer(200_000, undefined, () => false);
|
|
190
|
+
await observe(300_000);
|
|
191
|
+
await observe(400_000);
|
|
192
|
+
expect(notes).toEqual([]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("renders with the session's model at render time", async () => {
|
|
196
|
+
let model = "made-up-model";
|
|
197
|
+
const { observe, notes } = observer(200_000, undefined, undefined, () => model);
|
|
198
|
+
model = "claude-opus-5-5";
|
|
199
|
+
await observe(210_000);
|
|
200
|
+
expect(notes).toEqual([
|
|
201
|
+
"Context is 210,000 tokens (21% of the 1,000,000-token window). Keep this task bounded: finish the current step, store-progress, and complete or defer instead of starting new exploration.",
|
|
202
|
+
]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("checks the window after rendering, not before", async () => {
|
|
206
|
+
let open = true;
|
|
207
|
+
const { observe, notes } = observer(200_000, undefined, () => open);
|
|
208
|
+
const pending = observe(300_000);
|
|
209
|
+
open = false;
|
|
210
|
+
await pending;
|
|
211
|
+
expect(notes).toEqual([]);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("renderContextBoundNote", () => {
|
|
216
|
+
const rest =
|
|
217
|
+
"Keep this task bounded: finish the current step, store-progress, and complete or defer instead of starting new exploration.";
|
|
218
|
+
|
|
219
|
+
test("renders the code default with a formatted token count and no model", async () => {
|
|
220
|
+
expect(await renderContextBoundNote(213_456)).toBe(`Context is 213,456 tokens. ${rest}`);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("adds the window share for a known model", async () => {
|
|
224
|
+
expect(await renderContextBoundNote(210_000, "claude-opus-5-5")).toBe(
|
|
225
|
+
`Context is 210,000 tokens (21% of the 1,000,000-token window). ${rest}`,
|
|
226
|
+
);
|
|
227
|
+
expect(await renderContextBoundNote(200_000, "gpt-5")).toBe(
|
|
228
|
+
`Context is 200,000 tokens (50% of the 400,000-token window). ${rest}`,
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("omits the share for an unknown model instead of using the 200k fallback", async () => {
|
|
233
|
+
expect(await renderContextBoundNote(210_000, "made-up-model")).toBe(
|
|
234
|
+
`Context is 210,000 tokens. ${rest}`,
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("normalizeClaudeMessage requestContextTokens", () => {
|
|
240
|
+
const state = {
|
|
241
|
+
taskId: "task-fixture",
|
|
242
|
+
agentId: "agent-fixture",
|
|
243
|
+
model: "claude-opus-5-5",
|
|
244
|
+
contextWindowSize: 1_000_000,
|
|
245
|
+
transport: "cli" as const,
|
|
246
|
+
};
|
|
247
|
+
const usage = {
|
|
248
|
+
input_tokens: 10,
|
|
249
|
+
cache_read_input_tokens: 180_000,
|
|
250
|
+
cache_creation_input_tokens: 25_000,
|
|
251
|
+
output_tokens: 4_000,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
test("is input + cache read + cache write of a main-thread request", () => {
|
|
255
|
+
const normalized = normalizeClaudeMessage(
|
|
256
|
+
{ type: "assistant", message: { content: [], usage } },
|
|
257
|
+
state,
|
|
258
|
+
);
|
|
259
|
+
expect(normalized.requestContextTokens).toBe(205_010);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("is unset for subagent messages", () => {
|
|
263
|
+
const normalized = normalizeClaudeMessage(
|
|
264
|
+
{ type: "assistant", parent_tool_use_id: "toolu_1", message: { content: [], usage } },
|
|
265
|
+
state,
|
|
266
|
+
);
|
|
267
|
+
expect(normalized.requestContextTokens).toBeUndefined();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
@@ -376,7 +376,7 @@ describe("dsh harness", () => {
|
|
|
376
376
|
await rename(join(config.cwd, "dsh"), join(config.cwd, "npx"));
|
|
377
377
|
config.env = { ...config.env, DSH_BINARY: "", PATH: config.cwd };
|
|
378
378
|
await expect(new DshAdapter().createSession(config)).rejects.toThrow(
|
|
379
|
-
"dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-rc.
|
|
379
|
+
"dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-rc.3",
|
|
380
380
|
);
|
|
381
381
|
expect(await Bun.file(join(config.cwd, "invocation.json")).exists()).toBe(false);
|
|
382
382
|
});
|