@pikku/core 0.12.79 → 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 +161 -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.js +2 -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/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 +9 -2
- 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 -2
- 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 +28 -147
- package/dist/wirings/workflow/pikku-workflow-service.js +71 -493
- 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/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 +13 -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 +2 -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/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-channel-meta.test.ts +44 -0
- package/src/wirings/gateway/gateway-runner.ts +27 -19
- 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 +22 -0
- package/src/wirings/secret/validate-secret-definitions.ts +2 -2
- 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 +190 -743
- 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-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,168 +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
|
-
/**
|
|
150
|
-
* How long a step may sit `pending` before the relay assumes its dispatch was
|
|
151
|
-
* lost. This is a bet that no healthy queue takes this long to move a job from
|
|
152
|
-
* `pending` to `running`; set it above the observed p99 of that latency.
|
|
153
|
-
*/
|
|
154
|
-
const DEFAULT_UNDISPATCHED_STEP_MS = 30_000;
|
|
155
|
-
/** Steps re-driven per `relayUndispatchedSteps` call, so one tick is bounded. */
|
|
156
|
-
const DEFAULT_UNDISPATCHED_STEP_LIMIT = 100;
|
|
157
|
-
/** First wait before a step already re-dispatched once is re-dispatched again. */
|
|
158
|
-
const REDISPATCH_BACKOFF_MS = 30_000;
|
|
159
|
-
/** Ceiling on the doubling backoff, so a permanently stuck step still gets swept. */
|
|
160
|
-
const REDISPATCH_BACKOFF_MAX_MS = 10 * 60_000;
|
|
161
|
-
/** Bound on the in-process backoff map, so a long-lived process cannot grow it without bound. */
|
|
162
|
-
const REDISPATCH_BACKOFF_MAX_ENTRIES = 10_000;
|
|
163
|
-
const WORKFLOW_POLL_MIN_MS = 10;
|
|
164
|
-
const WORKFLOW_POLL_FACTOR = 1.6;
|
|
165
|
-
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';
|
|
166
19
|
export class PikkuWorkflowService {
|
|
167
20
|
runExtension;
|
|
168
21
|
runContexts = new Map();
|
|
@@ -236,76 +89,12 @@ export class PikkuWorkflowService {
|
|
|
236
89
|
return written;
|
|
237
90
|
}
|
|
238
91
|
wireQueueWorkers() {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
functionType: 'helper',
|
|
245
|
-
inputSchemaName: null,
|
|
246
|
-
outputSchemaName: null,
|
|
92
|
+
wireWorkflowQueueWorkers({
|
|
93
|
+
strategy: this.queueStrategy,
|
|
94
|
+
concurrency: this.queueConcurrency,
|
|
95
|
+
groupConcurrency: this.queueGroupConcurrency,
|
|
96
|
+
getLogger: () => this.logger,
|
|
247
97
|
});
|
|
248
|
-
const queueMeta = pikkuState(null, 'queue', 'meta');
|
|
249
|
-
const registerWorkflowFunc = (funcId, func, queueName, config) => {
|
|
250
|
-
if (functions.has(funcId))
|
|
251
|
-
return;
|
|
252
|
-
addFunction(funcId, func);
|
|
253
|
-
if (!queueMeta[queueName]) {
|
|
254
|
-
queueMeta[queueName] = { pikkuFuncId: funcId, name: queueName };
|
|
255
|
-
}
|
|
256
|
-
wireQueueWorker({ name: queueName, func, config });
|
|
257
|
-
if (!functionsMeta[funcId]) {
|
|
258
|
-
functionsMeta[funcId] = mkMeta(funcId);
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
const sharedGroups = this.queueStrategy === 'shared-groups';
|
|
262
|
-
const sharedQueueConfig = sharedGroups
|
|
263
|
-
? {
|
|
264
|
-
batchSize: this.queueConcurrency,
|
|
265
|
-
groupConcurrency: this.queueGroupConcurrency,
|
|
266
|
-
}
|
|
267
|
-
: undefined;
|
|
268
|
-
registerWorkflowFunc('pikkuWorkflowOrchestrator', { func: pikkuWorkflowOrchestratorFunc }, 'pikku-workflow-orchestrator', sharedQueueConfig);
|
|
269
|
-
registerWorkflowFunc('pikkuWorkflowStepWorker', { func: pikkuWorkflowWorkerFunc }, 'pikku-workflow-step-worker', sharedQueueConfig);
|
|
270
|
-
const registerQueueWorkers = (queueMeta) => {
|
|
271
|
-
for (const [queueName, meta] of Object.entries(queueMeta)) {
|
|
272
|
-
if (functions.has(meta.pikkuFuncId))
|
|
273
|
-
continue;
|
|
274
|
-
if (queueName.startsWith('wf-orchestrator-')) {
|
|
275
|
-
registerWorkflowFunc(meta.pikkuFuncId, { func: pikkuWorkflowOrchestratorFunc }, queueName);
|
|
276
|
-
}
|
|
277
|
-
else if (queueName.startsWith('wf-step-')) {
|
|
278
|
-
registerWorkflowFunc(meta.pikkuFuncId, { func: pikkuWorkflowWorkerFunc }, queueName);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
};
|
|
282
|
-
if (!sharedGroups) {
|
|
283
|
-
registerQueueWorkers(pikkuState(null, 'queue', 'meta'));
|
|
284
|
-
const addons = pikkuState(null, 'addons', 'packages');
|
|
285
|
-
if (addons) {
|
|
286
|
-
for (const [, addon] of addons) {
|
|
287
|
-
const addonQueueMeta = pikkuState(addon.package, 'queue', 'meta');
|
|
288
|
-
if (addonQueueMeta) {
|
|
289
|
-
registerQueueWorkers(addonQueueMeta);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
const workflowCount = Object.keys(pikkuState(null, 'workflows', 'meta') ?? {}).length;
|
|
295
|
-
const perWorkflowQueues = Object.keys(queueMeta).filter((name) => name.startsWith('wf-orchestrator-')).length;
|
|
296
|
-
if (workflowCount > 0 && perWorkflowQueues === 0) {
|
|
297
|
-
this.logger?.warn?.(`[pikku] ${workflowCount} workflow(s) registered but no per-workflow orchestrator queues were found in queue meta. ` +
|
|
298
|
-
`All workflows will share a single orchestrator queue, where one slow step blocks every other workflow behind it. ` +
|
|
299
|
-
`Check that the generated bootstrap imports the queue-workers meta (pikku-queue-workers-wirings-meta.gen.js).`);
|
|
300
|
-
}
|
|
301
|
-
if (!functions.has('pikkuWorkflowSleeper')) {
|
|
302
|
-
addFunction('pikkuWorkflowSleeper', {
|
|
303
|
-
func: pikkuWorkflowSleeperFunc,
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
if (!functionsMeta.pikkuWorkflowSleeper) {
|
|
307
|
-
functionsMeta.pikkuWorkflowSleeper = mkMeta('pikkuWorkflowSleeper');
|
|
308
|
-
}
|
|
309
98
|
}
|
|
310
99
|
async isInline(runId) {
|
|
311
100
|
const context = this.runContexts.get(runId);
|
|
@@ -465,41 +254,6 @@ export class PikkuWorkflowService {
|
|
|
465
254
|
async findStalledRunIds(_before, _limit) {
|
|
466
255
|
return [];
|
|
467
256
|
}
|
|
468
|
-
/**
|
|
469
|
-
* Re-drive runs whose next move was lost, and report which were resumed.
|
|
470
|
-
*
|
|
471
|
-
* Arming a step is two writes to two systems — the step row, then the queue
|
|
472
|
-
* or scheduler job — so a process that dies between them leaves a run that is
|
|
473
|
-
* `running` with nothing in flight. Nothing notices: the run parks on a step
|
|
474
|
-
* that will never complete and never error, so it neither finishes nor fails.
|
|
475
|
-
* (Seen on a `workflow.sleep()`: a deploy restart landed between the sleep
|
|
476
|
-
* step's insert and its timer, parking the run permanently.)
|
|
477
|
-
*
|
|
478
|
-
* Replay is the recovery — `resumeWorkflow` re-orchestrates from persisted
|
|
479
|
-
* step state, and every settled step is memoized, so resuming a run that was
|
|
480
|
-
* not actually stuck costs an orchestration pass and changes nothing. That
|
|
481
|
-
* idempotence is what makes an idle-time heuristic safe here; a run that is
|
|
482
|
-
* legitimately mid-sleep is excluded anyway, since its step is `scheduled`.
|
|
483
|
-
*
|
|
484
|
-
* This is not self-starting. Call it from a scheduled task at whatever
|
|
485
|
-
* interval suits the workload.
|
|
486
|
-
*/
|
|
487
|
-
async recoverStalledRuns(options) {
|
|
488
|
-
const before = new Date(Date.now() - (options?.stalledAfterMs ?? DEFAULT_STALLED_RUN_MS));
|
|
489
|
-
const runIds = await this.findStalledRunIds(before, options?.limit ?? DEFAULT_STALLED_RUN_LIMIT);
|
|
490
|
-
const resumed = [];
|
|
491
|
-
for (const runId of runIds) {
|
|
492
|
-
try {
|
|
493
|
-
await this.resumeWorkflow(runId);
|
|
494
|
-
resumed.push(runId);
|
|
495
|
-
}
|
|
496
|
-
catch (err) {
|
|
497
|
-
// One unresumable run must not stop the sweep from recovering the rest.
|
|
498
|
-
getSingletonServices()?.logger?.error(`Failed to resume stalled workflow run ${runId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
return { resumed };
|
|
502
|
-
}
|
|
503
257
|
/**
|
|
504
258
|
* Runs holding a step that has sat `pending` since before `before`, paired
|
|
505
259
|
* with the step that flagged them.
|
|
@@ -516,89 +270,30 @@ export class PikkuWorkflowService {
|
|
|
516
270
|
async findUndispatchedSteps(_before, _limit) {
|
|
517
271
|
return [];
|
|
518
272
|
}
|
|
273
|
+
redispatchBackoff = new RedispatchBackoff();
|
|
274
|
+
get sweepDeps() {
|
|
275
|
+
return {
|
|
276
|
+
resume: (runId) => this.resumeWorkflow(runId),
|
|
277
|
+
logger: getSingletonServices()?.logger,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
519
280
|
/**
|
|
520
|
-
* Re-drive
|
|
521
|
-
*
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
* indistinguishable from one whose job is merely still queued — so a step
|
|
531
|
-
* past `undispatchedAfterMs` is re-dispatched regardless, and correctness
|
|
532
|
-
* rests on the claim rather than on the guess being right. A redundant
|
|
533
|
-
* dispatch costs one queue message: the loser reads `running` and returns
|
|
534
|
-
* without invoking anything.
|
|
535
|
-
*
|
|
536
|
-
* Re-dispatches back off per step (doubling from 30s, capped at 10m) so a
|
|
537
|
-
* genuine queue backlog is not amplified by a tick that keeps firing at the
|
|
538
|
-
* steps the backlog is already delaying. The backoff is per process and
|
|
539
|
-
* advisory — losing it on restart costs extra dispatches, never correctness.
|
|
540
|
-
*
|
|
541
|
-
* This is not self-starting. Call it from a scheduled task; ~30s suits a
|
|
542
|
-
* queue whose `pending`→`running` latency is well under that.
|
|
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.
|
|
283
|
+
*/
|
|
284
|
+
async recoverStalledRuns(options) {
|
|
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.
|
|
543
291
|
*/
|
|
544
292
|
async relayUndispatchedSteps(options) {
|
|
545
|
-
|
|
546
|
-
(options?.undispatchedAfterMs ?? DEFAULT_UNDISPATCHED_STEP_MS));
|
|
547
|
-
const steps = await this.findUndispatchedSteps(before, options?.limit ?? DEFAULT_UNDISPATCHED_STEP_LIMIT);
|
|
548
|
-
// Backoff is keyed by run, not step, because the run is the unit of
|
|
549
|
-
// re-drive: `resumeWorkflow` replays the whole run and re-dispatches every
|
|
550
|
-
// step still owed a job. Holding off a single step while resuming its run
|
|
551
|
-
// would not suppress anything.
|
|
552
|
-
const now = Date.now();
|
|
553
|
-
const runIds = new Set();
|
|
554
|
-
for (const { runId } of steps) {
|
|
555
|
-
const eligibleAt = this.redispatchBackoff.get(runId);
|
|
556
|
-
if (eligibleAt !== undefined && eligibleAt > now) {
|
|
557
|
-
continue;
|
|
558
|
-
}
|
|
559
|
-
this.noteRedispatch(runId, now);
|
|
560
|
-
runIds.add(runId);
|
|
561
|
-
}
|
|
562
|
-
const redispatched = [];
|
|
563
|
-
for (const runId of runIds) {
|
|
564
|
-
try {
|
|
565
|
-
await this.resumeWorkflow(runId);
|
|
566
|
-
redispatched.push(runId);
|
|
567
|
-
}
|
|
568
|
-
catch (err) {
|
|
569
|
-
// One unresumable run must not stop the tick from relaying the rest.
|
|
570
|
-
getSingletonServices()?.logger?.error(`Failed to re-dispatch workflow run ${runId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
return { redispatched };
|
|
574
|
-
}
|
|
575
|
-
/** Advisory, per-process record of when a run may next be re-dispatched. */
|
|
576
|
-
redispatchBackoff = new Map();
|
|
577
|
-
redispatchDelays = new Map();
|
|
578
|
-
noteRedispatch(runId, now) {
|
|
579
|
-
const previous = this.redispatchDelays.get(runId);
|
|
580
|
-
const delay = Math.min(previous === undefined ? REDISPATCH_BACKOFF_MS : previous * 2, REDISPATCH_BACKOFF_MAX_MS);
|
|
581
|
-
// A run that settles is never returned again, so entries are only evicted by
|
|
582
|
-
// this bound — oldest first, which is also least recently re-dispatched.
|
|
583
|
-
if (this.redispatchBackoff.size >= REDISPATCH_BACKOFF_MAX_ENTRIES) {
|
|
584
|
-
const oldest = this.redispatchBackoff.keys().next();
|
|
585
|
-
if (!oldest.done) {
|
|
586
|
-
this.redispatchBackoff.delete(oldest.value);
|
|
587
|
-
this.redispatchDelays.delete(oldest.value);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
this.redispatchDelays.set(runId, delay);
|
|
591
|
-
this.redispatchBackoff.set(runId, now + delay);
|
|
293
|
+
return sweepUndispatchedSteps((before, limit) => this.findUndispatchedSteps(before, limit), this.redispatchBackoff, options, this.sweepDeps);
|
|
592
294
|
}
|
|
593
295
|
resolveStepJobOptions(stepOptions) {
|
|
594
|
-
|
|
595
|
-
const retryDelay = stepOptions?.retryDelay;
|
|
596
|
-
const backoff = retryDelay !== undefined && retryDelay !== 'exponential'
|
|
597
|
-
? { type: 'fixed', delay: getDurationInMilliseconds(retryDelay) }
|
|
598
|
-
: retries > 0 || retryDelay === 'exponential'
|
|
599
|
-
? 'exponential'
|
|
600
|
-
: undefined;
|
|
601
|
-
return { attempts: retries + 1, ...(backoff ? { backoff } : {}) };
|
|
296
|
+
return stepJobOptions(stepOptions);
|
|
602
297
|
}
|
|
603
298
|
async queueStepWorker(runId, stepName, rpcName, data, stepOptions, fromStepName) {
|
|
604
299
|
const queueService = this.verifyQueueService();
|
|
@@ -859,6 +554,9 @@ export class PikkuWorkflowService {
|
|
|
859
554
|
return;
|
|
860
555
|
}
|
|
861
556
|
const registrations = pikkuState(pkgName, 'workflows', 'registrations');
|
|
557
|
+
if (!workflowMeta) {
|
|
558
|
+
throw new PikkuMissingMetaError(`Missing generated metadata for workflow '${run.workflow}'`);
|
|
559
|
+
}
|
|
862
560
|
const workflow = registrations.get(resolved?.resolvedName ?? run.workflow);
|
|
863
561
|
if (!workflow) {
|
|
864
562
|
throw new WorkflowNotFoundError(run.workflow);
|
|
@@ -869,11 +567,10 @@ export class PikkuWorkflowService {
|
|
|
869
567
|
: null;
|
|
870
568
|
const workflowWire = this.createWorkflowWire(run.workflow, runId, rpcService, addonNs);
|
|
871
569
|
workflowWire.pikkuUserId = run.wire?.pikkuUserId;
|
|
570
|
+
// No `rpc` yet — runPikkuFunc attaches it lazily for the invocation.
|
|
872
571
|
const wire = {
|
|
873
572
|
workflow: workflowWire,
|
|
874
573
|
pikkuUserId: run.wire?.pikkuUserId,
|
|
875
|
-
session: rpcService?.wire?.session,
|
|
876
|
-
rpc: rpcService?.wire?.rpc,
|
|
877
574
|
};
|
|
878
575
|
this.runExtension?.decorateRunWire(wire, {
|
|
879
576
|
runId,
|
|
@@ -990,6 +687,11 @@ export class PikkuWorkflowService {
|
|
|
990
687
|
async executeWorkflowStepInner(runId, stepName, rpcName, data, rpcService) {
|
|
991
688
|
const claimed = await this.withStepLock(runId, stepName, async () => {
|
|
992
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
|
+
}
|
|
993
695
|
if (stepState.status === 'succeeded' || stepState.status === 'running') {
|
|
994
696
|
return null;
|
|
995
697
|
}
|
|
@@ -1027,7 +729,7 @@ export class PikkuWorkflowService {
|
|
|
1027
729
|
}
|
|
1028
730
|
}
|
|
1029
731
|
if (graphNodeId) {
|
|
1030
|
-
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);
|
|
1031
733
|
}
|
|
1032
734
|
else {
|
|
1033
735
|
const subWorkflowMeta = meta[rpcName];
|
|
@@ -1037,7 +739,7 @@ export class PikkuWorkflowService {
|
|
|
1037
739
|
id: rpcName,
|
|
1038
740
|
parentRunId: runId,
|
|
1039
741
|
parentStepId: stepState.stepId,
|
|
1040
|
-
pikkuUserId:
|
|
742
|
+
pikkuUserId: run.wire?.pikkuUserId,
|
|
1041
743
|
};
|
|
1042
744
|
const shouldInline = !getSingletonServices()?.queueService;
|
|
1043
745
|
const { runId: childRunId } = await this.startWorkflow(rpcName, data, childWire, rpcService, { inline: shouldInline });
|
|
@@ -1202,7 +904,7 @@ export class PikkuWorkflowService {
|
|
|
1202
904
|
type: 'workflow',
|
|
1203
905
|
id: rpcName,
|
|
1204
906
|
parentRunId: runId,
|
|
1205
|
-
pikkuUserId:
|
|
907
|
+
pikkuUserId: (await this.getRunIdentity(runId))?.wire?.pikkuUserId,
|
|
1206
908
|
};
|
|
1207
909
|
const { runId: childRunId } = await this.startWorkflow(rpcName, data, childWire, rpcService, { inline: true });
|
|
1208
910
|
await this.setStepChildRunId(currentStepState.stepId, childRunId);
|
|
@@ -1281,39 +983,10 @@ export class PikkuWorkflowService {
|
|
|
1281
983
|
await new Promise((resolve) => setTimeout(resolve, getDurationInMilliseconds(duration)));
|
|
1282
984
|
await this.setStepResult(stepState.stepId, null);
|
|
1283
985
|
}
|
|
1284
|
-
getSuspendStepName(reason) {
|
|
1285
|
-
return `__workflow_suspend:${reason}`;
|
|
1286
|
-
}
|
|
1287
986
|
async suspendStep(runId, reason) {
|
|
1288
987
|
const fromStepName = this.lastStepName(runId);
|
|
1289
|
-
const suspendStepName = this.nextStepKey(runId,
|
|
1290
|
-
await this.withStepLock(runId, suspendStepName,
|
|
1291
|
-
let stepState;
|
|
1292
|
-
try {
|
|
1293
|
-
stepState = await this.getStepState(runId, suspendStepName);
|
|
1294
|
-
}
|
|
1295
|
-
catch {
|
|
1296
|
-
stepState = await this.insertStepState(runId, suspendStepName, 'pikkuWorkflowSuspend', {
|
|
1297
|
-
reason,
|
|
1298
|
-
}, undefined, fromStepName);
|
|
1299
|
-
}
|
|
1300
|
-
if (!stepState.stepId) {
|
|
1301
|
-
stepState = await this.insertStepState(runId, suspendStepName, 'pikkuWorkflowSuspend', {
|
|
1302
|
-
reason,
|
|
1303
|
-
}, undefined, fromStepName);
|
|
1304
|
-
}
|
|
1305
|
-
if (stepState.status === 'succeeded') {
|
|
1306
|
-
return;
|
|
1307
|
-
}
|
|
1308
|
-
if (stepState.status === 'pending') {
|
|
1309
|
-
await this.setStepRunning(stepState.stepId);
|
|
1310
|
-
}
|
|
1311
|
-
await this.setStepResult(stepState.stepId, {
|
|
1312
|
-
reason,
|
|
1313
|
-
suspendedAt: new Date().toISOString(),
|
|
1314
|
-
});
|
|
1315
|
-
throw new WorkflowSuspendedException(runId, reason);
|
|
1316
|
-
});
|
|
988
|
+
const suspendStepName = this.nextStepKey(runId, suspendStepNameFor(reason));
|
|
989
|
+
await this.withStepLock(runId, suspendStepName, () => recordSuspension(this.approvalStore, runId, reason, suspendStepName, fromStepName));
|
|
1317
990
|
}
|
|
1318
991
|
async scheduleRunWake(runId, delay) {
|
|
1319
992
|
try {
|
|
@@ -1331,96 +1004,26 @@ export class PikkuWorkflowService {
|
|
|
1331
1004
|
this.logger?.warn(`Failed to schedule approval expiry wake for run ${runId}; expiry will still resolve on the next replay`, error);
|
|
1332
1005
|
}
|
|
1333
1006
|
}
|
|
1334
|
-
|
|
1335
|
-
return
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
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
|
+
};
|
|
1343
1018
|
}
|
|
1344
|
-
async approveStep(runId, reason, decision) {
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
let resolved;
|
|
1348
|
-
try {
|
|
1349
|
-
resolved = await this.getStepState(runId, stepName);
|
|
1350
|
-
}
|
|
1351
|
-
catch {
|
|
1352
|
-
// knowledge: decisions/security/workflow-approval-payloads-are-validated-on-replay-inside-the-workflow.md
|
|
1353
|
-
}
|
|
1354
|
-
if (resolved?.stepId && resolved.status === 'succeeded') {
|
|
1355
|
-
const outcome = resolved.result;
|
|
1356
|
-
throw new WorkflowApprovalResolvedError(reason, outcome?.status ?? 'decided');
|
|
1357
|
-
}
|
|
1358
|
-
const state = await this.getRunState(runId);
|
|
1359
|
-
const record = (state[stateKey] ?? {});
|
|
1360
|
-
await this.updateRunState(runId, stateKey, {
|
|
1361
|
-
...record,
|
|
1362
|
-
decision,
|
|
1363
|
-
decidedAt: new Date().toISOString(),
|
|
1364
|
-
error: undefined,
|
|
1365
|
-
});
|
|
1366
|
-
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);
|
|
1367
1022
|
}
|
|
1368
1023
|
async approvalStep(runId, reason, options) {
|
|
1369
1024
|
const fromStepName = this.lastStepName(runId);
|
|
1370
|
-
const approvalStepName = this.nextStepKey(runId,
|
|
1371
|
-
return await this.withStepLock(runId, approvalStepName,
|
|
1372
|
-
let stepState;
|
|
1373
|
-
try {
|
|
1374
|
-
stepState = await this.getStepState(runId, approvalStepName);
|
|
1375
|
-
}
|
|
1376
|
-
catch {
|
|
1377
|
-
stepState = await this.insertStepState(runId, approvalStepName, 'pikkuWorkflowApproval', { reason, expiry: options.expiry }, undefined, fromStepName);
|
|
1378
|
-
}
|
|
1379
|
-
if (!stepState.stepId) {
|
|
1380
|
-
stepState = await this.insertStepState(runId, approvalStepName, 'pikkuWorkflowApproval', { reason, expiry: options.expiry }, undefined, fromStepName);
|
|
1381
|
-
}
|
|
1382
|
-
if (stepState.status === 'succeeded') {
|
|
1383
|
-
return stepState.result;
|
|
1384
|
-
}
|
|
1385
|
-
const stateKey = this.approvalStateKey(approvalStepName);
|
|
1386
|
-
let record = ((await this.getRunState(runId))[stateKey] ?? {});
|
|
1387
|
-
if (stepState.status === 'pending') {
|
|
1388
|
-
await this.setStepRunning(stepState.stepId);
|
|
1389
|
-
if (options.expiry !== undefined && !record.expiresAt) {
|
|
1390
|
-
const expiresAt = new Date(Date.now() + getDurationInMilliseconds(options.expiry)).toISOString();
|
|
1391
|
-
record = { ...record, expiresAt };
|
|
1392
|
-
await this.updateRunState(runId, stateKey, record);
|
|
1393
|
-
await this.scheduleRunWake(runId, getDurationInMilliseconds(options.expiry));
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
if (record.decision !== undefined) {
|
|
1397
|
-
const validation = await options.schema['~standard'].validate(record.decision);
|
|
1398
|
-
if (validation.issues) {
|
|
1399
|
-
await this.updateRunState(runId, stateKey, {
|
|
1400
|
-
...record,
|
|
1401
|
-
decision: undefined,
|
|
1402
|
-
decidedAt: undefined,
|
|
1403
|
-
error: validation.issues.map((issue) => ({
|
|
1404
|
-
message: issue.message,
|
|
1405
|
-
path: issue.path?.map((segment) => typeof segment === 'object' ? segment.key : segment),
|
|
1406
|
-
})),
|
|
1407
|
-
});
|
|
1408
|
-
throw new WorkflowSuspendedException(runId, reason);
|
|
1409
|
-
}
|
|
1410
|
-
const outcome = {
|
|
1411
|
-
status: 'decided',
|
|
1412
|
-
data: validation.value,
|
|
1413
|
-
};
|
|
1414
|
-
await this.setStepResult(stepState.stepId, outcome);
|
|
1415
|
-
return outcome;
|
|
1416
|
-
}
|
|
1417
|
-
if (record.expiresAt && Date.now() >= Date.parse(record.expiresAt)) {
|
|
1418
|
-
const outcome = { status: 'expired' };
|
|
1419
|
-
await this.setStepResult(stepState.stepId, outcome);
|
|
1420
|
-
return outcome;
|
|
1421
|
-
}
|
|
1422
|
-
throw new WorkflowSuspendedException(runId, reason);
|
|
1423
|
-
});
|
|
1025
|
+
const approvalStepName = this.nextStepKey(runId, approvalStepNameFor(reason));
|
|
1026
|
+
return await this.withStepLock(runId, approvalStepName, () => evaluateApprovalStep(this.approvalStore, runId, reason, approvalStepName, fromStepName, options));
|
|
1424
1027
|
}
|
|
1425
1028
|
createWorkflowWire(name, runId, rpcService, addonNamespace) {
|
|
1426
1029
|
const workflowWire = {
|
|
@@ -1466,40 +1069,15 @@ export class PikkuWorkflowService {
|
|
|
1466
1069
|
}
|
|
1467
1070
|
}
|
|
1468
1071
|
getConfig() {
|
|
1469
|
-
|
|
1470
|
-
const workflow = singletonServices.config?.workflow;
|
|
1471
|
-
return {
|
|
1472
|
-
retries: workflow?.retries ?? DEFAULT_STEP_RETRIES,
|
|
1473
|
-
retryDelay: workflow?.retryDelay ?? 0,
|
|
1474
|
-
orchestratorQueueName: workflow?.orchestratorQueueName ?? 'pikku-workflow-orchestrator',
|
|
1475
|
-
stepWorkerQueueName: workflow?.stepWorkerQueueName ?? 'pikku-workflow-step-worker',
|
|
1476
|
-
sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowSleeper',
|
|
1477
|
-
};
|
|
1072
|
+
return resolveWorkflowConfig();
|
|
1478
1073
|
}
|
|
1479
1074
|
getOrchestratorQueueName(workflowName) {
|
|
1480
|
-
|
|
1481
|
-
const perWorkflow = `wf-orchestrator-${toKebab(workflowName)}`;
|
|
1482
|
-
const meta = pikkuState(null, 'queue', 'meta');
|
|
1483
|
-
if (meta[perWorkflow]) {
|
|
1484
|
-
return perWorkflow;
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
return this.getConfig().orchestratorQueueName;
|
|
1075
|
+
return orchestratorQueueName(this.queueStrategy, workflowName);
|
|
1488
1076
|
}
|
|
1489
1077
|
getStepWorkerQueueName(rpcName) {
|
|
1490
|
-
|
|
1491
|
-
const perStep = `wf-step-${toKebab(rpcName)}`;
|
|
1492
|
-
const meta = pikkuState(null, 'queue', 'meta');
|
|
1493
|
-
if (meta[perStep]) {
|
|
1494
|
-
return perStep;
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
return this.getConfig().stepWorkerQueueName;
|
|
1078
|
+
return stepWorkerQueueName(this.queueStrategy, rpcName);
|
|
1498
1079
|
}
|
|
1499
1080
|
getJobGroup(id) {
|
|
1500
|
-
|
|
1501
|
-
return undefined;
|
|
1502
|
-
}
|
|
1503
|
-
return { id, tier: id };
|
|
1081
|
+
return jobGroupFor(this.queueStrategy, id);
|
|
1504
1082
|
}
|
|
1505
1083
|
}
|