@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
|
@@ -48,6 +48,9 @@ export const signedContentPath = (urlOrPath: string): string => {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** How long a presigned upload URL stays valid. Short, like an S3 PUT URL. */
|
|
52
|
+
const UPLOAD_URL_TTL_MS = 15 * 60 * 1000
|
|
53
|
+
|
|
51
54
|
export class LocalContent implements ContentService {
|
|
52
55
|
constructor(
|
|
53
56
|
private config: LocalContentConfig,
|
|
@@ -137,13 +140,24 @@ export class LocalContent implements ContentService {
|
|
|
137
140
|
})
|
|
138
141
|
}
|
|
139
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Presigned upload, mirroring the read side: the URL carries a short-lived,
|
|
145
|
+
* path-bound signature that {@link createLocalContentRequestHandler} verifies
|
|
146
|
+
* before writing. An unsigned upload URL cannot be verified, and the handler
|
|
147
|
+
* now refuses one — so this must sign, or every upload 403s.
|
|
148
|
+
*/
|
|
140
149
|
public async getUploadURL(args: GetUploadURLArgs): Promise<UploadURLResult> {
|
|
141
150
|
void args.visibility
|
|
142
151
|
const fullKey = this.joinKey(args.bucket, args.fileKey)
|
|
143
152
|
this.logger.debug(`Going to upload with key: ${fullKey}`)
|
|
153
|
+
const uploadUrl = await this.signURL({
|
|
154
|
+
url: `${this.config.uploadUrlPrefix}/${fullKey}`,
|
|
155
|
+
dateLessThan: new Date(Date.now() + UPLOAD_URL_TTL_MS),
|
|
156
|
+
})
|
|
144
157
|
return {
|
|
145
|
-
uploadUrl
|
|
158
|
+
uploadUrl,
|
|
146
159
|
assetKey: fullKey,
|
|
160
|
+
uploadMethod: 'PUT',
|
|
147
161
|
}
|
|
148
162
|
}
|
|
149
163
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
EmailService,
|
|
3
|
+
SendEmailInput,
|
|
4
|
+
SendEmailResult,
|
|
5
|
+
} from './email-service.js'
|
|
2
6
|
|
|
3
7
|
export class LocalEmailService implements EmailService {
|
|
4
8
|
async send(input: SendEmailInput): Promise<SendEmailResult> {
|
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
SystemRoleShadowedError,
|
|
12
12
|
} from '../errors/errors.js'
|
|
13
13
|
|
|
14
|
-
const system =
|
|
14
|
+
const system =
|
|
15
|
+
(...names: string[]) =>
|
|
16
|
+
(name: string) =>
|
|
17
|
+
names.includes(name)
|
|
15
18
|
|
|
16
19
|
describe('mutating a role', () => {
|
|
17
20
|
test('a declared role refuses to be deleted or re-scoped', async () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SerializedError } from '../types/core.types.js'
|
|
1
|
+
import type { CoreUserSession, SerializedError } from '../types/core.types.js'
|
|
2
2
|
import type {
|
|
3
3
|
WorkflowRun,
|
|
4
4
|
WorkflowPlannedStep,
|
|
@@ -47,8 +47,15 @@ export interface WorkflowService {
|
|
|
47
47
|
/**
|
|
48
48
|
* The payload is stored as given and validated on replay inside the workflow
|
|
49
49
|
* body, so an invalid one re-closes the gate rather than failing the run.
|
|
50
|
+
*
|
|
51
|
+
* The session must belong to the run's owner — see `assertWorkflowRunOwner`.
|
|
50
52
|
*/
|
|
51
|
-
approveStep(
|
|
53
|
+
approveStep(
|
|
54
|
+
runId: string,
|
|
55
|
+
reason: string,
|
|
56
|
+
decision: unknown,
|
|
57
|
+
session?: CoreUserSession
|
|
58
|
+
): Promise<void>
|
|
52
59
|
startWorkflow<I>(
|
|
53
60
|
name: string,
|
|
54
61
|
input: I,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { join, dirname, relative } from 'node:path'
|
|
6
|
+
|
|
7
|
+
const srcRoot = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const packageRoot = join(srcRoot, '..')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A statement that runs when the module is imported, rather than when
|
|
12
|
+
* something in it is called. Today every one is an `addError(...)` registering
|
|
13
|
+
* an error class in the runtime registry.
|
|
14
|
+
*/
|
|
15
|
+
const TOP_LEVEL_CALL = /^[a-zA-Z_$][\w.$]*\(/
|
|
16
|
+
|
|
17
|
+
const collectSourceFiles = (directory: string): string[] =>
|
|
18
|
+
readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
19
|
+
const entryPath = join(directory, entry.name)
|
|
20
|
+
if (entry.isDirectory()) {
|
|
21
|
+
return entry.name === 'dist' ? [] : collectSourceFiles(entryPath)
|
|
22
|
+
}
|
|
23
|
+
return entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')
|
|
24
|
+
? [entryPath]
|
|
25
|
+
: []
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
/** Source modules that do something merely by being imported. */
|
|
29
|
+
const modulesWithSideEffects = (): string[] =>
|
|
30
|
+
collectSourceFiles(srcRoot)
|
|
31
|
+
.filter((file) =>
|
|
32
|
+
readFileSync(file, 'utf-8')
|
|
33
|
+
.split('\n')
|
|
34
|
+
.some((line) => TOP_LEVEL_CALL.test(line))
|
|
35
|
+
)
|
|
36
|
+
.map((file) => `./dist/${relative(srcRoot, file).replace(/\.ts$/, '.js')}`)
|
|
37
|
+
.sort()
|
|
38
|
+
|
|
39
|
+
const declared = (): string[] => {
|
|
40
|
+
const pkg = JSON.parse(
|
|
41
|
+
readFileSync(join(packageRoot, 'package.json'), 'utf-8')
|
|
42
|
+
)
|
|
43
|
+
return [...(pkg.sideEffects ?? [])].sort()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* `sideEffects` tells a bundler which modules it may not drop. Claiming
|
|
48
|
+
* `false` outright would be untrue — the error registry is built by
|
|
49
|
+
* `addError(...)` calls that run on import, and a bundler that dropped
|
|
50
|
+
* `errors/errors.js` would leave `getErrorResponse` unable to find any of
|
|
51
|
+
* them. Naming the modules exactly lets everything else be tree-shaken.
|
|
52
|
+
*
|
|
53
|
+
* knowledge: decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md
|
|
54
|
+
*/
|
|
55
|
+
describe('every module with import-time side effects is declared', () => {
|
|
56
|
+
test('the allowlist matches the modules that actually have them', () => {
|
|
57
|
+
const actual = modulesWithSideEffects()
|
|
58
|
+
const listed = declared()
|
|
59
|
+
|
|
60
|
+
assert.deepEqual(
|
|
61
|
+
{ missing: actual.filter((m) => !listed.includes(m)) },
|
|
62
|
+
{ missing: [] },
|
|
63
|
+
'a module runs code on import but is not in package.json sideEffects. ' +
|
|
64
|
+
'A bundler may drop it, and whatever it registered will be absent at ' +
|
|
65
|
+
'runtime.'
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
assert.deepEqual(
|
|
69
|
+
{ stale: listed.filter((m) => !actual.includes(m)) },
|
|
70
|
+
{ stale: [] },
|
|
71
|
+
'package.json sideEffects names a module that no longer has any — ' +
|
|
72
|
+
'remove it so bundlers can tree-shake it.'
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('the detector finds a known side effect', () => {
|
|
77
|
+
// Guards the test above: a detector matching nothing would pass whatever
|
|
78
|
+
// package.json claimed.
|
|
79
|
+
assert.ok(
|
|
80
|
+
modulesWithSideEffects().includes('./dist/errors/errors.js'),
|
|
81
|
+
'errors/errors.js registers every error class at import and must match'
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { join, dirname, relative } from 'node:path'
|
|
6
|
+
|
|
7
|
+
const srcRoot = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The ceiling a single module may reach before it has to be composed out of
|
|
11
|
+
* smaller ones.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_LINES = 2000
|
|
14
|
+
|
|
15
|
+
const collectSourceFiles = (directory: string): string[] =>
|
|
16
|
+
readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
17
|
+
const entryPath = join(directory, entry.name)
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
return entry.name === 'dist' ? [] : collectSourceFiles(entryPath)
|
|
20
|
+
}
|
|
21
|
+
return entry.name.endsWith('.ts') ? [entryPath] : []
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('source files stay composable', () => {
|
|
25
|
+
test(`no module in @pikku/core exceeds ${MAX_LINES} lines`, () => {
|
|
26
|
+
const offenders = collectSourceFiles(srcRoot)
|
|
27
|
+
.map((file) => ({
|
|
28
|
+
file: relative(srcRoot, file),
|
|
29
|
+
lines: readFileSync(file, 'utf-8').split('\n').length,
|
|
30
|
+
}))
|
|
31
|
+
.filter(({ lines }) => lines > MAX_LINES)
|
|
32
|
+
.sort((a, b) => b.lines - a.lines)
|
|
33
|
+
|
|
34
|
+
assert.deepEqual(
|
|
35
|
+
offenders,
|
|
36
|
+
[],
|
|
37
|
+
`modules over ${MAX_LINES} lines:\n` +
|
|
38
|
+
offenders.map(({ file, lines }) => ` ${lines} ${file}`).join('\n')
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, test, before } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import type { AgentRunService } from '../../wirings/ai-agent/ai-agent.types.js'
|
|
5
|
+
import type { ServiceTestConfig } from '../service-tests.js'
|
|
6
|
+
|
|
7
|
+
/** Conformance suite for `agentRunService`. Runs only when a backend supplies one. */
|
|
8
|
+
export const defineAgentRunServiceTests = (
|
|
9
|
+
name: string,
|
|
10
|
+
agentRunService: NonNullable<
|
|
11
|
+
ServiceTestConfig['services']['agentRunService']
|
|
12
|
+
>,
|
|
13
|
+
/**
|
|
14
|
+
* Thread-ownership is a property of the two services together — the run
|
|
15
|
+
* service lists what the storage service owns — so that half of the suite
|
|
16
|
+
* only runs when a backend supplies both. Passed explicitly rather than
|
|
17
|
+
* closed over, so the dependency is visible in the signature.
|
|
18
|
+
*/
|
|
19
|
+
aiStorageService?: ServiceTestConfig['services']['aiStorageService']
|
|
20
|
+
): void => {
|
|
21
|
+
const factory = agentRunService
|
|
22
|
+
describe(`AgentRunService [${name}]`, () => {
|
|
23
|
+
let agentService: AgentRunService
|
|
24
|
+
|
|
25
|
+
before(async () => {
|
|
26
|
+
agentService = await factory()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('listThreads', async () => {
|
|
30
|
+
const threads = await agentService.listThreads()
|
|
31
|
+
assert.ok(Array.isArray(threads))
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
if (aiStorageService) {
|
|
35
|
+
const storageFactory = aiStorageService
|
|
36
|
+
|
|
37
|
+
// The `owners` constraint is what keeps the generated thread-management
|
|
38
|
+
// functions from leaking across tenants: a caller may only list threads
|
|
39
|
+
// owned by one of their session principals, matching the
|
|
40
|
+
// `principal:sub-partition` composition resolveOwnerResourceId writes.
|
|
41
|
+
test('listThreads scopes to the given owners, including sub-partitions', async () => {
|
|
42
|
+
const storage = await storageFactory()
|
|
43
|
+
await storage.createThread('owner-alice')
|
|
44
|
+
await storage.createThread('owner-alice:project-1')
|
|
45
|
+
await storage.createThread('owner-bob:secret')
|
|
46
|
+
|
|
47
|
+
const threads = await agentService.listThreads({
|
|
48
|
+
owners: ['owner-alice'],
|
|
49
|
+
})
|
|
50
|
+
const ids = threads.map((t) => t.resourceId)
|
|
51
|
+
|
|
52
|
+
assert.ok(ids.includes('owner-alice'))
|
|
53
|
+
assert.ok(ids.includes('owner-alice:project-1'))
|
|
54
|
+
assert.ok(
|
|
55
|
+
!ids.some((id) => id.startsWith('owner-bob')),
|
|
56
|
+
"another owner's threads must not be listed"
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('listThreads with an owner does not match a lookalike prefix', async () => {
|
|
61
|
+
const storage = await storageFactory()
|
|
62
|
+
await storage.createThread('owner-al')
|
|
63
|
+
await storage.createThread('owner-alice-evil:p')
|
|
64
|
+
|
|
65
|
+
const threads = await agentService.listThreads({
|
|
66
|
+
owners: ['owner-al'],
|
|
67
|
+
})
|
|
68
|
+
const ids = threads.map((t) => t.resourceId)
|
|
69
|
+
|
|
70
|
+
assert.ok(ids.includes('owner-al'))
|
|
71
|
+
assert.ok(!ids.includes('owner-alice-evil:p'))
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('listThreads with an empty owners list returns nothing', async () => {
|
|
75
|
+
const storage = await storageFactory()
|
|
76
|
+
await storage.createThread('owner-empty-check')
|
|
77
|
+
|
|
78
|
+
const threads = await agentService.listThreads({ owners: [] })
|
|
79
|
+
assert.deepEqual(threads, [])
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
test('getThread returns null for missing', async () => {
|
|
84
|
+
const thread = await agentService.getThread('missing-thread')
|
|
85
|
+
assert.equal(thread, null)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('getDistinctAgentNames', async () => {
|
|
89
|
+
const names = await agentService.getDistinctAgentNames()
|
|
90
|
+
assert.ok(Array.isArray(names))
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('deleteThread returns false for missing', async () => {
|
|
94
|
+
const result = await agentService.deleteThread('missing-thread')
|
|
95
|
+
assert.equal(result, false)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { describe, test, before } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import type { AIStorageService } from '../../services/ai-storage-service.js'
|
|
5
|
+
import type { AIRunStateService } from '../../services/ai-run-state-service.js'
|
|
6
|
+
import type { ServiceTestConfig } from '../service-tests.js'
|
|
7
|
+
|
|
8
|
+
/** Conformance suite for `aiStorageService`. Runs only when a backend supplies one. */
|
|
9
|
+
export const defineAiStorageServiceTests = (
|
|
10
|
+
name: string,
|
|
11
|
+
aiStorageService: NonNullable<
|
|
12
|
+
ServiceTestConfig['services']['aiStorageService']
|
|
13
|
+
>
|
|
14
|
+
): void => {
|
|
15
|
+
const factory = aiStorageService
|
|
16
|
+
describe(`AIStorageService [${name}]`, () => {
|
|
17
|
+
let storage: AIStorageService & AIRunStateService
|
|
18
|
+
|
|
19
|
+
before(async () => {
|
|
20
|
+
storage = await factory()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('createThread and getThread', async () => {
|
|
24
|
+
const thread = await storage.createThread('resource-1', {
|
|
25
|
+
title: 'Test Thread',
|
|
26
|
+
metadata: { key: 'val' },
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
assert.ok(thread.id)
|
|
30
|
+
assert.equal(thread.resourceId, 'resource-1')
|
|
31
|
+
assert.equal(thread.title, 'Test Thread')
|
|
32
|
+
assert.deepEqual(thread.metadata, { key: 'val' })
|
|
33
|
+
|
|
34
|
+
const fetched = await storage.getThread(thread.id)
|
|
35
|
+
assert.equal(fetched.id, thread.id)
|
|
36
|
+
assert.equal(fetched.title, 'Test Thread')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('getThreads', async () => {
|
|
40
|
+
const threads = await storage.getThreads('resource-1')
|
|
41
|
+
assert.ok(threads.length >= 1)
|
|
42
|
+
assert.ok(threads.every((t) => t.resourceId === 'resource-1'))
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('getThread throws for missing', async () => {
|
|
46
|
+
await assert.rejects(() => storage.getThread('missing-thread'))
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('saveMessages and getMessages', async () => {
|
|
50
|
+
const thread = await storage.createThread('resource-2')
|
|
51
|
+
const now = new Date()
|
|
52
|
+
|
|
53
|
+
await storage.saveMessages(thread.id, [
|
|
54
|
+
{ id: 'msg-1', role: 'user', content: 'Hello', createdAt: now },
|
|
55
|
+
{
|
|
56
|
+
id: 'msg-2',
|
|
57
|
+
role: 'assistant',
|
|
58
|
+
content: 'Hi there',
|
|
59
|
+
createdAt: new Date(now.getTime() + 1000),
|
|
60
|
+
},
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
const messages = await storage.getMessages(thread.id)
|
|
64
|
+
assert.equal(messages.length, 2)
|
|
65
|
+
assert.equal(messages[0]!.role, 'user')
|
|
66
|
+
assert.equal(messages[0]!.content, 'Hello')
|
|
67
|
+
assert.equal(messages[1]!.role, 'assistant')
|
|
68
|
+
assert.equal(messages[1]!.content, 'Hi there')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('saveMessages with tool calls and results', async () => {
|
|
72
|
+
const thread = await storage.createThread('resource-3')
|
|
73
|
+
const now = new Date()
|
|
74
|
+
|
|
75
|
+
await storage.saveMessages(thread.id, [
|
|
76
|
+
{
|
|
77
|
+
id: 'msg-tc',
|
|
78
|
+
role: 'assistant',
|
|
79
|
+
content: 'Let me call a tool',
|
|
80
|
+
toolCalls: [{ id: 'tc-1', name: 'search', args: { query: 'test' } }],
|
|
81
|
+
createdAt: now,
|
|
82
|
+
},
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
await storage.saveMessages(thread.id, [
|
|
86
|
+
{
|
|
87
|
+
id: 'tool-results-msg-tc',
|
|
88
|
+
role: 'tool',
|
|
89
|
+
toolResults: [{ id: 'tc-1', name: 'search', result: 'found it' }],
|
|
90
|
+
createdAt: new Date(now.getTime() + 1000),
|
|
91
|
+
},
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
const messages = await storage.getMessages(thread.id)
|
|
95
|
+
assert.equal(messages.length, 2)
|
|
96
|
+
|
|
97
|
+
const assistantMsg = messages[0]!
|
|
98
|
+
assert.equal(assistantMsg.role, 'assistant')
|
|
99
|
+
assert.ok(assistantMsg.toolCalls)
|
|
100
|
+
assert.equal(assistantMsg.toolCalls[0]!.name, 'search')
|
|
101
|
+
|
|
102
|
+
const toolMsg = messages[1]!
|
|
103
|
+
assert.equal(toolMsg.role, 'tool')
|
|
104
|
+
assert.ok(toolMsg.toolResults)
|
|
105
|
+
assert.equal(toolMsg.toolResults[0]!.result, 'found it')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('getMessages with lastN', async () => {
|
|
109
|
+
const thread = await storage.createThread('resource-4')
|
|
110
|
+
const base = Date.now()
|
|
111
|
+
|
|
112
|
+
await storage.saveMessages(
|
|
113
|
+
thread.id,
|
|
114
|
+
Array.from({ length: 5 }, (_, i) => ({
|
|
115
|
+
id: `bulk-msg-${i}`,
|
|
116
|
+
role: 'user' as const,
|
|
117
|
+
content: `Message ${i}`,
|
|
118
|
+
createdAt: new Date(base + i * 1000),
|
|
119
|
+
}))
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
const messages = await storage.getMessages(thread.id, { lastN: 2 })
|
|
123
|
+
assert.equal(messages.length, 2)
|
|
124
|
+
assert.equal(messages[0]!.content, 'Message 3')
|
|
125
|
+
assert.equal(messages[1]!.content, 'Message 4')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('working memory: save and get', async () => {
|
|
129
|
+
await storage.saveWorkingMemory('res-1', 'resource', {
|
|
130
|
+
key: 'value',
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const mem = await storage.getWorkingMemory('res-1', 'resource')
|
|
134
|
+
assert.deepEqual(mem, { key: 'value' })
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('working memory: upsert overwrites', async () => {
|
|
138
|
+
await storage.saveWorkingMemory('res-1', 'resource', {
|
|
139
|
+
key: 'updated',
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const mem = await storage.getWorkingMemory('res-1', 'resource')
|
|
143
|
+
assert.deepEqual(mem, { key: 'updated' })
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('working memory: returns null for missing', async () => {
|
|
147
|
+
const mem = await storage.getWorkingMemory('missing', 'thread')
|
|
148
|
+
assert.equal(mem, null)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('createRun and getRun', async () => {
|
|
152
|
+
const thread = await storage.createThread('resource-5')
|
|
153
|
+
const now = new Date()
|
|
154
|
+
|
|
155
|
+
const runId = await storage.createRun({
|
|
156
|
+
agentName: 'test-agent',
|
|
157
|
+
threadId: thread.id,
|
|
158
|
+
resourceId: 'resource-5',
|
|
159
|
+
status: 'running',
|
|
160
|
+
usage: { inputTokens: 100, outputTokens: 50, model: 'test-model' },
|
|
161
|
+
createdAt: now,
|
|
162
|
+
updatedAt: now,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
assert.ok(runId)
|
|
166
|
+
|
|
167
|
+
const run = await storage.getRun(runId)
|
|
168
|
+
assert.ok(run)
|
|
169
|
+
assert.equal(run.agentName, 'test-agent')
|
|
170
|
+
assert.equal(run.status, 'running')
|
|
171
|
+
assert.equal(run.usage.inputTokens, 100)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('updateRun', async () => {
|
|
175
|
+
const thread = await storage.createThread('resource-6')
|
|
176
|
+
const now = new Date()
|
|
177
|
+
|
|
178
|
+
const runId = await storage.createRun({
|
|
179
|
+
agentName: 'update-agent',
|
|
180
|
+
threadId: thread.id,
|
|
181
|
+
resourceId: 'resource-6',
|
|
182
|
+
status: 'running',
|
|
183
|
+
usage: { inputTokens: 0, outputTokens: 0, model: 'test' },
|
|
184
|
+
createdAt: now,
|
|
185
|
+
updatedAt: now,
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
await storage.updateRun(runId, {
|
|
189
|
+
status: 'completed',
|
|
190
|
+
usage: { inputTokens: 200, outputTokens: 100, model: 'test-v2' },
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const run = await storage.getRun(runId)
|
|
194
|
+
assert.ok(run)
|
|
195
|
+
assert.equal(run.status, 'completed')
|
|
196
|
+
assert.equal(run.usage.inputTokens, 200)
|
|
197
|
+
assert.equal(run.usage.model, 'test-v2')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('getRunsByThread', async () => {
|
|
201
|
+
const thread = await storage.createThread('resource-7')
|
|
202
|
+
const now = new Date()
|
|
203
|
+
|
|
204
|
+
await storage.createRun({
|
|
205
|
+
agentName: 'multi-agent',
|
|
206
|
+
threadId: thread.id,
|
|
207
|
+
resourceId: 'resource-7',
|
|
208
|
+
status: 'completed',
|
|
209
|
+
usage: { inputTokens: 10, outputTokens: 5, model: 'test' },
|
|
210
|
+
createdAt: now,
|
|
211
|
+
updatedAt: now,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
const runs = await storage.getRunsByThread(thread.id)
|
|
215
|
+
assert.ok(runs.length >= 1)
|
|
216
|
+
assert.ok(runs.every((r) => r.threadId === thread.id))
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('resolveApproval', async () => {
|
|
220
|
+
const thread = await storage.createThread('resource-8')
|
|
221
|
+
const now = new Date()
|
|
222
|
+
|
|
223
|
+
await storage.saveMessages(thread.id, [
|
|
224
|
+
{
|
|
225
|
+
id: 'approval-msg',
|
|
226
|
+
role: 'assistant',
|
|
227
|
+
toolCalls: [{ id: 'approval-tc', name: 'dangerous-tool', args: {} }],
|
|
228
|
+
createdAt: now,
|
|
229
|
+
},
|
|
230
|
+
])
|
|
231
|
+
|
|
232
|
+
const runId = await storage.createRun({
|
|
233
|
+
agentName: 'approval-agent',
|
|
234
|
+
threadId: thread.id,
|
|
235
|
+
resourceId: 'resource-8',
|
|
236
|
+
status: 'suspended',
|
|
237
|
+
suspendReason: 'approval',
|
|
238
|
+
pendingApprovals: [
|
|
239
|
+
{
|
|
240
|
+
type: 'tool-call',
|
|
241
|
+
toolCallId: 'approval-tc',
|
|
242
|
+
toolName: 'dangerous-tool',
|
|
243
|
+
args: {},
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
usage: { inputTokens: 0, outputTokens: 0, model: 'test' },
|
|
247
|
+
createdAt: now,
|
|
248
|
+
updatedAt: now,
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
let run = await storage.getRun(runId)
|
|
252
|
+
assert.ok(run)
|
|
253
|
+
assert.ok(run.pendingApprovals)
|
|
254
|
+
assert.equal(run.pendingApprovals.length, 1)
|
|
255
|
+
|
|
256
|
+
const claimed = await storage.resolveApproval('approval-tc', 'approved')
|
|
257
|
+
assert.equal(claimed, true, 'the first approver claims the approval')
|
|
258
|
+
|
|
259
|
+
run = await storage.getRun(runId)
|
|
260
|
+
assert.ok(run)
|
|
261
|
+
assert.equal(run.pendingApprovals, undefined)
|
|
262
|
+
|
|
263
|
+
const second = await storage.resolveApproval('approval-tc', 'approved')
|
|
264
|
+
assert.equal(
|
|
265
|
+
second,
|
|
266
|
+
false,
|
|
267
|
+
'a second approver of the same tool call claims nothing'
|
|
268
|
+
)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
test('deleteThread cascades', async () => {
|
|
272
|
+
const thread = await storage.createThread('resource-del')
|
|
273
|
+
await storage.saveMessages(thread.id, [
|
|
274
|
+
{
|
|
275
|
+
id: 'del-msg',
|
|
276
|
+
role: 'user',
|
|
277
|
+
content: 'goodbye',
|
|
278
|
+
createdAt: new Date(),
|
|
279
|
+
},
|
|
280
|
+
])
|
|
281
|
+
|
|
282
|
+
await storage.deleteThread(thread.id)
|
|
283
|
+
await assert.rejects(() => storage.getThread(thread.id))
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, test, before } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import type { ChannelStore } from '../../wirings/channel/channel-store.js'
|
|
5
|
+
import type { ServiceTestConfig } from '../service-tests.js'
|
|
6
|
+
|
|
7
|
+
/** Conformance suite for `channelStore`. Runs only when a backend supplies one. */
|
|
8
|
+
export const defineChannelStoreTests = (
|
|
9
|
+
name: string,
|
|
10
|
+
channelStore: NonNullable<ServiceTestConfig['services']['channelStore']>
|
|
11
|
+
): void => {
|
|
12
|
+
const factory = channelStore
|
|
13
|
+
describe(`ChannelStore [${name}]`, () => {
|
|
14
|
+
let store: ChannelStore
|
|
15
|
+
|
|
16
|
+
before(async () => {
|
|
17
|
+
store = await factory()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('addChannel and getChannel', async () => {
|
|
21
|
+
await store.addChannel({
|
|
22
|
+
channelId: 'ch-1',
|
|
23
|
+
channelName: 'test-channel',
|
|
24
|
+
openingData: { foo: 'bar' },
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const result = await store.getChannel('ch-1')
|
|
28
|
+
assert.equal(result.channelId, 'ch-1')
|
|
29
|
+
assert.equal(result.channelName, 'test-channel')
|
|
30
|
+
assert.deepEqual(result.openingData, { foo: 'bar' })
|
|
31
|
+
assert.equal(result.pikkuUserId, undefined)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('setPikkuUserId', async () => {
|
|
35
|
+
await store.setPikkuUserId('ch-1', 'user-1')
|
|
36
|
+
|
|
37
|
+
const result = await store.getChannel('ch-1')
|
|
38
|
+
assert.equal(result.pikkuUserId, 'user-1')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('setPikkuUserId to null', async () => {
|
|
42
|
+
await store.setPikkuUserId('ch-1', null)
|
|
43
|
+
|
|
44
|
+
const result = await store.getChannel('ch-1')
|
|
45
|
+
assert.equal(result.pikkuUserId, undefined)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('getChannel throws for missing channel', async () => {
|
|
49
|
+
await assert.rejects(
|
|
50
|
+
async () => {
|
|
51
|
+
await store.getChannel('missing')
|
|
52
|
+
},
|
|
53
|
+
{ message: 'Channel not found: missing' }
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('removeChannels', async () => {
|
|
58
|
+
await store.addChannel({
|
|
59
|
+
channelId: 'ch-2',
|
|
60
|
+
channelName: 'temp-channel',
|
|
61
|
+
})
|
|
62
|
+
await store.removeChannels(['ch-2'])
|
|
63
|
+
await assert.rejects(async () => {
|
|
64
|
+
await store.getChannel('ch-2')
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('removeChannels with empty array is no-op', async () => {
|
|
69
|
+
await store.removeChannels([])
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('session round-trip — set / get / clear', async () => {
|
|
73
|
+
await store.addChannel({
|
|
74
|
+
channelId: 'ch-state',
|
|
75
|
+
channelName: 'test-channel',
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const empty = await store.getState('ch-state')
|
|
79
|
+
assert.equal(empty, undefined)
|
|
80
|
+
|
|
81
|
+
const payload = { userId: 'u-7', meta: { foo: 1 } }
|
|
82
|
+
await store.setState('ch-state', payload)
|
|
83
|
+
const got = await store.getState('ch-state')
|
|
84
|
+
assert.deepEqual(got, payload)
|
|
85
|
+
|
|
86
|
+
const next = { userId: 'u-8' }
|
|
87
|
+
await store.setState('ch-state', next)
|
|
88
|
+
const got2 = await store.getState('ch-state')
|
|
89
|
+
assert.deepEqual(got2, next)
|
|
90
|
+
|
|
91
|
+
await store.clearState('ch-state')
|
|
92
|
+
const after = await store.getState('ch-state')
|
|
93
|
+
assert.equal(after, undefined)
|
|
94
|
+
|
|
95
|
+
await store.removeChannels(['ch-state'])
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
}
|