@xdelivered/emberflow 0.2.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/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/commands.test.ts +185 -0
- package/bin/commands.ts +306 -0
- package/bin/emberflow.mjs +38 -0
- package/bin/init.test.ts +165 -0
- package/bin/init.ts +297 -0
- package/bin/runtime.mjs +105 -0
- package/bin/runtime.test.ts +167 -0
- package/bin/skillTargets.test.ts +108 -0
- package/bin/skillTargets.ts +36 -0
- package/bin/skillTemplates.test.ts +37 -0
- package/bin/tsconfig.json +23 -0
- package/dist/bin/commands.d.ts +24 -0
- package/dist/bin/commands.js +252 -0
- package/dist/bin/init.d.ts +17 -0
- package/dist/bin/init.js +241 -0
- package/dist/bin/runtime.d.mts +39 -0
- package/dist/bin/runtime.mjs +96 -0
- package/dist/bin/skillTargets.d.ts +13 -0
- package/dist/bin/skillTargets.js +22 -0
- package/dist/server/agents/claudeAdapter.d.ts +8 -0
- package/dist/server/agents/claudeAdapter.js +190 -0
- package/dist/server/agents/claudeParse.d.ts +2 -0
- package/dist/server/agents/claudeParse.js +162 -0
- package/dist/server/agents/codexAdapter.d.ts +13 -0
- package/dist/server/agents/codexAdapter.js +174 -0
- package/dist/server/agents/codexParse.d.ts +2 -0
- package/dist/server/agents/codexParse.js +74 -0
- package/dist/server/agents/detect.d.ts +23 -0
- package/dist/server/agents/detect.js +35 -0
- package/dist/server/agents/gitScope.d.ts +24 -0
- package/dist/server/agents/gitScope.js +95 -0
- package/dist/server/agents/modelRejectionHint.d.ts +8 -0
- package/dist/server/agents/modelRejectionHint.js +12 -0
- package/dist/server/agents/prompt.d.ts +79 -0
- package/dist/server/agents/prompt.js +364 -0
- package/dist/server/agents/runManager.d.ts +90 -0
- package/dist/server/agents/runManager.js +259 -0
- package/dist/server/agents/types.d.ts +15 -0
- package/dist/server/agents/types.js +1 -0
- package/dist/server/apiStore.d.ts +60 -0
- package/dist/server/apiStore.js +249 -0
- package/dist/server/auth/enforce.d.ts +1 -0
- package/dist/server/auth/enforce.js +6 -0
- package/dist/server/auth/types.d.ts +1 -0
- package/dist/server/auth/types.js +1 -0
- package/dist/server/auth/verifiers.d.ts +1 -0
- package/dist/server/auth/verifiers.js +4 -0
- package/dist/server/authAttach.d.ts +19 -0
- package/dist/server/authAttach.js +79 -0
- package/dist/server/cli.d.ts +7 -0
- package/dist/server/cli.js +804 -0
- package/dist/server/client.d.ts +130 -0
- package/dist/server/client.js +247 -0
- package/dist/server/environments.d.ts +103 -0
- package/dist/server/environments.js +486 -0
- package/dist/server/flowStore.d.ts +37 -0
- package/dist/server/flowStore.js +117 -0
- package/dist/server/httpOperations.d.ts +32 -0
- package/dist/server/httpOperations.js +62 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1022 -0
- package/dist/server/infrastructure.d.ts +51 -0
- package/dist/server/infrastructure.js +116 -0
- package/dist/server/login.d.ts +21 -0
- package/dist/server/login.js +44 -0
- package/dist/server/mcp.d.ts +1 -0
- package/dist/server/mcp.js +182 -0
- package/dist/server/migrateFlows.d.ts +8 -0
- package/dist/server/migrateFlows.js +25 -0
- package/dist/server/mockHandler.d.ts +15 -0
- package/dist/server/mockHandler.js +35 -0
- package/dist/server/nodesPayload.d.ts +14 -0
- package/dist/server/nodesPayload.js +14 -0
- package/dist/server/normalizeFlow.d.ts +21 -0
- package/dist/server/normalizeFlow.js +48 -0
- package/dist/server/openBrowser.d.ts +8 -0
- package/dist/server/openBrowser.js +16 -0
- package/dist/server/operationResult.d.ts +12 -0
- package/dist/server/operationResult.js +26 -0
- package/dist/server/pathGuard.d.ts +1 -0
- package/dist/server/pathGuard.js +16 -0
- package/dist/server/pathSafety.d.ts +20 -0
- package/dist/server/pathSafety.js +35 -0
- package/dist/server/projectConfig.d.ts +47 -0
- package/dist/server/projectConfig.js +61 -0
- package/dist/server/projectMode.d.ts +26 -0
- package/dist/server/projectMode.js +44 -0
- package/dist/server/redact.d.ts +6 -0
- package/dist/server/redact.js +37 -0
- package/dist/server/runRegistry.d.ts +84 -0
- package/dist/server/runRegistry.js +200 -0
- package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
- package/dist/server/runtime/RuntimeAdapter.js +1 -0
- package/dist/server/runtime/expressAdapter.d.ts +19 -0
- package/dist/server/runtime/expressAdapter.js +56 -0
- package/dist/server/scenarioTest.d.ts +14 -0
- package/dist/server/scenarioTest.js +66 -0
- package/dist/server/subflowRunner.d.ts +32 -0
- package/dist/server/subflowRunner.js +72 -0
- package/dist/server/testRunner.d.ts +70 -0
- package/dist/server/testRunner.js +151 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +2 -0
- package/dist/src/engine/authVerify.d.ts +33 -0
- package/dist/src/engine/authVerify.js +63 -0
- package/dist/src/engine/diagnostics.d.ts +62 -0
- package/dist/src/engine/diagnostics.js +148 -0
- package/dist/src/engine/executor.d.ts +151 -0
- package/dist/src/engine/executor.js +684 -0
- package/dist/src/engine/httpError.d.ts +7 -0
- package/dist/src/engine/httpError.js +14 -0
- package/dist/src/engine/index.d.ts +11 -0
- package/dist/src/engine/index.js +11 -0
- package/dist/src/engine/output.d.ts +11 -0
- package/dist/src/engine/output.js +20 -0
- package/dist/src/engine/publish.d.ts +30 -0
- package/dist/src/engine/publish.js +95 -0
- package/dist/src/engine/registry.d.ts +34 -0
- package/dist/src/engine/registry.js +67 -0
- package/dist/src/engine/scenarios.d.ts +12 -0
- package/dist/src/engine/scenarios.js +19 -0
- package/dist/src/engine/schemaCheck.d.ts +4 -0
- package/dist/src/engine/schemaCheck.js +34 -0
- package/dist/src/engine/trace.d.ts +12 -0
- package/dist/src/engine/trace.js +12 -0
- package/dist/src/engine/types.d.ts +308 -0
- package/dist/src/engine/types.js +1 -0
- package/dist/src/engine/validation.d.ts +26 -0
- package/dist/src/engine/validation.js +318 -0
- package/dist/src/flows/anomaly-flows.d.ts +23 -0
- package/dist/src/flows/anomaly-flows.js +151 -0
- package/dist/src/flows/city-sweep-flow.d.ts +10 -0
- package/dist/src/flows/city-sweep-flow.js +127 -0
- package/dist/src/flows/login-flow.d.ts +13 -0
- package/dist/src/flows/login-flow.js +145 -0
- package/dist/src/flows/pradar-flows.d.ts +5 -0
- package/dist/src/flows/pradar-flows.js +375 -0
- package/dist/src/flows/weather-flow.d.ts +12 -0
- package/dist/src/flows/weather-flow.js +150 -0
- package/dist/src/lib/pathParams.d.ts +3 -0
- package/dist/src/lib/pathParams.js +6 -0
- package/dist/src/nodes/anomaly.d.ts +2 -0
- package/dist/src/nodes/anomaly.js +556 -0
- package/dist/src/nodes/flow-control.d.ts +7 -0
- package/dist/src/nodes/flow-control.js +270 -0
- package/dist/src/nodes/index.d.ts +3 -0
- package/dist/src/nodes/index.js +20 -0
- package/dist/src/nodes/login.d.ts +7 -0
- package/dist/src/nodes/login.js +158 -0
- package/dist/src/nodes/loops.d.ts +11 -0
- package/dist/src/nodes/loops.js +60 -0
- package/dist/src/nodes/pradar/index.d.ts +7 -0
- package/dist/src/nodes/pradar/index.js +9 -0
- package/dist/src/nodes/pradar/logic.d.ts +189 -0
- package/dist/src/nodes/pradar/logic.js +370 -0
- package/dist/src/nodes/pradar/nodes.d.ts +42 -0
- package/dist/src/nodes/pradar/nodes.js +801 -0
- package/dist/src/nodes/requireAuth.d.ts +20 -0
- package/dist/src/nodes/requireAuth.js +48 -0
- package/dist/src/nodes/response.d.ts +8 -0
- package/dist/src/nodes/response.js +33 -0
- package/dist/src/nodes/weather.d.ts +33 -0
- package/dist/src/nodes/weather.js +260 -0
- package/package.json +92 -0
- package/server/agentRoute.test.ts +157 -0
- package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
- package/server/agents/__fixtures__/fake-claude.mjs +55 -0
- package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
- package/server/agents/__fixtures__/fake-codex.mjs +52 -0
- package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
- package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
- package/server/agents/claudeAdapter.test.ts +127 -0
- package/server/agents/claudeAdapter.ts +212 -0
- package/server/agents/claudeParse.ts +193 -0
- package/server/agents/codexAdapter.test.ts +125 -0
- package/server/agents/codexAdapter.ts +198 -0
- package/server/agents/codexParse.test.ts +58 -0
- package/server/agents/codexParse.ts +126 -0
- package/server/agents/detect.test.ts +65 -0
- package/server/agents/detect.ts +43 -0
- package/server/agents/gitScope.test.ts +129 -0
- package/server/agents/gitScope.ts +110 -0
- package/server/agents/modelRejectionHint.test.ts +30 -0
- package/server/agents/modelRejectionHint.ts +14 -0
- package/server/agents/prompt.test.ts +613 -0
- package/server/agents/prompt.ts +527 -0
- package/server/agents/runManager.test.ts +251 -0
- package/server/agents/runManager.ts +286 -0
- package/server/agents/types.ts +17 -0
- package/server/apiMount.test.ts +591 -0
- package/server/apiStore.test.ts +324 -0
- package/server/apiStore.ts +241 -0
- package/server/auth/enforce.test.ts +61 -0
- package/server/auth/enforce.ts +6 -0
- package/server/auth/types.ts +5 -0
- package/server/auth/verifiers.test.ts +37 -0
- package/server/auth/verifiers.ts +9 -0
- package/server/authAttach.test.ts +175 -0
- package/server/authAttach.ts +99 -0
- package/server/cli.ts +847 -0
- package/server/cliAuth.test.ts +229 -0
- package/server/cliCreate.test.ts +131 -0
- package/server/cliDoctor.test.ts +326 -0
- package/server/cliTest.test.ts +735 -0
- package/server/client.ts +314 -0
- package/server/demoProject.test.ts +29 -0
- package/server/diagnosticsRoute.test.ts +169 -0
- package/server/environments.test.ts +620 -0
- package/server/environments.ts +603 -0
- package/server/environmentsRoute.test.ts +385 -0
- package/server/errorWorkflow.test.ts +205 -0
- package/server/flowStore.test.ts +84 -0
- package/server/flowStore.ts +125 -0
- package/server/httpOperations.test.ts +377 -0
- package/server/httpOperations.ts +96 -0
- package/server/index.ts +1111 -0
- package/server/infrastructure.test.ts +154 -0
- package/server/infrastructure.ts +161 -0
- package/server/infrastructureRoute.test.ts +105 -0
- package/server/login.test.ts +153 -0
- package/server/login.ts +67 -0
- package/server/mcp.ts +278 -0
- package/server/migrateFlows.test.ts +55 -0
- package/server/migrateFlows.ts +23 -0
- package/server/mockHandler.test.ts +138 -0
- package/server/mockHandler.ts +52 -0
- package/server/mockRun.test.ts +367 -0
- package/server/mockServe.test.ts +467 -0
- package/server/nodesPayload.test.ts +29 -0
- package/server/nodesPayload.ts +26 -0
- package/server/normalizeFlow.test.ts +80 -0
- package/server/normalizeFlow.ts +59 -0
- package/server/openBrowser.test.ts +20 -0
- package/server/openBrowser.ts +22 -0
- package/server/operationResult.test.ts +67 -0
- package/server/operationResult.ts +27 -0
- package/server/pathGuard.test.ts +19 -0
- package/server/pathGuard.ts +12 -0
- package/server/pathSafety.test.ts +38 -0
- package/server/pathSafety.ts +33 -0
- package/server/projectConfig.test.ts +67 -0
- package/server/projectConfig.ts +101 -0
- package/server/projectMode.test.ts +106 -0
- package/server/projectMode.ts +59 -0
- package/server/redact.test.ts +88 -0
- package/server/redact.ts +35 -0
- package/server/redactEgress.test.ts +281 -0
- package/server/runRegistry.ts +270 -0
- package/server/runsAuth.test.ts +119 -0
- package/server/runtime/RuntimeAdapter.ts +24 -0
- package/server/runtime/expressAdapter.test.ts +38 -0
- package/server/runtime/expressAdapter.ts +70 -0
- package/server/scenarioTest.test.ts +148 -0
- package/server/scenarioTest.ts +88 -0
- package/server/setupStatus.test.ts +127 -0
- package/server/subflowRunner.ts +104 -0
- package/server/testRunner.ts +216 -0
- package/server/tsconfig.json +20 -0
- package/server/workflowTestRoute.test.ts +297 -0
- package/src/App.tsx +233 -0
- package/src/components/AgentConsole.tsx +461 -0
- package/src/components/BranchRulesEditor.tsx +156 -0
- package/src/components/CreateModal.tsx +275 -0
- package/src/components/Dock.tsx +487 -0
- package/src/components/EnvironmentDialog.tsx +569 -0
- package/src/components/EnvironmentPicker.tsx +357 -0
- package/src/components/EnvironmentsDialog.tsx +144 -0
- package/src/components/ExecutionPager.tsx +67 -0
- package/src/components/InfraPanel.test.tsx +103 -0
- package/src/components/InfraPanel.tsx +171 -0
- package/src/components/Inspector.tsx +1182 -0
- package/src/components/Json.tsx +50 -0
- package/src/components/JsonModal.tsx +52 -0
- package/src/components/LogMessage.tsx +61 -0
- package/src/components/NodeRunModal.tsx +213 -0
- package/src/components/RunLogPanel.tsx +397 -0
- package/src/components/RunbookView.tsx +1085 -0
- package/src/components/Scenarios.tsx +476 -0
- package/src/components/SettingsDialog.tsx +229 -0
- package/src/components/Sidebar.tsx +436 -0
- package/src/components/StatusBar.tsx +302 -0
- package/src/components/Toolbar.tsx +663 -0
- package/src/components/WelcomeDialog.test.tsx +107 -0
- package/src/components/WelcomeDialog.tsx +412 -0
- package/src/components/agentMarkdown.test.tsx +91 -0
- package/src/components/agentMarkdown.tsx +158 -0
- package/src/components/ui/badge.tsx +31 -0
- package/src/components/ui/button.tsx +46 -0
- package/src/components/ui/combobox.tsx +114 -0
- package/src/components/ui/command.tsx +82 -0
- package/src/components/ui/dialog.tsx +73 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/popover.tsx +32 -0
- package/src/components/ui/resizable.tsx +44 -0
- package/src/components/ui/tabs.tsx +36 -0
- package/src/config.ts +4 -0
- package/src/engine/authVerify.ts +81 -0
- package/src/engine/diagnostics.test.ts +437 -0
- package/src/engine/diagnostics.ts +211 -0
- package/src/engine/executor.test.ts +1339 -0
- package/src/engine/executor.ts +840 -0
- package/src/engine/httpError.test.ts +12 -0
- package/src/engine/httpError.ts +16 -0
- package/src/engine/index.ts +11 -0
- package/src/engine/output.ts +21 -0
- package/src/engine/publish.test.ts +176 -0
- package/src/engine/publish.ts +127 -0
- package/src/engine/registry.test.ts +64 -0
- package/src/engine/registry.ts +80 -0
- package/src/engine/scenarios.test.ts +55 -0
- package/src/engine/scenarios.ts +29 -0
- package/src/engine/schemaCheck.test.ts +21 -0
- package/src/engine/schemaCheck.ts +35 -0
- package/src/engine/trace.test.ts +19 -0
- package/src/engine/trace.ts +23 -0
- package/src/engine/types.test.ts +22 -0
- package/src/engine/types.ts +316 -0
- package/src/engine/validation.test.ts +314 -0
- package/src/engine/validation.ts +354 -0
- package/src/flows/anomaly-flows.test.ts +81 -0
- package/src/flows/anomaly-flows.ts +202 -0
- package/src/flows/city-sweep-flow.ts +131 -0
- package/src/flows/flows.test.ts +30 -0
- package/src/flows/login-flow.ts +150 -0
- package/src/flows/pradar-flows.test.ts +204 -0
- package/src/flows/pradar-flows.ts +419 -0
- package/src/flows/weather-flow.ts +154 -0
- package/src/globals.css +147 -0
- package/src/lib/pathParams.test.ts +24 -0
- package/src/lib/pathParams.ts +6 -0
- package/src/lib/registerLens.test.ts +79 -0
- package/src/lib/registerLens.ts +59 -0
- package/src/lib/runbookModel.test.ts +61 -0
- package/src/lib/runbookModel.ts +290 -0
- package/src/lib/runbookProjection.test.ts +417 -0
- package/src/lib/runbookProjection.ts +254 -0
- package/src/lib/useTailAnchor.ts +53 -0
- package/src/lib/utils.ts +7 -0
- package/src/main.tsx +13 -0
- package/src/nodes/anomaly.test.ts +98 -0
- package/src/nodes/anomaly.ts +637 -0
- package/src/nodes/effects-scan.test.ts +44 -0
- package/src/nodes/flow-control.test.ts +280 -0
- package/src/nodes/flow-control.ts +305 -0
- package/src/nodes/index.ts +22 -0
- package/src/nodes/login.test.ts +81 -0
- package/src/nodes/login.ts +186 -0
- package/src/nodes/loops.ts +69 -0
- package/src/nodes/pradar/index.ts +11 -0
- package/src/nodes/pradar/logic.test.ts +288 -0
- package/src/nodes/pradar/logic.ts +582 -0
- package/src/nodes/pradar/nodes.test.ts +146 -0
- package/src/nodes/pradar/nodes.ts +955 -0
- package/src/nodes/requireAuth.test.ts +75 -0
- package/src/nodes/requireAuth.ts +55 -0
- package/src/nodes/response.test.ts +33 -0
- package/src/nodes/response.ts +39 -0
- package/src/nodes/traceDetail.test.ts +39 -0
- package/src/nodes/weather.test.ts +65 -0
- package/src/nodes/weather.ts +306 -0
- package/src/store/agentClient.test.ts +103 -0
- package/src/store/agentClient.ts +131 -0
- package/src/store/apiTree.test.ts +17 -0
- package/src/store/apiTree.ts +73 -0
- package/src/store/builderStore.test.ts +1234 -0
- package/src/store/builderStore.ts +1879 -0
- package/src/store/infraClient.ts +63 -0
- package/src/store/nodeMeta.test.ts +24 -0
- package/src/store/nodeMeta.ts +21 -0
- package/src/store/persistence.test.ts +15 -0
- package/src/store/persistence.ts +54 -0
- package/src/store/serverRunner.ts +381 -0
- package/src/store/setupClient.ts +57 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/studio-dist/assets/index-DNJwW-hM.css +1 -0
- package/studio-dist/assets/index-O26dKRjW.js +65 -0
- package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/studio-dist/favicon.svg +1 -0
- package/studio-dist/icons.svg +24 -0
- package/studio-dist/index.html +14 -0
- package/templates/skills/emberflow-basics/SKILL.md +316 -0
- package/templates/skills/emberflow-model-process/SKILL.md +209 -0
- package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
- package/templates/skills/emberflow-review-workflow/SKILL.md +148 -0
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExecutionRecord, LogLevel, LogLine, NodeRunState, SubflowResult,
|
|
3
|
+
WorkflowDefinition, WorkflowNode, WorkflowRun,
|
|
4
|
+
} from './types';
|
|
5
|
+
import type { NodeRegistry } from './registry';
|
|
6
|
+
import type { TraceSink } from './trace';
|
|
7
|
+
import { computeLoopRegions, topoSort, validateFlow } from './validation';
|
|
8
|
+
import type { LoopRegion } from './validation';
|
|
9
|
+
|
|
10
|
+
export function getByPath(obj: unknown, path: string): unknown {
|
|
11
|
+
if (path === '$') return obj;
|
|
12
|
+
return path.split('.').reduce<unknown>(
|
|
13
|
+
(acc, key) =>
|
|
14
|
+
acc !== null && typeof acc === 'object' ? (acc as Record<string, unknown>)[key] : undefined,
|
|
15
|
+
obj,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Matches the V8 shapes ("Cannot read properties of undefined/null (reading 'x')")
|
|
20
|
+
// and the older/Safari-style shape ("undefined is not an object (evaluating 'a.x')")
|
|
21
|
+
// for reading a property off undefined or null.
|
|
22
|
+
const UNDEFINED_READ_PATTERN =
|
|
23
|
+
/^(Cannot read properties of (?:undefined|null) \(reading '[^']*'\)|undefined is not an object \(evaluating '[^']*'\))$/;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Enriches a "reading property of undefined/null" TypeError message with the
|
|
27
|
+
* node's resolved input keys, so the recorded error points at what the node
|
|
28
|
+
* actually received instead of just the raw JS engine message. Every other
|
|
29
|
+
* message passes through byte-identical.
|
|
30
|
+
*/
|
|
31
|
+
export function explainUndefinedRead(message: string, input: Record<string, unknown>): string {
|
|
32
|
+
if (!UNDEFINED_READ_PATTERN.test(message)) return message;
|
|
33
|
+
// A field mapped in `inputMap` whose source produced nothing still appears as
|
|
34
|
+
// a key, holding `undefined` — and that is almost always the culprit here.
|
|
35
|
+
// Listing it alongside the fields that actually carry a value is what turns
|
|
36
|
+
// this from a raw JS message into a pointer at the broken mapping.
|
|
37
|
+
const present = Object.keys(input).filter((k) => input[k] !== undefined).sort();
|
|
38
|
+
const undef = Object.keys(input).filter((k) => input[k] === undefined).sort();
|
|
39
|
+
const got = present.length > 0 ? `[${present.join(', ')}]` : '(none)';
|
|
40
|
+
const missing =
|
|
41
|
+
undef.length > 0
|
|
42
|
+
? ` These mapped fields resolved to undefined: ${undef.join(', ')}.`
|
|
43
|
+
: '';
|
|
44
|
+
return `${message} — this node received ${got}.${missing} Check the node's inputMap and the run input.`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A `{"$env": "NAME"}` reference resolves to `vars["NAME"]`. */
|
|
48
|
+
function envRefName(value: unknown): string | undefined {
|
|
49
|
+
if (
|
|
50
|
+
value !== null &&
|
|
51
|
+
typeof value === 'object' &&
|
|
52
|
+
!Array.isArray(value) &&
|
|
53
|
+
typeof (value as Record<string, unknown>).$env === 'string'
|
|
54
|
+
) {
|
|
55
|
+
return (value as { $env: string }).$env;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveEnvRef(name: string, vars: Record<string, string>): string {
|
|
61
|
+
if (!(name in vars)) {
|
|
62
|
+
// The most common cause is not a missing key but a run with NO environment
|
|
63
|
+
// at all: the in-tab engine has no vars, so flows seeded with $env refs
|
|
64
|
+
// need the server runner. Say so, instead of a bare "missing".
|
|
65
|
+
const hint =
|
|
66
|
+
Object.keys(vars).length === 0
|
|
67
|
+
? ' — this run has no environment variables (in-tab/browser runs never do; make sure the runner is online and the engine is set to Server or Auto)'
|
|
68
|
+
: '';
|
|
69
|
+
throw new Error(`Missing environment variable: ${name}${hint}`);
|
|
70
|
+
}
|
|
71
|
+
return vars[name];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Recursively replaces every `{"$env": "NAME"}` reference in a value (objects
|
|
76
|
+
* and arrays walked) with its resolved var. A missing var throws
|
|
77
|
+
* `Missing environment variable: NAME`. Non-ref values pass through untouched.
|
|
78
|
+
*/
|
|
79
|
+
function resolveEnvDeep(value: unknown, vars: Record<string, string>): unknown {
|
|
80
|
+
const name = envRefName(value);
|
|
81
|
+
if (name !== undefined) return resolveEnvRef(name, vars);
|
|
82
|
+
if (Array.isArray(value)) return value.map((v) => resolveEnvDeep(v, vars));
|
|
83
|
+
if (value !== null && typeof value === 'object') {
|
|
84
|
+
const out: Record<string, unknown> = {};
|
|
85
|
+
for (const [key, v] of Object.entries(value as Record<string, unknown>)) {
|
|
86
|
+
out[key] = resolveEnvDeep(v, vars);
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Appends one ExecutionRecord to a loop-body (or ForEach) node's
|
|
95
|
+
* accumulated per-iteration history. Pure — callers must read the prior
|
|
96
|
+
* array themselves, from whatever point in the node's state lifecycle it is
|
|
97
|
+
* still intact (setNodeState replaces the state object wholesale on every
|
|
98
|
+
* call, so an intermediate 'running'/'skipped' write can otherwise wipe it
|
|
99
|
+
* out before the caller gets a chance to read it back).
|
|
100
|
+
*/
|
|
101
|
+
function appendExecution(
|
|
102
|
+
prior: ExecutionRecord[] | undefined,
|
|
103
|
+
record: ExecutionRecord,
|
|
104
|
+
): ExecutionRecord[] {
|
|
105
|
+
return [...(prior ?? []), record];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ExecutorEvents {
|
|
109
|
+
onNodeStateChange?: (nodeId: string, state: NodeRunState) => void;
|
|
110
|
+
onLog?: (line: LogLine) => void;
|
|
111
|
+
onRunFinished?: (run: WorkflowRun) => void;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Mutable state for the loop region currently being driven by the cursor. */
|
|
115
|
+
interface ActiveLoopState {
|
|
116
|
+
region: LoopRegion;
|
|
117
|
+
forEachNode: WorkflowNode;
|
|
118
|
+
collectNode: WorkflowNode;
|
|
119
|
+
/** Body node ids in region topo order (main topo order filtered to the region). */
|
|
120
|
+
bodyOrder: string[];
|
|
121
|
+
/** The full `items` array resolved once, at loop start. */
|
|
122
|
+
items: unknown[];
|
|
123
|
+
/** One entry per iteration: the element (batchSize 1) or the chunk array. */
|
|
124
|
+
chunks: unknown[];
|
|
125
|
+
/** Iteration currently in flight (or about to start once the body position resets). */
|
|
126
|
+
iterIndex: number;
|
|
127
|
+
/** Position within bodyOrder for the current iteration. */
|
|
128
|
+
bodyCursor: number;
|
|
129
|
+
/** Collect's mapped value, one per completed iteration. */
|
|
130
|
+
results: unknown[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface StartRunOptions {
|
|
134
|
+
flow: WorkflowDefinition;
|
|
135
|
+
registry: NodeRegistry;
|
|
136
|
+
trace?: TraceSink;
|
|
137
|
+
events?: ExecutorEvents;
|
|
138
|
+
now?: () => string;
|
|
139
|
+
newId?: () => string;
|
|
140
|
+
secrets?: Record<string, string>;
|
|
141
|
+
/** Non-secret environment values, exposed as ctx.vars and used to resolve {"$env": NAME}. */
|
|
142
|
+
vars?: Record<string, string>;
|
|
143
|
+
/** When true, mutation nodes dry-run their side effects; recorded on the run. */
|
|
144
|
+
safeMode?: boolean;
|
|
145
|
+
/** The environment name this run points at; recorded on the run. */
|
|
146
|
+
environment?: string;
|
|
147
|
+
/** nodeId -> pinned output. Pinned nodes skip execution and tracing. */
|
|
148
|
+
pins?: Record<string, unknown>;
|
|
149
|
+
/**
|
|
150
|
+
* When true, runs in Mock mode: nodes with a canned output in `mocks` return
|
|
151
|
+
* it verbatim without invoking their implementation or the retry loop; nodes
|
|
152
|
+
* that would touch infrastructure (`traceKind` 'db'/'http'/'llm') and have no
|
|
153
|
+
* mock fail loud (a normal node failure — respects `optional`, halts like any
|
|
154
|
+
* error). Pure/compute nodes (`traceKind` 'compute' or absent) always execute
|
|
155
|
+
* their real implementation, mocked or not. Defaults false; when false,
|
|
156
|
+
* `mocks` is ignored entirely (Live behavior is byte-identical to today).
|
|
157
|
+
*/
|
|
158
|
+
mockRun?: boolean;
|
|
159
|
+
/** nodeId -> canned output, consulted only when `mockRun` is true. See `mockRun`. */
|
|
160
|
+
mocks?: Record<string, unknown>;
|
|
161
|
+
/** The invocation payload, exposed to nodes as ctx.runInput. */
|
|
162
|
+
input?: Record<string, unknown>;
|
|
163
|
+
/**
|
|
164
|
+
* Host-provided runner for Subflow nodes: given a workflow id + input, runs
|
|
165
|
+
* that child flow to completion and returns its collected output. The
|
|
166
|
+
* executor injects the calling node's id (for log prefixing / cycle context);
|
|
167
|
+
* the host owns workflow lookup and the recursion/cycle guard. Absent when
|
|
168
|
+
* the host can't resolve other workflows.
|
|
169
|
+
*/
|
|
170
|
+
subflowRunner?: (
|
|
171
|
+
workflowId: string,
|
|
172
|
+
input: Record<string, unknown>,
|
|
173
|
+
callerNodeId: string,
|
|
174
|
+
) => Promise<SubflowResult>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export class FlowRun {
|
|
178
|
+
readonly run: WorkflowRun;
|
|
179
|
+
private readonly flow: WorkflowDefinition;
|
|
180
|
+
private readonly registry: NodeRegistry;
|
|
181
|
+
private readonly trace?: TraceSink;
|
|
182
|
+
private readonly events: ExecutorEvents;
|
|
183
|
+
private readonly now: () => string;
|
|
184
|
+
private readonly newId: () => string;
|
|
185
|
+
private readonly secrets: Record<string, string>;
|
|
186
|
+
private readonly vars: Record<string, string>;
|
|
187
|
+
private readonly environment?: string;
|
|
188
|
+
private readonly safeMode: boolean;
|
|
189
|
+
private readonly runInput: Record<string, unknown>;
|
|
190
|
+
private readonly pins: Record<string, unknown>;
|
|
191
|
+
private readonly mockRun: boolean;
|
|
192
|
+
private readonly mocks: Record<string, unknown>;
|
|
193
|
+
private readonly subflowRunner?: StartRunOptions['subflowRunner'];
|
|
194
|
+
private readonly order: string[];
|
|
195
|
+
private readonly outputs = new Map<string, unknown>();
|
|
196
|
+
private readonly skippedNodes = new Set<string>();
|
|
197
|
+
private readonly branchTaken = new Map<string, string>();
|
|
198
|
+
private readonly regionByForEach: Map<string, LoopRegion>;
|
|
199
|
+
/** Nodes a completed loop region already executed; the main cursor consumes them silently. */
|
|
200
|
+
private readonly loopExecuted = new Set<string>();
|
|
201
|
+
private activeLoop?: ActiveLoopState;
|
|
202
|
+
private cursor = 0;
|
|
203
|
+
/** Run-wide execution counter — numbers every node execution in the log. */
|
|
204
|
+
private execSeq = 0;
|
|
205
|
+
|
|
206
|
+
constructor(opts: StartRunOptions) {
|
|
207
|
+
this.flow = opts.flow;
|
|
208
|
+
this.registry = opts.registry;
|
|
209
|
+
this.trace = opts.trace;
|
|
210
|
+
this.events = opts.events ?? {};
|
|
211
|
+
this.now = opts.now ?? (() => new Date().toISOString());
|
|
212
|
+
this.newId = opts.newId ?? (() => crypto.randomUUID());
|
|
213
|
+
this.secrets = opts.secrets ?? {};
|
|
214
|
+
this.vars = opts.vars ?? {};
|
|
215
|
+
this.environment = opts.environment;
|
|
216
|
+
this.safeMode = opts.safeMode ?? false;
|
|
217
|
+
// Resolve {"$env": NAME} anywhere in the run-input payload up front, so
|
|
218
|
+
// nodes read concrete values from ctx.runInput and a missing var fails the
|
|
219
|
+
// run at start rather than deep inside a node.
|
|
220
|
+
this.runInput = resolveEnvDeep(opts.input ?? {}, this.vars) as Record<string, unknown>;
|
|
221
|
+
this.pins = opts.pins ?? {};
|
|
222
|
+
this.mockRun = opts.mockRun ?? false;
|
|
223
|
+
this.mocks = opts.mocks ?? {};
|
|
224
|
+
this.subflowRunner = opts.subflowRunner;
|
|
225
|
+
this.order = topoSort(this.flow);
|
|
226
|
+
this.regionByForEach = new Map(computeLoopRegions(this.flow).map((r) => [r.forEachId, r]));
|
|
227
|
+
this.run = {
|
|
228
|
+
id: this.newId(),
|
|
229
|
+
workflowId: this.flow.id,
|
|
230
|
+
status: 'running',
|
|
231
|
+
startedAt: this.now(),
|
|
232
|
+
...(opts.environment !== undefined ? { environment: opts.environment } : {}),
|
|
233
|
+
...(opts.safeMode !== undefined ? { safeMode: opts.safeMode } : {}),
|
|
234
|
+
nodeStates: {},
|
|
235
|
+
};
|
|
236
|
+
// No events from the constructor: subscribers' closures may not be
|
|
237
|
+
// initialized yet. Initial states are readable via handle.run.
|
|
238
|
+
for (const node of this.flow.nodes) {
|
|
239
|
+
this.run.nodeStates[node.id] = { status: 'queued' };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async step(): Promise<boolean> {
|
|
244
|
+
if (this.run.status !== 'running' || this.cursor >= this.order.length) return false;
|
|
245
|
+
|
|
246
|
+
if (this.activeLoop) return this.stepLoop();
|
|
247
|
+
|
|
248
|
+
// Consume nodes a finished loop region already executed (topo order may
|
|
249
|
+
// interleave them with parallel non-region nodes), and nodes whose every
|
|
250
|
+
// incoming edge is dead (untaken route branch or skipped ancestor) —
|
|
251
|
+
// the latter are marked skipped, not executed.
|
|
252
|
+
while (this.cursor < this.order.length) {
|
|
253
|
+
const id = this.order[this.cursor];
|
|
254
|
+
if (this.loopExecuted.has(id)) {
|
|
255
|
+
this.cursor += 1;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (!this.isReachable(id)) {
|
|
259
|
+
this.cursor += 1;
|
|
260
|
+
this.skippedNodes.add(id);
|
|
261
|
+
this.setNodeState(id, { status: 'skipped' });
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
if (this.cursor >= this.order.length) {
|
|
267
|
+
this.finish('succeeded');
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const nodeId = this.order[this.cursor];
|
|
272
|
+
this.cursor += 1;
|
|
273
|
+
|
|
274
|
+
// A reachable ForEach hands execution to the loop machinery, which owns
|
|
275
|
+
// the whole region (ForEach + body + Collect) until the Collect
|
|
276
|
+
// completes; the main cursor then resumes right after it.
|
|
277
|
+
const region = this.regionByForEach.get(nodeId);
|
|
278
|
+
if (region) return this.startLoop(region);
|
|
279
|
+
|
|
280
|
+
const ok = await this.executeNode(nodeId);
|
|
281
|
+
if (!ok) return false;
|
|
282
|
+
return this.epilogue();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Runs one node's implementation (or reuses its pin) end to end: resolve
|
|
287
|
+
* config/input, execute, record state + trace, handle failure. Shared by
|
|
288
|
+
* the main cursor and by loop-body execution, which additionally tags the
|
|
289
|
+
* resulting NodeRunState with the current iteration.
|
|
290
|
+
*/
|
|
291
|
+
private async executeNode(
|
|
292
|
+
nodeId: string,
|
|
293
|
+
iteration?: { index: number; total: number },
|
|
294
|
+
): Promise<boolean> {
|
|
295
|
+
const node = this.flow.nodes.find((n) => n.id === nodeId)!;
|
|
296
|
+
const iterationState = iteration ? { iteration } : {};
|
|
297
|
+
// Ordered execution receipt: every execution gets a run-wide sequence
|
|
298
|
+
// number, so the log is proof of exactly what ran, in what order — and
|
|
299
|
+
// that non-loop nodes run once (only loop iterations repeat a label).
|
|
300
|
+
this.execSeq += 1;
|
|
301
|
+
this.emitLog(
|
|
302
|
+
'debug',
|
|
303
|
+
`#${this.execSeq} ▶ execute${iteration ? ` (iteration ${iteration.index + 1}/${iteration.total})` : ''}`,
|
|
304
|
+
node,
|
|
305
|
+
);
|
|
306
|
+
// Captured up front: the 'running' state set below replaces the node's
|
|
307
|
+
// state object wholesale, which would otherwise wipe out `executions`
|
|
308
|
+
// before the completion branch gets a chance to read it back out.
|
|
309
|
+
const priorExecutions = iteration ? this.run.nodeStates[nodeId]?.executions : undefined;
|
|
310
|
+
|
|
311
|
+
if (Object.prototype.hasOwnProperty.call(this.pins, nodeId)) {
|
|
312
|
+
const output = this.pins[nodeId];
|
|
313
|
+
const at = this.now();
|
|
314
|
+
this.outputs.set(nodeId, output);
|
|
315
|
+
this.captureBranch(nodeId, output);
|
|
316
|
+
const executions = iteration
|
|
317
|
+
? appendExecution(priorExecutions, { iteration, output, status: 'succeeded' })
|
|
318
|
+
: undefined;
|
|
319
|
+
this.setNodeState(nodeId, {
|
|
320
|
+
status: 'succeeded', output, pinned: true, startedAt: at, completedAt: at, ...iterationState,
|
|
321
|
+
...(executions ? { executions } : {}),
|
|
322
|
+
});
|
|
323
|
+
this.emitLog('info', `${node.label}: using pinned output`, node);
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const { definition, implementation } = this.registry.get(node.type);
|
|
328
|
+
const startedAt = this.now();
|
|
329
|
+
let input: Record<string, unknown> = {};
|
|
330
|
+
// Hoisted above the try so both the success path and the shared catch
|
|
331
|
+
// below can read the final attempt count. Stays at 1 (so `attempts` is
|
|
332
|
+
// never stamped) unless the retry loop actually runs more than once —
|
|
333
|
+
// config/input-resolution failures above the loop never touch it.
|
|
334
|
+
let attempt = 1;
|
|
335
|
+
|
|
336
|
+
// In a mock run, a mocked node's canned output and an infra-no-mock
|
|
337
|
+
// failure both apply BEFORE config resolution: the node's real
|
|
338
|
+
// implementation never runs either way, so a config value it would
|
|
339
|
+
// otherwise need (e.g. a `$secret` ref with no secret configured) must
|
|
340
|
+
// never fail the node first. Only the inputMap-derived fields (other
|
|
341
|
+
// nodes' outputs — never secrets) are resolved for `state.input` here;
|
|
342
|
+
// config-sourced input fields are skipped (lenient), since resolving them
|
|
343
|
+
// is exactly the thing that could throw.
|
|
344
|
+
const isMocked = this.mockRun && Object.prototype.hasOwnProperty.call(this.mocks, nodeId);
|
|
345
|
+
const isInfraNoMock =
|
|
346
|
+
this.mockRun &&
|
|
347
|
+
!isMocked &&
|
|
348
|
+
(definition.traceKind === 'db' || definition.traceKind === 'http' || definition.traceKind === 'llm');
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
if (isMocked) {
|
|
352
|
+
// Canned output: short-circuits before config resolution and the
|
|
353
|
+
// retry loop — the implementation is never invoked, so no retry
|
|
354
|
+
// machinery applies and a `$secret` config ref never throws.
|
|
355
|
+
input = this.resolveInputLenient(node);
|
|
356
|
+
this.setNodeState(nodeId, { status: 'running', input, startedAt, ...iterationState });
|
|
357
|
+
const output = this.mocks[nodeId];
|
|
358
|
+
const completedAt = this.now();
|
|
359
|
+
this.outputs.set(nodeId, output);
|
|
360
|
+
this.captureBranch(nodeId, output);
|
|
361
|
+
const executions = iteration
|
|
362
|
+
? appendExecution(priorExecutions, { iteration, input, output, status: 'succeeded' })
|
|
363
|
+
: undefined;
|
|
364
|
+
this.setNodeState(nodeId, {
|
|
365
|
+
status: 'succeeded', input, output, startedAt, completedAt, mocked: true, ...iterationState,
|
|
366
|
+
...(executions ? { executions } : {}),
|
|
367
|
+
});
|
|
368
|
+
this.recordSample(node, input, startedAt, completedAt, 'succeeded', output);
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
if (isInfraNoMock) {
|
|
372
|
+
// Fail-loud infrastructure boundary: thrown before config resolution
|
|
373
|
+
// (a `$secret` ref must not out-race this with "Missing secret"), as
|
|
374
|
+
// a normal implementation error (caught below), so `optional`
|
|
375
|
+
// fail-soft, halting semantics, and the error-workflow path all apply
|
|
376
|
+
// unchanged.
|
|
377
|
+
input = this.resolveInputLenient(node);
|
|
378
|
+
this.setNodeState(nodeId, { status: 'running', input, startedAt, ...iterationState });
|
|
379
|
+
throw new Error(
|
|
380
|
+
`"${node.label}" would touch real infrastructure (${definition.traceKind}) and has no mock — ` +
|
|
381
|
+
`in Mock, infrastructure nodes need a canned output. Add one under this scenario's mocks ` +
|
|
382
|
+
`(Cover with AI writes them), or go Live to run it for real.`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const config = this.resolveConfig(node);
|
|
387
|
+
input = this.resolveInput(node, config);
|
|
388
|
+
this.setNodeState(nodeId, { status: 'running', input, startedAt, ...iterationState });
|
|
389
|
+
|
|
390
|
+
const rawRetry = node.retry;
|
|
391
|
+
const maxTries =
|
|
392
|
+
rawRetry && typeof rawRetry === 'object' && Number.isFinite(rawRetry.maxTries) && rawRetry.maxTries >= 1
|
|
393
|
+
? rawRetry.maxTries
|
|
394
|
+
: 1;
|
|
395
|
+
const waitMs = rawRetry?.waitMs ?? 0;
|
|
396
|
+
const callImplementation = () =>
|
|
397
|
+
implementation({
|
|
398
|
+
input,
|
|
399
|
+
config,
|
|
400
|
+
secrets: this.secrets,
|
|
401
|
+
vars: this.vars,
|
|
402
|
+
environment: this.environment,
|
|
403
|
+
safeMode: this.safeMode,
|
|
404
|
+
runInput: this.runInput,
|
|
405
|
+
log: (level: LogLevel, message: string) => this.emitLog(level, message, node),
|
|
406
|
+
// Inject the calling node's id so the host can prefix child logs and
|
|
407
|
+
// report cycle context; the node's ctx.runSubflow stays (id, input).
|
|
408
|
+
...(this.subflowRunner
|
|
409
|
+
? {
|
|
410
|
+
runSubflow: (workflowId: string, subInput: Record<string, unknown>) =>
|
|
411
|
+
this.subflowRunner!(workflowId, subInput, node.id),
|
|
412
|
+
}
|
|
413
|
+
: {}),
|
|
414
|
+
});
|
|
415
|
+
let output: unknown;
|
|
416
|
+
for (attempt = 1; ; attempt++) {
|
|
417
|
+
try {
|
|
418
|
+
output = await callImplementation();
|
|
419
|
+
break;
|
|
420
|
+
} catch (err) {
|
|
421
|
+
if (attempt >= maxTries) throw err;
|
|
422
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
423
|
+
this.emitLog('warn', `retry ${attempt}/${maxTries} after error: ${msg}`, node);
|
|
424
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const completedAt = this.now();
|
|
428
|
+
this.outputs.set(nodeId, output);
|
|
429
|
+
this.captureBranch(nodeId, output);
|
|
430
|
+
const executions = iteration
|
|
431
|
+
? appendExecution(priorExecutions, { iteration, input, output, status: 'succeeded' })
|
|
432
|
+
: undefined;
|
|
433
|
+
const mutationBlocked = this.safeMode && definition.effects === 'mutation';
|
|
434
|
+
this.setNodeState(nodeId, {
|
|
435
|
+
status: 'succeeded', input, output, startedAt, completedAt, ...iterationState,
|
|
436
|
+
...(mutationBlocked ? { mutationBlocked: true } : {}),
|
|
437
|
+
...(executions ? { executions } : {}),
|
|
438
|
+
...(attempt > 1 ? { attempts: attempt } : {}),
|
|
439
|
+
});
|
|
440
|
+
this.recordSample(node, input, startedAt, completedAt, 'succeeded', output);
|
|
441
|
+
return true;
|
|
442
|
+
} catch (err) {
|
|
443
|
+
const completedAt = this.now();
|
|
444
|
+
const rawMessage = err instanceof Error ? err.message : String(err);
|
|
445
|
+
const error =
|
|
446
|
+
err instanceof TypeError ? explainUndefinedRead(rawMessage, input) : rawMessage;
|
|
447
|
+
const executions = iteration
|
|
448
|
+
? appendExecution(priorExecutions, { iteration, input, error, status: 'failed' })
|
|
449
|
+
: undefined;
|
|
450
|
+
this.setNodeState(nodeId, {
|
|
451
|
+
status: 'failed', input, error, startedAt, completedAt, ...iterationState,
|
|
452
|
+
...(executions ? { executions } : {}),
|
|
453
|
+
...(attempt > 1 ? { attempts: attempt } : {}),
|
|
454
|
+
});
|
|
455
|
+
this.recordSample(node, input, startedAt, completedAt, 'failed');
|
|
456
|
+
this.emitLog('error', error, node);
|
|
457
|
+
// Fail-soft: an `optional` node's failure is recorded (state stays
|
|
458
|
+
// 'failed', visible in the runbook) but does not abort the run. It set no
|
|
459
|
+
// output, so its downstream edges are dead (isReachable) and dependent
|
|
460
|
+
// nodes skip, while independent branches keep running. The cursor was
|
|
461
|
+
// already advanced past this node (step()), so returning true resumes at
|
|
462
|
+
// the next node instead of finishing the run.
|
|
463
|
+
if (node.optional) {
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
this.finish('failed');
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ── ForEach/Collect loop region execution ───────────────────────────
|
|
472
|
+
//
|
|
473
|
+
// A region is driven across many step() calls, one node-execution each:
|
|
474
|
+
// the ForEach "executes" once per iteration (producing {item, index,
|
|
475
|
+
// total}), then each body node runs once per iteration in region topo
|
|
476
|
+
// order (with branch-skip state reset per iteration), then once all
|
|
477
|
+
// iterations are done the Collect executes exactly once. The main cursor
|
|
478
|
+
// is parked at the position right after the Collect for the duration.
|
|
479
|
+
|
|
480
|
+
/** First step on a reachable ForEach: resolve items, chunk, and start (or short-circuit) the region. */
|
|
481
|
+
private async startLoop(region: LoopRegion): Promise<boolean> {
|
|
482
|
+
const forEachNode = this.flow.nodes.find((n) => n.id === region.forEachId)!;
|
|
483
|
+
const collectNode = this.flow.nodes.find((n) => n.id === region.collectId)!;
|
|
484
|
+
const bodySet = new Set(region.bodyIds);
|
|
485
|
+
const bodyOrder = this.order.filter((id) => bodySet.has(id));
|
|
486
|
+
|
|
487
|
+
const startedAt = this.now();
|
|
488
|
+
let input: Record<string, unknown> = {};
|
|
489
|
+
try {
|
|
490
|
+
const config = this.resolveConfig(forEachNode);
|
|
491
|
+
input = this.resolveInput(forEachNode, config);
|
|
492
|
+
const items = input.items;
|
|
493
|
+
if (!Array.isArray(items)) {
|
|
494
|
+
throw new Error(`ForEach "${forEachNode.label}": "items" input is not an array`);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const rawBatchSize = Number(config.batchSize);
|
|
498
|
+
const batchSize = Number.isFinite(rawBatchSize) && rawBatchSize > 0 ? Math.floor(rawBatchSize) : 1;
|
|
499
|
+
let chunks: unknown[] = [];
|
|
500
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
501
|
+
chunks.push(batchSize === 1 ? items[i] : items.slice(i, i + batchSize));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const rawMax = config.maxIterations;
|
|
505
|
+
const maxIterations =
|
|
506
|
+
rawMax === undefined || rawMax === null || rawMax === '' ? undefined : Number(rawMax);
|
|
507
|
+
if (maxIterations !== undefined && Number.isFinite(maxIterations) && chunks.length > maxIterations) {
|
|
508
|
+
const truncated = chunks.length - maxIterations;
|
|
509
|
+
this.emitLog(
|
|
510
|
+
'warn',
|
|
511
|
+
`maxIterations ${maxIterations} reached, truncating ${truncated} iterations`,
|
|
512
|
+
forEachNode,
|
|
513
|
+
);
|
|
514
|
+
chunks = chunks.slice(0, maxIterations);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
this.activeLoop = {
|
|
518
|
+
region, forEachNode, collectNode, bodyOrder,
|
|
519
|
+
items, chunks, iterIndex: 0, bodyCursor: 0, results: [],
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
if (chunks.length === 0) {
|
|
523
|
+
const completedAt = this.now();
|
|
524
|
+
const output = { item: undefined, index: 0, total: 0 };
|
|
525
|
+
this.outputs.set(forEachNode.id, output);
|
|
526
|
+
this.setNodeState(forEachNode.id, {
|
|
527
|
+
status: 'succeeded', input, output, startedAt, completedAt, iteration: { index: 0, total: 0 },
|
|
528
|
+
});
|
|
529
|
+
this.recordSample(forEachNode, output, startedAt, completedAt, 'succeeded', output);
|
|
530
|
+
for (const id of bodyOrder) {
|
|
531
|
+
this.skippedNodes.add(id);
|
|
532
|
+
this.setNodeState(id, { status: 'skipped', iteration: { index: 0, total: 0 } });
|
|
533
|
+
}
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
this.beginIteration(this.activeLoop);
|
|
538
|
+
return true;
|
|
539
|
+
} catch (err) {
|
|
540
|
+
const completedAt = this.now();
|
|
541
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
542
|
+
this.setNodeState(forEachNode.id, { status: 'failed', input, error, startedAt, completedAt });
|
|
543
|
+
this.recordSample(forEachNode, input, startedAt, completedAt, 'failed');
|
|
544
|
+
this.emitLog('error', error, forEachNode);
|
|
545
|
+
this.finish('failed');
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Sets the ForEach's per-iteration output and resets body branch-skip
|
|
552
|
+
* state. Also clears each body id's prior-iteration output: without this,
|
|
553
|
+
* a body node that a branch skips this iteration would still resolve
|
|
554
|
+
* downstream inputMap reads to whatever it produced last iteration, since
|
|
555
|
+
* `outputs` is otherwise only ever overwritten (never cleared) by execution.
|
|
556
|
+
*/
|
|
557
|
+
private beginIteration(loop: ActiveLoopState): void {
|
|
558
|
+
const total = loop.chunks.length;
|
|
559
|
+
const chunk = loop.chunks[loop.iterIndex];
|
|
560
|
+
const output = { item: chunk, index: loop.iterIndex, total };
|
|
561
|
+
const at = this.now();
|
|
562
|
+
const iteration = { index: loop.iterIndex, total };
|
|
563
|
+
this.outputs.set(loop.forEachNode.id, output);
|
|
564
|
+
const executions = appendExecution(
|
|
565
|
+
this.run.nodeStates[loop.forEachNode.id]?.executions,
|
|
566
|
+
{ iteration, output, status: 'succeeded' },
|
|
567
|
+
);
|
|
568
|
+
this.setNodeState(loop.forEachNode.id, {
|
|
569
|
+
status: 'succeeded',
|
|
570
|
+
input: { items: loop.items },
|
|
571
|
+
output,
|
|
572
|
+
startedAt: at,
|
|
573
|
+
completedAt: at,
|
|
574
|
+
iteration,
|
|
575
|
+
executions,
|
|
576
|
+
});
|
|
577
|
+
this.recordSample(loop.forEachNode, chunk, at, at, 'succeeded', output);
|
|
578
|
+
|
|
579
|
+
for (const id of loop.bodyOrder) {
|
|
580
|
+
this.skippedNodes.delete(id);
|
|
581
|
+
this.branchTaken.delete(id);
|
|
582
|
+
this.outputs.delete(id);
|
|
583
|
+
}
|
|
584
|
+
loop.bodyCursor = 0;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** One step of an in-progress region: a body node, an iteration advance, or the final Collect. */
|
|
588
|
+
private async stepLoop(): Promise<boolean> {
|
|
589
|
+
const loop = this.activeLoop!;
|
|
590
|
+
|
|
591
|
+
if (loop.chunks.length === 0) {
|
|
592
|
+
this.finishLoop(loop);
|
|
593
|
+
return this.epilogue();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
while (
|
|
597
|
+
loop.bodyCursor < loop.bodyOrder.length &&
|
|
598
|
+
!this.isReachable(loop.bodyOrder[loop.bodyCursor])
|
|
599
|
+
) {
|
|
600
|
+
const deadId = loop.bodyOrder[loop.bodyCursor];
|
|
601
|
+
loop.bodyCursor += 1;
|
|
602
|
+
this.skippedNodes.add(deadId);
|
|
603
|
+
// A skip is a status, not an execution: no ExecutionRecord is appended
|
|
604
|
+
// for this iteration. But setNodeState replaces the state object
|
|
605
|
+
// wholesale, so any executions accumulated in earlier iterations (this
|
|
606
|
+
// branch may have been taken before, or will be again) must be carried
|
|
607
|
+
// forward explicitly or they'd be lost.
|
|
608
|
+
const priorExecutions = this.run.nodeStates[deadId]?.executions;
|
|
609
|
+
this.setNodeState(deadId, {
|
|
610
|
+
status: 'skipped',
|
|
611
|
+
iteration: { index: loop.iterIndex, total: loop.chunks.length },
|
|
612
|
+
...(priorExecutions ? { executions: priorExecutions } : {}),
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (loop.bodyCursor < loop.bodyOrder.length) {
|
|
617
|
+
const nodeId = loop.bodyOrder[loop.bodyCursor];
|
|
618
|
+
loop.bodyCursor += 1;
|
|
619
|
+
const ok = await this.executeNode(nodeId, { index: loop.iterIndex, total: loop.chunks.length });
|
|
620
|
+
return ok;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// This iteration's body is exhausted: gather Collect's mapped value and advance.
|
|
624
|
+
loop.results.push(this.resolveCollectValue(loop.collectNode));
|
|
625
|
+
loop.iterIndex += 1;
|
|
626
|
+
|
|
627
|
+
if (loop.iterIndex < loop.chunks.length) {
|
|
628
|
+
this.beginIteration(loop);
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
this.finishLoop(loop);
|
|
633
|
+
return this.epilogue();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Resolves Collect's `value` input against the current iteration's body outputs. */
|
|
637
|
+
private resolveCollectValue(collectNode: WorkflowNode): unknown {
|
|
638
|
+
const config = this.resolveConfig(collectNode);
|
|
639
|
+
const input = this.resolveInput(collectNode, config);
|
|
640
|
+
return input.value;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Executes Collect once with the accumulated results and hands the region's
|
|
645
|
+
* nodes to `loopExecuted` so the main cursor consumes them silently — it
|
|
646
|
+
* must NOT jump past the Collect, because topo order can interleave
|
|
647
|
+
* parallel non-region nodes between the ForEach and the Collect.
|
|
648
|
+
*/
|
|
649
|
+
private finishLoop(loop: ActiveLoopState): void {
|
|
650
|
+
const at = this.now();
|
|
651
|
+
const output = { items: loop.results, count: loop.results.length };
|
|
652
|
+
const lastValue = loop.results.length > 0 ? loop.results[loop.results.length - 1] : undefined;
|
|
653
|
+
this.outputs.set(loop.collectNode.id, output);
|
|
654
|
+
this.setNodeState(loop.collectNode.id, {
|
|
655
|
+
status: 'succeeded', input: { value: lastValue }, output, startedAt: at, completedAt: at,
|
|
656
|
+
});
|
|
657
|
+
this.recordSample(loop.collectNode, { value: lastValue }, at, at, 'succeeded', output);
|
|
658
|
+
for (const id of [...loop.region.bodyIds, loop.region.collectId]) {
|
|
659
|
+
this.loopExecuted.add(id);
|
|
660
|
+
}
|
|
661
|
+
this.activeLoop = undefined;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** Remember a node's taken branch when its output carries `$branch`. */
|
|
665
|
+
private captureBranch(nodeId: string, output: unknown): void {
|
|
666
|
+
if (output !== null && typeof output === 'object') {
|
|
667
|
+
const branch = (output as Record<string, unknown>).$branch;
|
|
668
|
+
if (typeof branch === 'string') this.branchTaken.set(nodeId, branch);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* A node is reachable if it has no incoming edges, or at least one incoming
|
|
674
|
+
* edge from a non-skipped, executed source whose branch (if the edge names
|
|
675
|
+
* one via sourceHandle) was taken.
|
|
676
|
+
*/
|
|
677
|
+
private isReachable(nodeId: string): boolean {
|
|
678
|
+
const incoming = this.flow.edges.filter((e) => e.target === nodeId);
|
|
679
|
+
if (incoming.length === 0) return true;
|
|
680
|
+
const live = (edge: (typeof incoming)[number]): boolean => {
|
|
681
|
+
if (this.skippedNodes.has(edge.source)) return false;
|
|
682
|
+
if (!this.outputs.has(edge.source)) return false;
|
|
683
|
+
if (!edge.sourceHandle) return true;
|
|
684
|
+
return this.branchTaken.get(edge.source) === edge.sourceHandle;
|
|
685
|
+
};
|
|
686
|
+
// Branch edges are guards: when a node sits on named branches, only those
|
|
687
|
+
// edges decide reachability — a plain data edge from an always-executed
|
|
688
|
+
// ancestor must not smuggle the node past a dead gate. Nodes with no
|
|
689
|
+
// branch edges keep join semantics (any live data edge suffices).
|
|
690
|
+
const branchEdges = incoming.filter((e) => e.sourceHandle);
|
|
691
|
+
if (branchEdges.length > 0) return branchEdges.some(live);
|
|
692
|
+
return incoming.some(live);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private epilogue(): boolean {
|
|
696
|
+
if (this.cursor >= this.order.length) {
|
|
697
|
+
this.finish('succeeded');
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async runToEnd(): Promise<WorkflowRun> {
|
|
704
|
+
while (await this.step()) {
|
|
705
|
+
// step until done
|
|
706
|
+
}
|
|
707
|
+
return this.run;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
cancel(): void {
|
|
711
|
+
if (this.run.status !== 'running') return;
|
|
712
|
+
this.finish('cancelled');
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Resolve {"$secret": NAME} and {"$env": NAME} config values; a missing
|
|
717
|
+
* secret/var fails the node. `$secret` is only recognized at a config
|
|
718
|
+
* value's top level (matching today's behavior — secrets are never nested
|
|
719
|
+
* inside larger config structures). `$env` is resolved deeply, the same
|
|
720
|
+
* walk used for the run-input payload, so refs nested in objects/arrays
|
|
721
|
+
* (e.g. an Input node's `config.defaults`) resolve too.
|
|
722
|
+
*/
|
|
723
|
+
private resolveConfig(node: WorkflowNode): Record<string, unknown> {
|
|
724
|
+
const config: Record<string, unknown> = {};
|
|
725
|
+
for (const [key, value] of Object.entries(node.config)) {
|
|
726
|
+
if (
|
|
727
|
+
value !== null &&
|
|
728
|
+
typeof value === 'object' &&
|
|
729
|
+
!Array.isArray(value) &&
|
|
730
|
+
typeof (value as Record<string, unknown>).$secret === 'string'
|
|
731
|
+
) {
|
|
732
|
+
const name = (value as { $secret: string }).$secret;
|
|
733
|
+
if (!(name in this.secrets)) throw new Error(`Missing secret: ${name}`);
|
|
734
|
+
config[key] = this.secrets[name];
|
|
735
|
+
} else {
|
|
736
|
+
config[key] = resolveEnvDeep(value, this.vars);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return config;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
private resolveInput(
|
|
743
|
+
node: WorkflowNode,
|
|
744
|
+
config: Record<string, unknown>,
|
|
745
|
+
): Record<string, unknown> {
|
|
746
|
+
const input: Record<string, unknown> = {};
|
|
747
|
+
const inputSchema = this.registry.get(node.type).definition.inputSchema;
|
|
748
|
+
for (const field of inputSchema?.fields ?? []) {
|
|
749
|
+
if (config[field.name] !== undefined) input[field.name] = config[field.name];
|
|
750
|
+
}
|
|
751
|
+
for (const [field, mapping] of Object.entries(node.inputMap ?? {})) {
|
|
752
|
+
input[field] = getByPath(this.outputs.get(mapping.sourceNodeId), mapping.sourceField);
|
|
753
|
+
}
|
|
754
|
+
return input;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* A lenient variant of `resolveInput` for nodes whose implementation never
|
|
759
|
+
* runs (mocked, or an infra node with no mock, in a mock run): resolves
|
|
760
|
+
* only `inputMap`-sourced fields (other nodes' already-computed outputs,
|
|
761
|
+
* which can never carry a `$secret`/`$env` ref). Skips the config-sourced
|
|
762
|
+
* fields `resolveInput` also merges in — those come from `resolveConfig`,
|
|
763
|
+
* which this path deliberately never calls, since that is exactly what
|
|
764
|
+
* could throw (e.g. `Missing secret`) for a node this run will never
|
|
765
|
+
* actually execute.
|
|
766
|
+
*/
|
|
767
|
+
private resolveInputLenient(node: WorkflowNode): Record<string, unknown> {
|
|
768
|
+
const input: Record<string, unknown> = {};
|
|
769
|
+
for (const [field, mapping] of Object.entries(node.inputMap ?? {})) {
|
|
770
|
+
input[field] = getByPath(this.outputs.get(mapping.sourceNodeId), mapping.sourceField);
|
|
771
|
+
}
|
|
772
|
+
return input;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
private finish(status: WorkflowRun['status']): void {
|
|
776
|
+
// Only nodes that never got a chance to run are swept to 'skipped' here.
|
|
777
|
+
// While a loop region is active, `this.cursor` stays parked at the
|
|
778
|
+
// region's entry for the whole loop lifetime (body/collect execution is
|
|
779
|
+
// driven by the loop's own bodyCursor, not the main cursor), so this
|
|
780
|
+
// range can include body nodes that already reached a terminal state in
|
|
781
|
+
// an earlier iteration — or, on failure, the very node whose 'failed'
|
|
782
|
+
// state we must not clobber back to 'skipped'.
|
|
783
|
+
for (; this.cursor < this.order.length; this.cursor += 1) {
|
|
784
|
+
const id = this.order[this.cursor];
|
|
785
|
+
if (this.run.nodeStates[id]?.status === 'queued') {
|
|
786
|
+
this.setNodeState(id, { status: 'skipped' });
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
this.run.status = status;
|
|
790
|
+
this.run.completedAt = this.now();
|
|
791
|
+
this.events.onRunFinished?.(this.run);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
private setNodeState(nodeId: string, state: NodeRunState): void {
|
|
795
|
+
this.run.nodeStates[nodeId] = state;
|
|
796
|
+
this.events.onNodeStateChange?.(nodeId, state);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private emitLog(level: LogLevel, message: string, node: WorkflowNode): void {
|
|
800
|
+
this.events.onLog?.({
|
|
801
|
+
timestamp: this.now(),
|
|
802
|
+
level,
|
|
803
|
+
runId: this.run.id,
|
|
804
|
+
nodeId: node.id,
|
|
805
|
+
nodeLabel: node.label,
|
|
806
|
+
message,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
private recordSample(
|
|
811
|
+
node: WorkflowNode,
|
|
812
|
+
input: unknown,
|
|
813
|
+
startedAt: string,
|
|
814
|
+
completedAt: string,
|
|
815
|
+
status: 'succeeded' | 'failed',
|
|
816
|
+
output?: unknown,
|
|
817
|
+
): void {
|
|
818
|
+
this.trace?.record({
|
|
819
|
+
id: this.newId(),
|
|
820
|
+
workflowId: this.flow.id,
|
|
821
|
+
runId: this.run.id,
|
|
822
|
+
nodeId: node.id,
|
|
823
|
+
nodeType: node.type,
|
|
824
|
+
nodeLabel: node.label,
|
|
825
|
+
input,
|
|
826
|
+
output,
|
|
827
|
+
status,
|
|
828
|
+
startedAt,
|
|
829
|
+
completedAt,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
export function startRun(opts: StartRunOptions): FlowRun {
|
|
835
|
+
const errors = validateFlow(opts.flow, opts.registry).filter((i) => i.severity === 'error');
|
|
836
|
+
if (errors.length > 0) {
|
|
837
|
+
throw new Error(`Invalid flow: ${errors.map((i) => i.message).join('; ')}`);
|
|
838
|
+
}
|
|
839
|
+
return new FlowRun(opts);
|
|
840
|
+
}
|