@pikku/core 0.12.78 → 0.12.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +234 -0
- package/dist/dev/hot-reload.js +1 -6
- package/dist/ecosystem.d.ts +27 -0
- package/dist/ecosystem.js +26 -0
- package/dist/errors/error-handler.js +4 -2
- package/dist/function/function-runner.js +20 -42
- package/dist/function/functions.types.d.ts +9 -5
- package/dist/index.d.ts +4 -9
- package/dist/index.js +3 -8
- package/dist/middleware/auth-cookie.d.ts +0 -4
- package/dist/middleware/auth-cookie.js +38 -2
- package/dist/middleware/cors.js +1 -0
- package/dist/middleware/index.d.ts +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/remote-auth.js +14 -3
- package/dist/permissions.d.ts +8 -10
- package/dist/permissions.js +0 -11
- package/dist/pikku-state.js +12 -5
- package/dist/schema.js +35 -1
- package/dist/services/ai-run-state-service.d.ts +7 -1
- package/dist/services/in-memory-ai-run-state-service.d.ts +1 -1
- package/dist/services/in-memory-ai-run-state-service.js +2 -1
- package/dist/services/in-memory-workflow-service.d.ts +10 -0
- package/dist/services/in-memory-workflow-service.js +25 -0
- package/dist/services/local-content-request-handler.d.ts +21 -0
- package/dist/services/local-content-request-handler.js +72 -53
- package/dist/services/local-content.d.ts +6 -0
- package/dist/services/local-content.js +14 -1
- package/dist/services/workflow-service.d.ts +4 -2
- package/dist/testing/service-tests/agent-run-service-tests.d.ts +10 -0
- package/dist/testing/service-tests/agent-run-service-tests.js +72 -0
- package/dist/testing/service-tests/ai-storage-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/ai-storage-service-tests.js +226 -0
- package/dist/testing/service-tests/channel-store-tests.d.ts +3 -0
- package/dist/testing/service-tests/channel-store-tests.js +72 -0
- package/dist/testing/service-tests/credential-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/credential-service-tests.js +109 -0
- package/dist/testing/service-tests/deployment-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/deployment-service-tests.js +21 -0
- package/dist/testing/service-tests/event-hub-store-tests.d.ts +3 -0
- package/dist/testing/service-tests/event-hub-store-tests.js +34 -0
- package/dist/testing/service-tests/secret-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/secret-service-tests.js +80 -0
- package/dist/testing/service-tests/session-store-tests.d.ts +3 -0
- package/dist/testing/service-tests/session-store-tests.js +43 -0
- package/dist/testing/service-tests/workflow-run-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/workflow-run-service-tests.js +42 -0
- package/dist/testing/service-tests/workflow-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/workflow-service-tests.js +150 -0
- package/dist/testing/service-tests.d.ts +6 -0
- package/dist/testing/service-tests.js +26 -791
- package/dist/types/core.types.d.ts +11 -1
- package/dist/types/state.types.d.ts +1 -1
- package/dist/utils/node-host-resolver.d.ts +12 -0
- package/dist/utils/node-host-resolver.js +16 -0
- package/dist/utils/safe-fetch.d.ts +18 -0
- package/dist/utils/safe-fetch.js +167 -29
- package/dist/wirings/actor-flow/run-conversation.js +1 -6
- package/dist/wirings/ai-agent/agent-rpc.d.ts +15 -0
- package/dist/wirings/ai-agent/agent-rpc.js +53 -0
- package/dist/wirings/ai-agent/ai-agent-agui.js +1 -5
- package/dist/wirings/ai-agent/ai-agent-memory.js +3 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +3 -9
- package/dist/wirings/ai-agent/ai-agent-runner.js +28 -107
- package/dist/wirings/ai-agent/ai-agent-stream.js +20 -61
- package/dist/wirings/ai-agent/ai-agent-turn.d.ts +56 -0
- package/dist/wirings/ai-agent/ai-agent-turn.js +81 -0
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +1 -1
- package/dist/wirings/ai-agent/voice-input.js +1 -6
- package/dist/wirings/ai-agent/voice-output.js +2 -12
- package/dist/wirings/channel/channel-common.js +1 -0
- package/dist/wirings/channel/channel-handler.js +3 -5
- package/dist/wirings/channel/channel-rpc-service.d.ts +0 -6
- package/dist/wirings/channel/channel-rpc-service.js +0 -8
- package/dist/wirings/channel/channel-rpc.types.d.ts +6 -0
- package/dist/wirings/channel/channel-rpc.types.js +8 -0
- package/dist/wirings/channel/channel-runner.d.ts +1 -3
- package/dist/wirings/channel/channel-runner.js +16 -8
- package/dist/wirings/channel/channel.types.d.ts +2 -0
- package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -0
- package/dist/wirings/cli/channel/cli-channel-runner.js +2 -0
- package/dist/wirings/cli/cli-runner.js +4 -2
- package/dist/wirings/cli/cli.types.d.ts +0 -8
- package/dist/wirings/cli/command-parser.js +13 -0
- package/dist/wirings/gateway/gateway-runner.js +41 -5
- package/dist/wirings/http/http-routes.js +2 -0
- package/dist/wirings/http/http-runner.d.ts +0 -10
- package/dist/wirings/http/http-runner.js +6 -13
- package/dist/wirings/http/http.types.d.ts +0 -10
- package/dist/wirings/http/index.d.ts +1 -1
- package/dist/wirings/http/index.js +1 -1
- package/dist/wirings/mcp/mcp-runner.d.ts +0 -7
- package/dist/wirings/mcp/mcp-runner.js +0 -6
- package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
- package/dist/wirings/rpc/rpc-runner.js +53 -86
- package/dist/wirings/rpc/rpc-types.d.ts +3 -0
- package/dist/wirings/secret/validate-secret-definitions.js +2 -0
- package/dist/wirings/trigger/pikku-trigger-service.d.ts +0 -4
- package/dist/wirings/trigger/trigger-runner.js +1 -0
- package/dist/wirings/virtual-user/run-virtual-user.js +11 -11
- package/dist/wirings/virtual-user/virtual-user-derive.js +4 -25
- package/dist/wirings/virtual-user/virtual-user-dispositions.js +1 -4
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +29 -2
- package/dist/wirings/workflow/feature.js +7 -4
- package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -2
- package/dist/wirings/workflow/graph/graph-runner.js +8 -7
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -4
- package/dist/wirings/workflow/index.d.ts +7 -2
- package/dist/wirings/workflow/index.js +5 -1
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
- package/dist/wirings/workflow/pikku-scenario-service.js +38 -10
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +44 -111
- package/dist/wirings/workflow/pikku-workflow-service.js +86 -404
- package/dist/wirings/workflow/workflow-approval.d.ts +39 -0
- package/dist/wirings/workflow/workflow-approval.js +114 -0
- package/dist/wirings/workflow/workflow-constants.d.ts +26 -0
- package/dist/wirings/workflow/workflow-constants.js +35 -0
- package/dist/wirings/workflow/workflow-errors.d.ts +58 -0
- package/dist/wirings/workflow/workflow-errors.js +112 -0
- package/dist/wirings/workflow/workflow-meta-resolver.d.ts +11 -0
- package/dist/wirings/workflow/workflow-meta-resolver.js +31 -0
- package/dist/wirings/workflow/workflow-queue-routing.d.ts +8 -0
- package/dist/wirings/workflow/workflow-queue-routing.js +38 -0
- package/dist/wirings/workflow/workflow-queue-wiring.d.ts +20 -0
- package/dist/wirings/workflow/workflow-queue-wiring.js +79 -0
- package/dist/wirings/workflow/workflow-recovery.d.ts +68 -0
- package/dist/wirings/workflow/workflow-recovery.js +101 -0
- package/dist/wirings/workflow/workflow-run-engine.types.d.ts +54 -0
- package/dist/wirings/workflow/workflow-run-engine.types.js +1 -0
- package/dist/wirings/workflow/workflow-run-ownership.d.ts +16 -0
- package/dist/wirings/workflow/workflow-run-ownership.js +29 -0
- package/dist/wirings/workflow/workflow-suspend.d.ts +12 -0
- package/dist/wirings/workflow/workflow-suspend.js +33 -0
- package/dist/wirings/workflow/workflow.types.d.ts +7 -0
- package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md +22 -0
- package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md +20 -0
- package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +21 -0
- package/knowledge/decisions/internals/a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md +21 -0
- package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +46 -0
- package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +45 -0
- package/knowledge/decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md +21 -0
- package/knowledge/decisions/internals/agent-speech-travels-as-a-custom-agui-event.md +22 -0
- package/knowledge/decisions/internals/an-agent-interrupt-is-not-a-failure.md +23 -0
- package/knowledge/decisions/internals/an-agent-run-owned-by-another-instance-says-so.md +25 -0
- package/knowledge/decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md +22 -0
- package/knowledge/decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md +20 -0
- package/knowledge/decisions/internals/an-empty-transcript-is-not-recorded.md +22 -0
- package/knowledge/decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md +66 -0
- package/knowledge/decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md +33 -0
- package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +43 -0
- package/knowledge/decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md +26 -0
- package/knowledge/decisions/internals/index.md +38 -26
- package/knowledge/decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md +21 -0
- package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +24 -0
- package/knowledge/decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md +34 -0
- package/knowledge/decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md +24 -0
- package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +22 -0
- package/knowledge/decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md +26 -0
- package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +43 -0
- package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +58 -0
- package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md +40 -0
- package/knowledge/decisions/internals/the-per-invocation-rpc-view-is-a-class.md +40 -0
- package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +28 -0
- package/knowledge/decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md +25 -0
- package/knowledge/decisions/internals/the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md +21 -0
- package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +22 -0
- package/knowledge/decisions/internals/thread-history-records-the-transcript-not-the-audio.md +25 -0
- package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +21 -0
- package/knowledge/decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md +24 -0
- package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +35 -0
- package/knowledge/decisions/security/a-dropped-audit-write-is-always-logged.md +4 -2
- package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md +33 -0
- package/knowledge/decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md +33 -0
- package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +37 -0
- package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +28 -0
- package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +34 -0
- package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
- package/knowledge/decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md +24 -0
- package/knowledge/decisions/security/gateway-handlers-run-through-the-function-runner-gate.md +13 -3
- package/knowledge/decisions/security/index.md +7 -0
- package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +35 -0
- package/knowledge/questions/index.md +2 -1
- package/knowledge/questions/unauthorized-channel-replies-escape-the-declared-out-type.md +44 -0
- package/package.json +14 -3
- package/scripts/generate-api-report.d.mts +1 -0
- package/scripts/generate-api-report.mjs +89 -0
- package/scripts/generate-api-report.mts +218 -0
- package/src/api-report.test.ts +69 -0
- package/src/column-form.ts +1 -4
- package/src/crypto-utils.test.ts +15 -2
- package/src/dev/hot-reload.ts +2 -7
- package/src/ecosystem.ts +40 -0
- package/src/errors/error-handler.ts +5 -3
- package/src/errors/error.test.ts +4 -1
- package/src/function/function-runner.test.ts +1 -1
- package/src/function/function-runner.ts +28 -39
- package/src/function/functions.types.ts +15 -9
- package/src/handle-error.ts +1 -1
- package/src/index.ts +3 -22
- package/src/middleware/auth-cookie.test.ts +50 -0
- package/src/middleware/auth-cookie.ts +38 -2
- package/src/middleware/cors.ts +2 -1
- package/src/middleware/index.ts +1 -1
- package/src/middleware/remote-auth.test.ts +43 -0
- package/src/middleware/remote-auth.ts +17 -3
- package/src/middleware-runner.ts +4 -2
- package/src/no-any-casts.test.ts +57 -0
- package/src/permissions.test.ts +3 -1
- package/src/permissions.ts +22 -24
- package/src/pikku-state.ts +17 -6
- package/src/public-surface.json +578 -0
- package/src/public-surface.json.README +25 -0
- package/src/public-surface.test.ts +105 -0
- package/src/removed-legacy-exports.test.ts +62 -0
- package/src/schema.test.ts +78 -0
- package/src/schema.ts +36 -1
- package/src/services/ai-run-state-service.ts +7 -1
- package/src/services/audit-service.ts +2 -2
- package/src/services/in-memory-ai-run-state-service.ts +3 -2
- package/src/services/in-memory-workflow-service.ts +25 -0
- package/src/services/index.ts +1 -4
- package/src/services/local-content-request-handler.test.ts +43 -5
- package/src/services/local-content-request-handler.ts +103 -74
- package/src/services/local-content.ts +15 -1
- package/src/services/local-email-service.ts +5 -1
- package/src/services/system-role-guard.test.ts +4 -1
- package/src/services/workflow-service.ts +9 -2
- package/src/side-effects-are-declared.test.ts +84 -0
- package/src/source-files-stay-composable.test.ts +41 -0
- package/src/testing/service-tests/agent-run-service-tests.ts +98 -0
- package/src/testing/service-tests/ai-storage-service-tests.ts +286 -0
- package/src/testing/service-tests/channel-store-tests.ts +98 -0
- package/src/testing/service-tests/credential-service-tests.ts +143 -0
- package/src/testing/service-tests/deployment-service-tests.ts +33 -0
- package/src/testing/service-tests/event-hub-store-tests.ts +48 -0
- package/src/testing/service-tests/secret-service-tests.ts +105 -0
- package/src/testing/service-tests/session-store-tests.ts +62 -0
- package/src/testing/service-tests/workflow-run-service-tests.ts +59 -0
- package/src/testing/service-tests/workflow-service-tests.ts +308 -0
- package/src/testing/service-tests.ts +31 -1111
- package/src/types/core.types.ts +15 -1
- package/src/types/state.types.ts +1 -1
- package/src/utils/node-host-resolver.ts +20 -0
- package/src/utils/safe-fetch.test.ts +143 -1
- package/src/utils/safe-fetch.ts +200 -25
- package/src/wirings/actor-flow/run-conversation.ts +1 -6
- package/src/wirings/ai-agent/agent-rpc.ts +120 -0
- package/src/wirings/ai-agent/ai-agent-agui.ts +1 -5
- package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
- package/src/wirings/ai-agent/ai-agent-memory.ts +8 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +10 -12
- package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +1 -0
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +82 -2
- package/src/wirings/ai-agent/ai-agent-runner.ts +49 -120
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +36 -63
- package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
- package/src/wirings/ai-agent/ai-agent-turn.ts +121 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +1 -1
- package/src/wirings/ai-agent/voice-input.ts +1 -6
- package/src/wirings/ai-agent/voice-output.ts +2 -12
- package/src/wirings/channel/channel-common.ts +5 -2
- package/src/wirings/channel/channel-handler-shapes.test.ts +72 -0
- package/src/wirings/channel/channel-handler.ts +7 -7
- package/src/wirings/channel/channel-rpc-service.ts +0 -14
- package/src/wirings/channel/channel-rpc.test.ts +25 -4
- package/src/wirings/channel/channel-rpc.types.ts +14 -0
- package/src/wirings/channel/channel-runner.ts +33 -20
- package/src/wirings/channel/channel.types.ts +2 -0
- package/src/wirings/channel/local/local-channel-runner.ts +1 -1
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +2 -1
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +6 -3
- package/src/wirings/cli/channel/cli-channel-runner.ts +3 -1
- package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
- package/src/wirings/cli/cli-runner.ts +7 -4
- package/src/wirings/cli/cli.types.ts +0 -39
- package/src/wirings/cli/command-parser.test.ts +19 -0
- package/src/wirings/cli/command-parser.ts +17 -1
- package/src/wirings/gateway/gateway-authorization.test.ts +131 -0
- package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
- package/src/wirings/gateway/gateway-runner.ts +62 -22
- package/src/wirings/http/http-routes.ts +4 -1
- package/src/wirings/http/http-runner.ts +11 -26
- package/src/wirings/http/http.types.ts +0 -15
- package/src/wirings/http/index.ts +1 -7
- package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
- package/src/wirings/http/web-request.ts +1 -1
- package/src/wirings/mcp/mcp-runner.ts +2 -16
- package/src/wirings/persona/persona-environments.test.ts +14 -3
- package/src/wirings/persona/persona.test.ts +13 -3
- package/src/wirings/persona/validate-personas.ts +5 -1
- package/src/wirings/queue/queue-runner.ts +1 -1
- package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
- package/src/wirings/rpc/rpc-runner.ts +58 -136
- package/src/wirings/rpc/rpc-types.ts +7 -0
- package/src/wirings/rpc/wire-addon.ts +3 -1
- package/src/wirings/secret/validate-secret-definitions.test.ts +69 -0
- package/src/wirings/secret/validate-secret-definitions.ts +2 -0
- package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
- package/src/wirings/trigger/trigger-runner.ts +7 -5
- package/src/wirings/virtual-user/run-virtual-user.test.ts +18 -9
- package/src/wirings/virtual-user/run-virtual-user.ts +28 -15
- package/src/wirings/virtual-user/virtual-user-agents.test.ts +4 -1
- package/src/wirings/virtual-user/virtual-user-derive.ts +4 -25
- package/src/wirings/virtual-user/virtual-user-dispositions.test.ts +7 -2
- package/src/wirings/virtual-user/virtual-user-dispositions.ts +1 -4
- package/src/wirings/virtual-user/virtual-user-intents.test.ts +13 -3
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +35 -2
- package/src/wirings/workflow/feature.ts +7 -4
- package/src/wirings/workflow/graph/graph-node.ts +1 -1
- package/src/wirings/workflow/graph/graph-runner.test.ts +4 -2
- package/src/wirings/workflow/graph/graph-runner.ts +13 -16
- package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -5
- package/src/wirings/workflow/index.ts +12 -4
- package/src/wirings/workflow/pikku-scenario-service.ts +54 -25
- package/src/wirings/workflow/pikku-workflow-service.test.ts +2 -2
- package/src/wirings/workflow/pikku-workflow-service.ts +208 -630
- package/src/wirings/workflow/scenario-hooks.test.ts +51 -0
- package/src/wirings/workflow/workflow-approval.ts +185 -0
- package/src/wirings/workflow/workflow-child-run-session.test.ts +89 -0
- package/src/wirings/workflow/workflow-constants.ts +47 -0
- package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
- package/src/wirings/workflow/workflow-dispatch-relay.test.ts +128 -0
- package/src/wirings/workflow/workflow-errors.ts +128 -0
- package/src/wirings/workflow/workflow-inline-authority.test.ts +12 -6
- package/src/wirings/workflow/workflow-meta-resolver.ts +45 -0
- package/src/wirings/workflow/workflow-missing-meta.test.ts +92 -0
- package/src/wirings/workflow/workflow-queue-routing.ts +85 -0
- package/src/wirings/workflow/workflow-queue-wiring.ts +149 -0
- package/src/wirings/workflow/workflow-recovery.ts +162 -0
- package/src/wirings/workflow/workflow-retry-policy.test.ts +1 -1
- package/src/wirings/workflow/workflow-run-authority.test.ts +215 -0
- package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
- package/src/wirings/workflow/workflow-run-ownership.ts +36 -0
- package/src/wirings/workflow/workflow-suspend.ts +61 -0
- package/src/wirings/workflow/workflow.types.ts +7 -0
- package/src/wirings-stay-decoupled.test.ts +123 -0
- package/tsconfig.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/src/internal.ts +0 -10
|
@@ -3,6 +3,7 @@ import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, resolv
|
|
|
3
3
|
import { checkForApprovals, appendStepMessages } from './ai-agent-stream.js';
|
|
4
4
|
import { AgentInterruptedError, isAbortError, persistOrphanedToolResults, registerInterruptibleRun, trackInterruptNote, trackToolExecution, } from './ai-agent-interrupt.js';
|
|
5
5
|
import { pikkuState, getSingletonServices } from '../../pikku-state.js';
|
|
6
|
+
import { applyInputMiddleware, describeApprovals, notifyAfterStep, toAccumulatedStep, } from './ai-agent-turn.js';
|
|
6
7
|
import { resolveModelConfig } from './ai-agent-model-config.js';
|
|
7
8
|
import { AIProviderNotConfiguredError } from '../../errors/errors.js';
|
|
8
9
|
import { randomUUID } from './ai-agent-utils.js';
|
|
@@ -75,25 +76,13 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
|
|
|
75
76
|
];
|
|
76
77
|
// One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
|
|
77
78
|
const sharedNotes = {};
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const result = await mw.modifyInput(singletonServices, {
|
|
83
|
-
messages: modifiedMessages,
|
|
84
|
-
instructions: modifiedInstructions,
|
|
85
|
-
shared: sharedNotes,
|
|
86
|
-
});
|
|
87
|
-
modifiedMessages = result.messages;
|
|
88
|
-
modifiedInstructions = result.instructions;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
79
|
+
const { messages: modifiedMessages, instructions: modifiedInstructions } = await applyInputMiddleware(aiMiddlewares, singletonServices, {
|
|
80
|
+
messages: runnerParams.messages,
|
|
81
|
+
instructions: runnerParams.instructions,
|
|
82
|
+
}, sharedNotes);
|
|
91
83
|
runnerParams.messages = modifiedMessages;
|
|
92
84
|
runnerParams.instructions = modifiedInstructions;
|
|
93
|
-
//
|
|
94
|
-
// transcript rather than the base64 audio that arrived — see the same note on
|
|
95
|
-
// the streaming path. Identity-checked, because a middleware may legitimately
|
|
96
|
-
// replace the message list with something unrelated to this turn.
|
|
85
|
+
// knowledge: decisions/internals/thread-history-records-the-transcript-not-the-audio.md
|
|
97
86
|
const lastModified = modifiedMessages[modifiedMessages.length - 1];
|
|
98
87
|
const persistedUserMessage = lastModified?.id === userMessage.id ? lastModified : userMessage;
|
|
99
88
|
const runId = await aiRunState.createRun({
|
|
@@ -105,10 +94,7 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
|
|
|
105
94
|
createdAt: new Date(),
|
|
106
95
|
updatedAt: new Date(),
|
|
107
96
|
});
|
|
108
|
-
//
|
|
109
|
-
// visible to `interruptAIAgent` through `aiRunState` either way, so skipping
|
|
110
|
-
// this would leave that call finding the run, passing the ownership check and
|
|
111
|
-
// then failing to stop it — reported as if it were running on another host.
|
|
97
|
+
// knowledge: decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md
|
|
112
98
|
const interruptHandle = registerInterruptibleRun(runId);
|
|
113
99
|
runnerParams.abortSignal = interruptHandle.signal;
|
|
114
100
|
runnerParams.tools = trackToolExecution(runnerParams.tools, interruptHandle);
|
|
@@ -136,44 +122,13 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
|
|
|
136
122
|
lastStepResult = stepResult;
|
|
137
123
|
totalUsage.inputTokens += stepResult.usage.inputTokens;
|
|
138
124
|
totalUsage.outputTokens += stepResult.usage.outputTokens;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
await mw.afterStep(singletonServices, {
|
|
142
|
-
stepNumber: step,
|
|
143
|
-
text: stepResult.text,
|
|
144
|
-
toolCalls: stepResult.toolCalls,
|
|
145
|
-
toolResults: stepResult.toolResults,
|
|
146
|
-
usage: stepResult.usage,
|
|
147
|
-
finishReason: stepResult.finishReason,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
accumulatedSteps.push({
|
|
152
|
-
usage: stepResult.usage,
|
|
153
|
-
toolCalls: stepResult.toolCalls.map((tc) => {
|
|
154
|
-
const tr = stepResult.toolResults.find((r) => r.toolCallId === tc.toolCallId);
|
|
155
|
-
return {
|
|
156
|
-
name: tc.toolName,
|
|
157
|
-
args: tc.args,
|
|
158
|
-
result: typeof tr?.result === 'string'
|
|
159
|
-
? tr.result
|
|
160
|
-
: JSON.stringify(tr?.result ?? ''),
|
|
161
|
-
};
|
|
162
|
-
}),
|
|
163
|
-
});
|
|
125
|
+
await notifyAfterStep(aiMiddlewares, singletonServices, step, stepResult);
|
|
126
|
+
accumulatedSteps.push(toAccumulatedStep(stepResult));
|
|
164
127
|
if (stepResult.toolCalls.length === 0)
|
|
165
128
|
break;
|
|
166
129
|
const approvalsNeeded = checkForApprovals(stepResult, runnerParams.tools);
|
|
167
130
|
if (approvalsNeeded.length > 0) {
|
|
168
|
-
|
|
169
|
-
const toolDef = runnerParams.tools.find((t) => t.name === approval.toolName);
|
|
170
|
-
if (toolDef?.approvalDescriptionFn && !approval.reason) {
|
|
171
|
-
try {
|
|
172
|
-
approval.reason = await toolDef.approvalDescriptionFn(approval.args);
|
|
173
|
-
}
|
|
174
|
-
catch { }
|
|
175
|
-
}
|
|
176
|
-
}
|
|
131
|
+
await describeApprovals(approvalsNeeded, runnerParams.tools);
|
|
177
132
|
const pendingApprovals = approvalsNeeded.map((a) => a.agentRunId
|
|
178
133
|
? {
|
|
179
134
|
type: 'agent-call',
|
|
@@ -270,10 +225,7 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
|
|
|
270
225
|
};
|
|
271
226
|
}
|
|
272
227
|
catch (error) {
|
|
273
|
-
//
|
|
274
|
-
// becomes an `errorMessage`. Unlike the streaming path there is no partial
|
|
275
|
-
// reply to hand back — nothing was delivered — so the caller gets a typed
|
|
276
|
-
// throw it can tell apart from a provider outage instead of a result.
|
|
228
|
+
// knowledge: decisions/internals/an-agent-interrupt-is-not-a-failure.md
|
|
277
229
|
const interruption = interruptHandle.interruption;
|
|
278
230
|
if (interruption || isAbortError(error)) {
|
|
279
231
|
await aiRunState.updateRun(runId, { status: 'interrupted' });
|
|
@@ -331,8 +283,18 @@ export async function resumeAIAgentSync(runId, approvals, params, expectedAgentN
|
|
|
331
283
|
const approvedIds = new Set(approvals.filter((a) => a.approved).map((a) => a.toolCallId));
|
|
332
284
|
const rejectedIds = new Set(approvals.filter((a) => !a.approved).map((a) => a.toolCallId));
|
|
333
285
|
const savedPendingApprovals = [...(run.pendingApprovals ?? [])];
|
|
286
|
+
// The read above is not a claim — concurrent resumes all see the same pending
|
|
287
|
+
// list. `resolveApproval` is the claim, and only the caller it returns true
|
|
288
|
+
// for may run the tool.
|
|
289
|
+
const claimedIds = new Set();
|
|
334
290
|
for (const { toolCallId, approved } of approvals) {
|
|
335
|
-
await aiRunState.resolveApproval(toolCallId, approved ? 'approved' : 'denied');
|
|
291
|
+
const claimed = await aiRunState.resolveApproval(toolCallId, approved ? 'approved' : 'denied');
|
|
292
|
+
if (claimed) {
|
|
293
|
+
claimedIds.add(toolCallId);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (approvals.length > 0 && claimedIds.size === 0) {
|
|
297
|
+
throw new Error(`Approvals for run ${runId} were already resolved by another caller`);
|
|
336
298
|
}
|
|
337
299
|
const { tools } = await buildToolDefs(params, new Map(), run.resourceId, resolvedName, packageName, undefined, agent.aiMiddleware ?? []);
|
|
338
300
|
const toolCallMessages = [];
|
|
@@ -340,6 +302,8 @@ export async function resumeAIAgentSync(runId, approvals, params, expectedAgentN
|
|
|
340
302
|
if (pending.type !== 'tool-call')
|
|
341
303
|
continue;
|
|
342
304
|
const toolCallId = pending.toolCallId;
|
|
305
|
+
if (!claimedIds.has(toolCallId))
|
|
306
|
+
continue;
|
|
343
307
|
let resultStr;
|
|
344
308
|
if (rejectedIds.has(toolCallId)) {
|
|
345
309
|
resultStr =
|
|
@@ -427,19 +391,7 @@ async function continueAfterToolResultSync(run, agent, packageName, resolvedName
|
|
|
427
391
|
];
|
|
428
392
|
// One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
|
|
429
393
|
const sharedNotes = {};
|
|
430
|
-
|
|
431
|
-
let modifiedInstructions = instructions;
|
|
432
|
-
for (const mw of aiMiddlewares) {
|
|
433
|
-
if (mw.modifyInput) {
|
|
434
|
-
const result = await mw.modifyInput(singletonServices, {
|
|
435
|
-
messages: modifiedMessages,
|
|
436
|
-
instructions: modifiedInstructions,
|
|
437
|
-
shared: sharedNotes,
|
|
438
|
-
});
|
|
439
|
-
modifiedMessages = result.messages;
|
|
440
|
-
modifiedInstructions = result.instructions;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
394
|
+
const { messages: modifiedMessages, instructions: modifiedInstructions } = await applyInputMiddleware(aiMiddlewares, singletonServices, { messages: trimmedMessages, instructions: instructions }, sharedNotes);
|
|
443
395
|
const { tools: resumeTools } = await buildToolDefs(params, new Map(), run.resourceId, resolvedName, packageName, undefined, aiMiddlewares);
|
|
444
396
|
const resolved = resolveModelConfig(resolvedName, agent);
|
|
445
397
|
const maxSteps = resolved.maxSteps ?? 10;
|
|
@@ -472,44 +424,13 @@ async function continueAfterToolResultSync(run, agent, packageName, resolvedName
|
|
|
472
424
|
lastStepResult = stepResult;
|
|
473
425
|
totalUsage.inputTokens += stepResult.usage.inputTokens;
|
|
474
426
|
totalUsage.outputTokens += stepResult.usage.outputTokens;
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
await mw.afterStep(singletonServices, {
|
|
478
|
-
stepNumber: step,
|
|
479
|
-
text: stepResult.text,
|
|
480
|
-
toolCalls: stepResult.toolCalls,
|
|
481
|
-
toolResults: stepResult.toolResults,
|
|
482
|
-
usage: stepResult.usage,
|
|
483
|
-
finishReason: stepResult.finishReason,
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
accumulatedSteps.push({
|
|
488
|
-
usage: stepResult.usage,
|
|
489
|
-
toolCalls: stepResult.toolCalls.map((tc) => {
|
|
490
|
-
const tr = stepResult.toolResults.find((r) => r.toolCallId === tc.toolCallId);
|
|
491
|
-
return {
|
|
492
|
-
name: tc.toolName,
|
|
493
|
-
args: tc.args,
|
|
494
|
-
result: typeof tr?.result === 'string'
|
|
495
|
-
? tr.result
|
|
496
|
-
: JSON.stringify(tr?.result ?? ''),
|
|
497
|
-
};
|
|
498
|
-
}),
|
|
499
|
-
});
|
|
427
|
+
await notifyAfterStep(aiMiddlewares, singletonServices, step, stepResult);
|
|
428
|
+
accumulatedSteps.push(toAccumulatedStep(stepResult));
|
|
500
429
|
if (stepResult.toolCalls.length === 0)
|
|
501
430
|
break;
|
|
502
431
|
const approvalsNeeded = checkForApprovals(stepResult, runnerParams.tools);
|
|
503
432
|
if (approvalsNeeded.length > 0) {
|
|
504
|
-
|
|
505
|
-
const toolDef = runnerParams.tools.find((t) => t.name === approval.toolName);
|
|
506
|
-
if (toolDef?.approvalDescriptionFn && !approval.reason) {
|
|
507
|
-
try {
|
|
508
|
-
approval.reason = await toolDef.approvalDescriptionFn(approval.args);
|
|
509
|
-
}
|
|
510
|
-
catch { }
|
|
511
|
-
}
|
|
512
|
-
}
|
|
433
|
+
await describeApprovals(approvalsNeeded, runnerParams.tools);
|
|
513
434
|
const pendingApprovals = approvalsNeeded.map((a) => a.agentRunId
|
|
514
435
|
? {
|
|
515
436
|
type: 'agent-call',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { pikkuState, getSingletonServices } from '../../pikku-state.js';
|
|
2
|
+
import { applyInputMiddleware } from './ai-agent-turn.js';
|
|
2
3
|
import { AIProviderNotConfiguredError } from '../../errors/errors.js';
|
|
3
4
|
import { randomUUID } from './ai-agent-utils.js';
|
|
4
5
|
import { SPOKEN_TRANSCRIPT } from './voice-input.js';
|
|
@@ -414,36 +415,18 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
|
|
|
414
415
|
];
|
|
415
416
|
// One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
|
|
416
417
|
const sharedNotes = {};
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const result = await mw.modifyInput(singletonServices, {
|
|
422
|
-
messages: modifiedMessages,
|
|
423
|
-
instructions: modifiedInstructions,
|
|
424
|
-
shared: sharedNotes,
|
|
425
|
-
});
|
|
426
|
-
modifiedMessages = result.messages;
|
|
427
|
-
modifiedInstructions = result.instructions;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
418
|
+
const { messages: modifiedMessages, instructions: modifiedInstructions } = await applyInputMiddleware(aiMiddlewares, singletonServices, {
|
|
419
|
+
messages: runnerParams.messages,
|
|
420
|
+
instructions: runnerParams.instructions,
|
|
421
|
+
}, sharedNotes);
|
|
430
422
|
runnerParams.messages = modifiedMessages;
|
|
431
423
|
runnerParams.instructions = modifiedInstructions;
|
|
432
|
-
//
|
|
433
|
-
// has no idea what it said; until this arrives its own message is a blank
|
|
434
|
-
// bubble. Ahead of the run rather than alongside it because the answer starts
|
|
435
|
-
// streaming within a few hundred milliseconds, and a question that appears
|
|
436
|
-
// after its answer reads as the wrong question.
|
|
424
|
+
// knowledge: decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md
|
|
437
425
|
const transcript = sharedNotes[SPOKEN_TRANSCRIPT];
|
|
438
426
|
if (typeof transcript === 'string') {
|
|
439
427
|
channel.send({ type: 'transcript', text: transcript });
|
|
440
428
|
}
|
|
441
|
-
//
|
|
442
|
-
// spoken turn that is not what arrived over the wire: the wire carried a
|
|
443
|
-
// base64 audio blob, and persisting it would write megabytes of unreadable
|
|
444
|
-
// data into the history while losing the only readable record of the turn.
|
|
445
|
-
// Identity-checked rather than assumed — a middleware is free to rewrite the
|
|
446
|
-
// message list into something with no relation to the turn.
|
|
429
|
+
// knowledge: decisions/internals/thread-history-records-the-transcript-not-the-audio.md
|
|
447
430
|
const lastModified = modifiedMessages[modifiedMessages.length - 1];
|
|
448
431
|
const persistedUserMessage = lastModified?.id === userMessage.id ? lastModified : userMessage;
|
|
449
432
|
const runId = await aiRunState.createRun({
|
|
@@ -508,10 +491,7 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
|
|
|
508
491
|
: persistingChannel;
|
|
509
492
|
const credentialFilteredChannel = {
|
|
510
493
|
...wrappedChannel,
|
|
511
|
-
//
|
|
512
|
-
// swallowing the promise here makes every `await channel.send(...)` upstream
|
|
513
|
-
// a no-op — including the one that waits for a buffering hook to flush
|
|
514
|
-
// before the channel closes.
|
|
494
|
+
// knowledge: decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md
|
|
515
495
|
send: (event) => {
|
|
516
496
|
if (event.type === 'tool-result' &&
|
|
517
497
|
event.result !== null &&
|
|
@@ -559,11 +539,7 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
|
|
|
559
539
|
return persistingChannel.fullText;
|
|
560
540
|
}
|
|
561
541
|
await postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, runnerParams.messages, aiRunState, runId);
|
|
562
|
-
//
|
|
563
|
-
// `done` is the only signal a stream hook gets that the reply is over, and
|
|
564
|
-
// the ones that buffer need it: `voiceOutput` speaks a trailing fragment
|
|
565
|
-
// that never reached a full stop, and waits for audio it has already paid
|
|
566
|
-
// to synthesize. Sent raw, that work is discarded by the `close()` below.
|
|
542
|
+
// knowledge: decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md
|
|
567
543
|
await outputChannel.send({ type: 'done' });
|
|
568
544
|
channel.close();
|
|
569
545
|
return persistingChannel.fullText;
|
|
@@ -650,12 +626,7 @@ export async function interruptAIAgent(input, params) {
|
|
|
650
626
|
const stopped = signalRunInterrupt(input.runId, {
|
|
651
627
|
reason: input.reason ?? 'user',
|
|
652
628
|
});
|
|
653
|
-
//
|
|
654
|
-
// another instance. Returning a bare `false` there is indistinguishable from
|
|
655
|
-
// "it already finished", so the one deployment shape this registry does not
|
|
656
|
-
// cover fails silently — an agent that simply refuses to shut up, with nothing
|
|
657
|
-
// in the logs. Say so instead. See `signalRunInterrupt` for the fix (fan the
|
|
658
|
-
// interrupt out over eventHub so every instance tries locally).
|
|
629
|
+
// knowledge: decisions/internals/an-agent-run-owned-by-another-instance-says-so.md
|
|
659
630
|
if (!stopped && run.status === 'running') {
|
|
660
631
|
logger?.warn(`Could not interrupt run ${input.runId}: it is running in another process. ` +
|
|
661
632
|
'Interrupts are process-local; fan them out over eventHub to support multiple instances.');
|
|
@@ -681,7 +652,13 @@ export async function resumeAIAgent(input, channel, params, options) {
|
|
|
681
652
|
}
|
|
682
653
|
const { agent, packageName, resolvedName } = resolveAgent(run.agentName);
|
|
683
654
|
await assertAgentAuthorized(agent, params, packageName);
|
|
684
|
-
|
|
655
|
+
// The read above is not a claim — concurrent resumes all see the same pending
|
|
656
|
+
// approval. `resolveApproval` is the claim, and the loser must not go on to
|
|
657
|
+
// run the tool a second time.
|
|
658
|
+
const claimed = await aiRunState.resolveApproval(input.toolCallId, input.approved ? 'approved' : 'denied');
|
|
659
|
+
if (!claimed) {
|
|
660
|
+
throw new Error(`Approval for toolCallId ${input.toolCallId} was already resolved by another caller`);
|
|
661
|
+
}
|
|
685
662
|
const { storage } = resolveMemoryServices(agent, singletonServices);
|
|
686
663
|
const memoryConfig = agent.memory;
|
|
687
664
|
const agentRunner = singletonServices.aiAgentRunner;
|
|
@@ -865,23 +842,8 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
|
|
|
865
842
|
];
|
|
866
843
|
// One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
|
|
867
844
|
const sharedNotes = {};
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
for (const mw of aiMiddlewares) {
|
|
871
|
-
if (mw.modifyInput) {
|
|
872
|
-
const result = await mw.modifyInput(singletonServices, {
|
|
873
|
-
messages: modifiedMessages,
|
|
874
|
-
instructions: modifiedInstructions,
|
|
875
|
-
shared: sharedNotes,
|
|
876
|
-
});
|
|
877
|
-
modifiedMessages = result.messages;
|
|
878
|
-
modifiedInstructions = result.instructions;
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
// Resuming is as interruptible as the first turn. It is the same person
|
|
882
|
-
// listening to the same voice, and after an approval it is where most of the
|
|
883
|
-
// reply actually gets spoken — an approved delete is followed by the agent
|
|
884
|
-
// talking about it, and that is a normal thing to talk over.
|
|
845
|
+
const { messages: modifiedMessages, instructions: modifiedInstructions } = await applyInputMiddleware(aiMiddlewares, singletonServices, { messages: trimmedMessages, instructions: instructions }, sharedNotes);
|
|
846
|
+
// knowledge: decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md
|
|
885
847
|
const interruptHandle = registerInterruptibleRun(run.runId);
|
|
886
848
|
const streamMiddleware = aiMiddlewares
|
|
887
849
|
.filter((mw) => mw.modifyOutputStream)
|
|
@@ -961,10 +923,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
|
|
|
961
923
|
return;
|
|
962
924
|
}
|
|
963
925
|
await postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, runnerParams.messages, aiRunState, run.runId);
|
|
964
|
-
//
|
|
965
|
-
// matters more here. After an approval, most of what gets spoken is the
|
|
966
|
-
// agent talking about what it just did, so this is the half of the reply a
|
|
967
|
-
// dropped flush would silence.
|
|
926
|
+
// knowledge: decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md
|
|
968
927
|
await wrappedChannel.send({ type: 'done' });
|
|
969
928
|
channel.close();
|
|
970
929
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { CoreSingletonServices } from '../../types/core.types.js';
|
|
2
|
+
import type { PikkuAIMiddlewareHooks } from './ai-agent.types.js';
|
|
3
|
+
/** Exactly what `modifyInput` accepts and returns, taken from the hook itself. */
|
|
4
|
+
type TurnInput = Pick<Parameters<NonNullable<PikkuAIMiddlewareHooks['modifyInput']>>[1], 'messages' | 'instructions'>;
|
|
5
|
+
/**
|
|
6
|
+
* Run every `modifyInput` hook in order, threading each result into the next.
|
|
7
|
+
*
|
|
8
|
+
* Shared by the four places a turn starts — first turn and resume, on both the
|
|
9
|
+
* streaming and non-streaming paths. `sharedNotes` is one bag per run that the
|
|
10
|
+
* caller keeps, because hooks use it to pass things forward (`voiceInput`
|
|
11
|
+
* leaves the transcript there for `voiceOutput`).
|
|
12
|
+
*/
|
|
13
|
+
export declare const applyInputMiddleware: (aiMiddlewares: PikkuAIMiddlewareHooks[], singletonServices: CoreSingletonServices, input: TurnInput, sharedNotes: Record<string, unknown>) => Promise<TurnInput>;
|
|
14
|
+
/** The step fields both agent paths hand to `afterStep`, taken from the hook. */
|
|
15
|
+
type StepResult = Omit<Parameters<NonNullable<PikkuAIMiddlewareHooks['afterStep']>>[1], 'stepNumber'>;
|
|
16
|
+
/**
|
|
17
|
+
* Notify every `afterStep` hook. The payload is the step verbatim plus its
|
|
18
|
+
* ordinal — a hook that wants the run's totals accumulates them itself.
|
|
19
|
+
*/
|
|
20
|
+
export declare const notifyAfterStep: (aiMiddlewares: PikkuAIMiddlewareHooks[], singletonServices: CoreSingletonServices, stepNumber: number, stepResult: StepResult) => Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* The step shape kept on the run: each tool call paired with its result.
|
|
23
|
+
*
|
|
24
|
+
* A result is stringified unless it already is a string, because this is what
|
|
25
|
+
* gets persisted and read back as history — an object there is unreadable.
|
|
26
|
+
*/
|
|
27
|
+
export declare const toAccumulatedStep: (stepResult: StepResult) => {
|
|
28
|
+
usage: {
|
|
29
|
+
inputTokens: number;
|
|
30
|
+
outputTokens: number;
|
|
31
|
+
};
|
|
32
|
+
toolCalls: {
|
|
33
|
+
name: string;
|
|
34
|
+
args: Record<string, unknown>;
|
|
35
|
+
result: string;
|
|
36
|
+
}[];
|
|
37
|
+
};
|
|
38
|
+
type PendingApproval = {
|
|
39
|
+
toolName: string;
|
|
40
|
+
args: unknown;
|
|
41
|
+
reason?: string;
|
|
42
|
+
};
|
|
43
|
+
type ToolDef = {
|
|
44
|
+
name: string;
|
|
45
|
+
approvalDescriptionFn?: (args: any) => Promise<string> | string;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Fill in each approval's human-readable reason from the tool's own
|
|
49
|
+
* `approvalDescriptionFn`, where it did not supply one.
|
|
50
|
+
*
|
|
51
|
+
* A description that throws is swallowed: the approval still has to be raised,
|
|
52
|
+
* and a gate that disappears because its label failed to render is the worst
|
|
53
|
+
* available outcome.
|
|
54
|
+
*/
|
|
55
|
+
export declare const describeApprovals: (approvals: PendingApproval[], tools: ToolDef[]) => Promise<void>;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run every `modifyInput` hook in order, threading each result into the next.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the four places a turn starts — first turn and resume, on both the
|
|
5
|
+
* streaming and non-streaming paths. `sharedNotes` is one bag per run that the
|
|
6
|
+
* caller keeps, because hooks use it to pass things forward (`voiceInput`
|
|
7
|
+
* leaves the transcript there for `voiceOutput`).
|
|
8
|
+
*/
|
|
9
|
+
export const applyInputMiddleware = async (aiMiddlewares, singletonServices, input, sharedNotes) => {
|
|
10
|
+
let { messages, instructions } = input;
|
|
11
|
+
for (const mw of aiMiddlewares) {
|
|
12
|
+
if (!mw.modifyInput)
|
|
13
|
+
continue;
|
|
14
|
+
const result = await mw.modifyInput(singletonServices, {
|
|
15
|
+
messages,
|
|
16
|
+
instructions,
|
|
17
|
+
shared: sharedNotes,
|
|
18
|
+
});
|
|
19
|
+
messages = result.messages;
|
|
20
|
+
instructions = result.instructions;
|
|
21
|
+
}
|
|
22
|
+
return { messages, instructions };
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Notify every `afterStep` hook. The payload is the step verbatim plus its
|
|
26
|
+
* ordinal — a hook that wants the run's totals accumulates them itself.
|
|
27
|
+
*/
|
|
28
|
+
export const notifyAfterStep = async (aiMiddlewares, singletonServices, stepNumber, stepResult) => {
|
|
29
|
+
for (const mw of aiMiddlewares) {
|
|
30
|
+
if (!mw.afterStep)
|
|
31
|
+
continue;
|
|
32
|
+
await mw.afterStep(singletonServices, {
|
|
33
|
+
stepNumber,
|
|
34
|
+
text: stepResult.text,
|
|
35
|
+
toolCalls: stepResult.toolCalls,
|
|
36
|
+
toolResults: stepResult.toolResults,
|
|
37
|
+
usage: stepResult.usage,
|
|
38
|
+
finishReason: stepResult.finishReason,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The step shape kept on the run: each tool call paired with its result.
|
|
44
|
+
*
|
|
45
|
+
* A result is stringified unless it already is a string, because this is what
|
|
46
|
+
* gets persisted and read back as history — an object there is unreadable.
|
|
47
|
+
*/
|
|
48
|
+
export const toAccumulatedStep = (stepResult) => ({
|
|
49
|
+
usage: stepResult.usage,
|
|
50
|
+
toolCalls: stepResult.toolCalls.map((tc) => {
|
|
51
|
+
const tr = stepResult.toolResults.find((r) => r.toolCallId === tc.toolCallId);
|
|
52
|
+
return {
|
|
53
|
+
name: tc.toolName,
|
|
54
|
+
args: tc.args,
|
|
55
|
+
result: typeof tr?.result === 'string'
|
|
56
|
+
? tr.result
|
|
57
|
+
: JSON.stringify(tr?.result ?? ''),
|
|
58
|
+
};
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
/**
|
|
62
|
+
* Fill in each approval's human-readable reason from the tool's own
|
|
63
|
+
* `approvalDescriptionFn`, where it did not supply one.
|
|
64
|
+
*
|
|
65
|
+
* A description that throws is swallowed: the approval still has to be raised,
|
|
66
|
+
* and a gate that disappears because its label failed to render is the worst
|
|
67
|
+
* available outcome.
|
|
68
|
+
*/
|
|
69
|
+
export const describeApprovals = async (approvals, tools) => {
|
|
70
|
+
for (const approval of approvals) {
|
|
71
|
+
if (approval.reason)
|
|
72
|
+
continue;
|
|
73
|
+
const toolDef = tools.find((t) => t.name === approval.toolName);
|
|
74
|
+
if (!toolDef?.approvalDescriptionFn)
|
|
75
|
+
continue;
|
|
76
|
+
try {
|
|
77
|
+
approval.reason = await toolDef.approvalDescriptionFn(approval.args);
|
|
78
|
+
}
|
|
79
|
+
catch { }
|
|
80
|
+
}
|
|
81
|
+
};
|
|
@@ -135,12 +135,7 @@ export const voiceInput = (config) => pikkuAIMiddleware({
|
|
|
135
135
|
// to send, and a message with no content is not a question.
|
|
136
136
|
if (updatedContent.length === 0)
|
|
137
137
|
throw new NoSpeechDetectedError();
|
|
138
|
-
//
|
|
139
|
-
// reads as non-speech and still have content — an image with a silent
|
|
140
|
-
// caption clip — which leaves nothing above to throw. Recording `''` here
|
|
141
|
-
// would send a transcript event saying the user said nothing, and a client
|
|
142
|
-
// that tells "not transcribed yet" from "transcribed" by the key being
|
|
143
|
-
// absent would render the turn as an empty bubble rather than a pending one.
|
|
138
|
+
// knowledge: decisions/internals/an-empty-transcript-is-not-recorded.md
|
|
144
139
|
if (heard.length > 0) {
|
|
145
140
|
shared[SPOKEN_TRANSCRIPT] = heard.join(' ');
|
|
146
141
|
}
|
|
@@ -105,11 +105,7 @@ export const voiceOutput = (config) => pikkuAIMiddleware({
|
|
|
105
105
|
const { aiAgentRunner, logger } = services;
|
|
106
106
|
if (!aiAgentRunner?.generateSpeech)
|
|
107
107
|
return event;
|
|
108
|
-
//
|
|
109
|
-
// every turn it handles, so `false` means a real user really typed; the
|
|
110
|
-
// key being absent means nothing reported either way — no voice input is
|
|
111
|
-
// wired — and that caller's replies are spoken exactly as they were before
|
|
112
|
-
// this option existed.
|
|
108
|
+
// knowledge: decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md
|
|
113
109
|
if (!config?.always && shared[SPOKEN_TURN] === false)
|
|
114
110
|
return event;
|
|
115
111
|
/**
|
|
@@ -124,13 +120,7 @@ export const voiceOutput = (config) => pikkuAIMiddleware({
|
|
|
124
120
|
if (!config?.model) {
|
|
125
121
|
throw new Error('voiceOutput requires a speech model (e.g. openai/tts-1)');
|
|
126
122
|
}
|
|
127
|
-
//
|
|
128
|
-
// should still speak the half it can, and repeating the notice for
|
|
129
|
-
// every sentence of a long one would bury the reply itself.
|
|
130
|
-
//
|
|
131
|
-
// Per sentence is also what makes the voice right. A reply that answers
|
|
132
|
-
// in English and then quotes a Chinese title is two sentences in two
|
|
133
|
-
// scripts, and each is synthesized in the voice its own script needs.
|
|
123
|
+
// knowledge: decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md
|
|
134
124
|
let voice = config.voice;
|
|
135
125
|
if (config.speakableScripts) {
|
|
136
126
|
const unspeakable = unspeakableScripts(text, config.speakableScripts);
|
|
@@ -12,6 +12,7 @@ export const runChannelLifecycleWithMiddleware = async ({ channelConfig, meta, l
|
|
|
12
12
|
});
|
|
13
13
|
const allChannelMiddleware = combineChannelMiddleware('channel', `${channelConfig.name}:${lifecycleType}:cm`, {
|
|
14
14
|
wireInheritedChannelMiddleware: channelMiddlewareMeta,
|
|
15
|
+
// knowledge: questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md
|
|
15
16
|
wireChannelMiddleware: channelConfig.channelMiddleware,
|
|
16
17
|
});
|
|
17
18
|
let wire = {
|
|
@@ -46,7 +46,7 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
|
|
|
46
46
|
? `route '${routingProperty}:${routerValue}'`
|
|
47
47
|
: 'the default message route';
|
|
48
48
|
logger.error(`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.`);
|
|
49
|
-
//
|
|
49
|
+
// knowledge: questions/unauthorized-channel-replies-escape-the-declared-out-type.md
|
|
50
50
|
channelHandler.getChannel().send(`Unauthorized for ${routeMessage}`);
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
@@ -55,10 +55,7 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
|
|
|
55
55
|
const pikkuFuncId = routeMeta.pikkuFuncId;
|
|
56
56
|
// Get wire middleware: channel-level middleware + message-specific middleware
|
|
57
57
|
const channelWireMiddleware = channelConfig.middleware || [];
|
|
58
|
-
//
|
|
59
|
-
// - Direct config: onMessage.func is a plain Function
|
|
60
|
-
// - Wrapper: onMessage.func is a CorePikkuFunctionConfig (has onMessage.func.func)
|
|
61
|
-
// - Simple wrapper: onMessage has both func (plain Function) and middleware properties
|
|
58
|
+
// knowledge: decisions/internals/channel-message-handlers-accept-three-config-shapes.md
|
|
62
59
|
const isWrapper = onMessage &&
|
|
63
60
|
typeof onMessage === 'object' &&
|
|
64
61
|
'func' in onMessage &&
|
|
@@ -83,6 +80,7 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
|
|
|
83
80
|
inheritedMiddleware,
|
|
84
81
|
wireMiddleware,
|
|
85
82
|
inheritedChannelMiddleware: channelMeta?.channelMiddleware,
|
|
83
|
+
// knowledge: questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md
|
|
86
84
|
wireChannelMiddleware: wireChannelMiddleware,
|
|
87
85
|
coerceDataFromSchema: true,
|
|
88
86
|
tags: channelConfig.tags,
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import type { DeploymentService } from '../../services/deployment-service.js';
|
|
2
2
|
import { ChannelRPCRegistry } from './channel-rpc-registry.js';
|
|
3
3
|
import { type ChannelRPCValidator } from './channel-rpc.types.js';
|
|
4
|
-
/**
|
|
5
|
-
* `remote` for channels that only ever flow one way — a server-sent stream, an
|
|
6
|
-
* agent's output, a local CLI. Nothing is listening for a request on them, so
|
|
7
|
-
* the call is refused rather than waiting out a timeout.
|
|
8
|
-
*/
|
|
9
|
-
export declare const unsupportedChannelRemote: (funcName: string) => Promise<never>;
|
|
10
4
|
/**
|
|
11
5
|
* A `DeploymentService` whose transport is an open channel rather than an
|
|
12
6
|
* address, so a server-side function can call back into a peer that has no
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import { ChannelRPCRegistry } from './channel-rpc-registry.js';
|
|
2
2
|
import { CHANNEL_RPC_REQUEST, CHANNEL_RPC_RESPONSE, ChannelRPCError, isChannelRPCPending, isChannelRPCResponse, } from './channel-rpc.types.js';
|
|
3
|
-
/**
|
|
4
|
-
* `remote` for channels that only ever flow one way — a server-sent stream, an
|
|
5
|
-
* agent's output, a local CLI. Nothing is listening for a request on them, so
|
|
6
|
-
* the call is refused rather than waiting out a timeout.
|
|
7
|
-
*/
|
|
8
|
-
export const unsupportedChannelRemote = async (funcName) => {
|
|
9
|
-
throw new ChannelRPCError(`Cannot call "${funcName}" remotely: this channel has no peer that answers`, 'unsupported');
|
|
10
|
-
};
|
|
11
3
|
/**
|
|
12
4
|
* A `DeploymentService` whose transport is an open channel rather than an
|
|
13
5
|
* address, so a server-side function can call back into a peer that has no
|
|
@@ -88,3 +88,9 @@ export type ApprovalRequester = (request: {
|
|
|
88
88
|
}) => Promise<boolean> | boolean;
|
|
89
89
|
/** Checks one end of a call — arguments going out, or the answer coming back. */
|
|
90
90
|
export type ChannelRPCValidator = (funcName: string, value: unknown) => Promise<void> | void;
|
|
91
|
+
/**
|
|
92
|
+
* `remote` for wires with no peer that answers — a server-sent stream, an
|
|
93
|
+
* agent's output, a local CLI. Nothing is listening for a request on them, so
|
|
94
|
+
* the call is refused rather than waiting out a timeout.
|
|
95
|
+
*/
|
|
96
|
+
export declare const unsupportedChannelRemote: (funcName: string) => Promise<never>;
|
|
@@ -48,3 +48,11 @@ export const isCapabilityDef = (capability) => typeof capability === 'object' &&
|
|
|
48
48
|
export const resolveCapability = (capability) => isCapabilityDef(capability)
|
|
49
49
|
? capability
|
|
50
50
|
: { execute: capability, needsApproval: true };
|
|
51
|
+
/**
|
|
52
|
+
* `remote` for wires with no peer that answers — a server-sent stream, an
|
|
53
|
+
* agent's output, a local CLI. Nothing is listening for a request on them, so
|
|
54
|
+
* the call is refused rather than waiting out a timeout.
|
|
55
|
+
*/
|
|
56
|
+
export const unsupportedChannelRemote = async (funcName) => {
|
|
57
|
+
throw new ChannelRPCError(`Cannot call "${funcName}" remotely: this channel has no peer that answers`, 'unsupported');
|
|
58
|
+
};
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import type { CorePikkuPermission } from '../../function/functions.types.js';
|
|
2
1
|
import type { SessionService } from '../../services/user-session-service.js';
|
|
3
|
-
import type { CorePikkuMiddleware } from '../../types/core.types.js';
|
|
4
2
|
import type { ChannelMeta, CoreChannel, RunChannelOptions, RunChannelParams } from './channel.types.js';
|
|
5
|
-
export declare const wireChannel: <In, Channel extends string,
|
|
3
|
+
export declare const wireChannel: <In, Channel extends string, ChannelConnect, ChannelDisconnect, ChannelFunctionMessage>(channel: CoreChannel<In, Channel, ChannelConnect, ChannelDisconnect, ChannelFunctionMessage>) => void;
|
|
6
4
|
export declare const openChannel: ({ route, coerceDataFromSchema, request, }: Pick<CoreChannel<unknown, string>, "route"> & RunChannelParams<unknown> & {
|
|
7
5
|
userSession: SessionService<any>;
|
|
8
6
|
} & RunChannelOptions) => Promise<{
|