@desplega.ai/agent-swarm 2.4.5 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/{acp-adapter-2wdkktv6.js → acp-adapter-11gp67nr.js} +8 -6
- package/dist/{actions-c40ebcm4.js → actions-8p2kze8j.js} +23 -21
- package/dist/{amp-adapter-wb67m95z.js → amp-adapter-z4qvhacj.js} +2 -2
- package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
- package/dist/{app-353vre8g.js → app-0qp0a2gk.js} +3 -3
- package/dist/{assistant-sj1njh23.js → assistant-jedd3ktm.js} +24 -22
- package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
- package/dist/{boot-reembed-qdkvxeem.js → boot-reembed-4xwq5tg0.js} +1 -1
- package/dist/{boot-reembed-b9c3az3c.js → boot-reembed-n4wq5dd2.js} +1 -1
- package/dist/{boot-scrub-sweep-m3wh8eks.js → boot-scrub-sweep-69h90xcq.js} +4 -4
- package/dist/{boot-seeds-j2273s54.js → boot-seeds-3vn0m9sg.js} +4 -4
- package/dist/{claude-adapter-m7q0a085.js → claude-adapter-221mvad5.js} +8 -6
- package/dist/{claude-managed-adapter-5ptb6tzs.js → claude-managed-adapter-gt6pa35d.js} +2 -2
- package/dist/{claude-managed-setup-smrqk5j5.js → claude-managed-setup-c3hp36ph.js} +4 -4
- package/dist/{claude-sdk-session-38xnz21e.js → claude-sdk-session-8thjsk5e.js} +9012 -8928
- package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
- package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
- package/dist/{cli-dk44dxec.js → cli-0y6f0ckq.js} +1 -1
- package/dist/{cli-kpw04va0.js → cli-0z3vjrpm.js} +1 -1
- package/dist/{cli-94gwyg7m.js → cli-179xcyky.js} +9 -9
- package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
- package/dist/{cli-4zfqgjc5.js → cli-1pr672hz.js} +2 -2
- package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
- package/dist/{cli-43x64hv3.js → cli-36j4jnk3.js} +4 -4
- package/dist/{cli-k8g16gz3.js → cli-3qv8naq8.js} +115 -119
- package/dist/cli-3zwye9sn.js +1193 -0
- package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
- package/dist/{cli-v8fjakwf.js → cli-4w24f7z3.js} +1 -1
- package/dist/{cli-n72rg53p.js → cli-5kzk3472.js} +812 -418
- package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
- package/dist/{cli-j99azg3g.js → cli-5rvg1ddk.js} +5 -5
- package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
- package/dist/{cli-848hxdwj.js → cli-6rg2jn2y.js} +2 -1
- package/dist/{cli-fv5hepyy.js → cli-7bwtc6yx.js} +3 -3
- package/dist/{cli-ms85309d.js → cli-7g40g8hq.js} +11 -11
- package/dist/{cli-xpa64rek.js → cli-92mqenkc.js} +2362 -159
- package/dist/{cli-018r4jq6.js → cli-9ce2xw98.js} +8 -8
- package/dist/{cli-1syn2xy1.js → cli-bgdtv62r.js} +32 -18
- package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
- package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
- package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
- package/dist/{cli-wt0jf9hm.js → cli-fcc5qe3f.js} +553 -193
- package/dist/{cli-ftc1g5tj.js → cli-fes51ya2.js} +3 -3
- package/dist/{cli-3a45zcgm.js → cli-ftmq4nvk.js} +2 -2
- package/dist/{cli-37j04k05.js → cli-fy1jyyym.js} +13 -13
- package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
- package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
- package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
- package/dist/{cli-g35m2xwm.js → cli-j1eghqfm.js} +2 -2
- package/dist/{cli-qfxc84gh.js → cli-jpxsrxcb.js} +1 -1
- package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
- package/dist/{cli-p14m2cnp.js → cli-mbfj11fq.js} +1 -1
- package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
- package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
- package/dist/{cli-f01h25yt.js → cli-n6kmqngw.js} +2 -2
- package/dist/{cli-v0yrfmdh.js → cli-nnvjb17s.js} +3 -3
- package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
- package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
- package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
- package/dist/cli-pkaqk57w.js +128 -0
- package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
- package/dist/{cli-9yqgm9nd.js → cli-r25qpaxt.js} +161 -63
- package/dist/{cli-eww120g8.js → cli-rh5smt8t.js} +18 -4
- package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
- package/dist/{cli-a92r1127.js → cli-s4sntwt4.js} +2 -2
- package/dist/{cli-be2161h1.js → cli-segs7hx9.js} +6 -6
- package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
- package/dist/{cli-z7d29rw9.js → cli-sy8f9705.js} +4 -4
- package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
- package/dist/{cli-st8aw1m6.js → cli-we0ra057.js} +4 -4
- package/dist/{cli-7c9qs530.js → cli-x21dqn18.js} +5 -5
- package/dist/{cli-1sp861s4.js → cli-x6vf4sad.js} +11 -11
- package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
- package/dist/{cli-tw4p90jb.js → cli-xawhpgc9.js} +5 -5
- package/dist/{cli-f0g3pgvf.js → cli-xd4eqtr2.js} +2 -2
- package/dist/{cli-1f3qzw6y.js → cli-xt768r5m.js} +2 -2
- package/dist/{cli-cxxe61f6.js → cli-y4x8ww6s.js} +2 -2
- package/dist/{cli-bnes4z6b.js → cli-y9jvxfhk.js} +3 -3
- package/dist/{cli-qwhy2qma.js → cli-z63a269p.js} +2 -2
- package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
- package/dist/cli.js +29 -27
- package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
- package/dist/{codex-adapter-tn6k5mkg.js → codex-adapter-tcg1q3jk.js} +6 -4
- package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
- package/dist/{codex-session-runner-7vjt7jbw.js → codex-session-runner-xfx4wd9x.js} +7 -5
- package/dist/{commands-t7rz9146.js → commands-ew83nj70.js} +2 -2
- package/dist/{controller-m2jasma5.js → controller-gnq1k1mf.js} +4 -4
- package/dist/{controllers-connect-9ftpex99.js → controllers-connect-tw1dan9r.js} +25 -23
- package/dist/{cursor-adapter-mjv79e4z.js → cursor-adapter-yq5xgfcg.js} +1 -1
- package/dist/{db-6sfrn9g3.js → db-7gbc53fd.js} +14 -2
- package/dist/{db-rn673r06.js → db-t86xrnes.js} +3 -3
- package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
- package/dist/{dispatcher-c155xz0p.js → dispatcher-64bdyhps.js} +22 -20
- package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
- package/dist/{dsh-adapter-3mmszc1x.js → dsh-adapter-tvhc4k2d.js} +1 -1
- package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
- package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
- package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
- package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
- package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
- package/dist/{external-channel-35k6tkv8.js → external-channel-e9nw24zv.js} +2 -2
- package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
- package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
- package/dist/{grok-adapter-tfrq7tg1.js → grok-adapter-v82srhrx.js} +9 -7
- package/dist/handlers-c9ajzhbn.js +41 -0
- package/dist/{heartbeat-dyg4npkp.js → heartbeat-2vqs1319.js} +25 -23
- package/dist/{hook-x26b31bg.js → hook-01nk13wx.js} +7 -5
- package/dist/{hook-drspbwpe.js → hook-jzpheegg.js} +9 -7
- package/dist/{http-pk972xyw.js → http-fc1k6ec8.js} +544 -76
- package/dist/index-07ef7nkm.js +42 -0
- package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
- package/dist/index-7yts8n9s.js +43 -0
- package/dist/index-q3t40ryv.js +46 -0
- package/dist/{index-arytgj5q.js → index-sepja052.js} +1 -1
- package/dist/{index-jwxkkex2.js → index-zdycv3ja.js} +13 -11
- package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
- package/dist/{keepalive-7yrng7px.js → keepalive-5nh2rhmr.js} +4 -4
- package/dist/lead-aayq64jn.js +79 -0
- package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
- package/dist/{maintenance-e6cemdqt.js → maintenance-hjhzdt3k.js} +1 -1
- package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
- package/dist/{node-de6akczr.js → node-jzjj7r9x.js} +1 -1
- package/dist/{oauth-refresh-sweep-yw3xdbcj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
- package/dist/{onboard-0a9y5yrc.js → onboard-1zq3m7db.js} +2 -2
- package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
- package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
- package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
- package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
- package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
- package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
- package/dist/{otel-impl-trjpkfb7.js → otel-impl-h9yyza47.js} +2 -2
- package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
- package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
- package/dist/{pricing-refresh-d2b5my0y.js → pricing-refresh-nevhat8h.js} +3 -3
- package/dist/{rbac-roles-nxh5ahwk.js → rbac-roles-4y6ngvzq.js} +1 -1
- package/dist/{rbac-roles-hey3yv9d.js → rbac-roles-exbg8xb6.js} +2 -2
- package/dist/{render-v2-rr3p2atg.js → render-v2-cfsh13z3.js} +3 -3
- package/dist/{resume-2efkf1s1.js → resume-5s2t0dbs.js} +10 -8
- package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
- package/dist/{session-access-s2mttxpw.js → session-access-dnbyp3cn.js} +6 -4
- package/dist/{setup-vypma5yt.js → setup-1wj1y77j.js} +1 -1
- package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
- package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
- package/dist/{work-objects-rvhckdkp.js → work-objects-7mg6216g.js} +25 -23
- package/dist/worker-nwb789mg.js +79 -0
- package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
- package/openapi.json +785 -5
- package/package.json +9 -9
- package/src/assets/key.ts +17 -0
- package/src/azure-devops/handlers.ts +1 -1
- package/src/be/db/approvals.ts +18 -0
- package/src/be/db/session-moves.ts +164 -0
- package/src/be/db/session-tree.ts +384 -0
- package/src/be/db/tasks/read.ts +40 -4
- package/src/be/db/workflow-runs.ts +96 -0
- package/src/be/db.ts +80 -236
- package/src/be/onboarding.ts +3 -0
- package/src/be/skill-sync.ts +43 -2
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/bitbucket/api.ts +81 -0
- package/src/bitbucket/auth.ts +64 -0
- package/src/bitbucket/handlers.ts +434 -0
- package/src/bitbucket/index.ts +28 -0
- package/src/bitbucket/templates.ts +134 -0
- package/src/bitbucket/types.ts +65 -0
- package/src/commands/runner.ts +55 -3
- package/src/e2b/env.ts +1 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +2 -2
- package/src/extensions/post-bridge.ts +1 -0
- package/src/github/handlers.ts +6 -6
- package/src/gitlab/handlers.ts +5 -5
- package/src/http/assets.ts +14 -2
- package/src/http/core.ts +12 -0
- package/src/http/index.ts +4 -0
- package/src/http/onboarding.ts +1 -0
- package/src/http/sessions.ts +366 -12
- package/src/http/skills.ts +48 -1
- package/src/http/tasks.ts +1 -1
- package/src/http/users.ts +1 -0
- package/src/http/webhooks.ts +141 -0
- package/src/integrations/chat-sdk/adapters.ts +19 -3
- package/src/integrations/chat-sdk/facts.ts +30 -12
- package/src/integrations/chat-sdk/outbound.ts +38 -1
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +5 -53
- package/src/scripts-runtime/types/swarm-extension.d.ts +6 -4
- package/src/slack/handlers.test.ts +158 -0
- package/src/slack/handlers.ts +19 -28
- package/src/tasks/context-key.ts +61 -1
- package/src/tests/bitbucket-auth.test.ts +156 -0
- package/src/tests/bitbucket-handlers.test.ts +425 -0
- package/src/tests/bitbucket-webhook.test.ts +220 -0
- package/src/tests/chat-sdk-inbound.test.ts +39 -0
- package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
- package/src/tests/context-key.test.ts +50 -0
- package/src/tests/core-auth.test.ts +5 -0
- package/src/tests/db-task-read-contract.test.ts +7 -0
- package/src/tests/db-task-write-contract.test.ts +6 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
- package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
- package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
- package/src/tests/generate-mcp-docs.test.ts +101 -0
- package/src/tests/gitlab-handlers.test.ts +3 -3
- package/src/tests/gitlab-vcs-db.test.ts +4 -4
- package/src/tests/integration-disable-flags.test.ts +36 -0
- package/src/tests/mcp-tools.test.ts +1 -1
- package/src/tests/onboarding.test.ts +12 -0
- package/src/tests/pi-codemode.test.ts +8 -2
- package/src/tests/preload-env.test.ts +57 -0
- package/src/tests/preload.ts +21 -0
- package/src/tests/prompt-template-remaining.test.ts +38 -0
- package/src/tests/run-root-coverage.test.ts +39 -0
- package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
- package/src/tests/runner-skills-refresh.test.ts +58 -0
- package/src/tests/secret-scrubber.test.ts +15 -0
- package/src/tests/sessions-move.test.ts +576 -0
- package/src/tests/sessions-tree.test.ts +493 -0
- package/src/tests/skill-files-http.test.ts +50 -0
- package/src/tests/skills-bundle.test.ts +99 -0
- package/src/tests/swarm-config-export-to-env.test.ts +2 -1
- package/src/tests/vcs-bitbucket.test.ts +91 -0
- package/src/tests/vcs-provider-isolation.test.ts +123 -0
- package/src/tests/vcs-provider.test.ts +16 -1
- package/src/tests/vcs-tracking.test.ts +3 -3
- package/src/tests/workflow-foreach-bodies.test.ts +637 -0
- package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
- package/src/tests/workflow-foreach-retry.test.ts +537 -0
- package/src/tests/workflow-foreach.test.ts +44 -23
- package/src/tests/workflow-step-claims.test.ts +6 -6
- package/src/tests/workflow-tla-races.test.ts +2 -2
- package/src/tools/script-delete.ts +1 -1
- package/src/tools/workflows/create-workflow.ts +5 -3
- package/src/types.ts +3 -2
- package/src/utils/config-env-export.ts +1 -0
- package/src/utils/secret-scrubber.ts +4 -0
- package/src/utils/skills-refresh.ts +83 -43
- package/src/vcs/bitbucket.ts +58 -0
- package/src/vcs/index.ts +2 -0
- package/src/vcs/types.ts +2 -2
- package/src/workflows/approval-resolution.ts +4 -1
- package/src/workflows/checkpoint.ts +9 -1
- package/src/workflows/constants.ts +3 -0
- package/src/workflows/definition.ts +48 -17
- package/src/workflows/engine.ts +25 -15
- package/src/workflows/executors/foreach.ts +45 -99
- package/src/workflows/foreach-body.ts +146 -0
- package/src/workflows/foreach-driver.ts +552 -0
- package/src/workflows/foreach-join.ts +206 -7
- package/src/workflows/node-timeout.ts +21 -0
- package/src/workflows/recovery.ts +68 -3
- package/src/workflows/resume.ts +221 -43
- package/src/workflows/retry-poller.ts +39 -24
- package/src/workflows/step-input.ts +59 -0
- package/src/workflows/task-step-routing.ts +114 -56
- package/templates/skills/slack-interaction/content.md +5 -0
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/dist/cli-9hstj09h.js +0 -2657
- package/dist/cli-yw91rnmw.js +0 -13
- package/dist/handlers-7gw64dxw.js +0 -39
- package/dist/index-bgdyvcnv.js +0 -40
- package/dist/index-gabbcdn2.js +0 -43
- package/dist/index-tfb26zhy.js +0 -41
- package/dist/lead-7e02z839.js +0 -77
- package/dist/worker-5snytpbd.js +0 -77
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { renderAutomationTokens } from "../be/automation-preflight";
|
|
2
|
+
import {
|
|
3
|
+
detachTaskFromWorkflowRunStep,
|
|
4
|
+
getDbClient,
|
|
5
|
+
getLatestStepForNode,
|
|
6
|
+
getTaskByWorkflowRunStepId,
|
|
7
|
+
getWorkflow,
|
|
8
|
+
getWorkflowRun,
|
|
9
|
+
getWorkflowRunStep,
|
|
10
|
+
getWorkflowRunStepsByRunId,
|
|
11
|
+
updateWorkflowRun,
|
|
12
|
+
updateWorkflowRunStep,
|
|
13
|
+
} from "../be/db";
|
|
14
|
+
import type {
|
|
15
|
+
Workflow,
|
|
16
|
+
WorkflowDefinition,
|
|
17
|
+
WorkflowNode,
|
|
18
|
+
WorkflowRun,
|
|
19
|
+
WorkflowRunStep,
|
|
20
|
+
} from "../types";
|
|
21
|
+
import { checkpointStepWaiting } from "./checkpoint";
|
|
22
|
+
import { FOREACH_ITEMS_WAIT_FOR } from "./constants";
|
|
23
|
+
import {
|
|
24
|
+
hasRunningStep,
|
|
25
|
+
holdWorkflowRun,
|
|
26
|
+
interpolateNodeConfig,
|
|
27
|
+
scriptBodyInterpolationError,
|
|
28
|
+
walkGraph,
|
|
29
|
+
} from "./engine";
|
|
30
|
+
import type { AsyncExecutorResult } from "./executors/base";
|
|
31
|
+
import { ForeachConfigSchema } from "./executors/foreach";
|
|
32
|
+
import type { ExecutorRegistry } from "./executors/registry";
|
|
33
|
+
import { systemOneUnresolvedError } from "./executors/system-one-decision";
|
|
34
|
+
import {
|
|
35
|
+
effectiveFailurePolicy,
|
|
36
|
+
foreachConcurrencyCap,
|
|
37
|
+
isForeachBodyType,
|
|
38
|
+
resolveStepNode,
|
|
39
|
+
} from "./foreach-body";
|
|
40
|
+
import {
|
|
41
|
+
cancelQueuedForeachChildren,
|
|
42
|
+
FOREACH_TERMINAL_STEP_STATUSES,
|
|
43
|
+
type ForeachChildOutcome,
|
|
44
|
+
latestForeachChildren,
|
|
45
|
+
settleForeachChild,
|
|
46
|
+
} from "./foreach-join";
|
|
47
|
+
import { getSecretInputKeys } from "./input";
|
|
48
|
+
import { resolveNodeTimeoutMs } from "./node-timeout";
|
|
49
|
+
import { rebuildNodeExecutionInput } from "./step-input";
|
|
50
|
+
|
|
51
|
+
export { FOREACH_ITEMS_WAIT_FOR };
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Child step ids a drive in this process is starting right now. A `running`
|
|
55
|
+
* child outside this set has no owner: a crash (or a lost drive) left it
|
|
56
|
+
* between its claim and its settlement, and the next drive restarts it.
|
|
57
|
+
*/
|
|
58
|
+
const ownedChildren = new Set<string>();
|
|
59
|
+
|
|
60
|
+
/** Fire-and-forget drives, tracked so tests and shutdown can drain them. */
|
|
61
|
+
const activeDrives = new Set<Promise<void>>();
|
|
62
|
+
|
|
63
|
+
export interface ForeachDriveResult {
|
|
64
|
+
/** True when a settlement in this drive closed the join. */
|
|
65
|
+
joined: boolean;
|
|
66
|
+
/** The parent's successors to walk, when `joined`. */
|
|
67
|
+
successors: WorkflowNode[];
|
|
68
|
+
/** How many children this drive claimed (queued or orphaned). */
|
|
69
|
+
claimed: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const NOT_JOINED: ForeachDriveResult = { joined: false, successors: [], claimed: 0 };
|
|
73
|
+
|
|
74
|
+
export function isForeachChildOwned(stepId: string): boolean {
|
|
75
|
+
return ownedChildren.has(stepId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Drop in-process child ownership, as a restart would. Tests only. */
|
|
79
|
+
export function _resetForeachOwnershipForTests(): void {
|
|
80
|
+
ownedChildren.clear();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Park a foreach parent whose executor materialized its children, then drive
|
|
85
|
+
* them. The run stays `running` while the drive starts the children; when the
|
|
86
|
+
* join stays open it parks as `waiting` like any async step. Shared by
|
|
87
|
+
* executeStep and the retry poller.
|
|
88
|
+
*/
|
|
89
|
+
export async function parkAndDriveForeach(input: {
|
|
90
|
+
runId: string;
|
|
91
|
+
stepId: string;
|
|
92
|
+
parentNodeId: string;
|
|
93
|
+
ctx: Record<string, unknown>;
|
|
94
|
+
registry: ExecutorRegistry;
|
|
95
|
+
}): Promise<ForeachDriveResult & { parked: boolean }> {
|
|
96
|
+
// A re-walk of an open fan-out (a run retry, recovery) reuses a parent step
|
|
97
|
+
// that is already parked: drive it as it is.
|
|
98
|
+
const parked =
|
|
99
|
+
(await checkpointStepWaiting(input.runId, input.stepId, input.ctx, {
|
|
100
|
+
keepRunStatus: true,
|
|
101
|
+
})) || (await getWorkflowRunStep(input.stepId))?.status === "waiting";
|
|
102
|
+
if (!parked) return { ...NOT_JOINED, parked: false };
|
|
103
|
+
const drive = await driveForeach(input.runId, input.parentNodeId, input.registry, {
|
|
104
|
+
ctx: input.ctx,
|
|
105
|
+
});
|
|
106
|
+
if (!drive.joined) await parkRunIfRunning(input.runId);
|
|
107
|
+
return { ...drive, parked: true };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Claim and start the children of foreach `parentNodeId` that are free to
|
|
112
|
+
* run: queued (`pending`) children up to the cap, plus orphaned `running`
|
|
113
|
+
* children no drive owns. Instant bodies run and settle here, each against its
|
|
114
|
+
* own watchdog budget. Async bodies park, and their own resume path settles
|
|
115
|
+
* them through `settleForeachChild`.
|
|
116
|
+
*
|
|
117
|
+
* Idempotent: the claim flips each child out of `pending` in a `BEGIN
|
|
118
|
+
* IMMEDIATE` transaction, and every async executor dedupes on the child step
|
|
119
|
+
* id, so a repeated drive never duplicates a task, child run or approval.
|
|
120
|
+
*/
|
|
121
|
+
export async function driveForeach(
|
|
122
|
+
runId: string,
|
|
123
|
+
parentNodeId: string,
|
|
124
|
+
registry: ExecutorRegistry,
|
|
125
|
+
options: { ctx?: Record<string, unknown> } = {},
|
|
126
|
+
): Promise<ForeachDriveResult> {
|
|
127
|
+
const run = await getWorkflowRun(runId);
|
|
128
|
+
if (!run || (run.status !== "running" && run.status !== "waiting")) return NOT_JOINED;
|
|
129
|
+
const workflow = await getRenderedWorkflow(run.workflowId);
|
|
130
|
+
if (!workflow) return NOT_JOINED;
|
|
131
|
+
const def = workflow.definition;
|
|
132
|
+
const parentNode = def.nodes.find((node) => node.id === parentNodeId);
|
|
133
|
+
if (!parentNode || parentNode.type !== "foreach") return NOT_JOINED;
|
|
134
|
+
const parentStep = await getLatestStepForNode(runId, parentNodeId);
|
|
135
|
+
if (!parentStep || FOREACH_TERMINAL_STEP_STATUSES.has(parentStep.status)) return NOT_JOINED;
|
|
136
|
+
|
|
137
|
+
const body = parentNode.config.body as { type?: unknown } | undefined;
|
|
138
|
+
const bodyType = typeof body?.type === "string" ? body.type : "";
|
|
139
|
+
// Rebuilt once, on the first claim: most drives (a settlement with nothing
|
|
140
|
+
// queued behind it) claim nothing and never pay for the context rebuild.
|
|
141
|
+
let parentInput: Awaited<ReturnType<typeof loadParentInput>> | undefined;
|
|
142
|
+
let claimedTotal = 0;
|
|
143
|
+
const notJoined = (): ForeachDriveResult => ({ ...NOT_JOINED, claimed: claimedTotal });
|
|
144
|
+
|
|
145
|
+
for (;;) {
|
|
146
|
+
const current = await getWorkflowRun(runId);
|
|
147
|
+
if (!current || (current.status !== "running" && current.status !== "waiting")) {
|
|
148
|
+
return notJoined();
|
|
149
|
+
}
|
|
150
|
+
const claimed = await claimChildren(def, runId, parentNode);
|
|
151
|
+
if (claimed.length === 0) return notJoined();
|
|
152
|
+
claimedTotal += claimed.length;
|
|
153
|
+
const started = new Set<string>();
|
|
154
|
+
let failedFast: string | undefined;
|
|
155
|
+
try {
|
|
156
|
+
parentInput ??= await loadParentInput(def, run, parentNode, registry, workflow.id, options);
|
|
157
|
+
const { ctx, items, itemKeyField, meta } = parentInput;
|
|
158
|
+
for (const child of claimed) {
|
|
159
|
+
// An earlier body in this batch can run for a long time. A cancel (of
|
|
160
|
+
// the run or this child) that landed meanwhile must stop the rest of
|
|
161
|
+
// the batch before it reaches an executor. A fail-fast failure of a
|
|
162
|
+
// sibling elsewhere ends the run too; `finally` cancels the rest.
|
|
163
|
+
const liveness = await childLiveness(runId, child.id);
|
|
164
|
+
if (liveness === "run-ended") return notJoined();
|
|
165
|
+
if (liveness === "child-ended") continue;
|
|
166
|
+
started.add(child.id);
|
|
167
|
+
const outcome = await startChild({
|
|
168
|
+
def,
|
|
169
|
+
child,
|
|
170
|
+
registry,
|
|
171
|
+
ctx,
|
|
172
|
+
meta,
|
|
173
|
+
bodyType,
|
|
174
|
+
item: items.find((candidate) => candidate[itemKeyField] === childItemKey(child)),
|
|
175
|
+
});
|
|
176
|
+
if (!outcome) continue;
|
|
177
|
+
const settled = await settleForeachChild(def, runId, child.id, outcome, ctx, {
|
|
178
|
+
from: ["running"],
|
|
179
|
+
});
|
|
180
|
+
ownedChildren.delete(child.id);
|
|
181
|
+
if (settled.joined) {
|
|
182
|
+
return { joined: true, successors: settled.successors, claimed: claimedTotal };
|
|
183
|
+
}
|
|
184
|
+
if (settled.runFailed) {
|
|
185
|
+
failedFast = child.nodeId;
|
|
186
|
+
return notJoined();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} finally {
|
|
190
|
+
// A child claimed but never started (an earlier sibling failed the run,
|
|
191
|
+
// or the input rebuild threw) goes back to the queue, so a run retry
|
|
192
|
+
// starts it. After a fail-fast failure it is cancelled with the rest of
|
|
193
|
+
// the queue instead: it never started.
|
|
194
|
+
for (const child of claimed) {
|
|
195
|
+
ownedChildren.delete(child.id);
|
|
196
|
+
if (!started.has(child.id)) await requeueChild(child.id);
|
|
197
|
+
}
|
|
198
|
+
if (
|
|
199
|
+
effectiveFailurePolicy(parentNode, def) === "fail-fast" &&
|
|
200
|
+
(failedFast || (await getWorkflowRun(runId))?.status === "failed")
|
|
201
|
+
) {
|
|
202
|
+
await cancelQueuedForeachChildren(def, runId, parentNodeId, failedFast);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The parent's execution input, rebuilt from persisted state the way the walk
|
|
210
|
+
* built it. Items are looked up by key in the re-interpolated `over`, never
|
|
211
|
+
* persisted on the child row, so resolved secrets stay out of step rows.
|
|
212
|
+
*/
|
|
213
|
+
async function loadParentInput(
|
|
214
|
+
def: WorkflowDefinition,
|
|
215
|
+
run: WorkflowRun,
|
|
216
|
+
parentNode: WorkflowNode,
|
|
217
|
+
registry: ExecutorRegistry,
|
|
218
|
+
workflowId: string,
|
|
219
|
+
options: { ctx?: Record<string, unknown> },
|
|
220
|
+
) {
|
|
221
|
+
const input = await rebuildNodeExecutionInput(def, run, parentNode, registry, {
|
|
222
|
+
ctx: options.ctx,
|
|
223
|
+
});
|
|
224
|
+
const parsed = ForeachConfigSchema.safeParse(input.config);
|
|
225
|
+
return {
|
|
226
|
+
ctx: input.ctx,
|
|
227
|
+
items: parsed.success ? parsed.data.over : [],
|
|
228
|
+
itemKeyField: parsed.success ? parsed.data.itemKey : "",
|
|
229
|
+
meta: {
|
|
230
|
+
runId: run.id,
|
|
231
|
+
workflowId,
|
|
232
|
+
dryRun: false,
|
|
233
|
+
requestedByUserId: run.createdBy,
|
|
234
|
+
inputCtx: input.inputCtx,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Start a drive without awaiting it. When it closes the join, it walks the
|
|
241
|
+
* parent's successors (or finalizes); otherwise it parks the run if nothing is
|
|
242
|
+
* left running. Errors are logged.
|
|
243
|
+
*/
|
|
244
|
+
export function startForeachDrive(
|
|
245
|
+
runId: string,
|
|
246
|
+
parentNodeId: string,
|
|
247
|
+
registry: ExecutorRegistry,
|
|
248
|
+
): void {
|
|
249
|
+
const drive = runForeachDrive(runId, parentNodeId, registry)
|
|
250
|
+
.then(() => undefined)
|
|
251
|
+
.catch((err) => {
|
|
252
|
+
console.error(`[workflows] foreach drive of ${parentNodeId} in run ${runId} failed:`, err);
|
|
253
|
+
})
|
|
254
|
+
.finally(() => {
|
|
255
|
+
activeDrives.delete(drive);
|
|
256
|
+
});
|
|
257
|
+
activeDrives.add(drive);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Drive a fan-out outside a live walk and route what it closes. Holds the run
|
|
262
|
+
* for the duration so the running-run recovery leaves it alone. Returns true
|
|
263
|
+
* when the drive started or settled at least one child.
|
|
264
|
+
*/
|
|
265
|
+
export async function runForeachDrive(
|
|
266
|
+
runId: string,
|
|
267
|
+
parentNodeId: string,
|
|
268
|
+
registry: ExecutorRegistry,
|
|
269
|
+
): Promise<ForeachDriveResult> {
|
|
270
|
+
const release = holdWorkflowRun(runId);
|
|
271
|
+
try {
|
|
272
|
+
const drive = await driveForeach(runId, parentNodeId, registry);
|
|
273
|
+
if (!drive.joined) {
|
|
274
|
+
await parkRunIfRunning(runId);
|
|
275
|
+
return drive;
|
|
276
|
+
}
|
|
277
|
+
const run = await getWorkflowRun(runId);
|
|
278
|
+
const workflow = run ? await getRenderedWorkflow(run.workflowId) : null;
|
|
279
|
+
if (!run || !workflow) return drive;
|
|
280
|
+
const ctx = (run.context ?? {}) as Record<string, unknown>;
|
|
281
|
+
if (drive.successors.length > 0) {
|
|
282
|
+
await walkGraph(
|
|
283
|
+
workflow.definition,
|
|
284
|
+
runId,
|
|
285
|
+
ctx,
|
|
286
|
+
drive.successors,
|
|
287
|
+
registry,
|
|
288
|
+
workflow.id,
|
|
289
|
+
getSecretInputKeys(workflow.input),
|
|
290
|
+
);
|
|
291
|
+
} else {
|
|
292
|
+
const { finalizeOrWait } = await import("./resume");
|
|
293
|
+
await finalizeOrWait(runId);
|
|
294
|
+
}
|
|
295
|
+
return drive;
|
|
296
|
+
} finally {
|
|
297
|
+
release();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Wait for every fire-and-forget drive, including drives they start. */
|
|
302
|
+
export async function drainForeachDrives(): Promise<void> {
|
|
303
|
+
while (activeDrives.size > 0) {
|
|
304
|
+
await Promise.allSettled([...activeDrives]);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* The run's workflow with its install params rendered in, as
|
|
310
|
+
* startWorkflowExecution rendered it before the walk. The driver and its
|
|
311
|
+
* post-join walk reload the definition, so without this a `{{PARAM}}` token in
|
|
312
|
+
* the body config or in `over` would reach the per-item pass unresolved.
|
|
313
|
+
*/
|
|
314
|
+
async function getRenderedWorkflow(workflowId: string): Promise<Workflow | null> {
|
|
315
|
+
const workflow = await getWorkflow(workflowId);
|
|
316
|
+
return workflow ? renderAutomationTokens(workflow, workflow.params ?? {}) : null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Whether a claimed child may still start: its run is live and its row is still claimed. */
|
|
320
|
+
async function childLiveness(
|
|
321
|
+
runId: string,
|
|
322
|
+
stepId: string,
|
|
323
|
+
): Promise<"live" | "run-ended" | "child-ended"> {
|
|
324
|
+
const run = await getWorkflowRun(runId);
|
|
325
|
+
if (!run || (run.status !== "running" && run.status !== "waiting")) return "run-ended";
|
|
326
|
+
const step = await getWorkflowRunStep(stepId);
|
|
327
|
+
return step?.status === "running" ? "live" : "child-ended";
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function requeueChild(stepId: string): Promise<void> {
|
|
331
|
+
await getDbClient().run(
|
|
332
|
+
"UPDATE workflow_run_steps SET status = 'pending' WHERE id = ? AND status = 'running'",
|
|
333
|
+
[stepId],
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Park a `running` run as `waiting` once nothing in it is executing and a step
|
|
339
|
+
* is waiting. Never completes the run: a drive that did not close the join
|
|
340
|
+
* must not finalize a run whose joiner is still walking the successors.
|
|
341
|
+
*/
|
|
342
|
+
async function parkRunIfRunning(runId: string): Promise<void> {
|
|
343
|
+
await getDbClient().transaction(async () => {
|
|
344
|
+
const run = await getWorkflowRun(runId);
|
|
345
|
+
if (run?.status !== "running") return;
|
|
346
|
+
const steps = await getWorkflowRunStepsByRunId(runId);
|
|
347
|
+
if (hasRunningStep(steps)) return;
|
|
348
|
+
if (!steps.some((step) => step.status === "waiting")) return;
|
|
349
|
+
await updateWorkflowRun(runId, { status: "waiting" });
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Flip `pending` children to `running`, in item order, while fewer than the
|
|
355
|
+
* fan-out's `concurrency` cap are in flight (`running` or `waiting`, latest
|
|
356
|
+
* attempt rows only), and collect unowned `running` ones. Ownership is registered before commit, so no other
|
|
357
|
+
* drive can see a claimed row without also seeing it owned.
|
|
358
|
+
*/
|
|
359
|
+
async function claimChildren(
|
|
360
|
+
def: WorkflowDefinition,
|
|
361
|
+
runId: string,
|
|
362
|
+
parentNode: WorkflowNode,
|
|
363
|
+
): Promise<WorkflowRunStep[]> {
|
|
364
|
+
const claimed: WorkflowRunStep[] = [];
|
|
365
|
+
try {
|
|
366
|
+
// The in-flight count and the claim share one BEGIN IMMEDIATE transaction,
|
|
367
|
+
// so concurrent drives (a settlement's drive and the recovery sweep)
|
|
368
|
+
// serialize here and never exceed the cap together.
|
|
369
|
+
await getDbClient().transaction(async () => {
|
|
370
|
+
const children = latestForeachChildren(
|
|
371
|
+
def,
|
|
372
|
+
parentNode.id,
|
|
373
|
+
await getWorkflowRunStepsByRunId(runId),
|
|
374
|
+
);
|
|
375
|
+
for (const child of children) {
|
|
376
|
+
if (child.status === "running" && !ownedChildren.has(child.id)) {
|
|
377
|
+
ownedChildren.add(child.id);
|
|
378
|
+
claimed.push(child);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const inFlight = children.filter(
|
|
382
|
+
(child) => child.status === "running" || child.status === "waiting",
|
|
383
|
+
).length;
|
|
384
|
+
let free = foreachConcurrencyCap(parentNode) - inFlight;
|
|
385
|
+
for (const child of children) {
|
|
386
|
+
if (free <= 0) break;
|
|
387
|
+
if (child.status !== "pending") continue;
|
|
388
|
+
const row = await getDbClient().get<{ id: string }>(
|
|
389
|
+
"UPDATE workflow_run_steps SET status = 'running' WHERE id = ? AND status = 'pending' RETURNING id",
|
|
390
|
+
[child.id],
|
|
391
|
+
);
|
|
392
|
+
if (!row) continue;
|
|
393
|
+
ownedChildren.add(child.id);
|
|
394
|
+
claimed.push({ ...child, status: "running" });
|
|
395
|
+
free--;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
} catch (err) {
|
|
399
|
+
for (const child of claimed) ownedChildren.delete(child.id);
|
|
400
|
+
throw err;
|
|
401
|
+
}
|
|
402
|
+
return claimed;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Run one claimed child's body. Returns its terminal outcome, or null when it
|
|
407
|
+
* parked (or another handler moved it on).
|
|
408
|
+
*/
|
|
409
|
+
async function startChild(input: {
|
|
410
|
+
def: WorkflowDefinition;
|
|
411
|
+
child: WorkflowRunStep;
|
|
412
|
+
registry: ExecutorRegistry;
|
|
413
|
+
ctx: Record<string, unknown>;
|
|
414
|
+
meta: {
|
|
415
|
+
runId: string;
|
|
416
|
+
workflowId: string;
|
|
417
|
+
dryRun: boolean;
|
|
418
|
+
requestedByUserId?: string;
|
|
419
|
+
inputCtx: Record<string, unknown>;
|
|
420
|
+
};
|
|
421
|
+
bodyType: string;
|
|
422
|
+
item: Record<string, unknown> | undefined;
|
|
423
|
+
}): Promise<ForeachChildOutcome | null> {
|
|
424
|
+
const { def, child, registry, ctx, meta, bodyType, item } = input;
|
|
425
|
+
if (!item) {
|
|
426
|
+
return {
|
|
427
|
+
status: "failed",
|
|
428
|
+
error: `item "${childItemKey(child)}" is no longer present in over`,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
if (!isForeachBodyType(bodyType, registry)) {
|
|
432
|
+
return { status: "failed", error: `foreach body type "${bodyType}" cannot run` };
|
|
433
|
+
}
|
|
434
|
+
const bodyNode = resolveStepNode(def, child.nodeId);
|
|
435
|
+
if (!bodyNode) return { status: "failed", error: `foreach child ${child.nodeId} has no body` };
|
|
436
|
+
|
|
437
|
+
// The per-item pass interpolates against the parent's declared-inputs
|
|
438
|
+
// context plus item/index, NOT the full run context, so the body obeys the
|
|
439
|
+
// same explicit-dataflow boundary as a normal node. A script body keeps the
|
|
440
|
+
// script-source allowlist: `item` reaches it through `args`, never source.
|
|
441
|
+
const index = childIndex(child);
|
|
442
|
+
const bodyCtx = { ...meta.inputCtx, item, index };
|
|
443
|
+
const interpolation = interpolateNodeConfig(bodyNode, bodyCtx);
|
|
444
|
+
if (interpolation.scriptBodyUnresolved && interpolation.scriptBodyUnresolved.length > 0) {
|
|
445
|
+
return {
|
|
446
|
+
status: "failed",
|
|
447
|
+
error: scriptBodyInterpolationError(child.nodeId, interpolation.scriptBodyUnresolved),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
if (interpolation.strictUnresolved && interpolation.strictUnresolved.length > 0) {
|
|
451
|
+
return {
|
|
452
|
+
status: "failed",
|
|
453
|
+
error: systemOneUnresolvedError(child.nodeId, interpolation.strictUnresolved, bodyType),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
if (interpolation.unresolved.length > 0) {
|
|
457
|
+
console.warn(
|
|
458
|
+
`[workflow] Step ${child.nodeId}: unresolved interpolation tokens: ${interpolation.unresolved.join(", ")}`,
|
|
459
|
+
);
|
|
460
|
+
await updateWorkflowRunStep(child.id, {
|
|
461
|
+
diagnostics: JSON.stringify({ unresolvedTokens: interpolation.unresolved }),
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
const config = interpolation.value as Record<string, unknown>;
|
|
465
|
+
|
|
466
|
+
if (bodyType === "agent-task") {
|
|
467
|
+
// A retried item needs a fresh task. Detaching the stale terminal task keeps
|
|
468
|
+
// the executor's one-task-per-step de-duplication deterministic and scoped.
|
|
469
|
+
const linkedTask = await getTaskByWorkflowRunStepId(child.id);
|
|
470
|
+
if (linkedTask?.status === "failed" || linkedTask?.status === "cancelled") {
|
|
471
|
+
await detachTaskFromWorkflowRunStep(linkedTask.id);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const executor = registry.get(bodyType);
|
|
476
|
+
const timeoutMs = resolveNodeTimeoutMs({ type: bodyType, config });
|
|
477
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
478
|
+
let result: Awaited<ReturnType<typeof executor.run>>;
|
|
479
|
+
try {
|
|
480
|
+
result = await Promise.race([
|
|
481
|
+
executor.run({
|
|
482
|
+
config,
|
|
483
|
+
context: { ...ctx, ...bodyCtx },
|
|
484
|
+
meta: { ...meta, stepId: child.id, nodeId: child.nodeId },
|
|
485
|
+
}),
|
|
486
|
+
new Promise<never>((_, reject) => {
|
|
487
|
+
timer = setTimeout(
|
|
488
|
+
() => reject(new Error(`Step timed out after ${timeoutMs}ms`)),
|
|
489
|
+
timeoutMs,
|
|
490
|
+
);
|
|
491
|
+
}),
|
|
492
|
+
]);
|
|
493
|
+
} catch (err) {
|
|
494
|
+
return { status: "failed", error: err instanceof Error ? err.message : String(err) };
|
|
495
|
+
} finally {
|
|
496
|
+
if (timer) clearTimeout(timer);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (result.status === "failed") {
|
|
500
|
+
return {
|
|
501
|
+
status: "failed",
|
|
502
|
+
error: result.error || "Executor returned failed status",
|
|
503
|
+
...(result.output !== undefined ? { output: result.output } : {}),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if ("async" in result && (result as AsyncExecutorResult).async) {
|
|
508
|
+
const parked = await getDbClient().get<{ id: string }>(
|
|
509
|
+
"UPDATE workflow_run_steps SET status = 'waiting' WHERE id = ? AND status = 'running' RETURNING id",
|
|
510
|
+
[child.id],
|
|
511
|
+
);
|
|
512
|
+
// A child run that finished before the item parked had its event dropped.
|
|
513
|
+
if (parked && (result as AsyncExecutorResult).waitFor === "workflow.child.finished") {
|
|
514
|
+
ownedChildren.delete(child.id);
|
|
515
|
+
const { resumeFromChildRun } = await import("./resume");
|
|
516
|
+
await resumeFromChildRun(child.id, registry);
|
|
517
|
+
}
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
status: "completed",
|
|
523
|
+
output: bodyType === "agent-task" ? normalizeExistingTaskOutput(result.output) : result.output,
|
|
524
|
+
...(result.nextPort ? { nextPort: result.nextPort } : {}),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function childItemKey(child: WorkflowRunStep): string | undefined {
|
|
529
|
+
const input = child.input as Record<string, unknown> | null | undefined;
|
|
530
|
+
return typeof input?.itemKey === "string" ? input.itemKey : undefined;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function childIndex(child: WorkflowRunStep): number {
|
|
534
|
+
const input = child.input as Record<string, unknown> | null | undefined;
|
|
535
|
+
return typeof input?.index === "number" ? input.index : 0;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** An agent-task body that reconnected to an already-completed task. */
|
|
539
|
+
function normalizeExistingTaskOutput(output: unknown): unknown {
|
|
540
|
+
if (typeof output !== "object" || output === null) return output;
|
|
541
|
+
const record = output as Record<string, unknown>;
|
|
542
|
+
if (typeof record.taskOutput !== "string") return output;
|
|
543
|
+
try {
|
|
544
|
+
const parsed = JSON.parse(record.taskOutput);
|
|
545
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
546
|
+
return { ...record, taskOutput: parsed };
|
|
547
|
+
}
|
|
548
|
+
} catch {
|
|
549
|
+
// Non-structured task output remains a string.
|
|
550
|
+
}
|
|
551
|
+
return output;
|
|
552
|
+
}
|