@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
|
@@ -1629,6 +1629,7 @@ describe('resumeAIAgent', () => {
|
|
|
1629
1629
|
getRun: async () => remainingRun,
|
|
1630
1630
|
resolveApproval: async (toolCallId: string, status: string) => {
|
|
1631
1631
|
resolveCalls.push({ toolCallId, status })
|
|
1632
|
+
return true
|
|
1632
1633
|
},
|
|
1633
1634
|
updateRun: async (_runId: string, patch: any) => {
|
|
1634
1635
|
updates.push(patch)
|
|
@@ -1750,6 +1751,7 @@ describe('resumeAIAgent', () => {
|
|
|
1750
1751
|
},
|
|
1751
1752
|
resolveApproval: async (toolCallId: string, status: string) => {
|
|
1752
1753
|
resolveCalls.push({ toolCallId, status })
|
|
1754
|
+
return true
|
|
1753
1755
|
},
|
|
1754
1756
|
updateRun: async (_runId: string, patch: any) => {
|
|
1755
1757
|
updates.push(patch)
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
AIAgentMemoryConfig,
|
|
11
11
|
} from './ai-agent.types.js'
|
|
12
12
|
import { pikkuState, getSingletonServices } from '../../pikku-state.js'
|
|
13
|
+
import { applyInputMiddleware } from './ai-agent-turn.js'
|
|
13
14
|
import { AIProviderNotConfiguredError } from '../../errors/errors.js'
|
|
14
15
|
import { randomUUID } from './ai-agent-utils.js'
|
|
15
16
|
import { SPOKEN_TRANSCRIPT } from './voice-input.js'
|
|
@@ -676,38 +677,26 @@ export async function streamAIAgent(
|
|
|
676
677
|
// One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
|
|
677
678
|
const sharedNotes: Record<string, unknown> = {}
|
|
678
679
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
messages:
|
|
685
|
-
instructions:
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
modifiedInstructions = result.instructions
|
|
690
|
-
}
|
|
691
|
-
}
|
|
680
|
+
const { messages: modifiedMessages, instructions: modifiedInstructions } =
|
|
681
|
+
await applyInputMiddleware(
|
|
682
|
+
aiMiddlewares,
|
|
683
|
+
singletonServices,
|
|
684
|
+
{
|
|
685
|
+
messages: runnerParams.messages,
|
|
686
|
+
instructions: runnerParams.instructions,
|
|
687
|
+
},
|
|
688
|
+
sharedNotes
|
|
689
|
+
)
|
|
692
690
|
runnerParams.messages = modifiedMessages
|
|
693
691
|
runnerParams.instructions = modifiedInstructions
|
|
694
692
|
|
|
695
|
-
//
|
|
696
|
-
// has no idea what it said; until this arrives its own message is a blank
|
|
697
|
-
// bubble. Ahead of the run rather than alongside it because the answer starts
|
|
698
|
-
// streaming within a few hundred milliseconds, and a question that appears
|
|
699
|
-
// after its answer reads as the wrong question.
|
|
693
|
+
// knowledge: decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md
|
|
700
694
|
const transcript = sharedNotes[SPOKEN_TRANSCRIPT]
|
|
701
695
|
if (typeof transcript === 'string') {
|
|
702
696
|
channel.send({ type: 'transcript', text: transcript })
|
|
703
697
|
}
|
|
704
698
|
|
|
705
|
-
//
|
|
706
|
-
// spoken turn that is not what arrived over the wire: the wire carried a
|
|
707
|
-
// base64 audio blob, and persisting it would write megabytes of unreadable
|
|
708
|
-
// data into the history while losing the only readable record of the turn.
|
|
709
|
-
// Identity-checked rather than assumed — a middleware is free to rewrite the
|
|
710
|
-
// message list into something with no relation to the turn.
|
|
699
|
+
// knowledge: decisions/internals/thread-history-records-the-transcript-not-the-audio.md
|
|
711
700
|
const lastModified = modifiedMessages[modifiedMessages.length - 1]
|
|
712
701
|
const persistedUserMessage =
|
|
713
702
|
lastModified?.id === userMessage.id ? lastModified : userMessage
|
|
@@ -768,7 +757,7 @@ export async function streamAIAgent(
|
|
|
768
757
|
{
|
|
769
758
|
wireInheritedChannelMiddleware: meta?.channelMiddleware,
|
|
770
759
|
wireChannelMiddleware: [
|
|
771
|
-
...(
|
|
760
|
+
...(agent.channelMiddleware ?? []),
|
|
772
761
|
...streamMiddleware,
|
|
773
762
|
],
|
|
774
763
|
}
|
|
@@ -792,10 +781,7 @@ export async function streamAIAgent(
|
|
|
792
781
|
|
|
793
782
|
const credentialFilteredChannel: AIStreamChannel = {
|
|
794
783
|
...wrappedChannel,
|
|
795
|
-
//
|
|
796
|
-
// swallowing the promise here makes every `await channel.send(...)` upstream
|
|
797
|
-
// a no-op — including the one that waits for a buffering hook to flush
|
|
798
|
-
// before the channel closes.
|
|
784
|
+
// knowledge: decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md
|
|
799
785
|
send: (event: AIStreamEvent) => {
|
|
800
786
|
if (
|
|
801
787
|
event.type === 'tool-result' &&
|
|
@@ -872,11 +858,7 @@ export async function streamAIAgent(
|
|
|
872
858
|
runId
|
|
873
859
|
)
|
|
874
860
|
|
|
875
|
-
//
|
|
876
|
-
// `done` is the only signal a stream hook gets that the reply is over, and
|
|
877
|
-
// the ones that buffer need it: `voiceOutput` speaks a trailing fragment
|
|
878
|
-
// that never reached a full stop, and waits for audio it has already paid
|
|
879
|
-
// to synthesize. Sent raw, that work is discarded by the `close()` below.
|
|
861
|
+
// knowledge: decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md
|
|
880
862
|
await outputChannel.send({ type: 'done' })
|
|
881
863
|
channel.close()
|
|
882
864
|
return persistingChannel.fullText
|
|
@@ -979,12 +961,7 @@ export async function interruptAIAgent(
|
|
|
979
961
|
reason: input.reason ?? 'user',
|
|
980
962
|
})
|
|
981
963
|
|
|
982
|
-
//
|
|
983
|
-
// another instance. Returning a bare `false` there is indistinguishable from
|
|
984
|
-
// "it already finished", so the one deployment shape this registry does not
|
|
985
|
-
// cover fails silently — an agent that simply refuses to shut up, with nothing
|
|
986
|
-
// in the logs. Say so instead. See `signalRunInterrupt` for the fix (fan the
|
|
987
|
-
// interrupt out over eventHub so every instance tries locally).
|
|
964
|
+
// knowledge: decisions/internals/an-agent-run-owned-by-another-instance-says-so.md
|
|
988
965
|
if (!stopped && run.status === 'running') {
|
|
989
966
|
logger?.warn(
|
|
990
967
|
`Could not interrupt run ${input.runId}: it is running in another process. ` +
|
|
@@ -1037,10 +1014,18 @@ export async function resumeAIAgent(
|
|
|
1037
1014
|
|
|
1038
1015
|
await assertAgentAuthorized(agent, params, packageName)
|
|
1039
1016
|
|
|
1040
|
-
|
|
1017
|
+
// The read above is not a claim — concurrent resumes all see the same pending
|
|
1018
|
+
// approval. `resolveApproval` is the claim, and the loser must not go on to
|
|
1019
|
+
// run the tool a second time.
|
|
1020
|
+
const claimed = await aiRunState.resolveApproval(
|
|
1041
1021
|
input.toolCallId,
|
|
1042
1022
|
input.approved ? 'approved' : 'denied'
|
|
1043
1023
|
)
|
|
1024
|
+
if (!claimed) {
|
|
1025
|
+
throw new Error(
|
|
1026
|
+
`Approval for toolCallId ${input.toolCallId} was already resolved by another caller`
|
|
1027
|
+
)
|
|
1028
|
+
}
|
|
1044
1029
|
|
|
1045
1030
|
const { storage } = resolveMemoryServices(agent, singletonServices)
|
|
1046
1031
|
const memoryConfig = agent.memory
|
|
@@ -1317,24 +1302,15 @@ async function continueAfterToolResult(
|
|
|
1317
1302
|
// One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
|
|
1318
1303
|
const sharedNotes: Record<string, unknown> = {}
|
|
1319
1304
|
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
shared: sharedNotes,
|
|
1328
|
-
})
|
|
1329
|
-
modifiedMessages = result.messages
|
|
1330
|
-
modifiedInstructions = result.instructions
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1305
|
+
const { messages: modifiedMessages, instructions: modifiedInstructions } =
|
|
1306
|
+
await applyInputMiddleware(
|
|
1307
|
+
aiMiddlewares,
|
|
1308
|
+
singletonServices,
|
|
1309
|
+
{ messages: trimmedMessages, instructions: instructions },
|
|
1310
|
+
sharedNotes
|
|
1311
|
+
)
|
|
1333
1312
|
|
|
1334
|
-
//
|
|
1335
|
-
// listening to the same voice, and after an approval it is where most of the
|
|
1336
|
-
// reply actually gets spoken — an approved delete is followed by the agent
|
|
1337
|
-
// talking about it, and that is a normal thing to talk over.
|
|
1313
|
+
// knowledge: decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md
|
|
1338
1314
|
const interruptHandle = registerInterruptibleRun(run.runId)
|
|
1339
1315
|
|
|
1340
1316
|
const streamMiddleware = aiMiddlewares
|
|
@@ -1369,7 +1345,7 @@ async function continueAfterToolResult(
|
|
|
1369
1345
|
{
|
|
1370
1346
|
wireInheritedChannelMiddleware: meta?.channelMiddleware,
|
|
1371
1347
|
wireChannelMiddleware: [
|
|
1372
|
-
...(
|
|
1348
|
+
...(agent.channelMiddleware ?? []),
|
|
1373
1349
|
...streamMiddleware,
|
|
1374
1350
|
],
|
|
1375
1351
|
}
|
|
@@ -1467,10 +1443,7 @@ async function continueAfterToolResult(
|
|
|
1467
1443
|
run.runId
|
|
1468
1444
|
)
|
|
1469
1445
|
|
|
1470
|
-
//
|
|
1471
|
-
// matters more here. After an approval, most of what gets spoken is the
|
|
1472
|
-
// agent talking about what it just did, so this is the half of the reply a
|
|
1473
|
-
// dropped flush would silence.
|
|
1446
|
+
// knowledge: decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md
|
|
1474
1447
|
await wrappedChannel.send({ type: 'done' })
|
|
1475
1448
|
channel.close()
|
|
1476
1449
|
} catch (err) {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { CoreSingletonServices } from '../../types/core.types.js'
|
|
2
|
+
import type { PikkuAIMiddlewareHooks } from './ai-agent.types.js'
|
|
3
|
+
|
|
4
|
+
/** Exactly what `modifyInput` accepts and returns, taken from the hook itself. */
|
|
5
|
+
type TurnInput = Pick<
|
|
6
|
+
Parameters<NonNullable<PikkuAIMiddlewareHooks['modifyInput']>>[1],
|
|
7
|
+
'messages' | 'instructions'
|
|
8
|
+
>
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Run every `modifyInput` hook in order, threading each result into the next.
|
|
12
|
+
*
|
|
13
|
+
* Shared by the four places a turn starts — first turn and resume, on both the
|
|
14
|
+
* streaming and non-streaming paths. `sharedNotes` is one bag per run that the
|
|
15
|
+
* caller keeps, because hooks use it to pass things forward (`voiceInput`
|
|
16
|
+
* leaves the transcript there for `voiceOutput`).
|
|
17
|
+
*/
|
|
18
|
+
export const applyInputMiddleware = async (
|
|
19
|
+
aiMiddlewares: PikkuAIMiddlewareHooks[],
|
|
20
|
+
singletonServices: CoreSingletonServices,
|
|
21
|
+
input: TurnInput,
|
|
22
|
+
sharedNotes: Record<string, unknown>
|
|
23
|
+
): Promise<TurnInput> => {
|
|
24
|
+
let { messages, instructions } = input
|
|
25
|
+
for (const mw of aiMiddlewares) {
|
|
26
|
+
if (!mw.modifyInput) continue
|
|
27
|
+
const result = await mw.modifyInput(singletonServices, {
|
|
28
|
+
messages,
|
|
29
|
+
instructions,
|
|
30
|
+
shared: sharedNotes,
|
|
31
|
+
})
|
|
32
|
+
messages = result.messages
|
|
33
|
+
instructions = result.instructions
|
|
34
|
+
}
|
|
35
|
+
return { messages, instructions }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The step fields both agent paths hand to `afterStep`, taken from the hook. */
|
|
39
|
+
type StepResult = Omit<
|
|
40
|
+
Parameters<NonNullable<PikkuAIMiddlewareHooks['afterStep']>>[1],
|
|
41
|
+
'stepNumber'
|
|
42
|
+
>
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Notify every `afterStep` hook. The payload is the step verbatim plus its
|
|
46
|
+
* ordinal — a hook that wants the run's totals accumulates them itself.
|
|
47
|
+
*/
|
|
48
|
+
export const notifyAfterStep = async (
|
|
49
|
+
aiMiddlewares: PikkuAIMiddlewareHooks[],
|
|
50
|
+
singletonServices: CoreSingletonServices,
|
|
51
|
+
stepNumber: number,
|
|
52
|
+
stepResult: StepResult
|
|
53
|
+
): Promise<void> => {
|
|
54
|
+
for (const mw of aiMiddlewares) {
|
|
55
|
+
if (!mw.afterStep) continue
|
|
56
|
+
await mw.afterStep(singletonServices, {
|
|
57
|
+
stepNumber,
|
|
58
|
+
text: stepResult.text,
|
|
59
|
+
toolCalls: stepResult.toolCalls,
|
|
60
|
+
toolResults: stepResult.toolResults,
|
|
61
|
+
usage: stepResult.usage,
|
|
62
|
+
finishReason: stepResult.finishReason,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The step shape kept on the run: each tool call paired with its result.
|
|
69
|
+
*
|
|
70
|
+
* A result is stringified unless it already is a string, because this is what
|
|
71
|
+
* gets persisted and read back as history — an object there is unreadable.
|
|
72
|
+
*/
|
|
73
|
+
export const toAccumulatedStep = (stepResult: StepResult) => ({
|
|
74
|
+
usage: stepResult.usage,
|
|
75
|
+
toolCalls: stepResult.toolCalls.map((tc) => {
|
|
76
|
+
const tr = stepResult.toolResults.find(
|
|
77
|
+
(r) => r.toolCallId === tc.toolCallId
|
|
78
|
+
)
|
|
79
|
+
return {
|
|
80
|
+
name: tc.toolName,
|
|
81
|
+
args: tc.args as Record<string, unknown>,
|
|
82
|
+
result:
|
|
83
|
+
typeof tr?.result === 'string'
|
|
84
|
+
? tr.result
|
|
85
|
+
: JSON.stringify(tr?.result ?? ''),
|
|
86
|
+
}
|
|
87
|
+
}),
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
type PendingApproval = {
|
|
91
|
+
toolName: string
|
|
92
|
+
args: unknown
|
|
93
|
+
reason?: string
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type ToolDef = {
|
|
97
|
+
name: string
|
|
98
|
+
approvalDescriptionFn?: (args: any) => Promise<string> | string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Fill in each approval's human-readable reason from the tool's own
|
|
103
|
+
* `approvalDescriptionFn`, where it did not supply one.
|
|
104
|
+
*
|
|
105
|
+
* A description that throws is swallowed: the approval still has to be raised,
|
|
106
|
+
* and a gate that disappears because its label failed to render is the worst
|
|
107
|
+
* available outcome.
|
|
108
|
+
*/
|
|
109
|
+
export const describeApprovals = async (
|
|
110
|
+
approvals: PendingApproval[],
|
|
111
|
+
tools: ToolDef[]
|
|
112
|
+
): Promise<void> => {
|
|
113
|
+
for (const approval of approvals) {
|
|
114
|
+
if (approval.reason) continue
|
|
115
|
+
const toolDef = tools.find((t) => t.name === approval.toolName)
|
|
116
|
+
if (!toolDef?.approvalDescriptionFn) continue
|
|
117
|
+
try {
|
|
118
|
+
approval.reason = await toolDef.approvalDescriptionFn(approval.args)
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -164,12 +164,7 @@ export const voiceInput = (config?: {
|
|
|
164
164
|
// to send, and a message with no content is not a question.
|
|
165
165
|
if (updatedContent.length === 0) throw new NoSpeechDetectedError()
|
|
166
166
|
|
|
167
|
-
//
|
|
168
|
-
// reads as non-speech and still have content — an image with a silent
|
|
169
|
-
// caption clip — which leaves nothing above to throw. Recording `''` here
|
|
170
|
-
// would send a transcript event saying the user said nothing, and a client
|
|
171
|
-
// that tells "not transcribed yet" from "transcribed" by the key being
|
|
172
|
-
// absent would render the turn as an empty bubble rather than a pending one.
|
|
167
|
+
// knowledge: decisions/internals/an-empty-transcript-is-not-recorded.md
|
|
173
168
|
if (heard.length > 0) {
|
|
174
169
|
shared[SPOKEN_TRANSCRIPT] = heard.join(' ')
|
|
175
170
|
}
|
|
@@ -206,11 +206,7 @@ export const voiceOutput = (config?: {
|
|
|
206
206
|
}
|
|
207
207
|
if (!aiAgentRunner?.generateSpeech) return event
|
|
208
208
|
|
|
209
|
-
//
|
|
210
|
-
// every turn it handles, so `false` means a real user really typed; the
|
|
211
|
-
// key being absent means nothing reported either way — no voice input is
|
|
212
|
-
// wired — and that caller's replies are spoken exactly as they were before
|
|
213
|
-
// this option existed.
|
|
209
|
+
// knowledge: decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md
|
|
214
210
|
if (!config?.always && shared[SPOKEN_TURN] === false) return event
|
|
215
211
|
|
|
216
212
|
/**
|
|
@@ -228,13 +224,7 @@ export const voiceOutput = (config?: {
|
|
|
228
224
|
)
|
|
229
225
|
}
|
|
230
226
|
|
|
231
|
-
//
|
|
232
|
-
// should still speak the half it can, and repeating the notice for
|
|
233
|
-
// every sentence of a long one would bury the reply itself.
|
|
234
|
-
//
|
|
235
|
-
// Per sentence is also what makes the voice right. A reply that answers
|
|
236
|
-
// in English and then quotes a Chinese title is two sentences in two
|
|
237
|
-
// scripts, and each is synthesized in the voice its own script needs.
|
|
227
|
+
// knowledge: decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md
|
|
238
228
|
let voice = config.voice
|
|
239
229
|
if (config.speakableScripts) {
|
|
240
230
|
const unspeakable = unspeakableScripts(text, config.speakableScripts)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CorePikkuChannelMiddleware } from './channel.types.js'
|
|
1
2
|
import type {
|
|
2
3
|
CoreSingletonServices,
|
|
3
4
|
CorePikkuMiddleware,
|
|
@@ -61,7 +62,9 @@ export const runChannelLifecycleWithMiddleware = async ({
|
|
|
61
62
|
`${channelConfig.name}:${lifecycleType}:cm`,
|
|
62
63
|
{
|
|
63
64
|
wireInheritedChannelMiddleware: channelMiddlewareMeta,
|
|
64
|
-
|
|
65
|
+
// knowledge: questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md
|
|
66
|
+
wireChannelMiddleware:
|
|
67
|
+
channelConfig.channelMiddleware as CorePikkuChannelMiddleware[],
|
|
65
68
|
}
|
|
66
69
|
)
|
|
67
70
|
|
|
@@ -76,7 +79,7 @@ export const runChannelLifecycleWithMiddleware = async ({
|
|
|
76
79
|
const runLifecycle = async () => {
|
|
77
80
|
return await runPikkuFunc('channel', channelConfig.name, meta.pikkuFuncId, {
|
|
78
81
|
singletonServices: services,
|
|
79
|
-
data: () => data
|
|
82
|
+
data: () => data,
|
|
80
83
|
wire,
|
|
81
84
|
sessionService: userSession,
|
|
82
85
|
tags: meta.tags ?? [],
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { pikkuState, resetPikkuState } from '../../pikku-state.js'
|
|
5
|
+
import { wireChannel } from './channel-runner.js'
|
|
6
|
+
|
|
7
|
+
const registeredFunc = (funcId: string) =>
|
|
8
|
+
pikkuState(null, 'function', 'functions').get(funcId)
|
|
9
|
+
|
|
10
|
+
const wire = (channel: any) => {
|
|
11
|
+
resetPikkuState()
|
|
12
|
+
pikkuState(null, 'channel', 'meta', {
|
|
13
|
+
shapes: {
|
|
14
|
+
name: 'shapes',
|
|
15
|
+
route: '/shapes',
|
|
16
|
+
input: null,
|
|
17
|
+
connect: { pikkuFuncId: 'shapes__connect' },
|
|
18
|
+
disconnect: { pikkuFuncId: 'shapes__disconnect' },
|
|
19
|
+
message: { pikkuFuncId: 'shapes__message' },
|
|
20
|
+
messageWirings: {},
|
|
21
|
+
},
|
|
22
|
+
} as any)
|
|
23
|
+
wireChannel({ name: 'shapes', route: '/shapes', ...channel })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* knowledge: decisions/internals/channel-message-handlers-accept-three-config-shapes.md
|
|
28
|
+
*/
|
|
29
|
+
describe('every channel handler shape registers a callable function config', () => {
|
|
30
|
+
const handler = async () => undefined
|
|
31
|
+
|
|
32
|
+
test('a direct function config registers as-is', () => {
|
|
33
|
+
wire({ onConnect: { func: handler } })
|
|
34
|
+
|
|
35
|
+
assert.equal(typeof registeredFunc('shapes__connect')?.func, 'function')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('a wrapper around a function config registers the inner config', () => {
|
|
39
|
+
wire({ onConnect: { func: { func: handler }, middleware: [] } })
|
|
40
|
+
|
|
41
|
+
// Registering the wrapper itself leaves `.func` an object, and the function
|
|
42
|
+
// runner calls `.func(...)` — that is a TypeError at connect time.
|
|
43
|
+
assert.equal(
|
|
44
|
+
typeof registeredFunc('shapes__connect')?.func,
|
|
45
|
+
'function',
|
|
46
|
+
'the wrapper was registered instead of the config it wraps'
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('onDisconnect unwraps the same way', () => {
|
|
51
|
+
wire({ onDisconnect: { func: { func: handler }, middleware: [] } })
|
|
52
|
+
|
|
53
|
+
assert.equal(typeof registeredFunc('shapes__disconnect')?.func, 'function')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('onMessage unwraps the same way', () => {
|
|
57
|
+
wire({ onMessage: { func: { func: handler } } })
|
|
58
|
+
|
|
59
|
+
assert.equal(typeof registeredFunc('shapes__message')?.func, 'function')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('a simple wrapper — a function plus sibling middleware — stays whole', () => {
|
|
63
|
+
wire({ onConnect: { func: handler, middleware: [] } })
|
|
64
|
+
|
|
65
|
+
const registered = registeredFunc('shapes__connect')
|
|
66
|
+
assert.equal(typeof registered?.func, 'function')
|
|
67
|
+
assert.ok(
|
|
68
|
+
'middleware' in (registered as object),
|
|
69
|
+
'the sibling middleware must survive registration'
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
CoreUserSession,
|
|
5
5
|
} from '../../types/core.types.js'
|
|
6
6
|
import type {
|
|
7
|
+
CorePikkuChannelMiddleware,
|
|
7
8
|
BinaryData,
|
|
8
9
|
ChannelMessageMeta,
|
|
9
10
|
CoreChannel,
|
|
@@ -94,7 +95,7 @@ export const processMessageHandlers = (
|
|
|
94
95
|
logger.error(
|
|
95
96
|
`Channel ${channelConfig.name} with id ${channelHandler.getChannel().channelId} requires a session for ${routeMessage}. No session is attached to this websocket connection. Ensure auth middleware establishes a session during websocket upgrade, and configure sessionStore when the runtime needs persisted sessions.`
|
|
96
97
|
)
|
|
97
|
-
//
|
|
98
|
+
// knowledge: questions/unauthorized-channel-replies-escape-the-declared-out-type.md
|
|
98
99
|
channelHandler.getChannel().send(`Unauthorized for ${routeMessage}`)
|
|
99
100
|
return
|
|
100
101
|
}
|
|
@@ -111,10 +112,7 @@ export const processMessageHandlers = (
|
|
|
111
112
|
// Get wire middleware: channel-level middleware + message-specific middleware
|
|
112
113
|
const channelWireMiddleware = channelConfig.middleware || []
|
|
113
114
|
|
|
114
|
-
//
|
|
115
|
-
// - Direct config: onMessage.func is a plain Function
|
|
116
|
-
// - Wrapper: onMessage.func is a CorePikkuFunctionConfig (has onMessage.func.func)
|
|
117
|
-
// - Simple wrapper: onMessage has both func (plain Function) and middleware properties
|
|
115
|
+
// knowledge: decisions/internals/channel-message-handlers-accept-three-config-shapes.md
|
|
118
116
|
const isWrapper =
|
|
119
117
|
onMessage &&
|
|
120
118
|
typeof onMessage === 'object' &&
|
|
@@ -145,7 +143,9 @@ export const processMessageHandlers = (
|
|
|
145
143
|
inheritedMiddleware,
|
|
146
144
|
wireMiddleware,
|
|
147
145
|
inheritedChannelMiddleware: channelMeta?.channelMiddleware,
|
|
148
|
-
|
|
146
|
+
// knowledge: questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md
|
|
147
|
+
wireChannelMiddleware:
|
|
148
|
+
wireChannelMiddleware as CorePikkuChannelMiddleware[],
|
|
149
149
|
coerceDataFromSchema: true,
|
|
150
150
|
tags: channelConfig.tags,
|
|
151
151
|
sessionService: userSession,
|
|
@@ -191,7 +191,7 @@ export const processMessageHandlers = (
|
|
|
191
191
|
|
|
192
192
|
processed = true
|
|
193
193
|
const routeResult = await processMessage(
|
|
194
|
-
data
|
|
194
|
+
data,
|
|
195
195
|
routes[routerValue],
|
|
196
196
|
routingProperty,
|
|
197
197
|
routerValue
|
|
@@ -10,20 +10,6 @@ import {
|
|
|
10
10
|
type ChannelRPCValidator,
|
|
11
11
|
} from './channel-rpc.types.js'
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* `remote` for channels that only ever flow one way — a server-sent stream, an
|
|
15
|
-
* agent's output, a local CLI. Nothing is listening for a request on them, so
|
|
16
|
-
* the call is refused rather than waiting out a timeout.
|
|
17
|
-
*/
|
|
18
|
-
export const unsupportedChannelRemote = async (
|
|
19
|
-
funcName: string
|
|
20
|
-
): Promise<never> => {
|
|
21
|
-
throw new ChannelRPCError(
|
|
22
|
-
`Cannot call "${funcName}" remotely: this channel has no peer that answers`,
|
|
23
|
-
'unsupported'
|
|
24
|
-
)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
13
|
/**
|
|
28
14
|
* A `DeploymentService` whose transport is an open channel rather than an
|
|
29
15
|
* address, so a server-side function can call back into a peer that has no
|
|
@@ -109,21 +109,42 @@ describe('ChannelRPCRegistry', () => {
|
|
|
109
109
|
})
|
|
110
110
|
})
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
/**
|
|
113
|
+
* The registry unrefs its timeout timer so an in-flight call can never hold
|
|
114
|
+
* a process open. That makes the timeout unawaitable in a test: when the
|
|
115
|
+
* pending call is the only thing left, the loop drains before the timer
|
|
116
|
+
* fires, and node:test cancels the file — taking every later test with it.
|
|
117
|
+
*
|
|
118
|
+
* Driving the clock by hand removes the dependency on real time entirely.
|
|
119
|
+
* `t.mock.timers` is scoped to the test and resets itself, so the tests that
|
|
120
|
+
* do want a real clock are unaffected.
|
|
121
|
+
*
|
|
122
|
+
* knowledge: decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md
|
|
123
|
+
*/
|
|
124
|
+
test('times out instead of hanging forever', async (t) => {
|
|
125
|
+
t.mock.timers.enable({ apis: ['setTimeout'] })
|
|
126
|
+
|
|
113
127
|
const registry = new ChannelRPCRegistry(10)
|
|
114
128
|
const call = registry.register()
|
|
115
129
|
|
|
116
|
-
|
|
130
|
+
const rejected = assert.rejects(call.promise, (e: ChannelRPCError) => {
|
|
117
131
|
assert.equal(e.reason, 'timeout')
|
|
118
132
|
return true
|
|
119
133
|
})
|
|
134
|
+
t.mock.timers.tick(10)
|
|
135
|
+
await rejected
|
|
136
|
+
|
|
120
137
|
assert.equal(registry.inFlight, 0, 'a timed out call is not leaked')
|
|
121
138
|
})
|
|
122
139
|
|
|
123
|
-
test('drops a late response for an already-timed-out call', async () => {
|
|
140
|
+
test('drops a late response for an already-timed-out call', async (t) => {
|
|
141
|
+
t.mock.timers.enable({ apis: ['setTimeout'] })
|
|
142
|
+
|
|
124
143
|
const registry = new ChannelRPCRegistry(10)
|
|
125
144
|
const call = registry.register()
|
|
126
|
-
|
|
145
|
+
const rejected = assert.rejects(call.promise)
|
|
146
|
+
t.mock.timers.tick(10)
|
|
147
|
+
await rejected
|
|
127
148
|
|
|
128
149
|
const settled = registry.settle({
|
|
129
150
|
action: CHANNEL_RPC_RESPONSE,
|
|
@@ -148,3 +148,17 @@ export type ChannelRPCValidator = (
|
|
|
148
148
|
funcName: string,
|
|
149
149
|
value: unknown
|
|
150
150
|
) => Promise<void> | void
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* `remote` for wires with no peer that answers — a server-sent stream, an
|
|
154
|
+
* agent's output, a local CLI. Nothing is listening for a request on them, so
|
|
155
|
+
* the call is refused rather than waiting out a timeout.
|
|
156
|
+
*/
|
|
157
|
+
export const unsupportedChannelRemote = async (
|
|
158
|
+
funcName: string
|
|
159
|
+
): Promise<never> => {
|
|
160
|
+
throw new ChannelRPCError(
|
|
161
|
+
`Cannot call "${funcName}" remotely: this channel has no peer that answers`,
|
|
162
|
+
'unsupported'
|
|
163
|
+
)
|
|
164
|
+
}
|