@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
|
@@ -17,6 +17,11 @@ import { dirname, join, resolve } from "node:path";
|
|
|
17
17
|
import { promisify } from "node:util";
|
|
18
18
|
import { deriveDefaultRoute, routeCredentialStatus, routeUnsetEnv } from "@desplega/model-routing";
|
|
19
19
|
import { type Span, trace } from "@opentelemetry/api";
|
|
20
|
+
import {
|
|
21
|
+
contextBoundNoteMarkerPath,
|
|
22
|
+
contextStatePath,
|
|
23
|
+
recordContextState,
|
|
24
|
+
} from "../hooks/context-bound-note";
|
|
20
25
|
import {
|
|
21
26
|
type RunStopHookSessionSummaryOpts,
|
|
22
27
|
runStopHookSessionSummarySubprocess,
|
|
@@ -130,10 +135,17 @@ async function writeTaskFile(key: string | number, data: TaskFileData): Promise<
|
|
|
130
135
|
}
|
|
131
136
|
|
|
132
137
|
export async function cleanupTaskFile(key: string | number): Promise<void> {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
const taskFilePath = getTaskFilePath(key);
|
|
139
|
+
for (const path of [
|
|
140
|
+
taskFilePath,
|
|
141
|
+
contextStatePath(taskFilePath),
|
|
142
|
+
contextBoundNoteMarkerPath(taskFilePath),
|
|
143
|
+
]) {
|
|
144
|
+
try {
|
|
145
|
+
await unlink(path);
|
|
146
|
+
} catch {
|
|
147
|
+
// File might already be deleted or never created
|
|
148
|
+
}
|
|
137
149
|
}
|
|
138
150
|
}
|
|
139
151
|
|
|
@@ -909,6 +921,8 @@ class ClaudeSession implements ProviderSession {
|
|
|
909
921
|
private appliedReasoningEffort: ReasoningEffort | null;
|
|
910
922
|
/** Last non-empty assistant text seen; surfaced as ProviderResult.output — same pattern as pi-mono/claude-managed. */
|
|
911
923
|
private lastAssistantText = "";
|
|
924
|
+
/** Last value written to the context state file read by the context-bound note hook. */
|
|
925
|
+
private lastContextTokens: number | undefined;
|
|
912
926
|
/** Per-session stream-json transcript used by the parent-owned session summarizer. */
|
|
913
927
|
private transcript: string[];
|
|
914
928
|
/** Keeps the session heartbeat fresh while Claude idles on background tasks. */
|
|
@@ -1263,6 +1277,17 @@ class ClaudeSession implements ProviderSession {
|
|
|
1263
1277
|
if (normalized.contextWindowSize) this.contextWindowSize = normalized.contextWindowSize;
|
|
1264
1278
|
if (normalized.cost) setCost(normalized.cost);
|
|
1265
1279
|
if (normalized.assistantText) this.lastAssistantText = normalized.assistantText;
|
|
1280
|
+
if (
|
|
1281
|
+
normalized.requestContextTokens !== undefined &&
|
|
1282
|
+
normalized.requestContextTokens !== this.lastContextTokens
|
|
1283
|
+
) {
|
|
1284
|
+
this.lastContextTokens = normalized.requestContextTokens;
|
|
1285
|
+
recordContextState(
|
|
1286
|
+
getTaskFilePath(this.taskFileKey),
|
|
1287
|
+
normalized.requestContextTokens,
|
|
1288
|
+
this.model,
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1266
1291
|
this.transcript.push(...normalized.transcriptEntries);
|
|
1267
1292
|
for (const event of normalized.events) this.emit(event);
|
|
1268
1293
|
|
|
@@ -56,6 +56,7 @@ import type {
|
|
|
56
56
|
BetaManagedAgentsSessionEvent as SessionEvent,
|
|
57
57
|
} from "@anthropic-ai/sdk/resources/beta/sessions";
|
|
58
58
|
|
|
59
|
+
import { contextBoundNoteThreshold, createContextBoundNote } from "../hooks/context-bound-note";
|
|
59
60
|
import { checkToolLoop } from "../hooks/tool-loop-detection";
|
|
60
61
|
import {
|
|
61
62
|
CONTEXT_FORMULA,
|
|
@@ -268,6 +269,12 @@ class ClaudeManagedSession implements ProviderSession {
|
|
|
268
269
|
private readonly taskId: string | null;
|
|
269
270
|
private aborted = false;
|
|
270
271
|
private settled = false;
|
|
272
|
+
/** Prompt size of the latest model request: input + cache read + cache write. */
|
|
273
|
+
private lastRequestContextTokens: number | undefined;
|
|
274
|
+
/** Fed the latest request's prompt size on each tool call; steers the one-time context-bound note. */
|
|
275
|
+
private readonly observeContextBound: (contextTokens: number | undefined) => Promise<void>;
|
|
276
|
+
/** Tool calls the stream started and has not returned a result for. */
|
|
277
|
+
private readonly openToolCalls = new Set<string>();
|
|
271
278
|
|
|
272
279
|
constructor(
|
|
273
280
|
client: ManagedAgentsClient,
|
|
@@ -286,6 +293,14 @@ class ClaudeManagedSession implements ProviderSession {
|
|
|
286
293
|
this.completionPromise = new Promise<ProviderResult>((resolve) => {
|
|
287
294
|
this.resolveCompletion = resolve;
|
|
288
295
|
});
|
|
296
|
+
this.observeContextBound = createContextBoundNote({
|
|
297
|
+
threshold: contextBoundNoteThreshold(config.env ?? process.env),
|
|
298
|
+
model: () => this.cost.model,
|
|
299
|
+
// A message with no tool call left to return, or after idle or abort,
|
|
300
|
+
// would start another turn.
|
|
301
|
+
canDeliver: () => !this.settled && !this.aborted && this.openToolCalls.size > 0,
|
|
302
|
+
inject: (note) => this.deliverSteering({ mode: "steer", text: note }),
|
|
303
|
+
});
|
|
289
304
|
|
|
290
305
|
// Phase 5: adapter-side swarm hooks. Lower-latency cancellation poll,
|
|
291
306
|
// tool-loop detection (the handler also calls `checkToolLoop` on
|
|
@@ -497,6 +512,11 @@ class ClaudeManagedSession implements ProviderSession {
|
|
|
497
512
|
case "agent.tool_use": {
|
|
498
513
|
const tu = event as BetaManagedAgentsAgentToolUseEvent;
|
|
499
514
|
this.runToolLoopCheck(tu.name, tu.input);
|
|
515
|
+
// Only on tool calls: a message after the final reply would start another
|
|
516
|
+
// turn. The stream can send the call before its request ends, so this may
|
|
517
|
+
// read the previous request: at most one request late.
|
|
518
|
+
this.openToolCalls.add(tu.id);
|
|
519
|
+
void this.observeContextBound(this.lastRequestContextTokens);
|
|
500
520
|
this.emit({
|
|
501
521
|
type: "tool_start",
|
|
502
522
|
toolCallId: tu.id,
|
|
@@ -509,6 +529,8 @@ class ClaudeManagedSession implements ProviderSession {
|
|
|
509
529
|
const tu = event as BetaManagedAgentsAgentMCPToolUseEvent;
|
|
510
530
|
const fqToolName = `${tu.mcp_server_name}:${tu.name}`;
|
|
511
531
|
this.runToolLoopCheck(fqToolName, tu.input);
|
|
532
|
+
this.openToolCalls.add(tu.id);
|
|
533
|
+
void this.observeContextBound(this.lastRequestContextTokens);
|
|
512
534
|
this.emit({
|
|
513
535
|
type: "tool_start",
|
|
514
536
|
toolCallId: tu.id,
|
|
@@ -519,6 +541,7 @@ class ClaudeManagedSession implements ProviderSession {
|
|
|
519
541
|
}
|
|
520
542
|
case "agent.tool_result": {
|
|
521
543
|
const tr = event as BetaManagedAgentsAgentToolResultEvent;
|
|
544
|
+
this.openToolCalls.delete(tr.tool_use_id);
|
|
522
545
|
this.emit({
|
|
523
546
|
type: "tool_end",
|
|
524
547
|
toolCallId: tr.tool_use_id,
|
|
@@ -533,6 +556,7 @@ class ClaudeManagedSession implements ProviderSession {
|
|
|
533
556
|
}
|
|
534
557
|
case "agent.mcp_tool_result": {
|
|
535
558
|
const tr = event as BetaManagedAgentsAgentMCPToolResultEvent;
|
|
559
|
+
this.openToolCalls.delete(tr.mcp_tool_use_id);
|
|
536
560
|
this.emit({
|
|
537
561
|
type: "tool_end",
|
|
538
562
|
toolCallId: tr.mcp_tool_use_id,
|
|
@@ -571,6 +595,8 @@ class ClaudeManagedSession implements ProviderSession {
|
|
|
571
595
|
case "span.model_request_end": {
|
|
572
596
|
const sp = event as BetaManagedAgentsSpanModelRequestEndEvent;
|
|
573
597
|
const usage = sp.model_usage;
|
|
598
|
+
this.lastRequestContextTokens =
|
|
599
|
+
usage.input_tokens + usage.cache_read_input_tokens + usage.cache_creation_input_tokens;
|
|
574
600
|
this.cost.inputTokens = (this.cost.inputTokens ?? 0) + usage.input_tokens;
|
|
575
601
|
this.cost.outputTokens = (this.cost.outputTokens ?? 0) + usage.output_tokens;
|
|
576
602
|
this.cost.cacheReadTokens =
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type SpawnedProcess,
|
|
10
10
|
type SpawnOptions,
|
|
11
11
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
12
|
+
import { recordContextState } from "../hooks/context-bound-note";
|
|
12
13
|
import type { RunStopHookSessionSummaryOpts } from "../hooks/hook";
|
|
13
14
|
import { getContextWindowSize } from "../utils/context-window";
|
|
14
15
|
import {
|
|
@@ -316,6 +317,8 @@ class ClaudeSdkSession implements ProviderSession {
|
|
|
316
317
|
private contextWindowSize: number;
|
|
317
318
|
private transcript: string[];
|
|
318
319
|
private lastAssistantText = "";
|
|
320
|
+
/** Last value written to the context state file read by the context-bound note hook. */
|
|
321
|
+
private lastContextTokens: number | undefined;
|
|
319
322
|
private lastCost: CostData | undefined;
|
|
320
323
|
private aborted = false;
|
|
321
324
|
private abortReason = "cancelled";
|
|
@@ -434,6 +437,13 @@ class ClaudeSdkSession implements ProviderSession {
|
|
|
434
437
|
if (normalized.contextWindowSize) this.contextWindowSize = normalized.contextWindowSize;
|
|
435
438
|
if (normalized.cost) this.lastCost = normalized.cost;
|
|
436
439
|
if (normalized.assistantText) this.lastAssistantText = normalized.assistantText;
|
|
440
|
+
if (
|
|
441
|
+
normalized.requestContextTokens !== undefined &&
|
|
442
|
+
normalized.requestContextTokens !== this.lastContextTokens
|
|
443
|
+
) {
|
|
444
|
+
this.lastContextTokens = normalized.requestContextTokens;
|
|
445
|
+
recordContextState(this.options.taskFilePath, normalized.requestContextTokens, this.model);
|
|
446
|
+
}
|
|
437
447
|
this.transcript.push(...normalized.transcriptEntries);
|
|
438
448
|
for (const event of normalized.events) this.emit(event);
|
|
439
449
|
trackErrorFromJson(json, this.errorTracker);
|
|
@@ -60,6 +60,11 @@ export type NormalizedClaudeMessage = {
|
|
|
60
60
|
contextWindowSize?: number;
|
|
61
61
|
cost?: CostData;
|
|
62
62
|
assistantText?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Prompt size of the latest main-thread request: input + cache read + cache
|
|
65
|
+
* write tokens. Unset for subagent messages and messages without usage.
|
|
66
|
+
*/
|
|
67
|
+
requestContextTokens?: number;
|
|
63
68
|
isResult: boolean;
|
|
64
69
|
};
|
|
65
70
|
|
|
@@ -150,6 +155,7 @@ export function normalizeClaudeMessage(
|
|
|
150
155
|
let model: string | undefined;
|
|
151
156
|
let contextWindowSize: number | undefined;
|
|
152
157
|
let assistantText: string | undefined;
|
|
158
|
+
let requestContextTokens: number | undefined;
|
|
153
159
|
|
|
154
160
|
if (json.type === "system" && json.subtype === "init" && json.session_id) {
|
|
155
161
|
sessionId = json.session_id;
|
|
@@ -227,6 +233,12 @@ export function normalizeClaudeMessage(
|
|
|
227
233
|
outputTokens: usage.output_tokens ?? 0,
|
|
228
234
|
contextFormula: CONTEXT_FORMULA,
|
|
229
235
|
});
|
|
236
|
+
if (!json.parent_tool_use_id) {
|
|
237
|
+
requestContextTokens =
|
|
238
|
+
(usage.input_tokens ?? 0) +
|
|
239
|
+
(usage.cache_read_input_tokens ?? 0) +
|
|
240
|
+
(usage.cache_creation_input_tokens ?? 0);
|
|
241
|
+
}
|
|
230
242
|
}
|
|
231
243
|
}
|
|
232
244
|
|
|
@@ -247,6 +259,7 @@ export function normalizeClaudeMessage(
|
|
|
247
259
|
contextWindowSize,
|
|
248
260
|
cost,
|
|
249
261
|
assistantText,
|
|
262
|
+
requestContextTokens,
|
|
250
263
|
isResult: json.type === "result",
|
|
251
264
|
};
|
|
252
265
|
}
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import { existsSync as nodeExistsSync } from "node:fs";
|
|
32
32
|
import { join } from "node:path";
|
|
33
33
|
import { buildRatingsFromLlm, fetchRetrievalsForTask, postRatings } from "../be/memory/raters/llm";
|
|
34
|
+
import { contextBoundNoteThreshold, createContextBoundNote } from "../hooks/context-bound-note";
|
|
34
35
|
import { getApiKey } from "../utils/api-key";
|
|
35
36
|
import {
|
|
36
37
|
CONTEXT_FORMULA,
|
|
@@ -995,6 +996,8 @@ export class CodexSession implements ProviderSession {
|
|
|
995
996
|
* summary path which materially relies on this ordering for testability.
|
|
996
997
|
*/
|
|
997
998
|
private pendingResult: ProviderResult | null = null;
|
|
999
|
+
/** Fed each main-thread request's prompt size; steers the one-time context-bound note. */
|
|
1000
|
+
private readonly observeContextBound: (contextTokens: number | undefined) => Promise<void>;
|
|
998
1001
|
|
|
999
1002
|
constructor(
|
|
1000
1003
|
thread: CodexThread,
|
|
@@ -1015,6 +1018,13 @@ export class CodexSession implements ProviderSession {
|
|
|
1015
1018
|
skillsDir ?? process.env.CODEX_SKILLS_DIR ?? join(codexUserHome(), ".codex", "skills");
|
|
1016
1019
|
this.summarizeDeps = summarizeDeps;
|
|
1017
1020
|
this.logFileHandle = Bun.file(config.logFile).writer();
|
|
1021
|
+
this.observeContextBound = createContextBoundNote({
|
|
1022
|
+
threshold: contextBoundNoteThreshold(config.env ?? process.env),
|
|
1023
|
+
model: () => this.resolvedModel,
|
|
1024
|
+
// `turn/steer` only joins the active turn, so a late note never starts one.
|
|
1025
|
+
canDeliver: () => this.acceptsSteering && !this.settled && !this.aborted,
|
|
1026
|
+
inject: (note) => this.deliverSteering({ mode: "steer", text: note }),
|
|
1027
|
+
});
|
|
1018
1028
|
// Mirrors the resolution `buildCodexConfig` already applied when
|
|
1019
1029
|
// constructing `mergedConfig` — cheap, pure, and keeps the applied-level
|
|
1020
1030
|
// feedback co-located with `settle()` rather than threading an extra
|
|
@@ -1425,6 +1435,8 @@ export class CodexSession implements ProviderSession {
|
|
|
1425
1435
|
break;
|
|
1426
1436
|
case "context.updated":
|
|
1427
1437
|
this.emitContextUsage(event.usage, event.contextWindow ?? this.contextWindow);
|
|
1438
|
+
// Codex input tokens already include the cached read and write subsets.
|
|
1439
|
+
void this.observeContextBound(event.usage.input_tokens);
|
|
1428
1440
|
break;
|
|
1429
1441
|
case "error": {
|
|
1430
1442
|
const { message } = this.formatTerminalError(event.message, event.codexErrorInfo);
|
|
@@ -37,7 +37,7 @@ import type {
|
|
|
37
37
|
} from "./types";
|
|
38
38
|
|
|
39
39
|
// npm's `latest` is older and lacks the JSON/stdin contract used here.
|
|
40
|
-
export const DSH_PACKAGE = "@deepseek-ai/dsh@0.2.1-rc.
|
|
40
|
+
export const DSH_PACKAGE = "@deepseek-ai/dsh@0.2.1-rc.3";
|
|
41
41
|
|
|
42
42
|
export function checkDshCredentials(env: Record<string, string | undefined>): CredStatus {
|
|
43
43
|
return env.DEEPSEEK_API_KEY?.trim() || env.OPENROUTER_API_KEY?.trim()
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
providerKeyVars,
|
|
22
22
|
} from "@desplega/model-catalog";
|
|
23
23
|
import type { OpenCode, OpenCodeEvent } from "@opencode/sdk";
|
|
24
|
+
import { contextBoundNoteThreshold, createContextBoundNote } from "../hooks/context-bound-note";
|
|
24
25
|
import {
|
|
25
26
|
CONTEXT_FORMULA,
|
|
26
27
|
clampContextPercent,
|
|
@@ -534,6 +535,8 @@ export class OpencodeSession implements ProviderSession {
|
|
|
534
535
|
// Assistant text by message, in part order; the runner keeps the last emit.
|
|
535
536
|
private textByMessage = new Map<string, string[]>();
|
|
536
537
|
private transcript: string[] = [];
|
|
538
|
+
/** Fed each step's prompt size; steers the one-time context-bound note. */
|
|
539
|
+
private readonly observeContextBound: (contextTokens: number | undefined) => Promise<void>;
|
|
537
540
|
|
|
538
541
|
constructor(
|
|
539
542
|
private readonly host: OpenCode.Interface,
|
|
@@ -547,6 +550,20 @@ export class OpencodeSession implements ProviderSession {
|
|
|
547
550
|
this.completionPromise = new Promise<ProviderResult>((resolve) => {
|
|
548
551
|
this.completionResolve = resolve;
|
|
549
552
|
});
|
|
553
|
+
this.observeContextBound = createContextBoundNote({
|
|
554
|
+
threshold: contextBoundNoteThreshold(swarm.env ?? process.env),
|
|
555
|
+
model: () => this.model,
|
|
556
|
+
canDeliver: () => !this.completed && !this.aborted,
|
|
557
|
+
// A steer is promoted at the next step boundary mid-work; `resume: false`
|
|
558
|
+
// leaves an idle session idle. Same route as OpenCode's own Plan reminder.
|
|
559
|
+
inject: (note) =>
|
|
560
|
+
host.sessions.synthetic({
|
|
561
|
+
sessionID: _sessionId,
|
|
562
|
+
text: note,
|
|
563
|
+
delivery: "steer",
|
|
564
|
+
resume: false,
|
|
565
|
+
}),
|
|
566
|
+
});
|
|
550
567
|
}
|
|
551
568
|
|
|
552
569
|
get sessionId(): string {
|
|
@@ -685,6 +702,7 @@ export class OpencodeSession implements ProviderSession {
|
|
|
685
702
|
// Per-step usage drives the dashboard's context gauge.
|
|
686
703
|
const t = ev.data.tokens;
|
|
687
704
|
const contextUsed = t.input + t.cache.read + t.cache.write + t.output;
|
|
705
|
+
void this.observeContextBound(t.input + t.cache.read + t.cache.write);
|
|
688
706
|
const contextTotal = getContextWindowSize(this.model || "default");
|
|
689
707
|
if (contextTotal > 0 && contextUsed > 0) {
|
|
690
708
|
this.emit({
|
|
@@ -61,6 +61,7 @@ import { type TSchema, Type } from "typebox";
|
|
|
61
61
|
import "./pi-codemode-runtime";
|
|
62
62
|
// Registers the reprompt template in the code registry (the runner loads it too).
|
|
63
63
|
import "../commands/templates";
|
|
64
|
+
import { contextBoundNoteThreshold, createContextBoundNote } from "../hooks/context-bound-note";
|
|
64
65
|
import { resolveTemplateAsync } from "../prompts/resolver";
|
|
65
66
|
import { CORE_TOOLS } from "../tools/tool-config";
|
|
66
67
|
import { classifyAwsSdkError } from "../utils/aws-error-classifier";
|
|
@@ -907,6 +908,17 @@ function safeLabel(value: unknown): string {
|
|
|
907
908
|
return typeof value === "string" && /^[A-Za-z0-9_-]{1,32}$/.test(value) ? value : "other";
|
|
908
909
|
}
|
|
909
910
|
|
|
911
|
+
/** Prompt size of the request behind an assistant message: input + cache read + cache write. */
|
|
912
|
+
function requestContextTokens(message: unknown): number | undefined {
|
|
913
|
+
const usage = (
|
|
914
|
+
message as { usage?: { input?: unknown; cacheRead?: unknown; cacheWrite?: unknown } }
|
|
915
|
+
)?.usage;
|
|
916
|
+
if (typeof usage?.input !== "number") return undefined;
|
|
917
|
+
const cacheRead = typeof usage.cacheRead === "number" ? usage.cacheRead : 0;
|
|
918
|
+
const cacheWrite = typeof usage.cacheWrite === "number" ? usage.cacheWrite : 0;
|
|
919
|
+
return usage.input + cacheRead + cacheWrite;
|
|
920
|
+
}
|
|
921
|
+
|
|
910
922
|
function describeAssistantTurn(message: unknown): AssistantTurnShape {
|
|
911
923
|
const msg = (message ?? {}) as {
|
|
912
924
|
content?: unknown;
|
|
@@ -1008,6 +1020,14 @@ export class PiMonoSession implements ProviderSession {
|
|
|
1008
1020
|
private abortRequested = false;
|
|
1009
1021
|
/** Removes this session's own models.json once the session ends. */
|
|
1010
1022
|
private disposeModels: () => Promise<void>;
|
|
1023
|
+
/** Fed each assistant request's prompt size; steers the one-time context-bound note. */
|
|
1024
|
+
private readonly observeContextBound: (contextTokens: number | undefined) => Promise<void>;
|
|
1025
|
+
/**
|
|
1026
|
+
* From a tool-call reply until its turn ends. Pi reads steering after the
|
|
1027
|
+
* turn's tool results; a steer queued later waits for the next reply, and
|
|
1028
|
+
* when that is the final one it starts another turn.
|
|
1029
|
+
*/
|
|
1030
|
+
private toolTurnOpen = false;
|
|
1011
1031
|
|
|
1012
1032
|
constructor(
|
|
1013
1033
|
agentSession: AgentSession,
|
|
@@ -1024,6 +1044,12 @@ export class PiMonoSession implements ProviderSession {
|
|
|
1024
1044
|
this.logFileHandle = Bun.file(config.logFile).writer();
|
|
1025
1045
|
this._sessionId = agentSession.sessionId;
|
|
1026
1046
|
this.sessionStartedAt = Date.now();
|
|
1047
|
+
this.observeContextBound = createContextBoundNote({
|
|
1048
|
+
threshold: contextBoundNoteThreshold(config.env ?? process.env),
|
|
1049
|
+
model: () => this.reportedModel(),
|
|
1050
|
+
canDeliver: () => this.toolTurnOpen && !this.sessionEnded && !this.abortRequested,
|
|
1051
|
+
inject: (note) => this.deliverSteering({ mode: "steer", text: note }),
|
|
1052
|
+
});
|
|
1027
1053
|
|
|
1028
1054
|
// Emit session_init immediately
|
|
1029
1055
|
const piVersion = readPkgVersion("@earendil-works/pi-coding-agent");
|
|
@@ -1098,6 +1124,12 @@ export class PiMonoSession implements ProviderSession {
|
|
|
1098
1124
|
}
|
|
1099
1125
|
// A successful assistant turn means any prior error has recovered.
|
|
1100
1126
|
this.terminalError = null;
|
|
1127
|
+
// Only on tool-call turns: a steer after the final reply would start
|
|
1128
|
+
// another turn.
|
|
1129
|
+
if (turn.hasToolCall) {
|
|
1130
|
+
this.toolTurnOpen = true;
|
|
1131
|
+
void this.observeContextBound(requestContextTokens(event.message));
|
|
1132
|
+
}
|
|
1101
1133
|
if (!turn.hasText && !turn.hasToolCall) {
|
|
1102
1134
|
// Nothing else records these turns: no text and no tool call leaves
|
|
1103
1135
|
// no trace in session logs. Block types only, never content.
|
|
@@ -1152,6 +1184,9 @@ export class PiMonoSession implements ProviderSession {
|
|
|
1152
1184
|
}
|
|
1153
1185
|
break;
|
|
1154
1186
|
}
|
|
1187
|
+
case "turn_end":
|
|
1188
|
+
this.toolTurnOpen = false;
|
|
1189
|
+
break;
|
|
1155
1190
|
case "tool_execution_start": {
|
|
1156
1191
|
const status = (event.args as { status?: unknown } | null)?.status;
|
|
1157
1192
|
if (
|
|
@@ -177,7 +177,9 @@ export class DockerRuntimeProvider implements RuntimeProvider {
|
|
|
177
177
|
try {
|
|
178
178
|
await dockerApi(target, "POST", `/containers/${created.Id}/start`);
|
|
179
179
|
} catch (err) {
|
|
180
|
-
await dockerApi(target, "DELETE", `/containers/${created.Id}?force=true`).catch(
|
|
180
|
+
await dockerApi(target, "DELETE", `/containers/${created.Id}?force=true&v=true`).catch(
|
|
181
|
+
() => {},
|
|
182
|
+
);
|
|
181
183
|
throw err;
|
|
182
184
|
}
|
|
183
185
|
return { kind: "docker", externalId: created.Id };
|
|
@@ -188,7 +190,9 @@ export class DockerRuntimeProvider implements RuntimeProvider {
|
|
|
188
190
|
|
|
189
191
|
async terminate(target: RuntimeTargetConfig, handle: RuntimeHandle): Promise<void> {
|
|
190
192
|
try {
|
|
191
|
-
|
|
193
|
+
// v=true also removes the container's anonymous volumes (the worker image declares
|
|
194
|
+
// VOLUME /logs); named volumes are never removed by it.
|
|
195
|
+
await dockerApi(target, "DELETE", `/containers/${handle.externalId}?force=true&v=true`);
|
|
192
196
|
} catch (err) {
|
|
193
197
|
if (!(err instanceof DockerApiError) || err.statusCode !== 404) throw err;
|
|
194
198
|
}
|
|
@@ -402,6 +402,36 @@ declare module "swarm-extension" {
|
|
|
402
402
|
origin: TaskCreateOrigin;
|
|
403
403
|
requestInfo?: RequestInfo;
|
|
404
404
|
}
|
|
405
|
+
/** A pre.task.create hook blocked a task before it was created. */
|
|
406
|
+
export interface TaskBlockedEvent {
|
|
407
|
+
/** The extension that blocked, or whose model/agent rewrite was rejected. */
|
|
408
|
+
extension: {
|
|
409
|
+
id: string;
|
|
410
|
+
name: string;
|
|
411
|
+
};
|
|
412
|
+
/** The scrubbed block reason. */
|
|
413
|
+
reason: string;
|
|
414
|
+
origin: TaskCreateOrigin;
|
|
415
|
+
source?: AgentTask["source"];
|
|
416
|
+
requester: {
|
|
417
|
+
userId?: string;
|
|
418
|
+
agentId?: string;
|
|
419
|
+
slackUserId?: string;
|
|
420
|
+
};
|
|
421
|
+
context?: {
|
|
422
|
+
contextKey?: string;
|
|
423
|
+
scheduleId?: string;
|
|
424
|
+
workflowRunId?: string;
|
|
425
|
+
workflowRunStepId?: string;
|
|
426
|
+
parentTaskId?: string;
|
|
427
|
+
vcsRepo?: string;
|
|
428
|
+
vcsNumber?: number;
|
|
429
|
+
slackChannelId?: string;
|
|
430
|
+
slackThreadTs?: string;
|
|
431
|
+
};
|
|
432
|
+
/** The original task description, scrubbed and cut to 500 characters. */
|
|
433
|
+
description: string;
|
|
434
|
+
}
|
|
405
435
|
export type TaskCreateModify = Partial<
|
|
406
436
|
Pick<
|
|
407
437
|
CreateTaskOptions,
|
|
@@ -704,6 +734,11 @@ declare module "swarm-extension" {
|
|
|
704
734
|
modify: never;
|
|
705
735
|
result: void;
|
|
706
736
|
};
|
|
737
|
+
"post.task.blocked": {
|
|
738
|
+
event: TaskBlockedEvent;
|
|
739
|
+
modify: never;
|
|
740
|
+
result: void;
|
|
741
|
+
};
|
|
707
742
|
"post.task.completed": {
|
|
708
743
|
event: TaskCompletedEvent;
|
|
709
744
|
modify: never;
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
validateProviderCredentials,
|
|
9
9
|
} from "../commands/provider-credentials";
|
|
10
10
|
import { recomputeSessionCost } from "../http/session-cost-recompute";
|
|
11
|
+
import { renderContextBoundNote } from "../prompts/context-bound-note";
|
|
11
12
|
import { createProviderAdapter } from "../providers";
|
|
12
13
|
import {
|
|
13
14
|
AMP_PACKAGE,
|
|
@@ -31,6 +32,7 @@ import { getModelAwareCredentialVars } from "../utils/credentials";
|
|
|
31
32
|
import { resolveHarnessProvider } from "../utils/harness-provider";
|
|
32
33
|
import { clearVolatileSecretsForTesting } from "../utils/secret-scrubber";
|
|
33
34
|
import { ampPricingLookup } from "./amp-pricing-helpers";
|
|
35
|
+
import { holdTemplateRender } from "./fixtures/held-template-render";
|
|
34
36
|
|
|
35
37
|
const KEY = "sgamp_test_credential_value";
|
|
36
38
|
|
|
@@ -769,6 +771,101 @@ describe("amp session", () => {
|
|
|
769
771
|
}, 20_000);
|
|
770
772
|
});
|
|
771
773
|
|
|
774
|
+
describe("amp context-bound note", () => {
|
|
775
|
+
// Each fake request is 4 input + 200 cache read + 1000 cache write = 1204 tokens.
|
|
776
|
+
async function toolResults(
|
|
777
|
+
mode: string,
|
|
778
|
+
env: Record<string, string>,
|
|
779
|
+
overrides: Partial<ProviderSessionConfig> = {},
|
|
780
|
+
) {
|
|
781
|
+
const { config } = await fixture(mode, overrides, env);
|
|
782
|
+
const { events, result } = await runToCompletion(config);
|
|
783
|
+
const results = events.flatMap((e) => (e.type === "tool_end" ? [e.result] : []));
|
|
784
|
+
return { results, result };
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
test("queues the note into the first tool result at the threshold, then never again", async () => {
|
|
788
|
+
const { results, result } = await toolResults("steer-twice", {
|
|
789
|
+
CONTEXT_BOUND_NOTE_TOKENS: "1204",
|
|
790
|
+
});
|
|
791
|
+
expect(results).toEqual([`2\n\n${await renderContextBoundNote(1204)}`, "2"]);
|
|
792
|
+
// Carried inside the turn: no extra turn replays it.
|
|
793
|
+
expect(result).toMatchObject({ isError: false, output: "DONE1" });
|
|
794
|
+
expect(result.cost?.numTurns).toBe(1);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
test("a pinned model gets the window share; a mode names no model and gets none", async () => {
|
|
798
|
+
const pin = "anthropic/claude-sonnet-4-6";
|
|
799
|
+
const pinned = await toolResults(
|
|
800
|
+
"steer-twice",
|
|
801
|
+
{ CONTEXT_BOUND_NOTE_TOKENS: "1204" },
|
|
802
|
+
{ model: pin },
|
|
803
|
+
);
|
|
804
|
+
const note = await renderContextBoundNote(1204, pin);
|
|
805
|
+
expect(note).toContain("of the 1,000,000-token window)");
|
|
806
|
+
expect(pinned.results).toEqual([`2\n\n${note}`, "2"]);
|
|
807
|
+
const mode = await toolResults("steer-twice", { CONTEXT_BOUND_NOTE_TOKENS: "1204" });
|
|
808
|
+
expect(mode.results[0]).not.toContain("token window");
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
test("no-op below the threshold and when disabled at 0", async () => {
|
|
812
|
+
for (const threshold of ["1205", "0"]) {
|
|
813
|
+
const { results } = await toolResults("steer-twice", {
|
|
814
|
+
CONTEXT_BOUND_NOTE_TOKENS: threshold,
|
|
815
|
+
});
|
|
816
|
+
expect(results).toEqual(["2", "2"]);
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
test("fails open on missing or corrupt request usage", async () => {
|
|
821
|
+
for (const usage of ["missing", "corrupt"]) {
|
|
822
|
+
const { results, result } = await toolResults("steer", {
|
|
823
|
+
CONTEXT_BOUND_NOTE_TOKENS: "1",
|
|
824
|
+
AMP_TEST_REQUEST_USAGE: usage,
|
|
825
|
+
});
|
|
826
|
+
expect(results).toEqual(["2"]);
|
|
827
|
+
expect(result).toMatchObject({ isError: false, output: "DONE1" });
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test("a final reply with no tool call never queues it, so no new turn starts", async () => {
|
|
832
|
+
const { results, result } = await toolResults("steer-no-tool", {
|
|
833
|
+
CONTEXT_BOUND_NOTE_TOKENS: "1",
|
|
834
|
+
});
|
|
835
|
+
expect(results).toEqual([]);
|
|
836
|
+
expect(result).toMatchObject({ isError: false, output: "DONE1" });
|
|
837
|
+
expect(result.cost?.numTurns).toBe(1);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
test("a note rendered after the last tool result is dropped, not replayed as a new turn", async () => {
|
|
841
|
+
const held = holdTemplateRender();
|
|
842
|
+
try {
|
|
843
|
+
const { config } = await fixture(
|
|
844
|
+
"steer",
|
|
845
|
+
{},
|
|
846
|
+
{
|
|
847
|
+
CONTEXT_BOUND_NOTE_TOKENS: "1204",
|
|
848
|
+
AMP_TEST_POST_RESULT_DELAY_MS: "150",
|
|
849
|
+
},
|
|
850
|
+
);
|
|
851
|
+
const session = await new AmpAdapter().createSession(config);
|
|
852
|
+
const results: unknown[] = [];
|
|
853
|
+
session.onEvent((event) => {
|
|
854
|
+
if (event.type !== "tool_end") return;
|
|
855
|
+
results.push(event.result);
|
|
856
|
+
// Ready while the final reply is pending.
|
|
857
|
+
held.release();
|
|
858
|
+
});
|
|
859
|
+
const result = await session.waitForCompletion();
|
|
860
|
+
expect(results).toEqual(["2"]);
|
|
861
|
+
expect(result).toMatchObject({ isError: false, output: "DONE1" });
|
|
862
|
+
expect(result.cost?.numTurns).toBe(1);
|
|
863
|
+
} finally {
|
|
864
|
+
held.stop();
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
|
|
772
869
|
describe("amp live test", () => {
|
|
773
870
|
const env = async (mode: string) => {
|
|
774
871
|
const { config } = await fixture(mode);
|