@bastani/atomic 0.8.28-alpha.1 → 0.8.28-alpha.3
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/CHANGELOG.md +60 -0
- package/README.md +120 -118
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +26 -0
- package/dist/builtin/workflows/README.md +1 -1
- package/dist/builtin/workflows/builtin/open-claude-design.ts +150 -13
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/authoring.d.ts +5 -2
- package/dist/builtin/workflows/src/extension/dispatcher.ts +2 -0
- package/dist/builtin/workflows/src/extension/index.ts +8 -0
- package/dist/builtin/workflows/src/extension/render-result.ts +5 -2
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +18 -0
- package/dist/builtin/workflows/src/runs/background/status.ts +4 -0
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +1251 -110
- package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +34 -10
- package/dist/builtin/workflows/src/shared/expanded-workflow-graph.ts +10 -2
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +28 -9
- package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +9 -3
- package/dist/builtin/workflows/src/shared/store-types.ts +10 -3
- package/dist/builtin/workflows/src/shared/store.ts +29 -7
- package/dist/builtin/workflows/src/shared/types.ts +12 -10
- package/dist/builtin/workflows/src/tui/chat-surface.ts +32 -33
- package/dist/builtin/workflows/src/tui/run-detail.ts +23 -4
- package/dist/builtin/workflows/src/tui/status-helpers.ts +4 -0
- package/dist/builtin/workflows/src/tui/status-list.ts +47 -3
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +1 -1
- package/dist/builtin/workflows/src/tui/widget.ts +12 -3
- package/dist/builtin/workflows/src/workflows/define-workflow.ts +3 -3
- package/dist/cli/args.d.ts +4 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +35 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/project-trust.d.ts +10 -0
- package/dist/cli/project-trust.d.ts.map +1 -0
- package/dist/cli/project-trust.js +36 -0
- package/dist/cli/project-trust.js.map +1 -0
- package/dist/cli/startup-ui.d.ts +7 -0
- package/dist/cli/startup-ui.d.ts.map +1 -0
- package/dist/cli/startup-ui.js +57 -0
- package/dist/cli/startup-ui.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +24 -3
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts +3 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +1 -0
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +3 -1
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +3 -2
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +9 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +70 -21
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +4 -3
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts +3 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +9 -3
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +18 -24
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/utils.d.ts +1 -1
- package/dist/core/compaction/utils.d.ts.map +1 -1
- package/dist/core/compaction/utils.js +1 -1
- package/dist/core/compaction/utils.js.map +1 -1
- package/dist/core/experimental.d.ts +2 -0
- package/dist/core/experimental.d.ts.map +1 -0
- package/dist/core/experimental.js +5 -0
- package/dist/core/experimental.js.map +1 -0
- package/dist/core/export-html/template.js +19 -6
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +6 -4
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +11 -4
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +53 -3
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +34 -4
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +2 -0
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +27 -1
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +64 -7
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +1 -0
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/output-guard.d.ts +1 -0
- package/dist/core/output-guard.d.ts.map +1 -1
- package/dist/core/output-guard.js +52 -22
- package/dist/core/output-guard.js.map +1 -1
- package/dist/core/package-manager.d.ts +1 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +20 -8
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/project-trust.d.ts +15 -0
- package/dist/core/project-trust.d.ts.map +1 -0
- package/dist/core/project-trust.js +58 -0
- package/dist/core/project-trust.js.map +1 -0
- package/dist/core/prompt-templates.d.ts +5 -4
- package/dist/core/prompt-templates.d.ts.map +1 -1
- package/dist/core/prompt-templates.js +30 -29
- package/dist/core/prompt-templates.js.map +1 -1
- package/dist/core/provider-attribution.d.ts +4 -0
- package/dist/core/provider-attribution.d.ts.map +1 -0
- package/dist/core/provider-attribution.js +73 -0
- package/dist/core/provider-attribution.js.map +1 -0
- package/dist/core/provider-display-names.d.ts.map +1 -1
- package/dist/core/provider-display-names.js +3 -0
- package/dist/core/provider-display-names.js.map +1 -1
- package/dist/core/resolve-config-value.d.ts +9 -1
- package/dist/core/resolve-config-value.d.ts.map +1 -1
- package/dist/core/resolve-config-value.js +134 -11
- package/dist/core/resolve-config-value.js.map +1 -1
- package/dist/core/resource-loader.d.ts +12 -2
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +108 -18
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +4 -2
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +13 -42
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +6 -7
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +99 -35
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +15 -2
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +69 -10
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +0 -3
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/inline-input.d.ts +28 -0
- package/dist/core/tools/ask-user-question/state/inline-input.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/state/inline-input.js +56 -0
- package/dist/core/tools/ask-user-question/state/inline-input.js.map +1 -0
- package/dist/core/tools/ask-user-question/state/key-router.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/key-router.js +30 -4
- package/dist/core/tools/ask-user-question/state/key-router.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/questionnaire-session.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/questionnaire-session.js +9 -8
- package/dist/core/tools/ask-user-question/state/questionnaire-session.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/row-intent.d.ts +3 -2
- package/dist/core/tools/ask-user-question/state/row-intent.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/row-intent.js +1 -1
- package/dist/core/tools/ask-user-question/state/row-intent.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/selectors/contract.d.ts +2 -0
- package/dist/core/tools/ask-user-question/state/selectors/contract.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/selectors/contract.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/selectors/projections.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/selectors/projections.js +2 -0
- package/dist/core/tools/ask-user-question/state/selectors/projections.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/state-reducer.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/state-reducer.js +36 -24
- package/dist/core/tools/ask-user-question/state/state-reducer.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/state.d.ts +8 -0
- package/dist/core/tools/ask-user-question/state/state.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/state.js.map +1 -1
- package/dist/core/tools/ask-user-question/tool/format-answer.d.ts +6 -0
- package/dist/core/tools/ask-user-question/tool/format-answer.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/tool/format-answer.js +19 -1
- package/dist/core/tools/ask-user-question/tool/format-answer.js.map +1 -1
- package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts +3 -2
- package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/tool/response-envelope.js +15 -3
- package/dist/core/tools/ask-user-question/tool/response-envelope.js.map +1 -1
- package/dist/core/tools/ask-user-question/tool/types.d.ts +2 -1
- package/dist/core/tools/ask-user-question/tool/types.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/tool/types.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/chat-row-view.d.ts +5 -2
- package/dist/core/tools/ask-user-question/view/components/chat-row-view.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/chat-row-view.js +2 -0
- package/dist/core/tools/ask-user-question/view/components/chat-row-view.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts +1 -0
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.js +2 -1
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/props-adapter.d.ts +3 -3
- package/dist/core/tools/ask-user-question/view/props-adapter.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/props-adapter.js +11 -4
- package/dist/core/tools/ask-user-question/view/props-adapter.js.map +1 -1
- package/dist/core/tools/bash-policy.d.ts +62 -0
- package/dist/core/tools/bash-policy.d.ts.map +1 -0
- package/dist/core/tools/bash-policy.js +1069 -0
- package/dist/core/tools/bash-policy.js.map +1 -0
- package/dist/core/tools/bash.d.ts +5 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +9 -1
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +7 -10
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +1 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +1 -1
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/oversized-tool-result.d.ts +53 -0
- package/dist/core/tools/oversized-tool-result.d.ts.map +1 -0
- package/dist/core/tools/oversized-tool-result.js +206 -0
- package/dist/core/tools/oversized-tool-result.js.map +1 -0
- package/dist/core/tools/read.d.ts +12 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +99 -34
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/render-utils.d.ts +6 -0
- package/dist/core/tools/render-utils.d.ts.map +1 -1
- package/dist/core/tools/render-utils.js +17 -1
- package/dist/core/tools/render-utils.js.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.d.ts +6 -0
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.js +2 -0
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
- package/dist/core/tools/tool-limits.d.ts +25 -0
- package/dist/core/tools/tool-limits.d.ts.map +1 -0
- package/dist/core/tools/tool-limits.js +25 -0
- package/dist/core/tools/tool-limits.js.map +1 -0
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +1 -1
- package/dist/core/tools/write.js.map +1 -1
- package/dist/core/trust-manager.d.ts +31 -0
- package/dist/core/trust-manager.d.ts.map +1 -0
- package/dist/core/trust-manager.js +196 -0
- package/dist/core/trust-manager.js.map +1 -0
- package/dist/index.d.ts +11 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +142 -30
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts +3 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +325 -7
- package/dist/migrations.js.map +1 -1
- package/dist/modes/index.d.ts +1 -1
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +2 -2
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +6 -0
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +9 -16
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +20 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +22 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/trust-selector.d.ts +23 -0
- package/dist/modes/interactive/components/trust-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/trust-selector.js +85 -0
- package/dist/modes/interactive/components/trust-selector.js.map +1 -0
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +1 -1
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +9 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +130 -9
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +10 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +1 -0
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +3 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +50 -6
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +23 -4
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +1 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/package-manager-cli.d.ts +6 -2
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +104 -10
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/changelog.d.ts +1 -0
- package/dist/utils/changelog.d.ts.map +1 -1
- package/dist/utils/changelog.js +72 -0
- package/dist/utils/changelog.js.map +1 -1
- package/dist/utils/deprecation.d.ts +4 -0
- package/dist/utils/deprecation.d.ts.map +1 -0
- package/dist/utils/deprecation.js +13 -0
- package/dist/utils/deprecation.js.map +1 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +54 -22
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/json.d.ts +3 -0
- package/dist/utils/json.d.ts.map +1 -0
- package/dist/utils/json.js +7 -0
- package/dist/utils/json.js.map +1 -0
- package/dist/utils/open-browser.d.ts +9 -0
- package/dist/utils/open-browser.d.ts.map +1 -0
- package/dist/utils/open-browser.js +22 -0
- package/dist/utils/open-browser.js.map +1 -0
- package/docs/containerization.md +111 -0
- package/docs/custom-provider.md +9 -9
- package/docs/development.md +1 -1
- package/docs/docs.json +2 -0
- package/docs/extensions.md +40 -4
- package/docs/index.md +2 -0
- package/docs/models.md +10 -10
- package/docs/packages.md +1 -1
- package/docs/prompt-templates.md +9 -2
- package/docs/providers.md +18 -5
- package/docs/quickstart.md +1 -0
- package/docs/rpc.md +3 -2
- package/docs/sdk.md +47 -0
- package/docs/security.md +58 -0
- package/docs/session-format.md +2 -2
- package/docs/sessions.md +8 -0
- package/docs/settings.md +21 -4
- package/docs/skills.md +1 -1
- package/docs/terminal-setup.md +44 -2
- package/docs/themes.md +1 -1
- package/docs/tmux.md +4 -2
- package/docs/tui.md +14 -5
- package/docs/usage.md +17 -3
- package/docs/workflows.md +127 -15
- package/examples/README.md +1 -1
- package/examples/extensions/README.md +8 -5
- package/examples/extensions/bash-spawn-hook.ts +1 -1
- package/examples/extensions/built-in-tool-renderer.ts +1 -1
- package/examples/extensions/claude-rules.ts +1 -1
- package/examples/extensions/commands.ts +1 -1
- package/examples/extensions/custom-header.ts +1 -1
- package/examples/extensions/custom-provider-anthropic/index.ts +3 -3
- package/examples/extensions/custom-provider-anthropic/package-lock.json +4 -4
- package/examples/extensions/custom-provider-anthropic/package.json +6 -6
- package/examples/extensions/custom-provider-gitlab-duo/index.ts +55 -4
- package/examples/extensions/custom-provider-gitlab-duo/package.json +3 -3
- package/examples/extensions/doom-overlay/README.md +1 -1
- package/examples/extensions/doom-overlay/index.ts +2 -2
- package/examples/extensions/git-merge-and-resolve.ts +115 -0
- package/examples/extensions/gondolin/index.ts +523 -0
- package/examples/extensions/gondolin/package-lock.json +185 -0
- package/examples/extensions/gondolin/package.json +19 -0
- package/examples/extensions/handoff.ts +1 -1
- package/examples/extensions/hidden-thinking-label.ts +1 -1
- package/examples/extensions/inline-bash.ts +2 -2
- package/examples/extensions/input-transform-streaming.ts +39 -0
- package/examples/extensions/input-transform.ts +3 -3
- package/examples/extensions/interactive-shell.ts +2 -2
- package/examples/extensions/mac-system-theme.ts +2 -2
- package/examples/extensions/minimal-mode.ts +1 -1
- package/examples/extensions/modal-editor.ts +1 -1
- package/examples/extensions/model-status.ts +1 -1
- package/examples/extensions/overlay-qa-tests.ts +198 -179
- package/examples/extensions/overlay-test.ts +1 -1
- package/examples/extensions/pirate.ts +1 -1
- package/examples/extensions/preset.ts +14 -12
- package/examples/extensions/project-trust.ts +64 -0
- package/examples/extensions/prompt-customizer.ts +1 -1
- package/examples/extensions/qna.ts +1 -1
- package/examples/extensions/question.ts +1 -1
- package/examples/extensions/questionnaire.ts +1 -1
- package/examples/extensions/rainbow-editor.ts +1 -1
- package/examples/extensions/sandbox/index.ts +16 -14
- package/examples/extensions/sandbox/package-lock.json +90 -90
- package/examples/extensions/sandbox/package.json +17 -17
- package/examples/extensions/snake.ts +1 -1
- package/examples/extensions/space-invaders.ts +1 -1
- package/examples/extensions/ssh.ts +2 -2
- package/examples/extensions/subagent/README.md +13 -13
- package/examples/extensions/subagent/agents.ts +4 -2
- package/examples/extensions/subagent/index.ts +6 -6
- package/examples/extensions/summarize.ts +1 -1
- package/examples/extensions/tic-tac-toe.ts +1 -1
- package/examples/extensions/titlebar-spinner.ts +1 -1
- package/examples/extensions/todo.ts +1 -1
- package/examples/extensions/tool-override.ts +1 -1
- package/examples/extensions/tools.ts +6 -1
- package/examples/extensions/with-deps/package-lock.json +4 -4
- package/examples/extensions/with-deps/package.json +7 -7
- package/examples/extensions/working-indicator.ts +4 -4
- package/examples/extensions/working-message-test.ts +1 -1
- package/examples/sdk/01-minimal.ts +1 -1
- package/examples/sdk/03-custom-prompt.ts +1 -1
- package/examples/sdk/04-skills.ts +1 -1
- package/examples/sdk/06-extensions.ts +2 -2
- package/examples/sdk/08-prompt-templates.ts +1 -1
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/README.md +2 -2
- package/package.json +4 -4
|
@@ -40,6 +40,8 @@ import type {
|
|
|
40
40
|
WorkflowExecutionMode,
|
|
41
41
|
WorkflowRunChildOptions,
|
|
42
42
|
WorkflowChildResult,
|
|
43
|
+
WorkflowExitOptions,
|
|
44
|
+
WorkflowExitStatus,
|
|
43
45
|
WorkflowOutputSchema,
|
|
44
46
|
WorkflowOutputValues,
|
|
45
47
|
WorkflowInputValues,
|
|
@@ -98,6 +100,7 @@ import type { WorkflowFailure } from "../../shared/workflow-failures.js";
|
|
|
98
100
|
import { classifyWorkflowFailure } from "../../shared/workflow-failures.js";
|
|
99
101
|
import { selectPromptCallsiteFrame } from "../shared/prompt-callsite.js";
|
|
100
102
|
import {
|
|
103
|
+
WORKFLOW_SERIALIZABLE_DESCRIPTION,
|
|
101
104
|
assertWorkflowSerializableObject,
|
|
102
105
|
workflowSerializableValidationError,
|
|
103
106
|
workflowSerializableTypeName,
|
|
@@ -195,7 +198,7 @@ export interface RunOpts extends Omit<AuthoringContract.RunOpts, "adapters" | "s
|
|
|
195
198
|
onRunStart?: (snapshot: RunSnapshot) => void;
|
|
196
199
|
onStageStart?: (runId: string, snapshot: StageSnapshot) => void;
|
|
197
200
|
onStageEnd?: (runId: string, snapshot: StageSnapshot) => void;
|
|
198
|
-
onRunEnd?: (runId: string, status: RunStatus, result?: WorkflowOutputValues, error?: string) => void;
|
|
201
|
+
onRunEnd?: (runId: string, status: RunStatus, result?: WorkflowOutputValues, error?: string, exitReason?: string) => void;
|
|
199
202
|
}
|
|
200
203
|
|
|
201
204
|
export interface RunResult {
|
|
@@ -203,9 +206,393 @@ export interface RunResult {
|
|
|
203
206
|
readonly status: RunStatus;
|
|
204
207
|
readonly result?: WorkflowOutputValues;
|
|
205
208
|
readonly error?: string;
|
|
209
|
+
/** True when the run reached its terminal status through ctx.exit(). */
|
|
210
|
+
readonly exited?: boolean;
|
|
211
|
+
readonly exitReason?: string;
|
|
206
212
|
readonly stages: StageSnapshot[];
|
|
207
213
|
}
|
|
208
214
|
|
|
215
|
+
const WORKFLOW_EXIT_SIGNAL = Symbol("atomic-workflows.workflow-exit-signal");
|
|
216
|
+
const WORKFLOW_EXIT_STATUSES: ReadonlySet<WorkflowExitStatus> = new Set([
|
|
217
|
+
"completed",
|
|
218
|
+
"skipped",
|
|
219
|
+
"cancelled",
|
|
220
|
+
"blocked",
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
type WorkflowExitOutputSnapshot =
|
|
224
|
+
| {
|
|
225
|
+
readonly ok: true;
|
|
226
|
+
readonly value: unknown;
|
|
227
|
+
}
|
|
228
|
+
| {
|
|
229
|
+
readonly ok: false;
|
|
230
|
+
readonly error: Error;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
interface WorkflowExitSignal {
|
|
234
|
+
readonly [WORKFLOW_EXIT_SIGNAL]: true;
|
|
235
|
+
readonly scope: symbol;
|
|
236
|
+
readonly status: WorkflowExitStatus;
|
|
237
|
+
readonly reason?: string;
|
|
238
|
+
readonly outputSnapshot?: WorkflowExitOutputSnapshot;
|
|
239
|
+
readonly validationError?: Error;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const WORKFLOW_EXIT_SNAPSHOT_INVALID_VALUE = Symbol("atomic-workflows.workflow-exit-snapshot-invalid-value");
|
|
243
|
+
|
|
244
|
+
interface WorkflowExitSnapshotInvalidValue {
|
|
245
|
+
readonly [WORKFLOW_EXIT_SNAPSHOT_INVALID_VALUE]: true;
|
|
246
|
+
readonly typeName: string;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
type SafePropertyRead =
|
|
250
|
+
| { readonly ok: true; readonly value: unknown }
|
|
251
|
+
| { readonly ok: false };
|
|
252
|
+
|
|
253
|
+
function safeGetProperty(value: object, key: PropertyKey): SafePropertyRead {
|
|
254
|
+
try {
|
|
255
|
+
return { ok: true, value: (value as Record<PropertyKey, unknown>)[key] };
|
|
256
|
+
} catch {
|
|
257
|
+
return { ok: false };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function unknownErrorMessage(error: unknown): string {
|
|
262
|
+
if (error !== null && (typeof error === "object" || typeof error === "function")) {
|
|
263
|
+
const message = safeGetProperty(error, "message");
|
|
264
|
+
if (message.ok && typeof message.value === "string" && message.value.length > 0) {
|
|
265
|
+
return message.value;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (typeof error === "string") return error;
|
|
269
|
+
try {
|
|
270
|
+
return String(error);
|
|
271
|
+
} catch {
|
|
272
|
+
return "<unprintable thrown value>";
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function workflowExitSnapshotError(message: string, cause: unknown): Error {
|
|
277
|
+
return new Error(`${message}: ${unknownErrorMessage(cause)}`, { cause });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function workflowExitOptionReadError(key: "status" | "reason" | "outputs", cause: unknown): Error {
|
|
281
|
+
return workflowExitSnapshotError(`atomic-workflows: ctx.exit() ${key} option could not be read`, cause);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function readWorkflowExitOption(
|
|
285
|
+
options: { readonly status?: unknown; readonly reason?: unknown; readonly outputs?: unknown } | null | undefined,
|
|
286
|
+
key: "status" | "reason" | "outputs",
|
|
287
|
+
): { readonly ok: true; readonly value: unknown } | { readonly ok: false; readonly error: Error } {
|
|
288
|
+
try {
|
|
289
|
+
return { ok: true, value: options?.[key] };
|
|
290
|
+
} catch (err) {
|
|
291
|
+
return { ok: false, error: workflowExitOptionReadError(key, err) };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function describeWorkflowExitOptionValue(value: unknown): string {
|
|
296
|
+
try {
|
|
297
|
+
const json = JSON.stringify(value);
|
|
298
|
+
if (json !== undefined) return json;
|
|
299
|
+
} catch {
|
|
300
|
+
// Fall back to a coarse type name below. This path is diagnostic only and
|
|
301
|
+
// must never make ctx.exit() throw before workflow-exit cleanup can run.
|
|
302
|
+
}
|
|
303
|
+
return workflowSerializableTypeName(value);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function isPlainWorkflowExitSnapshotObject(value: object): boolean {
|
|
307
|
+
const proto = Object.getPrototypeOf(value);
|
|
308
|
+
return proto === Object.prototype || proto === null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function makeWorkflowExitSnapshotInvalidValue(typeName: string): WorkflowExitSnapshotInvalidValue {
|
|
312
|
+
const marker = {} as { [WORKFLOW_EXIT_SNAPSHOT_INVALID_VALUE]?: true; typeName?: string };
|
|
313
|
+
Object.defineProperty(marker, WORKFLOW_EXIT_SNAPSHOT_INVALID_VALUE, {
|
|
314
|
+
value: true,
|
|
315
|
+
enumerable: false,
|
|
316
|
+
});
|
|
317
|
+
Object.defineProperty(marker, "typeName", {
|
|
318
|
+
value: typeName,
|
|
319
|
+
enumerable: false,
|
|
320
|
+
});
|
|
321
|
+
return Object.freeze(marker) as WorkflowExitSnapshotInvalidValue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isWorkflowExitSnapshotInvalidValue(value: unknown): value is WorkflowExitSnapshotInvalidValue {
|
|
325
|
+
return value !== null && typeof value === "object" &&
|
|
326
|
+
(value as Record<PropertyKey, unknown>)[WORKFLOW_EXIT_SNAPSHOT_INVALID_VALUE] === true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function cloneWorkflowExitSnapshotValue(
|
|
330
|
+
value: unknown,
|
|
331
|
+
seen: Map<object, unknown>,
|
|
332
|
+
stack: Set<object> = new Set(),
|
|
333
|
+
): unknown {
|
|
334
|
+
if (value === null) return null;
|
|
335
|
+
const valueType = typeof value;
|
|
336
|
+
if (valueType !== "object") {
|
|
337
|
+
return valueType === "function"
|
|
338
|
+
? makeWorkflowExitSnapshotInvalidValue("function")
|
|
339
|
+
: value;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const objectValue = value as object;
|
|
343
|
+
const previousClone = seen.get(objectValue);
|
|
344
|
+
if (previousClone !== undefined) {
|
|
345
|
+
return stack.has(objectValue)
|
|
346
|
+
? makeWorkflowExitSnapshotInvalidValue("circular object")
|
|
347
|
+
: previousClone;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (Array.isArray(value)) {
|
|
351
|
+
const clone: unknown[] = [];
|
|
352
|
+
seen.set(objectValue, clone);
|
|
353
|
+
stack.add(objectValue);
|
|
354
|
+
try {
|
|
355
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
356
|
+
clone[index] = cloneWorkflowExitSnapshotValue(value[index], seen, stack);
|
|
357
|
+
}
|
|
358
|
+
} finally {
|
|
359
|
+
stack.delete(objectValue);
|
|
360
|
+
}
|
|
361
|
+
return clone;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!isPlainWorkflowExitSnapshotObject(objectValue)) {
|
|
365
|
+
return makeWorkflowExitSnapshotInvalidValue(workflowSerializableTypeName(value));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const clone: Record<string, unknown> = {};
|
|
369
|
+
seen.set(objectValue, clone);
|
|
370
|
+
stack.add(objectValue);
|
|
371
|
+
try {
|
|
372
|
+
for (const key of Object.keys(value as Record<string, unknown>)) {
|
|
373
|
+
clone[key] = cloneWorkflowExitSnapshotValue((value as Record<string, unknown>)[key], seen, stack);
|
|
374
|
+
}
|
|
375
|
+
} finally {
|
|
376
|
+
stack.delete(objectValue);
|
|
377
|
+
}
|
|
378
|
+
return clone;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Recursively freeze the (already-private) deep clone so the snapshot stored on
|
|
382
|
+
// the thrown WorkflowExitSignal is immutable. Combined with freezing the signal
|
|
383
|
+
// object itself, this stops author code that catches ctx.exit()'s signal from
|
|
384
|
+
// rewriting the captured outputs before finalization reads them (finalization
|
|
385
|
+
// recovers the same object via the abort reason / rethrow, and the reconstruction
|
|
386
|
+
// path reads `outputSnapshot.value` by reference). The clone is acyclic — cycles
|
|
387
|
+
// became frozen invalid-value markers — and the `Object.isFrozen` short-circuit
|
|
388
|
+
// keeps shared (DAG) nodes terminating.
|
|
389
|
+
function deepFreezeWorkflowExitSnapshotValue(value: unknown): void {
|
|
390
|
+
if (value === null || typeof value !== "object" || Object.isFrozen(value)) return;
|
|
391
|
+
Object.freeze(value);
|
|
392
|
+
if (Array.isArray(value)) {
|
|
393
|
+
for (const item of value) deepFreezeWorkflowExitSnapshotValue(item);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
for (const key of Object.keys(value as Record<string, unknown>)) {
|
|
397
|
+
deepFreezeWorkflowExitSnapshotValue((value as Record<string, unknown>)[key]);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function freezeWorkflowExitOutputSnapshot(snapshot: WorkflowExitOutputSnapshot): WorkflowExitOutputSnapshot {
|
|
402
|
+
return Object.freeze(snapshot);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function captureWorkflowExitOutputSnapshot(rawOutputs: unknown): WorkflowExitOutputSnapshot {
|
|
406
|
+
let snapshot: WorkflowExitOutputSnapshot;
|
|
407
|
+
try {
|
|
408
|
+
const value = cloneWorkflowExitSnapshotValue(rawOutputs, new Map());
|
|
409
|
+
deepFreezeWorkflowExitSnapshotValue(value);
|
|
410
|
+
snapshot = { ok: true, value };
|
|
411
|
+
} catch (err) {
|
|
412
|
+
snapshot = {
|
|
413
|
+
ok: false,
|
|
414
|
+
error: workflowExitSnapshotError("atomic-workflows: ctx.exit() outputs could not be snapshotted", err),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
return freezeWorkflowExitOutputSnapshot(snapshot);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function formatWorkflowExitSnapshotPath(parent: string, key: string): string {
|
|
421
|
+
// `segment` already encodes the structure: bracketed for numeric/non-identifier keys,
|
|
422
|
+
// dotted for identifiers with a parent, and the bare key for an identifier at the root
|
|
423
|
+
// (where `parent === ""` so `segment === key`). Every case therefore reduces to the
|
|
424
|
+
// concatenation below.
|
|
425
|
+
const segment = /^\d+$/.test(key)
|
|
426
|
+
? `[${key}]`
|
|
427
|
+
: /^[A-Za-z_$][\w$]*$/.test(key)
|
|
428
|
+
? (parent.length > 0 ? `.${key}` : key)
|
|
429
|
+
: `[${JSON.stringify(key)}]`;
|
|
430
|
+
return `${parent}${segment}`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function findWorkflowExitSnapshotInvalidValue(
|
|
434
|
+
value: unknown,
|
|
435
|
+
path = "",
|
|
436
|
+
seen = new Set<unknown>(),
|
|
437
|
+
): { readonly path: string; readonly typeName: string } | undefined {
|
|
438
|
+
if (isWorkflowExitSnapshotInvalidValue(value)) {
|
|
439
|
+
return { path, typeName: value.typeName };
|
|
440
|
+
}
|
|
441
|
+
if (value === null || typeof value !== "object") return undefined;
|
|
442
|
+
if (seen.has(value)) return undefined;
|
|
443
|
+
seen.add(value);
|
|
444
|
+
|
|
445
|
+
if (Array.isArray(value)) {
|
|
446
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
447
|
+
const found = findWorkflowExitSnapshotInvalidValue(value[index], `${path}[${index}]`, seen);
|
|
448
|
+
if (found !== undefined) return found;
|
|
449
|
+
}
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
for (const key of Object.keys(value as Record<string, unknown>)) {
|
|
454
|
+
const found = findWorkflowExitSnapshotInvalidValue(
|
|
455
|
+
(value as Record<string, unknown>)[key],
|
|
456
|
+
formatWorkflowExitSnapshotPath(path, key),
|
|
457
|
+
seen,
|
|
458
|
+
);
|
|
459
|
+
if (found !== undefined) return found;
|
|
460
|
+
}
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function workflowExitSnapshotInvalidValueMessage(label: string, value: unknown): string | undefined {
|
|
465
|
+
const invalid = findWorkflowExitSnapshotInvalidValue(value);
|
|
466
|
+
if (invalid === undefined) return undefined;
|
|
467
|
+
const location = invalid.path.length > 0 ? ` at ${invalid.path}` : "";
|
|
468
|
+
return `${label}${location} must be ${WORKFLOW_SERIALIZABLE_DESCRIPTION}, got ${invalid.typeName}`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const PARENT_WORKFLOW_EXIT_ABORT = Symbol("atomic-workflows.parent-workflow-exit-abort");
|
|
472
|
+
|
|
473
|
+
interface ParentWorkflowExitAbortReason extends Error {
|
|
474
|
+
readonly [PARENT_WORKFLOW_EXIT_ABORT]: true;
|
|
475
|
+
readonly workflowExitReason?: string;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function parentWorkflowExitRunReason(reason?: string): string {
|
|
479
|
+
return reason === undefined || reason.length === 0
|
|
480
|
+
? "parent workflow exited"
|
|
481
|
+
: `parent workflow exited: ${reason}`;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function makeParentWorkflowExitAbortReason(reason?: string): ParentWorkflowExitAbortReason {
|
|
485
|
+
const error = new Error(parentWorkflowExitRunReason(reason)) as ParentWorkflowExitAbortReason & {
|
|
486
|
+
[PARENT_WORKFLOW_EXIT_ABORT]: true;
|
|
487
|
+
workflowExitReason?: string;
|
|
488
|
+
};
|
|
489
|
+
Object.defineProperty(error, PARENT_WORKFLOW_EXIT_ABORT, {
|
|
490
|
+
value: true,
|
|
491
|
+
enumerable: false,
|
|
492
|
+
});
|
|
493
|
+
if (reason !== undefined) error.workflowExitReason = reason;
|
|
494
|
+
return error;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
interface ParentWorkflowExitAbortProbe {
|
|
498
|
+
readonly workflowExitReason?: string;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function parentWorkflowExitAbortReason(value: unknown): ParentWorkflowExitAbortProbe | undefined {
|
|
502
|
+
if (value === null || (typeof value !== "object" && typeof value !== "function")) return undefined;
|
|
503
|
+
const marker = safeGetProperty(value, PARENT_WORKFLOW_EXIT_ABORT);
|
|
504
|
+
if (!marker.ok || marker.value !== true) return undefined;
|
|
505
|
+
|
|
506
|
+
const reason = safeGetProperty(value, "workflowExitReason");
|
|
507
|
+
return reason.ok && typeof reason.value === "string"
|
|
508
|
+
? { workflowExitReason: reason.value }
|
|
509
|
+
: {};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function isWorkflowExitStatus(value: unknown): value is WorkflowExitStatus {
|
|
513
|
+
return typeof value === "string" && WORKFLOW_EXIT_STATUSES.has(value as WorkflowExitStatus);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function safeErrorValue(value: unknown): Error {
|
|
517
|
+
try {
|
|
518
|
+
if (value instanceof Error) return value;
|
|
519
|
+
} catch {
|
|
520
|
+
// Fall through to a safe wrapper below.
|
|
521
|
+
}
|
|
522
|
+
return new Error(unknownErrorMessage(value));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function readWorkflowExitOutputSnapshot(value: unknown): WorkflowExitOutputSnapshot | undefined {
|
|
526
|
+
if (value === undefined) return undefined;
|
|
527
|
+
if (value === null || (typeof value !== "object" && typeof value !== "function")) return undefined;
|
|
528
|
+
const ok = safeGetProperty(value, "ok");
|
|
529
|
+
if (!ok.ok) return undefined;
|
|
530
|
+
if (ok.value === true) {
|
|
531
|
+
const snapshotValue = safeGetProperty(value, "value");
|
|
532
|
+
return snapshotValue.ok ? { ok: true, value: snapshotValue.value } : undefined;
|
|
533
|
+
}
|
|
534
|
+
if (ok.value === false) {
|
|
535
|
+
const error = safeGetProperty(value, "error");
|
|
536
|
+
return error.ok ? { ok: false, error: safeErrorValue(error.value) } : undefined;
|
|
537
|
+
}
|
|
538
|
+
return undefined;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function readWorkflowExitSignalCandidate(value: object, scope: symbol): WorkflowExitSignal | undefined {
|
|
542
|
+
const marker = safeGetProperty(value, WORKFLOW_EXIT_SIGNAL);
|
|
543
|
+
if (!marker.ok || marker.value !== true) return undefined;
|
|
544
|
+
|
|
545
|
+
const signalScope = safeGetProperty(value, "scope");
|
|
546
|
+
if (!signalScope.ok || signalScope.value !== scope) return undefined;
|
|
547
|
+
|
|
548
|
+
const status = safeGetProperty(value, "status");
|
|
549
|
+
if (!status.ok || !isWorkflowExitStatus(status.value)) return undefined;
|
|
550
|
+
|
|
551
|
+
const reason = safeGetProperty(value, "reason");
|
|
552
|
+
if (!reason.ok || (reason.value !== undefined && typeof reason.value !== "string")) return undefined;
|
|
553
|
+
|
|
554
|
+
const outputSnapshotValue = safeGetProperty(value, "outputSnapshot");
|
|
555
|
+
if (!outputSnapshotValue.ok) return undefined;
|
|
556
|
+
const outputSnapshot = readWorkflowExitOutputSnapshot(outputSnapshotValue.value);
|
|
557
|
+
if (outputSnapshotValue.value !== undefined && outputSnapshot === undefined) return undefined;
|
|
558
|
+
|
|
559
|
+
const validationError = safeGetProperty(value, "validationError");
|
|
560
|
+
if (!validationError.ok) return undefined;
|
|
561
|
+
|
|
562
|
+
return {
|
|
563
|
+
[WORKFLOW_EXIT_SIGNAL]: true,
|
|
564
|
+
scope,
|
|
565
|
+
status: status.value,
|
|
566
|
+
...(reason.value !== undefined ? { reason: reason.value } : {}),
|
|
567
|
+
...(outputSnapshot !== undefined ? { outputSnapshot } : {}),
|
|
568
|
+
...(validationError.value !== undefined ? { validationError: safeErrorValue(validationError.value) } : {}),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function findWorkflowExitSignal(error: unknown, scope: symbol, seen = new Set<unknown>()): WorkflowExitSignal | undefined {
|
|
573
|
+
if (error === null || (typeof error !== "object" && typeof error !== "function")) return undefined;
|
|
574
|
+
if (seen.has(error)) return undefined;
|
|
575
|
+
seen.add(error);
|
|
576
|
+
|
|
577
|
+
const directSignal = readWorkflowExitSignalCandidate(error, scope);
|
|
578
|
+
if (directSignal !== undefined) return directSignal;
|
|
579
|
+
|
|
580
|
+
const errors = safeExecutorAggregateErrorItems(error);
|
|
581
|
+
for (const item of errors) {
|
|
582
|
+
const signal = findWorkflowExitSignal(item, scope, seen);
|
|
583
|
+
if (signal !== undefined) return signal;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const cause = safeGetProperty(error, "cause");
|
|
587
|
+
if (cause.ok) {
|
|
588
|
+
const causeSignal = findWorkflowExitSignal(cause.value, scope, seen);
|
|
589
|
+
if (causeSignal !== undefined) return causeSignal;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const reason = safeGetProperty(error, "reason");
|
|
593
|
+
return reason.ok ? findWorkflowExitSignal(reason.value, scope, seen) : undefined;
|
|
594
|
+
}
|
|
595
|
+
|
|
209
596
|
// ---------------------------------------------------------------------------
|
|
210
597
|
// Input resolution / validation
|
|
211
598
|
// ---------------------------------------------------------------------------
|
|
@@ -434,9 +821,12 @@ function mergeHilSignals(primary: AbortSignal, secondary: AbortSignal | undefine
|
|
|
434
821
|
};
|
|
435
822
|
}
|
|
436
823
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
824
|
+
/**
|
|
825
|
+
* Build a UI context whose every interactive primitive rejects with a clear,
|
|
826
|
+
* actionable error. Parameterized by `msg` so the "no UI adapter" and
|
|
827
|
+
* "headless mode" variants share one implementation and never drift (#1339).
|
|
828
|
+
*/
|
|
829
|
+
function makeRejectingUIContext(msg: (primitive: string) => string): WorkflowUIContext {
|
|
440
830
|
return {
|
|
441
831
|
input: () => Promise.reject(new Error(msg("input"))),
|
|
442
832
|
confirm: () => Promise.reject(new Error(msg("confirm"))),
|
|
@@ -446,24 +836,55 @@ function makeUnavailableUIContext(): WorkflowUIContext {
|
|
|
446
836
|
};
|
|
447
837
|
}
|
|
448
838
|
|
|
839
|
+
function makeUnavailableUIContext(): WorkflowUIContext {
|
|
840
|
+
return makeRejectingUIContext(
|
|
841
|
+
(primitive) =>
|
|
842
|
+
`atomic-workflows: HIL ctx.ui.${primitive} is unavailable because Atomic runtime did not provide a UI adapter`,
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* UI context for headless (non-interactive) runs without a UI adapter: every
|
|
848
|
+
* interactive primitive fails with a clear, actionable error that names the
|
|
849
|
+
* headless mode instead of surfacing a raw
|
|
850
|
+
* `TypeError: ctx.ui.custom is not a function` from a missing TUI (#1339).
|
|
851
|
+
*/
|
|
852
|
+
function makeHeadlessUnavailableUIContext(): WorkflowUIContext {
|
|
853
|
+
return makeRejectingUIContext(
|
|
854
|
+
(primitive) =>
|
|
855
|
+
`atomic-workflows: interactive ctx.ui.${primitive} is unavailable in headless (non-interactive) mode; run the workflow in interactive mode or remove the interactive prompt from this stage`,
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
|
|
449
859
|
function normalizeUIContext(adapter: WorkflowUIAdapter | undefined): WorkflowUIContext {
|
|
450
860
|
const unavailable = makeUnavailableUIContext();
|
|
451
861
|
if (adapter === undefined) return unavailable;
|
|
862
|
+
// Guard every method: loosely-typed callers can hand over partial adapters
|
|
863
|
+
// (headless hosts especially), and an unguarded call would surface a raw
|
|
864
|
+
// "x is not a function" TypeError that kills the whole run (#1339).
|
|
452
865
|
return {
|
|
453
866
|
input(prompt) {
|
|
454
|
-
return adapter.input
|
|
867
|
+
return typeof adapter.input === "function"
|
|
868
|
+
? adapter.input.call(adapter, prompt)
|
|
869
|
+
: unavailable.input(prompt);
|
|
455
870
|
},
|
|
456
871
|
confirm(message) {
|
|
457
|
-
return adapter.confirm
|
|
872
|
+
return typeof adapter.confirm === "function"
|
|
873
|
+
? adapter.confirm.call(adapter, message)
|
|
874
|
+
: unavailable.confirm(message);
|
|
458
875
|
},
|
|
459
876
|
select<T extends string>(message: string, options: readonly T[]): Promise<T> {
|
|
460
|
-
return adapter.select
|
|
877
|
+
return typeof adapter.select === "function"
|
|
878
|
+
? adapter.select.call(adapter, message, options) as Promise<T>
|
|
879
|
+
: unavailable.select(message, options);
|
|
461
880
|
},
|
|
462
881
|
editor(initial) {
|
|
463
|
-
return adapter.editor
|
|
882
|
+
return typeof adapter.editor === "function"
|
|
883
|
+
? adapter.editor.call(adapter, initial)
|
|
884
|
+
: unavailable.editor(initial);
|
|
464
885
|
},
|
|
465
886
|
custom<T>(factory: WorkflowCustomUiFactory<T>, options?: WorkflowCustomUiOptions): Promise<T> {
|
|
466
|
-
return adapter.custom
|
|
887
|
+
return typeof adapter.custom === "function"
|
|
467
888
|
? adapter.custom.call(adapter, factory, options) as Promise<T>
|
|
468
889
|
: unavailable.custom(factory, options);
|
|
469
890
|
},
|
|
@@ -955,6 +1376,11 @@ async function mapParallelSteps<T>(
|
|
|
955
1376
|
failFast: boolean | undefined,
|
|
956
1377
|
mapper: (step: WorkflowTaskStep) => Promise<T>,
|
|
957
1378
|
onFirstFailure?: (error: unknown) => void,
|
|
1379
|
+
control?: {
|
|
1380
|
+
readonly beforeDequeue?: () => void;
|
|
1381
|
+
readonly beforeMap?: () => void;
|
|
1382
|
+
readonly isControlSignal?: (error: unknown) => boolean;
|
|
1383
|
+
},
|
|
958
1384
|
): Promise<T[]> {
|
|
959
1385
|
const limit = positiveConcurrency(concurrency) ?? steps.length;
|
|
960
1386
|
const failFastEnabled = failFast !== false;
|
|
@@ -962,27 +1388,55 @@ async function mapParallelSteps<T>(
|
|
|
962
1388
|
const failures: Array<{ readonly index: number; readonly error: unknown }> = [];
|
|
963
1389
|
let nextIndex = 0;
|
|
964
1390
|
let firstFailure: unknown;
|
|
1391
|
+
let controlSignal: unknown;
|
|
965
1392
|
let rejectFirstFailure: (reason: unknown) => void = () => {};
|
|
966
1393
|
const firstFailurePromise = new Promise<never>((_, reject) => {
|
|
967
1394
|
rejectFirstFailure = reject;
|
|
968
1395
|
});
|
|
969
1396
|
|
|
1397
|
+
const isControlSignal = (error: unknown): boolean => control?.isControlSignal?.(error) === true;
|
|
1398
|
+
const selectControlSignal = (error: unknown): void => {
|
|
1399
|
+
if (controlSignal !== undefined) return;
|
|
1400
|
+
controlSignal = error;
|
|
1401
|
+
if (failFastEnabled) rejectFirstFailure(error);
|
|
1402
|
+
};
|
|
1403
|
+
const recordFailure = (index: number, error: unknown): void => {
|
|
1404
|
+
failures.push({ index, error });
|
|
1405
|
+
if (firstFailure === undefined) {
|
|
1406
|
+
firstFailure = error;
|
|
1407
|
+
onFirstFailure?.(error);
|
|
1408
|
+
if (failFastEnabled) rejectFirstFailure(error);
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
|
|
970
1412
|
async function worker(): Promise<void> {
|
|
971
1413
|
while (true) {
|
|
1414
|
+
if (controlSignal !== undefined) return;
|
|
972
1415
|
if (failFastEnabled && firstFailure !== undefined) return;
|
|
1416
|
+
try {
|
|
1417
|
+
control?.beforeDequeue?.();
|
|
1418
|
+
} catch (err) {
|
|
1419
|
+
if (isControlSignal(err)) {
|
|
1420
|
+
selectControlSignal(err);
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
recordFailure(nextIndex, err);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
if (controlSignal !== undefined) return;
|
|
973
1427
|
const index = nextIndex;
|
|
974
1428
|
nextIndex += 1;
|
|
975
1429
|
const step = steps[index];
|
|
976
1430
|
if (step === undefined) return;
|
|
977
1431
|
try {
|
|
1432
|
+
control?.beforeMap?.();
|
|
978
1433
|
results[index] = await mapper(step);
|
|
979
1434
|
} catch (err) {
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
onFirstFailure?.(err);
|
|
984
|
-
if (failFastEnabled) rejectFirstFailure(err);
|
|
1435
|
+
if (isControlSignal(err)) {
|
|
1436
|
+
selectControlSignal(err);
|
|
1437
|
+
return;
|
|
985
1438
|
}
|
|
1439
|
+
recordFailure(index, err);
|
|
986
1440
|
if (failFastEnabled) return;
|
|
987
1441
|
}
|
|
988
1442
|
}
|
|
@@ -1002,6 +1456,10 @@ async function mapParallelSteps<T>(
|
|
|
1002
1456
|
}
|
|
1003
1457
|
}
|
|
1004
1458
|
|
|
1459
|
+
if (controlSignal !== undefined) {
|
|
1460
|
+
throw controlSignal;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1005
1463
|
if (failures.length > 0) {
|
|
1006
1464
|
throw new AggregateError(
|
|
1007
1465
|
failures.map((failure) => failure.error),
|
|
@@ -1263,8 +1721,8 @@ function workflowDetailsFromRun(
|
|
|
1263
1721
|
mode,
|
|
1264
1722
|
action: "run",
|
|
1265
1723
|
runId: runResult.runId,
|
|
1266
|
-
status: runResult.status
|
|
1267
|
-
?
|
|
1724
|
+
status: isWorkflowExitStatus(runResult.status)
|
|
1725
|
+
? runResult.status
|
|
1268
1726
|
: runResult.status === "failed"
|
|
1269
1727
|
? "failed"
|
|
1270
1728
|
: runResult.status === "killed"
|
|
@@ -1277,6 +1735,8 @@ function workflowDetailsFromRun(
|
|
|
1277
1735
|
...(artifacts.length > 0 ? { artifacts } : {}),
|
|
1278
1736
|
...(allWarnings.length > 0 ? { warnings: allWarnings } : {}),
|
|
1279
1737
|
...(runResult.error !== undefined ? { error: runResult.error } : {}),
|
|
1738
|
+
...(runResult.exited !== undefined ? { exited: runResult.exited } : {}),
|
|
1739
|
+
...(runResult.exitReason !== undefined ? { exitReason: runResult.exitReason } : {}),
|
|
1280
1740
|
};
|
|
1281
1741
|
}
|
|
1282
1742
|
|
|
@@ -1560,6 +2020,8 @@ function appendRunEndWhenRecorded(
|
|
|
1560
2020
|
readonly status: RunStatus;
|
|
1561
2021
|
readonly result?: WorkflowOutputValues;
|
|
1562
2022
|
readonly error?: string;
|
|
2023
|
+
readonly exited?: boolean;
|
|
2024
|
+
readonly exitReason?: string;
|
|
1563
2025
|
readonly failureKind?: WorkflowFailureKind;
|
|
1564
2026
|
readonly failureCode?: WorkflowFailureCode;
|
|
1565
2027
|
readonly failureRecoverability?: WorkflowFailureRecoverability;
|
|
@@ -1575,6 +2037,54 @@ function appendRunEndWhenRecorded(
|
|
|
1575
2037
|
appendRunEnd(persistence, payload);
|
|
1576
2038
|
}
|
|
1577
2039
|
|
|
2040
|
+
function isTerminalRunStatus(status: RunStatus): boolean {
|
|
2041
|
+
return status === "completed" ||
|
|
2042
|
+
status === "failed" ||
|
|
2043
|
+
status === "killed" ||
|
|
2044
|
+
status === "skipped" ||
|
|
2045
|
+
status === "cancelled" ||
|
|
2046
|
+
status === "blocked";
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
function runResultFromSnapshot(snapshot: RunSnapshot): RunResult {
|
|
2050
|
+
return {
|
|
2051
|
+
runId: snapshot.id,
|
|
2052
|
+
status: snapshot.status,
|
|
2053
|
+
...(snapshot.result !== undefined ? { result: snapshot.result } : {}),
|
|
2054
|
+
...(snapshot.error !== undefined ? { error: snapshot.error } : {}),
|
|
2055
|
+
...(snapshot.exited !== undefined ? { exited: snapshot.exited } : {}),
|
|
2056
|
+
...(snapshot.exitReason !== undefined ? { exitReason: snapshot.exitReason } : {}),
|
|
2057
|
+
stages: [...snapshot.stages],
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
function reconcileTerminalRunResult(
|
|
2062
|
+
runId: string,
|
|
2063
|
+
runSnapshot: RunSnapshot,
|
|
2064
|
+
activeStore: Store,
|
|
2065
|
+
fallback: Omit<RunResult, "runId" | "stages">,
|
|
2066
|
+
onRunEnd: RunOpts["onRunEnd"],
|
|
2067
|
+
): RunResult {
|
|
2068
|
+
const canonical = activeStore.runs().find((snapshot) =>
|
|
2069
|
+
snapshot.id === runId && isTerminalRunStatus(snapshot.status)
|
|
2070
|
+
);
|
|
2071
|
+
const result = canonical !== undefined
|
|
2072
|
+
? runResultFromSnapshot(canonical)
|
|
2073
|
+
: {
|
|
2074
|
+
runId,
|
|
2075
|
+
...fallback,
|
|
2076
|
+
stages: [...runSnapshot.stages],
|
|
2077
|
+
};
|
|
2078
|
+
// `recordRunEnd` is the terminal authority. If this finalizer lost because
|
|
2079
|
+
// an external kill or another terminal writer won while async cleanup was
|
|
2080
|
+
// pending, callbacks must observe the canonical store status, not the stale
|
|
2081
|
+
// intent that attempted this write. Persistence remains guarded separately by
|
|
2082
|
+
// the `recordRunEnd` boolean, so losing writes do not append duplicate
|
|
2083
|
+
// run-end entries.
|
|
2084
|
+
onRunEnd?.(runId, result.status, result.result, result.error, result.exitReason);
|
|
2085
|
+
return result;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
1578
2088
|
interface RunFailureMetadata {
|
|
1579
2089
|
readonly errorMessage: string;
|
|
1580
2090
|
readonly failureKind: WorkflowFailureKind;
|
|
@@ -1675,23 +2185,40 @@ function runFailureMetadataFromFailure(
|
|
|
1675
2185
|
};
|
|
1676
2186
|
}
|
|
1677
2187
|
|
|
1678
|
-
function
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
2188
|
+
function safeArrayItems(value: unknown): readonly unknown[] {
|
|
2189
|
+
try {
|
|
2190
|
+
if (!Array.isArray(value)) return [];
|
|
2191
|
+
const items: unknown[] = [];
|
|
2192
|
+
const { length } = value;
|
|
2193
|
+
for (let index = 0; index < length; index += 1) {
|
|
2194
|
+
try {
|
|
2195
|
+
items.push(value[index]);
|
|
2196
|
+
} catch {
|
|
2197
|
+
// Treat an inaccessible aggregate item as no signal for that item while
|
|
2198
|
+
// preserving other readable items in the same aggregate branch.
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
return items;
|
|
2202
|
+
} catch {
|
|
2203
|
+
return [];
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
function safeExecutorAggregateErrorItems(error: unknown): readonly unknown[] {
|
|
2208
|
+
if (error === null || (typeof error !== "object" && typeof error !== "function")) return [];
|
|
2209
|
+
const errors = safeGetProperty(error, "errors");
|
|
2210
|
+
return errors.ok ? safeArrayItems(errors.value) : [];
|
|
1684
2211
|
}
|
|
1685
2212
|
|
|
1686
2213
|
function isAggregateWrapper(error: unknown): boolean {
|
|
1687
|
-
return
|
|
2214
|
+
return safeExecutorAggregateErrorItems(error).length > 0;
|
|
1688
2215
|
}
|
|
1689
2216
|
|
|
1690
2217
|
function aggregateInnerFailures(
|
|
1691
2218
|
error: unknown,
|
|
1692
2219
|
classifyFailure: (error: unknown) => WorkflowFailure,
|
|
1693
2220
|
): readonly WorkflowFailure[] {
|
|
1694
|
-
return
|
|
2221
|
+
return safeExecutorAggregateErrorItems(error).map((innerError) => classifyFailure(innerError));
|
|
1695
2222
|
}
|
|
1696
2223
|
|
|
1697
2224
|
type StageFailureCandidate = {
|
|
@@ -2062,7 +2589,6 @@ function finalizeKilled(
|
|
|
2062
2589
|
resumable: false,
|
|
2063
2590
|
};
|
|
2064
2591
|
const recorded = activeStore.recordRunEnd(runId, "killed", undefined, errorMessage, metadata);
|
|
2065
|
-
onRunEnd?.(runId, "killed", undefined, errorMessage);
|
|
2066
2592
|
appendRunEndWhenRecorded(persistence, recorded, {
|
|
2067
2593
|
runId,
|
|
2068
2594
|
status: "killed",
|
|
@@ -2070,12 +2596,10 @@ function finalizeKilled(
|
|
|
2070
2596
|
...metadata,
|
|
2071
2597
|
ts: Date.now(),
|
|
2072
2598
|
});
|
|
2073
|
-
return {
|
|
2074
|
-
runId,
|
|
2599
|
+
return reconcileTerminalRunResult(runId, runSnapshot, activeStore, {
|
|
2075
2600
|
status: "killed",
|
|
2076
2601
|
error: errorMessage,
|
|
2077
|
-
|
|
2078
|
-
};
|
|
2602
|
+
}, onRunEnd);
|
|
2079
2603
|
}
|
|
2080
2604
|
|
|
2081
2605
|
function finalizeKilledByFailure(
|
|
@@ -2087,7 +2611,6 @@ function finalizeKilledByFailure(
|
|
|
2087
2611
|
metadata: RunFailureMetadata,
|
|
2088
2612
|
): RunResult {
|
|
2089
2613
|
const recorded = activeStore.recordRunEnd(runId, "killed", undefined, metadata.errorMessage, metadata);
|
|
2090
|
-
onRunEnd?.(runId, "killed", undefined, metadata.errorMessage);
|
|
2091
2614
|
appendRunEndWhenRecorded(persistence, recorded, {
|
|
2092
2615
|
runId,
|
|
2093
2616
|
status: "killed",
|
|
@@ -2102,12 +2625,10 @@ function finalizeKilledByFailure(
|
|
|
2102
2625
|
...(metadata.retryAfterMs !== undefined ? { retryAfterMs: metadata.retryAfterMs } : {}),
|
|
2103
2626
|
ts: Date.now(),
|
|
2104
2627
|
});
|
|
2105
|
-
return {
|
|
2106
|
-
runId,
|
|
2628
|
+
return reconcileTerminalRunResult(runId, runSnapshot, activeStore, {
|
|
2107
2629
|
status: "killed",
|
|
2108
2630
|
error: metadata.errorMessage,
|
|
2109
|
-
|
|
2110
|
-
};
|
|
2631
|
+
}, onRunEnd);
|
|
2111
2632
|
}
|
|
2112
2633
|
|
|
2113
2634
|
function recordActiveBlockedFailure(
|
|
@@ -2232,9 +2753,10 @@ function assertWorkflowOutputsExplicit(
|
|
|
2232
2753
|
}
|
|
2233
2754
|
}
|
|
2234
2755
|
|
|
2235
|
-
function
|
|
2756
|
+
function normalizeWorkflowOutputObject(
|
|
2236
2757
|
workflowName: string,
|
|
2237
2758
|
rawOutput: unknown,
|
|
2759
|
+
label: string,
|
|
2238
2760
|
): WorkflowOutputValues | undefined {
|
|
2239
2761
|
if (rawOutput === undefined) return undefined;
|
|
2240
2762
|
// Drop top-level keys explicitly set to `undefined` so conditional outputs
|
|
@@ -2247,10 +2769,33 @@ function normalizeWorkflowRunOutput(
|
|
|
2247
2769
|
Object.entries(rawOutput as Record<string, unknown>).filter(([, v]) => v !== undefined),
|
|
2248
2770
|
)
|
|
2249
2771
|
: rawOutput;
|
|
2250
|
-
assertWorkflowSerializableObject(normalized, `workflow "${workflowName}"
|
|
2772
|
+
assertWorkflowSerializableObject(normalized, `workflow "${workflowName}" ${label}`);
|
|
2251
2773
|
return normalized;
|
|
2252
2774
|
}
|
|
2253
2775
|
|
|
2776
|
+
function normalizeWorkflowRunOutput(
|
|
2777
|
+
workflowName: string,
|
|
2778
|
+
rawOutput: unknown,
|
|
2779
|
+
): WorkflowOutputValues | undefined {
|
|
2780
|
+
return normalizeWorkflowOutputObject(workflowName, rawOutput, ".run() return");
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
function normalizeWorkflowExitOutput(
|
|
2784
|
+
workflowName: string,
|
|
2785
|
+
snapshot: WorkflowExitOutputSnapshot | undefined,
|
|
2786
|
+
): WorkflowOutputValues | undefined {
|
|
2787
|
+
if (snapshot === undefined) return undefined;
|
|
2788
|
+
if (!snapshot.ok) throw snapshot.error;
|
|
2789
|
+
if (isWorkflowExitSnapshotInvalidValue(snapshot.value)) {
|
|
2790
|
+
const invalidMessage = workflowExitSnapshotInvalidValueMessage(
|
|
2791
|
+
`workflow "${workflowName}" ctx.exit() outputs`,
|
|
2792
|
+
snapshot.value,
|
|
2793
|
+
);
|
|
2794
|
+
throw new Error(`atomic-workflows: ${invalidMessage ?? `workflow "${workflowName}" ctx.exit() outputs must be ${WORKFLOW_SERIALIZABLE_DESCRIPTION}, got object`}`);
|
|
2795
|
+
}
|
|
2796
|
+
return normalizeWorkflowOutputObject(workflowName, snapshot.value, "ctx.exit() outputs");
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2254
2799
|
function assertWorkflowRunOutputs(
|
|
2255
2800
|
workflowName: string,
|
|
2256
2801
|
result: WorkflowOutputValues | undefined,
|
|
@@ -2263,6 +2808,50 @@ function assertWorkflowRunOutputs(
|
|
|
2263
2808
|
);
|
|
2264
2809
|
}
|
|
2265
2810
|
|
|
2811
|
+
function assertWorkflowExitOutputs(
|
|
2812
|
+
workflowName: string,
|
|
2813
|
+
result: WorkflowOutputValues | undefined,
|
|
2814
|
+
declaredOutputs: Readonly<Record<string, WorkflowOutputSchema>> | undefined,
|
|
2815
|
+
): void {
|
|
2816
|
+
const declarations = declaredOutputs ?? {};
|
|
2817
|
+
const sourceOutput = result ?? {};
|
|
2818
|
+
const scope = `workflow "${workflowName}" ctx.exit()`;
|
|
2819
|
+
for (const key of Object.keys(sourceOutput)) {
|
|
2820
|
+
if (!hasOwnWorkflowOutput(declarations, key)) {
|
|
2821
|
+
throw new Error(
|
|
2822
|
+
`atomic-workflows: ${scope} provided undeclared output "${key}"; declare it with .output("${key}", Type....) or remove it from ctx.exit({ outputs })`,
|
|
2823
|
+
);
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
for (const [key, schema] of Object.entries(declarations)) {
|
|
2827
|
+
if (!(key in sourceOutput)) continue;
|
|
2828
|
+
const value = sourceOutput[key];
|
|
2829
|
+
const invalidSnapshotValue = workflowExitSnapshotInvalidValueMessage(`${scope} output "${key}"`, value);
|
|
2830
|
+
if (invalidSnapshotValue !== undefined) {
|
|
2831
|
+
throw new Error(`atomic-workflows: ${invalidSnapshotValue}`);
|
|
2832
|
+
}
|
|
2833
|
+
const kind = schemaFieldKind(schema);
|
|
2834
|
+
if (!Value.Check(schema, value)) {
|
|
2835
|
+
const choices = schemaChoices(schema);
|
|
2836
|
+
if (kind === "select" && choices !== undefined && typeof value === "string") {
|
|
2837
|
+
throw new Error(
|
|
2838
|
+
`atomic-workflows: ${scope} output "${key}" must be one of [${choices.join(", ")}], got ${JSON.stringify(value)}`,
|
|
2839
|
+
);
|
|
2840
|
+
}
|
|
2841
|
+
throw new Error(
|
|
2842
|
+
`atomic-workflows: ${scope} output "${key}" expected ${kind}, got ${workflowSerializableTypeName(value)}`,
|
|
2843
|
+
);
|
|
2844
|
+
}
|
|
2845
|
+
const serializableError = workflowSerializableValidationError(
|
|
2846
|
+
value,
|
|
2847
|
+
`${scope} output "${key}"`,
|
|
2848
|
+
);
|
|
2849
|
+
if (serializableError !== undefined) {
|
|
2850
|
+
throw new Error(`atomic-workflows: ${serializableError}`);
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2266
2855
|
function selectWorkflowOutputs(
|
|
2267
2856
|
child: WorkflowDefinition,
|
|
2268
2857
|
rawOutput: WorkflowOutputValues | undefined,
|
|
@@ -2317,7 +2906,9 @@ function cloneWorkflowChildReplaySnapshot(snapshot: WorkflowChildReplaySnapshot)
|
|
|
2317
2906
|
workflow: snapshot.workflow,
|
|
2318
2907
|
runId: snapshot.runId,
|
|
2319
2908
|
status: snapshot.status,
|
|
2909
|
+
...(snapshot.exited !== undefined ? { exited: snapshot.exited } : {}),
|
|
2320
2910
|
outputs: cloneWorkflowChildValue(snapshot.outputs),
|
|
2911
|
+
...(snapshot.exitReason !== undefined ? { exitReason: snapshot.exitReason } : {}),
|
|
2321
2912
|
};
|
|
2322
2913
|
}
|
|
2323
2914
|
|
|
@@ -2338,12 +2929,15 @@ function workflowChildReplaySnapshot(
|
|
|
2338
2929
|
}
|
|
2339
2930
|
}
|
|
2340
2931
|
|
|
2932
|
+
const exitReason = childResult.exited === true ? childResult.exitReason : undefined;
|
|
2341
2933
|
return {
|
|
2342
2934
|
alias,
|
|
2343
2935
|
workflow: childResult.workflow,
|
|
2344
2936
|
runId: childResult.runId,
|
|
2345
2937
|
status: childResult.status,
|
|
2938
|
+
exited: childResult.exited,
|
|
2346
2939
|
outputs,
|
|
2940
|
+
...(exitReason !== undefined ? { exitReason } : {}),
|
|
2347
2941
|
};
|
|
2348
2942
|
}
|
|
2349
2943
|
|
|
@@ -2384,6 +2978,8 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
2384
2978
|
|
|
2385
2979
|
// 2. Generate runId (or use pre-allocated seam from caller)
|
|
2386
2980
|
const runId = opts.runId ?? crypto.randomUUID();
|
|
2981
|
+
const exitScope = Symbol(`workflow-exit:${runId}`);
|
|
2982
|
+
let selectedExit: WorkflowExitSignal | undefined;
|
|
2387
2983
|
const replayIndex = createContinuationReplayIndex(opts.continuation);
|
|
2388
2984
|
|
|
2389
2985
|
// 2a. Create own AbortController; forward caller signal if provided
|
|
@@ -2420,7 +3016,16 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
2420
3016
|
const classifyExecutorFailure = (error: unknown): WorkflowFailure => {
|
|
2421
3017
|
const cached = classifiedFailures.get(error);
|
|
2422
3018
|
if (cached !== undefined) return cached;
|
|
2423
|
-
|
|
3019
|
+
let classified: WorkflowFailure;
|
|
3020
|
+
try {
|
|
3021
|
+
classified = classifyWorkflowFailure(error);
|
|
3022
|
+
} catch {
|
|
3023
|
+
// Failure classification can inspect provider-shaped metadata such as
|
|
3024
|
+
// `cause`/`errors`. If an arbitrary workflow-thrown object uses throwing
|
|
3025
|
+
// accessors for those names, keep the executor catch path on the ordinary
|
|
3026
|
+
// failed-run rail instead of letting the accessor escape and strand the run.
|
|
3027
|
+
classified = classifyWorkflowFailure(new Error(unknownErrorMessage(error)));
|
|
3028
|
+
}
|
|
2424
3029
|
classifiedFailures.set(error, classified);
|
|
2425
3030
|
return classified;
|
|
2426
3031
|
};
|
|
@@ -2484,6 +3089,88 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
2484
3089
|
const isTerminalStage = (stage: StageSnapshot): boolean =>
|
|
2485
3090
|
stage.status === "completed" || stage.status === "failed" || stage.status === "skipped";
|
|
2486
3091
|
|
|
3092
|
+
interface WorkflowExitCleanup {
|
|
3093
|
+
skipForWorkflowExit(reason?: string): void | Promise<void>;
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
const exitCleanups = new Map<string, WorkflowExitCleanup>();
|
|
3097
|
+
const workflowExitCleanupPromises = new Set<Promise<void>>();
|
|
3098
|
+
const workflowExitSkippedReason = (reason?: string): string =>
|
|
3099
|
+
reason === undefined || reason.length === 0 ? "workflow-exit" : `workflow-exit: ${reason}`;
|
|
3100
|
+
const isWorkflowExitSkippedReason = (reason: string | undefined): boolean =>
|
|
3101
|
+
reason === "workflow-exit" || reason?.startsWith("workflow-exit: ") === true;
|
|
3102
|
+
const currentWorkflowExitAbortReason = (): { readonly reason?: string } | undefined => {
|
|
3103
|
+
const scopedExit = selectedExit ?? findWorkflowExitSignal(ownController.signal.reason, exitScope);
|
|
3104
|
+
if (scopedExit !== undefined) {
|
|
3105
|
+
return scopedExit.reason === undefined ? {} : { reason: scopedExit.reason };
|
|
3106
|
+
}
|
|
3107
|
+
const parentExit = parentWorkflowExitAbortReason(ownController.signal.reason);
|
|
3108
|
+
if (parentExit !== undefined) {
|
|
3109
|
+
return parentExit.workflowExitReason === undefined ? {} : { reason: parentExit.workflowExitReason };
|
|
3110
|
+
}
|
|
3111
|
+
return undefined;
|
|
3112
|
+
};
|
|
3113
|
+
const preserveWorkflowExitSkippedReason = (stage: StageSnapshot, fallback: string): void => {
|
|
3114
|
+
if (isWorkflowExitSkippedReason(stage.skippedReason)) return;
|
|
3115
|
+
const workflowExitAbort = currentWorkflowExitAbortReason();
|
|
3116
|
+
if (workflowExitAbort !== undefined) {
|
|
3117
|
+
stage.skippedReason = workflowExitSkippedReason(workflowExitAbort.reason);
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
stage.skippedReason = fallback;
|
|
3121
|
+
};
|
|
3122
|
+
const trackWorkflowExitCleanup = (operation: void | Promise<void>): void => {
|
|
3123
|
+
if (operation === undefined) return;
|
|
3124
|
+
let tracked: Promise<void>;
|
|
3125
|
+
tracked = Promise.resolve(operation)
|
|
3126
|
+
.catch(() => {
|
|
3127
|
+
// Cleanup is best-effort and must never surface as an unhandled rejection
|
|
3128
|
+
// or convert an intentional workflow exit into a failed run.
|
|
3129
|
+
})
|
|
3130
|
+
.finally(() => {
|
|
3131
|
+
workflowExitCleanupPromises.delete(tracked);
|
|
3132
|
+
});
|
|
3133
|
+
workflowExitCleanupPromises.add(tracked);
|
|
3134
|
+
};
|
|
3135
|
+
const invokeWorkflowExitCleanup = (cleanup: WorkflowExitCleanup, reason?: string): void => {
|
|
3136
|
+
try {
|
|
3137
|
+
trackWorkflowExitCleanup(cleanup.skipForWorkflowExit(reason));
|
|
3138
|
+
} catch (err) {
|
|
3139
|
+
trackWorkflowExitCleanup(Promise.reject(err));
|
|
3140
|
+
}
|
|
3141
|
+
};
|
|
3142
|
+
const registerWorkflowExitCleanup = (stageId: string, cleanup: WorkflowExitCleanup): (() => void) => {
|
|
3143
|
+
if (selectedExit !== undefined) {
|
|
3144
|
+
invokeWorkflowExitCleanup(cleanup, selectedExit.reason);
|
|
3145
|
+
return () => undefined;
|
|
3146
|
+
}
|
|
3147
|
+
exitCleanups.set(stageId, cleanup);
|
|
3148
|
+
return () => {
|
|
3149
|
+
if (exitCleanups.get(stageId) === cleanup) exitCleanups.delete(stageId);
|
|
3150
|
+
};
|
|
3151
|
+
};
|
|
3152
|
+
const runWorkflowExitCleanups = (reason?: string): void => {
|
|
3153
|
+
for (const cleanup of [...exitCleanups.values()]) {
|
|
3154
|
+
invokeWorkflowExitCleanup(cleanup, reason);
|
|
3155
|
+
}
|
|
3156
|
+
};
|
|
3157
|
+
const drainWorkflowExitCleanups = async (reason?: string): Promise<void> => {
|
|
3158
|
+
runWorkflowExitCleanups(reason);
|
|
3159
|
+
while (workflowExitCleanupPromises.size > 0) {
|
|
3160
|
+
await Promise.all([...workflowExitCleanupPromises]);
|
|
3161
|
+
}
|
|
3162
|
+
};
|
|
3163
|
+
const throwIfWorkflowExitSelected = (): void => {
|
|
3164
|
+
if (selectedExit !== undefined) {
|
|
3165
|
+
if (!ownController.signal.aborted) ownController.abort(selectedExit);
|
|
3166
|
+
runWorkflowExitCleanups(selectedExit.reason);
|
|
3167
|
+
throw selectedExit;
|
|
3168
|
+
}
|
|
3169
|
+
if (ownController.signal.aborted) {
|
|
3170
|
+
throw ownController.signal.reason ?? new DOMException("workflow killed", "AbortError");
|
|
3171
|
+
}
|
|
3172
|
+
};
|
|
3173
|
+
|
|
2487
3174
|
const stageById = (stageId: string): StageSnapshot | undefined =>
|
|
2488
3175
|
runSnapshot.stages.find((stage) => stage.id === stageId);
|
|
2489
3176
|
|
|
@@ -2623,21 +3310,161 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
2623
3310
|
{ once: true },
|
|
2624
3311
|
);
|
|
2625
3312
|
|
|
3313
|
+
const finalizeWorkflowExitValidationFailure = (err: unknown, exitReason?: string): RunResult => {
|
|
3314
|
+
const failure = classifyExecutorFailure(err);
|
|
3315
|
+
const classifiedMetadata = runFailureMetadata(failure, runSnapshot.stages);
|
|
3316
|
+
const metadata = {
|
|
3317
|
+
...classifiedMetadata,
|
|
3318
|
+
// A selected ctx.exit has already unwound the workflow and run exit cleanup;
|
|
3319
|
+
// invalid exit options/outputs must never be offered as resumable snapshots.
|
|
3320
|
+
resumable: false,
|
|
3321
|
+
...(exitReason !== undefined ? { exitReason } : {}),
|
|
3322
|
+
} as const;
|
|
3323
|
+
const recorded = activeStore.recordRunEnd(runId, "failed", undefined, metadata.errorMessage, metadata);
|
|
3324
|
+
appendRunEndWhenRecorded(opts.persistence, recorded, {
|
|
3325
|
+
runId,
|
|
3326
|
+
status: "failed",
|
|
3327
|
+
error: metadata.errorMessage,
|
|
3328
|
+
failureKind: metadata.failureKind,
|
|
3329
|
+
...(metadata.failureCode !== undefined ? { failureCode: metadata.failureCode } : {}),
|
|
3330
|
+
...(metadata.failureRecoverability !== undefined ? { failureRecoverability: metadata.failureRecoverability } : {}),
|
|
3331
|
+
...(metadata.failureDisposition !== undefined ? { failureDisposition: metadata.failureDisposition } : {}),
|
|
3332
|
+
failureMessage: metadata.failureMessage,
|
|
3333
|
+
...(metadata.failedStageId !== undefined ? { failedStageId: metadata.failedStageId } : {}),
|
|
3334
|
+
resumable: false,
|
|
3335
|
+
...(metadata.exitReason !== undefined ? { exitReason: metadata.exitReason } : {}),
|
|
3336
|
+
...(metadata.retryAfterMs !== undefined ? { retryAfterMs: metadata.retryAfterMs } : {}),
|
|
3337
|
+
ts: Date.now(),
|
|
3338
|
+
});
|
|
3339
|
+
return reconcileTerminalRunResult(runId, runSnapshot, activeStore, {
|
|
3340
|
+
status: "failed",
|
|
3341
|
+
error: metadata.errorMessage,
|
|
3342
|
+
...(metadata.exitReason !== undefined ? { exitReason: metadata.exitReason } : {}),
|
|
3343
|
+
}, opts.onRunEnd);
|
|
3344
|
+
};
|
|
3345
|
+
|
|
3346
|
+
const finalizeWorkflowExit = async (signal: WorkflowExitSignal): Promise<RunResult> => {
|
|
3347
|
+
await drainWorkflowExitCleanups(signal.reason);
|
|
3348
|
+
if (signal.validationError !== undefined) {
|
|
3349
|
+
return finalizeWorkflowExitValidationFailure(signal.validationError, signal.reason);
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
let outputs: WorkflowOutputValues | undefined;
|
|
3353
|
+
try {
|
|
3354
|
+
outputs = normalizeWorkflowExitOutput(def.name, signal.outputSnapshot);
|
|
3355
|
+
assertWorkflowExitOutputs(def.name, outputs, def.outputs);
|
|
3356
|
+
} catch (err) {
|
|
3357
|
+
return finalizeWorkflowExitValidationFailure(err, signal.reason);
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
const metadata = {
|
|
3361
|
+
resumable: false,
|
|
3362
|
+
exited: true,
|
|
3363
|
+
...(signal.reason !== undefined ? { exitReason: signal.reason } : {}),
|
|
3364
|
+
} as const;
|
|
3365
|
+
const recorded = activeStore.recordRunEnd(runId, signal.status, outputs, undefined, metadata);
|
|
3366
|
+
appendRunEndWhenRecorded(opts.persistence, recorded, {
|
|
3367
|
+
runId,
|
|
3368
|
+
status: signal.status,
|
|
3369
|
+
result: outputs,
|
|
3370
|
+
exited: true,
|
|
3371
|
+
...(signal.reason !== undefined ? { exitReason: signal.reason } : {}),
|
|
3372
|
+
resumable: false,
|
|
3373
|
+
ts: Date.now(),
|
|
3374
|
+
});
|
|
3375
|
+
return reconcileTerminalRunResult(runId, runSnapshot, activeStore, {
|
|
3376
|
+
status: signal.status,
|
|
3377
|
+
result: outputs,
|
|
3378
|
+
exited: true,
|
|
3379
|
+
...(signal.reason !== undefined ? { exitReason: signal.reason } : {}),
|
|
3380
|
+
}, opts.onRunEnd);
|
|
3381
|
+
};
|
|
3382
|
+
|
|
3383
|
+
const finalizeParentWorkflowExitCancellation = async (abortReason: ParentWorkflowExitAbortProbe): Promise<RunResult> => {
|
|
3384
|
+
const parentReason = abortReason.workflowExitReason;
|
|
3385
|
+
await drainWorkflowExitCleanups(parentReason);
|
|
3386
|
+
const exitReason = parentWorkflowExitRunReason(parentReason);
|
|
3387
|
+
const metadata = {
|
|
3388
|
+
resumable: false,
|
|
3389
|
+
exited: true,
|
|
3390
|
+
exitReason,
|
|
3391
|
+
} as const;
|
|
3392
|
+
const recorded = activeStore.recordRunEnd(runId, "cancelled", undefined, undefined, metadata);
|
|
3393
|
+
appendRunEndWhenRecorded(opts.persistence, recorded, {
|
|
3394
|
+
runId,
|
|
3395
|
+
status: "cancelled",
|
|
3396
|
+
exited: true,
|
|
3397
|
+
exitReason,
|
|
3398
|
+
resumable: false,
|
|
3399
|
+
ts: Date.now(),
|
|
3400
|
+
});
|
|
3401
|
+
return reconcileTerminalRunResult(runId, runSnapshot, activeStore, {
|
|
3402
|
+
status: "cancelled",
|
|
3403
|
+
exited: true,
|
|
3404
|
+
exitReason,
|
|
3405
|
+
}, opts.onRunEnd);
|
|
3406
|
+
};
|
|
3407
|
+
|
|
3408
|
+
interface LinkedChildWorkflowExitState {
|
|
3409
|
+
readonly ref: WorkflowChildRunRef;
|
|
3410
|
+
readonly controller: AbortController;
|
|
3411
|
+
runPromise?: Promise<RunResult>;
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
const requestLinkedChildWorkflowExit = (
|
|
3415
|
+
linkedChild: LinkedChildWorkflowExitState,
|
|
3416
|
+
reason?: string,
|
|
3417
|
+
): void => {
|
|
3418
|
+
if (!linkedChild.controller.signal.aborted) {
|
|
3419
|
+
linkedChild.controller.abort(makeParentWorkflowExitAbortReason(reason));
|
|
3420
|
+
}
|
|
3421
|
+
};
|
|
3422
|
+
|
|
3423
|
+
const waitForLinkedChildWorkflowExit = async (
|
|
3424
|
+
linkedChild: LinkedChildWorkflowExitState,
|
|
3425
|
+
): Promise<void> => {
|
|
3426
|
+
const childRun = linkedChild.runPromise;
|
|
3427
|
+
if (childRun === undefined) return;
|
|
3428
|
+
try {
|
|
3429
|
+
await childRun;
|
|
3430
|
+
} catch {
|
|
3431
|
+
// The child workflow call itself observes and reports failures. Parent
|
|
3432
|
+
// exit cleanup only needs to await child-owned teardown and must not leak
|
|
3433
|
+
// an unhandled rejection while the parent is already intentionally exiting.
|
|
3434
|
+
}
|
|
3435
|
+
};
|
|
3436
|
+
|
|
2626
3437
|
interface WorkflowBoundaryStage {
|
|
2627
3438
|
readonly id: string;
|
|
2628
3439
|
readonly replayedChild?: WorkflowChildResult;
|
|
2629
3440
|
finalizeReplay(): void;
|
|
2630
|
-
linkChildRun(ref: WorkflowChildRunRef): void;
|
|
3441
|
+
linkChildRun(ref: WorkflowChildRunRef, childController: AbortController): void;
|
|
3442
|
+
observeChildRun(promise: Promise<RunResult>): void;
|
|
2631
3443
|
complete(summary: string, workflowChild: WorkflowChildReplaySnapshot): void;
|
|
3444
|
+
skipForWorkflowExit(reason?: string): Promise<void>;
|
|
2632
3445
|
fail(error: unknown): void;
|
|
2633
3446
|
}
|
|
2634
3447
|
|
|
2635
|
-
const workflowChildResultFromReplay = (snapshot: WorkflowChildReplaySnapshot): WorkflowChildResult =>
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
3448
|
+
const workflowChildResultFromReplay = (snapshot: WorkflowChildReplaySnapshot): WorkflowChildResult => {
|
|
3449
|
+
const outputs = cloneWorkflowChildValue(snapshot.outputs);
|
|
3450
|
+
if (snapshot.exited === true || snapshot.status !== "completed") {
|
|
3451
|
+
return {
|
|
3452
|
+
workflow: snapshot.workflow,
|
|
3453
|
+
runId: snapshot.runId,
|
|
3454
|
+
status: snapshot.status,
|
|
3455
|
+
exited: true,
|
|
3456
|
+
outputs,
|
|
3457
|
+
...(snapshot.exitReason !== undefined ? { exitReason: snapshot.exitReason } : {}),
|
|
3458
|
+
};
|
|
3459
|
+
}
|
|
3460
|
+
return {
|
|
3461
|
+
workflow: snapshot.workflow,
|
|
3462
|
+
runId: snapshot.runId,
|
|
3463
|
+
status: "completed",
|
|
3464
|
+
exited: false,
|
|
3465
|
+
outputs,
|
|
3466
|
+
};
|
|
3467
|
+
};
|
|
2641
3468
|
|
|
2642
3469
|
const workflowBoundaryReplayCounts = new Map<string, number>();
|
|
2643
3470
|
const nextWorkflowBoundaryReplayKey = (name: string): string => {
|
|
@@ -2687,6 +3514,8 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
2687
3514
|
} : {}),
|
|
2688
3515
|
};
|
|
2689
3516
|
let finalized = false;
|
|
3517
|
+
let unregisterWorkflowExitCleanup = (): void => {};
|
|
3518
|
+
let linkedChild: LinkedChildWorkflowExitState | undefined;
|
|
2690
3519
|
|
|
2691
3520
|
const appendStageStartOnce = (): void => {
|
|
2692
3521
|
if (!opts.persistence) return;
|
|
@@ -2714,25 +3543,38 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
2714
3543
|
...(stageSnapshot.failureDisposition !== undefined ? { failureDisposition: stageSnapshot.failureDisposition } : {}),
|
|
2715
3544
|
...(stageSnapshot.failureMessage !== undefined ? { failureMessage: stageSnapshot.failureMessage } : {}),
|
|
2716
3545
|
...(stageSnapshot.retryAfterMs !== undefined ? { retryAfterMs: stageSnapshot.retryAfterMs } : {}),
|
|
3546
|
+
...(stageSnapshot.skippedReason !== undefined ? { skippedReason: stageSnapshot.skippedReason } : {}),
|
|
2717
3547
|
...(stageSnapshot.result !== undefined && stageSnapshot.status === "completed" ? { summary: stageSnapshot.result } : {}),
|
|
2718
3548
|
...stageReplayFields(stageSnapshot),
|
|
2719
|
-
...(stageSnapshot.
|
|
3549
|
+
...(stageSnapshot.status === "completed" && stageSnapshot.workflowChild !== undefined
|
|
3550
|
+
? { workflowChild: stageSnapshot.workflowChild }
|
|
3551
|
+
: {}),
|
|
2720
3552
|
});
|
|
2721
3553
|
};
|
|
2722
3554
|
|
|
3555
|
+
const clearBoundaryChildMetadata = (): void => {
|
|
3556
|
+
delete stageSnapshot.workflowChildRun;
|
|
3557
|
+
delete stageSnapshot.workflowChild;
|
|
3558
|
+
};
|
|
3559
|
+
|
|
2723
3560
|
const finalize = (
|
|
2724
|
-
status: "completed" | "failed",
|
|
3561
|
+
status: "completed" | "failed" | "skipped",
|
|
2725
3562
|
summaryOrError: string,
|
|
2726
3563
|
workflowChild?: WorkflowChildReplaySnapshot,
|
|
2727
3564
|
failureError?: unknown,
|
|
2728
3565
|
): void => {
|
|
2729
3566
|
if (finalized) return;
|
|
2730
3567
|
finalized = true;
|
|
3568
|
+
unregisterWorkflowExitCleanup();
|
|
2731
3569
|
stageSnapshot.status = status;
|
|
2732
3570
|
if (status === "completed") {
|
|
2733
3571
|
stageSnapshot.result = summaryOrError;
|
|
2734
3572
|
if (workflowChild !== undefined) stageSnapshot.workflowChild = workflowChild;
|
|
3573
|
+
} else if (status === "skipped") {
|
|
3574
|
+
clearBoundaryChildMetadata();
|
|
3575
|
+
stageSnapshot.skippedReason = summaryOrError;
|
|
2735
3576
|
} else {
|
|
3577
|
+
clearBoundaryChildMetadata();
|
|
2736
3578
|
applyFailureToStage(stageSnapshot, classifyExecutorFailure(failureError));
|
|
2737
3579
|
}
|
|
2738
3580
|
stageSnapshot.endedAt = Date.now();
|
|
@@ -2747,29 +3589,60 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
2747
3589
|
opts.onStageStart?.(runId, stageSnapshot);
|
|
2748
3590
|
appendStageStartOnce();
|
|
2749
3591
|
|
|
3592
|
+
unregisterWorkflowExitCleanup = registerWorkflowExitCleanup(stageId, {
|
|
3593
|
+
async skipForWorkflowExit(reason?: string): Promise<void> {
|
|
3594
|
+
const child = linkedChild;
|
|
3595
|
+
if (child !== undefined) {
|
|
3596
|
+
requestLinkedChildWorkflowExit(child, reason);
|
|
3597
|
+
}
|
|
3598
|
+
finalize("skipped", workflowExitSkippedReason(reason));
|
|
3599
|
+
if (child !== undefined) {
|
|
3600
|
+
await waitForLinkedChildWorkflowExit(child);
|
|
3601
|
+
}
|
|
3602
|
+
},
|
|
3603
|
+
});
|
|
3604
|
+
|
|
2750
3605
|
const finalizeReplay = (): void => {
|
|
2751
3606
|
if (replayedChild === undefined || finalized) return;
|
|
2752
3607
|
finalized = true;
|
|
3608
|
+
unregisterWorkflowExitCleanup();
|
|
2753
3609
|
activeStore.recordStageEnd(runId, stageSnapshot);
|
|
2754
3610
|
opts.onStageEnd?.(runId, stageSnapshot);
|
|
2755
3611
|
appendStageEndForSnapshot();
|
|
2756
3612
|
tracker.onSettle(stageId);
|
|
2757
3613
|
};
|
|
2758
3614
|
|
|
2759
|
-
const linkChildRun = (ref: WorkflowChildRunRef): void => {
|
|
3615
|
+
const linkChildRun = (ref: WorkflowChildRunRef, childController: AbortController): void => {
|
|
2760
3616
|
if (finalized) return;
|
|
3617
|
+
linkedChild = { ref: { ...ref }, controller: childController };
|
|
2761
3618
|
stageSnapshot.workflowChildRun = { ...ref };
|
|
2762
3619
|
activeStore.recordStageWorkflowChildRun(runId, stageId, ref);
|
|
2763
3620
|
};
|
|
2764
3621
|
|
|
3622
|
+
const observeChildRun = (promise: Promise<RunResult>): void => {
|
|
3623
|
+
if (linkedChild === undefined || finalized) return;
|
|
3624
|
+
linkedChild.runPromise = promise;
|
|
3625
|
+
};
|
|
3626
|
+
|
|
2765
3627
|
return {
|
|
2766
3628
|
id: stageId,
|
|
2767
3629
|
...(replayedChild !== undefined ? { replayedChild } : {}),
|
|
2768
3630
|
finalizeReplay,
|
|
2769
3631
|
linkChildRun,
|
|
3632
|
+
observeChildRun,
|
|
2770
3633
|
complete(summary: string, workflowChild: WorkflowChildReplaySnapshot): void {
|
|
2771
3634
|
finalize("completed", summary, workflowChild);
|
|
2772
3635
|
},
|
|
3636
|
+
async skipForWorkflowExit(reason?: string): Promise<void> {
|
|
3637
|
+
const child = linkedChild;
|
|
3638
|
+
if (child !== undefined) {
|
|
3639
|
+
requestLinkedChildWorkflowExit(child, reason);
|
|
3640
|
+
}
|
|
3641
|
+
finalize("skipped", workflowExitSkippedReason(reason));
|
|
3642
|
+
if (child !== undefined) {
|
|
3643
|
+
await waitForLinkedChildWorkflowExit(child);
|
|
3644
|
+
}
|
|
3645
|
+
},
|
|
2773
3646
|
fail(error: unknown): void {
|
|
2774
3647
|
finalize("failed", error instanceof Error ? error.message : String(error), undefined, error);
|
|
2775
3648
|
},
|
|
@@ -2778,6 +3651,7 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
2778
3651
|
|
|
2779
3652
|
const buildPromptNodeUiAdapter = (): WorkflowUIContext => {
|
|
2780
3653
|
const ask = async <T>(descriptor: PromptDescriptor<T>): Promise<unknown> => {
|
|
3654
|
+
throwIfWorkflowExitSelected();
|
|
2781
3655
|
const isCustom = isCustomPromptDescriptor(descriptor);
|
|
2782
3656
|
if (ownController.signal.aborted) {
|
|
2783
3657
|
if (isCustom) throw hilAbortError(ownController.signal);
|
|
@@ -2836,9 +3710,11 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
2836
3710
|
} : {}),
|
|
2837
3711
|
};
|
|
2838
3712
|
let finalized = false;
|
|
3713
|
+
let unregisterWorkflowExitCleanup = (): void => {};
|
|
2839
3714
|
const finalizePromptStage = (status: "completed" | "failed" | "skipped"): void => {
|
|
2840
3715
|
if (finalized) return;
|
|
2841
3716
|
finalized = true;
|
|
3717
|
+
unregisterWorkflowExitCleanup();
|
|
2842
3718
|
stageSnapshot.status = status;
|
|
2843
3719
|
stageSnapshot.endedAt = Date.now();
|
|
2844
3720
|
stageSnapshot.durationMs = elapsedStageMs(stageSnapshot, stageSnapshot.endedAt);
|
|
@@ -2867,6 +3743,20 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
2867
3743
|
|
|
2868
3744
|
activeStore.recordStageStart(runId, stageSnapshot);
|
|
2869
3745
|
opts.onStageStart?.(runId, stageSnapshot);
|
|
3746
|
+
unregisterWorkflowExitCleanup = registerWorkflowExitCleanup(stageId, {
|
|
3747
|
+
skipForWorkflowExit(reason?: string): void {
|
|
3748
|
+
if (finalized) return;
|
|
3749
|
+
stageSnapshot.skippedReason = workflowExitSkippedReason(reason);
|
|
3750
|
+
if (!shouldReplay) {
|
|
3751
|
+
stageUiBroker.cancelStagePrompt(
|
|
3752
|
+
runId,
|
|
3753
|
+
stageId,
|
|
3754
|
+
new Error(`atomic-workflows: prompt ${stageId} skipped by workflow exit`),
|
|
3755
|
+
);
|
|
3756
|
+
}
|
|
3757
|
+
finalizePromptStage("skipped");
|
|
3758
|
+
},
|
|
3759
|
+
});
|
|
2870
3760
|
if (opts.persistence) {
|
|
2871
3761
|
appendStageStart(opts.persistence, {
|
|
2872
3762
|
runId,
|
|
@@ -2879,6 +3769,7 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
2879
3769
|
}
|
|
2880
3770
|
if (shouldReplay) {
|
|
2881
3771
|
await Promise.resolve();
|
|
3772
|
+
throwIfWorkflowExitSelected();
|
|
2882
3773
|
finalizePromptStage("completed");
|
|
2883
3774
|
return replayAnswer.value;
|
|
2884
3775
|
}
|
|
@@ -2917,7 +3808,10 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
2917
3808
|
activeStore.recordStageAwaitingInput(runId, stageId, false);
|
|
2918
3809
|
stageUiBroker.cancelStagePrompt(runId, stageId, err);
|
|
2919
3810
|
if (mergedSignal.signal.aborted) {
|
|
2920
|
-
|
|
3811
|
+
preserveWorkflowExitSkippedReason(
|
|
3812
|
+
stageSnapshot,
|
|
3813
|
+
ownController.signal.aborted ? "run-aborted" : "prompt-aborted",
|
|
3814
|
+
);
|
|
2921
3815
|
finalizePromptStage("skipped");
|
|
2922
3816
|
throw hilAbortError(mergedSignal.signal);
|
|
2923
3817
|
}
|
|
@@ -2971,7 +3865,7 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
2971
3865
|
return response;
|
|
2972
3866
|
} catch (err) {
|
|
2973
3867
|
if (ownController.signal.aborted) {
|
|
2974
|
-
stageSnapshot
|
|
3868
|
+
preserveWorkflowExitSkippedReason(stageSnapshot, "run-aborted");
|
|
2975
3869
|
finalizePromptStage("skipped");
|
|
2976
3870
|
} else {
|
|
2977
3871
|
applyFailureToStage(stageSnapshot, classifyExecutorFailure(err));
|
|
@@ -3015,15 +3909,121 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3015
3909
|
};
|
|
3016
3910
|
};
|
|
3017
3911
|
|
|
3912
|
+
const buildExitGatedUiContext = (): WorkflowUIContext => {
|
|
3913
|
+
// Headless (non-interactive) runs without an adapter get a context whose
|
|
3914
|
+
// interactive primitives fail with a clear "unavailable in headless mode"
|
|
3915
|
+
// error instead of a raw TypeError (#1339).
|
|
3916
|
+
const base = opts.usePromptNodesForUi === true
|
|
3917
|
+
? buildPromptNodeUiAdapter()
|
|
3918
|
+
: opts.executionMode === "non_interactive" && opts.ui === undefined
|
|
3919
|
+
? makeHeadlessUnavailableUIContext()
|
|
3920
|
+
: normalizeUIContext(opts.ui);
|
|
3921
|
+
return {
|
|
3922
|
+
async input(promptText: string): Promise<string> {
|
|
3923
|
+
throwIfWorkflowExitSelected();
|
|
3924
|
+
return await base.input(promptText);
|
|
3925
|
+
},
|
|
3926
|
+
async confirm(message: string): Promise<boolean> {
|
|
3927
|
+
throwIfWorkflowExitSelected();
|
|
3928
|
+
return await base.confirm(message);
|
|
3929
|
+
},
|
|
3930
|
+
async select<T extends string>(message: string, options: readonly T[]): Promise<T> {
|
|
3931
|
+
throwIfWorkflowExitSelected();
|
|
3932
|
+
return await base.select(message, options);
|
|
3933
|
+
},
|
|
3934
|
+
async editor(initial?: string): Promise<string> {
|
|
3935
|
+
throwIfWorkflowExitSelected();
|
|
3936
|
+
return await base.editor(initial);
|
|
3937
|
+
},
|
|
3938
|
+
async custom<T>(factory: WorkflowCustomUiFactory<T>, options?: WorkflowCustomUiOptions): Promise<T> {
|
|
3939
|
+
throwIfWorkflowExitSelected();
|
|
3940
|
+
return await base.custom(factory, options);
|
|
3941
|
+
},
|
|
3942
|
+
};
|
|
3943
|
+
};
|
|
3944
|
+
|
|
3018
3945
|
// 5. Build WorkflowRunContext
|
|
3019
3946
|
const ctx: WorkflowRunContext<TInputs> = {
|
|
3020
3947
|
inputs: resolvedInputs as TInputs,
|
|
3021
3948
|
get cwd() { return resolveWorkflowCwd(); },
|
|
3949
|
+
exit(options?: WorkflowExitOptions): never {
|
|
3950
|
+
if (selectedExit !== undefined) {
|
|
3951
|
+
if (!ownController.signal.aborted) ownController.abort(selectedExit);
|
|
3952
|
+
runWorkflowExitCleanups(selectedExit.reason);
|
|
3953
|
+
throw selectedExit;
|
|
3954
|
+
}
|
|
3955
|
+
if (ownController.signal.aborted) {
|
|
3956
|
+
throw ownController.signal.reason ?? new DOMException("workflow killed", "AbortError");
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
const throwNestedSelectedExit = (): void => {
|
|
3960
|
+
if (selectedExit === undefined) return;
|
|
3961
|
+
if (!ownController.signal.aborted) ownController.abort(selectedExit);
|
|
3962
|
+
runWorkflowExitCleanups(selectedExit.reason);
|
|
3963
|
+
throw selectedExit;
|
|
3964
|
+
};
|
|
3965
|
+
const rawOptions = options as { readonly status?: unknown; readonly reason?: unknown; readonly outputs?: unknown } | null | undefined;
|
|
3966
|
+
let validationError: Error | undefined;
|
|
3967
|
+
const captureValidationError = (error: Error): void => {
|
|
3968
|
+
validationError ??= error;
|
|
3969
|
+
};
|
|
3970
|
+
|
|
3971
|
+
const statusRead = readWorkflowExitOption(rawOptions, "status");
|
|
3972
|
+
throwNestedSelectedExit();
|
|
3973
|
+
const rawStatus = statusRead.ok ? statusRead.value ?? "completed" : "completed";
|
|
3974
|
+
if (!statusRead.ok) {
|
|
3975
|
+
captureValidationError(statusRead.error);
|
|
3976
|
+
} else if (!isWorkflowExitStatus(rawStatus)) {
|
|
3977
|
+
captureValidationError(new TypeError(
|
|
3978
|
+
`atomic-workflows: ctx.exit() status must be one of completed, skipped, cancelled, blocked; got ${describeWorkflowExitOptionValue(rawStatus)}`,
|
|
3979
|
+
));
|
|
3980
|
+
}
|
|
3981
|
+
const status = isWorkflowExitStatus(rawStatus) ? rawStatus : "completed";
|
|
3982
|
+
|
|
3983
|
+
const reasonRead = readWorkflowExitOption(rawOptions, "reason");
|
|
3984
|
+
throwNestedSelectedExit();
|
|
3985
|
+
const rawReason = reasonRead.ok ? reasonRead.value : undefined;
|
|
3986
|
+
if (!reasonRead.ok) {
|
|
3987
|
+
captureValidationError(reasonRead.error);
|
|
3988
|
+
} else if (rawReason !== undefined && typeof rawReason !== "string") {
|
|
3989
|
+
captureValidationError(new TypeError(
|
|
3990
|
+
`atomic-workflows: ctx.exit() reason must be a string when provided; got ${workflowSerializableTypeName(rawReason)}`,
|
|
3991
|
+
));
|
|
3992
|
+
}
|
|
3993
|
+
const reason = typeof rawReason === "string" ? rawReason : undefined;
|
|
3994
|
+
|
|
3995
|
+
const outputsRead = readWorkflowExitOption(rawOptions, "outputs");
|
|
3996
|
+
throwNestedSelectedExit();
|
|
3997
|
+
const outputSnapshot = !outputsRead.ok
|
|
3998
|
+
? freezeWorkflowExitOutputSnapshot({ ok: false, error: outputsRead.error })
|
|
3999
|
+
: outputsRead.value !== undefined
|
|
4000
|
+
? captureWorkflowExitOutputSnapshot(outputsRead.value)
|
|
4001
|
+
: undefined;
|
|
4002
|
+
throwNestedSelectedExit();
|
|
4003
|
+
|
|
4004
|
+
// Freeze the signal so a broad author `catch (signal) { signal.* = ...; throw signal; }`
|
|
4005
|
+
// cannot rewrite the terminal status/reason/outputs. Finalization recovers this exact
|
|
4006
|
+
// object (via the abort reason or the rethrow) and the outputSnapshot value is already
|
|
4007
|
+
// deep-frozen, so the first selected exit is the authoritative terminal result.
|
|
4008
|
+
const signal: WorkflowExitSignal = {
|
|
4009
|
+
[WORKFLOW_EXIT_SIGNAL]: true,
|
|
4010
|
+
scope: exitScope,
|
|
4011
|
+
status,
|
|
4012
|
+
...(reason !== undefined ? { reason } : {}),
|
|
4013
|
+
...(outputSnapshot !== undefined ? { outputSnapshot } : {}),
|
|
4014
|
+
...(validationError !== undefined ? { validationError } : {}),
|
|
4015
|
+
};
|
|
4016
|
+
selectedExit = Object.freeze(signal);
|
|
4017
|
+
ownController.abort(selectedExit);
|
|
4018
|
+
runWorkflowExitCleanups(reason);
|
|
4019
|
+
throw selectedExit;
|
|
4020
|
+
},
|
|
3022
4021
|
// Prompt nodes and caller-provided UI adapters are mutually exclusive;
|
|
3023
4022
|
// executor-owned prompt nodes intentionally take precedence when enabled.
|
|
3024
|
-
ui:
|
|
4023
|
+
ui: buildExitGatedUiContext(),
|
|
3025
4024
|
|
|
3026
4025
|
stage(name: string, options?: StageOptions, stageFailFastScope?: ParallelFailFastScope) {
|
|
4026
|
+
throwIfWorkflowExitSelected();
|
|
3027
4027
|
options = stageOptionsWithGitWorktree(stageOptionsWithInputDefaults(options, inputRuntimeDefaults), workflowInvocationCwd);
|
|
3028
4028
|
// a. Generate stageId
|
|
3029
4029
|
const stageId = crypto.randomUUID();
|
|
@@ -3091,27 +4091,45 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3091
4091
|
opts.onStageStart?.(runId, stageSnapshot);
|
|
3092
4092
|
appendStageStartOnce();
|
|
3093
4093
|
let replayFinalized = false;
|
|
3094
|
-
|
|
4094
|
+
let unregisterWorkflowExitCleanup = (): void => {};
|
|
4095
|
+
const appendReplayStageEnd = (): void => {
|
|
4096
|
+
if (!opts.persistence) return;
|
|
4097
|
+
appendStageEnd(opts.persistence, {
|
|
4098
|
+
runId,
|
|
4099
|
+
stageId,
|
|
4100
|
+
status: stageSnapshot.status,
|
|
4101
|
+
durationMs: stageSnapshot.durationMs ?? 0,
|
|
4102
|
+
...(stageSnapshot.status === "completed" && stageSnapshot.result !== undefined ? { summary: stageSnapshot.result } : {}),
|
|
4103
|
+
...(stageSnapshot.skippedReason !== undefined ? { skippedReason: stageSnapshot.skippedReason } : {}),
|
|
4104
|
+
...stageReplayFields(stageSnapshot),
|
|
4105
|
+
});
|
|
4106
|
+
};
|
|
4107
|
+
const finalizeReplayStage = (status: "completed" | "skipped", reason?: string): void => {
|
|
3095
4108
|
if (replayFinalized) return;
|
|
3096
4109
|
replayFinalized = true;
|
|
4110
|
+
unregisterWorkflowExitCleanup();
|
|
4111
|
+
stageSnapshot.status = status;
|
|
4112
|
+
if (status === "skipped") {
|
|
4113
|
+
delete stageSnapshot.result;
|
|
4114
|
+
stageSnapshot.skippedReason = workflowExitSkippedReason(reason);
|
|
4115
|
+
}
|
|
4116
|
+
stageSnapshot.endedAt = Date.now();
|
|
4117
|
+
stageSnapshot.durationMs = elapsedStageMs(stageSnapshot, stageSnapshot.endedAt);
|
|
3097
4118
|
activeStore.recordStageEnd(runId, stageSnapshot);
|
|
3098
4119
|
opts.onStageEnd?.(runId, stageSnapshot);
|
|
3099
|
-
|
|
3100
|
-
appendStageEnd(opts.persistence, {
|
|
3101
|
-
runId,
|
|
3102
|
-
stageId,
|
|
3103
|
-
status: "completed",
|
|
3104
|
-
durationMs: 0,
|
|
3105
|
-
...(stageSnapshot.result !== undefined ? { summary: stageSnapshot.result } : {}),
|
|
3106
|
-
...stageReplayFields(stageSnapshot),
|
|
3107
|
-
});
|
|
3108
|
-
}
|
|
4120
|
+
appendReplayStageEnd();
|
|
3109
4121
|
tracker.onSettle(stageId);
|
|
3110
4122
|
};
|
|
4123
|
+
unregisterWorkflowExitCleanup = registerWorkflowExitCleanup(stageId, {
|
|
4124
|
+
skipForWorkflowExit(reason?: string): void {
|
|
4125
|
+
finalizeReplayStage("skipped", reason);
|
|
4126
|
+
},
|
|
4127
|
+
});
|
|
3111
4128
|
const replayResult = replaySource.result ?? "";
|
|
3112
4129
|
const replayText = async (): Promise<string> => {
|
|
3113
4130
|
await Promise.resolve();
|
|
3114
|
-
|
|
4131
|
+
throwIfWorkflowExitSelected();
|
|
4132
|
+
finalizeReplayStage("completed");
|
|
3115
4133
|
return replayResult;
|
|
3116
4134
|
};
|
|
3117
4135
|
const rejectReplayMutation = (action: string): never => {
|
|
@@ -3270,6 +4288,14 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3270
4288
|
// cleared by the host.
|
|
3271
4289
|
dropStageControlHandle();
|
|
3272
4290
|
};
|
|
4291
|
+
let stageClosedByWorkflowExit = false;
|
|
4292
|
+
const throwIfStageMutationBlocked = (): void => {
|
|
4293
|
+
if (stageClosedByWorkflowExit) {
|
|
4294
|
+
throwIfWorkflowExitSelected();
|
|
4295
|
+
throw new Error(`atomic-workflows: stage "${name}" skipped by workflow exit`);
|
|
4296
|
+
}
|
|
4297
|
+
throwIfWorkflowExitSelected();
|
|
4298
|
+
};
|
|
3273
4299
|
|
|
3274
4300
|
// e. Register a live stage-control handle so attached panes can
|
|
3275
4301
|
// prompt/steer/pause/resume the underlying Pi session lazily.
|
|
@@ -3303,26 +4329,33 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3303
4329
|
return innerCtx.__agentSession();
|
|
3304
4330
|
},
|
|
3305
4331
|
async ensureAttached() {
|
|
4332
|
+
throwIfStageMutationBlocked();
|
|
3306
4333
|
await innerCtx.__ensureSession();
|
|
4334
|
+
throwIfStageMutationBlocked();
|
|
3307
4335
|
const meta = innerCtx.__sessionMeta();
|
|
3308
4336
|
if (meta.sessionId !== undefined || meta.sessionFile !== undefined) {
|
|
3309
4337
|
activeStore.recordStageSession(runId, stageId, meta);
|
|
3310
4338
|
}
|
|
3311
4339
|
},
|
|
3312
4340
|
async prompt(text: string) {
|
|
4341
|
+
throwIfStageMutationBlocked();
|
|
3313
4342
|
await innerCtx.prompt(text);
|
|
4343
|
+
throwIfStageMutationBlocked();
|
|
3314
4344
|
const meta = innerCtx.__sessionMeta();
|
|
3315
4345
|
if (meta.sessionId !== undefined || meta.sessionFile !== undefined) {
|
|
3316
4346
|
activeStore.recordStageSession(runId, stageId, meta);
|
|
3317
4347
|
}
|
|
3318
4348
|
},
|
|
3319
4349
|
async steer(text: string) {
|
|
4350
|
+
throwIfStageMutationBlocked();
|
|
3320
4351
|
await innerCtx.steer(text);
|
|
3321
4352
|
},
|
|
3322
4353
|
async followUp(text: string) {
|
|
4354
|
+
throwIfStageMutationBlocked();
|
|
3323
4355
|
await innerCtx.followUp(text);
|
|
3324
4356
|
},
|
|
3325
4357
|
async pause() {
|
|
4358
|
+
throwIfStageMutationBlocked();
|
|
3326
4359
|
const statusBeforePause = stageSnapshot.status;
|
|
3327
4360
|
const changed = activeStore.recordStagePaused(runId, stageId);
|
|
3328
4361
|
if (changed) {
|
|
@@ -3334,6 +4367,7 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3334
4367
|
}
|
|
3335
4368
|
},
|
|
3336
4369
|
async resume(message?: string) {
|
|
4370
|
+
throwIfStageMutationBlocked();
|
|
3337
4371
|
const changed = activeStore.recordStageResumed(runId, stageId);
|
|
3338
4372
|
if (changed) {
|
|
3339
4373
|
releaseStageBarrier(stageId);
|
|
@@ -3349,9 +4383,18 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3349
4383
|
},
|
|
3350
4384
|
};
|
|
3351
4385
|
let stageFinalized = false;
|
|
4386
|
+
let unregisterWorkflowExitCleanup = (): void => {};
|
|
3352
4387
|
const finalizeStageSnapshot = (): boolean => {
|
|
3353
4388
|
if (stageFinalized) return false;
|
|
4389
|
+
if (stageSnapshot.endedAt !== undefined && isTerminalStage(stageSnapshot)) {
|
|
4390
|
+
stageFinalized = true;
|
|
4391
|
+
unregisterWorkflowExitCleanup();
|
|
4392
|
+
stageFailFastScope?.activeStages.delete(stageId);
|
|
4393
|
+
tracker.onSettle(stageId);
|
|
4394
|
+
return false;
|
|
4395
|
+
}
|
|
3354
4396
|
stageFinalized = true;
|
|
4397
|
+
unregisterWorkflowExitCleanup();
|
|
3355
4398
|
stageSnapshot.endedAt = Date.now();
|
|
3356
4399
|
stageSnapshot.durationMs = elapsedStageMs(stageSnapshot, stageSnapshot.endedAt);
|
|
3357
4400
|
|
|
@@ -3405,6 +4448,18 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3405
4448
|
void dropStageControlForCompletion().catch(() => {});
|
|
3406
4449
|
};
|
|
3407
4450
|
stageFailFastScope?.activeStages.set(stageId, { skip: skipForParallelFailFast });
|
|
4451
|
+
unregisterWorkflowExitCleanup = registerWorkflowExitCleanup(stageId, {
|
|
4452
|
+
async skipForWorkflowExit(reason?: string): Promise<void> {
|
|
4453
|
+
stageClosedByWorkflowExit = true;
|
|
4454
|
+
if (!isTerminalStage(stageSnapshot)) {
|
|
4455
|
+
stageSnapshot.status = "skipped";
|
|
4456
|
+
stageSnapshot.skippedReason = workflowExitSkippedReason(reason);
|
|
4457
|
+
finalizeStageSnapshot();
|
|
4458
|
+
}
|
|
4459
|
+
await innerCtx.abort().catch(() => {});
|
|
4460
|
+
await releaseLiveHandle().catch(() => {});
|
|
4461
|
+
},
|
|
4462
|
+
});
|
|
3408
4463
|
|
|
3409
4464
|
let stageControlDropped = false;
|
|
3410
4465
|
dropStageControlHandle = (): void => {
|
|
@@ -3463,6 +4518,7 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3463
4518
|
};
|
|
3464
4519
|
|
|
3465
4520
|
const runTrackedStageCall = async (call: () => Promise<string>, eagerSession = false): Promise<string> => {
|
|
4521
|
+
throwIfWorkflowExitSelected();
|
|
3466
4522
|
await waitForStageRelease();
|
|
3467
4523
|
if (stageFinalized) {
|
|
3468
4524
|
throw parallelFailFastError();
|
|
@@ -3473,6 +4529,7 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3473
4529
|
|
|
3474
4530
|
try {
|
|
3475
4531
|
await waitForStageRelease();
|
|
4532
|
+
throwIfWorkflowExitSelected();
|
|
3476
4533
|
if (stageFinalized) {
|
|
3477
4534
|
throw parallelFailFastError();
|
|
3478
4535
|
}
|
|
@@ -3615,7 +4672,16 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3615
4672
|
}
|
|
3616
4673
|
return result;
|
|
3617
4674
|
} catch (err) {
|
|
3618
|
-
|
|
4675
|
+
const workflowExitAbort = ownController.signal.aborted
|
|
4676
|
+
? currentWorkflowExitAbortReason()
|
|
4677
|
+
: undefined;
|
|
4678
|
+
if (workflowExitAbort !== undefined && !skippedForParallelFailFast) {
|
|
4679
|
+
stageClosedByWorkflowExit = true;
|
|
4680
|
+
if (!isTerminalStage(stageSnapshot)) {
|
|
4681
|
+
stageSnapshot.status = "skipped";
|
|
4682
|
+
stageSnapshot.skippedReason = workflowExitSkippedReason(workflowExitAbort.reason);
|
|
4683
|
+
}
|
|
4684
|
+
} else if (!ownController.signal.aborted && !skippedForParallelFailFast) {
|
|
3619
4685
|
applyFailureToStage(stageSnapshot, classifyExecutorFailure(err));
|
|
3620
4686
|
}
|
|
3621
4687
|
throw err;
|
|
@@ -3625,11 +4691,15 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3625
4691
|
}
|
|
3626
4692
|
|
|
3627
4693
|
finalizeStageSnapshot();
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
4694
|
+
if (stageClosedByWorkflowExit || currentWorkflowExitAbortReason() !== undefined) {
|
|
4695
|
+
await releaseLiveHandle().catch(() => {});
|
|
4696
|
+
} else {
|
|
4697
|
+
// The stage has finished participating in workflow scheduling. Drop it
|
|
4698
|
+
// from run-level pause/resume and cascade-pause lookups immediately,
|
|
4699
|
+
// while retaining the direct chat handle so completed nodes can be
|
|
4700
|
+
// reopened and continued instead of becoming read-only archives.
|
|
4701
|
+
await dropStageControlForCompletion().catch(() => {});
|
|
4702
|
+
}
|
|
3633
4703
|
limiter.release();
|
|
3634
4704
|
}
|
|
3635
4705
|
};
|
|
@@ -3672,29 +4742,46 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3672
4742
|
|
|
3673
4743
|
const stageContext: StageContext & Pick<InternalStageContext, "__modelFallbackMeta"> = {
|
|
3674
4744
|
name: innerCtx.name,
|
|
3675
|
-
prompt: (text, promptOptions) =>
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
4745
|
+
prompt: (text, promptOptions) => {
|
|
4746
|
+
throwIfStageMutationBlocked();
|
|
4747
|
+
return runTrackedStageCall(() => innerCtx.prompt(text, promptOptions), true);
|
|
4748
|
+
},
|
|
4749
|
+
complete: (text, completeOptions) => {
|
|
4750
|
+
throwIfStageMutationBlocked();
|
|
4751
|
+
return runTrackedStageCall(() => innerCtx.complete(text, completeOptions));
|
|
4752
|
+
},
|
|
4753
|
+
steer: (text) => {
|
|
4754
|
+
throwIfStageMutationBlocked();
|
|
4755
|
+
return innerCtx.steer(text);
|
|
4756
|
+
},
|
|
4757
|
+
followUp: (text) => {
|
|
4758
|
+
throwIfStageMutationBlocked();
|
|
4759
|
+
return innerCtx.followUp(text);
|
|
4760
|
+
},
|
|
3679
4761
|
subscribe: (listener) => innerCtx.subscribe(listener),
|
|
3680
4762
|
get sessionFile() { return innerCtx.sessionFile; },
|
|
3681
4763
|
get sessionId() { return innerCtx.sessionId; },
|
|
3682
4764
|
setModel: async (model) => {
|
|
4765
|
+
throwIfStageMutationBlocked();
|
|
3683
4766
|
await innerCtx.__ensureSession();
|
|
4767
|
+
throwIfStageMutationBlocked();
|
|
3684
4768
|
recordStageNotice({ kind: "model", from: noticeValue(innerCtx.model), to: noticeValue(model) });
|
|
3685
4769
|
await innerCtx.setModel(model);
|
|
3686
4770
|
},
|
|
3687
4771
|
setThinkingLevel: (level) => {
|
|
4772
|
+
throwIfStageMutationBlocked();
|
|
3688
4773
|
recordStageNotice({ kind: "thinking", from: noticeValue(innerCtx.thinkingLevel), to: noticeValue(level) });
|
|
3689
4774
|
innerCtx.setThinkingLevel(level);
|
|
3690
4775
|
},
|
|
3691
4776
|
cycleModel: async () => {
|
|
4777
|
+
throwIfStageMutationBlocked();
|
|
3692
4778
|
const from = noticeValue(innerCtx.model);
|
|
3693
4779
|
const result = await innerCtx.cycleModel();
|
|
3694
4780
|
recordStageNotice({ kind: "model", from, to: noticeValue(innerCtx.model) });
|
|
3695
4781
|
return result;
|
|
3696
4782
|
},
|
|
3697
4783
|
cycleThinkingLevel: () => {
|
|
4784
|
+
throwIfStageMutationBlocked();
|
|
3698
4785
|
const from = noticeValue(innerCtx.thinkingLevel);
|
|
3699
4786
|
const result = innerCtx.cycleThinkingLevel();
|
|
3700
4787
|
recordStageNotice({ kind: "thinking", from, to: noticeValue(innerCtx.thinkingLevel) });
|
|
@@ -3706,16 +4793,22 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3706
4793
|
get messages() { return innerCtx.messages; },
|
|
3707
4794
|
get isStreaming() { return innerCtx.isStreaming; },
|
|
3708
4795
|
navigateTree: async (targetId, treeOptions) => {
|
|
4796
|
+
throwIfStageMutationBlocked();
|
|
3709
4797
|
recordStageNotice({ kind: "tree", to: targetId });
|
|
3710
4798
|
return innerCtx.navigateTree(targetId, treeOptions);
|
|
3711
4799
|
},
|
|
3712
4800
|
compact: async () => {
|
|
4801
|
+
throwIfStageMutationBlocked();
|
|
3713
4802
|
const result = await innerCtx.compact();
|
|
3714
4803
|
recordStageNotice({ kind: "compaction", to: "compacted", meta: compactionMeta(result) });
|
|
3715
4804
|
return result;
|
|
3716
4805
|
},
|
|
3717
|
-
abortCompaction: () =>
|
|
4806
|
+
abortCompaction: () => {
|
|
4807
|
+
throwIfStageMutationBlocked();
|
|
4808
|
+
innerCtx.abortCompaction();
|
|
4809
|
+
},
|
|
3718
4810
|
abort: async () => {
|
|
4811
|
+
throwIfStageMutationBlocked();
|
|
3719
4812
|
recordStageNotice({ kind: "abort", to: "interrupted" });
|
|
3720
4813
|
await innerCtx.abort();
|
|
3721
4814
|
},
|
|
@@ -3725,7 +4818,9 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3725
4818
|
},
|
|
3726
4819
|
|
|
3727
4820
|
async task(name: string, options: WorkflowTaskOptions, stageFailFastScope?: ParallelFailFastScope): Promise<WorkflowTaskResult> {
|
|
4821
|
+
throwIfWorkflowExitSelected();
|
|
3728
4822
|
const runTaskOnce = async (taskOptions: WorkflowTaskOptions): Promise<WorkflowTaskResult> => {
|
|
4823
|
+
throwIfWorkflowExitSelected();
|
|
3729
4824
|
const resolvedTaskOptions = stageOptionsWithGitWorktree(stageOptionsWithInputDefaults(taskOptions, inputRuntimeDefaults), workflowInvocationCwd) ?? taskOptions;
|
|
3730
4825
|
const stage = (ctx.stage as typeof ctx.stage & ((stageName: string, stageOptions?: StageOptions, scope?: ParallelFailFastScope) => StageContext))(
|
|
3731
4826
|
name,
|
|
@@ -3780,8 +4875,10 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3780
4875
|
},
|
|
3781
4876
|
|
|
3782
4877
|
async chain(steps: readonly WorkflowTaskStep[], options: WorkflowChainOptions = {}): Promise<WorkflowTaskResult[]> {
|
|
4878
|
+
throwIfWorkflowExitSelected();
|
|
3783
4879
|
const results: WorkflowTaskResult[] = [];
|
|
3784
4880
|
for (let index = 0; index < steps.length; index += 1) {
|
|
4881
|
+
throwIfWorkflowExitSelected();
|
|
3785
4882
|
const step = steps[index]!;
|
|
3786
4883
|
const explicitPrevious = taskPrevious(step);
|
|
3787
4884
|
const previous = explicitPrevious ?? (index > 0 ? results[index - 1] : undefined);
|
|
@@ -3795,11 +4892,13 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3795
4892
|
},
|
|
3796
4893
|
|
|
3797
4894
|
async parallel(steps: readonly WorkflowTaskStep[], options: WorkflowParallelOptions = {}): Promise<WorkflowTaskResult[]> {
|
|
4895
|
+
throwIfWorkflowExitSelected();
|
|
3798
4896
|
const fallback = parallelFallbackTask(steps, options);
|
|
3799
4897
|
const failFastScope: ParallelFailFastScope | undefined = options.failFast === false
|
|
3800
4898
|
? undefined
|
|
3801
4899
|
: { failed: false, activeStages: new Map<string, ParallelFailFastStage>() };
|
|
3802
4900
|
return mapParallelSteps(steps, options.concurrency, options.failFast, async (step) => {
|
|
4901
|
+
throwIfWorkflowExitSelected();
|
|
3803
4902
|
const prompt = replaceTaskPlaceholder(step.prompt ?? step.task ?? fallback, options.task ?? fallback);
|
|
3804
4903
|
return await (ctx.task as typeof ctx.task & ((taskName: string, taskOptions: WorkflowTaskOptions, scope?: ParallelFailFastScope) => Promise<WorkflowTaskResult>))(
|
|
3805
4904
|
step.name,
|
|
@@ -3813,6 +4912,10 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3813
4912
|
for (const stage of failFastScope.activeStages.values()) {
|
|
3814
4913
|
stage.skip();
|
|
3815
4914
|
}
|
|
4915
|
+
}, {
|
|
4916
|
+
beforeDequeue: throwIfWorkflowExitSelected,
|
|
4917
|
+
beforeMap: throwIfWorkflowExitSelected,
|
|
4918
|
+
isControlSignal: (error) => findWorkflowExitSignal(error, exitScope) !== undefined,
|
|
3816
4919
|
});
|
|
3817
4920
|
},
|
|
3818
4921
|
|
|
@@ -3820,6 +4923,7 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3820
4923
|
child: WorkflowDefinition<TChildInputs, TChildOutputs>,
|
|
3821
4924
|
options: WorkflowRunChildOptions<TChildInputs> = {},
|
|
3822
4925
|
): Promise<WorkflowChildResult<TChildOutputs>> {
|
|
4926
|
+
throwIfWorkflowExitSelected();
|
|
3823
4927
|
// The executor operates on type-erased definitions at runtime; the child's
|
|
3824
4928
|
// declared output contract is validated dynamically by the child run and
|
|
3825
4929
|
// selectWorkflowOutputs, so the typed result is reconstructed via casts.
|
|
@@ -3830,16 +4934,6 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3830
4934
|
const boundaryName = options.stageName ?? `workflow:${childName}`;
|
|
3831
4935
|
const boundaryReplayKey = nextWorkflowBoundaryReplayKey(boundaryName);
|
|
3832
4936
|
const boundary = startWorkflowBoundaryStage(boundaryName, boundaryReplayKey);
|
|
3833
|
-
if (boundary.replayedChild !== undefined) {
|
|
3834
|
-
// Continuation replay returns the persisted child boundary exactly as
|
|
3835
|
-
// written; input validation and output remapping are intentionally not
|
|
3836
|
-
// re-run against edited workflow code for a completed child boundary.
|
|
3837
|
-
// Defer settling by one microtask so concurrent replayed boundaries
|
|
3838
|
-
// spawned in the same turn see the same frontier as the source run.
|
|
3839
|
-
await Promise.resolve();
|
|
3840
|
-
boundary.finalizeReplay();
|
|
3841
|
-
return boundary.replayedChild as WorkflowChildResult<TChildOutputs>;
|
|
3842
|
-
}
|
|
3843
4937
|
|
|
3844
4938
|
// Tracked so the finally can detach the parent-abort listener and release
|
|
3845
4939
|
// the pre-registered child controller on every exit path — including the
|
|
@@ -3849,28 +4943,48 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3849
4943
|
let childRunId: string | undefined;
|
|
3850
4944
|
let detachParentAbort: (() => void) | undefined;
|
|
3851
4945
|
try {
|
|
4946
|
+
if (boundary.replayedChild !== undefined) {
|
|
4947
|
+
// Continuation replay returns the persisted child boundary exactly as
|
|
4948
|
+
// written; input validation and output remapping are intentionally not
|
|
4949
|
+
// re-run against edited workflow code for a completed child boundary.
|
|
4950
|
+
// Defer settling by one microtask so concurrent replayed boundaries
|
|
4951
|
+
// spawned in the same turn see the same frontier as the source run.
|
|
4952
|
+
await Promise.resolve();
|
|
4953
|
+
throwIfWorkflowExitSelected();
|
|
4954
|
+
boundary.finalizeReplay();
|
|
4955
|
+
return boundary.replayedChild as WorkflowChildResult<TChildOutputs>;
|
|
4956
|
+
}
|
|
4957
|
+
|
|
3852
4958
|
const childInputs = resolveAndValidateInputs(
|
|
3853
4959
|
child.inputs,
|
|
3854
4960
|
options.inputs ?? {},
|
|
3855
4961
|
`child workflow "${childName}" (${child.name})`,
|
|
3856
4962
|
);
|
|
4963
|
+
throwIfWorkflowExitSelected();
|
|
3857
4964
|
|
|
3858
4965
|
childRunId = crypto.randomUUID();
|
|
3859
|
-
|
|
4966
|
+
const childController = new AbortController();
|
|
4967
|
+
const childRef: WorkflowChildRunRef = {
|
|
3860
4968
|
alias: childName,
|
|
3861
4969
|
workflow: child.normalizedName,
|
|
3862
4970
|
runId: childRunId,
|
|
3863
|
-
}
|
|
4971
|
+
};
|
|
4972
|
+
boundary.linkChildRun(childRef, childController);
|
|
3864
4973
|
|
|
3865
|
-
const
|
|
4974
|
+
const abortChildFromParent = (): void => {
|
|
4975
|
+
const parentExit = findWorkflowExitSignal(ownController.signal.reason, exitScope);
|
|
4976
|
+
childController.abort(parentExit !== undefined
|
|
4977
|
+
? makeParentWorkflowExitAbortReason(parentExit.reason)
|
|
4978
|
+
: ownController.signal.reason);
|
|
4979
|
+
};
|
|
3866
4980
|
if (ownController.signal.aborted) {
|
|
3867
|
-
|
|
4981
|
+
abortChildFromParent();
|
|
3868
4982
|
} else {
|
|
3869
|
-
|
|
3870
|
-
ownController.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
4983
|
+
ownController.signal.addEventListener("abort", abortChildFromParent, { once: true });
|
|
3871
4984
|
detachParentAbort = () =>
|
|
3872
|
-
ownController.signal.removeEventListener("abort",
|
|
4985
|
+
ownController.signal.removeEventListener("abort", abortChildFromParent);
|
|
3873
4986
|
}
|
|
4987
|
+
throwIfWorkflowExitSelected();
|
|
3874
4988
|
// Pre-register the child controller under its own runId *before* run()
|
|
3875
4989
|
// so a kill targeting the child runId works even before the nested run
|
|
3876
4990
|
// would register itself. The nested run() sees opts.signal set and skips
|
|
@@ -3878,6 +4992,7 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3878
4992
|
// key) while still running its finally{} unregister(runId) cleanup, so
|
|
3879
4993
|
// both branches must agree on this key.
|
|
3880
4994
|
opts.cancellation?.register(childRunId, childController);
|
|
4995
|
+
throwIfWorkflowExitSelected();
|
|
3881
4996
|
|
|
3882
4997
|
const {
|
|
3883
4998
|
runId: _parentRunId,
|
|
@@ -3888,7 +5003,7 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3888
5003
|
onRunEnd: _parentOnRunEnd,
|
|
3889
5004
|
...childBaseOpts
|
|
3890
5005
|
} = opts;
|
|
3891
|
-
const
|
|
5006
|
+
const childRunPromise = run(child, childInputs, {
|
|
3892
5007
|
...childBaseOpts,
|
|
3893
5008
|
runId: childRunId,
|
|
3894
5009
|
cwd: resolveWorkflowCwd(),
|
|
@@ -3902,8 +5017,11 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3902
5017
|
signal: childController.signal,
|
|
3903
5018
|
deferWorkflowStart: false,
|
|
3904
5019
|
});
|
|
5020
|
+
boundary.observeChildRun(childRunPromise);
|
|
5021
|
+
const childRun = await childRunPromise;
|
|
5022
|
+
throwIfWorkflowExitSelected();
|
|
3905
5023
|
|
|
3906
|
-
if (childRun.status
|
|
5024
|
+
if (!isWorkflowExitStatus(childRun.status)) {
|
|
3907
5025
|
const failedChildStage = childRun.stages.find((stage) => stage.failureKind !== undefined);
|
|
3908
5026
|
throw new Error(
|
|
3909
5027
|
`atomic-workflows: child workflow "${childName}" (${child.name}) failed with status ${childRun.status}${childRun.error !== undefined ? `: ${childRun.error}` : ""}`,
|
|
@@ -3917,20 +5035,36 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3917
5035
|
}
|
|
3918
5036
|
|
|
3919
5037
|
const outputs = selectWorkflowOutputs(child, childRun.result);
|
|
3920
|
-
const
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
5038
|
+
const childExited = childRun.exited === true || childRun.status !== "completed";
|
|
5039
|
+
const childResult: WorkflowChildResult<TChildOutputs> = childExited
|
|
5040
|
+
? {
|
|
5041
|
+
workflow: child.normalizedName,
|
|
5042
|
+
runId: childRun.runId,
|
|
5043
|
+
status: childRun.status,
|
|
5044
|
+
exited: true,
|
|
5045
|
+
outputs: outputs as Partial<TChildOutputs>,
|
|
5046
|
+
...(childRun.exitReason !== undefined ? { exitReason: childRun.exitReason } : {}),
|
|
5047
|
+
}
|
|
5048
|
+
: {
|
|
5049
|
+
workflow: child.normalizedName,
|
|
5050
|
+
runId: childRun.runId,
|
|
5051
|
+
status: "completed",
|
|
5052
|
+
exited: false,
|
|
5053
|
+
outputs: outputs as TChildOutputs,
|
|
5054
|
+
};
|
|
3926
5055
|
const workflowChild = workflowChildReplaySnapshot(childName, childResult);
|
|
3927
5056
|
const outputKeys = Object.keys(outputs);
|
|
3928
5057
|
boundary.complete(
|
|
3929
|
-
`Workflow "${child.name}"
|
|
5058
|
+
`Workflow "${child.name}" ${childRun.status} (runId: ${childRun.runId}; outputs: ${outputKeys.length > 0 ? outputKeys.join(", ") : "(none)"})`,
|
|
3930
5059
|
workflowChild,
|
|
3931
5060
|
);
|
|
3932
5061
|
return childResult;
|
|
3933
5062
|
} catch (err) {
|
|
5063
|
+
const exit = findWorkflowExitSignal(err, exitScope) ?? findWorkflowExitSignal(ownController.signal.reason, exitScope);
|
|
5064
|
+
if (exit !== undefined) {
|
|
5065
|
+
await boundary.skipForWorkflowExit(exit.reason);
|
|
5066
|
+
throw exit;
|
|
5067
|
+
}
|
|
3934
5068
|
boundary.fail(err);
|
|
3935
5069
|
throw err;
|
|
3936
5070
|
} finally {
|
|
@@ -3947,6 +5081,10 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3947
5081
|
if (opts.deferWorkflowStart === true) {
|
|
3948
5082
|
await nextEventLoopTurn();
|
|
3949
5083
|
if (ownController.signal.aborted) {
|
|
5084
|
+
const exit = findWorkflowExitSignal(ownController.signal.reason, exitScope);
|
|
5085
|
+
if (exit !== undefined) return await finalizeWorkflowExit(exit);
|
|
5086
|
+
const parentExit = parentWorkflowExitAbortReason(ownController.signal.reason);
|
|
5087
|
+
if (parentExit !== undefined) return await finalizeParentWorkflowExitCancellation(parentExit);
|
|
3950
5088
|
return finalizeKilled(runId, runSnapshot, activeStore, opts.persistence, opts.onRunEnd);
|
|
3951
5089
|
}
|
|
3952
5090
|
}
|
|
@@ -3954,8 +5092,12 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3954
5092
|
const rawResult = await def.run(ctx);
|
|
3955
5093
|
|
|
3956
5094
|
// Post-body abort check: if signal was aborted at any point before we record
|
|
3957
|
-
// completion,
|
|
5095
|
+
// completion, classify a scoped author exit before falling back to killed.
|
|
3958
5096
|
if (ownController.signal.aborted) {
|
|
5097
|
+
const exit = findWorkflowExitSignal(ownController.signal.reason, exitScope);
|
|
5098
|
+
if (exit !== undefined) return await finalizeWorkflowExit(exit);
|
|
5099
|
+
const parentExit = parentWorkflowExitAbortReason(ownController.signal.reason);
|
|
5100
|
+
if (parentExit !== undefined) return await finalizeParentWorkflowExitCancellation(parentExit);
|
|
3959
5101
|
return finalizeKilled(runId, runSnapshot, activeStore, opts.persistence, opts.onRunEnd);
|
|
3960
5102
|
}
|
|
3961
5103
|
|
|
@@ -3965,7 +5107,6 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3965
5107
|
assertWorkflowCreatedStage(runSnapshot);
|
|
3966
5108
|
|
|
3967
5109
|
const recorded = activeStore.recordRunEnd(runId, "completed", result);
|
|
3968
|
-
opts.onRunEnd?.(runId, "completed", result);
|
|
3969
5110
|
|
|
3970
5111
|
appendRunEndWhenRecorded(opts.persistence, recorded, {
|
|
3971
5112
|
runId,
|
|
@@ -3974,14 +5115,17 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
3974
5115
|
ts: Date.now(),
|
|
3975
5116
|
});
|
|
3976
5117
|
|
|
3977
|
-
return {
|
|
3978
|
-
runId,
|
|
5118
|
+
return reconcileTerminalRunResult(runId, runSnapshot, activeStore, {
|
|
3979
5119
|
status: "completed",
|
|
3980
5120
|
result,
|
|
3981
|
-
|
|
3982
|
-
};
|
|
5121
|
+
}, opts.onRunEnd);
|
|
3983
5122
|
} catch (err) {
|
|
5123
|
+
const exit = findWorkflowExitSignal(err, exitScope) ?? findWorkflowExitSignal(ownController.signal.reason, exitScope);
|
|
5124
|
+
if (exit !== undefined) return await finalizeWorkflowExit(exit);
|
|
5125
|
+
|
|
3984
5126
|
if (ownController.signal.aborted) {
|
|
5127
|
+
const parentExit = parentWorkflowExitAbortReason(ownController.signal.reason);
|
|
5128
|
+
if (parentExit !== undefined) return await finalizeParentWorkflowExitCancellation(parentExit);
|
|
3985
5129
|
return finalizeKilled(runId, runSnapshot, activeStore, opts.persistence, opts.onRunEnd);
|
|
3986
5130
|
}
|
|
3987
5131
|
|
|
@@ -4020,7 +5164,6 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
4020
5164
|
}
|
|
4021
5165
|
|
|
4022
5166
|
const recorded = activeStore.recordRunEnd(runId, "failed", undefined, metadata.errorMessage, metadata);
|
|
4023
|
-
opts.onRunEnd?.(runId, "failed", undefined, metadata.errorMessage);
|
|
4024
5167
|
|
|
4025
5168
|
appendRunEndWhenRecorded(opts.persistence, recorded, {
|
|
4026
5169
|
runId,
|
|
@@ -4037,12 +5180,10 @@ export async function run<TInputs extends WorkflowInputValues>(
|
|
|
4037
5180
|
ts: Date.now(),
|
|
4038
5181
|
});
|
|
4039
5182
|
|
|
4040
|
-
return {
|
|
4041
|
-
runId,
|
|
5183
|
+
return reconcileTerminalRunResult(runId, runSnapshot, activeStore, {
|
|
4042
5184
|
status: "failed",
|
|
4043
5185
|
error: metadata.errorMessage,
|
|
4044
|
-
|
|
4045
|
-
};
|
|
5186
|
+
}, opts.onRunEnd);
|
|
4046
5187
|
} finally {
|
|
4047
5188
|
opts.cancellation?.unregister(runId);
|
|
4048
5189
|
}
|