@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
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// Generated by scripts/bundle-extension-types.ts. Do not edit.
|
|
2
2
|
// biome-ignore-all lint/suspicious/noTemplateCurlyInString: Type declarations contain literal template placeholders.
|
|
3
3
|
export const EXTENSION_TYPE_DEFINITIONS =
|
|
4
|
-
'// biome-ignore-all lint/suspicious/noConfusingVoidType: Extension handlers may explicitly return no result.\ndeclare module "swarm-extension" {\n export type CreateTaskOptions = {\n key?: string | undefined;\n agentId?: string | null | undefined;\n creatorAgentId?: string | undefined;\n source?:\n | "workflow"\n | "schedule"\n | "slack"\n | "agentmail"\n | "mcp"\n | "api"\n | "ui"\n | "github"\n | "gitlab"\n | "azure-devops"\n | "bitbucket"\n | "system"\n | "linear"\n | "jira"\n | "comb"\n | "chat"\n | undefined;\n routingReason?:\n | "skill"\n | "continuity"\n | "overflow"\n | "human_pinned"\n | "reroute_fault"\n | undefined;\n routingSource?: "declared" | "engine_default" | undefined;\n routingNote?: string | undefined;\n taskType?: string | undefined;\n tags?: string[] | undefined;\n priority?: number | undefined;\n dependsOn?: string[] | undefined;\n offeredTo?: string | undefined;\n status?: "draft" | "backlog" | "unassigned" | undefined;\n slackChannelId?: string | undefined;\n slackThreadTs?: string | undefined;\n slackTriggerMessageTs?: string | undefined;\n slackUserId?: string | undefined;\n overrideSlackContext?: boolean | undefined;\n vcsProvider?: "github" | "gitlab" | "azure-devops" | "bitbucket" | undefined;\n vcsRepo?: string | undefined;\n vcsEventType?: string | undefined;\n vcsNumber?: number | undefined;\n vcsCommentId?: number | undefined;\n vcsAuthor?: string | undefined;\n vcsUrl?: string | undefined;\n vcsInstallationId?: number | undefined;\n vcsNodeId?: string | undefined;\n agentmailInboxId?: string | undefined;\n agentmailMessageId?: string | undefined;\n agentmailThreadId?: string | undefined;\n mentionMessageId?: string | undefined;\n mentionChannelId?: string | undefined;\n dir?: string | undefined;\n parentTaskId?: string | undefined;\n model?: string | undefined;\n modelTier?: "smol" | "regular" | "smart" | "ultra" | undefined;\n effort?: "off" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;\n profile?: string | undefined;\n scheduleId?: string | undefined;\n workflowRunId?: string | undefined;\n workflowRunStepId?: string | undefined;\n sourceTaskId?: string | undefined;\n outputSchema?: Record<string, unknown> | undefined;\n inheritParentOutputSchema?: boolean | undefined;\n inheritParentRequester?: boolean | undefined;\n inheritParentRoutingAffinity?: boolean | undefined;\n inheritParentFollowUpConfig?: boolean | undefined;\n followUpConfig?:\n | {\n disabled?: boolean | undefined;\n onCompleted?: string | undefined;\n onFailed?: string | undefined;\n }\n | undefined;\n requestedByUserId?: string | undefined;\n contextKey?: string | undefined;\n bypassTrackerContextDedup?: boolean | undefined;\n routingAffinity?:\n | {\n capabilities: string[];\n sourceAgentId?: string | undefined;\n role?: string | undefined;\n harnessProvider?:\n | "claude"\n | "codex"\n | "pi"\n | "devin"\n | "claude-managed"\n | "opencode"\n | "acp"\n | "dsh"\n | "amp"\n | "cursor"\n | "grok"\n | undefined;\n leadOnly?: boolean | undefined;\n }\n | undefined;\n };\n\n export type AgentTask = {\n id: string;\n key: string;\n agentId: string | null;\n task: string;\n status:\n | "draft"\n | "backlog"\n | "unassigned"\n | "offered"\n | "reviewing"\n | "pending"\n | "in_progress"\n | "paused"\n | "completed"\n | "failed"\n | "cancelled"\n | "superseded";\n source:\n | "workflow"\n | "schedule"\n | "slack"\n | "agentmail"\n | "mcp"\n | "api"\n | "ui"\n | "github"\n | "gitlab"\n | "azure-devops"\n | "bitbucket"\n | "system"\n | "linear"\n | "jira"\n | "comb"\n | "chat";\n tags: string[];\n priority: number;\n dependsOn: string[];\n createdAt: string;\n lastUpdatedAt: string;\n slackReplySent: boolean;\n wasPaused: boolean;\n creatorAgentId?: string | undefined;\n title?: string | undefined;\n routingReason?:\n | "skill"\n | "continuity"\n | "overflow"\n | "human_pinned"\n | "reroute_fault"\n | undefined;\n routingSource?: "declared" | "engine_default" | undefined;\n routingNote?: string | undefined;\n poolWakeAt?: string | undefined;\n taskType?: string | undefined;\n offeredTo?: string | undefined;\n offeredAt?: string | undefined;\n acceptedAt?: string | undefined;\n rejectionReason?: string | undefined;\n finishedAt?: string | undefined;\n notifiedAt?: string | undefined;\n failureReason?: string | undefined;\n output?: string | undefined;\n progress?: string | undefined;\n slackChannelId?: string | undefined;\n slackThreadTs?: string | undefined;\n slackTriggerMessageTs?: string | undefined;\n slackUserId?: string | undefined;\n slackProgressMessageTs?: string | undefined;\n slackTreeRootMessageTs?: string | undefined;\n vcsProvider?: "github" | "gitlab" | "azure-devops" | "bitbucket" | undefined;\n vcsRepo?: string | undefined;\n vcsEventType?: string | undefined;\n vcsNumber?: number | undefined;\n vcsCommentId?: number | undefined;\n vcsAuthor?: string | undefined;\n vcsUrl?: string | undefined;\n vcsInstallationId?: number | undefined;\n vcsNodeId?: string | undefined;\n agentmailInboxId?: string | undefined;\n agentmailMessageId?: string | undefined;\n agentmailThreadId?: string | undefined;\n mentionMessageId?: string | undefined;\n mentionChannelId?: string | undefined;\n dir?: string | undefined;\n parentTaskId?: string | undefined;\n claudeSessionId?: string | undefined;\n model?: string | undefined;\n modelTier?: "smol" | "regular" | "smart" | "ultra" | undefined;\n effort?: "off" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;\n resolvedModel?: string | undefined;\n modelSource?: string | undefined;\n modelAlias?: string | undefined;\n resolvedEffort?: "off" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;\n profile?: string | undefined;\n resolvedProfile?: string | undefined;\n scheduleId?: string | undefined;\n workflowRunId?: string | null | undefined;\n workflowRunStepId?: string | null | undefined;\n contextKey?: string | undefined;\n outputSchema?: Record<string, unknown> | undefined;\n followUpConfig?:\n | {\n disabled?: boolean | undefined;\n onCompleted?: string | undefined;\n onFailed?: string | undefined;\n }\n | undefined;\n deferredAt?: string | undefined;\n compactionCount?: number | undefined;\n peakContextPercent?: number | undefined;\n peakContextTokens?: number | undefined;\n contextWindowSize?: number | undefined;\n credentialKeySuffix?: string | undefined;\n credentialKeyType?: string | undefined;\n requestedByUserId?: string | undefined;\n swarmVersion?: string | undefined;\n provider?:\n | "claude"\n | "codex"\n | "pi"\n | "devin"\n | "claude-managed"\n | "opencode"\n | "acp"\n | "dsh"\n | "amp"\n | "cursor"\n | "grok"\n | undefined;\n providerMeta?: Record<string, unknown> | undefined;\n harnessVariant?: string | undefined;\n harnessVariantMeta?: Record<string, unknown> | undefined;\n totalCostUsd?: number | undefined;\n routingAffinity?:\n | {\n capabilities: string[];\n sourceAgentId?: string | undefined;\n role?: string | undefined;\n harnessProvider?:\n | "claude"\n | "codex"\n | "pi"\n | "devin"\n | "claude-managed"\n | "opencode"\n | "acp"\n | "dsh"\n | "amp"\n | "cursor"\n | "grok"\n | undefined;\n leadOnly?: boolean | undefined;\n }\n | undefined;\n routingAffinityInvalid?: boolean | undefined;\n };\n\n export type ActiveSession = {\n id: string;\n agentId: string;\n taskId: string | null;\n triggerType: string;\n inboxMessageId: string | null;\n taskDescription: string | null;\n runnerSessionId: string | null;\n providerSessionId: string | null;\n startedAt: string;\n lastHeartbeatAt: string;\n runtimeInstanceId?: string | null | undefined;\n };\n\n export type RequestInfo = {\n sessionId: string | undefined;\n agentId: string | undefined;\n runtimeInstanceId: string | undefined;\n sourceTaskId: string | undefined;\n contextKey: string | undefined;\n callOrigin: "mcp" | "extension" | "script-sdk";\n };\n\n import type { SwarmSdk } from "swarm-sdk";\n import type { z } from "zod";\n export type Runtime = "api" | "worker";\n export interface ExtensionScriptAsset {\n name: string;\n file: string;\n description: string;\n intent?: string;\n }\n export interface ExtensionScheduleAsset {\n name: string;\n description?: string;\n script: string;\n cronExpression?: string;\n intervalMs?: number;\n timezone?: string;\n args?: Record<string, unknown>;\n }\n export interface ExtensionWorkflowAsset {\n file: string;\n }\n export interface ExtensionSkillAsset {\n dir: string;\n }\n export interface ExtensionManifest {\n $schema?: string;\n name: string;\n description: string;\n version: string;\n runtime: Runtime;\n assets: {\n hooks: string;\n scripts?: readonly ExtensionScriptAsset[];\n schedules?: readonly ExtensionScheduleAsset[];\n workflows?: readonly ExtensionWorkflowAsset[];\n skills?: readonly ExtensionSkillAsset[];\n };\n homepage?: string;\n author?: string;\n }\n export interface HooksModule {\n default: SwarmExtension;\n config?: z.ZodTypeAny;\n }\n export interface ExtensionState {\n get<T = unknown>(key: string): Promise<T | null>;\n set<T>(key: string, value: T): Promise<void>;\n incr(key: string, by?: number): Promise<number>;\n del(key: string): Promise<void>;\n }\n export interface ExtensionLogger {\n debug(message: string, data?: unknown): void;\n info(message: string, data?: unknown): void;\n warn(message: string, data?: unknown): void;\n error(message: string, data?: unknown): void;\n }\n type ConfigFor<M extends ExtensionManifest> = M extends {\n config: infer C extends z.ZodTypeAny;\n }\n ? z.infer<C>\n : Record<string, unknown>;\n export interface ApiCtx<Config = Record<string, unknown>> {\n swarm: SwarmSdk;\n state: ExtensionState;\n config: Config;\n log: ExtensionLogger;\n signal: AbortSignal;\n event: {\n name: keyof SwarmEventMap;\n at: string;\n extension: {\n id: string;\n version: number;\n };\n };\n }\n export interface WorkerCtx<Config = Record<string, unknown>>\n extends Omit<ApiCtx<Config>, "swarm"> {\n swarm: SwarmSdk;\n worker: {\n agentId: string;\n taskId: string;\n harness: string;\n };\n }\n export type CtxFor<M extends ExtensionManifest> = M["runtime"] extends "api"\n ? ApiCtx<ConfigFor<M>>\n : WorkerCtx<ConfigFor<M>>;\n export type PreResult<Modify> =\n | void\n | {\n action: "continue";\n }\n | {\n action: "modify";\n data: Modify;\n }\n | {\n action: "block";\n reason: string;\n };\n export type TaskCreateOrigin =\n | "rest"\n | "app"\n | "mcp"\n | "slack"\n | "schedule"\n | "workflow"\n | "webhook"\n | "followUp"\n | `extension:${string}`;\n export interface TaskCreateEvent {\n options: CreateTaskOptions;\n description: string;\n origin: TaskCreateOrigin;\n requestInfo?: RequestInfo;\n }\n export type TaskCreateModify = Partial<\n Pick<\n CreateTaskOptions,\n | "agentId"\n | "creatorAgentId"\n | "source"\n | "taskType"\n | "tags"\n | "priority"\n | "dependsOn"\n | "offeredTo"\n | "vcsProvider"\n | "vcsRepo"\n | "vcsEventType"\n | "vcsNumber"\n | "vcsCommentId"\n | "vcsAuthor"\n | "vcsUrl"\n | "vcsInstallationId"\n | "vcsNodeId"\n | "agentmailInboxId"\n | "agentmailMessageId"\n | "agentmailThreadId"\n | "mentionMessageId"\n | "mentionChannelId"\n | "dir"\n | "model"\n | "modelTier"\n | "effort"\n | "outputSchema"\n | "followUpConfig"\n | "bypassTrackerContextDedup"\n >\n > & {\n description?: string;\n };\n export interface TaskFollowUpEvent {\n completedTask: AgentTask;\n status: "completed" | "failed";\n output?: string;\n failureReason?: string;\n workerAgentId: string;\n leadAgentId: string;\n summary: string;\n }\n export type TaskFollowUpModify = Partial<\n Pick<CreateTaskOptions, "agentId" | "priority" | "followUpConfig">\n > & {\n description?: string;\n };\n export interface SlackRouteEvent {\n channelId: string;\n userId: string;\n text: string;\n threadTs?: string;\n botMentioned: boolean;\n threadContext?: {\n channelId: string;\n threadTs: string;\n };\n }\n export interface SlackRouteModify {\n target:\n | {\n kind: "agent";\n agentId: string;\n }\n | {\n kind: "lead";\n }\n | {\n kind: "broadcast";\n };\n }\n /** A Chat SDK message after its thread buffer flushed, before routing. */\n export interface ChatRouteEvent {\n /** Adapter name, such as `telegram` or `slack`. */\n adapter: string;\n channelId: string;\n /** The Chat SDK thread id, prefixed with the adapter name. */\n threadId: string;\n /** The first buffered sender. */\n userId: string;\n userName?: string;\n /** The buffered messages, joined. */\n text: string;\n /** True for an @mention and for a reply to one of the bot\'s messages. */\n botMentioned: boolean;\n isDirectMessage: boolean;\n /** Platform chat type, such as Telegram `private`, `group`, `supergroup` or `channel`. */\n chatType?: string;\n /** Set for a Telegram supergroup with topics. */\n isForum?: boolean;\n /** A buffered message replies to one of the bot\'s own messages. */\n replyToBot: boolean;\n /** Set when earlier swarm tasks exist in this thread. */\n threadContext?: {\n adapter: string;\n threadId: string;\n };\n }\n export interface ChatRouteModify {\n target:\n | {\n kind: "agent";\n agentId: string;\n }\n | {\n kind: "lead";\n }\n | {\n kind: "broadcast";\n }\n | {\n kind: "workflow";\n workflowId: string;\n };\n }\n export type HeartbeatClassification = "no-session" | "stale-session" | "fresh-stalled";\n export type HeartbeatAction = "supersede-resume" | "fail" | "record";\n export interface HeartbeatRemediateEvent {\n task: AgentTask;\n session?: ActiveSession;\n classification: HeartbeatClassification;\n proposedAction: HeartbeatAction;\n reason: string;\n taskAgeMs: number;\n sessionHeartbeatAgeMs?: number;\n }\n export interface HeartbeatRemediateModify {\n proposedAction: HeartbeatAction;\n }\n export interface ToolCallEvent {\n tool: string;\n args: unknown;\n requestInfo: RequestInfo;\n }\n export interface ToolCallModify {\n args: unknown;\n }\n export interface TaskEvent {\n task: AgentTask;\n }\n export interface TaskCompletedEvent extends TaskEvent {\n output: string;\n }\n export interface TaskFailedEvent extends TaskEvent {\n failureReason: string;\n }\n export interface TaskSupersededEvent extends TaskEvent {\n supersededBy: string;\n }\n export interface TaskProgressEvent extends TaskEvent {\n progress: string;\n }\n export interface SlackMessageEvent {\n channelId: string;\n userId: string;\n text: string;\n threadTs?: string;\n taskId?: string;\n }\n /** A Chat SDK message the swarm received or sent. */\n export interface ChatMessageEvent {\n adapter: string;\n channelId: string;\n threadId: string;\n /** The sender, for an inbound message. */\n userId?: string;\n text: string;\n direction: "inbound" | "outbound";\n taskId?: string;\n messageId?: string;\n }\n /**\n * A person answered an approval request. Fires for workflow and standalone\n * requests. Timeouts and cancellations do not emit it today.\n */\n export interface ApprovalResolvedEvent {\n requestId: string;\n status: "approved" | "rejected" | "timeout" | "cancelled";\n responses: Record<string, unknown> | null;\n /** Set when a workflow human-in-the-loop step asked. Absent for a standalone request. */\n workflowRunId?: string;\n workflowRunStepId?: string;\n /** The task whose agent asked, for a standalone request. */\n sourceTaskId?: string;\n }\n /** A task was refused because a daily spend budget is used up. */\n export interface TaskBudgetRefusedEvent extends TaskEvent {\n /** The agent the task was refused for. */\n agentId: string;\n /** Which budget refused it. The matching spend and budget fields are set. */\n cause: "agent" | "global" | "user";\n agentSpendUsd?: number;\n agentBudgetUsd?: number;\n globalSpendUsd?: number;\n globalBudgetUsd?: number;\n userSpendUsd?: number;\n userBudgetUsd?: number;\n /** ISO 8601 time the daily budget resets. */\n resetAt: string;\n }\n /** An inbound AgentMail message. */\n export interface EmailReceivedEvent {\n inboxId: string;\n /** The From header as sent. */\n from: string;\n subject: string;\n /** The text body, or the HTML when there is no text, cut to 500 characters with `...` appended. */\n body: string;\n threadId: string;\n messageId: string;\n }\n /** An inbound Kapso (WhatsApp) message. */\n export interface KapsoMessageEvent {\n phoneNumberId: string;\n conversationId?: string;\n messageId: string;\n from?: string;\n /** Kapso message type, such as `text` or `image`. */\n type?: string;\n /** The text, or a placeholder naming the type for a non-text message. */\n text: string;\n }\n export type VcsProvider = "github" | "gitlab" | "azure-devops" | "bitbucket";\n /**\n * The provider object the event is about, as the provider names it. GitLab\n * calls a pull request a merge request, and a comment a `note`.\n */\n export type VcsEventKind =\n | "pull_request"\n | "merge_request"\n | "issue"\n | "issue_comment"\n | "note"\n | "pull_request_review"\n | "pipeline";\n /**\n * A webhook event from a connected code host. `action` is the provider\'s own\n * value: `opened`, `closed`, `synchronize`, `merge`, `created`, `commented`, or,\n * for a pipeline, its status such as `success` or `failed`. Fields a kind does\n * not carry are absent.\n */\n export interface VcsEvent {\n provider: VcsProvider;\n kind: VcsEventKind;\n action: string;\n /** `owner/name` on GitHub and GitLab, `workspace/repo_slug` on Bitbucket, the repository URL on Azure DevOps. */\n repo: string;\n /** The PR, merge request, or issue number. Absent for a pipeline outside a merge request. */\n number?: number;\n title?: string;\n body?: string | null;\n /** Login of the user behind the event. Set for pull requests, merge requests, and Azure DevOps comments. */\n author?: string;\n url?: string;\n merged?: boolean;\n changedFiles?: number;\n /** The review verdict, on a pull request review. */\n reviewState?: string;\n }\n export interface ToolCallCompletedEvent extends ToolCallEvent {\n result: unknown;\n durationMs: number;\n }\n export interface SwarmEventMap {\n "pre.task.create": {\n event: TaskCreateEvent;\n modify: TaskCreateModify;\n result: PreResult<TaskCreateModify>;\n };\n "pre.task.followUp": {\n event: TaskFollowUpEvent;\n modify: TaskFollowUpModify;\n result: PreResult<TaskFollowUpModify>;\n };\n "pre.slack.route": {\n event: SlackRouteEvent;\n modify: SlackRouteModify;\n result: PreResult<SlackRouteModify>;\n };\n "pre.chat.route": {\n event: ChatRouteEvent;\n modify: ChatRouteModify;\n result: PreResult<ChatRouteModify>;\n };\n "pre.heartbeat.remediate": {\n event: HeartbeatRemediateEvent;\n modify: HeartbeatRemediateModify;\n result: PreResult<HeartbeatRemediateModify>;\n };\n "pre.tool.call": {\n event: ToolCallEvent;\n modify: ToolCallModify;\n result: PreResult<ToolCallModify>;\n };\n "post.task.created": {\n event: TaskEvent;\n modify: never;\n result: void;\n };\n "post.task.completed": {\n event: TaskCompletedEvent;\n modify: never;\n result: void;\n };\n "post.task.failed": {\n event: TaskFailedEvent;\n modify: never;\n result: void;\n };\n "post.task.cancelled": {\n event: TaskEvent;\n modify: never;\n result: void;\n };\n "post.task.superseded": {\n event: TaskSupersededEvent;\n modify: never;\n result: void;\n };\n "post.task.progress": {\n event: TaskProgressEvent;\n modify: never;\n result: void;\n };\n "post.slack.message": {\n event: SlackMessageEvent;\n modify: never;\n result: void;\n };\n "post.chat.message": {\n event: ChatMessageEvent;\n modify: never;\n result: void;\n };\n "post.approval.resolved": {\n event: ApprovalResolvedEvent;\n modify: never;\n result: void;\n };\n "post.task.budgetRefused": {\n event: TaskBudgetRefusedEvent;\n modify: never;\n result: void;\n };\n "post.email.received": {\n event: EmailReceivedEvent;\n modify: never;\n result: void;\n };\n "post.kapso.message": {\n event: KapsoMessageEvent;\n modify: never;\n result: void;\n };\n "post.vcs.event": {\n event: VcsEvent;\n modify: never;\n result: void;\n };\n "post.tool.call": {\n event: ToolCallCompletedEvent;\n modify: never;\n result: void;\n };\n }\n export interface ExtensionApi<\n M extends ExtensionManifest = ExtensionManifest & {\n runtime: "api";\n },\n > {\n on<E extends keyof SwarmEventMap>(\n event: E,\n handler: (\n event: SwarmEventMap[E]["event"],\n ctx: CtxFor<M>,\n ) => Promise<SwarmEventMap[E]["result"]> | SwarmEventMap[E]["result"],\n opts?: {\n priority?: number;\n },\n ): void;\n }\n export type SwarmExtension<\n M extends ExtensionManifest = ExtensionManifest & {\n runtime: "api";\n },\n > = (api: ExtensionApi<M>) => void;\n export { block, modify } from "./contract-runtime";\n}\n';
|
|
4
|
+
'// biome-ignore-all lint/suspicious/noConfusingVoidType: Extension handlers may explicitly return no result.\ndeclare module "swarm-extension" {\n export type CreateTaskOptions = {\n key?: string | undefined;\n agentId?: string | null | undefined;\n creatorAgentId?: string | undefined;\n source?:\n | "workflow"\n | "schedule"\n | "slack"\n | "agentmail"\n | "mcp"\n | "api"\n | "ui"\n | "github"\n | "gitlab"\n | "azure-devops"\n | "bitbucket"\n | "system"\n | "linear"\n | "jira"\n | "comb"\n | "chat"\n | undefined;\n routingReason?:\n | "skill"\n | "continuity"\n | "overflow"\n | "human_pinned"\n | "reroute_fault"\n | undefined;\n routingSource?: "declared" | "engine_default" | undefined;\n routingNote?: string | undefined;\n taskType?: string | undefined;\n tags?: string[] | undefined;\n priority?: number | undefined;\n dependsOn?: string[] | undefined;\n offeredTo?: string | undefined;\n status?: "draft" | "backlog" | "unassigned" | undefined;\n slackChannelId?: string | undefined;\n slackThreadTs?: string | undefined;\n slackTriggerMessageTs?: string | undefined;\n slackUserId?: string | undefined;\n overrideSlackContext?: boolean | undefined;\n vcsProvider?: "github" | "gitlab" | "azure-devops" | "bitbucket" | undefined;\n vcsRepo?: string | undefined;\n vcsEventType?: string | undefined;\n vcsNumber?: number | undefined;\n vcsCommentId?: number | undefined;\n vcsAuthor?: string | undefined;\n vcsUrl?: string | undefined;\n vcsInstallationId?: number | undefined;\n vcsNodeId?: string | undefined;\n agentmailInboxId?: string | undefined;\n agentmailMessageId?: string | undefined;\n agentmailThreadId?: string | undefined;\n mentionMessageId?: string | undefined;\n mentionChannelId?: string | undefined;\n dir?: string | undefined;\n parentTaskId?: string | undefined;\n model?: string | undefined;\n modelTier?: "smol" | "regular" | "smart" | "ultra" | undefined;\n effort?: "off" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;\n profile?: string | undefined;\n scheduleId?: string | undefined;\n workflowRunId?: string | undefined;\n workflowRunStepId?: string | undefined;\n sourceTaskId?: string | undefined;\n outputSchema?: Record<string, unknown> | undefined;\n inheritParentOutputSchema?: boolean | undefined;\n inheritParentRequester?: boolean | undefined;\n inheritParentRoutingAffinity?: boolean | undefined;\n inheritParentFollowUpConfig?: boolean | undefined;\n followUpConfig?:\n | {\n disabled?: boolean | undefined;\n onCompleted?: string | undefined;\n onFailed?: string | undefined;\n }\n | undefined;\n requestedByUserId?: string | undefined;\n contextKey?: string | undefined;\n bypassTrackerContextDedup?: boolean | undefined;\n routingAffinity?:\n | {\n capabilities: string[];\n sourceAgentId?: string | undefined;\n role?: string | undefined;\n harnessProvider?:\n | "claude"\n | "codex"\n | "pi"\n | "devin"\n | "claude-managed"\n | "opencode"\n | "acp"\n | "dsh"\n | "amp"\n | "cursor"\n | "grok"\n | undefined;\n leadOnly?: boolean | undefined;\n }\n | undefined;\n };\n\n export type AgentTask = {\n id: string;\n key: string;\n agentId: string | null;\n task: string;\n status:\n | "draft"\n | "backlog"\n | "unassigned"\n | "offered"\n | "reviewing"\n | "pending"\n | "in_progress"\n | "paused"\n | "completed"\n | "failed"\n | "cancelled"\n | "superseded";\n source:\n | "workflow"\n | "schedule"\n | "slack"\n | "agentmail"\n | "mcp"\n | "api"\n | "ui"\n | "github"\n | "gitlab"\n | "azure-devops"\n | "bitbucket"\n | "system"\n | "linear"\n | "jira"\n | "comb"\n | "chat";\n tags: string[];\n priority: number;\n dependsOn: string[];\n createdAt: string;\n lastUpdatedAt: string;\n slackReplySent: boolean;\n wasPaused: boolean;\n creatorAgentId?: string | undefined;\n title?: string | undefined;\n routingReason?:\n | "skill"\n | "continuity"\n | "overflow"\n | "human_pinned"\n | "reroute_fault"\n | undefined;\n routingSource?: "declared" | "engine_default" | undefined;\n routingNote?: string | undefined;\n poolWakeAt?: string | undefined;\n taskType?: string | undefined;\n offeredTo?: string | undefined;\n offeredAt?: string | undefined;\n acceptedAt?: string | undefined;\n rejectionReason?: string | undefined;\n finishedAt?: string | undefined;\n notifiedAt?: string | undefined;\n failureReason?: string | undefined;\n output?: string | undefined;\n progress?: string | undefined;\n slackChannelId?: string | undefined;\n slackThreadTs?: string | undefined;\n slackTriggerMessageTs?: string | undefined;\n slackUserId?: string | undefined;\n slackProgressMessageTs?: string | undefined;\n slackTreeRootMessageTs?: string | undefined;\n vcsProvider?: "github" | "gitlab" | "azure-devops" | "bitbucket" | undefined;\n vcsRepo?: string | undefined;\n vcsEventType?: string | undefined;\n vcsNumber?: number | undefined;\n vcsCommentId?: number | undefined;\n vcsAuthor?: string | undefined;\n vcsUrl?: string | undefined;\n vcsInstallationId?: number | undefined;\n vcsNodeId?: string | undefined;\n agentmailInboxId?: string | undefined;\n agentmailMessageId?: string | undefined;\n agentmailThreadId?: string | undefined;\n mentionMessageId?: string | undefined;\n mentionChannelId?: string | undefined;\n dir?: string | undefined;\n parentTaskId?: string | undefined;\n claudeSessionId?: string | undefined;\n model?: string | undefined;\n modelTier?: "smol" | "regular" | "smart" | "ultra" | undefined;\n effort?: "off" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;\n resolvedModel?: string | undefined;\n modelSource?: string | undefined;\n modelAlias?: string | undefined;\n resolvedEffort?: "off" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;\n profile?: string | undefined;\n resolvedProfile?: string | undefined;\n scheduleId?: string | undefined;\n workflowRunId?: string | null | undefined;\n workflowRunStepId?: string | null | undefined;\n contextKey?: string | undefined;\n outputSchema?: Record<string, unknown> | undefined;\n followUpConfig?:\n | {\n disabled?: boolean | undefined;\n onCompleted?: string | undefined;\n onFailed?: string | undefined;\n }\n | undefined;\n deferredAt?: string | undefined;\n compactionCount?: number | undefined;\n peakContextPercent?: number | undefined;\n peakContextTokens?: number | undefined;\n contextWindowSize?: number | undefined;\n credentialKeySuffix?: string | undefined;\n credentialKeyType?: string | undefined;\n requestedByUserId?: string | undefined;\n swarmVersion?: string | undefined;\n provider?:\n | "claude"\n | "codex"\n | "pi"\n | "devin"\n | "claude-managed"\n | "opencode"\n | "acp"\n | "dsh"\n | "amp"\n | "cursor"\n | "grok"\n | undefined;\n providerMeta?: Record<string, unknown> | undefined;\n harnessVariant?: string | undefined;\n harnessVariantMeta?: Record<string, unknown> | undefined;\n totalCostUsd?: number | undefined;\n routingAffinity?:\n | {\n capabilities: string[];\n sourceAgentId?: string | undefined;\n role?: string | undefined;\n harnessProvider?:\n | "claude"\n | "codex"\n | "pi"\n | "devin"\n | "claude-managed"\n | "opencode"\n | "acp"\n | "dsh"\n | "amp"\n | "cursor"\n | "grok"\n | undefined;\n leadOnly?: boolean | undefined;\n }\n | undefined;\n routingAffinityInvalid?: boolean | undefined;\n };\n\n export type ActiveSession = {\n id: string;\n agentId: string;\n taskId: string | null;\n triggerType: string;\n inboxMessageId: string | null;\n taskDescription: string | null;\n runnerSessionId: string | null;\n providerSessionId: string | null;\n startedAt: string;\n lastHeartbeatAt: string;\n runtimeInstanceId?: string | null | undefined;\n };\n\n export type RequestInfo = {\n sessionId: string | undefined;\n agentId: string | undefined;\n runtimeInstanceId: string | undefined;\n sourceTaskId: string | undefined;\n contextKey: string | undefined;\n callOrigin: "mcp" | "extension" | "script-sdk";\n };\n\n import type { SwarmSdk } from "swarm-sdk";\n import type { z } from "zod";\n export type Runtime = "api" | "worker";\n export interface ExtensionScriptAsset {\n name: string;\n file: string;\n description: string;\n intent?: string;\n }\n export interface ExtensionScheduleAsset {\n name: string;\n description?: string;\n script: string;\n cronExpression?: string;\n intervalMs?: number;\n timezone?: string;\n args?: Record<string, unknown>;\n }\n export interface ExtensionWorkflowAsset {\n file: string;\n }\n export interface ExtensionSkillAsset {\n dir: string;\n }\n export interface ExtensionManifest {\n $schema?: string;\n name: string;\n description: string;\n version: string;\n runtime: Runtime;\n assets: {\n hooks: string;\n scripts?: readonly ExtensionScriptAsset[];\n schedules?: readonly ExtensionScheduleAsset[];\n workflows?: readonly ExtensionWorkflowAsset[];\n skills?: readonly ExtensionSkillAsset[];\n };\n homepage?: string;\n author?: string;\n }\n export interface HooksModule {\n default: SwarmExtension;\n config?: z.ZodTypeAny;\n }\n export interface ExtensionState {\n get<T = unknown>(key: string): Promise<T | null>;\n set<T>(key: string, value: T): Promise<void>;\n incr(key: string, by?: number): Promise<number>;\n del(key: string): Promise<void>;\n }\n export interface ExtensionLogger {\n debug(message: string, data?: unknown): void;\n info(message: string, data?: unknown): void;\n warn(message: string, data?: unknown): void;\n error(message: string, data?: unknown): void;\n }\n type ConfigFor<M extends ExtensionManifest> = M extends {\n config: infer C extends z.ZodTypeAny;\n }\n ? z.infer<C>\n : Record<string, unknown>;\n export interface ApiCtx<Config = Record<string, unknown>> {\n swarm: SwarmSdk;\n state: ExtensionState;\n config: Config;\n log: ExtensionLogger;\n signal: AbortSignal;\n event: {\n name: keyof SwarmEventMap;\n at: string;\n extension: {\n id: string;\n version: number;\n };\n };\n }\n export interface WorkerCtx<Config = Record<string, unknown>>\n extends Omit<ApiCtx<Config>, "swarm"> {\n swarm: SwarmSdk;\n worker: {\n agentId: string;\n taskId: string;\n harness: string;\n };\n }\n export type CtxFor<M extends ExtensionManifest> = M["runtime"] extends "api"\n ? ApiCtx<ConfigFor<M>>\n : WorkerCtx<ConfigFor<M>>;\n export type PreResult<Modify> =\n | void\n | {\n action: "continue";\n }\n | {\n action: "modify";\n data: Modify;\n }\n | {\n action: "block";\n reason: string;\n };\n export type TaskCreateOrigin =\n | "rest"\n | "app"\n | "mcp"\n | "slack"\n | "schedule"\n | "workflow"\n | "webhook"\n | "followUp"\n | `extension:${string}`;\n export interface TaskCreateEvent {\n options: CreateTaskOptions;\n description: string;\n origin: TaskCreateOrigin;\n requestInfo?: RequestInfo;\n }\n /** A pre.task.create hook blocked a task before it was created. */\n export interface TaskBlockedEvent {\n /** The extension that blocked, or whose model/agent rewrite was rejected. */\n extension: {\n id: string;\n name: string;\n };\n /** The scrubbed block reason. */\n reason: string;\n origin: TaskCreateOrigin;\n source?: AgentTask["source"];\n requester: {\n userId?: string;\n agentId?: string;\n slackUserId?: string;\n };\n context?: {\n contextKey?: string;\n scheduleId?: string;\n workflowRunId?: string;\n workflowRunStepId?: string;\n parentTaskId?: string;\n vcsRepo?: string;\n vcsNumber?: number;\n slackChannelId?: string;\n slackThreadTs?: string;\n };\n /** The original task description, scrubbed and cut to 500 characters. */\n description: string;\n }\n export type TaskCreateModify = Partial<\n Pick<\n CreateTaskOptions,\n | "agentId"\n | "creatorAgentId"\n | "source"\n | "taskType"\n | "tags"\n | "priority"\n | "dependsOn"\n | "offeredTo"\n | "vcsProvider"\n | "vcsRepo"\n | "vcsEventType"\n | "vcsNumber"\n | "vcsCommentId"\n | "vcsAuthor"\n | "vcsUrl"\n | "vcsInstallationId"\n | "vcsNodeId"\n | "agentmailInboxId"\n | "agentmailMessageId"\n | "agentmailThreadId"\n | "mentionMessageId"\n | "mentionChannelId"\n | "dir"\n | "model"\n | "modelTier"\n | "effort"\n | "outputSchema"\n | "followUpConfig"\n | "bypassTrackerContextDedup"\n >\n > & {\n description?: string;\n };\n export interface TaskFollowUpEvent {\n completedTask: AgentTask;\n status: "completed" | "failed";\n output?: string;\n failureReason?: string;\n workerAgentId: string;\n leadAgentId: string;\n summary: string;\n }\n export type TaskFollowUpModify = Partial<\n Pick<CreateTaskOptions, "agentId" | "priority" | "followUpConfig">\n > & {\n description?: string;\n };\n export interface SlackRouteEvent {\n channelId: string;\n userId: string;\n text: string;\n threadTs?: string;\n botMentioned: boolean;\n threadContext?: {\n channelId: string;\n threadTs: string;\n };\n }\n export interface SlackRouteModify {\n target:\n | {\n kind: "agent";\n agentId: string;\n }\n | {\n kind: "lead";\n }\n | {\n kind: "broadcast";\n };\n }\n /** A Chat SDK message after its thread buffer flushed, before routing. */\n export interface ChatRouteEvent {\n /** Adapter name, such as `telegram` or `slack`. */\n adapter: string;\n channelId: string;\n /** The Chat SDK thread id, prefixed with the adapter name. */\n threadId: string;\n /** The first buffered sender. */\n userId: string;\n userName?: string;\n /** The buffered messages, joined. */\n text: string;\n /** True for an @mention and for a reply to one of the bot\'s messages. */\n botMentioned: boolean;\n isDirectMessage: boolean;\n /** Platform chat type, such as Telegram `private`, `group`, `supergroup` or `channel`. */\n chatType?: string;\n /** Set for a Telegram supergroup with topics. */\n isForum?: boolean;\n /** A buffered message replies to one of the bot\'s own messages. */\n replyToBot: boolean;\n /** Set when earlier swarm tasks exist in this thread. */\n threadContext?: {\n adapter: string;\n threadId: string;\n };\n }\n export interface ChatRouteModify {\n target:\n | {\n kind: "agent";\n agentId: string;\n }\n | {\n kind: "lead";\n }\n | {\n kind: "broadcast";\n }\n | {\n kind: "workflow";\n workflowId: string;\n };\n }\n export type HeartbeatClassification = "no-session" | "stale-session" | "fresh-stalled";\n export type HeartbeatAction = "supersede-resume" | "fail" | "record";\n export interface HeartbeatRemediateEvent {\n task: AgentTask;\n session?: ActiveSession;\n classification: HeartbeatClassification;\n proposedAction: HeartbeatAction;\n reason: string;\n taskAgeMs: number;\n sessionHeartbeatAgeMs?: number;\n }\n export interface HeartbeatRemediateModify {\n proposedAction: HeartbeatAction;\n }\n export interface ToolCallEvent {\n tool: string;\n args: unknown;\n requestInfo: RequestInfo;\n }\n export interface ToolCallModify {\n args: unknown;\n }\n export interface TaskEvent {\n task: AgentTask;\n }\n export interface TaskCompletedEvent extends TaskEvent {\n output: string;\n }\n export interface TaskFailedEvent extends TaskEvent {\n failureReason: string;\n }\n export interface TaskSupersededEvent extends TaskEvent {\n supersededBy: string;\n }\n export interface TaskProgressEvent extends TaskEvent {\n progress: string;\n }\n export interface SlackMessageEvent {\n channelId: string;\n userId: string;\n text: string;\n threadTs?: string;\n taskId?: string;\n }\n /** A Chat SDK message the swarm received or sent. */\n export interface ChatMessageEvent {\n adapter: string;\n channelId: string;\n threadId: string;\n /** The sender, for an inbound message. */\n userId?: string;\n text: string;\n direction: "inbound" | "outbound";\n taskId?: string;\n messageId?: string;\n }\n /**\n * A person answered an approval request. Fires for workflow and standalone\n * requests. Timeouts and cancellations do not emit it today.\n */\n export interface ApprovalResolvedEvent {\n requestId: string;\n status: "approved" | "rejected" | "timeout" | "cancelled";\n responses: Record<string, unknown> | null;\n /** Set when a workflow human-in-the-loop step asked. Absent for a standalone request. */\n workflowRunId?: string;\n workflowRunStepId?: string;\n /** The task whose agent asked, for a standalone request. */\n sourceTaskId?: string;\n }\n /** A task was refused because a daily spend budget is used up. */\n export interface TaskBudgetRefusedEvent extends TaskEvent {\n /** The agent the task was refused for. */\n agentId: string;\n /** Which budget refused it. The matching spend and budget fields are set. */\n cause: "agent" | "global" | "user";\n agentSpendUsd?: number;\n agentBudgetUsd?: number;\n globalSpendUsd?: number;\n globalBudgetUsd?: number;\n userSpendUsd?: number;\n userBudgetUsd?: number;\n /** ISO 8601 time the daily budget resets. */\n resetAt: string;\n }\n /** An inbound AgentMail message. */\n export interface EmailReceivedEvent {\n inboxId: string;\n /** The From header as sent. */\n from: string;\n subject: string;\n /** The text body, or the HTML when there is no text, cut to 500 characters with `...` appended. */\n body: string;\n threadId: string;\n messageId: string;\n }\n /** An inbound Kapso (WhatsApp) message. */\n export interface KapsoMessageEvent {\n phoneNumberId: string;\n conversationId?: string;\n messageId: string;\n from?: string;\n /** Kapso message type, such as `text` or `image`. */\n type?: string;\n /** The text, or a placeholder naming the type for a non-text message. */\n text: string;\n }\n export type VcsProvider = "github" | "gitlab" | "azure-devops" | "bitbucket";\n /**\n * The provider object the event is about, as the provider names it. GitLab\n * calls a pull request a merge request, and a comment a `note`.\n */\n export type VcsEventKind =\n | "pull_request"\n | "merge_request"\n | "issue"\n | "issue_comment"\n | "note"\n | "pull_request_review"\n | "pipeline";\n /**\n * A webhook event from a connected code host. `action` is the provider\'s own\n * value: `opened`, `closed`, `synchronize`, `merge`, `created`, `commented`, or,\n * for a pipeline, its status such as `success` or `failed`. Fields a kind does\n * not carry are absent.\n */\n export interface VcsEvent {\n provider: VcsProvider;\n kind: VcsEventKind;\n action: string;\n /** `owner/name` on GitHub and GitLab, `workspace/repo_slug` on Bitbucket, the repository URL on Azure DevOps. */\n repo: string;\n /** The PR, merge request, or issue number. Absent for a pipeline outside a merge request. */\n number?: number;\n title?: string;\n body?: string | null;\n /** Login of the user behind the event. Set for pull requests, merge requests, and Azure DevOps comments. */\n author?: string;\n url?: string;\n merged?: boolean;\n changedFiles?: number;\n /** The review verdict, on a pull request review. */\n reviewState?: string;\n }\n export interface ToolCallCompletedEvent extends ToolCallEvent {\n result: unknown;\n durationMs: number;\n }\n export interface SwarmEventMap {\n "pre.task.create": {\n event: TaskCreateEvent;\n modify: TaskCreateModify;\n result: PreResult<TaskCreateModify>;\n };\n "pre.task.followUp": {\n event: TaskFollowUpEvent;\n modify: TaskFollowUpModify;\n result: PreResult<TaskFollowUpModify>;\n };\n "pre.slack.route": {\n event: SlackRouteEvent;\n modify: SlackRouteModify;\n result: PreResult<SlackRouteModify>;\n };\n "pre.chat.route": {\n event: ChatRouteEvent;\n modify: ChatRouteModify;\n result: PreResult<ChatRouteModify>;\n };\n "pre.heartbeat.remediate": {\n event: HeartbeatRemediateEvent;\n modify: HeartbeatRemediateModify;\n result: PreResult<HeartbeatRemediateModify>;\n };\n "pre.tool.call": {\n event: ToolCallEvent;\n modify: ToolCallModify;\n result: PreResult<ToolCallModify>;\n };\n "post.task.created": {\n event: TaskEvent;\n modify: never;\n result: void;\n };\n "post.task.blocked": {\n event: TaskBlockedEvent;\n modify: never;\n result: void;\n };\n "post.task.completed": {\n event: TaskCompletedEvent;\n modify: never;\n result: void;\n };\n "post.task.failed": {\n event: TaskFailedEvent;\n modify: never;\n result: void;\n };\n "post.task.cancelled": {\n event: TaskEvent;\n modify: never;\n result: void;\n };\n "post.task.superseded": {\n event: TaskSupersededEvent;\n modify: never;\n result: void;\n };\n "post.task.progress": {\n event: TaskProgressEvent;\n modify: never;\n result: void;\n };\n "post.slack.message": {\n event: SlackMessageEvent;\n modify: never;\n result: void;\n };\n "post.chat.message": {\n event: ChatMessageEvent;\n modify: never;\n result: void;\n };\n "post.approval.resolved": {\n event: ApprovalResolvedEvent;\n modify: never;\n result: void;\n };\n "post.task.budgetRefused": {\n event: TaskBudgetRefusedEvent;\n modify: never;\n result: void;\n };\n "post.email.received": {\n event: EmailReceivedEvent;\n modify: never;\n result: void;\n };\n "post.kapso.message": {\n event: KapsoMessageEvent;\n modify: never;\n result: void;\n };\n "post.vcs.event": {\n event: VcsEvent;\n modify: never;\n result: void;\n };\n "post.tool.call": {\n event: ToolCallCompletedEvent;\n modify: never;\n result: void;\n };\n }\n export interface ExtensionApi<\n M extends ExtensionManifest = ExtensionManifest & {\n runtime: "api";\n },\n > {\n on<E extends keyof SwarmEventMap>(\n event: E,\n handler: (\n event: SwarmEventMap[E]["event"],\n ctx: CtxFor<M>,\n ) => Promise<SwarmEventMap[E]["result"]> | SwarmEventMap[E]["result"],\n opts?: {\n priority?: number;\n },\n ): void;\n }\n export type SwarmExtension<\n M extends ExtensionManifest = ExtensionManifest & {\n runtime: "api";\n },\n > = (api: ExtensionApi<M>) => void;\n export { block, modify } from "./contract-runtime";\n}\n';
|
|
@@ -119,6 +119,30 @@ export interface TaskCreateEvent {
|
|
|
119
119
|
requestInfo?: RequestInfo;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/** A pre.task.create hook blocked a task before it was created. */
|
|
123
|
+
export interface TaskBlockedEvent {
|
|
124
|
+
/** The extension that blocked, or whose model/agent rewrite was rejected. */
|
|
125
|
+
extension: { id: string; name: string };
|
|
126
|
+
/** The scrubbed block reason. */
|
|
127
|
+
reason: string;
|
|
128
|
+
origin: TaskCreateOrigin;
|
|
129
|
+
source?: AgentTask["source"];
|
|
130
|
+
requester: { userId?: string; agentId?: string; slackUserId?: string };
|
|
131
|
+
context?: {
|
|
132
|
+
contextKey?: string;
|
|
133
|
+
scheduleId?: string;
|
|
134
|
+
workflowRunId?: string;
|
|
135
|
+
workflowRunStepId?: string;
|
|
136
|
+
parentTaskId?: string;
|
|
137
|
+
vcsRepo?: string;
|
|
138
|
+
vcsNumber?: number;
|
|
139
|
+
slackChannelId?: string;
|
|
140
|
+
slackThreadTs?: string;
|
|
141
|
+
};
|
|
142
|
+
/** The original task description, scrubbed and cut to 500 characters. */
|
|
143
|
+
description: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
122
146
|
export type TaskCreateModify = Partial<
|
|
123
147
|
Pick<
|
|
124
148
|
CreateTaskOptions,
|
|
@@ -414,6 +438,7 @@ export interface SwarmEventMap {
|
|
|
414
438
|
result: PreResult<ToolCallModify>;
|
|
415
439
|
};
|
|
416
440
|
"post.task.created": { event: TaskEvent; modify: never; result: void };
|
|
441
|
+
"post.task.blocked": { event: TaskBlockedEvent; modify: never; result: void };
|
|
417
442
|
"post.task.completed": { event: TaskCompletedEvent; modify: never; result: void };
|
|
418
443
|
"post.task.failed": { event: TaskFailedEvent; modify: never; result: void };
|
|
419
444
|
"post.task.cancelled": { event: TaskEvent; modify: never; result: void };
|
|
@@ -148,6 +148,10 @@ export function runContext(event: keyof SwarmEventMap, payload: unknown): RunCon
|
|
|
148
148
|
agentId = requestAgent ?? stringField(payload, "options", "agentId");
|
|
149
149
|
subject = `${stringField(payload, "origin") ?? "?"}: ${snippet(field(payload, "description"))}`;
|
|
150
150
|
break;
|
|
151
|
+
case "post.task.blocked":
|
|
152
|
+
agentId = stringField(payload, "requester", "agentId");
|
|
153
|
+
subject = `${stringField(payload, "origin") ?? "?"}: ${stringField(payload, "extension", "name") ?? "?"}`;
|
|
154
|
+
break;
|
|
151
155
|
case "pre.task.followUp":
|
|
152
156
|
agentId = stringField(payload, "workerAgentId");
|
|
153
157
|
subject =
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ApprovalResolvedEvent,
|
|
6
6
|
ChatMessageEvent,
|
|
7
7
|
KapsoMessageEvent,
|
|
8
|
+
TaskBlockedEvent,
|
|
8
9
|
TaskBudgetRefusedEvent,
|
|
9
10
|
VcsEvent,
|
|
10
11
|
VcsEventKind,
|
|
@@ -44,6 +45,11 @@ const onTaskCreated = observed("task.created", async (data) => {
|
|
|
44
45
|
);
|
|
45
46
|
});
|
|
46
47
|
|
|
48
|
+
const onTaskBlocked = observed("task.create_blocked", async (data) => {
|
|
49
|
+
const { skipExtensionId, ...event } = data as TaskBlockedEvent & { skipExtensionId?: string };
|
|
50
|
+
await dispatchPost("post.task.blocked", event, { skipExtensionId });
|
|
51
|
+
});
|
|
52
|
+
|
|
47
53
|
const onTaskCompleted = observed("task.completed", async (data) => {
|
|
48
54
|
const task = await taskFrom(data);
|
|
49
55
|
if (!task) return;
|
|
@@ -358,6 +364,7 @@ export function initExtensionPostBridge(): void {
|
|
|
358
364
|
if (subscribed) return;
|
|
359
365
|
subscribed = true;
|
|
360
366
|
workflowEventBus.on("task.created", onTaskCreated);
|
|
367
|
+
workflowEventBus.on("task.create_blocked", onTaskBlocked);
|
|
361
368
|
workflowEventBus.on("task.completed", onTaskCompleted);
|
|
362
369
|
workflowEventBus.on("task.failed", onTaskFailed);
|
|
363
370
|
workflowEventBus.on("task.cancelled", onTaskCancelled);
|
|
@@ -377,6 +384,7 @@ export function teardownExtensionPostBridge(): void {
|
|
|
377
384
|
if (!subscribed) return;
|
|
378
385
|
subscribed = false;
|
|
379
386
|
workflowEventBus.off("task.created", onTaskCreated);
|
|
387
|
+
workflowEventBus.off("task.create_blocked", onTaskBlocked);
|
|
380
388
|
workflowEventBus.off("task.completed", onTaskCompleted);
|
|
381
389
|
workflowEventBus.off("task.failed", onTaskFailed);
|
|
382
390
|
workflowEventBus.off("task.cancelled", onTaskCancelled);
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { renderContextBoundNote } from "../prompts/context-bound-note";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One-time "keep this turn bounded" note for long worker sessions, on every
|
|
7
|
+
* harness.
|
|
8
|
+
*
|
|
9
|
+
* This module owns the threshold, the once-per-task claim and the rendering.
|
|
10
|
+
* A harness supplies only two things: the prompt size of its latest
|
|
11
|
+
* main-thread request (input + cache read + cache write tokens) and a way to
|
|
12
|
+
* put text in front of the model mid-session.
|
|
13
|
+
*
|
|
14
|
+
* - Claude: the session writes the size next to the task file
|
|
15
|
+
* (`recordContextState`); the PostToolUse hook claims a marker file
|
|
16
|
+
* (`claimContextBoundNote`) and returns the note as tool context.
|
|
17
|
+
* - In-process harnesses (codex, pi, opencode, amp, claude-managed): the session
|
|
18
|
+
* feeds each request's size to `createContextBoundNote`, which injects the
|
|
19
|
+
* note through the harness's own mid-turn input, and only while that input
|
|
20
|
+
* still lands inside the current turn.
|
|
21
|
+
*
|
|
22
|
+
* Every failure path means "no note": nothing here throws into a session or
|
|
23
|
+
* blocks a tool call.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Threshold from `CONTEXT_BOUND_NOTE_TOKENS`. Off by default: unset, empty,
|
|
28
|
+
* 0 or invalid returns 0 (off); a positive integer turns the note on at that
|
|
29
|
+
* many tokens.
|
|
30
|
+
*/
|
|
31
|
+
export function contextBoundNoteThreshold(
|
|
32
|
+
env: Record<string, string | undefined> = process.env,
|
|
33
|
+
): number {
|
|
34
|
+
const raw = env.CONTEXT_BOUND_NOTE_TOKENS?.trim();
|
|
35
|
+
if (!raw || !/^\d+$/.test(raw)) return 0;
|
|
36
|
+
const parsed = Number(raw);
|
|
37
|
+
return Number.isSafeInteger(parsed) ? parsed : 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** True when a valid token count reaches an enabled (> 0) threshold. */
|
|
41
|
+
function reachesThreshold(contextTokens: unknown, threshold: number): contextTokens is number {
|
|
42
|
+
return (
|
|
43
|
+
threshold > 0 &&
|
|
44
|
+
typeof contextTokens === "number" &&
|
|
45
|
+
Number.isFinite(contextTokens) &&
|
|
46
|
+
contextTokens >= threshold
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Render the note and hand it to `inject`. Fails open. */
|
|
51
|
+
async function deliverContextBoundNote(
|
|
52
|
+
contextTokens: number,
|
|
53
|
+
model: string | undefined,
|
|
54
|
+
inject: (note: string) => unknown,
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
try {
|
|
57
|
+
const note = await renderContextBoundNote(contextTokens, model);
|
|
58
|
+
if (note) await inject(note);
|
|
59
|
+
} catch {
|
|
60
|
+
// Fail open.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function contextStatePath(taskFilePath: string): string {
|
|
65
|
+
return `${taskFilePath}.context.json`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function contextBoundNoteMarkerPath(taskFilePath: string): string {
|
|
69
|
+
return `${taskFilePath}.bound-note`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** What the session records for the hook: the latest request's size and the session's model. */
|
|
73
|
+
export interface ContextState {
|
|
74
|
+
contextTokens: number;
|
|
75
|
+
model?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Record the latest request's context size (and the session's model, for the
|
|
80
|
+
* window share) for the hook. Synchronous so
|
|
81
|
+
* writes land in stream order; tmp + rename so the hook never reads a partial
|
|
82
|
+
* file. Errors are swallowed: a missing state file only means no note.
|
|
83
|
+
*/
|
|
84
|
+
export function recordContextState(
|
|
85
|
+
taskFilePath: string,
|
|
86
|
+
contextTokens: number,
|
|
87
|
+
model?: string,
|
|
88
|
+
): void {
|
|
89
|
+
const path = contextStatePath(taskFilePath);
|
|
90
|
+
const tmp = `${path}.tmp`;
|
|
91
|
+
try {
|
|
92
|
+
writeFileSync(
|
|
93
|
+
tmp,
|
|
94
|
+
JSON.stringify({ contextTokens, model, updatedAt: new Date().toISOString() }),
|
|
95
|
+
);
|
|
96
|
+
renameSync(tmp, path);
|
|
97
|
+
} catch {
|
|
98
|
+
// Fail open.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Returns the recorded state when the note should fire now, else null. Fires at
|
|
104
|
+
* most once per task file: the marker is created with `wx`, so a second call
|
|
105
|
+
* (or a concurrent hook) loses the race and gets null.
|
|
106
|
+
*/
|
|
107
|
+
export async function claimContextBoundNote(
|
|
108
|
+
taskFilePath: string | undefined,
|
|
109
|
+
threshold: number,
|
|
110
|
+
): Promise<ContextState | null> {
|
|
111
|
+
if (!taskFilePath || threshold <= 0) return null;
|
|
112
|
+
let contextTokens: unknown;
|
|
113
|
+
let model: unknown;
|
|
114
|
+
try {
|
|
115
|
+
const state = JSON.parse(await readFile(contextStatePath(taskFilePath), "utf8")) as {
|
|
116
|
+
contextTokens?: unknown;
|
|
117
|
+
model?: unknown;
|
|
118
|
+
};
|
|
119
|
+
contextTokens = state?.contextTokens;
|
|
120
|
+
model = state?.model;
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (!reachesThreshold(contextTokens, threshold)) return null;
|
|
125
|
+
try {
|
|
126
|
+
await writeFile(contextBoundNoteMarkerPath(taskFilePath), new Date().toISOString(), {
|
|
127
|
+
flag: "wx",
|
|
128
|
+
});
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return { contextTokens, model: typeof model === "string" && model ? model : undefined };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Claude PostToolUse path: claim from the task file, then hand the note to `inject`. */
|
|
136
|
+
export async function injectClaimedContextBoundNote(
|
|
137
|
+
taskFilePath: string | undefined,
|
|
138
|
+
threshold: number,
|
|
139
|
+
inject: (note: string) => unknown,
|
|
140
|
+
): Promise<void> {
|
|
141
|
+
const state = await claimContextBoundNote(taskFilePath, threshold).catch(() => null);
|
|
142
|
+
if (state) await deliverContextBoundNote(state.contextTokens, state.model, inject);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* In-process path. Returns an observer the session calls with each
|
|
147
|
+
* main-thread request's prompt size. The first size at or above `threshold`
|
|
148
|
+
* claims the note for this session (one session runs one task) and renders
|
|
149
|
+
* it; later sizes are ignored. The returned promise never rejects.
|
|
150
|
+
*
|
|
151
|
+
* Rendering is async (HTTP on workers), so the session may have moved on by
|
|
152
|
+
* the time the note is ready. `canDeliver` decides then: true injects it;
|
|
153
|
+
* false holds it, and each later call retries the held note instead of
|
|
154
|
+
* rendering a new one. A session that settles, aborts or ends its last turn
|
|
155
|
+
* first never gets it.
|
|
156
|
+
*/
|
|
157
|
+
export function createContextBoundNote(options: {
|
|
158
|
+
threshold: number;
|
|
159
|
+
/** The session's model, read when the note renders, for the window share. */
|
|
160
|
+
model?: () => string | undefined;
|
|
161
|
+
inject: (note: string) => unknown;
|
|
162
|
+
/** True while a note injected now reaches the model inside the current turn. */
|
|
163
|
+
canDeliver: () => boolean;
|
|
164
|
+
}): (contextTokens: number | undefined) => Promise<void> {
|
|
165
|
+
let state: "idle" | "rendering" | "held" | "done" = "idle";
|
|
166
|
+
let held = "";
|
|
167
|
+
const tryDeliver = async (note: string): Promise<void> => {
|
|
168
|
+
try {
|
|
169
|
+
if (!options.canDeliver()) {
|
|
170
|
+
held = note;
|
|
171
|
+
state = "held";
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
state = "done";
|
|
175
|
+
await options.inject(note);
|
|
176
|
+
} catch {
|
|
177
|
+
// Fail open.
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
return async (contextTokens) => {
|
|
181
|
+
if (state === "held") return tryDeliver(held);
|
|
182
|
+
if (state !== "idle" || !reachesThreshold(contextTokens, options.threshold)) return;
|
|
183
|
+
state = "rendering";
|
|
184
|
+
let note = "";
|
|
185
|
+
try {
|
|
186
|
+
note = await renderContextBoundNote(contextTokens, options.model?.());
|
|
187
|
+
} catch {
|
|
188
|
+
// Fail open.
|
|
189
|
+
}
|
|
190
|
+
if (!note) {
|
|
191
|
+
state = "done";
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
await tryDeliver(note);
|
|
195
|
+
};
|
|
196
|
+
}
|
package/src/hooks/hook.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { getMcpBaseUrl, MAX_PROFILE_FILE_LENGTH } from "../utils/constants";
|
|
|
24
24
|
import { isSessionSummarySkipped } from "../utils/env-flag";
|
|
25
25
|
import { summarizeSession as runSummarize } from "../utils/internal-ai";
|
|
26
26
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
27
|
+
import { contextBoundNoteThreshold, injectClaimedContextBoundNote } from "./context-bound-note";
|
|
27
28
|
import { guardGhPrBody } from "./pr-body-guard";
|
|
28
29
|
import { guardGhPrRoom } from "./pr-room-guard";
|
|
29
30
|
import { checkToolLoop, clearToolHistory } from "./tool-loop-detection";
|
|
@@ -78,6 +79,8 @@ interface HookMessage {
|
|
|
78
79
|
tool_input?: Record<string, unknown>;
|
|
79
80
|
tool_response?: Record<string, unknown>;
|
|
80
81
|
tool_use_id?: string;
|
|
82
|
+
/** Set only when the hook fires inside a subagent. */
|
|
83
|
+
agent_id?: string;
|
|
81
84
|
prompt?: string;
|
|
82
85
|
stop_hook_active?: boolean;
|
|
83
86
|
}
|
|
@@ -1339,6 +1342,15 @@ export async function handleHook(): Promise<void> {
|
|
|
1339
1342
|
`Task sent successfully.${maybeTaskId ? ` Task ID: ${maybeTaskId}.` : ""} Monitor progress using the get-task-details tool periodically.`,
|
|
1340
1343
|
);
|
|
1341
1344
|
}
|
|
1345
|
+
} else if (!msg.agent_id) {
|
|
1346
|
+
// One-time nudge once the session's context crosses the threshold.
|
|
1347
|
+
// Main thread only: a subagent call must not consume the marker.
|
|
1348
|
+
// Any failure means no note; it never blocks the tool call.
|
|
1349
|
+
await injectClaimedContextBoundNote(
|
|
1350
|
+
process.env.TASK_FILE,
|
|
1351
|
+
contextBoundNoteThreshold(),
|
|
1352
|
+
(note) => outputToolContext("PostToolUse", note),
|
|
1353
|
+
);
|
|
1342
1354
|
}
|
|
1343
1355
|
}
|
|
1344
1356
|
break;
|
package/src/http/config.ts
CHANGED
|
@@ -23,8 +23,9 @@ import {
|
|
|
23
23
|
isInternalConfigKey,
|
|
24
24
|
validateConfigEntry,
|
|
25
25
|
} from "../be/swarm-config-guard";
|
|
26
|
+
import { isScriptRunToken } from "../be/users";
|
|
26
27
|
import { can, isRbacEnabled, type PermissionVerb } from "../rbac";
|
|
27
|
-
import { SwarmConfigSchema } from "../types";
|
|
28
|
+
import { type SwarmConfig, SwarmConfigSchema } from "../types";
|
|
28
29
|
import { getRequestAuth, type HttpRequestAuth } from "../utils/request-auth-context";
|
|
29
30
|
import { registerVolatileSecret } from "../utils/secret-scrubber";
|
|
30
31
|
import { reloadGlobalConfigsAndIntegrations, scheduleIntegrationsReload } from "./core";
|
|
@@ -111,6 +112,49 @@ async function resolveSecretsRead(
|
|
|
111
112
|
};
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Ids of the rows a script run's token reads unmasked on its own
|
|
117
|
+
* `/api/config/resolved` (its agent over global, no repo secrets). The run
|
|
118
|
+
* already holds these values in `ctx.swarm.config`. `undefined` for any other
|
|
119
|
+
* caller.
|
|
120
|
+
*/
|
|
121
|
+
async function scriptRunHeldConfigIds(req: IncomingMessage): Promise<Set<string> | undefined> {
|
|
122
|
+
const auth = getRequestAuth(req);
|
|
123
|
+
if (auth?.kind !== "agent" || auth.tokenKind !== "agent") return undefined;
|
|
124
|
+
if (!(await isScriptRunToken(auth.tokenId))) return undefined;
|
|
125
|
+
const held = stripApiOnlyKeys(
|
|
126
|
+
await getResolvedConfig(auth.agentId, undefined, { includeRepoSecrets: false }),
|
|
127
|
+
);
|
|
128
|
+
return new Set(held.map((config) => config.id));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Mask a raw list or by-id read for this caller. A script run's token gets
|
|
133
|
+
* the rows it holds (see {@link scriptRunHeldConfigIds}) unmasked, so
|
|
134
|
+
* `config_list` and `ctx.swarm.config` agree; every other secret row stays
|
|
135
|
+
* masked. Unmasked secret values are registered with the scrubber.
|
|
136
|
+
*/
|
|
137
|
+
async function maskConfigRead(
|
|
138
|
+
req: IncomingMessage,
|
|
139
|
+
configs: SwarmConfig[],
|
|
140
|
+
includeSecrets: boolean,
|
|
141
|
+
): Promise<{ result: SwarmConfig[]; secretsNote: string }> {
|
|
142
|
+
const { effectiveIncludeSecrets, secretsNote } = await resolveSecretsRead(req, includeSecrets);
|
|
143
|
+
const held =
|
|
144
|
+
includeSecrets && !effectiveIncludeSecrets ? await scriptRunHeldConfigIds(req) : undefined;
|
|
145
|
+
const reveal = (config: SwarmConfig) => effectiveIncludeSecrets || !!held?.has(config.id);
|
|
146
|
+
const result = configs.map((config) =>
|
|
147
|
+
config.isSecret && !reveal(config) ? maskSecrets([config])[0]! : config,
|
|
148
|
+
);
|
|
149
|
+
for (const config of result) {
|
|
150
|
+
if (config.isSecret && config.value && reveal(config)) {
|
|
151
|
+
registerVolatileSecret(config.value, `config:${config.key}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const anyMasked = result.some((config) => config.isSecret && !reveal(config));
|
|
155
|
+
return { result, secretsNote: held && !anyMasked ? "" : secretsNote };
|
|
156
|
+
}
|
|
157
|
+
|
|
114
158
|
/**
|
|
115
159
|
* Gate a config write/delete over HTTP (DES-445 follow-up).
|
|
116
160
|
*
|
|
@@ -455,13 +499,9 @@ export async function handleConfig(
|
|
|
455
499
|
jsonError(res, "Config not found", 404);
|
|
456
500
|
return true;
|
|
457
501
|
}
|
|
458
|
-
const {
|
|
459
|
-
const singleResult = effectiveIncludeSecrets ? config : maskSecrets([config])[0]!;
|
|
460
|
-
if (effectiveIncludeSecrets && singleResult.isSecret && singleResult.value) {
|
|
461
|
-
registerVolatileSecret(singleResult.value, `config:${singleResult.key}`);
|
|
462
|
-
}
|
|
502
|
+
const { result, secretsNote } = await maskConfigRead(req, [config], includeSecrets);
|
|
463
503
|
getConfigById.respond(res, 200, {
|
|
464
|
-
...
|
|
504
|
+
...result[0]!,
|
|
465
505
|
...(secretsNote ? { message: `Found 1 config(s).${secretsNote}` } : {}),
|
|
466
506
|
});
|
|
467
507
|
return true;
|
|
@@ -471,7 +511,6 @@ export async function handleConfig(
|
|
|
471
511
|
const parsed = await listConfig.parse(req, res, pathSegments, queryParams);
|
|
472
512
|
if (!parsed) return true;
|
|
473
513
|
const includeSecrets = parsed.query.includeSecrets === "true";
|
|
474
|
-
const { effectiveIncludeSecrets, secretsNote } = await resolveSecretsRead(req, includeSecrets);
|
|
475
514
|
const configs = stripApiOnlyKeys(
|
|
476
515
|
await getSwarmConfigs({
|
|
477
516
|
scope: parsed.query.scope || undefined,
|
|
@@ -479,14 +518,7 @@ export async function handleConfig(
|
|
|
479
518
|
key: parsed.query.key,
|
|
480
519
|
}),
|
|
481
520
|
);
|
|
482
|
-
const listResult =
|
|
483
|
-
if (effectiveIncludeSecrets) {
|
|
484
|
-
for (const c of listResult) {
|
|
485
|
-
if (c.isSecret && c.value) {
|
|
486
|
-
registerVolatileSecret(c.value, `config:${c.key}`);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
521
|
+
const { result: listResult, secretsNote } = await maskConfigRead(req, configs, includeSecrets);
|
|
490
522
|
listConfig.respond(res, 200, {
|
|
491
523
|
configs: listResult,
|
|
492
524
|
...(secretsNote ? { message: `Found ${listResult.length} config(s).${secretsNote}` } : {}),
|
|
@@ -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
|
+
}
|