@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,154 +1,21 @@
|
|
|
1
|
-
import { runPikkuFunc
|
|
2
|
-
import { pikkuWorkflowWorkerFunc, pikkuWorkflowOrchestratorFunc, pikkuWorkflowSleeperFunc, } from './workflow-queue-workers.js';
|
|
3
|
-
import { wireQueueWorker } from '../queue/queue-runner.js';
|
|
1
|
+
import { runPikkuFunc } from '../../function/function-runner.js';
|
|
4
2
|
import { getSingletonServices, getCreateWireServices, pikkuState, } from '../../pikku-state.js';
|
|
5
3
|
import { getDurationInMilliseconds } from '../../time-utils.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return { meta: rootMeta[name], packageName: null, resolvedName: name };
|
|
10
|
-
}
|
|
11
|
-
const colonIndex = name.indexOf(':');
|
|
12
|
-
if (colonIndex !== -1) {
|
|
13
|
-
const namespace = name.substring(0, colonIndex);
|
|
14
|
-
const localName = name.substring(colonIndex + 1);
|
|
15
|
-
const addons = pikkuState(null, 'addons', 'packages');
|
|
16
|
-
const pkgConfig = addons?.get(namespace);
|
|
17
|
-
if (pkgConfig) {
|
|
18
|
-
const addonMeta = pikkuState(pkgConfig.package, 'workflows', 'meta');
|
|
19
|
-
if (addonMeta?.[localName]) {
|
|
20
|
-
return {
|
|
21
|
-
meta: addonMeta[localName],
|
|
22
|
-
packageName: pkgConfig.package,
|
|
23
|
-
resolvedName: localName,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return null;
|
|
29
|
-
};
|
|
30
|
-
const toKebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
31
|
-
import { continueGraph, executeGraphStep, runWorkflowGraph, runFromMeta, } from './graph/graph-runner.js';
|
|
32
|
-
import { PikkuError, addError, isExpectedError, } from '../../errors/error-handler.js';
|
|
4
|
+
import { ChildWorkflowStartedException, continueGraph, executeGraphStep, runWorkflowGraph, runFromMeta, } from './graph/graph-runner.js';
|
|
5
|
+
import { isExpectedError } from '../../errors/error-handler.js';
|
|
6
|
+
import { PikkuMissingMetaError } from '../../errors/errors.js';
|
|
33
7
|
import { RPCNotFoundError } from '../rpc/rpc-runner.js';
|
|
34
|
-
import { ChildWorkflowStartedException } from './graph/graph-runner.js';
|
|
35
8
|
import { deriveInvocationId } from './workflow-invocation-id.js';
|
|
9
|
+
import { assertWorkflowRunOwner } from './workflow-run-ownership.js';
|
|
36
10
|
import { buildRunTimeline, reconstructStateAt, } from './run-timeline.js';
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
this.name = 'WorkflowAsyncException';
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
export class WorkflowCancelledException extends Error {
|
|
49
|
-
runId;
|
|
50
|
-
reason;
|
|
51
|
-
constructor(runId, reason) {
|
|
52
|
-
super(reason || 'Workflow cancelled');
|
|
53
|
-
this.runId = runId;
|
|
54
|
-
this.reason = reason;
|
|
55
|
-
this.name = 'WorkflowCancelledException';
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
export class WorkflowSuspendedException extends Error {
|
|
59
|
-
runId;
|
|
60
|
-
reason;
|
|
61
|
-
constructor(runId, reason) {
|
|
62
|
-
super(reason || 'Workflow suspended');
|
|
63
|
-
this.runId = runId;
|
|
64
|
-
this.reason = reason;
|
|
65
|
-
this.name = 'WorkflowSuspendedException';
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
export class WorkflowDispatchException extends Error {
|
|
69
|
-
runId;
|
|
70
|
-
stepName;
|
|
71
|
-
constructor(runId, stepName, options) {
|
|
72
|
-
super(`Failed to dispatch workflow step '${stepName}' (run ${runId})`, options);
|
|
73
|
-
this.runId = runId;
|
|
74
|
-
this.stepName = stepName;
|
|
75
|
-
this.name = 'WorkflowDispatchException';
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
export class WorkflowNotFoundError extends PikkuError {
|
|
79
|
-
constructor(name) {
|
|
80
|
-
super(`Workflow not found: ${name}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
addError(WorkflowNotFoundError, {
|
|
84
|
-
status: 404,
|
|
85
|
-
message: 'Workflow not found.',
|
|
86
|
-
});
|
|
87
|
-
export class WorkflowRunNotFoundError extends PikkuError {
|
|
88
|
-
constructor(runId) {
|
|
89
|
-
super(`Workflow run not found: ${runId}`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
addError(WorkflowRunNotFoundError, {
|
|
93
|
-
status: 404,
|
|
94
|
-
message: 'Workflow run not found.',
|
|
95
|
-
});
|
|
96
|
-
export class WorkflowRunFailedError extends PikkuError {
|
|
97
|
-
payload;
|
|
98
|
-
constructor(message) {
|
|
99
|
-
super(`Workflow run failed: ${message ?? 'unknown'}`);
|
|
100
|
-
this.payload = { message };
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
addError(WorkflowRunFailedError, {
|
|
104
|
-
status: 422,
|
|
105
|
-
message: 'Workflow run failed.',
|
|
106
|
-
});
|
|
107
|
-
export class WorkflowRunCancelledError extends PikkuError {
|
|
108
|
-
constructor() {
|
|
109
|
-
super('Workflow was cancelled');
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
addError(WorkflowRunCancelledError, {
|
|
113
|
-
status: 409,
|
|
114
|
-
message: 'Workflow was cancelled.',
|
|
115
|
-
});
|
|
116
|
-
export class WorkflowApprovalResolvedError extends PikkuError {
|
|
117
|
-
payload;
|
|
118
|
-
constructor(reason, outcome) {
|
|
119
|
-
super(`Approval already ${outcome}: ${reason}`);
|
|
120
|
-
this.payload = { reason, outcome };
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
addError(WorkflowApprovalResolvedError, {
|
|
124
|
-
status: 409,
|
|
125
|
-
message: 'Approval has already been resolved.',
|
|
126
|
-
});
|
|
127
|
-
export class WorkflowServiceNotInitialized extends Error {
|
|
128
|
-
}
|
|
129
|
-
export class WorkflowStepNameNotString extends Error {
|
|
130
|
-
constructor(stepName) {
|
|
131
|
-
super(`Workflow step name must be a string. Received: ${typeof stepName}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
const WORKFLOW_END_STATES = new Set([
|
|
135
|
-
'completed',
|
|
136
|
-
'failed',
|
|
137
|
-
'cancelled',
|
|
138
|
-
'suspended',
|
|
139
|
-
]);
|
|
140
|
-
const WORKFLOW_TERMINAL_STATES = new Set([
|
|
141
|
-
'completed',
|
|
142
|
-
'failed',
|
|
143
|
-
'cancelled',
|
|
144
|
-
]);
|
|
145
|
-
/** Idle window before a `running` run with nothing in flight is treated as stalled. */
|
|
146
|
-
const DEFAULT_STALLED_RUN_MS = 5 * 60_000;
|
|
147
|
-
/** Runs re-driven per `recoverStalledRuns` call, so one sweep is bounded. */
|
|
148
|
-
const DEFAULT_STALLED_RUN_LIMIT = 100;
|
|
149
|
-
const WORKFLOW_POLL_MIN_MS = 10;
|
|
150
|
-
const WORKFLOW_POLL_FACTOR = 1.6;
|
|
151
|
-
const WORKFLOW_CHILD_POLL_MAX_MS = 500;
|
|
11
|
+
import { DEFAULT_STEP_RETRIES, WORKFLOW_CHILD_POLL_MAX_MS, WORKFLOW_END_STATES, WORKFLOW_POLL_FACTOR, WORKFLOW_POLL_MIN_MS, WORKFLOW_TERMINAL_STATES, } from './workflow-constants.js';
|
|
12
|
+
import { WorkflowAsyncException, WorkflowCancelledException, WorkflowDispatchException, WorkflowNotFoundError, WorkflowRunCancelledError, WorkflowRunFailedError, WorkflowRunNotFoundError, WorkflowStepFunctionMismatchError, WorkflowStepNameNotString, WorkflowSuspendedException, } from './workflow-errors.js';
|
|
13
|
+
import { resolveWorkflowMeta } from './workflow-meta-resolver.js';
|
|
14
|
+
import { jobGroupFor, orchestratorQueueName, resolveWorkflowConfig, stepJobOptions, stepWorkerQueueName, } from './workflow-queue-routing.js';
|
|
15
|
+
import { wireWorkflowQueueWorkers } from './workflow-queue-wiring.js';
|
|
16
|
+
import { approvalStepNameFor, evaluateApprovalStep, recordApprovalDecision, } from './workflow-approval.js';
|
|
17
|
+
import { recordSuspension, suspendStepNameFor } from './workflow-suspend.js';
|
|
18
|
+
import { RedispatchBackoff, sweepStalledRuns, sweepUndispatchedSteps, } from './workflow-recovery.js';
|
|
152
19
|
export class PikkuWorkflowService {
|
|
153
20
|
runExtension;
|
|
154
21
|
runContexts = new Map();
|
|
@@ -222,76 +89,12 @@ export class PikkuWorkflowService {
|
|
|
222
89
|
return written;
|
|
223
90
|
}
|
|
224
91
|
wireQueueWorkers() {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
functionType: 'helper',
|
|
231
|
-
inputSchemaName: null,
|
|
232
|
-
outputSchemaName: null,
|
|
92
|
+
wireWorkflowQueueWorkers({
|
|
93
|
+
strategy: this.queueStrategy,
|
|
94
|
+
concurrency: this.queueConcurrency,
|
|
95
|
+
groupConcurrency: this.queueGroupConcurrency,
|
|
96
|
+
getLogger: () => this.logger,
|
|
233
97
|
});
|
|
234
|
-
const queueMeta = pikkuState(null, 'queue', 'meta');
|
|
235
|
-
const registerWorkflowFunc = (funcId, func, queueName, config) => {
|
|
236
|
-
if (functions.has(funcId))
|
|
237
|
-
return;
|
|
238
|
-
addFunction(funcId, func);
|
|
239
|
-
if (!queueMeta[queueName]) {
|
|
240
|
-
queueMeta[queueName] = { pikkuFuncId: funcId, name: queueName };
|
|
241
|
-
}
|
|
242
|
-
wireQueueWorker({ name: queueName, func, config });
|
|
243
|
-
if (!functionsMeta[funcId]) {
|
|
244
|
-
functionsMeta[funcId] = mkMeta(funcId);
|
|
245
|
-
}
|
|
246
|
-
};
|
|
247
|
-
const sharedGroups = this.queueStrategy === 'shared-groups';
|
|
248
|
-
const sharedQueueConfig = sharedGroups
|
|
249
|
-
? {
|
|
250
|
-
batchSize: this.queueConcurrency,
|
|
251
|
-
groupConcurrency: this.queueGroupConcurrency,
|
|
252
|
-
}
|
|
253
|
-
: undefined;
|
|
254
|
-
registerWorkflowFunc('pikkuWorkflowOrchestrator', { func: pikkuWorkflowOrchestratorFunc }, 'pikku-workflow-orchestrator', sharedQueueConfig);
|
|
255
|
-
registerWorkflowFunc('pikkuWorkflowStepWorker', { func: pikkuWorkflowWorkerFunc }, 'pikku-workflow-step-worker', sharedQueueConfig);
|
|
256
|
-
const registerQueueWorkers = (queueMeta) => {
|
|
257
|
-
for (const [queueName, meta] of Object.entries(queueMeta)) {
|
|
258
|
-
if (functions.has(meta.pikkuFuncId))
|
|
259
|
-
continue;
|
|
260
|
-
if (queueName.startsWith('wf-orchestrator-')) {
|
|
261
|
-
registerWorkflowFunc(meta.pikkuFuncId, { func: pikkuWorkflowOrchestratorFunc }, queueName);
|
|
262
|
-
}
|
|
263
|
-
else if (queueName.startsWith('wf-step-')) {
|
|
264
|
-
registerWorkflowFunc(meta.pikkuFuncId, { func: pikkuWorkflowWorkerFunc }, queueName);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
if (!sharedGroups) {
|
|
269
|
-
registerQueueWorkers(pikkuState(null, 'queue', 'meta'));
|
|
270
|
-
const addons = pikkuState(null, 'addons', 'packages');
|
|
271
|
-
if (addons) {
|
|
272
|
-
for (const [, addon] of addons) {
|
|
273
|
-
const addonQueueMeta = pikkuState(addon.package, 'queue', 'meta');
|
|
274
|
-
if (addonQueueMeta) {
|
|
275
|
-
registerQueueWorkers(addonQueueMeta);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
const workflowCount = Object.keys(pikkuState(null, 'workflows', 'meta') ?? {}).length;
|
|
281
|
-
const perWorkflowQueues = Object.keys(queueMeta).filter((name) => name.startsWith('wf-orchestrator-')).length;
|
|
282
|
-
if (workflowCount > 0 && perWorkflowQueues === 0) {
|
|
283
|
-
this.logger?.warn?.(`[pikku] ${workflowCount} workflow(s) registered but no per-workflow orchestrator queues were found in queue meta. ` +
|
|
284
|
-
`All workflows will share a single orchestrator queue, where one slow step blocks every other workflow behind it. ` +
|
|
285
|
-
`Check that the generated bootstrap imports the queue-workers meta (pikku-queue-workers-wirings-meta.gen.js).`);
|
|
286
|
-
}
|
|
287
|
-
if (!functions.has('pikkuWorkflowSleeper')) {
|
|
288
|
-
addFunction('pikkuWorkflowSleeper', {
|
|
289
|
-
func: pikkuWorkflowSleeperFunc,
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
if (!functionsMeta.pikkuWorkflowSleeper) {
|
|
293
|
-
functionsMeta.pikkuWorkflowSleeper = mkMeta('pikkuWorkflowSleeper');
|
|
294
|
-
}
|
|
295
98
|
}
|
|
296
99
|
async isInline(runId) {
|
|
297
100
|
const context = this.runContexts.get(runId);
|
|
@@ -452,49 +255,45 @@ export class PikkuWorkflowService {
|
|
|
452
255
|
return [];
|
|
453
256
|
}
|
|
454
257
|
/**
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
* Arming a step is two writes to two systems — the step row, then the queue
|
|
458
|
-
* or scheduler job — so a process that dies between them leaves a run that is
|
|
459
|
-
* `running` with nothing in flight. Nothing notices: the run parks on a step
|
|
460
|
-
* that will never complete and never error, so it neither finishes nor fails.
|
|
461
|
-
* (Seen on a `workflow.sleep()`: a deploy restart landed between the sleep
|
|
462
|
-
* step's insert and its timer, parking the run permanently.)
|
|
258
|
+
* Runs holding a step that has sat `pending` since before `before`, paired
|
|
259
|
+
* with the step that flagged them.
|
|
463
260
|
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
261
|
+
* Returns nothing by default so a store that cannot express the query keeps
|
|
262
|
+
* working unchanged — and, because it does not opt in, gains no re-dispatches
|
|
263
|
+
* either. A store must have an atomic `withStepLock` before overriding this,
|
|
264
|
+
* or no concurrency for one to exclude: the relay makes duplicate dispatch
|
|
265
|
+
* routine, and the claim in `executeWorkflowStepInner` is what keeps a
|
|
266
|
+
* duplicate from becoming a second execution. `kysely-postgres` and
|
|
267
|
+
* `kysely-mysql` qualify on the lock, `in-memory` on being inline and
|
|
268
|
+
* single-process; `mongodb` and `kysely-sqlite` qualify on neither.
|
|
269
|
+
*/
|
|
270
|
+
async findUndispatchedSteps(_before, _limit) {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
redispatchBackoff = new RedispatchBackoff();
|
|
274
|
+
get sweepDeps() {
|
|
275
|
+
return {
|
|
276
|
+
resume: (runId) => this.resumeWorkflow(runId),
|
|
277
|
+
logger: getSingletonServices()?.logger,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Re-drive runs whose next move was lost. Not self-starting — call it from a
|
|
282
|
+
* scheduled task at whatever interval suits the workload.
|
|
472
283
|
*/
|
|
473
284
|
async recoverStalledRuns(options) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
// One unresumable run must not stop the sweep from recovering the rest.
|
|
484
|
-
getSingletonServices()?.logger?.error(`Failed to resume stalled workflow run ${runId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
return { resumed };
|
|
285
|
+
return sweepStalledRuns((before, limit) => this.findStalledRunIds(before, limit), options, this.sweepDeps);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Re-drive steps whose dispatch was lost. Not self-starting — call it from a
|
|
289
|
+
* scheduled task; ~30s suits a queue whose `pending`→`running` latency is
|
|
290
|
+
* well under that.
|
|
291
|
+
*/
|
|
292
|
+
async relayUndispatchedSteps(options) {
|
|
293
|
+
return sweepUndispatchedSteps((before, limit) => this.findUndispatchedSteps(before, limit), this.redispatchBackoff, options, this.sweepDeps);
|
|
488
294
|
}
|
|
489
295
|
resolveStepJobOptions(stepOptions) {
|
|
490
|
-
|
|
491
|
-
const retryDelay = stepOptions?.retryDelay;
|
|
492
|
-
const backoff = retryDelay !== undefined && retryDelay !== 'exponential'
|
|
493
|
-
? { type: 'fixed', delay: getDurationInMilliseconds(retryDelay) }
|
|
494
|
-
: retries > 0 || retryDelay === 'exponential'
|
|
495
|
-
? 'exponential'
|
|
496
|
-
: undefined;
|
|
497
|
-
return { attempts: retries + 1, ...(backoff ? { backoff } : {}) };
|
|
296
|
+
return stepJobOptions(stepOptions);
|
|
498
297
|
}
|
|
499
298
|
async queueStepWorker(runId, stepName, rpcName, data, stepOptions, fromStepName) {
|
|
500
299
|
const queueService = this.verifyQueueService();
|
|
@@ -755,6 +554,9 @@ export class PikkuWorkflowService {
|
|
|
755
554
|
return;
|
|
756
555
|
}
|
|
757
556
|
const registrations = pikkuState(pkgName, 'workflows', 'registrations');
|
|
557
|
+
if (!workflowMeta) {
|
|
558
|
+
throw new PikkuMissingMetaError(`Missing generated metadata for workflow '${run.workflow}'`);
|
|
559
|
+
}
|
|
758
560
|
const workflow = registrations.get(resolved?.resolvedName ?? run.workflow);
|
|
759
561
|
if (!workflow) {
|
|
760
562
|
throw new WorkflowNotFoundError(run.workflow);
|
|
@@ -765,11 +567,10 @@ export class PikkuWorkflowService {
|
|
|
765
567
|
: null;
|
|
766
568
|
const workflowWire = this.createWorkflowWire(run.workflow, runId, rpcService, addonNs);
|
|
767
569
|
workflowWire.pikkuUserId = run.wire?.pikkuUserId;
|
|
570
|
+
// No `rpc` yet — runPikkuFunc attaches it lazily for the invocation.
|
|
768
571
|
const wire = {
|
|
769
572
|
workflow: workflowWire,
|
|
770
573
|
pikkuUserId: run.wire?.pikkuUserId,
|
|
771
|
-
session: rpcService?.wire?.session,
|
|
772
|
-
rpc: rpcService?.wire?.rpc,
|
|
773
574
|
};
|
|
774
575
|
this.runExtension?.decorateRunWire(wire, {
|
|
775
576
|
runId,
|
|
@@ -886,6 +687,11 @@ export class PikkuWorkflowService {
|
|
|
886
687
|
async executeWorkflowStepInner(runId, stepName, rpcName, data, rpcService) {
|
|
887
688
|
const claimed = await this.withStepLock(runId, stepName, async () => {
|
|
888
689
|
const stepState = await this.getStepState(runId, stepName);
|
|
690
|
+
// knowledge: decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md
|
|
691
|
+
if (stepState.rpcName !== undefined &&
|
|
692
|
+
stepState.rpcName !== (rpcName ?? null)) {
|
|
693
|
+
throw new WorkflowStepFunctionMismatchError(runId, stepName);
|
|
694
|
+
}
|
|
889
695
|
if (stepState.status === 'succeeded' || stepState.status === 'running') {
|
|
890
696
|
return null;
|
|
891
697
|
}
|
|
@@ -923,7 +729,7 @@ export class PikkuWorkflowService {
|
|
|
923
729
|
}
|
|
924
730
|
}
|
|
925
731
|
if (graphNodeId) {
|
|
926
|
-
result = await executeGraphStep(this, rpcService, runId, stepState.stepId, graphNodeId, rpcName, data, run.workflow);
|
|
732
|
+
result = await executeGraphStep(this, rpcService, runId, stepState.stepId, graphNodeId, workflowMeta.nodes[graphNodeId].rpcName, data, run.workflow);
|
|
927
733
|
}
|
|
928
734
|
else {
|
|
929
735
|
const subWorkflowMeta = meta[rpcName];
|
|
@@ -933,7 +739,7 @@ export class PikkuWorkflowService {
|
|
|
933
739
|
id: rpcName,
|
|
934
740
|
parentRunId: runId,
|
|
935
741
|
parentStepId: stepState.stepId,
|
|
936
|
-
pikkuUserId:
|
|
742
|
+
pikkuUserId: run.wire?.pikkuUserId,
|
|
937
743
|
};
|
|
938
744
|
const shouldInline = !getSingletonServices()?.queueService;
|
|
939
745
|
const { runId: childRunId } = await this.startWorkflow(rpcName, data, childWire, rpcService, { inline: shouldInline });
|
|
@@ -1098,7 +904,7 @@ export class PikkuWorkflowService {
|
|
|
1098
904
|
type: 'workflow',
|
|
1099
905
|
id: rpcName,
|
|
1100
906
|
parentRunId: runId,
|
|
1101
|
-
pikkuUserId:
|
|
907
|
+
pikkuUserId: (await this.getRunIdentity(runId))?.wire?.pikkuUserId,
|
|
1102
908
|
};
|
|
1103
909
|
const { runId: childRunId } = await this.startWorkflow(rpcName, data, childWire, rpcService, { inline: true });
|
|
1104
910
|
await this.setStepChildRunId(currentStepState.stepId, childRunId);
|
|
@@ -1177,39 +983,10 @@ export class PikkuWorkflowService {
|
|
|
1177
983
|
await new Promise((resolve) => setTimeout(resolve, getDurationInMilliseconds(duration)));
|
|
1178
984
|
await this.setStepResult(stepState.stepId, null);
|
|
1179
985
|
}
|
|
1180
|
-
getSuspendStepName(reason) {
|
|
1181
|
-
return `__workflow_suspend:${reason}`;
|
|
1182
|
-
}
|
|
1183
986
|
async suspendStep(runId, reason) {
|
|
1184
987
|
const fromStepName = this.lastStepName(runId);
|
|
1185
|
-
const suspendStepName = this.nextStepKey(runId,
|
|
1186
|
-
await this.withStepLock(runId, suspendStepName,
|
|
1187
|
-
let stepState;
|
|
1188
|
-
try {
|
|
1189
|
-
stepState = await this.getStepState(runId, suspendStepName);
|
|
1190
|
-
}
|
|
1191
|
-
catch {
|
|
1192
|
-
stepState = await this.insertStepState(runId, suspendStepName, 'pikkuWorkflowSuspend', {
|
|
1193
|
-
reason,
|
|
1194
|
-
}, undefined, fromStepName);
|
|
1195
|
-
}
|
|
1196
|
-
if (!stepState.stepId) {
|
|
1197
|
-
stepState = await this.insertStepState(runId, suspendStepName, 'pikkuWorkflowSuspend', {
|
|
1198
|
-
reason,
|
|
1199
|
-
}, undefined, fromStepName);
|
|
1200
|
-
}
|
|
1201
|
-
if (stepState.status === 'succeeded') {
|
|
1202
|
-
return;
|
|
1203
|
-
}
|
|
1204
|
-
if (stepState.status === 'pending') {
|
|
1205
|
-
await this.setStepRunning(stepState.stepId);
|
|
1206
|
-
}
|
|
1207
|
-
await this.setStepResult(stepState.stepId, {
|
|
1208
|
-
reason,
|
|
1209
|
-
suspendedAt: new Date().toISOString(),
|
|
1210
|
-
});
|
|
1211
|
-
throw new WorkflowSuspendedException(runId, reason);
|
|
1212
|
-
});
|
|
988
|
+
const suspendStepName = this.nextStepKey(runId, suspendStepNameFor(reason));
|
|
989
|
+
await this.withStepLock(runId, suspendStepName, () => recordSuspension(this.approvalStore, runId, reason, suspendStepName, fromStepName));
|
|
1213
990
|
}
|
|
1214
991
|
async scheduleRunWake(runId, delay) {
|
|
1215
992
|
try {
|
|
@@ -1227,96 +1004,26 @@ export class PikkuWorkflowService {
|
|
|
1227
1004
|
this.logger?.warn(`Failed to schedule approval expiry wake for run ${runId}; expiry will still resolve on the next replay`, error);
|
|
1228
1005
|
}
|
|
1229
1006
|
}
|
|
1230
|
-
|
|
1231
|
-
return
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1007
|
+
get approvalStore() {
|
|
1008
|
+
return {
|
|
1009
|
+
getStepState: (runId, stepName) => this.getStepState(runId, stepName),
|
|
1010
|
+
insertStepState: (runId, stepName, rpcName, input, output, fromStepName) => this.insertStepState(runId, stepName, rpcName, input, output, fromStepName),
|
|
1011
|
+
setStepRunning: (stepId) => this.setStepRunning(stepId),
|
|
1012
|
+
setStepResult: (stepId, result) => this.setStepResult(stepId, result),
|
|
1013
|
+
getRunState: (runId) => this.getRunState(runId),
|
|
1014
|
+
updateRunState: (runId, key, value) => this.updateRunState(runId, key, value),
|
|
1015
|
+
resumeWorkflow: (runId) => this.resumeWorkflow(runId),
|
|
1016
|
+
scheduleRunWake: (runId, delay) => this.scheduleRunWake(runId, delay),
|
|
1017
|
+
};
|
|
1239
1018
|
}
|
|
1240
|
-
async approveStep(runId, reason, decision) {
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
let resolved;
|
|
1244
|
-
try {
|
|
1245
|
-
resolved = await this.getStepState(runId, stepName);
|
|
1246
|
-
}
|
|
1247
|
-
catch {
|
|
1248
|
-
// knowledge: decisions/security/workflow-approval-payloads-are-validated-on-replay-inside-the-workflow.md
|
|
1249
|
-
}
|
|
1250
|
-
if (resolved?.stepId && resolved.status === 'succeeded') {
|
|
1251
|
-
const outcome = resolved.result;
|
|
1252
|
-
throw new WorkflowApprovalResolvedError(reason, outcome?.status ?? 'decided');
|
|
1253
|
-
}
|
|
1254
|
-
const state = await this.getRunState(runId);
|
|
1255
|
-
const record = (state[stateKey] ?? {});
|
|
1256
|
-
await this.updateRunState(runId, stateKey, {
|
|
1257
|
-
...record,
|
|
1258
|
-
decision,
|
|
1259
|
-
decidedAt: new Date().toISOString(),
|
|
1260
|
-
error: undefined,
|
|
1261
|
-
});
|
|
1262
|
-
await this.resumeWorkflow(runId);
|
|
1019
|
+
async approveStep(runId, reason, decision, session) {
|
|
1020
|
+
assertWorkflowRunOwner((await this.getRunIdentity(runId))?.wire, session);
|
|
1021
|
+
return recordApprovalDecision(this.approvalStore, runId, reason, decision);
|
|
1263
1022
|
}
|
|
1264
1023
|
async approvalStep(runId, reason, options) {
|
|
1265
1024
|
const fromStepName = this.lastStepName(runId);
|
|
1266
|
-
const approvalStepName = this.nextStepKey(runId,
|
|
1267
|
-
return await this.withStepLock(runId, approvalStepName,
|
|
1268
|
-
let stepState;
|
|
1269
|
-
try {
|
|
1270
|
-
stepState = await this.getStepState(runId, approvalStepName);
|
|
1271
|
-
}
|
|
1272
|
-
catch {
|
|
1273
|
-
stepState = await this.insertStepState(runId, approvalStepName, 'pikkuWorkflowApproval', { reason, expiry: options.expiry }, undefined, fromStepName);
|
|
1274
|
-
}
|
|
1275
|
-
if (!stepState.stepId) {
|
|
1276
|
-
stepState = await this.insertStepState(runId, approvalStepName, 'pikkuWorkflowApproval', { reason, expiry: options.expiry }, undefined, fromStepName);
|
|
1277
|
-
}
|
|
1278
|
-
if (stepState.status === 'succeeded') {
|
|
1279
|
-
return stepState.result;
|
|
1280
|
-
}
|
|
1281
|
-
const stateKey = this.approvalStateKey(approvalStepName);
|
|
1282
|
-
let record = ((await this.getRunState(runId))[stateKey] ?? {});
|
|
1283
|
-
if (stepState.status === 'pending') {
|
|
1284
|
-
await this.setStepRunning(stepState.stepId);
|
|
1285
|
-
if (options.expiry !== undefined && !record.expiresAt) {
|
|
1286
|
-
const expiresAt = new Date(Date.now() + getDurationInMilliseconds(options.expiry)).toISOString();
|
|
1287
|
-
record = { ...record, expiresAt };
|
|
1288
|
-
await this.updateRunState(runId, stateKey, record);
|
|
1289
|
-
await this.scheduleRunWake(runId, getDurationInMilliseconds(options.expiry));
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
if (record.decision !== undefined) {
|
|
1293
|
-
const validation = await options.schema['~standard'].validate(record.decision);
|
|
1294
|
-
if (validation.issues) {
|
|
1295
|
-
await this.updateRunState(runId, stateKey, {
|
|
1296
|
-
...record,
|
|
1297
|
-
decision: undefined,
|
|
1298
|
-
decidedAt: undefined,
|
|
1299
|
-
error: validation.issues.map((issue) => ({
|
|
1300
|
-
message: issue.message,
|
|
1301
|
-
path: issue.path?.map((segment) => typeof segment === 'object' ? segment.key : segment),
|
|
1302
|
-
})),
|
|
1303
|
-
});
|
|
1304
|
-
throw new WorkflowSuspendedException(runId, reason);
|
|
1305
|
-
}
|
|
1306
|
-
const outcome = {
|
|
1307
|
-
status: 'decided',
|
|
1308
|
-
data: validation.value,
|
|
1309
|
-
};
|
|
1310
|
-
await this.setStepResult(stepState.stepId, outcome);
|
|
1311
|
-
return outcome;
|
|
1312
|
-
}
|
|
1313
|
-
if (record.expiresAt && Date.now() >= Date.parse(record.expiresAt)) {
|
|
1314
|
-
const outcome = { status: 'expired' };
|
|
1315
|
-
await this.setStepResult(stepState.stepId, outcome);
|
|
1316
|
-
return outcome;
|
|
1317
|
-
}
|
|
1318
|
-
throw new WorkflowSuspendedException(runId, reason);
|
|
1319
|
-
});
|
|
1025
|
+
const approvalStepName = this.nextStepKey(runId, approvalStepNameFor(reason));
|
|
1026
|
+
return await this.withStepLock(runId, approvalStepName, () => evaluateApprovalStep(this.approvalStore, runId, reason, approvalStepName, fromStepName, options));
|
|
1320
1027
|
}
|
|
1321
1028
|
createWorkflowWire(name, runId, rpcService, addonNamespace) {
|
|
1322
1029
|
const workflowWire = {
|
|
@@ -1362,40 +1069,15 @@ export class PikkuWorkflowService {
|
|
|
1362
1069
|
}
|
|
1363
1070
|
}
|
|
1364
1071
|
getConfig() {
|
|
1365
|
-
|
|
1366
|
-
const workflow = singletonServices.config?.workflow;
|
|
1367
|
-
return {
|
|
1368
|
-
retries: workflow?.retries ?? DEFAULT_STEP_RETRIES,
|
|
1369
|
-
retryDelay: workflow?.retryDelay ?? 0,
|
|
1370
|
-
orchestratorQueueName: workflow?.orchestratorQueueName ?? 'pikku-workflow-orchestrator',
|
|
1371
|
-
stepWorkerQueueName: workflow?.stepWorkerQueueName ?? 'pikku-workflow-step-worker',
|
|
1372
|
-
sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowSleeper',
|
|
1373
|
-
};
|
|
1072
|
+
return resolveWorkflowConfig();
|
|
1374
1073
|
}
|
|
1375
1074
|
getOrchestratorQueueName(workflowName) {
|
|
1376
|
-
|
|
1377
|
-
const perWorkflow = `wf-orchestrator-${toKebab(workflowName)}`;
|
|
1378
|
-
const meta = pikkuState(null, 'queue', 'meta');
|
|
1379
|
-
if (meta[perWorkflow]) {
|
|
1380
|
-
return perWorkflow;
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
return this.getConfig().orchestratorQueueName;
|
|
1075
|
+
return orchestratorQueueName(this.queueStrategy, workflowName);
|
|
1384
1076
|
}
|
|
1385
1077
|
getStepWorkerQueueName(rpcName) {
|
|
1386
|
-
|
|
1387
|
-
const perStep = `wf-step-${toKebab(rpcName)}`;
|
|
1388
|
-
const meta = pikkuState(null, 'queue', 'meta');
|
|
1389
|
-
if (meta[perStep]) {
|
|
1390
|
-
return perStep;
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
return this.getConfig().stepWorkerQueueName;
|
|
1078
|
+
return stepWorkerQueueName(this.queueStrategy, rpcName);
|
|
1394
1079
|
}
|
|
1395
1080
|
getJobGroup(id) {
|
|
1396
|
-
|
|
1397
|
-
return undefined;
|
|
1398
|
-
}
|
|
1399
|
-
return { id, tier: id };
|
|
1081
|
+
return jobGroupFor(this.queueStrategy, id);
|
|
1400
1082
|
}
|
|
1401
1083
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ApprovalOutcome, StepState, WorkflowApprovalOptions } from './workflow.types.js';
|
|
2
|
+
/** The durable step name an approval point is recorded under. */
|
|
3
|
+
export declare const approvalStepNameFor: (reason: string) => string;
|
|
4
|
+
/**
|
|
5
|
+
* The run-state key holding an approval's decision.
|
|
6
|
+
*
|
|
7
|
+
* Hex-encoded because the key is composed from a caller-supplied reason, and
|
|
8
|
+
* run state is a flat record — an unescaped reason could collide with, or
|
|
9
|
+
* shadow, another key.
|
|
10
|
+
*/
|
|
11
|
+
export declare const approvalStateKey: (stepName: string) => string;
|
|
12
|
+
/** What the approval gate needs from the workflow service. */
|
|
13
|
+
export type ApprovalStore = {
|
|
14
|
+
getStepState: (runId: string, stepName: string) => Promise<StepState>;
|
|
15
|
+
insertStepState: (runId: string, stepName: string, rpcName: string, input: unknown, output: undefined, fromStepName?: string) => Promise<StepState>;
|
|
16
|
+
setStepRunning: (stepId: string) => Promise<void>;
|
|
17
|
+
setStepResult: (stepId: string, result: unknown) => Promise<void>;
|
|
18
|
+
getRunState: (runId: string) => Promise<Record<string, unknown>>;
|
|
19
|
+
updateRunState: (runId: string, key: string, value: unknown) => Promise<void>;
|
|
20
|
+
resumeWorkflow: (runId: string) => Promise<void>;
|
|
21
|
+
scheduleRunWake: (runId: string, delay: number) => Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Record a decision against an approval point and wake the run.
|
|
25
|
+
*
|
|
26
|
+
* A decision arriving for an already-settled approval is refused rather than
|
|
27
|
+
* overwriting it: the run has moved on, and a second answer would be recorded
|
|
28
|
+
* against a gate nobody is waiting at.
|
|
29
|
+
*/
|
|
30
|
+
export declare const recordApprovalDecision: (store: ApprovalStore, runId: string, reason: string, decision: unknown) => Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Evaluate an approval gate on replay.
|
|
33
|
+
*
|
|
34
|
+
* The payload is validated here rather than where it was submitted, because the
|
|
35
|
+
* schema is a value on the workflow and only the workflow has it. A payload
|
|
36
|
+
* that fails validation is cleared and the run suspends again, so a bad
|
|
37
|
+
* submission cannot settle the gate.
|
|
38
|
+
*/
|
|
39
|
+
export declare const evaluateApprovalStep: (store: ApprovalStore, runId: string, reason: string, approvalStepName: string, fromStepName: string | undefined, options: WorkflowApprovalOptions) => Promise<ApprovalOutcome<unknown>>;
|