@ultimat3/ai 24.0.0 → 25.0.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 +65 -29
- package/README.md +207 -66
- package/package.json +12 -12
- package/src/agent-facts.ts +9 -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/src/tools.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
// permissions" concept in Ultimate, because there is no second authz system.
|
|
7
7
|
//
|
|
8
8
|
// `run` below is `ProjectableAction`'s — the projection SEAM, which is what carries `invoke`.
|
|
9
|
-
// It is not a member of the action facade: an `action()` is `as`/`
|
|
10
|
-
// `contract` and the callable itself
|
|
9
|
+
// It is not a member of the action facade: an `action()` is `as`/`openapi`/`client`/`job`/
|
|
10
|
+
// `contract` and the callable itself (no `tool` since 25.0.0 — `@ultimat3/mcp`'s `toolFrom` is
|
|
11
|
+
// the MCP projection), and this header claimed `action.run` until 2026-08.
|
|
11
12
|
// `asProjectableAction` is what BUILDS that seam out of a real `action()`, so an app writes
|
|
12
13
|
// `agent({ tools: [publishPost] })` and never a hand-shaped stand-in — the same union
|
|
13
14
|
// @ultimat3/mcp's `ListedPrimitive` accepts, adapted at this package's own edge because the two
|
|
@@ -20,14 +21,15 @@
|
|
|
20
21
|
import type { AnyAction } from '@ultimat3/action';
|
|
21
22
|
import { actionName, invoke, isAction } from '@ultimat3/action';
|
|
22
23
|
import type { Actor } from '@ultimat3/core';
|
|
23
|
-
import {
|
|
24
|
-
import { statusFor } from '@ultimat3/http';
|
|
24
|
+
import { isMcpExposed, tryUseContext } from '@ultimat3/core';
|
|
25
25
|
import type { WireJsonSchema } from '@ultimat3/schema';
|
|
26
26
|
import { toWireSchema } from '@ultimat3/schema';
|
|
27
|
+
import { discloseFailure } from './failure-disclosure';
|
|
28
|
+
import { takeToolIdempotencyKey, withToolIdempotencyKey } from './tool-idempotency';
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* The JSON Schema subset the framework emits for tool arguments — `@ultimat3/schema`'s
|
|
30
|
-
* `WireJsonSchema`, the one document
|
|
32
|
+
* `WireJsonSchema`, the one document `toolFrom`, `tools/list` and the model are all handed.
|
|
31
33
|
*/
|
|
32
34
|
export type JsonSchema = WireJsonSchema;
|
|
33
35
|
|
|
@@ -75,7 +77,7 @@ export interface ProjectableAction {
|
|
|
75
77
|
*
|
|
76
78
|
* The real `action()` comes first because it is what an app has. Until 2026-08 this list took
|
|
77
79
|
* `ProjectableAction` alone, which no `action()` structurally satisfies — an action carries
|
|
78
|
-
* `as`/`
|
|
80
|
+
* `as`/`openapi`/`client`/`job`/`contract` and never `run` — so the documented shape
|
|
79
81
|
* `agent({ tools: [publishPost] })` was a `TS2741` and every test in this package hand-built a
|
|
80
82
|
* stand-in, which is why the suite stayed green over an API that did not compile (issue #124).
|
|
81
83
|
* `ProjectableAction` stays in the union for a surface that builds its catalog programmatically
|
|
@@ -84,8 +86,8 @@ export interface ProjectableAction {
|
|
|
84
86
|
export type AgentTool = AnyAction | ProjectableAction;
|
|
85
87
|
|
|
86
88
|
/**
|
|
87
|
-
* Adapt whatever the author listed. The same shape @ultimat3/mcp's `
|
|
88
|
-
* the same `invoke` — an in-app agent and an external MCP client end at one execution path, so one
|
|
89
|
+
* Adapt whatever the author listed. The same shape @ultimat3/mcp's `primitiveFromAction` produces,
|
|
90
|
+
* from the same `invoke` — an in-app agent and an external MCP client end at one execution path, so one
|
|
89
91
|
* policy decides both. The execution adapter is not shared code and cannot be — `mcp` is this
|
|
90
92
|
* package's own tier — but the schema is: both publish `@ultimat3/schema`'s `toWireSchema`.
|
|
91
93
|
*
|
|
@@ -96,17 +98,30 @@ export type AgentTool = AnyAction | ProjectableAction;
|
|
|
96
98
|
export function asProjectableAction(listed: AgentTool): ProjectableAction {
|
|
97
99
|
if (!isAction(listed)) return listed;
|
|
98
100
|
const mcp = listed.mcp;
|
|
101
|
+
// Throws `X_ACTION_UNREGISTERED` on an unnamed action rather than offering a tool called `''`:
|
|
102
|
+
// a nameless tool is unaddressable by the model, by `runLlmToolCall` and by the author.
|
|
103
|
+
const name = actionName(listed);
|
|
104
|
+
const wire = toWireSchema(listed.input);
|
|
105
|
+
// `idempotent: true` is honoured here as over MCP and HTTP: the model's key reaches `invoke`,
|
|
106
|
+
// which files it under the action, the caller and the key. Until 25.0.0 this adapter dropped it,
|
|
107
|
+
// so an action an external agent could retry safely was one an in-app agent could not.
|
|
108
|
+
const keyed = listed.describe().idempotent;
|
|
99
109
|
return {
|
|
100
|
-
|
|
101
|
-
// a nameless tool is unaddressable by the model, by `runLlmToolCall` and by the author.
|
|
102
|
-
name: actionName(listed),
|
|
110
|
+
name,
|
|
103
111
|
...(mcp === undefined ? {} : { mcp }),
|
|
104
112
|
...(mcp?.description === undefined ? {} : { description: mcp.description }),
|
|
105
|
-
inputJsonSchema:
|
|
113
|
+
inputJsonSchema: keyed ? withToolIdempotencyKey(name, wire) : wire,
|
|
106
114
|
// The actor rides in on the options and `invoke` swaps it inside the one execution path —
|
|
107
115
|
// the action's own `policy` still decides, and its `input:` still parses what the model sent,
|
|
108
116
|
// which is what drops a `{ actor: 'admin' }` the model invented before any handler sees it.
|
|
109
|
-
|
|
117
|
+
// No `admit`: MCP's is its early gate ahead of ITS argument validation, and this surface has
|
|
118
|
+
// none — `invoke` asks the same policy before it reads the input, which is the same order.
|
|
119
|
+
run: keyed
|
|
120
|
+
? ({ input: args, actor }) => {
|
|
121
|
+
const { input, idempotencyKey } = takeToolIdempotencyKey(args);
|
|
122
|
+
return invoke(listed, input, { surface: 'mcp', actor, idempotencyKey });
|
|
123
|
+
}
|
|
124
|
+
: ({ input, actor }) => invoke(listed, input, { surface: 'mcp', actor }),
|
|
110
125
|
};
|
|
111
126
|
}
|
|
112
127
|
|
|
@@ -162,7 +177,7 @@ export async function runLlmToolCall(
|
|
|
162
177
|
return resultOf(call.id, output);
|
|
163
178
|
} catch (error) {
|
|
164
179
|
// A policy denial is an outcome the model should read and react to, not a crash.
|
|
165
|
-
return { toolUseId: call.id, content: describeFailure(error), isError: true };
|
|
180
|
+
return { toolUseId: call.id, content: describeFailure(error, call.name), isError: true };
|
|
166
181
|
}
|
|
167
182
|
}
|
|
168
183
|
|
|
@@ -195,29 +210,16 @@ function resultOf(toolUseId: string, output: unknown): LlmToolResult {
|
|
|
195
210
|
* it: the provider keeps every request it is sent, and a cause that reached it cannot be recalled.
|
|
196
211
|
*/
|
|
197
212
|
export const HIDDEN_TOOL_CAUSE =
|
|
198
|
-
'the tool failed inside the server; the details are withheld from this conversation
|
|
213
|
+
'the tool failed inside the server; the details are withheld from this conversation; the server log records its code and where it failed';
|
|
199
214
|
|
|
200
215
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* that has nothing left to answer the model with if the probe itself raises.
|
|
205
|
-
*
|
|
206
|
-
* A 5xx code's cause is shown only when core's `hasPublicCause` allows it — the rule a production
|
|
207
|
-
* problem document follows (`@ultimat3/http`'s `toProblem`). Hidden, the developer's `fix` goes with
|
|
208
|
-
* it (a driver writes it from the statement it failed on); a `callerFix` an `UltimateError` built in
|
|
209
|
-
* this process is authored for a remote reader and stays.
|
|
216
|
+
* `discloseFailure`'s verdict, rendered as the `tool_result` line — the same rule a hive member's
|
|
217
|
+
* reason follows, so what an agent may read of a throw is decided once. An uncoded throw says only
|
|
218
|
+
* that it failed; a withheld cause becomes `HIDDEN_TOOL_CAUSE`, and the log has the rest.
|
|
210
219
|
*/
|
|
211
|
-
function describeFailure(error: unknown): string {
|
|
212
|
-
const
|
|
213
|
-
if (code === undefined) return 'tool failed';
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return callerFix === undefined
|
|
217
|
-
? `${code}: ${HIDDEN_TOOL_CAUSE}`
|
|
218
|
-
: `${code}: ${HIDDEN_TOOL_CAUSE} (fix: ${callerFix})`;
|
|
219
|
-
}
|
|
220
|
-
const cause = stringField(error, 'cause') ?? 'unknown';
|
|
221
|
-
const fix = stringField(error, 'fix') ?? '';
|
|
222
|
-
return fix === '' ? `${code}: ${cause}` : `${code}: ${cause} (fix: ${fix})`;
|
|
220
|
+
function describeFailure(error: unknown, toolName: string): string {
|
|
221
|
+
const shown = discloseFailure(error, tryUseContext()?.logger, `tool ${toolName}`);
|
|
222
|
+
if (shown.code === undefined) return 'tool failed';
|
|
223
|
+
const line = `${shown.code}: ${shown.cause ?? HIDDEN_TOOL_CAUSE}`;
|
|
224
|
+
return shown.fix === undefined ? line : `${line} (fix: ${shown.fix})`;
|
|
223
225
|
}
|
package/src/vector-scope.ts
CHANGED
|
@@ -33,10 +33,10 @@ export const UNSCOPED: VectorScope = Object.freeze({ crossTenant: true });
|
|
|
33
33
|
export const UNBOUND: VectorScope = Object.freeze({});
|
|
34
34
|
|
|
35
35
|
/** The tenant column value a row carries when its store had no tenant bound. */
|
|
36
|
-
export const
|
|
36
|
+
export const NO_VECTOR_TENANT = '';
|
|
37
37
|
|
|
38
38
|
export function tenantOf(scope: VectorScope): string {
|
|
39
|
-
return scope.tenant ??
|
|
39
|
+
return scope.tenant ?? NO_VECTOR_TENANT;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
package/src/vector.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Vector storage and retrieval: the `VectorStore` contract and the in-memory dev store.
|
|
2
|
-
// `
|
|
2
|
+
// `PostgresVectorStore` — the production path — implements this same contract in `pg-vector.ts`.
|
|
3
3
|
//
|
|
4
4
|
// The store interface is shaped for pgvector (one table, one index, SQL you can read), with
|
|
5
5
|
// an in-memory cosine implementation as the dev default. Hybrid search is first-class rather
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
// be comparable — which they never are.
|
|
10
10
|
|
|
11
11
|
import { finiteCount, finiteOption } from '@ultimat3/core';
|
|
12
|
-
import { cosine,
|
|
12
|
+
import { cosine, wordTokens } from './embeddings';
|
|
13
13
|
import { VectorDimMismatchError } from './errors';
|
|
14
14
|
import {
|
|
15
15
|
assertTenantRead,
|
|
16
|
-
|
|
16
|
+
NO_VECTOR_TENANT,
|
|
17
17
|
narrowScope,
|
|
18
18
|
scopeAdmits,
|
|
19
19
|
UNBOUND,
|
|
@@ -95,6 +95,15 @@ export interface StoredRecord extends VectorRecord {
|
|
|
95
95
|
readonly tenant: string;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/** The row's primary key, `(tenant, id)` as the pg table declares it. NUL never occurs in a tenant. */
|
|
99
|
+
const storageKey = (tenant: string, id: string): string => `${tenant}\u0000${id}`;
|
|
100
|
+
|
|
101
|
+
/** A row and its score inside one ranking, before the tenant is dropped from the public hit. */
|
|
102
|
+
interface Scored {
|
|
103
|
+
readonly record: StoredRecord;
|
|
104
|
+
readonly score: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
98
107
|
export class MemoryVectorStore implements VectorStore {
|
|
99
108
|
readonly name: string;
|
|
100
109
|
readonly dimension: number;
|
|
@@ -119,7 +128,7 @@ export class MemoryVectorStore implements VectorStore {
|
|
|
119
128
|
}
|
|
120
129
|
|
|
121
130
|
/**
|
|
122
|
-
* Same envelope, same rule as `
|
|
131
|
+
* Same envelope, same rule as `PostgresVectorStore.scoped`. The dev store enforces it too, because
|
|
123
132
|
* a tenant leak that only reproduces against production Postgres is a leak nobody finds.
|
|
124
133
|
*/
|
|
125
134
|
scoped(scope: VectorScope): MemoryVectorStore {
|
|
@@ -131,10 +140,10 @@ export class MemoryVectorStore implements VectorStore {
|
|
|
131
140
|
}
|
|
132
141
|
|
|
133
142
|
async upsert(records: readonly VectorRecord[]): Promise<void> {
|
|
134
|
-
const tenant = this.scope.tenant ??
|
|
143
|
+
const tenant = this.scope.tenant ?? NO_VECTOR_TENANT;
|
|
135
144
|
for (const record of records) {
|
|
136
145
|
this.assertDimension(record.vector.length);
|
|
137
|
-
this.records.set(
|
|
146
|
+
this.records.set(storageKey(tenant, record.id), { ...record, tenant });
|
|
138
147
|
}
|
|
139
148
|
}
|
|
140
149
|
|
|
@@ -147,10 +156,9 @@ export class MemoryVectorStore implements VectorStore {
|
|
|
147
156
|
this.assertDimension(vector.length);
|
|
148
157
|
// `k` carries no default, so `??` never sees it and nothing else does either: `slice(0, NaN)`
|
|
149
158
|
// is `[]`, which is a search that answers "no matches" for every query and reports success.
|
|
150
|
-
return this.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
.slice(0, finiteCount(SUBJECT, 'k', k));
|
|
159
|
+
return this.dense(vector, finiteCount(SUBJECT, 'k', k), filter).map(({ record, score }) =>
|
|
160
|
+
this.hit(record, score),
|
|
161
|
+
);
|
|
154
162
|
}
|
|
155
163
|
|
|
156
164
|
/** BM25 over the stored text. Real lexical scoring, so a rare exact term actually wins. */
|
|
@@ -160,12 +168,66 @@ export class MemoryVectorStore implements VectorStore {
|
|
|
160
168
|
filter?: MetadataFilter,
|
|
161
169
|
): Promise<readonly SearchHit[]> {
|
|
162
170
|
assertTenantRead(this.name, this.scope);
|
|
163
|
-
|
|
171
|
+
return this.lexical(query, finiteCount(SUBJECT, 'k', k), filter).map(({ record, score }) =>
|
|
172
|
+
this.hit(record, score),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Reciprocal-rank fusion. Each ranking contributes `1 / (rrfK + rank)`, so only ORDER
|
|
178
|
+
* matters — the two score scales never have to be reconciled, and a document ranked first
|
|
179
|
+
* by exact term match beats one that merely leads a flat vector ranking.
|
|
180
|
+
*
|
|
181
|
+
* Fused on the stored `(tenant, id)`, as `hybridSql` groups: an unscoped read sees every
|
|
182
|
+
* tenant's rows, and two tenants may share an id. Fusing on the id alone summed both rows'
|
|
183
|
+
* ranks into whichever came first and dropped the other. `fuse` keys on `SearchHit.id`, so the
|
|
184
|
+
* rankings reach it under the row's storage key and leave under the caller's id again.
|
|
185
|
+
*/
|
|
186
|
+
async hybrid(input: HybridSearchInput): Promise<readonly SearchHit[]> {
|
|
187
|
+
assertTenantRead(this.name, this.scope);
|
|
188
|
+
this.assertDimension(input.vector.length);
|
|
189
|
+
// Three bounds, none of which `Math.max` screens — it PROPAGATES a `NaN`. A `NaN` `k` collapses
|
|
190
|
+
// both candidate widths and the final slice to `[]`; a `NaN` `rrfK` makes every fused score
|
|
191
|
+
// `NaN`, and the ranking this method exists to produce becomes whatever order the sort left.
|
|
192
|
+
const k = finiteCount(HYBRID, 'k', input.k);
|
|
193
|
+
const width = finiteCount(HYBRID, 'candidates', input.candidates ?? Math.max(k * 4, 20));
|
|
194
|
+
const rrfK = finiteOption(HYBRID, 'rrfK', input.rrfK ?? 60);
|
|
195
|
+
const rows = new Map<string, StoredRecord>();
|
|
196
|
+
const keyed = (ranking: readonly Scored[]): readonly SearchHit[] =>
|
|
197
|
+
ranking.map(({ record, score }) => {
|
|
198
|
+
const key = storageKey(record.tenant, record.id);
|
|
199
|
+
rows.set(key, record);
|
|
200
|
+
return { ...this.hit(record, score), id: key };
|
|
201
|
+
});
|
|
202
|
+
const fused = fuse(
|
|
203
|
+
[
|
|
204
|
+
keyed(this.dense(input.vector, width, input.filter)),
|
|
205
|
+
keyed(this.lexical(input.query, width, input.filter)),
|
|
206
|
+
],
|
|
207
|
+
rrfK,
|
|
208
|
+
);
|
|
209
|
+
// Re-sorted once the ids are the caller's again: the tie-break is `d."id" asc`, never the key.
|
|
210
|
+
return fused
|
|
211
|
+
.map((hit) => ({ ...hit, id: rows.get(hit.id)?.id ?? hit.id }))
|
|
212
|
+
.sort(byScoreDesc)
|
|
213
|
+
.slice(0, k);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** The cosine ranking `search` answers and `hybrid` fuses, rows still carrying their tenant. */
|
|
217
|
+
private dense(vector: Float32Array, k: number, filter?: MetadataFilter): readonly Scored[] {
|
|
218
|
+
return this.candidates(filter)
|
|
219
|
+
.map((record) => ({ record, score: cosine(vector, record.vector) }))
|
|
220
|
+
.sort(byRankDesc)
|
|
221
|
+
.slice(0, k);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** BM25 over the stored text, rows still carrying their tenant. Only a positive score matches. */
|
|
225
|
+
private lexical(query: string, width: number, filter?: MetadataFilter): readonly Scored[] {
|
|
164
226
|
const candidates = this.candidates(filter);
|
|
165
227
|
if (candidates.length === 0) return [];
|
|
166
|
-
const docs = candidates.map((record) => ({ record, tokens:
|
|
228
|
+
const docs = candidates.map((record) => ({ record, tokens: wordTokens(record.text) }));
|
|
167
229
|
const avgLength = docs.reduce((sum, d) => sum + d.tokens.length, 0) / docs.length;
|
|
168
|
-
const terms = [...new Set(
|
|
230
|
+
const terms = [...new Set(wordTokens(query))];
|
|
169
231
|
|
|
170
232
|
return docs
|
|
171
233
|
.map(({ record, tokens }) => {
|
|
@@ -178,33 +240,13 @@ export class MemoryVectorStore implements VectorStore {
|
|
|
178
240
|
const norm = this.k1 * (1 - this.b + (this.b * tokens.length) / avgLength);
|
|
179
241
|
score += idf * ((tf * (this.k1 + 1)) / (tf + norm));
|
|
180
242
|
}
|
|
181
|
-
return
|
|
243
|
+
return { record, score };
|
|
182
244
|
})
|
|
183
|
-
.filter((
|
|
184
|
-
.sort(
|
|
245
|
+
.filter((scored) => scored.score > 0)
|
|
246
|
+
.sort(byRankDesc)
|
|
185
247
|
.slice(0, width);
|
|
186
248
|
}
|
|
187
249
|
|
|
188
|
-
/**
|
|
189
|
-
* Reciprocal-rank fusion. Each ranking contributes `1 / (rrfK + rank)`, so only ORDER
|
|
190
|
-
* matters — the two score scales never have to be reconciled, and a document ranked first
|
|
191
|
-
* by exact term match beats one that merely leads a flat vector ranking.
|
|
192
|
-
*/
|
|
193
|
-
async hybrid(input: HybridSearchInput): Promise<readonly SearchHit[]> {
|
|
194
|
-
assertTenantRead(this.name, this.scope);
|
|
195
|
-
// Three bounds, none of which `Math.max` screens — it PROPAGATES a `NaN`. A `NaN` `k` collapses
|
|
196
|
-
// both candidate widths and the final slice to `[]`; a `NaN` `rrfK` makes every fused score
|
|
197
|
-
// `NaN`, and the ranking this method exists to produce becomes whatever order the sort left.
|
|
198
|
-
const k = finiteCount(HYBRID, 'k', input.k);
|
|
199
|
-
const width = finiteCount(HYBRID, 'candidates', input.candidates ?? Math.max(k * 4, 20));
|
|
200
|
-
const rrfK = finiteOption(HYBRID, 'rrfK', input.rrfK ?? 60);
|
|
201
|
-
const [dense, lexical] = await Promise.all([
|
|
202
|
-
this.search(input.vector, width, input.filter),
|
|
203
|
-
this.searchText(input.query, width, input.filter),
|
|
204
|
-
]);
|
|
205
|
-
return fuse([dense, lexical], rrfK).slice(0, k);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
250
|
/** Scoped and filtered, exactly like `pruneSql`: everything `filter` matches but `keep`. */
|
|
209
251
|
async prune(filter: MetadataFilter, keep: readonly string[]): Promise<void> {
|
|
210
252
|
const kept = new Set(keep);
|
|
@@ -247,6 +289,14 @@ export class MemoryVectorStore implements VectorStore {
|
|
|
247
289
|
}
|
|
248
290
|
}
|
|
249
291
|
|
|
292
|
+
/**
|
|
293
|
+
* The one way to build the in-process store — the twin of `postgresVectorStore()`. The class is a
|
|
294
|
+
* type in the barrel only (`X_FACTORY_NAME_SPELLING`), so `new` is never a second spelling.
|
|
295
|
+
*/
|
|
296
|
+
export function memoryVectorStore(input: MemoryVectorStoreInput): MemoryVectorStore {
|
|
297
|
+
return new MemoryVectorStore(input);
|
|
298
|
+
}
|
|
299
|
+
|
|
250
300
|
/**
|
|
251
301
|
* Fuse ranked lists by reciprocal rank. Exported so a reranker can reuse it — which is why the
|
|
252
302
|
* damping is screened HERE too and not only in `hybrid`: a default parameter is a bound like any
|
|
@@ -279,4 +329,20 @@ export function fuse(
|
|
|
279
329
|
* sort then resolves them by dense-list insertion order, which no SQL engine reproduces.
|
|
280
330
|
*/
|
|
281
331
|
const byScoreDesc = (a: SearchHit, b: SearchHit): number =>
|
|
282
|
-
|
|
332
|
+
byScore(a.score, b.score) || byId(a.id, b.id);
|
|
333
|
+
|
|
334
|
+
/** The same order over a ranking's rows, which still carry their tenant. */
|
|
335
|
+
const byRankDesc = (a: Scored, b: Scored): number =>
|
|
336
|
+
byScore(a.score, b.score) || byId(a.record.id, b.record.id);
|
|
337
|
+
|
|
338
|
+
const byId = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Descending, `NaN` last. A cosine against a zero-norm vector is `NaN`, and float8 orders `NaN`
|
|
342
|
+
* above every number, so pg's `order by distance` puts that row at the end. `b - a` alone is `NaN`
|
|
343
|
+
* for any pair holding one, which reads as a tie and leaves the sort inconsistent.
|
|
344
|
+
*/
|
|
345
|
+
function byScore(a: number, b: number): number {
|
|
346
|
+
if (Number.isNaN(a) || Number.isNaN(b)) return Number.isNaN(a) ? (Number.isNaN(b) ? 0 : 1) : -1;
|
|
347
|
+
return b - a;
|
|
348
|
+
}
|
package/src/wire.ts
CHANGED
|
@@ -185,9 +185,14 @@ export class MessageStream {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
/**
|
|
188
|
+
/**
|
|
189
|
+
* True once `message_stop` arrived with every tool_use block closed. False means the connection
|
|
190
|
+
* died mid-answer — or the stream ended with a call's arguments still open, which drops the call
|
|
191
|
+
* (it is emitted whole or not at all), and for `llm()` that call IS the answer. `openai-wire.ts`
|
|
192
|
+
* refuses the same case.
|
|
193
|
+
*/
|
|
189
194
|
isComplete(): boolean {
|
|
190
|
-
return this.stopped;
|
|
195
|
+
return this.stopped && this.pending.size === 0;
|
|
191
196
|
}
|
|
192
197
|
|
|
193
198
|
state(): StreamState {
|
package/src/fix-line.evals.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The eval attached to `fixLinePrompt`. Cases live here rather than in the test file, matching
|
|
3
|
-
* every app's own prompts — a fixture-driven, deterministic `exact` scorer, no judge, because
|
|
4
|
-
* this is a proof of the eval mechanism itself and a proof that can drift is not one.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { defineEval } from './evals';
|
|
8
|
-
import { fixLinePrompt } from './fix-line';
|
|
9
|
-
import { exact } from './scorers';
|
|
10
|
-
|
|
11
|
-
export const fixLineCases = [
|
|
12
|
-
{
|
|
13
|
-
name: 'runnable/x-command',
|
|
14
|
-
vars: { fixLine: 'bun run scripts/verify.ts --only lint,boundaries' },
|
|
15
|
-
expected: 'runnable',
|
|
16
|
-
},
|
|
17
|
-
{ name: 'runnable/cli-flag', vars: { fixLine: 'x db migrate' }, expected: 'runnable' },
|
|
18
|
-
{
|
|
19
|
-
name: 'vague/check',
|
|
20
|
-
vars: { fixLine: 'check the configuration and try again' },
|
|
21
|
-
expected: 'vague',
|
|
22
|
-
},
|
|
23
|
-
{ name: 'vague/see-docs', vars: { fixLine: 'see the docs for details' }, expected: 'vague' },
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
export const fixLineEval = defineEval({
|
|
27
|
-
name: 'ai.fix-line-runnable',
|
|
28
|
-
prompt: fixLinePrompt,
|
|
29
|
-
// The gate is the drop from this recorded score, never an absolute number: models drift,
|
|
30
|
-
// prompts should not. Accepting a new number is a diff in the committed baseline file.
|
|
31
|
-
baseline: import.meta.resolve('./fix-line.v1.baseline.json'),
|
|
32
|
-
tolerance: 0.05,
|
|
33
|
-
scorers: [exact],
|
|
34
|
-
cases: fixLineCases,
|
|
35
|
-
});
|
package/src/fix-line.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The prompt behind `fix-line` — `@ultimat3/ai`'s own dogfood eval, and the package's first
|
|
3
|
-
* framework-level `*.eval.test.ts`. Not an app feature: every app that declares a prompt is
|
|
4
|
-
* required to pair it with an eval and a committed baseline (`defineEval`, `X_EVAL_MISSING`,
|
|
5
|
-
* `X_EVAL_BASELINE_MISSING`), and this proves that whole convention actually catches a
|
|
6
|
-
* regression from inside the package that owns it — through the real opt-in suite `x verify`'s
|
|
7
|
-
* `eval` step runs, not only through `evals.test.ts`'s unit fixtures against a temp-dir baseline.
|
|
8
|
-
*
|
|
9
|
-
* The task is small on purpose: classify whether an error's `fix:` line is a runnable command —
|
|
10
|
-
* axiom 4, "errors are instructions" — or vague guidance ("check the config", "see the docs").
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { definePrompt } from './prompt';
|
|
14
|
-
|
|
15
|
-
export const fixLinePrompt = definePrompt<{ fixLine: string }>({
|
|
16
|
-
id: 'ai.fix-line-runnable',
|
|
17
|
-
version: '1',
|
|
18
|
-
template:
|
|
19
|
-
'Reply with exactly one word: "runnable" if the fix line below names a command to run, or ' +
|
|
20
|
-
'"vague" if it only describes what to do without naming one.\nFix line: {{fixLine}}',
|
|
21
|
-
input: {
|
|
22
|
-
type: 'object',
|
|
23
|
-
properties: { fixLine: { type: 'string' } },
|
|
24
|
-
required: ['fixLine'],
|
|
25
|
-
},
|
|
26
|
-
output: { type: 'string', enum: ['runnable', 'vague'] },
|
|
27
|
-
});
|
package/src/openai-models.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// The OpenAI-format built-in catalogue: the vendor's own list prices, registered through the same
|
|
2
|
-
// public `registerModel` the Anthropic built-ins use. Here rather than in models.ts because these
|
|
3
|
-
// rows belong to a PROVIDER — models.ts owns the registry mechanism, never one vendor's price list.
|
|
4
|
-
|
|
5
|
-
import type { Money } from '@ultimat3/money';
|
|
6
|
-
import type { ModelId } from './models';
|
|
7
|
-
import { registerModel } from './models';
|
|
8
|
-
|
|
9
|
-
/** A price per million tokens, in INTEGER MINOR UNITS. Same rule as models.ts: never a float. */
|
|
10
|
-
const usd = (minor: number): Money => ({ minor, currency: 'USD' });
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The ids this package prices, in ladder order (most capable first). A provider's `models` list is
|
|
14
|
-
* still its own — an endpoint speaking this format serves whatever ids it was deployed with, and
|
|
15
|
-
* `openAiProvider({ models })` is where those are named.
|
|
16
|
-
*/
|
|
17
|
-
export const OPENAI_MODEL_IDS: readonly ModelId[] = [
|
|
18
|
-
'gpt-5.6-sol',
|
|
19
|
-
'gpt-5.6-terra',
|
|
20
|
-
'gpt-5.6-luna',
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Shared by the whole 5.6 family: a 1.05M context, a 128k output ceiling, and `reasoning_effort`
|
|
25
|
-
* over exactly the five rungs `EFFORTS` declares (the endpoint also takes `none`, which is what
|
|
26
|
-
* `thinking: 'disabled'` maps onto). `adaptive` is false because the format has no
|
|
27
|
-
* adaptive-thinking control at all — depth is `reasoning_effort` and nothing else.
|
|
28
|
-
*/
|
|
29
|
-
const FAMILY = {
|
|
30
|
-
/** One ladder: `moreCapableThan` compares these three with each other and with nothing else. */
|
|
31
|
-
family: 'openai',
|
|
32
|
-
contextWindow: 1_050_000,
|
|
33
|
-
maxOutput: 128_000,
|
|
34
|
-
/** Automatic caching starts at a 1024-token prefix; a shorter one silently does not cache. */
|
|
35
|
-
cacheMinimumTokens: 1_024,
|
|
36
|
-
reasoning: { effort: true, adaptive: false, disableThinkingUpTo: undefined },
|
|
37
|
-
} as const;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* The three models this package is confident enough to price, and no more.
|
|
41
|
-
*
|
|
42
|
-
* Prices are the vendor's published list, read from developers.openai.com/api/docs/pricing on
|
|
43
|
-
* **2026-08-16**, in USD per million tokens:
|
|
44
|
-
*
|
|
45
|
-
* | id | input | cached input | output |
|
|
46
|
-
* |---|---|---|---|
|
|
47
|
-
* | `gpt-5.6-sol` | $5.00 | $0.50 | $30.00 |
|
|
48
|
-
* | `gpt-5.6-terra` | $2.00 | $0.20 | $12.00 |
|
|
49
|
-
* | `gpt-5.6-luna` | $0.20 | $0.02 | $1.20 |
|
|
50
|
-
*
|
|
51
|
-
* Deliberately not registered: `gpt-4o`, `gpt-4o-mini` and the `o1` family, whose cached input is
|
|
52
|
-
* **0.5x** their input rate rather than the 0.1x `costOf` assumes — a spec that prices them would
|
|
53
|
-
* under-report a cache-heavy workload by four fifths, and `costOf` answers confidently either way.
|
|
54
|
-
* `gpt-5.5-pro` and `o1-pro` are out for the same class of reason: they publish no cached rate.
|
|
55
|
-
* A wrong price is worse than no entry, so an app wanting one of those registers it itself, with
|
|
56
|
-
* the rate its own contract names.
|
|
57
|
-
*/
|
|
58
|
-
export function registerOpenAiModels(): void {
|
|
59
|
-
registerModel({
|
|
60
|
-
id: 'gpt-5.6-sol',
|
|
61
|
-
...FAMILY,
|
|
62
|
-
inputPerMillion: usd(500),
|
|
63
|
-
outputPerMillion: usd(3_000),
|
|
64
|
-
});
|
|
65
|
-
registerModel({
|
|
66
|
-
id: 'gpt-5.6-terra',
|
|
67
|
-
...FAMILY,
|
|
68
|
-
inputPerMillion: usd(200),
|
|
69
|
-
outputPerMillion: usd(1_200),
|
|
70
|
-
});
|
|
71
|
-
registerModel({
|
|
72
|
-
id: 'gpt-5.6-luna',
|
|
73
|
-
...FAMILY,
|
|
74
|
-
inputPerMillion: usd(20),
|
|
75
|
-
outputPerMillion: usd(120),
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// The same shape models.ts uses for its built-ins: registration is a module side effect, so the
|
|
80
|
-
// default path is the app's path and importing the provider is enough to price what it serves.
|
|
81
|
-
// Exported as well as called, because the registry is module state and a suite that clears it with
|
|
82
|
-
// `resetModels()` otherwise leaves this provider serving ids nothing can price. Re-registering
|
|
83
|
-
// REPLACES, so an app with a negotiated rate calls `registerModel` after this one and wins.
|
|
84
|
-
registerOpenAiModels();
|