okengine 0.23.2 → 0.24.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/AGENTS.md +5 -1
- package/manifest.v1.schema.json +53 -3
- package/package.json +2 -1
- package/site/content/docs/ai/mcp.mdx +1 -1
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/elements/ai/decide.mdx +95 -417
- package/site/content/docs/elements/ai/deciders.mdx +154 -0
- package/site/content/docs/elements/ai/index.mdx +2 -1
- package/site/content/docs/elements/ai/meta.json +1 -1
- package/site/content/docs/reference/cli.mdx +2 -2
- package/site/content/docs/reference/configuration.mdx +2 -1
- package/site/content/docs/reference/fx.mdx +11 -9
- package/src/cli/agents-md.test.ts +8 -2
- package/src/cli/decide-certify.test.ts +166 -0
- package/src/cli/decide.ts +170 -3
- package/src/cli/decision-lock-watch.test.ts +4 -4
- package/src/cli/eval.ts +38 -59
- package/src/cli/mcp-from-console.ts +1 -1
- package/src/cli/registry.ts +45 -9
- package/src/compiler/decisions.extract.test.ts +86 -31
- package/src/compiler/effects-infer.ts +1 -0
- package/src/compiler/extract.ts +239 -24
- package/src/config/index.ts +6 -0
- package/src/console/server/decisions.test.ts +6 -1
- package/src/console/server/decisions.ts +32 -1
- package/src/console/ui-next/dist/assets/{access-page-zBMhZnFm.js → access-page-DX2dKLmq.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-CBj9f7Ju.js → agent-disclosure-CiRvA3M9.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-D3MY49h3.js → cache-glyph-CC1yFJ8A.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-L9JxdBhp.js → call-pii-button-CLXQgIDD.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-BvFaX-Zt.js → collapsible-C0AThBZg.js} +1 -1
- package/src/console/ui-next/dist/assets/{decisions-page-DgB76m-X.js → decisions-page-Bf3AhTAa.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-l1DSJ2Vz.js → duration-tone-Bptur2gX.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-Bb9t1lu6.js → flows-page-DRZVeTZU.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-DMvsi3Ti.js → highlighted-json-BKf0PjBK.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BwhJBTcQ.js → http-method-uhrnWrGC.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-RhV2jT_7.js → index-Duxus_sG.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-BRfUILBq.js → observability-page-B-QM8X0O.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-BsceAzIG.js → replica-lag-DuqHCANi.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-C6lTyCXp.js → request-meta-BTBZ3Ulc.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-YmE806bJ.js → store-page-fpo7VxYb.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-Di4irS49.js → trace-detail-sheet-DKtr1-K1.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-DYeL6Rye.js → tree-expand-toggle-QeTceKKI.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-07agasRA.js → units-page-ChCp8mOy.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-CHBck4n_.js → vault-page-CEYLXhUX.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/client.ts +8 -0
- package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +16 -0
- package/src/elements/ai/deciders/presets.ts +182 -0
- package/src/elements/ai/decisions/bind.ts +6 -5
- package/src/elements/ai/decisions/catalog.ts +93 -0
- package/src/elements/ai/decisions/certificate.test.ts +47 -19
- package/src/elements/ai/decisions/certificate.ts +107 -24
- package/src/elements/ai/decisions/certify.ts +104 -9
- package/src/elements/ai/decisions/codec.test.ts +142 -0
- package/src/elements/ai/decisions/codec.ts +302 -0
- package/src/elements/ai/decisions/decide.test.ts +108 -31
- package/src/elements/ai/decisions/decider-runtime.test.ts +415 -0
- package/src/elements/ai/decisions/decider.types.test.ts +62 -0
- package/src/elements/ai/decisions/e2e.test.ts +20 -16
- package/src/elements/ai/decisions/http.ts +15 -7
- package/src/elements/ai/decisions/labels.test.ts +6 -1
- package/src/elements/ai/decisions/labels.ts +4 -3
- package/src/elements/ai/decisions/openai.live.test.ts +42 -0
- package/src/elements/ai/decisions/openrouter.live.test.ts +5 -1
- package/src/elements/ai/decisions/provider.ts +22 -1
- package/src/elements/ai/declare.ts +208 -51
- package/src/elements/ai.ts +1 -0
- package/src/index.ts +1 -0
- package/src/kernel/app.ts +8 -0
- package/src/kernel/decision-budget-entry.ts +11 -0
- package/src/kernel/element-registries.ts +3 -0
- package/src/kernel/fx-decide.ts +276 -88
- package/src/manifest/types.ts +38 -2
- package/src/release/measure.ts +47 -8
- package/src/test/create-test-app.ts +21 -1
|
@@ -37,26 +37,43 @@ export interface DecisionCertSlice {
|
|
|
37
37
|
readonly metrics?: Readonly<Record<string, number>>;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** One
|
|
41
|
-
export interface
|
|
40
|
+
/** One decider's certificate inside a decision. */
|
|
41
|
+
export interface DecisionDeciderCert {
|
|
42
42
|
readonly model: string;
|
|
43
|
+
/** False when the provider only echoes an alias. */
|
|
44
|
+
readonly pinned: boolean;
|
|
45
|
+
/** Epoch ms an alias certificate stops granting auto. */
|
|
46
|
+
readonly expiresAt?: number;
|
|
43
47
|
/** Epoch ms the certificate was written. Drift counts labels after this. */
|
|
44
48
|
readonly certifiedAt?: number;
|
|
45
49
|
readonly questions: Readonly<Record<string, Readonly<Record<string, DecisionCertSlice>>>>;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
|
-
/**
|
|
52
|
+
/** One decision inside `oke-decisions.lock.json`. */
|
|
53
|
+
export interface DecisionLockEntry {
|
|
54
|
+
readonly deciders: Readonly<Record<string, DecisionDeciderCert>>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** App lockfile. Version 1 is rejected. */
|
|
49
58
|
export interface DecisionLockfile {
|
|
59
|
+
readonly version: 2;
|
|
50
60
|
readonly decisions: Readonly<Record<string, DecisionLockEntry>>;
|
|
51
61
|
}
|
|
52
62
|
|
|
63
|
+
/** A lockfile that is not version 2. */
|
|
64
|
+
export class DecisionLockVersionError extends Error {
|
|
65
|
+
constructor() {
|
|
66
|
+
super("oke-decisions.lock.json is not version 2. Run `oke decide certify` to recertify.");
|
|
67
|
+
this.name = "DecisionLockVersionError";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
53
71
|
/** Why a question is not auto. */
|
|
54
|
-
export type
|
|
55
|
-
| "
|
|
56
|
-
| "
|
|
57
|
-
| "
|
|
58
|
-
| "
|
|
59
|
-
| "uncertified-locale"
|
|
72
|
+
export type DecisionWhy =
|
|
73
|
+
| "uncertain"
|
|
74
|
+
| "refused"
|
|
75
|
+
| "none_of_these"
|
|
76
|
+
| "uncertified"
|
|
60
77
|
| "drift"
|
|
61
78
|
| "outage";
|
|
62
79
|
|
|
@@ -84,10 +101,14 @@ export interface DecisionLabel {
|
|
|
84
101
|
readonly reviewId?: string;
|
|
85
102
|
}
|
|
86
103
|
|
|
87
|
-
/** App-wide candidate
|
|
104
|
+
/** App-wide candidate for one decider. */
|
|
88
105
|
export interface DecisionCandidate {
|
|
106
|
+
readonly decider: string;
|
|
89
107
|
readonly model: string;
|
|
90
|
-
readonly
|
|
108
|
+
readonly pinned: boolean;
|
|
109
|
+
readonly expiresAt?: number;
|
|
110
|
+
readonly certifiedAt?: number;
|
|
111
|
+
readonly questions: DecisionDeciderCert["questions"];
|
|
91
112
|
}
|
|
92
113
|
|
|
93
114
|
/** Lockfile name at the app project root. */
|
|
@@ -292,7 +313,9 @@ export function recordDecisionLabel(label: DecisionLabel): void {
|
|
|
292
313
|
export function aggregateDecisionCandidate(
|
|
293
314
|
decision: string,
|
|
294
315
|
fit: (rows: readonly DecisionLabel[]) => DecisionCandidate = () => ({
|
|
316
|
+
decider: "",
|
|
295
317
|
model: "",
|
|
318
|
+
pinned: true,
|
|
296
319
|
questions: {},
|
|
297
320
|
}),
|
|
298
321
|
): DecisionCandidate {
|
|
@@ -312,21 +335,39 @@ export function lockFromCandidate(
|
|
|
312
335
|
candidate: unknown,
|
|
313
336
|
current: DecisionLockfile | undefined,
|
|
314
337
|
): DecisionLockfile {
|
|
315
|
-
const
|
|
316
|
-
if (!
|
|
338
|
+
const parsed = parseDecisionCandidate(candidate);
|
|
339
|
+
if (!parsed) throw new TypeError("promote: candidate is not a lock entry");
|
|
317
340
|
setDecisionDrift(name, false);
|
|
318
|
-
|
|
341
|
+
const prev = current?.decisions[name]?.deciders ?? {};
|
|
342
|
+
return {
|
|
343
|
+
version: 2,
|
|
344
|
+
decisions: {
|
|
345
|
+
...(current?.decisions ?? {}),
|
|
346
|
+
[name]: { deciders: { ...prev, [parsed.decider]: certFromCandidate(parsed) } },
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function certFromCandidate(candidate: DecisionCandidate): DecisionDeciderCert {
|
|
352
|
+
return {
|
|
353
|
+
model: candidate.model,
|
|
354
|
+
pinned: candidate.pinned,
|
|
355
|
+
...(candidate.expiresAt !== undefined ? { expiresAt: candidate.expiresAt } : {}),
|
|
356
|
+
...(candidate.certifiedAt !== undefined ? { certifiedAt: candidate.certifiedAt } : {}),
|
|
357
|
+
questions: candidate.questions,
|
|
358
|
+
};
|
|
319
359
|
}
|
|
320
360
|
|
|
321
361
|
/**
|
|
322
|
-
* Accept a
|
|
362
|
+
* Accept a candidate for one decider.
|
|
323
363
|
*
|
|
324
|
-
* @param body - Candidate
|
|
364
|
+
* @param body - Candidate body
|
|
325
365
|
*/
|
|
326
|
-
export function
|
|
366
|
+
export function parseDecisionCandidate(body: unknown): DecisionCandidate | undefined {
|
|
327
367
|
if (!body || typeof body !== "object") return undefined;
|
|
328
368
|
const record = body as Record<string, unknown>;
|
|
329
|
-
if (typeof record.model !== "string") return undefined;
|
|
369
|
+
if (typeof record.decider !== "string" || typeof record.model !== "string") return undefined;
|
|
370
|
+
if (typeof record.pinned !== "boolean") return undefined;
|
|
330
371
|
if (
|
|
331
372
|
!record.questions ||
|
|
332
373
|
typeof record.questions !== "object" ||
|
|
@@ -335,28 +376,70 @@ export function parseDecisionLockEntry(body: unknown): DecisionLockEntry | undef
|
|
|
335
376
|
return undefined;
|
|
336
377
|
}
|
|
337
378
|
return {
|
|
379
|
+
decider: record.decider,
|
|
338
380
|
model: record.model,
|
|
381
|
+
pinned: record.pinned,
|
|
382
|
+
...(typeof record.expiresAt === "number" ? { expiresAt: record.expiresAt } : {}),
|
|
339
383
|
...(typeof record.certifiedAt === "number" ? { certifiedAt: record.certifiedAt } : {}),
|
|
340
|
-
questions: record.questions as
|
|
384
|
+
questions: record.questions as DecisionCandidate["questions"],
|
|
341
385
|
};
|
|
342
386
|
}
|
|
343
387
|
|
|
344
388
|
/**
|
|
345
|
-
* Accept
|
|
389
|
+
* Accept one decision slot (`deciders` map).
|
|
390
|
+
*
|
|
391
|
+
* @param body - File slot
|
|
392
|
+
*/
|
|
393
|
+
export function parseDecisionLockEntry(body: unknown): DecisionLockEntry | undefined {
|
|
394
|
+
if (!body || typeof body !== "object") return undefined;
|
|
395
|
+
const record = body as Record<string, unknown>;
|
|
396
|
+
if (typeof record.model === "string" && record.deciders === undefined) {
|
|
397
|
+
throw new DecisionLockVersionError();
|
|
398
|
+
}
|
|
399
|
+
if (!record.deciders || typeof record.deciders !== "object" || Array.isArray(record.deciders)) {
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
const deciders: Record<string, DecisionDeciderCert> = {};
|
|
403
|
+
for (const [name, entry] of Object.entries(record.deciders)) {
|
|
404
|
+
const parsed = parseDecisionCandidate({
|
|
405
|
+
...(entry as object),
|
|
406
|
+
decider: name,
|
|
407
|
+
pinned: (entry as { pinned?: unknown }).pinned ?? true,
|
|
408
|
+
});
|
|
409
|
+
if (!parsed) return undefined;
|
|
410
|
+
deciders[name] = certFromCandidate(parsed);
|
|
411
|
+
}
|
|
412
|
+
return { deciders };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Accept a version 2 lockfile. Version 1 throws {@link DecisionLockVersionError}.
|
|
346
417
|
*
|
|
347
418
|
* @param body - Parsed JSON
|
|
348
419
|
*/
|
|
349
420
|
export function parseDecisionLockfile(body: unknown): DecisionLockfile | undefined {
|
|
350
421
|
if (!body || typeof body !== "object") return undefined;
|
|
351
|
-
const
|
|
352
|
-
if (
|
|
422
|
+
const record = body as { version?: unknown; decisions?: unknown };
|
|
423
|
+
if (record.version !== 2) {
|
|
424
|
+
if (record.decisions && typeof record.decisions === "object") {
|
|
425
|
+
throw new DecisionLockVersionError();
|
|
426
|
+
}
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
if (
|
|
430
|
+
!record.decisions ||
|
|
431
|
+
typeof record.decisions !== "object" ||
|
|
432
|
+
Array.isArray(record.decisions)
|
|
433
|
+
) {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
353
436
|
const next: Record<string, DecisionLockEntry> = {};
|
|
354
|
-
for (const [name, entry] of Object.entries(decisions)) {
|
|
437
|
+
for (const [name, entry] of Object.entries(record.decisions)) {
|
|
355
438
|
const parsed = parseDecisionLockEntry(entry);
|
|
356
439
|
if (!parsed) return undefined;
|
|
357
440
|
next[name] = parsed;
|
|
358
441
|
}
|
|
359
|
-
return { decisions: next };
|
|
442
|
+
return { version: 2, decisions: next };
|
|
360
443
|
}
|
|
361
444
|
|
|
362
445
|
/**
|
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
questionHash,
|
|
11
11
|
type DecisionCalibrator,
|
|
12
12
|
type DecisionCertSlice,
|
|
13
|
+
type DecisionDeciderCert,
|
|
13
14
|
type DecisionLabel,
|
|
14
|
-
type DecisionLockEntry,
|
|
15
15
|
} from "./certificate.ts";
|
|
16
16
|
import type { DecisionResponse } from "./provider.ts";
|
|
17
17
|
|
|
@@ -26,6 +26,24 @@ export interface DecisionSeedCase {
|
|
|
26
26
|
/** One provider call used while certifying. */
|
|
27
27
|
export type DecisionEvaluate = (input: unknown) => Promise<DecisionResponse>;
|
|
28
28
|
|
|
29
|
+
/** Printed for one question after certify. */
|
|
30
|
+
export interface CertifyQuestionReport {
|
|
31
|
+
readonly accuracy: number;
|
|
32
|
+
readonly ece: number;
|
|
33
|
+
readonly coverage: number;
|
|
34
|
+
readonly p50Ms: number;
|
|
35
|
+
readonly p95Ms: number;
|
|
36
|
+
readonly cost: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Certificate plus the comparison row. */
|
|
40
|
+
export interface CertifySeedResult {
|
|
41
|
+
readonly cert: DecisionDeciderCert;
|
|
42
|
+
readonly report: Readonly<Record<string, CertifyQuestionReport>>;
|
|
43
|
+
/** True when the certificate is an expiring alias. */
|
|
44
|
+
readonly unpinned: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
29
47
|
/**
|
|
30
48
|
* Parse decision seed JSONL. Blank lines are skipped.
|
|
31
49
|
*
|
|
@@ -73,29 +91,51 @@ interface FitRow {
|
|
|
73
91
|
*/
|
|
74
92
|
export async function certifySeed(options: {
|
|
75
93
|
readonly model: string;
|
|
94
|
+
readonly pinned?: boolean;
|
|
95
|
+
readonly expiresAt?: number;
|
|
76
96
|
readonly maxError: number;
|
|
77
97
|
readonly delta?: number;
|
|
78
98
|
readonly jsonl: string;
|
|
79
99
|
readonly ask: Readonly<Record<string, AiDecisionQuestion>>;
|
|
80
100
|
readonly evaluate: DecisionEvaluate;
|
|
81
|
-
|
|
101
|
+
readonly now?: () => number;
|
|
102
|
+
}): Promise<CertifySeedResult> {
|
|
82
103
|
const cases = parseDecisionSeed(options.jsonl);
|
|
83
104
|
const buckets = new Map<string, FitRow[]>();
|
|
105
|
+
const samples = new Map<string, { correct: boolean; confidence: number }[]>();
|
|
106
|
+
const latencies: number[] = [];
|
|
107
|
+
let cost = 0;
|
|
84
108
|
let resolved = options.model;
|
|
109
|
+
const now = options.now ?? Date.now;
|
|
85
110
|
for (const row of cases) {
|
|
111
|
+
const started = now();
|
|
86
112
|
const response = await options.evaluate(row.input);
|
|
113
|
+
latencies.push(Math.max(0, now() - started));
|
|
114
|
+
cost += response.usage.cost ?? 0;
|
|
87
115
|
if (response.model) resolved = response.model;
|
|
88
116
|
const locale = row.locale ?? "";
|
|
89
117
|
for (const [id, expected] of Object.entries(row.expect)) {
|
|
90
118
|
const question = options.ask[id];
|
|
91
119
|
if (!question) continue;
|
|
92
|
-
const
|
|
120
|
+
const answers = response.answers;
|
|
121
|
+
const answer = Array.isArray(answers)
|
|
122
|
+
? undefined
|
|
123
|
+
: (answers as Readonly<Record<string, unknown>>)[id];
|
|
93
124
|
const fit = fitRow(question, answer, expected);
|
|
94
125
|
if (!fit) continue;
|
|
95
126
|
const key = `${id}\0${locale}`;
|
|
96
127
|
const list = buckets.get(key) ?? [];
|
|
97
128
|
list.push(fit);
|
|
98
129
|
buckets.set(key, list);
|
|
130
|
+
const confidence =
|
|
131
|
+
fit.noul !== undefined ? Math.max(fit.noul, 1 - fit.noul) : Math.max(...fit.probs, 0);
|
|
132
|
+
const correct =
|
|
133
|
+
fit.booleanLabel !== undefined
|
|
134
|
+
? fit.noul !== undefined && fit.noul >= 0.5 === fit.booleanLabel
|
|
135
|
+
: fit.probs[fit.labelIndex] === Math.max(...fit.probs);
|
|
136
|
+
const bag = samples.get(id) ?? [];
|
|
137
|
+
bag.push({ correct, confidence });
|
|
138
|
+
samples.set(id, bag);
|
|
99
139
|
}
|
|
100
140
|
}
|
|
101
141
|
const entry = entryFromBuckets(
|
|
@@ -105,7 +145,30 @@ export async function certifySeed(options: {
|
|
|
105
145
|
options.maxError,
|
|
106
146
|
options.delta ?? 0.1,
|
|
107
147
|
);
|
|
108
|
-
|
|
148
|
+
const pinned = options.pinned !== false;
|
|
149
|
+
const cert: DecisionDeciderCert = {
|
|
150
|
+
...entry,
|
|
151
|
+
pinned,
|
|
152
|
+
...(options.expiresAt !== undefined ? { expiresAt: options.expiresAt } : {}),
|
|
153
|
+
certifiedAt: Date.now(),
|
|
154
|
+
};
|
|
155
|
+
const report: Record<string, CertifyQuestionReport> = {};
|
|
156
|
+
for (const [id, rows] of samples) {
|
|
157
|
+
const threshold = cert.questions[id]?.[""]?.threshold;
|
|
158
|
+
const covered =
|
|
159
|
+
threshold === undefined
|
|
160
|
+
? 0
|
|
161
|
+
: rows.filter((row) => row.confidence >= threshold).length / rows.length;
|
|
162
|
+
report[id] = {
|
|
163
|
+
accuracy: rows.filter((row) => row.correct).length / rows.length,
|
|
164
|
+
ece: expectedCalibrationError(rows),
|
|
165
|
+
coverage: covered,
|
|
166
|
+
p50Ms: percentile(latencies, 0.5),
|
|
167
|
+
p95Ms: percentile(latencies, 0.95),
|
|
168
|
+
cost,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return { cert, report, unpinned: !pinned };
|
|
109
172
|
}
|
|
110
173
|
|
|
111
174
|
/**
|
|
@@ -119,7 +182,7 @@ export function certifyLabels(options: {
|
|
|
119
182
|
readonly delta?: number;
|
|
120
183
|
readonly ask: Readonly<Record<string, AiDecisionQuestion>>;
|
|
121
184
|
readonly labels: readonly DecisionLabel[];
|
|
122
|
-
}):
|
|
185
|
+
}): DecisionDeciderCert {
|
|
123
186
|
const groups = new Map<string, DecisionLabel[]>();
|
|
124
187
|
for (const label of options.labels) {
|
|
125
188
|
if (label.raw === undefined) continue;
|
|
@@ -148,7 +211,7 @@ export function certifyLabels(options: {
|
|
|
148
211
|
bucket[locale] = slice;
|
|
149
212
|
questions[id] = bucket;
|
|
150
213
|
}
|
|
151
|
-
return { model, certifiedAt: Date.now(), questions };
|
|
214
|
+
return { model, pinned: true, certifiedAt: Date.now(), questions };
|
|
152
215
|
}
|
|
153
216
|
|
|
154
217
|
function entryFromBuckets(
|
|
@@ -157,7 +220,7 @@ function entryFromBuckets(
|
|
|
157
220
|
buckets: ReadonlyMap<string, readonly FitRow[]>,
|
|
158
221
|
maxError: number,
|
|
159
222
|
delta: number,
|
|
160
|
-
):
|
|
223
|
+
): DecisionDeciderCert {
|
|
161
224
|
const questions: Record<string, Record<string, DecisionCertSlice>> = {};
|
|
162
225
|
for (const [key, rows] of buckets) {
|
|
163
226
|
const split = key.indexOf("\0");
|
|
@@ -171,7 +234,7 @@ function entryFromBuckets(
|
|
|
171
234
|
bucket[locale] = slice;
|
|
172
235
|
questions[id] = bucket;
|
|
173
236
|
}
|
|
174
|
-
return { model, questions };
|
|
237
|
+
return { model, pinned: true, questions };
|
|
175
238
|
}
|
|
176
239
|
|
|
177
240
|
function sliceFromRows(
|
|
@@ -270,6 +333,32 @@ function scoreFit(
|
|
|
270
333
|
*
|
|
271
334
|
* @param record - Answer or stored raw
|
|
272
335
|
*/
|
|
336
|
+
function percentile(values: readonly number[], p: number): number {
|
|
337
|
+
if (values.length === 0) return 0;
|
|
338
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
339
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(p * sorted.length) - 1));
|
|
340
|
+
return sorted[index] ?? 0;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function expectedCalibrationError(
|
|
344
|
+
rows: readonly { readonly confidence: number; readonly correct: boolean }[],
|
|
345
|
+
): number {
|
|
346
|
+
if (rows.length === 0) return 0;
|
|
347
|
+
const bins = Array.from({ length: 10 }, () => ({ n: 0, confidence: 0, correct: 0 }));
|
|
348
|
+
for (const row of rows) {
|
|
349
|
+
const index = Math.min(9, Math.floor(row.confidence * 10));
|
|
350
|
+
const bin = bins[index];
|
|
351
|
+
if (!bin) continue;
|
|
352
|
+
bin.n += 1;
|
|
353
|
+
bin.confidence += row.confidence;
|
|
354
|
+
bin.correct += row.correct ? 1 : 0;
|
|
355
|
+
}
|
|
356
|
+
return bins.reduce((sum, bin) => {
|
|
357
|
+
if (bin.n === 0) return sum;
|
|
358
|
+
return sum + (bin.n / rows.length) * Math.abs(bin.correct / bin.n - bin.confidence / bin.n);
|
|
359
|
+
}, 0);
|
|
360
|
+
}
|
|
361
|
+
|
|
273
362
|
function storedDistribution(record: Record<string, unknown>): unknown {
|
|
274
363
|
if (record.probabilities !== undefined) return record.probabilities;
|
|
275
364
|
return record;
|
|
@@ -281,12 +370,18 @@ function fitRow(
|
|
|
281
370
|
expected: unknown,
|
|
282
371
|
): FitRow | undefined {
|
|
283
372
|
const record = answer && typeof answer === "object" ? (answer as Record<string, unknown>) : {};
|
|
373
|
+
if (record.type === "refusal" || record.type === "malformed") return undefined;
|
|
284
374
|
if (question.kind === "boolean") {
|
|
285
375
|
if (typeof expected !== "boolean") return undefined;
|
|
286
376
|
return {
|
|
287
377
|
probs: [],
|
|
288
378
|
labelIndex: expected ? 1 : 0,
|
|
289
|
-
noul:
|
|
379
|
+
noul:
|
|
380
|
+
typeof record.probability === "number"
|
|
381
|
+
? record.probability
|
|
382
|
+
: typeof record.noul === "number"
|
|
383
|
+
? record.noul
|
|
384
|
+
: 0,
|
|
290
385
|
booleanLabel: expected,
|
|
291
386
|
weight: 1,
|
|
292
387
|
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codec round-trips for the verified System One body and the OpenAI decisions wire.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { ai } from "../../ai.ts";
|
|
7
|
+
import { encodeDecisionRequest, normalizeDecisionResponse, wireQuestions } from "./codec.ts";
|
|
8
|
+
|
|
9
|
+
const openRouterBody = {
|
|
10
|
+
model: "typesafe/jev-1.13-20260917",
|
|
11
|
+
answers: {
|
|
12
|
+
team: {
|
|
13
|
+
type: "choice",
|
|
14
|
+
choice: "technical",
|
|
15
|
+
probabilities: { billing: 0.49, none_of_these: 0.02, technical: 0.49 },
|
|
16
|
+
confidence: 0.23,
|
|
17
|
+
},
|
|
18
|
+
urgent: { type: "noul", noul: 0.54 },
|
|
19
|
+
},
|
|
20
|
+
usage: { input_tokens: 348, output_tokens: 58, cost: 0.000014616 },
|
|
21
|
+
id: "gen-dec-1791389894-FvrNcwqrVRdAEvlEDRh9",
|
|
22
|
+
provider: "TypeSafe",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe("decision codecs", () => {
|
|
26
|
+
test("System One normalizes the verified OpenRouter body", () => {
|
|
27
|
+
const response = normalizeDecisionResponse("systemone", openRouterBody);
|
|
28
|
+
expect(response.model).toBe("typesafe/jev-1.13-20260917");
|
|
29
|
+
expect(response.provider).toBe("TypeSafe");
|
|
30
|
+
expect(response.usage).toEqual({ inputTokens: 348, outputTokens: 58, cost: 0.000014616 });
|
|
31
|
+
expect(response.answers).toMatchObject({
|
|
32
|
+
team: {
|
|
33
|
+
type: "choice",
|
|
34
|
+
choice: "technical",
|
|
35
|
+
probabilities: { billing: 0.49, none_of_these: 0.02, technical: 0.49 },
|
|
36
|
+
},
|
|
37
|
+
urgent: { type: "boolean", probability: 0.54 },
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("a choice without a choice string uses the highest probability", () => {
|
|
42
|
+
const response = normalizeDecisionResponse("systemone", {
|
|
43
|
+
model: "m",
|
|
44
|
+
answers: {
|
|
45
|
+
team: { type: "choice", probabilities: { billing: 0.2, technical: 0.8 } },
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
expect(response.answers).toMatchObject({
|
|
49
|
+
team: { type: "choice", choice: "technical" },
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("refusal drops the text and a partial body is malformed", () => {
|
|
54
|
+
const response = normalizeDecisionResponse("systemone", {
|
|
55
|
+
model: "m",
|
|
56
|
+
answers: {
|
|
57
|
+
team: { type: "refusal", text: "not a label" },
|
|
58
|
+
urgent: { type: "choice" },
|
|
59
|
+
empty: { nope: true },
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
expect(response.answers).toEqual({
|
|
63
|
+
team: { type: "refusal" },
|
|
64
|
+
urgent: { type: "malformed" },
|
|
65
|
+
empty: { type: "malformed" },
|
|
66
|
+
});
|
|
67
|
+
expect(JSON.stringify(response.answers)).not.toContain("not a label");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("OpenAI decisions encode and normalize predicate, choice, score, and refusal", () => {
|
|
71
|
+
const questions = wireQuestions({
|
|
72
|
+
urgent: ai.boolean("Does this need a person today?"),
|
|
73
|
+
team: ai.choice("Which team owns this ticket?", {
|
|
74
|
+
billing: "Billing",
|
|
75
|
+
technical: "Technical",
|
|
76
|
+
}),
|
|
77
|
+
severity: ai.score("How severe is this?", ["low", "high"]),
|
|
78
|
+
});
|
|
79
|
+
const encoded = encodeDecisionRequest("openai-decisions", {
|
|
80
|
+
model: "gpt-6-luna",
|
|
81
|
+
state: { ticket: "Checkout is blank" },
|
|
82
|
+
questions,
|
|
83
|
+
}) as {
|
|
84
|
+
model: string;
|
|
85
|
+
input: string;
|
|
86
|
+
questions: {
|
|
87
|
+
name: string;
|
|
88
|
+
type: string;
|
|
89
|
+
choices?: { value: string }[];
|
|
90
|
+
levels?: { label: string }[];
|
|
91
|
+
}[];
|
|
92
|
+
};
|
|
93
|
+
expect(encoded.model).toBe("gpt-6-luna");
|
|
94
|
+
expect(encoded.input).toBe(JSON.stringify({ ticket: "Checkout is blank" }));
|
|
95
|
+
expect(encoded.questions.map((question) => question.type)).toEqual([
|
|
96
|
+
"predicate",
|
|
97
|
+
"choice",
|
|
98
|
+
"score",
|
|
99
|
+
]);
|
|
100
|
+
expect(encoded.questions[1]?.choices?.map((choice) => choice.value)).toEqual([
|
|
101
|
+
"billing",
|
|
102
|
+
"technical",
|
|
103
|
+
"none_of_these",
|
|
104
|
+
]);
|
|
105
|
+
expect(encoded.questions[2]?.levels).toEqual([{ label: "low" }, { label: "high" }]);
|
|
106
|
+
|
|
107
|
+
const response = normalizeDecisionResponse("openai-decisions", {
|
|
108
|
+
model: "gpt-6-luna",
|
|
109
|
+
answers: [
|
|
110
|
+
{ name: "urgent", type: "predicate", probability: 0.12 },
|
|
111
|
+
{
|
|
112
|
+
name: "team",
|
|
113
|
+
type: "choice",
|
|
114
|
+
choice: "none_of_these",
|
|
115
|
+
probabilities: [
|
|
116
|
+
{ value: "billing", probability: 0.1 },
|
|
117
|
+
{ value: "technical", probability: 0.2 },
|
|
118
|
+
{ value: "none_of_these", probability: 0.7 },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "severity",
|
|
123
|
+
type: "score",
|
|
124
|
+
score: "low",
|
|
125
|
+
probabilities: [
|
|
126
|
+
{ label: "low", probability: 0.8 },
|
|
127
|
+
{ label: "high", probability: 0.2 },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
{ name: "held", type: "refusal" },
|
|
131
|
+
],
|
|
132
|
+
usage: { input_tokens: 20, output_tokens: 8 },
|
|
133
|
+
});
|
|
134
|
+
expect(response.model).toBe("gpt-6-luna");
|
|
135
|
+
expect(response.answers).toMatchObject({
|
|
136
|
+
urgent: { type: "boolean", probability: 0.12 },
|
|
137
|
+
team: { type: "choice", choice: "none_of_these" },
|
|
138
|
+
severity: { type: "score", score: "low", probabilities: { low: 0.8, high: 0.2 } },
|
|
139
|
+
held: { type: "refusal" },
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|