@retinue/agentkit 0.1.0
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/LICENSE +22 -0
- package/README.md +310 -0
- package/dist/adapters/bullmq/consumer.d.ts +33 -0
- package/dist/adapters/bullmq/consumer.js +41 -0
- package/dist/adapters/bullmq/dispatcher.d.ts +74 -0
- package/dist/adapters/bullmq/dispatcher.js +160 -0
- package/dist/adapters/bullmq/export.d.ts +31 -0
- package/dist/adapters/bullmq/export.js +53 -0
- package/dist/adapters/bullmq/extraction.d.ts +42 -0
- package/dist/adapters/bullmq/extraction.js +63 -0
- package/dist/adapters/bullmq/index.d.ts +13 -0
- package/dist/adapters/bullmq/index.js +13 -0
- package/dist/adapters/bullmq/lock.d.ts +77 -0
- package/dist/adapters/bullmq/lock.js +126 -0
- package/dist/adapters/bullmq/queue.d.ts +50 -0
- package/dist/adapters/bullmq/queue.js +81 -0
- package/dist/adapters/memory/artifact-exports.d.ts +11 -0
- package/dist/adapters/memory/artifact-exports.js +102 -0
- package/dist/adapters/memory/artifacts.d.ts +15 -0
- package/dist/adapters/memory/artifacts.js +134 -0
- package/dist/adapters/memory/blobs.d.ts +7 -0
- package/dist/adapters/memory/blobs.js +27 -0
- package/dist/adapters/memory/evaluation.d.ts +18 -0
- package/dist/adapters/memory/evaluation.js +148 -0
- package/dist/adapters/memory/files.d.ts +27 -0
- package/dist/adapters/memory/files.js +0 -0
- package/dist/adapters/memory/flows.d.ts +16 -0
- package/dist/adapters/memory/flows.js +117 -0
- package/dist/adapters/memory/hitl.d.ts +9 -0
- package/dist/adapters/memory/hitl.js +130 -0
- package/dist/adapters/memory/idempotency.d.ts +13 -0
- package/dist/adapters/memory/idempotency.js +32 -0
- package/dist/adapters/memory/index.d.ts +39 -0
- package/dist/adapters/memory/index.js +107 -0
- package/dist/adapters/memory/knowledge.d.ts +43 -0
- package/dist/adapters/memory/knowledge.js +248 -0
- package/dist/adapters/memory/mcp.d.ts +9 -0
- package/dist/adapters/memory/mcp.js +37 -0
- package/dist/adapters/memory/message-store.d.ts +17 -0
- package/dist/adapters/memory/message-store.js +70 -0
- package/dist/adapters/memory/principal-memory.d.ts +7 -0
- package/dist/adapters/memory/principal-memory.js +83 -0
- package/dist/adapters/memory/runtime.d.ts +29 -0
- package/dist/adapters/memory/runtime.js +0 -0
- package/dist/adapters/memory/sessions.d.ts +29 -0
- package/dist/adapters/memory/sessions.js +0 -0
- package/dist/adapters/memory/skills.d.ts +10 -0
- package/dist/adapters/memory/skills.js +41 -0
- package/dist/adapters/memory/thread-summary.d.ts +7 -0
- package/dist/adapters/memory/thread-summary.js +29 -0
- package/dist/adapters/memory/usage-limits.d.ts +13 -0
- package/dist/adapters/memory/usage-limits.js +72 -0
- package/dist/adapters/memory/usage.d.ts +16 -0
- package/dist/adapters/memory/usage.js +279 -0
- package/dist/adapters/otel/index.d.ts +111 -0
- package/dist/adapters/otel/index.js +133 -0
- package/dist/adapters/postgres/artifact-exports.d.ts +12 -0
- package/dist/adapters/postgres/artifact-exports.js +117 -0
- package/dist/adapters/postgres/artifacts.d.ts +16 -0
- package/dist/adapters/postgres/artifacts.js +172 -0
- package/dist/adapters/postgres/checkpoint-store.d.ts +16 -0
- package/dist/adapters/postgres/checkpoint-store.js +34 -0
- package/dist/adapters/postgres/config.d.ts +15 -0
- package/dist/adapters/postgres/config.js +187 -0
- package/dist/adapters/postgres/conversation-store.d.ts +4 -0
- package/dist/adapters/postgres/conversation-store.js +82 -0
- package/dist/adapters/postgres/evaluation.d.ts +17 -0
- package/dist/adapters/postgres/evaluation.js +193 -0
- package/dist/adapters/postgres/file-content.d.ts +30 -0
- package/dist/adapters/postgres/file-content.js +111 -0
- package/dist/adapters/postgres/files.d.ts +19 -0
- package/dist/adapters/postgres/files.js +209 -0
- package/dist/adapters/postgres/flows.d.ts +20 -0
- package/dist/adapters/postgres/flows.js +206 -0
- package/dist/adapters/postgres/hitl.d.ts +5 -0
- package/dist/adapters/postgres/hitl.js +247 -0
- package/dist/adapters/postgres/index.d.ts +35 -0
- package/dist/adapters/postgres/index.js +35 -0
- package/dist/adapters/postgres/knowledge.d.ts +48 -0
- package/dist/adapters/postgres/knowledge.js +255 -0
- package/dist/adapters/postgres/memory.d.ts +14 -0
- package/dist/adapters/postgres/memory.js +194 -0
- package/dist/adapters/postgres/message-store.d.ts +11 -0
- package/dist/adapters/postgres/message-store.js +145 -0
- package/dist/adapters/postgres/migrations.d.ts +69 -0
- package/dist/adapters/postgres/migrations.js +1594 -0
- package/dist/adapters/postgres/pg-executor.d.ts +19 -0
- package/dist/adapters/postgres/pg-executor.js +32 -0
- package/dist/adapters/postgres/retention.d.ts +26 -0
- package/dist/adapters/postgres/retention.js +59 -0
- package/dist/adapters/postgres/rollups.d.ts +17 -0
- package/dist/adapters/postgres/rollups.js +267 -0
- package/dist/adapters/postgres/run-coordinator.d.ts +5 -0
- package/dist/adapters/postgres/run-coordinator.js +98 -0
- package/dist/adapters/postgres/run-event-log.d.ts +26 -0
- package/dist/adapters/postgres/run-event-log.js +30 -0
- package/dist/adapters/postgres/run-store.d.ts +4 -0
- package/dist/adapters/postgres/run-store.js +199 -0
- package/dist/adapters/postgres/schema.d.ts +39 -0
- package/dist/adapters/postgres/schema.js +70 -0
- package/dist/adapters/postgres/session-state.d.ts +7 -0
- package/dist/adapters/postgres/session-state.js +99 -0
- package/dist/adapters/postgres/sql.d.ts +8 -0
- package/dist/adapters/postgres/sql.js +2 -0
- package/dist/adapters/postgres/transaction.d.ts +37 -0
- package/dist/adapters/postgres/transaction.js +93 -0
- package/dist/adapters/postgres/unit-of-work.d.ts +18 -0
- package/dist/adapters/postgres/unit-of-work.js +8 -0
- package/dist/adapters/postgres/usage-limits.d.ts +15 -0
- package/dist/adapters/postgres/usage-limits.js +136 -0
- package/dist/adapters/postgres/usage.d.ts +15 -0
- package/dist/adapters/postgres/usage.js +226 -0
- package/dist/adapters/redis/index.d.ts +9 -0
- package/dist/adapters/redis/index.js +9 -0
- package/dist/adapters/redis/realtime.d.ts +74 -0
- package/dist/adapters/redis/realtime.js +112 -0
- package/dist/adapters/supabase/index.d.ts +88 -0
- package/dist/adapters/supabase/index.js +84 -0
- package/dist/adapters/supabase/realtime.d.ts +30 -0
- package/dist/adapters/supabase/realtime.js +53 -0
- package/dist/adapters/supabase/rls.d.ts +99 -0
- package/dist/adapters/supabase/rls.js +216 -0
- package/dist/adapters/supabase/storage.d.ts +50 -0
- package/dist/adapters/supabase/storage.js +207 -0
- package/dist/agents/agent.d.ts +66 -0
- package/dist/agents/agent.js +209 -0
- package/dist/agents/define.d.ts +21 -0
- package/dist/agents/define.js +63 -0
- package/dist/agents/engine.d.ts +98 -0
- package/dist/agents/engine.js +462 -0
- package/dist/agents/index.d.ts +50 -0
- package/dist/agents/index.js +17 -0
- package/dist/artifacts/index.d.ts +114 -0
- package/dist/artifacts/index.js +219 -0
- package/dist/authorization/index.d.ts +76 -0
- package/dist/authorization/index.js +76 -0
- package/dist/capabilities/index.d.ts +120 -0
- package/dist/capabilities/index.js +167 -0
- package/dist/capabilities/runtime.d.ts +89 -0
- package/dist/capabilities/runtime.js +84 -0
- package/dist/citations/index.d.ts +161 -0
- package/dist/citations/index.js +182 -0
- package/dist/context/assembler.d.ts +82 -0
- package/dist/context/assembler.js +129 -0
- package/dist/context/compaction.d.ts +45 -0
- package/dist/context/compaction.js +55 -0
- package/dist/context/index.d.ts +75 -0
- package/dist/context/index.js +17 -0
- package/dist/core/content-parts.d.ts +194 -0
- package/dist/core/content-parts.js +23 -0
- package/dist/core/context.d.ts +51 -0
- package/dist/core/context.js +9 -0
- package/dist/core/env.d.ts +25 -0
- package/dist/core/env.js +41 -0
- package/dist/core/errors.d.ts +30 -0
- package/dist/core/errors.js +65 -0
- package/dist/core/events.d.ts +139 -0
- package/dist/core/events.js +99 -0
- package/dist/core/ids.d.ts +52 -0
- package/dist/core/ids.js +13 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.js +9 -0
- package/dist/core/tokens.d.ts +22 -0
- package/dist/core/tokens.js +22 -0
- package/dist/core/validation.d.ts +34 -0
- package/dist/core/validation.js +176 -0
- package/dist/documents/extraction.d.ts +121 -0
- package/dist/documents/extraction.js +293 -0
- package/dist/documents/index.d.ts +199 -0
- package/dist/documents/index.js +65 -0
- package/dist/documents/parsers/pdf.d.ts +47 -0
- package/dist/documents/parsers/pdf.js +508 -0
- package/dist/documents/parsers/text.d.ts +59 -0
- package/dist/documents/parsers/text.js +325 -0
- package/dist/documents/read-tool.d.ts +52 -0
- package/dist/documents/read-tool.js +109 -0
- package/dist/documents/render.d.ts +29 -0
- package/dist/documents/render.js +59 -0
- package/dist/documents/vision.d.ts +159 -0
- package/dist/documents/vision.js +214 -0
- package/dist/entries/adapters-bullmq.d.ts +8 -0
- package/dist/entries/adapters-bullmq.js +8 -0
- package/dist/entries/adapters-otel.d.ts +13 -0
- package/dist/entries/adapters-otel.js +13 -0
- package/dist/entries/adapters-postgres.d.ts +10 -0
- package/dist/entries/adapters-postgres.js +10 -0
- package/dist/entries/adapters-redis.d.ts +3 -0
- package/dist/entries/adapters-redis.js +3 -0
- package/dist/entries/context.d.ts +20 -0
- package/dist/entries/context.js +20 -0
- package/dist/entries/flows.d.ts +15 -0
- package/dist/entries/flows.js +15 -0
- package/dist/entries/hitl.d.ts +10 -0
- package/dist/entries/hitl.js +10 -0
- package/dist/entries/knowledge.d.ts +18 -0
- package/dist/entries/knowledge.js +19 -0
- package/dist/entries/mcp.d.ts +10 -0
- package/dist/entries/mcp.js +10 -0
- package/dist/entries/observability.d.ts +14 -0
- package/dist/entries/observability.js +16 -0
- package/dist/entries/persistence.d.ts +11 -0
- package/dist/entries/persistence.js +11 -0
- package/dist/entries/providers.d.ts +14 -0
- package/dist/entries/providers.js +14 -0
- package/dist/entries/runtime.d.ts +13 -0
- package/dist/entries/runtime.js +13 -0
- package/dist/entries/server.d.ts +24 -0
- package/dist/entries/server.js +24 -0
- package/dist/entries/tools.d.ts +21 -0
- package/dist/entries/tools.js +21 -0
- package/dist/entries/usage.d.ts +10 -0
- package/dist/entries/usage.js +10 -0
- package/dist/evaluation/gate.d.ts +168 -0
- package/dist/evaluation/gate.js +180 -0
- package/dist/evaluation/graders.d.ts +125 -0
- package/dist/evaluation/graders.js +203 -0
- package/dist/evaluation/index.d.ts +120 -0
- package/dist/evaluation/index.js +183 -0
- package/dist/evaluation/judge.d.ts +75 -0
- package/dist/evaluation/judge.js +111 -0
- package/dist/export/index.d.ts +162 -0
- package/dist/export/index.js +363 -0
- package/dist/export/markdown.d.ts +19 -0
- package/dist/export/markdown.js +29 -0
- package/dist/export/pdf.d.ts +73 -0
- package/dist/export/pdf.js +407 -0
- package/dist/files/context.d.ts +97 -0
- package/dist/files/context.js +185 -0
- package/dist/files/index.d.ts +210 -0
- package/dist/files/index.js +338 -0
- package/dist/files/read-tool.d.ts +81 -0
- package/dist/files/read-tool.js +163 -0
- package/dist/files/turn-parts.d.ts +96 -0
- package/dist/files/turn-parts.js +171 -0
- package/dist/flows/index.d.ts +270 -0
- package/dist/flows/index.js +62 -0
- package/dist/flows/interpreter.d.ts +146 -0
- package/dist/flows/interpreter.js +426 -0
- package/dist/flows/runner.d.ts +145 -0
- package/dist/flows/runner.js +270 -0
- package/dist/graphql/index.d.ts +8 -0
- package/dist/graphql/index.js +8 -0
- package/dist/graphql/resolvers.d.ts +237 -0
- package/dist/graphql/resolvers.js +253 -0
- package/dist/graphql/schema.d.ts +11 -0
- package/dist/graphql/schema.js +258 -0
- package/dist/graphql/sse.d.ts +77 -0
- package/dist/graphql/sse.js +100 -0
- package/dist/hitl/approved-execution.d.ts +127 -0
- package/dist/hitl/approved-execution.js +177 -0
- package/dist/hitl/index.d.ts +79 -0
- package/dist/hitl/index.js +12 -0
- package/dist/hitl/service.d.ts +221 -0
- package/dist/hitl/service.js +268 -0
- package/dist/idempotency/index.d.ts +70 -0
- package/dist/idempotency/index.js +59 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +60 -0
- package/dist/knowledge/chunking.d.ts +57 -0
- package/dist/knowledge/chunking.js +158 -0
- package/dist/knowledge/index.d.ts +119 -0
- package/dist/knowledge/index.js +166 -0
- package/dist/knowledge/retrieval.d.ts +146 -0
- package/dist/knowledge/retrieval.js +170 -0
- package/dist/loadtest/harness.d.ts +168 -0
- package/dist/loadtest/harness.js +507 -0
- package/dist/loadtest/index.d.ts +13 -0
- package/dist/loadtest/index.js +13 -0
- package/dist/loadtest/injection.d.ts +89 -0
- package/dist/loadtest/injection.js +147 -0
- package/dist/loadtest/metrics.d.ts +197 -0
- package/dist/loadtest/metrics.js +160 -0
- package/dist/loadtest/runbooks.d.ts +28 -0
- package/dist/loadtest/runbooks.js +159 -0
- package/dist/loadtest/scenario.d.ts +104 -0
- package/dist/loadtest/scenario.js +208 -0
- package/dist/mcp/egress.d.ts +53 -0
- package/dist/mcp/egress.js +115 -0
- package/dist/mcp/index.d.ts +93 -0
- package/dist/mcp/index.js +33 -0
- package/dist/mcp/provider.d.ts +62 -0
- package/dist/mcp/provider.js +0 -0
- package/dist/models/index.d.ts +98 -0
- package/dist/models/index.js +74 -0
- package/dist/models/pricing.d.ts +24 -0
- package/dist/models/pricing.js +37 -0
- package/dist/models/provider-factory.d.ts +31 -0
- package/dist/models/provider-factory.js +67 -0
- package/dist/models/streaming.d.ts +145 -0
- package/dist/models/streaming.js +272 -0
- package/dist/models/vision.d.ts +38 -0
- package/dist/models/vision.js +62 -0
- package/dist/persistence/index.d.ts +1654 -0
- package/dist/persistence/index.js +226 -0
- package/dist/principal-memory/index.d.ts +106 -0
- package/dist/principal-memory/index.js +89 -0
- package/dist/retention/index.d.ts +89 -0
- package/dist/retention/index.js +70 -0
- package/dist/runtime/checkpoint.d.ts +37 -0
- package/dist/runtime/checkpoint.js +22 -0
- package/dist/runtime/index.d.ts +118 -0
- package/dist/runtime/index.js +69 -0
- package/dist/runtime/retry.d.ts +95 -0
- package/dist/runtime/retry.js +126 -0
- package/dist/runtime/serialization.d.ts +85 -0
- package/dist/runtime/serialization.js +95 -0
- package/dist/runtime/streaming.d.ts +54 -0
- package/dist/runtime/streaming.js +115 -0
- package/dist/runtime/worker.d.ts +130 -0
- package/dist/runtime/worker.js +405 -0
- package/dist/security/checklist.d.ts +53 -0
- package/dist/security/checklist.js +204 -0
- package/dist/security/findings.d.ts +56 -0
- package/dist/security/findings.js +168 -0
- package/dist/security/index.d.ts +14 -0
- package/dist/security/index.js +14 -0
- package/dist/security/prompt-safety.d.ts +100 -0
- package/dist/security/prompt-safety.js +133 -0
- package/dist/server/boot.d.ts +32 -0
- package/dist/server/boot.js +36 -0
- package/dist/server/cli-worker.d.ts +37 -0
- package/dist/server/cli-worker.js +151 -0
- package/dist/server/cli.d.ts +27 -0
- package/dist/server/cli.js +74 -0
- package/dist/server/config.d.ts +42 -0
- package/dist/server/config.js +127 -0
- package/dist/server/health.d.ts +59 -0
- package/dist/server/health.js +90 -0
- package/dist/server/host.d.ts +39 -0
- package/dist/server/host.js +124 -0
- package/dist/server/index.d.ts +15 -0
- package/dist/server/index.js +15 -0
- package/dist/server/main.d.ts +16 -0
- package/dist/server/main.js +31 -0
- package/dist/server/sse-route.d.ts +21 -0
- package/dist/server/sse-route.js +282 -0
- package/dist/skills/index.d.ts +67 -0
- package/dist/skills/index.js +31 -0
- package/dist/skills/resolver.d.ts +54 -0
- package/dist/skills/resolver.js +121 -0
- package/dist/teams/index.d.ts +93 -0
- package/dist/teams/index.js +207 -0
- package/dist/telemetry/index.d.ts +157 -0
- package/dist/telemetry/index.js +71 -0
- package/dist/telemetry/instrument.d.ts +108 -0
- package/dist/telemetry/instrument.js +232 -0
- package/dist/telemetry/log-events.d.ts +17 -0
- package/dist/telemetry/log-events.js +58 -0
- package/dist/telemetry/metrics.d.ts +123 -0
- package/dist/telemetry/metrics.js +135 -0
- package/dist/telemetry/noop.d.ts +39 -0
- package/dist/telemetry/noop.js +143 -0
- package/dist/telemetry/redaction.d.ts +64 -0
- package/dist/telemetry/redaction.js +153 -0
- package/dist/telemetry/spans.d.ts +56 -0
- package/dist/telemetry/spans.js +78 -0
- package/dist/telemetry/trace-context.d.ts +55 -0
- package/dist/telemetry/trace-context.js +60 -0
- package/dist/toolkit/compute.d.ts +53 -0
- package/dist/toolkit/compute.js +152 -0
- package/dist/toolkit/data.d.ts +98 -0
- package/dist/toolkit/data.js +235 -0
- package/dist/toolkit/http.d.ts +113 -0
- package/dist/toolkit/http.js +205 -0
- package/dist/toolkit/index.d.ts +21 -0
- package/dist/toolkit/index.js +17 -0
- package/dist/toolkit/web.d.ts +107 -0
- package/dist/toolkit/web.js +147 -0
- package/dist/tools/define.d.ts +25 -0
- package/dist/tools/define.js +45 -0
- package/dist/tools/delegating.d.ts +132 -0
- package/dist/tools/delegating.js +211 -0
- package/dist/tools/index.d.ts +129 -0
- package/dist/tools/index.js +33 -0
- package/dist/tools/library/compute.d.ts +11 -0
- package/dist/tools/library/compute.js +46 -0
- package/dist/tools/library/data.d.ts +16 -0
- package/dist/tools/library/data.js +92 -0
- package/dist/tools/library/http.d.ts +28 -0
- package/dist/tools/library/http.js +71 -0
- package/dist/tools/library/index.d.ts +97 -0
- package/dist/tools/library/index.js +134 -0
- package/dist/tools/library/knowledge.d.ts +39 -0
- package/dist/tools/library/knowledge.js +58 -0
- package/dist/tools/library/web.d.ts +19 -0
- package/dist/tools/library/web.js +65 -0
- package/dist/tools/meta-tools.d.ts +19 -0
- package/dist/tools/meta-tools.js +36 -0
- package/dist/tools/registry.d.ts +146 -0
- package/dist/tools/registry.js +291 -0
- package/dist/usage/index.d.ts +105 -0
- package/dist/usage/index.js +20 -0
- package/dist/usage/quota.d.ts +258 -0
- package/dist/usage/quota.js +510 -0
- package/dist/usage/recorder.d.ts +29 -0
- package/dist/usage/recorder.js +96 -0
- package/dist/usage/rollups.d.ts +121 -0
- package/dist/usage/rollups.js +157 -0
- package/dist/worker/export.d.ts +57 -0
- package/dist/worker/export.js +81 -0
- package/dist/worker/extraction.d.ts +57 -0
- package/dist/worker/extraction.js +84 -0
- package/dist/worker/main.d.ts +103 -0
- package/dist/worker/main.js +159 -0
- package/package.json +187 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pgvector `KnowledgeStore` and `VectorIndex` (#135).
|
|
3
|
+
*
|
|
4
|
+
* One table backs both ports, which is the point of splitting them: a deployment on pgvector answers both from
|
|
5
|
+
* `knowledge_chunks`, and a deployment on a dedicated vector database answers them from two systems, with
|
|
6
|
+
* nothing above changing.
|
|
7
|
+
*
|
|
8
|
+
* **AC-3 is a `WHERE` clause, and it is the reason this adapter exists rather than a filter above it.** The
|
|
9
|
+
* permission predicate is in the same statement as the nearest-neighbour scan, so an unauthorised chunk is
|
|
10
|
+
* never a candidate. Filtering afterwards leaks through result counts — ask for ten, get three, and you have
|
|
11
|
+
* learned that seven exist you may not see. `EXPLAIN` shows the predicate inside the scan, which is what the
|
|
12
|
+
* test asserts.
|
|
13
|
+
*
|
|
14
|
+
* **`replaceSource` is one transaction-shaped pair of statements.** Delete-then-insert, in that order, so a
|
|
15
|
+
* changed document's old chunks are never searchable alongside its new ones. Appending would leave a stale
|
|
16
|
+
* chunk that cites text no longer in the document.
|
|
17
|
+
*/
|
|
18
|
+
import { AgentPlatformError } from "../../core/errors.js";
|
|
19
|
+
import { stripStopwords } from "../../persistence/index.js";
|
|
20
|
+
import { VECTOR_DIMENSIONS } from "./migrations.js";
|
|
21
|
+
const iso = (v) => (v instanceof Date ? v.toISOString() : v);
|
|
22
|
+
const toChunk = (r) => ({
|
|
23
|
+
id: r.id,
|
|
24
|
+
sourceType: r.source_type,
|
|
25
|
+
sourceId: r.source_id,
|
|
26
|
+
chunkIndex: Number(r.chunk_index),
|
|
27
|
+
content: r.content,
|
|
28
|
+
tokenCount: Number(r.token_count),
|
|
29
|
+
authSubject: r.auth_subject,
|
|
30
|
+
embeddingModel: {
|
|
31
|
+
modelId: r.embedding_model,
|
|
32
|
+
version: r.embedding_version,
|
|
33
|
+
dimensions: Number(r.embedding_dims),
|
|
34
|
+
},
|
|
35
|
+
...(r.locator === null ? {} : { locator: r.locator }),
|
|
36
|
+
createdAt: iso(r.created_at),
|
|
37
|
+
});
|
|
38
|
+
const COLUMNS = `id, source_type, source_id, chunk_index, content, token_count, auth_subject,
|
|
39
|
+
embedding_model, embedding_version, embedding_dims, locator, created_at`;
|
|
40
|
+
/**
|
|
41
|
+
* pgvector's literal form: `[0.1,0.2,...]`.
|
|
42
|
+
*
|
|
43
|
+
* Built as a string rather than passed as an array, because node-postgres has no vector type and would send an
|
|
44
|
+
* array as `{...}` — which pgvector rejects. `Number.isFinite` is checked because a `NaN` in a vector makes
|
|
45
|
+
* every distance `NaN`, and the symptom is a search that returns nothing rather than an error.
|
|
46
|
+
*/
|
|
47
|
+
export const toVectorLiteral = (embedding) => {
|
|
48
|
+
if (embedding.length !== VECTOR_DIMENSIONS)
|
|
49
|
+
throw new AgentPlatformError({
|
|
50
|
+
code: "invalid_input",
|
|
51
|
+
message: `embedding has ${embedding.length} dimensions but the schema declares ${VECTOR_DIMENSIONS}`,
|
|
52
|
+
retryable: false,
|
|
53
|
+
});
|
|
54
|
+
for (const value of embedding)
|
|
55
|
+
if (!Number.isFinite(value))
|
|
56
|
+
throw new AgentPlatformError({
|
|
57
|
+
code: "invalid_input",
|
|
58
|
+
message: "embedding contains a non-finite value",
|
|
59
|
+
retryable: false,
|
|
60
|
+
});
|
|
61
|
+
return `[${embedding.join(",")}]`;
|
|
62
|
+
};
|
|
63
|
+
export const createPostgresKnowledgeStore = (sql) => ({
|
|
64
|
+
async replaceSource({ tenantId, sourceType, sourceId, chunks }) {
|
|
65
|
+
for (const chunk of chunks) {
|
|
66
|
+
if (chunk.embedding.length !== chunk.embeddingModel.dimensions)
|
|
67
|
+
// Caught before the write: a vector of the wrong length would score against whatever prefix overlapped,
|
|
68
|
+
// which looks like bad relevance rather than a bug.
|
|
69
|
+
throw new AgentPlatformError({
|
|
70
|
+
code: "invalid_input",
|
|
71
|
+
message: `chunk ${chunk.id} has ${chunk.embedding.length} dimensions but its model declares ${chunk.embeddingModel.dimensions}`,
|
|
72
|
+
retryable: false,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// Delete first. Old chunks must never be searchable next to new ones: a stale chunk cites text that is no
|
|
76
|
+
// longer in the document, which is worse than the document being briefly absent.
|
|
77
|
+
const removedRows = await sql.query(`DELETE FROM knowledge_chunks
|
|
78
|
+
WHERE tenant_id = $1 AND source_type = $2 AND source_id = $3
|
|
79
|
+
RETURNING id`, [tenantId, sourceType, sourceId]);
|
|
80
|
+
for (const chunk of chunks) {
|
|
81
|
+
await sql.query(`INSERT INTO knowledge_chunks (tenant_id, id, source_type, source_id, chunk_index, content,
|
|
82
|
+
token_count, auth_subject, embedding, embedding_model,
|
|
83
|
+
embedding_version, embedding_dims, locator, created_at)
|
|
84
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::vector,$10,$11,$12,$13,$14)
|
|
85
|
+
-- A re-run of the same batch overwrites its own rows rather than failing: an interrupted re-index is
|
|
86
|
+
-- resumed by running it again, and that has to be safe (AC-5).
|
|
87
|
+
ON CONFLICT (tenant_id, id) DO UPDATE SET
|
|
88
|
+
chunk_index = EXCLUDED.chunk_index,
|
|
89
|
+
content = EXCLUDED.content,
|
|
90
|
+
token_count = EXCLUDED.token_count,
|
|
91
|
+
auth_subject = EXCLUDED.auth_subject,
|
|
92
|
+
embedding = EXCLUDED.embedding,
|
|
93
|
+
embedding_model = EXCLUDED.embedding_model,
|
|
94
|
+
embedding_version = EXCLUDED.embedding_version,
|
|
95
|
+
embedding_dims = EXCLUDED.embedding_dims,
|
|
96
|
+
locator = EXCLUDED.locator`, [
|
|
97
|
+
tenantId,
|
|
98
|
+
chunk.id,
|
|
99
|
+
sourceType,
|
|
100
|
+
sourceId,
|
|
101
|
+
chunk.chunkIndex,
|
|
102
|
+
chunk.content,
|
|
103
|
+
chunk.tokenCount,
|
|
104
|
+
chunk.authSubject,
|
|
105
|
+
toVectorLiteral(chunk.embedding),
|
|
106
|
+
chunk.embeddingModel.modelId,
|
|
107
|
+
chunk.embeddingModel.version,
|
|
108
|
+
chunk.embeddingModel.dimensions,
|
|
109
|
+
chunk.locator ?? null,
|
|
110
|
+
chunk.createdAt,
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
return { written: chunks.length, removed: removedRows.length };
|
|
114
|
+
},
|
|
115
|
+
async listBySource({ tenantId, sourceType, sourceId, limit, cursor }) {
|
|
116
|
+
// Cursored on `chunk_index`, which is contiguous within a source and therefore exact -- and "the chunks of
|
|
117
|
+
// this document" is an ordered thing, so the order must be the document's.
|
|
118
|
+
const after = cursor === undefined ? -1 : Number.parseInt(cursor, 10);
|
|
119
|
+
const from = Number.isSafeInteger(after) ? after : -1;
|
|
120
|
+
const rows = await sql.query(`SELECT ${COLUMNS} FROM knowledge_chunks
|
|
121
|
+
WHERE tenant_id = $1 AND source_type = $2 AND source_id = $3 AND chunk_index > $4
|
|
122
|
+
ORDER BY chunk_index
|
|
123
|
+
LIMIT $5`, [tenantId, sourceType, sourceId, from, limit + 1]);
|
|
124
|
+
const items = rows.slice(0, limit).map(toChunk);
|
|
125
|
+
const last = items[items.length - 1];
|
|
126
|
+
return rows.length > limit && last !== undefined
|
|
127
|
+
? { items, nextCursor: String(last.chunkIndex) }
|
|
128
|
+
: { items };
|
|
129
|
+
},
|
|
130
|
+
async get({ tenantId, id }) {
|
|
131
|
+
const rows = await sql.query(`SELECT ${COLUMNS} FROM knowledge_chunks WHERE tenant_id = $1 AND id = $2`, [tenantId, id]);
|
|
132
|
+
// A foreign id yields no row, so it is null without a comparison anyone could get wrong.
|
|
133
|
+
return rows[0] === undefined ? null : toChunk(rows[0]);
|
|
134
|
+
},
|
|
135
|
+
async deleteSource({ tenantId, sourceType, sourceId }) {
|
|
136
|
+
const rows = await sql.query(`DELETE FROM knowledge_chunks
|
|
137
|
+
WHERE tenant_id = $1 AND source_type = $2 AND source_id = $3
|
|
138
|
+
RETURNING id`, [tenantId, sourceType, sourceId]);
|
|
139
|
+
return { removed: rows.length };
|
|
140
|
+
},
|
|
141
|
+
async listStaleSources({ tenantId, current, limit, cursor }) {
|
|
142
|
+
const rows = await sql.query(`SELECT source_type, source_id, COUNT(*) AS chunk_count
|
|
143
|
+
FROM knowledge_chunks
|
|
144
|
+
WHERE tenant_id = $1
|
|
145
|
+
-- Anything not embedded by exactly this model. Derived from what is *stored*, which is what makes an
|
|
146
|
+
-- interrupted re-index resumable: it asks again rather than remembering where it was.
|
|
147
|
+
AND NOT (embedding_model = $2 AND embedding_version = $3 AND embedding_dims = $4)
|
|
148
|
+
AND ($5::text IS NULL OR (source_type || ' ' || source_id) > $5::text)
|
|
149
|
+
GROUP BY source_type, source_id
|
|
150
|
+
-- Stable order, so a resumed re-index does not revisit sources it finished and skip ones it did not.
|
|
151
|
+
ORDER BY source_type, source_id
|
|
152
|
+
LIMIT $6`, [tenantId, current.modelId, current.version, current.dimensions, cursor ?? null, limit + 1]);
|
|
153
|
+
const items = rows.slice(0, limit).map((r) => ({
|
|
154
|
+
sourceType: r.source_type,
|
|
155
|
+
sourceId: r.source_id,
|
|
156
|
+
chunkCount: Number(r.chunk_count),
|
|
157
|
+
}));
|
|
158
|
+
const last = items[items.length - 1];
|
|
159
|
+
return rows.length > limit && last !== undefined
|
|
160
|
+
? { items, nextCursor: `${last.sourceType} ${last.sourceId}` }
|
|
161
|
+
: { items };
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
export const createPostgresVectorIndex = (sql) => ({
|
|
165
|
+
async search({ tenantId, embedding, authSubjects, limit, minScore, sourceTypes }) {
|
|
166
|
+
// An empty subject list means "no subjects", which correctly returns nothing. Short-circuited so the
|
|
167
|
+
// database is not asked a question whose answer is already known -- and so `= ANY('{}')` semantics are
|
|
168
|
+
// never relied on.
|
|
169
|
+
if (authSubjects.length === 0)
|
|
170
|
+
return [];
|
|
171
|
+
const rows = await sql.query(`SELECT ${COLUMNS},
|
|
172
|
+
-- Cosine *distance* is 0 (identical) to 2 (opposite); the ports normalise to a 0-1 score where
|
|
173
|
+
-- higher is closer, so a caller never has to know the metric.
|
|
174
|
+
1 - (embedding <=> $2::vector) / 2 AS score
|
|
175
|
+
FROM knowledge_chunks
|
|
176
|
+
WHERE tenant_id = $1
|
|
177
|
+
-- AC-3: the permission predicate is in the same statement as the scan, so an unauthorised chunk is
|
|
178
|
+
-- never a candidate. EXPLAIN shows it inside the scan, which is what the test asserts.
|
|
179
|
+
AND auth_subject = ANY($3::text[])
|
|
180
|
+
AND ($4::text[] IS NULL OR source_type = ANY($4::text[]))
|
|
181
|
+
AND ($5::float8 IS NULL OR 1 - (embedding <=> $2::vector) / 2 >= $5::float8)
|
|
182
|
+
ORDER BY embedding <=> $2::vector, id
|
|
183
|
+
LIMIT $6`, [
|
|
184
|
+
tenantId,
|
|
185
|
+
toVectorLiteral(embedding),
|
|
186
|
+
authSubjects,
|
|
187
|
+
sourceTypes === undefined ? null : [...sourceTypes],
|
|
188
|
+
minScore ?? null,
|
|
189
|
+
limit,
|
|
190
|
+
]);
|
|
191
|
+
return rows.map((r) => ({ chunk: toChunk(r), score: Number(r.score) }));
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
/** Re-exported so a caller comparing a stored chunk's model to the current one does it the same way. */
|
|
195
|
+
export const sameEmbeddingModel = (a, b) => a.modelId === b.modelId && a.version === b.version && a.dimensions === b.dimensions;
|
|
196
|
+
/**
|
|
197
|
+
* Postgres full-text `KeywordIndex` (#136).
|
|
198
|
+
*
|
|
199
|
+
* Over `knowledge_chunks.content_tsv`, the same rows the vector index searches, so a chunk cannot be visible to
|
|
200
|
+
* one signal and invisible to the other.
|
|
201
|
+
*
|
|
202
|
+
* **`websearch_to_tsquery`, not `to_tsquery`.** The latter throws on ordinary user input — an unbalanced quote,
|
|
203
|
+
* a bare `&`, a trailing operator — and a search box that errors on a stray character is a search box that
|
|
204
|
+
* errors. `websearch_to_tsquery` accepts anything and interprets quotes and `or` the way a person expects.
|
|
205
|
+
*
|
|
206
|
+
* **The score is normalised against the best hit in the result set**, not against an absolute. `ts_rank_cd`
|
|
207
|
+
* has no upper bound and its scale depends on the corpus, so an absolute threshold would mean different things
|
|
208
|
+
* in different tenants — and fusing an unbounded score with a bounded cosine similarity would let the
|
|
209
|
+
* unbounded one dominate whenever it happened to be larger.
|
|
210
|
+
*/
|
|
211
|
+
export const createPostgresKeywordIndex = (sql) => ({
|
|
212
|
+
async search({ tenantId, query, authSubjects, limit, minScore, sourceTypes }) {
|
|
213
|
+
// "No subjects" means nothing, not everything. Short-circuited so the database is never asked a question
|
|
214
|
+
// whose answer is already known.
|
|
215
|
+
if (authSubjects.length === 0)
|
|
216
|
+
return [];
|
|
217
|
+
// Stopwords come off the query, not the index. `simple` is chosen over `english` so identifiers survive
|
|
218
|
+
// (`english` stems, which destroys `ERR-4021`), and the price of `simple` is that it keeps stopwords —
|
|
219
|
+
// paid here rather than by matching every document that says "the".
|
|
220
|
+
const terms = stripStopwords(query);
|
|
221
|
+
if (terms === "")
|
|
222
|
+
return [];
|
|
223
|
+
const rows = await sql.query(`WITH q AS (SELECT websearch_to_tsquery('simple', $2) AS tsq),
|
|
224
|
+
scored AS (
|
|
225
|
+
SELECT ${COLUMNS},
|
|
226
|
+
-- cd = cover density: it rewards query terms appearing close together, which is what
|
|
227
|
+
-- distinguishes a chunk *about* the terms from one that mentions them separately.
|
|
228
|
+
ts_rank_cd(content_tsv, q.tsq) AS rank
|
|
229
|
+
FROM knowledge_chunks, q
|
|
230
|
+
WHERE tenant_id = $1
|
|
231
|
+
-- AC-3, for the keyword signal too: the permission predicate is in the same statement as the
|
|
232
|
+
-- match, so an excluded chunk is never a candidate and never influences the ranking of the
|
|
233
|
+
-- ones that are.
|
|
234
|
+
AND auth_subject = ANY($3::text[])
|
|
235
|
+
AND ($4::text[] IS NULL OR source_type = ANY($4::text[]))
|
|
236
|
+
AND content_tsv @@ q.tsq
|
|
237
|
+
)
|
|
238
|
+
SELECT *, rank / NULLIF(MAX(rank) OVER (), 0) AS normalised
|
|
239
|
+
FROM scored
|
|
240
|
+
ORDER BY rank DESC, id
|
|
241
|
+
LIMIT $5`, [tenantId, terms, authSubjects, sourceTypes === undefined ? null : [...sourceTypes], limit]);
|
|
242
|
+
return rows
|
|
243
|
+
.map((r) => {
|
|
244
|
+
const normalised = r.normalised;
|
|
245
|
+
return {
|
|
246
|
+
chunk: toChunk(r),
|
|
247
|
+
// `NULLIF(...,0)` yields null when every rank is zero, which cannot happen for a matching row but is
|
|
248
|
+
// cheaper to handle than to prove impossible.
|
|
249
|
+
score: normalised === null ? 0 : Number(normalised),
|
|
250
|
+
};
|
|
251
|
+
})
|
|
252
|
+
.filter((h) => minScore === undefined || h.score >= minScore);
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
//# sourceMappingURL=knowledge.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BlobStore } from "../../persistence/index.js";
|
|
2
|
+
import type { PrincipalMemoryStore } from "../../principal-memory/index.js";
|
|
3
|
+
import type { SqlExecutor } from "./sql.js";
|
|
4
|
+
export declare const createPostgresPrincipalMemoryStore: (sql: SqlExecutor, options?: {
|
|
5
|
+
readonly clock?: () => string;
|
|
6
|
+
}) => PrincipalMemoryStore;
|
|
7
|
+
/**
|
|
8
|
+
* The tool-output offload. Stores the **value**, because that is what the port does: `put(value)`
|
|
9
|
+
* returns a ref and `get(ref)` must hand the value back. The `blob_refs` metadata-and-pointer table
|
|
10
|
+
* the SPEC described cannot implement this — there is nothing in the port to fetch bytes with — and
|
|
11
|
+
* belongs to `FileMetadataStore` (#129) or `ArtifactStore` (#133) instead.
|
|
12
|
+
*/
|
|
13
|
+
export declare const createPostgresBlobStore: (sql: SqlExecutor) => BlobStore;
|
|
14
|
+
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL `PrincipalMemoryStore` and `BlobStore` (#102) — per-user memory and the tool-output
|
|
3
|
+
* offload. Completes the Postgres column at 19/19.
|
|
4
|
+
*
|
|
5
|
+
* Per-user memory is the feature most visibly broken by memory-only storage: a user says "remember
|
|
6
|
+
* this" and it is gone after the next deploy. Two properties carry the weight, and both are enforced
|
|
7
|
+
* in the statement rather than by convention:
|
|
8
|
+
*
|
|
9
|
+
* - **Cross-principal isolation is structural.** Every method takes `principalId` and every statement
|
|
10
|
+
* filters on it as part of the primary key. There is no method that *could* read across principals,
|
|
11
|
+
* which is a stronger guarantee than a policy that each query remembers to apply.
|
|
12
|
+
* - **A disabled entry is never a retrieval candidate.** `retrieve` filters `disabled_at IS NULL`, and
|
|
13
|
+
* the index it reads is partial on the same predicate — an index cannot return what it does not
|
|
14
|
+
* contain. A user switching a memory off and having it still shape answers is the failure this
|
|
15
|
+
* prevents, and it is a privacy failure, not a correctness nicety.
|
|
16
|
+
*/
|
|
17
|
+
import { AgentPlatformError } from "../../core/errors.js";
|
|
18
|
+
import { asId } from "../../core/ids.js";
|
|
19
|
+
const iso = (v) => (v instanceof Date ? v.toISOString() : new Date(v).toISOString());
|
|
20
|
+
const notFound = (id) => new AgentPlatformError({ code: "not_found", message: `Memory ${id} not found`, retryable: false });
|
|
21
|
+
const conflict = (m) => new AgentPlatformError({ code: "conflict", message: m, retryable: false });
|
|
22
|
+
const json = (value) => (typeof value === "string" ? JSON.parse(value) : value);
|
|
23
|
+
const toEntry = (r) => ({
|
|
24
|
+
id: r.id,
|
|
25
|
+
tenantId: r.tenant_id,
|
|
26
|
+
principalId: r.principal_id,
|
|
27
|
+
text: r.text,
|
|
28
|
+
tags: json(r.tags) ?? [],
|
|
29
|
+
salience: Number(r.salience),
|
|
30
|
+
version: Number(r.version),
|
|
31
|
+
createdAt: iso(r.created_at),
|
|
32
|
+
updatedAt: iso(r.updated_at),
|
|
33
|
+
...(r.disabled_at === null ? {} : { disabledAt: iso(r.disabled_at) }),
|
|
34
|
+
});
|
|
35
|
+
const MEMORY_COLUMNS = `tenant_id, principal_id, id, text, tags, salience, version,
|
|
36
|
+
created_at, updated_at, disabled_at`;
|
|
37
|
+
export const createPostgresPrincipalMemoryStore = (sql, options = {}) => {
|
|
38
|
+
const clock = options.clock ?? (() => new Date().toISOString());
|
|
39
|
+
return {
|
|
40
|
+
async put({ tenantId, principalId, id, text, tags, salience }) {
|
|
41
|
+
const now = clock();
|
|
42
|
+
// Version incremented and created_at preserved server-side, matching the reference adapter.
|
|
43
|
+
// Doing it in the statement means two concurrent puts on one id cannot both compute version 2.
|
|
44
|
+
const rows = await sql.query(`INSERT INTO principal_memory
|
|
45
|
+
(tenant_id, principal_id, id, text, tags, salience, version, created_at, updated_at)
|
|
46
|
+
VALUES ($1, $2, COALESCE($3, 'mem-' || nextval('blob_ref_seq')), $4, $5::jsonb, $6, 1,
|
|
47
|
+
$7::timestamptz, $7::timestamptz)
|
|
48
|
+
ON CONFLICT (tenant_id, principal_id, id) DO UPDATE
|
|
49
|
+
SET text = excluded.text,
|
|
50
|
+
tags = excluded.tags,
|
|
51
|
+
salience = excluded.salience,
|
|
52
|
+
version = principal_memory.version + 1,
|
|
53
|
+
updated_at = excluded.updated_at
|
|
54
|
+
RETURNING ${MEMORY_COLUMNS}`, [
|
|
55
|
+
tenantId,
|
|
56
|
+
principalId,
|
|
57
|
+
id ?? null,
|
|
58
|
+
text,
|
|
59
|
+
JSON.stringify(tags ?? []),
|
|
60
|
+
salience ?? 1,
|
|
61
|
+
now,
|
|
62
|
+
]);
|
|
63
|
+
const row = rows[0];
|
|
64
|
+
// Unreachable: the upsert always returns a row. Throwing rather than returning a fabricated
|
|
65
|
+
// entry, because a caller that thinks a memory was stored when it was not is worse than an error.
|
|
66
|
+
if (!row)
|
|
67
|
+
throw conflict(`Could not store memory for ${principalId}`);
|
|
68
|
+
return toEntry(row);
|
|
69
|
+
},
|
|
70
|
+
async get({ tenantId, principalId, id }) {
|
|
71
|
+
const rows = await sql.query(`SELECT ${MEMORY_COLUMNS} FROM principal_memory
|
|
72
|
+
WHERE tenant_id = $1 AND principal_id = $2 AND id = $3`, [tenantId, principalId, id]);
|
|
73
|
+
const row = rows[0];
|
|
74
|
+
return row ? toEntry(row) : null;
|
|
75
|
+
},
|
|
76
|
+
async list({ tenantId, principalId, limit, cursor }) {
|
|
77
|
+
// Keyset on (created_at, id): the cursor names a row, and paging resumes strictly after it, so
|
|
78
|
+
// a concurrent put cannot shift a page boundary the way an OFFSET would.
|
|
79
|
+
const rows = await sql.query(`WITH anchor AS (
|
|
80
|
+
SELECT created_at, id FROM principal_memory
|
|
81
|
+
WHERE tenant_id = $1 AND principal_id = $2 AND id = $3
|
|
82
|
+
)
|
|
83
|
+
SELECT ${MEMORY_COLUMNS} FROM principal_memory
|
|
84
|
+
WHERE tenant_id = $1 AND principal_id = $2
|
|
85
|
+
AND ($3::text IS NULL
|
|
86
|
+
OR (created_at, id) > ((SELECT created_at FROM anchor), (SELECT id FROM anchor)))
|
|
87
|
+
ORDER BY created_at, id
|
|
88
|
+
LIMIT $4`, [tenantId, principalId, cursor ?? null, limit + 1]);
|
|
89
|
+
const hasMore = rows.length > limit;
|
|
90
|
+
const items = (hasMore ? rows.slice(0, limit) : rows).map(toEntry);
|
|
91
|
+
const last = items[items.length - 1];
|
|
92
|
+
const page = hasMore && last ? { items, nextCursor: last.id } : { items };
|
|
93
|
+
return page;
|
|
94
|
+
},
|
|
95
|
+
async update({ tenantId, principalId, id, expectedVersion, patch }) {
|
|
96
|
+
// The version guard is in the WHERE clause, so a stale write matches no row rather than
|
|
97
|
+
// overwriting a newer one. COALESCE leaves an omitted patch field untouched.
|
|
98
|
+
//
|
|
99
|
+
// `disabled` is three-valued on the way in: undefined leaves it alone, true stamps now, false
|
|
100
|
+
// clears it. Expressed as a CASE because COALESCE cannot distinguish "set to null" from "leave".
|
|
101
|
+
const rows = await sql.query(`UPDATE principal_memory
|
|
102
|
+
SET text = COALESCE($5, text),
|
|
103
|
+
tags = COALESCE($6::jsonb, tags),
|
|
104
|
+
salience = COALESCE($7, salience),
|
|
105
|
+
disabled_at = CASE
|
|
106
|
+
WHEN $8::boolean IS NULL THEN disabled_at
|
|
107
|
+
WHEN $8::boolean THEN $9::timestamptz
|
|
108
|
+
ELSE NULL
|
|
109
|
+
END,
|
|
110
|
+
version = version + 1,
|
|
111
|
+
updated_at = $9::timestamptz
|
|
112
|
+
WHERE tenant_id = $1 AND principal_id = $2 AND id = $3 AND version = $4
|
|
113
|
+
RETURNING ${MEMORY_COLUMNS}`, [
|
|
114
|
+
tenantId,
|
|
115
|
+
principalId,
|
|
116
|
+
id,
|
|
117
|
+
expectedVersion,
|
|
118
|
+
patch.text ?? null,
|
|
119
|
+
patch.tags === undefined ? null : JSON.stringify(patch.tags),
|
|
120
|
+
patch.salience ?? null,
|
|
121
|
+
patch.disabled ?? null,
|
|
122
|
+
clock(),
|
|
123
|
+
]);
|
|
124
|
+
const row = rows[0];
|
|
125
|
+
if (row)
|
|
126
|
+
return toEntry(row);
|
|
127
|
+
// No row: either it does not exist, or the version was stale. The distinction is the caller's
|
|
128
|
+
// next move — retry with a fresh read, versus stop.
|
|
129
|
+
const current = await this.get({ tenantId, principalId, id });
|
|
130
|
+
if (!current)
|
|
131
|
+
throw notFound(id);
|
|
132
|
+
throw conflict(`Memory ${id} version ${expectedVersion} is stale (current ${current.version})`);
|
|
133
|
+
},
|
|
134
|
+
async delete({ tenantId, principalId, id }) {
|
|
135
|
+
// Hard delete, as the port documents: a deleted entry cannot resurface in a later prompt. A
|
|
136
|
+
// soft delete would leave the text on disk after a user asked for it to be gone.
|
|
137
|
+
await sql.query(`DELETE FROM principal_memory WHERE tenant_id = $1 AND principal_id = $2 AND id = $3`, [tenantId, principalId, id]);
|
|
138
|
+
},
|
|
139
|
+
async retrieve({ tenantId, principalId, query, limit }) {
|
|
140
|
+
const q = query?.trim();
|
|
141
|
+
// Active only, matched against text or any tag, most salient first.
|
|
142
|
+
//
|
|
143
|
+
// `strpos` on lowercased text, deliberately **not** `ILIKE '%' || $3 || '%'`. Binding the query
|
|
144
|
+
// as a parameter stops SQL injection but does nothing about `LIKE` metacharacters: a query of
|
|
145
|
+
// "%" would still be a wildcard and return every memory the principal has. The reference
|
|
146
|
+
// adapter uses `String.includes`, which is literal, so ILIKE also made the two adapters
|
|
147
|
+
// disagree. `strpos` is a literal substring search and matches `includes` exactly.
|
|
148
|
+
//
|
|
149
|
+
// Neither form can use an index for a leading-wildcard search, so nothing is lost by it — see
|
|
150
|
+
// the open question on #102 about whether substring matching is the intended semantics at all.
|
|
151
|
+
const rows = await sql.query(`SELECT ${MEMORY_COLUMNS} FROM principal_memory
|
|
152
|
+
WHERE tenant_id = $1
|
|
153
|
+
AND principal_id = $2
|
|
154
|
+
AND disabled_at IS NULL
|
|
155
|
+
AND ($3::text IS NULL
|
|
156
|
+
OR strpos(lower(text), lower($3::text)) > 0
|
|
157
|
+
OR EXISTS (
|
|
158
|
+
SELECT 1 FROM jsonb_array_elements_text(tags) AS t(tag)
|
|
159
|
+
WHERE strpos(lower(tag), lower($3::text)) > 0
|
|
160
|
+
))
|
|
161
|
+
ORDER BY salience DESC, created_at, id
|
|
162
|
+
LIMIT $4`, [tenantId, principalId, q === undefined || q === "" ? null : q, limit]);
|
|
163
|
+
return rows.map(toEntry);
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* The tool-output offload. Stores the **value**, because that is what the port does: `put(value)`
|
|
169
|
+
* returns a ref and `get(ref)` must hand the value back. The `blob_refs` metadata-and-pointer table
|
|
170
|
+
* the SPEC described cannot implement this — there is nothing in the port to fetch bytes with — and
|
|
171
|
+
* belongs to `FileMetadataStore` (#129) or `ArtifactStore` (#133) instead.
|
|
172
|
+
*/
|
|
173
|
+
export const createPostgresBlobStore = (sql) => ({
|
|
174
|
+
async put({ tenantId, value }) {
|
|
175
|
+
// The ref is generated in-statement from a sequence, so it is durable and unique without a
|
|
176
|
+
// read-then-write. The reference adapter's process-local counter would, after a restart, hand out
|
|
177
|
+
// a ref that already belongs to someone else's value.
|
|
178
|
+
const rows = await sql.query(`INSERT INTO blobs (tenant_id, ref, value)
|
|
179
|
+
VALUES ($1, 'blob:' || $1 || ':' || nextval('blob_ref_seq'), $2::jsonb)
|
|
180
|
+
RETURNING ref`, [tenantId, JSON.stringify(value ?? null)]);
|
|
181
|
+
const row = rows[0];
|
|
182
|
+
if (!row)
|
|
183
|
+
throw conflict(`Could not store a blob for ${tenantId}`);
|
|
184
|
+
return asId(row.ref);
|
|
185
|
+
},
|
|
186
|
+
async get({ tenantId, ref }) {
|
|
187
|
+
// Tenant-scoped by primary key, so a ref from one tenant cannot resolve another's value even if
|
|
188
|
+
// it is guessed or leaked — the port's docstring calls that out specifically.
|
|
189
|
+
const rows = await sql.query(`SELECT value FROM blobs WHERE tenant_id = $1 AND ref = $2`, [tenantId, ref]);
|
|
190
|
+
const row = rows[0];
|
|
191
|
+
return row === undefined ? null : json(row.value);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
//# sourceMappingURL=memory.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AgentManifest } from "../../agents/index.js";
|
|
2
|
+
import type { MessageId, RunId, TenantId } from "../../core/ids.js";
|
|
3
|
+
import type { AgentStore, ConversationBindingStore, MessageStore } from "../../persistence/index.js";
|
|
4
|
+
import type { SqlExecutor } from "./sql.js";
|
|
5
|
+
export declare const createPostgresMessageStore: (sql: SqlExecutor) => MessageStore;
|
|
6
|
+
export declare const createPostgresAgentStore: (sql: SqlExecutor) => AgentStore & {
|
|
7
|
+
put(tenantId: string, manifest: AgentManifest): Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
export declare const createPostgresConversationBindingStore: (sql: SqlExecutor) => ConversationBindingStore;
|
|
10
|
+
export type { MessageId, RunId, TenantId };
|
|
11
|
+
//# sourceMappingURL=message-store.d.ts.map
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL `MessageStore`, `AgentStore` and `ConversationBindingStore` (#96) — the stores that make
|
|
3
|
+
* a conversation durable rather than a thing that exists until the next deploy.
|
|
4
|
+
*
|
|
5
|
+
* Paging uses the same `(created_at, id)` composite cursor as `createPostgresConversationStore`. That
|
|
6
|
+
* pairing is the whole of AC-2: a timestamp alone lets a concurrently-inserted row sharing the same
|
|
7
|
+
* `created_at` slip between pages, and `OFFSET` shifts every subsequent page when a row lands early.
|
|
8
|
+
* The `id` tiebreak plus a keyset comparison makes the sequence stable while the conversation is
|
|
9
|
+
* still being written to.
|
|
10
|
+
*/
|
|
11
|
+
import { AgentPlatformError } from "../../core/errors.js";
|
|
12
|
+
import { parseMessagePart } from "../../core/validation.js";
|
|
13
|
+
const iso = (v) => (v instanceof Date ? v.toISOString() : new Date(v).toISOString());
|
|
14
|
+
const invalid = (message) => new AgentPlatformError({ code: "invalid_input", message, retryable: false });
|
|
15
|
+
/**
|
|
16
|
+
* Validate on read. A hand-edited or half-migrated `parts` column otherwise flows into a client's
|
|
17
|
+
* stream, where the failure surfaces far from its cause — so the error names the message that carries
|
|
18
|
+
* the bad payload (AC-5).
|
|
19
|
+
*/
|
|
20
|
+
const toMessage = (r) => {
|
|
21
|
+
const stored = typeof r.parts === "string" ? JSON.parse(r.parts) : r.parts;
|
|
22
|
+
if (!Array.isArray(stored))
|
|
23
|
+
throw invalid(`Message ${r.id} has a non-array parts column`);
|
|
24
|
+
// There is no whole-message validator, so each part goes through `parseMessagePart`. Wrapping the
|
|
25
|
+
// failure keeps the message id in the error — otherwise a bad row reports only "invalid part" and
|
|
26
|
+
// leaves you grepping a table for it (AC-5).
|
|
27
|
+
let parts;
|
|
28
|
+
try {
|
|
29
|
+
parts = stored.map(parseMessagePart);
|
|
30
|
+
}
|
|
31
|
+
catch (cause) {
|
|
32
|
+
throw invalid(`Message ${r.id} has an invalid stored part: ${cause.message}`);
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
id: r.id,
|
|
36
|
+
conversationId: r.conversation_id,
|
|
37
|
+
...(r.run_id === null ? {} : { runId: r.run_id }),
|
|
38
|
+
role: r.role,
|
|
39
|
+
parts,
|
|
40
|
+
createdAt: iso(r.created_at),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const encodeCursor = (r) => Buffer.from(JSON.stringify({ c: iso(r.created_at), i: r.id })).toString("base64");
|
|
44
|
+
const decodeCursor = (cursor) => JSON.parse(Buffer.from(cursor, "base64").toString("utf8"));
|
|
45
|
+
export const createPostgresMessageStore = (sql) => ({
|
|
46
|
+
/**
|
|
47
|
+
* On the port as of #157, no longer a "test-only affordance".
|
|
48
|
+
*
|
|
49
|
+
* The signature changed from `(tenantId, message)` to the object form every other port method uses — a
|
|
50
|
+
* positional pair was the shape a test-only helper could get away with, and it is the wrong shape for
|
|
51
|
+
* something callers depend on.
|
|
52
|
+
*
|
|
53
|
+
* Messages stay immutable: `DO NOTHING` on conflict, no update, no delete. Editing one would rewrite history a
|
|
54
|
+
* client has already streamed and a model has already been shown, and a repeat of the same id is a retry
|
|
55
|
+
* rather than an error.
|
|
56
|
+
*/
|
|
57
|
+
async append({ tenantId, message }) {
|
|
58
|
+
await sql.query(`INSERT INTO messages (tenant_id, id, conversation_id, run_id, role, parts, created_at)
|
|
59
|
+
VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::timestamptz)
|
|
60
|
+
ON CONFLICT (tenant_id, id) DO NOTHING`, [
|
|
61
|
+
tenantId,
|
|
62
|
+
message.id,
|
|
63
|
+
message.conversationId,
|
|
64
|
+
message.runId ?? null,
|
|
65
|
+
message.role,
|
|
66
|
+
JSON.stringify(message.parts),
|
|
67
|
+
message.createdAt,
|
|
68
|
+
]);
|
|
69
|
+
},
|
|
70
|
+
async findById({ tenantId, id }) {
|
|
71
|
+
const rows = await sql.query(`SELECT * FROM messages WHERE tenant_id = $1 AND id = $2`, [tenantId, id]);
|
|
72
|
+
const row = rows[0];
|
|
73
|
+
return row ? toMessage(row) : null;
|
|
74
|
+
},
|
|
75
|
+
async listByConversation({ tenantId, conversationId, limit, cursor, newestFirst }) {
|
|
76
|
+
const params = [tenantId, conversationId, limit + 1];
|
|
77
|
+
let where = `tenant_id = $1 AND conversation_id = $2`;
|
|
78
|
+
// The comparator and the sort flip together — #167. Split apart they produce a page that scans one way and
|
|
79
|
+
// pages the other, which returns rows before the cursor and looks like duplicates.
|
|
80
|
+
const descending = newestFirst === true;
|
|
81
|
+
if (cursor) {
|
|
82
|
+
const { c, i } = decodeCursor(cursor);
|
|
83
|
+
// Keyset, not OFFSET: strictly past (created_at, id) in the direction being read, so an insert landing
|
|
84
|
+
// earlier in the ordering cannot shift this page or duplicate a row into the next one.
|
|
85
|
+
where += descending
|
|
86
|
+
? ` AND (created_at, id) < ($4::timestamptz, $5)`
|
|
87
|
+
: ` AND (created_at, id) > ($4::timestamptz, $5)`;
|
|
88
|
+
params.push(c, i);
|
|
89
|
+
}
|
|
90
|
+
const rows = await sql.query(`SELECT * FROM messages WHERE ${where}
|
|
91
|
+
ORDER BY created_at ${descending ? "DESC" : "ASC"}, id ${descending ? "DESC" : "ASC"}
|
|
92
|
+
LIMIT $3`, params);
|
|
93
|
+
const hasMore = rows.length > limit;
|
|
94
|
+
const items = (hasMore ? rows.slice(0, limit) : rows).map(toMessage);
|
|
95
|
+
const last = hasMore ? rows[limit - 1] : undefined;
|
|
96
|
+
const page = last ? { items, nextCursor: encodeCursor(last) } : { items };
|
|
97
|
+
return page;
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
export const createPostgresAgentStore = (sql) => ({
|
|
101
|
+
async put(tenantId, manifest) {
|
|
102
|
+
await sql.query(`INSERT INTO agents (tenant_id, id, version, manifest, created_at, updated_at)
|
|
103
|
+
VALUES ($1, $2, $3, $4::jsonb, now(), now())
|
|
104
|
+
ON CONFLICT (tenant_id, id, version) DO UPDATE
|
|
105
|
+
SET manifest = excluded.manifest, updated_at = now()`, [tenantId, manifest.id, manifest.version, JSON.stringify(manifest)]);
|
|
106
|
+
},
|
|
107
|
+
async findByVersion({ tenantId, agentId, version }) {
|
|
108
|
+
// Tenant-scoped, unlike the in-memory adapter before #91 fixed it: a manifest is owned by the
|
|
109
|
+
// tenant that registered it, and the key leads with tenant_id so a cross-tenant read is a miss.
|
|
110
|
+
const rows = await sql.query(`SELECT manifest FROM agents WHERE tenant_id = $1 AND id = $2 AND version = $3`, [tenantId, agentId, version]);
|
|
111
|
+
const row = rows[0];
|
|
112
|
+
if (!row)
|
|
113
|
+
return null;
|
|
114
|
+
return (typeof row.manifest === "string" ? JSON.parse(row.manifest) : row.manifest);
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
const toBinding = (r) => ({
|
|
118
|
+
conversationId: r.conversation_id,
|
|
119
|
+
agentId: r.agent_id,
|
|
120
|
+
agentVersionPolicy: r.agent_version_policy,
|
|
121
|
+
// Absent rather than null for a `latest` binding: the type makes it optional, and a null here
|
|
122
|
+
// would round-trip as a present-but-empty version.
|
|
123
|
+
...(r.agent_version === null ? {} : { agentVersion: r.agent_version }),
|
|
124
|
+
});
|
|
125
|
+
export const createPostgresConversationBindingStore = (sql) => ({
|
|
126
|
+
async bind({ tenantId, conversationId, agentId, agentVersionPolicy, agentVersion }) {
|
|
127
|
+
// Idempotent by conversation: re-binding is a legitimate operation (an agent upgrade), not a
|
|
128
|
+
// conflict, so the last write wins rather than erroring.
|
|
129
|
+
await sql.query(`INSERT INTO conversation_bindings
|
|
130
|
+
(tenant_id, conversation_id, agent_id, agent_version_policy, agent_version, bound_at)
|
|
131
|
+
VALUES ($1, $2, $3, $4, $5, now())
|
|
132
|
+
ON CONFLICT (tenant_id, conversation_id) DO UPDATE
|
|
133
|
+
SET agent_id = excluded.agent_id,
|
|
134
|
+
agent_version_policy = excluded.agent_version_policy,
|
|
135
|
+
agent_version = excluded.agent_version,
|
|
136
|
+
bound_at = excluded.bound_at`, [tenantId, conversationId, agentId, agentVersionPolicy, agentVersion ?? null]);
|
|
137
|
+
},
|
|
138
|
+
async get({ tenantId, conversationId }) {
|
|
139
|
+
const rows = await sql.query(`SELECT conversation_id, agent_id, agent_version_policy, agent_version
|
|
140
|
+
FROM conversation_bindings WHERE tenant_id = $1 AND conversation_id = $2`, [tenantId, conversationId]);
|
|
141
|
+
const row = rows[0];
|
|
142
|
+
return row ? toBinding(row) : null;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
//# sourceMappingURL=message-store.js.map
|