@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
|
@@ -233,6 +233,11 @@ export interface CoreUserSession {
|
|
|
233
233
|
* Populated by whoever builds the session — core reads them, never fetches.
|
|
234
234
|
*/
|
|
235
235
|
scopes?: string[];
|
|
236
|
+
/**
|
|
237
|
+
* Restricts the session to functions declared `readonly`. The function runner
|
|
238
|
+
* throws `ReadonlySessionError` for anything else.
|
|
239
|
+
*/
|
|
240
|
+
readonly?: boolean;
|
|
236
241
|
}
|
|
237
242
|
/**
|
|
238
243
|
* Kept structural so core stays independent of any one auth package —
|
|
@@ -299,12 +304,17 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
|
|
|
299
304
|
*/
|
|
300
305
|
auth?: () => Promise<AuthInstance>;
|
|
301
306
|
}
|
|
302
|
-
export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boolean = false, UserSession extends CoreUserSession = CoreUserSession, TypedRPC extends PikkuRPC = PikkuRPC, IsChannel extends true | null = null, MCPTools extends string | never = never, TypedWorkflow extends PikkuWorkflowWire | never = PikkuWorkflowWire, TriggerOutput = unknown, TypedScenario extends PikkuScenarioWire | never = PikkuScenarioWire
|
|
307
|
+
export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boolean = false, UserSession extends CoreUserSession = CoreUserSession, TypedRPC extends PikkuRPC = PikkuRPC, IsChannel extends true | null = null, MCPTools extends string | never = never, TypedWorkflow extends PikkuWorkflowWire | never = PikkuWorkflowWire, TriggerOutput = unknown, TypedScenario extends PikkuScenarioWire<any> | never = PikkuScenarioWire<any>, TypedActors extends ScenarioPersonas = ScenarioPersonas> = {
|
|
303
308
|
/** Always present — lazily initialised on first access for every function invocation */
|
|
304
309
|
rpc: TypedRPC;
|
|
305
310
|
} & Partial<{
|
|
306
311
|
wireType: PikkuWiringTypes;
|
|
307
312
|
wireId: string;
|
|
313
|
+
/**
|
|
314
|
+
* A logger scoped to this invocation, when a host attaches one. Core never
|
|
315
|
+
* sets it; services that log fall back to the singleton logger.
|
|
316
|
+
*/
|
|
317
|
+
logger: Logger;
|
|
308
318
|
/** Trace ID for distributed tracing — propagated across remote RPC calls via x-trace-id header */
|
|
309
319
|
traceId: string;
|
|
310
320
|
functionId: string;
|
|
@@ -70,12 +70,7 @@ function actorInstructions(persona, task) {
|
|
|
70
70
|
: '',
|
|
71
71
|
`Your goal in this conversation: ${task}.`,
|
|
72
72
|
`Send one message at a time. Set "done" to true only once your goal is clearly accomplished, or clearly impossible.`,
|
|
73
|
-
//
|
|
74
|
-
// that cannot take a JSON *schema* degrades to OpenAI's `json_object`
|
|
75
|
-
// mode — which refuses the request outright unless the word "json" appears
|
|
76
|
-
// somewhere in the prompt. Saying it once here covers all three call sites
|
|
77
|
-
// (turn, approvals, verdict), since each builds on these instructions, and
|
|
78
|
-
// it costs nothing on providers that never needed telling.
|
|
73
|
+
// knowledge: decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md
|
|
79
74
|
`Reply with json matching the schema you are given, and nothing else.`,
|
|
80
75
|
]
|
|
81
76
|
.filter(Boolean)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PikkuRawWire } from '../../types/core.types.js';
|
|
2
|
+
import type { SessionService } from '../../services/user-session-service.js';
|
|
3
|
+
import type { CoreUserSession } from '../../types/core.types.js';
|
|
4
|
+
import type { PikkuRPC } from '../rpc/rpc-types.js';
|
|
5
|
+
export type AgentRPCOptions = {
|
|
6
|
+
sessionService?: SessionService<CoreUserSession>;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* `wire.rpc.agent`, implemented.
|
|
10
|
+
*
|
|
11
|
+
* Lives here rather than in `rpc-runner` so the agent surface is one file next
|
|
12
|
+
* to the runner and stream code it delegates to, instead of a wing of the RPC
|
|
13
|
+
* primitive.
|
|
14
|
+
*/
|
|
15
|
+
export declare const createAgentRPC: (wire: PikkuRawWire, options: AgentRPCOptions) => PikkuRPC["agent"];
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js';
|
|
2
|
+
import { streamAIAgent, resumeAIAgent, interruptAIAgent, } from './ai-agent-stream.js';
|
|
3
|
+
import { wrapChannelWithAGUI } from './ai-agent-agui.js';
|
|
4
|
+
/**
|
|
5
|
+
* `wire.rpc.agent`, implemented.
|
|
6
|
+
*
|
|
7
|
+
* Lives here rather than in `rpc-runner` so the agent surface is one file next
|
|
8
|
+
* to the runner and stream code it delegates to, instead of a wing of the RPC
|
|
9
|
+
* primitive.
|
|
10
|
+
*/
|
|
11
|
+
export const createAgentRPC = (wire, options) => {
|
|
12
|
+
const params = () => ({
|
|
13
|
+
sessionService: options.sessionService,
|
|
14
|
+
getCredential: wire.getCredential?.bind(wire),
|
|
15
|
+
});
|
|
16
|
+
const streamingChannel = () => {
|
|
17
|
+
const channel = wire.channel;
|
|
18
|
+
if (!channel)
|
|
19
|
+
throw new Error('No channel available for streaming');
|
|
20
|
+
return channel;
|
|
21
|
+
};
|
|
22
|
+
/** `run` and `approve` return the same shape; only the call differs. */
|
|
23
|
+
const asRunResult = (result) => ({
|
|
24
|
+
runId: result.runId,
|
|
25
|
+
result: result.object ?? result.text,
|
|
26
|
+
usage: result.usage,
|
|
27
|
+
...(result.status === 'suspended' && {
|
|
28
|
+
status: 'suspended',
|
|
29
|
+
pendingApprovals: result.pendingApprovals,
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
run: async (agentName, input) => asRunResult(await runAIAgent(agentName, input, params())),
|
|
34
|
+
stream: async (agentName, input, streamOptions) => {
|
|
35
|
+
let currentRunId;
|
|
36
|
+
await streamAIAgent(agentName, input, wrapChannelWithAGUI(streamingChannel(), {
|
|
37
|
+
threadId: input.threadId,
|
|
38
|
+
getRunId: () => currentRunId,
|
|
39
|
+
}), params(), undefined, {
|
|
40
|
+
...streamOptions,
|
|
41
|
+
onRunCreated: (runId) => {
|
|
42
|
+
currentRunId = runId;
|
|
43
|
+
streamOptions?.onRunCreated?.(runId);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
resume: async (runId, input, streamOptions) => {
|
|
48
|
+
await resumeAIAgent({ runId, ...input }, wrapChannelWithAGUI(streamingChannel(), { runId }), params(), streamOptions);
|
|
49
|
+
},
|
|
50
|
+
interrupt: async (runId, reason) => interruptAIAgent({ runId, ...(reason ? { reason } : {}) }, { sessionService: options.sessionService }),
|
|
51
|
+
approve: async (runId, approvals, expectedAgentName) => asRunResult(await resumeAIAgentSync(runId, approvals, { sessionService: options.sessionService }, expectedAgentName)),
|
|
52
|
+
};
|
|
53
|
+
};
|
|
@@ -296,11 +296,7 @@ export function wrapChannelWithAGUI(inner, options) {
|
|
|
296
296
|
});
|
|
297
297
|
break;
|
|
298
298
|
}
|
|
299
|
-
//
|
|
300
|
-
// pikku-specific ones. Dropping it instead — which is what this did —
|
|
301
|
-
// means a voice agent reached over HTTP is inaudible: `voiceOutput`
|
|
302
|
-
// synthesizes every sentence, the provider bills for it, and nothing
|
|
303
|
-
// gets past the mapper.
|
|
299
|
+
// knowledge: decisions/internals/agent-speech-travels-as-a-custom-agui-event.md
|
|
304
300
|
case 'audio-delta': {
|
|
305
301
|
send({
|
|
306
302
|
type: 'CUSTOM',
|
|
@@ -2,7 +2,9 @@ import { randomUUID } from 'crypto';
|
|
|
2
2
|
export function resolveMemoryServices(agent, singletonServices) {
|
|
3
3
|
const memoryConfig = agent.memory;
|
|
4
4
|
const storage = memoryConfig?.storage
|
|
5
|
-
?
|
|
5
|
+
? // The service is named by string in agent config, so the lookup cannot be
|
|
6
|
+
// checked — only the shape it is required to have.
|
|
7
|
+
singletonServices[memoryConfig.storage]
|
|
6
8
|
: singletonServices.aiStorage;
|
|
7
9
|
return { storage };
|
|
8
10
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PikkuError } from '../../errors/error-handler.js';
|
|
2
|
-
import { checkAuthPermissions, runPermissions } from '../../permissions.js';
|
|
2
|
+
import { checkAuthPermissions, runPermissions, } from '../../permissions.js';
|
|
3
3
|
import { AIProviderNotConfiguredError } from '../../errors/errors.js';
|
|
4
4
|
import { ForbiddenError } from '../../errors/errors.js';
|
|
5
5
|
import { verifyScopes } from '../../scopes.js';
|
|
@@ -648,10 +648,7 @@ export async function prepareAgentRun(agentName, input, params, agentSessionMap,
|
|
|
648
648
|
}
|
|
649
649
|
let messages = [];
|
|
650
650
|
if (storage) {
|
|
651
|
-
//
|
|
652
|
-
// thread. In voice the next turn lands within a second or two, so without
|
|
653
|
-
// this the model would load context that is missing the very thing it is
|
|
654
|
-
// about to be asked about.
|
|
651
|
+
// knowledge: decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md
|
|
655
652
|
await awaitPendingInterruptNote(threadId);
|
|
656
653
|
messages = await storage.getMessages(threadId, {
|
|
657
654
|
lastN: memoryConfig?.lastMessages ?? 20,
|
|
@@ -660,10 +657,7 @@ export async function prepareAgentRun(agentName, input, params, agentSessionMap,
|
|
|
660
657
|
const contextMessages = await loadContextMessages(memoryConfig, storage, input, workingMemoryJsonSchema);
|
|
661
658
|
const userContent = input.attachments?.length
|
|
662
659
|
? [
|
|
663
|
-
//
|
|
664
|
-
// real turn — a spoken one carries audio and no text at all — and an
|
|
665
|
-
// empty text part alongside it is a part providers are entitled to
|
|
666
|
-
// reject, for a caller who never wrote one.
|
|
660
|
+
// knowledge: decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md
|
|
667
661
|
...(input.message
|
|
668
662
|
? [{ type: 'text', text: input.message }]
|
|
669
663
|
: []),
|
|
@@ -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 {};
|