@desplega.ai/agent-swarm 2.4.5 → 2.5.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 +4 -2
- package/dist/{acp-adapter-2wdkktv6.js → acp-adapter-11gp67nr.js} +8 -6
- package/dist/{actions-c40ebcm4.js → actions-8p2kze8j.js} +23 -21
- package/dist/{amp-adapter-wb67m95z.js → amp-adapter-z4qvhacj.js} +2 -2
- package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
- package/dist/{app-353vre8g.js → app-0qp0a2gk.js} +3 -3
- package/dist/{assistant-sj1njh23.js → assistant-jedd3ktm.js} +24 -22
- package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
- package/dist/{boot-reembed-qdkvxeem.js → boot-reembed-4xwq5tg0.js} +1 -1
- package/dist/{boot-reembed-b9c3az3c.js → boot-reembed-n4wq5dd2.js} +1 -1
- package/dist/{boot-scrub-sweep-m3wh8eks.js → boot-scrub-sweep-69h90xcq.js} +4 -4
- package/dist/{boot-seeds-j2273s54.js → boot-seeds-3vn0m9sg.js} +4 -4
- package/dist/{claude-adapter-m7q0a085.js → claude-adapter-221mvad5.js} +8 -6
- package/dist/{claude-managed-adapter-5ptb6tzs.js → claude-managed-adapter-gt6pa35d.js} +2 -2
- package/dist/{claude-managed-setup-smrqk5j5.js → claude-managed-setup-c3hp36ph.js} +4 -4
- package/dist/{claude-sdk-session-38xnz21e.js → claude-sdk-session-8thjsk5e.js} +9012 -8928
- package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
- package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
- package/dist/{cli-dk44dxec.js → cli-0y6f0ckq.js} +1 -1
- package/dist/{cli-kpw04va0.js → cli-0z3vjrpm.js} +1 -1
- package/dist/{cli-94gwyg7m.js → cli-179xcyky.js} +9 -9
- package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
- package/dist/{cli-4zfqgjc5.js → cli-1pr672hz.js} +2 -2
- package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
- package/dist/{cli-43x64hv3.js → cli-36j4jnk3.js} +4 -4
- package/dist/{cli-k8g16gz3.js → cli-3qv8naq8.js} +115 -119
- package/dist/cli-3zwye9sn.js +1193 -0
- package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
- package/dist/{cli-v8fjakwf.js → cli-4w24f7z3.js} +1 -1
- package/dist/{cli-n72rg53p.js → cli-5kzk3472.js} +812 -418
- package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
- package/dist/{cli-j99azg3g.js → cli-5rvg1ddk.js} +5 -5
- package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
- package/dist/{cli-848hxdwj.js → cli-6rg2jn2y.js} +2 -1
- package/dist/{cli-fv5hepyy.js → cli-7bwtc6yx.js} +3 -3
- package/dist/{cli-ms85309d.js → cli-7g40g8hq.js} +11 -11
- package/dist/{cli-xpa64rek.js → cli-92mqenkc.js} +2362 -159
- package/dist/{cli-018r4jq6.js → cli-9ce2xw98.js} +8 -8
- package/dist/{cli-1syn2xy1.js → cli-bgdtv62r.js} +32 -18
- package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
- package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
- package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
- package/dist/{cli-wt0jf9hm.js → cli-fcc5qe3f.js} +553 -193
- package/dist/{cli-ftc1g5tj.js → cli-fes51ya2.js} +3 -3
- package/dist/{cli-3a45zcgm.js → cli-ftmq4nvk.js} +2 -2
- package/dist/{cli-37j04k05.js → cli-fy1jyyym.js} +13 -13
- package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
- package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
- package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
- package/dist/{cli-g35m2xwm.js → cli-j1eghqfm.js} +2 -2
- package/dist/{cli-qfxc84gh.js → cli-jpxsrxcb.js} +1 -1
- package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
- package/dist/{cli-p14m2cnp.js → cli-mbfj11fq.js} +1 -1
- package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
- package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
- package/dist/{cli-f01h25yt.js → cli-n6kmqngw.js} +2 -2
- package/dist/{cli-v0yrfmdh.js → cli-nnvjb17s.js} +3 -3
- package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
- package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
- package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
- package/dist/cli-pkaqk57w.js +128 -0
- package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
- package/dist/{cli-9yqgm9nd.js → cli-r25qpaxt.js} +161 -63
- package/dist/{cli-eww120g8.js → cli-rh5smt8t.js} +18 -4
- package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
- package/dist/{cli-a92r1127.js → cli-s4sntwt4.js} +2 -2
- package/dist/{cli-be2161h1.js → cli-segs7hx9.js} +6 -6
- package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
- package/dist/{cli-z7d29rw9.js → cli-sy8f9705.js} +4 -4
- package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
- package/dist/{cli-st8aw1m6.js → cli-we0ra057.js} +4 -4
- package/dist/{cli-7c9qs530.js → cli-x21dqn18.js} +5 -5
- package/dist/{cli-1sp861s4.js → cli-x6vf4sad.js} +11 -11
- package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
- package/dist/{cli-tw4p90jb.js → cli-xawhpgc9.js} +5 -5
- package/dist/{cli-f0g3pgvf.js → cli-xd4eqtr2.js} +2 -2
- package/dist/{cli-1f3qzw6y.js → cli-xt768r5m.js} +2 -2
- package/dist/{cli-cxxe61f6.js → cli-y4x8ww6s.js} +2 -2
- package/dist/{cli-bnes4z6b.js → cli-y9jvxfhk.js} +3 -3
- package/dist/{cli-qwhy2qma.js → cli-z63a269p.js} +2 -2
- package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
- package/dist/cli.js +29 -27
- package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
- package/dist/{codex-adapter-tn6k5mkg.js → codex-adapter-tcg1q3jk.js} +6 -4
- package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
- package/dist/{codex-session-runner-7vjt7jbw.js → codex-session-runner-xfx4wd9x.js} +7 -5
- package/dist/{commands-t7rz9146.js → commands-ew83nj70.js} +2 -2
- package/dist/{controller-m2jasma5.js → controller-gnq1k1mf.js} +4 -4
- package/dist/{controllers-connect-9ftpex99.js → controllers-connect-tw1dan9r.js} +25 -23
- package/dist/{cursor-adapter-mjv79e4z.js → cursor-adapter-yq5xgfcg.js} +1 -1
- package/dist/{db-6sfrn9g3.js → db-7gbc53fd.js} +14 -2
- package/dist/{db-rn673r06.js → db-t86xrnes.js} +3 -3
- package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
- package/dist/{dispatcher-c155xz0p.js → dispatcher-64bdyhps.js} +22 -20
- package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
- package/dist/{dsh-adapter-3mmszc1x.js → dsh-adapter-tvhc4k2d.js} +1 -1
- package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
- package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
- package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
- package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
- package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
- package/dist/{external-channel-35k6tkv8.js → external-channel-e9nw24zv.js} +2 -2
- package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
- package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
- package/dist/{grok-adapter-tfrq7tg1.js → grok-adapter-v82srhrx.js} +9 -7
- package/dist/handlers-c9ajzhbn.js +41 -0
- package/dist/{heartbeat-dyg4npkp.js → heartbeat-2vqs1319.js} +25 -23
- package/dist/{hook-x26b31bg.js → hook-01nk13wx.js} +7 -5
- package/dist/{hook-drspbwpe.js → hook-jzpheegg.js} +9 -7
- package/dist/{http-pk972xyw.js → http-fc1k6ec8.js} +544 -76
- package/dist/index-07ef7nkm.js +42 -0
- package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
- package/dist/index-7yts8n9s.js +43 -0
- package/dist/index-q3t40ryv.js +46 -0
- package/dist/{index-arytgj5q.js → index-sepja052.js} +1 -1
- package/dist/{index-jwxkkex2.js → index-zdycv3ja.js} +13 -11
- package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
- package/dist/{keepalive-7yrng7px.js → keepalive-5nh2rhmr.js} +4 -4
- package/dist/lead-aayq64jn.js +79 -0
- package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
- package/dist/{maintenance-e6cemdqt.js → maintenance-hjhzdt3k.js} +1 -1
- package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
- package/dist/{node-de6akczr.js → node-jzjj7r9x.js} +1 -1
- package/dist/{oauth-refresh-sweep-yw3xdbcj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
- package/dist/{onboard-0a9y5yrc.js → onboard-1zq3m7db.js} +2 -2
- package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
- package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
- package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
- package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
- package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
- package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
- package/dist/{otel-impl-trjpkfb7.js → otel-impl-h9yyza47.js} +2 -2
- package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
- package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
- package/dist/{pricing-refresh-d2b5my0y.js → pricing-refresh-nevhat8h.js} +3 -3
- package/dist/{rbac-roles-nxh5ahwk.js → rbac-roles-4y6ngvzq.js} +1 -1
- package/dist/{rbac-roles-hey3yv9d.js → rbac-roles-exbg8xb6.js} +2 -2
- package/dist/{render-v2-rr3p2atg.js → render-v2-cfsh13z3.js} +3 -3
- package/dist/{resume-2efkf1s1.js → resume-5s2t0dbs.js} +10 -8
- package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
- package/dist/{session-access-s2mttxpw.js → session-access-dnbyp3cn.js} +6 -4
- package/dist/{setup-vypma5yt.js → setup-1wj1y77j.js} +1 -1
- package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
- package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
- package/dist/{work-objects-rvhckdkp.js → work-objects-7mg6216g.js} +25 -23
- package/dist/worker-nwb789mg.js +79 -0
- package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
- package/openapi.json +785 -5
- package/package.json +9 -9
- package/src/assets/key.ts +17 -0
- package/src/azure-devops/handlers.ts +1 -1
- package/src/be/db/approvals.ts +18 -0
- package/src/be/db/session-moves.ts +164 -0
- package/src/be/db/session-tree.ts +384 -0
- package/src/be/db/tasks/read.ts +40 -4
- package/src/be/db/workflow-runs.ts +96 -0
- package/src/be/db.ts +80 -236
- package/src/be/onboarding.ts +3 -0
- package/src/be/skill-sync.ts +43 -2
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/bitbucket/api.ts +81 -0
- package/src/bitbucket/auth.ts +64 -0
- package/src/bitbucket/handlers.ts +434 -0
- package/src/bitbucket/index.ts +28 -0
- package/src/bitbucket/templates.ts +134 -0
- package/src/bitbucket/types.ts +65 -0
- package/src/commands/runner.ts +55 -3
- package/src/e2b/env.ts +1 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +2 -2
- package/src/extensions/post-bridge.ts +1 -0
- package/src/github/handlers.ts +6 -6
- package/src/gitlab/handlers.ts +5 -5
- package/src/http/assets.ts +14 -2
- package/src/http/core.ts +12 -0
- package/src/http/index.ts +4 -0
- package/src/http/onboarding.ts +1 -0
- package/src/http/sessions.ts +366 -12
- package/src/http/skills.ts +48 -1
- package/src/http/tasks.ts +1 -1
- package/src/http/users.ts +1 -0
- package/src/http/webhooks.ts +141 -0
- package/src/integrations/chat-sdk/adapters.ts +19 -3
- package/src/integrations/chat-sdk/facts.ts +30 -12
- package/src/integrations/chat-sdk/outbound.ts +38 -1
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +5 -53
- package/src/scripts-runtime/types/swarm-extension.d.ts +6 -4
- package/src/slack/handlers.test.ts +158 -0
- package/src/slack/handlers.ts +19 -28
- package/src/tasks/context-key.ts +61 -1
- package/src/tests/bitbucket-auth.test.ts +156 -0
- package/src/tests/bitbucket-handlers.test.ts +425 -0
- package/src/tests/bitbucket-webhook.test.ts +220 -0
- package/src/tests/chat-sdk-inbound.test.ts +39 -0
- package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
- package/src/tests/context-key.test.ts +50 -0
- package/src/tests/core-auth.test.ts +5 -0
- package/src/tests/db-task-read-contract.test.ts +7 -0
- package/src/tests/db-task-write-contract.test.ts +6 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
- package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
- package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
- package/src/tests/generate-mcp-docs.test.ts +101 -0
- package/src/tests/gitlab-handlers.test.ts +3 -3
- package/src/tests/gitlab-vcs-db.test.ts +4 -4
- package/src/tests/integration-disable-flags.test.ts +36 -0
- package/src/tests/mcp-tools.test.ts +1 -1
- package/src/tests/onboarding.test.ts +12 -0
- package/src/tests/pi-codemode.test.ts +8 -2
- package/src/tests/preload-env.test.ts +57 -0
- package/src/tests/preload.ts +21 -0
- package/src/tests/prompt-template-remaining.test.ts +38 -0
- package/src/tests/run-root-coverage.test.ts +39 -0
- package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
- package/src/tests/runner-skills-refresh.test.ts +58 -0
- package/src/tests/secret-scrubber.test.ts +15 -0
- package/src/tests/sessions-move.test.ts +576 -0
- package/src/tests/sessions-tree.test.ts +493 -0
- package/src/tests/skill-files-http.test.ts +50 -0
- package/src/tests/skills-bundle.test.ts +99 -0
- package/src/tests/swarm-config-export-to-env.test.ts +2 -1
- package/src/tests/vcs-bitbucket.test.ts +91 -0
- package/src/tests/vcs-provider-isolation.test.ts +123 -0
- package/src/tests/vcs-provider.test.ts +16 -1
- package/src/tests/vcs-tracking.test.ts +3 -3
- package/src/tests/workflow-foreach-bodies.test.ts +637 -0
- package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
- package/src/tests/workflow-foreach-retry.test.ts +537 -0
- package/src/tests/workflow-foreach.test.ts +44 -23
- package/src/tests/workflow-step-claims.test.ts +6 -6
- package/src/tests/workflow-tla-races.test.ts +2 -2
- package/src/tools/script-delete.ts +1 -1
- package/src/tools/workflows/create-workflow.ts +5 -3
- package/src/types.ts +3 -2
- package/src/utils/config-env-export.ts +1 -0
- package/src/utils/secret-scrubber.ts +4 -0
- package/src/utils/skills-refresh.ts +83 -43
- package/src/vcs/bitbucket.ts +58 -0
- package/src/vcs/index.ts +2 -0
- package/src/vcs/types.ts +2 -2
- package/src/workflows/approval-resolution.ts +4 -1
- package/src/workflows/checkpoint.ts +9 -1
- package/src/workflows/constants.ts +3 -0
- package/src/workflows/definition.ts +48 -17
- package/src/workflows/engine.ts +25 -15
- package/src/workflows/executors/foreach.ts +45 -99
- package/src/workflows/foreach-body.ts +146 -0
- package/src/workflows/foreach-driver.ts +552 -0
- package/src/workflows/foreach-join.ts +206 -7
- package/src/workflows/node-timeout.ts +21 -0
- package/src/workflows/recovery.ts +68 -3
- package/src/workflows/resume.ts +221 -43
- package/src/workflows/retry-poller.ts +39 -24
- package/src/workflows/step-input.ts +59 -0
- package/src/workflows/task-step-routing.ts +114 -56
- package/templates/skills/slack-interaction/content.md +5 -0
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/dist/cli-9hstj09h.js +0 -2657
- package/dist/cli-yw91rnmw.js +0 -13
- package/dist/handlers-7gw64dxw.js +0 -39
- package/dist/index-bgdyvcnv.js +0 -40
- package/dist/index-gabbcdn2.js +0 -43
- package/dist/index-tfb26zhy.js +0 -41
- package/dist/lead-7e02z839.js +0 -77
- package/dist/worker-5snytpbd.js +0 -77
|
@@ -213,7 +213,7 @@ describe("TLA+ workflow counterexamples", () => {
|
|
|
213
213
|
const stepB = await untilStatus(runId, "B", "waiting");
|
|
214
214
|
|
|
215
215
|
// EF: B's task fails -> run failed.
|
|
216
|
-
await failStepAndRunIfWaiting(stepB!.id, runId, "task failed");
|
|
216
|
+
await failStepAndRunIfWaiting(def, stepB!.id, runId, "task failed");
|
|
217
217
|
expect((await getWorkflowRun(runId))?.status).toBe("failed");
|
|
218
218
|
|
|
219
219
|
// U1/U2: the user retries; findReadyNodes returns A because it is not completed.
|
|
@@ -240,7 +240,7 @@ describe("TLA+ workflow counterexamples", () => {
|
|
|
240
240
|
|
|
241
241
|
// EF: B's task fails -> run failed. XCkWait(A): the checkpoint CAS sees a
|
|
242
242
|
// failed run, so A's row stays `running` with its task out.
|
|
243
|
-
await failStepAndRunIfWaiting(stepB!.id, runId, "task failed");
|
|
243
|
+
await failStepAndRunIfWaiting(def, stepB!.id, runId, "task failed");
|
|
244
244
|
holdA.release();
|
|
245
245
|
await initial;
|
|
246
246
|
expect((await stepsOf(runId, "A"))[0]?.status).toBe("running");
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "./script-common";
|
|
10
10
|
|
|
11
11
|
export const SCRIPT_DELETE_DESCRIPTION =
|
|
12
|
-
"
|
|
12
|
+
"Permanently remove a swarm-shared script and all its versions from the catalog. This cannot be undone.";
|
|
13
13
|
|
|
14
14
|
export const registerScriptDeleteTool = (server: McpServer) => {
|
|
15
15
|
createToolRegistrar(server)(
|
|
@@ -40,11 +40,13 @@ export const registerCreateWorkflowTool = (server: McpServer) => {
|
|
|
40
40
|
"- STRUCTURED OUTPUT: For agent-task nodes, put outputSchema inside 'config' to validate the agent's raw JSON output. " +
|
|
41
41
|
"Node-level outputSchema validates the executor's return ({taskId, taskOutput}), which is different.\n" +
|
|
42
42
|
"- Agent-task config: { template, outputSchema?, agentId?, tags?, priority?, dir?, vcsRepo?, model? }.\n" +
|
|
43
|
-
"- FOREACH NODE: type 'foreach'
|
|
44
|
-
"{ over: <array or exact {{input}} token>, itemKey: <property name>, body: { type:
|
|
43
|
+
"- FOREACH NODE: type 'foreach' runs its body once per item. Config: " +
|
|
44
|
+
"{ over: <array or exact {{input}} token>, itemKey: <property name>, body: { type: <any executor except foreach>, config: {...} }, " +
|
|
45
|
+
"concurrency?: <integer >= 1, items in flight; unbounded when absent>, failurePolicy?: 'fail-fast' | 'collect-outcomes' }. " +
|
|
45
46
|
"The body config is interpolated once per item with {{item.*}} and {{index}}. Child steps use synthetic IDs " +
|
|
46
47
|
"'<foreachNodeId>#<itemKey>'; the parent waits for every child and exposes one aggregate result to successors. " +
|
|
47
|
-
"
|
|
48
|
+
"failurePolicy defaults from onNodeFailure ('continue' = collect-outcomes, else fail-fast); fail-fast cancels queued items. " +
|
|
49
|
+
"retry-workflow-run on a failed fan-out reruns only failed and cancelled items.\n" +
|
|
48
50
|
"- TRIGGER SCHEMA: Optional 'triggerSchema' is a JSON-Schema object that validates incoming trigger payloads. " +
|
|
49
51
|
"Supported keywords: type, required, properties, enum, const, items (recursive into arrays). " +
|
|
50
52
|
"Other JSON-Schema keywords (oneOf/anyOf/$ref/pattern/format/additionalProperties) are silently ignored.\n" +
|
package/src/types.ts
CHANGED
|
@@ -384,6 +384,7 @@ export const AgentTaskSourceSchema = z.enum([
|
|
|
384
384
|
"github",
|
|
385
385
|
"gitlab",
|
|
386
386
|
"azure-devops",
|
|
387
|
+
"bitbucket",
|
|
387
388
|
"agentmail",
|
|
388
389
|
"system",
|
|
389
390
|
"schedule",
|
|
@@ -697,7 +698,7 @@ export const AgentTaskSchema = z
|
|
|
697
698
|
slackTreeRootMessageTs: z.string().optional(),
|
|
698
699
|
|
|
699
700
|
// VCS metadata (GitHub / GitLab / Azure DevOps — provider-agnostic)
|
|
700
|
-
vcsProvider: z.enum(["github", "gitlab", "azure-devops"]).optional(),
|
|
701
|
+
vcsProvider: z.enum(["github", "gitlab", "azure-devops", "bitbucket"]).optional(),
|
|
701
702
|
vcsRepo: z.string().optional(),
|
|
702
703
|
vcsEventType: z.string().optional(),
|
|
703
704
|
vcsNumber: z.number().int().optional(),
|
|
@@ -863,7 +864,7 @@ export const CreateTaskOptionsSchema = z.object({
|
|
|
863
864
|
* this boundary must not let a caller silently persist a mismatch.
|
|
864
865
|
*/
|
|
865
866
|
overrideSlackContext: z.boolean().optional(),
|
|
866
|
-
vcsProvider: z.enum(["github", "gitlab", "azure-devops"]).optional(),
|
|
867
|
+
vcsProvider: z.enum(["github", "gitlab", "azure-devops", "bitbucket"]).optional(),
|
|
867
868
|
vcsRepo: z.string().optional(),
|
|
868
869
|
vcsEventType: z.string().optional(),
|
|
869
870
|
vcsNumber: z.number().int().optional(),
|
|
@@ -24,6 +24,7 @@ export const ENV_EXPORT_RESTRICTED_KEYS: ReadonlySet<string> = new Set([
|
|
|
24
24
|
"JIRA_CLIENT_SECRET",
|
|
25
25
|
"JIRA_WEBHOOK_TOKEN",
|
|
26
26
|
"AZURE_DEVOPS_WEBHOOK_SECRET",
|
|
27
|
+
"BITBUCKET_WEBHOOK_SECRET",
|
|
27
28
|
"AGENTMAIL_WEBHOOK_SECRET",
|
|
28
29
|
"API_AGENT_FS_API_KEY",
|
|
29
30
|
"TRANSCRIPTION_API_KEY",
|
|
@@ -53,6 +53,10 @@ const SENSITIVE_KEY_EXACT = new Set<string>([
|
|
|
53
53
|
"AZURE_DEVOPS_TOKEN",
|
|
54
54
|
// Worker-side copy of AZURE_DEVOPS_TOKEN read by the az azure-devops extension
|
|
55
55
|
"AZURE_DEVOPS_EXT_PAT",
|
|
56
|
+
// The `_TOKEN` / `_SECRET` suffixes already match these, so listing them
|
|
57
|
+
// does not change redaction and does not bump SCRUBBER_RULES_VERSION.
|
|
58
|
+
"BITBUCKET_TOKEN",
|
|
59
|
+
"BITBUCKET_WEBHOOK_SECRET",
|
|
56
60
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
57
61
|
"ANTHROPIC_API_KEY",
|
|
58
62
|
"OPENAI_API_KEY",
|
|
@@ -77,26 +77,56 @@ export async function refreshSkillsIfChanged(
|
|
|
77
77
|
isEnabled: boolean;
|
|
78
78
|
isActive: boolean;
|
|
79
79
|
};
|
|
80
|
+
type BundledFile = { path: string; content: string; isBinary: boolean };
|
|
80
81
|
let skillRows: SkillRow[] = [];
|
|
82
|
+
// Present when the bundle call succeeded: every complex skill's files arrive
|
|
83
|
+
// in the same response, so Step 3 never fans out per file.
|
|
84
|
+
let bundledFiles: Map<string, BundledFile[]> | null = null;
|
|
81
85
|
let newHash: string | null = null;
|
|
82
86
|
let listFetchOk = false;
|
|
87
|
+
|
|
88
|
+
// Preferred path: one request for the skills and all of their bundled files.
|
|
89
|
+
// A 4xx from an older server (no bundle endpoint yet) falls through to the
|
|
90
|
+
// legacy list + per-file path below.
|
|
83
91
|
try {
|
|
84
|
-
const
|
|
92
|
+
const bundleResp = await fetch(`${apiUrl}/api/agents/${agentId}/skills/bundle`, {
|
|
85
93
|
headers: authHeaders,
|
|
86
94
|
});
|
|
87
|
-
if (
|
|
88
|
-
const
|
|
89
|
-
skills: SkillRow[];
|
|
95
|
+
if (bundleResp.ok) {
|
|
96
|
+
const bundle = (await bundleResp.json()) as {
|
|
97
|
+
skills: (SkillRow & { files?: BundledFile[] })[];
|
|
90
98
|
signature?: string;
|
|
91
99
|
};
|
|
92
|
-
skillRows =
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
skillRows = bundle.skills;
|
|
101
|
+
bundledFiles = new Map(bundle.skills.map((skill) => [skill.id, skill.files ?? []]));
|
|
102
|
+
if (typeof bundle.signature === "string") {
|
|
103
|
+
newHash = bundle.signature;
|
|
95
104
|
}
|
|
96
105
|
listFetchOk = true;
|
|
97
106
|
}
|
|
98
107
|
} catch {
|
|
99
|
-
// Transient network / parse error —
|
|
108
|
+
// Transient network / parse error — the legacy path below gets a chance.
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!listFetchOk) {
|
|
112
|
+
try {
|
|
113
|
+
const skillsResp = await fetch(`${apiUrl}/api/agents/${agentId}/skills`, {
|
|
114
|
+
headers: authHeaders,
|
|
115
|
+
});
|
|
116
|
+
if (skillsResp.ok) {
|
|
117
|
+
const skillsData = (await skillsResp.json()) as {
|
|
118
|
+
skills: SkillRow[];
|
|
119
|
+
signature?: string;
|
|
120
|
+
};
|
|
121
|
+
skillRows = skillsData.skills;
|
|
122
|
+
if (typeof skillsData.signature === "string") {
|
|
123
|
+
newHash = skillsData.signature;
|
|
124
|
+
}
|
|
125
|
+
listFetchOk = true;
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Transient network / parse error — bail out without touching the local FS
|
|
129
|
+
}
|
|
100
130
|
}
|
|
101
131
|
|
|
102
132
|
// Guard: a failed list fetch must not proceed to writeSkillsToFilesystem.
|
|
@@ -125,45 +155,55 @@ export async function refreshSkillsIfChanged(
|
|
|
125
155
|
const files: { path: string; content: string; isBinary: boolean }[] = [];
|
|
126
156
|
|
|
127
157
|
if (skill.isComplex) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
files
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
const bundled = bundledFiles?.get(skill.id);
|
|
159
|
+
if (bundled) {
|
|
160
|
+
// Bundle path: every file (including binaries) arrived with the
|
|
161
|
+
// skill list, so there is nothing left to fetch.
|
|
162
|
+
files.push(...bundled);
|
|
163
|
+
} else {
|
|
164
|
+
// Legacy server: fetch manifest to know which files exist + which are binary
|
|
165
|
+
try {
|
|
166
|
+
const manifestResp = await fetch(`${apiUrl}/api/skills/${skill.id}/files`, {
|
|
167
|
+
headers: authHeaders,
|
|
168
|
+
});
|
|
169
|
+
if (manifestResp.ok) {
|
|
170
|
+
const manifestData = (await manifestResp.json()) as {
|
|
171
|
+
files: { path: string; isBinary: boolean }[];
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Fetch content for each non-binary file (N+1 — acceptable for v1)
|
|
175
|
+
for (const manifestEntry of manifestData.files) {
|
|
176
|
+
if (manifestEntry.isBinary) {
|
|
177
|
+
files.push({ path: manifestEntry.path, content: "", isBinary: true });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const encodedPath = manifestEntry.path
|
|
182
|
+
.split("/")
|
|
183
|
+
.map(encodeURIComponent)
|
|
184
|
+
.join("/");
|
|
185
|
+
const fileResp = await fetch(
|
|
186
|
+
`${apiUrl}/api/skills/${skill.id}/files/${encodedPath}`,
|
|
187
|
+
{ headers: authHeaders },
|
|
188
|
+
);
|
|
189
|
+
if (fileResp.ok) {
|
|
190
|
+
const fileData = (await fileResp.json()) as {
|
|
191
|
+
file: { path: string; content: string; isBinary: boolean };
|
|
192
|
+
};
|
|
193
|
+
files.push({
|
|
194
|
+
path: fileData.file.path,
|
|
195
|
+
content: fileData.file.content,
|
|
196
|
+
isBinary: fileData.file.isBinary,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// Non-fatal — skip this file
|
|
159
201
|
}
|
|
160
|
-
} catch {
|
|
161
|
-
// Non-fatal — skip this file
|
|
162
202
|
}
|
|
163
203
|
}
|
|
204
|
+
} catch {
|
|
205
|
+
// Non-fatal — treat as no files (will skip complex skill per writer logic)
|
|
164
206
|
}
|
|
165
|
-
} catch {
|
|
166
|
-
// Non-fatal — treat as no files (will skip complex skill per writer logic)
|
|
167
207
|
}
|
|
168
208
|
}
|
|
169
209
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bitbucket Cloud URL helpers. Pure functions, safe on both the API server and
|
|
3
|
+
* the worker (no DB imports). Bitbucket Data Center is out of scope.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* True when the URL's host is bitbucket.org. Only the host counts, so a
|
|
8
|
+
* `bitbucket.org` path segment on another host (`gitlab.com/bitbucket.org/x`)
|
|
9
|
+
* is not Bitbucket.
|
|
10
|
+
*/
|
|
11
|
+
export function isBitbucketUrl(url: string): boolean {
|
|
12
|
+
const trimmed = url.trim();
|
|
13
|
+
// SCP-style SSH: user@host:path
|
|
14
|
+
const scp = trimmed.match(/^[^@/\s:]+@([^:/\s]+):/);
|
|
15
|
+
if (scp) return (scp[1] as string).toLowerCase() === "bitbucket.org";
|
|
16
|
+
try {
|
|
17
|
+
return new URL(trimmed).hostname.toLowerCase() === "bitbucket.org";
|
|
18
|
+
} catch {
|
|
19
|
+
// Scheme-less `bitbucket.org/ws/repo`
|
|
20
|
+
return /^bitbucket\.org(\/|$)/i.test(trimmed);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Split a Bitbucket Cloud remote into its workspace and repository slug.
|
|
26
|
+
* Accepts the HTTPS forms (with or without a `user@` prefix, trailing `/` or
|
|
27
|
+
* `.git`) and the SSH forms (`git@bitbucket.org:ws/repo.git`,
|
|
28
|
+
* `ssh://git@bitbucket.org/ws/repo.git`). Returns null otherwise.
|
|
29
|
+
*/
|
|
30
|
+
export function parseBitbucketRepoUrl(url: string): { workspace: string; repoSlug: string } | null {
|
|
31
|
+
const trimmed = url.trim();
|
|
32
|
+
if (!isBitbucketUrl(trimmed)) return null;
|
|
33
|
+
|
|
34
|
+
// SCP-style SSH: git@bitbucket.org:{workspace}/{repo}.git
|
|
35
|
+
const scp = trimmed.match(/^[^@/\s]+@bitbucket\.org:([^/]+)\/([^/]+?)(?:\.git)?\/?$/i);
|
|
36
|
+
if (scp) return { workspace: scp[1] as string, repoSlug: scp[2] as string };
|
|
37
|
+
|
|
38
|
+
let parsed: URL;
|
|
39
|
+
try {
|
|
40
|
+
parsed = new URL(trimmed);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (parsed.hostname.toLowerCase() !== "bitbucket.org") return null;
|
|
45
|
+
const parts = parsed.pathname
|
|
46
|
+
.replace(/\.git\/?$/, "")
|
|
47
|
+
.split("/")
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
const [workspace, repoSlug] = parts;
|
|
50
|
+
if (!workspace || !repoSlug) return null;
|
|
51
|
+
return { workspace: decodeURIComponent(workspace), repoSlug: decodeURIComponent(repoSlug) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `workspace/repo_slug` for a Bitbucket Cloud remote, or null. */
|
|
55
|
+
export function bitbucketFullName(url: string): string | null {
|
|
56
|
+
const parsed = parseBitbucketRepoUrl(url);
|
|
57
|
+
return parsed ? `${parsed.workspace}/${parsed.repoSlug}` : null;
|
|
58
|
+
}
|
package/src/vcs/index.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { isAzureDevOpsUrl } from "./azure-devops";
|
|
6
|
+
import { isBitbucketUrl } from "./bitbucket";
|
|
6
7
|
import type { VcsProvider } from "./types";
|
|
7
8
|
|
|
8
9
|
export type { VcsProvider } from "./types";
|
|
@@ -13,6 +14,7 @@ export type { VcsProvider } from "./types";
|
|
|
13
14
|
*/
|
|
14
15
|
export function detectVcsProvider(url: string): VcsProvider | null {
|
|
15
16
|
if (isAzureDevOpsUrl(url)) return "azure-devops";
|
|
17
|
+
if (isBitbucketUrl(url)) return "bitbucket";
|
|
16
18
|
if (url.includes("gitlab.com") || url.includes("gitlab.")) return "gitlab";
|
|
17
19
|
if (url.includes("github.com") || /^[\w.-]+\/[\w.-]+$/.test(url)) return "github";
|
|
18
20
|
return null;
|
package/src/vcs/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* VCS Provider types — shared across GitHub/GitLab/Azure DevOps integration.
|
|
2
|
+
* VCS Provider types — shared across GitHub/GitLab/Azure DevOps/Bitbucket integration.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export type VcsProvider = "github" | "gitlab" | "azure-devops";
|
|
5
|
+
export type VcsProvider = "github" | "gitlab" | "azure-devops" | "bitbucket";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { WorkflowDefinition } from "../types";
|
|
2
2
|
import type { ApprovalOutcome, ResolvedApproval } from "./executors/base";
|
|
3
3
|
import type { ExecutorRegistry } from "./executors/registry";
|
|
4
|
+
import { resolveStepNode } from "./foreach-body";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Step output and output port for a step whose approval request resolved.
|
|
@@ -18,7 +19,9 @@ export function shapeApprovalResolution(
|
|
|
18
19
|
parkedOutput: unknown,
|
|
19
20
|
approval: ResolvedApproval,
|
|
20
21
|
): ApprovalOutcome {
|
|
21
|
-
|
|
22
|
+
// A foreach child resolves to its body node, so a system-one-decision body
|
|
23
|
+
// keeps its own shaping.
|
|
24
|
+
const node = resolveStepNode(definition, nodeId);
|
|
22
25
|
if (node && registry.has(node.type)) {
|
|
23
26
|
const shaped = registry.get(node.type).resolveApproval(parkedOutput, approval);
|
|
24
27
|
if (shaped) return shaped;
|
|
@@ -83,11 +83,16 @@ export async function checkpointStepFailure(
|
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
85
|
* Checkpoint a step entering waiting state (async executor).
|
|
86
|
+
*
|
|
87
|
+
* `keepRunStatus` parks the step and persists the context but leaves the run
|
|
88
|
+
* `running`: a foreach parent parks before its driver starts the children, and
|
|
89
|
+
* the run must not read as `waiting` until they are dispatched.
|
|
86
90
|
*/
|
|
87
91
|
export async function checkpointStepWaiting(
|
|
88
92
|
runId: string,
|
|
89
93
|
stepId: string,
|
|
90
94
|
ctx: Record<string, unknown>,
|
|
95
|
+
options: { keepRunStatus?: boolean } = {},
|
|
91
96
|
): Promise<boolean> {
|
|
92
97
|
return getDbClient().transaction(async () => {
|
|
93
98
|
const step = await getDbClient().get<{ id: string }>(
|
|
@@ -103,7 +108,10 @@ export async function checkpointStepWaiting(
|
|
|
103
108
|
);
|
|
104
109
|
if (!step) return false;
|
|
105
110
|
|
|
106
|
-
await updateWorkflowRun(
|
|
111
|
+
await updateWorkflowRun(
|
|
112
|
+
runId,
|
|
113
|
+
options.keepRunStatus ? { context: ctx } : { status: "waiting", context: ctx },
|
|
114
|
+
);
|
|
107
115
|
return true;
|
|
108
116
|
});
|
|
109
117
|
}
|
|
@@ -11,6 +11,12 @@ import {
|
|
|
11
11
|
systemOneStaticShapeViolations,
|
|
12
12
|
} from "./executors/system-one-decision";
|
|
13
13
|
import { systemOneExtractionStaticShapeViolations } from "./executors/system-one-extraction";
|
|
14
|
+
import {
|
|
15
|
+
FOREACH_FAILURE_POLICIES,
|
|
16
|
+
type ForeachFailurePolicy,
|
|
17
|
+
foreachBodyTypeError,
|
|
18
|
+
isForeachBodyType,
|
|
19
|
+
} from "./foreach-body";
|
|
14
20
|
|
|
15
21
|
/** Extract all target node IDs from a node's `next` field */
|
|
16
22
|
export function getNextTargets(next: string | string[] | Record<string, string>): string[] {
|
|
@@ -220,7 +226,7 @@ export function validateDefinition(
|
|
|
220
226
|
...systemOneRetryViolations(node),
|
|
221
227
|
);
|
|
222
228
|
if (node.type === "foreach") {
|
|
223
|
-
validateForeachNode(node, errors);
|
|
229
|
+
validateForeachNode(node, errors, registry);
|
|
224
230
|
// A legacy `#` id may stay editable as a NORMAL node, but never as a foreach
|
|
225
231
|
// parent: its children would be `a#b#item`, and parseSyntheticNodeId splits on
|
|
226
232
|
// the FIRST `#`, so the join would resolve them to parent "a" and never close.
|
|
@@ -312,20 +318,25 @@ export function validateDefinition(
|
|
|
312
318
|
|
|
313
319
|
reportStaticConfigIssues(node, registry.get(node.type).configSchema, node.config, errors);
|
|
314
320
|
|
|
315
|
-
// A foreach body is executed by
|
|
316
|
-
// static config to the same schema a top-level
|
|
317
|
-
// invalid field (priority: 101, tags: "x") fails at authoring instead of
|
|
321
|
+
// A foreach body is executed by its own executor per item — hold its
|
|
322
|
+
// static config to the same schema a top-level node of that type gets, so
|
|
323
|
+
// an invalid field (priority: 101, tags: "x") fails at authoring instead of
|
|
318
324
|
// after the fan-out materialized. Interpolated fields defer as usual.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
+
const body = node.type === "foreach" ? node.config.body : undefined;
|
|
326
|
+
const bodyRecord =
|
|
327
|
+
typeof body === "object" && body !== null && !Array.isArray(body)
|
|
328
|
+
? (body as Record<string, unknown>)
|
|
329
|
+
: undefined;
|
|
330
|
+
if (
|
|
331
|
+
bodyRecord &&
|
|
332
|
+
typeof bodyRecord.type === "string" &&
|
|
333
|
+
isForeachBodyType(bodyRecord.type, registry)
|
|
334
|
+
) {
|
|
335
|
+
const bodyConfig = bodyRecord.config;
|
|
325
336
|
if (typeof bodyConfig === "object" && bodyConfig !== null && !Array.isArray(bodyConfig)) {
|
|
326
337
|
reportStaticConfigIssues(
|
|
327
338
|
node,
|
|
328
|
-
registry.get(
|
|
339
|
+
registry.get(bodyRecord.type).configSchema,
|
|
329
340
|
bodyConfig,
|
|
330
341
|
errors,
|
|
331
342
|
"config.body.config",
|
|
@@ -401,10 +412,27 @@ function reportStaticConfigIssues(
|
|
|
401
412
|
}
|
|
402
413
|
}
|
|
403
414
|
|
|
404
|
-
function validateForeachNode(
|
|
415
|
+
function validateForeachNode(
|
|
416
|
+
node: WorkflowNode,
|
|
417
|
+
errors: string[],
|
|
418
|
+
registry?: ExecutorRegistry,
|
|
419
|
+
): void {
|
|
405
420
|
const config = node.config;
|
|
406
|
-
if (
|
|
407
|
-
|
|
421
|
+
if (
|
|
422
|
+
config.concurrency !== undefined &&
|
|
423
|
+
!(Number.isInteger(config.concurrency) && (config.concurrency as number) >= 1)
|
|
424
|
+
) {
|
|
425
|
+
errors.push(
|
|
426
|
+
`Node "${node.id}" foreach config.concurrency must be an integer of 1 or more, got ${JSON.stringify(config.concurrency)}`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
if (
|
|
430
|
+
config.failurePolicy !== undefined &&
|
|
431
|
+
!FOREACH_FAILURE_POLICIES.includes(config.failurePolicy as ForeachFailurePolicy)
|
|
432
|
+
) {
|
|
433
|
+
errors.push(
|
|
434
|
+
`Node "${node.id}" foreach config.failurePolicy must be one of ${FOREACH_FAILURE_POLICIES.map((p) => `"${p}"`).join(", ")}, got ${JSON.stringify(config.failurePolicy)}`,
|
|
435
|
+
);
|
|
408
436
|
}
|
|
409
437
|
|
|
410
438
|
// The asynchronous join (joinForeach) checkpoints the aggregate and routes
|
|
@@ -436,8 +464,11 @@ function validateForeachNode(node: WorkflowNode, errors: string[]): void {
|
|
|
436
464
|
return;
|
|
437
465
|
}
|
|
438
466
|
const bodyRecord = body as Record<string, unknown>;
|
|
439
|
-
if (bodyRecord.type !== "
|
|
440
|
-
errors.push(`Node "${node.id}" foreach config.body.type must be
|
|
467
|
+
if (typeof bodyRecord.type !== "string" || bodyRecord.type.length === 0) {
|
|
468
|
+
errors.push(`Node "${node.id}" foreach config.body.type must be a non-empty string`);
|
|
469
|
+
} else {
|
|
470
|
+
const typeError = foreachBodyTypeError(node.id, bodyRecord.type, registry);
|
|
471
|
+
if (typeError) errors.push(typeError);
|
|
441
472
|
}
|
|
442
473
|
if (
|
|
443
474
|
typeof bodyRecord.config !== "object" ||
|
|
@@ -448,7 +479,7 @@ function validateForeachNode(node: WorkflowNode, errors: string[]): void {
|
|
|
448
479
|
return;
|
|
449
480
|
}
|
|
450
481
|
const bodyConfig = bodyRecord.config as Record<string, unknown>;
|
|
451
|
-
if (typeof bodyConfig.template !== "string") {
|
|
482
|
+
if (bodyRecord.type === "agent-task" && typeof bodyConfig.template !== "string") {
|
|
452
483
|
errors.push(`Node "${node.id}" foreach agent-task body.config.template must be a string`);
|
|
453
484
|
}
|
|
454
485
|
}
|
package/src/workflows/engine.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { telemetry } from "../telemetry";
|
|
|
25
25
|
import type { Workflow, WorkflowDefinition, WorkflowNode, WorkflowRunStep } from "../types";
|
|
26
26
|
import { checkpointStep, checkpointStepFailure, checkpointStepWaiting } from "./checkpoint";
|
|
27
27
|
import { loadCompletedStepRouting } from "./completed-step-routing";
|
|
28
|
+
import { FOREACH_ITEMS_WAIT_FOR } from "./constants";
|
|
28
29
|
import { shouldSkipCooldown } from "./cooldown";
|
|
29
30
|
import { findEntryNodes, getNextTargets, getSuccessors, resolveValidationPort } from "./definition";
|
|
30
31
|
import type { AsyncExecutorResult } from "./executors/base";
|
|
@@ -34,15 +35,16 @@ import {
|
|
|
34
35
|
systemOneRetryViolations,
|
|
35
36
|
systemOneUnresolvedError,
|
|
36
37
|
} from "./executors/system-one-decision";
|
|
38
|
+
import { parkAndDriveForeach } from "./foreach-driver";
|
|
37
39
|
import { FOREACH_TERMINAL_STEP_STATUSES, resolveForeachParent } from "./foreach-join";
|
|
38
40
|
import { getSecretInputKeys, redactSecretsForStorage, resolveInputs } from "./input";
|
|
39
41
|
import { validateJsonSchema } from "./json-schema-validator";
|
|
40
42
|
import { getMaxWorkflowStepsPerRun } from "./limits";
|
|
43
|
+
import { resolveNodeTimeoutMs } from "./node-timeout";
|
|
41
44
|
import { findWorkflowReadinessProblems, formatReadinessRunError } from "./readiness";
|
|
42
45
|
import { deepInterpolate } from "./template";
|
|
43
46
|
import { runStepValidation, type ValidationRunResult } from "./validation";
|
|
44
47
|
|
|
45
|
-
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
46
48
|
// Read dynamically, NOT captured at module load: `src/http/index.ts` imports
|
|
47
49
|
// this module before `loadGlobalConfigsIntoEnv()` hydrates `swarm_config` into
|
|
48
50
|
// `process.env`, so a module-level const would permanently ignore DB-saved
|
|
@@ -510,14 +512,19 @@ async function walkGraphOwned(
|
|
|
510
512
|
const hasPendingRetries = finalSteps.some(
|
|
511
513
|
(s) => s.status === "failed" && s.nextRetryAt != null,
|
|
512
514
|
);
|
|
513
|
-
|
|
515
|
+
// A foreach child's outcome is in its parent's aggregate; the parent step
|
|
516
|
+
// is authoritative for the run outcome.
|
|
517
|
+
const isForeachChild = (s: WorkflowRunStep) => resolveForeachParent(def, s.nodeId) !== null;
|
|
518
|
+
const failedSteps = finalSteps.filter(
|
|
519
|
+
(s) => s.status === "failed" && s.nextRetryAt == null && !isForeachChild(s),
|
|
520
|
+
);
|
|
514
521
|
// Exclude entry/trigger nodes when checking for completed steps — a trigger
|
|
515
522
|
// completing doesn't mean a meaningful branch succeeded. Without this filter,
|
|
516
523
|
// a linear workflow (trigger → mustPass validator → action) would be marked
|
|
517
524
|
// as partial-failure instead of failed when the validator fails.
|
|
518
525
|
const entryNodeIds = new Set(findEntryNodes(def).map((n) => n.id));
|
|
519
526
|
const hasCompletedSteps = finalSteps.some(
|
|
520
|
-
(s) => s.status === "completed" && !entryNodeIds.has(s.nodeId),
|
|
527
|
+
(s) => s.status === "completed" && !entryNodeIds.has(s.nodeId) && !isForeachChild(s),
|
|
521
528
|
);
|
|
522
529
|
|
|
523
530
|
if (hasWaitingSteps) {
|
|
@@ -880,18 +887,7 @@ async function runClaimedStep(
|
|
|
880
887
|
inputCtx: interpolationCtx,
|
|
881
888
|
};
|
|
882
889
|
|
|
883
|
-
|
|
884
|
-
// `config.timeout`, while the workflow watchdog reads `config.timeoutMs` for
|
|
885
|
-
// every other executor. Keep the outer watchdog aligned with the script
|
|
886
|
-
// executor so a script configured for (for example) 90s is not killed by the
|
|
887
|
-
// engine's 30s default first.
|
|
888
|
-
const configuredTimeoutMs =
|
|
889
|
-
typeof node.config?.timeoutMs === "number"
|
|
890
|
-
? node.config.timeoutMs
|
|
891
|
-
: node.type === "script" && typeof node.config?.timeout === "number"
|
|
892
|
-
? node.config.timeout
|
|
893
|
-
: undefined;
|
|
894
|
-
const timeoutMs = configuredTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
890
|
+
const timeoutMs = resolveNodeTimeoutMs(node);
|
|
895
891
|
|
|
896
892
|
let result: Awaited<ReturnType<typeof executor.run>>;
|
|
897
893
|
try {
|
|
@@ -949,6 +945,20 @@ async function runClaimedStep(
|
|
|
949
945
|
|
|
950
946
|
// Check for async result
|
|
951
947
|
if ("async" in result && (result as AsyncExecutorResult).async) {
|
|
948
|
+
if ((result as AsyncExecutorResult).waitFor === FOREACH_ITEMS_WAIT_FOR) {
|
|
949
|
+
// The foreach executor materialized its children; park the parent and
|
|
950
|
+
// start them. Instant bodies run here, each against its own budget, and
|
|
951
|
+
// can close the join before the drive returns.
|
|
952
|
+
const drive = await parkAndDriveForeach({
|
|
953
|
+
runId,
|
|
954
|
+
stepId,
|
|
955
|
+
parentNodeId: node.id,
|
|
956
|
+
ctx,
|
|
957
|
+
registry,
|
|
958
|
+
});
|
|
959
|
+
if (drive.joined) return { outcome: "completed", successors: drive.successors };
|
|
960
|
+
return { outcome: drive.parked ? "waiting" : "completed", successors: [] };
|
|
961
|
+
}
|
|
952
962
|
const waiting = await checkpointStepWaiting(runId, stepId, ctx);
|
|
953
963
|
// A child run that finished before the step parked had its event dropped.
|
|
954
964
|
if (waiting && (result as AsyncExecutorResult).waitFor === "workflow.child.finished") {
|