@pikku/core 0.12.79 → 0.12.82
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 +473 -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/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/function/function-runner.js +22 -47
- package/dist/function/functions.types.d.ts +9 -5
- package/dist/function/index.d.ts +1 -1
- package/dist/index.d.ts +14 -19
- package/dist/index.js +6 -11
- 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 +16 -5
- package/dist/schema.js +35 -1
- package/dist/services/ai-agent-runner-service.d.ts +7 -0
- package/dist/services/ai-run-state-service.d.ts +17 -1
- package/dist/services/in-memory-ai-run-state-service.d.ts +6 -2
- package/dist/services/in-memory-ai-run-state-service.js +11 -1
- package/dist/services/in-memory-workflow-service.js +2 -0
- package/dist/services/index.d.ts +15 -15
- package/dist/services/index.js +5 -5
- 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/scoped-credential-service.d.ts +21 -0
- package/dist/services/scoped-credential-service.js +53 -0
- 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 +302 -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 -3
- package/dist/types/state.types.d.ts +14 -1
- package/dist/wirings/actor-flow/index.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-finalize.d.ts +58 -0
- package/dist/wirings/ai-agent/ai-agent-finalize.js +138 -0
- package/dist/wirings/ai-agent/ai-agent-interrupt.js +1 -0
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +2 -8
- package/dist/wirings/ai-agent/ai-agent-memory.js +37 -18
- package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +7 -0
- package/dist/wirings/ai-agent/ai-agent-model-config.js +44 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +5 -9
- package/dist/wirings/ai-agent/ai-agent-runner.js +89 -147
- package/dist/wirings/ai-agent/ai-agent-stream.js +109 -97
- package/dist/wirings/ai-agent/ai-agent-turn.d.ts +57 -0
- package/dist/wirings/ai-agent/ai-agent-turn.js +82 -0
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +47 -2
- package/dist/wirings/ai-agent/index.d.ts +8 -7
- package/dist/wirings/ai-agent/index.js +5 -4
- package/dist/wirings/ai-agent/voice-input.js +1 -6
- package/dist/wirings/ai-agent/voice-output.js +2 -12
- package/dist/wirings/ai-scorer/ai-scorer-grade.d.ts +26 -0
- package/dist/wirings/ai-scorer/ai-scorer-grade.js +33 -0
- package/dist/wirings/ai-scorer/ai-scorer-judge.d.ts +17 -0
- package/dist/wirings/ai-scorer/ai-scorer-judge.js +92 -0
- package/dist/wirings/ai-scorer/ai-scorer-live.d.ts +15 -0
- package/dist/wirings/ai-scorer/ai-scorer-live.js +38 -0
- package/dist/wirings/ai-scorer/ai-scorer-registry.d.ts +18 -0
- package/dist/wirings/ai-scorer/ai-scorer-registry.js +46 -0
- package/dist/wirings/ai-scorer/ai-scorer-sampling.d.ts +8 -0
- package/dist/wirings/ai-scorer/ai-scorer-sampling.js +31 -0
- package/dist/wirings/ai-scorer/ai-scorer-snapshots.d.ts +10 -0
- package/dist/wirings/ai-scorer/ai-scorer-snapshots.js +40 -0
- package/dist/wirings/ai-scorer/ai-scorer-worker.d.ts +15 -0
- package/dist/wirings/ai-scorer/ai-scorer-worker.js +58 -0
- package/dist/wirings/ai-scorer/ai-scorer.d.ts +39 -0
- package/dist/wirings/ai-scorer/ai-scorer.js +40 -0
- package/dist/wirings/ai-scorer/ai-scorer.types.d.ts +90 -0
- package/dist/wirings/ai-scorer/ai-scorer.types.js +4 -0
- package/dist/wirings/ai-scorer/index.d.ts +6 -0
- package/dist/wirings/ai-scorer/index.js +5 -0
- 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/index.d.ts +5 -6
- package/dist/wirings/channel/index.js +3 -4
- package/dist/wirings/channel/local/local-channel-runner.js +8 -1
- 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/channel/cli-raw-channel-runner.js +9 -1
- package/dist/wirings/cli/channel/index.d.ts +1 -2
- package/dist/wirings/cli/channel/index.js +0 -1
- package/dist/wirings/cli/cli-runner.js +17 -3
- package/dist/wirings/cli/cli.types.d.ts +0 -8
- package/dist/wirings/cli/command-parser.js +13 -0
- package/dist/wirings/credential/index.d.ts +1 -1
- package/dist/wirings/gateway/gateway-runner.js +9 -2
- package/dist/wirings/gateway/index.d.ts +1 -1
- 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 +14 -15
- package/dist/wirings/http/http.types.d.ts +0 -10
- package/dist/wirings/http/index.d.ts +2 -3
- package/dist/wirings/http/index.js +1 -1
- package/dist/wirings/mcp/index.d.ts +1 -1
- package/dist/wirings/mcp/mcp-runner.d.ts +15 -7
- package/dist/wirings/mcp/mcp-runner.js +18 -11
- package/dist/wirings/persona/index.d.ts +3 -4
- package/dist/wirings/persona/index.js +2 -3
- package/dist/wirings/queue/index.d.ts +1 -3
- package/dist/wirings/queue/index.js +1 -3
- package/dist/wirings/rpc/addon-runner.d.ts +4 -0
- package/dist/wirings/rpc/addon-runner.js +19 -3
- package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
- package/dist/wirings/rpc/rpc-runner.js +55 -86
- package/dist/wirings/rpc/rpc-types.d.ts +7 -0
- package/dist/wirings/rpc/wire-addon.d.ts +13 -0
- package/dist/wirings/rpc/wire-addon.js +4 -0
- package/dist/wirings/scheduler/index.d.ts +1 -1
- package/dist/wirings/secret/validate-secret-definitions.js +2 -2
- package/dist/wirings/trigger/index.d.ts +1 -1
- 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/index.d.ts +5 -6
- package/dist/wirings/virtual-user/index.js +2 -4
- 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 +100 -3
- 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 +12 -7
- package/dist/wirings/workflow/index.js +7 -3
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
- package/dist/wirings/workflow/pikku-scenario-service.js +64 -10
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +28 -147
- package/dist/wirings/workflow/pikku-workflow-service.js +85 -493
- package/dist/wirings/workflow/scenario-step.types.d.ts +8 -0
- package/dist/wirings/workflow/workflow-approval-audit.d.ts +16 -0
- package/dist/wirings/workflow/workflow-approval-audit.js +40 -0
- package/dist/wirings/workflow/workflow-approval-policy.d.ts +20 -0
- package/dist/wirings/workflow/workflow-approval-policy.js +48 -0
- package/dist/wirings/workflow/workflow-approval.d.ts +67 -0
- package/dist/wirings/workflow/workflow-approval.js +177 -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-ownership.d.ts +17 -0
- package/dist/wirings/workflow/workflow-run-ownership.js +30 -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 +8 -1
- 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/addon-pikku-meta-ships-at-the-package-root-or-under-dist.md +32 -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-addon-scope-root-loses-to-a-root-the-host-already-declares.md +39 -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 +42 -3
- 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/validate-runs-checks-by-precondition.md +115 -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-function-never-receives-the-secret-service.md +37 -0
- 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 +50 -0
- package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
- package/knowledge/decisions/security/an-approval-answer-outlives-the-run-it-answered.md +59 -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 +9 -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 +15 -3
- package/scripts/generate-api-report.d.mts +1 -0
- package/scripts/generate-api-report.mjs +89 -0
- package/scripts/generate-api-report.mts +343 -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/errors/index.ts +1 -1
- package/src/function/function-runner.test.ts +53 -1
- package/src/function/function-runner.ts +33 -48
- package/src/function/functions.types.ts +15 -9
- package/src/function/index.ts +0 -2
- package/src/handle-error.ts +1 -1
- package/src/index.ts +3 -57
- 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 +22 -6
- package/src/public-surface.json +554 -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-agent-runner-service.ts +12 -1
- package/src/services/ai-run-state-service.ts +18 -1
- package/src/services/audit-service.ts +2 -2
- package/src/services/in-memory-ai-run-state-service.ts +16 -2
- package/src/services/in-memory-workflow-service.ts +2 -0
- package/src/services/index.ts +4 -47
- 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/scoped-credential-service.test.ts +86 -0
- package/src/services/scoped-credential-service.ts +63 -0
- 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 +379 -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 +14 -3
- package/src/types/state.types.ts +17 -1
- package/src/wirings/actor-flow/index.ts +0 -3
- 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-finalize.test.ts +186 -0
- package/src/wirings/ai-agent/ai-agent-finalize.ts +197 -0
- package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
- package/src/wirings/ai-agent/ai-agent-interrupt.ts +1 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +62 -39
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +72 -3
- package/src/wirings/ai-agent/ai-agent-model-config.ts +49 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +12 -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 +120 -160
- package/src/wirings/ai-agent/ai-agent-stream-output-hooks.test.ts +353 -0
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +152 -117
- package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
- package/src/wirings/ai-agent/ai-agent-turn.test.ts +67 -0
- package/src/wirings/ai-agent/ai-agent-turn.ts +122 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +65 -5
- package/src/wirings/ai-agent/index.ts +2 -16
- package/src/wirings/ai-agent/voice-input.ts +1 -6
- package/src/wirings/ai-agent/voice-output.ts +2 -12
- package/src/wirings/ai-scorer/ai-scorer-grade.test.ts +106 -0
- package/src/wirings/ai-scorer/ai-scorer-grade.ts +55 -0
- package/src/wirings/ai-scorer/ai-scorer-judge.test.ts +143 -0
- package/src/wirings/ai-scorer/ai-scorer-judge.ts +120 -0
- package/src/wirings/ai-scorer/ai-scorer-live.test.ts +174 -0
- package/src/wirings/ai-scorer/ai-scorer-live.ts +56 -0
- package/src/wirings/ai-scorer/ai-scorer-registry.ts +63 -0
- package/src/wirings/ai-scorer/ai-scorer-sampling.test.ts +34 -0
- package/src/wirings/ai-scorer/ai-scorer-sampling.ts +36 -0
- package/src/wirings/ai-scorer/ai-scorer-snapshots.test.ts +49 -0
- package/src/wirings/ai-scorer/ai-scorer-snapshots.ts +46 -0
- package/src/wirings/ai-scorer/ai-scorer-worker.test.ts +122 -0
- package/src/wirings/ai-scorer/ai-scorer-worker.ts +69 -0
- package/src/wirings/ai-scorer/ai-scorer.ts +76 -0
- package/src/wirings/ai-scorer/ai-scorer.types.ts +107 -0
- package/src/wirings/ai-scorer/index.ts +24 -0
- 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/index.ts +1 -20
- package/src/wirings/channel/local/local-channel-runner.test.ts +68 -0
- package/src/wirings/channel/local/local-channel-runner.ts +9 -2
- 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-channel-runner.test.ts +23 -0
- package/src/wirings/cli/channel/cli-raw-channel-runner.ts +12 -1
- package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
- package/src/wirings/cli/channel/index.ts +0 -7
- package/src/wirings/cli/cli-runner.test.ts +68 -0
- package/src/wirings/cli/cli-runner.ts +25 -5
- 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/credential/index.ts +0 -1
- package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
- package/src/wirings/gateway/gateway-runner.ts +27 -19
- package/src/wirings/gateway/index.ts +0 -3
- package/src/wirings/http/http-routes.ts +4 -1
- package/src/wirings/http/http-runner.test.ts +66 -0
- package/src/wirings/http/http-runner.ts +21 -28
- package/src/wirings/http/http.types.ts +0 -15
- package/src/wirings/http/index.ts +2 -8
- package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
- package/src/wirings/http/web-request.ts +1 -1
- package/src/wirings/mcp/index.ts +0 -1
- package/src/wirings/mcp/mcp-runner.test.ts +181 -0
- package/src/wirings/mcp/mcp-runner.ts +37 -21
- package/src/wirings/persona/index.ts +0 -8
- 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/index.ts +0 -14
- package/src/wirings/queue/queue-runner.ts +1 -1
- package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
- package/src/wirings/rpc/addon-runner.ts +34 -3
- package/src/wirings/rpc/addon-secrets.test.ts +261 -0
- package/src/wirings/rpc/rpc-runner.test.ts +2 -0
- package/src/wirings/rpc/rpc-runner.ts +60 -136
- package/src/wirings/rpc/rpc-types.ts +11 -0
- package/src/wirings/rpc/wire-addon.ts +20 -1
- package/src/wirings/scheduler/index.ts +0 -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/index.ts +0 -1
- package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
- package/src/wirings/trigger/trigger-runner.ts +7 -5
- package/src/wirings/virtual-user/index.ts +0 -16
- 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 +117 -4
- 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 +76 -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 +14 -24
- package/src/wirings/workflow/pikku-scenario-service.ts +101 -27
- package/src/wirings/workflow/pikku-workflow-service.test.ts +15 -14
- package/src/wirings/workflow/pikku-workflow-service.ts +214 -743
- package/src/wirings/workflow/scenario-expectations.test.ts +75 -0
- package/src/wirings/workflow/scenario-hooks.test.ts +52 -0
- package/src/wirings/workflow/scenario-step.types.ts +8 -0
- package/src/wirings/workflow/workflow-approval-audit.ts +47 -0
- package/src/wirings/workflow/workflow-approval-policy.test.ts +524 -0
- package/src/wirings/workflow/workflow-approval-policy.ts +68 -0
- package/src/wirings/workflow/workflow-approval.ts +289 -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 +212 -0
- package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
- package/src/wirings/workflow/workflow-run-ownership.ts +37 -0
- package/src/wirings/workflow/workflow-suspend.ts +61 -0
- package/src/wirings/workflow/workflow.types.ts +7 -9
- package/src/wirings-stay-decoupled.test.ts +127 -0
- package/tsconfig.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/internal.d.ts +0 -3
- package/dist/internal.js +0 -2
- package/dist/middleware/timeout.d.ts +0 -9
- package/dist/middleware/timeout.js +0 -15
- package/dist/pikku-response.d.ts +0 -6
- package/dist/pikku-response.js +0 -6
- package/dist/services/gopass-secrets.d.ts +0 -15
- package/dist/services/gopass-secrets.js +0 -76
- package/dist/services/http-scenario-actors.d.ts +0 -75
- package/dist/services/http-scenario-actors.js +0 -195
- package/dist/services/http-user-flow-actors.d.ts +0 -67
- package/dist/services/http-user-flow-actors.js +0 -193
- package/dist/services/scenario-actors-service.d.ts +0 -127
- package/dist/services/scenario-actors-service.js +0 -40
- package/dist/services/user-flow-actors-service.d.ts +0 -39
- package/dist/wirings/credential/wire-credential.d.ts +0 -48
- package/dist/wirings/credential/wire-credential.js +0 -47
- package/dist/wirings/oauth2/oauth2-client.d.ts +0 -47
- package/dist/wirings/oauth2/oauth2-client.js +0 -263
- package/dist/wirings/oauth2/oauth2-routes.d.ts +0 -35
- package/dist/wirings/oauth2/oauth2-routes.js +0 -146
- package/dist/wirings/scope/wire-scope.d.ts +0 -33
- package/dist/wirings/scope/wire-scope.js +0 -32
- package/dist/wirings/workflow/dsl/index.d.ts +0 -5
- package/dist/wirings/workflow/dsl/index.js +0 -4
- package/dist/wirings/workflow/graph/index.d.ts +0 -5
- package/dist/wirings/workflow/graph/index.js +0 -4
- package/src/internal.ts +0 -10
- /package/dist/{services/user-flow-actors-service.js → wirings/workflow/workflow-run-engine.types.js} +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getSingletonServices, pikkuState } from '../../pikku-state.js';
|
|
2
|
+
import { getDurationInMilliseconds } from '../../time-utils.js';
|
|
3
|
+
import { DEFAULT_STEP_RETRIES } from './workflow-constants.js';
|
|
4
|
+
const toKebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
5
|
+
export const resolveWorkflowConfig = () => {
|
|
6
|
+
const workflow = getSingletonServices().config?.workflow;
|
|
7
|
+
return {
|
|
8
|
+
retries: workflow?.retries ?? DEFAULT_STEP_RETRIES,
|
|
9
|
+
retryDelay: workflow?.retryDelay ?? 0,
|
|
10
|
+
orchestratorQueueName: workflow?.orchestratorQueueName ?? 'pikku-workflow-orchestrator',
|
|
11
|
+
stepWorkerQueueName: workflow?.stepWorkerQueueName ?? 'pikku-workflow-step-worker',
|
|
12
|
+
sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowSleeper',
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Falls back to the shared queue whenever the dedicated one was not generated
|
|
17
|
+
* into queue meta, so a workflow added without a rebuild still runs.
|
|
18
|
+
*/
|
|
19
|
+
const dedicatedQueueName = (prefix, name, strategy, fallback) => {
|
|
20
|
+
if (!name || strategy === 'shared-groups') {
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
const dedicated = `${prefix}${toKebab(name)}`;
|
|
24
|
+
return pikkuState(null, 'queue', 'meta')[dedicated] ? dedicated : fallback;
|
|
25
|
+
};
|
|
26
|
+
export const orchestratorQueueName = (strategy, workflowName) => dedicatedQueueName('wf-orchestrator-', workflowName, strategy, resolveWorkflowConfig().orchestratorQueueName);
|
|
27
|
+
export const stepWorkerQueueName = (strategy, rpcName) => dedicatedQueueName('wf-step-', rpcName, strategy, resolveWorkflowConfig().stepWorkerQueueName);
|
|
28
|
+
export const jobGroupFor = (strategy, id) => id && strategy === 'shared-groups' ? { id, tier: id } : undefined;
|
|
29
|
+
export const stepJobOptions = (stepOptions) => {
|
|
30
|
+
const retries = stepOptions?.retries ?? DEFAULT_STEP_RETRIES;
|
|
31
|
+
const retryDelay = stepOptions?.retryDelay;
|
|
32
|
+
const backoff = retryDelay !== undefined && retryDelay !== 'exponential'
|
|
33
|
+
? { type: 'fixed', delay: getDurationInMilliseconds(retryDelay) }
|
|
34
|
+
: retries > 0 || retryDelay === 'exponential'
|
|
35
|
+
? 'exponential'
|
|
36
|
+
: undefined;
|
|
37
|
+
return { attempts: retries + 1, ...(backoff ? { backoff } : {}) };
|
|
38
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { GroupConcurrencyConfig } from '../queue/queue.types.js';
|
|
2
|
+
import type { Logger } from '../../services/logger.js';
|
|
3
|
+
import type { WorkflowQueueStrategy } from './workflow-queue-routing.js';
|
|
4
|
+
export type WorkflowQueueWiringOptions = {
|
|
5
|
+
strategy: WorkflowQueueStrategy;
|
|
6
|
+
concurrency: number;
|
|
7
|
+
groupConcurrency: number | GroupConcurrencyConfig;
|
|
8
|
+
/**
|
|
9
|
+
* Resolved only when there is something to warn about: reading the logger
|
|
10
|
+
* throws while singleton services are uninitialized, and wiring queues before
|
|
11
|
+
* that point is legitimate.
|
|
12
|
+
*/
|
|
13
|
+
getLogger: () => Logger | undefined;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Registers the orchestrator, step-worker and sleeper functions and binds them
|
|
17
|
+
* to their queues. Idempotent: a queue already carrying a registered function
|
|
18
|
+
* is left alone.
|
|
19
|
+
*/
|
|
20
|
+
export declare const wireWorkflowQueueWorkers: ({ strategy, concurrency, groupConcurrency, getLogger, }: WorkflowQueueWiringOptions) => void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { addFunction } from '../../function/function-runner.js';
|
|
2
|
+
import { pikkuState } from '../../pikku-state.js';
|
|
3
|
+
import { wireQueueWorker } from '../queue/queue-runner.js';
|
|
4
|
+
import { pikkuWorkflowOrchestratorFunc, pikkuWorkflowSleeperFunc, pikkuWorkflowWorkerFunc, } from './workflow-queue-workers.js';
|
|
5
|
+
const ORCHESTRATOR_QUEUE_PREFIX = 'wf-orchestrator-';
|
|
6
|
+
const STEP_QUEUE_PREFIX = 'wf-step-';
|
|
7
|
+
const helperFunctionMeta = (funcId) => ({
|
|
8
|
+
pikkuFuncId: funcId,
|
|
9
|
+
sessionless: true,
|
|
10
|
+
functionType: 'helper',
|
|
11
|
+
inputSchemaName: null,
|
|
12
|
+
outputSchemaName: null,
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* Registers the orchestrator, step-worker and sleeper functions and binds them
|
|
16
|
+
* to their queues. Idempotent: a queue already carrying a registered function
|
|
17
|
+
* is left alone.
|
|
18
|
+
*/
|
|
19
|
+
export const wireWorkflowQueueWorkers = ({ strategy, concurrency, groupConcurrency, getLogger, }) => {
|
|
20
|
+
const functions = pikkuState(null, 'function', 'functions');
|
|
21
|
+
const functionsMeta = pikkuState(null, 'function', 'meta');
|
|
22
|
+
const queueMeta = pikkuState(null, 'queue', 'meta');
|
|
23
|
+
const registerWorkflowFunc = (funcId, func, queueName, config) => {
|
|
24
|
+
if (functions.has(funcId))
|
|
25
|
+
return;
|
|
26
|
+
addFunction(funcId, func);
|
|
27
|
+
if (!queueMeta[queueName]) {
|
|
28
|
+
queueMeta[queueName] = { pikkuFuncId: funcId, name: queueName };
|
|
29
|
+
}
|
|
30
|
+
wireQueueWorker({ name: queueName, func, config });
|
|
31
|
+
if (!functionsMeta[funcId]) {
|
|
32
|
+
functionsMeta[funcId] = helperFunctionMeta(funcId);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const sharedGroups = strategy === 'shared-groups';
|
|
36
|
+
const sharedQueueConfig = sharedGroups
|
|
37
|
+
? { batchSize: concurrency, groupConcurrency }
|
|
38
|
+
: undefined;
|
|
39
|
+
registerWorkflowFunc('pikkuWorkflowOrchestrator', { func: pikkuWorkflowOrchestratorFunc }, 'pikku-workflow-orchestrator', sharedQueueConfig);
|
|
40
|
+
registerWorkflowFunc('pikkuWorkflowStepWorker', { func: pikkuWorkflowWorkerFunc }, 'pikku-workflow-step-worker', sharedQueueConfig);
|
|
41
|
+
const registerDedicatedQueues = (meta) => {
|
|
42
|
+
for (const [queueName, entry] of Object.entries(meta)) {
|
|
43
|
+
if (functions.has(entry.pikkuFuncId))
|
|
44
|
+
continue;
|
|
45
|
+
if (queueName.startsWith(ORCHESTRATOR_QUEUE_PREFIX)) {
|
|
46
|
+
registerWorkflowFunc(entry.pikkuFuncId, { func: pikkuWorkflowOrchestratorFunc }, queueName);
|
|
47
|
+
}
|
|
48
|
+
else if (queueName.startsWith(STEP_QUEUE_PREFIX)) {
|
|
49
|
+
registerWorkflowFunc(entry.pikkuFuncId, { func: pikkuWorkflowWorkerFunc }, queueName);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
if (!sharedGroups) {
|
|
54
|
+
registerDedicatedQueues(pikkuState(null, 'queue', 'meta'));
|
|
55
|
+
const addons = pikkuState(null, 'addons', 'packages');
|
|
56
|
+
for (const [, addon] of addons ?? []) {
|
|
57
|
+
const addonQueueMeta = pikkuState(addon.package, 'queue', 'meta');
|
|
58
|
+
if (addonQueueMeta) {
|
|
59
|
+
registerDedicatedQueues(addonQueueMeta);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
warnOnMissingDedicatedQueues(queueMeta, getLogger);
|
|
64
|
+
if (!functions.has('pikkuWorkflowSleeper')) {
|
|
65
|
+
addFunction('pikkuWorkflowSleeper', { func: pikkuWorkflowSleeperFunc });
|
|
66
|
+
}
|
|
67
|
+
if (!functionsMeta.pikkuWorkflowSleeper) {
|
|
68
|
+
functionsMeta.pikkuWorkflowSleeper = helperFunctionMeta('pikkuWorkflowSleeper');
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const warnOnMissingDedicatedQueues = (queueMeta, getLogger) => {
|
|
72
|
+
const workflowCount = Object.keys(pikkuState(null, 'workflows', 'meta') ?? {}).length;
|
|
73
|
+
const dedicatedQueues = Object.keys(queueMeta).filter((name) => name.startsWith(ORCHESTRATOR_QUEUE_PREFIX)).length;
|
|
74
|
+
if (workflowCount > 0 && dedicatedQueues === 0) {
|
|
75
|
+
getLogger()?.warn?.(`[pikku] ${workflowCount} workflow(s) registered but no per-workflow orchestrator queues were found in queue meta. ` +
|
|
76
|
+
`All workflows will share a single orchestrator queue, where one slow step blocks every other workflow behind it. ` +
|
|
77
|
+
`Check that the generated bootstrap imports the queue-workers meta (pikku-queue-workers-wirings-meta.gen.js).`);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Logger } from '../../services/logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* Per-process, advisory record of when a run may next be re-dispatched.
|
|
4
|
+
*
|
|
5
|
+
* Keyed by run rather than by step because the run is the unit of re-drive:
|
|
6
|
+
* `resumeWorkflow` replays the whole run and re-dispatches every step still
|
|
7
|
+
* owed a job, so holding off a single step while resuming its run would
|
|
8
|
+
* suppress nothing.
|
|
9
|
+
*
|
|
10
|
+
* Losing this on restart costs extra dispatches, never correctness.
|
|
11
|
+
*/
|
|
12
|
+
export declare class RedispatchBackoff {
|
|
13
|
+
private readonly eligibleAt;
|
|
14
|
+
private readonly delays;
|
|
15
|
+
isEligible(runId: string, now: number): boolean;
|
|
16
|
+
note(runId: string, now: number): void;
|
|
17
|
+
}
|
|
18
|
+
type SweepDeps = {
|
|
19
|
+
resume: (runId: string) => Promise<void>;
|
|
20
|
+
logger?: Logger;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Re-drive runs whose next move was lost, and report which were resumed.
|
|
24
|
+
*
|
|
25
|
+
* Arming a step is two writes to two systems — the step row, then the queue or
|
|
26
|
+
* scheduler job — so a process that dies between them leaves a run that is
|
|
27
|
+
* `running` with nothing in flight. Nothing notices: the run parks on a step
|
|
28
|
+
* that will never complete and never error, so it neither finishes nor fails.
|
|
29
|
+
* (Seen on a `workflow.sleep()`: a deploy restart landed between the sleep
|
|
30
|
+
* step's insert and its timer, parking the run permanently.)
|
|
31
|
+
*
|
|
32
|
+
* Replay is the recovery — `resumeWorkflow` re-orchestrates from persisted step
|
|
33
|
+
* state, and every settled step is memoized, so resuming a run that was not
|
|
34
|
+
* actually stuck costs an orchestration pass and changes nothing. That
|
|
35
|
+
* idempotence is what makes an idle-time heuristic safe here; a run that is
|
|
36
|
+
* legitimately mid-sleep is excluded anyway, since its step is `scheduled`.
|
|
37
|
+
*/
|
|
38
|
+
export declare const sweepStalledRuns: (findStalledRunIds: (before: Date, limit: number) => Promise<string[]>, options: {
|
|
39
|
+
stalledAfterMs?: number;
|
|
40
|
+
limit?: number;
|
|
41
|
+
} | undefined, deps: SweepDeps) => Promise<{
|
|
42
|
+
resumed: string[];
|
|
43
|
+
}>;
|
|
44
|
+
/**
|
|
45
|
+
* Re-drive steps whose dispatch was lost, and report which runs were nudged.
|
|
46
|
+
*
|
|
47
|
+
* The step row is the outbox record and this is the relay. Age is the only
|
|
48
|
+
* signal available — a step `pending` because its dispatch was lost is
|
|
49
|
+
* indistinguishable from one whose job is merely still queued — so a step past
|
|
50
|
+
* `undispatchedAfterMs` is re-dispatched regardless, and correctness rests on
|
|
51
|
+
* the claim in `executeWorkflowStepInner` rather than on the guess being right.
|
|
52
|
+
* A redundant dispatch costs one queue message: the loser reads `running` and
|
|
53
|
+
* returns without invoking anything.
|
|
54
|
+
*
|
|
55
|
+
* Re-dispatches back off per run (doubling from 30s, capped at 10m) so a
|
|
56
|
+
* genuine queue backlog is not amplified by a tick that keeps firing at the
|
|
57
|
+
* steps the backlog is already delaying.
|
|
58
|
+
*/
|
|
59
|
+
export declare const sweepUndispatchedSteps: (findUndispatchedSteps: (before: Date, limit: number) => Promise<Array<{
|
|
60
|
+
runId: string;
|
|
61
|
+
stepId: string;
|
|
62
|
+
}>>, backoff: RedispatchBackoff, options: {
|
|
63
|
+
undispatchedAfterMs?: number;
|
|
64
|
+
limit?: number;
|
|
65
|
+
} | undefined, deps: SweepDeps) => Promise<{
|
|
66
|
+
redispatched: string[];
|
|
67
|
+
}>;
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { DEFAULT_STALLED_RUN_LIMIT, DEFAULT_STALLED_RUN_MS, DEFAULT_UNDISPATCHED_STEP_LIMIT, DEFAULT_UNDISPATCHED_STEP_MS, REDISPATCH_BACKOFF_MAX_ENTRIES, REDISPATCH_BACKOFF_MAX_MS, REDISPATCH_BACKOFF_MS, } from './workflow-constants.js';
|
|
2
|
+
/**
|
|
3
|
+
* Per-process, advisory record of when a run may next be re-dispatched.
|
|
4
|
+
*
|
|
5
|
+
* Keyed by run rather than by step because the run is the unit of re-drive:
|
|
6
|
+
* `resumeWorkflow` replays the whole run and re-dispatches every step still
|
|
7
|
+
* owed a job, so holding off a single step while resuming its run would
|
|
8
|
+
* suppress nothing.
|
|
9
|
+
*
|
|
10
|
+
* Losing this on restart costs extra dispatches, never correctness.
|
|
11
|
+
*/
|
|
12
|
+
export class RedispatchBackoff {
|
|
13
|
+
eligibleAt = new Map();
|
|
14
|
+
delays = new Map();
|
|
15
|
+
isEligible(runId, now) {
|
|
16
|
+
const at = this.eligibleAt.get(runId);
|
|
17
|
+
return at === undefined || at <= now;
|
|
18
|
+
}
|
|
19
|
+
note(runId, now) {
|
|
20
|
+
const previous = this.delays.get(runId);
|
|
21
|
+
const delay = Math.min(previous === undefined ? REDISPATCH_BACKOFF_MS : previous * 2, REDISPATCH_BACKOFF_MAX_MS);
|
|
22
|
+
// A run that settles is never returned again, so entries are only evicted
|
|
23
|
+
// by this bound — oldest first, which is also least recently re-dispatched.
|
|
24
|
+
if (this.eligibleAt.size >= REDISPATCH_BACKOFF_MAX_ENTRIES) {
|
|
25
|
+
const oldest = this.eligibleAt.keys().next();
|
|
26
|
+
if (!oldest.done) {
|
|
27
|
+
this.eligibleAt.delete(oldest.value);
|
|
28
|
+
this.delays.delete(oldest.value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
this.delays.set(runId, delay);
|
|
32
|
+
this.eligibleAt.set(runId, now + delay);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const resumeEach = async (runIds, { resume, logger }, failure) => {
|
|
36
|
+
const succeeded = [];
|
|
37
|
+
for (const runId of runIds) {
|
|
38
|
+
try {
|
|
39
|
+
await resume(runId);
|
|
40
|
+
succeeded.push(runId);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
// One unresumable run must not stop the sweep from recovering the rest.
|
|
44
|
+
logger?.error(failure(runId, err instanceof Error ? err.message : String(err)));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return succeeded;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Re-drive runs whose next move was lost, and report which were resumed.
|
|
51
|
+
*
|
|
52
|
+
* Arming a step is two writes to two systems — the step row, then the queue or
|
|
53
|
+
* scheduler job — so a process that dies between them leaves a run that is
|
|
54
|
+
* `running` with nothing in flight. Nothing notices: the run parks on a step
|
|
55
|
+
* that will never complete and never error, so it neither finishes nor fails.
|
|
56
|
+
* (Seen on a `workflow.sleep()`: a deploy restart landed between the sleep
|
|
57
|
+
* step's insert and its timer, parking the run permanently.)
|
|
58
|
+
*
|
|
59
|
+
* Replay is the recovery — `resumeWorkflow` re-orchestrates from persisted step
|
|
60
|
+
* state, and every settled step is memoized, so resuming a run that was not
|
|
61
|
+
* actually stuck costs an orchestration pass and changes nothing. That
|
|
62
|
+
* idempotence is what makes an idle-time heuristic safe here; a run that is
|
|
63
|
+
* legitimately mid-sleep is excluded anyway, since its step is `scheduled`.
|
|
64
|
+
*/
|
|
65
|
+
export const sweepStalledRuns = async (findStalledRunIds, options, deps) => {
|
|
66
|
+
const before = new Date(Date.now() - (options?.stalledAfterMs ?? DEFAULT_STALLED_RUN_MS));
|
|
67
|
+
const runIds = await findStalledRunIds(before, options?.limit ?? DEFAULT_STALLED_RUN_LIMIT);
|
|
68
|
+
return {
|
|
69
|
+
resumed: await resumeEach(runIds, deps, (runId, detail) => `Failed to resume stalled workflow run ${runId}: ${detail}`),
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Re-drive steps whose dispatch was lost, and report which runs were nudged.
|
|
74
|
+
*
|
|
75
|
+
* The step row is the outbox record and this is the relay. Age is the only
|
|
76
|
+
* signal available — a step `pending` because its dispatch was lost is
|
|
77
|
+
* indistinguishable from one whose job is merely still queued — so a step past
|
|
78
|
+
* `undispatchedAfterMs` is re-dispatched regardless, and correctness rests on
|
|
79
|
+
* the claim in `executeWorkflowStepInner` rather than on the guess being right.
|
|
80
|
+
* A redundant dispatch costs one queue message: the loser reads `running` and
|
|
81
|
+
* returns without invoking anything.
|
|
82
|
+
*
|
|
83
|
+
* Re-dispatches back off per run (doubling from 30s, capped at 10m) so a
|
|
84
|
+
* genuine queue backlog is not amplified by a tick that keeps firing at the
|
|
85
|
+
* steps the backlog is already delaying.
|
|
86
|
+
*/
|
|
87
|
+
export const sweepUndispatchedSteps = async (findUndispatchedSteps, backoff, options, deps) => {
|
|
88
|
+
const before = new Date(Date.now() - (options?.undispatchedAfterMs ?? DEFAULT_UNDISPATCHED_STEP_MS));
|
|
89
|
+
const steps = await findUndispatchedSteps(before, options?.limit ?? DEFAULT_UNDISPATCHED_STEP_LIMIT);
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
const runIds = new Set();
|
|
92
|
+
for (const { runId } of steps) {
|
|
93
|
+
if (!backoff.isEligible(runId, now))
|
|
94
|
+
continue;
|
|
95
|
+
backoff.note(runId, now);
|
|
96
|
+
runIds.add(runId);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
redispatched: await resumeEach(runIds, deps, (runId, detail) => `Failed to re-dispatch workflow run ${runId}: ${detail}`),
|
|
100
|
+
};
|
|
101
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { PikkuRawWire, SerializedError } from '../../types/core.types.js';
|
|
2
|
+
import type { CoreWorkflow, PikkuWorkflowWire, StepState, WorkflowRun, WorkflowStatus, WorkflowStepOptions } from './workflow.types.js';
|
|
3
|
+
export interface RunLifecycleContext {
|
|
4
|
+
runId: string;
|
|
5
|
+
run: WorkflowRun;
|
|
6
|
+
workflowMeta: any;
|
|
7
|
+
workflow: CoreWorkflow;
|
|
8
|
+
wire: PikkuRawWire;
|
|
9
|
+
packageName: string | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The subset of the workflow service a step executor is allowed to call back
|
|
13
|
+
* into.
|
|
14
|
+
*/
|
|
15
|
+
export interface WorkflowRunEngine {
|
|
16
|
+
inlineStep(runId: string, logicalStepName: string, fn: Function, stepOptions?: WorkflowStepOptions, data?: any, funcName?: string): Promise<any>;
|
|
17
|
+
updateRunStatus(runId: string, status: WorkflowStatus, output?: any, error?: SerializedError): Promise<void>;
|
|
18
|
+
onChildWorkflowFailed(run: WorkflowRun, error: unknown): Promise<void>;
|
|
19
|
+
verifyStepName(stepName: unknown): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Hooks a host may install to decorate runs it did not start. Used by the
|
|
23
|
+
* scenario service to attach its own per-run state.
|
|
24
|
+
*/
|
|
25
|
+
export interface WorkflowRunExtension {
|
|
26
|
+
attachRunContext(runId: string, workflowMeta: any, options?: Record<string, any>): Promise<void>;
|
|
27
|
+
detachRunContext(runId: string): void;
|
|
28
|
+
decorateRunWire(wire: PikkuRawWire, context: {
|
|
29
|
+
runId: string;
|
|
30
|
+
workflowMeta: any;
|
|
31
|
+
workflowWire: PikkuWorkflowWire;
|
|
32
|
+
}): void;
|
|
33
|
+
decorateWorkflowWire(workflowWire: PikkuWorkflowWire, context: {
|
|
34
|
+
name: string;
|
|
35
|
+
runId: string;
|
|
36
|
+
rpcService: any;
|
|
37
|
+
addonNamespace?: string | null;
|
|
38
|
+
}): void;
|
|
39
|
+
onBeforeRunFunc(context: RunLifecycleContext): Promise<void>;
|
|
40
|
+
onAfterRunFunc(context: RunLifecycleContext, outcome: 'completed' | 'failed' | 'interrupted', failure: unknown): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Per-run bookkeeping held only for the lifetime of an in-process execution.
|
|
44
|
+
*/
|
|
45
|
+
export type RunContext = {
|
|
46
|
+
activeExecutions: number;
|
|
47
|
+
inline?: boolean;
|
|
48
|
+
ordinals: Map<string, number>;
|
|
49
|
+
lastStep?: string;
|
|
50
|
+
replay?: {
|
|
51
|
+
steps?: Map<string, StepState>;
|
|
52
|
+
run?: WorkflowRun;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ForbiddenError } from '../../errors/errors.js';
|
|
2
|
+
import type { CoreUserSession } from '../../types/core.types.js';
|
|
3
|
+
import type { WorkflowRunWire } from './workflow.types.js';
|
|
4
|
+
export declare class WorkflowRunForbiddenError extends ForbiddenError {
|
|
5
|
+
constructor();
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A run started through a session records that session's user as its owner, and
|
|
9
|
+
* only that user may read it. Who may *answer* a run's approval gates is the
|
|
10
|
+
* gate's own declaration — see `approvers` on `workflow.approval()`.
|
|
11
|
+
*
|
|
12
|
+
* A run with no recorded owner — started by a trigger, a scheduler, or a route
|
|
13
|
+
* wired without auth — has nobody to compare a caller against, so ownership is
|
|
14
|
+
* not a control that exists for it. Gate those with `auth` or `permissions` on
|
|
15
|
+
* the entrypoint instead.
|
|
16
|
+
*/
|
|
17
|
+
export declare const assertWorkflowRunOwner: (wire: WorkflowRunWire | undefined, session: CoreUserSession | undefined) => void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ForbiddenError } from '../../errors/errors.js';
|
|
2
|
+
import { addError } from '../../errors/error-handler.js';
|
|
3
|
+
export class WorkflowRunForbiddenError extends ForbiddenError {
|
|
4
|
+
constructor() {
|
|
5
|
+
super('Not authorized to access this workflow run');
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
addError(WorkflowRunForbiddenError, {
|
|
9
|
+
status: 403,
|
|
10
|
+
message: 'Not authorized to access this workflow run.',
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* A run started through a session records that session's user as its owner, and
|
|
14
|
+
* only that user may read it. Who may *answer* a run's approval gates is the
|
|
15
|
+
* gate's own declaration — see `approvers` on `workflow.approval()`.
|
|
16
|
+
*
|
|
17
|
+
* A run with no recorded owner — started by a trigger, a scheduler, or a route
|
|
18
|
+
* wired without auth — has nobody to compare a caller against, so ownership is
|
|
19
|
+
* not a control that exists for it. Gate those with `auth` or `permissions` on
|
|
20
|
+
* the entrypoint instead.
|
|
21
|
+
*/
|
|
22
|
+
export const assertWorkflowRunOwner = (wire, session) => {
|
|
23
|
+
const owner = wire?.pikkuUserId;
|
|
24
|
+
if (!owner) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!session?.userId || session.userId !== owner) {
|
|
28
|
+
throw new WorkflowRunForbiddenError();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ApprovalStore } from './workflow-approval.js';
|
|
2
|
+
/** The durable step name a suspension point is recorded under. */
|
|
3
|
+
export declare const suspendStepNameFor: (reason: string) => string;
|
|
4
|
+
/** What the suspend gate needs from the workflow service. */
|
|
5
|
+
export type SuspendStore = Pick<ApprovalStore, 'getStepState' | 'insertStepState' | 'setStepRunning' | 'setStepResult'>;
|
|
6
|
+
/**
|
|
7
|
+
* Record a suspension point and unwind the run.
|
|
8
|
+
*
|
|
9
|
+
* A suspension that has already succeeded returns instead of throwing, so a
|
|
10
|
+
* replay walks past a gate the run has already passed through.
|
|
11
|
+
*/
|
|
12
|
+
export declare const recordSuspension: (store: SuspendStore, runId: string, reason: string, stepName: string, fromStepName: string | undefined) => Promise<void>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { WorkflowSuspendedException } from './workflow-errors.js';
|
|
2
|
+
/** The durable step name a suspension point is recorded under. */
|
|
3
|
+
export const suspendStepNameFor = (reason) => `__workflow_suspend:${reason}`;
|
|
4
|
+
/**
|
|
5
|
+
* Record a suspension point and unwind the run.
|
|
6
|
+
*
|
|
7
|
+
* A suspension that has already succeeded returns instead of throwing, so a
|
|
8
|
+
* replay walks past a gate the run has already passed through.
|
|
9
|
+
*/
|
|
10
|
+
export const recordSuspension = async (store, runId, reason, stepName, fromStepName) => {
|
|
11
|
+
const insert = () => store.insertStepState(runId, stepName, 'pikkuWorkflowSuspend', { reason }, undefined, fromStepName);
|
|
12
|
+
let stepState;
|
|
13
|
+
try {
|
|
14
|
+
stepState = await store.getStepState(runId, stepName);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
stepState = await insert();
|
|
18
|
+
}
|
|
19
|
+
if (!stepState.stepId) {
|
|
20
|
+
stepState = await insert();
|
|
21
|
+
}
|
|
22
|
+
if (stepState.status === 'succeeded') {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (stepState.status === 'pending') {
|
|
26
|
+
await store.setStepRunning(stepState.stepId);
|
|
27
|
+
}
|
|
28
|
+
await store.setStepResult(stepState.stepId, {
|
|
29
|
+
reason,
|
|
30
|
+
suspendedAt: new Date().toISOString(),
|
|
31
|
+
});
|
|
32
|
+
throw new WorkflowSuspendedException(runId, reason);
|
|
33
|
+
};
|
|
@@ -2,7 +2,7 @@ import type { SerializedError, CommonWireMeta } from '../../types/core.types.js'
|
|
|
2
2
|
import type { CorePikkuFunctionConfig, CorePikkuFunctionHook } from '../../function/functions.types.js';
|
|
3
3
|
import type { GroupConcurrencyConfig } from '../queue/queue.types.js';
|
|
4
4
|
export type { WorkflowService } from '../../services/workflow-service.js';
|
|
5
|
-
export type { WorkflowStepOptions,
|
|
5
|
+
export type { WorkflowStepOptions, WorkflowWireDoRPC, WorkflowApprovalOptions, ApprovalOutcome, InputSource, OutputBinding, RpcStepMeta, Condition, BranchStepMeta, ParallelGroupStepMeta, FanoutStepMeta, ReturnStepMeta, InlineStepMeta, SleepStepMeta, CancelStepMeta, SuspendStepMeta, ApprovalStepMeta, SetStepMeta, SwitchCaseMeta, SwitchStepMeta, FilterStepMeta, ArrayPredicateStepMeta, ScenarioStepInvocation, ScenarioStepMeta, WorkflowStepMeta, WorkflowStepWire, PikkuWorkflowWire, PikkuScenarioWire, } from './dsl/workflow-dsl.types.js';
|
|
6
6
|
export type { ScenarioStepPhase, ScenarioStepOptions, PikkuScenarioStepWire, PikkuBrowserWire, ScenarioBrowserProvider, } from './scenario-step.types.js';
|
|
7
7
|
import type { WorkflowStepMeta } from './dsl/workflow-dsl.types.js';
|
|
8
8
|
export interface WorkflowRunWire {
|
|
@@ -49,6 +49,13 @@ export interface WorkflowRun {
|
|
|
49
49
|
export interface StepState {
|
|
50
50
|
stepId: string;
|
|
51
51
|
status: StepStatus;
|
|
52
|
+
/**
|
|
53
|
+
* The function the workflow dispatched this step with, recorded so a worker
|
|
54
|
+
* can reject a queue message naming anything else. `null` is a step with no
|
|
55
|
+
* function of its own (inline work); `undefined` is a store that never
|
|
56
|
+
* recorded one, and cannot be compared against.
|
|
57
|
+
*/
|
|
58
|
+
rpcName?: string | null;
|
|
52
59
|
result?: any;
|
|
53
60
|
error?: SerializedError;
|
|
54
61
|
attemptCount: number;
|
package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A non-streaming agent run registers with aiRunState on the same terms as a streaming one
|
|
4
|
+
description: Otherwise interruptAIAgent finds the run, passes the ownership check, then cannot stop it — and reports that as if the run were on another host
|
|
5
|
+
tags: core, ai-agent
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A non-streaming agent run registers with `aiRunState` too
|
|
9
|
+
|
|
10
|
+
`runAIAgent` registers its run with `aiRunState` exactly as `streamAIAgent`
|
|
11
|
+
does, even though nothing is streaming and there is no channel to interrupt.
|
|
12
|
+
|
|
13
|
+
`interruptAIAgent` resolves a run through `aiRunState` first, checks ownership,
|
|
14
|
+
then looks for a local abort handle. A run that skipped registration is invisible
|
|
15
|
+
at the first step. A run that registered but has no handle is visible, passes the
|
|
16
|
+
ownership check, and then cannot be stopped — which the interrupt path reports as
|
|
17
|
+
"running on another instance". That message would be wrong and actively
|
|
18
|
+
misleading: the run is right here, and the deployment is single-instance.
|
|
19
|
+
|
|
20
|
+
**What this rules out:** treating registration as a streaming concern. It is an
|
|
21
|
+
addressability concern, and the two paths have to be addressable the same way for
|
|
22
|
+
the interrupt path's diagnosis to mean anything.
|
package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A resumed turn is as interruptible as the first one
|
|
4
|
+
description: It is the same person listening to the same voice, and after an approval it is where most of the reply actually gets spoken
|
|
5
|
+
tags: core, ai-agent
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A resumed turn is as interruptible as the first one
|
|
9
|
+
|
|
10
|
+
Resuming after an approval registers for interruption on the same terms as the
|
|
11
|
+
original turn.
|
|
12
|
+
|
|
13
|
+
It is the same person listening to the same voice, so the reason to allow
|
|
14
|
+
interruption has not changed. And after an approval is precisely where most of
|
|
15
|
+
the reply gets spoken: an approved delete is followed by the agent describing
|
|
16
|
+
what it did, which is a normal thing for a listener to talk over.
|
|
17
|
+
|
|
18
|
+
**What this rules out:** treating the resume as a short continuation not worth
|
|
19
|
+
wiring for interruption. By volume of speech it is usually the larger half of
|
|
20
|
+
the exchange.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A scenario step's prose template is offered to a virtual user unfilled
|
|
4
|
+
description: A reporter fills placeholders from a run that happened; there is no run yet, and the filled form would answer the question the user is there to answer
|
|
5
|
+
tags: core, virtual-user
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A scenario step's prose template is offered unfilled
|
|
9
|
+
|
|
10
|
+
When a scenario step becomes a catalogue entry, its prose template goes in with
|
|
11
|
+
its placeholders intact — braces and all.
|
|
12
|
+
|
|
13
|
+
A reporter fills those placeholders from a run that already happened. There is
|
|
14
|
+
no run yet at derivation time, so there is nothing to fill them from. More
|
|
15
|
+
importantly, filling them would answer the wrong question: "invites {email}"
|
|
16
|
+
tells the user to choose someone, where "invites ada@example.com" tells it whom
|
|
17
|
+
— and *whom* is the scenario author's answer, not something the virtual user
|
|
18
|
+
worked out.
|
|
19
|
+
|
|
20
|
+
**What this rules out:** substituting example or fixture values to make the
|
|
21
|
+
catalogue read more naturally. It reads better and tests less.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A virtual user decides whether to trust its notes once per turn, by one roll
|
|
4
|
+
description: The difference between the stale, newcomer and auditor dispositions is expressed as a single probability rather than as prose in each prompt
|
|
5
|
+
tags: core, virtual-user
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A virtual user decides whether to trust its notes once per turn
|
|
9
|
+
|
|
10
|
+
Each turn the run makes one weighted decision: does this user act on what it
|
|
11
|
+
already recorded, or go and look again.
|
|
12
|
+
|
|
13
|
+
That single roll is where the dispositions differ. `stale` almost always trusts
|
|
14
|
+
its notes — that is what makes it stale. A `newcomer` has none to trust. An
|
|
15
|
+
`auditor` re-checks nearly everything, which is the entire point of an auditor.
|
|
16
|
+
Expressing it as one probability keeps the difference between those runs in one
|
|
17
|
+
readable place instead of spread through three prompts as English that drifts.
|
|
18
|
+
|
|
19
|
+
**What this rules out:** encoding "you are suspicious of your own notes" into
|
|
20
|
+
each disposition's prompt text, where the behaviour becomes a property of how
|
|
21
|
+
the model reads prose rather than something the run controls and can report.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A wall-clock threshold is a load test in disguise
|
|
4
|
+
description: The KEK derivation test asserted a fixed 50ms budget for work that took 10ms, which went red about one run in five once the suite was large enough to compete for the machine
|
|
5
|
+
tags: core, testing, crypto
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A wall-clock threshold is a load test in disguise
|
|
9
|
+
|
|
10
|
+
`crypto-utils.test.ts` guards a real property: unwrapping N secrets must not
|
|
11
|
+
cost N KEK derivations. Deriving a KEK is a deliberately slow KDF, so doing it
|
|
12
|
+
per secret turns a 10ms operation into a 4-second one.
|
|
13
|
+
|
|
14
|
+
It guarded it with `assert.ok(elapsed < 50)`. Measured on this machine:
|
|
15
|
+
|
|
16
|
+
| | |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| one `deriveKEK` | ~86ms |
|
|
19
|
+
| 50 `envelopeDecrypt` | 10.8ms |
|
|
20
|
+
| the old 50ms threshold | 4.6x headroom |
|
|
21
|
+
| the failure it guards against | ~4281ms — a **396x** separation |
|
|
22
|
+
|
|
23
|
+
So the assertion had 4.6x of margin to detect a 396x regression. Everything
|
|
24
|
+
between those two numbers was noise, and once core's suite reached ~2000 tests
|
|
25
|
+
competing for the same cores, a 10ms window drifting past 50ms became routine:
|
|
26
|
+
roughly one run in five went red, always on a machine where nothing was wrong.
|
|
27
|
+
|
|
28
|
+
**Calibrate against a measurement taken in the same run.** The test already
|
|
29
|
+
derives a KEK, so timing that call is free, and the assertion becomes
|
|
30
|
+
`elapsed < oneDerivation` — unwrapping all fifty must cost less than deriving
|
|
31
|
+
once. Under load both sides slow down together, so the ratio holds.
|
|
32
|
+
|
|
33
|
+
**What this rules out:** raising the constant. 100ms or 200ms buys a smaller
|
|
34
|
+
flake rate and the same class of bug, and it drifts again the next time the
|
|
35
|
+
suite grows or CI moves to a noisier runner. Any assertion of the form
|
|
36
|
+
"operation X takes less than N milliseconds" has this problem; express it as a
|
|
37
|
+
ratio against something measured alongside it.
|
|
38
|
+
|
|
39
|
+
Worth knowing: `envelopeDecrypt(kek: CryptoKey, …)` takes the derived key as a
|
|
40
|
+
parameter, so it *cannot* derive one — the property is already enforced by the
|
|
41
|
+
signature, and [[the-api-report-pins-members-not-just-names]] would catch a
|
|
42
|
+
change to it. The test is defence in depth, which is a reason to make it cheap
|
|
43
|
+
and quiet rather than to delete it.
|
|
44
|
+
|
|
45
|
+
Related: [[an-unref-d-timer-cannot-be-awaited-under-node-test]], the other
|
|
46
|
+
source of nondeterminism found in the same pass.
|