@k2b/cloud 0.6.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -0
- package/package.json +128 -0
- package/public/logo.svg +1 -0
- package/scripts/app-favicon.test.ts +49 -0
- package/scripts/app-favicon.ts +51 -0
- package/scripts/build.ts +205 -0
- package/scripts/preload.ts +164 -0
- package/scripts/sync-recovery-smoke.ts +200 -0
- package/src/_internal/app-favicon.ts +2 -0
- package/src/_internal/app-presentation.ts +61 -0
- package/src/_internal/app-workspace-state.ts +24 -0
- package/src/_internal/bounded-json.ts +47 -0
- package/src/_internal/build-metadata.ts +9 -0
- package/src/_internal/capabilities.ts +1118 -0
- package/src/_internal/define-app.ts +801 -0
- package/src/_internal/heartbeat.ts +167 -0
- package/src/_internal/help-catalog.ts +241 -0
- package/src/_internal/help.ts +95 -0
- package/src/_internal/nats-connection.ts +34 -0
- package/src/_internal/page-responses.ts +40 -0
- package/src/_internal/process-sync.ts +117 -0
- package/src/_internal/readiness.ts +3 -0
- package/src/_internal/registry-validation.ts +136 -0
- package/src/_internal/registry-watch.ts +52 -0
- package/src/_internal/registry.ts +338 -0
- package/src/_internal/runtime-compatibility.ts +64 -0
- package/src/_internal/runtime-context.ts +84 -0
- package/src/_internal/runtime-watcher.ts +78 -0
- package/src/_internal/static-assets.ts +86 -0
- package/src/_internal/status-preserving-ssr.ts +49 -0
- package/src/access/PermissionEditor.tsx +486 -0
- package/src/access/ResourceApiKeys.tsx +271 -0
- package/src/access/messages.ts +121 -0
- package/src/access/ui.ts +16 -0
- package/src/account/Avatar.tsx +28 -0
- package/src/account/EntitySearch.tsx +363 -0
- package/src/account/avatar-upload-dialog.tsx +197 -0
- package/src/account/avatar-upload.ts +149 -0
- package/src/account/ui.ts +3 -0
- package/src/ai/admin.ts +47 -0
- package/src/ai/approval-routes.ts +130 -0
- package/src/ai/approvals.ts +139 -0
- package/src/ai/assistant-models.ts +61 -0
- package/src/ai/attachments.ts +40 -0
- package/src/ai/browser.ts +92 -0
- package/src/ai/capabilities.ts +714 -0
- package/src/ai/capability-execution.ts +151 -0
- package/src/ai/chat/blocks.render.test.tsx +1108 -0
- package/src/ai/chat/blocks.tsx +730 -0
- package/src/ai/chat/builtin-tools.tsx +428 -0
- package/src/ai/chat/capability-table.tsx +87 -0
- package/src/ai/chat/composer-adapter.ts +169 -0
- package/src/ai/chat/file-tools.tsx +81 -0
- package/src/ai/chat/message-actions.tsx +370 -0
- package/src/ai/chat/message-utils.ts +479 -0
- package/src/ai/chat/messages.ts +43 -0
- package/src/ai/chat/presentation.tsx +395 -0
- package/src/ai/chat/primitives.tsx +3 -0
- package/src/ai/chat/tool-disclosure.tsx +41 -0
- package/src/ai/chat/turn-navigator-utils.ts +28 -0
- package/src/ai/chat/turn-navigator.tsx +164 -0
- package/src/ai/chat/user-message.tsx +149 -0
- package/src/ai/chat/visual-tools.tsx +435 -0
- package/src/ai/chat/web-tools.tsx +202 -0
- package/src/ai/chat-task-contracts.ts +113 -0
- package/src/ai/chat-tasks.ts +952 -0
- package/src/ai/client/controller.ts +1211 -0
- package/src/ai/client/file-source.ts +95 -0
- package/src/ai/client/live-connection.ts +140 -0
- package/src/ai/client/projection.ts +178 -0
- package/src/ai/client/transport.ts +111 -0
- package/src/ai/compaction.ts +182 -0
- package/src/ai/credentials.ts +124 -0
- package/src/ai/default-tool-contracts.ts +108 -0
- package/src/ai/default-tools.ts +127 -0
- package/src/ai/enrich.ts +280 -0
- package/src/ai/executor.ts +1624 -0
- package/src/ai/fetch-file-tool.ts +313 -0
- package/src/ai/file-context.ts +92 -0
- package/src/ai/file-media-type.ts +28 -0
- package/src/ai/file-mount.ts +16 -0
- package/src/ai/file-tools.ts +566 -0
- package/src/ai/files-store.ts +390 -0
- package/src/ai/firecrawl-tools.ts +305 -0
- package/src/ai/grids-skill.ts +126 -0
- package/src/ai/http.ts +265 -0
- package/src/ai/index.ts +337 -0
- package/src/ai/kit-skill.ts +22 -0
- package/src/ai/limits.ts +5 -0
- package/src/ai/live-events.ts +289 -0
- package/src/ai/live-messages.ts +45 -0
- package/src/ai/live-outbox.ts +102 -0
- package/src/ai/live-routes.ts +412 -0
- package/src/ai/live.ts +2 -0
- package/src/ai/maintenance.ts +302 -0
- package/src/ai/markdown-pdf-tool.ts +95 -0
- package/src/ai/memories.ts +605 -0
- package/src/ai/memory-learning-runs.ts +222 -0
- package/src/ai/memory-learning.ts +619 -0
- package/src/ai/memory-tool.ts +120 -0
- package/src/ai/memory-workflow-evidence.ts +158 -0
- package/src/ai/migrate.ts +2082 -0
- package/src/ai/model-access-migrate.ts +30 -0
- package/src/ai/model-access.ts +191 -0
- package/src/ai/personal-agent.ts +6 -0
- package/src/ai/prefs.ts +72 -0
- package/src/ai/project-tool.ts +94 -0
- package/src/ai/projects-routes.ts +277 -0
- package/src/ai/projects.ts +902 -0
- package/src/ai/protocol.ts +279 -0
- package/src/ai/provider.ts +73 -0
- package/src/ai/public-projection.ts +25 -0
- package/src/ai/resource-markers.ts +31 -0
- package/src/ai/resource-refs.ts +70 -0
- package/src/ai/routes.ts +1131 -0
- package/src/ai/runtime-tools.ts +39 -0
- package/src/ai/runtime.ts +544 -0
- package/src/ai/settings.ts +529 -0
- package/src/ai/short-id.ts +45 -0
- package/src/ai/skill-catalog.ts +73 -0
- package/src/ai/skill-format.ts +145 -0
- package/src/ai/skill-seeds.ts +430 -0
- package/src/ai/skill-tool.ts +81 -0
- package/src/ai/skills-routes.ts +143 -0
- package/src/ai/skills.ts +934 -0
- package/src/ai/solid.ts +27 -0
- package/src/ai/store.ts +3113 -0
- package/src/ai/stream.ts +308 -0
- package/src/ai/structured-runs.ts +66 -0
- package/src/ai/structured.ts +186 -0
- package/src/ai/system-prompt.ts +170 -0
- package/src/ai/task-prompt.ts +22 -0
- package/src/ai/timeline.ts +148 -0
- package/src/ai/tool-audit.ts +177 -0
- package/src/ai/tool-result-budget.ts +16 -0
- package/src/ai/tools.ts +155 -0
- package/src/ai/types.ts +967 -0
- package/src/ai/ui.tsx +39 -0
- package/src/ai/usage-migrate.ts +81 -0
- package/src/ai/usage.ts +315 -0
- package/src/ai/validate.ts +54 -0
- package/src/ai/vision-tool.ts +79 -0
- package/src/api/accounts-entities.ts +174 -0
- package/src/api/admin-ai-projects.ts +74 -0
- package/src/api/admin-ai-skills.ts +76 -0
- package/src/api/admin-ai-usage.ts +42 -0
- package/src/api/admin-core-settings.ts +508 -0
- package/src/api/admin-identity.ts +148 -0
- package/src/api/admin-lifecycle.ts +206 -0
- package/src/api/admin-linux-identities.ts +66 -0
- package/src/api/announcements.ts +131 -0
- package/src/api/app-approval.ts +129 -0
- package/src/api/apps.ts +51 -0
- package/src/api/auth/schemas.ts +54 -0
- package/src/api/auth.ts +370 -0
- package/src/api/capabilities.ts +658 -0
- package/src/api/help-messages.ts +19 -0
- package/src/api/help.ts +78 -0
- package/src/api/index.ts +108 -0
- package/src/api/mcp.ts +744 -0
- package/src/api/me-rail.ts +45 -0
- package/src/api/me.ts +753 -0
- package/src/api/search/schemas.ts +79 -0
- package/src/api/search.bench.ts +185 -0
- package/src/api/search.ts +407 -0
- package/src/api/sync-ops.ts +79 -0
- package/src/api/widgets.ts +143 -0
- package/src/browser/CloudResourceSearch.behavior.test.tsx +74 -0
- package/src/browser/CloudResourceSearch.tsx +791 -0
- package/src/browser/app-approval-vault.ts +305 -0
- package/src/browser/app-approval.ts +234 -0
- package/src/browser/live-websocket.ts +268 -0
- package/src/browser/locale-preference.ts +25 -0
- package/src/browser/notification-target.ts +27 -0
- package/src/browser/notification-timeout.ts +14 -0
- package/src/browser/notifications.ts +181 -0
- package/src/browser/resource-clipboard.ts +70 -0
- package/src/browser/resource-picker.tsx +39 -0
- package/src/browser/resource-search-messages.ts +55 -0
- package/src/browser/resource-search.ts +27 -0
- package/src/capabilities/client.ts +149 -0
- package/src/capabilities/response.ts +39 -0
- package/src/capabilities/server.ts +215 -0
- package/src/capabilities/signals.ts +4 -0
- package/src/capabilities/testing.ts +12 -0
- package/src/capabilities/types.ts +32 -0
- package/src/cli/access.ts +499 -0
- package/src/cli/account.ts +382 -0
- package/src/cli/admin/account-administration.ts +45 -0
- package/src/cli/admin/account-categories.ts +49 -0
- package/src/cli/admin/ai-usage.ts +118 -0
- package/src/cli/admin/app-sign-in.ts +49 -0
- package/src/cli/admin/data.ts +343 -0
- package/src/cli/admin/documentation.ts +34 -0
- package/src/cli/admin/gateway.ts +155 -0
- package/src/cli/admin/index.ts +61 -0
- package/src/cli/admin/instance.ts +250 -0
- package/src/cli/admin/jobs.ts +223 -0
- package/src/cli/admin/legal.ts +122 -0
- package/src/cli/admin/linux.ts +53 -0
- package/src/cli/admin/logs.ts +324 -0
- package/src/cli/admin/metrics.ts +142 -0
- package/src/cli/admin/nats.ts +167 -0
- package/src/cli/admin/notifications.ts +516 -0
- package/src/cli/admin/shared.ts +113 -0
- package/src/cli/admin/sync.ts +188 -0
- package/src/cli/admin/telemetry.ts +314 -0
- package/src/cli/admin/webhooks.ts +222 -0
- package/src/cli/admin/workflows.ts +367 -0
- package/src/cli/apps.ts +40 -0
- package/src/cli/capabilities.ts +239 -0
- package/src/cli/commands.ts +604 -0
- package/src/cli/index.ts +61 -0
- package/src/cli/locale.ts +31 -0
- package/src/cli/output.ts +58 -0
- package/src/clients/core.ts +20 -0
- package/src/config/env.ts +54 -0
- package/src/config/index.ts +6 -0
- package/src/contracts/account-categories.ts +62 -0
- package/src/contracts/announcements.ts +121 -0
- package/src/contracts/app-approval.ts +197 -0
- package/src/contracts/app.ts +162 -0
- package/src/contracts/capabilities.ts +484 -0
- package/src/contracts/index.ts +14 -0
- package/src/contracts/notification-live.ts +66 -0
- package/src/contracts/notification-types.ts +235 -0
- package/src/contracts/posix.ts +69 -0
- package/src/contracts/profile.ts +85 -0
- package/src/contracts/rail-preferences.ts +53 -0
- package/src/contracts/registry.ts +110 -0
- package/src/contracts/resource-clipboard.ts +33 -0
- package/src/contracts/settings-types.ts +98 -0
- package/src/contracts/shared.ts +408 -0
- package/src/contracts/user-notifications.ts +101 -0
- package/src/contracts/widgets.ts +183 -0
- package/src/desktop/index.ts +704 -0
- package/src/desktop/solid.tsx +938 -0
- package/src/index.ts +58 -0
- package/src/server/actor.ts +41 -0
- package/src/server/api/index.ts +2 -0
- package/src/server/api/respond.ts +96 -0
- package/src/server/api-client.ts +54 -0
- package/src/server/app-context.ts +39 -0
- package/src/server/help.ts +187 -0
- package/src/server/index.ts +89 -0
- package/src/server/locale.ts +55 -0
- package/src/server/middleware/auth.ts +386 -0
- package/src/server/middleware/index.ts +28 -0
- package/src/server/middleware/invocation.ts +55 -0
- package/src/server/middleware/middleware.ts +65 -0
- package/src/server/middleware/openapi.ts +126 -0
- package/src/server/middleware/rate-limit.ts +125 -0
- package/src/server/middleware/request-logger.ts +41 -0
- package/src/server/middleware/route-template.ts +46 -0
- package/src/server/middleware/runtime.ts +22 -0
- package/src/server/middleware/settings.ts +55 -0
- package/src/server/middleware/validator.ts +49 -0
- package/src/server/middleware/workload.ts +22 -0
- package/src/server/ratelimit.ts +151 -0
- package/src/server/request-cookies.ts +18 -0
- package/src/server/services/access.ts +644 -0
- package/src/server/services/freeipa/client.ts +137 -0
- package/src/server/services/freeipa/index.ts +9 -0
- package/src/server/services/freeipa/session.ts +123 -0
- package/src/server/services/freeipa/test-certificates.ts +44 -0
- package/src/server/services/freeipa/tls.ts +44 -0
- package/src/server/services/freeipa/transport.ts +116 -0
- package/src/server/services/freeipa/util.ts +59 -0
- package/src/server/services/geo.ts +198 -0
- package/src/server/services/index.ts +48 -0
- package/src/server/services/pagination.ts +22 -0
- package/src/server/services/services.ts +13 -0
- package/src/server/time.ts +33 -0
- package/src/services/account-category-policy.ts +51 -0
- package/src/services/account-lifecycle/audit.ts +41 -0
- package/src/services/account-lifecycle/index.ts +940 -0
- package/src/services/account-lifecycle/ipa-backfill.ts +152 -0
- package/src/services/account-lifecycle/notification-sender.ts +13 -0
- package/src/services/account-lifecycle/scheduler.ts +506 -0
- package/src/services/account-model.ts +22 -0
- package/src/services/accounts/app.ts +1671 -0
- package/src/services/accounts/authz.ts +44 -0
- package/src/services/accounts/avatar.ts +120 -0
- package/src/services/accounts/base-group.ts +11 -0
- package/src/services/accounts/base-user.ts +46 -0
- package/src/services/accounts/duplicate-emails.ts +82 -0
- package/src/services/accounts/entities.ts +622 -0
- package/src/services/accounts/group-sql.ts +106 -0
- package/src/services/accounts/groups.ts +272 -0
- package/src/services/accounts/index.ts +14 -0
- package/src/services/accounts/ipa-data.ts +66 -0
- package/src/services/accounts/lifecycle.ts +2 -0
- package/src/services/accounts/local-groups.ts +499 -0
- package/src/services/accounts/model.ts +129 -0
- package/src/services/accounts/notification-sender.ts +31 -0
- package/src/services/accounts/posix.ts +386 -0
- package/src/services/accounts/request-policy.ts +11 -0
- package/src/services/accounts/switching.ts +126 -0
- package/src/services/accounts/users.ts +745 -0
- package/src/services/announcements/index.ts +224 -0
- package/src/services/app-approval.ts +498 -0
- package/src/services/audit/index.ts +438 -0
- package/src/services/auth-flows/index.ts +15 -0
- package/src/services/auth-flows/ipa.ts +197 -0
- package/src/services/auth-flows/magic-link.ts +185 -0
- package/src/services/auth-flows/notification-sender.ts +10 -0
- package/src/services/auth-flows/password-reset.ts +268 -0
- package/src/services/auth-flows/proxy-return.ts +49 -0
- package/src/services/document-extraction.ts +148 -0
- package/src/services/freeipa-config.ts +179 -0
- package/src/services/gateway.ts +186 -0
- package/src/services/identity/constants.ts +18 -0
- package/src/services/identity/index.ts +52 -0
- package/src/services/identity/invocation-actor.ts +120 -0
- package/src/services/identity/invocation-authority.ts +53 -0
- package/src/services/identity/invocation-operations.ts +15 -0
- package/src/services/identity/invocation-token.ts +276 -0
- package/src/services/identity/key-config.ts +41 -0
- package/src/services/identity/key-ring.ts +530 -0
- package/src/services/identity/metrics.ts +29 -0
- package/src/services/identity/routes.ts +18 -0
- package/src/services/identity/runtime-config.ts +88 -0
- package/src/services/identity/session-token.ts +152 -0
- package/src/services/identity/signer-refresh.ts +44 -0
- package/src/services/identity/workload-auth.ts +64 -0
- package/src/services/index.ts +187 -0
- package/src/services/ipa/auth.ts +216 -0
- package/src/services/ipa/connection.ts +57 -0
- package/src/services/ipa/effective-groups.ts +70 -0
- package/src/services/ipa/groups.ts +722 -0
- package/src/services/ipa/guard.ts +17 -0
- package/src/services/ipa/index.ts +17 -0
- package/src/services/ipa/posix.ts +19 -0
- package/src/services/ipa/profile.ts +132 -0
- package/src/services/ipa/runtime.ts +28 -0
- package/src/services/ipa/search.ts +154 -0
- package/src/services/ipa/service-account.ts +15 -0
- package/src/services/ipa/sync-planning.ts +100 -0
- package/src/services/ipa/sync.ts +862 -0
- package/src/services/ipa/users.ts +806 -0
- package/src/services/legal-consent.ts +76 -0
- package/src/services/logging/index.ts +477 -0
- package/src/services/logging/redaction.ts +20 -0
- package/src/services/logging/trace.ts +1231 -0
- package/src/services/mandates/index.ts +855 -0
- package/src/services/mandates/policy.ts +85 -0
- package/src/services/network-security.ts +31 -0
- package/src/services/notifications/batches.ts +722 -0
- package/src/services/notifications/browser.ts +252 -0
- package/src/services/notifications/catalog.ts +113 -0
- package/src/services/notifications/channels.ts +104 -0
- package/src/services/notifications/dispatcher.ts +139 -0
- package/src/services/notifications/email.ts +118 -0
- package/src/services/notifications/index.ts +633 -0
- package/src/services/notifications/live.ts +50 -0
- package/src/services/notifications/observability.ts +407 -0
- package/src/services/notifications/platform.ts +421 -0
- package/src/services/notifications/runtime.ts +120 -0
- package/src/services/notifications/user.ts +248 -0
- package/src/services/notifications/web-push-transport.ts +135 -0
- package/src/services/oauth-tokens.ts +209 -0
- package/src/services/outbox.ts +187 -0
- package/src/services/pdf/gotenberg.ts +290 -0
- package/src/services/pdf/index.ts +43 -0
- package/src/services/pdf/markdown.ts +209 -0
- package/src/services/pdf/template-preview.ts +122 -0
- package/src/services/postgres.ts +76 -0
- package/src/services/providers/index.ts +27 -0
- package/src/services/providers/local/auth.ts +51 -0
- package/src/services/providers/local/index.ts +4 -0
- package/src/services/providers/local/users.ts +263 -0
- package/src/services/rail-preferences.ts +27 -0
- package/src/services/runtime-lifecycle.ts +191 -0
- package/src/services/secrets.ts +10 -0
- package/src/services/service-account-credentials.ts +891 -0
- package/src/services/service-accounts.ts +184 -0
- package/src/services/session/index.ts +236 -0
- package/src/services/session/test-fixture.ts +18 -0
- package/src/services/session/user.ts +144 -0
- package/src/services/settings/api.ts +61 -0
- package/src/services/settings/app.ts +86 -0
- package/src/services/settings/core-settings.ts +796 -0
- package/src/services/settings/crypto.ts +69 -0
- package/src/services/settings/defaults.ts +374 -0
- package/src/services/settings/index.ts +215 -0
- package/src/services/settings/namespace.ts +9 -0
- package/src/services/settings/snapshot.ts +49 -0
- package/src/services/settings/store.ts +241 -0
- package/src/services/settings/templates.ts +4 -0
- package/src/services/sync-ops.ts +403 -0
- package/src/services/topic-cursor.ts +22 -0
- package/src/services/weather/forecast.ts +323 -0
- package/src/services/weather/geo.ts +119 -0
- package/src/services/weather/index.ts +67 -0
- package/src/services/weather/location.ts +24 -0
- package/src/services/weather/locations.ts +133 -0
- package/src/services/weather/migrate.ts +45 -0
- package/src/services/weather/settings.ts +66 -0
- package/src/services/weather/short-id.ts +57 -0
- package/src/services/weather/types.ts +61 -0
- package/src/services/weather/ui.ts +50 -0
- package/src/services/webauthn.ts +385 -0
- package/src/shared/account-action-notice.ts +70 -0
- package/src/shared/account-display.ts +15 -0
- package/src/shared/account-session.ts +15 -0
- package/src/shared/ai-platform-prompt.ts +134 -0
- package/src/shared/ai-usage.ts +49 -0
- package/src/shared/app-presentation.ts +48 -0
- package/src/shared/app-url.ts +12 -0
- package/src/shared/branding.ts +14 -0
- package/src/shared/capability-messages.ts +70 -0
- package/src/shared/email-html.ts +54 -0
- package/src/shared/format.ts +91 -0
- package/src/shared/help.ts +68 -0
- package/src/shared/icons.ts +400 -0
- package/src/shared/index.ts +54 -0
- package/src/shared/locale.ts +48 -0
- package/src/shared/login-method.ts +44 -0
- package/src/shared/markdown/client.ts +130 -0
- package/src/shared/markdown/extensions/code.ts +105 -0
- package/src/shared/markdown/extensions/guided-help.ts +100 -0
- package/src/shared/markdown/extensions/images.ts +79 -0
- package/src/shared/markdown/extensions/info-blocks.ts +97 -0
- package/src/shared/markdown/extensions/katex.ts +120 -0
- package/src/shared/markdown/extensions/links.ts +43 -0
- package/src/shared/markdown/extensions/mark.ts +48 -0
- package/src/shared/markdown/extensions/sub-sup.ts +60 -0
- package/src/shared/markdown/extensions/tables.ts +109 -0
- package/src/shared/markdown/extensions/task-list.ts +53 -0
- package/src/shared/markdown/formula.ts +1187 -0
- package/src/shared/markdown/index.ts +193 -0
- package/src/shared/markdown/shared.ts +36 -0
- package/src/shared/mock-cover.ts +130 -0
- package/src/shared/network-address.ts +106 -0
- package/src/shared/redirect.ts +56 -0
- package/src/shared/template-rendering.ts +140 -0
- package/src/shared/theme.ts +68 -0
- package/src/shared/time.ts +13 -0
- package/src/ssr/AdminLayout.tsx +53 -0
- package/src/ssr/AdminSidebar.tsx +50 -0
- package/src/ssr/AppLaunchpad.island.tsx +236 -0
- package/src/ssr/AppWorkspaceController.island.tsx +21 -0
- package/src/ssr/BrowserNotifications.island.tsx +135 -0
- package/src/ssr/GlobalAnnouncements.island.tsx +142 -0
- package/src/ssr/GlobalSearchDialog.behavior.test.tsx +96 -0
- package/src/ssr/GlobalSearchDialog.tsx +35 -0
- package/src/ssr/GlobalSearchHelpDialog.tsx +104 -0
- package/src/ssr/GlobalSearchTrigger.island.tsx +55 -0
- package/src/ssr/HotkeysHelpRail.island.tsx +55 -0
- package/src/ssr/Layout.render.test.tsx +148 -0
- package/src/ssr/Layout.tsx +292 -0
- package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
- package/src/ssr/LayoutFooter.tsx +22 -0
- package/src/ssr/LayoutHeader.tsx +81 -0
- package/src/ssr/LayoutHelp.tsx +869 -0
- package/src/ssr/LayoutPreferences.island.tsx +36 -0
- package/src/ssr/LayoutRail.tsx +58 -0
- package/src/ssr/MinimalLayout.tsx +46 -0
- package/src/ssr/PageError.tsx +29 -0
- package/src/ssr/ProfilePreferences.island.tsx +92 -0
- package/src/ssr/RailApps.island.tsx +61 -0
- package/src/ssr/RailEditor.tsx +293 -0
- package/src/ssr/RegisteredHelpDocuments.island.tsx +3 -0
- package/src/ssr/TimezoneCookie.island.tsx +23 -0
- package/src/ssr/admin-active-link.ts +47 -0
- package/src/ssr/admin-navigation.ts +78 -0
- package/src/ssr/app-appearance.ts +23 -0
- package/src/ssr/app-navigation.ts +41 -0
- package/src/ssr/help-messages.ts +104 -0
- package/src/ssr/index.ts +14 -0
- package/src/ssr/islands/SearchBar.island.tsx +80 -0
- package/src/ssr/islands/index.ts +16 -0
- package/src/ssr/layout-context.ts +17 -0
- package/src/ssr/layout-help-markdown.ts +9 -0
- package/src/ssr/layout-help-navigation.ts +24 -0
- package/src/ssr/layout-help-url.ts +5 -0
- package/src/ssr/layout-runtime.ts +22 -0
- package/src/ssr/page-error-messages.ts +25 -0
- package/src/ssr/platform-messages.ts +135 -0
- package/src/ssr/preference-controller.ts +44 -0
- package/src/ssr/profile-preferences-messages.ts +29 -0
- package/src/ssr/rail-context.ts +21 -0
- package/src/ssr/rail-messages.ts +66 -0
- package/src/ssr/rail-navigation.ts +60 -0
- package/src/ssr/runtime.ts +31 -0
- package/src/ssr/url-filter.ts +127 -0
- package/src/styles/base-popover.css +36 -0
- package/src/styles/effects.css +851 -0
- package/src/styles/global.css +268 -0
- package/src/styles/input.css +58 -0
- package/src/styles/tokens.css +230 -0
- package/src/styles/utilities-buttons.css +405 -0
- package/src/styles/utilities-data.css +33 -0
- package/src/styles/utilities-feedback.css +232 -0
- package/src/styles/utilities-layout.css +603 -0
- package/src/styles/utilities-markdown-editor.css +525 -0
- package/src/styles/utilities-markdown-table.css +234 -0
- package/src/styles/utilities-navigation.css +1054 -0
- package/src/types/ambient.d.ts +24 -0
- package/src/workflows/ai/index.ts +17 -0
- package/src/workflows/ai/runtime.ts +286 -0
- package/src/workflows/ai/store.ts +314 -0
- package/src/workflows/ai/types.ts +111 -0
- package/src/workflows/ai-actions.ts +269 -0
- package/src/workflows/authoring.ts +88 -0
- package/src/workflows/builtins.ts +193 -0
- package/src/workflows/contracts.ts +280 -0
- package/src/workflows/definition.ts +334 -0
- package/src/workflows/editor.ts +113 -0
- package/src/workflows/index.ts +71 -0
- package/src/workflows/language/binder.ts +45 -0
- package/src/workflows/language/canonical.ts +29 -0
- package/src/workflows/language/compiler.ts +396 -0
- package/src/workflows/language/expressions.ts +34 -0
- package/src/workflows/language/index.ts +20 -0
- package/src/workflows/language/references.ts +55 -0
- package/src/workflows/language/schema.ts +134 -0
- package/src/workflows/language/strict-yaml.ts +131 -0
- package/src/workflows/manifest.ts +24 -0
- package/src/workflows/module.ts +12 -0
- package/src/workflows/runtime/coordinator.ts +115 -0
- package/src/workflows/runtime/executor.ts +1003 -0
- package/src/workflows/runtime/index.ts +60 -0
- package/src/workflows/runtime/instant.ts +8 -0
- package/src/workflows/runtime/ports.ts +213 -0
- package/src/workflows/runtime/schedule.ts +203 -0
- package/src/workflows/runtime/trigger.ts +37 -0
- package/src/workflows/runtime/wake.ts +13 -0
- package/src/workflows/store/actions.ts +438 -0
- package/src/workflows/store/budget.ts +145 -0
- package/src/workflows/store/definitions.ts +345 -0
- package/src/workflows/store/events.ts +383 -0
- package/src/workflows/store/index.ts +107 -0
- package/src/workflows/store/observability.ts +594 -0
- package/src/workflows/store/runs.ts +975 -0
- package/src/workflows/store/transaction.ts +23 -0
- package/src/workflows/store/worker.ts +439 -0
- package/src/workflows/testing/conformance.ts +72 -0
- package/src/workflows/testing/index.ts +11 -0
- package/src/workflows/testing/process-fixtures.ts +275 -0
|
@@ -0,0 +1,1003 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WorkflowCondition,
|
|
3
|
+
WorkflowControlTraversal,
|
|
4
|
+
WorkflowDependency,
|
|
5
|
+
WorkflowExecutionError,
|
|
6
|
+
WorkflowIrStep,
|
|
7
|
+
WorkflowJsonValue,
|
|
8
|
+
WorkflowPlanningIssue,
|
|
9
|
+
WorkflowPlanningOutcome,
|
|
10
|
+
WorkflowStepOutcome,
|
|
11
|
+
} from "../contracts";
|
|
12
|
+
import { workflowPathKey } from "../contracts";
|
|
13
|
+
import { readWorkflowValuePath } from "../language/references";
|
|
14
|
+
import type {
|
|
15
|
+
WorkflowActionStep,
|
|
16
|
+
WorkflowDryRunActionContext,
|
|
17
|
+
WorkflowDryRunIssue,
|
|
18
|
+
WorkflowDryRunOptions,
|
|
19
|
+
WorkflowDryRunResult,
|
|
20
|
+
WorkflowExecuteActionContext,
|
|
21
|
+
WorkflowExecuteOptions,
|
|
22
|
+
WorkflowExecutionResult,
|
|
23
|
+
WorkflowHeartbeatOutcome,
|
|
24
|
+
WorkflowRestoredStep,
|
|
25
|
+
WorkflowRuntimeRunIdentity,
|
|
26
|
+
WorkflowRuntimeStepIdentity,
|
|
27
|
+
WorkflowRuntimeStepResult,
|
|
28
|
+
WorkflowTraceEvent,
|
|
29
|
+
WorkflowValueResolution,
|
|
30
|
+
WorkflowVariableScope,
|
|
31
|
+
} from "./ports";
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_MAX_LOOP_ITEMS = 100;
|
|
34
|
+
|
|
35
|
+
const EXACT_EXPRESSION = /^\$\{\{\s*([^{}]+?)\s*\}\}$/;
|
|
36
|
+
|
|
37
|
+
class RuntimeVariableScope implements WorkflowVariableScope {
|
|
38
|
+
readonly #values = new Map<string, WorkflowJsonValue>();
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
initial: Record<string, WorkflowJsonValue> = {},
|
|
42
|
+
readonly parent?: RuntimeVariableScope,
|
|
43
|
+
) {
|
|
44
|
+
for (const [name, value] of Object.entries(initial)) this.#values.set(name, value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get(name: string): WorkflowJsonValue | undefined {
|
|
48
|
+
return this.#values.has(name) ? this.#values.get(name) : this.parent?.get(name);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
has(name: string): boolean {
|
|
52
|
+
return this.#values.has(name) || this.parent?.has(name) === true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
set(name: string, value: WorkflowJsonValue): void {
|
|
56
|
+
this.#values.set(name, value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
snapshot(): Record<string, WorkflowJsonValue> {
|
|
60
|
+
return {
|
|
61
|
+
...(this.parent?.snapshot() ?? {}),
|
|
62
|
+
...Object.fromEntries(this.#values),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
child(initial: Record<string, WorkflowJsonValue> = {}): RuntimeVariableScope {
|
|
67
|
+
return new RuntimeVariableScope(initial, this);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class WorkflowCancellation extends Error {
|
|
72
|
+
constructor(readonly cancellation: Extract<WorkflowHeartbeatOutcome, { state: "canceled" }>) {
|
|
73
|
+
super(cancellation.message ?? "workflow canceled");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class WorkflowLeaseLostError extends Error {
|
|
78
|
+
override readonly name = "WorkflowLeaseLostError";
|
|
79
|
+
|
|
80
|
+
constructor() {
|
|
81
|
+
super("workflow execution lease was lost");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class WorkflowRetryableStepError extends Error {
|
|
86
|
+
override readonly name = "WorkflowRetryableStepError";
|
|
87
|
+
|
|
88
|
+
constructor(
|
|
89
|
+
readonly step: WorkflowRuntimeStepIdentity,
|
|
90
|
+
readonly executionError: WorkflowExecutionError,
|
|
91
|
+
) {
|
|
92
|
+
super(executionError.message);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class WorkflowValueError extends Error {}
|
|
97
|
+
|
|
98
|
+
class WorkflowValueWaiting extends Error {
|
|
99
|
+
constructor(
|
|
100
|
+
reference: string,
|
|
101
|
+
readonly dependency: WorkflowDependency,
|
|
102
|
+
) {
|
|
103
|
+
super(`workflow reference "${reference}" is waiting`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type RuntimeOptions = (WorkflowExecuteOptions & { mode: "execute" }) | (WorkflowDryRunOptions & { mode: "dryRun" });
|
|
108
|
+
|
|
109
|
+
type RuntimeState = {
|
|
110
|
+
options: RuntimeOptions;
|
|
111
|
+
run: WorkflowRuntimeRunIdentity;
|
|
112
|
+
effects: WorkflowJsonValue[];
|
|
113
|
+
issues: WorkflowDryRunIssue[];
|
|
114
|
+
cancellationTraced: boolean;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
type Continue = { state: "continue"; output?: WorkflowJsonValue };
|
|
118
|
+
type Halt =
|
|
119
|
+
| { state: "waiting"; dependency: Extract<WorkflowStepOutcome, { state: "waiting" }>["dependency"]; step: WorkflowRuntimeStepIdentity }
|
|
120
|
+
| { state: "failed"; error: WorkflowExecutionError; step: WorkflowRuntimeStepIdentity }
|
|
121
|
+
| { state: "needs_attention"; error: WorkflowExecutionError; step: WorkflowRuntimeStepIdentity }
|
|
122
|
+
| { state: "canceled"; message?: string; step?: WorkflowRuntimeStepIdentity }
|
|
123
|
+
| { state: "terminal_succeeded"; message?: string }
|
|
124
|
+
| { state: "terminal_planned"; status: "succeeded" | "failed"; message?: string; step: WorkflowRuntimeStepIdentity }
|
|
125
|
+
| { state: "unsupported"; reason: string; step: WorkflowRuntimeStepIdentity }
|
|
126
|
+
| { state: "indeterminate"; reason: string; step: WorkflowRuntimeStepIdentity };
|
|
127
|
+
type Flow = Continue | Halt;
|
|
128
|
+
|
|
129
|
+
type StepEvaluation = {
|
|
130
|
+
flow: Flow;
|
|
131
|
+
result?: WorkflowRuntimeStepResult;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const executionError = (code: string, message: string, retryable = false): WorkflowExecutionError => ({
|
|
135
|
+
code,
|
|
136
|
+
message,
|
|
137
|
+
retryable,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const errorMessage = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
|
141
|
+
|
|
142
|
+
const optionalOutput = (output: WorkflowJsonValue | undefined): { output?: WorkflowJsonValue } => (output === undefined ? {} : { output });
|
|
143
|
+
|
|
144
|
+
const optionalMessage = (message: string | undefined): { message?: string } => (message === undefined ? {} : { message });
|
|
145
|
+
|
|
146
|
+
const emit = async (state: RuntimeState, event: WorkflowTraceEvent): Promise<void> => {
|
|
147
|
+
try {
|
|
148
|
+
await state.options.trace?.emit(event);
|
|
149
|
+
} catch {
|
|
150
|
+
// Observability is never part of the run's correctness contract.
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const storedIssues = (issues: WorkflowDryRunIssue[]): WorkflowPlanningIssue[] =>
|
|
155
|
+
issues.map((issue) => ({
|
|
156
|
+
...issue,
|
|
157
|
+
step: {
|
|
158
|
+
key: issue.step.key,
|
|
159
|
+
sourcePath: [...issue.step.sourcePath],
|
|
160
|
+
iterationPath: [...issue.step.iterationPath],
|
|
161
|
+
path: [...issue.step.path],
|
|
162
|
+
kind: issue.step.kind,
|
|
163
|
+
...(issue.step.action ? { action: issue.step.action } : {}),
|
|
164
|
+
},
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
const restoredIssues = (state: RuntimeState, issues: WorkflowPlanningIssue[] | undefined): WorkflowDryRunIssue[] =>
|
|
168
|
+
issues?.map((issue) => ({ ...issue, step: { ...state.run, ...issue.step } })) ?? [];
|
|
169
|
+
|
|
170
|
+
const stepIdentity = (state: RuntimeState, step: WorkflowIrStep, iterationPath: number[]): WorkflowRuntimeStepIdentity => {
|
|
171
|
+
const iterationKey = iterationPath.length === 0 ? "" : `#${iterationPath.join(".")}`;
|
|
172
|
+
const path = [...step.sourcePath];
|
|
173
|
+
for (const iteration of iterationPath) path.push("$iteration", iteration);
|
|
174
|
+
return {
|
|
175
|
+
...state.run,
|
|
176
|
+
key: `${workflowPathKey(step.sourcePath)}${iterationKey}`,
|
|
177
|
+
sourcePath: [...step.sourcePath],
|
|
178
|
+
iterationPath: [...iterationPath],
|
|
179
|
+
path,
|
|
180
|
+
kind: step.kind,
|
|
181
|
+
...(step.kind === "action" ? { action: step.action } : {}),
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const resolveLocalReference = (state: RuntimeState, scope: RuntimeVariableScope, reference: string): WorkflowValueResolution => {
|
|
186
|
+
const segments = reference.split(".");
|
|
187
|
+
const rootName = segments.shift() ?? "";
|
|
188
|
+
let root: WorkflowJsonValue | undefined;
|
|
189
|
+
if (rootName === "inputs") root = state.options.invocation.inputs;
|
|
190
|
+
else if (rootName === "bindings") root = state.options.plan.bindings;
|
|
191
|
+
else if (rootName === "context") root = state.options.invocation.context ?? {};
|
|
192
|
+
else root = scope.get(rootName);
|
|
193
|
+
const value = root === undefined || segments.length === 0 ? root : readWorkflowValuePath(root, segments);
|
|
194
|
+
return value === undefined ? { state: "missing" } : { state: "resolved", value };
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const resolveReference = async (
|
|
198
|
+
state: RuntimeState,
|
|
199
|
+
scope: RuntimeVariableScope,
|
|
200
|
+
reference: string,
|
|
201
|
+
path: Array<string | number> = [],
|
|
202
|
+
): Promise<WorkflowValueResolution> => {
|
|
203
|
+
const local = () => resolveLocalReference(state, scope, reference);
|
|
204
|
+
if (!state.options.values) return local();
|
|
205
|
+
const resolved = await state.options.values.resolve({
|
|
206
|
+
reference,
|
|
207
|
+
path,
|
|
208
|
+
plan: state.options.plan,
|
|
209
|
+
invocation: state.options.invocation,
|
|
210
|
+
variables: scope,
|
|
211
|
+
fallback: () => {
|
|
212
|
+
const fallback = local();
|
|
213
|
+
return fallback.state === "resolved" ? fallback.value : undefined;
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
return resolved;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const evaluateValue = async (
|
|
220
|
+
state: RuntimeState,
|
|
221
|
+
scope: RuntimeVariableScope,
|
|
222
|
+
value: WorkflowJsonValue,
|
|
223
|
+
path: Array<string | number> = [],
|
|
224
|
+
): Promise<WorkflowJsonValue> => {
|
|
225
|
+
if (typeof value === "string") {
|
|
226
|
+
const match = EXACT_EXPRESSION.exec(value);
|
|
227
|
+
if (!match) return value;
|
|
228
|
+
const expression = match[1]?.trim() ?? "";
|
|
229
|
+
if (expression === "now()") return state.options.clock.now();
|
|
230
|
+
const resolved = await resolveReference(state, scope, expression, path);
|
|
231
|
+
if (resolved.state === "waiting") throw new WorkflowValueWaiting(expression, resolved.dependency);
|
|
232
|
+
if (resolved.state === "missing") throw new WorkflowValueError(`workflow reference "${expression}" is unavailable`);
|
|
233
|
+
return resolved.value;
|
|
234
|
+
}
|
|
235
|
+
if (Array.isArray(value)) {
|
|
236
|
+
const evaluated: WorkflowJsonValue[] = [];
|
|
237
|
+
for (const [index, item] of value.entries()) evaluated.push(await evaluateValue(state, scope, item, [...path, index]));
|
|
238
|
+
return evaluated;
|
|
239
|
+
}
|
|
240
|
+
if (value !== null && typeof value === "object") {
|
|
241
|
+
const evaluated: Record<string, WorkflowJsonValue> = {};
|
|
242
|
+
for (const [key, item] of Object.entries(value)) evaluated[key] = await evaluateValue(state, scope, item, [...path, key]);
|
|
243
|
+
return evaluated;
|
|
244
|
+
}
|
|
245
|
+
return value;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const jsonEqual = (left: WorkflowJsonValue, right: WorkflowJsonValue): boolean => {
|
|
249
|
+
if (left === right) return true;
|
|
250
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
251
|
+
return (
|
|
252
|
+
Array.isArray(left) &&
|
|
253
|
+
Array.isArray(right) &&
|
|
254
|
+
left.length === right.length &&
|
|
255
|
+
left.every((item, index) => jsonEqual(item, right[index]!))
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
if (left === null || right === null || typeof left !== "object" || typeof right !== "object") return false;
|
|
259
|
+
const leftKeys = Object.keys(left).sort();
|
|
260
|
+
const rightKeys = Object.keys(right).sort();
|
|
261
|
+
return (
|
|
262
|
+
leftKeys.length === rightKeys.length && leftKeys.every((key, index) => key === rightKeys[index] && jsonEqual(left[key]!, right[key]!))
|
|
263
|
+
);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const normalizeWorkflowText = (value: string): string => value.normalize("NFKC").toLowerCase();
|
|
267
|
+
|
|
268
|
+
const evaluateCondition = async (
|
|
269
|
+
state: RuntimeState,
|
|
270
|
+
scope: RuntimeVariableScope,
|
|
271
|
+
condition: WorkflowCondition,
|
|
272
|
+
path: Array<string | number>,
|
|
273
|
+
): Promise<boolean> => {
|
|
274
|
+
if (condition.operator === "all") {
|
|
275
|
+
let waiting: WorkflowValueWaiting | undefined;
|
|
276
|
+
for (const [index, child] of condition.conditions.entries()) {
|
|
277
|
+
try {
|
|
278
|
+
if (!(await evaluateCondition(state, scope, child, [...path, "all", index]))) return false;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (!(error instanceof WorkflowValueWaiting)) throw error;
|
|
281
|
+
waiting ??= error;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (waiting) throw waiting;
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
if (condition.operator === "any") {
|
|
288
|
+
let waiting: WorkflowValueWaiting | undefined;
|
|
289
|
+
for (const [index, child] of condition.conditions.entries()) {
|
|
290
|
+
try {
|
|
291
|
+
if (await evaluateCondition(state, scope, child, [...path, "any", index])) return true;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (!(error instanceof WorkflowValueWaiting)) throw error;
|
|
294
|
+
waiting ??= error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (waiting) throw waiting;
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
if (condition.operator === "not") return !(await evaluateCondition(state, scope, condition.condition, [...path, "not"]));
|
|
301
|
+
if (condition.operator === "exists") {
|
|
302
|
+
const resolved = await resolveReference(state, scope, condition.reference, [...path, "exists"]);
|
|
303
|
+
if (resolved.state === "waiting") throw new WorkflowValueWaiting(condition.reference, resolved.dependency);
|
|
304
|
+
return resolved.state === "resolved" && resolved.value !== null && resolved.value !== "";
|
|
305
|
+
}
|
|
306
|
+
const left = await evaluateValue(state, scope, condition.operands[0], [...path, condition.operator, 0]);
|
|
307
|
+
const right = await evaluateValue(state, scope, condition.operands[1], [...path, condition.operator, 1]);
|
|
308
|
+
if (condition.operator === "equals") return jsonEqual(left, right);
|
|
309
|
+
if (condition.operator === "notEquals") return !jsonEqual(left, right);
|
|
310
|
+
if (condition.operator === "includes") {
|
|
311
|
+
if (!Array.isArray(left)) throw new WorkflowValueError("includes requires an array as its first operand");
|
|
312
|
+
return left.some((item) => jsonEqual(item, right));
|
|
313
|
+
}
|
|
314
|
+
if (typeof left !== "string" || typeof right !== "string") {
|
|
315
|
+
throw new WorkflowValueError(`${condition.operator} requires two text operands`);
|
|
316
|
+
}
|
|
317
|
+
const normalizedLeft = normalizeWorkflowText(left);
|
|
318
|
+
const normalizedRight = normalizeWorkflowText(right);
|
|
319
|
+
if (condition.operator === "textEquals") return normalizedLeft === normalizedRight;
|
|
320
|
+
if (condition.operator === "contains") return normalizedLeft.includes(normalizedRight);
|
|
321
|
+
if (condition.operator === "startsWith") return normalizedLeft.startsWith(normalizedRight);
|
|
322
|
+
return normalizedLeft.endsWith(normalizedRight);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const traceCancellation = async (
|
|
326
|
+
state: RuntimeState,
|
|
327
|
+
cancellation: Extract<WorkflowHeartbeatOutcome, { state: "canceled" }>,
|
|
328
|
+
): Promise<void> => {
|
|
329
|
+
if (state.cancellationTraced) return;
|
|
330
|
+
state.cancellationTraced = true;
|
|
331
|
+
await emit(state, { type: "run.canceled", run: state.run, ...optionalMessage(cancellation.message) });
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const heartbeat = async (state: RuntimeState): Promise<Extract<WorkflowHeartbeatOutcome, { state: "canceled" }> | null> => {
|
|
335
|
+
const outcome = await state.options.repository.heartbeat(state.run);
|
|
336
|
+
if (outcome.state === "active") return null;
|
|
337
|
+
if (outcome.state === "stale") throw new WorkflowLeaseLostError();
|
|
338
|
+
await traceCancellation(state, outcome);
|
|
339
|
+
return outcome;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const actionContext = (
|
|
343
|
+
state: RuntimeState,
|
|
344
|
+
scope: RuntimeVariableScope,
|
|
345
|
+
step: WorkflowRuntimeStepIdentity,
|
|
346
|
+
): WorkflowExecuteActionContext | WorkflowDryRunActionContext => {
|
|
347
|
+
const common = {
|
|
348
|
+
run: state.run,
|
|
349
|
+
step,
|
|
350
|
+
plan: state.options.plan,
|
|
351
|
+
invocation: state.options.invocation,
|
|
352
|
+
variables: scope,
|
|
353
|
+
evaluate: (value: WorkflowJsonValue, path?: Array<string | number>) => evaluateValue(state, scope, value, path ?? step.sourcePath),
|
|
354
|
+
resolveReference: async (reference: string, path?: Array<string | number>) => {
|
|
355
|
+
const resolved = await resolveReference(state, scope, reference, path ?? step.sourcePath);
|
|
356
|
+
if (resolved.state === "waiting") throw new WorkflowValueWaiting(reference, resolved.dependency);
|
|
357
|
+
return resolved.state === "resolved" ? resolved.value : undefined;
|
|
358
|
+
},
|
|
359
|
+
heartbeat: async () => {
|
|
360
|
+
const cancellation = await heartbeat(state);
|
|
361
|
+
if (cancellation) throw new WorkflowCancellation(cancellation);
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
return state.options.mode === "execute"
|
|
365
|
+
? ({ ...common, mode: "execute" } as WorkflowExecuteActionContext)
|
|
366
|
+
: ({ ...common, mode: "dryRun" } as WorkflowDryRunActionContext);
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const invalidRestoration = (state: RuntimeState, step: WorkflowRuntimeStepIdentity, message: string): Flow =>
|
|
370
|
+
state.options.mode === "execute"
|
|
371
|
+
? { state: "needs_attention", error: executionError("WORKFLOW_RESTORE_INVALID", message), step }
|
|
372
|
+
: { state: "indeterminate", reason: message, step };
|
|
373
|
+
|
|
374
|
+
const persistedTraversalRecord = (kind: WorkflowControlTraversal["kind"], value: unknown): Record<string, unknown> => {
|
|
375
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
376
|
+
throw new WorkflowValueError(`completed ${kind} step has an invalid persisted control traversal`);
|
|
377
|
+
}
|
|
378
|
+
const traversal = value as Record<string, unknown>;
|
|
379
|
+
if (traversal.kind !== kind) {
|
|
380
|
+
throw new WorkflowValueError(`completed ${kind} step has a ${String(traversal.kind)} traversal`);
|
|
381
|
+
}
|
|
382
|
+
return traversal;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const persistedIfTraversal = (value: unknown): Extract<WorkflowControlTraversal, { kind: "if" }> => {
|
|
386
|
+
const traversal = persistedTraversalRecord("if", value);
|
|
387
|
+
if (!Array.isArray(traversal.branches) || traversal.branches.length === 0) {
|
|
388
|
+
throw new WorkflowValueError("completed if step has invalid persisted traversal branches");
|
|
389
|
+
}
|
|
390
|
+
if (
|
|
391
|
+
traversal.branches.length > 2 ||
|
|
392
|
+
traversal.branches.some((branch) => branch !== "then" && branch !== "else") ||
|
|
393
|
+
new Set(traversal.branches).size !== traversal.branches.length
|
|
394
|
+
) {
|
|
395
|
+
throw new WorkflowValueError("completed if step has invalid persisted traversal branches");
|
|
396
|
+
}
|
|
397
|
+
return { kind: "if", branches: traversal.branches as Array<"then" | "else"> };
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const persistedSwitchTraversal = (
|
|
401
|
+
irStep: Extract<WorkflowIrStep, { kind: "switch" }>,
|
|
402
|
+
value: unknown,
|
|
403
|
+
): Extract<WorkflowControlTraversal, { kind: "switch" }> => {
|
|
404
|
+
const traversal = persistedTraversalRecord("switch", value);
|
|
405
|
+
if (!Array.isArray(traversal.branches) || traversal.branches.length === 0) {
|
|
406
|
+
throw new WorkflowValueError("completed switch step has invalid persisted traversal branches");
|
|
407
|
+
}
|
|
408
|
+
const branches = traversal.branches as unknown[];
|
|
409
|
+
if (
|
|
410
|
+
branches.length > irStep.cases.length + 1 ||
|
|
411
|
+
branches.some(
|
|
412
|
+
(branch) => branch !== "default" && (!Number.isSafeInteger(branch) || Number(branch) < 0 || Number(branch) >= irStep.cases.length),
|
|
413
|
+
) ||
|
|
414
|
+
new Set(branches).size !== branches.length
|
|
415
|
+
) {
|
|
416
|
+
throw new WorkflowValueError("completed switch step has invalid persisted traversal branches");
|
|
417
|
+
}
|
|
418
|
+
return { kind: "switch", branches: branches as Array<number | "default"> };
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const persistedForEachTraversal = (value: unknown): Extract<WorkflowControlTraversal, { kind: "forEach" }> => {
|
|
422
|
+
const traversal = persistedTraversalRecord("forEach", value);
|
|
423
|
+
if (!Array.isArray(traversal.items)) {
|
|
424
|
+
throw new WorkflowValueError("completed forEach step has invalid persisted traversal items");
|
|
425
|
+
}
|
|
426
|
+
return { kind: "forEach", items: traversal.items as WorkflowJsonValue[] };
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const restoreCompletedSteps = async (
|
|
430
|
+
state: RuntimeState,
|
|
431
|
+
scope: RuntimeVariableScope,
|
|
432
|
+
steps: WorkflowIrStep[],
|
|
433
|
+
iterationPath: number[],
|
|
434
|
+
): Promise<Flow> => {
|
|
435
|
+
for (const irStep of steps) {
|
|
436
|
+
const step = stepIdentity(state, irStep, iterationPath);
|
|
437
|
+
const restored = await state.options.repository.restoreStepOutcome(step);
|
|
438
|
+
if (!restored) return invalidRestoration(state, step, `completed control step is missing descendant "${step.key}"`);
|
|
439
|
+
const flow = await restoreStep(state, scope, irStep, step, restored, false);
|
|
440
|
+
if (state.options.mode === "dryRun" && (flow.state === "unsupported" || flow.state === "indeterminate")) continue;
|
|
441
|
+
if (flow.state !== "continue") return flow;
|
|
442
|
+
}
|
|
443
|
+
return { state: "continue" };
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const restoreCompletedIfDescendants = async (
|
|
447
|
+
state: RuntimeState,
|
|
448
|
+
scope: RuntimeVariableScope,
|
|
449
|
+
irStep: Extract<WorkflowIrStep, { kind: "if" }>,
|
|
450
|
+
iterationPath: number[],
|
|
451
|
+
persistedTraversal: unknown,
|
|
452
|
+
): Promise<Flow> => {
|
|
453
|
+
const traversal = persistedIfTraversal(persistedTraversal);
|
|
454
|
+
for (const branch of traversal.branches) {
|
|
455
|
+
const flow = await restoreCompletedSteps(state, scope.child(), branch === "then" ? irStep.then : irStep.else, iterationPath);
|
|
456
|
+
if (flow.state !== "continue") return flow;
|
|
457
|
+
}
|
|
458
|
+
return { state: "continue" };
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const restoreCompletedSwitchDescendants = async (
|
|
462
|
+
state: RuntimeState,
|
|
463
|
+
scope: RuntimeVariableScope,
|
|
464
|
+
irStep: Extract<WorkflowIrStep, { kind: "switch" }>,
|
|
465
|
+
iterationPath: number[],
|
|
466
|
+
persistedTraversal: unknown,
|
|
467
|
+
): Promise<Flow> => {
|
|
468
|
+
const traversal = persistedSwitchTraversal(irStep, persistedTraversal);
|
|
469
|
+
for (const branch of traversal.branches) {
|
|
470
|
+
const steps = branch === "default" ? irStep.default : irStep.cases[branch]?.steps;
|
|
471
|
+
if (!steps) throw new WorkflowValueError(`completed switch step references unavailable branch "${branch}"`);
|
|
472
|
+
const flow = await restoreCompletedSteps(state, scope.child(), steps, iterationPath);
|
|
473
|
+
if (flow.state !== "continue") return flow;
|
|
474
|
+
}
|
|
475
|
+
return { state: "continue" };
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const restoreCompletedForEachDescendants = async (
|
|
479
|
+
state: RuntimeState,
|
|
480
|
+
scope: RuntimeVariableScope,
|
|
481
|
+
irStep: Extract<WorkflowIrStep, { kind: "forEach" }>,
|
|
482
|
+
iterationPath: number[],
|
|
483
|
+
persistedTraversal: unknown,
|
|
484
|
+
): Promise<Flow> => {
|
|
485
|
+
const traversal = persistedForEachTraversal(persistedTraversal);
|
|
486
|
+
const limit = loopItemLimit(state);
|
|
487
|
+
if (traversal.items.length > limit) {
|
|
488
|
+
throw new WorkflowValueError(`completed forEach traversal has ${traversal.items.length} items; limit is ${limit}`);
|
|
489
|
+
}
|
|
490
|
+
for (let index = 0; index < traversal.items.length; index += 1) {
|
|
491
|
+
const flow = await restoreCompletedSteps(state, scope.child({ [irStep.alias]: traversal.items[index]! }), irStep.steps, [
|
|
492
|
+
...iterationPath,
|
|
493
|
+
index,
|
|
494
|
+
]);
|
|
495
|
+
if (flow.state !== "continue") return flow;
|
|
496
|
+
}
|
|
497
|
+
return { state: "continue" };
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const restoreCompletedControlDescendants = (
|
|
501
|
+
state: RuntimeState,
|
|
502
|
+
scope: RuntimeVariableScope,
|
|
503
|
+
irStep: Exclude<WorkflowIrStep, { kind: "action" }>,
|
|
504
|
+
iterationPath: number[],
|
|
505
|
+
persistedTraversal: WorkflowControlTraversal | undefined,
|
|
506
|
+
): Promise<Flow> => {
|
|
507
|
+
if (!persistedTraversal) {
|
|
508
|
+
throw new WorkflowValueError(`completed ${irStep.kind} step is missing its persisted control traversal`);
|
|
509
|
+
}
|
|
510
|
+
if (irStep.kind === "if") return restoreCompletedIfDescendants(state, scope, irStep, iterationPath, persistedTraversal);
|
|
511
|
+
if (irStep.kind === "switch") return restoreCompletedSwitchDescendants(state, scope, irStep, iterationPath, persistedTraversal);
|
|
512
|
+
return restoreCompletedForEachDescendants(state, scope, irStep, iterationPath, persistedTraversal);
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const restoreCompletedState = async (
|
|
516
|
+
state: RuntimeState,
|
|
517
|
+
scope: RuntimeVariableScope,
|
|
518
|
+
irStep: WorkflowIrStep,
|
|
519
|
+
step: WorkflowRuntimeStepIdentity,
|
|
520
|
+
restored: WorkflowRestoredStep,
|
|
521
|
+
): Promise<Flow> => {
|
|
522
|
+
if (irStep.kind !== "action") {
|
|
523
|
+
if (restored.mode === "execute" && restored.outcome.state === "completed") {
|
|
524
|
+
return restoreCompletedControlDescendants(state, scope, irStep, step.iterationPath, restored.outcome.control);
|
|
525
|
+
}
|
|
526
|
+
if (restored.mode === "dryRun" && restored.outcome.state === "planned") {
|
|
527
|
+
return restoreCompletedControlDescendants(state, scope, irStep, step.iterationPath, restored.outcome.control);
|
|
528
|
+
}
|
|
529
|
+
return { state: "continue" };
|
|
530
|
+
}
|
|
531
|
+
if (restored.mode === "execute" && restored.outcome.state === "completed") {
|
|
532
|
+
const handler = state.options.mode === "execute" ? state.options.actions.get(irStep.action) : undefined;
|
|
533
|
+
if (!handler) return invalidRestoration(state, step, `action "${irStep.action}" is unavailable while restoring step "${step.key}"`);
|
|
534
|
+
await handler.restoreCompleted?.(actionContext(state, scope, step) as WorkflowExecuteActionContext, irStep, restored.outcome);
|
|
535
|
+
}
|
|
536
|
+
if (restored.mode === "dryRun" && restored.outcome.state === "planned") {
|
|
537
|
+
const handler = state.options.mode === "dryRun" ? state.options.actions.get(irStep.action) : undefined;
|
|
538
|
+
if (!handler)
|
|
539
|
+
return invalidRestoration(state, step, `planner for action "${irStep.action}" is unavailable while restoring step "${step.key}"`);
|
|
540
|
+
await handler.restoreCompleted?.(actionContext(state, scope, step) as WorkflowDryRunActionContext, irStep, restored.outcome);
|
|
541
|
+
}
|
|
542
|
+
return { state: "continue" };
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
const restoreStep = async (
|
|
546
|
+
state: RuntimeState,
|
|
547
|
+
scope: RuntimeVariableScope,
|
|
548
|
+
irStep: WorkflowIrStep,
|
|
549
|
+
step: WorkflowRuntimeStepIdentity,
|
|
550
|
+
restored: WorkflowRestoredStep,
|
|
551
|
+
collectPlanningOutcome = true,
|
|
552
|
+
): Promise<Flow> => {
|
|
553
|
+
if (restored.mode !== state.options.mode) {
|
|
554
|
+
return invalidRestoration(state, step, `restored step "${step.key}" belongs to ${restored.mode}, not ${state.options.mode}`);
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
const descendants = await restoreCompletedState(state, scope, irStep, step, restored);
|
|
558
|
+
if (descendants.state !== "continue") return descendants;
|
|
559
|
+
} catch (error) {
|
|
560
|
+
if (error instanceof WorkflowLeaseLostError) throw error;
|
|
561
|
+
if (error instanceof WorkflowCancellation) {
|
|
562
|
+
await traceCancellation(state, error.cancellation);
|
|
563
|
+
return { state: "canceled", ...optionalMessage(error.cancellation.message), step };
|
|
564
|
+
}
|
|
565
|
+
return invalidRestoration(state, step, `restoring step "${step.key}" failed: ${errorMessage(error)}`);
|
|
566
|
+
}
|
|
567
|
+
if (
|
|
568
|
+
collectPlanningOutcome &&
|
|
569
|
+
restored.mode === "dryRun" &&
|
|
570
|
+
(restored.outcome.state === "planned" || restored.outcome.state === "terminal")
|
|
571
|
+
) {
|
|
572
|
+
state.effects.push(...restored.outcome.effects);
|
|
573
|
+
state.issues.push(...restoredIssues(state, restored.outcome.issues));
|
|
574
|
+
}
|
|
575
|
+
await emit(state, { type: "step.restored", step, restored });
|
|
576
|
+
const cancellation = await heartbeat(state);
|
|
577
|
+
if (cancellation) return { state: "canceled", ...optionalMessage(cancellation.message), step };
|
|
578
|
+
return restored.mode === "execute" ? flowFromExecutionOutcome(restored.outcome, step) : flowFromPlanningOutcome(restored.outcome, step);
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const flowFromExecutionOutcome = (outcome: WorkflowStepOutcome, step: WorkflowRuntimeStepIdentity): Flow => {
|
|
582
|
+
if (outcome.state === "completed") return { state: "continue", ...optionalOutput(outcome.output) };
|
|
583
|
+
if (outcome.state === "waiting") return { state: "waiting", dependency: outcome.dependency, step };
|
|
584
|
+
if (outcome.state === "failed") return { state: "failed", error: outcome.error, step };
|
|
585
|
+
if (outcome.state === "needs_attention") return { state: "needs_attention", error: outcome.error, step };
|
|
586
|
+
return outcome.status === "succeeded"
|
|
587
|
+
? { state: "terminal_succeeded", ...optionalMessage(outcome.message) }
|
|
588
|
+
: { state: "canceled", ...optionalMessage(outcome.message), step };
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const flowFromPlanningOutcome = (outcome: WorkflowPlanningOutcome, step: WorkflowRuntimeStepIdentity): Flow => {
|
|
592
|
+
if (outcome.state === "planned") return { state: "continue", ...optionalOutput(outcome.output) };
|
|
593
|
+
if (outcome.state === "terminal") {
|
|
594
|
+
return { state: "terminal_planned", status: outcome.status, ...optionalMessage(outcome.message), step };
|
|
595
|
+
}
|
|
596
|
+
if (outcome.state === "canceled") return { state: "canceled", ...optionalMessage(outcome.message), step };
|
|
597
|
+
return outcome.state === "unsupported"
|
|
598
|
+
? { state: "unsupported", reason: outcome.reason, step }
|
|
599
|
+
: { state: "indeterminate", reason: outcome.reason, step };
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
const evaluateAction = async (
|
|
603
|
+
state: RuntimeState,
|
|
604
|
+
scope: RuntimeVariableScope,
|
|
605
|
+
irStep: WorkflowActionStep,
|
|
606
|
+
step: WorkflowRuntimeStepIdentity,
|
|
607
|
+
): Promise<StepEvaluation> => {
|
|
608
|
+
const policy = state.options.plan.actionPolicies[irStep.action];
|
|
609
|
+
if (!policy) {
|
|
610
|
+
const message = `action "${irStep.action}" has no bound runtime policy`;
|
|
611
|
+
if (state.options.mode === "execute") {
|
|
612
|
+
const outcome: WorkflowStepOutcome = {
|
|
613
|
+
state: "failed",
|
|
614
|
+
error: executionError("WORKFLOW_ACTION_POLICY_MISSING", message),
|
|
615
|
+
};
|
|
616
|
+
return { flow: flowFromExecutionOutcome(outcome, step), result: { mode: "execute", outcome } };
|
|
617
|
+
}
|
|
618
|
+
const outcome: WorkflowPlanningOutcome = { state: "unsupported", reason: message };
|
|
619
|
+
return { flow: flowFromPlanningOutcome(outcome, step), result: { mode: "dryRun", outcome } };
|
|
620
|
+
}
|
|
621
|
+
if (state.options.mode === "execute") {
|
|
622
|
+
const handler = state.options.actions.get(irStep.action);
|
|
623
|
+
const outcome: WorkflowStepOutcome = handler
|
|
624
|
+
? await handler.execute(actionContext(state, scope, step) as WorkflowExecuteActionContext, irStep)
|
|
625
|
+
: {
|
|
626
|
+
state: "failed",
|
|
627
|
+
error: executionError("WORKFLOW_ACTION_UNSUPPORTED", `action "${irStep.action}" has no execute handler`),
|
|
628
|
+
};
|
|
629
|
+
return { flow: flowFromExecutionOutcome(outcome, step), result: { mode: "execute", outcome } };
|
|
630
|
+
}
|
|
631
|
+
if (policy.dryRun === "unsupported") {
|
|
632
|
+
const outcome: WorkflowPlanningOutcome = {
|
|
633
|
+
state: "unsupported",
|
|
634
|
+
reason: `action "${irStep.action}" does not support dry-run`,
|
|
635
|
+
};
|
|
636
|
+
return { flow: flowFromPlanningOutcome(outcome, step), result: { mode: "dryRun", outcome } };
|
|
637
|
+
}
|
|
638
|
+
const handler = state.options.actions.get(irStep.action);
|
|
639
|
+
const outcome: WorkflowPlanningOutcome = handler
|
|
640
|
+
? await handler.plan(actionContext(state, scope, step) as WorkflowDryRunActionContext, irStep)
|
|
641
|
+
: { state: "unsupported", reason: `action "${irStep.action}" has no dry-run handler` };
|
|
642
|
+
if (outcome.state === "planned" || outcome.state === "terminal") {
|
|
643
|
+
state.effects.push(...outcome.effects);
|
|
644
|
+
state.issues.push(...restoredIssues(state, outcome.issues));
|
|
645
|
+
}
|
|
646
|
+
return { flow: flowFromPlanningOutcome(outcome, step), result: { mode: "dryRun", outcome } };
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const resultForCompletedControl = (
|
|
650
|
+
state: RuntimeState,
|
|
651
|
+
output: WorkflowJsonValue | undefined,
|
|
652
|
+
control: WorkflowControlTraversal,
|
|
653
|
+
effectsStart: number,
|
|
654
|
+
issuesStart: number,
|
|
655
|
+
): WorkflowRuntimeStepResult =>
|
|
656
|
+
state.options.mode === "execute"
|
|
657
|
+
? { mode: "execute", outcome: { state: "completed", ...optionalOutput(output), control } }
|
|
658
|
+
: {
|
|
659
|
+
mode: "dryRun",
|
|
660
|
+
outcome: {
|
|
661
|
+
state: "planned",
|
|
662
|
+
...optionalOutput(output),
|
|
663
|
+
control,
|
|
664
|
+
effects: state.effects.slice(effectsStart),
|
|
665
|
+
issues: storedIssues(state.issues.slice(issuesStart)),
|
|
666
|
+
},
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const resultForHaltedControl = (state: RuntimeState, flow: Halt, effectsStart: number, issuesStart: number): WorkflowRuntimeStepResult => {
|
|
670
|
+
if (state.options.mode === "execute") {
|
|
671
|
+
if (flow.state === "waiting") return { mode: "execute", outcome: { state: "waiting", dependency: flow.dependency } };
|
|
672
|
+
if (flow.state === "failed") return { mode: "execute", outcome: { state: "failed", error: flow.error } };
|
|
673
|
+
if (flow.state === "needs_attention") return { mode: "execute", outcome: { state: "needs_attention", error: flow.error } };
|
|
674
|
+
if (flow.state === "canceled") {
|
|
675
|
+
return { mode: "execute", outcome: { state: "terminal", status: "canceled", ...optionalMessage(flow.message) } };
|
|
676
|
+
}
|
|
677
|
+
if (flow.state === "terminal_succeeded") {
|
|
678
|
+
return { mode: "execute", outcome: { state: "terminal", status: "succeeded", ...optionalMessage(flow.message) } };
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
mode: "execute",
|
|
682
|
+
outcome: {
|
|
683
|
+
state: "failed",
|
|
684
|
+
error: executionError(
|
|
685
|
+
"WORKFLOW_RUNTIME_INVALID",
|
|
686
|
+
flow.state === "terminal_planned" ? "execute traversal produced a dry-run terminal outcome" : flow.reason,
|
|
687
|
+
),
|
|
688
|
+
},
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (flow.state === "terminal_planned") {
|
|
693
|
+
return {
|
|
694
|
+
mode: "dryRun",
|
|
695
|
+
outcome: {
|
|
696
|
+
state: "terminal",
|
|
697
|
+
status: flow.status,
|
|
698
|
+
...optionalMessage(flow.message),
|
|
699
|
+
effects: state.effects.slice(effectsStart),
|
|
700
|
+
issues: storedIssues(state.issues.slice(issuesStart)),
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
if (flow.state === "unsupported" || flow.state === "indeterminate") {
|
|
705
|
+
return { mode: "dryRun", outcome: { state: flow.state, reason: flow.reason } };
|
|
706
|
+
}
|
|
707
|
+
if (flow.state === "canceled") {
|
|
708
|
+
return { mode: "dryRun", outcome: { state: "canceled", ...optionalMessage(flow.message) } };
|
|
709
|
+
}
|
|
710
|
+
return { mode: "dryRun", outcome: { state: "indeterminate", reason: `dry-run traversal produced ${flow.state}` } };
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
const loopItemLimit = (state: RuntimeState): number => {
|
|
714
|
+
const limits = [state.options.plan.maxLoopItems, state.options.maxLoopItems].filter((value): value is number => value !== undefined);
|
|
715
|
+
for (const limit of limits) {
|
|
716
|
+
if (!Number.isSafeInteger(limit) || limit < 0) throw new WorkflowValueError("maxLoopItems must be a non-negative safe integer");
|
|
717
|
+
}
|
|
718
|
+
return limits.length > 0 ? Math.min(...limits) : DEFAULT_MAX_LOOP_ITEMS;
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const analyzeUnknownBranches = async (
|
|
722
|
+
state: RuntimeState,
|
|
723
|
+
scope: RuntimeVariableScope,
|
|
724
|
+
irStep: Extract<WorkflowIrStep, { kind: "if" | "switch" }>,
|
|
725
|
+
iterationPath: number[],
|
|
726
|
+
reason: string,
|
|
727
|
+
): Promise<Flow> => {
|
|
728
|
+
state.issues.push({ state: "indeterminate", reason, step: stepIdentity(state, irStep, iterationPath) });
|
|
729
|
+
const branches =
|
|
730
|
+
irStep.kind === "if" ? [irStep.then, irStep.else] : [...irStep.cases.map((candidate) => candidate.steps), irStep.default];
|
|
731
|
+
for (const branch of branches) {
|
|
732
|
+
const flow = await runSteps(state, branch, scope.child(), iterationPath);
|
|
733
|
+
if (flow.state === "canceled") return flow;
|
|
734
|
+
}
|
|
735
|
+
return { state: "continue" };
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const evaluateControl = async (
|
|
739
|
+
state: RuntimeState,
|
|
740
|
+
scope: RuntimeVariableScope,
|
|
741
|
+
irStep: Exclude<WorkflowIrStep, { kind: "action" }>,
|
|
742
|
+
iterationPath: number[],
|
|
743
|
+
): Promise<StepEvaluation> => {
|
|
744
|
+
const effectsStart = state.effects.length;
|
|
745
|
+
const issuesStart = state.issues.length;
|
|
746
|
+
let control: WorkflowControlTraversal;
|
|
747
|
+
let flow: Flow;
|
|
748
|
+
if (irStep.kind === "if") {
|
|
749
|
+
try {
|
|
750
|
+
const matched = await evaluateCondition(state, scope, irStep.condition, [...irStep.sourcePath, "if"]);
|
|
751
|
+
control = { kind: "if", branches: [matched ? "then" : "else"] };
|
|
752
|
+
flow = await runSteps(state, matched ? irStep.then : irStep.else, scope.child(), iterationPath);
|
|
753
|
+
} catch (error) {
|
|
754
|
+
if (state.options.mode !== "dryRun") throw error;
|
|
755
|
+
control = { kind: "if", branches: ["then", "else"] };
|
|
756
|
+
flow = await analyzeUnknownBranches(state, scope, irStep, iterationPath, errorMessage(error));
|
|
757
|
+
}
|
|
758
|
+
} else if (irStep.kind === "switch") {
|
|
759
|
+
try {
|
|
760
|
+
const value = await evaluateValue(state, scope, irStep.value, [...irStep.sourcePath, "switch"]);
|
|
761
|
+
let matched: (typeof irStep.cases)[number] | undefined;
|
|
762
|
+
for (const [index, candidate] of irStep.cases.entries()) {
|
|
763
|
+
if (jsonEqual(value, await evaluateValue(state, scope, candidate.when, [...irStep.sourcePath, "cases", index, "when"]))) {
|
|
764
|
+
matched = candidate;
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
control = { kind: "switch", branches: [matched ? irStep.cases.indexOf(matched) : "default"] };
|
|
769
|
+
flow = await runSteps(state, matched?.steps ?? irStep.default, scope.child(), iterationPath);
|
|
770
|
+
} catch (error) {
|
|
771
|
+
if (state.options.mode !== "dryRun") throw error;
|
|
772
|
+
control = { kind: "switch", branches: [...irStep.cases.map((_, index) => index), "default"] };
|
|
773
|
+
flow = await analyzeUnknownBranches(state, scope, irStep, iterationPath, errorMessage(error));
|
|
774
|
+
}
|
|
775
|
+
} else {
|
|
776
|
+
const resolved = await resolveReference(state, scope, irStep.reference, [...irStep.sourcePath, "forEach"]);
|
|
777
|
+
if (resolved.state === "waiting" && state.options.mode !== "dryRun") {
|
|
778
|
+
throw new WorkflowValueWaiting(irStep.reference, resolved.dependency);
|
|
779
|
+
}
|
|
780
|
+
const value = resolved.state === "resolved" ? resolved.value : undefined;
|
|
781
|
+
if (!Array.isArray(value)) {
|
|
782
|
+
const reason =
|
|
783
|
+
resolved.state === "waiting"
|
|
784
|
+
? `forEach reference "${irStep.reference}" is waiting`
|
|
785
|
+
: `forEach reference "${irStep.reference}" must resolve to a JSON array`;
|
|
786
|
+
if (state.options.mode !== "dryRun") throw new WorkflowValueError(reason);
|
|
787
|
+
state.issues.push({ state: "indeterminate", reason, step: stepIdentity(state, irStep, iterationPath) });
|
|
788
|
+
control = { kind: "forEach", items: [null] };
|
|
789
|
+
flow = await runSteps(state, irStep.steps, scope.child({ [irStep.alias]: null }), [...iterationPath, 0]);
|
|
790
|
+
if (flow.state === "canceled") {
|
|
791
|
+
return { flow, result: resultForHaltedControl(state, flow, effectsStart, issuesStart) };
|
|
792
|
+
}
|
|
793
|
+
flow = { state: "continue" };
|
|
794
|
+
return { flow, result: resultForCompletedControl(state, undefined, control, effectsStart, issuesStart) };
|
|
795
|
+
}
|
|
796
|
+
const limit = loopItemLimit(state);
|
|
797
|
+
if (value.length > limit)
|
|
798
|
+
throw new WorkflowValueError(`forEach reference "${irStep.reference}" has ${value.length} items; limit is ${limit}`);
|
|
799
|
+
control = { kind: "forEach", items: value };
|
|
800
|
+
flow = { state: "continue" };
|
|
801
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
802
|
+
const cancellation = await heartbeat(state);
|
|
803
|
+
if (cancellation) {
|
|
804
|
+
const flow = { state: "canceled", ...optionalMessage(cancellation.message) } as const;
|
|
805
|
+
return { flow, result: resultForHaltedControl(state, flow, effectsStart, issuesStart) };
|
|
806
|
+
}
|
|
807
|
+
flow = await runSteps(state, irStep.steps, scope.child({ [irStep.alias]: value[index]! }), [...iterationPath, index]);
|
|
808
|
+
if (flow.state !== "continue") break;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return flow.state === "continue"
|
|
812
|
+
? { flow, result: resultForCompletedControl(state, flow.output, control, effectsStart, issuesStart) }
|
|
813
|
+
: { flow, result: resultForHaltedControl(state, flow, effectsStart, issuesStart) };
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
const evaluateStep = async (
|
|
817
|
+
state: RuntimeState,
|
|
818
|
+
scope: RuntimeVariableScope,
|
|
819
|
+
irStep: WorkflowIrStep,
|
|
820
|
+
step: WorkflowRuntimeStepIdentity,
|
|
821
|
+
iterationPath: number[],
|
|
822
|
+
): Promise<StepEvaluation> => {
|
|
823
|
+
try {
|
|
824
|
+
return irStep.kind === "action"
|
|
825
|
+
? await evaluateAction(state, scope, irStep, step)
|
|
826
|
+
: await evaluateControl(state, scope, irStep, iterationPath);
|
|
827
|
+
} catch (error) {
|
|
828
|
+
if (error instanceof WorkflowRetryableStepError) throw error;
|
|
829
|
+
if (error instanceof WorkflowLeaseLostError) throw error;
|
|
830
|
+
if (error instanceof WorkflowCancellation) {
|
|
831
|
+
await traceCancellation(state, error.cancellation);
|
|
832
|
+
const flow = { state: "canceled", ...optionalMessage(error.cancellation.message), step } as const;
|
|
833
|
+
return {
|
|
834
|
+
flow,
|
|
835
|
+
result:
|
|
836
|
+
state.options.mode === "execute"
|
|
837
|
+
? {
|
|
838
|
+
mode: "execute",
|
|
839
|
+
outcome: { state: "terminal", status: "canceled", ...optionalMessage(error.cancellation.message) },
|
|
840
|
+
}
|
|
841
|
+
: { mode: "dryRun", outcome: { state: "canceled", ...optionalMessage(error.cancellation.message) } },
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
if (error instanceof WorkflowValueWaiting) {
|
|
845
|
+
if (state.options.mode === "execute") {
|
|
846
|
+
const outcome: WorkflowStepOutcome = { state: "waiting", dependency: error.dependency };
|
|
847
|
+
return { flow: { state: "waiting", dependency: error.dependency, step }, result: { mode: "execute", outcome } };
|
|
848
|
+
}
|
|
849
|
+
const outcome: WorkflowPlanningOutcome = { state: "indeterminate", reason: error.message };
|
|
850
|
+
return { flow: { state: "indeterminate", reason: outcome.reason, step }, result: { mode: "dryRun", outcome } };
|
|
851
|
+
}
|
|
852
|
+
if (state.options.mode === "dryRun") {
|
|
853
|
+
const outcome: WorkflowPlanningOutcome = { state: "indeterminate", reason: errorMessage(error) };
|
|
854
|
+
return { flow: { state: "indeterminate", reason: outcome.reason, step }, result: { mode: "dryRun", outcome } };
|
|
855
|
+
}
|
|
856
|
+
const outcome: WorkflowStepOutcome = {
|
|
857
|
+
state: "failed",
|
|
858
|
+
error: executionError(
|
|
859
|
+
error instanceof WorkflowValueError ? "WORKFLOW_VALUE_UNAVAILABLE" : "WORKFLOW_ACTION_ERROR",
|
|
860
|
+
errorMessage(error),
|
|
861
|
+
false,
|
|
862
|
+
),
|
|
863
|
+
};
|
|
864
|
+
return { flow: { state: "failed", error: outcome.error, step }, result: { mode: "execute", outcome } };
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
const runStep = async (
|
|
869
|
+
state: RuntimeState,
|
|
870
|
+
scope: RuntimeVariableScope,
|
|
871
|
+
irStep: WorkflowIrStep,
|
|
872
|
+
iterationPath: number[],
|
|
873
|
+
): Promise<Flow> => {
|
|
874
|
+
const step = stepIdentity(state, irStep, iterationPath);
|
|
875
|
+
const cancellation = await heartbeat(state);
|
|
876
|
+
if (cancellation) return { state: "canceled", ...optionalMessage(cancellation.message), step };
|
|
877
|
+
const restored = await state.options.repository.restoreStepOutcome(step);
|
|
878
|
+
if (restored) return restoreStep(state, scope, irStep, step, restored);
|
|
879
|
+
|
|
880
|
+
await state.options.repository.startStep(step);
|
|
881
|
+
await emit(state, { type: "step.started", step });
|
|
882
|
+
const evaluated = await evaluateStep(state, scope, irStep, step, iterationPath);
|
|
883
|
+
if (
|
|
884
|
+
evaluated.result?.mode === "execute" &&
|
|
885
|
+
evaluated.result.outcome.state === "failed" &&
|
|
886
|
+
evaluated.result.outcome.error.retryable &&
|
|
887
|
+
evaluated.flow.state === "failed" &&
|
|
888
|
+
evaluated.flow.step.key === step.key
|
|
889
|
+
) {
|
|
890
|
+
throw new WorkflowRetryableStepError(step, evaluated.result.outcome.error);
|
|
891
|
+
}
|
|
892
|
+
const waitingForDependency =
|
|
893
|
+
evaluated.result?.mode === "execute" && evaluated.result.outcome.state === "waiting" && evaluated.flow.state === "waiting";
|
|
894
|
+
// A control step only propagates the leaf's attention result. Journaling the
|
|
895
|
+
// control as settled would make resolution restore the wrapper and stop at
|
|
896
|
+
// the same attention state forever.
|
|
897
|
+
const propagatedAttention =
|
|
898
|
+
irStep.kind !== "action" &&
|
|
899
|
+
evaluated.result?.mode === "execute" &&
|
|
900
|
+
evaluated.result.outcome.state === "needs_attention" &&
|
|
901
|
+
evaluated.flow.state === "needs_attention";
|
|
902
|
+
if (evaluated.result && !waitingForDependency && !propagatedAttention) {
|
|
903
|
+
await state.options.repository.finishStep(step, evaluated.result);
|
|
904
|
+
await emit(state, { type: "step.finished", step, result: evaluated.result });
|
|
905
|
+
}
|
|
906
|
+
if (evaluated.flow.state !== "continue") return evaluated.flow;
|
|
907
|
+
const after = await heartbeat(state);
|
|
908
|
+
return after ? { state: "canceled", ...optionalMessage(after.message), step } : evaluated.flow;
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
const runSteps = async (
|
|
912
|
+
state: RuntimeState,
|
|
913
|
+
steps: WorkflowIrStep[],
|
|
914
|
+
scope: RuntimeVariableScope,
|
|
915
|
+
iterationPath: number[],
|
|
916
|
+
): Promise<Flow> => {
|
|
917
|
+
let last: WorkflowJsonValue | undefined;
|
|
918
|
+
for (const step of steps) {
|
|
919
|
+
const flow = await runStep(state, scope, step, iterationPath);
|
|
920
|
+
if (state.options.mode === "dryRun" && (flow.state === "unsupported" || flow.state === "indeterminate")) {
|
|
921
|
+
state.issues.push({ state: flow.state, reason: flow.reason, step: flow.step });
|
|
922
|
+
last = undefined;
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
if (flow.state !== "continue") return flow;
|
|
926
|
+
last = flow.output;
|
|
927
|
+
}
|
|
928
|
+
return { state: "continue", ...optionalOutput(last) };
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
const createState = (options: RuntimeOptions): RuntimeState => ({
|
|
932
|
+
options,
|
|
933
|
+
run: {
|
|
934
|
+
runId: options.runId,
|
|
935
|
+
executionGeneration: options.executionGeneration,
|
|
936
|
+
mode: options.mode,
|
|
937
|
+
workflowId: options.invocation.workflowId,
|
|
938
|
+
sourceHash: options.plan.sourceHash,
|
|
939
|
+
idempotencyKey: options.invocation.idempotencyKey,
|
|
940
|
+
},
|
|
941
|
+
effects: [],
|
|
942
|
+
issues: [],
|
|
943
|
+
cancellationTraced: false,
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
export const executeWorkflowPlan = async (options: WorkflowExecuteOptions): Promise<WorkflowExecutionResult> => {
|
|
947
|
+
const state = createState({ ...options, mode: "execute" });
|
|
948
|
+
const flow = await runSteps(state, state.options.plan.steps, new RuntimeVariableScope(options.initialVariables), []);
|
|
949
|
+
if (flow.state === "waiting") {
|
|
950
|
+
const result = { mode: "execute", outcome: { state: "waiting", dependency: flow.dependency } } as const;
|
|
951
|
+
await state.options.repository.parkStep(flow.step, flow.dependency);
|
|
952
|
+
await emit(state, { type: "step.waiting", step: flow.step, dependency: flow.dependency });
|
|
953
|
+
await emit(state, { type: "step.finished", step: flow.step, result });
|
|
954
|
+
return flow;
|
|
955
|
+
}
|
|
956
|
+
if (flow.state === "continue") return { state: "succeeded", ...optionalOutput(flow.output) };
|
|
957
|
+
if (flow.state === "terminal_succeeded") return { state: "succeeded", ...optionalMessage(flow.message) };
|
|
958
|
+
if (flow.state === "unsupported" || flow.state === "indeterminate" || flow.state === "terminal_planned") {
|
|
959
|
+
return {
|
|
960
|
+
state: "failed",
|
|
961
|
+
error: executionError(
|
|
962
|
+
"WORKFLOW_RUNTIME_INVALID",
|
|
963
|
+
flow.state === "terminal_planned" ? "execute traversal produced a dry-run terminal outcome" : flow.reason,
|
|
964
|
+
),
|
|
965
|
+
step: flow.step,
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
return flow;
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
export const dryRunWorkflowPlan = async (options: WorkflowDryRunOptions): Promise<WorkflowDryRunResult> => {
|
|
972
|
+
const state = createState({ ...options, mode: "dryRun" });
|
|
973
|
+
const flow = await runSteps(state, state.options.plan.steps, new RuntimeVariableScope(options.initialVariables), []);
|
|
974
|
+
const issues = state.issues;
|
|
975
|
+
const optionalIssues = issues.length > 0 ? { issues } : {};
|
|
976
|
+
if (flow.state === "continue") {
|
|
977
|
+
if (issues.length === 0) return { state: "planned", ...optionalOutput(flow.output), effects: state.effects };
|
|
978
|
+
const stateName = issues.some((issue) => issue.state === "indeterminate") ? "indeterminate" : "unsupported";
|
|
979
|
+
const primary = issues.find((issue) => issue.state === stateName) ?? issues[0]!;
|
|
980
|
+
return { state: stateName, reason: primary.reason, effects: state.effects, step: primary.step, issues };
|
|
981
|
+
}
|
|
982
|
+
if (flow.state === "terminal_planned") {
|
|
983
|
+
return { state: "terminal", status: flow.status, ...optionalMessage(flow.message), effects: state.effects, ...optionalIssues };
|
|
984
|
+
}
|
|
985
|
+
if (flow.state === "unsupported" || flow.state === "indeterminate") {
|
|
986
|
+
const issue = { state: flow.state, reason: flow.reason, step: flow.step };
|
|
987
|
+
return { state: flow.state, reason: flow.reason, effects: state.effects, step: flow.step, issues: [...issues, issue] };
|
|
988
|
+
}
|
|
989
|
+
if (flow.state === "canceled") {
|
|
990
|
+
return {
|
|
991
|
+
state: "canceled",
|
|
992
|
+
...optionalMessage(flow.message),
|
|
993
|
+
effects: state.effects,
|
|
994
|
+
...(flow.step ? { step: flow.step } : {}),
|
|
995
|
+
...optionalIssues,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
const step = "step" in flow && flow.step ? flow.step : stepIdentity(state, state.options.plan.steps[0] ?? emptyActionStep, []);
|
|
999
|
+
const issue = { state: "indeterminate", reason: `dry-run traversal produced ${flow.state}`, step } as const;
|
|
1000
|
+
return { state: "indeterminate", reason: issue.reason, effects: state.effects, step, issues: [...issues, issue] };
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
const emptyActionStep: WorkflowActionStep = { kind: "action", action: "<workflow>", config: {}, sourcePath: ["steps"] };
|