toolnexus 0.25.0 → 0.27.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/README.md +35 -0
- package/dist/acp.d.ts +10 -2
- package/dist/acp.js +36 -5
- package/dist/classifier.d.ts +199 -31
- package/dist/classifier.js +639 -31
- package/dist/client.d.ts +6 -0
- package/dist/judge.d.ts +6 -3
- package/dist/judge.js +13 -1
- package/dist/mcp.js +16 -6
- package/dist/mcp2026/source.d.ts +5 -0
- package/dist/mcp2026/source.js +11 -2
- package/dist/mcp2026/stdio.d.ts +43 -0
- package/dist/mcp2026/stdio.js +144 -20
- package/dist/mcphandler.js +2 -4
- package/dist/mcpserve.js +2 -1
- package/dist/serve.js +12 -1
- package/dist/skill.d.ts +14 -0
- package/dist/skill.js +33 -2
- package/package.json +2 -2
- package/src/acp.ts +59 -7
- package/src/classifier.ts +723 -47
- package/src/client.ts +6 -0
- package/src/judge.ts +19 -4
- package/src/mcp.ts +18 -6
- package/src/mcp2026/source.ts +14 -4
- package/src/mcp2026/stdio.ts +148 -23
- package/src/mcphandler.ts +2 -3
- package/src/mcpserve.ts +2 -1
- package/src/serve.ts +13 -2
- package/src/skill.ts +51 -2
package/README.md
CHANGED
|
@@ -136,6 +136,41 @@ const out = await gate(c, state, questions, [
|
|
|
136
136
|
filter, content and model batteries into `hooks`. A `beforeLLM` hook may return `{ model }` to
|
|
137
137
|
send another model for that turn only.
|
|
138
138
|
|
|
139
|
+
### Backends, images, cache and helpers
|
|
140
|
+
|
|
141
|
+
Every default below is a **proposed default, owner to confirm**
|
|
142
|
+
(`openspec/changes/add-classifier-backends`).
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { createClassifier, judge, MemoryCache, validateDecision, thresholdBand, chunkQuestions } from "toolnexus"
|
|
146
|
+
|
|
147
|
+
const c = createClassifier({ backend: "openrouter", cache: new MemoryCache(3_600_000, 1000) })
|
|
148
|
+
const d = await c.state({ note: "listing", photo: { type: "image", mimeType: "image/png", data: b64 } })
|
|
149
|
+
.evaluate(judge.noul("red", "Is the object in `photo` red?")) // fails closed on an invalid reply
|
|
150
|
+
const rs = await c.states("a", "b").evaluate(judge.noul("q", "…")) // [{ decision } | { error }], in order
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- `backend`: `typesafe` (default) · `openrouter` (`typesafe/jev-1.14`) · `cloudflare` (`/ai/run`
|
|
154
|
+
envelope; `${CLOUDFLARE_ACCOUNT_ID}` in the URL expands at call time, errors name it unexpanded)
|
|
155
|
+
· `vercel` · `custom` (requires `baseUrl` and `supports`). The table is data:
|
|
156
|
+
`CLASSIFIER_BACKENDS`; `supports: { images }` overrides it per classifier.
|
|
157
|
+
- Images (style `systemone` only): image parts in the state are lifted into `assets` (cloudflare:
|
|
158
|
+
data URLs). PNG/JPEG, 280–1568 px a side, ≤ 16 MiB each and 32 MiB per request — never resized.
|
|
159
|
+
A backend without image support throws `ClassifierUnsupported` before any request. A state with
|
|
160
|
+
no content part is sent byte-identically to before. JS has no distinct part type, so **any**
|
|
161
|
+
object shaped like a `ContentPart` (`{type:"text",text}`, `{type:"image",mimeType,…}`) inside a
|
|
162
|
+
systemone state is treated as a part.
|
|
163
|
+
- `cache: { get(key), set(key, value) }` (sync or async, string values) — off when absent; only
|
|
164
|
+
valid replies are stored; keys follow the served model. A hit sets `decision.cached` and the
|
|
165
|
+
`classifier.evaluate` event's `cached`. Every such event also carries `questions` and
|
|
166
|
+
`questionsHash`.
|
|
167
|
+
- Helpers: `validateDecision(questions, decision)` → `Error | undefined`; `thresholdBand(answer,
|
|
168
|
+
thresholds?)` with `DEFAULT_THRESHOLDS` 0.8 / 0.2 / 0.6 (inclusive; `Bands` stays the `ask`/`gate`
|
|
169
|
+
default); `chunkQuestions(questions, size)`.
|
|
170
|
+
- Open union: `judge.raw(name, type, instructions, criteria?)` / `raw(...)`; an answer of an
|
|
171
|
+
unmodelled type decodes to `{ type, value }` (`decision.raw(key)`).
|
|
172
|
+
- `style: "llm"` is **deprecated** — it still works unchanged, with no runtime warning.
|
|
173
|
+
|
|
139
174
|
## Media generation
|
|
140
175
|
|
|
141
176
|
Generate images, video and speech (plus transcription, voice cloning and talking-head video)
|
package/dist/acp.d.ts
CHANGED
|
@@ -48,15 +48,23 @@ export interface ACPOptions {
|
|
|
48
48
|
/** One warm ACP connection: one child process, one session, alive across many
|
|
49
49
|
* turns. `.generate` has exactly the shape {@link InProcessOptions.generate}
|
|
50
50
|
* wants, so it plugs straight into {@link createInProcessClient}. */
|
|
51
|
+
/** Per-call options for {@link ACPClient.generate} / {@link ACPClient.generateStream}. */
|
|
52
|
+
export interface ACPGenerateOptions {
|
|
53
|
+
/** Cancels the turn: when it aborts mid-turn the `session/cancel` notification is sent,
|
|
54
|
+
* the turn's reply is awaited (bounded by `requestTimeoutMs`, default 30s) so the next
|
|
55
|
+
* turn never lands on a busy session, and the call rejects with an `AbortError`. The
|
|
56
|
+
* process and session stay warm. */
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
}
|
|
51
59
|
export interface ACPClient {
|
|
52
|
-
generate: (request: InProcessRequest) => Promise<InProcessResponse>;
|
|
60
|
+
generate: (request: InProcessRequest, opts?: ACPGenerateOptions) => Promise<InProcessResponse>;
|
|
53
61
|
/** {@link generate}, streaming: `emit` fires as each `session/update` arrives —
|
|
54
62
|
* agent_message_chunk → `text`, agent_thought_chunk → `thought` (via "acp"),
|
|
55
63
|
* tool_call → `tool_call` and a completed/failed tool_call_update → `tool_result`
|
|
56
64
|
* (both via "acp": the agent's own tools, reported, never executed by the loop).
|
|
57
65
|
* Shaped for {@link InProcessOptions.generateStream}. The returned content is the
|
|
58
66
|
* message text only. */
|
|
59
|
-
generateStream: (request: InProcessRequest, emit: (event: StreamEvent) => void) => Promise<InProcessResponse>;
|
|
67
|
+
generateStream: (request: InProcessRequest, emit: (event: StreamEvent) => void, opts?: ACPGenerateOptions) => Promise<InProcessResponse>;
|
|
60
68
|
/** Terminates the agent's process group. Idempotent — a second call is a no-op. */
|
|
61
69
|
close: () => Promise<void>;
|
|
62
70
|
/** The agent's raw `initialize` result. */
|
package/dist/acp.js
CHANGED
|
@@ -298,7 +298,9 @@ export async function loadACP(opts) {
|
|
|
298
298
|
// each generate() call attaches after whatever is already queued, and the
|
|
299
299
|
// queue itself never rejects (a failed turn does not poison later ones).
|
|
300
300
|
let turnQueue = Promise.resolve();
|
|
301
|
-
async function promptOnce(request, emit) {
|
|
301
|
+
async function promptOnce(request, emit, signal) {
|
|
302
|
+
if (signal?.aborted)
|
|
303
|
+
throw acpAbortError();
|
|
302
304
|
const text = renderPrompt(request);
|
|
303
305
|
const id = `c-${++nextId}`;
|
|
304
306
|
let content = "";
|
|
@@ -361,8 +363,30 @@ export async function loadACP(opts) {
|
|
|
361
363
|
const timeout = new Promise((_, reject) => {
|
|
362
364
|
timer = setTimeout(() => reject(new Error(`toolnexus: ACP turn timed out after ${permissionTimeoutMs}ms`)), permissionTimeoutMs);
|
|
363
365
|
});
|
|
366
|
+
let onAbort;
|
|
367
|
+
const cancelled = new Promise((resolve) => {
|
|
368
|
+
if (!signal)
|
|
369
|
+
return;
|
|
370
|
+
onAbort = () => resolve("cancelled");
|
|
371
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
372
|
+
});
|
|
364
373
|
try {
|
|
365
|
-
const msg = await Promise.race([replyPromise, timeout]);
|
|
374
|
+
const msg = await Promise.race([replyPromise, timeout, cancelled]);
|
|
375
|
+
if (msg === "cancelled") {
|
|
376
|
+
// ACP: session/cancel is a notification (no id); the agent then ends the turn with
|
|
377
|
+
// stopReason "cancelled". Wait (bounded) for that reply so the session is idle again.
|
|
378
|
+
write({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
|
|
379
|
+
const waitMs = opts.requestTimeoutMs && opts.requestTimeoutMs > 0 ? opts.requestTimeoutMs : 30000;
|
|
380
|
+
let wait;
|
|
381
|
+
await Promise.race([
|
|
382
|
+
replyPromise,
|
|
383
|
+
new Promise((r) => {
|
|
384
|
+
wait = setTimeout(r, waitMs);
|
|
385
|
+
}),
|
|
386
|
+
]);
|
|
387
|
+
clearTimeout(wait);
|
|
388
|
+
throw acpAbortError();
|
|
389
|
+
}
|
|
366
390
|
if (msg.error && msg.error.code === 0)
|
|
367
391
|
throw new Error(msg.error.message);
|
|
368
392
|
if (msg.error)
|
|
@@ -371,14 +395,16 @@ export async function loadACP(opts) {
|
|
|
371
395
|
}
|
|
372
396
|
finally {
|
|
373
397
|
clearTimeout(timer);
|
|
398
|
+
if (onAbort)
|
|
399
|
+
signal.removeEventListener("abort", onAbort);
|
|
374
400
|
pending.delete(id);
|
|
375
401
|
updateSink = null;
|
|
376
402
|
}
|
|
377
403
|
}
|
|
378
|
-
const generateStream = (request, emit) => {
|
|
404
|
+
const generateStream = (request, emit, callOpts) => {
|
|
379
405
|
if (closed)
|
|
380
406
|
return Promise.reject(new Error("toolnexus: ACP client is closed"));
|
|
381
|
-
const turn = turnQueue.then(() => promptOnce(request, emit));
|
|
407
|
+
const turn = turnQueue.then(() => promptOnce(request, emit, callOpts?.signal));
|
|
382
408
|
turnQueue = turn.then(() => undefined, () => undefined);
|
|
383
409
|
return turn;
|
|
384
410
|
};
|
|
@@ -400,13 +426,18 @@ export async function loadACP(opts) {
|
|
|
400
426
|
if (t === "late")
|
|
401
427
|
await Promise.race([childGone, new Promise((r) => setTimeout(r, 1000).unref())]);
|
|
402
428
|
}
|
|
403
|
-
const generate = (request) => generateStream(request);
|
|
429
|
+
const generate = (request, callOpts) => generateStream(request, undefined, callOpts);
|
|
404
430
|
const client = { generate, generateStream, close, initializeResult };
|
|
405
431
|
// Non-enumerable, undocumented — internal test hook only, so the public
|
|
406
432
|
// ACPClient surface stays exactly {generate, generateStream, close}.
|
|
407
433
|
Object.defineProperty(client, "_pid", { value: child.pid, enumerable: false });
|
|
408
434
|
return client;
|
|
409
435
|
}
|
|
436
|
+
function acpAbortError() {
|
|
437
|
+
const e = new Error("toolnexus: ACP turn cancelled");
|
|
438
|
+
e.name = "AbortError";
|
|
439
|
+
return e;
|
|
440
|
+
}
|
|
410
441
|
/** Signals the child's whole process group (POSIX), else the child itself. Never throws. */
|
|
411
442
|
function killGroup(pid, signal) {
|
|
412
443
|
if (process.platform !== "win32") {
|
package/dist/classifier.d.ts
CHANGED
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Classifier (SPEC.md §8B) — the contract for a JUDGMENT, as `Tool` is the contract for an
|
|
3
|
-
* ACTION. A System One model takes a state plus pre-declared, typed questions and returns
|
|
4
|
-
* calibrated answers with no free text. It has no messages, no tool calling and no streaming,
|
|
5
|
-
* so it never enters the §8 client loop.
|
|
6
|
-
*
|
|
7
|
-
* A classifier INTERPRETS; it never AUTHORISES. Schema validity is not correctness: a decision
|
|
8
|
-
* can be confidently wrong, and "cannot hallucinate" means only that the returned value is in
|
|
9
|
-
* the declared schema. Numeric limits, permission checks and allowlists stay in code. Nothing
|
|
10
|
-
* here is a security control.
|
|
11
|
-
*/
|
|
12
1
|
import type { Client } from "./client.js";
|
|
13
2
|
import { type ErrorInfo, type ErrorTier, type MetricEvent } from "./client.js";
|
|
14
3
|
/** The System One endpoint base. */
|
|
@@ -24,15 +13,57 @@ export declare const DEFAULT_BATCH_CONCURRENCY = 16;
|
|
|
24
13
|
/**
|
|
25
14
|
* A named BACKEND: `baseUrl`, `model` and `apiKeyEnv` are only jointly valid, so they travel as
|
|
26
15
|
* a UNIT (ADR 0027 D1). `jev-latest` is TypeSafe's spelling; the gateway spells the same model
|
|
27
|
-
* `typesafe/jev-1.
|
|
16
|
+
* `typesafe/jev-1.14`, and mixing the two is issue #91. `custom` is a self-hosted System One
|
|
17
|
+
* endpoint and requires `baseUrl` + `supports` (SPEC §8B "Backends, images, cache and validation").
|
|
28
18
|
*/
|
|
29
|
-
export type ClassifierBackend = "typesafe" | "openrouter";
|
|
30
|
-
/**
|
|
31
|
-
|
|
19
|
+
export type ClassifierBackend = "typesafe" | "openrouter" | "cloudflare" | "vercel" | "custom";
|
|
20
|
+
/** What a backend+model can take. DATA: a fail-fast for what the table says, never a claim about
|
|
21
|
+
* the vendor. Override per classifier with `ClassifierOptions.supports`. */
|
|
22
|
+
export interface ClassifierSupports {
|
|
23
|
+
images: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** One row of the backend table. */
|
|
26
|
+
export interface ClassifierBackendPreset {
|
|
27
|
+
/** `""` on `custom`, which requires `baseUrl`. May carry `${NAME}`, expanded at call time. */
|
|
32
28
|
baseUrl: string;
|
|
33
29
|
model: string;
|
|
34
30
|
apiKeyEnv: string;
|
|
35
|
-
}
|
|
31
|
+
/** `systemone`: POST {baseUrl}/systemone. `cloudflare`: POST {baseUrl}, `/ai/run` envelope. */
|
|
32
|
+
wire: "systemone" | "cloudflare";
|
|
33
|
+
/** Send `TypeSafe-Danger-Zone-Images: true` when a request carries images. */
|
|
34
|
+
imagesHeader: boolean;
|
|
35
|
+
/** The default support for a model on this backend; absent on `custom`. */
|
|
36
|
+
supports?: (model: string) => ClassifierSupports;
|
|
37
|
+
}
|
|
38
|
+
/** TypeSafe's required opt-in header for image ("danger zone") input. */
|
|
39
|
+
export declare const CLASSIFIER_IMAGES_HEADER = "TypeSafe-Danger-Zone-Images";
|
|
40
|
+
/** Image limits (docs.typesafe.ai/images): checked client-side, never resized. */
|
|
41
|
+
export declare const CLASSIFIER_IMAGE_MIN_SIDE = 280;
|
|
42
|
+
export declare const CLASSIFIER_IMAGE_MAX_SIDE = 1568;
|
|
43
|
+
export declare const CLASSIFIER_IMAGE_MAX_BYTES: number;
|
|
44
|
+
export declare const CLASSIFIER_REQUEST_IMAGE_BYTES: number;
|
|
45
|
+
/**
|
|
46
|
+
* The backend table (PROPOSED defaults, owner to confirm — design.md PD-1..PD-4). Vercel and
|
|
47
|
+
* Cloudflare-Jev images stay false until a live probe proves them.
|
|
48
|
+
*/
|
|
49
|
+
export declare const CLASSIFIER_BACKENDS: Readonly<Record<ClassifierBackend, Readonly<ClassifierBackendPreset>>>;
|
|
50
|
+
/**
|
|
51
|
+
* The env var whose key may be sent to `baseUrl` (already `${NAME}`-expanded): the backend's own
|
|
52
|
+
* variable, and only when `baseUrl` is that backend's own host over https (OpenRouter also its
|
|
53
|
+
* subdomains). `""` when none may — `custom` has no preset key at all. A key is never sent to a
|
|
54
|
+
* host the caller did not name (#155, SPEC §8B).
|
|
55
|
+
*/
|
|
56
|
+
export declare function classifierKeyEnv(backend: ClassifierBackend, baseUrl: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Raised BEFORE any network call when the backend table says the backend cannot take what the
|
|
59
|
+
* request carries. Message, exact: `classifier: <backend> (<model>) does not support <feature>`.
|
|
60
|
+
*/
|
|
61
|
+
export declare class ClassifierUnsupported extends Error {
|
|
62
|
+
readonly backend: string;
|
|
63
|
+
readonly model: string;
|
|
64
|
+
readonly feature: string;
|
|
65
|
+
constructor(backend: string, model: string, feature: string);
|
|
66
|
+
}
|
|
36
67
|
/** Client-side cap on a choice's named options (§8B). */
|
|
37
68
|
export declare const MAX_CHOICE_OPTIONS = 255;
|
|
38
69
|
/** Bounds of a score rubric (§8B). */
|
|
@@ -83,8 +114,25 @@ export interface ScoreQuestion {
|
|
|
83
114
|
instructions: string;
|
|
84
115
|
criteria: string[];
|
|
85
116
|
}
|
|
86
|
-
/** The
|
|
117
|
+
/** The modelled question types, discriminated by `type`. */
|
|
87
118
|
export type Question = NoulQuestion | ChoiceQuestion | ScoreQuestion;
|
|
119
|
+
/**
|
|
120
|
+
* The OPEN end of the question union (§8B): a System One type the library does not model (e.g.
|
|
121
|
+
* Jev 1.14 `box`), sent as written with no client-side limits. `type` must be non-empty.
|
|
122
|
+
*/
|
|
123
|
+
export interface RawQuestion {
|
|
124
|
+
type: string;
|
|
125
|
+
instructions: string;
|
|
126
|
+
/** Sent verbatim when present, absent otherwise. */
|
|
127
|
+
criteria?: unknown;
|
|
128
|
+
}
|
|
129
|
+
/** Every question `evaluate` accepts: the modelled three plus a raw one. */
|
|
130
|
+
export type AnyQuestion = Question | RawQuestion;
|
|
131
|
+
/** True when `q` is a raw question (its type is not noul/choice/score). Not a type guard: every
|
|
132
|
+
* modelled question is structurally a `RawQuestion` too. */
|
|
133
|
+
export declare function isRawQuestion(q: AnyQuestion): boolean;
|
|
134
|
+
/** A raw question, sent as written. */
|
|
135
|
+
export declare function raw(type: string, instructions: string, criteria?: unknown): RawQuestion;
|
|
88
136
|
/** A noul question. Pass `criteria` only when you mean it — absent ≠ empty on the wire. */
|
|
89
137
|
export declare function noul(instructions: string, criteria?: NoulCriteria): NoulQuestion;
|
|
90
138
|
/** A choice question. Every description must say what picking that option would MEAN. */
|
|
@@ -98,7 +146,7 @@ export declare function score(instructions: string, criteria: string[]): ScoreQu
|
|
|
98
146
|
*/
|
|
99
147
|
export declare function choiceOver(instructions: string, items: Record<string, string>): ChoiceQuestion;
|
|
100
148
|
/** The wire form of one question — the object itself, minus an absent `criteria`. */
|
|
101
|
-
export declare function questionWire(
|
|
149
|
+
export declare function questionWire(question: AnyQuestion): Record<string, unknown>;
|
|
102
150
|
/**
|
|
103
151
|
* Canonical JSON: object keys sorted recursively in CODE-UNIT (ASCII) order, arrays NEVER
|
|
104
152
|
* reordered, compact separators, and `<>&'"` plus non-ASCII transmitted raw.
|
|
@@ -120,7 +168,7 @@ export declare function canonicalJson(value: unknown): string;
|
|
|
120
168
|
* ways across our own seven runtimes). Do not re-widen this: a caller who needs their state
|
|
121
169
|
* pinned canonicalises it themselves before handing it over.
|
|
122
170
|
*/
|
|
123
|
-
export declare function canonicalRequest(model: string, questions: Record<string,
|
|
171
|
+
export declare function canonicalRequest(model: string, questions: Record<string, AnyQuestion>): string;
|
|
124
172
|
/** The probability that a statement holds. NO confidence: the number is the answer. */
|
|
125
173
|
export interface NoulAnswer {
|
|
126
174
|
type: "noul";
|
|
@@ -155,6 +203,18 @@ export interface ScoreAnswer {
|
|
|
155
203
|
* `Answer` (the suspension resolution) — same idea, different seam.
|
|
156
204
|
*/
|
|
157
205
|
export type DecisionAnswer = NoulAnswer | ChoiceAnswer | ScoreAnswer;
|
|
206
|
+
/**
|
|
207
|
+
* An answer of a type the library does not model (the open union, §8B). It may appear in
|
|
208
|
+
* `Decision.answers` at runtime (read it with `decision.raw(key)` or `isRawAnswer`); `value` is the
|
|
209
|
+
* whole answer object as decoded, `type` included. It is kept out of `DecisionAnswer` so existing
|
|
210
|
+
* `switch (a.type)` narrowing keeps compiling.
|
|
211
|
+
*/
|
|
212
|
+
export interface RawAnswer {
|
|
213
|
+
type: string;
|
|
214
|
+
value: Record<string, unknown>;
|
|
215
|
+
}
|
|
216
|
+
/** True when an answer is a raw (unmodelled-type) answer. */
|
|
217
|
+
export declare function isRawAnswer(a: unknown): a is RawAnswer;
|
|
158
218
|
/** The wire's usage block. `cost` is absent on some backends. */
|
|
159
219
|
export interface ClassifierUsage {
|
|
160
220
|
inputTokens: number;
|
|
@@ -178,6 +238,8 @@ export declare class Decision {
|
|
|
178
238
|
* BACKEND DOES NOT TRANSFER TO ANOTHER.
|
|
179
239
|
*/
|
|
180
240
|
readonly calibrated: boolean;
|
|
241
|
+
/** True when the decision came from `ClassifierOptions.cache` and no request was sent. */
|
|
242
|
+
readonly cached: boolean;
|
|
181
243
|
constructor(
|
|
182
244
|
/** What actually answered, which may be more specific than the model that was asked for. */
|
|
183
245
|
model: string, answers: Record<string, DecisionAnswer>, usage: ClassifierUsage,
|
|
@@ -186,7 +248,11 @@ export declare class Decision {
|
|
|
186
248
|
* unless it derived them from provider token probabilities. A THRESHOLD TUNED AGAINST ONE
|
|
187
249
|
* BACKEND DOES NOT TRANSFER TO ANOTHER.
|
|
188
250
|
*/
|
|
189
|
-
calibrated: boolean
|
|
251
|
+
calibrated: boolean,
|
|
252
|
+
/** True when the decision came from `ClassifierOptions.cache` and no request was sent. */
|
|
253
|
+
cached?: boolean);
|
|
254
|
+
/** Typed read of a raw (unmodelled-type) answer. Throws on an absent key or a modelled type. */
|
|
255
|
+
raw(key: string): RawAnswer;
|
|
190
256
|
/** Typed read. Throws (never returns undefined) on a wrong-type or absent key. */
|
|
191
257
|
noul(key: string): NoulAnswer;
|
|
192
258
|
choice(key: string): ChoiceAnswer;
|
|
@@ -210,7 +276,12 @@ export declare function nearUniform(probabilities: Record<string, number>): bool
|
|
|
210
276
|
* `nearUniform` is derived. A malformed answer is an error, never a repaired one (ADR 0020).
|
|
211
277
|
*/
|
|
212
278
|
export declare function decodeDecision(raw: unknown, fallbackModel: string): Decision;
|
|
213
|
-
/**
|
|
279
|
+
/**
|
|
280
|
+
* `"systemone" | "llm" | "custom" | "static"` — the same slot as the client's `style`.
|
|
281
|
+
*
|
|
282
|
+
* `"llm"` is DEPRECATED (ADR 0038 D3: System One is the single product wire). It keeps working
|
|
283
|
+
* byte-identically, as an alias, for at least one minor release; there is no runtime warning.
|
|
284
|
+
*/
|
|
214
285
|
export type ClassifierStyle = "systemone" | "llm" | "custom" | "static";
|
|
215
286
|
/**
|
|
216
287
|
* One entry of the `static` corpus. It is keyed on the canonical request AND the state: several
|
|
@@ -220,12 +291,12 @@ export type ClassifierStyle = "systemone" | "llm" | "custom" | "static";
|
|
|
220
291
|
*/
|
|
221
292
|
export interface RecordedDecision {
|
|
222
293
|
state: unknown;
|
|
223
|
-
questions: Record<string,
|
|
294
|
+
questions: Record<string, AnyQuestion>;
|
|
224
295
|
/** The recorded backend response body, verbatim. */
|
|
225
296
|
response: unknown;
|
|
226
297
|
}
|
|
227
298
|
/** The host's own backend (`style: "custom"`). Every wire option is ignored. */
|
|
228
|
-
export type EvaluateFn = (state: unknown, questions: Record<string,
|
|
299
|
+
export type EvaluateFn = (state: unknown, questions: Record<string, AnyQuestion>, signal?: AbortSignal) => Promise<Decision> | Decision;
|
|
229
300
|
/**
|
|
230
301
|
* Mirrors §8 `ClientOptions` field-for-field wherever a field makes sense, so a host that has
|
|
231
302
|
* configured one has configured the other.
|
|
@@ -233,7 +304,7 @@ export type EvaluateFn = (state: unknown, questions: Record<string, Question>, s
|
|
|
233
304
|
export interface ClassifierOptions {
|
|
234
305
|
/** Default `"systemone"`. */
|
|
235
306
|
style?: ClassifierStyle;
|
|
236
|
-
/** A named `
|
|
307
|
+
/** A named backend row (`"typesafe"` | `"openrouter"` | `"cloudflare"` | `"vercel"` | `"custom"`), set as a
|
|
237
308
|
* UNIT. Default `"typesafe"`. The three individual options still win where given, for a
|
|
238
309
|
* self-hosted origin — but a known-bad mixture is refused at construction, not 700 ms later
|
|
239
310
|
* as an "Unknown model" 400. */
|
|
@@ -277,12 +348,34 @@ export interface ClassifierOptions {
|
|
|
277
348
|
/** Emits `classifier.evaluate` events into the SAME §8 sink, and carries the
|
|
278
349
|
* degenerate-criteria warning as `classifier.warning`. */
|
|
279
350
|
onMetric?: (ev: MetricEvent) => void;
|
|
280
|
-
/** `style: "llm"` only — the §8 Client to emulate over.
|
|
351
|
+
/** `style: "llm"` only — the §8 Client to emulate over.
|
|
352
|
+
* @deprecated `style: "llm"` is deprecated (System One is the single wire); it still works. */
|
|
281
353
|
client?: Client;
|
|
282
354
|
/** `style: "custom"` only — the host's own function. */
|
|
283
355
|
evaluate?: EvaluateFn;
|
|
284
356
|
/** `style: "static"` only — the recorded corpus. */
|
|
285
357
|
decisions?: RecordedDecision[];
|
|
358
|
+
/** Overrides the backend table's support row for this classifier. Required with `backend: "custom"`. */
|
|
359
|
+
supports?: ClassifierSupports;
|
|
360
|
+
/** Opt-in answer cache (§8B). Absent ⇒ no caching. Consulted by style `systemone` only; only
|
|
361
|
+
* replies that pass `validateDecision` are stored. Methods may return values or Promises. */
|
|
362
|
+
cache?: ClassifierCache;
|
|
363
|
+
}
|
|
364
|
+
/** An injected answer cache. Values are opaque strings: the served model under a model key, the
|
|
365
|
+
* System One reply body under a decision key. */
|
|
366
|
+
export interface ClassifierCache {
|
|
367
|
+
get(key: string): string | undefined | null | Promise<string | undefined | null>;
|
|
368
|
+
set(key: string, value: string): void | Promise<void>;
|
|
369
|
+
}
|
|
370
|
+
/** One state's outcome from `states(...).evaluate`: exactly one of `decision` / `error` is set. */
|
|
371
|
+
export interface BatchResult {
|
|
372
|
+
decision?: Decision;
|
|
373
|
+
error?: Error;
|
|
374
|
+
}
|
|
375
|
+
/** A named question (`judge.noul(...)` etc. build these). */
|
|
376
|
+
export interface NamedClassifierQuestion {
|
|
377
|
+
name: string;
|
|
378
|
+
question: AnyQuestion;
|
|
286
379
|
}
|
|
287
380
|
/** The whole seam: one verb. */
|
|
288
381
|
export declare class Classifier {
|
|
@@ -291,7 +384,13 @@ export declare class Classifier {
|
|
|
291
384
|
private readonly baseUrl;
|
|
292
385
|
private readonly model;
|
|
293
386
|
private readonly apiKeyEnv;
|
|
387
|
+
/** The caller NAMED apiKeyEnv: sent wherever baseUrl points. Otherwise the preset's own key,
|
|
388
|
+
* and only to its own host (classifierKeyEnv, #155). */
|
|
389
|
+
private readonly explicitKeyEnv;
|
|
294
390
|
private readonly timeoutMs;
|
|
391
|
+
private readonly backend;
|
|
392
|
+
private readonly preset;
|
|
393
|
+
private readonly supports;
|
|
295
394
|
private readonly corpus;
|
|
296
395
|
/** The once-per-question-key set for the degenerate warning, so a per-turn judge does not
|
|
297
396
|
* flood the sink. */
|
|
@@ -301,14 +400,33 @@ export declare class Classifier {
|
|
|
301
400
|
* The whole contract: a state plus typed questions in, a Decision out. Questions are
|
|
302
401
|
* INDEPENDENT — one answer is never context for another.
|
|
303
402
|
*/
|
|
304
|
-
evaluate(state: unknown, questions: Record<string,
|
|
403
|
+
evaluate(state: unknown, questions: Record<string, AnyQuestion>, signal?: AbortSignal): Promise<Decision>;
|
|
404
|
+
/**
|
|
405
|
+
* Binds one state (a VALUE, not a session: nothing is sent until `evaluate`). Its `evaluate`
|
|
406
|
+
* runs the same path as `evaluate` PLUS fail-closed validation (`validateDecision`).
|
|
407
|
+
*/
|
|
408
|
+
state(value: unknown, opts?: {
|
|
409
|
+
signal?: AbortSignal;
|
|
410
|
+
}): {
|
|
411
|
+
evaluate(...questions: NamedClassifierQuestion[]): Promise<Decision>;
|
|
412
|
+
};
|
|
413
|
+
/**
|
|
414
|
+
* Binds a batch of states. Its `evaluate` runs `state(v).evaluate` per state, at most 16 in
|
|
415
|
+
* flight, and returns one `{decision | error}` per state IN ORDER. One state's failure never
|
|
416
|
+
* fails the batch; only a question-set error (duplicate name) throws, before any request.
|
|
417
|
+
*/
|
|
418
|
+
states(...values: unknown[]): {
|
|
419
|
+
evaluate(...questions: NamedClassifierQuestion[]): Promise<BatchResult[]>;
|
|
420
|
+
};
|
|
421
|
+
/** The one path. `validate` = fail closed on `validateDecision` (the value builders). */
|
|
422
|
+
private run;
|
|
305
423
|
/**
|
|
306
424
|
* The same questions over many states (SPEC §8B "Batch"). Each state goes through
|
|
307
425
|
* `evaluate`, at most `concurrency` (default 16) in flight; decisions come back in STATE
|
|
308
426
|
* order. Fails closed: the first failing state's index is named and no decisions return.
|
|
309
427
|
* No states is an error and sends nothing.
|
|
310
428
|
*/
|
|
311
|
-
evaluateBatch(states: readonly unknown[], questions: Record<string,
|
|
429
|
+
evaluateBatch(states: readonly unknown[], questions: Record<string, AnyQuestion>, opts?: {
|
|
312
430
|
signal?: AbortSignal;
|
|
313
431
|
concurrency?: number;
|
|
314
432
|
}): Promise<Decision[]>;
|
|
@@ -319,15 +437,18 @@ export declare class Classifier {
|
|
|
319
437
|
*/
|
|
320
438
|
private reportDegenerate;
|
|
321
439
|
/**
|
|
322
|
-
* Assembles the request: the canonical model + questions, plus
|
|
323
|
-
*
|
|
324
|
-
* bodyTransform → marshal).
|
|
440
|
+
* Assembles the request for the backend's wire: the canonical model + questions, plus the state
|
|
441
|
+
* (VERBATIM unless it carried content parts) and, on systemone, `assets`; then the §8 Gap 1
|
|
442
|
+
* pipeline in §8 order (base → requestParams merge → bodyTransform → marshal).
|
|
325
443
|
*/
|
|
326
444
|
private body;
|
|
327
445
|
/** Identifies a recorded decision by the canonical request AND the state. */
|
|
328
446
|
private staticKey;
|
|
329
447
|
private evaluateStatic;
|
|
330
|
-
|
|
448
|
+
/** Lift images, apply the support table and the limits — all before any request. */
|
|
449
|
+
private prepare;
|
|
450
|
+
/** The systemone style on every backend: lift, check, cache, post, decode. */
|
|
451
|
+
private evaluateWire;
|
|
331
452
|
/**
|
|
332
453
|
* The one POST, with the §8 retry budget, reusing the client's `ErrorInfo`/`ErrorTier`
|
|
333
454
|
* classifier and the `Retry-After` delay-seconds rule verbatim.
|
|
@@ -369,3 +490,50 @@ export declare function degenerateCriteria(criteria: Record<string, string>): st
|
|
|
369
490
|
* with, or `""` when the pairing is fine.
|
|
370
491
|
*/
|
|
371
492
|
export declare function backendMismatch(baseUrl: string, model: string): string;
|
|
493
|
+
/** §8B model key: which model an endpoint + requested model (alias) served last. */
|
|
494
|
+
export declare function classifierModelCacheKey(endpoint: string, model: string): string;
|
|
495
|
+
/** §8B decision key, on the SERVED model. No assets ⇒ the `assets` member is omitted. */
|
|
496
|
+
export declare function classifierDecisionCacheKey(endpoint: string, servedModel: string, state: unknown, assets: Record<string, unknown> | undefined, questions: Record<string, AnyQuestion>): string;
|
|
497
|
+
/** The canonical JSON a decision key hashes (exported for conformance tests). */
|
|
498
|
+
export declare function classifierDecisionCacheKeyBytes(endpoint: string, servedModel: string, state: unknown, assets: Record<string, unknown> | undefined, questions: Record<string, AnyQuestion>): string;
|
|
499
|
+
/**
|
|
500
|
+
* In-process TTL cache with a size cap (oldest-inserted out first). `ttlMs` 0 = no expiry; `max`
|
|
501
|
+
* 0 = unbounded. toolnexus persists nothing itself: implement `ClassifierCache` to persist.
|
|
502
|
+
*/
|
|
503
|
+
export declare class MemoryCache implements ClassifierCache {
|
|
504
|
+
readonly ttlMs: number;
|
|
505
|
+
readonly max: number;
|
|
506
|
+
private readonly now;
|
|
507
|
+
private readonly m;
|
|
508
|
+
constructor(ttlMs?: number, max?: number, now?: () => number);
|
|
509
|
+
get(key: string): string | undefined;
|
|
510
|
+
set(key: string, value: string): void;
|
|
511
|
+
get size(): number;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* V1: fail closed on a reply that does not answer exactly the questions asked, within their
|
|
515
|
+
* schema. First failure wins, in the §8B order. Returns the error, or undefined when valid.
|
|
516
|
+
*/
|
|
517
|
+
export declare function validateDecision(questions: Record<string, AnyQuestion>, decision: {
|
|
518
|
+
answers: Record<string, unknown>;
|
|
519
|
+
}): Error | undefined;
|
|
520
|
+
/** jevx's INCLUSIVE cut-points (T1). They sit alongside `Bands` (exclusive), which stays the
|
|
521
|
+
* default of `ask` / `gate`. */
|
|
522
|
+
export interface Thresholds {
|
|
523
|
+
yes: number;
|
|
524
|
+
no: number;
|
|
525
|
+
minConfidence: number;
|
|
526
|
+
}
|
|
527
|
+
/** 0.8 / 0.2 / 0.6 (jevx's builtin profile). */
|
|
528
|
+
export declare const DEFAULT_THRESHOLDS: Readonly<Thresholds>;
|
|
529
|
+
/**
|
|
530
|
+
* The band of one answer under `Thresholds`: noul `p >= yes` ⇒ yes, `p <= no` ⇒ no, else
|
|
531
|
+
* uncertain; choice/score `confidence >= minConfidence` ⇒ yes, else uncertain. No near-uniform
|
|
532
|
+
* demotion (that is `Bands`' rule). A raw answer is uncertain.
|
|
533
|
+
*/
|
|
534
|
+
export declare function thresholdBand(answer: DecisionAnswer | RawAnswer, t?: Thresholds): "yes" | "no" | "uncertain";
|
|
535
|
+
/**
|
|
536
|
+
* Split a question set into chunks of `size`, keys in ASCII order. A helper, not automatic:
|
|
537
|
+
* joint calibration across a split is unproven, so the host decides to split and merges.
|
|
538
|
+
*/
|
|
539
|
+
export declare function chunkQuestions<Q extends AnyQuestion>(questions: Record<string, Q>, size: number): Record<string, Q>[];
|