@desplega.ai/agent-swarm 2.4.4 → 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-thq04adh.js → acp-adapter-11gp67nr.js} +8 -6
- package/dist/{actions-j6ywh6x5.js → actions-8p2kze8j.js} +24 -22
- package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-z4qvhacj.js} +2 -2
- package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
- package/dist/{app-rb7vc02s.js → app-0qp0a2gk.js} +3 -3
- package/dist/{assistant-ftta1zjf.js → assistant-jedd3ktm.js} +27 -25
- package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
- package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-4xwq5tg0.js} +1 -1
- package/dist/{boot-reembed-srp6v6af.js → boot-reembed-n4wq5dd2.js} +1 -1
- package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-69h90xcq.js} +4 -4
- package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-3vn0m9sg.js} +4 -4
- package/dist/{claude-adapter-k5ksyres.js → claude-adapter-221mvad5.js} +8 -6
- package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-gt6pa35d.js} +2 -2
- package/dist/{claude-managed-setup-n5a5eqks.js → claude-managed-setup-c3hp36ph.js} +4 -4
- package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-8thjsk5e.js} +9729 -9628
- package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
- package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
- package/dist/{cli-amyhm3gj.js → cli-0y6f0ckq.js} +1 -1
- package/dist/{cli-4dg55wsk.js → cli-0z3vjrpm.js} +1 -1
- package/dist/{cli-pccfmtb5.js → cli-179xcyky.js} +11 -11
- package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
- package/dist/{cli-hj3debgw.js → cli-1pr672hz.js} +3 -3
- package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
- package/dist/{cli-t7cyef5g.js → cli-36j4jnk3.js} +4 -4
- package/dist/{cli-er83cjbn.js → cli-3qv8naq8.js} +527 -231
- package/dist/cli-3zwye9sn.js +1193 -0
- package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
- package/dist/{cli-xj0re5tm.js → cli-4w24f7z3.js} +1 -1
- package/dist/{cli-v3yr2gbg.js → cli-5kzk3472.js} +2198 -968
- package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
- package/dist/{cli-w6nadkh4.js → cli-5rvg1ddk.js} +5 -5
- package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
- package/dist/{cli-9qtjfd60.js → cli-6rg2jn2y.js} +3 -2
- package/dist/{cli-2v045xyf.js → cli-7bwtc6yx.js} +3 -3
- package/dist/{cli-3cgngyd8.js → cli-7g40g8hq.js} +35 -17
- package/dist/{cli-q6r7t299.js → cli-92mqenkc.js} +2379 -154
- package/dist/{cli-cynvc9fj.js → cli-9ce2xw98.js} +8 -8
- package/dist/{cli-gqvh8d8a.js → cli-bgdtv62r.js} +36 -22
- 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-p20r8w42.js → cli-fcc5qe3f.js} +763 -268
- package/dist/{cli-1ephgwnq.js → cli-fes51ya2.js} +3 -3
- package/dist/{cli-1d70crsy.js → cli-ftmq4nvk.js} +2 -2
- package/dist/{cli-qkhc5aqy.js → cli-fy1jyyym.js} +13 -13
- package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
- package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
- package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
- package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
- package/dist/{cli-bnz0r3kc.js → cli-j1eghqfm.js} +5 -5
- package/dist/{cli-2zqpkvtt.js → cli-jpxsrxcb.js} +1 -1
- package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
- package/dist/{cli-37yb7307.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-a0kcty51.js → cli-n6kmqngw.js} +2 -2
- package/dist/{cli-te6b5wvg.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-zrwpctp4.js → cli-r25qpaxt.js} +161 -63
- package/dist/{cli-nc35pxb7.js → cli-rh5smt8t.js} +19 -4
- package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
- package/dist/{cli-esxerzg7.js → cli-s4sntwt4.js} +2 -2
- package/dist/{cli-b8k7epj1.js → cli-segs7hx9.js} +8 -8
- package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
- package/dist/{cli-vdm20wej.js → cli-sy8f9705.js} +4 -4
- package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
- package/dist/{cli-2y91wnbh.js → cli-we0ra057.js} +222 -10
- package/dist/{cli-55qmbp6z.js → cli-x21dqn18.js} +5 -5
- package/dist/{cli-dqb4n629.js → cli-x6vf4sad.js} +11 -11
- package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
- package/dist/{cli-vq97g1jq.js → cli-xawhpgc9.js} +95 -195
- package/dist/{cli-82d3nkc3.js → cli-xd4eqtr2.js} +2 -2
- package/dist/{cli-9fhkkpj5.js → cli-xt768r5m.js} +2 -2
- package/dist/{cli-zrk6jxv8.js → cli-y4x8ww6s.js} +2 -2
- package/dist/{cli-xc27fwzg.js → cli-y9jvxfhk.js} +3 -3
- package/dist/{cli-44159k28.js → cli-z63a269p.js} +2 -2
- package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
- package/dist/cli.js +31 -29
- package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
- package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tcg1q3jk.js} +6 -4
- package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
- package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-xfx4wd9x.js} +7 -5
- package/dist/{commands-pcymaeg9.js → commands-ew83nj70.js} +2 -2
- package/dist/{controller-fpjyk46r.js → controller-gnq1k1mf.js} +4 -4
- package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-tw1dan9r.js} +26 -24
- package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-yq5xgfcg.js} +1 -1
- package/dist/{db-xsjg9jqq.js → db-7gbc53fd.js} +14 -2
- package/dist/{db-sgd12bt9.js → db-t86xrnes.js} +3 -3
- package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
- package/dist/{dispatcher-x0jfw2c9.js → dispatcher-64bdyhps.js} +23 -21
- package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
- package/dist/{dsh-adapter-jrq7ma7h.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-8njj4h3b.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-kkr97v2f.js → grok-adapter-v82srhrx.js} +9 -7
- package/dist/handlers-c9ajzhbn.js +41 -0
- package/dist/{heartbeat-wd03zhn0.js → heartbeat-2vqs1319.js} +26 -24
- package/dist/{hook-xzj2ts2p.js → hook-01nk13wx.js} +7 -5
- package/dist/{hook-f0vghj42.js → hook-jzpheegg.js} +9 -7
- package/dist/{http-4hwxv62p.js → http-fc1k6ec8.js} +859 -252
- 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-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
- package/dist/index-q3t40ryv.js +46 -0
- package/dist/{index-ww2vznkp.js → index-sepja052.js} +1 -1
- package/dist/{index-97q4xn03.js → index-zdycv3ja.js} +135 -79
- package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
- package/dist/{keepalive-hrh7xnr9.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-e8wnwnqx.js → maintenance-hjhzdt3k.js} +1 -1
- package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
- package/dist/{node-3v3f08f7.js → node-jzjj7r9x.js} +10 -9
- package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
- package/dist/{onboard-dtwcq603.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-w3fbhpn0.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-j38dehfs.js → pricing-refresh-nevhat8h.js} +3 -3
- package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-4y6ngvzq.js} +1 -1
- package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-exbg8xb6.js} +2 -2
- package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
- package/dist/{render-v2-n16n0phe.js → render-v2-cfsh13z3.js} +3 -3
- package/dist/{resume-z0fgjc38.js → resume-5s2t0dbs.js} +10 -8
- package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
- package/dist/{session-access-cv3mq6mc.js → session-access-dnbyp3cn.js} +6 -4
- package/dist/{setup-mafhj3yt.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-8b86mgjp.js → work-objects-7mg6216g.js} +26 -24
- package/dist/worker-nwb789mg.js +79 -0
- package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
- package/openapi.json +793 -6
- package/package.json +11 -11
- package/src/apps/catalog.generated.json +206 -4
- package/src/apps/page-validator.ts +13 -1
- package/src/assets/key.ts +17 -0
- package/src/azure-devops/handlers.ts +1 -1
- package/src/be/chunking.ts +42 -9
- 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/draft-text.ts +15 -0
- package/src/be/db/tasks/read.ts +54 -5
- package/src/be/db/workflow-runs.ts +96 -0
- package/src/be/db.ts +85 -237
- package/src/be/memory/index-content.ts +36 -17
- package/src/be/memory/providers/openai-embedding.ts +115 -61
- package/src/be/memory/providers/sqlite-store.ts +69 -23
- package/src/be/memory/reranker.ts +31 -1
- package/src/be/memory/search-candidates.ts +47 -0
- package/src/be/memory/types.ts +14 -0
- package/src/be/onboarding.ts +3 -0
- package/src/be/scripts/typecheck.ts +2 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
- package/src/be/seed-scripts/index.ts +2 -2
- package/src/be/skill-sync.ts +43 -2
- package/src/be/swarm-config-guard.ts +25 -1
- package/src/be/task-attachment-store.ts +30 -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/catalog.generated.json +4 -4
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +9 -2
- package/src/extensions/post-bridge.ts +1 -0
- package/src/fs/provider.ts +13 -1
- package/src/github/handlers.ts +6 -6
- package/src/gitlab/handlers.ts +5 -5
- package/src/heartbeat/heartbeat.ts +31 -11
- package/src/http/assets.ts +14 -2
- package/src/http/core.ts +12 -0
- package/src/http/fs.ts +54 -34
- package/src/http/index.ts +4 -0
- package/src/http/mcp-body.ts +37 -0
- package/src/http/mcp-user.ts +3 -5
- package/src/http/mcp.ts +6 -7
- package/src/http/memory.ts +26 -15
- package/src/http/onboarding.ts +1 -0
- package/src/http/page-proxy.ts +61 -5
- package/src/http/pages-public.ts +27 -44
- package/src/http/pages.ts +4 -4
- package/src/http/script-runs.ts +2 -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 +13 -1
- package/src/http/webhooks.ts +141 -0
- package/src/integrations/chat-sdk/adapters.ts +24 -4
- package/src/integrations/chat-sdk/attachments.ts +90 -12
- package/src/integrations/chat-sdk/facts.ts +138 -0
- package/src/integrations/chat-sdk/inbound.ts +156 -8
- package/src/integrations/chat-sdk/limits.ts +24 -0
- package/src/integrations/chat-sdk/outbound-files.ts +186 -0
- package/src/integrations/chat-sdk/outbound.ts +272 -5
- package/src/integrations/chat-sdk/relay.ts +4 -1
- package/src/integrations/chat-sdk/router.ts +28 -2
- package/src/integrations/chat-sdk/steering.ts +9 -0
- package/src/integrations/chat-sdk/templates.ts +14 -0
- package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
- package/src/mcp-limits.ts +20 -0
- package/src/pages/browser-sdk.ts +8 -3
- package/src/pages/feedback-overlay.ts +1 -1
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +106 -119
- package/src/providers/codex-agents-md.ts +35 -229
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +5 -53
- package/src/realtime/transport.ts +1 -2
- package/src/script-workflows/workflow-ctx.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +2 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +14 -4
- package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
- package/src/server-user.ts +2 -0
- package/src/server.ts +2 -0
- package/src/slack/assistant.ts +2 -2
- package/src/slack/files.ts +2 -0
- package/src/slack/handlers.test.ts +158 -0
- package/src/slack/handlers.ts +23 -32
- package/src/slack/inbound-files.ts +39 -6
- package/src/slack/thread-buffer.ts +2 -1
- package/src/tasks/context-key.ts +61 -1
- package/src/tasks/task-terminal-effects.ts +21 -17
- package/src/tests/apps-card-grid.test.ts +197 -0
- package/src/tests/apps-catalog-additions.test.ts +120 -0
- package/src/tests/apps-tree.test.ts +129 -0
- 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-attachments-reply.test.ts +52 -0
- package/src/tests/chat-sdk-config.test.ts +11 -0
- package/src/tests/chat-sdk-facts.test.ts +23 -0
- package/src/tests/chat-sdk-inbound.test.ts +597 -4
- package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
- package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
- package/src/tests/codex-adapter.test.ts +167 -242
- package/src/tests/config-api-only-keys.test.ts +9 -0
- package/src/tests/context-key.test.ts +50 -0
- package/src/tests/core-auth.test.ts +5 -0
- package/src/tests/cors-allowlist.test.ts +1 -1
- 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/extensions-example-slack-reply-gate.test.ts +262 -91
- 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/heartbeat-checklist.test.ts +33 -0
- package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
- package/src/tests/heartbeat-jev-gate.test.ts +242 -1
- package/src/tests/integration-disable-flags.test.ts +36 -0
- package/src/tests/kv-page-proxy.test.ts +91 -5
- package/src/tests/launch-password-rejection.test.ts +2 -2
- package/src/tests/mcp-input-caps.test.ts +294 -0
- package/src/tests/mcp-tools.test.ts +1 -1
- package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
- package/src/tests/memory-embedding-input-cap.test.ts +114 -0
- package/src/tests/memory.test.ts +14 -0
- package/src/tests/onboarding.test.ts +12 -0
- package/src/tests/page-guest-session.test.ts +19 -8
- package/src/tests/page-proxy-authed.test.ts +1 -1
- package/src/tests/page-proxy.test.ts +17 -10
- package/src/tests/page-session.test.ts +102 -1
- package/src/tests/pages-authed-mode.test.ts +82 -12
- package/src/tests/pages-password-mode.test.ts +5 -5
- 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/realtime-transport.test.ts +13 -7
- 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/script-runs-http.test.ts +84 -0
- package/src/tests/scripts-http.test.ts +10 -1
- 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 +23 -3
- package/src/tests/task-audio-transcription.test.ts +299 -0
- package/src/tests/task-completion-memory-chunking.test.ts +434 -0
- package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
- package/src/tests/transcription.test.ts +299 -0
- 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-system-one-decision-providers.test.ts +98 -1
- package/src/tests/workflow-system-one-decision.test.ts +2 -1
- package/src/tests/workflow-tla-races.test.ts +2 -2
- package/src/tools/chat-access.ts +10 -0
- package/src/tools/chat-post.ts +31 -11
- package/src/tools/chat-reply.ts +32 -10
- package/src/tools/memory-search.ts +3 -1
- package/src/tools/script-delete.ts +1 -1
- package/src/tools/system-one-decide.ts +17 -4
- package/src/tools/workflows/create-workflow.ts +5 -3
- package/src/types.ts +9 -2
- package/src/utils/config-env-export.ts +4 -1
- package/src/utils/page-session.ts +133 -20
- package/src/utils/secret-scrubber.ts +4 -0
- package/src/utils/skills-refresh.ts +83 -43
- package/src/utils/transcription.ts +246 -0
- 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/executors/system-one-decision.ts +28 -7
- package/src/workflows/executors/system-one-providers.ts +39 -4
- 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/extensions/slack-reply-gate/README.md +24 -22
- package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
- package/templates/extensions/slack-reply-gate/manifest.json +2 -2
- package/templates/skills/apps/SKILL.md +28 -6
- package/templates/skills/apps/content.md +28 -6
- package/templates/skills/chat-sdk/content.md +6 -2
- package/templates/skills/code-quality/content.md +2 -1
- package/templates/skills/heartbeat-runbook/content.md +5 -5
- package/templates/skills/pages/content.md +6 -4
- package/templates/skills/slack-interaction/content.md +5 -0
- package/templates/skills/swarm-scripts/SKILL.md +2 -2
- package/templates/skills/swarm-scripts/content.md +2 -2
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/dist/cli-51y09c6p.js +0 -2657
- package/dist/cli-yw91rnmw.js +0 -13
- package/dist/handlers-0pvee9nw.js +0 -39
- package/dist/index-gdh8jrdm.js +0 -43
- package/dist/index-hg0x2cdv.js +0 -40
- package/dist/index-thm0y6a8.js +0 -41
- package/dist/lead-ee8vazz2.js +0 -77
- package/dist/worker-16s75hm5.js +0 -77
|
@@ -1,27 +1,64 @@
|
|
|
1
1
|
import {
|
|
2
|
+
telemetry,
|
|
3
|
+
init_telemetry,
|
|
4
|
+
scrubJsonValue,
|
|
5
|
+
init_scrub_json,
|
|
6
|
+
getDbClient2,
|
|
2
7
|
getTaskById2,
|
|
8
|
+
createForeachChildSteps,
|
|
9
|
+
init_workflow_runs,
|
|
10
|
+
getOAuthApp,
|
|
11
|
+
getOAuthTokens,
|
|
12
|
+
init_oauth,
|
|
13
|
+
WorkflowReplayStateError,
|
|
14
|
+
init_workflow_replay,
|
|
3
15
|
createTaskExtended2,
|
|
16
|
+
getSwarmConfigs2,
|
|
17
|
+
getWorkflow2,
|
|
18
|
+
createWorkflowRun2,
|
|
19
|
+
getWorkflowRun2,
|
|
20
|
+
updateWorkflowRun2,
|
|
21
|
+
createWorkflowRunStep2,
|
|
22
|
+
getWorkflowRunStep2,
|
|
23
|
+
updateWorkflowRunStep2,
|
|
24
|
+
getWorkflowRunStepsByRunId2,
|
|
25
|
+
getLastSuccessfulRun2,
|
|
26
|
+
getLastRunStart2,
|
|
27
|
+
getCompletedStepNodeIds2,
|
|
28
|
+
getTaskByWorkflowRunStepId2,
|
|
29
|
+
detachTaskFromWorkflowRunStep2,
|
|
30
|
+
getStepByIdempotencyKey2,
|
|
31
|
+
getStepCountForNode2,
|
|
32
|
+
getLatestStepForNode2,
|
|
33
|
+
getCurrentStepForNode2,
|
|
4
34
|
claimApprovalCancellationNotification2,
|
|
5
35
|
completeApprovalCancellationNotificationClaim2,
|
|
6
36
|
releaseApprovalCancellationNotificationClaim2,
|
|
7
37
|
init_db3
|
|
8
|
-
} from "./cli-
|
|
38
|
+
} from "./cli-fcc5qe3f.js";
|
|
9
39
|
import {
|
|
10
40
|
getSlackApp2
|
|
11
|
-
} from "./cli-
|
|
41
|
+
} from "./cli-fy1jyyym.js";
|
|
12
42
|
import {
|
|
13
43
|
DEFAULT_MODEL,
|
|
14
44
|
resolveCredential
|
|
15
|
-
} from "./cli-
|
|
45
|
+
} from "./cli-qqzmjxkf.js";
|
|
16
46
|
import {
|
|
17
47
|
TERMINAL_TASK_STATUSES,
|
|
18
48
|
init_types
|
|
19
|
-
} from "./cli-
|
|
49
|
+
} from "./cli-rh5smt8t.js";
|
|
50
|
+
import {
|
|
51
|
+
getSlackConfiguration,
|
|
52
|
+
isSlackConfigured,
|
|
53
|
+
init_config
|
|
54
|
+
} from "./cli-qeaq3j6t.js";
|
|
20
55
|
import {
|
|
21
56
|
selectCredential,
|
|
22
57
|
init_credentials
|
|
23
58
|
} from "./cli-zw88zjba.js";
|
|
24
59
|
import {
|
|
60
|
+
deepInterpolate,
|
|
61
|
+
init_template,
|
|
25
62
|
resolveTemplate,
|
|
26
63
|
init_resolver
|
|
27
64
|
} from "./cli-tx00c0h6.js";
|
|
@@ -55,9 +92,14 @@ import {
|
|
|
55
92
|
init_constants2
|
|
56
93
|
} from "./cli-gf2850q7.js";
|
|
57
94
|
import {
|
|
95
|
+
isEnvFlagEnabled,
|
|
96
|
+
init_env_flag
|
|
97
|
+
} from "./cli-cebvbc9t.js";
|
|
98
|
+
import {
|
|
99
|
+
isSensitiveKey,
|
|
58
100
|
scrubSecrets,
|
|
59
101
|
init_secret_scrubber
|
|
60
|
-
} from "./cli-
|
|
102
|
+
} from "./cli-n4c8e3s5.js";
|
|
61
103
|
import {
|
|
62
104
|
providerKeyVars,
|
|
63
105
|
providerBaseUrl,
|
|
@@ -65,6 +107,300 @@ import {
|
|
|
65
107
|
init_src2
|
|
66
108
|
} from "./cli-nwyw3zhg.js";
|
|
67
109
|
|
|
110
|
+
// src/automation-preflight-alert.ts
|
|
111
|
+
init_constants2();
|
|
112
|
+
init_secret_scrubber();
|
|
113
|
+
async function notifyAutomationPreflightFailure(preflight) {
|
|
114
|
+
const channel = process.env.SLACK_ALERTS_CHANNEL?.trim();
|
|
115
|
+
if (!channel) {
|
|
116
|
+
console.warn("[Automation Preflight] SLACK_ALERTS_CHANNEL not set; skipping alert");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const { getSlackApp } = await import("./app-0qp0a2gk.js");
|
|
121
|
+
const app = getSlackApp();
|
|
122
|
+
if (!app) {
|
|
123
|
+
console.warn("[Automation Preflight] Slack not available, cannot send notification");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
await app.client.chat.postMessage({
|
|
127
|
+
channel,
|
|
128
|
+
text: scrubSecrets(`⚠️ Automation needs setup
|
|
129
|
+
` + `Automation: ${preflight.kind} "${preflight.name}"
|
|
130
|
+
Missing params: ${preflight.missing.params.join(", ") || "none"}
|
|
131
|
+
Missing integrations: ${preflight.missing.integrations.join(", ") || "none"}
|
|
132
|
+
Fire SKIPPED. It was not retried or queued.
|
|
133
|
+
Fix setup: ${getAppUrl()}${preflight.fixUrl}`),
|
|
134
|
+
unfurl_links: false,
|
|
135
|
+
unfurl_media: false
|
|
136
|
+
});
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error("[Automation Preflight] Failed to send Slack notification:", scrubSecrets(error instanceof Error ? error.message : String(error)));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/be/automation-preflight.ts
|
|
143
|
+
init_config();
|
|
144
|
+
init_env_flag();
|
|
145
|
+
init_secret_scrubber();
|
|
146
|
+
init_db3();
|
|
147
|
+
init_oauth();
|
|
148
|
+
init_scrub_json();
|
|
149
|
+
var NEEDS_SETUP_PREFIX = "needs_setup:";
|
|
150
|
+
function hasValue(value) {
|
|
151
|
+
if (value == null)
|
|
152
|
+
return false;
|
|
153
|
+
if (typeof value === "string")
|
|
154
|
+
return value.trim().length > 0;
|
|
155
|
+
if (Array.isArray(value))
|
|
156
|
+
return value.length > 0;
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
var GITHUB_REPOSITORY_SLUG = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9][A-Za-z0-9._-]{0,99}(?:\.git)?$/;
|
|
160
|
+
var SAFE_SHELL_DATA = /^[A-Za-z0-9._~%/:@ -]+$/;
|
|
161
|
+
var DNS_NAME = /^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$/;
|
|
162
|
+
var SAFE_EXTERNAL_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
163
|
+
var SAFE_GIT_REF = /^(?!-)(?!.*(?:\.\.|@\{|\/\/))[A-Za-z0-9._/-]+$/;
|
|
164
|
+
var SAFE_TAG_PATTERN = /^(?!-)(?!.*(?:\.\.|\/\/))[A-Za-z0-9._*?/-]+$/;
|
|
165
|
+
var SAFE_RELATIVE_PATH = /^(?![-/])(?!.*(?:^|\/)\.\.(?:\/|$))[A-Za-z0-9._/-]+$/;
|
|
166
|
+
var SAFE_REPORT_NAME = /^(?!\.{1,2}$)[A-Za-z0-9][A-Za-z0-9._ -]{0,127}$/;
|
|
167
|
+
function isSafeRepository(value) {
|
|
168
|
+
if (!SAFE_SHELL_DATA.test(value) || value.trim() !== value || value.includes(" "))
|
|
169
|
+
return false;
|
|
170
|
+
const slug = value.replace(/^https:\/\/github\.com\//, "").replace(/^git@github\.com:/, "").replace(/\/$/, "");
|
|
171
|
+
return GITHUB_REPOSITORY_SLUG.test(slug);
|
|
172
|
+
}
|
|
173
|
+
function isSafeGscProperty(value) {
|
|
174
|
+
if (!SAFE_SHELL_DATA.test(value) || value.trim() !== value)
|
|
175
|
+
return false;
|
|
176
|
+
return value.split(" ").every((property) => {
|
|
177
|
+
if (!property)
|
|
178
|
+
return false;
|
|
179
|
+
if (property.startsWith("sc-domain:"))
|
|
180
|
+
return DNS_NAME.test(property.slice(10));
|
|
181
|
+
if (DNS_NAME.test(property))
|
|
182
|
+
return true;
|
|
183
|
+
try {
|
|
184
|
+
const url = new URL(property);
|
|
185
|
+
return url.protocol === "https:" && !url.username && !url.password && !url.port && !url.search && !url.hash && DNS_NAME.test(url.hostname);
|
|
186
|
+
} catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
var PARAM_VALIDATORS = {
|
|
192
|
+
REPO_URL: (value) => typeof value === "string" && isSafeRepository(value),
|
|
193
|
+
GSC_PROPERTY: (value) => typeof value === "string" && isSafeGscProperty(value),
|
|
194
|
+
BRANCH: (value) => typeof value === "string" && SAFE_GIT_REF.test(value),
|
|
195
|
+
SCOPE_PATH: (value) => typeof value === "string" && SAFE_RELATIVE_PATH.test(value),
|
|
196
|
+
REPORT_NAME: (value) => typeof value === "string" && value.trim() === value && SAFE_REPORT_NAME.test(value),
|
|
197
|
+
TAG_PATTERN: (value) => typeof value === "string" && SAFE_TAG_PATTERN.test(value),
|
|
198
|
+
AGENT_FS_ORG_ID: (value) => typeof value === "string" && SAFE_EXTERNAL_ID.test(value),
|
|
199
|
+
ORG_ID: (value) => typeof value === "string" && SAFE_EXTERNAL_ID.test(value)
|
|
200
|
+
};
|
|
201
|
+
function isUsableParam(key, value) {
|
|
202
|
+
if (!hasValue(value))
|
|
203
|
+
return false;
|
|
204
|
+
return PARAM_VALIDATORS[key]?.(value) ?? true;
|
|
205
|
+
}
|
|
206
|
+
function uniqueSorted(values) {
|
|
207
|
+
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
208
|
+
}
|
|
209
|
+
function automationUrl(input) {
|
|
210
|
+
return input.kind === "schedule" ? `/schedules/${input.id}` : `/workflows/${input.id}`;
|
|
211
|
+
}
|
|
212
|
+
var INTEGRATION_FIX_URL = {
|
|
213
|
+
slack: "/settings/integrations/slack",
|
|
214
|
+
github: "/settings/integrations/github",
|
|
215
|
+
linear: "/settings/integrations/linear",
|
|
216
|
+
jira: "/settings/integrations/jira",
|
|
217
|
+
agentmail: "/settings/integrations/agentmail",
|
|
218
|
+
gsc: "/settings/integrations/gsc",
|
|
219
|
+
agentfs: "/settings/integrations/agentfs"
|
|
220
|
+
};
|
|
221
|
+
var INTEGRATION_FIRST_REQUIREMENTS = new Set([
|
|
222
|
+
"gsc",
|
|
223
|
+
"linear",
|
|
224
|
+
"agentmail"
|
|
225
|
+
]);
|
|
226
|
+
function automationIntegrationFixUrl(id) {
|
|
227
|
+
return INTEGRATION_FIX_URL[id];
|
|
228
|
+
}
|
|
229
|
+
function isNeedsSetupFailure(reason) {
|
|
230
|
+
return reason?.startsWith(NEEDS_SETUP_PREFIX) ?? false;
|
|
231
|
+
}
|
|
232
|
+
function preflightAutomation(input, setup) {
|
|
233
|
+
const params = input.params ?? {};
|
|
234
|
+
const guardedParams = Object.keys(params).filter((key) => (key in PARAM_VALIDATORS));
|
|
235
|
+
const missingParams = uniqueSorted([...input.requiredParams ?? [], ...guardedParams].filter((key) => !isUsableParam(key, params[key])));
|
|
236
|
+
const missingIntegrations = uniqueSorted((input.requires ?? []).filter((id) => setup[id] === "unverified"));
|
|
237
|
+
const missing = { params: missingParams, integrations: missingIntegrations };
|
|
238
|
+
const baseUrl = automationUrl(input);
|
|
239
|
+
const fixes = [
|
|
240
|
+
...missingParams.map((key) => ({
|
|
241
|
+
type: "param",
|
|
242
|
+
key,
|
|
243
|
+
url: `${baseUrl}?param=${encodeURIComponent(key)}`
|
|
244
|
+
})),
|
|
245
|
+
...missingIntegrations.map((key) => ({
|
|
246
|
+
type: "integration",
|
|
247
|
+
key,
|
|
248
|
+
url: automationIntegrationFixUrl(key)
|
|
249
|
+
}))
|
|
250
|
+
];
|
|
251
|
+
if (missingParams.length === 0 && missingIntegrations.length === 0) {
|
|
252
|
+
return {
|
|
253
|
+
id: input.id,
|
|
254
|
+
name: input.name,
|
|
255
|
+
kind: input.kind,
|
|
256
|
+
state: "running",
|
|
257
|
+
missing,
|
|
258
|
+
fixes,
|
|
259
|
+
fixUrl: baseUrl
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const firstParam = missingParams[0];
|
|
263
|
+
const firstIntegration = (input.requires ?? []).find((id) => missingIntegrations.includes(id));
|
|
264
|
+
const integrationFirst = (input.requires ?? []).some((id) => INTEGRATION_FIRST_REQUIREMENTS.has(id));
|
|
265
|
+
const fixUrl = integrationFirst && firstIntegration ? automationIntegrationFixUrl(firstIntegration) : firstParam ? `${baseUrl}?param=${encodeURIComponent(firstParam)}` : firstIntegration ? automationIntegrationFixUrl(firstIntegration) : baseUrl;
|
|
266
|
+
const failureReason = `${NEEDS_SETUP_PREFIX} params=[${missingParams.join(",")}] integrations=[${missingIntegrations.join(",")}]`;
|
|
267
|
+
return {
|
|
268
|
+
id: input.id,
|
|
269
|
+
name: input.name,
|
|
270
|
+
kind: input.kind,
|
|
271
|
+
state: "needs_setup",
|
|
272
|
+
missing,
|
|
273
|
+
fixes,
|
|
274
|
+
fixUrl,
|
|
275
|
+
failureReason
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
var EXACT_TOKEN = /^\{\{\s*([^}]+?)\s*\}\}$/;
|
|
279
|
+
var TOKEN = /\{\{\s*([^}]+?)\s*\}\}/g;
|
|
280
|
+
function renderAutomationTokens(value, params) {
|
|
281
|
+
function render(current) {
|
|
282
|
+
if (typeof current === "string") {
|
|
283
|
+
const exact = EXACT_TOKEN.exec(current);
|
|
284
|
+
if (exact?.[1] && Object.hasOwn(params, exact[1]))
|
|
285
|
+
return params[exact[1]];
|
|
286
|
+
return current.replace(TOKEN, (token, key) => {
|
|
287
|
+
if (!Object.hasOwn(params, key))
|
|
288
|
+
return token;
|
|
289
|
+
const replacement = params[key];
|
|
290
|
+
return typeof replacement === "object" ? JSON.stringify(replacement) : String(replacement);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (Array.isArray(current))
|
|
294
|
+
return current.map(render);
|
|
295
|
+
if (current != null && typeof current === "object") {
|
|
296
|
+
return Object.fromEntries(Object.entries(current).map(([key, nested]) => [key, render(nested)]));
|
|
297
|
+
}
|
|
298
|
+
return current;
|
|
299
|
+
}
|
|
300
|
+
return render(value);
|
|
301
|
+
}
|
|
302
|
+
function present(value) {
|
|
303
|
+
return !!value?.trim();
|
|
304
|
+
}
|
|
305
|
+
async function getAutomationSetupStates() {
|
|
306
|
+
const [linearTokens, jiraTokens, jiraApp] = await Promise.all([
|
|
307
|
+
getOAuthTokens("linear"),
|
|
308
|
+
getOAuthTokens("jira"),
|
|
309
|
+
getOAuthApp("jira")
|
|
310
|
+
]);
|
|
311
|
+
let jiraCloudId = false;
|
|
312
|
+
try {
|
|
313
|
+
const metadata = jiraApp?.metadata ? JSON.parse(jiraApp.metadata) : null;
|
|
314
|
+
jiraCloudId = !!(metadata && typeof metadata === "object" && metadata.cloudId);
|
|
315
|
+
} catch {
|
|
316
|
+
jiraCloudId = false;
|
|
317
|
+
}
|
|
318
|
+
const agentFsKey = process.env.API_AGENT_FS_API_KEY ?? process.env.AGENT_FS_API_KEY;
|
|
319
|
+
const agentFsOrg = process.env.AGENT_FS_DEFAULT_ORG_ID ?? process.env.AGENT_FS_SHARED_ORG_ID;
|
|
320
|
+
const gscConfigured = present(process.env.GSC_SERVICE_ACCOUNT_BASE64) || present(process.env.GSC_SERVICE_ACCOUNT_JSON);
|
|
321
|
+
return {
|
|
322
|
+
slack: getSlackConfiguration().mode === "socket" && isSlackConfigured() ? "configured" : "unverified",
|
|
323
|
+
github: present(process.env.GITHUB_WEBHOOK_SECRET) && present(process.env.GITHUB_APP_ID) && present(process.env.GITHUB_APP_PRIVATE_KEY) ? "verified" : "unverified",
|
|
324
|
+
linear: linearTokens ? "verified" : "unverified",
|
|
325
|
+
jira: jiraTokens && jiraCloudId ? "verified" : "unverified",
|
|
326
|
+
gsc: gscConfigured ? "verified" : "unverified",
|
|
327
|
+
agentmail: !isEnvFlagEnabled("AGENTMAIL_DISABLE", false) && present(process.env.AGENTMAIL_API_KEY) ? "verified" : "unverified",
|
|
328
|
+
agentfs: present(process.env.AGENT_FS_API_URL) && present(agentFsKey) && present(agentFsOrg) && present(process.env.AGENT_FS_DEFAULT_DRIVE_ID) ? "verified" : "unverified"
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function schedulePreflightInput(schedule) {
|
|
332
|
+
return { ...schedule, kind: "schedule" };
|
|
333
|
+
}
|
|
334
|
+
function workflowPreflightInput(workflow) {
|
|
335
|
+
return { ...workflow, kind: "workflow" };
|
|
336
|
+
}
|
|
337
|
+
async function listEnabledAutomationPreflightInputs() {
|
|
338
|
+
const rows = await getDbClient2().query(`SELECT id, name, 'schedule' AS kind, params, requiredParams, requires
|
|
339
|
+
FROM scheduled_tasks
|
|
340
|
+
WHERE enabled = 1
|
|
341
|
+
UNION ALL
|
|
342
|
+
SELECT id, name, 'workflow' AS kind, params, requiredParams, requires
|
|
343
|
+
FROM workflows
|
|
344
|
+
WHERE enabled = 1`);
|
|
345
|
+
return rows.map((row) => ({
|
|
346
|
+
id: row.id,
|
|
347
|
+
name: row.name,
|
|
348
|
+
kind: row.kind,
|
|
349
|
+
params: JSON.parse(row.params),
|
|
350
|
+
requiredParams: JSON.parse(row.requiredParams),
|
|
351
|
+
requires: JSON.parse(row.requires)
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
function utcDayBounds(now) {
|
|
355
|
+
const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
356
|
+
return { start: start.toISOString(), end: new Date(start.getTime() + 86400000).toISOString() };
|
|
357
|
+
}
|
|
358
|
+
async function recordSchedulePreflightFailure(scheduleId, failureReason, now = new Date) {
|
|
359
|
+
const message = scrubSecrets(failureReason);
|
|
360
|
+
return await getDbClient2().transaction(async (tx) => {
|
|
361
|
+
const row = await tx.get("SELECT lastErrorAt, lastErrorMessage FROM scheduled_tasks WHERE id = ?", [scheduleId]);
|
|
362
|
+
if (!row)
|
|
363
|
+
return false;
|
|
364
|
+
if (row.lastErrorMessage === message && row.lastErrorAt?.slice(0, 10) === now.toISOString().slice(0, 10)) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
await tx.run(`UPDATE scheduled_tasks
|
|
368
|
+
SET lastErrorAt = ?, lastErrorMessage = ?, lastUpdatedAt = ?
|
|
369
|
+
WHERE id = ?`, [now.toISOString(), message, now.toISOString(), scheduleId]);
|
|
370
|
+
return true;
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
async function recordWorkflowPreflightFailure(input) {
|
|
374
|
+
const now = input.now ?? new Date;
|
|
375
|
+
const bounds = utcDayBounds(now);
|
|
376
|
+
return await getDbClient2().transaction(async (tx) => {
|
|
377
|
+
const existing = await tx.get(`SELECT id FROM workflow_runs
|
|
378
|
+
WHERE workflowId = ? AND status = 'failed'
|
|
379
|
+
AND error LIKE 'needs_setup:%'
|
|
380
|
+
AND startedAt >= ? AND startedAt < ?
|
|
381
|
+
ORDER BY startedAt ASC LIMIT 1`, [input.workflowId, bounds.start, bounds.end]);
|
|
382
|
+
if (existing)
|
|
383
|
+
return { runId: existing.id, recorded: false };
|
|
384
|
+
const runId = crypto.randomUUID();
|
|
385
|
+
const timestamp = now.toISOString();
|
|
386
|
+
await tx.run(`INSERT INTO workflow_runs
|
|
387
|
+
(id, workflowId, status, triggerType, triggerData, error, created_by,
|
|
388
|
+
startedAt, lastUpdatedAt, finishedAt)
|
|
389
|
+
VALUES (?, ?, 'failed', ?, ?, ?, ?, ?, ?, ?)`, [
|
|
390
|
+
runId,
|
|
391
|
+
input.workflowId,
|
|
392
|
+
input.triggerType,
|
|
393
|
+
input.triggerData === undefined ? null : scrubJsonValue(input.triggerData),
|
|
394
|
+
scrubSecrets(input.failureReason),
|
|
395
|
+
input.createdBy ?? null,
|
|
396
|
+
timestamp,
|
|
397
|
+
timestamp,
|
|
398
|
+
timestamp
|
|
399
|
+
]);
|
|
400
|
+
return { runId, recorded: true };
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
68
404
|
// src/workflows/executors/system-one-decision.ts
|
|
69
405
|
init_zod();
|
|
70
406
|
init_secret_scrubber();
|
|
@@ -454,7 +790,7 @@ class HumanInTheLoopExecutor extends BaseExecutor {
|
|
|
454
790
|
}
|
|
455
791
|
if (notification.channel === "slack") {
|
|
456
792
|
try {
|
|
457
|
-
const { getSlackApp } = await import("./app-
|
|
793
|
+
const { getSlackApp } = await import("./app-0qp0a2gk.js");
|
|
458
794
|
const slackApp = getSlackApp();
|
|
459
795
|
if (!slackApp) {
|
|
460
796
|
console.warn("[HITL] Slack not initialized — cannot send notification");
|
|
@@ -903,15 +1239,28 @@ var SYSTEM_ONE_PROVIDER_IDS = hostsServing("decide");
|
|
|
903
1239
|
var SYSTEM_ONE_EXTRACT_PROVIDER_IDS = hostsServing("extract");
|
|
904
1240
|
var SYSTEM_ONE_DEFAULT_PROVIDER = "typesafe";
|
|
905
1241
|
var SYSTEM_ONE_EXTRACT_DEFAULT_PROVIDER = "cloudflare";
|
|
1242
|
+
var SYSTEM_ONE_DEFAULT_PROVIDER_CONFIG_KEY = "SYSTEM_ONE_DEFAULT_PROVIDER";
|
|
906
1243
|
function isSystemOneProviderId(value) {
|
|
907
1244
|
return typeof value === "string" && SYSTEM_ONE_PROVIDER_IDS.includes(value);
|
|
908
1245
|
}
|
|
1246
|
+
async function resolveSystemOneDefaultProvider(ctx) {
|
|
1247
|
+
let stored;
|
|
1248
|
+
try {
|
|
1249
|
+
stored = await readGlobalConfig(ctx.db, SYSTEM_ONE_DEFAULT_PROVIDER_CONFIG_KEY);
|
|
1250
|
+
} catch {}
|
|
1251
|
+
for (const raw of [stored, ctx.env[SYSTEM_ONE_DEFAULT_PROVIDER_CONFIG_KEY]]) {
|
|
1252
|
+
const id = raw?.trim().toLowerCase();
|
|
1253
|
+
if (isSystemOneProviderId(id))
|
|
1254
|
+
return id;
|
|
1255
|
+
}
|
|
1256
|
+
return SYSTEM_ONE_DEFAULT_PROVIDER;
|
|
1257
|
+
}
|
|
909
1258
|
function isSystemOneExtractProviderId(value) {
|
|
910
1259
|
return typeof value === "string" && SYSTEM_ONE_EXTRACT_PROVIDER_IDS.includes(value);
|
|
911
1260
|
}
|
|
912
|
-
function systemOneProviderOf(config) {
|
|
1261
|
+
function systemOneProviderOf(config, defaultId = SYSTEM_ONE_DEFAULT_PROVIDER) {
|
|
913
1262
|
if (config.provider === undefined)
|
|
914
|
-
return
|
|
1263
|
+
return defaultId;
|
|
915
1264
|
return isSystemOneProviderId(config.provider) ? config.provider : undefined;
|
|
916
1265
|
}
|
|
917
1266
|
function systemOneExtractProviderOf(config) {
|
|
@@ -1014,7 +1363,7 @@ async function resolveSystemOneTarget(id, ctx, overrides = {}) {
|
|
|
1014
1363
|
const service = serviceOf(id, capability);
|
|
1015
1364
|
const deploymentProblem = host.deploymentProblem?.(ctx.env);
|
|
1016
1365
|
if (deploymentProblem)
|
|
1017
|
-
return { ok: false, error: deploymentProblem };
|
|
1366
|
+
return { ok: false, error: deploymentProblem, deploymentRefused: true };
|
|
1018
1367
|
const errors = [];
|
|
1019
1368
|
let endpoint;
|
|
1020
1369
|
if (typeof service.endpoint === "string") {
|
|
@@ -1938,7 +2287,7 @@ var HumanReviewSchema = strictObject2({
|
|
|
1938
2287
|
notifications: array2(NotificationConfigSchema).optional()
|
|
1939
2288
|
});
|
|
1940
2289
|
var SystemOneDecisionConfigSchema = strictObject2({
|
|
1941
|
-
provider: _enum2(SYSTEM_ONE_PROVIDER_IDS).
|
|
2290
|
+
provider: _enum2(SYSTEM_ONE_PROVIDER_IDS).optional(),
|
|
1942
2291
|
model: string2().min(1).optional(),
|
|
1943
2292
|
state: SystemOneStateSchema,
|
|
1944
2293
|
timeoutMs: number2().int().min(SYSTEM_ONE_MIN_TIMEOUT_MS).max(SYSTEM_ONE_MAX_TIMEOUT_MS).default(SYSTEM_ONE_DEFAULT_TIMEOUT_MS),
|
|
@@ -1987,6 +2336,8 @@ var SystemOneDecisionConfigSchema = strictObject2({
|
|
|
1987
2336
|
});
|
|
1988
2337
|
}
|
|
1989
2338
|
}
|
|
2339
|
+
if (config.provider === undefined)
|
|
2340
|
+
return;
|
|
1990
2341
|
const modelProblem = systemOneModelProblem(config.provider, config.model ?? SYSTEM_ONE_PROVIDERS[config.provider].defaultModel);
|
|
1991
2342
|
if (modelProblem)
|
|
1992
2343
|
ctx.addIssue({ code: "custom", path: ["model"], message: modelProblem });
|
|
@@ -2373,6 +2724,9 @@ class SystemOneDecisionExecutor extends BaseExecutor {
|
|
|
2373
2724
|
providerContext() {
|
|
2374
2725
|
return { db: this.deps.db, env: this.options.env ?? process.env };
|
|
2375
2726
|
}
|
|
2727
|
+
defaultProvider() {
|
|
2728
|
+
return resolveSystemOneDefaultProvider(this.providerContext());
|
|
2729
|
+
}
|
|
2376
2730
|
resolveTarget(id, model) {
|
|
2377
2731
|
return resolveSystemOneTarget(id, this.providerContext(), {
|
|
2378
2732
|
apiKey: this.options.getApiKey,
|
|
@@ -2382,8 +2736,9 @@ class SystemOneDecisionExecutor extends BaseExecutor {
|
|
|
2382
2736
|
}
|
|
2383
2737
|
async checkReadiness(nodes) {
|
|
2384
2738
|
const nodeIdsByProvider = new Map;
|
|
2739
|
+
const defaultId = await this.defaultProvider();
|
|
2385
2740
|
for (const node of nodes) {
|
|
2386
|
-
const id = systemOneProviderOf(node.config);
|
|
2741
|
+
const id = systemOneProviderOf(node.config, defaultId);
|
|
2387
2742
|
if (id)
|
|
2388
2743
|
nodeIdsByProvider.set(id, [...nodeIdsByProvider.get(id) ?? [], node.id]);
|
|
2389
2744
|
}
|
|
@@ -2410,15 +2765,18 @@ class SystemOneDecisionExecutor extends BaseExecutor {
|
|
|
2410
2765
|
return this.reviewDecision(config, config.humanReview, decided.output, context, meta);
|
|
2411
2766
|
}
|
|
2412
2767
|
async decide(config) {
|
|
2413
|
-
const providerId = config.provider;
|
|
2768
|
+
const providerId = config.provider ?? await this.defaultProvider();
|
|
2414
2769
|
const provider = SYSTEM_ONE_PROVIDERS[providerId];
|
|
2415
2770
|
const model = config.model ?? provider.defaultModel;
|
|
2416
2771
|
const modelProblem = systemOneModelProblem(providerId, model);
|
|
2417
2772
|
if (modelProblem)
|
|
2418
2773
|
return { ok: false, error: modelProblem };
|
|
2774
|
+
const limitProblem = systemOneQuestionLimitProblem(providerId, Object.keys(config.questions).length);
|
|
2775
|
+
if (limitProblem)
|
|
2776
|
+
return { ok: false, error: limitProblem };
|
|
2419
2777
|
const target = await this.resolveTarget(providerId, model);
|
|
2420
2778
|
if (!target.ok)
|
|
2421
|
-
return
|
|
2779
|
+
return target;
|
|
2422
2780
|
const { apiKey, endpoint, keyName } = target;
|
|
2423
2781
|
const wire = provider.wire ?? SYSTEM_ONE_WIRE;
|
|
2424
2782
|
const body = JSON.stringify(wire.buildRequest(config, model));
|
|
@@ -2525,162 +2883,2021 @@ class SystemOneDecisionExecutor extends BaseExecutor {
|
|
|
2525
2883
|
}
|
|
2526
2884
|
}
|
|
2527
2885
|
|
|
2528
|
-
// src/workflows/
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2886
|
+
// src/workflows/foreach-join.ts
|
|
2887
|
+
init_db3();
|
|
2888
|
+
|
|
2889
|
+
// src/workflows/checkpoint.ts
|
|
2890
|
+
init_db3();
|
|
2891
|
+
async function checkpointStep(runId, stepId, nodeId, result, ctx) {
|
|
2892
|
+
await getDbClient2().transaction(async () => {
|
|
2893
|
+
await updateWorkflowRunStep2(stepId, {
|
|
2894
|
+
status: "completed",
|
|
2895
|
+
output: result.output,
|
|
2896
|
+
nextPort: result.nextPort || undefined,
|
|
2897
|
+
finishedAt: new Date().toISOString()
|
|
2898
|
+
});
|
|
2899
|
+
ctx[nodeId] = result.output;
|
|
2900
|
+
await updateWorkflowRun2(runId, {
|
|
2901
|
+
context: ctx
|
|
2902
|
+
});
|
|
2903
|
+
});
|
|
2535
2904
|
}
|
|
2536
|
-
function
|
|
2537
|
-
const
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2905
|
+
async function checkpointStepFailure(runId, stepId, error, retryCount, retryPolicy, options) {
|
|
2906
|
+
const now = new Date().toISOString();
|
|
2907
|
+
if (retryPolicy && retryCount < retryPolicy.maxRetries) {
|
|
2908
|
+
const delay = calculateBackoff(retryPolicy, retryCount);
|
|
2909
|
+
const nextRetryAt = new Date(Date.now() + delay).toISOString();
|
|
2910
|
+
await updateWorkflowRunStep2(stepId, {
|
|
2911
|
+
status: "failed",
|
|
2912
|
+
error,
|
|
2913
|
+
retryCount: retryCount + 1,
|
|
2914
|
+
maxRetries: retryPolicy.maxRetries,
|
|
2915
|
+
nextRetryAt
|
|
2916
|
+
});
|
|
2917
|
+
return { shouldRetry: true };
|
|
2918
|
+
}
|
|
2919
|
+
await updateWorkflowRunStep2(stepId, {
|
|
2920
|
+
status: "failed",
|
|
2921
|
+
error,
|
|
2922
|
+
finishedAt: now,
|
|
2923
|
+
nextRetryAt: null
|
|
2924
|
+
});
|
|
2925
|
+
const markRunFailed = options?.markRunFailed ?? true;
|
|
2926
|
+
if (markRunFailed) {
|
|
2927
|
+
await getDbClient2().transaction(async () => {
|
|
2928
|
+
const run = await getWorkflowRun2(runId);
|
|
2929
|
+
if (run?.status !== "running" && run?.status !== "waiting")
|
|
2930
|
+
return;
|
|
2931
|
+
await updateWorkflowRun2(runId, {
|
|
2932
|
+
status: "failed",
|
|
2933
|
+
error: `Step failed: ${error}`,
|
|
2934
|
+
finishedAt: now
|
|
2547
2935
|
});
|
|
2548
|
-
}
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2936
|
+
});
|
|
2937
|
+
}
|
|
2938
|
+
return { shouldRetry: false };
|
|
2939
|
+
}
|
|
2940
|
+
async function checkpointStepWaiting(runId, stepId, ctx, options = {}) {
|
|
2941
|
+
return getDbClient2().transaction(async () => {
|
|
2942
|
+
const step = await getDbClient2().get(`UPDATE workflow_run_steps
|
|
2943
|
+
SET status = 'waiting'
|
|
2944
|
+
WHERE id = ? AND runId = ? AND status = 'running'
|
|
2945
|
+
AND EXISTS (
|
|
2946
|
+
SELECT 1 FROM workflow_runs
|
|
2947
|
+
WHERE id = ? AND status IN ('running', 'waiting')
|
|
2948
|
+
)
|
|
2949
|
+
RETURNING id`, [stepId, runId, runId]);
|
|
2950
|
+
if (!step)
|
|
2951
|
+
return false;
|
|
2952
|
+
await updateWorkflowRun2(runId, options.keepRunStatus ? { context: ctx } : { status: "waiting", context: ctx });
|
|
2953
|
+
return true;
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
function calculateBackoff(policy, attempt) {
|
|
2957
|
+
let delay;
|
|
2958
|
+
switch (policy.strategy) {
|
|
2959
|
+
case "exponential": {
|
|
2960
|
+
const base = policy.baseDelayMs * 2 ** attempt;
|
|
2961
|
+
delay = Math.random() * Math.min(base, policy.maxDelayMs);
|
|
2962
|
+
break;
|
|
2566
2963
|
}
|
|
2964
|
+
case "linear":
|
|
2965
|
+
delay = policy.baseDelayMs * (attempt + 1);
|
|
2966
|
+
break;
|
|
2967
|
+
case "static":
|
|
2968
|
+
delay = policy.baseDelayMs;
|
|
2969
|
+
break;
|
|
2970
|
+
default:
|
|
2971
|
+
delay = policy.baseDelayMs;
|
|
2567
2972
|
}
|
|
2568
|
-
return
|
|
2973
|
+
return Math.min(delay, policy.maxDelayMs);
|
|
2569
2974
|
}
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2975
|
+
|
|
2976
|
+
// src/workflows/foreach-driver.ts
|
|
2977
|
+
init_db3();
|
|
2978
|
+
|
|
2979
|
+
// src/workflows/constants.ts
|
|
2980
|
+
var FAILED_TASK_OUTPUT_PREFIX = "[FAILED:";
|
|
2981
|
+
var FOREACH_ITEMS_WAIT_FOR = "foreach.items";
|
|
2982
|
+
|
|
2983
|
+
// src/workflows/engine.ts
|
|
2984
|
+
init_db3();
|
|
2985
|
+
init_workflow_replay();
|
|
2986
|
+
init_telemetry();
|
|
2987
|
+
|
|
2988
|
+
// src/workflows/completed-step-routing.ts
|
|
2989
|
+
init_db3();
|
|
2990
|
+
async function loadCompletedStepRouting(def, runId, completedNodeIds) {
|
|
2991
|
+
const activeEdges = new Set;
|
|
2992
|
+
const stepsByNodeId = new Map;
|
|
2993
|
+
for (const nodeId of completedNodeIds) {
|
|
2994
|
+
if (resolveForeachParent(def, nodeId))
|
|
2574
2995
|
continue;
|
|
2575
|
-
|
|
2576
|
-
|
|
2996
|
+
const step = await getLatestStepForNode2(runId, nodeId);
|
|
2997
|
+
if (step)
|
|
2998
|
+
stepsByNodeId.set(nodeId, step);
|
|
2999
|
+
const successors = step?.nextPort ? getSuccessors(def, nodeId, step.nextPort) : getSuccessors(def, nodeId);
|
|
3000
|
+
for (const successor of successors) {
|
|
3001
|
+
activeEdges.add(`${nodeId}→${successor.id}`);
|
|
2577
3002
|
}
|
|
2578
3003
|
}
|
|
2579
|
-
return
|
|
3004
|
+
return { activeEdges, stepsByNodeId };
|
|
2580
3005
|
}
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
if (
|
|
2587
|
-
|
|
2588
|
-
|
|
3006
|
+
|
|
3007
|
+
// src/workflows/cooldown.ts
|
|
3008
|
+
init_db3();
|
|
3009
|
+
function cooldownToMs(cooldown) {
|
|
3010
|
+
let ms = 0;
|
|
3011
|
+
if (cooldown.hours)
|
|
3012
|
+
ms += cooldown.hours * 60 * 60 * 1000;
|
|
3013
|
+
if (cooldown.minutes)
|
|
3014
|
+
ms += cooldown.minutes * 60 * 1000;
|
|
3015
|
+
if (cooldown.seconds)
|
|
3016
|
+
ms += cooldown.seconds * 1000;
|
|
3017
|
+
return ms;
|
|
2589
3018
|
}
|
|
2590
|
-
function
|
|
2591
|
-
const
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
3019
|
+
async function shouldSkipCooldown(workflowId, cooldown) {
|
|
3020
|
+
const cooldownMs = cooldownToMs(cooldown);
|
|
3021
|
+
const now = Date.now();
|
|
3022
|
+
const lastSuccess = await getLastSuccessfulRun2(workflowId);
|
|
3023
|
+
if (lastSuccess?.finishedAt) {
|
|
3024
|
+
const lastFinished = new Date(lastSuccess.finishedAt).getTime();
|
|
3025
|
+
if (now - lastFinished < cooldownMs)
|
|
3026
|
+
return true;
|
|
3027
|
+
}
|
|
3028
|
+
const lastAttempt = await getLastRunStart2(workflowId);
|
|
3029
|
+
if (lastAttempt?.startedAt) {
|
|
3030
|
+
const lastStarted = new Date(lastAttempt.startedAt).getTime();
|
|
3031
|
+
if (now - lastStarted < cooldownMs)
|
|
3032
|
+
return true;
|
|
3033
|
+
}
|
|
3034
|
+
return false;
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
// src/workflows/input.ts
|
|
3038
|
+
init_db3();
|
|
3039
|
+
init_secret_scrubber();
|
|
3040
|
+
async function resolveInputs(input) {
|
|
3041
|
+
const resolved = {};
|
|
3042
|
+
for (const [key, value] of Object.entries(input)) {
|
|
3043
|
+
resolved[key] = await resolveInputValue(value);
|
|
3044
|
+
}
|
|
3045
|
+
return resolved;
|
|
3046
|
+
}
|
|
3047
|
+
async function resolveInputValue(value) {
|
|
3048
|
+
const envMatch = /^\$\{(.+)\}$/.exec(value);
|
|
3049
|
+
if (envMatch?.[1]) {
|
|
3050
|
+
const envName = envMatch[1];
|
|
3051
|
+
const envValue = process.env[envName];
|
|
3052
|
+
if (envValue === undefined) {
|
|
3053
|
+
throw new Error(`Environment variable "${envName}" is not set`);
|
|
3054
|
+
}
|
|
3055
|
+
return envValue;
|
|
3056
|
+
}
|
|
3057
|
+
if (value.startsWith("secret.")) {
|
|
3058
|
+
const secretName = value.slice("secret.".length);
|
|
3059
|
+
const configs = await getSwarmConfigs2({ scope: "global", key: secretName });
|
|
3060
|
+
const secretConfig = configs.find((c) => c.isSecret);
|
|
3061
|
+
if (!secretConfig) {
|
|
3062
|
+
throw new Error(`Secret "${secretName}" not found in config store`);
|
|
2606
3063
|
}
|
|
3064
|
+
return secretConfig.value;
|
|
2607
3065
|
}
|
|
2608
|
-
return
|
|
3066
|
+
return value;
|
|
2609
3067
|
}
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
3068
|
+
var REDACTED_SECRET_VALUE = "***REDACTED***";
|
|
3069
|
+
function getSecretInputKeys(input) {
|
|
3070
|
+
const keys = new Set;
|
|
3071
|
+
if (!input)
|
|
3072
|
+
return keys;
|
|
3073
|
+
for (const [key, value] of Object.entries(input)) {
|
|
3074
|
+
if (typeof value !== "string")
|
|
3075
|
+
continue;
|
|
3076
|
+
if (value.startsWith("secret.")) {
|
|
3077
|
+
keys.add(key);
|
|
2614
3078
|
continue;
|
|
2615
|
-
for (const target of getNextTargets(node.next)) {
|
|
2616
|
-
if (!reverseDeps.has(target))
|
|
2617
|
-
reverseDeps.set(target, []);
|
|
2618
|
-
reverseDeps.get(target).push(node.id);
|
|
2619
3079
|
}
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
while (queue.length > 0) {
|
|
2624
|
-
const current = queue.shift();
|
|
2625
|
-
const preds = reverseDeps.get(current) || [];
|
|
2626
|
-
for (const pred of preds) {
|
|
2627
|
-
if (visited.has(pred))
|
|
2628
|
-
continue;
|
|
2629
|
-
visited.add(pred);
|
|
2630
|
-
if (pred === sourceId)
|
|
2631
|
-
return true;
|
|
2632
|
-
queue.push(pred);
|
|
3080
|
+
const envMatch = /^\$\{(.+)\}$/.exec(value);
|
|
3081
|
+
if (envMatch?.[1] && isSensitiveKey(envMatch[1])) {
|
|
3082
|
+
keys.add(key);
|
|
2633
3083
|
}
|
|
2634
3084
|
}
|
|
2635
|
-
return
|
|
3085
|
+
return keys;
|
|
2636
3086
|
}
|
|
2637
|
-
function
|
|
2638
|
-
if (
|
|
2639
|
-
return
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
3087
|
+
function redactSecretsForStorage(ctx, secretKeys) {
|
|
3088
|
+
if (secretKeys.size === 0)
|
|
3089
|
+
return ctx;
|
|
3090
|
+
const inputBlock = ctx.input;
|
|
3091
|
+
if (!inputBlock || typeof inputBlock !== "object")
|
|
3092
|
+
return ctx;
|
|
3093
|
+
const redactedInput = { ...inputBlock };
|
|
3094
|
+
let touched = false;
|
|
3095
|
+
for (const key of secretKeys) {
|
|
3096
|
+
if (key in redactedInput) {
|
|
3097
|
+
redactedInput[key] = REDACTED_SECRET_VALUE;
|
|
3098
|
+
touched = true;
|
|
3099
|
+
}
|
|
2644
3100
|
}
|
|
2645
|
-
|
|
3101
|
+
if (!touched)
|
|
3102
|
+
return ctx;
|
|
3103
|
+
return { ...ctx, input: redactedInput };
|
|
2646
3104
|
}
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
function validateDefinition(def, registry, options = {}) {
|
|
3105
|
+
|
|
3106
|
+
// src/workflows/json-schema-validator.ts
|
|
3107
|
+
function validateJsonSchema(schema, data) {
|
|
2651
3108
|
const errors = [];
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
3109
|
+
validate(schema, data, "", errors);
|
|
3110
|
+
return errors;
|
|
3111
|
+
}
|
|
3112
|
+
function validate(schema, data, path, errors) {
|
|
3113
|
+
const prefix = path || "root";
|
|
3114
|
+
if (schema.type !== undefined) {
|
|
3115
|
+
const expected = schema.type;
|
|
3116
|
+
if (!matchesType(expected, data)) {
|
|
3117
|
+
errors.push(`${prefix}: expected type "${expected}", got ${typeOf(data)}`);
|
|
3118
|
+
return;
|
|
2659
3119
|
}
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
if (isUpstream(def, node.id, node.id)) {
|
|
2667
|
-
errors.push("foreach inside a loop is not supported in v1");
|
|
2668
|
-
}
|
|
2669
|
-
const collidingSibling = def.nodes.find((other) => other.id !== node.id && other.id.startsWith(`${node.id}#`));
|
|
2670
|
-
if (collidingSibling) {
|
|
2671
|
-
errors.push(`Node "${node.id}" cannot be a foreach: node "${collidingSibling.id}" collides with its synthetic child id space ("${node.id}#…")`);
|
|
3120
|
+
}
|
|
3121
|
+
if (Array.isArray(schema.required) && typeof data === "object" && data !== null) {
|
|
3122
|
+
const obj = data;
|
|
3123
|
+
for (const key of schema.required) {
|
|
3124
|
+
if (!(key in obj)) {
|
|
3125
|
+
errors.push(`${prefix}: missing required property "${key}"`);
|
|
2672
3126
|
}
|
|
2673
3127
|
}
|
|
2674
3128
|
}
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
3129
|
+
if (Array.isArray(schema.enum)) {
|
|
3130
|
+
const allowed = schema.enum;
|
|
3131
|
+
if (!allowed.some((v) => JSON.stringify(v) === JSON.stringify(data))) {
|
|
3132
|
+
errors.push(`${prefix}: value ${JSON.stringify(data)} not in enum [${allowed.map((v) => JSON.stringify(v)).join(", ")}]`);
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
if ("const" in schema) {
|
|
3137
|
+
if (JSON.stringify(data) !== JSON.stringify(schema.const)) {
|
|
3138
|
+
errors.push(`${prefix}: value ${JSON.stringify(data)} does not match const ${JSON.stringify(schema.const)}`);
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
if (schema.properties !== undefined && typeof schema.properties === "object" && schema.properties !== null && typeof data === "object" && data !== null) {
|
|
3143
|
+
const props = schema.properties;
|
|
3144
|
+
const obj = data;
|
|
3145
|
+
for (const [key, subSchema] of Object.entries(props)) {
|
|
3146
|
+
if (key in obj) {
|
|
3147
|
+
validate(subSchema, obj[key], path ? `${path}.${key}` : key, errors);
|
|
2681
3148
|
}
|
|
2682
|
-
}
|
|
2683
|
-
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
if (schema.items !== undefined && typeof schema.items === "object" && schema.items !== null && Array.isArray(data)) {
|
|
3152
|
+
const itemSchema = schema.items;
|
|
3153
|
+
for (let i = 0;i < data.length; i++) {
|
|
3154
|
+
validate(itemSchema, data[i], `${prefix}[${i}]`, errors);
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
function matchesType(expected, data) {
|
|
3159
|
+
switch (expected) {
|
|
3160
|
+
case "string":
|
|
3161
|
+
return typeof data === "string";
|
|
3162
|
+
case "number":
|
|
3163
|
+
return typeof data === "number";
|
|
3164
|
+
case "boolean":
|
|
3165
|
+
return typeof data === "boolean";
|
|
3166
|
+
case "array":
|
|
3167
|
+
return Array.isArray(data);
|
|
3168
|
+
case "object":
|
|
3169
|
+
return typeof data === "object" && data !== null && !Array.isArray(data);
|
|
3170
|
+
default:
|
|
3171
|
+
return true;
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
function typeOf(data) {
|
|
3175
|
+
if (data === null)
|
|
3176
|
+
return "null";
|
|
3177
|
+
if (data === undefined)
|
|
3178
|
+
return "undefined";
|
|
3179
|
+
if (Array.isArray(data))
|
|
3180
|
+
return "array";
|
|
3181
|
+
return typeof data;
|
|
3182
|
+
}
|
|
3183
|
+
function isPlainObject(value) {
|
|
3184
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3185
|
+
}
|
|
3186
|
+
function findJsonSchemaShapeErrors(schema, path = "outputSchema") {
|
|
3187
|
+
const errors = [];
|
|
3188
|
+
if (!isPlainObject(schema)) {
|
|
3189
|
+
errors.push(`${path}: must be a JSON Schema object, got ${typeOf(schema)}`);
|
|
3190
|
+
return errors;
|
|
3191
|
+
}
|
|
3192
|
+
if (schema.type !== undefined && typeof schema.type !== "string") {
|
|
3193
|
+
errors.push(`${path}.type: must be a string, got ${typeOf(schema.type)}`);
|
|
3194
|
+
}
|
|
3195
|
+
if (schema.required !== undefined && (!Array.isArray(schema.required) || !schema.required.every((v) => typeof v === "string"))) {
|
|
3196
|
+
errors.push(`${path}.required: must be an array of strings`);
|
|
3197
|
+
}
|
|
3198
|
+
if (schema.enum !== undefined && !Array.isArray(schema.enum)) {
|
|
3199
|
+
errors.push(`${path}.enum: must be an array`);
|
|
3200
|
+
}
|
|
3201
|
+
if (schema.properties !== undefined) {
|
|
3202
|
+
if (!isPlainObject(schema.properties)) {
|
|
3203
|
+
errors.push(`${path}.properties: must be an object mapping property names to schemas`);
|
|
3204
|
+
} else {
|
|
3205
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
3206
|
+
errors.push(...findJsonSchemaShapeErrors(value, `${path}.properties.${key}`));
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
if (schema.items !== undefined) {
|
|
3211
|
+
errors.push(...findJsonSchemaShapeErrors(schema.items, `${path}.items`));
|
|
3212
|
+
}
|
|
3213
|
+
return errors;
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
// src/workflows/limits.ts
|
|
3217
|
+
var DEFAULT_MAX_STEPS_PER_RUN = 500;
|
|
3218
|
+
function getMaxWorkflowStepsPerRun() {
|
|
3219
|
+
return Number(process.env.WORKFLOW_MAX_STEPS_PER_RUN) || DEFAULT_MAX_STEPS_PER_RUN;
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
// src/workflows/node-timeout.ts
|
|
3223
|
+
var DEFAULT_NODE_TIMEOUT_MS = 30000;
|
|
3224
|
+
function resolveNodeTimeoutMs(node) {
|
|
3225
|
+
const config = node.config ?? {};
|
|
3226
|
+
if (typeof config.timeoutMs === "number")
|
|
3227
|
+
return config.timeoutMs;
|
|
3228
|
+
if (node.type === "script" && typeof config.timeout === "number")
|
|
3229
|
+
return config.timeout;
|
|
3230
|
+
return DEFAULT_NODE_TIMEOUT_MS;
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
// src/workflows/readiness.ts
|
|
3234
|
+
async function findWorkflowReadinessProblems(definition, registry) {
|
|
3235
|
+
const nodesByType = new Map;
|
|
3236
|
+
for (const node of definition.nodes) {
|
|
3237
|
+
if (!registry.has(node.type))
|
|
3238
|
+
continue;
|
|
3239
|
+
const group = nodesByType.get(node.type) ?? [];
|
|
3240
|
+
group.push({ id: node.id, config: node.config });
|
|
3241
|
+
nodesByType.set(node.type, group);
|
|
3242
|
+
}
|
|
3243
|
+
const problems = [];
|
|
3244
|
+
for (const [type, nodes] of nodesByType) {
|
|
3245
|
+
try {
|
|
3246
|
+
for (const problem of await registry.get(type).checkReadiness(nodes)) {
|
|
3247
|
+
problems.push({ type, ...problem });
|
|
3248
|
+
}
|
|
3249
|
+
} catch {
|
|
3250
|
+
problems.push({
|
|
3251
|
+
type,
|
|
3252
|
+
nodeIds: nodes.map((node) => node.id),
|
|
3253
|
+
message: `Could not check whether "${type}" nodes are ready to run`
|
|
3254
|
+
});
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
return problems;
|
|
3258
|
+
}
|
|
3259
|
+
function describeNodes(problem) {
|
|
3260
|
+
const noun = problem.nodeIds.length === 1 ? "node" : "nodes";
|
|
3261
|
+
return `${problem.type} ${noun} ${problem.nodeIds.map((id) => `"${id}"`).join(", ")}`;
|
|
3262
|
+
}
|
|
3263
|
+
function formatReadinessWarning(problem) {
|
|
3264
|
+
return `${problem.message} Affects ${describeNodes(problem)}. Runs of this workflow fail before any node executes until this is fixed.`;
|
|
3265
|
+
}
|
|
3266
|
+
function formatReadinessProblems(problems) {
|
|
3267
|
+
return problems.map((problem) => `${problem.message} Needed by ${describeNodes(problem)}.`).join(" ");
|
|
3268
|
+
}
|
|
3269
|
+
function formatReadinessRunError(problems) {
|
|
3270
|
+
return `Run not started, no node executed: ${formatReadinessProblems(problems)}`;
|
|
3271
|
+
}
|
|
3272
|
+
async function workflowSaveWarnings(definition, registry) {
|
|
3273
|
+
return (await findWorkflowReadinessProblems(definition, registry)).map(formatReadinessWarning);
|
|
3274
|
+
}
|
|
3275
|
+
function withSaveWarnings(text, warnings) {
|
|
3276
|
+
return warnings.length === 0 ? text : `${text} ${warnings.map((warning) => `Warning: ${warning}`).join(" ")}`;
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
// src/workflows/template.ts
|
|
3280
|
+
init_template();
|
|
3281
|
+
|
|
3282
|
+
// src/workflows/validation.ts
|
|
3283
|
+
function extractPassResult(executorType, output) {
|
|
3284
|
+
if (!output || typeof output !== "object")
|
|
3285
|
+
return false;
|
|
3286
|
+
const o = output;
|
|
3287
|
+
switch (executorType) {
|
|
3288
|
+
case "validate":
|
|
3289
|
+
return o.pass === true;
|
|
3290
|
+
case "script":
|
|
3291
|
+
return o.exitCode === 0;
|
|
3292
|
+
case "property-match":
|
|
3293
|
+
return o.passed === true;
|
|
3294
|
+
case "raw-llm":
|
|
3295
|
+
if (typeof o.result === "object" && o.result !== null) {
|
|
3296
|
+
return o.result.pass === true;
|
|
3297
|
+
}
|
|
3298
|
+
return false;
|
|
3299
|
+
default:
|
|
3300
|
+
return o.pass === true || o.passed === true || o.exitCode === 0;
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
async function runStepValidation(registry, stepNode, stepOutput, context, meta) {
|
|
3304
|
+
if (!stepNode.validation) {
|
|
3305
|
+
return { outcome: "pass" };
|
|
3306
|
+
}
|
|
3307
|
+
const validation = stepNode.validation;
|
|
3308
|
+
const executorType = validation.executor || "validate";
|
|
3309
|
+
const executor = registry.get(executorType);
|
|
3310
|
+
const validationConfig = {
|
|
3311
|
+
targetNodeId: meta.nodeId,
|
|
3312
|
+
...validation.config
|
|
3313
|
+
};
|
|
3314
|
+
const validationContext = {
|
|
3315
|
+
...context,
|
|
3316
|
+
[meta.nodeId]: stepOutput
|
|
3317
|
+
};
|
|
3318
|
+
const result = await executor.run({
|
|
3319
|
+
config: validationConfig,
|
|
3320
|
+
context: validationContext,
|
|
3321
|
+
meta: {
|
|
3322
|
+
...meta,
|
|
3323
|
+
stepId: crypto.randomUUID()
|
|
3324
|
+
}
|
|
3325
|
+
});
|
|
3326
|
+
const passed = result.status === "success" && extractPassResult(executorType, result.output);
|
|
3327
|
+
if (passed) {
|
|
3328
|
+
return { outcome: "pass", passed: true };
|
|
3329
|
+
}
|
|
3330
|
+
if (validation.mustPass) {
|
|
3331
|
+
if (validation.retry) {
|
|
3332
|
+
return {
|
|
3333
|
+
outcome: "retry",
|
|
3334
|
+
passed: false,
|
|
3335
|
+
retryContext: {
|
|
3336
|
+
previousOutput: stepOutput,
|
|
3337
|
+
validationResult: result.output
|
|
3338
|
+
}
|
|
3339
|
+
};
|
|
3340
|
+
}
|
|
3341
|
+
return { outcome: "halt", passed: false };
|
|
3342
|
+
}
|
|
3343
|
+
return { outcome: "pass", passed: false };
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
// src/workflows/engine.ts
|
|
3347
|
+
function maxIterations() {
|
|
3348
|
+
return Number(process.env.WORKFLOW_MAX_ITERATIONS) || 100;
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
class TriggerSchemaError extends Error {
|
|
3352
|
+
validationErrors;
|
|
3353
|
+
constructor(validationErrors) {
|
|
3354
|
+
super(`Trigger schema validation failed: ${validationErrors.join("; ")}`);
|
|
3355
|
+
this.validationErrors = validationErrors;
|
|
3356
|
+
this.name = "TriggerSchemaError";
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
async function resolveRenderedWorkflowInputs(input) {
|
|
3360
|
+
const stringInputs = {};
|
|
3361
|
+
const resolved = {};
|
|
3362
|
+
for (const [key, value] of Object.entries(input)) {
|
|
3363
|
+
if (typeof value === "string")
|
|
3364
|
+
stringInputs[key] = value;
|
|
3365
|
+
else
|
|
3366
|
+
resolved[key] = value;
|
|
3367
|
+
}
|
|
3368
|
+
return { ...resolved, ...await resolveInputs(stringInputs) };
|
|
3369
|
+
}
|
|
3370
|
+
async function startWorkflowExecution2(workflow, triggerData, registry, options = {}) {
|
|
3371
|
+
const preflight = preflightAutomation(workflowPreflightInput(workflow), await getAutomationSetupStates());
|
|
3372
|
+
if (preflight.state === "needs_setup") {
|
|
3373
|
+
const { runId, recorded } = await recordWorkflowPreflightFailure({
|
|
3374
|
+
workflowId: workflow.id,
|
|
3375
|
+
triggerType: options.triggerType ?? "manual",
|
|
3376
|
+
triggerData,
|
|
3377
|
+
failureReason: preflight.failureReason,
|
|
3378
|
+
createdBy: options.requestedByUserId
|
|
3379
|
+
});
|
|
3380
|
+
if (recorded)
|
|
3381
|
+
await notifyAutomationPreflightFailure(preflight);
|
|
3382
|
+
return runId;
|
|
3383
|
+
}
|
|
3384
|
+
workflow = renderAutomationTokens(workflow, workflow.params ?? {});
|
|
3385
|
+
if (workflow.triggerSchema) {
|
|
3386
|
+
const validationErrors = validateJsonSchema(workflow.triggerSchema, triggerData);
|
|
3387
|
+
if (validationErrors.length > 0) {
|
|
3388
|
+
throw new TriggerSchemaError(validationErrors);
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
if (workflow.cooldown && await shouldSkipCooldown(workflow.id, workflow.cooldown)) {
|
|
3392
|
+
const runId = crypto.randomUUID();
|
|
3393
|
+
await createWorkflowRun2({
|
|
3394
|
+
id: runId,
|
|
3395
|
+
workflowId: workflow.id,
|
|
3396
|
+
triggerType: options.triggerType ?? "manual",
|
|
3397
|
+
triggerData,
|
|
3398
|
+
createdBy: options.requestedByUserId,
|
|
3399
|
+
parentStepId: options.parentStepId
|
|
3400
|
+
});
|
|
3401
|
+
await updateWorkflowRun2(runId, {
|
|
3402
|
+
status: "skipped",
|
|
3403
|
+
error: "cooldown",
|
|
3404
|
+
finishedAt: new Date().toISOString()
|
|
3405
|
+
});
|
|
3406
|
+
return runId;
|
|
3407
|
+
}
|
|
3408
|
+
const runId = crypto.randomUUID();
|
|
3409
|
+
await createWorkflowRun2({
|
|
3410
|
+
id: runId,
|
|
3411
|
+
workflowId: workflow.id,
|
|
3412
|
+
triggerType: options.triggerType ?? "manual",
|
|
3413
|
+
triggerData,
|
|
3414
|
+
createdBy: options.requestedByUserId,
|
|
3415
|
+
parentStepId: options.parentStepId
|
|
3416
|
+
});
|
|
3417
|
+
telemetry.workflow("started", {
|
|
3418
|
+
workflowId: workflow.id,
|
|
3419
|
+
nodeCount: workflow.definition.nodes.length,
|
|
3420
|
+
triggerType: options.triggerType ?? "manual"
|
|
3421
|
+
});
|
|
3422
|
+
const notReady = await findWorkflowReadinessProblems(workflow.definition, registry);
|
|
3423
|
+
if (notReady.length > 0) {
|
|
3424
|
+
await updateWorkflowRun2(runId, {
|
|
3425
|
+
status: "failed",
|
|
3426
|
+
error: formatReadinessRunError(notReady),
|
|
3427
|
+
finishedAt: new Date().toISOString()
|
|
3428
|
+
});
|
|
3429
|
+
return runId;
|
|
3430
|
+
}
|
|
3431
|
+
const ctx = { trigger: triggerData };
|
|
3432
|
+
if (options.requestedByUserId) {
|
|
3433
|
+
ctx.swarm = { requestedByUserId: options.requestedByUserId };
|
|
3434
|
+
}
|
|
3435
|
+
if (workflow.dir || workflow.vcsRepo) {
|
|
3436
|
+
ctx.workflow = { dir: workflow.dir, vcsRepo: workflow.vcsRepo };
|
|
3437
|
+
}
|
|
3438
|
+
if (workflow.input) {
|
|
3439
|
+
try {
|
|
3440
|
+
const resolved = await resolveRenderedWorkflowInputs(workflow.input);
|
|
3441
|
+
Object.assign(ctx, { input: resolved });
|
|
3442
|
+
} catch (err) {
|
|
3443
|
+
await updateWorkflowRun2(runId, {
|
|
3444
|
+
status: "failed",
|
|
3445
|
+
error: `Input resolution failed: ${err}`,
|
|
3446
|
+
finishedAt: new Date().toISOString()
|
|
3447
|
+
});
|
|
3448
|
+
return runId;
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
const entryNodes = findEntryNodes(workflow.definition);
|
|
3452
|
+
const secretKeys = getSecretInputKeys(workflow.input);
|
|
3453
|
+
const walk = walkGraph(workflow.definition, runId, ctx, entryNodes, registry, workflow.id, secretKeys, options);
|
|
3454
|
+
if (options.parentStepId) {
|
|
3455
|
+
walk.catch((err) => console.error(`[workflows] Child workflow run ${runId} failed:`, err));
|
|
3456
|
+
return runId;
|
|
3457
|
+
}
|
|
3458
|
+
await walk;
|
|
3459
|
+
return runId;
|
|
3460
|
+
}
|
|
3461
|
+
var activeWalks = new Map;
|
|
3462
|
+
var executingSteps = new Set;
|
|
3463
|
+
async function failRunOnUnreadableReplay(runId, err, stepId) {
|
|
3464
|
+
if (!(err instanceof WorkflowReplayStateError))
|
|
3465
|
+
return false;
|
|
3466
|
+
const finishedAt = new Date().toISOString();
|
|
3467
|
+
console.error(`[workflow] ${err.message}`);
|
|
3468
|
+
if (stepId) {
|
|
3469
|
+
await updateWorkflowRunStep2(stepId, {
|
|
3470
|
+
status: "failed",
|
|
3471
|
+
error: err.message,
|
|
3472
|
+
finishedAt,
|
|
3473
|
+
nextRetryAt: null
|
|
3474
|
+
});
|
|
3475
|
+
}
|
|
3476
|
+
await updateWorkflowRun2(runId, { status: "failed", error: err.message, finishedAt });
|
|
3477
|
+
return true;
|
|
3478
|
+
}
|
|
3479
|
+
function isWorkflowRunActive(runId) {
|
|
3480
|
+
return activeWalks.has(runId);
|
|
3481
|
+
}
|
|
3482
|
+
function holdWorkflowRun(runId) {
|
|
3483
|
+
activeWalks.set(runId, (activeWalks.get(runId) ?? 0) + 1);
|
|
3484
|
+
let released = false;
|
|
3485
|
+
return () => {
|
|
3486
|
+
if (released)
|
|
3487
|
+
return;
|
|
3488
|
+
released = true;
|
|
3489
|
+
const remaining = activeWalks.get(runId) - 1;
|
|
3490
|
+
if (remaining === 0)
|
|
3491
|
+
activeWalks.delete(runId);
|
|
3492
|
+
else
|
|
3493
|
+
activeWalks.set(runId, remaining);
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
async function walkGraph(def, runId, ctx, startNodes, registry, workflowId, secretKeys = new Set, options = {}) {
|
|
3497
|
+
const release = holdWorkflowRun(runId);
|
|
3498
|
+
try {
|
|
3499
|
+
await walkGraphOwned(def, runId, ctx, startNodes, registry, workflowId, secretKeys, options);
|
|
3500
|
+
} catch (err) {
|
|
3501
|
+
if (!await failRunOnUnreadableReplay(runId, err))
|
|
3502
|
+
throw err;
|
|
3503
|
+
} finally {
|
|
3504
|
+
release();
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
async function walkGraphOwned(def, runId, ctx, startNodes, registry, workflowId, secretKeys = new Set, options = {}) {
|
|
3508
|
+
if (!options.requestedByUserId) {
|
|
3509
|
+
options = {
|
|
3510
|
+
...options,
|
|
3511
|
+
requestedByUserId: (await getWorkflowRun2(runId))?.createdBy
|
|
3512
|
+
};
|
|
3513
|
+
}
|
|
3514
|
+
let nodeExecutionCount = 0;
|
|
3515
|
+
if (!("run" in ctx)) {
|
|
3516
|
+
ctx.run = { id: runId };
|
|
3517
|
+
}
|
|
3518
|
+
const { completedNodeIds } = await rehydrateCompletedStepOutputs(def, runId, ctx, registry);
|
|
3519
|
+
const { activeEdges } = await loadCompletedStepRouting(def, runId, completedNodeIds);
|
|
3520
|
+
const allSteps = await getWorkflowRunStepsByRunId2(runId);
|
|
3521
|
+
if (allSteps.length >= getMaxWorkflowStepsPerRun()) {
|
|
3522
|
+
await updateWorkflowRun2(runId, {
|
|
3523
|
+
status: "failed",
|
|
3524
|
+
error: `Circuit breaker: run exceeded ${getMaxWorkflowStepsPerRun()} total steps (WORKFLOW_MAX_STEPS_PER_RUN)`,
|
|
3525
|
+
finishedAt: new Date().toISOString()
|
|
3526
|
+
});
|
|
3527
|
+
return;
|
|
3528
|
+
}
|
|
3529
|
+
for (const step of allSteps) {
|
|
3530
|
+
if (step.status !== "waiting")
|
|
3531
|
+
continue;
|
|
3532
|
+
const successors = getSuccessors(def, step.nodeId);
|
|
3533
|
+
for (const succ of successors) {
|
|
3534
|
+
activeEdges.add(`${step.nodeId}→${succ.id}`);
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
const awaited = awaitedNodeIds(allSteps, activeEdges);
|
|
3538
|
+
let pendingNodes = startNodes.filter((n) => {
|
|
3539
|
+
const preds = getAllPredecessors(def, n.id);
|
|
3540
|
+
if (preds.length === 0) {
|
|
3541
|
+
return !completedNodeIds.has(n.id);
|
|
3542
|
+
}
|
|
3543
|
+
const activePreds = preds.filter((predId) => activeEdges.has(`${predId}→${n.id}`) || awaited.has(predId));
|
|
3544
|
+
const predsToCheck = activePreds.length > 0 ? activePreds : preds;
|
|
3545
|
+
return predsToCheck.every((p) => completedNodeIds.has(p));
|
|
3546
|
+
});
|
|
3547
|
+
const executedInThisWalk = new Set;
|
|
3548
|
+
while (pendingNodes.length > 0) {
|
|
3549
|
+
nodeExecutionCount += pendingNodes.length;
|
|
3550
|
+
if (nodeExecutionCount > maxIterations()) {
|
|
3551
|
+
await updateWorkflowRun2(runId, {
|
|
3552
|
+
status: "failed",
|
|
3553
|
+
error: `Max node executions (${maxIterations()}) exceeded — possible infinite loop`,
|
|
3554
|
+
finishedAt: new Date().toISOString()
|
|
3555
|
+
});
|
|
3556
|
+
return;
|
|
3557
|
+
}
|
|
3558
|
+
const results = await Promise.all(pendingNodes.map((node) => executeStep(def, runId, ctx, node, registry, workflowId, secretKeys, options).catch(async (err) => {
|
|
3559
|
+
await failRunOnUnreadableReplay(runId, err);
|
|
3560
|
+
return { outcome: "failed", successors: [] };
|
|
3561
|
+
})));
|
|
3562
|
+
const nextBatch = new Map;
|
|
3563
|
+
let hasWaiting = false;
|
|
3564
|
+
for (let i = 0;i < results.length; i++) {
|
|
3565
|
+
const result = results[i];
|
|
3566
|
+
if (result.outcome === "failed") {
|
|
3567
|
+
const currentRun = await getWorkflowRun2(runId);
|
|
3568
|
+
if (currentRun?.status === "failed")
|
|
3569
|
+
return;
|
|
3570
|
+
continue;
|
|
3571
|
+
}
|
|
3572
|
+
if (result.outcome === "waiting") {
|
|
3573
|
+
hasWaiting = true;
|
|
3574
|
+
continue;
|
|
3575
|
+
}
|
|
3576
|
+
const sourceNodeId = pendingNodes[i].id;
|
|
3577
|
+
completedNodeIds.add(sourceNodeId);
|
|
3578
|
+
executedInThisWalk.add(sourceNodeId);
|
|
3579
|
+
for (const succ of result.successors) {
|
|
3580
|
+
activeEdges.add(`${sourceNodeId}→${succ.id}`);
|
|
3581
|
+
nextBatch.set(succ.id, succ);
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
if (hasWaiting)
|
|
3585
|
+
return;
|
|
3586
|
+
const readyNext = [];
|
|
3587
|
+
const batchSteps = nextBatch.size > 0 ? await getWorkflowRunStepsByRunId2(runId) : [];
|
|
3588
|
+
const batchAwaited = awaitedNodeIds(batchSteps, activeEdges);
|
|
3589
|
+
const batchLatest = latestStepByNode(batchSteps);
|
|
3590
|
+
for (const [nodeId, node] of nextBatch) {
|
|
3591
|
+
if (executedInThisWalk.has(nodeId))
|
|
3592
|
+
continue;
|
|
3593
|
+
const allPreds = getAllPredecessors(def, nodeId);
|
|
3594
|
+
for (const predId of allPreds) {
|
|
3595
|
+
if (completedNodeIds.has(predId))
|
|
3596
|
+
continue;
|
|
3597
|
+
const latest = batchLatest.get(predId);
|
|
3598
|
+
if (latest?.status !== "completed")
|
|
3599
|
+
continue;
|
|
3600
|
+
completedNodeIds.add(predId);
|
|
3601
|
+
if (latest.output !== undefined)
|
|
3602
|
+
ctx[predId] = latest.output;
|
|
3603
|
+
}
|
|
3604
|
+
const activePreds = allPreds.filter((predId) => activeEdges.has(`${predId}→${nodeId}`) || batchAwaited.has(predId));
|
|
3605
|
+
const allActivePredsCompleted = activePreds.every((p) => completedNodeIds.has(p));
|
|
3606
|
+
if (allActivePredsCompleted) {
|
|
3607
|
+
readyNext.push(node);
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
pendingNodes = readyNext;
|
|
3611
|
+
}
|
|
3612
|
+
await getDbClient2().transaction(async () => {
|
|
3613
|
+
const run = await getWorkflowRun2(runId);
|
|
3614
|
+
if (!run || run.status !== "running")
|
|
3615
|
+
return;
|
|
3616
|
+
const finalSteps = await getWorkflowRunStepsByRunId2(runId);
|
|
3617
|
+
if (hasRunningStep(finalSteps))
|
|
3618
|
+
return;
|
|
3619
|
+
const hasWaitingSteps = finalSteps.some((s) => s.status === "waiting");
|
|
3620
|
+
const hasPendingRetries = finalSteps.some((s) => s.status === "failed" && s.nextRetryAt != null);
|
|
3621
|
+
const isForeachChild = (s) => resolveForeachParent(def, s.nodeId) !== null;
|
|
3622
|
+
const failedSteps = finalSteps.filter((s) => s.status === "failed" && s.nextRetryAt == null && !isForeachChild(s));
|
|
3623
|
+
const entryNodeIds = new Set(findEntryNodes(def).map((n) => n.id));
|
|
3624
|
+
const hasCompletedSteps = finalSteps.some((s) => s.status === "completed" && !entryNodeIds.has(s.nodeId) && !isForeachChild(s));
|
|
3625
|
+
if (hasWaitingSteps) {
|
|
3626
|
+
await updateWorkflowRun2(runId, { status: "waiting" });
|
|
3627
|
+
} else if (!hasPendingRetries) {
|
|
3628
|
+
if (failedSteps.length > 0 && !hasCompletedSteps) {
|
|
3629
|
+
const failedNodeIds = failedSteps.map((s) => s.nodeId).join(", ");
|
|
3630
|
+
await updateWorkflowRun2(runId, {
|
|
3631
|
+
status: "failed",
|
|
3632
|
+
error: `All branches failed. Failed nodes: ${failedNodeIds}`,
|
|
3633
|
+
context: ctx,
|
|
3634
|
+
finishedAt: new Date().toISOString()
|
|
3635
|
+
});
|
|
3636
|
+
} else if (failedSteps.length > 0) {
|
|
3637
|
+
const failedNodeIds = failedSteps.map((s) => s.nodeId).join(", ");
|
|
3638
|
+
await updateWorkflowRun2(runId, {
|
|
3639
|
+
status: "completed",
|
|
3640
|
+
error: `Partial failure: nodes [${failedNodeIds}] failed (mustPass validation), but other branches completed successfully`,
|
|
3641
|
+
context: ctx,
|
|
3642
|
+
finishedAt: new Date().toISOString()
|
|
3643
|
+
});
|
|
3644
|
+
} else {
|
|
3645
|
+
await updateWorkflowRun2(runId, {
|
|
3646
|
+
status: "completed",
|
|
3647
|
+
context: ctx,
|
|
3648
|
+
finishedAt: new Date().toISOString()
|
|
3649
|
+
});
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
});
|
|
3653
|
+
}
|
|
3654
|
+
function latestStepByNode(steps) {
|
|
3655
|
+
const latest = new Map;
|
|
3656
|
+
for (const step of steps)
|
|
3657
|
+
latest.set(step.nodeId, step);
|
|
3658
|
+
return latest;
|
|
3659
|
+
}
|
|
3660
|
+
function awaitedNodeIds(steps, activeEdges) {
|
|
3661
|
+
const latest = latestStepByNode(steps);
|
|
3662
|
+
const awaited = new Set;
|
|
3663
|
+
for (const [nodeId, step] of latest) {
|
|
3664
|
+
if (step.status === "running" || step.status === "waiting")
|
|
3665
|
+
awaited.add(nodeId);
|
|
3666
|
+
}
|
|
3667
|
+
for (const edge of activeEdges) {
|
|
3668
|
+
const target = edge.slice(edge.indexOf("→") + 1);
|
|
3669
|
+
if (!latest.has(target))
|
|
3670
|
+
awaited.add(target);
|
|
3671
|
+
}
|
|
3672
|
+
return awaited;
|
|
3673
|
+
}
|
|
3674
|
+
function hasRunningStep(steps) {
|
|
3675
|
+
for (const step of latestStepByNode(steps).values()) {
|
|
3676
|
+
if (step.status === "running")
|
|
3677
|
+
return true;
|
|
3678
|
+
}
|
|
3679
|
+
return false;
|
|
3680
|
+
}
|
|
3681
|
+
async function rehydrateCompletedStepOutputs(def, runId, ctx, registry) {
|
|
3682
|
+
const completedNodeIds = new Set(await getCompletedStepNodeIds2(runId));
|
|
3683
|
+
const latestSteps = new Map;
|
|
3684
|
+
for (const nodeId of completedNodeIds) {
|
|
3685
|
+
if (resolveForeachParent(def, nodeId))
|
|
3686
|
+
continue;
|
|
3687
|
+
const step = await getLatestStepForNode2(runId, nodeId);
|
|
3688
|
+
if (!step)
|
|
3689
|
+
continue;
|
|
3690
|
+
if (step.output !== undefined) {
|
|
3691
|
+
const node = def.nodes.find((candidate) => candidate.id === nodeId);
|
|
3692
|
+
if (node && registry.has(node.type)) {
|
|
3693
|
+
const executor = registry.get(node.type);
|
|
3694
|
+
const parseResult = executor.outputSchema.safeParse(step.output);
|
|
3695
|
+
if (!parseResult.success) {
|
|
3696
|
+
console.warn(`[workflow] Recovery: step ${nodeId} output failed validation: ${parseResult.error.message}`);
|
|
3697
|
+
continue;
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
ctx[nodeId] = step.output;
|
|
3701
|
+
}
|
|
3702
|
+
latestSteps.set(nodeId, step);
|
|
3703
|
+
}
|
|
3704
|
+
return { completedNodeIds, latestSteps };
|
|
3705
|
+
}
|
|
3706
|
+
function getAllPredecessors(def, nodeId) {
|
|
3707
|
+
const preds = [];
|
|
3708
|
+
for (const node of def.nodes) {
|
|
3709
|
+
if (!node.next)
|
|
3710
|
+
continue;
|
|
3711
|
+
if (getNextTargets(node.next).includes(nodeId)) {
|
|
3712
|
+
preds.push(node.id);
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
return preds;
|
|
3716
|
+
}
|
|
3717
|
+
async function executeStep(def, runId, ctx, node, registry, workflowId, secretKeys = new Set, options = {}) {
|
|
3718
|
+
let claimedStepId;
|
|
3719
|
+
const claimStep = async () => {
|
|
3720
|
+
const run = await getWorkflowRun2(runId);
|
|
3721
|
+
if (!run || run.status !== "running" && run.status !== "waiting") {
|
|
3722
|
+
return { halted: true };
|
|
3723
|
+
}
|
|
3724
|
+
if (node.type !== "foreach") {
|
|
3725
|
+
const current = await getCurrentStepForNode2(runId, node.id, getAllPredecessors(def, node.id));
|
|
3726
|
+
if (current && (current.status !== "running" || executingSteps.has(current.id))) {
|
|
3727
|
+
return { current };
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
const iteration = await getStepCountForNode2(runId, node.id);
|
|
3731
|
+
const idempotencyKey = `${runId}:${node.id}:${iteration}`;
|
|
3732
|
+
const existingStep = await getStepByIdempotencyKey2(idempotencyKey);
|
|
3733
|
+
if (existingStep && (existingStep.status === "completed" || existingStep.status === "waiting")) {
|
|
3734
|
+
return { existingStep, stepId: existingStep.id, deduped: true };
|
|
3735
|
+
}
|
|
3736
|
+
const latestForeachStep = node.type === "foreach" ? await getLatestStepForNode2(runId, node.id) : undefined;
|
|
3737
|
+
const reusableForeachStep = latestForeachStep && !FOREACH_TERMINAL_STEP_STATUSES.has(latestForeachStep.status) ? latestForeachStep : undefined;
|
|
3738
|
+
const stepId = reusableForeachStep?.id ?? crypto.randomUUID();
|
|
3739
|
+
if (!reusableForeachStep) {
|
|
3740
|
+
await createWorkflowRunStep2({
|
|
3741
|
+
id: stepId,
|
|
3742
|
+
runId,
|
|
3743
|
+
nodeId: node.id,
|
|
3744
|
+
nodeType: node.type,
|
|
3745
|
+
input: redactSecretsForStorage(ctx, secretKeys),
|
|
3746
|
+
idempotencyKey
|
|
3747
|
+
});
|
|
3748
|
+
}
|
|
3749
|
+
claimedStepId = stepId;
|
|
3750
|
+
executingSteps.add(stepId);
|
|
3751
|
+
return { existingStep, stepId, deduped: false };
|
|
3752
|
+
};
|
|
3753
|
+
const dedup = await getDbClient2().transaction(claimStep).catch((err) => {
|
|
3754
|
+
if (claimedStepId)
|
|
3755
|
+
executingSteps.delete(claimedStepId);
|
|
3756
|
+
throw err;
|
|
3757
|
+
});
|
|
3758
|
+
if ("halted" in dedup)
|
|
3759
|
+
return { outcome: "completed", successors: [] };
|
|
3760
|
+
if ("current" in dedup && dedup.current) {
|
|
3761
|
+
if (dedup.current.status !== "completed")
|
|
3762
|
+
return { outcome: "waiting", successors: [] };
|
|
3763
|
+
ctx[node.id] = dedup.current.output;
|
|
3764
|
+
return { outcome: "completed", successors: [] };
|
|
3765
|
+
}
|
|
3766
|
+
if (dedup.deduped && dedup.existingStep) {
|
|
3767
|
+
if (dedup.existingStep.status === "completed") {
|
|
3768
|
+
ctx[node.id] = dedup.existingStep.output;
|
|
3769
|
+
const successors = getSuccessors(def, node.id);
|
|
3770
|
+
return { outcome: "completed", successors };
|
|
3771
|
+
}
|
|
3772
|
+
return { outcome: "waiting", successors: [] };
|
|
3773
|
+
}
|
|
3774
|
+
try {
|
|
3775
|
+
return await runClaimedStep(def, runId, ctx, node, registry, dedup.existingStep, dedup.stepId, workflowId, options);
|
|
3776
|
+
} catch (err) {
|
|
3777
|
+
if (await failRunOnUnreadableReplay(runId, err, dedup.stepId)) {
|
|
3778
|
+
return { outcome: "failed", successors: [] };
|
|
3779
|
+
}
|
|
3780
|
+
throw err;
|
|
3781
|
+
} finally {
|
|
3782
|
+
executingSteps.delete(dedup.stepId);
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
async function runClaimedStep(def, runId, ctx, node, registry, existingStep, stepId, workflowId, options = {}) {
|
|
3786
|
+
const executor = registry.get(node.type);
|
|
3787
|
+
const systemOneRetryProblems = systemOneRetryViolations(node);
|
|
3788
|
+
if (systemOneRetryProblems.length > 0) {
|
|
3789
|
+
await checkpointStepFailure(runId, stepId, systemOneRetryProblems.join("; "), 0);
|
|
3790
|
+
return { outcome: "failed", successors: [] };
|
|
3791
|
+
}
|
|
3792
|
+
const interpolationCtx = buildNodeInterpolationCtx(node, ctx);
|
|
3793
|
+
if (node.inputSchema) {
|
|
3794
|
+
const inputErrors = validateJsonSchema(node.inputSchema, interpolationCtx);
|
|
3795
|
+
if (inputErrors.length > 0) {
|
|
3796
|
+
const errorMsg = `Input schema validation failed: ${inputErrors.join("; ")}`;
|
|
3797
|
+
await checkpointStepFailure(runId, stepId, errorMsg, 0);
|
|
3798
|
+
throw new Error(errorMsg);
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
const {
|
|
3802
|
+
value: interpolatedValue,
|
|
3803
|
+
unresolved,
|
|
3804
|
+
scriptBodyUnresolved,
|
|
3805
|
+
strictUnresolved
|
|
3806
|
+
} = interpolateNodeConfig(node, interpolationCtx);
|
|
3807
|
+
const interpolatedConfig = interpolatedValue;
|
|
3808
|
+
const executionCtx = { ...ctx, ...interpolationCtx };
|
|
3809
|
+
if (scriptBodyUnresolved && scriptBodyUnresolved.length > 0) {
|
|
3810
|
+
const errorMsg = scriptBodyInterpolationError(node.id, scriptBodyUnresolved);
|
|
3811
|
+
await checkpointStepFailure(runId, stepId, errorMsg, 0);
|
|
3812
|
+
return { outcome: "failed", successors: [] };
|
|
3813
|
+
}
|
|
3814
|
+
if (strictUnresolved && strictUnresolved.length > 0) {
|
|
3815
|
+
await checkpointStepFailure(runId, stepId, systemOneUnresolvedError(node.id, strictUnresolved, node.type), 0);
|
|
3816
|
+
return { outcome: "failed", successors: [] };
|
|
3817
|
+
}
|
|
3818
|
+
if (unresolved.length > 0) {
|
|
3819
|
+
console.warn(`[workflow] Step ${node.id}: unresolved interpolation tokens: ${unresolved.join(", ")}`);
|
|
3820
|
+
await updateWorkflowRunStep2(stepId, {
|
|
3821
|
+
diagnostics: JSON.stringify({ unresolvedTokens: unresolved })
|
|
3822
|
+
});
|
|
3823
|
+
}
|
|
3824
|
+
const meta = {
|
|
3825
|
+
runId,
|
|
3826
|
+
stepId,
|
|
3827
|
+
nodeId: node.id,
|
|
3828
|
+
workflowId: workflowId || "",
|
|
3829
|
+
dryRun: false,
|
|
3830
|
+
requestedByUserId: options.requestedByUserId,
|
|
3831
|
+
inputCtx: interpolationCtx
|
|
3832
|
+
};
|
|
3833
|
+
const timeoutMs = resolveNodeTimeoutMs(node);
|
|
3834
|
+
let result;
|
|
3835
|
+
try {
|
|
3836
|
+
result = await Promise.race([
|
|
3837
|
+
executor.run({
|
|
3838
|
+
config: interpolatedConfig,
|
|
3839
|
+
context: executionCtx,
|
|
3840
|
+
meta
|
|
3841
|
+
}),
|
|
3842
|
+
timeoutPromise(timeoutMs)
|
|
3843
|
+
]);
|
|
3844
|
+
} catch (err) {
|
|
3845
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
3846
|
+
const retryPolicy = node.retry || executor.retryPolicy;
|
|
3847
|
+
const currentRetryCount = existingStep?.retryCount || 0;
|
|
3848
|
+
const { shouldRetry } = await checkpointStepFailure(runId, stepId, errorMsg, currentRetryCount, retryPolicy);
|
|
3849
|
+
if (!shouldRetry) {
|
|
3850
|
+
throw err;
|
|
3851
|
+
}
|
|
3852
|
+
return { outcome: "completed", successors: [] };
|
|
3853
|
+
}
|
|
3854
|
+
if (result.status === "failed") {
|
|
3855
|
+
const retryPolicy = node.retry || executor.retryPolicy;
|
|
3856
|
+
const currentRetryCount = existingStep?.retryCount || 0;
|
|
3857
|
+
const { shouldRetry } = await checkpointStepFailure(runId, stepId, result.error || "Executor returned failed status", currentRetryCount, retryPolicy);
|
|
3858
|
+
if (result.output !== undefined) {
|
|
3859
|
+
await updateWorkflowRunStep2(stepId, { output: result.output });
|
|
3860
|
+
}
|
|
3861
|
+
if (!shouldRetry) {
|
|
3862
|
+
throw new Error(result.error || "Step execution failed");
|
|
3863
|
+
}
|
|
3864
|
+
return { outcome: "completed", successors: [] };
|
|
3865
|
+
}
|
|
3866
|
+
if ("async" in result && result.async) {
|
|
3867
|
+
if (result.waitFor === FOREACH_ITEMS_WAIT_FOR) {
|
|
3868
|
+
const drive = await parkAndDriveForeach({
|
|
3869
|
+
runId,
|
|
3870
|
+
stepId,
|
|
3871
|
+
parentNodeId: node.id,
|
|
3872
|
+
ctx,
|
|
3873
|
+
registry
|
|
3874
|
+
});
|
|
3875
|
+
if (drive.joined)
|
|
3876
|
+
return { outcome: "completed", successors: drive.successors };
|
|
3877
|
+
return { outcome: drive.parked ? "waiting" : "completed", successors: [] };
|
|
3878
|
+
}
|
|
3879
|
+
const waiting = await checkpointStepWaiting(runId, stepId, ctx);
|
|
3880
|
+
if (waiting && result.waitFor === "workflow.child.finished") {
|
|
3881
|
+
const { resumeFromChildRun } = await import("./resume-5s2t0dbs.js");
|
|
3882
|
+
await resumeFromChildRun(stepId, registry);
|
|
3883
|
+
}
|
|
3884
|
+
return { outcome: waiting ? "waiting" : "completed", successors: [] };
|
|
3885
|
+
}
|
|
3886
|
+
if (node.outputSchema && result.status === "success") {
|
|
3887
|
+
const outputErrors = validateJsonSchema(node.outputSchema, result.output);
|
|
3888
|
+
if (outputErrors.length > 0) {
|
|
3889
|
+
const errorMsg = `Output schema validation failed: ${outputErrors.join("; ")}`;
|
|
3890
|
+
await checkpointStepFailure(runId, stepId, errorMsg, 0);
|
|
3891
|
+
throw new Error(errorMsg);
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
let validationResult;
|
|
3895
|
+
if (node.validation) {
|
|
3896
|
+
validationResult = await runStepValidation(registry, node, result.output, executionCtx, meta);
|
|
3897
|
+
if (validationResult.outcome === "halt") {
|
|
3898
|
+
const errorMsg = "Validation failed (mustPass)";
|
|
3899
|
+
await checkpointStepFailure(runId, stepId, errorMsg, 0, undefined, { markRunFailed: false });
|
|
3900
|
+
return { outcome: "failed", successors: [] };
|
|
3901
|
+
}
|
|
3902
|
+
if (validationResult.outcome === "retry") {
|
|
3903
|
+
if (validationResult.retryContext) {
|
|
3904
|
+
const historyKey = `${node.id}_validations`;
|
|
3905
|
+
const existing = ctx[historyKey] || [];
|
|
3906
|
+
ctx[historyKey] = [...existing, validationResult.retryContext];
|
|
3907
|
+
}
|
|
3908
|
+
const retryPolicy = node.validation.retry || node.retry;
|
|
3909
|
+
const currentRetryCount = existingStep?.retryCount || 0;
|
|
3910
|
+
await checkpointStepFailure(runId, stepId, "Validation failed, retrying", currentRetryCount, retryPolicy);
|
|
3911
|
+
return { outcome: "completed", successors: [] };
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
result.nextPort = resolveValidationPort(node.next, result.nextPort, validationResult?.passed);
|
|
3915
|
+
await checkpointStep(runId, stepId, node.id, result, ctx);
|
|
3916
|
+
const successors = result.nextPort ? getSuccessors(def, node.id, result.nextPort) : getSuccessors(def, node.id);
|
|
3917
|
+
return { outcome: "completed", successors };
|
|
3918
|
+
}
|
|
3919
|
+
function findReadyNodes(def, completedNodeIds, activeEdges) {
|
|
3920
|
+
const predecessors = new Map;
|
|
3921
|
+
for (const node of def.nodes) {
|
|
3922
|
+
if (!predecessors.has(node.id)) {
|
|
3923
|
+
predecessors.set(node.id, new Set);
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
for (const node of def.nodes) {
|
|
3927
|
+
if (!node.next)
|
|
3928
|
+
continue;
|
|
3929
|
+
for (const target of getNextTargets(node.next)) {
|
|
3930
|
+
if (!predecessors.has(target)) {
|
|
3931
|
+
predecessors.set(target, new Set);
|
|
3932
|
+
}
|
|
3933
|
+
predecessors.get(target).add(node.id);
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
return def.nodes.filter((node) => {
|
|
3937
|
+
if (completedNodeIds.has(node.id))
|
|
3938
|
+
return false;
|
|
3939
|
+
const preds = predecessors.get(node.id);
|
|
3940
|
+
if (!preds || preds.size === 0)
|
|
3941
|
+
return true;
|
|
3942
|
+
if (activeEdges) {
|
|
3943
|
+
const activePreds = [...preds].filter((predId) => activeEdges.has(`${predId}→${node.id}`));
|
|
3944
|
+
if (activePreds.length === 0)
|
|
3945
|
+
return false;
|
|
3946
|
+
return activePreds.every((pred) => completedNodeIds.has(pred));
|
|
3947
|
+
}
|
|
3948
|
+
for (const pred of preds) {
|
|
3949
|
+
if (!completedNodeIds.has(pred))
|
|
3950
|
+
return false;
|
|
3951
|
+
}
|
|
3952
|
+
return true;
|
|
3953
|
+
});
|
|
3954
|
+
}
|
|
3955
|
+
function buildNodeInterpolationCtx(node, ctx) {
|
|
3956
|
+
const interpolationCtx = {};
|
|
3957
|
+
if (ctx.trigger !== undefined)
|
|
3958
|
+
interpolationCtx.trigger = ctx.trigger;
|
|
3959
|
+
if (ctx.input !== undefined)
|
|
3960
|
+
interpolationCtx.input = ctx.input;
|
|
3961
|
+
if (ctx.workflow !== undefined)
|
|
3962
|
+
interpolationCtx.workflow = ctx.workflow;
|
|
3963
|
+
if (ctx.swarm !== undefined)
|
|
3964
|
+
interpolationCtx.swarm = ctx.swarm;
|
|
3965
|
+
if (ctx.run !== undefined)
|
|
3966
|
+
interpolationCtx.run = ctx.run;
|
|
3967
|
+
if (!node.inputs)
|
|
3968
|
+
return interpolationCtx;
|
|
3969
|
+
for (const [localName, sourcePath] of Object.entries(node.inputs)) {
|
|
3970
|
+
const keys = sourcePath.split(".");
|
|
3971
|
+
let value = ctx;
|
|
3972
|
+
for (const key of keys) {
|
|
3973
|
+
if (value == null || typeof value !== "object") {
|
|
3974
|
+
value = undefined;
|
|
3975
|
+
break;
|
|
3976
|
+
}
|
|
3977
|
+
value = value[key];
|
|
3978
|
+
}
|
|
3979
|
+
interpolationCtx[localName] = value;
|
|
3980
|
+
}
|
|
3981
|
+
return interpolationCtx;
|
|
3982
|
+
}
|
|
3983
|
+
var SCRIPT_BODY_ALLOWED_SOURCES = new Set(["input", "workflow", "swarm", "run"]);
|
|
3984
|
+
function buildScriptBodyCtx(node, interpolationCtx) {
|
|
3985
|
+
const aliases = node.inputs ?? {};
|
|
3986
|
+
const safe = {};
|
|
3987
|
+
for (const [key, value] of Object.entries(interpolationCtx)) {
|
|
3988
|
+
const sourcePath = aliases[key];
|
|
3989
|
+
const root = sourcePath === undefined ? key : sourcePath.split(".")[0] ?? "";
|
|
3990
|
+
if (SCRIPT_BODY_ALLOWED_SOURCES.has(root))
|
|
3991
|
+
safe[key] = value;
|
|
3992
|
+
}
|
|
3993
|
+
return safe;
|
|
3994
|
+
}
|
|
3995
|
+
var SYSTEM_ONE_DATA_FIELDS = new Set(["state", "questions", "fields", "images"]);
|
|
3996
|
+
function interpolateNodeConfig(node, interpolationCtx) {
|
|
3997
|
+
if (isSystemOneNodeType(node.type)) {
|
|
3998
|
+
const rest = {};
|
|
3999
|
+
const dynamic = {};
|
|
4000
|
+
for (const [key, value] of Object.entries(node.config)) {
|
|
4001
|
+
if (SYSTEM_ONE_DATA_FIELDS.has(key))
|
|
4002
|
+
dynamic[key] = value;
|
|
4003
|
+
else
|
|
4004
|
+
rest[key] = value;
|
|
4005
|
+
}
|
|
4006
|
+
const restResult = deepInterpolate(rest, interpolationCtx);
|
|
4007
|
+
const dynamicResult = deepInterpolate(dynamic, interpolationCtx, { preserveRawTokens: true });
|
|
4008
|
+
const unresolved = [...restResult.unresolved, ...dynamicResult.unresolved];
|
|
4009
|
+
return {
|
|
4010
|
+
value: {
|
|
4011
|
+
...restResult.value,
|
|
4012
|
+
...dynamicResult.value
|
|
4013
|
+
},
|
|
4014
|
+
unresolved,
|
|
4015
|
+
strictUnresolved: unresolved
|
|
4016
|
+
};
|
|
4017
|
+
}
|
|
4018
|
+
if (node.type === "foreach" && Object.hasOwn(node.config, "over")) {
|
|
4019
|
+
const { over, body, ...configWithoutOverAndBody } = node.config;
|
|
4020
|
+
const configResult = deepInterpolate(configWithoutOverAndBody, interpolationCtx);
|
|
4021
|
+
const overResult = deepInterpolate(over, interpolationCtx, { preserveRawTokens: true });
|
|
4022
|
+
return {
|
|
4023
|
+
value: {
|
|
4024
|
+
...configResult.value,
|
|
4025
|
+
over: overResult.value,
|
|
4026
|
+
body
|
|
4027
|
+
},
|
|
4028
|
+
unresolved: [...configResult.unresolved, ...overResult.unresolved]
|
|
4029
|
+
};
|
|
4030
|
+
}
|
|
4031
|
+
if (node.type === "human-in-the-loop" && typeof node.config.questions === "string") {
|
|
4032
|
+
const { questions, ...configWithoutQuestions } = node.config;
|
|
4033
|
+
const configResult = deepInterpolate(configWithoutQuestions, interpolationCtx);
|
|
4034
|
+
const questionsResult = deepInterpolate(questions, interpolationCtx, {
|
|
4035
|
+
preserveRawTokens: true
|
|
4036
|
+
});
|
|
4037
|
+
return {
|
|
4038
|
+
value: {
|
|
4039
|
+
...configResult.value,
|
|
4040
|
+
questions: questionsResult.value
|
|
4041
|
+
},
|
|
4042
|
+
unresolved: [...configResult.unresolved, ...questionsResult.unresolved]
|
|
4043
|
+
};
|
|
4044
|
+
}
|
|
4045
|
+
if (node.type === "script" && typeof node.config.script === "string") {
|
|
4046
|
+
const { script, ...configWithoutScript } = node.config;
|
|
4047
|
+
const restResult = deepInterpolate(configWithoutScript, interpolationCtx);
|
|
4048
|
+
const scriptResult = deepInterpolate(script, buildScriptBodyCtx(node, interpolationCtx));
|
|
4049
|
+
return {
|
|
4050
|
+
value: {
|
|
4051
|
+
...restResult.value,
|
|
4052
|
+
script: scriptResult.unresolved.length > 0 ? script : scriptResult.value
|
|
4053
|
+
},
|
|
4054
|
+
unresolved: [...restResult.unresolved, ...scriptResult.unresolved],
|
|
4055
|
+
scriptBodyUnresolved: scriptResult.unresolved
|
|
4056
|
+
};
|
|
4057
|
+
}
|
|
4058
|
+
if (node.type !== "swarm-script" || !Object.hasOwn(node.config, "args")) {
|
|
4059
|
+
return deepInterpolate(node.config, interpolationCtx);
|
|
4060
|
+
}
|
|
4061
|
+
const { args, ...configWithoutArgs } = node.config;
|
|
4062
|
+
const configResult = deepInterpolate(configWithoutArgs, interpolationCtx);
|
|
4063
|
+
const argsResult = deepInterpolate(args, interpolationCtx, { preserveRawTokens: true });
|
|
4064
|
+
return {
|
|
4065
|
+
value: {
|
|
4066
|
+
...configResult.value,
|
|
4067
|
+
args: argsResult.value
|
|
4068
|
+
},
|
|
4069
|
+
unresolved: [...configResult.unresolved, ...argsResult.unresolved]
|
|
4070
|
+
};
|
|
4071
|
+
}
|
|
4072
|
+
function scriptBodyInterpolationError(nodeId, tokens) {
|
|
4073
|
+
const renderedTokens = [...new Set(tokens)].map((token) => `{{${token}}}`).join(", ");
|
|
4074
|
+
return `Script body interpolation failed for node "${nodeId}": unresolved or disallowed token(s): ${renderedTokens}. Executable source may interpolate only input, workflow, swarm, or run values. Pass dynamic trigger or upstream values through config.args and read them from argv.`;
|
|
4075
|
+
}
|
|
4076
|
+
function timeoutPromise(ms) {
|
|
4077
|
+
return new Promise((_, reject) => {
|
|
4078
|
+
setTimeout(() => reject(new Error(`Step timed out after ${ms}ms`)), ms);
|
|
4079
|
+
});
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
// src/workflows/executors/foreach.ts
|
|
4083
|
+
init_zod();
|
|
4084
|
+
init_workflow_runs();
|
|
4085
|
+
var ForeachBodySchema = object2({
|
|
4086
|
+
type: string2().min(1),
|
|
4087
|
+
config: record2(string2(), unknown())
|
|
4088
|
+
});
|
|
4089
|
+
var ForeachConfigSchema = object2({
|
|
4090
|
+
over: array2(record2(string2(), unknown())),
|
|
4091
|
+
itemKey: string2().min(1),
|
|
4092
|
+
body: ForeachBodySchema,
|
|
4093
|
+
concurrency: number2().int().min(1).optional(),
|
|
4094
|
+
failurePolicy: _enum2(["fail-fast", "collect-outcomes"]).optional()
|
|
4095
|
+
});
|
|
4096
|
+
var ForeachResultSchema = object2({
|
|
4097
|
+
itemKey: string2(),
|
|
4098
|
+
status: _enum2(["completed", "failed", "cancelled", "skipped"]),
|
|
4099
|
+
output: unknown().optional()
|
|
4100
|
+
});
|
|
4101
|
+
var ForeachOutputSchema = object2({
|
|
4102
|
+
results: array2(ForeachResultSchema),
|
|
4103
|
+
okCount: number2().int().nonnegative(),
|
|
4104
|
+
failedCount: number2().int().nonnegative(),
|
|
4105
|
+
cancelledCount: number2().int().nonnegative().optional()
|
|
4106
|
+
});
|
|
4107
|
+
|
|
4108
|
+
class ForeachExecutor extends BaseExecutor {
|
|
4109
|
+
type = "foreach";
|
|
4110
|
+
mode = "async";
|
|
4111
|
+
configSchema = ForeachConfigSchema;
|
|
4112
|
+
outputSchema = ForeachOutputSchema;
|
|
4113
|
+
async execute(config, _context, meta) {
|
|
4114
|
+
if (config.over.length === 0) {
|
|
4115
|
+
return {
|
|
4116
|
+
status: "success",
|
|
4117
|
+
output: { results: [], okCount: 0, failedCount: 0 }
|
|
4118
|
+
};
|
|
4119
|
+
}
|
|
4120
|
+
const items = config.over.map((item, index) => {
|
|
4121
|
+
const rawItemKey = item[config.itemKey];
|
|
4122
|
+
if (typeof rawItemKey !== "string" || rawItemKey.length === 0) {
|
|
4123
|
+
throw new Error(`foreach item at index ${index} is missing non-empty string itemKey property "${config.itemKey}"`);
|
|
4124
|
+
}
|
|
4125
|
+
return { item, index, itemKey: rawItemKey };
|
|
4126
|
+
});
|
|
4127
|
+
const seenItemKeys = new Set;
|
|
4128
|
+
for (const { itemKey } of items) {
|
|
4129
|
+
if (seenItemKeys.has(itemKey)) {
|
|
4130
|
+
throw new Error(`foreach itemKey "${itemKey}" is duplicated`);
|
|
4131
|
+
}
|
|
4132
|
+
seenItemKeys.add(itemKey);
|
|
4133
|
+
}
|
|
4134
|
+
const bodyTypeError = foreachBodyTypeError(meta.nodeId, config.body.type);
|
|
4135
|
+
if (bodyTypeError)
|
|
4136
|
+
return { status: "failed", error: bodyTypeError };
|
|
4137
|
+
const runSteps = await this.deps.db.getWorkflowRunStepsByRunId(meta.runId);
|
|
4138
|
+
const existingChildNodeIds = new Set(runSteps.filter((step) => parseSyntheticNodeId(step.nodeId)?.parentNodeId === meta.nodeId).map((step) => step.nodeId));
|
|
4139
|
+
const missingItems = items.filter(({ itemKey }) => !existingChildNodeIds.has(`${meta.nodeId}#${itemKey}`));
|
|
4140
|
+
const childCountToCreate = missingItems.length;
|
|
4141
|
+
const maxSteps = getMaxWorkflowStepsPerRun();
|
|
4142
|
+
const workflow = await this.deps.db.getWorkflow(meta.workflowId);
|
|
4143
|
+
const hasSuccessors = workflow ? getSuccessors(workflow.definition, meta.nodeId).length > 0 : true;
|
|
4144
|
+
const totalSteps = runSteps.length + childCountToCreate;
|
|
4145
|
+
const capBreached = hasSuccessors ? totalSteps >= maxSteps : totalSteps > maxSteps;
|
|
4146
|
+
if (childCountToCreate > 0 && capBreached) {
|
|
4147
|
+
return {
|
|
4148
|
+
status: "failed",
|
|
4149
|
+
error: `foreach would reach ${maxSteps} total steps (WORKFLOW_MAX_STEPS_PER_RUN): ${runSteps.length} existing + ${childCountToCreate} children${hasSuccessors ? " leaves no room for the post-join walk" : " exceeds the per-run cap"}`
|
|
4150
|
+
};
|
|
4151
|
+
}
|
|
4152
|
+
await createForeachChildSteps(meta.runId, meta.nodeId, missingItems.map(({ index, itemKey }) => {
|
|
4153
|
+
const childNodeId = `${meta.nodeId}#${itemKey}`;
|
|
4154
|
+
return {
|
|
4155
|
+
id: crypto.randomUUID(),
|
|
4156
|
+
nodeId: childNodeId,
|
|
4157
|
+
nodeType: config.body.type,
|
|
4158
|
+
input: { itemKey, index, attempt: 0 },
|
|
4159
|
+
idempotencyKey: `${meta.runId}:${childNodeId}:0`
|
|
4160
|
+
};
|
|
4161
|
+
}));
|
|
4162
|
+
const childSteps = (await this.deps.db.getWorkflowRunStepsByRunId(meta.runId)).filter((step) => parseSyntheticNodeId(step.nodeId)?.parentNodeId === meta.nodeId);
|
|
4163
|
+
if (childSteps.every((step) => FOREACH_TERMINAL_STEP_STATUSES.has(step.status))) {
|
|
4164
|
+
return { status: "success", output: buildForeachAggregate(childSteps) };
|
|
4165
|
+
}
|
|
4166
|
+
return {
|
|
4167
|
+
status: "success",
|
|
4168
|
+
async: true,
|
|
4169
|
+
waitFor: FOREACH_ITEMS_WAIT_FOR,
|
|
4170
|
+
correlationId: meta.stepId
|
|
4171
|
+
};
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
// src/workflows/step-input.ts
|
|
4176
|
+
async function rebuildNodeExecutionInput(def, run, node, registry, options = {}) {
|
|
4177
|
+
const ctx = options.ctx ?? (run.context ?? {});
|
|
4178
|
+
if (!("run" in ctx))
|
|
4179
|
+
ctx.run = { id: run.id };
|
|
4180
|
+
if (!options.ctx)
|
|
4181
|
+
await rehydrateCompletedStepOutputs(def, run.id, ctx, registry);
|
|
4182
|
+
const inputCtx = buildNodeInterpolationCtx(node, ctx);
|
|
4183
|
+
const { value, unresolved, scriptBodyUnresolved, strictUnresolved } = interpolateNodeConfig(node, inputCtx);
|
|
4184
|
+
return {
|
|
4185
|
+
ctx,
|
|
4186
|
+
inputCtx,
|
|
4187
|
+
config: value,
|
|
4188
|
+
unresolved,
|
|
4189
|
+
scriptBodyUnresolved,
|
|
4190
|
+
strictUnresolved
|
|
4191
|
+
};
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
// src/workflows/foreach-driver.ts
|
|
4195
|
+
var ownedChildren = new Set;
|
|
4196
|
+
var activeDrives = new Set;
|
|
4197
|
+
var NOT_JOINED = { joined: false, successors: [], claimed: 0 };
|
|
4198
|
+
function isForeachChildOwned(stepId) {
|
|
4199
|
+
return ownedChildren.has(stepId);
|
|
4200
|
+
}
|
|
4201
|
+
async function parkAndDriveForeach(input) {
|
|
4202
|
+
const parked = await checkpointStepWaiting(input.runId, input.stepId, input.ctx, {
|
|
4203
|
+
keepRunStatus: true
|
|
4204
|
+
}) || (await getWorkflowRunStep2(input.stepId))?.status === "waiting";
|
|
4205
|
+
if (!parked)
|
|
4206
|
+
return { ...NOT_JOINED, parked: false };
|
|
4207
|
+
const drive = await driveForeach(input.runId, input.parentNodeId, input.registry, {
|
|
4208
|
+
ctx: input.ctx
|
|
4209
|
+
});
|
|
4210
|
+
if (!drive.joined)
|
|
4211
|
+
await parkRunIfRunning(input.runId);
|
|
4212
|
+
return { ...drive, parked: true };
|
|
4213
|
+
}
|
|
4214
|
+
async function driveForeach(runId, parentNodeId, registry, options = {}) {
|
|
4215
|
+
const run = await getWorkflowRun2(runId);
|
|
4216
|
+
if (!run || run.status !== "running" && run.status !== "waiting")
|
|
4217
|
+
return NOT_JOINED;
|
|
4218
|
+
const workflow = await getRenderedWorkflow(run.workflowId);
|
|
4219
|
+
if (!workflow)
|
|
4220
|
+
return NOT_JOINED;
|
|
4221
|
+
const def = workflow.definition;
|
|
4222
|
+
const parentNode = def.nodes.find((node) => node.id === parentNodeId);
|
|
4223
|
+
if (!parentNode || parentNode.type !== "foreach")
|
|
4224
|
+
return NOT_JOINED;
|
|
4225
|
+
const parentStep = await getLatestStepForNode2(runId, parentNodeId);
|
|
4226
|
+
if (!parentStep || FOREACH_TERMINAL_STEP_STATUSES.has(parentStep.status))
|
|
4227
|
+
return NOT_JOINED;
|
|
4228
|
+
const body = parentNode.config.body;
|
|
4229
|
+
const bodyType = typeof body?.type === "string" ? body.type : "";
|
|
4230
|
+
let parentInput;
|
|
4231
|
+
let claimedTotal = 0;
|
|
4232
|
+
const notJoined = () => ({ ...NOT_JOINED, claimed: claimedTotal });
|
|
4233
|
+
for (;; ) {
|
|
4234
|
+
const current = await getWorkflowRun2(runId);
|
|
4235
|
+
if (!current || current.status !== "running" && current.status !== "waiting") {
|
|
4236
|
+
return notJoined();
|
|
4237
|
+
}
|
|
4238
|
+
const claimed = await claimChildren(def, runId, parentNode);
|
|
4239
|
+
if (claimed.length === 0)
|
|
4240
|
+
return notJoined();
|
|
4241
|
+
claimedTotal += claimed.length;
|
|
4242
|
+
const started = new Set;
|
|
4243
|
+
let failedFast;
|
|
4244
|
+
try {
|
|
4245
|
+
parentInput ??= await loadParentInput(def, run, parentNode, registry, workflow.id, options);
|
|
4246
|
+
const { ctx, items, itemKeyField, meta } = parentInput;
|
|
4247
|
+
for (const child of claimed) {
|
|
4248
|
+
const liveness = await childLiveness(runId, child.id);
|
|
4249
|
+
if (liveness === "run-ended")
|
|
4250
|
+
return notJoined();
|
|
4251
|
+
if (liveness === "child-ended")
|
|
4252
|
+
continue;
|
|
4253
|
+
started.add(child.id);
|
|
4254
|
+
const outcome = await startChild({
|
|
4255
|
+
def,
|
|
4256
|
+
child,
|
|
4257
|
+
registry,
|
|
4258
|
+
ctx,
|
|
4259
|
+
meta,
|
|
4260
|
+
bodyType,
|
|
4261
|
+
item: items.find((candidate) => candidate[itemKeyField] === childItemKey(child))
|
|
4262
|
+
});
|
|
4263
|
+
if (!outcome)
|
|
4264
|
+
continue;
|
|
4265
|
+
const settled = await settleForeachChild(def, runId, child.id, outcome, ctx, {
|
|
4266
|
+
from: ["running"]
|
|
4267
|
+
});
|
|
4268
|
+
ownedChildren.delete(child.id);
|
|
4269
|
+
if (settled.joined) {
|
|
4270
|
+
return { joined: true, successors: settled.successors, claimed: claimedTotal };
|
|
4271
|
+
}
|
|
4272
|
+
if (settled.runFailed) {
|
|
4273
|
+
failedFast = child.nodeId;
|
|
4274
|
+
return notJoined();
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
} finally {
|
|
4278
|
+
for (const child of claimed) {
|
|
4279
|
+
ownedChildren.delete(child.id);
|
|
4280
|
+
if (!started.has(child.id))
|
|
4281
|
+
await requeueChild(child.id);
|
|
4282
|
+
}
|
|
4283
|
+
if (effectiveFailurePolicy(parentNode, def) === "fail-fast" && (failedFast || (await getWorkflowRun2(runId))?.status === "failed")) {
|
|
4284
|
+
await cancelQueuedForeachChildren(def, runId, parentNodeId, failedFast);
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
4289
|
+
async function loadParentInput(def, run, parentNode, registry, workflowId, options) {
|
|
4290
|
+
const input = await rebuildNodeExecutionInput(def, run, parentNode, registry, {
|
|
4291
|
+
ctx: options.ctx
|
|
4292
|
+
});
|
|
4293
|
+
const parsed = ForeachConfigSchema.safeParse(input.config);
|
|
4294
|
+
return {
|
|
4295
|
+
ctx: input.ctx,
|
|
4296
|
+
items: parsed.success ? parsed.data.over : [],
|
|
4297
|
+
itemKeyField: parsed.success ? parsed.data.itemKey : "",
|
|
4298
|
+
meta: {
|
|
4299
|
+
runId: run.id,
|
|
4300
|
+
workflowId,
|
|
4301
|
+
dryRun: false,
|
|
4302
|
+
requestedByUserId: run.createdBy,
|
|
4303
|
+
inputCtx: input.inputCtx
|
|
4304
|
+
}
|
|
4305
|
+
};
|
|
4306
|
+
}
|
|
4307
|
+
function startForeachDrive(runId, parentNodeId, registry) {
|
|
4308
|
+
const drive = runForeachDrive(runId, parentNodeId, registry).then(() => {
|
|
4309
|
+
return;
|
|
4310
|
+
}).catch((err) => {
|
|
4311
|
+
console.error(`[workflows] foreach drive of ${parentNodeId} in run ${runId} failed:`, err);
|
|
4312
|
+
}).finally(() => {
|
|
4313
|
+
activeDrives.delete(drive);
|
|
4314
|
+
});
|
|
4315
|
+
activeDrives.add(drive);
|
|
4316
|
+
}
|
|
4317
|
+
async function runForeachDrive(runId, parentNodeId, registry) {
|
|
4318
|
+
const release = holdWorkflowRun(runId);
|
|
4319
|
+
try {
|
|
4320
|
+
const drive = await driveForeach(runId, parentNodeId, registry);
|
|
4321
|
+
if (!drive.joined) {
|
|
4322
|
+
await parkRunIfRunning(runId);
|
|
4323
|
+
return drive;
|
|
4324
|
+
}
|
|
4325
|
+
const run = await getWorkflowRun2(runId);
|
|
4326
|
+
const workflow = run ? await getRenderedWorkflow(run.workflowId) : null;
|
|
4327
|
+
if (!run || !workflow)
|
|
4328
|
+
return drive;
|
|
4329
|
+
const ctx = run.context ?? {};
|
|
4330
|
+
if (drive.successors.length > 0) {
|
|
4331
|
+
await walkGraph(workflow.definition, runId, ctx, drive.successors, registry, workflow.id, getSecretInputKeys(workflow.input));
|
|
4332
|
+
} else {
|
|
4333
|
+
const { finalizeOrWait } = await import("./resume-5s2t0dbs.js");
|
|
4334
|
+
await finalizeOrWait(runId);
|
|
4335
|
+
}
|
|
4336
|
+
return drive;
|
|
4337
|
+
} finally {
|
|
4338
|
+
release();
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
async function getRenderedWorkflow(workflowId) {
|
|
4342
|
+
const workflow = await getWorkflow2(workflowId);
|
|
4343
|
+
return workflow ? renderAutomationTokens(workflow, workflow.params ?? {}) : null;
|
|
4344
|
+
}
|
|
4345
|
+
async function childLiveness(runId, stepId) {
|
|
4346
|
+
const run = await getWorkflowRun2(runId);
|
|
4347
|
+
if (!run || run.status !== "running" && run.status !== "waiting")
|
|
4348
|
+
return "run-ended";
|
|
4349
|
+
const step = await getWorkflowRunStep2(stepId);
|
|
4350
|
+
return step?.status === "running" ? "live" : "child-ended";
|
|
4351
|
+
}
|
|
4352
|
+
async function requeueChild(stepId) {
|
|
4353
|
+
await getDbClient2().run("UPDATE workflow_run_steps SET status = 'pending' WHERE id = ? AND status = 'running'", [stepId]);
|
|
4354
|
+
}
|
|
4355
|
+
async function parkRunIfRunning(runId) {
|
|
4356
|
+
await getDbClient2().transaction(async () => {
|
|
4357
|
+
const run = await getWorkflowRun2(runId);
|
|
4358
|
+
if (run?.status !== "running")
|
|
4359
|
+
return;
|
|
4360
|
+
const steps = await getWorkflowRunStepsByRunId2(runId);
|
|
4361
|
+
if (hasRunningStep(steps))
|
|
4362
|
+
return;
|
|
4363
|
+
if (!steps.some((step) => step.status === "waiting"))
|
|
4364
|
+
return;
|
|
4365
|
+
await updateWorkflowRun2(runId, { status: "waiting" });
|
|
4366
|
+
});
|
|
4367
|
+
}
|
|
4368
|
+
async function claimChildren(def, runId, parentNode) {
|
|
4369
|
+
const claimed = [];
|
|
4370
|
+
try {
|
|
4371
|
+
await getDbClient2().transaction(async () => {
|
|
4372
|
+
const children = latestForeachChildren(def, parentNode.id, await getWorkflowRunStepsByRunId2(runId));
|
|
4373
|
+
for (const child of children) {
|
|
4374
|
+
if (child.status === "running" && !ownedChildren.has(child.id)) {
|
|
4375
|
+
ownedChildren.add(child.id);
|
|
4376
|
+
claimed.push(child);
|
|
4377
|
+
}
|
|
4378
|
+
}
|
|
4379
|
+
const inFlight = children.filter((child) => child.status === "running" || child.status === "waiting").length;
|
|
4380
|
+
let free = foreachConcurrencyCap(parentNode) - inFlight;
|
|
4381
|
+
for (const child of children) {
|
|
4382
|
+
if (free <= 0)
|
|
4383
|
+
break;
|
|
4384
|
+
if (child.status !== "pending")
|
|
4385
|
+
continue;
|
|
4386
|
+
const row = await getDbClient2().get("UPDATE workflow_run_steps SET status = 'running' WHERE id = ? AND status = 'pending' RETURNING id", [child.id]);
|
|
4387
|
+
if (!row)
|
|
4388
|
+
continue;
|
|
4389
|
+
ownedChildren.add(child.id);
|
|
4390
|
+
claimed.push({ ...child, status: "running" });
|
|
4391
|
+
free--;
|
|
4392
|
+
}
|
|
4393
|
+
});
|
|
4394
|
+
} catch (err) {
|
|
4395
|
+
for (const child of claimed)
|
|
4396
|
+
ownedChildren.delete(child.id);
|
|
4397
|
+
throw err;
|
|
4398
|
+
}
|
|
4399
|
+
return claimed;
|
|
4400
|
+
}
|
|
4401
|
+
async function startChild(input) {
|
|
4402
|
+
const { def, child, registry, ctx, meta, bodyType, item } = input;
|
|
4403
|
+
if (!item) {
|
|
4404
|
+
return {
|
|
4405
|
+
status: "failed",
|
|
4406
|
+
error: `item "${childItemKey(child)}" is no longer present in over`
|
|
4407
|
+
};
|
|
4408
|
+
}
|
|
4409
|
+
if (!isForeachBodyType(bodyType, registry)) {
|
|
4410
|
+
return { status: "failed", error: `foreach body type "${bodyType}" cannot run` };
|
|
4411
|
+
}
|
|
4412
|
+
const bodyNode = resolveStepNode(def, child.nodeId);
|
|
4413
|
+
if (!bodyNode)
|
|
4414
|
+
return { status: "failed", error: `foreach child ${child.nodeId} has no body` };
|
|
4415
|
+
const index = childIndex(child);
|
|
4416
|
+
const bodyCtx = { ...meta.inputCtx, item, index };
|
|
4417
|
+
const interpolation = interpolateNodeConfig(bodyNode, bodyCtx);
|
|
4418
|
+
if (interpolation.scriptBodyUnresolved && interpolation.scriptBodyUnresolved.length > 0) {
|
|
4419
|
+
return {
|
|
4420
|
+
status: "failed",
|
|
4421
|
+
error: scriptBodyInterpolationError(child.nodeId, interpolation.scriptBodyUnresolved)
|
|
4422
|
+
};
|
|
4423
|
+
}
|
|
4424
|
+
if (interpolation.strictUnresolved && interpolation.strictUnresolved.length > 0) {
|
|
4425
|
+
return {
|
|
4426
|
+
status: "failed",
|
|
4427
|
+
error: systemOneUnresolvedError(child.nodeId, interpolation.strictUnresolved, bodyType)
|
|
4428
|
+
};
|
|
4429
|
+
}
|
|
4430
|
+
if (interpolation.unresolved.length > 0) {
|
|
4431
|
+
console.warn(`[workflow] Step ${child.nodeId}: unresolved interpolation tokens: ${interpolation.unresolved.join(", ")}`);
|
|
4432
|
+
await updateWorkflowRunStep2(child.id, {
|
|
4433
|
+
diagnostics: JSON.stringify({ unresolvedTokens: interpolation.unresolved })
|
|
4434
|
+
});
|
|
4435
|
+
}
|
|
4436
|
+
const config = interpolation.value;
|
|
4437
|
+
if (bodyType === "agent-task") {
|
|
4438
|
+
const linkedTask = await getTaskByWorkflowRunStepId2(child.id);
|
|
4439
|
+
if (linkedTask?.status === "failed" || linkedTask?.status === "cancelled") {
|
|
4440
|
+
await detachTaskFromWorkflowRunStep2(linkedTask.id);
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
const executor = registry.get(bodyType);
|
|
4444
|
+
const timeoutMs = resolveNodeTimeoutMs({ type: bodyType, config });
|
|
4445
|
+
let timer;
|
|
4446
|
+
let result;
|
|
4447
|
+
try {
|
|
4448
|
+
result = await Promise.race([
|
|
4449
|
+
executor.run({
|
|
4450
|
+
config,
|
|
4451
|
+
context: { ...ctx, ...bodyCtx },
|
|
4452
|
+
meta: { ...meta, stepId: child.id, nodeId: child.nodeId }
|
|
4453
|
+
}),
|
|
4454
|
+
new Promise((_, reject) => {
|
|
4455
|
+
timer = setTimeout(() => reject(new Error(`Step timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
4456
|
+
})
|
|
4457
|
+
]);
|
|
4458
|
+
} catch (err) {
|
|
4459
|
+
return { status: "failed", error: err instanceof Error ? err.message : String(err) };
|
|
4460
|
+
} finally {
|
|
4461
|
+
if (timer)
|
|
4462
|
+
clearTimeout(timer);
|
|
4463
|
+
}
|
|
4464
|
+
if (result.status === "failed") {
|
|
4465
|
+
return {
|
|
4466
|
+
status: "failed",
|
|
4467
|
+
error: result.error || "Executor returned failed status",
|
|
4468
|
+
...result.output !== undefined ? { output: result.output } : {}
|
|
4469
|
+
};
|
|
4470
|
+
}
|
|
4471
|
+
if ("async" in result && result.async) {
|
|
4472
|
+
const parked = await getDbClient2().get("UPDATE workflow_run_steps SET status = 'waiting' WHERE id = ? AND status = 'running' RETURNING id", [child.id]);
|
|
4473
|
+
if (parked && result.waitFor === "workflow.child.finished") {
|
|
4474
|
+
ownedChildren.delete(child.id);
|
|
4475
|
+
const { resumeFromChildRun } = await import("./resume-5s2t0dbs.js");
|
|
4476
|
+
await resumeFromChildRun(child.id, registry);
|
|
4477
|
+
}
|
|
4478
|
+
return null;
|
|
4479
|
+
}
|
|
4480
|
+
return {
|
|
4481
|
+
status: "completed",
|
|
4482
|
+
output: bodyType === "agent-task" ? normalizeExistingTaskOutput(result.output) : result.output,
|
|
4483
|
+
...result.nextPort ? { nextPort: result.nextPort } : {}
|
|
4484
|
+
};
|
|
4485
|
+
}
|
|
4486
|
+
function childItemKey(child) {
|
|
4487
|
+
const input = child.input;
|
|
4488
|
+
return typeof input?.itemKey === "string" ? input.itemKey : undefined;
|
|
4489
|
+
}
|
|
4490
|
+
function childIndex(child) {
|
|
4491
|
+
const input = child.input;
|
|
4492
|
+
return typeof input?.index === "number" ? input.index : 0;
|
|
4493
|
+
}
|
|
4494
|
+
function normalizeExistingTaskOutput(output) {
|
|
4495
|
+
if (typeof output !== "object" || output === null)
|
|
4496
|
+
return output;
|
|
4497
|
+
const record = output;
|
|
4498
|
+
if (typeof record.taskOutput !== "string")
|
|
4499
|
+
return output;
|
|
4500
|
+
try {
|
|
4501
|
+
const parsed = JSON.parse(record.taskOutput);
|
|
4502
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
4503
|
+
return { ...record, taskOutput: parsed };
|
|
4504
|
+
}
|
|
4505
|
+
} catch {}
|
|
4506
|
+
return output;
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4509
|
+
// src/workflows/foreach-join.ts
|
|
4510
|
+
var FOREACH_TERMINAL_STEP_STATUSES = new Set([
|
|
4511
|
+
"completed",
|
|
4512
|
+
"failed",
|
|
4513
|
+
"cancelled",
|
|
4514
|
+
"skipped"
|
|
4515
|
+
]);
|
|
4516
|
+
function parseSyntheticNodeId(nodeId) {
|
|
4517
|
+
const separatorIndex = nodeId.indexOf("#");
|
|
4518
|
+
if (separatorIndex <= 0 || separatorIndex === nodeId.length - 1)
|
|
4519
|
+
return null;
|
|
4520
|
+
return {
|
|
4521
|
+
parentNodeId: nodeId.slice(0, separatorIndex),
|
|
4522
|
+
itemKey: nodeId.slice(separatorIndex + 1)
|
|
4523
|
+
};
|
|
4524
|
+
}
|
|
4525
|
+
function resolveForeachParent(def, nodeId) {
|
|
4526
|
+
const parsed = parseSyntheticNodeId(nodeId);
|
|
4527
|
+
if (!parsed)
|
|
4528
|
+
return null;
|
|
4529
|
+
const parent = def.nodes.find((node) => node.id === parsed.parentNodeId);
|
|
4530
|
+
return parent?.type === "foreach" ? parent : null;
|
|
4531
|
+
}
|
|
4532
|
+
async function joinForeach(def, runId, childStep, ctx) {
|
|
4533
|
+
const parent = resolveForeachParent(def, childStep.nodeId);
|
|
4534
|
+
if (!parent) {
|
|
4535
|
+
throw new Error(`Step "${childStep.nodeId}" is not a foreach child`);
|
|
4536
|
+
}
|
|
4537
|
+
const children = latestForeachChildren(def, parent.id, await getWorkflowRunStepsByRunId2(runId));
|
|
4538
|
+
if (children.some((step) => !FOREACH_TERMINAL_STEP_STATUSES.has(step.status))) {
|
|
4539
|
+
return { joined: false, parentNodeId: parent.id, successors: [] };
|
|
4540
|
+
}
|
|
4541
|
+
const parentStep = await getLatestStepForNode2(runId, parent.id);
|
|
4542
|
+
if (!parentStep) {
|
|
4543
|
+
throw new Error(`Waiting foreach parent step "${parent.id}" was not found`);
|
|
4544
|
+
}
|
|
4545
|
+
if (parentStep.status === "completed") {
|
|
4546
|
+
return { joined: false, parentNodeId: parent.id, successors: [] };
|
|
4547
|
+
}
|
|
4548
|
+
const aggregate = buildForeachAggregate(children);
|
|
4549
|
+
await checkpointStep(runId, parentStep.id, parent.id, { output: aggregate }, ctx);
|
|
4550
|
+
return {
|
|
4551
|
+
joined: true,
|
|
4552
|
+
parentNodeId: parent.id,
|
|
4553
|
+
successors: getSuccessors(def, parent.id)
|
|
4554
|
+
};
|
|
4555
|
+
}
|
|
4556
|
+
function latestForeachChildren(def, parentNodeId, steps) {
|
|
4557
|
+
const latest = new Map;
|
|
4558
|
+
for (const step of steps) {
|
|
4559
|
+
if (resolveForeachParent(def, step.nodeId)?.id === parentNodeId)
|
|
4560
|
+
latest.set(step.nodeId, step);
|
|
4561
|
+
}
|
|
4562
|
+
return [...latest.values()].sort((a, b) => childIndex2(a) - childIndex2(b));
|
|
4563
|
+
}
|
|
4564
|
+
function buildForeachAggregate(children) {
|
|
4565
|
+
const latest = new Map;
|
|
4566
|
+
for (const step of children)
|
|
4567
|
+
latest.set(step.nodeId, step);
|
|
4568
|
+
const orderedChildren = [...latest.values()].sort((a, b) => childIndex2(a) - childIndex2(b));
|
|
4569
|
+
const results = orderedChildren.map((step) => {
|
|
4570
|
+
const parsed = parseSyntheticNodeId(step.nodeId);
|
|
4571
|
+
const status = resultStatus(step);
|
|
4572
|
+
return {
|
|
4573
|
+
itemKey: parsed.itemKey,
|
|
4574
|
+
status,
|
|
4575
|
+
output: step.output
|
|
4576
|
+
};
|
|
4577
|
+
});
|
|
4578
|
+
const failedCount = results.filter((result) => result.status !== "completed").length;
|
|
4579
|
+
const cancelledCount = results.filter((result) => result.status === "cancelled").length;
|
|
4580
|
+
const aggregate = {
|
|
4581
|
+
results,
|
|
4582
|
+
okCount: results.length - failedCount,
|
|
4583
|
+
failedCount,
|
|
4584
|
+
...cancelledCount > 0 ? { cancelledCount } : {}
|
|
4585
|
+
};
|
|
4586
|
+
return aggregate;
|
|
4587
|
+
}
|
|
4588
|
+
function childIndex2(step) {
|
|
4589
|
+
if (typeof step.input !== "object" || step.input === null)
|
|
4590
|
+
return Number.MAX_SAFE_INTEGER;
|
|
4591
|
+
const index = step.input.index;
|
|
4592
|
+
return typeof index === "number" ? index : Number.MAX_SAFE_INTEGER;
|
|
4593
|
+
}
|
|
4594
|
+
function resultStatus(step) {
|
|
4595
|
+
if (step.status !== "completed")
|
|
4596
|
+
return step.status;
|
|
4597
|
+
return step.error ? "failed" : "completed";
|
|
4598
|
+
}
|
|
4599
|
+
async function settleForeachChild(def, runId, childStepId, outcome, ctx, options = {}) {
|
|
4600
|
+
const from = options.from ?? ["waiting"];
|
|
4601
|
+
return await getDbClient2().transaction(async () => {
|
|
4602
|
+
const unclaimed = {
|
|
4603
|
+
claimed: false,
|
|
4604
|
+
foreachChild: true,
|
|
4605
|
+
joined: false,
|
|
4606
|
+
successors: [],
|
|
4607
|
+
runFailed: false
|
|
4608
|
+
};
|
|
4609
|
+
const current = await getWorkflowRunStep2(childStepId);
|
|
4610
|
+
if (!current || !from.includes(current.status))
|
|
4611
|
+
return unclaimed;
|
|
4612
|
+
const parent = resolveForeachParent(def, current.nodeId);
|
|
4613
|
+
if (!parent)
|
|
4614
|
+
throw new Error(`Step "${current.nodeId}" is not a foreach child`);
|
|
4615
|
+
const run = await getWorkflowRun2(runId);
|
|
4616
|
+
if (!run)
|
|
4617
|
+
return unclaimed;
|
|
4618
|
+
const finishedAt = new Date().toISOString();
|
|
4619
|
+
await updateWorkflowRunStep2(childStepId, {
|
|
4620
|
+
status: outcome.status,
|
|
4621
|
+
...outcome.output !== undefined ? { output: outcome.output } : {},
|
|
4622
|
+
...outcome.error !== undefined ? { error: outcome.error } : {},
|
|
4623
|
+
...outcome.nextPort ? { nextPort: outcome.nextPort } : {},
|
|
4624
|
+
finishedAt
|
|
4625
|
+
});
|
|
4626
|
+
if (run.status !== "running" && run.status !== "waiting")
|
|
4627
|
+
return unclaimed;
|
|
4628
|
+
if (outcome.status !== "completed" && effectiveFailurePolicy(parent, def) === "fail-fast") {
|
|
4629
|
+
const reason = options.runError ?? outcome.error ?? `foreach item ${current.nodeId} failed`;
|
|
4630
|
+
await cancelQueuedForeachChildren(def, runId, parent.id, current.nodeId);
|
|
4631
|
+
await updateWorkflowRun2(runId, { status: "failed", error: reason, finishedAt });
|
|
4632
|
+
return { ...unclaimed, claimed: true, runFailed: true };
|
|
4633
|
+
}
|
|
4634
|
+
const join = await joinForeach(def, runId, current, ctx);
|
|
4635
|
+
await updateWorkflowRun2(runId, { status: "running" });
|
|
4636
|
+
const registry = options.registry;
|
|
4637
|
+
if (!join.joined && registry && await hasQueuedChild(def, runId, parent.id)) {
|
|
4638
|
+
getDbClient2().afterCommit(() => startForeachDrive(runId, parent.id, registry));
|
|
4639
|
+
}
|
|
4640
|
+
return {
|
|
4641
|
+
claimed: true,
|
|
4642
|
+
foreachChild: true,
|
|
4643
|
+
joined: join.joined,
|
|
4644
|
+
successors: join.successors,
|
|
4645
|
+
runFailed: false
|
|
4646
|
+
};
|
|
4647
|
+
});
|
|
4648
|
+
}
|
|
4649
|
+
async function cancelQueuedForeachChildren(def, runId, parentNodeId, failedChildNodeId) {
|
|
4650
|
+
const failedKey = failedChildNodeId ? parseSyntheticNodeId(failedChildNodeId)?.itemKey ?? failedChildNodeId : undefined;
|
|
4651
|
+
const error = failedKey ? `not started: fail-fast after item ${failedKey} failed` : "not started: fail-fast after the run failed";
|
|
4652
|
+
const finishedAt = new Date().toISOString();
|
|
4653
|
+
let cancelled = 0;
|
|
4654
|
+
await getDbClient2().transaction(async () => {
|
|
4655
|
+
const children = latestForeachChildren(def, parentNodeId, await getWorkflowRunStepsByRunId2(runId));
|
|
4656
|
+
for (const child of children) {
|
|
4657
|
+
if (child.status !== "pending")
|
|
4658
|
+
continue;
|
|
4659
|
+
const row = await getDbClient2().get("UPDATE workflow_run_steps SET status = 'cancelled', error = ?, finishedAt = ? WHERE id = ? AND status = 'pending' RETURNING id", [error, finishedAt, child.id]);
|
|
4660
|
+
if (row)
|
|
4661
|
+
cancelled++;
|
|
4662
|
+
}
|
|
4663
|
+
});
|
|
4664
|
+
return cancelled;
|
|
4665
|
+
}
|
|
4666
|
+
async function hasQueuedChild(def, runId, parentNodeId) {
|
|
4667
|
+
return latestForeachChildren(def, parentNodeId, await getWorkflowRunStepsByRunId2(runId)).some((child) => child.status === "pending");
|
|
4668
|
+
}
|
|
4669
|
+
|
|
4670
|
+
// src/workflows/foreach-body.ts
|
|
4671
|
+
var FOREACH_BODY_REJECTED = {
|
|
4672
|
+
foreach: "nested fan-out is not supported: synthetic ids and the join resolve one level only"
|
|
4673
|
+
};
|
|
4674
|
+
var FOREACH_BODY_ACCEPTED = {
|
|
4675
|
+
script: "inline",
|
|
4676
|
+
"swarm-script": "inline",
|
|
4677
|
+
"raw-llm": "inline",
|
|
4678
|
+
notify: "inline",
|
|
4679
|
+
"property-match": "inline",
|
|
4680
|
+
"code-match": "inline",
|
|
4681
|
+
validate: "inline",
|
|
4682
|
+
vcs: "inline",
|
|
4683
|
+
"system-one-extraction": "inline",
|
|
4684
|
+
"agent-task": "parks",
|
|
4685
|
+
"sub-workflow": "parks",
|
|
4686
|
+
"human-in-the-loop": "parks",
|
|
4687
|
+
wait: "parks",
|
|
4688
|
+
"system-one-decision": "parks"
|
|
4689
|
+
};
|
|
4690
|
+
function isForeachBodyType(type, registry) {
|
|
4691
|
+
if (Object.hasOwn(FOREACH_BODY_REJECTED, type))
|
|
4692
|
+
return false;
|
|
4693
|
+
return registry ? registry.has(type) : Object.hasOwn(FOREACH_BODY_ACCEPTED, type);
|
|
4694
|
+
}
|
|
4695
|
+
function foreachBodyTypeError(nodeId, type, registry) {
|
|
4696
|
+
const reason = FOREACH_BODY_REJECTED[type];
|
|
4697
|
+
if (reason !== undefined) {
|
|
4698
|
+
return `Node "${nodeId}" foreach config.body.type "${type}" is not supported: ${reason} (a ${type} body inside foreach "${nodeId}" is rejected)`;
|
|
4699
|
+
}
|
|
4700
|
+
if (registry && !registry.has(type)) {
|
|
4701
|
+
return `Node "${nodeId}" foreach config.body.type "${type}" is not a registered executor`;
|
|
4702
|
+
}
|
|
4703
|
+
return null;
|
|
4704
|
+
}
|
|
4705
|
+
function resolveStepNode(def, nodeId) {
|
|
4706
|
+
const node = def.nodes.find((candidate) => candidate.id === nodeId);
|
|
4707
|
+
if (node)
|
|
4708
|
+
return node;
|
|
4709
|
+
const parent = resolveForeachParent(def, nodeId);
|
|
4710
|
+
if (!parent)
|
|
4711
|
+
return null;
|
|
4712
|
+
const body = parent.config.body;
|
|
4713
|
+
if (!isRecord3(body) || typeof body.type !== "string")
|
|
4714
|
+
return null;
|
|
4715
|
+
const inputs = parent.inputs ? Object.fromEntries(Object.entries(parent.inputs).filter(([alias]) => !FOREACH_ITEM_BINDINGS.has(alias))) : undefined;
|
|
4716
|
+
return {
|
|
4717
|
+
id: nodeId,
|
|
4718
|
+
type: body.type,
|
|
4719
|
+
config: isRecord3(body.config) ? body.config : {},
|
|
4720
|
+
...inputs ? { inputs } : {}
|
|
4721
|
+
};
|
|
4722
|
+
}
|
|
4723
|
+
var FOREACH_ITEM_BINDINGS = new Set(["item", "index"]);
|
|
4724
|
+
var FOREACH_FAILURE_POLICIES = ["fail-fast", "collect-outcomes"];
|
|
4725
|
+
function effectiveFailurePolicy(node, def) {
|
|
4726
|
+
const configured = node.config.failurePolicy;
|
|
4727
|
+
if (configured === "fail-fast" || configured === "collect-outcomes")
|
|
4728
|
+
return configured;
|
|
4729
|
+
return def.onNodeFailure === "continue" ? "collect-outcomes" : "fail-fast";
|
|
4730
|
+
}
|
|
4731
|
+
function foreachConcurrencyCap(node) {
|
|
4732
|
+
const cap = node.config.concurrency;
|
|
4733
|
+
return typeof cap === "number" && Number.isInteger(cap) && cap >= 1 ? cap : Number.POSITIVE_INFINITY;
|
|
4734
|
+
}
|
|
4735
|
+
function stepFailureMode(def, nodeId) {
|
|
4736
|
+
const parent = resolveForeachParent(def, nodeId);
|
|
4737
|
+
if (parent)
|
|
4738
|
+
return effectiveFailurePolicy(parent, def) === "fail-fast" ? "fail" : "continue";
|
|
4739
|
+
return def.onNodeFailure ?? "fail";
|
|
4740
|
+
}
|
|
4741
|
+
function isRecord3(value) {
|
|
4742
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4743
|
+
}
|
|
4744
|
+
|
|
4745
|
+
// src/workflows/definition.ts
|
|
4746
|
+
function getNextTargets(next) {
|
|
4747
|
+
if (typeof next === "string")
|
|
4748
|
+
return [next];
|
|
4749
|
+
if (Array.isArray(next))
|
|
4750
|
+
return next;
|
|
4751
|
+
return Object.values(next);
|
|
4752
|
+
}
|
|
4753
|
+
function generateEdges(def) {
|
|
4754
|
+
const edges = [];
|
|
4755
|
+
for (const node of def.nodes) {
|
|
4756
|
+
if (!node.next)
|
|
4757
|
+
continue;
|
|
4758
|
+
if (typeof node.next === "string") {
|
|
4759
|
+
edges.push({
|
|
4760
|
+
id: `${node.id}→${node.next}`,
|
|
4761
|
+
source: node.id,
|
|
4762
|
+
target: node.next,
|
|
4763
|
+
sourcePort: "default"
|
|
4764
|
+
});
|
|
4765
|
+
} else if (Array.isArray(node.next)) {
|
|
4766
|
+
for (const targetId of node.next) {
|
|
4767
|
+
edges.push({
|
|
4768
|
+
id: `${node.id}→${targetId}`,
|
|
4769
|
+
source: node.id,
|
|
4770
|
+
target: targetId,
|
|
4771
|
+
sourcePort: "default"
|
|
4772
|
+
});
|
|
4773
|
+
}
|
|
4774
|
+
} else {
|
|
4775
|
+
for (const [port, targetId] of Object.entries(node.next)) {
|
|
4776
|
+
edges.push({
|
|
4777
|
+
id: `${node.id}→${targetId}:${port}`,
|
|
4778
|
+
source: node.id,
|
|
4779
|
+
target: targetId,
|
|
4780
|
+
sourcePort: port
|
|
4781
|
+
});
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4785
|
+
return edges;
|
|
4786
|
+
}
|
|
4787
|
+
function findEntryNodes(def) {
|
|
4788
|
+
const targets = new Set;
|
|
4789
|
+
for (const node of def.nodes) {
|
|
4790
|
+
if (!node.next)
|
|
4791
|
+
continue;
|
|
4792
|
+
for (const targetId of getNextTargets(node.next)) {
|
|
4793
|
+
targets.add(targetId);
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
return def.nodes.filter((n) => !targets.has(n.id));
|
|
4797
|
+
}
|
|
4798
|
+
function resolveValidationPort(next, executorPort, validationPassed) {
|
|
4799
|
+
if (validationPassed === undefined)
|
|
4800
|
+
return executorPort;
|
|
4801
|
+
if (!next || typeof next !== "object" || Array.isArray(next))
|
|
4802
|
+
return executorPort;
|
|
4803
|
+
if (executorPort && Object.hasOwn(next, executorPort))
|
|
4804
|
+
return executorPort;
|
|
4805
|
+
return validationPassed ? "pass" : "fail";
|
|
4806
|
+
}
|
|
4807
|
+
function getSuccessors(def, nodeId, port) {
|
|
4808
|
+
const node = def.nodes.find((n) => n.id === nodeId);
|
|
4809
|
+
if (!node?.next)
|
|
4810
|
+
return [];
|
|
4811
|
+
const targetIds = [];
|
|
4812
|
+
if (typeof node.next === "string") {
|
|
4813
|
+
targetIds.push(node.next);
|
|
4814
|
+
} else if (Array.isArray(node.next)) {
|
|
4815
|
+
targetIds.push(...node.next);
|
|
4816
|
+
} else {
|
|
4817
|
+
if (port) {
|
|
4818
|
+
const targetId = node.next[port];
|
|
4819
|
+
if (targetId)
|
|
4820
|
+
targetIds.push(targetId);
|
|
4821
|
+
} else {
|
|
4822
|
+
targetIds.push(...Object.values(node.next));
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
return targetIds.map((id) => def.nodes.find((n) => n.id === id)).filter((n) => n != null);
|
|
4826
|
+
}
|
|
4827
|
+
function isUpstream(def, sourceId, targetId) {
|
|
4828
|
+
const reverseDeps = new Map;
|
|
4829
|
+
for (const node of def.nodes) {
|
|
4830
|
+
if (!node.next)
|
|
4831
|
+
continue;
|
|
4832
|
+
for (const target of getNextTargets(node.next)) {
|
|
4833
|
+
if (!reverseDeps.has(target))
|
|
4834
|
+
reverseDeps.set(target, []);
|
|
4835
|
+
reverseDeps.get(target).push(node.id);
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
const visited = new Set;
|
|
4839
|
+
const queue = [targetId];
|
|
4840
|
+
while (queue.length > 0) {
|
|
4841
|
+
const current = queue.shift();
|
|
4842
|
+
const preds = reverseDeps.get(current) || [];
|
|
4843
|
+
for (const pred of preds) {
|
|
4844
|
+
if (visited.has(pred))
|
|
4845
|
+
continue;
|
|
4846
|
+
visited.add(pred);
|
|
4847
|
+
if (pred === sourceId)
|
|
4848
|
+
return true;
|
|
4849
|
+
queue.push(pred);
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4852
|
+
return false;
|
|
4853
|
+
}
|
|
4854
|
+
function containsInterpolationToken(value) {
|
|
4855
|
+
if (typeof value === "string")
|
|
4856
|
+
return value.includes("{{") && value.includes("}}");
|
|
4857
|
+
if (Array.isArray(value))
|
|
4858
|
+
return value.some(containsInterpolationToken);
|
|
4859
|
+
if (value && typeof value === "object") {
|
|
4860
|
+
return Object.values(value).some(containsInterpolationToken);
|
|
4861
|
+
}
|
|
4862
|
+
return false;
|
|
4863
|
+
}
|
|
4864
|
+
function definitionNodeIds(def) {
|
|
4865
|
+
return new Set(def.nodes.map((node) => node.id));
|
|
4866
|
+
}
|
|
4867
|
+
function validateDefinition(def, registry, options = {}) {
|
|
4868
|
+
const errors = [];
|
|
4869
|
+
const nodeIds = new Set(def.nodes.map((n) => n.id));
|
|
4870
|
+
for (const node of def.nodes) {
|
|
4871
|
+
if (node.id.includes("#") && !options.legacyNodeIds?.has(node.id)) {
|
|
4872
|
+
errors.push(`Node "${node.id}" contains reserved character "#"`);
|
|
4873
|
+
}
|
|
4874
|
+
if (node.type === "human-in-the-loop" && typeof node.config.questions === "string" && !/^\{\{[^}]+\}\}$/.test(node.config.questions.trim())) {
|
|
4875
|
+
errors.push(`Node "${node.id}" human-in-the-loop config.questions must be an array or one exact {{interpolation}} token`);
|
|
4876
|
+
}
|
|
4877
|
+
errors.push(...systemOneStaticShapeViolations(node), ...systemOneExtractionStaticShapeViolations(node), ...systemOneRetryViolations(node));
|
|
4878
|
+
if (node.type === "foreach") {
|
|
4879
|
+
validateForeachNode(node, errors, registry);
|
|
4880
|
+
if (node.id.includes("#")) {
|
|
4881
|
+
errors.push(`Node "${node.id}" cannot be a foreach: its id contains "#", which is reserved for synthetic child ids`);
|
|
4882
|
+
}
|
|
4883
|
+
if (isUpstream(def, node.id, node.id)) {
|
|
4884
|
+
errors.push("foreach inside a loop is not supported in v1");
|
|
4885
|
+
}
|
|
4886
|
+
const collidingSibling = def.nodes.find((other) => other.id !== node.id && other.id.startsWith(`${node.id}#`));
|
|
4887
|
+
if (collidingSibling) {
|
|
4888
|
+
errors.push(`Node "${node.id}" cannot be a foreach: node "${collidingSibling.id}" collides with its synthetic child id space ("${node.id}#…")`);
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
}
|
|
4892
|
+
for (const node of def.nodes) {
|
|
4893
|
+
if (!node.next)
|
|
4894
|
+
continue;
|
|
4895
|
+
if (typeof node.next === "string") {
|
|
4896
|
+
if (!nodeIds.has(node.next)) {
|
|
4897
|
+
errors.push(`Node "${node.id}" references non-existent next target "${node.next}"`);
|
|
4898
|
+
}
|
|
4899
|
+
} else if (Array.isArray(node.next)) {
|
|
4900
|
+
for (const targetId of node.next) {
|
|
2684
4901
|
if (!nodeIds.has(targetId)) {
|
|
2685
4902
|
errors.push(`Node "${node.id}" fan-out references non-existent target "${targetId}"`);
|
|
2686
4903
|
}
|
|
@@ -2728,11 +4945,12 @@ function validateDefinition(def, registry, options = {}) {
|
|
|
2728
4945
|
continue;
|
|
2729
4946
|
}
|
|
2730
4947
|
reportStaticConfigIssues(node, registry.get(node.type).configSchema, node.config, errors);
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
4948
|
+
const body = node.type === "foreach" ? node.config.body : undefined;
|
|
4949
|
+
const bodyRecord = typeof body === "object" && body !== null && !Array.isArray(body) ? body : undefined;
|
|
4950
|
+
if (bodyRecord && typeof bodyRecord.type === "string" && isForeachBodyType(bodyRecord.type, registry)) {
|
|
4951
|
+
const bodyConfig = bodyRecord.config;
|
|
2734
4952
|
if (typeof bodyConfig === "object" && bodyConfig !== null && !Array.isArray(bodyConfig)) {
|
|
2735
|
-
reportStaticConfigIssues(node, registry.get(
|
|
4953
|
+
reportStaticConfigIssues(node, registry.get(bodyRecord.type).configSchema, bodyConfig, errors, "config.body.config");
|
|
2736
4954
|
}
|
|
2737
4955
|
}
|
|
2738
4956
|
}
|
|
@@ -2780,10 +4998,13 @@ function reportStaticConfigIssues(node, schema, config, errors, pathPrefix = "co
|
|
|
2780
4998
|
errors.push(`Node "${node.id}" (${node.type}) ${path} has invalid value ${renderedValue}: ${issue.message}`);
|
|
2781
4999
|
}
|
|
2782
5000
|
}
|
|
2783
|
-
function validateForeachNode(node, errors) {
|
|
5001
|
+
function validateForeachNode(node, errors, registry) {
|
|
2784
5002
|
const config = node.config;
|
|
2785
|
-
if (
|
|
2786
|
-
errors.push(`Node "${node.id}" foreach concurrency
|
|
5003
|
+
if (config.concurrency !== undefined && !(Number.isInteger(config.concurrency) && config.concurrency >= 1)) {
|
|
5004
|
+
errors.push(`Node "${node.id}" foreach config.concurrency must be an integer of 1 or more, got ${JSON.stringify(config.concurrency)}`);
|
|
5005
|
+
}
|
|
5006
|
+
if (config.failurePolicy !== undefined && !FOREACH_FAILURE_POLICIES.includes(config.failurePolicy)) {
|
|
5007
|
+
errors.push(`Node "${node.id}" foreach config.failurePolicy must be one of ${FOREACH_FAILURE_POLICIES.map((p) => `"${p}"`).join(", ")}, got ${JSON.stringify(config.failurePolicy)}`);
|
|
2787
5008
|
}
|
|
2788
5009
|
if (node.outputSchema !== undefined || node.validation !== undefined) {
|
|
2789
5010
|
errors.push(`Node "${node.id}" foreach node-level outputSchema/validation is not supported in v1`);
|
|
@@ -2802,15 +5023,19 @@ function validateForeachNode(node, errors) {
|
|
|
2802
5023
|
return;
|
|
2803
5024
|
}
|
|
2804
5025
|
const bodyRecord = body;
|
|
2805
|
-
if (bodyRecord.type !== "
|
|
2806
|
-
errors.push(`Node "${node.id}" foreach config.body.type must be
|
|
5026
|
+
if (typeof bodyRecord.type !== "string" || bodyRecord.type.length === 0) {
|
|
5027
|
+
errors.push(`Node "${node.id}" foreach config.body.type must be a non-empty string`);
|
|
5028
|
+
} else {
|
|
5029
|
+
const typeError = foreachBodyTypeError(node.id, bodyRecord.type, registry);
|
|
5030
|
+
if (typeError)
|
|
5031
|
+
errors.push(typeError);
|
|
2807
5032
|
}
|
|
2808
5033
|
if (typeof bodyRecord.config !== "object" || bodyRecord.config === null || Array.isArray(bodyRecord.config)) {
|
|
2809
5034
|
errors.push(`Node "${node.id}" foreach config.body.config must be an object`);
|
|
2810
5035
|
return;
|
|
2811
5036
|
}
|
|
2812
5037
|
const bodyConfig = bodyRecord.config;
|
|
2813
|
-
if (typeof bodyConfig.template !== "string") {
|
|
5038
|
+
if (bodyRecord.type === "agent-task" && typeof bodyConfig.template !== "string") {
|
|
2814
5039
|
errors.push(`Node "${node.id}" foreach agent-task body.config.template must be a string`);
|
|
2815
5040
|
}
|
|
2816
5041
|
}
|
|
@@ -2858,4 +5083,4 @@ function applyDefinitionPatch(def, patch) {
|
|
|
2858
5083
|
return { definition: patchedDef, errors };
|
|
2859
5084
|
}
|
|
2860
5085
|
|
|
2861
|
-
export { BaseExecutor, postApprovalCancellationUpdates, createApprovalFollowUpTask, HumanInTheLoopExecutor, resolveWorkflowLlmConfig, SYSTEM_ONE_PROVIDER_IDS, SystemOneExtractionExecutor, SYSTEM_ONE_DECISION_NODE_TYPE, SystemOneDecisionConfigSchema,
|
|
5086
|
+
export { notifyAutomationPreflightFailure, automationIntegrationFixUrl, isNeedsSetupFailure, preflightAutomation, renderAutomationTokens, getAutomationSetupStates, schedulePreflightInput, listEnabledAutomationPreflightInputs, recordSchedulePreflightFailure, BaseExecutor, postApprovalCancellationUpdates, createApprovalFollowUpTask, HumanInTheLoopExecutor, resolveWorkflowLlmConfig, SYSTEM_ONE_PROVIDER_IDS, SystemOneExtractionExecutor, SYSTEM_ONE_DECISION_NODE_TYPE, SystemOneDecisionConfigSchema, SystemOneDecisionExecutor, checkpointStep, checkpointStepFailure, checkpointStepWaiting, FAILED_TASK_OUTPUT_PREFIX, FOREACH_ITEMS_WAIT_FOR, loadCompletedStepRouting, resolveInputValue, getSecretInputKeys, validateJsonSchema, findJsonSchemaShapeErrors, getMaxWorkflowStepsPerRun, findWorkflowReadinessProblems, formatReadinessProblems, workflowSaveWarnings, withSaveWarnings, runStepValidation, TriggerSchemaError, startWorkflowExecution2, failRunOnUnreadableReplay, isWorkflowRunActive, holdWorkflowRun, walkGraph, hasRunningStep, findReadyNodes, scriptBodyInterpolationError, ForeachExecutor, rebuildNodeExecutionInput, isForeachChildOwned, parkAndDriveForeach, startForeachDrive, runForeachDrive, FOREACH_TERMINAL_STEP_STATUSES, parseSyntheticNodeId, resolveForeachParent, settleForeachChild, resolveStepNode, stepFailureMode, getNextTargets, generateEdges, resolveValidationPort, getSuccessors, definitionNodeIds, validateDefinition, applyDefinitionPatch };
|