@ultimat3/ai 24.0.0 → 25.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/CLAUDE.md +72 -34
- package/README.md +271 -69
- package/package.json +12 -12
- package/src/agent-facts.ts +9 -2
- package/src/agent-job.ts +41 -2
- package/src/agent.ts +30 -13
- package/src/budget.ts +41 -6
- package/src/content-blocks.ts +227 -0
- package/src/content-errors.ts +67 -0
- package/src/echo-provider.ts +102 -0
- package/src/embeddings.ts +30 -10
- package/src/errors.ts +17 -11
- package/src/eval-errors.ts +3 -1
- package/src/evals.ts +1 -1
- package/src/failure-disclosure.ts +90 -0
- package/src/gateway.ts +40 -16
- package/src/hive-pool.ts +24 -16
- package/src/hive-result.ts +4 -1
- package/src/hive.ts +6 -4
- package/src/index.ts +63 -24
- package/src/llm-cache.ts +1 -1
- package/src/llm.ts +15 -11
- package/src/model-resolve.ts +79 -0
- package/src/models.ts +125 -111
- package/src/openai-body.ts +13 -2
- package/src/openai-messages.ts +86 -15
- package/src/openai-provider.ts +28 -31
- package/src/openai-wire.ts +4 -1
- package/src/pg-vector-sql.ts +11 -7
- package/src/pg-vector.ts +14 -6
- package/src/prompt-fence.ts +128 -0
- package/src/prompt.ts +43 -13
- package/src/provider.ts +75 -112
- package/src/rag.ts +7 -6
- package/src/remote-embedder.ts +10 -5
- package/src/runtime.ts +7 -14
- package/src/scorers.ts +1 -1
- package/src/tool-idempotency.ts +79 -0
- package/src/tools.ts +38 -36
- package/src/vector-scope.ts +2 -2
- package/src/vector.ts +103 -37
- package/src/wire.ts +7 -2
- package/src/fix-line.evals.ts +0 -35
- package/src/fix-line.ts +0 -27
- package/src/openai-models.ts +0 -84
package/CLAUDE.md
CHANGED
|
@@ -15,14 +15,24 @@ rather than imported. Same contract, two wire formats — and the same *decision
|
|
|
15
15
|
local `=== true`. An in-app agent and an external one must be offered exactly the same tools.
|
|
16
16
|
|
|
17
17
|
**And the same NAME**: `toLlmTool` passes `action.name` through untouched (the export name
|
|
18
|
-
`@ultimat3/mcp` serves); `llm()` and `agent()`
|
|
18
|
+
`@ultimat3/mcp` serves); `llm()` and `agent()` tool names are verbatim too. Never derive one.
|
|
19
|
+
|
|
20
|
+
**And the same TOOL**: `asProjectableAction` and mcp's `primitiveFromAction` are two copies the
|
|
21
|
+
tier table forces; `scripts/ai-mcp-tool-parity.test.ts` holds them equal — name, description,
|
|
22
|
+
exposure, the whole input schema (an idempotent action's reserved `idempotencyKey` argument,
|
|
23
|
+
`tool-idempotency.ts`), the policy verdict and the replay. A field one adds, the other adds.
|
|
19
24
|
|
|
20
25
|
## Owns
|
|
21
26
|
|
|
22
27
|
| File | Job |
|
|
23
28
|
|---|---|
|
|
24
|
-
| `models.ts` | the model REGISTRY: `registerModel`, limits, prices, the reasoning controls each one accepts |
|
|
25
|
-
| `
|
|
29
|
+
| `models.ts` | the model REGISTRY: `registerModel`, limits, prices, the reasoning controls each one accepts — and NO row of its own |
|
|
30
|
+
| `model-resolve.ts` | `resolveModel` / `modelForCall` — declaration → prompt → gateway `defaultModel`, none is `X_AI_MODEL_UNRESOLVED` naming all three; `describedModel` for a published fact |
|
|
31
|
+
| `model-fixture.ts` | the rows this package's own suites register (`useFixtureModels()`), as an app's `models.ts` would. Not shipped (`!src/**/*-fixture.ts`) |
|
|
32
|
+
| `provider.ts` | `Provider` interface, the request half, the money arithmetic, `anthropicProvider()` |
|
|
33
|
+
| `echo-provider.ts` | `echoProvider()`, the deterministic double — split from `provider.ts` at its line ceiling |
|
|
34
|
+
| `content-blocks.ts` | the `image` / `document` block shapes, the screen every request runs them through, their token estimate |
|
|
35
|
+
| `content-errors.ts` | the `X_AI_CONTENT_UNSUPPORTED` class; its code and title belong in `errors.ts` |
|
|
26
36
|
| `wire.ts` | the response half: `usage` / `stop_reason` shapes, and the SSE `MessageStream` |
|
|
27
37
|
| `error-body.ts` | what a failure body SAYS (`detailOf`) and what must never survive into it (`withoutKey`) — one copy, both transports |
|
|
28
38
|
| `sse.ts` | Server-Sent Events framing — protocol only, knows nothing about Anthropic |
|
|
@@ -30,12 +40,12 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
|
|
|
30
40
|
| `openai-messages.ts` | the format mapping's request half: `AiMessage` blocks → OpenAI messages, `LlmTool` → functions, `tool_choice` |
|
|
31
41
|
| `openai-body.ts` | one chat-completions body, and the per-model reasoning mapping |
|
|
32
42
|
| `openai-wire.ts` | its response half: one completion, and `ChatCompletionStream` (fragmented tool calls, trailing usage, `[DONE]`) |
|
|
33
|
-
| `openai-models.ts` | the OpenAI-format price rows, registered through the same `registerModel` |
|
|
34
43
|
| `gateway.ts` | routing, retries, cache, budget wiring |
|
|
35
44
|
| `budget.ts` | token ledgers per request/actor/org, ALS carrier |
|
|
36
|
-
| `prompt.ts` | `definePrompt`,
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
45
|
+
| `prompt.ts` | `definePrompt`, `promptHash`, version registry |
|
|
46
|
+
| `prompt-fence.ts` | a template's data fences: which tag pairs enclose each slot (`promptFences`), and the closer-breaking `render` applies inside one |
|
|
47
|
+
| `embeddings.ts` | `Embedder`, `hashEmbedder()`, cosine helpers |
|
|
48
|
+
| `remote-embedder.ts` | `remoteEmbedder()` — the production `/v1/embeddings` client |
|
|
39
49
|
| `evals.ts` | `defineEval`, the run, the baseline gate, prompt coverage |
|
|
40
50
|
| `eval-baseline.ts` | the recorded scores: path, read/write, what counts as a regression |
|
|
41
51
|
| `scorers.ts` | what a `Scorer` is, the built-in ones, and `llmJudge` |
|
|
@@ -43,9 +53,11 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
|
|
|
43
53
|
| `pg-vector.live.test.ts` | the same store against a real pgvector — DDL, fusion, scope, plan |
|
|
44
54
|
| `vector-scope.ts` | the tenant + policy envelope, and the tighten-only derive rule |
|
|
45
55
|
| `pg-vector-sql.ts` | every pgvector statement: DDL, upsert, cosine, FTS, RRF fusion |
|
|
46
|
-
| `pg-vector.ts` | `
|
|
56
|
+
| `pg-vector.ts` | `postgresVectorStore()` — the production store |
|
|
47
57
|
| `rag.ts` | chunker, retriever, reranker, budgeted context assembler |
|
|
48
58
|
| `tools.ts` | action → LLM tool definition; the `AgentTool` union and `asProjectableAction`; `runLlmToolCall` |
|
|
59
|
+
| `tool-idempotency.ts` | an idempotent action's reserved `idempotencyKey` tool argument — MCP's, restated (same tier), held equal by the parity test |
|
|
60
|
+
| `failure-disclosure.ts` | `discloseFailure` — the ONE rule for what of a throw a remote reader sees (tool result, hive member); the withheld half is logged as code, name and stack frames — never its message or cause |
|
|
49
61
|
| `llm.ts` | `llm()` — the model call, declared as an `action`; and what a streamed answer must satisfy |
|
|
50
62
|
| `respond.ts` | the `respond` tool for one `output` (a non-object one wrapped in `{ value }`), its reader, and `parseJsonish` |
|
|
51
63
|
| `llm-stream.ts` | `.stream()`'s plumbing: the sink, the ambient mark, the one-turn drive |
|
|
@@ -61,9 +73,9 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
|
|
|
61
73
|
| `llm-cache.ts` | the semantic cache half of `llm()`: what a declaration may partition on, and the store it reaches |
|
|
62
74
|
| `llm-fixture.ts` | the harness `llm.test.ts` and `llm-cache.test.ts` share. Not shipped (`!src/**/*-fixture.ts`) |
|
|
63
75
|
| `bounds-fixture.ts` | the one `refusal`/`asyncRefusal` every numeric-bound suite here asserts through. Not shipped, same rule |
|
|
64
|
-
| `agent-bounds.test.ts` | the agent loop's ceilings
|
|
76
|
+
| `agent-bounds.test.ts` | the agent loop's ceilings (split from `agent.test.ts`, at its line ceiling) |
|
|
65
77
|
| `runtime.ts` | the ambient gateway / embedder / semantic caches an `llm()` reaches |
|
|
66
|
-
| `fix-line.ts` / `fix-line.evals.ts` / `fix-line.v1.baseline.json` / `fix-line.eval.test.ts` | the package's
|
|
78
|
+
| `fix-line-fixture.ts` / `fix-line.evals-fixture.ts` / `fix-line.v1.baseline.json` / `fix-line.eval.test.ts` | the package's dogfood eval — proof the baseline convention fails a build |
|
|
67
79
|
|
|
68
80
|
## Invariants — bounds and budget
|
|
69
81
|
|
|
@@ -74,7 +86,7 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
|
|
|
74
86
|
off. `Gateway.generate`/`stream` screen it at the one seam, `registerModel` screens `maxOutput`, and
|
|
75
87
|
`llm()`/`agent()` screen theirs at DECLARATION.
|
|
76
88
|
- **A bound is screened under the key the DECLARATION uses** (`budget.tokensPerRun` in `limitsOf`
|
|
77
|
-
before the ledger's `request`; `retrieve()`'s own `k`; `
|
|
89
|
+
before the ledger's `request`; `retrieve()`'s own `k`; `remoteEmbedder()`'s `maxResponseBytes`).
|
|
78
90
|
- A per-call budget `derive`s from the ambient ledger and only TIGHTENS. **A derived ledger reports
|
|
79
91
|
back up the chain** (every debit on every ancestor; `reserve` checks each `request` scope); the
|
|
80
92
|
STORE is written once, by the ledger the call was made on.
|
|
@@ -84,13 +96,14 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
|
|
|
84
96
|
at a later scope gives back what the earlier ones took.
|
|
85
97
|
- **The store is touched only for a scope whose ceiling is DECLARED** (`takeScope`, `meteredKeys`):
|
|
86
98
|
every call carries keys, so a write per key grew the default store by one entry per caller.
|
|
87
|
-
`
|
|
99
|
+
`memoryBudgetStore()` has no window and no eviction (eviction is a ceiling bypass); a counter back
|
|
88
100
|
at zero is deleted.
|
|
89
101
|
- **`llm()`, `agent()`, `hive()` root as `currentBudget() ?? gateway.callLedger(budgetKeysFor(ctx.actor))`**
|
|
90
|
-
— the gateway's ceilings, keyed `actor:<kind>:<id>` / `org:<orgId
|
|
102
|
+
— the gateway's ceilings, keyed `actor:<kind>:<id>` / `org:<orgId>` (none for an `undefined`/`null`/`''`
|
|
103
|
+
org — the `orgless` test `query`'s cache uses). `callLedger` is required on
|
|
91
104
|
`Gateway`; `hive()` asks `installedGateway()` so a hive of plain actions needs none.
|
|
92
105
|
- **`BudgetStore` is where `actor` and `org` live; the default is per PROCESS**
|
|
93
|
-
(`
|
|
106
|
+
(`providerGateway({ budgetStore })`). `add` takes a negative `tokens` for a release — a store that
|
|
94
107
|
clamps at zero leaks the ceiling.
|
|
95
108
|
- Cost is `Money` (integer minor units), rounded **up**. A budget throws `X_AI_BUDGET_EXCEEDED`
|
|
96
109
|
**before** the provider call. Never truncate. List price over-reserves (no introductory prices).
|
|
@@ -112,7 +125,7 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
|
|
|
112
125
|
`ctx.locale` (in the unconditional half). **The default scope is the calling ACTOR**
|
|
113
126
|
(`JSON.stringify([actor.kind, actor.id, actor.orgId ?? null])`); `scope` receives
|
|
114
127
|
`{ input, ctx }`; widening is `scope: () => 'global'`. The instance map is bounded
|
|
115
|
-
(`
|
|
128
|
+
(core's `MAX_CACHED_FORMATTERS`).
|
|
116
129
|
- `cache.invalidates` is not on `llm()`: a `SemanticCache` is not a `CacheTier`. Version bump + `ttl`
|
|
117
130
|
is the invalidation.
|
|
118
131
|
- The gateway is ambient (`configureAi`); absent at call time is `X_AI_GATEWAY_MISSING`, never a
|
|
@@ -127,6 +140,10 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
|
|
|
127
140
|
- **Fallback is across PROVIDERS serving one model, never across models**; `GenerateResult.provider`
|
|
128
141
|
is stamped by the gateway and put on the span as `llm.provider`.
|
|
129
142
|
- Server-side `fallbacks` (beta) are never sent — the stable `2023-06-01` surface only.
|
|
143
|
+
- **A slot inside a tag pair is DATA** (`prompt-fence.ts`, #689): `render` breaks, in the ASSEMBLED
|
|
144
|
+
prompt, every fence closer a fenced value wrote any character of (never per value: two slots can
|
|
145
|
+
each hold half) — never deletes, never touches a template-only closer. The
|
|
146
|
+
fences are read once at `definePrompt`; no hash moves.
|
|
130
147
|
- `definePrompt` refuses a re-registered version whose hash moved; the hash is over core's
|
|
131
148
|
`canonicalJson` (`prompt.test.ts` pins one hash literally); an absent schema hashes as `''`.
|
|
132
149
|
|
|
@@ -147,7 +164,7 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
|
|
|
147
164
|
stays UNREGISTERED on purpose.
|
|
148
165
|
- **The retry SCHEDULE is core's (`backoffDelay`, `isRetryableStatus`); the LOOP is this package's** —
|
|
149
166
|
core's `retry()` retries unclassified throws, and every value here is an app `Provider`'s plain
|
|
150
|
-
object, so it would retry a 400. `gateway-backoff.test.ts`. `RetryPolicy`/`
|
|
167
|
+
object, so it would retry a 400. `gateway-backoff.test.ts`. `RetryPolicy`/`DEFAULT_GATEWAY_RETRY` keep the
|
|
151
168
|
field names an app writes.
|
|
152
169
|
- Every non-2xx and every in-band `error` frame is `AiTransportError` with a real `status`.
|
|
153
170
|
- **Both wire formats answer the same question the same way** (`provider-parity.test.ts`): an in-band
|
|
@@ -160,15 +177,33 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
|
|
|
160
177
|
- A tool call is emitted whole. Thinking chunks are never appended to `text`.
|
|
161
178
|
- Anthropic body: no `temperature`/`top_p`/`top_k`, no `budget_tokens`, `effort` inside
|
|
162
179
|
`output_config`. Model IDs are exact aliases, never date-suffixed.
|
|
180
|
+
- **Apps bring their own models and providers (M12, 25.0.0): no vendor choice, no vendor data.** No
|
|
181
|
+
default model, no catalogue row, no id list ships; the registry starts EMPTY and `resetModels()`
|
|
182
|
+
empties it. A model resolves declaration → prompt → `Gateway.defaultModel` through `resolveModel`
|
|
183
|
+
/ `modelForCall` only; none is `X_AI_MODEL_UNRESOLVED` naming all three (with no gateway and nothing
|
|
184
|
+
declared, `X_AI_GATEWAY_MISSING` — the third place does not exist yet). `vendor-neutral.test.ts`
|
|
185
|
+
proves it in a fresh process; no fix line names a vendor id.
|
|
163
186
|
- **`ModelId` is `string`; the catalogue is an open registry** — `modelSpec(id)` refuses an
|
|
164
|
-
unregistered id (`X_AI_MODEL_UNKNOWN`)
|
|
165
|
-
**Re-registering REPLACES the spec and keeps
|
|
166
|
-
the ladder within a `family`**, read only by
|
|
167
|
-
ABOVE or nothing.
|
|
168
|
-
-
|
|
187
|
+
unregistered id (`X_AI_MODEL_UNKNOWN`, fix `registerModel(…)`) at `costOf`, the budget estimate and
|
|
188
|
+
the body builders, before a socket or a reservation. **Re-registering REPLACES the spec and keeps
|
|
189
|
+
its rung** (negotiated rates). **Registration order is the ladder within a `family`**, read only by
|
|
190
|
+
`moreCapableThan`, over the APP's rows; `X_LLM_REFUSED`'s fix names a rung ABOVE or nothing.
|
|
191
|
+
- **Every provider serves the app's list**: `anthropicProvider({ models })` and
|
|
192
|
+
`openAiProvider({ models })` require a non-empty one (`X_AI_REQUEST_INVALID`, a missing config
|
|
193
|
+
too); `echoProvider().models` reads the registry. **No provider has a model of its own**: a direct
|
|
194
|
+
call naming none is `X_AI_MODEL_UNRESOLVED` from all three — the list is what it serves, never a
|
|
195
|
+
fourth place (`vendor-neutral.test.ts`, one parity case).
|
|
169
196
|
- **The reasoning controls are PER MODEL (`models.ts`)**: an unasked control is omitted; an asked
|
|
170
|
-
control the model lacks is `X_AI_REQUEST_INVALID`, never dropped. Adding a model is a row
|
|
197
|
+
control the model lacks is `X_AI_REQUEST_INVALID`, never dropped. Adding a model is a row the app
|
|
198
|
+
registers. `disableThinkingUpTo: 'never'` refuses `thinking: 'disabled'` at every effort;
|
|
199
|
+
`disabledThinking: 'between_tools'` is the wire spelling of the same declaration
|
|
200
|
+
(`model-rules.test.ts`, over the fixture rows).
|
|
201
|
+
- **Media blocks are screened once, by `assertMediaContent`, at the top of BOTH body builders** —
|
|
202
|
+
malformed is `X_AI_REQUEST_INVALID`, untakeable (role, the row's `input`, the wire) is
|
|
203
|
+
`X_AI_CONTENT_UNSUPPORTED`; never dropped, never approximated (`provider-parity-content.test.ts`).
|
|
171
204
|
- `generate()` above `STREAM_ONLY_MAX_TOKENS` runs the streaming transport.
|
|
205
|
+
- **Every provider and embedder is built by its factory** (`anthropicProvider()`, `echoProvider()`,
|
|
206
|
+
`hashEmbedder()`, `remoteEmbedder()`); the classes are type exports only (`bun run factory-names`).
|
|
172
207
|
- **`openAiProvider()` is a FORMAT, not a vendor** — `baseUrl`, `auth`, `headers` are the
|
|
173
208
|
differences; never a second class per vendor:
|
|
174
209
|
- structured output is the `respond` tool, never `response_format`; `tool_choice` names the tool
|
|
@@ -178,17 +213,16 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
|
|
|
178
213
|
ESTIMATE when usage never arrives; subtract `cached_tokens` from `prompt_tokens`;
|
|
179
214
|
- tool-call deltas merge by `tool_calls[].index`; `isComplete()` accepts `[DONE]` or a finish reason,
|
|
180
215
|
except `[DONE]` with pending tool fragments, which is refused;
|
|
181
|
-
-
|
|
182
|
-
`
|
|
183
|
-
- no new `X_*` code; `AiTransportError` takes the provider's `envVar`; the key is revealed late,
|
|
216
|
+
- no code of its own (a media refusal is the shared `X_AI_CONTENT_UNSUPPORTED`);
|
|
217
|
+
`AiTransportError` takes the provider's `envVar`; the key is revealed late,
|
|
184
218
|
never stored, scrubbed from `detail`.
|
|
185
219
|
- **`embedBatched` enforces the `Embedder` arity per batch** (`X_AI_EMBEDDER_INVALID` with both
|
|
186
|
-
counts). One `
|
|
220
|
+
counts). One `remoteEmbedder()` for every vendor; vectors L2-normalised; a wrong width is
|
|
187
221
|
`X_VECTOR_DIM_MISMATCH`.
|
|
188
222
|
- **A caught value is read with `renderThrowable`** (`remote-embedder.ts`, `wire.ts`,
|
|
189
223
|
`openai-wire.ts`) — `scripts/error-render.ts` cannot see `catch` bindings.
|
|
190
224
|
- **A caller's string is never an object KEY**: the wire tables are `Map`s; `prompt.render` and
|
|
191
|
-
`
|
|
225
|
+
`echoProvider()`'s `replies` use `Object.hasOwn`.
|
|
192
226
|
|
|
193
227
|
## Invariants — `agent()`, `hive()`, `agentJob()`
|
|
194
228
|
|
|
@@ -211,19 +245,23 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
|
|
|
211
245
|
never as a failure); the throw is read with `stringField`.
|
|
212
246
|
- **A 5xx code's cause reaches the model only when core's `hasPublicCause` allows it** (status from
|
|
213
247
|
`@ultimat3/http`'s `statusFor`) — else `CODE: HIDDEN_TOOL_CAUSE`, plus a branded `callerFix`. The
|
|
214
|
-
same verdict a problem document and `@ultimat3/mcp` give.
|
|
248
|
+
same verdict a problem document and `@ultimat3/mcp` give, from `failure-disclosure.ts` — the
|
|
249
|
+
rule a hive member's `reason` follows too. Never a second copy.
|
|
215
250
|
- **`hive()` is a fan-out action** (`PRIMITIVE_FACTORIES`): `HiveResult` is a SCHEMA from the
|
|
216
251
|
member's `output`; three arms (`ok`/`failed`/`skipped`) and three counters; `members` in SPLIT
|
|
217
252
|
order with `index`; the hive never names an actor; no hive budget code (the reservation holds
|
|
218
|
-
it); a member's throw is recorded
|
|
253
|
+
it); a member's throw is recorded through `discloseFailure` — a 5xx cause or an uncoded throw's
|
|
254
|
+
message becomes `HIDDEN_MEMBER_CAUSE`, logged as facts only; `skipped` reasons
|
|
219
255
|
`SKIPPED_ABORTED` / `SKIPPED_NO_INPUT`; `onMemberError` is required; `concurrency` 4, `minMembers`
|
|
220
256
|
2 (a below-floor split still runs serially); an empty split is `X_HIVE_EMPTY`; an aborted `ctx`
|
|
221
257
|
unwinds with `X_ABORTED`.
|
|
222
258
|
- **`describeAgents()` / `registeredModels()` are offered, not published** — their only legal consumer
|
|
223
259
|
is `@ultimat3/cli`. The facts are a THUNK; an unnamed agent has no row.
|
|
224
|
-
- **`agentJob()` composes `job()`** (never an imitation
|
|
225
|
-
REQUIRED; both reads of `target.job()` are LAZY.
|
|
226
|
-
|
|
260
|
+
- **`agentJob()` composes `job()`** (never an imitation). `name`, `tenant`, `retry` are
|
|
261
|
+
REQUIRED; both reads of `target.job()` are LAZY. No result is kept: wrap an ACTION that runs the
|
|
262
|
+
agent and writes, target bound from its input — never a model-aimed write tool; every write
|
|
263
|
+
idempotent (unenforceable). **`actor`** re-resolves who it acts FOR each attempt (a served worker:
|
|
264
|
+
ANONYMOUS) via `impersonate`; off-tenant is TERMINAL `X_JOB_TENANT_MISMATCH`.
|
|
227
265
|
|
|
228
266
|
## Invariants — evals, retrieval, fix lines
|
|
229
267
|
|
|
@@ -241,8 +279,8 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
|
|
|
241
279
|
- **A store with no tenant bound, read inside a request whose actor has an `orgId`, is
|
|
242
280
|
`X_VECTOR_UNSCOPED`** (`assertTenantRead`, both stores, every read). `scope: UNSCOPED`
|
|
243
281
|
(`crossTenant: true`) is the named opt-in; an omitted scope is `UNBOUND`.
|
|
244
|
-
- `
|
|
245
|
-
`
|
|
282
|
+
- `postgresVectorStore()` is the only production vector path (pgvector + FTS in the app's Postgres);
|
|
283
|
+
`memoryVectorStore()` enforces the same envelope. **`pg-vector.live.test.ts` refuses to skip** on a
|
|
246
284
|
Postgres without the extension (CI uses `pgvector/pgvector:pg17`).
|
|
247
285
|
- The distance ordering lives in an ascending raw subquery (the only shape hnsw answers), pinned by a
|
|
248
286
|
plan assertion. hnsw scopes after the scan — assert rows, never the node; `analyze` after a backfill.
|