@desplega.ai/agent-swarm 2.5.3 → 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-gr2fcyyj.js → acp-adapter-ek38cwsv.js} +4 -4
- package/dist/{actions-wv766ghf.js → actions-92mepq03.js} +17 -17
- package/dist/{amp-adapter-speb906t.js → amp-adapter-9gaxg015.js} +2 -2
- package/dist/{app-ag29n14f.js → app-ctn8gsgx.js} +3 -3
- package/dist/{assistant-3x8bz3eg.js → assistant-zyr5kz47.js} +19 -19
- package/dist/{boot-reembed-jsmfrpzf.js → boot-reembed-dwzfttdr.js} +1 -1
- package/dist/{boot-reembed-5b3d2eea.js → boot-reembed-w75maga2.js} +1 -1
- package/dist/{boot-scrub-sweep-s73gerxy.js → boot-scrub-sweep-e9kq43kf.js} +3 -3
- package/dist/{boot-seeds-5zmax2wr.js → boot-seeds-x28v3fhb.js} +4 -4
- package/dist/{claude-adapter-hswdnm3t.js → claude-adapter-f0afqnne.js} +4 -4
- package/dist/{claude-managed-adapter-cvbzj1h6.js → claude-managed-adapter-x81a7r65.js} +3 -3
- package/dist/{claude-managed-setup-3k8m3hhc.js → claude-managed-setup-bqzwjd9x.js} +1 -1
- package/dist/{claude-sdk-session-fkbyk66x.js → claude-sdk-session-7b7whkaq.js} +9307 -9254
- package/dist/{cli-a5ebc95n.js → cli-0shx9j93.js} +16 -13
- package/dist/{cli-vwn81jbs.js → cli-19202kew.js} +3 -3
- package/dist/{cli-svrj71pg.js → cli-1r5ad7j5.js} +7 -7
- package/dist/{cli-hhxjnjzs.js → cli-25n3mv5d.js} +8 -6
- package/dist/cli-2655k16q.js +140 -0
- package/dist/{cli-hcpj2xqz.js → cli-277c31dw.js} +1 -1
- package/dist/{cli-35gtk1p6.js → cli-4b2z6n0m.js} +29 -7
- package/dist/{cli-40jsnrjm.js → cli-5bnsc0e1.js} +7 -5
- package/dist/{cli-fzcn3d87.js → cli-621ce7kw.js} +1 -1
- package/dist/{cli-s49hpn37.js → cli-64mh9mf3.js} +19 -5
- package/dist/{cli-e8jyq7d8.js → cli-6a1xg1p7.js} +6 -2
- package/dist/{cli-p9rafdrb.js → cli-7p4jxgtt.js} +4 -4
- package/dist/{cli-w1zwctj2.js → cli-9j129brq.js} +9 -3
- package/dist/{cli-ce0ak9ms.js → cli-abs8ndtr.js} +11 -11
- package/dist/{cli-aecdcr77.js → cli-asq5ydm4.js} +123 -50
- package/dist/{cli-n45mr7zh.js → cli-bvpgr9ny.js} +2 -2
- package/dist/{cli-210xd1f4.js → cli-c61mbdds.js} +3 -3
- 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-s53vp709.js → cli-eftzyh9y.js} +1 -1
- package/dist/{cli-3ngdk5te.js → cli-ethgy7ha.js} +1 -1
- package/dist/{cli-99xgw26r.js → cli-ff1tm9n9.js} +1 -1
- package/dist/{cli-ewm3pwbq.js → cli-g9vd9ece.js} +37 -12
- package/dist/{cli-jjznk3f7.js → cli-gbfxp48k.js} +62 -28
- package/dist/{cli-z21whrrg.js → cli-gn5af0x5.js} +14 -14
- package/dist/{cli-1fc85mvt.js → cli-gwhjmh8g.js} +4 -4
- package/dist/{cli-avdvgcay.js → cli-hh5we352.js} +24 -5
- package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
- package/dist/{cli-kxawjrxg.js → cli-jb30dzvk.js} +332 -36
- package/dist/{cli-25j01dpj.js → cli-jbnn4vxb.js} +3 -3
- package/dist/{cli-qzme5949.js → cli-k4tjsdx9.js} +44 -39
- package/dist/{cli-8s5wx3nz.js → cli-m4ecdxvz.js} +40 -13
- package/dist/{cli-ta8bd7th.js → cli-pzatvw94.js} +1 -1
- package/dist/{cli-p6zs9h4y.js → cli-rr7zx19j.js} +767 -249
- package/dist/{cli-502eqv93.js → cli-s682zgqz.js} +1 -1
- package/dist/{cli-fsdprwrg.js → cli-sbwsxvtd.js} +1 -1
- package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
- package/dist/{cli-egjggehm.js → cli-tsma7p8v.js} +1 -1
- package/dist/{cli-qgf8emrn.js → cli-wghjkr0e.js} +5 -5
- package/dist/{cli-p0rzjkya.js → cli-wx2nj19m.js} +29 -10
- package/dist/{cli-e2ar4bp1.js → cli-x0f2v69c.js} +111 -42
- package/dist/{cli-y561h5a9.js → cli-xtrz6bzs.js} +5 -5
- package/dist/{cli-gpsz7g88.js → cli-xzfq4at3.js} +7 -7
- package/dist/cli.js +18 -18
- package/dist/{codex-adapter-n8ka66vf.js → codex-adapter-wch12q99.js} +2 -2
- package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
- package/dist/{codex-session-runner-fsebts37.js → codex-session-runner-41pfcr95.js} +2 -2
- package/dist/{commands-t04rt2pc.js → commands-9849dkj6.js} +2 -2
- package/dist/{controller-rgna04r2.js → controller-ja0sd95p.js} +27 -1
- package/dist/{controllers-connect-bqm1rtft.js → controllers-connect-xp05dj6x.js} +16 -16
- package/dist/{cursor-adapter-swr2k95y.js → cursor-adapter-623qxqew.js} +1 -2
- package/dist/{db-a4m7haw0.js → db-47vj40dy.js} +3 -3
- package/dist/{db-24ck09xy.js → db-w759qmx0.js} +2 -2
- package/dist/{dispatcher-nwf9ss0q.js → dispatcher-7b8e4z2j.js} +15 -15
- package/dist/{docker-nfy2gc8t.js → docker-s84hjs91.js} +2 -2
- package/dist/{dsh-adapter-kpdnhjst.js → dsh-adapter-2ezw3xea.js} +1 -2
- package/dist/{external-channel-j1e2p34p.js → external-channel-e2s028da.js} +2 -2
- package/dist/{grok-adapter-qtjz0whk.js → grok-adapter-ys7gaqee.js} +5 -5
- package/dist/{handlers-19xr6dnz.js → handlers-wy1dr6ja.js} +18 -18
- package/dist/{heartbeat-vtesda5d.js → heartbeat-w3mntghj.js} +17 -17
- package/dist/{hook-8wqaszda.js → hook-3jjywnpb.js} +9 -3
- package/dist/{hook-b2hhj6rd.js → hook-g7kvv6x9.js} +3 -2
- package/dist/{http-mxcqhhcs.js → http-tkp7d4kb.js} +267 -75
- package/dist/{index-srg522m6.js → index-1ef1myq2.js} +15 -15
- package/dist/{index-1r8wfpzs.js → index-3n8m9ea2.js} +1 -1
- package/dist/{index-kw2mmat7.js → index-nmm79qgr.js} +17 -17
- package/dist/{index-n1vbvta9.js → index-q7m26kvs.js} +24 -22
- package/dist/{index-spwxjgay.js → index-xdaahthb.js} +15 -15
- package/dist/{keepalive-dsd8rtgx.js → keepalive-v4ta8wgg.js} +3 -3
- package/dist/{lead-zyerk1x2.js → lead-sawnyqxk.js} +33 -33
- package/dist/{maintenance-jqmqwnet.js → maintenance-84n1ex21.js} +1 -1
- package/dist/{oauth-refresh-sweep-q2c3bnvm.js → oauth-refresh-sweep-vjnnandw.js} +2 -2
- package/dist/{onboard-w4yd7s8t.js → onboard-sfmpe4np.js} +2 -2
- package/dist/{opencode-adapter-9tppvxar.js → opencode-adapter-jmpst4q4.js} +3 -3
- package/dist/{otel-impl-f36byzbd.js → otel-impl-v6cc0zve.js} +1 -1
- package/dist/{pi-mono-adapter-tqsge1ay.js → pi-mono-adapter-fk8fkcf6.js} +33 -6
- package/dist/{pricing-refresh-xvak5p4v.js → pricing-refresh-fsk1e1zc.js} +3 -3
- package/dist/{rbac-roles-gfn30fb8.js → rbac-roles-prmkdnkw.js} +2 -2
- package/dist/{rbac-roles-682k2mt5.js → rbac-roles-zxmxqa39.js} +1 -1
- package/dist/{render-v2-hgytz8nn.js → render-v2-pjwxvnkw.js} +3 -3
- package/dist/{resume-2dvq348g.js → resume-frdtgfa0.js} +8 -6
- package/dist/{session-access-7f21ekq9.js → session-access-fpez4y5q.js} +4 -4
- package/dist/{setup-epffxtbk.js → setup-mq6m3m8q.js} +1 -1
- package/dist/{work-objects-xz1a5rqk.js → work-objects-kfahhjsq.js} +18 -18
- package/dist/{worker-42t8emjt.js → worker-v5e5rxj3.js} +33 -33
- package/openapi.json +68 -3
- package/package.json +5 -5
- package/plugin/commands/create-pr.md +2 -2
- package/plugin/pi-skills/create-pr/SKILL.md +2 -2
- package/src/be/db/approvals.ts +48 -0
- 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 +318 -7
- package/src/be/db.ts +67 -17
- 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 +2 -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/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/config.ts +48 -16
- package/src/http/idempotency-key.ts +117 -0
- package/src/http/workflows.ts +129 -24
- 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/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-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/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/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/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 +3 -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 +37 -0
- 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/tools/workflows/cancel-workflow-run.ts +1 -1
- package/src/utils/context-window.ts +8 -6
- package/src/workflows/approval-cancel.ts +15 -13
- package/src/workflows/approval-notifications.ts +5 -1
- package/src/workflows/checkpoint.ts +6 -3
- package/src/workflows/engine.ts +30 -8
- package/src/workflows/executors/foreach.ts +2 -2
- package/src/workflows/foreach-driver.ts +10 -3
- package/src/workflows/foreach-join.ts +29 -6
- package/src/workflows/recovery.ts +6 -1
- package/src/workflows/resume.ts +168 -56
- package/src/workflows/retry-poller.ts +49 -3
- 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/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/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
package/src/http/workflows.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { resolveHttpAuditUserId } from "../be/audit-user";
|
|
|
5
5
|
import {
|
|
6
6
|
createWorkflow,
|
|
7
7
|
deleteWorkflow,
|
|
8
|
+
getDbClient,
|
|
8
9
|
getWorkflow,
|
|
9
10
|
getWorkflowRun,
|
|
10
11
|
getWorkflowRunStepsByRunId,
|
|
@@ -15,6 +16,13 @@ import {
|
|
|
15
16
|
type updateWorkflow,
|
|
16
17
|
withFavoriteFlags,
|
|
17
18
|
} from "../be/db";
|
|
19
|
+
import {
|
|
20
|
+
assertWorkflowIdempotencyKeyUnused,
|
|
21
|
+
IdempotencyKeyTakenError,
|
|
22
|
+
recordAcceptedWorkflowOperation,
|
|
23
|
+
type WorkflowIdempotencyClaim,
|
|
24
|
+
workflowRequestFingerprint,
|
|
25
|
+
} from "../be/db/workflow-idempotency";
|
|
18
26
|
import { getWorkflowRunParent } from "../be/db/workflow-runs";
|
|
19
27
|
import {
|
|
20
28
|
AssetKeySchema,
|
|
@@ -46,6 +54,11 @@ import { cancelWorkflowRun, retryFailedRun } from "../workflows/resume";
|
|
|
46
54
|
import { handleWebhookTrigger, WebhookError } from "../workflows/triggers";
|
|
47
55
|
import { snapshotAndUpdateWorkflow } from "../workflows/version";
|
|
48
56
|
import { resolveHttpFavoriteOwner } from "./favorite-owner";
|
|
57
|
+
import {
|
|
58
|
+
idempotencyKeyHeader,
|
|
59
|
+
readIdempotencyKey,
|
|
60
|
+
replayAcceptedOperation,
|
|
61
|
+
} from "./idempotency-key";
|
|
49
62
|
import { route } from "./route-def";
|
|
50
63
|
import { jsonError, parseBody, triggerSchemaErrorResponse } from "./utils";
|
|
51
64
|
|
|
@@ -142,6 +155,7 @@ const createWorkflowRoute = route({
|
|
|
142
155
|
pattern: ["api", "workflows"],
|
|
143
156
|
summary: "Create a new workflow",
|
|
144
157
|
tags: ["Workflows"],
|
|
158
|
+
headers: idempotencyKeyHeader("creates one workflow"),
|
|
145
159
|
body: z.object({
|
|
146
160
|
name: z.string().min(1),
|
|
147
161
|
key: AssetKeySchema.optional(),
|
|
@@ -159,7 +173,9 @@ const createWorkflowRoute = route({
|
|
|
159
173
|
}),
|
|
160
174
|
responses: {
|
|
161
175
|
201: { description: "Workflow created", schema: WorkflowSaveResponseSchema },
|
|
162
|
-
400: { description: "Invalid definition" },
|
|
176
|
+
400: { description: "Invalid definition, or a malformed Idempotency-Key" },
|
|
177
|
+
409: { description: "Idempotency-Key was already used for a different request" },
|
|
178
|
+
410: { description: "The workflow created under this Idempotency-Key was deleted" },
|
|
163
179
|
},
|
|
164
180
|
});
|
|
165
181
|
|
|
@@ -272,16 +288,21 @@ const triggerWorkflowRoute = route({
|
|
|
272
288
|
path: "/api/workflows/{id}/trigger",
|
|
273
289
|
pattern: ["api", "workflows", null, "trigger"],
|
|
274
290
|
summary: "Trigger a workflow execution",
|
|
291
|
+
description:
|
|
292
|
+
"With an `Idempotency-Key`, a retry returns the accepted run's `runId` and `skipped` without starting, resuming or waiting for anything, even after the workflow was disabled or edited and whatever state the run is in now.",
|
|
275
293
|
tags: ["Workflows"],
|
|
276
294
|
params: z.object({ id: z.string() }),
|
|
295
|
+
headers: idempotencyKeyHeader("starts one run"),
|
|
277
296
|
responses: {
|
|
278
297
|
201: {
|
|
279
298
|
description: "Workflow run started (or skipped if cooldown active)",
|
|
280
299
|
schema: z.object({ runId: z.string(), skipped: z.boolean() }),
|
|
281
300
|
},
|
|
282
|
-
400: { description: "Workflow is disabled" },
|
|
301
|
+
400: { description: "Workflow is disabled, or a malformed Idempotency-Key" },
|
|
283
302
|
401: { description: "Unauthorized" },
|
|
284
303
|
404: { description: "Workflow not found" },
|
|
304
|
+
409: { description: "Idempotency-Key was already used for a different request" },
|
|
305
|
+
410: { description: "The workflow triggered under this Idempotency-Key was deleted" },
|
|
285
306
|
},
|
|
286
307
|
});
|
|
287
308
|
|
|
@@ -574,8 +595,21 @@ export async function handleWorkflows(
|
|
|
574
595
|
}
|
|
575
596
|
|
|
576
597
|
if (createWorkflowRoute.match(req.method, pathSegments)) {
|
|
598
|
+
const idempotency = readIdempotencyKey(req, res, myAgentId);
|
|
599
|
+
if (idempotency === null) return true;
|
|
577
600
|
const parsed = await createWorkflowRoute.parse(req, res, pathSegments, queryParams);
|
|
578
601
|
if (!parsed) return true;
|
|
602
|
+
const claim: WorkflowIdempotencyClaim | undefined = idempotency && {
|
|
603
|
+
...idempotency,
|
|
604
|
+
operation: "workflow.create",
|
|
605
|
+
targetId: "",
|
|
606
|
+
fingerprint: workflowRequestFingerprint("workflow.create", parsed.body),
|
|
607
|
+
};
|
|
608
|
+
const replayCreate = (body: unknown) =>
|
|
609
|
+
createWorkflowRoute.respond(res, 201, body as z.input<typeof WorkflowSaveResponseSchema>);
|
|
610
|
+
// Before validation: a retry gets the original result even if the same
|
|
611
|
+
// definition would no longer validate.
|
|
612
|
+
if (claim && (await replayAcceptedOperation(res, claim, replayCreate))) return true;
|
|
579
613
|
|
|
580
614
|
// Validate definition structure
|
|
581
615
|
const validation = validateDefinition(parsed.body.definition, getExecutorRegistry());
|
|
@@ -603,27 +637,55 @@ export async function handleWorkflows(
|
|
|
603
637
|
throw error;
|
|
604
638
|
}
|
|
605
639
|
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
"api"
|
|
625
|
-
|
|
626
|
-
|
|
640
|
+
const data: Parameters<typeof createWorkflow>[0] = {
|
|
641
|
+
key,
|
|
642
|
+
name: parsed.body.name,
|
|
643
|
+
description: parsed.body.description,
|
|
644
|
+
definition: parsed.body.definition,
|
|
645
|
+
triggers: parsed.body.triggers,
|
|
646
|
+
cooldown: parsed.body.cooldown,
|
|
647
|
+
input: parsed.body.input,
|
|
648
|
+
triggerSchema: parsed.body.triggerSchema,
|
|
649
|
+
params: parsed.body.params,
|
|
650
|
+
requiredParams: parsed.body.requiredParams,
|
|
651
|
+
requires: parsed.body.requires,
|
|
652
|
+
dir: parsed.body.dir,
|
|
653
|
+
vcsRepo: parsed.body.vcsRepo,
|
|
654
|
+
createdByAgentId: myAgentId ?? undefined,
|
|
655
|
+
createdBy: (await resolveHttpAuditUserId(req, myAgentId)) ?? undefined,
|
|
656
|
+
};
|
|
657
|
+
if (!claim) {
|
|
658
|
+
const workflow = await createWorkflow(data, "api");
|
|
659
|
+
createWorkflowRoute.respond(res, 201, await withSaveWarnings(workflow));
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Warnings are computed before the transaction (a readiness check may
|
|
664
|
+
// call out) and stored with the workflow, so a retry gets this snapshot,
|
|
665
|
+
// not the workflow as it is by then.
|
|
666
|
+
const warnings = await workflowSaveWarnings(parsed.body.definition, getExecutorRegistry());
|
|
667
|
+
let saved: z.input<typeof WorkflowSaveResponseSchema>;
|
|
668
|
+
try {
|
|
669
|
+
saved = await getDbClient().transaction(async () => {
|
|
670
|
+
// Before the insert: a concurrent winner's workflow holds the same name.
|
|
671
|
+
await assertWorkflowIdempotencyKeyUnused(claim);
|
|
672
|
+
const workflow = await createWorkflow(data, "api");
|
|
673
|
+
const body = warnings.length > 0 ? { ...workflow, warnings } : workflow;
|
|
674
|
+
await recordAcceptedWorkflowOperation(claim, {
|
|
675
|
+
workflowId: workflow.id,
|
|
676
|
+
status: 201,
|
|
677
|
+
body,
|
|
678
|
+
});
|
|
679
|
+
return body;
|
|
680
|
+
});
|
|
681
|
+
} catch (err) {
|
|
682
|
+
// A concurrent request with this key committed first; nothing of this one did.
|
|
683
|
+
if (err instanceof IdempotencyKeyTakenError) {
|
|
684
|
+
if (await replayAcceptedOperation(res, claim, replayCreate)) return true;
|
|
685
|
+
}
|
|
686
|
+
throw err;
|
|
687
|
+
}
|
|
688
|
+
createWorkflowRoute.respond(res, 201, saved);
|
|
627
689
|
return true;
|
|
628
690
|
}
|
|
629
691
|
|
|
@@ -858,8 +920,26 @@ export async function handleWorkflows(
|
|
|
858
920
|
}
|
|
859
921
|
|
|
860
922
|
if (triggerWorkflowRoute.match(req.method, pathSegments)) {
|
|
923
|
+
const idempotency = readIdempotencyKey(req, res, myAgentId);
|
|
924
|
+
if (idempotency === null) return true;
|
|
861
925
|
const parsed = await triggerWorkflowRoute.parse(req, res, pathSegments, queryParams);
|
|
862
926
|
if (!parsed) return true;
|
|
927
|
+
let claim: WorkflowIdempotencyClaim | undefined;
|
|
928
|
+
let keyedBody: Record<string, unknown> | undefined;
|
|
929
|
+
const replayTrigger = (body: unknown) =>
|
|
930
|
+
triggerWorkflowRoute.respond(res, 201, body as { runId: string; skipped: boolean });
|
|
931
|
+
if (idempotency) {
|
|
932
|
+
keyedBody = await parseBody<Record<string, unknown>>(req);
|
|
933
|
+
claim = {
|
|
934
|
+
...idempotency,
|
|
935
|
+
operation: "workflow.trigger",
|
|
936
|
+
targetId: parsed.params.id,
|
|
937
|
+
fingerprint: workflowRequestFingerprint("workflow.trigger", keyedBody),
|
|
938
|
+
};
|
|
939
|
+
// Before the enabled, cooldown, readiness and trigger-schema checks: an
|
|
940
|
+
// accepted trigger replays even after the workflow changed.
|
|
941
|
+
if (await replayAcceptedOperation(res, claim, replayTrigger)) return true;
|
|
942
|
+
}
|
|
863
943
|
const workflow = await getWorkflow(parsed.params.id);
|
|
864
944
|
if (!workflow) {
|
|
865
945
|
res.writeHead(404);
|
|
@@ -870,20 +950,45 @@ export async function handleWorkflows(
|
|
|
870
950
|
jsonError(res, "Workflow is disabled", 400);
|
|
871
951
|
return true;
|
|
872
952
|
}
|
|
873
|
-
|
|
953
|
+
// Not `??`: a JSON `null` body is a parsed body too, and reading the
|
|
954
|
+
// already-ended stream again would wait forever.
|
|
955
|
+
const body = idempotency ? keyedBody : await parseBody<Record<string, unknown>>(req);
|
|
874
956
|
let runId: string;
|
|
957
|
+
let accepted: { runId: string; skipped: boolean } | undefined;
|
|
875
958
|
try {
|
|
876
959
|
runId = await startWorkflowExecution(workflow, body, getExecutorRegistry(), {
|
|
877
960
|
triggerType: "api",
|
|
878
961
|
requestedByUserId: (await resolveHttpAuditUserId(req, myAgentId)) ?? undefined,
|
|
962
|
+
// Recorded in the run's own transaction, before the walk starts.
|
|
963
|
+
accept: claim
|
|
964
|
+
? async (run) => {
|
|
965
|
+
const response = { runId: run.runId, skipped: run.skipped };
|
|
966
|
+
await recordAcceptedWorkflowOperation(claim, {
|
|
967
|
+
workflowId: workflow.id,
|
|
968
|
+
runId: run.runId,
|
|
969
|
+
status: 201,
|
|
970
|
+
body: response,
|
|
971
|
+
});
|
|
972
|
+
accepted = response;
|
|
973
|
+
}
|
|
974
|
+
: undefined,
|
|
879
975
|
});
|
|
880
976
|
} catch (err) {
|
|
881
977
|
if (err instanceof TriggerSchemaError) {
|
|
882
978
|
triggerSchemaErrorResponse(res, err.message, err.validationErrors);
|
|
883
979
|
return true;
|
|
884
980
|
}
|
|
981
|
+
// A concurrent request with this key committed its run first; this one
|
|
982
|
+
// created none and walks nothing.
|
|
983
|
+
if (claim && err instanceof IdempotencyKeyTakenError) {
|
|
984
|
+
if (await replayAcceptedOperation(res, claim, replayTrigger)) return true;
|
|
985
|
+
}
|
|
885
986
|
throw err;
|
|
886
987
|
}
|
|
988
|
+
if (accepted) {
|
|
989
|
+
triggerWorkflowRoute.respond(res, 201, accepted);
|
|
990
|
+
return true;
|
|
991
|
+
}
|
|
887
992
|
|
|
888
993
|
// Check if skipped due to cooldown
|
|
889
994
|
const run = await getWorkflowRun(runId);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Worker-side hooks do not load base-prompt, which normally registers these
|
|
2
|
+
// code-default templates.
|
|
3
|
+
import "./session-templates.ts";
|
|
4
|
+
import { resolveTemplateAsync, resolveTemplateFromCode } from "./resolver.ts";
|
|
5
|
+
|
|
6
|
+
const CONTEXT_BOUND_NOTE_TEMPLATE = "system.agent.context.bound_note";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `contextShare` for the note: " (21% of the 1,000,000-token window)", or ""
|
|
10
|
+
* when the model's window is unknown. Never a percent of the 200k fallback.
|
|
11
|
+
*/
|
|
12
|
+
function contextShare(contextTokens: number, model: string | undefined): string {
|
|
13
|
+
if (!model) return "";
|
|
14
|
+
try {
|
|
15
|
+
// Lazy and synchronous: the catalog snapshot is large and the Claude hook
|
|
16
|
+
// process loads this module on every tool call, but renders the note at
|
|
17
|
+
// most once. Sync so in-process sessions render no later than before.
|
|
18
|
+
const { clampContextPercent, getKnownContextWindowSize } =
|
|
19
|
+
require("../utils/context-window.ts") as typeof import("../utils/context-window.ts");
|
|
20
|
+
const window = getKnownContextWindowSize(model);
|
|
21
|
+
const percent = clampContextPercent(contextTokens, window);
|
|
22
|
+
if (!window || percent === null) return "";
|
|
23
|
+
return ` (${Math.round(percent)}% of the ${window.toLocaleString("en-US")}-token window)`;
|
|
24
|
+
} catch {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Render the one-time context-bound note. `model` is the session's model, for
|
|
31
|
+
* the window share. Returns "" when an operator disabled the template; falls
|
|
32
|
+
* back to the code default if resolution fails.
|
|
33
|
+
*/
|
|
34
|
+
export async function renderContextBoundNote(
|
|
35
|
+
contextTokens: number,
|
|
36
|
+
model?: string,
|
|
37
|
+
): Promise<string> {
|
|
38
|
+
const variables = {
|
|
39
|
+
contextTokens: Math.round(contextTokens).toLocaleString("en-US"),
|
|
40
|
+
contextShare: contextShare(contextTokens, model),
|
|
41
|
+
};
|
|
42
|
+
try {
|
|
43
|
+
const result = await resolveTemplateAsync(CONTEXT_BOUND_NOTE_TEMPLATE, variables);
|
|
44
|
+
return result.skipped ? "" : result.text;
|
|
45
|
+
} catch {
|
|
46
|
+
return resolveTemplateFromCode(CONTEXT_BOUND_NOTE_TEMPLATE, variables).text;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -202,7 +202,7 @@ agent-fs is the shared drive between agents and people. Documents (reports, dige
|
|
|
202
202
|
Pages: only for polished artifacts shared with a wider audience or explicitly requested; see \`pages\` skill.
|
|
203
203
|
A tool a person will use, with data and actions: build an app. See the \`apps\` skill.
|
|
204
204
|
Link env: \`APP_URL\` for pages, \`MCP_BASE_URL\` for the API, \`AGENT_FS_LIVE_URL\` for files. Report missing variables.
|
|
205
|
-
|
|
205
|
+
Give people the live app link \`\${AGENT_FS_LIVE_URL}/file/~/<org>/<drive>/<path>\`. Make an \`agent-fs share-create\` link only when the requester asks for one or the reader has no agent-fs login.
|
|
206
206
|
`,
|
|
207
207
|
variables: [],
|
|
208
208
|
category: "system",
|
|
@@ -353,6 +353,30 @@ Task: {{task}}{{progressLine}}
|
|
|
353
353
|
category: "system",
|
|
354
354
|
});
|
|
355
355
|
|
|
356
|
+
/**
|
|
357
|
+
* One-time note injected into a worker session once its latest main-thread
|
|
358
|
+
* request crosses CONTEXT_BOUND_NOTE_TOKENS (`renderContextBoundNote`, via
|
|
359
|
+
* `src/hooks/context-bound-note.ts`).
|
|
360
|
+
*/
|
|
361
|
+
registerTemplate({
|
|
362
|
+
eventType: "system.agent.context.bound_note",
|
|
363
|
+
header: "",
|
|
364
|
+
defaultBody:
|
|
365
|
+
"Context is {{contextTokens}} tokens{{contextShare}}. Keep this task bounded: finish the current step, store-progress, and complete or defer instead of starting new exploration.",
|
|
366
|
+
variables: [
|
|
367
|
+
{
|
|
368
|
+
name: "contextTokens",
|
|
369
|
+
description: "Prompt size of the latest request (input + cache read + cache write tokens)",
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "contextShare",
|
|
373
|
+
description:
|
|
374
|
+
'How full the model\'s context window is, e.g. " (21% of the 1,000,000-token window)". Empty when the window is unknown',
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
category: "system",
|
|
378
|
+
});
|
|
379
|
+
|
|
356
380
|
/**
|
|
357
381
|
* First message of a cursor session on the first-message path. Cursor keeps
|
|
358
382
|
* its own system prompt there, so the swarm's rides on top of the task prompt.
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "nod
|
|
|
2
2
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { contextBoundNoteThreshold, createContextBoundNote } from "../hooks/context-bound-note";
|
|
5
6
|
import {
|
|
6
7
|
type AmpMode,
|
|
7
8
|
type AmpModelSelection,
|
|
@@ -44,7 +45,7 @@ import type {
|
|
|
44
45
|
* default; the image installs exactly this version and the per-task settings
|
|
45
46
|
* set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
|
|
46
47
|
*/
|
|
47
|
-
export const AMP_PACKAGE = "@ampcode/cli@0.0.
|
|
48
|
+
export const AMP_PACKAGE = "@ampcode/cli@0.0.1792844927-g360c96";
|
|
48
49
|
|
|
49
50
|
/** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
|
|
50
51
|
const AMP_MCP_SERVER_NAME = "agent-swarm";
|
|
@@ -442,6 +443,8 @@ interface QueuedSteer {
|
|
|
442
443
|
* answer from the model that received it settles the steer.
|
|
443
444
|
*/
|
|
444
445
|
receiver?: string | null;
|
|
446
|
+
/** The swarm's own note: rides a tool result or is dropped, never replayed as a new turn. */
|
|
447
|
+
internal?: boolean;
|
|
445
448
|
resolve: (result: SteerDeliveryResult) => void;
|
|
446
449
|
}
|
|
447
450
|
|
|
@@ -486,6 +489,8 @@ class AmpSession implements ProviderSession {
|
|
|
486
489
|
private steeringSeq = 0;
|
|
487
490
|
/** Accepted steering not yet consumed, by inbox file name. Each settles exactly once. */
|
|
488
491
|
private steers = new Map<string, QueuedSteer>();
|
|
492
|
+
/** Fed each top-level request's prompt size; queues the one-time context-bound note. */
|
|
493
|
+
private readonly observeContextBound: (contextTokens: number | undefined) => Promise<void>;
|
|
489
494
|
|
|
490
495
|
constructor(
|
|
491
496
|
private proc: Bun.Subprocess<"pipe", "pipe", "pipe">,
|
|
@@ -523,6 +528,15 @@ class AmpSession implements ProviderSession {
|
|
|
523
528
|
});
|
|
524
529
|
}
|
|
525
530
|
};
|
|
531
|
+
// The plugin's `tool.result` hook carries queued steering mid-turn; a note
|
|
532
|
+
// no tool result took is dropped at turn end (`onTurnEnd`).
|
|
533
|
+
this.observeContextBound = createContextBoundNote({
|
|
534
|
+
threshold: contextBoundNoteThreshold(run.env),
|
|
535
|
+
// A mode names no model in advance, so only a pinned model gets a window share.
|
|
536
|
+
model: () => run.selection.pin,
|
|
537
|
+
canDeliver: () => !this.inputClosed && !this.aborted,
|
|
538
|
+
inject: (note) => this.queueSteering(note, true),
|
|
539
|
+
});
|
|
526
540
|
this.completion = this.runSession(prompt);
|
|
527
541
|
}
|
|
528
542
|
|
|
@@ -553,13 +567,13 @@ class AmpSession implements ProviderSession {
|
|
|
553
567
|
* check and the rename, no turn end can drain the inbox and close input, so
|
|
554
568
|
* a message is either visible to the next drain or refused.
|
|
555
569
|
*/
|
|
556
|
-
private queueSteering(text: string): Promise<SteerDeliveryResult> {
|
|
570
|
+
private queueSteering(text: string, internal = false): Promise<SteerDeliveryResult> {
|
|
557
571
|
const name = `${String(++this.steeringSeq).padStart(6, "0")}.json`;
|
|
558
572
|
const temp = join(this.run.steeringInbox, `.${name}.tmp`);
|
|
559
573
|
writeFileSync(temp, JSON.stringify(text), { mode: 0o600 });
|
|
560
574
|
renameSync(temp, join(this.run.steeringInbox, name));
|
|
561
575
|
return new Promise((resolve) => {
|
|
562
|
-
this.steers.set(name, { text, via: "inbox", resolve });
|
|
576
|
+
this.steers.set(name, { text, via: "inbox", internal, resolve });
|
|
563
577
|
});
|
|
564
578
|
}
|
|
565
579
|
|
|
@@ -631,8 +645,8 @@ class AmpSession implements ProviderSession {
|
|
|
631
645
|
* Claim the messages no tool result carried, with the plugin's rename
|
|
632
646
|
* protocol (`.json -> .stdin`). They stay open until Amp echoes them.
|
|
633
647
|
*/
|
|
634
|
-
private takeSteering(): { name: string; text: string }[] {
|
|
635
|
-
const taken: { name: string; text: string }[] = [];
|
|
648
|
+
private takeSteering(): { name: string; text: string; internal?: boolean }[] {
|
|
649
|
+
const taken: { name: string; text: string; internal?: boolean }[] = [];
|
|
636
650
|
for (const [name, steer] of [...this.steers].sort(([a], [b]) => a.localeCompare(b))) {
|
|
637
651
|
if (steer.via !== "inbox") continue;
|
|
638
652
|
try {
|
|
@@ -641,7 +655,7 @@ class AmpSession implements ProviderSession {
|
|
|
641
655
|
continue; // The plugin took it.
|
|
642
656
|
}
|
|
643
657
|
steer.via = "stdin";
|
|
644
|
-
taken.push({ name, text: steer.text });
|
|
658
|
+
taken.push({ name, text: steer.text, internal: steer.internal });
|
|
645
659
|
}
|
|
646
660
|
return taken;
|
|
647
661
|
}
|
|
@@ -704,11 +718,16 @@ class AmpSession implements ProviderSession {
|
|
|
704
718
|
/**
|
|
705
719
|
* A top-level assistant message with no tool call ends the turn. Steering
|
|
706
720
|
* that arrived after the last tool result reached no hook, so it goes in as
|
|
707
|
-
* the next user message and starts another turn.
|
|
721
|
+
* the next user message and starts another turn. The swarm's own note is
|
|
722
|
+
* withdrawn instead: it must not start a turn.
|
|
708
723
|
*/
|
|
709
724
|
private onTurnEnd(): void {
|
|
710
725
|
if (this.inputClosed) return;
|
|
711
|
-
for (const { name, text } of this.takeSteering()) {
|
|
726
|
+
for (const { name, text, internal } of this.takeSteering()) {
|
|
727
|
+
if (internal) {
|
|
728
|
+
this.settleSteer(name, { delivered: false, reason: "no tool result carried the note" });
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
712
731
|
this.writeInput(text).catch((error) => {
|
|
713
732
|
this.settleSteer(name, {
|
|
714
733
|
delivered: false,
|
|
@@ -817,6 +836,9 @@ class AmpSession implements ProviderSession {
|
|
|
817
836
|
});
|
|
818
837
|
// The window is not in the stream; the final snapshot after the export carries it.
|
|
819
838
|
if (used > 0) this.emitContext(used, output, null);
|
|
839
|
+
// Only when a tool result follows to carry the note; the turn end drops
|
|
840
|
+
// a note no tool result carried.
|
|
841
|
+
if (topLevel && calledTool) void this.observeContextBound(input + cacheRead + cacheCreation);
|
|
820
842
|
}
|
|
821
843
|
if (topLevel && !calledTool && message.stop_reason !== "tool_use") this.onTurnEnd();
|
|
822
844
|
}
|
|
@@ -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
|
}
|