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
|
@@ -118,6 +118,17 @@ export function DecisionsPage(): JSX.Element {
|
|
|
118
118
|
>
|
|
119
119
|
<span className="min-w-0 flex-1 truncate">{decision.name}</span>
|
|
120
120
|
<span className={EXPLORER_COUNT_CLASS}>{decisionStateLabel(decision.state)}</span>
|
|
121
|
+
{decision.certificates?.map((certificate) => (
|
|
122
|
+
<span
|
|
123
|
+
key={certificate.decider}
|
|
124
|
+
className={EXPLORER_COUNT_CLASS}
|
|
125
|
+
data-slot="decision-certificate"
|
|
126
|
+
data-pinned={certificate.pinned ? "true" : "false"}
|
|
127
|
+
>
|
|
128
|
+
{certificate.decider} {certificate.model}
|
|
129
|
+
{certificate.pinned ? "" : " unpinned"}
|
|
130
|
+
</span>
|
|
131
|
+
))}
|
|
121
132
|
{decision.metrics ? (
|
|
122
133
|
<span className={EXPLORER_COUNT_CLASS} data-slot="decision-metrics">
|
|
123
134
|
{Object.entries(decision.metrics)
|
|
@@ -151,6 +162,11 @@ export function DecisionsPage(): JSX.Element {
|
|
|
151
162
|
<span className="min-w-0 flex-1 truncate">
|
|
152
163
|
{row.decision}
|
|
153
164
|
{row.labelOnly ? " · label" : ""}
|
|
165
|
+
{row.why === "refused" && row.by
|
|
166
|
+
? ` · refused by ${row.by}`
|
|
167
|
+
: row.why
|
|
168
|
+
? ` · ${row.why}`
|
|
169
|
+
: ""}
|
|
154
170
|
</span>
|
|
155
171
|
<span className={EXPLORER_COUNT_CLASS} data-slot="decision-age">
|
|
156
172
|
{formatAge(row.ageMs)}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verified decision hosts. Anything else is `driverId` + `baseUrl` + `capabilities`.
|
|
3
|
+
* Codecs stay out of this module so the edge graph does not load them.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Wire a decider speaks. */
|
|
7
|
+
export type DeciderProtocol = "systemone" | "openai-decisions";
|
|
8
|
+
|
|
9
|
+
/** How a certificate binds to a model id. */
|
|
10
|
+
export type DeciderPinning = "dated" | "alias";
|
|
11
|
+
|
|
12
|
+
/** What one decider can answer. Numeric limits apply only when present. */
|
|
13
|
+
export interface DeciderCapabilities {
|
|
14
|
+
readonly boolean: boolean;
|
|
15
|
+
readonly choice: boolean;
|
|
16
|
+
readonly score: boolean;
|
|
17
|
+
readonly refusal: boolean;
|
|
18
|
+
readonly maxChoices?: number;
|
|
19
|
+
readonly minLevels?: number;
|
|
20
|
+
readonly maxLevels?: number;
|
|
21
|
+
readonly maxContext?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One verified host. */
|
|
25
|
+
export interface DeciderPreset {
|
|
26
|
+
readonly protocol: DeciderProtocol;
|
|
27
|
+
readonly baseUrl: string;
|
|
28
|
+
readonly secret: string;
|
|
29
|
+
readonly pinning: DeciderPinning;
|
|
30
|
+
readonly capabilities: DeciderCapabilities;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Literal capabilities for the OpenRouter preset. */
|
|
34
|
+
type OpenRouterPreset = {
|
|
35
|
+
readonly protocol: "systemone";
|
|
36
|
+
readonly baseUrl: "https://openrouter.ai/api/alpha/decisions";
|
|
37
|
+
readonly secret: "OPENROUTER_API_KEY";
|
|
38
|
+
readonly pinning: "dated";
|
|
39
|
+
readonly capabilities: {
|
|
40
|
+
readonly boolean: true;
|
|
41
|
+
readonly choice: true;
|
|
42
|
+
readonly score: true;
|
|
43
|
+
readonly refusal: false;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Literal capabilities for the OpenAI preset. */
|
|
48
|
+
type OpenAIPreset = {
|
|
49
|
+
readonly protocol: "openai-decisions";
|
|
50
|
+
readonly baseUrl: "https://api.openai.com/v1/decisions";
|
|
51
|
+
readonly secret: "OPENAI_API_KEY";
|
|
52
|
+
readonly pinning: "alias";
|
|
53
|
+
readonly capabilities: {
|
|
54
|
+
readonly boolean: true;
|
|
55
|
+
readonly choice: true;
|
|
56
|
+
readonly score: true;
|
|
57
|
+
readonly refusal: true;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** OpenRouter System One and OpenAI Decisions. No other host is filled in. */
|
|
62
|
+
export const DECIDER_PRESETS: {
|
|
63
|
+
readonly openrouter: OpenRouterPreset;
|
|
64
|
+
readonly openai: OpenAIPreset;
|
|
65
|
+
} = {
|
|
66
|
+
openrouter: {
|
|
67
|
+
protocol: "systemone",
|
|
68
|
+
baseUrl: "https://openrouter.ai/api/alpha/decisions",
|
|
69
|
+
secret: "OPENROUTER_API_KEY",
|
|
70
|
+
pinning: "dated",
|
|
71
|
+
capabilities: { boolean: true, choice: true, score: true, refusal: false },
|
|
72
|
+
},
|
|
73
|
+
openai: {
|
|
74
|
+
protocol: "openai-decisions",
|
|
75
|
+
baseUrl: "https://api.openai.com/v1/decisions",
|
|
76
|
+
secret: "OPENAI_API_KEY",
|
|
77
|
+
pinning: "alias",
|
|
78
|
+
capabilities: { boolean: true, choice: true, score: true, refusal: true },
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Preset provider id. */
|
|
83
|
+
export type DeciderPresetName = keyof typeof DECIDER_PRESETS;
|
|
84
|
+
|
|
85
|
+
/** Alias certificates expire this long after certify. */
|
|
86
|
+
export const ALIAS_CERTIFICATE_MS: number = 30 * 24 * 60 * 60 * 1000;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether `provider` is a shipped preset.
|
|
90
|
+
*
|
|
91
|
+
* @param provider - Author provider id
|
|
92
|
+
*/
|
|
93
|
+
export function isDeciderPreset(provider: string): provider is DeciderPresetName {
|
|
94
|
+
return Object.prototype.hasOwnProperty.call(DECIDER_PRESETS, provider);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Fields shared by a preset and a custom host. */
|
|
98
|
+
export interface ResolvedDecider {
|
|
99
|
+
readonly protocol: DeciderProtocol;
|
|
100
|
+
readonly baseUrl: string;
|
|
101
|
+
readonly model: string;
|
|
102
|
+
readonly secret: string;
|
|
103
|
+
readonly pinning: DeciderPinning;
|
|
104
|
+
readonly capabilities: DeciderCapabilities;
|
|
105
|
+
readonly provider?: string;
|
|
106
|
+
readonly region?: string;
|
|
107
|
+
readonly regionStatus?: "declared";
|
|
108
|
+
readonly zdr?: boolean;
|
|
109
|
+
readonly zdrStatus?: "declared";
|
|
110
|
+
readonly timeout?: number | string;
|
|
111
|
+
readonly concurrency?: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Fill a preset, or require the custom host fields.
|
|
116
|
+
* Region and ZDR are the app's declaration. Neither preset publishes a verified value.
|
|
117
|
+
*
|
|
118
|
+
* @param name - Decider name, for the error
|
|
119
|
+
* @param options - Author options
|
|
120
|
+
*/
|
|
121
|
+
export function resolveDecider(
|
|
122
|
+
name: string,
|
|
123
|
+
options: {
|
|
124
|
+
readonly provider?: string;
|
|
125
|
+
readonly driverId?: string;
|
|
126
|
+
readonly baseUrl?: string;
|
|
127
|
+
readonly model?: string;
|
|
128
|
+
readonly secret?: string;
|
|
129
|
+
readonly capabilities?: DeciderCapabilities;
|
|
130
|
+
readonly region?: string;
|
|
131
|
+
readonly zdr?: boolean;
|
|
132
|
+
readonly timeout?: number | string;
|
|
133
|
+
readonly concurrency?: number;
|
|
134
|
+
},
|
|
135
|
+
): ResolvedDecider {
|
|
136
|
+
if (!options.model) throw new TypeError(`ai.decider("${name}"): model is required`);
|
|
137
|
+
const declared = {
|
|
138
|
+
...(options.region !== undefined
|
|
139
|
+
? { region: options.region, regionStatus: "declared" as const }
|
|
140
|
+
: {}),
|
|
141
|
+
...(options.zdr !== undefined ? { zdr: options.zdr, zdrStatus: "declared" as const } : {}),
|
|
142
|
+
...(options.timeout !== undefined ? { timeout: options.timeout } : {}),
|
|
143
|
+
...(options.concurrency !== undefined ? { concurrency: options.concurrency } : {}),
|
|
144
|
+
};
|
|
145
|
+
if (options.provider && isDeciderPreset(options.provider)) {
|
|
146
|
+
const preset = DECIDER_PRESETS[options.provider];
|
|
147
|
+
return {
|
|
148
|
+
protocol: preset.protocol,
|
|
149
|
+
baseUrl: preset.baseUrl,
|
|
150
|
+
model: options.model,
|
|
151
|
+
secret: options.secret ?? preset.secret,
|
|
152
|
+
pinning: preset.pinning,
|
|
153
|
+
capabilities: preset.capabilities,
|
|
154
|
+
provider: options.provider,
|
|
155
|
+
...declared,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (options.driverId !== "systemone" && options.driverId !== "openai-decisions") {
|
|
159
|
+
throw new TypeError(
|
|
160
|
+
`ai.decider("${name}"): provider "${options.provider ?? ""}" needs driverId "systemone" or "openai-decisions", baseUrl, and capabilities`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (!options.baseUrl) {
|
|
164
|
+
throw new TypeError(`ai.decider("${name}"): baseUrl is required`);
|
|
165
|
+
}
|
|
166
|
+
if (!options.secret) {
|
|
167
|
+
throw new TypeError(`ai.decider("${name}"): secret is required`);
|
|
168
|
+
}
|
|
169
|
+
if (!options.capabilities) {
|
|
170
|
+
throw new TypeError(`ai.decider("${name}"): capabilities are required`);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
protocol: options.driverId,
|
|
174
|
+
baseUrl: options.baseUrl,
|
|
175
|
+
model: options.model,
|
|
176
|
+
secret: options.secret,
|
|
177
|
+
pinning: options.driverId === "openai-decisions" ? "alias" : "dated",
|
|
178
|
+
capabilities: options.capabilities,
|
|
179
|
+
...(options.provider !== undefined ? { provider: options.provider } : {}),
|
|
180
|
+
...declared,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -63,15 +63,16 @@ export function bindDecisionFlows(adopt: (binding: Binding) => void, manifest: M
|
|
|
63
63
|
const decl = aiDecisionRegistry.find((item) => item.name === name);
|
|
64
64
|
const pinned = pinnedDecision(name);
|
|
65
65
|
const rows = await loadDecisionLabels(name);
|
|
66
|
-
const fitted = aggregateDecisionCandidate(name, () =>
|
|
67
|
-
certifyLabels({
|
|
68
|
-
model: pinned?.model ??
|
|
66
|
+
const fitted = aggregateDecisionCandidate(name, () => {
|
|
67
|
+
const cert = certifyLabels({
|
|
68
|
+
model: pinned?.model ?? "",
|
|
69
69
|
maxError: decl?.autonomy?.maxError ?? 0.05,
|
|
70
70
|
delta: decl?.autonomy?.risk ?? 0.1,
|
|
71
71
|
ask: decl?.ask ?? {},
|
|
72
72
|
labels: rows,
|
|
73
|
-
})
|
|
74
|
-
|
|
73
|
+
});
|
|
74
|
+
return { decider: decl?.decider ?? "", ...cert };
|
|
75
|
+
});
|
|
75
76
|
await persistDecisionCandidate(name, fitted);
|
|
76
77
|
}
|
|
77
78
|
return { ok: true };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRouter decision-model catalog. Only fields the catalog actually returns.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const CATALOG_URL = "https://openrouter.ai/api/v1/models?output_modalities=decisions";
|
|
6
|
+
|
|
7
|
+
interface CatalogRow {
|
|
8
|
+
readonly id?: unknown;
|
|
9
|
+
readonly canonical_slug?: unknown;
|
|
10
|
+
readonly context_length?: unknown;
|
|
11
|
+
readonly architecture?: { readonly input_modalities?: unknown };
|
|
12
|
+
readonly alias_target?: { readonly slug?: unknown };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Lines for `oke decide models`: id, canonical slug, context, input modalities.
|
|
17
|
+
*
|
|
18
|
+
* @param body - Catalog JSON
|
|
19
|
+
*/
|
|
20
|
+
export function formatDecisionModels(body: unknown): string {
|
|
21
|
+
const rows = catalogRows(body);
|
|
22
|
+
if (rows.length === 0) return "oke decide models: no decision models\n";
|
|
23
|
+
const lines = ["id\tcanonical_slug\tcontext\tinput"];
|
|
24
|
+
for (const row of rows) {
|
|
25
|
+
const id = typeof row.id === "string" ? row.id : "";
|
|
26
|
+
const canonical = typeof row.canonical_slug === "string" ? row.canonical_slug : "";
|
|
27
|
+
const context = typeof row.context_length === "number" ? String(row.context_length) : "";
|
|
28
|
+
const input = Array.isArray(row.architecture?.input_modalities)
|
|
29
|
+
? row.architecture.input_modalities.filter((item) => typeof item === "string").join(",")
|
|
30
|
+
: "";
|
|
31
|
+
lines.push(`${id}\t${canonical}\t${context}\t${input}`);
|
|
32
|
+
}
|
|
33
|
+
return `${lines.join("\n")}\n`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Fetch the public decision catalog and format it.
|
|
38
|
+
*
|
|
39
|
+
* @param fetcher - Injected fetch
|
|
40
|
+
*/
|
|
41
|
+
export async function fetchDecisionModels(
|
|
42
|
+
fetcher: (input: string, init?: RequestInit) => Promise<Response> = fetch,
|
|
43
|
+
): Promise<string> {
|
|
44
|
+
const res = await fetcher(CATALOG_URL);
|
|
45
|
+
if (!res.ok) throw new Error(`oke decide models: ${res.status}`);
|
|
46
|
+
return formatDecisionModels(await res.json());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Dated slug the catalog lists for `model`, when it differs from `model`.
|
|
51
|
+
* Missing catalog, unknown id, or an undated canonical slug returns undefined.
|
|
52
|
+
*
|
|
53
|
+
* @param model - Decider model id
|
|
54
|
+
* @param baseUrl - Decider URL. Only OpenRouter is queried.
|
|
55
|
+
* @param pinning - `dated` looks up a slug. `alias` does not.
|
|
56
|
+
* @param fetcher - Injected fetch
|
|
57
|
+
*/
|
|
58
|
+
export async function requiredDatedModel(
|
|
59
|
+
model: string,
|
|
60
|
+
baseUrl: string,
|
|
61
|
+
pinning: "dated" | "alias",
|
|
62
|
+
fetcher: (input: string, init?: RequestInit) => Promise<Response> = fetch,
|
|
63
|
+
): Promise<string | undefined> {
|
|
64
|
+
if (pinning !== "dated" || !baseUrl.includes("openrouter.ai")) return undefined;
|
|
65
|
+
try {
|
|
66
|
+
const res = await fetcher(CATALOG_URL);
|
|
67
|
+
if (!res.ok) return undefined;
|
|
68
|
+
return datedSlug(catalogRows(await res.json()), model);
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function catalogRows(body: unknown): CatalogRow[] {
|
|
75
|
+
if (!body || typeof body !== "object") return [];
|
|
76
|
+
const data = (body as { data?: unknown }).data;
|
|
77
|
+
return Array.isArray(data) ? (data as CatalogRow[]) : [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function datedSlug(rows: readonly CatalogRow[], model: string): string | undefined {
|
|
81
|
+
const row = rows.find((item) => item.id === model);
|
|
82
|
+
if (!row) return undefined;
|
|
83
|
+
const target = typeof row.alias_target?.slug === "string" ? row.alias_target.slug : undefined;
|
|
84
|
+
const followed = target ? rows.find((item) => item.id === target) : undefined;
|
|
85
|
+
const canonical =
|
|
86
|
+
typeof followed?.canonical_slug === "string"
|
|
87
|
+
? followed.canonical_slug
|
|
88
|
+
: typeof row.canonical_slug === "string"
|
|
89
|
+
? row.canonical_slug
|
|
90
|
+
: undefined;
|
|
91
|
+
if (!canonical || !/-\d{8}$/.test(canonical) || canonical === model) return undefined;
|
|
92
|
+
return canonical;
|
|
93
|
+
}
|
|
@@ -36,6 +36,7 @@ function answer() {
|
|
|
36
36
|
answers: {
|
|
37
37
|
team: {
|
|
38
38
|
type: "choice",
|
|
39
|
+
choice: "technical",
|
|
39
40
|
probabilities: { billing: 0.08, technical: 0.9, none_of_these: 0.02 },
|
|
40
41
|
},
|
|
41
42
|
},
|
|
@@ -54,22 +55,29 @@ async function run(
|
|
|
54
55
|
) {
|
|
55
56
|
const question = ai.choice("which team", { billing: "Billing", technical: "Technical" });
|
|
56
57
|
const decl = ai.decision("triage", {
|
|
57
|
-
|
|
58
|
+
decider: ai.decider("jev", { provider: "openrouter", model: "typesafe/jev-1.13.0" }),
|
|
59
|
+
otherwise: "abstain",
|
|
58
60
|
autonomy: { maxError: 0.05, audit: 0 },
|
|
59
61
|
...(extra?.locale ? { locale: () => "ar" } : {}),
|
|
60
62
|
ask: { team: question },
|
|
61
63
|
});
|
|
62
64
|
if (lock) {
|
|
63
65
|
setDecisionLock({
|
|
66
|
+
version: 2,
|
|
64
67
|
decisions: {
|
|
65
68
|
triage: {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
deciders: {
|
|
70
|
+
jev: {
|
|
71
|
+
model: extra?.mismatch ? "other" : "typesafe/jev-1.13.0",
|
|
72
|
+
pinned: true,
|
|
73
|
+
questions: {
|
|
74
|
+
team: {
|
|
75
|
+
"": {
|
|
76
|
+
hash: extra?.stale ? "nope" : questionHash(question),
|
|
77
|
+
calibrator: { kind: "temperature" as const, t: 1 },
|
|
78
|
+
threshold: 0.5,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
73
81
|
},
|
|
74
82
|
},
|
|
75
83
|
},
|
|
@@ -133,7 +141,9 @@ describe("decision certificates", () => {
|
|
|
133
141
|
|
|
134
142
|
test("promote writes the fetched candidate", async () => {
|
|
135
143
|
const candidate = {
|
|
144
|
+
decider: "jev",
|
|
136
145
|
model: "typesafe/jev-1.13.0",
|
|
146
|
+
pinned: true,
|
|
137
147
|
questions: {
|
|
138
148
|
team: {
|
|
139
149
|
"": {
|
|
@@ -144,34 +154,45 @@ describe("decision certificates", () => {
|
|
|
144
154
|
},
|
|
145
155
|
},
|
|
146
156
|
};
|
|
157
|
+
const slot = {
|
|
158
|
+
deciders: {
|
|
159
|
+
jev: {
|
|
160
|
+
model: candidate.model,
|
|
161
|
+
pinned: true,
|
|
162
|
+
questions: candidate.questions,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
};
|
|
147
166
|
const dir = await mkdtemp(join(tmpdir(), "oke-decide-"));
|
|
148
167
|
const lockPath = join(dir, "oke-decisions.lock.json");
|
|
149
168
|
const written = await promoteDecision({
|
|
150
169
|
name: "triage",
|
|
151
170
|
origin: "http://127.0.0.1:6530",
|
|
152
171
|
lockPath,
|
|
153
|
-
current: { decisions: {} },
|
|
172
|
+
current: { version: 2, decisions: {} },
|
|
154
173
|
fetcher: async () => new Response(JSON.stringify(candidate), { status: 200 }),
|
|
155
174
|
});
|
|
156
|
-
expect(written.
|
|
175
|
+
expect(written.version).toBe(2);
|
|
176
|
+
expect(written.decisions.triage).toEqual(slot);
|
|
157
177
|
const disk = JSON.parse(await readFile(lockPath, "utf8")) as { decisions: { triage: unknown } };
|
|
158
|
-
expect(disk.decisions.triage).toEqual(
|
|
159
|
-
await Bun.write(lockPath, `${JSON.stringify({ decisions: { other:
|
|
178
|
+
expect(disk.decisions.triage).toEqual(slot);
|
|
179
|
+
await Bun.write(lockPath, `${JSON.stringify({ version: 2, decisions: { other: slot } })}\n`);
|
|
160
180
|
const merged = await promoteDecision({
|
|
161
181
|
name: "triage",
|
|
162
182
|
origin: "http://127.0.0.1:6530",
|
|
163
183
|
lockPath,
|
|
164
184
|
fetcher: async () => new Response(JSON.stringify(candidate), { status: 200 }),
|
|
165
185
|
});
|
|
166
|
-
expect(merged.decisions.other).toEqual(
|
|
167
|
-
expect(merged.decisions.triage).toEqual(
|
|
186
|
+
expect(merged.decisions.other).toEqual(slot);
|
|
187
|
+
expect(merged.decisions.triage).toEqual(slot);
|
|
168
188
|
expect(await loadDecisionLockfile(dir)).toEqual(merged);
|
|
169
189
|
});
|
|
170
190
|
|
|
171
191
|
test("certify fits a threshold from the provider and hashes the question", async () => {
|
|
172
192
|
const question = ai.choice("which team", { billing: "Billing", technical: "Technical" });
|
|
173
193
|
const decl = ai.decision("triage", {
|
|
174
|
-
|
|
194
|
+
decider: ai.decider("jev", { provider: "openrouter", model: "typesafe/jev-1.13.0" }),
|
|
195
|
+
otherwise: "abstain",
|
|
175
196
|
autonomy: { maxError: 0.05, audit: 0 },
|
|
176
197
|
evals: "seed.jsonl",
|
|
177
198
|
ask: { team: question },
|
|
@@ -192,14 +213,15 @@ describe("decision certificates", () => {
|
|
|
192
213
|
answers: {
|
|
193
214
|
team: {
|
|
194
215
|
type: "choice",
|
|
216
|
+
choice: "technical",
|
|
195
217
|
probabilities: { billing: 0.05, technical: 0.93, none_of_these: 0.02 },
|
|
196
218
|
},
|
|
197
219
|
},
|
|
198
220
|
usage: {},
|
|
199
221
|
}),
|
|
200
222
|
});
|
|
201
|
-
expect(entry.questions.team?.[""]?.hash).toBe(questionHash(question));
|
|
202
|
-
expect(entry.questions.team?.[""]?.threshold).toBeGreaterThan(0);
|
|
223
|
+
expect(entry.cert.questions.team?.[""]?.hash).toBe(questionHash(question));
|
|
224
|
+
expect(entry.cert.questions.team?.[""]?.threshold).toBeGreaterThan(0);
|
|
203
225
|
const dir = await mkdtemp(join(tmpdir(), "oke-cert-"));
|
|
204
226
|
const seedPath = join(dir, "seed.jsonl");
|
|
205
227
|
await Bun.write(seedPath, seed);
|
|
@@ -212,6 +234,8 @@ describe("decision certificates", () => {
|
|
|
212
234
|
decisions: {
|
|
213
235
|
triage: {
|
|
214
236
|
mode: "abstain",
|
|
237
|
+
decider: "jev",
|
|
238
|
+
otherwise: "abstain",
|
|
215
239
|
questions: ["team"],
|
|
216
240
|
evals: seedPath,
|
|
217
241
|
autonomy: { maxError: 0.05, audit: 0 },
|
|
@@ -224,6 +248,7 @@ describe("decision certificates", () => {
|
|
|
224
248
|
answers: {
|
|
225
249
|
team: {
|
|
226
250
|
type: "choice",
|
|
251
|
+
choice: "technical",
|
|
227
252
|
probabilities: { billing: 0.05, technical: 0.93, none_of_these: 0.02 },
|
|
228
253
|
},
|
|
229
254
|
},
|
|
@@ -232,7 +257,9 @@ describe("decision certificates", () => {
|
|
|
232
257
|
});
|
|
233
258
|
expect(code).toBe(0);
|
|
234
259
|
const loaded = await loadDecisionLockfile(dir);
|
|
235
|
-
expect(loaded?.decisions.triage?.questions.team?.[""]?.hash).toBe(
|
|
260
|
+
expect(loaded?.decisions.triage?.deciders.jev?.questions.team?.[""]?.hash).toBe(
|
|
261
|
+
questionHash(question),
|
|
262
|
+
);
|
|
236
263
|
});
|
|
237
264
|
|
|
238
265
|
test("one seed row does not certify a slice", async () => {
|
|
@@ -247,13 +274,14 @@ describe("decision certificates", () => {
|
|
|
247
274
|
answers: {
|
|
248
275
|
team: {
|
|
249
276
|
type: "choice",
|
|
277
|
+
choice: "technical",
|
|
250
278
|
probabilities: { billing: 0.05, technical: 0.93, none_of_these: 0.02 },
|
|
251
279
|
},
|
|
252
280
|
},
|
|
253
281
|
usage: {},
|
|
254
282
|
}),
|
|
255
283
|
});
|
|
256
|
-
expect(entry.questions.team).toBeUndefined();
|
|
284
|
+
expect(entry.cert.questions.team).toBeUndefined();
|
|
257
285
|
});
|
|
258
286
|
|
|
259
287
|
test("choice and score labels certify from the stored distribution", () => {
|