chainlesschain 0.162.70 → 0.162.72
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/package.json +4 -3
- package/src/assets/web-panel/assets/{AIOps-CnHIXe2L.js → AIOps-1bl50Cen.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CzDyhTAp.js → ActionButton-Bw4kAx-1.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-rwvDMCd-.js → Analytics-junq7r42.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-D-Q1M-5V.js → AppLayout-vf1TVGeu.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-BIFSGnAE.js → Audit-CBzpgcL9.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CbUdiVeS.js → Backup-ChwGPeP4.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-D3P_pICH.js → BaseInput-2G2KGE6M.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DRrX7vVB.js → Chat-Cr9NwfDA.js} +4 -4
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-DTC1ElV-.js → ChatBubbleRenderer-CJvlo_ay.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-Dpq9X8Xd.js → Checkbox-DN_5PBT7.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-yQQOSuHA.js → Codegen-D4bluAgT.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DLFLDxar.js → Col-DBP3UjYW.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DYbdX-GB.js → Community-ge-SvR3c.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Dclrp1HE.js → Compact-DGXvlGNv.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-C9EQMgoP.js → Compliance-Cn7nBsFQ.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-B9IMFsMk.js → Cowork-DgHEO2de.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-V6TlbDey.js → Cron-DUbYxPs0.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-Cl_vG8R_.js → Crosschain-3J0MWPQq.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DoUUF9cz.js → DID-BR_Jpkh4.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DcA8WmrS.js → Dashboard-ffykLkAJ.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-wyGMYzw9.js → Dropdown-DaI38dXT.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-B1T3ziIH.js → EmailListRenderer-CnIzkUVG.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CW4CwYbZ.js → FamilyGuardDashboard-BxrgAAZg.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-9tEiw0KA.js → Federation-BuS_FRh3.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DbocfnNQ.js → FormItemContext-DBG-8g1w.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CGNsG2qI.js → GenericCardRenderer-CmEc8RgH.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BxA3dgT8.js → Git-CK10BYwY.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DqnhaC7n.js → Governance-C8cABM_I.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-D6Ll4t9c.js → Inference-Dr4_DIw3.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Bb8IXGl7.js → KnowledgeGraph-0_bLuvvb.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DRUUVMdc.js → Logs-PVGZxt9z.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-BYRM186w.js → Marketplace-BtVnS4HW.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DLbh7uS8.js → McpTools-BVulusCC.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-D2_uZEOI.js → Memory-DEMtNbJ9.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CXVJoMKk.js → MobileBridge-5QL654FQ.js} +3 -3
- package/src/assets/web-panel/assets/{MobileProjects-QRZYgIME.js → MobileProjects-G5Du9Thr.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-D3mv30gW.js → Mtc-CNFPJej0.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-__QPus22.js → MtcAudit-DAJ81Cej.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-Bu5gaTn6.js → Multisig-Cwj8tXfT.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-SjaViyfR.js → NLProgramming-Di0ptaL6.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-feXsl0dN.js → Notes-Cse_4Ox6.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BtVFokbM.js → NotificationSettings-ntpoi5ec.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-CbKsOM0D.js +1 -0
- package/src/assets/web-panel/assets/{Organization-CewL60Rd.js → Organization-Bhjya5h9.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-2ZRChJ7E.js → Overflow-C0u3aive.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DuMr6G9y.js → P2P-BWrTwf_a.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BMOcHCWd.js → PdhVaultBrowser-pld2Qlqk.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-Cj94ql75.js → Permissions-CW5FG3mE.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BYK4mlk7.js → PersonalDataHub-BuBmqfDa.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-maRvdrbO.js → Pipeline-nZr06BNO.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CKb2zZJz.js → Privacy-B_XvMHYv.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-C2pfXv4l.js → ProjectInit-CbRHJy85.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DGeNwq0h.js → ProjectSettings-CCH8SpEa.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BRypTQUV.js → Projects-D5Min2Fu.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-DYwMPXEO.js → Providers-DgCY5A8A.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DE6isYZV.js → QuickAsk-DEcKTiTV.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Rc-AE6y9.js → Recommend-BRaq2oKc.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-DDQ8_kmM.js → Reputation-DzdbBdVv.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Do5iQUFj.js → Row-Ds2JNh7c.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-cxG0ulSw.js → RssFeed-Bk1-t4Ie.js} +3 -3
- package/src/assets/web-panel/assets/{Search-D3-8EdNk.js → Search-DaVhRwoq.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DtBfws1D.js → Security-D9mXjPSu.js} +3 -3
- package/src/assets/web-panel/assets/{Services-DzeN8HsG.js → Services-D8sukL97.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BQCVeTUf.js → Skeleton-COCJt30X.js} +1 -1
- package/src/assets/web-panel/assets/Skills-DtFPSPSc.js +1 -0
- package/src/assets/web-panel/assets/{Sla-d2IOd-AA.js → Sla-DIab-wjR.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BjfopwT6.js → SpeechSettings-xxIH9hgB.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-D5_c1_rI.js → SyncSettings-DrpxzJOH.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-maiJ3jAT.js +1 -0
- package/src/assets/web-panel/assets/{Templates-CCXFgNy3.js → Templates-COE-dSks.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-Duz6eEK3.js → Tenant-CLGObu4M.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-Buc6UbYo.js → Terminal-Bw8WBvW3.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-Du5jKHJz.js → TimelineRenderer-D4gdk3Qb.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-B12vLtQm.js → Tokens-CSoL_uAw.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-s_pHOerx.js → Trigger-BCRcowFi.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-4sDhbAls.js → Trust-D5QPZ198.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-UeyFknip.js → UkeySign-C7SsMqfZ.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DsopM2S5.js → VideoEditing-CDQ4HNZL.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CreLjC5U.js → Wallet-VmG4P_Fm.js} +3 -3
- package/src/assets/web-panel/assets/{WebAuthn-CBuMMZbK.js → WebAuthn-qtC0dZxA.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-DWhHHSFW.js → WorkflowEditor-C_CSdk3l.js} +1 -1
- package/src/assets/web-panel/assets/{chat-Iurq_BMY.js → chat-BGm8YCIo.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BIOTIa89.js → colors-BScOmAwk.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-B7lPu9pO.js → compact-item-HDwaT31n.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-Bzzk7mrh.js → createContext-CheSjPm8.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CjO6o19j.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-B11xqp5O.js → hasIn-C1_NbX0O.js} +1 -1
- package/src/assets/web-panel/assets/index--jhpSxS1.js +1 -0
- package/src/assets/web-panel/assets/{index-vZCGFSJN.js → index-6C8mxO8j.js} +1 -1
- package/src/assets/web-panel/assets/{index-CLhpFnSR.js → index-B94_DZF-.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlR8YVlt.js → index-BGROuzQG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CxDOX4g6.js → index-BKQordyU.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMrzHgE6.js → index-BMxPL1oG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DsoyfOzW.js → index-Bd2zS3jK.js} +1 -1
- package/src/assets/web-panel/assets/{index-3LgeP4ij.js → index-ByoUjX8f.js} +1 -1
- package/src/assets/web-panel/assets/{index-Y4gmywZo.js → index-C4FSaMAu.js} +1 -1
- package/src/assets/web-panel/assets/{index-CCxGEK8b.js → index-C6WXnaG4.js} +1 -1
- package/src/assets/web-panel/assets/{index-C-lwsB2F.js → index-C71Z__li.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4wV32Dn.js → index-CSaztnyZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiXNVmcx.js → index-CYkezxqK.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6q-fkXs.js → index-Clqj2JTi.js} +1 -1
- package/src/assets/web-panel/assets/{index-R-YSpvg4.js → index-Cm55PcYG.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dn_Ht3v2.js → index-CxoOEb-l.js} +1 -1
- package/src/assets/web-panel/assets/{index-DEKeoJ6I.js → index-D2gy4IX0.js} +1 -1
- package/src/assets/web-panel/assets/{index-AmHa_sOB.js → index-D8xpncJ0.js} +1 -1
- package/src/assets/web-panel/assets/{index-CULCwI_H.js → index-DB6gbgU2.js} +1 -1
- package/src/assets/web-panel/assets/{index-tub9w6us.js → index-DDk0s3nO.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzY2mdiR.js → index-DQ01dBAS.js} +1 -1
- package/src/assets/web-panel/assets/{index-5BkUKVyj.js → index-DSuWcpsv.js} +1 -1
- package/src/assets/web-panel/assets/{index-D8tm9JnQ.js → index-DVyL9jbT.js} +1 -1
- package/src/assets/web-panel/assets/{index-BLREN4NE.js → index-DaATJtzu.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEKRqrI-.js → index-DbfAaBxL.js} +1 -1
- package/src/assets/web-panel/assets/{index-BVIXYV1R.js → index-DhTELUKN.js} +1 -1
- package/src/assets/web-panel/assets/{index-BgmBZgU9.js → index-Dk2N6hxE.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4rXFIOG.js → index-DkiIxTIS.js} +1 -1
- package/src/assets/web-panel/assets/{index-CA-GPOd9.js → index-DoZwBGUX.js} +1 -1
- package/src/assets/web-panel/assets/{index-6QGNBg3m.js → index-DppSbT8L.js} +3 -3
- package/src/assets/web-panel/assets/{index-DwGVPCZb.js → index-DzE-95XH.js} +1 -1
- package/src/assets/web-panel/assets/index-J4Rzclyc.js +1 -0
- package/src/assets/web-panel/assets/{index-BIFscFlm.js → index-KG9xVANC.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyV9331f.js → index-KXrUKw1h.js} +1 -1
- package/src/assets/web-panel/assets/{index-D25-u3Q2.js → index-eSBoP6E_.js} +1 -1
- package/src/assets/web-panel/assets/{index-B93pM-xx.js → index-ggl9cj35.js} +1 -1
- package/src/assets/web-panel/assets/{index-htxy7qDG.js → index-i-xYmSsZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CS2UWrIt.js → index-lB5kN9Yc.js} +1 -1
- package/src/assets/web-panel/assets/{index-DX7HGwP_.js → index-yKmudyK7.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-Bl6klCzd.js → initDefaultProps-D7Q7zDzP.js} +1 -1
- package/src/assets/web-panel/assets/{motion-B9IxS1-S.js → motion-B4DcaqPb.js} +1 -1
- package/src/assets/web-panel/assets/{move-NJ5XDNQe.js → move-CYxmYrFY.js} +1 -1
- package/src/assets/web-panel/assets/{omit-ByO8GI_H.js → omit-ze_9dKw3.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-W6iTcLuH.js → pickAttrs-D24HrRSv.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-JMndCFEP.js → placementArrow-BWJZVYyS.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DHV7-FuY.js → responsiveObserve-CseJia_L.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DioRUeNF.js → slide-CBWNEpey.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DMp2b9Ze.js → statusUtils-f4e6p7yd.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DamP5BTS.js → styleChecker-DZmfWZ8G.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-D338iARz.js → useFlexGapSupport-4RBaBnVI.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DE_HJgk6.js → useFs-Cv6bNoEA.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-914YxO9q.js → usePersonalDataHub-DOTETJIA.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CY39Ky7P.js → vnode-DqS1K6-J.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-BEghlCth.js → zoom-C1MYzIJG.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/cowork.js +8 -0
- package/src/commands/crosschain.js +32 -4
- package/src/commands/init.js +10 -10
- package/src/commands/loop.js +9 -3
- package/src/commands/memory.js +6 -4
- package/src/commands/orchestrate.js +5 -2
- package/src/commands/video.js +5 -1
- package/src/lib/cowork-workflow.js +664 -129
- package/src/lib/hook-manager.js +3 -2
- package/src/lib/micro-compact.js +52 -0
- package/src/lib/permission-rules.cjs +39 -0
- package/src/lib/skill-loader.js +62 -43
- package/src/lib/workflow-state-reader.js +10 -1
- package/src/repl/agent-repl.js +228 -20
- package/src/repl/chat-repl.js +4 -2
- package/src/repl/permission-tier.js +60 -0
- package/src/repl/stream-decision.js +16 -0
- package/src/repl/think-command.js +36 -0
- package/src/runtime/agent-core.js +42 -6
- package/src/runtime/file-ref-expander.js +209 -18
- package/src/runtime/headless-runner.js +3 -3
- package/src/runtime/headless-stream.js +16 -3
- package/src/assets/web-panel/assets/OrderTableRenderer-CJJ2GWDf.js +0 -1
- package/src/assets/web-panel/assets/Skills-D6mCVKt7.js +0 -1
- package/src/assets/web-panel/assets/Tasks-BCmxDMZx.js +0 -1
- package/src/assets/web-panel/assets/devWarning-CuIq7zBB.js +0 -1
- package/src/assets/web-panel/assets/index-BRsns-SP.js +0 -1
- package/src/assets/web-panel/assets/index-QTuoOvJA.js +0 -1
|
@@ -36,6 +36,9 @@ import { evaluate as evalExpr, resolveReference } from "./workflow-expr.js";
|
|
|
36
36
|
/** Maximum number of items a single forEach step can expand into. */
|
|
37
37
|
export const MAX_FAN_OUT = 500;
|
|
38
38
|
|
|
39
|
+
/** Absolute ceiling on a single loop step's iterations (infinite-loop guard). */
|
|
40
|
+
export const MAX_LOOP_ITERATIONS = 100;
|
|
41
|
+
|
|
39
42
|
export const _deps = {
|
|
40
43
|
existsSync,
|
|
41
44
|
mkdirSync,
|
|
@@ -46,6 +49,10 @@ export const _deps = {
|
|
|
46
49
|
appendFileSync,
|
|
47
50
|
now: () => Date.now(),
|
|
48
51
|
runTask: null, // injected by CLI
|
|
52
|
+
// Timer seams (injectable for deterministic retry/timeout tests).
|
|
53
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
54
|
+
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
55
|
+
clearTimeout: (t) => clearTimeout(t),
|
|
49
56
|
};
|
|
50
57
|
|
|
51
58
|
// ─── Paths ───────────────────────────────────────────────────────────────────
|
|
@@ -104,6 +111,61 @@ export function validateWorkflow(wf) {
|
|
|
104
111
|
);
|
|
105
112
|
}
|
|
106
113
|
}
|
|
114
|
+
if (
|
|
115
|
+
s.retries !== undefined &&
|
|
116
|
+
(typeof s.retries !== "number" ||
|
|
117
|
+
!Number.isInteger(s.retries) ||
|
|
118
|
+
s.retries < 0)
|
|
119
|
+
) {
|
|
120
|
+
errors.push(`steps[${i}].retries must be a non-negative integer`);
|
|
121
|
+
}
|
|
122
|
+
if (
|
|
123
|
+
s.timeoutMs !== undefined &&
|
|
124
|
+
(typeof s.timeoutMs !== "number" ||
|
|
125
|
+
!Number.isFinite(s.timeoutMs) ||
|
|
126
|
+
s.timeoutMs <= 0)
|
|
127
|
+
) {
|
|
128
|
+
errors.push(`steps[${i}].timeoutMs must be a positive number`);
|
|
129
|
+
}
|
|
130
|
+
if (
|
|
131
|
+
s.retryDelayMs !== undefined &&
|
|
132
|
+
(typeof s.retryDelayMs !== "number" ||
|
|
133
|
+
!Number.isFinite(s.retryDelayMs) ||
|
|
134
|
+
s.retryDelayMs < 0)
|
|
135
|
+
) {
|
|
136
|
+
errors.push(`steps[${i}].retryDelayMs must be a non-negative number`);
|
|
137
|
+
}
|
|
138
|
+
if (
|
|
139
|
+
s.retryBackoff !== undefined &&
|
|
140
|
+
s.retryBackoff !== "fixed" &&
|
|
141
|
+
s.retryBackoff !== "exponential"
|
|
142
|
+
) {
|
|
143
|
+
errors.push(
|
|
144
|
+
`steps[${i}].retryBackoff must be "fixed" or "exponential"`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const hasWhile = s.loopWhile !== undefined;
|
|
148
|
+
const hasUntil = s.loopUntil !== undefined;
|
|
149
|
+
if (hasWhile && typeof s.loopWhile !== "string") {
|
|
150
|
+
errors.push(`steps[${i}].loopWhile must be a string expression`);
|
|
151
|
+
}
|
|
152
|
+
if (hasUntil && typeof s.loopUntil !== "string") {
|
|
153
|
+
errors.push(`steps[${i}].loopUntil must be a string expression`);
|
|
154
|
+
}
|
|
155
|
+
if (hasWhile && hasUntil) {
|
|
156
|
+
errors.push(`steps[${i}] cannot set both loopWhile and loopUntil`);
|
|
157
|
+
}
|
|
158
|
+
if ((hasWhile || hasUntil) && s.forEach !== undefined) {
|
|
159
|
+
errors.push(`steps[${i}] cannot combine a loop with forEach`);
|
|
160
|
+
}
|
|
161
|
+
if (
|
|
162
|
+
s.maxIterations !== undefined &&
|
|
163
|
+
(typeof s.maxIterations !== "number" ||
|
|
164
|
+
!Number.isInteger(s.maxIterations) ||
|
|
165
|
+
s.maxIterations <= 0)
|
|
166
|
+
) {
|
|
167
|
+
errors.push(`steps[${i}].maxIterations must be a positive integer`);
|
|
168
|
+
}
|
|
107
169
|
}
|
|
108
170
|
// Check dependsOn references exist
|
|
109
171
|
for (const s of wf.steps) {
|
|
@@ -329,17 +391,467 @@ export function removeWorkflow(cwd, id) {
|
|
|
329
391
|
return true;
|
|
330
392
|
}
|
|
331
393
|
|
|
394
|
+
// ─── Per-step retry / timeout ─────────────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Compute the delay (ms) to wait BEFORE a retry, given the just-failed attempt
|
|
398
|
+
* number (1-based). `fixed` returns `retryDelayMs` verbatim; `exponential`
|
|
399
|
+
* doubles it per prior attempt (delay = base · 2^(attempt-1)).
|
|
400
|
+
*/
|
|
401
|
+
export function retryDelayFor(step, attempt) {
|
|
402
|
+
const base = Number(step.retryDelayMs) || 0;
|
|
403
|
+
if (base <= 0) return 0;
|
|
404
|
+
if (step.retryBackoff === "exponential") {
|
|
405
|
+
return base * Math.pow(2, attempt - 1);
|
|
406
|
+
}
|
|
407
|
+
return base;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Race a promise (produced by `factory`) against a per-attempt timeout.
|
|
412
|
+
* Resolves/rejects with the promise when it settles first; rejects with a
|
|
413
|
+
* "timed out" error if the timer fires first. The timer is always cleared, and
|
|
414
|
+
* a late rejection from the losing promise is swallowed to avoid an unhandled
|
|
415
|
+
* rejection (the underlying task is best-effort abandoned — runTask has no
|
|
416
|
+
* cancellation contract).
|
|
417
|
+
*/
|
|
418
|
+
export async function withTimeout(factory, timeoutMs) {
|
|
419
|
+
if (!timeoutMs || timeoutMs <= 0) return factory();
|
|
420
|
+
const p = Promise.resolve().then(factory);
|
|
421
|
+
let timer = null;
|
|
422
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
423
|
+
timer = _deps.setTimeout(() => {
|
|
424
|
+
reject(new Error(`step timed out after ${timeoutMs}ms`));
|
|
425
|
+
}, timeoutMs);
|
|
426
|
+
});
|
|
427
|
+
try {
|
|
428
|
+
return await Promise.race([p, timeout]);
|
|
429
|
+
} finally {
|
|
430
|
+
if (timer != null) _deps.clearTimeout(timer);
|
|
431
|
+
p.catch(() => {}); // guard a late rejection from the abandoned task
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Run one step's task with optional `timeoutMs` and `retries` (with `fixed` or
|
|
437
|
+
* `exponential` `retryDelayMs` backoff). An attempt counts as a failure if the
|
|
438
|
+
* task throws, times out, or returns a non-`completed` status. Returns
|
|
439
|
+
* `{ ok, entry?, error?, attempts }`. `attempts` is the total number of tries.
|
|
440
|
+
*/
|
|
441
|
+
export async function runStepWithRetry({ step, message, cwd, llmOptions }) {
|
|
442
|
+
const maxRetries = Math.max(0, Math.floor(Number(step.retries) || 0));
|
|
443
|
+
const timeoutMs = Number(step.timeoutMs) || 0;
|
|
444
|
+
let lastEntry = null;
|
|
445
|
+
let lastErr = null;
|
|
446
|
+
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
|
|
447
|
+
try {
|
|
448
|
+
const entry = await withTimeout(
|
|
449
|
+
() =>
|
|
450
|
+
_deps.runTask({
|
|
451
|
+
templateId: step.templateId || null,
|
|
452
|
+
userMessage: message,
|
|
453
|
+
files: step.files || [],
|
|
454
|
+
cwd,
|
|
455
|
+
llmOptions,
|
|
456
|
+
}),
|
|
457
|
+
timeoutMs,
|
|
458
|
+
);
|
|
459
|
+
if (entry && entry.status === "completed") {
|
|
460
|
+
return { ok: true, entry, attempts: attempt };
|
|
461
|
+
}
|
|
462
|
+
lastEntry = entry; // ran but not completed → retry-eligible
|
|
463
|
+
lastErr = null;
|
|
464
|
+
} catch (err) {
|
|
465
|
+
lastErr = err; // threw or timed out → retry-eligible
|
|
466
|
+
lastEntry = null;
|
|
467
|
+
}
|
|
468
|
+
if (attempt <= maxRetries) {
|
|
469
|
+
const delay = retryDelayFor(step, attempt);
|
|
470
|
+
if (delay > 0) await _deps.sleep(delay);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const attempts = maxRetries + 1;
|
|
474
|
+
if (lastEntry) return { ok: false, entry: lastEntry, attempts };
|
|
475
|
+
return { ok: false, error: lastErr, attempts };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Attach an `attempts` field only when more than one try occurred (keeps the
|
|
479
|
+
* single-attempt result shape byte-identical to the pre-retry behavior). */
|
|
480
|
+
function _withAttempts(result, attempts) {
|
|
481
|
+
if (attempts > 1) return { ...(result || {}), attempts };
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Build a single-step outcome object from a `runStepWithRetry` result. */
|
|
486
|
+
function outcomeFromRetry(recordId, r) {
|
|
487
|
+
if (r.ok || r.entry) {
|
|
488
|
+
return {
|
|
489
|
+
id: recordId,
|
|
490
|
+
status: r.entry.status,
|
|
491
|
+
taskId: r.entry.taskId,
|
|
492
|
+
result: _withAttempts(r.entry.result, r.attempts),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
id: recordId,
|
|
497
|
+
status: "failed",
|
|
498
|
+
taskId: null,
|
|
499
|
+
result: _withAttempts(
|
|
500
|
+
{ summary: `Step threw: ${r.error.message}` },
|
|
501
|
+
r.attempts,
|
|
502
|
+
),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ─── while / until loop nodes ──────────────────────────────────────────────────
|
|
507
|
+
|
|
508
|
+
/** True when a step is a loop node (`loopWhile` or `loopUntil`). */
|
|
509
|
+
export function isLoopStep(step) {
|
|
510
|
+
return step.loopWhile !== undefined || step.loopUntil !== undefined;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** Resolve a loop step's iteration cap, clamped to MAX_LOOP_ITERATIONS. */
|
|
514
|
+
export function loopIterationCap(step) {
|
|
515
|
+
const m = Number(step.maxIterations);
|
|
516
|
+
if (Number.isFinite(m) && m > 0) {
|
|
517
|
+
return Math.min(Math.floor(m), MAX_LOOP_ITERATIONS);
|
|
518
|
+
}
|
|
519
|
+
return MAX_LOOP_ITERATIONS;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Substitute loop-local tokens in a message template: `${iter}` → the 1-based
|
|
524
|
+
* iteration number, `${self.<field>}` → the step's own most-recent iteration
|
|
525
|
+
* result (empty on the first iteration). Other `${step.<id>.<field>}` tokens
|
|
526
|
+
* are left for `substitutePlaceholders`.
|
|
527
|
+
*/
|
|
528
|
+
export function substituteLoopVars(template, { stepId, iter, resultsById }) {
|
|
529
|
+
if (typeof template !== "string") return template;
|
|
530
|
+
let out = template.replace(/\$\{iter\}/g, String(iter));
|
|
531
|
+
out = out.replace(/\$\{self\.([\w-]+)\}/g, (_, field) => {
|
|
532
|
+
const entry = resultsById?.get?.(stepId);
|
|
533
|
+
if (!entry) return "";
|
|
534
|
+
if (field === "summary") return entry.result?.summary ?? "";
|
|
535
|
+
if (field === "status") return entry.status ?? "";
|
|
536
|
+
if (field === "taskId") return entry.taskId ?? "";
|
|
537
|
+
if (field === "iterations") return String(entry.result?.iterations ?? 0);
|
|
538
|
+
if (field === "tokenCount") return String(entry.result?.tokenCount ?? 0);
|
|
539
|
+
const v = entry.result?.[field];
|
|
540
|
+
return v == null ? "" : String(v);
|
|
541
|
+
});
|
|
542
|
+
return out;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Evaluate whether a loop step should run another iteration (post-test). The
|
|
547
|
+
* condition may reference `${self.<field>}` (the just-stored iteration result)
|
|
548
|
+
* and `${iter}`. `loopWhile` continues while the expression is true; `loopUntil`
|
|
549
|
+
* continues until it becomes true. Throws on a malformed expression.
|
|
550
|
+
*/
|
|
551
|
+
export function evalLoopContinue(step, { stepId, iter, resultsById }) {
|
|
552
|
+
const isWhile = step.loopWhile !== undefined;
|
|
553
|
+
const expr = isWhile ? step.loopWhile : step.loopUntil;
|
|
554
|
+
const subst = String(expr)
|
|
555
|
+
.replace(/\$\{iter\}/g, String(iter))
|
|
556
|
+
.replace(/\$\{self\.([\w-]+)\}/g, (_, f) => `\${step.${stepId}.${f}}`);
|
|
557
|
+
const val = evalExpr(subst, { step: resultsById });
|
|
558
|
+
return isWhile ? val === true : val !== true;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Run a loop step: repeat its task until the `loopWhile`/`loopUntil` condition
|
|
563
|
+
* says to stop, a failing iteration aborts it, or the iteration cap is hit.
|
|
564
|
+
* Each iteration inherits the step's retry/timeout config. The final result
|
|
565
|
+
* carries `iterations`, `loopExhausted`, and `loopStop` (`condition`|`cap`|
|
|
566
|
+
* `failed`|`bad-condition`).
|
|
567
|
+
*/
|
|
568
|
+
export async function runLoopStep({
|
|
569
|
+
step,
|
|
570
|
+
recordId,
|
|
571
|
+
cwd,
|
|
572
|
+
llmOptions,
|
|
573
|
+
resultsById,
|
|
574
|
+
}) {
|
|
575
|
+
const cap = loopIterationCap(step);
|
|
576
|
+
let last = null;
|
|
577
|
+
let iterations = 0;
|
|
578
|
+
let stopReason = "cap";
|
|
579
|
+
for (let iter = 1; iter <= cap; iter++) {
|
|
580
|
+
iterations = iter;
|
|
581
|
+
const withSelf = substituteLoopVars(step.message, {
|
|
582
|
+
stepId: recordId,
|
|
583
|
+
iter,
|
|
584
|
+
resultsById,
|
|
585
|
+
});
|
|
586
|
+
const message = substitutePlaceholders(withSelf, resultsById);
|
|
587
|
+
const r = await runStepWithRetry({ step, message, cwd, llmOptions });
|
|
588
|
+
last = outcomeFromRetry(recordId, r);
|
|
589
|
+
resultsById.set(recordId, last);
|
|
590
|
+
if (last.status === "failed") {
|
|
591
|
+
stopReason = "failed";
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
let cont;
|
|
595
|
+
try {
|
|
596
|
+
cont = evalLoopContinue(step, { stepId: recordId, iter, resultsById });
|
|
597
|
+
} catch (err) {
|
|
598
|
+
last = {
|
|
599
|
+
id: recordId,
|
|
600
|
+
status: "failed",
|
|
601
|
+
taskId: null,
|
|
602
|
+
result: {
|
|
603
|
+
summary: `invalid loop condition on '${recordId}': ${err.message}`,
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
resultsById.set(recordId, last);
|
|
607
|
+
stopReason = "bad-condition";
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
if (!cont) {
|
|
611
|
+
stopReason = "condition";
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
id: recordId,
|
|
617
|
+
status: last ? last.status : "skipped",
|
|
618
|
+
taskId: last?.taskId ?? null,
|
|
619
|
+
result: {
|
|
620
|
+
...(last?.result || {}),
|
|
621
|
+
iterations,
|
|
622
|
+
loopExhausted: stopReason === "cap",
|
|
623
|
+
loopStop: stopReason,
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ─── Step node + no-barrier pipeline ──────────────────────────────────────────
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Run a single step to completion, resolving its when-gate / loop / forEach /
|
|
632
|
+
* plain task exactly as the batch executor does, and storing results (including
|
|
633
|
+
* a forEach parent aggregate) in `resultsById`. Returns `{ outcomes, failed }`:
|
|
634
|
+
* `outcomes` are the entries to add to the run's step list (forEach contributes
|
|
635
|
+
* its children, not the parent); `failed` is true when any non-skipped outcome
|
|
636
|
+
* is not "completed". Used by the no-barrier pipeline scheduler.
|
|
637
|
+
*/
|
|
638
|
+
export async function runStepNode(step, ctx) {
|
|
639
|
+
const { resultsById, cwd, llmOptions, onStepStart, onStepComplete } = ctx;
|
|
640
|
+
const recordId = step.id;
|
|
641
|
+
const single = (status, summary) => {
|
|
642
|
+
const o = { id: recordId, status, taskId: null, result: { summary } };
|
|
643
|
+
resultsById.set(recordId, o);
|
|
644
|
+
return {
|
|
645
|
+
outcomes: [o],
|
|
646
|
+
failed: status !== "completed" && status !== "skipped",
|
|
647
|
+
};
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
let runThis;
|
|
651
|
+
try {
|
|
652
|
+
runThis = shouldRunStep(step, resultsById);
|
|
653
|
+
} catch (err) {
|
|
654
|
+
return single("failed", err.message);
|
|
655
|
+
}
|
|
656
|
+
if (!runThis) return single("skipped", "when-condition false");
|
|
657
|
+
|
|
658
|
+
if (isLoopStep(step)) {
|
|
659
|
+
if (onStepStart) onStepStart({ stepId: recordId, message: step.message });
|
|
660
|
+
const o = await runLoopStep({
|
|
661
|
+
step,
|
|
662
|
+
recordId,
|
|
663
|
+
cwd,
|
|
664
|
+
llmOptions,
|
|
665
|
+
resultsById,
|
|
666
|
+
});
|
|
667
|
+
resultsById.set(recordId, o);
|
|
668
|
+
if (onStepComplete) onStepComplete(o);
|
|
669
|
+
return {
|
|
670
|
+
outcomes: [o],
|
|
671
|
+
failed: o.status !== "completed" && o.status !== "skipped",
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (step.forEach !== undefined) {
|
|
676
|
+
let items;
|
|
677
|
+
try {
|
|
678
|
+
items = resolveForEachItems(step.forEach, resultsById);
|
|
679
|
+
} catch (err) {
|
|
680
|
+
return single("failed", err.message);
|
|
681
|
+
}
|
|
682
|
+
if (items.length === 0) return single("skipped", "forEach items empty");
|
|
683
|
+
const children = await Promise.all(
|
|
684
|
+
items.map(async (item, k) => {
|
|
685
|
+
const childId = `${recordId}[${k}]`;
|
|
686
|
+
const withItem = substituteItem(step.message, item);
|
|
687
|
+
const msg = substitutePlaceholders(withItem, resultsById);
|
|
688
|
+
if (onStepStart) onStepStart({ stepId: childId, message: msg });
|
|
689
|
+
const r = await runStepWithRetry({
|
|
690
|
+
step,
|
|
691
|
+
message: msg,
|
|
692
|
+
cwd,
|
|
693
|
+
llmOptions,
|
|
694
|
+
});
|
|
695
|
+
const co = outcomeFromRetry(childId, r);
|
|
696
|
+
resultsById.set(childId, co);
|
|
697
|
+
if (onStepComplete) onStepComplete(co);
|
|
698
|
+
return co;
|
|
699
|
+
}),
|
|
700
|
+
);
|
|
701
|
+
const allOk = children.every((c) => c.status === "completed");
|
|
702
|
+
const anyOk = children.some((c) => c.status === "completed");
|
|
703
|
+
resultsById.set(recordId, {
|
|
704
|
+
id: recordId,
|
|
705
|
+
status: allOk ? "completed" : anyOk ? "partial" : "failed",
|
|
706
|
+
taskId: null,
|
|
707
|
+
result: {
|
|
708
|
+
summary: children.map((c) => c.result?.summary ?? "").join("\n"),
|
|
709
|
+
children: children.length,
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
return {
|
|
713
|
+
outcomes: children,
|
|
714
|
+
failed: children.some((c) => c.status !== "completed"),
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const message = substitutePlaceholders(step.message, resultsById);
|
|
719
|
+
if (onStepStart) onStepStart({ stepId: recordId, message });
|
|
720
|
+
const r = await runStepWithRetry({ step, message, cwd, llmOptions });
|
|
721
|
+
const o = outcomeFromRetry(recordId, r);
|
|
722
|
+
resultsById.set(recordId, o);
|
|
723
|
+
if (onStepComplete) onStepComplete(o);
|
|
724
|
+
return { outcomes: [o], failed: o.status !== "completed" };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* No-barrier pipeline scheduler: start each step the instant *its own*
|
|
729
|
+
* dependencies finish, rather than waiting for the whole dependency level.
|
|
730
|
+
* Up to `maxParallel` step nodes run concurrently. On failure with
|
|
731
|
+
* `continueOnError` off, no new steps are scheduled (in-flight ones finish) and
|
|
732
|
+
* the rest are marked skipped. Produces the same outcome set as the batch
|
|
733
|
+
* executor — only the wall-clock idle between levels is removed.
|
|
734
|
+
*/
|
|
735
|
+
export async function runPipeline({
|
|
736
|
+
steps,
|
|
737
|
+
resultsById,
|
|
738
|
+
maxParallel = 4,
|
|
739
|
+
continueOnError = false,
|
|
740
|
+
cwd,
|
|
741
|
+
llmOptions = {},
|
|
742
|
+
onStepStart,
|
|
743
|
+
onStepComplete,
|
|
744
|
+
}) {
|
|
745
|
+
const limit = Math.max(1, Math.floor(maxParallel) || 1);
|
|
746
|
+
const remainingDeps = new Map(
|
|
747
|
+
steps.map((s) => [s.id, new Set(s.dependsOn || [])]),
|
|
748
|
+
);
|
|
749
|
+
const dependents = new Map(steps.map((s) => [s.id, []]));
|
|
750
|
+
for (const s of steps) {
|
|
751
|
+
for (const d of s.dependsOn || []) {
|
|
752
|
+
if (dependents.has(d)) dependents.get(d).push(s.id);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const scheduled = new Set();
|
|
756
|
+
const stepOutcomes = [];
|
|
757
|
+
let anyFailure = false;
|
|
758
|
+
let halted = false;
|
|
759
|
+
let active = 0;
|
|
760
|
+
|
|
761
|
+
await new Promise((resolve) => {
|
|
762
|
+
function pump() {
|
|
763
|
+
if (!halted) {
|
|
764
|
+
for (const s of steps) {
|
|
765
|
+
if (active >= limit) break;
|
|
766
|
+
if (scheduled.has(s.id)) continue;
|
|
767
|
+
if (remainingDeps.get(s.id).size !== 0) continue;
|
|
768
|
+
scheduled.add(s.id);
|
|
769
|
+
active++;
|
|
770
|
+
launch(s);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (active === 0 && (halted || scheduled.size === steps.length)) {
|
|
774
|
+
for (const s of steps) {
|
|
775
|
+
if (scheduled.has(s.id)) continue;
|
|
776
|
+
const o = {
|
|
777
|
+
id: s.id,
|
|
778
|
+
status: "skipped",
|
|
779
|
+
taskId: null,
|
|
780
|
+
result: { summary: "skipped due to earlier failure" },
|
|
781
|
+
};
|
|
782
|
+
resultsById.set(s.id, o);
|
|
783
|
+
stepOutcomes.push(o);
|
|
784
|
+
scheduled.add(s.id);
|
|
785
|
+
}
|
|
786
|
+
resolve();
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
async function launch(step) {
|
|
791
|
+
let res;
|
|
792
|
+
try {
|
|
793
|
+
res = await runStepNode(step, {
|
|
794
|
+
resultsById,
|
|
795
|
+
cwd,
|
|
796
|
+
llmOptions,
|
|
797
|
+
onStepStart,
|
|
798
|
+
onStepComplete,
|
|
799
|
+
});
|
|
800
|
+
} catch (err) {
|
|
801
|
+
const o = {
|
|
802
|
+
id: step.id,
|
|
803
|
+
status: "failed",
|
|
804
|
+
taskId: null,
|
|
805
|
+
result: { summary: `Step threw: ${err.message}` },
|
|
806
|
+
};
|
|
807
|
+
resultsById.set(step.id, o);
|
|
808
|
+
res = { outcomes: [o], failed: true };
|
|
809
|
+
}
|
|
810
|
+
stepOutcomes.push(...res.outcomes);
|
|
811
|
+
active--;
|
|
812
|
+
if (res.failed) {
|
|
813
|
+
anyFailure = true;
|
|
814
|
+
if (!continueOnError) halted = true;
|
|
815
|
+
}
|
|
816
|
+
for (const depId of dependents.get(step.id) || []) {
|
|
817
|
+
remainingDeps.get(depId).delete(step.id);
|
|
818
|
+
}
|
|
819
|
+
pump();
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
pump();
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
return { stepOutcomes, anyFailure };
|
|
826
|
+
}
|
|
827
|
+
|
|
332
828
|
// ─── Execution ───────────────────────────────────────────────────────────────
|
|
333
829
|
|
|
334
830
|
/**
|
|
335
831
|
* Execute a workflow. The runner for individual tasks must be injected via
|
|
336
832
|
* `_deps.runTask` (signature matches `runCoworkTask`).
|
|
337
833
|
*
|
|
834
|
+
* Per-step robustness fields (all optional): `retries` (extra attempts after
|
|
835
|
+
* the first; non-negative integer), `timeoutMs` (per-attempt timeout, positive
|
|
836
|
+
* number), `retryDelayMs` (base delay before each retry, non-negative number),
|
|
837
|
+
* `retryBackoff` (`"fixed"` (default) or `"exponential"`).
|
|
838
|
+
*
|
|
839
|
+
* Loop nodes (optional, mutually exclusive with `forEach`): `loopWhile` /
|
|
840
|
+
* `loopUntil` (a `workflow-expr` condition that may reference `${self.<field>}`
|
|
841
|
+
* — the step's own latest iteration result — and `${iter}`) repeat the step's
|
|
842
|
+
* task (post-test) until the condition stops it, an iteration fails, or
|
|
843
|
+
* `maxIterations` (≤ MAX_LOOP_ITERATIONS) is hit. Each iteration inherits the
|
|
844
|
+
* step's retry/timeout config.
|
|
845
|
+
*
|
|
338
846
|
* @param {object} options
|
|
339
847
|
* @param {object} options.workflow - Workflow definition
|
|
340
848
|
* @param {string} [options.cwd] - Working directory for history
|
|
341
|
-
* @param {number} [options.maxParallel] - Max parallel steps per batch
|
|
849
|
+
* @param {number} [options.maxParallel] - Max parallel steps (per batch, or
|
|
850
|
+
* concurrent step nodes in pipeline mode)
|
|
342
851
|
* @param {boolean} [options.continueOnError] - Keep running after a failure
|
|
852
|
+
* @param {boolean} [options.pipeline] - No-barrier scheduling: start each step
|
|
853
|
+
* as soon as its own deps finish instead of waiting for the dependency level.
|
|
854
|
+
* Defaults to `workflow.pipeline ?? false`. Same outcomes, less idle wait.
|
|
343
855
|
* @param {object} [options.llmOptions] - Forwarded to each task
|
|
344
856
|
* @param {function} [options.onStepStart]
|
|
345
857
|
* @param {function} [options.onStepComplete]
|
|
@@ -360,6 +872,7 @@ export async function executeWorkflow(options = {}) {
|
|
|
360
872
|
llmOptions = {},
|
|
361
873
|
onStepStart,
|
|
362
874
|
onStepComplete,
|
|
875
|
+
pipeline,
|
|
363
876
|
} = options;
|
|
364
877
|
|
|
365
878
|
const { valid, errors } = validateWorkflow(workflow);
|
|
@@ -370,67 +883,54 @@ export async function executeWorkflow(options = {}) {
|
|
|
370
883
|
);
|
|
371
884
|
}
|
|
372
885
|
|
|
373
|
-
const
|
|
886
|
+
const usePipeline = pipeline ?? workflow.pipeline ?? false;
|
|
374
887
|
const resultsById = new Map();
|
|
375
|
-
const stepOutcomes = [];
|
|
376
888
|
const startedAt = new Date(_deps.now()).toISOString();
|
|
377
|
-
let
|
|
889
|
+
let stepOutcomes;
|
|
890
|
+
let anyFailure;
|
|
378
891
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
892
|
+
if (usePipeline) {
|
|
893
|
+
({ stepOutcomes, anyFailure } = await runPipeline({
|
|
894
|
+
steps: workflow.steps,
|
|
895
|
+
resultsById,
|
|
896
|
+
maxParallel,
|
|
897
|
+
continueOnError,
|
|
898
|
+
cwd,
|
|
899
|
+
llmOptions,
|
|
900
|
+
onStepStart,
|
|
901
|
+
onStepComplete,
|
|
902
|
+
}));
|
|
903
|
+
} else {
|
|
904
|
+
stepOutcomes = [];
|
|
905
|
+
anyFailure = false;
|
|
906
|
+
const batches = planBatches(workflow.steps);
|
|
907
|
+
for (const batch of batches) {
|
|
908
|
+
// Respect maxParallel by slicing batch into chunks
|
|
909
|
+
const chunks = [];
|
|
910
|
+
for (let i = 0; i < batch.length; i += maxParallel) {
|
|
911
|
+
chunks.push(batch.slice(i, i + maxParallel));
|
|
912
|
+
}
|
|
385
913
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
try {
|
|
405
|
-
runThis = shouldRunStep(step, resultsById);
|
|
406
|
-
} catch (err) {
|
|
407
|
-
anyFailure = true;
|
|
408
|
-
const outcome = {
|
|
409
|
-
id: step.id,
|
|
410
|
-
status: "failed",
|
|
411
|
-
taskId: null,
|
|
412
|
-
result: { summary: err.message },
|
|
413
|
-
};
|
|
414
|
-
resultsById.set(step.id, outcome);
|
|
415
|
-
preOutcomes.push(outcome);
|
|
416
|
-
continue;
|
|
417
|
-
}
|
|
418
|
-
if (!runThis) {
|
|
419
|
-
const outcome = {
|
|
420
|
-
id: step.id,
|
|
421
|
-
status: "skipped",
|
|
422
|
-
taskId: null,
|
|
423
|
-
result: { summary: "when-condition false" },
|
|
424
|
-
};
|
|
425
|
-
resultsById.set(step.id, outcome);
|
|
426
|
-
preOutcomes.push(outcome);
|
|
427
|
-
continue;
|
|
428
|
-
}
|
|
429
|
-
// forEach-expansion
|
|
430
|
-
if (step.forEach !== undefined) {
|
|
431
|
-
let items;
|
|
914
|
+
for (const chunk of chunks) {
|
|
915
|
+
// Expand forEach / when into concrete tasks for this chunk
|
|
916
|
+
const runnable = []; // { step, message, recordId, parentId }
|
|
917
|
+
const preOutcomes = []; // outcomes produced synchronously (skipped)
|
|
918
|
+
for (const step of chunk) {
|
|
919
|
+
if (anyFailure && !continueOnError) {
|
|
920
|
+
const outcome = {
|
|
921
|
+
id: step.id,
|
|
922
|
+
status: "skipped",
|
|
923
|
+
taskId: null,
|
|
924
|
+
result: { summary: "skipped due to earlier failure" },
|
|
925
|
+
};
|
|
926
|
+
resultsById.set(step.id, outcome);
|
|
927
|
+
preOutcomes.push(outcome);
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
// when-gate
|
|
931
|
+
let runThis = true;
|
|
432
932
|
try {
|
|
433
|
-
|
|
933
|
+
runThis = shouldRunStep(step, resultsById);
|
|
434
934
|
} catch (err) {
|
|
435
935
|
anyFailure = true;
|
|
436
936
|
const outcome = {
|
|
@@ -443,97 +943,132 @@ export async function executeWorkflow(options = {}) {
|
|
|
443
943
|
preOutcomes.push(outcome);
|
|
444
944
|
continue;
|
|
445
945
|
}
|
|
446
|
-
if (
|
|
946
|
+
if (!runThis) {
|
|
447
947
|
const outcome = {
|
|
448
948
|
id: step.id,
|
|
449
949
|
status: "skipped",
|
|
450
950
|
taskId: null,
|
|
451
|
-
result: { summary: "
|
|
951
|
+
result: { summary: "when-condition false" },
|
|
452
952
|
};
|
|
453
953
|
resultsById.set(step.id, outcome);
|
|
454
954
|
preOutcomes.push(outcome);
|
|
455
955
|
continue;
|
|
456
956
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const msg = substitutePlaceholders(withItem, resultsById);
|
|
957
|
+
// loop node — runs its body repeatedly; per-iteration substitution
|
|
958
|
+
// happens inside runLoopStep, so push the raw template.
|
|
959
|
+
if (isLoopStep(step)) {
|
|
461
960
|
runnable.push({
|
|
462
961
|
step,
|
|
463
|
-
message:
|
|
464
|
-
recordId:
|
|
465
|
-
parentId:
|
|
962
|
+
message: step.message,
|
|
963
|
+
recordId: step.id,
|
|
964
|
+
parentId: null,
|
|
965
|
+
isLoop: true,
|
|
466
966
|
});
|
|
967
|
+
continue;
|
|
467
968
|
}
|
|
468
|
-
|
|
969
|
+
// forEach-expansion
|
|
970
|
+
if (step.forEach !== undefined) {
|
|
971
|
+
let items;
|
|
972
|
+
try {
|
|
973
|
+
items = resolveForEachItems(step.forEach, resultsById);
|
|
974
|
+
} catch (err) {
|
|
975
|
+
anyFailure = true;
|
|
976
|
+
const outcome = {
|
|
977
|
+
id: step.id,
|
|
978
|
+
status: "failed",
|
|
979
|
+
taskId: null,
|
|
980
|
+
result: { summary: err.message },
|
|
981
|
+
};
|
|
982
|
+
resultsById.set(step.id, outcome);
|
|
983
|
+
preOutcomes.push(outcome);
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
if (items.length === 0) {
|
|
987
|
+
const outcome = {
|
|
988
|
+
id: step.id,
|
|
989
|
+
status: "skipped",
|
|
990
|
+
taskId: null,
|
|
991
|
+
result: { summary: "forEach items empty" },
|
|
992
|
+
};
|
|
993
|
+
resultsById.set(step.id, outcome);
|
|
994
|
+
preOutcomes.push(outcome);
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
for (let k = 0; k < items.length; k++) {
|
|
998
|
+
const childId = `${step.id}[${k}]`;
|
|
999
|
+
const withItem = substituteItem(step.message, items[k]);
|
|
1000
|
+
const msg = substitutePlaceholders(withItem, resultsById);
|
|
1001
|
+
runnable.push({
|
|
1002
|
+
step,
|
|
1003
|
+
message: msg,
|
|
1004
|
+
recordId: childId,
|
|
1005
|
+
parentId: step.id,
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
const message = substitutePlaceholders(step.message, resultsById);
|
|
1011
|
+
runnable.push({ step, message, recordId: step.id, parentId: null });
|
|
469
1012
|
}
|
|
470
|
-
const message = substitutePlaceholders(step.message, resultsById);
|
|
471
|
-
runnable.push({ step, message, recordId: step.id, parentId: null });
|
|
472
|
-
}
|
|
473
1013
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
1014
|
+
const promises = runnable.map(
|
|
1015
|
+
async ({ step, message, recordId, isLoop }) => {
|
|
1016
|
+
if (onStepStart) onStepStart({ stepId: recordId, message });
|
|
1017
|
+
let outcome;
|
|
1018
|
+
if (isLoop) {
|
|
1019
|
+
outcome = await runLoopStep({
|
|
1020
|
+
step,
|
|
1021
|
+
recordId,
|
|
1022
|
+
cwd,
|
|
1023
|
+
llmOptions,
|
|
1024
|
+
resultsById,
|
|
1025
|
+
});
|
|
1026
|
+
} else {
|
|
1027
|
+
const r = await runStepWithRetry({
|
|
1028
|
+
step,
|
|
1029
|
+
message,
|
|
1030
|
+
cwd,
|
|
1031
|
+
llmOptions,
|
|
1032
|
+
});
|
|
1033
|
+
outcome = outcomeFromRetry(recordId, r);
|
|
1034
|
+
}
|
|
1035
|
+
if (outcome.status !== "completed") anyFailure = true;
|
|
1036
|
+
resultsById.set(recordId, outcome);
|
|
1037
|
+
if (onStepComplete) onStepComplete(outcome);
|
|
1038
|
+
return outcome;
|
|
1039
|
+
},
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
const results = await Promise.all(promises);
|
|
1043
|
+
stepOutcomes.push(...preOutcomes, ...results);
|
|
1044
|
+
|
|
1045
|
+
// Aggregate forEach children into a parent entry so downstream
|
|
1046
|
+
// `${step.<parent>.summary}` references still work.
|
|
1047
|
+
const byParent = new Map();
|
|
1048
|
+
for (let k = 0; k < runnable.length; k++) {
|
|
1049
|
+
const r = runnable[k];
|
|
1050
|
+
if (!r.parentId) continue;
|
|
1051
|
+
if (!byParent.has(r.parentId)) byParent.set(r.parentId, []);
|
|
1052
|
+
byParent.get(r.parentId).push(results[k]);
|
|
1053
|
+
}
|
|
1054
|
+
for (const [parentId, children] of byParent) {
|
|
1055
|
+
const allOk = children.every((c) => c.status === "completed");
|
|
1056
|
+
const anyOk = children.some((c) => c.status === "completed");
|
|
1057
|
+
const status = allOk ? "completed" : anyOk ? "partial" : "failed";
|
|
1058
|
+
resultsById.set(parentId, {
|
|
1059
|
+
id: parentId,
|
|
1060
|
+
status,
|
|
499
1061
|
taskId: null,
|
|
500
|
-
result: {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
1062
|
+
result: {
|
|
1063
|
+
summary: children.map((c) => c.result?.summary ?? "").join("\n"),
|
|
1064
|
+
children: children.length,
|
|
1065
|
+
},
|
|
1066
|
+
});
|
|
505
1067
|
}
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
const results = await Promise.all(promises);
|
|
509
|
-
stepOutcomes.push(...preOutcomes, ...results);
|
|
510
|
-
|
|
511
|
-
// Aggregate forEach children into a parent entry so downstream
|
|
512
|
-
// `${step.<parent>.summary}` references still work.
|
|
513
|
-
const byParent = new Map();
|
|
514
|
-
for (let k = 0; k < runnable.length; k++) {
|
|
515
|
-
const r = runnable[k];
|
|
516
|
-
if (!r.parentId) continue;
|
|
517
|
-
if (!byParent.has(r.parentId)) byParent.set(r.parentId, []);
|
|
518
|
-
byParent.get(r.parentId).push(results[k]);
|
|
519
|
-
}
|
|
520
|
-
for (const [parentId, children] of byParent) {
|
|
521
|
-
const allOk = children.every((c) => c.status === "completed");
|
|
522
|
-
const anyOk = children.some((c) => c.status === "completed");
|
|
523
|
-
const status = allOk ? "completed" : anyOk ? "partial" : "failed";
|
|
524
|
-
resultsById.set(parentId, {
|
|
525
|
-
id: parentId,
|
|
526
|
-
status,
|
|
527
|
-
taskId: null,
|
|
528
|
-
result: {
|
|
529
|
-
summary: children.map((c) => c.result?.summary ?? "").join("\n"),
|
|
530
|
-
children: children.length,
|
|
531
|
-
},
|
|
532
|
-
});
|
|
533
1068
|
}
|
|
534
|
-
}
|
|
535
1069
|
|
|
536
|
-
|
|
1070
|
+
if (anyFailure && !continueOnError) break;
|
|
1071
|
+
}
|
|
537
1072
|
}
|
|
538
1073
|
|
|
539
1074
|
const finishedAt = new Date(_deps.now()).toISOString();
|