@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 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()` `.tool()` names are verbatim too. Never derive one.
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
- | `provider.ts` | `Provider` interface, the request half, the money arithmetic, Anthropic + Echo |
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`, content hashing, version registry |
37
- | `embeddings.ts` | `Embedder`, `HashEmbedder`, cosine helpers |
38
- | `remote-embedder.ts` | `RemoteEmbedder` — the production `/v1/embeddings` client |
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` | `PgVectorStore` — the production store |
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, split out because `agent.test.ts` is at the 500-line ceiling |
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 own dogfood eval — the first framework-level `*.eval.test.ts`, proving the `defineEval`/baseline convention actually fails a build |
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`; `RemoteEmbedder`'s `maxResponseBytes`).
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
- `MemoryBudgetStore` has no window and no eviction (eviction is a ceiling bypass); a counter back
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>`. `callLedger` is required on
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
- (`createGateway({ budgetStore })`). `add` takes a negative `tokens` for a release — a store that
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
- (`MAX_SEMANTIC_CACHE_SCOPES`).
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`/`DEFAULT_RETRY` keep the
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`). The built-ins register through the same `registerModel`.
165
- **Re-registering REPLACES the spec and keeps its rung** (negotiated rates). **Registration order is
166
- the ladder within a `family`**, read only by `moreCapableThan`; `X_LLM_REFUSED`'s fix names a rung
167
- ABOVE or nothing.
168
- - `AnthropicProvider.models` is its own list; `EchoProvider.models` reads the registry.
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 in `MODELS`.
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
- - only `gpt-5.6-sol` / `-terra` / `-luna` are priced; `registerOpenAiModels()` re-registers after
182
- `resetModels()` (every `openai-*.test.ts` calls it in `beforeEach`);
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 `RemoteEmbedder` for every vendor; vectors L2-normalised; a wrong width is
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
- `EchoProvider`'s `replies` use `Object.hasOwn`.
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 via `isThrownError`/`stringField`; `skipped` reasons
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 handle). `name`, `tenant`, `retry` are
225
- REQUIRED; both reads of `target.job()` are LAZY. **Every tool an `agentJob()`'d agent may call must
226
- be idempotent** — stated in `AgentJobOptions.idempotencyKey`'s doc and the README, not enforceable.
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
- - `PgVectorStore` is the only production vector path (pgvector + FTS in the app's Postgres);
245
- `MemoryVectorStore` enforces the same envelope. **`pg-vector.live.test.ts` refuses to skip** on a
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.