@pikku/core 0.12.78 → 0.12.80
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 +234 -0
- package/dist/dev/hot-reload.js +1 -6
- package/dist/ecosystem.d.ts +27 -0
- package/dist/ecosystem.js +26 -0
- package/dist/errors/error-handler.js +4 -2
- package/dist/function/function-runner.js +20 -42
- package/dist/function/functions.types.d.ts +9 -5
- package/dist/index.d.ts +4 -9
- package/dist/index.js +3 -8
- package/dist/middleware/auth-cookie.d.ts +0 -4
- package/dist/middleware/auth-cookie.js +38 -2
- package/dist/middleware/cors.js +1 -0
- package/dist/middleware/index.d.ts +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/remote-auth.js +14 -3
- package/dist/permissions.d.ts +8 -10
- package/dist/permissions.js +0 -11
- package/dist/pikku-state.js +12 -5
- package/dist/schema.js +35 -1
- package/dist/services/ai-run-state-service.d.ts +7 -1
- package/dist/services/in-memory-ai-run-state-service.d.ts +1 -1
- package/dist/services/in-memory-ai-run-state-service.js +2 -1
- package/dist/services/in-memory-workflow-service.d.ts +10 -0
- package/dist/services/in-memory-workflow-service.js +25 -0
- package/dist/services/local-content-request-handler.d.ts +21 -0
- package/dist/services/local-content-request-handler.js +72 -53
- package/dist/services/local-content.d.ts +6 -0
- package/dist/services/local-content.js +14 -1
- package/dist/services/workflow-service.d.ts +4 -2
- package/dist/testing/service-tests/agent-run-service-tests.d.ts +10 -0
- package/dist/testing/service-tests/agent-run-service-tests.js +72 -0
- package/dist/testing/service-tests/ai-storage-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/ai-storage-service-tests.js +226 -0
- package/dist/testing/service-tests/channel-store-tests.d.ts +3 -0
- package/dist/testing/service-tests/channel-store-tests.js +72 -0
- package/dist/testing/service-tests/credential-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/credential-service-tests.js +109 -0
- package/dist/testing/service-tests/deployment-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/deployment-service-tests.js +21 -0
- package/dist/testing/service-tests/event-hub-store-tests.d.ts +3 -0
- package/dist/testing/service-tests/event-hub-store-tests.js +34 -0
- package/dist/testing/service-tests/secret-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/secret-service-tests.js +80 -0
- package/dist/testing/service-tests/session-store-tests.d.ts +3 -0
- package/dist/testing/service-tests/session-store-tests.js +43 -0
- package/dist/testing/service-tests/workflow-run-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/workflow-run-service-tests.js +42 -0
- package/dist/testing/service-tests/workflow-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/workflow-service-tests.js +150 -0
- package/dist/testing/service-tests.d.ts +6 -0
- package/dist/testing/service-tests.js +26 -791
- package/dist/types/core.types.d.ts +11 -1
- package/dist/types/state.types.d.ts +1 -1
- package/dist/utils/node-host-resolver.d.ts +12 -0
- package/dist/utils/node-host-resolver.js +16 -0
- package/dist/utils/safe-fetch.d.ts +18 -0
- package/dist/utils/safe-fetch.js +167 -29
- package/dist/wirings/actor-flow/run-conversation.js +1 -6
- package/dist/wirings/ai-agent/agent-rpc.d.ts +15 -0
- package/dist/wirings/ai-agent/agent-rpc.js +53 -0
- package/dist/wirings/ai-agent/ai-agent-agui.js +1 -5
- package/dist/wirings/ai-agent/ai-agent-memory.js +3 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +3 -9
- package/dist/wirings/ai-agent/ai-agent-runner.js +28 -107
- package/dist/wirings/ai-agent/ai-agent-stream.js +20 -61
- package/dist/wirings/ai-agent/ai-agent-turn.d.ts +56 -0
- package/dist/wirings/ai-agent/ai-agent-turn.js +81 -0
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +1 -1
- package/dist/wirings/ai-agent/voice-input.js +1 -6
- package/dist/wirings/ai-agent/voice-output.js +2 -12
- package/dist/wirings/channel/channel-common.js +1 -0
- package/dist/wirings/channel/channel-handler.js +3 -5
- package/dist/wirings/channel/channel-rpc-service.d.ts +0 -6
- package/dist/wirings/channel/channel-rpc-service.js +0 -8
- package/dist/wirings/channel/channel-rpc.types.d.ts +6 -0
- package/dist/wirings/channel/channel-rpc.types.js +8 -0
- package/dist/wirings/channel/channel-runner.d.ts +1 -3
- package/dist/wirings/channel/channel-runner.js +16 -8
- package/dist/wirings/channel/channel.types.d.ts +2 -0
- package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -0
- package/dist/wirings/cli/channel/cli-channel-runner.js +2 -0
- package/dist/wirings/cli/cli-runner.js +4 -2
- package/dist/wirings/cli/cli.types.d.ts +0 -8
- package/dist/wirings/cli/command-parser.js +13 -0
- package/dist/wirings/gateway/gateway-runner.js +41 -5
- package/dist/wirings/http/http-routes.js +2 -0
- package/dist/wirings/http/http-runner.d.ts +0 -10
- package/dist/wirings/http/http-runner.js +6 -13
- package/dist/wirings/http/http.types.d.ts +0 -10
- package/dist/wirings/http/index.d.ts +1 -1
- package/dist/wirings/http/index.js +1 -1
- package/dist/wirings/mcp/mcp-runner.d.ts +0 -7
- package/dist/wirings/mcp/mcp-runner.js +0 -6
- package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
- package/dist/wirings/rpc/rpc-runner.js +53 -86
- package/dist/wirings/rpc/rpc-types.d.ts +3 -0
- package/dist/wirings/secret/validate-secret-definitions.js +2 -0
- package/dist/wirings/trigger/pikku-trigger-service.d.ts +0 -4
- package/dist/wirings/trigger/trigger-runner.js +1 -0
- package/dist/wirings/virtual-user/run-virtual-user.js +11 -11
- package/dist/wirings/virtual-user/virtual-user-derive.js +4 -25
- package/dist/wirings/virtual-user/virtual-user-dispositions.js +1 -4
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +29 -2
- package/dist/wirings/workflow/feature.js +7 -4
- package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -2
- package/dist/wirings/workflow/graph/graph-runner.js +8 -7
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -4
- package/dist/wirings/workflow/index.d.ts +7 -2
- package/dist/wirings/workflow/index.js +5 -1
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
- package/dist/wirings/workflow/pikku-scenario-service.js +38 -10
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +44 -111
- package/dist/wirings/workflow/pikku-workflow-service.js +86 -404
- package/dist/wirings/workflow/workflow-approval.d.ts +39 -0
- package/dist/wirings/workflow/workflow-approval.js +114 -0
- package/dist/wirings/workflow/workflow-constants.d.ts +26 -0
- package/dist/wirings/workflow/workflow-constants.js +35 -0
- package/dist/wirings/workflow/workflow-errors.d.ts +58 -0
- package/dist/wirings/workflow/workflow-errors.js +112 -0
- package/dist/wirings/workflow/workflow-meta-resolver.d.ts +11 -0
- package/dist/wirings/workflow/workflow-meta-resolver.js +31 -0
- package/dist/wirings/workflow/workflow-queue-routing.d.ts +8 -0
- package/dist/wirings/workflow/workflow-queue-routing.js +38 -0
- package/dist/wirings/workflow/workflow-queue-wiring.d.ts +20 -0
- package/dist/wirings/workflow/workflow-queue-wiring.js +79 -0
- package/dist/wirings/workflow/workflow-recovery.d.ts +68 -0
- package/dist/wirings/workflow/workflow-recovery.js +101 -0
- package/dist/wirings/workflow/workflow-run-engine.types.d.ts +54 -0
- package/dist/wirings/workflow/workflow-run-engine.types.js +1 -0
- package/dist/wirings/workflow/workflow-run-ownership.d.ts +16 -0
- package/dist/wirings/workflow/workflow-run-ownership.js +29 -0
- package/dist/wirings/workflow/workflow-suspend.d.ts +12 -0
- package/dist/wirings/workflow/workflow-suspend.js +33 -0
- package/dist/wirings/workflow/workflow.types.d.ts +7 -0
- package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md +22 -0
- package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md +20 -0
- package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +21 -0
- package/knowledge/decisions/internals/a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md +21 -0
- package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +46 -0
- package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +45 -0
- package/knowledge/decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md +21 -0
- package/knowledge/decisions/internals/agent-speech-travels-as-a-custom-agui-event.md +22 -0
- package/knowledge/decisions/internals/an-agent-interrupt-is-not-a-failure.md +23 -0
- package/knowledge/decisions/internals/an-agent-run-owned-by-another-instance-says-so.md +25 -0
- package/knowledge/decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md +22 -0
- package/knowledge/decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md +20 -0
- package/knowledge/decisions/internals/an-empty-transcript-is-not-recorded.md +22 -0
- package/knowledge/decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md +66 -0
- package/knowledge/decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md +33 -0
- package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +43 -0
- package/knowledge/decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md +26 -0
- package/knowledge/decisions/internals/index.md +38 -26
- package/knowledge/decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md +21 -0
- package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +24 -0
- package/knowledge/decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md +34 -0
- package/knowledge/decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md +24 -0
- package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +22 -0
- package/knowledge/decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md +26 -0
- package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +43 -0
- package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +58 -0
- package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md +40 -0
- package/knowledge/decisions/internals/the-per-invocation-rpc-view-is-a-class.md +40 -0
- package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +28 -0
- package/knowledge/decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md +25 -0
- package/knowledge/decisions/internals/the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md +21 -0
- package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +22 -0
- package/knowledge/decisions/internals/thread-history-records-the-transcript-not-the-audio.md +25 -0
- package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +21 -0
- package/knowledge/decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md +24 -0
- package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +35 -0
- package/knowledge/decisions/security/a-dropped-audit-write-is-always-logged.md +4 -2
- package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md +33 -0
- package/knowledge/decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md +33 -0
- package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +37 -0
- package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +28 -0
- package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +34 -0
- package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
- package/knowledge/decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md +24 -0
- package/knowledge/decisions/security/gateway-handlers-run-through-the-function-runner-gate.md +13 -3
- package/knowledge/decisions/security/index.md +7 -0
- package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +35 -0
- package/knowledge/questions/index.md +2 -1
- package/knowledge/questions/unauthorized-channel-replies-escape-the-declared-out-type.md +44 -0
- package/package.json +14 -3
- package/scripts/generate-api-report.d.mts +1 -0
- package/scripts/generate-api-report.mjs +89 -0
- package/scripts/generate-api-report.mts +218 -0
- package/src/api-report.test.ts +69 -0
- package/src/column-form.ts +1 -4
- package/src/crypto-utils.test.ts +15 -2
- package/src/dev/hot-reload.ts +2 -7
- package/src/ecosystem.ts +40 -0
- package/src/errors/error-handler.ts +5 -3
- package/src/errors/error.test.ts +4 -1
- package/src/function/function-runner.test.ts +1 -1
- package/src/function/function-runner.ts +28 -39
- package/src/function/functions.types.ts +15 -9
- package/src/handle-error.ts +1 -1
- package/src/index.ts +3 -22
- package/src/middleware/auth-cookie.test.ts +50 -0
- package/src/middleware/auth-cookie.ts +38 -2
- package/src/middleware/cors.ts +2 -1
- package/src/middleware/index.ts +1 -1
- package/src/middleware/remote-auth.test.ts +43 -0
- package/src/middleware/remote-auth.ts +17 -3
- package/src/middleware-runner.ts +4 -2
- package/src/no-any-casts.test.ts +57 -0
- package/src/permissions.test.ts +3 -1
- package/src/permissions.ts +22 -24
- package/src/pikku-state.ts +17 -6
- package/src/public-surface.json +578 -0
- package/src/public-surface.json.README +25 -0
- package/src/public-surface.test.ts +105 -0
- package/src/removed-legacy-exports.test.ts +62 -0
- package/src/schema.test.ts +78 -0
- package/src/schema.ts +36 -1
- package/src/services/ai-run-state-service.ts +7 -1
- package/src/services/audit-service.ts +2 -2
- package/src/services/in-memory-ai-run-state-service.ts +3 -2
- package/src/services/in-memory-workflow-service.ts +25 -0
- package/src/services/index.ts +1 -4
- package/src/services/local-content-request-handler.test.ts +43 -5
- package/src/services/local-content-request-handler.ts +103 -74
- package/src/services/local-content.ts +15 -1
- package/src/services/local-email-service.ts +5 -1
- package/src/services/system-role-guard.test.ts +4 -1
- package/src/services/workflow-service.ts +9 -2
- package/src/side-effects-are-declared.test.ts +84 -0
- package/src/source-files-stay-composable.test.ts +41 -0
- package/src/testing/service-tests/agent-run-service-tests.ts +98 -0
- package/src/testing/service-tests/ai-storage-service-tests.ts +286 -0
- package/src/testing/service-tests/channel-store-tests.ts +98 -0
- package/src/testing/service-tests/credential-service-tests.ts +143 -0
- package/src/testing/service-tests/deployment-service-tests.ts +33 -0
- package/src/testing/service-tests/event-hub-store-tests.ts +48 -0
- package/src/testing/service-tests/secret-service-tests.ts +105 -0
- package/src/testing/service-tests/session-store-tests.ts +62 -0
- package/src/testing/service-tests/workflow-run-service-tests.ts +59 -0
- package/src/testing/service-tests/workflow-service-tests.ts +308 -0
- package/src/testing/service-tests.ts +31 -1111
- package/src/types/core.types.ts +15 -1
- package/src/types/state.types.ts +1 -1
- package/src/utils/node-host-resolver.ts +20 -0
- package/src/utils/safe-fetch.test.ts +143 -1
- package/src/utils/safe-fetch.ts +200 -25
- package/src/wirings/actor-flow/run-conversation.ts +1 -6
- package/src/wirings/ai-agent/agent-rpc.ts +120 -0
- package/src/wirings/ai-agent/ai-agent-agui.ts +1 -5
- package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
- package/src/wirings/ai-agent/ai-agent-memory.ts +8 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +10 -12
- package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +1 -0
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +82 -2
- package/src/wirings/ai-agent/ai-agent-runner.ts +49 -120
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +36 -63
- package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
- package/src/wirings/ai-agent/ai-agent-turn.ts +121 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +1 -1
- package/src/wirings/ai-agent/voice-input.ts +1 -6
- package/src/wirings/ai-agent/voice-output.ts +2 -12
- package/src/wirings/channel/channel-common.ts +5 -2
- package/src/wirings/channel/channel-handler-shapes.test.ts +72 -0
- package/src/wirings/channel/channel-handler.ts +7 -7
- package/src/wirings/channel/channel-rpc-service.ts +0 -14
- package/src/wirings/channel/channel-rpc.test.ts +25 -4
- package/src/wirings/channel/channel-rpc.types.ts +14 -0
- package/src/wirings/channel/channel-runner.ts +33 -20
- package/src/wirings/channel/channel.types.ts +2 -0
- package/src/wirings/channel/local/local-channel-runner.ts +1 -1
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +2 -1
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +6 -3
- package/src/wirings/cli/channel/cli-channel-runner.ts +3 -1
- package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
- package/src/wirings/cli/cli-runner.ts +7 -4
- package/src/wirings/cli/cli.types.ts +0 -39
- package/src/wirings/cli/command-parser.test.ts +19 -0
- package/src/wirings/cli/command-parser.ts +17 -1
- package/src/wirings/gateway/gateway-authorization.test.ts +131 -0
- package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
- package/src/wirings/gateway/gateway-runner.ts +62 -22
- package/src/wirings/http/http-routes.ts +4 -1
- package/src/wirings/http/http-runner.ts +11 -26
- package/src/wirings/http/http.types.ts +0 -15
- package/src/wirings/http/index.ts +1 -7
- package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
- package/src/wirings/http/web-request.ts +1 -1
- package/src/wirings/mcp/mcp-runner.ts +2 -16
- package/src/wirings/persona/persona-environments.test.ts +14 -3
- package/src/wirings/persona/persona.test.ts +13 -3
- package/src/wirings/persona/validate-personas.ts +5 -1
- package/src/wirings/queue/queue-runner.ts +1 -1
- package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
- package/src/wirings/rpc/rpc-runner.ts +58 -136
- package/src/wirings/rpc/rpc-types.ts +7 -0
- package/src/wirings/rpc/wire-addon.ts +3 -1
- package/src/wirings/secret/validate-secret-definitions.test.ts +69 -0
- package/src/wirings/secret/validate-secret-definitions.ts +2 -0
- package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
- package/src/wirings/trigger/trigger-runner.ts +7 -5
- package/src/wirings/virtual-user/run-virtual-user.test.ts +18 -9
- package/src/wirings/virtual-user/run-virtual-user.ts +28 -15
- package/src/wirings/virtual-user/virtual-user-agents.test.ts +4 -1
- package/src/wirings/virtual-user/virtual-user-derive.ts +4 -25
- package/src/wirings/virtual-user/virtual-user-dispositions.test.ts +7 -2
- package/src/wirings/virtual-user/virtual-user-dispositions.ts +1 -4
- package/src/wirings/virtual-user/virtual-user-intents.test.ts +13 -3
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +35 -2
- package/src/wirings/workflow/feature.ts +7 -4
- package/src/wirings/workflow/graph/graph-node.ts +1 -1
- package/src/wirings/workflow/graph/graph-runner.test.ts +4 -2
- package/src/wirings/workflow/graph/graph-runner.ts +13 -16
- package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -5
- package/src/wirings/workflow/index.ts +12 -4
- package/src/wirings/workflow/pikku-scenario-service.ts +54 -25
- package/src/wirings/workflow/pikku-workflow-service.test.ts +2 -2
- package/src/wirings/workflow/pikku-workflow-service.ts +208 -630
- package/src/wirings/workflow/scenario-hooks.test.ts +51 -0
- package/src/wirings/workflow/workflow-approval.ts +185 -0
- package/src/wirings/workflow/workflow-child-run-session.test.ts +89 -0
- package/src/wirings/workflow/workflow-constants.ts +47 -0
- package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
- package/src/wirings/workflow/workflow-dispatch-relay.test.ts +128 -0
- package/src/wirings/workflow/workflow-errors.ts +128 -0
- package/src/wirings/workflow/workflow-inline-authority.test.ts +12 -6
- package/src/wirings/workflow/workflow-meta-resolver.ts +45 -0
- package/src/wirings/workflow/workflow-missing-meta.test.ts +92 -0
- package/src/wirings/workflow/workflow-queue-routing.ts +85 -0
- package/src/wirings/workflow/workflow-queue-wiring.ts +149 -0
- package/src/wirings/workflow/workflow-recovery.ts +162 -0
- package/src/wirings/workflow/workflow-retry-policy.test.ts +1 -1
- package/src/wirings/workflow/workflow-run-authority.test.ts +215 -0
- package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
- package/src/wirings/workflow/workflow-run-ownership.ts +36 -0
- package/src/wirings/workflow/workflow-suspend.ts +61 -0
- package/src/wirings/workflow/workflow.types.ts +7 -0
- package/src/wirings-stay-decoupled.test.ts +123 -0
- package/tsconfig.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/src/internal.ts +0 -10
|
@@ -1,53 +1,23 @@
|
|
|
1
|
-
import { runPikkuFunc
|
|
2
|
-
import {
|
|
3
|
-
pikkuWorkflowWorkerFunc,
|
|
4
|
-
pikkuWorkflowOrchestratorFunc,
|
|
5
|
-
pikkuWorkflowSleeperFunc,
|
|
6
|
-
} from './workflow-queue-workers.js'
|
|
7
|
-
import { wireQueueWorker } from '../queue/queue-runner.js'
|
|
1
|
+
import { runPikkuFunc } from '../../function/function-runner.js'
|
|
8
2
|
import {
|
|
9
3
|
getSingletonServices,
|
|
10
4
|
getCreateWireServices,
|
|
11
5
|
pikkuState,
|
|
12
6
|
} from '../../pikku-state.js'
|
|
13
7
|
import { getDurationInMilliseconds } from '../../time-utils.js'
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const namespace = name.substring(0, colonIndex)
|
|
26
|
-
const localName = name.substring(colonIndex + 1)
|
|
27
|
-
const addons = pikkuState(null, 'addons', 'packages')
|
|
28
|
-
const pkgConfig = addons?.get(namespace)
|
|
29
|
-
if (pkgConfig) {
|
|
30
|
-
const addonMeta = pikkuState(pkgConfig.package, 'workflows', 'meta')
|
|
31
|
-
if (addonMeta?.[localName]) {
|
|
32
|
-
return {
|
|
33
|
-
meta: addonMeta[localName],
|
|
34
|
-
packageName: pkgConfig.package,
|
|
35
|
-
resolvedName: localName,
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return null
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const toKebab = (s: string) =>
|
|
45
|
-
s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
|
46
|
-
import type { PikkuWire, SerializedError } from '../../types/core.types.js'
|
|
47
|
-
import type { QueueService } from '../queue/queue.types.js'
|
|
8
|
+
import type {
|
|
9
|
+
CoreUserSession,
|
|
10
|
+
PikkuRawWire,
|
|
11
|
+
SerializedError,
|
|
12
|
+
} from '../../types/core.types.js'
|
|
13
|
+
import type {
|
|
14
|
+
GroupConcurrencyConfig,
|
|
15
|
+
JobGroup,
|
|
16
|
+
JobOptions,
|
|
17
|
+
QueueService,
|
|
18
|
+
} from '../queue/queue.types.js'
|
|
48
19
|
import type {
|
|
49
20
|
ApprovalOutcome,
|
|
50
|
-
CoreWorkflow,
|
|
51
21
|
PikkuWorkflowWire,
|
|
52
22
|
StepState,
|
|
53
23
|
StepStatus,
|
|
@@ -64,6 +34,7 @@ import type {
|
|
|
64
34
|
WorkflowStepOptions,
|
|
65
35
|
} from './workflow.types.js'
|
|
66
36
|
import {
|
|
37
|
+
ChildWorkflowStartedException,
|
|
67
38
|
continueGraph,
|
|
68
39
|
executeGraphStep,
|
|
69
40
|
runWorkflowGraph,
|
|
@@ -71,232 +42,65 @@ import {
|
|
|
71
42
|
} from './graph/graph-runner.js'
|
|
72
43
|
import type { WorkflowService } from '../../services/workflow-service.js'
|
|
73
44
|
import type { ScenarioPersonas } from '../../services/personas-service.js'
|
|
74
|
-
import {
|
|
75
|
-
|
|
76
|
-
addError,
|
|
77
|
-
isExpectedError,
|
|
78
|
-
} from '../../errors/error-handler.js'
|
|
45
|
+
import { isExpectedError } from '../../errors/error-handler.js'
|
|
46
|
+
import { PikkuMissingMetaError } from '../../errors/errors.js'
|
|
79
47
|
import { RPCNotFoundError } from '../rpc/rpc-runner.js'
|
|
80
|
-
import {
|
|
48
|
+
import type { PikkuRPC } from '../rpc/rpc-types.js'
|
|
81
49
|
import { deriveInvocationId } from './workflow-invocation-id.js'
|
|
50
|
+
import { assertWorkflowRunOwner } from './workflow-run-ownership.js'
|
|
82
51
|
import {
|
|
83
52
|
buildRunTimeline,
|
|
84
53
|
reconstructStateAt,
|
|
85
54
|
type RunTimeline,
|
|
86
55
|
type ReconstructedRunState,
|
|
87
56
|
} from './run-timeline.js'
|
|
57
|
+
import {
|
|
58
|
+
DEFAULT_STEP_RETRIES,
|
|
59
|
+
WORKFLOW_CHILD_POLL_MAX_MS,
|
|
60
|
+
WORKFLOW_END_STATES,
|
|
61
|
+
WORKFLOW_POLL_FACTOR,
|
|
62
|
+
WORKFLOW_POLL_MIN_MS,
|
|
63
|
+
WORKFLOW_TERMINAL_STATES,
|
|
64
|
+
} from './workflow-constants.js'
|
|
65
|
+
import {
|
|
66
|
+
WorkflowAsyncException,
|
|
67
|
+
WorkflowCancelledException,
|
|
68
|
+
WorkflowDispatchException,
|
|
69
|
+
WorkflowNotFoundError,
|
|
70
|
+
WorkflowRunCancelledError,
|
|
71
|
+
WorkflowRunFailedError,
|
|
72
|
+
WorkflowRunNotFoundError,
|
|
73
|
+
WorkflowStepFunctionMismatchError,
|
|
74
|
+
WorkflowStepNameNotString,
|
|
75
|
+
WorkflowSuspendedException,
|
|
76
|
+
} from './workflow-errors.js'
|
|
88
77
|
import type {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
} from '
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export class WorkflowSuspendedException extends Error {
|
|
118
|
-
constructor(
|
|
119
|
-
public readonly runId: string,
|
|
120
|
-
public readonly reason: string
|
|
121
|
-
) {
|
|
122
|
-
super(reason || 'Workflow suspended')
|
|
123
|
-
this.name = 'WorkflowSuspendedException'
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export class WorkflowDispatchException extends Error {
|
|
128
|
-
constructor(
|
|
129
|
-
public readonly runId: string,
|
|
130
|
-
public readonly stepName: string,
|
|
131
|
-
options?: { cause?: unknown }
|
|
132
|
-
) {
|
|
133
|
-
super(
|
|
134
|
-
`Failed to dispatch workflow step '${stepName}' (run ${runId})`,
|
|
135
|
-
options
|
|
136
|
-
)
|
|
137
|
-
this.name = 'WorkflowDispatchException'
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export class WorkflowNotFoundError extends PikkuError {
|
|
142
|
-
constructor(name: string) {
|
|
143
|
-
super(`Workflow not found: ${name}`)
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
addError(WorkflowNotFoundError, {
|
|
147
|
-
status: 404,
|
|
148
|
-
message: 'Workflow not found.',
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
export class WorkflowRunNotFoundError extends PikkuError {
|
|
152
|
-
constructor(runId: string) {
|
|
153
|
-
super(`Workflow run not found: ${runId}`)
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
addError(WorkflowRunNotFoundError, {
|
|
157
|
-
status: 404,
|
|
158
|
-
message: 'Workflow run not found.',
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
export class WorkflowRunFailedError extends PikkuError {
|
|
162
|
-
public payload: { message?: string }
|
|
163
|
-
constructor(message?: string) {
|
|
164
|
-
super(`Workflow run failed: ${message ?? 'unknown'}`)
|
|
165
|
-
this.payload = { message }
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
addError(WorkflowRunFailedError, {
|
|
169
|
-
status: 422,
|
|
170
|
-
message: 'Workflow run failed.',
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
export class WorkflowRunCancelledError extends PikkuError {
|
|
174
|
-
constructor() {
|
|
175
|
-
super('Workflow was cancelled')
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
addError(WorkflowRunCancelledError, {
|
|
179
|
-
status: 409,
|
|
180
|
-
message: 'Workflow was cancelled.',
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
export class WorkflowApprovalResolvedError extends PikkuError {
|
|
184
|
-
public payload: {
|
|
185
|
-
reason: string
|
|
186
|
-
outcome: ApprovalOutcome<unknown>['status']
|
|
187
|
-
}
|
|
188
|
-
constructor(reason: string, outcome: ApprovalOutcome<unknown>['status']) {
|
|
189
|
-
super(`Approval already ${outcome}: ${reason}`)
|
|
190
|
-
this.payload = { reason, outcome }
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
addError(WorkflowApprovalResolvedError, {
|
|
194
|
-
status: 409,
|
|
195
|
-
message: 'Approval has already been resolved.',
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
export class WorkflowServiceNotInitialized extends Error {}
|
|
199
|
-
export class WorkflowStepNameNotString extends Error {
|
|
200
|
-
constructor(stepName: any) {
|
|
201
|
-
super(`Workflow step name must be a string. Received: ${typeof stepName}`)
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const WORKFLOW_END_STATES: ReadonlySet<string> = new Set([
|
|
206
|
-
'completed',
|
|
207
|
-
'failed',
|
|
208
|
-
'cancelled',
|
|
209
|
-
'suspended',
|
|
210
|
-
])
|
|
211
|
-
|
|
212
|
-
export interface RunLifecycleContext {
|
|
213
|
-
runId: string
|
|
214
|
-
run: WorkflowRun
|
|
215
|
-
workflowMeta: any
|
|
216
|
-
workflow: CoreWorkflow
|
|
217
|
-
wire: PikkuWire
|
|
218
|
-
packageName: string | null
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export interface WorkflowRunEngine {
|
|
222
|
-
inlineStep(
|
|
223
|
-
runId: string,
|
|
224
|
-
logicalStepName: string,
|
|
225
|
-
fn: Function,
|
|
226
|
-
stepOptions?: WorkflowStepOptions,
|
|
227
|
-
data?: any,
|
|
228
|
-
funcName?: string
|
|
229
|
-
): Promise<any>
|
|
230
|
-
updateRunStatus(
|
|
231
|
-
runId: string,
|
|
232
|
-
status: WorkflowStatus,
|
|
233
|
-
output?: any,
|
|
234
|
-
error?: SerializedError
|
|
235
|
-
): Promise<void>
|
|
236
|
-
onChildWorkflowFailed(run: WorkflowRun, error: unknown): Promise<void>
|
|
237
|
-
verifyStepName(stepName: unknown): void
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export interface WorkflowRunExtension {
|
|
241
|
-
attachRunContext(
|
|
242
|
-
runId: string,
|
|
243
|
-
workflowMeta: any,
|
|
244
|
-
options?: Record<string, any>
|
|
245
|
-
): Promise<void>
|
|
246
|
-
detachRunContext(runId: string): void
|
|
247
|
-
decorateRunWire(
|
|
248
|
-
wire: PikkuWire,
|
|
249
|
-
context: {
|
|
250
|
-
runId: string
|
|
251
|
-
workflowMeta: any
|
|
252
|
-
workflowWire: PikkuWorkflowWire
|
|
253
|
-
}
|
|
254
|
-
): void
|
|
255
|
-
decorateWorkflowWire(
|
|
256
|
-
workflowWire: PikkuWorkflowWire,
|
|
257
|
-
context: {
|
|
258
|
-
name: string
|
|
259
|
-
runId: string
|
|
260
|
-
rpcService: any
|
|
261
|
-
addonNamespace?: string | null
|
|
262
|
-
}
|
|
263
|
-
): void
|
|
264
|
-
onBeforeRunFunc(context: RunLifecycleContext): Promise<void>
|
|
265
|
-
onAfterRunFunc(
|
|
266
|
-
context: RunLifecycleContext,
|
|
267
|
-
outcome: 'completed' | 'failed' | 'interrupted',
|
|
268
|
-
failure: unknown
|
|
269
|
-
): Promise<void>
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const WORKFLOW_TERMINAL_STATES: ReadonlySet<string> = new Set([
|
|
273
|
-
'completed',
|
|
274
|
-
'failed',
|
|
275
|
-
'cancelled',
|
|
276
|
-
])
|
|
277
|
-
|
|
278
|
-
/** Idle window before a `running` run with nothing in flight is treated as stalled. */
|
|
279
|
-
const DEFAULT_STALLED_RUN_MS = 5 * 60_000
|
|
280
|
-
|
|
281
|
-
/** Runs re-driven per `recoverStalledRuns` call, so one sweep is bounded. */
|
|
282
|
-
const DEFAULT_STALLED_RUN_LIMIT = 100
|
|
283
|
-
|
|
284
|
-
const WORKFLOW_POLL_MIN_MS = 10
|
|
285
|
-
|
|
286
|
-
const WORKFLOW_POLL_FACTOR = 1.6
|
|
287
|
-
|
|
288
|
-
const WORKFLOW_CHILD_POLL_MAX_MS = 500
|
|
289
|
-
|
|
290
|
-
type RunContext = {
|
|
291
|
-
activeExecutions: number
|
|
292
|
-
inline?: boolean
|
|
293
|
-
ordinals: Map<string, number>
|
|
294
|
-
lastStep?: string
|
|
295
|
-
replay?: {
|
|
296
|
-
steps?: Map<string, StepState>
|
|
297
|
-
run?: WorkflowRun
|
|
298
|
-
}
|
|
299
|
-
}
|
|
78
|
+
RunContext,
|
|
79
|
+
RunLifecycleContext,
|
|
80
|
+
WorkflowRunEngine,
|
|
81
|
+
WorkflowRunExtension,
|
|
82
|
+
} from './workflow-run-engine.types.js'
|
|
83
|
+
import { resolveWorkflowMeta } from './workflow-meta-resolver.js'
|
|
84
|
+
import {
|
|
85
|
+
jobGroupFor,
|
|
86
|
+
orchestratorQueueName,
|
|
87
|
+
resolveWorkflowConfig,
|
|
88
|
+
stepJobOptions,
|
|
89
|
+
stepWorkerQueueName,
|
|
90
|
+
} from './workflow-queue-routing.js'
|
|
91
|
+
import { wireWorkflowQueueWorkers } from './workflow-queue-wiring.js'
|
|
92
|
+
import {
|
|
93
|
+
approvalStepNameFor,
|
|
94
|
+
evaluateApprovalStep,
|
|
95
|
+
recordApprovalDecision,
|
|
96
|
+
type ApprovalStore,
|
|
97
|
+
} from './workflow-approval.js'
|
|
98
|
+
import { recordSuspension, suspendStepNameFor } from './workflow-suspend.js'
|
|
99
|
+
import {
|
|
100
|
+
RedispatchBackoff,
|
|
101
|
+
sweepStalledRuns,
|
|
102
|
+
sweepUndispatchedSteps,
|
|
103
|
+
} from './workflow-recovery.js'
|
|
300
104
|
|
|
301
105
|
export abstract class PikkuWorkflowService implements WorkflowService {
|
|
302
106
|
private runExtension?: WorkflowRunExtension
|
|
@@ -387,112 +191,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
387
191
|
}
|
|
388
192
|
|
|
389
193
|
public wireQueueWorkers(): void {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
sessionless: true,
|
|
396
|
-
functionType: 'helper' as const,
|
|
397
|
-
inputSchemaName: null,
|
|
398
|
-
outputSchemaName: null,
|
|
194
|
+
wireWorkflowQueueWorkers({
|
|
195
|
+
strategy: this.queueStrategy,
|
|
196
|
+
concurrency: this.queueConcurrency,
|
|
197
|
+
groupConcurrency: this.queueGroupConcurrency,
|
|
198
|
+
getLogger: () => this.logger,
|
|
399
199
|
})
|
|
400
|
-
|
|
401
|
-
const queueMeta = pikkuState(null, 'queue', 'meta')
|
|
402
|
-
|
|
403
|
-
const registerWorkflowFunc = (
|
|
404
|
-
funcId: string,
|
|
405
|
-
func: { func: unknown },
|
|
406
|
-
queueName: string,
|
|
407
|
-
config?: PikkuWorkerConfig
|
|
408
|
-
) => {
|
|
409
|
-
if (functions.has(funcId)) return
|
|
410
|
-
addFunction(funcId, func as never)
|
|
411
|
-
if (!queueMeta[queueName]) {
|
|
412
|
-
queueMeta[queueName] = { pikkuFuncId: funcId, name: queueName }
|
|
413
|
-
}
|
|
414
|
-
wireQueueWorker({ name: queueName, func, config } as never)
|
|
415
|
-
if (!functionsMeta[funcId]) {
|
|
416
|
-
functionsMeta[funcId] = mkMeta(funcId)
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const sharedGroups = this.queueStrategy === 'shared-groups'
|
|
421
|
-
const sharedQueueConfig: PikkuWorkerConfig | undefined = sharedGroups
|
|
422
|
-
? {
|
|
423
|
-
batchSize: this.queueConcurrency,
|
|
424
|
-
groupConcurrency: this.queueGroupConcurrency,
|
|
425
|
-
}
|
|
426
|
-
: undefined
|
|
427
|
-
|
|
428
|
-
registerWorkflowFunc(
|
|
429
|
-
'pikkuWorkflowOrchestrator',
|
|
430
|
-
{ func: pikkuWorkflowOrchestratorFunc },
|
|
431
|
-
'pikku-workflow-orchestrator',
|
|
432
|
-
sharedQueueConfig
|
|
433
|
-
)
|
|
434
|
-
registerWorkflowFunc(
|
|
435
|
-
'pikkuWorkflowStepWorker',
|
|
436
|
-
{ func: pikkuWorkflowWorkerFunc },
|
|
437
|
-
'pikku-workflow-step-worker',
|
|
438
|
-
sharedQueueConfig
|
|
439
|
-
)
|
|
440
|
-
|
|
441
|
-
const registerQueueWorkers = (queueMeta: Record<string, any>) => {
|
|
442
|
-
for (const [queueName, meta] of Object.entries(queueMeta)) {
|
|
443
|
-
if (functions.has(meta.pikkuFuncId)) continue
|
|
444
|
-
if (queueName.startsWith('wf-orchestrator-')) {
|
|
445
|
-
registerWorkflowFunc(
|
|
446
|
-
meta.pikkuFuncId,
|
|
447
|
-
{ func: pikkuWorkflowOrchestratorFunc },
|
|
448
|
-
queueName
|
|
449
|
-
)
|
|
450
|
-
} else if (queueName.startsWith('wf-step-')) {
|
|
451
|
-
registerWorkflowFunc(
|
|
452
|
-
meta.pikkuFuncId,
|
|
453
|
-
{ func: pikkuWorkflowWorkerFunc },
|
|
454
|
-
queueName
|
|
455
|
-
)
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (!sharedGroups) {
|
|
461
|
-
registerQueueWorkers(pikkuState(null, 'queue', 'meta'))
|
|
462
|
-
|
|
463
|
-
const addons = pikkuState(null, 'addons', 'packages')
|
|
464
|
-
if (addons) {
|
|
465
|
-
for (const [, addon] of addons) {
|
|
466
|
-
const addonQueueMeta = pikkuState(addon.package, 'queue', 'meta')
|
|
467
|
-
if (addonQueueMeta) {
|
|
468
|
-
registerQueueWorkers(addonQueueMeta)
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const workflowCount = Object.keys(
|
|
475
|
-
pikkuState(null, 'workflows', 'meta') ?? {}
|
|
476
|
-
).length
|
|
477
|
-
const perWorkflowQueues = Object.keys(queueMeta).filter((name) =>
|
|
478
|
-
name.startsWith('wf-orchestrator-')
|
|
479
|
-
).length
|
|
480
|
-
if (workflowCount > 0 && perWorkflowQueues === 0) {
|
|
481
|
-
this.logger?.warn?.(
|
|
482
|
-
`[pikku] ${workflowCount} workflow(s) registered but no per-workflow orchestrator queues were found in queue meta. ` +
|
|
483
|
-
`All workflows will share a single orchestrator queue, where one slow step blocks every other workflow behind it. ` +
|
|
484
|
-
`Check that the generated bootstrap imports the queue-workers meta (pikku-queue-workers-wirings-meta.gen.js).`
|
|
485
|
-
)
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
if (!functions.has('pikkuWorkflowSleeper')) {
|
|
489
|
-
addFunction('pikkuWorkflowSleeper', {
|
|
490
|
-
func: pikkuWorkflowSleeperFunc,
|
|
491
|
-
})
|
|
492
|
-
}
|
|
493
|
-
if (!functionsMeta.pikkuWorkflowSleeper) {
|
|
494
|
-
functionsMeta.pikkuWorkflowSleeper = mkMeta('pikkuWorkflowSleeper')
|
|
495
|
-
}
|
|
496
200
|
}
|
|
497
201
|
|
|
498
202
|
protected async isInline(runId: string): Promise<boolean> {
|
|
@@ -755,7 +459,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
755
459
|
const serialized: SerializedError = {
|
|
756
460
|
message: error.message,
|
|
757
461
|
stack: error.stack,
|
|
758
|
-
code: (error as
|
|
462
|
+
code: (error as { code?: string }).code,
|
|
759
463
|
expected: isExpectedError(error),
|
|
760
464
|
}
|
|
761
465
|
return mirror.setStepError(stepId, serialized)
|
|
@@ -777,7 +481,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
777
481
|
(mirror, newStep) =>
|
|
778
482
|
mirror.createRetryAttempt(failedStepId, {
|
|
779
483
|
...newStep,
|
|
780
|
-
stepName: (newStep as
|
|
484
|
+
stepName: (newStep as { stepName?: string }).stepName ?? '',
|
|
781
485
|
})
|
|
782
486
|
)
|
|
783
487
|
}
|
|
@@ -931,62 +635,70 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
931
635
|
}
|
|
932
636
|
|
|
933
637
|
/**
|
|
934
|
-
*
|
|
935
|
-
*
|
|
936
|
-
* Arming a step is two writes to two systems — the step row, then the queue
|
|
937
|
-
* or scheduler job — so a process that dies between them leaves a run that is
|
|
938
|
-
* `running` with nothing in flight. Nothing notices: the run parks on a step
|
|
939
|
-
* that will never complete and never error, so it neither finishes nor fails.
|
|
940
|
-
* (Seen on a `workflow.sleep()`: a deploy restart landed between the sleep
|
|
941
|
-
* step's insert and its timer, parking the run permanently.)
|
|
942
|
-
*
|
|
943
|
-
* Replay is the recovery — `resumeWorkflow` re-orchestrates from persisted
|
|
944
|
-
* step state, and every settled step is memoized, so resuming a run that was
|
|
945
|
-
* not actually stuck costs an orchestration pass and changes nothing. That
|
|
946
|
-
* idempotence is what makes an idle-time heuristic safe here; a run that is
|
|
947
|
-
* legitimately mid-sleep is excluded anyway, since its step is `scheduled`.
|
|
638
|
+
* Runs holding a step that has sat `pending` since before `before`, paired
|
|
639
|
+
* with the step that flagged them.
|
|
948
640
|
*
|
|
949
|
-
*
|
|
950
|
-
*
|
|
641
|
+
* Returns nothing by default so a store that cannot express the query keeps
|
|
642
|
+
* working unchanged — and, because it does not opt in, gains no re-dispatches
|
|
643
|
+
* either. A store must have an atomic `withStepLock` before overriding this,
|
|
644
|
+
* or no concurrency for one to exclude: the relay makes duplicate dispatch
|
|
645
|
+
* routine, and the claim in `executeWorkflowStepInner` is what keeps a
|
|
646
|
+
* duplicate from becoming a second execution. `kysely-postgres` and
|
|
647
|
+
* `kysely-mysql` qualify on the lock, `in-memory` on being inline and
|
|
648
|
+
* single-process; `mongodb` and `kysely-sqlite` qualify on neither.
|
|
649
|
+
*/
|
|
650
|
+
protected async findUndispatchedSteps(
|
|
651
|
+
_before: Date,
|
|
652
|
+
_limit: number
|
|
653
|
+
): Promise<Array<{ runId: string; stepId: string }>> {
|
|
654
|
+
return []
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private readonly redispatchBackoff = new RedispatchBackoff()
|
|
658
|
+
|
|
659
|
+
private get sweepDeps() {
|
|
660
|
+
return {
|
|
661
|
+
resume: (runId: string) => this.resumeWorkflow(runId),
|
|
662
|
+
logger: getSingletonServices()?.logger,
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Re-drive runs whose next move was lost. Not self-starting — call it from a
|
|
668
|
+
* scheduled task at whatever interval suits the workload.
|
|
951
669
|
*/
|
|
952
670
|
public async recoverStalledRuns(options?: {
|
|
953
671
|
stalledAfterMs?: number
|
|
954
672
|
limit?: number
|
|
955
673
|
}): Promise<{ resumed: string[] }> {
|
|
956
|
-
|
|
957
|
-
|
|
674
|
+
return sweepStalledRuns(
|
|
675
|
+
(before, limit) => this.findStalledRunIds(before, limit),
|
|
676
|
+
options,
|
|
677
|
+
this.sweepDeps
|
|
958
678
|
)
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Re-drive steps whose dispatch was lost. Not self-starting — call it from a
|
|
683
|
+
* scheduled task; ~30s suits a queue whose `pending`→`running` latency is
|
|
684
|
+
* well under that.
|
|
685
|
+
*/
|
|
686
|
+
public async relayUndispatchedSteps(options?: {
|
|
687
|
+
undispatchedAfterMs?: number
|
|
688
|
+
limit?: number
|
|
689
|
+
}): Promise<{ redispatched: string[] }> {
|
|
690
|
+
return sweepUndispatchedSteps(
|
|
691
|
+
(before, limit) => this.findUndispatchedSteps(before, limit),
|
|
692
|
+
this.redispatchBackoff,
|
|
693
|
+
options,
|
|
694
|
+
this.sweepDeps
|
|
962
695
|
)
|
|
963
|
-
const resumed: string[] = []
|
|
964
|
-
for (const runId of runIds) {
|
|
965
|
-
try {
|
|
966
|
-
await this.resumeWorkflow(runId)
|
|
967
|
-
resumed.push(runId)
|
|
968
|
-
} catch (err) {
|
|
969
|
-
// One unresumable run must not stop the sweep from recovering the rest.
|
|
970
|
-
getSingletonServices()?.logger?.error(
|
|
971
|
-
`Failed to resume stalled workflow run ${runId}: ${err instanceof Error ? err.message : String(err)}`
|
|
972
|
-
)
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
return { resumed }
|
|
976
696
|
}
|
|
977
697
|
|
|
978
698
|
protected resolveStepJobOptions(
|
|
979
699
|
stepOptions?: WorkflowStepOptions
|
|
980
700
|
): JobOptions {
|
|
981
|
-
|
|
982
|
-
const retryDelay = stepOptions?.retryDelay
|
|
983
|
-
const backoff =
|
|
984
|
-
retryDelay !== undefined && retryDelay !== 'exponential'
|
|
985
|
-
? { type: 'fixed', delay: getDurationInMilliseconds(retryDelay) }
|
|
986
|
-
: retries > 0 || retryDelay === 'exponential'
|
|
987
|
-
? 'exponential'
|
|
988
|
-
: undefined
|
|
989
|
-
return { attempts: retries + 1, ...(backoff ? { backoff } : {}) }
|
|
701
|
+
return stepJobOptions(stepOptions)
|
|
990
702
|
}
|
|
991
703
|
|
|
992
704
|
public async queueStepWorker(
|
|
@@ -1115,7 +827,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1115
827
|
name: string,
|
|
1116
828
|
input: I,
|
|
1117
829
|
wire: WorkflowRunWire,
|
|
1118
|
-
rpcService:
|
|
830
|
+
rpcService: PikkuRPC,
|
|
1119
831
|
options?: {
|
|
1120
832
|
inline?: boolean
|
|
1121
833
|
startNode?: string
|
|
@@ -1215,7 +927,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1215
927
|
public async runToCompletion<I>(
|
|
1216
928
|
name: string,
|
|
1217
929
|
input: I,
|
|
1218
|
-
rpcService:
|
|
930
|
+
rpcService: PikkuRPC,
|
|
1219
931
|
options?: { pollIntervalMs?: number; wire?: WorkflowRunWire }
|
|
1220
932
|
): Promise<any> {
|
|
1221
933
|
const { runId } = await this.startWorkflow(
|
|
@@ -1337,7 +1049,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1337
1049
|
return stepName
|
|
1338
1050
|
}
|
|
1339
1051
|
|
|
1340
|
-
public async runWorkflowJob(
|
|
1052
|
+
public async runWorkflowJob(
|
|
1053
|
+
runId: string,
|
|
1054
|
+
rpcService: PikkuRPC
|
|
1055
|
+
): Promise<void> {
|
|
1341
1056
|
await this.beginReplay(runId)
|
|
1342
1057
|
try {
|
|
1343
1058
|
await this.runWorkflowJobInner(runId, rpcService)
|
|
@@ -1348,7 +1063,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1348
1063
|
|
|
1349
1064
|
private async runWorkflowJobInner(
|
|
1350
1065
|
runId: string,
|
|
1351
|
-
rpcService:
|
|
1066
|
+
rpcService: PikkuRPC
|
|
1352
1067
|
): Promise<void> {
|
|
1353
1068
|
const run = await this.getRunIdentity(runId)
|
|
1354
1069
|
if (!run) {
|
|
@@ -1386,6 +1101,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1386
1101
|
}
|
|
1387
1102
|
|
|
1388
1103
|
const registrations = pikkuState(pkgName, 'workflows', 'registrations')
|
|
1104
|
+
if (!workflowMeta) {
|
|
1105
|
+
throw new PikkuMissingMetaError(
|
|
1106
|
+
`Missing generated metadata for workflow '${run.workflow}'`
|
|
1107
|
+
)
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1389
1110
|
const workflow = registrations.get(resolved?.resolvedName ?? run.workflow)
|
|
1390
1111
|
if (!workflow) {
|
|
1391
1112
|
throw new WorkflowNotFoundError(run.workflow)
|
|
@@ -1402,11 +1123,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1402
1123
|
addonNs
|
|
1403
1124
|
)
|
|
1404
1125
|
workflowWire.pikkuUserId = run.wire?.pikkuUserId
|
|
1405
|
-
|
|
1126
|
+
// No `rpc` yet — runPikkuFunc attaches it lazily for the invocation.
|
|
1127
|
+
const wire: PikkuRawWire = {
|
|
1406
1128
|
workflow: workflowWire,
|
|
1407
1129
|
pikkuUserId: run.wire?.pikkuUserId,
|
|
1408
|
-
session: rpcService?.wire?.session,
|
|
1409
|
-
rpc: rpcService?.wire?.rpc,
|
|
1410
1130
|
}
|
|
1411
1131
|
this.runExtension?.decorateRunWire(wire, {
|
|
1412
1132
|
runId,
|
|
@@ -1518,7 +1238,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1518
1238
|
private async runVersionMismatchFallback(
|
|
1519
1239
|
run: WorkflowRun,
|
|
1520
1240
|
currentMeta: { source: string },
|
|
1521
|
-
rpcService:
|
|
1241
|
+
rpcService: PikkuRPC
|
|
1522
1242
|
): Promise<void> {
|
|
1523
1243
|
const source = currentMeta.source
|
|
1524
1244
|
|
|
@@ -1549,7 +1269,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1549
1269
|
stepName: string,
|
|
1550
1270
|
rpcName: string,
|
|
1551
1271
|
data: any,
|
|
1552
|
-
rpcService:
|
|
1272
|
+
rpcService: PikkuRPC
|
|
1553
1273
|
): Promise<void> {
|
|
1554
1274
|
this.enterExecution(runId)
|
|
1555
1275
|
try {
|
|
@@ -1570,10 +1290,17 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1570
1290
|
stepName: string,
|
|
1571
1291
|
rpcName: string,
|
|
1572
1292
|
data: any,
|
|
1573
|
-
rpcService:
|
|
1293
|
+
rpcService: PikkuRPC
|
|
1574
1294
|
): Promise<void> {
|
|
1575
1295
|
const claimed = await this.withStepLock(runId, stepName, async () => {
|
|
1576
1296
|
const stepState = await this.getStepState(runId, stepName)
|
|
1297
|
+
// knowledge: decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md
|
|
1298
|
+
if (
|
|
1299
|
+
stepState.rpcName !== undefined &&
|
|
1300
|
+
stepState.rpcName !== (rpcName ?? null)
|
|
1301
|
+
) {
|
|
1302
|
+
throw new WorkflowStepFunctionMismatchError(runId, stepName)
|
|
1303
|
+
}
|
|
1577
1304
|
if (stepState.status === 'succeeded' || stepState.status === 'running') {
|
|
1578
1305
|
return null
|
|
1579
1306
|
}
|
|
@@ -1620,7 +1347,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1620
1347
|
runId,
|
|
1621
1348
|
stepState.stepId,
|
|
1622
1349
|
graphNodeId,
|
|
1623
|
-
rpcName,
|
|
1350
|
+
workflowMeta!.nodes![graphNodeId].rpcName,
|
|
1624
1351
|
data,
|
|
1625
1352
|
run.workflow
|
|
1626
1353
|
)
|
|
@@ -1632,7 +1359,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1632
1359
|
id: rpcName,
|
|
1633
1360
|
parentRunId: runId,
|
|
1634
1361
|
parentStepId: stepState.stepId,
|
|
1635
|
-
pikkuUserId:
|
|
1362
|
+
pikkuUserId: run.wire?.pikkuUserId,
|
|
1636
1363
|
}
|
|
1637
1364
|
const shouldInline = !getSingletonServices()?.queueService
|
|
1638
1365
|
const { runId: childRunId } = await this.startWorkflow(
|
|
@@ -1707,7 +1434,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1707
1434
|
|
|
1708
1435
|
public async orchestrateWorkflow(
|
|
1709
1436
|
runId: string,
|
|
1710
|
-
rpcService:
|
|
1437
|
+
rpcService: PikkuRPC
|
|
1711
1438
|
): Promise<void> {
|
|
1712
1439
|
try {
|
|
1713
1440
|
await this.runWorkflowJob(runId, rpcService)
|
|
@@ -1754,7 +1481,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1754
1481
|
stepState: StepState,
|
|
1755
1482
|
rpcName: string,
|
|
1756
1483
|
data: any,
|
|
1757
|
-
rpcService:
|
|
1484
|
+
rpcService: PikkuRPC,
|
|
1758
1485
|
knownRun?: WorkflowRun | null
|
|
1759
1486
|
): Promise<any> {
|
|
1760
1487
|
const run = knownRun ?? (await this.getRunIdentity(runId))
|
|
@@ -1812,7 +1539,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1812
1539
|
runId: string,
|
|
1813
1540
|
stepName: string,
|
|
1814
1541
|
onErrorRpcName: string,
|
|
1815
|
-
rpcService:
|
|
1542
|
+
rpcService: PikkuRPC,
|
|
1816
1543
|
error: Error
|
|
1817
1544
|
): Promise<void> {
|
|
1818
1545
|
await this.rpcStep(
|
|
@@ -1830,7 +1557,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1830
1557
|
logicalStepName: string,
|
|
1831
1558
|
rpcName: string,
|
|
1832
1559
|
data: any,
|
|
1833
|
-
rpcService:
|
|
1560
|
+
rpcService: PikkuRPC,
|
|
1834
1561
|
stepOptions?: WorkflowStepOptions
|
|
1835
1562
|
): Promise<any> {
|
|
1836
1563
|
const fromStepName = this.lastStepName(runId)
|
|
@@ -1912,7 +1639,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1912
1639
|
type: 'workflow',
|
|
1913
1640
|
id: rpcName,
|
|
1914
1641
|
parentRunId: runId,
|
|
1915
|
-
pikkuUserId:
|
|
1642
|
+
pikkuUserId: (await this.getRunIdentity(runId))?.wire?.pikkuUserId,
|
|
1916
1643
|
}
|
|
1917
1644
|
const { runId: childRunId } = await this.startWorkflow(
|
|
1918
1645
|
rpcName,
|
|
@@ -2051,59 +1778,18 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2051
1778
|
await this.setStepResult(stepState.stepId, null)
|
|
2052
1779
|
}
|
|
2053
1780
|
|
|
2054
|
-
private getSuspendStepName(reason: string): string {
|
|
2055
|
-
return `__workflow_suspend:${reason}`
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
1781
|
private async suspendStep(runId: string, reason: string): Promise<void> {
|
|
2059
1782
|
const fromStepName = this.lastStepName(runId)
|
|
2060
|
-
const suspendStepName = this.nextStepKey(
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
let stepState: StepState
|
|
2066
|
-
try {
|
|
2067
|
-
stepState = await this.getStepState(runId, suspendStepName)
|
|
2068
|
-
} catch {
|
|
2069
|
-
stepState = await this.insertStepState(
|
|
2070
|
-
runId,
|
|
2071
|
-
suspendStepName,
|
|
2072
|
-
'pikkuWorkflowSuspend',
|
|
2073
|
-
{
|
|
2074
|
-
reason,
|
|
2075
|
-
},
|
|
2076
|
-
undefined,
|
|
2077
|
-
fromStepName
|
|
2078
|
-
)
|
|
2079
|
-
}
|
|
2080
|
-
if (!stepState.stepId) {
|
|
2081
|
-
stepState = await this.insertStepState(
|
|
2082
|
-
runId,
|
|
2083
|
-
suspendStepName,
|
|
2084
|
-
'pikkuWorkflowSuspend',
|
|
2085
|
-
{
|
|
2086
|
-
reason,
|
|
2087
|
-
},
|
|
2088
|
-
undefined,
|
|
2089
|
-
fromStepName
|
|
2090
|
-
)
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
if (stepState.status === 'succeeded') {
|
|
2094
|
-
return
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
if (stepState.status === 'pending') {
|
|
2098
|
-
await this.setStepRunning(stepState.stepId)
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
await this.setStepResult(stepState.stepId, {
|
|
1783
|
+
const suspendStepName = this.nextStepKey(runId, suspendStepNameFor(reason))
|
|
1784
|
+
await this.withStepLock(runId, suspendStepName, () =>
|
|
1785
|
+
recordSuspension(
|
|
1786
|
+
this.approvalStore,
|
|
1787
|
+
runId,
|
|
2102
1788
|
reason,
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
1789
|
+
suspendStepName,
|
|
1790
|
+
fromStepName
|
|
1791
|
+
)
|
|
1792
|
+
)
|
|
2107
1793
|
}
|
|
2108
1794
|
|
|
2109
1795
|
private async scheduleRunWake(runId: string, delay: number): Promise<void> {
|
|
@@ -2128,49 +1814,44 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2128
1814
|
}
|
|
2129
1815
|
}
|
|
2130
1816
|
|
|
2131
|
-
private
|
|
2132
|
-
return
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
1817
|
+
private get approvalStore(): ApprovalStore {
|
|
1818
|
+
return {
|
|
1819
|
+
getStepState: (runId, stepName) => this.getStepState(runId, stepName),
|
|
1820
|
+
insertStepState: (
|
|
1821
|
+
runId,
|
|
1822
|
+
stepName,
|
|
1823
|
+
rpcName,
|
|
1824
|
+
input,
|
|
1825
|
+
output,
|
|
1826
|
+
fromStepName
|
|
1827
|
+
) =>
|
|
1828
|
+
this.insertStepState(
|
|
1829
|
+
runId,
|
|
1830
|
+
stepName,
|
|
1831
|
+
rpcName,
|
|
1832
|
+
input,
|
|
1833
|
+
output,
|
|
1834
|
+
fromStepName
|
|
1835
|
+
),
|
|
1836
|
+
setStepRunning: (stepId) => this.setStepRunning(stepId),
|
|
1837
|
+
setStepResult: (stepId, result) => this.setStepResult(stepId, result),
|
|
1838
|
+
getRunState: (runId) => this.getRunState(runId),
|
|
1839
|
+
updateRunState: (runId, key, value) =>
|
|
1840
|
+
this.updateRunState(runId, key, value),
|
|
1841
|
+
resumeWorkflow: (runId) => this.resumeWorkflow(runId),
|
|
1842
|
+
scheduleRunWake: (runId, delay) => this.scheduleRunWake(runId, delay),
|
|
2139
1843
|
}
|
|
2140
|
-
return `__approval_${hex}`
|
|
2141
1844
|
}
|
|
2142
1845
|
|
|
2143
1846
|
public async approveStep(
|
|
2144
1847
|
runId: string,
|
|
2145
1848
|
reason: string,
|
|
2146
|
-
decision: unknown
|
|
1849
|
+
decision: unknown,
|
|
1850
|
+
session?: CoreUserSession
|
|
2147
1851
|
): Promise<void> {
|
|
2148
|
-
|
|
2149
|
-
const stateKey = this.approvalStateKey(stepName)
|
|
1852
|
+
assertWorkflowRunOwner((await this.getRunIdentity(runId))?.wire, session)
|
|
2150
1853
|
|
|
2151
|
-
|
|
2152
|
-
try {
|
|
2153
|
-
resolved = await this.getStepState(runId, stepName)
|
|
2154
|
-
} catch {
|
|
2155
|
-
// knowledge: decisions/security/workflow-approval-payloads-are-validated-on-replay-inside-the-workflow.md
|
|
2156
|
-
}
|
|
2157
|
-
if (resolved?.stepId && resolved.status === 'succeeded') {
|
|
2158
|
-
const outcome = resolved.result as ApprovalOutcome<unknown> | undefined
|
|
2159
|
-
throw new WorkflowApprovalResolvedError(
|
|
2160
|
-
reason,
|
|
2161
|
-
outcome?.status ?? 'decided'
|
|
2162
|
-
)
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
|
-
const state = await this.getRunState(runId)
|
|
2166
|
-
const record = (state[stateKey] ?? {}) as Record<string, unknown>
|
|
2167
|
-
await this.updateRunState(runId, stateKey, {
|
|
2168
|
-
...record,
|
|
2169
|
-
decision,
|
|
2170
|
-
decidedAt: new Date().toISOString(),
|
|
2171
|
-
error: undefined,
|
|
2172
|
-
})
|
|
2173
|
-
await this.resumeWorkflow(runId)
|
|
1854
|
+
return recordApprovalDecision(this.approvalStore, runId, reason, decision)
|
|
2174
1855
|
}
|
|
2175
1856
|
|
|
2176
1857
|
private async approvalStep(
|
|
@@ -2181,100 +1862,24 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2181
1862
|
const fromStepName = this.lastStepName(runId)
|
|
2182
1863
|
const approvalStepName = this.nextStepKey(
|
|
2183
1864
|
runId,
|
|
2184
|
-
|
|
1865
|
+
approvalStepNameFor(reason)
|
|
1866
|
+
)
|
|
1867
|
+
return await this.withStepLock(runId, approvalStepName, () =>
|
|
1868
|
+
evaluateApprovalStep(
|
|
1869
|
+
this.approvalStore,
|
|
1870
|
+
runId,
|
|
1871
|
+
reason,
|
|
1872
|
+
approvalStepName,
|
|
1873
|
+
fromStepName,
|
|
1874
|
+
options
|
|
1875
|
+
)
|
|
2185
1876
|
)
|
|
2186
|
-
return await this.withStepLock(runId, approvalStepName, async () => {
|
|
2187
|
-
let stepState: StepState
|
|
2188
|
-
try {
|
|
2189
|
-
stepState = await this.getStepState(runId, approvalStepName)
|
|
2190
|
-
} catch {
|
|
2191
|
-
stepState = await this.insertStepState(
|
|
2192
|
-
runId,
|
|
2193
|
-
approvalStepName,
|
|
2194
|
-
'pikkuWorkflowApproval',
|
|
2195
|
-
{ reason, expiry: options.expiry },
|
|
2196
|
-
undefined,
|
|
2197
|
-
fromStepName
|
|
2198
|
-
)
|
|
2199
|
-
}
|
|
2200
|
-
if (!stepState.stepId) {
|
|
2201
|
-
stepState = await this.insertStepState(
|
|
2202
|
-
runId,
|
|
2203
|
-
approvalStepName,
|
|
2204
|
-
'pikkuWorkflowApproval',
|
|
2205
|
-
{ reason, expiry: options.expiry },
|
|
2206
|
-
undefined,
|
|
2207
|
-
fromStepName
|
|
2208
|
-
)
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
|
-
if (stepState.status === 'succeeded') {
|
|
2212
|
-
return stepState.result as ApprovalOutcome<unknown>
|
|
2213
|
-
}
|
|
2214
|
-
|
|
2215
|
-
const stateKey = this.approvalStateKey(approvalStepName)
|
|
2216
|
-
let record = ((await this.getRunState(runId))[stateKey] ?? {}) as {
|
|
2217
|
-
decision?: unknown
|
|
2218
|
-
decidedAt?: string
|
|
2219
|
-
expiresAt?: string
|
|
2220
|
-
error?: unknown
|
|
2221
|
-
}
|
|
2222
|
-
|
|
2223
|
-
if (stepState.status === 'pending') {
|
|
2224
|
-
await this.setStepRunning(stepState.stepId)
|
|
2225
|
-
if (options.expiry !== undefined && !record.expiresAt) {
|
|
2226
|
-
const expiresAt = new Date(
|
|
2227
|
-
Date.now() + getDurationInMilliseconds(options.expiry)
|
|
2228
|
-
).toISOString()
|
|
2229
|
-
record = { ...record, expiresAt }
|
|
2230
|
-
await this.updateRunState(runId, stateKey, record)
|
|
2231
|
-
await this.scheduleRunWake(
|
|
2232
|
-
runId,
|
|
2233
|
-
getDurationInMilliseconds(options.expiry)
|
|
2234
|
-
)
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2238
|
-
if (record.decision !== undefined) {
|
|
2239
|
-
const validation = await options.schema['~standard'].validate(
|
|
2240
|
-
record.decision
|
|
2241
|
-
)
|
|
2242
|
-
if (validation.issues) {
|
|
2243
|
-
await this.updateRunState(runId, stateKey, {
|
|
2244
|
-
...record,
|
|
2245
|
-
decision: undefined,
|
|
2246
|
-
decidedAt: undefined,
|
|
2247
|
-
error: validation.issues.map((issue) => ({
|
|
2248
|
-
message: issue.message,
|
|
2249
|
-
path: issue.path?.map((segment) =>
|
|
2250
|
-
typeof segment === 'object' ? segment.key : segment
|
|
2251
|
-
),
|
|
2252
|
-
})),
|
|
2253
|
-
})
|
|
2254
|
-
throw new WorkflowSuspendedException(runId, reason)
|
|
2255
|
-
}
|
|
2256
|
-
const outcome: ApprovalOutcome<unknown> = {
|
|
2257
|
-
status: 'decided',
|
|
2258
|
-
data: validation.value,
|
|
2259
|
-
}
|
|
2260
|
-
await this.setStepResult(stepState.stepId, outcome)
|
|
2261
|
-
return outcome
|
|
2262
|
-
}
|
|
2263
|
-
|
|
2264
|
-
if (record.expiresAt && Date.now() >= Date.parse(record.expiresAt)) {
|
|
2265
|
-
const outcome: ApprovalOutcome<unknown> = { status: 'expired' }
|
|
2266
|
-
await this.setStepResult(stepState.stepId, outcome)
|
|
2267
|
-
return outcome
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
throw new WorkflowSuspendedException(runId, reason)
|
|
2271
|
-
})
|
|
2272
1877
|
}
|
|
2273
1878
|
|
|
2274
1879
|
public createWorkflowWire(
|
|
2275
1880
|
name: string,
|
|
2276
1881
|
runId: string,
|
|
2277
|
-
rpcService:
|
|
1882
|
+
rpcService: PikkuRPC,
|
|
2278
1883
|
addonNamespace?: string | null
|
|
2279
1884
|
): PikkuWorkflowWire {
|
|
2280
1885
|
const workflowWire: PikkuWorkflowWire = {
|
|
@@ -2347,45 +1952,18 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2347
1952
|
}
|
|
2348
1953
|
|
|
2349
1954
|
private getConfig(): WorkflowServiceConfig {
|
|
2350
|
-
|
|
2351
|
-
const workflow = singletonServices.config?.workflow
|
|
2352
|
-
return {
|
|
2353
|
-
retries: workflow?.retries ?? DEFAULT_STEP_RETRIES,
|
|
2354
|
-
retryDelay: workflow?.retryDelay ?? 0,
|
|
2355
|
-
orchestratorQueueName:
|
|
2356
|
-
workflow?.orchestratorQueueName ?? 'pikku-workflow-orchestrator',
|
|
2357
|
-
stepWorkerQueueName:
|
|
2358
|
-
workflow?.stepWorkerQueueName ?? 'pikku-workflow-step-worker',
|
|
2359
|
-
sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowSleeper',
|
|
2360
|
-
}
|
|
1955
|
+
return resolveWorkflowConfig()
|
|
2361
1956
|
}
|
|
2362
1957
|
|
|
2363
1958
|
protected getOrchestratorQueueName(workflowName?: string): string {
|
|
2364
|
-
|
|
2365
|
-
const perWorkflow = `wf-orchestrator-${toKebab(workflowName)}`
|
|
2366
|
-
const meta = pikkuState(null, 'queue', 'meta')
|
|
2367
|
-
if (meta[perWorkflow]) {
|
|
2368
|
-
return perWorkflow
|
|
2369
|
-
}
|
|
2370
|
-
}
|
|
2371
|
-
return this.getConfig().orchestratorQueueName
|
|
1959
|
+
return orchestratorQueueName(this.queueStrategy, workflowName)
|
|
2372
1960
|
}
|
|
2373
1961
|
|
|
2374
1962
|
protected getStepWorkerQueueName(rpcName?: string): string {
|
|
2375
|
-
|
|
2376
|
-
const perStep = `wf-step-${toKebab(rpcName)}`
|
|
2377
|
-
const meta = pikkuState(null, 'queue', 'meta')
|
|
2378
|
-
if (meta[perStep]) {
|
|
2379
|
-
return perStep
|
|
2380
|
-
}
|
|
2381
|
-
}
|
|
2382
|
-
return this.getConfig().stepWorkerQueueName
|
|
1963
|
+
return stepWorkerQueueName(this.queueStrategy, rpcName)
|
|
2383
1964
|
}
|
|
2384
1965
|
|
|
2385
1966
|
protected getJobGroup(id?: string): JobGroup | undefined {
|
|
2386
|
-
|
|
2387
|
-
return undefined
|
|
2388
|
-
}
|
|
2389
|
-
return { id, tier: id }
|
|
1967
|
+
return jobGroupFor(this.queueStrategy, id)
|
|
2390
1968
|
}
|
|
2391
1969
|
}
|