@desplega.ai/agent-swarm 2.6.0 → 2.7.0
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/README.md +2 -0
- package/dist/{acp-adapter-dgfakfpw.js → acp-adapter-ma1js5n7.js} +6 -6
- package/dist/{actions-fbqma8aa.js → actions-1vhmtvg9.js} +18 -18
- package/dist/{amp-adapter-en0jy6hs.js → amp-adapter-sxqqvmdk.js} +3 -3
- package/dist/{app-k8a5cgpp.js → app-mwx1d5a9.js} +3 -3
- package/dist/{assistant-a16433gr.js → assistant-yx1p6g82.js} +20 -20
- package/dist/{boot-reembed-f4nn561n.js → boot-reembed-5h37a2c2.js} +1 -1
- package/dist/{boot-reembed-6c1sjfnr.js → boot-reembed-7h78vmvv.js} +2 -1
- package/dist/{boot-scrub-sweep-d4w9gajf.js → boot-scrub-sweep-8mawd4gw.js} +3 -3
- package/dist/{boot-seeds-9bmkspyw.js → boot-seeds-ca84dhnn.js} +4 -4
- package/dist/{claude-adapter-nys1kjxa.js → claude-adapter-wqt4hfkq.js} +7 -7
- package/dist/{claude-managed-adapter-55207973.js → claude-managed-adapter-6fjqtr2z.js} +2 -2
- package/dist/{claude-managed-setup-3cm35gey.js → claude-managed-setup-rqv98sea.js} +2 -2
- package/dist/{claude-sdk-session-nw5yd8cp.js → claude-sdk-session-f29f3b2x.js} +7 -7
- package/dist/{cli-sxgtf5fw.js → cli-07q03xm2.js} +1 -1
- package/dist/{cli-k21gx5ht.js → cli-1hhbzkbb.js} +6 -6
- package/dist/{cli-v49fkghp.js → cli-1m9x8dr5.js} +1 -1
- package/dist/{cli-24j1jeeb.js → cli-2pvzmas1.js} +3 -3
- package/dist/{cli-w2656ka1.js → cli-2y7v133v.js} +7 -7
- package/dist/{cli-g8ka5j4j.js → cli-48gxjmcz.js} +94 -36
- package/dist/{cli-2sjhpsns.js → cli-54sj64fc.js} +1 -1
- package/dist/{cli-aj8djyqt.js → cli-60sty27p.js} +2 -2
- package/dist/{cli-4hxrf0p4.js → cli-6a9qfa8b.js} +308 -47
- package/dist/{cli-n7asmce6.js → cli-6mgdba42.js} +3 -3
- package/dist/{cli-gf95q3bm.js → cli-6srv653c.js} +3 -3
- package/dist/{cli-9abdgn5t.js → cli-86ehn4s5.js} +3 -3
- package/dist/{cli-bqztfyve.js → cli-8mbc9za9.js} +1 -1
- package/dist/{cli-a2rr0fmg.js → cli-8wmj18cz.js} +1 -1
- package/dist/{cli-c2habvsf.js → cli-9gsm2rzp.js} +8 -8
- package/dist/{cli-4rf0f5vg.js → cli-9pshx6rd.js} +4 -2
- package/dist/{cli-abkm71vr.js → cli-a6ds2epe.js} +1 -1
- package/dist/{cli-esakngh8.js → cli-a89zdqz9.js} +1 -1
- package/dist/{cli-3ndmpxxv.js → cli-ay40dnj3.js} +2 -2
- package/dist/{cli-kf4jree9.js → cli-aypd09nf.js} +1 -1
- package/dist/{cli-6y51ryva.js → cli-b3yzyc57.js} +1 -1
- package/dist/{cli-ag2yrnt5.js → cli-c668vvcf.js} +2 -2
- package/dist/{cli-5sckpqcx.js → cli-d20qvpr2.js} +1 -1
- package/dist/{cli-a34pr3w1.js → cli-e1595bgx.js} +2 -2
- package/dist/{cli-m70bajzr.js → cli-f12mr20z.js} +1 -1
- package/dist/{cli-dqygeeh5.js → cli-f1e0ptjj.js} +10 -1
- package/dist/{cli-mvjz5an3.js → cli-f4dbh7a5.js} +7 -7
- package/dist/{cli-az25fn5f.js → cli-gsfk58nw.js} +2 -2
- package/dist/{cli-1tem6ns9.js → cli-gt4h8cxw.js} +3 -2
- package/dist/{cli-cz37jqss.js → cli-h5jeg46p.js} +2 -2
- package/dist/{cli-emhjcj2y.js → cli-he9gbdx0.js} +60 -10
- package/dist/{cli-3fwqy71j.js → cli-j0ppywdr.js} +38 -31
- package/dist/{cli-5rj83bam.js → cli-j3y0x2n4.js} +34 -33
- package/dist/{cli-3gfvmzht.js → cli-j9bpmykh.js} +73 -25
- package/dist/{cli-m5a9m0ak.js → cli-kv7sbc2c.js} +3 -3
- package/dist/{cli-g20jjwt4.js → cli-m30k9777.js} +12 -12
- package/dist/{cli-683y0b3a.js → cli-m5e802r7.js} +2 -2
- package/dist/{cli-nn6vx2mq.js → cli-ptfq202x.js} +5 -3
- package/dist/{cli-d2dpf95n.js → cli-qj442fws.js} +1 -1
- package/dist/{cli-03gg9pd1.js → cli-rd5m70gj.js} +4 -4
- package/dist/{cli-gnsb1zxa.js → cli-s09j9vp5.js} +1 -1
- package/dist/{cli-zqne1qcj.js → cli-s0mgx214.js} +2 -2
- package/dist/{cli-sqma6vw1.js → cli-sgqm2582.js} +5 -5
- package/dist/{cli-nq677csy.js → cli-sjctjr48.js} +1 -1
- package/dist/{cli-3aqd4jaa.js → cli-twctnkng.js} +3 -3
- package/dist/{cli-j6rktc91.js → cli-v6shg7na.js} +1 -1
- package/dist/{cli-7enjwya9.js → cli-vz78s5wm.js} +1 -1
- package/dist/{cli-art7c1zh.js → cli-wqr7cea4.js} +4 -4
- package/dist/{cli-hfeb68z4.js → cli-x37r7qwf.js} +11 -3
- package/dist/{cli-ffvsj2qw.js → cli-yd2nreqw.js} +2 -2
- package/dist/{cli-ca6banwz.js → cli-ymjpkxgn.js} +6 -6
- package/dist/{cli-mtnj4fwa.js → cli-yxbsmx56.js} +5 -5
- package/dist/cli.js +18 -16
- package/dist/{codex-adapter-d95q4qsn.js → codex-adapter-zk59x2zh.js} +5 -5
- package/dist/{codex-hook-brpnycn5.js → codex-hook-xyjhvxwd.js} +1 -1
- package/dist/{codex-session-runner-gdwd8h0c.js → codex-session-runner-kh6s4j3n.js} +5 -5
- package/dist/{commands-jqmgk54b.js → commands-68et1qv2.js} +2 -2
- package/dist/{controller-z3jq589f.js → controller-fyrbnzg4.js} +2 -2
- package/dist/{controllers-connect-r2za39ef.js → controllers-connect-nep9h2xv.js} +19 -19
- package/dist/{cursor-adapter-4mr4jgkw.js → cursor-adapter-yss3pg7t.js} +1 -1
- package/dist/{db-w36wwb5g.js → db-28gjyvxj.js} +2 -2
- package/dist/{db-ppv2tze7.js → db-8ftwvzba.js} +3 -3
- package/dist/{dispatcher-pv5h77vf.js → dispatcher-hayexmkr.js} +17 -17
- package/dist/{dsh-adapter-d4pdpb3r.js → dsh-adapter-46wawff9.js} +1 -1
- package/dist/{easter-egg-3d-4sp4dvmc.js → easter-egg-3d-8d1e0h4m.js} +1 -1
- package/dist/{execute-jev839y1.js → execute-cjrfxfnm.js} +1 -1
- package/dist/{external-channel-y6nkzhnn.js → external-channel-xbmxcg09.js} +2 -2
- package/dist/{grok-adapter-t08b9ntm.js → grok-adapter-ag4wks2a.js} +7 -7
- package/dist/handlers-tzmevqb5.js +41 -0
- package/dist/{heartbeat-vn31xwdk.js → heartbeat-psfvcgwp.js} +20 -20
- package/dist/{hook-6d0zbvvt.js → hook-8pz04697.js} +5 -5
- package/dist/{hook-cpjs8q51.js → hook-cyq4mryn.js} +6 -6
- package/dist/{http-9c6medda.js → http-haj1nd6k.js} +109 -114
- package/dist/{index-qhjf2htm.js → index-jwt2bj6f.js} +261 -96
- package/dist/{index-g6k9tctj.js → index-q52f47pe.js} +1 -1
- package/dist/{index-pkma1yf8.js → index-syvhnvtw.js} +1 -1
- package/dist/{index-yqgbfgaj.js → index-y1ajx04a.js} +20 -20
- package/dist/{index-k0yt1tvq.js → index-z5h22pes.js} +17 -17
- package/dist/{index-38t35h0n.js → index-zyq8ct52.js} +17 -17
- package/dist/{keepalive-em0117bp.js → keepalive-xdvsamvj.js} +3 -3
- package/dist/{lead-23e7z59t.js → lead-hqg57kdp.js} +35 -35
- package/dist/{maintenance-kbme3qy9.js → maintenance-4y2xzh3a.js} +1 -1
- package/dist/{oauth-refresh-sweep-xysj4yse.js → oauth-refresh-sweep-jd4r7zbs.js} +8 -3
- package/dist/{onboard-p6q0vz97.js → onboard-18447q9q.js} +5 -5
- package/dist/{opencode-adapter-esv3k9yn.js → opencode-adapter-26b2frdf.js} +5 -5
- package/dist/{otel-impl-54qhqh1z.js → otel-impl-f9tgr8yw.js} +1 -1
- package/dist/{pi-mono-adapter-mymbwhj0.js → pi-mono-adapter-qn6qbga1.js} +33 -12
- package/dist/{pricing-refresh-yb75wv33.js → pricing-refresh-dw2tgb53.js} +3 -3
- package/dist/{rbac-roles-1vp2mw4p.js → rbac-roles-aygm2760.js} +2 -2
- package/dist/{rbac-roles-dexhc9x8.js → rbac-roles-s4nwj2m3.js} +1 -1
- package/dist/{render-v2-tjay5hhf.js → render-v2-yfvdfr0x.js} +3 -3
- package/dist/{resume-8ayf37g5.js → resume-1bmvjbf3.js} +7 -7
- package/dist/{runtime-hqe5cqcd.js → runtime-f7tpzedy.js} +1 -1
- package/dist/{session-access-fdhp987s.js → session-access-r5garej6.js} +4 -4
- package/dist/{session-source-backfill-qgtc86z1.js → session-source-backfill-424wzv0r.js} +2 -2
- package/dist/{setup-4rrmqcq3.js → setup-8tygvxkq.js} +1 -1
- package/dist/{virtual-modules-52km00d6.js → virtual-modules-xsw323b3.js} +2 -2
- package/dist/{work-objects-p28fgd70.js → work-objects-ggnghpgf.js} +20 -20
- package/dist/{worker-e3w7vye1.js → worker-sqjd1nqf.js} +35 -35
- package/openapi.json +69 -1
- package/package.json +5 -3
- package/src/be/automation-preflight.ts +2 -0
- package/src/be/db/api-keys.ts +9 -0
- package/src/be/db/workflow-runs.ts +36 -5
- package/src/be/db.ts +80 -22
- package/src/be/memory/task-learning.ts +155 -0
- package/src/be/migrations/245_script_executor.sql +3 -0
- package/src/be/oauth-refresh-sweep.ts +2 -1
- package/src/be/scripts/boot-reembed.ts +2 -0
- package/src/be/scripts/db.ts +14 -4
- package/src/be/scripts/embeddings.ts +2 -0
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +9 -3
- package/src/be/seed-skills/index.ts +7 -0
- package/src/commands/onboard/compose-generator.ts +5 -3
- package/src/commands/provider-credentials.ts +2 -1
- package/src/heartbeat/heartbeat.ts +14 -1
- package/src/heartbeat/script-runner-probe.ts +47 -0
- package/src/http/api-keys.ts +24 -9
- package/src/http/idempotency-key.ts +1 -1
- package/src/http/index.ts +3 -3
- package/src/http/pages-public.ts +38 -5
- package/src/http/scripts.ts +27 -0
- package/src/http/workflows.ts +10 -0
- package/src/http/x.ts +5 -0
- package/src/prompts/base-prompt.ts +21 -1
- package/src/prompts/session-templates.ts +17 -3
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +41 -11
- package/src/scheduler/scheduler.ts +2 -1
- package/src/scripts-runtime/eval-harness.ts +8 -1
- package/src/scripts-runtime/executors/native.ts +22 -16
- package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
- package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
- package/src/scripts-runtime/executors/quickjs.ts +7 -0
- package/src/scripts-runtime/executors/registry.ts +32 -0
- package/src/scripts-runtime/executors/types.ts +7 -0
- package/src/scripts-runtime/host-validated-args.ts +133 -0
- package/src/scripts-runtime/loader.ts +30 -3
- package/src/scripts-runtime/swarm-sdk.ts +9 -2
- package/src/scripts-runtime/types/stdlib.d.ts +9 -1
- package/src/scripts-runtime/types/swarm-sdk.d.ts +9 -1
- package/src/tests/api-key-status-by-suffix.test.ts +242 -0
- package/src/tests/base-prompt.test.ts +70 -2
- package/src/tests/community-linear-delivery-loop.test.ts +2099 -0
- package/src/tests/credential-check.test.ts +4 -2
- package/src/tests/docs-index.test.ts +135 -0
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/fetch-resolved-env.test.ts +16 -2
- package/src/tests/heartbeat-checklist.test.ts +89 -0
- package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
- package/src/tests/oauth-refresh-sweep.test.ts +34 -1
- package/src/tests/onboard-compose.test.ts +20 -0
- package/src/tests/opencode-adapter.test.ts +2 -1
- package/src/tests/pages-frame-ancestors.test.ts +162 -0
- package/src/tests/pi-codemode.test.ts +2 -1
- package/src/tests/pi-mono-adapter.test.ts +54 -0
- package/src/tests/pi-native-mcp.test.ts +13 -1
- package/src/tests/pi-tool-exposure.test.ts +8 -8
- package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
- package/src/tests/prompt-template-session.test.ts +4 -3
- package/src/tests/request-composition-derive.test.ts +329 -0
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/script-executor-conformance.test.ts +36 -1
- package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
- package/src/tests/script-executor-quickjs.test.ts +140 -0
- package/src/tests/script-host-validated-args-native.test.ts +196 -0
- package/src/tests/script-per-script-executor.test.ts +415 -0
- package/src/tests/scripts-external-api.test.ts +45 -0
- package/src/tests/sdk-allowlist.test.ts +3 -2
- package/src/tests/self-improvement.test.ts +4 -4
- package/src/tests/store-progress-learning.test.ts +288 -0
- package/src/tests/ui-logs-parser.test.ts +180 -0
- package/src/tests/workflow-engine-v2.test.ts +116 -1
- package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
- package/src/tests/workflow-run-finished-event.test.ts +263 -0
- package/src/tests/workflow-swarm-script.test.ts +34 -0
- package/src/tools/script-common.ts +1 -0
- package/src/tools/script-run.ts +7 -0
- package/src/tools/script-upsert.ts +7 -0
- package/src/tools/store-progress.ts +67 -1
- package/src/tools/workflows/trigger-workflow.ts +104 -10
- package/src/types.ts +12 -1
- package/src/utils/credentials.ts +5 -2
- package/src/workflows/engine.ts +18 -16
- package/src/workflows/executors/swarm-script.ts +5 -0
- package/src/workflows/foreach-driver.ts +5 -11
- package/src/workflows/node-timeout.ts +30 -0
- package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
- package/templates/community/linear-delivery-loop/README.md +38 -0
- package/templates/community/linear-delivery-loop/config.example.json +31 -0
- package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
- package/templates/community/linear-delivery-loop/schedule.json +10 -0
- package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
- package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
- package/templates/skills/swarm-docs/config.json +14 -0
- package/templates/skills/swarm-docs/content.md +144 -0
- package/dist/handlers-eqsnmv95.js +0 -41
|
@@ -13,6 +13,11 @@ import {
|
|
|
13
13
|
updateAgentStatusFromCapacity,
|
|
14
14
|
updateTaskProgress,
|
|
15
15
|
} from "@/be/db";
|
|
16
|
+
import {
|
|
17
|
+
LEARNING_MAX_TAGS,
|
|
18
|
+
storeTaskLearning,
|
|
19
|
+
type TaskLearningResult,
|
|
20
|
+
} from "@/be/memory/task-learning";
|
|
16
21
|
import {
|
|
17
22
|
CitationInputSchema,
|
|
18
23
|
getTaskCitations,
|
|
@@ -114,8 +119,26 @@ export const storeProgressOutputSchema = swarmToolOutputSchema({
|
|
|
114
119
|
.describe(
|
|
115
120
|
"Present only when this progress text reads as blocked-waiting: milliseconds since the task's prior update. Drives the store-progress nudge toward defer-task.",
|
|
116
121
|
),
|
|
122
|
+
learning: z
|
|
123
|
+
.looseObject({
|
|
124
|
+
status: z.string().optional(),
|
|
125
|
+
key: z.string().optional(),
|
|
126
|
+
memoryId: z.string().optional(),
|
|
127
|
+
workstream: z.string().optional(),
|
|
128
|
+
error: z.string().optional(),
|
|
129
|
+
})
|
|
130
|
+
.optional()
|
|
131
|
+
.describe(
|
|
132
|
+
"Present when the call carried a learning: stored, replayed (same text already stored for this task), or failed with error.",
|
|
133
|
+
),
|
|
117
134
|
});
|
|
118
135
|
|
|
136
|
+
function learningSentence(result: TaskLearningResult): string {
|
|
137
|
+
if (result.status === "stored") return `Learning stored at ${result.key}.`;
|
|
138
|
+
if (result.status === "replayed") return `Learning already stored at ${result.key}; skipped.`;
|
|
139
|
+
return `Learning not stored: ${result.error}.`;
|
|
140
|
+
}
|
|
141
|
+
|
|
119
142
|
export const registerStoreProgressTool = (server: McpServer) => {
|
|
120
143
|
createToolRegistrar(server)(
|
|
121
144
|
"store-progress",
|
|
@@ -165,6 +188,19 @@ export const registerStoreProgressTool = (server: McpServer) => {
|
|
|
165
188
|
.describe(
|
|
166
189
|
'Claim sources, upserted by index across calls. Use only for factual claims the reader cannot already see in the thread or task tree, 1-2 sources; none on delegation, routing, acks, or status replies. Reference each one in output with [citation:N], or set general: true for a source that backs the whole answer (it renders under "General sources"). ref per kind: task = task UUID; memory = memory UUID (optional quote must appear verbatim in it); github = owner/repo#N, owner/repo@<sha>, or a github.com pull, issues, or commit URL; slack = permalink or channel/ts; agent-fs = file path; page = page id; script-run = script run id; url = http(s) URL. The first completing call is refused, and the task stays in progress, if a marker has no entry, a citation fails validation, or a non-general citation is unreferenced; the response lists each problem. After one refusal, completion proceeds and those markers and sources are dropped from rendered output. At most 50 citations per call/task, refs up to 2048 characters, labels up to 200. Invalid or oversized batches are ignored; existing indices can still be updated at capacity.',
|
|
167
190
|
),
|
|
191
|
+
learning: z
|
|
192
|
+
.union([
|
|
193
|
+
z.string(),
|
|
194
|
+
z.object({
|
|
195
|
+
text: z.string(),
|
|
196
|
+
workstream: z.string().optional(),
|
|
197
|
+
tags: z.array(z.string()).optional(),
|
|
198
|
+
}),
|
|
199
|
+
])
|
|
200
|
+
.optional()
|
|
201
|
+
.describe(
|
|
202
|
+
`A lesson to keep. Add one only when it would change how the next agent works: a gotcha, a dead end, a pattern that worked. Not a progress line. Stored as a swarm memory at /learnings/<workstream>/<slug>, tagged "learning" and tied to this task. The first line of text is the title. At most ~500 tokens and ${LEARNING_MAX_TAGS} kebab tags. workstream defaults to the task's repo name, else "general". Send it on or before the completing call: a call on a finished task stores nothing. A failed learning write is reported in the result and never blocks the progress update.`,
|
|
203
|
+
),
|
|
168
204
|
persistMemory: z
|
|
169
205
|
.boolean()
|
|
170
206
|
.optional()
|
|
@@ -193,6 +229,7 @@ export const registerStoreProgressTool = (server: McpServer) => {
|
|
|
193
229
|
failureReason,
|
|
194
230
|
attachments,
|
|
195
231
|
citations,
|
|
232
|
+
learning,
|
|
196
233
|
persistMemory,
|
|
197
234
|
force,
|
|
198
235
|
},
|
|
@@ -332,6 +369,7 @@ export const registerStoreProgressTool = (server: McpServer) => {
|
|
|
332
369
|
}
|
|
333
370
|
}
|
|
334
371
|
|
|
372
|
+
let learningWrite: Promise<TaskLearningResult> | undefined;
|
|
335
373
|
const result = await getDbClient().transaction(async () => {
|
|
336
374
|
const agent = await getAgentById(requestInfo.agentId ?? "");
|
|
337
375
|
|
|
@@ -435,6 +473,29 @@ export const registerStoreProgressTool = (server: McpServer) => {
|
|
|
435
473
|
await upsertTaskCitations(taskId, citations);
|
|
436
474
|
}
|
|
437
475
|
|
|
476
|
+
// A finished task takes no new writes, learnings included.
|
|
477
|
+
if (learning && isTerminal) {
|
|
478
|
+
learningWrite = Promise.resolve({
|
|
479
|
+
status: "failed",
|
|
480
|
+
error: "the task is already finished; use memory-store",
|
|
481
|
+
});
|
|
482
|
+
} else if (learning) {
|
|
483
|
+
// Post-commit, so a rolled-back progress write never leaves a learning
|
|
484
|
+
// behind, and a failed learning write never rolls back the progress.
|
|
485
|
+
learningWrite = new Promise((resolve) => {
|
|
486
|
+
getDbClient().afterCommit(() =>
|
|
487
|
+
storeTaskLearning({
|
|
488
|
+
agentId: agent.id,
|
|
489
|
+
isLead: agent.isLead === true,
|
|
490
|
+
task: existingTask,
|
|
491
|
+
learning,
|
|
492
|
+
}).then(resolve, (err) =>
|
|
493
|
+
resolve({ status: "failed", error: scrubSecrets(String(err)) }),
|
|
494
|
+
),
|
|
495
|
+
);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
438
499
|
// Idempotency guard: short-circuit terminal-status writes (completed/failed)
|
|
439
500
|
// BEFORE any side-effects fire (event emission, memory write, follow-up task,
|
|
440
501
|
// business-use ensure). Without this, a multi-session race causes duplicate
|
|
@@ -627,7 +688,11 @@ export const registerStoreProgressTool = (server: McpServer) => {
|
|
|
627
688
|
});
|
|
628
689
|
}
|
|
629
690
|
|
|
630
|
-
const
|
|
691
|
+
const learningResult = await learningWrite;
|
|
692
|
+
const { success } = result;
|
|
693
|
+
const message = learningResult
|
|
694
|
+
? `${result.message} ${learningSentence(learningResult)}`
|
|
695
|
+
: result.message;
|
|
631
696
|
const task = result.task
|
|
632
697
|
? {
|
|
633
698
|
id: result.task.id,
|
|
@@ -646,6 +711,7 @@ export const registerStoreProgressTool = (server: McpServer) => {
|
|
|
646
711
|
typeof result.blockedWaitingElapsedMs === "number"
|
|
647
712
|
? { blockedWaitingElapsedMs: result.blockedWaitingElapsedMs }
|
|
648
713
|
: {}),
|
|
714
|
+
...(learningResult ? { learning: learningResult } : {}),
|
|
649
715
|
};
|
|
650
716
|
const warnings =
|
|
651
717
|
success && status === "completed"
|
|
@@ -2,10 +2,42 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { resolveTaskAuditUserId } from "@/be/audit-user";
|
|
4
4
|
import { getWorkflow, getWorkflowRun } from "@/be/db";
|
|
5
|
+
import {
|
|
6
|
+
findAcceptedWorkflowOperation,
|
|
7
|
+
IdempotencyKeyTakenError,
|
|
8
|
+
recordAcceptedWorkflowOperation,
|
|
9
|
+
type WorkflowIdempotencyClaim,
|
|
10
|
+
workflowRequestFingerprint,
|
|
11
|
+
} from "@/be/db/workflow-idempotency";
|
|
12
|
+
import { IDEMPOTENCY_KEY_PATTERN } from "@/http/idempotency-key";
|
|
5
13
|
import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
|
|
6
14
|
import { getExecutorRegistry, startWorkflowExecution } from "@/workflows";
|
|
7
15
|
import { TriggerSchemaError } from "@/workflows/engine";
|
|
8
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Answer from the run already accepted under this key, or null when there is
|
|
19
|
+
* none. Same record and rules as the HTTP trigger's `Idempotency-Key`.
|
|
20
|
+
*/
|
|
21
|
+
async function replayAcceptedTrigger(claim: WorkflowIdempotencyClaim) {
|
|
22
|
+
const accepted = await findAcceptedWorkflowOperation(claim);
|
|
23
|
+
if (!accepted) return null;
|
|
24
|
+
if (accepted.fingerprint !== claim.fingerprint) {
|
|
25
|
+
return toolErr("idempotencyKey was already used to trigger this workflow with other data.", {
|
|
26
|
+
data: { idempotencyConflict: true },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (accepted.resourceDeletedAt !== null) {
|
|
30
|
+
return toolErr(
|
|
31
|
+
"The workflow this idempotencyKey triggered was deleted; send a new key to start over.",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const { runId, skipped } = accepted.body as { runId: string; skipped: boolean };
|
|
35
|
+
return toolOk("Run already accepted under this idempotencyKey; nothing new was started.", {
|
|
36
|
+
details: `Replayed the run accepted under this idempotencyKey — run ID: ${runId}.`,
|
|
37
|
+
data: { runId, skipped, replayed: true },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
9
41
|
export const registerTriggerWorkflowTool = (server: McpServer) => {
|
|
10
42
|
createToolRegistrar(server)(
|
|
11
43
|
"trigger-workflow",
|
|
@@ -21,16 +53,55 @@ export const registerTriggerWorkflowTool = (server: McpServer) => {
|
|
|
21
53
|
.record(z.string(), z.unknown())
|
|
22
54
|
.optional()
|
|
23
55
|
.describe("Optional data to pass as trigger context to the workflow"),
|
|
56
|
+
wait: z
|
|
57
|
+
.boolean()
|
|
58
|
+
.optional()
|
|
59
|
+
.describe(
|
|
60
|
+
"Default true: return after the run's instant steps ran, when it finishes or parks at its first async step. " +
|
|
61
|
+
"false: return as soon as the run is created and walk it in the background. Use false from a script or step that only needs the runId.",
|
|
62
|
+
),
|
|
63
|
+
idempotencyKey: z
|
|
64
|
+
.string()
|
|
65
|
+
.regex(IDEMPOTENCY_KEY_PATTERN)
|
|
66
|
+
.optional()
|
|
67
|
+
.describe(
|
|
68
|
+
"Optional, 1-255 visible ASCII characters, scoped to the calling agent and this workflow. " +
|
|
69
|
+
"The first trigger accepted under a key starts one run; the key is recorded in the run's own transaction and never expires. " +
|
|
70
|
+
"A retry with the same key and triggerData starts nothing and returns that run (replayed: true), even if the earlier call was killed before it saw the answer. " +
|
|
71
|
+
"The same key with other triggerData is an error with idempotencyConflict: true.",
|
|
72
|
+
),
|
|
24
73
|
}),
|
|
25
74
|
outputSchema: swarmToolOutputSchema({
|
|
26
75
|
runId: z.string().optional(),
|
|
27
76
|
skipped: z.boolean().optional(),
|
|
77
|
+
replayed: z.boolean().optional(),
|
|
78
|
+
idempotencyConflict: z.boolean().optional(),
|
|
28
79
|
validationErrors: z.array(z.string()).optional(),
|
|
29
80
|
triggerSchema: z.record(z.string(), z.unknown()).optional(),
|
|
30
81
|
}),
|
|
31
82
|
},
|
|
32
|
-
async ({ id, triggerData }, requestInfo) => {
|
|
83
|
+
async ({ id, triggerData, wait, idempotencyKey }, requestInfo) => {
|
|
33
84
|
try {
|
|
85
|
+
if (idempotencyKey !== undefined && !requestInfo.agentId) {
|
|
86
|
+
return toolErr("idempotencyKey needs an identified caller (X-Agent-ID).");
|
|
87
|
+
}
|
|
88
|
+
const claim: WorkflowIdempotencyClaim | undefined =
|
|
89
|
+
idempotencyKey === undefined
|
|
90
|
+
? undefined
|
|
91
|
+
: {
|
|
92
|
+
principal: `agent:${requestInfo.agentId}`,
|
|
93
|
+
key: idempotencyKey,
|
|
94
|
+
operation: "workflow.trigger",
|
|
95
|
+
targetId: id,
|
|
96
|
+
fingerprint: workflowRequestFingerprint("workflow.trigger", triggerData ?? {}),
|
|
97
|
+
};
|
|
98
|
+
if (claim) {
|
|
99
|
+
// Before the enabled, cooldown and trigger-schema checks: an
|
|
100
|
+
// accepted trigger replays even after the workflow changed.
|
|
101
|
+
const replayed = await replayAcceptedTrigger(claim);
|
|
102
|
+
if (replayed) return replayed;
|
|
103
|
+
}
|
|
104
|
+
|
|
34
105
|
const workflow = await getWorkflow(id);
|
|
35
106
|
if (!workflow) {
|
|
36
107
|
return toolErr(`Workflow not found: ${id}`);
|
|
@@ -38,21 +109,44 @@ export const registerTriggerWorkflowTool = (server: McpServer) => {
|
|
|
38
109
|
if (!workflow.enabled) {
|
|
39
110
|
return toolErr(`Workflow "${workflow.name}" is disabled.`);
|
|
40
111
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
getExecutorRegistry(),
|
|
45
|
-
{
|
|
112
|
+
let accepted: { runId: string; skipped: boolean } | undefined;
|
|
113
|
+
let runId: string;
|
|
114
|
+
try {
|
|
115
|
+
runId = await startWorkflowExecution(workflow, triggerData ?? {}, getExecutorRegistry(), {
|
|
46
116
|
triggerType: "manual",
|
|
47
117
|
requestedByUserId:
|
|
48
118
|
(await resolveTaskAuditUserId(requestInfo.sourceTaskId, requestInfo.agentId)) ??
|
|
49
119
|
undefined,
|
|
50
|
-
|
|
51
|
-
|
|
120
|
+
detach: wait === false,
|
|
121
|
+
// Recorded in the run's own transaction: the key exists if and
|
|
122
|
+
// only if the run committed.
|
|
123
|
+
accept: claim
|
|
124
|
+
? async (run) => {
|
|
125
|
+
const response = { runId: run.runId, skipped: run.skipped };
|
|
126
|
+
await recordAcceptedWorkflowOperation(claim, {
|
|
127
|
+
workflowId: workflow.id,
|
|
128
|
+
runId: run.runId,
|
|
129
|
+
status: 201,
|
|
130
|
+
body: response,
|
|
131
|
+
});
|
|
132
|
+
accepted = response;
|
|
133
|
+
}
|
|
134
|
+
: undefined,
|
|
135
|
+
});
|
|
136
|
+
} catch (err) {
|
|
137
|
+
// A concurrent call with this key committed its run first; this one
|
|
138
|
+
// created none.
|
|
139
|
+
if (claim && err instanceof IdempotencyKeyTakenError) {
|
|
140
|
+
const replayed = await replayAcceptedTrigger(claim);
|
|
141
|
+
if (replayed) return replayed;
|
|
142
|
+
}
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
52
145
|
|
|
53
146
|
// Check if the run was skipped due to cooldown
|
|
54
|
-
const
|
|
55
|
-
|
|
147
|
+
const skipped = accepted
|
|
148
|
+
? accepted.skipped
|
|
149
|
+
: (await getWorkflowRun(runId))?.status === "skipped";
|
|
56
150
|
|
|
57
151
|
if (skipped) {
|
|
58
152
|
return toolOk(`Workflow "${workflow.name}" skipped (cooldown).`, {
|
package/src/types.ts
CHANGED
|
@@ -2282,7 +2282,14 @@ export const ExecutorMetaSchema = z
|
|
|
2282
2282
|
inputCtx: z.record(z.string(), z.unknown()).optional(),
|
|
2283
2283
|
})
|
|
2284
2284
|
.openapi("ExecutorMeta");
|
|
2285
|
-
export type ExecutorMeta = z.infer<typeof ExecutorMetaSchema
|
|
2285
|
+
export type ExecutorMeta = z.infer<typeof ExecutorMetaSchema> & {
|
|
2286
|
+
/**
|
|
2287
|
+
* Aborted when the step's watchdog budget runs out (`startStepDeadline`).
|
|
2288
|
+
* Runtime-only, never serialized. An executor with a long or one-shot side
|
|
2289
|
+
* effect should forward it so a timed-out step stops instead of finishing.
|
|
2290
|
+
*/
|
|
2291
|
+
signal?: AbortSignal;
|
|
2292
|
+
};
|
|
2286
2293
|
|
|
2287
2294
|
// --- Validation ---
|
|
2288
2295
|
|
|
@@ -3203,6 +3210,9 @@ export type ScriptScope = z.infer<typeof ScriptScopeSchema>;
|
|
|
3203
3210
|
export const ScriptFsModeSchema = z.enum(["none", "workspace-rw"]);
|
|
3204
3211
|
export type ScriptFsMode = z.infer<typeof ScriptFsModeSchema>;
|
|
3205
3212
|
|
|
3213
|
+
export const ScriptExecutorNameSchema = z.enum(["native", "quickjs"]);
|
|
3214
|
+
export type ScriptExecutorName = z.infer<typeof ScriptExecutorNameSchema>;
|
|
3215
|
+
|
|
3206
3216
|
export const ScriptRecordSchema = z
|
|
3207
3217
|
.object({
|
|
3208
3218
|
id: z.string(),
|
|
@@ -3219,6 +3229,7 @@ export const ScriptRecordSchema = z
|
|
|
3219
3229
|
isScratch: z.boolean(),
|
|
3220
3230
|
typeChecked: z.boolean(),
|
|
3221
3231
|
fsMode: ScriptFsModeSchema,
|
|
3232
|
+
executor: ScriptExecutorNameSchema.nullable().default(null),
|
|
3222
3233
|
createdByAgentId: z.string().nullable(),
|
|
3223
3234
|
createdAt: z.string(),
|
|
3224
3235
|
updatedAt: z.string(),
|
package/src/utils/credentials.ts
CHANGED
|
@@ -406,10 +406,13 @@ async function fetchAvailableIndices(
|
|
|
406
406
|
for (const envVar of poolVars) {
|
|
407
407
|
const val = env[envVar];
|
|
408
408
|
if (val) {
|
|
409
|
-
|
|
409
|
+
// Status is matched by key suffix, so a key at the same index in another
|
|
410
|
+
// agent's or repo's pool never blocks one of ours (issue #2425).
|
|
411
|
+
const keySuffixes = splitCredentialPool(val).map((key) => key.slice(-5));
|
|
412
|
+
const totalKeys = keySuffixes.length;
|
|
410
413
|
try {
|
|
411
414
|
const resp = await fetch(
|
|
412
|
-
`${apiUrl}/api/keys/available?keyType=${encodeURIComponent(envVar)}&totalKeys=${totalKeys}${modelParam}`,
|
|
415
|
+
`${apiUrl}/api/keys/available?keyType=${encodeURIComponent(envVar)}&totalKeys=${totalKeys}${modelParam}&keySuffixes=${encodeURIComponent(keySuffixes.join(","))}`,
|
|
413
416
|
{ headers: { Authorization: `Bearer ${apiKey}` } },
|
|
414
417
|
);
|
|
415
418
|
if (resp.ok) {
|
package/src/workflows/engine.ts
CHANGED
|
@@ -51,7 +51,7 @@ import { FOREACH_TERMINAL_STEP_STATUSES, resolveForeachParent } from "./foreach-
|
|
|
51
51
|
import { getSecretInputKeys, redactSecretsForStorage, resolveInputs } from "./input";
|
|
52
52
|
import { validateJsonSchema } from "./json-schema-validator";
|
|
53
53
|
import { getMaxWorkflowStepsPerRun } from "./limits";
|
|
54
|
-
import { resolveNodeTimeoutMs } from "./node-timeout";
|
|
54
|
+
import { resolveNodeTimeoutMs, startStepDeadline } from "./node-timeout";
|
|
55
55
|
import { findWorkflowReadinessProblems, formatReadinessRunError } from "./readiness";
|
|
56
56
|
import { deepInterpolate } from "./template";
|
|
57
57
|
import { runStepValidation, type ValidationRunResult } from "./validation";
|
|
@@ -78,6 +78,13 @@ export interface WorkflowExecutionOptions {
|
|
|
78
78
|
* HTTP trigger records its `Idempotency-Key` here.
|
|
79
79
|
*/
|
|
80
80
|
accept?: (run: { runId: string; skipped: boolean }) => Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* Return once the run is committed and walk it in the background, as a
|
|
83
|
+
* child run does. By default a top-level start resolves only after the
|
|
84
|
+
* walk reaches its end or its first async step, so a caller that only
|
|
85
|
+
* needs the run id waits for every instant step before it.
|
|
86
|
+
*/
|
|
87
|
+
detach?: boolean;
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
/**
|
|
@@ -209,9 +216,12 @@ export async function startWorkflowExecution(
|
|
|
209
216
|
options,
|
|
210
217
|
);
|
|
211
218
|
// A child run walks on its own, outside the parent executor's timeout. Its
|
|
212
|
-
// terminal status wakes the parent step (`resumeFromChildRun`).
|
|
213
|
-
|
|
214
|
-
|
|
219
|
+
// terminal status wakes the parent step (`resumeFromChildRun`). A detached
|
|
220
|
+
// top-level run walks the same way; the walk holds the run before its
|
|
221
|
+
// first await, so releasing the start hold below leaves no gap.
|
|
222
|
+
if (options.parentStepId || options.detach) {
|
|
223
|
+
const kind = options.parentStepId ? "Child" : "Detached";
|
|
224
|
+
walk.catch((err) => console.error(`[workflows] ${kind} workflow run ${runId} failed:`, err));
|
|
215
225
|
return runId;
|
|
216
226
|
}
|
|
217
227
|
await walk;
|
|
@@ -1045,7 +1055,7 @@ async function runClaimedStep(
|
|
|
1045
1055
|
inputCtx: interpolationCtx,
|
|
1046
1056
|
};
|
|
1047
1057
|
|
|
1048
|
-
const
|
|
1058
|
+
const deadline = startStepDeadline(resolveNodeTimeoutMs(node));
|
|
1049
1059
|
|
|
1050
1060
|
let result: Awaited<ReturnType<typeof executor.run>>;
|
|
1051
1061
|
try {
|
|
@@ -1053,10 +1063,10 @@ async function runClaimedStep(
|
|
|
1053
1063
|
executor.run({
|
|
1054
1064
|
config: interpolatedConfig,
|
|
1055
1065
|
context: executionCtx,
|
|
1056
|
-
meta,
|
|
1066
|
+
meta: { ...meta, signal: deadline.signal },
|
|
1057
1067
|
}),
|
|
1058
|
-
|
|
1059
|
-
]);
|
|
1068
|
+
deadline.expired,
|
|
1069
|
+
]).finally(deadline.clear);
|
|
1060
1070
|
} catch (err) {
|
|
1061
1071
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1062
1072
|
|
|
@@ -1459,11 +1469,3 @@ export function scriptBodyInterpolationError(nodeId: string, tokens: string[]):
|
|
|
1459
1469
|
"Pass dynamic trigger or upstream values through config.args and read them from argv."
|
|
1460
1470
|
);
|
|
1461
1471
|
}
|
|
1462
|
-
|
|
1463
|
-
// ─── Helpers ───────────────────────────────────────────────
|
|
1464
|
-
|
|
1465
|
-
function timeoutPromise(ms: number): Promise<never> {
|
|
1466
|
-
return new Promise((_, reject) => {
|
|
1467
|
-
setTimeout(() => reject(new Error(`Step timed out after ${ms}ms`)), ms);
|
|
1468
|
-
});
|
|
1469
|
-
}
|
|
@@ -123,6 +123,7 @@ export class SwarmScriptExecutor extends BaseExecutor<
|
|
|
123
123
|
source: resolved.source,
|
|
124
124
|
args: config.args,
|
|
125
125
|
fsMode: "none",
|
|
126
|
+
executor: resolved.script.executor,
|
|
126
127
|
agentId: agentId ?? "workflow",
|
|
127
128
|
mcpBaseUrl: getMcpBaseUrl(),
|
|
128
129
|
egressSecrets: credentials.egressSecrets,
|
|
@@ -132,6 +133,10 @@ export class SwarmScriptExecutor extends BaseExecutor<
|
|
|
132
133
|
apiConnections: getScriptApiConnectionDescriptors({ agentId: agentId ?? undefined }),
|
|
133
134
|
mcpConnections: getScriptMcpConnectionDescriptors({ agentId: agentId ?? undefined }),
|
|
134
135
|
timeoutMs: config.timeoutMs,
|
|
136
|
+
// The step watchdog aborts this when the node budget runs out, which
|
|
137
|
+
// kills the script process: a step reported as timed out must not go
|
|
138
|
+
// on to perform the script's side effects.
|
|
139
|
+
signal: meta.signal,
|
|
135
140
|
});
|
|
136
141
|
if (output.exitCode === 0 && !output.error && !output.runtimeError) {
|
|
137
142
|
await touchScratchScriptLastUsed(resolved.script.id);
|
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
settleForeachChild,
|
|
48
48
|
} from "./foreach-join";
|
|
49
49
|
import { getSecretInputKeys } from "./input";
|
|
50
|
-
import { resolveNodeTimeoutMs } from "./node-timeout";
|
|
50
|
+
import { resolveNodeTimeoutMs, startStepDeadline } from "./node-timeout";
|
|
51
51
|
import { rebuildNodeExecutionInput } from "./step-input";
|
|
52
52
|
|
|
53
53
|
export { FOREACH_ITEMS_WAIT_FOR };
|
|
@@ -480,27 +480,21 @@ async function startChild(input: {
|
|
|
480
480
|
}
|
|
481
481
|
|
|
482
482
|
const executor = registry.get(bodyType);
|
|
483
|
-
const
|
|
484
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
483
|
+
const deadline = startStepDeadline(resolveNodeTimeoutMs({ type: bodyType, config }));
|
|
485
484
|
let result: Awaited<ReturnType<typeof executor.run>>;
|
|
486
485
|
try {
|
|
487
486
|
result = await Promise.race([
|
|
488
487
|
executor.run({
|
|
489
488
|
config,
|
|
490
489
|
context: { ...ctx, ...bodyCtx },
|
|
491
|
-
meta: { ...meta, stepId: child.id, nodeId: child.nodeId },
|
|
492
|
-
}),
|
|
493
|
-
new Promise<never>((_, reject) => {
|
|
494
|
-
timer = setTimeout(
|
|
495
|
-
() => reject(new Error(`Step timed out after ${timeoutMs}ms`)),
|
|
496
|
-
timeoutMs,
|
|
497
|
-
);
|
|
490
|
+
meta: { ...meta, stepId: child.id, nodeId: child.nodeId, signal: deadline.signal },
|
|
498
491
|
}),
|
|
492
|
+
deadline.expired,
|
|
499
493
|
]);
|
|
500
494
|
} catch (err) {
|
|
501
495
|
return { status: "failed", error: err instanceof Error ? err.message : String(err) };
|
|
502
496
|
} finally {
|
|
503
|
-
|
|
497
|
+
deadline.clear();
|
|
504
498
|
}
|
|
505
499
|
|
|
506
500
|
if (result.status === "failed") {
|
|
@@ -19,3 +19,33 @@ export function resolveNodeTimeoutMs(node: Pick<WorkflowNode, "type" | "config">
|
|
|
19
19
|
if (node.type === "script" && typeof config.timeout === "number") return config.timeout;
|
|
20
20
|
return DEFAULT_NODE_TIMEOUT_MS;
|
|
21
21
|
}
|
|
22
|
+
|
|
23
|
+
export interface StepDeadline {
|
|
24
|
+
/** Aborted when the budget runs out. The executor gets it as `meta.signal`. */
|
|
25
|
+
signal: AbortSignal;
|
|
26
|
+
/** Rejects with "Step timed out after <ms>ms" when the budget runs out. */
|
|
27
|
+
expired: Promise<never>;
|
|
28
|
+
/** Stop the timer once the executor call settled. */
|
|
29
|
+
clear: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The watchdog for one executor call. Racing `expired` alone settles the step
|
|
34
|
+
* but leaves the executor running, so a step reported as timed out could still
|
|
35
|
+
* perform its side effects. On expiry the deadline also aborts `signal`, so an
|
|
36
|
+
* executor that forwards it (swarm-script kills its script process) stops.
|
|
37
|
+
*/
|
|
38
|
+
export function startStepDeadline(timeoutMs: number): StepDeadline {
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
41
|
+
const expired = new Promise<never>((_, reject) => {
|
|
42
|
+
timer = setTimeout(() => {
|
|
43
|
+
const error = new Error(`Step timed out after ${timeoutMs}ms`);
|
|
44
|
+
// Reject first so the race settles on the timeout, not on the
|
|
45
|
+
// executor's "killed" result that the abort produces.
|
|
46
|
+
reject(error);
|
|
47
|
+
controller.abort(error);
|
|
48
|
+
}, timeoutMs);
|
|
49
|
+
});
|
|
50
|
+
return { signal: controller.signal, expired, clear: () => clearTimeout(timer) };
|
|
51
|
+
}
|