@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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
findAcceptedWorkflowOperation,
|
|
5
|
+
type WorkflowIdempotencyClaim,
|
|
6
|
+
} from "../be/db/workflow-idempotency";
|
|
7
|
+
import { getRequestAuth } from "../utils/request-auth-context";
|
|
8
|
+
import { jsonError } from "./utils";
|
|
9
|
+
|
|
10
|
+
/** Visible ASCII, at most 255 characters: room for a UUID, a ULID or a client's own scheme. */
|
|
11
|
+
const IDEMPOTENCY_KEY_PATTERN = /^[\x21-\x7E]{1,255}$/;
|
|
12
|
+
|
|
13
|
+
/** Set on a response answered from an earlier accepted request. */
|
|
14
|
+
export const IDEMPOTENT_REPLAYED_HEADER = "Idempotent-Replayed";
|
|
15
|
+
|
|
16
|
+
/** OpenAPI declaration of the optional `Idempotency-Key` request header. */
|
|
17
|
+
export const idempotencyKeyHeader = (accepted: string) =>
|
|
18
|
+
z.object({
|
|
19
|
+
"Idempotency-Key": z
|
|
20
|
+
.string()
|
|
21
|
+
.regex(IDEMPOTENCY_KEY_PATTERN)
|
|
22
|
+
.optional()
|
|
23
|
+
.describe(
|
|
24
|
+
"Optional client-generated key, 1-255 visible ASCII characters (a UUID works). " +
|
|
25
|
+
`The first request the server accepts under a key ${accepted}; a retry with the same key ` +
|
|
26
|
+
"and an equivalent body gets that request's status and body back (with " +
|
|
27
|
+
`\`${IDEMPOTENT_REPLAYED_HEADER}: true\`) and changes nothing. The key is scoped to the ` +
|
|
28
|
+
"caller's identity and this operation; it is not a credential. The same key with a " +
|
|
29
|
+
"different body is a 409. A request rejected before it is accepted (validation, auth, " +
|
|
30
|
+
"not found, disabled) does not use up the key. Accepted keys do not expire.",
|
|
31
|
+
),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/** A request that sent a usable `Idempotency-Key`. */
|
|
35
|
+
export interface IdempotencyKeyRequest {
|
|
36
|
+
/** Stable identity of the authenticated caller, never a credential value. */
|
|
37
|
+
principal: string;
|
|
38
|
+
key: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The caller identity a key is scoped to. A user or session token names its
|
|
43
|
+
* user or agent; rotating the token keeps the scope. Requests on the swarm API
|
|
44
|
+
* key are the operator, or the agent named by X-Agent-ID: every worker shares
|
|
45
|
+
* that key, so this keeps their keys apart (it is not an access boundary; the
|
|
46
|
+
* swarm key may act as any agent). A guest page session has no identity.
|
|
47
|
+
*/
|
|
48
|
+
function idempotencyPrincipal(req: IncomingMessage, myAgentId: string | undefined): string | null {
|
|
49
|
+
const auth = getRequestAuth(req);
|
|
50
|
+
switch (auth?.kind) {
|
|
51
|
+
case "user":
|
|
52
|
+
return `user:${auth.userId}`;
|
|
53
|
+
case "agent":
|
|
54
|
+
return `agent:${auth.agentId}`;
|
|
55
|
+
case "controller":
|
|
56
|
+
return `controller:${auth.controllerId}`;
|
|
57
|
+
case "guest":
|
|
58
|
+
return null;
|
|
59
|
+
default:
|
|
60
|
+
return myAgentId ? `agent:${myAgentId}` : "operator";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read the `Idempotency-Key` header. Undefined when none was sent; null when
|
|
66
|
+
* it is malformed or the caller has no identity to scope it to, after writing
|
|
67
|
+
* the 400.
|
|
68
|
+
*/
|
|
69
|
+
export function readIdempotencyKey(
|
|
70
|
+
req: IncomingMessage,
|
|
71
|
+
res: ServerResponse,
|
|
72
|
+
myAgentId: string | undefined,
|
|
73
|
+
): IdempotencyKeyRequest | null | undefined {
|
|
74
|
+
const raw = req.headers["idempotency-key"];
|
|
75
|
+
if (raw === undefined) return undefined;
|
|
76
|
+
const key = Array.isArray(raw) ? raw.join(", ") : raw;
|
|
77
|
+
if (!IDEMPOTENCY_KEY_PATTERN.test(key)) {
|
|
78
|
+
jsonError(res, "Idempotency-Key must be 1-255 visible ASCII characters", 400);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const principal = idempotencyPrincipal(req, myAgentId);
|
|
82
|
+
if (!principal) {
|
|
83
|
+
jsonError(res, "Idempotency-Key needs an identified caller", 400);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return { principal, key };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Answer from the operation already accepted under this claim's scope, if
|
|
91
|
+
* any. Returns false, writing nothing, when there is none. Otherwise writes a
|
|
92
|
+
* 409 when the stored request differs, a 410 when its workflow was deleted,
|
|
93
|
+
* or hands the stored body to `send` with the replay header set, and returns
|
|
94
|
+
* true. Runs after authentication and before any check that may have changed
|
|
95
|
+
* since the original request.
|
|
96
|
+
*/
|
|
97
|
+
export async function replayAcceptedOperation(
|
|
98
|
+
res: ServerResponse,
|
|
99
|
+
claim: WorkflowIdempotencyClaim,
|
|
100
|
+
send: (body: unknown) => void,
|
|
101
|
+
): Promise<boolean> {
|
|
102
|
+
const accepted = await findAcceptedWorkflowOperation(claim);
|
|
103
|
+
if (!accepted) return false;
|
|
104
|
+
if (accepted.fingerprint !== claim.fingerprint) {
|
|
105
|
+
jsonError(res, "Idempotency-Key was already used for a different request", 409);
|
|
106
|
+
} else if (accepted.resourceDeletedAt !== null) {
|
|
107
|
+
jsonError(
|
|
108
|
+
res,
|
|
109
|
+
"The workflow this Idempotency-Key created or triggered was deleted; send a new key to start over",
|
|
110
|
+
410,
|
|
111
|
+
);
|
|
112
|
+
} else {
|
|
113
|
+
res.setHeader(IDEMPOTENT_REPLAYED_HEADER, "true");
|
|
114
|
+
send(accepted.body);
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
package/src/http/sessions.ts
CHANGED
|
@@ -502,7 +502,22 @@ const SessionHideSourceSchema = z
|
|
|
502
502
|
.filter(Boolean),
|
|
503
503
|
)
|
|
504
504
|
.describe(
|
|
505
|
-
"Comma-separated root sources to leave out
|
|
505
|
+
"Comma-separated root sources to leave out, e.g. `slack,chat`. Older sessions v2 clients send this; `showSource` replaces it.",
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
const SessionShowSourceSchema = z
|
|
509
|
+
.string()
|
|
510
|
+
// The same cap as the `showSource` body field of `POST /api/session-triage`.
|
|
511
|
+
.refine((value) => value.split(",").length <= 32, "showSource takes at most 32 sources.")
|
|
512
|
+
.optional()
|
|
513
|
+
.transform((value) =>
|
|
514
|
+
value
|
|
515
|
+
?.split(",")
|
|
516
|
+
.map((source) => source.trim())
|
|
517
|
+
.filter(Boolean),
|
|
518
|
+
)
|
|
519
|
+
.describe(
|
|
520
|
+
"Comma-separated root sources to keep (the sessions v2 source filter), e.g. `ui,slack`. Present and empty keeps none; omit to keep every source.",
|
|
506
521
|
);
|
|
507
522
|
|
|
508
523
|
const listSessions = route({
|
|
@@ -568,6 +583,8 @@ const listSessions = route({
|
|
|
568
583
|
show: SessionShowSchema,
|
|
569
584
|
/** Root sources to leave out. Must match the tree's `hideSource`. */
|
|
570
585
|
hideSource: SessionHideSourceSchema,
|
|
586
|
+
/** Root sources to keep. Must match the tree's `showSource`. */
|
|
587
|
+
showSource: SessionShowSourceSchema,
|
|
571
588
|
}),
|
|
572
589
|
responses: {
|
|
573
590
|
200: {
|
|
@@ -590,7 +607,11 @@ const sessionTree = route({
|
|
|
590
607
|
pattern: ["api", "session-tree"],
|
|
591
608
|
summary: "Get session groups and triage counts",
|
|
592
609
|
tags: ["Sessions"],
|
|
593
|
-
query: z.object({
|
|
610
|
+
query: z.object({
|
|
611
|
+
show: SessionShowSchema,
|
|
612
|
+
hideSource: SessionHideSourceSchema,
|
|
613
|
+
showSource: SessionShowSourceSchema,
|
|
614
|
+
}),
|
|
594
615
|
responses: {
|
|
595
616
|
200: { description: "Visible groups and section counts", schema: SessionTreeResponseSchema },
|
|
596
617
|
401: { description: "Unauthorized" },
|
|
@@ -615,6 +636,8 @@ const triageSessionListRoute = route({
|
|
|
615
636
|
groupKey: SessionKeyFilterSchema.optional(),
|
|
616
637
|
show: SessionShowSchema,
|
|
617
638
|
hideSource: z.array(z.string().min(1)).max(32).optional(),
|
|
639
|
+
/** Root sources to keep; an empty array keeps none. */
|
|
640
|
+
showSource: z.array(z.string().min(1)).max(32).optional(),
|
|
618
641
|
})
|
|
619
642
|
.refine((body) => [body.triage, body.noGroup, body.groupKey].filter(Boolean).length === 1, {
|
|
620
643
|
message: "Name exactly one list: triage, noGroup, or groupKey",
|
|
@@ -941,6 +964,7 @@ export async function handleSessions(
|
|
|
941
964
|
const filterOpts = {
|
|
942
965
|
source: sources,
|
|
943
966
|
hideSource: parsed.query.hideSource,
|
|
967
|
+
showSource: parsed.query.showSource,
|
|
944
968
|
q: parsed.query.q,
|
|
945
969
|
requestedByUserId: parsed.query.requestedByUserId,
|
|
946
970
|
contextKeyPrefix: parsed.query.contextKeyPrefix,
|
|
@@ -994,6 +1018,7 @@ export async function handleSessions(
|
|
|
994
1018
|
await getSessionTree(actor, undefined, {
|
|
995
1019
|
mine: parsed.query.show !== "all",
|
|
996
1020
|
hideSource: parsed.query.hideSource,
|
|
1021
|
+
showSource: parsed.query.showSource,
|
|
997
1022
|
}),
|
|
998
1023
|
);
|
|
999
1024
|
return true;
|
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);
|
|
@@ -188,9 +188,10 @@ export async function handleChatInbound(
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
if (text || attachments.length > 0) {
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
191
|
+
// An unaddressed subscribed-thread message may be dropped by the router at
|
|
192
|
+
// flush time; it gets typing and a reaction once a task exists (see
|
|
193
|
+
// flushChatThread). A mention, reply to the bot, or DM is acknowledged now.
|
|
194
|
+
const acknowledge = kind !== "subscribed" || isDirectMessage || mentioned || facts.replyToBot;
|
|
194
195
|
bufferChatMessage(thread.id, {
|
|
195
196
|
messageId: message.id,
|
|
196
197
|
channelId: thread.channelId,
|
|
@@ -580,7 +581,7 @@ export async function flushChatThread(
|
|
|
580
581
|
if (created > 0) {
|
|
581
582
|
await rememberChatThreadRequester(threadId, requester.userId);
|
|
582
583
|
await subscribe(thread);
|
|
583
|
-
//
|
|
584
|
+
// Unaddressed subscribed-thread messages start typing only here, once a task exists.
|
|
584
585
|
startTypingKeepalive(adapter, threadId);
|
|
585
586
|
await clearChatMessageReactions(
|
|
586
587
|
adapter,
|
|
@@ -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
|
}
|