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
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System One and OpenAI Decisions codecs.
|
|
3
|
+
* Both become one internal answer. Loaded only from the decision module.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AiDecisionQuestion } from "../declare.ts";
|
|
7
|
+
import type { DeciderProtocol } from "../deciders/presets.ts";
|
|
8
|
+
import type { DecisionRequest, DecisionResponse, DecisionUsage, WireQuestion } from "./provider.ts";
|
|
9
|
+
|
|
10
|
+
/** One question after the codec. Refusal carries no text. */
|
|
11
|
+
export type NormalizedAnswer =
|
|
12
|
+
| { readonly type: "boolean"; readonly probability: number }
|
|
13
|
+
| {
|
|
14
|
+
readonly type: "choice";
|
|
15
|
+
readonly choice: string;
|
|
16
|
+
readonly probabilities: Readonly<Record<string, number>>;
|
|
17
|
+
readonly confidence?: number;
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
readonly type: "score";
|
|
21
|
+
readonly score: string;
|
|
22
|
+
readonly probabilities: Readonly<Record<string, number>>;
|
|
23
|
+
readonly confidence?: number;
|
|
24
|
+
}
|
|
25
|
+
| { readonly type: "refusal" }
|
|
26
|
+
| { readonly type: "malformed" };
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Token estimate used before a call. One token per four JSON characters.
|
|
30
|
+
*
|
|
31
|
+
* @param state - Calling input
|
|
32
|
+
*/
|
|
33
|
+
export function estimateDecisionTokens(state: unknown): number {
|
|
34
|
+
const text = typeof state === "string" ? state : JSON.stringify(state ?? null);
|
|
35
|
+
return Math.ceil(text.length / 4);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Encode a request for the decider's protocol.
|
|
40
|
+
*
|
|
41
|
+
* @param protocol - `systemone` or `openai-decisions`
|
|
42
|
+
* @param request - Model, state, and questions
|
|
43
|
+
*/
|
|
44
|
+
export function encodeDecisionRequest(
|
|
45
|
+
protocol: DeciderProtocol,
|
|
46
|
+
request: DecisionRequest,
|
|
47
|
+
): unknown {
|
|
48
|
+
if (protocol === "systemone") {
|
|
49
|
+
return {
|
|
50
|
+
model: request.model,
|
|
51
|
+
state: request.state,
|
|
52
|
+
questions: request.questions,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const input =
|
|
56
|
+
typeof request.state === "string" ? request.state : JSON.stringify(request.state ?? null);
|
|
57
|
+
return {
|
|
58
|
+
model: request.model,
|
|
59
|
+
input,
|
|
60
|
+
questions: Object.entries(request.questions).map(([name, question]) =>
|
|
61
|
+
encodeOpenAIQuestion(name, question),
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function encodeOpenAIQuestion(name: string, question: WireQuestion): unknown {
|
|
67
|
+
if (question.type === "noul") {
|
|
68
|
+
return { name, type: "predicate", instructions: question.instructions };
|
|
69
|
+
}
|
|
70
|
+
if (question.type === "choice") {
|
|
71
|
+
return {
|
|
72
|
+
name,
|
|
73
|
+
type: "choice",
|
|
74
|
+
instructions: question.instructions,
|
|
75
|
+
choices: Object.entries(question.criteria).map(([value, description]) =>
|
|
76
|
+
description == null || description === "" ? { value } : { value, description },
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
name,
|
|
82
|
+
type: "score",
|
|
83
|
+
instructions: question.instructions,
|
|
84
|
+
levels: question.criteria.map((label) => ({ label })),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Normalize a parsed provider body. Malformed answers stay on the question id.
|
|
90
|
+
*
|
|
91
|
+
* @param protocol - Decider protocol
|
|
92
|
+
* @param body - Parsed JSON, or an already parsed {@link DecisionResponse}
|
|
93
|
+
*/
|
|
94
|
+
export function normalizeDecisionResponse(
|
|
95
|
+
protocol: DeciderProtocol,
|
|
96
|
+
body: unknown,
|
|
97
|
+
): DecisionResponse {
|
|
98
|
+
if (!body || typeof body !== "object") {
|
|
99
|
+
return { model: "", answers: {}, usage: {} };
|
|
100
|
+
}
|
|
101
|
+
const record = body as {
|
|
102
|
+
model?: unknown;
|
|
103
|
+
provider?: unknown;
|
|
104
|
+
answers?: unknown;
|
|
105
|
+
usage?: unknown;
|
|
106
|
+
};
|
|
107
|
+
const model = typeof record.model === "string" ? record.model : "";
|
|
108
|
+
const answers =
|
|
109
|
+
protocol === "openai-decisions"
|
|
110
|
+
? normalizeOpenAIAnswers(record.answers)
|
|
111
|
+
: normalizeSystemOneAnswers(record.answers);
|
|
112
|
+
return {
|
|
113
|
+
model,
|
|
114
|
+
...(typeof record.provider === "string" ? { provider: record.provider } : {}),
|
|
115
|
+
answers,
|
|
116
|
+
usage: usageFrom(record.usage),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeSystemOneAnswers(raw: unknown): Record<string, NormalizedAnswer> {
|
|
121
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
122
|
+
const answers: Record<string, NormalizedAnswer> = {};
|
|
123
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
124
|
+
answers[id] = normalizeSystemOneAnswer(value);
|
|
125
|
+
}
|
|
126
|
+
return answers;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function normalizeSystemOneAnswer(value: unknown): NormalizedAnswer {
|
|
130
|
+
if (!value || typeof value !== "object") return { type: "malformed" };
|
|
131
|
+
const record = value as Record<string, unknown>;
|
|
132
|
+
if (record.type === "noul" && typeof record.noul === "number") {
|
|
133
|
+
return { type: "boolean", probability: record.noul };
|
|
134
|
+
}
|
|
135
|
+
if (record.type === "choice") {
|
|
136
|
+
const probabilities = numberRecord(record.probabilities);
|
|
137
|
+
const choice =
|
|
138
|
+
typeof record.choice === "string"
|
|
139
|
+
? record.choice
|
|
140
|
+
: probabilities
|
|
141
|
+
? argmax(probabilities)
|
|
142
|
+
: "";
|
|
143
|
+
if (!probabilities || !choice) return { type: "malformed" };
|
|
144
|
+
return {
|
|
145
|
+
type: "choice",
|
|
146
|
+
choice,
|
|
147
|
+
probabilities,
|
|
148
|
+
...(typeof record.confidence === "number" ? { confidence: record.confidence } : {}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (record.type === "score") {
|
|
152
|
+
const probabilities = numberRecord(record.probabilities);
|
|
153
|
+
if (!probabilities) return { type: "malformed" };
|
|
154
|
+
const score =
|
|
155
|
+
typeof record.score === "string"
|
|
156
|
+
? record.score
|
|
157
|
+
: typeof record.score === "number"
|
|
158
|
+
? String(record.score)
|
|
159
|
+
: argmax(probabilities);
|
|
160
|
+
return {
|
|
161
|
+
type: "score",
|
|
162
|
+
score,
|
|
163
|
+
probabilities,
|
|
164
|
+
...(typeof record.confidence === "number" ? { confidence: record.confidence } : {}),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (record.type === "refusal") return { type: "refusal" };
|
|
168
|
+
return { type: "malformed" };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function normalizeOpenAIAnswers(raw: unknown): Record<string, NormalizedAnswer> {
|
|
172
|
+
if (!Array.isArray(raw)) return {};
|
|
173
|
+
const answers: Record<string, NormalizedAnswer> = {};
|
|
174
|
+
for (const value of raw) {
|
|
175
|
+
if (!value || typeof value !== "object") continue;
|
|
176
|
+
const record = value as Record<string, unknown>;
|
|
177
|
+
const name = typeof record.name === "string" ? record.name : "";
|
|
178
|
+
if (!name) continue;
|
|
179
|
+
answers[name] = normalizeOpenAIAnswer(record);
|
|
180
|
+
}
|
|
181
|
+
return answers;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizeOpenAIAnswer(record: Record<string, unknown>): NormalizedAnswer {
|
|
185
|
+
if (record.type === "refusal") return { type: "refusal" };
|
|
186
|
+
if (record.type === "predicate" && typeof record.probability === "number") {
|
|
187
|
+
return { type: "boolean", probability: record.probability };
|
|
188
|
+
}
|
|
189
|
+
if (record.type === "choice" && typeof record.choice === "string") {
|
|
190
|
+
const probabilities = probabilityList(record.probabilities, "value");
|
|
191
|
+
if (!probabilities) return { type: "malformed" };
|
|
192
|
+
return {
|
|
193
|
+
type: "choice",
|
|
194
|
+
choice: String(record.choice),
|
|
195
|
+
probabilities,
|
|
196
|
+
...(typeof record.confidence === "number" ? { confidence: record.confidence } : {}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (record.type === "score") {
|
|
200
|
+
const probabilities = probabilityList(record.probabilities, "label");
|
|
201
|
+
if (!probabilities) return { type: "malformed" };
|
|
202
|
+
const score = typeof record.score === "string" ? record.score : argmax(probabilities);
|
|
203
|
+
return {
|
|
204
|
+
type: "score",
|
|
205
|
+
score,
|
|
206
|
+
probabilities,
|
|
207
|
+
...(typeof record.confidence === "number" ? { confidence: record.confidence } : {}),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return { type: "malformed" };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function probabilityList(raw: unknown, key: "value" | "label"): Record<string, number> | undefined {
|
|
214
|
+
if (!Array.isArray(raw)) return undefined;
|
|
215
|
+
const probabilities: Record<string, number> = {};
|
|
216
|
+
for (const item of raw) {
|
|
217
|
+
if (!item || typeof item !== "object") return undefined;
|
|
218
|
+
const row = item as Record<string, unknown>;
|
|
219
|
+
const name = row[key];
|
|
220
|
+
if (
|
|
221
|
+
(typeof name !== "string" && typeof name !== "boolean" && typeof name !== "number") ||
|
|
222
|
+
typeof row.probability !== "number"
|
|
223
|
+
) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
probabilities[String(name)] = row.probability;
|
|
227
|
+
}
|
|
228
|
+
return probabilities;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function numberRecord(raw: unknown): Record<string, number> | undefined {
|
|
232
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
233
|
+
const probabilities: Record<string, number> = {};
|
|
234
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
235
|
+
if (typeof value !== "number") return undefined;
|
|
236
|
+
probabilities[key] = value;
|
|
237
|
+
}
|
|
238
|
+
return probabilities;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function argmax(probabilities: Readonly<Record<string, number>>): string {
|
|
242
|
+
let best = "";
|
|
243
|
+
let score = -1;
|
|
244
|
+
for (const [key, value] of Object.entries(probabilities)) {
|
|
245
|
+
if (value > score) {
|
|
246
|
+
best = key;
|
|
247
|
+
score = value;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return best;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function usageFrom(raw: unknown): DecisionUsage {
|
|
254
|
+
if (!raw || typeof raw !== "object") return {};
|
|
255
|
+
const usage = raw as {
|
|
256
|
+
input_tokens?: unknown;
|
|
257
|
+
output_tokens?: unknown;
|
|
258
|
+
cost?: unknown;
|
|
259
|
+
inputTokens?: unknown;
|
|
260
|
+
outputTokens?: unknown;
|
|
261
|
+
};
|
|
262
|
+
const input = usage.input_tokens ?? usage.inputTokens;
|
|
263
|
+
const output = usage.output_tokens ?? usage.outputTokens;
|
|
264
|
+
return {
|
|
265
|
+
...(typeof input === "number" ? { inputTokens: input } : {}),
|
|
266
|
+
...(typeof output === "number" ? { outputTokens: output } : {}),
|
|
267
|
+
...(typeof usage.cost === "number" ? { cost: usage.cost } : {}),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Wire questions for one decision. Choice always injects `none_of_these`.
|
|
273
|
+
*
|
|
274
|
+
* @param ask - Declared questions
|
|
275
|
+
*/
|
|
276
|
+
export function wireQuestions(
|
|
277
|
+
ask: Readonly<Record<string, AiDecisionQuestion>>,
|
|
278
|
+
): Record<string, WireQuestion> {
|
|
279
|
+
const questions: Record<string, WireQuestion> = {};
|
|
280
|
+
for (const [id, question] of Object.entries(ask)) {
|
|
281
|
+
questions[id] = wireQuestion(question);
|
|
282
|
+
}
|
|
283
|
+
return questions;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function wireQuestion(question: AiDecisionQuestion): WireQuestion {
|
|
287
|
+
if (question.kind === "choice") {
|
|
288
|
+
return {
|
|
289
|
+
type: "choice",
|
|
290
|
+
instructions: question.instructions,
|
|
291
|
+
criteria: { ...question.options, none_of_these: null },
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (question.kind === "score") {
|
|
295
|
+
return { type: "score", instructions: question.instructions, criteria: question.levels };
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
type: "noul",
|
|
299
|
+
instructions: question.instructions,
|
|
300
|
+
...(question.criteria !== undefined ? { criteria: question.criteria } : {}),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
@@ -54,6 +54,44 @@ function choice() {
|
|
|
54
54
|
return ai.choice("which team", { billing: "Billing", technical: "Technical" });
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function jev(name = "jev") {
|
|
58
|
+
return ai.decider(name, { provider: "openrouter", model: "typesafe/jev-1.13.0" });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function setLock(
|
|
62
|
+
body:
|
|
63
|
+
| {
|
|
64
|
+
decisions: Record<
|
|
65
|
+
string,
|
|
66
|
+
{ model: string; certifiedAt?: number; questions: Record<string, unknown> }
|
|
67
|
+
>;
|
|
68
|
+
}
|
|
69
|
+
| undefined,
|
|
70
|
+
) {
|
|
71
|
+
if (!body) {
|
|
72
|
+
setDecisionLock(undefined);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
setDecisionLock({
|
|
76
|
+
version: 2,
|
|
77
|
+
decisions: Object.fromEntries(
|
|
78
|
+
Object.entries(body.decisions).map(([name, entry]) => [
|
|
79
|
+
name,
|
|
80
|
+
{
|
|
81
|
+
deciders: {
|
|
82
|
+
jev: {
|
|
83
|
+
model: entry.model,
|
|
84
|
+
pinned: true as const,
|
|
85
|
+
...(entry.certifiedAt !== undefined ? { certifiedAt: entry.certifiedAt } : {}),
|
|
86
|
+
questions: entry.questions as never,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
]),
|
|
91
|
+
),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
57
95
|
function provider() {
|
|
58
96
|
setDecisionProvider(async () => ({
|
|
59
97
|
model: "typesafe/jev-1.13.0",
|
|
@@ -74,11 +112,12 @@ describe("fx.decide", () => {
|
|
|
74
112
|
provider();
|
|
75
113
|
const question = choice();
|
|
76
114
|
const decl = ai.decision("triage", {
|
|
77
|
-
|
|
115
|
+
decider: jev(),
|
|
116
|
+
otherwise: "abstain",
|
|
78
117
|
autonomy: { maxError: 0.05, audit: 0 },
|
|
79
118
|
ask: { team: question },
|
|
80
119
|
});
|
|
81
|
-
|
|
120
|
+
setLock({
|
|
82
121
|
decisions: {
|
|
83
122
|
triage: {
|
|
84
123
|
model: "typesafe/jev-1.13.0",
|
|
@@ -122,7 +161,8 @@ describe("fx.decide", () => {
|
|
|
122
161
|
throw new DecisionOutageError("down");
|
|
123
162
|
});
|
|
124
163
|
const decl = ai.decision("triage", {
|
|
125
|
-
|
|
164
|
+
decider: jev(),
|
|
165
|
+
otherwise: "abstain",
|
|
126
166
|
ask: { team: choice() },
|
|
127
167
|
});
|
|
128
168
|
const fx = createFx({ flow: "run", effects: { decides: ["triage"] }, now: () => 1 });
|
|
@@ -139,11 +179,12 @@ describe("fx.decide", () => {
|
|
|
139
179
|
provider();
|
|
140
180
|
const question = choice();
|
|
141
181
|
const decl = ai.decision("triage", {
|
|
142
|
-
|
|
182
|
+
decider: jev(),
|
|
183
|
+
otherwise: "abstain",
|
|
143
184
|
autonomy: { maxError: 0.05, audit: 1 },
|
|
144
185
|
ask: { team: question },
|
|
145
186
|
});
|
|
146
|
-
|
|
187
|
+
setLock({
|
|
147
188
|
decisions: {
|
|
148
189
|
triage: {
|
|
149
190
|
model: "typesafe/jev-1.13.0",
|
|
@@ -187,7 +228,8 @@ describe("fx.decide", () => {
|
|
|
187
228
|
provider();
|
|
188
229
|
gate.policy("ops", (ctx) => ctx.operator.id !== null);
|
|
189
230
|
const decl = ai.decision("triage", {
|
|
190
|
-
|
|
231
|
+
decider: jev(),
|
|
232
|
+
otherwise: "ops",
|
|
191
233
|
ask: { team: choice() },
|
|
192
234
|
});
|
|
193
235
|
const store = await createPostgresJournalStore({ sql: createPostgresJournalFake() });
|
|
@@ -232,7 +274,8 @@ describe("fx.decide", () => {
|
|
|
232
274
|
test("the parked review masks secret input and keeps the rest", async () => {
|
|
233
275
|
provider();
|
|
234
276
|
const decl = ai.decision("triage", {
|
|
235
|
-
|
|
277
|
+
decider: jev(),
|
|
278
|
+
otherwise: "ops",
|
|
236
279
|
in: z.object({
|
|
237
280
|
ticket: z.string(),
|
|
238
281
|
token: z.string().describe("secret"),
|
|
@@ -298,11 +341,12 @@ describe("fx.decide", () => {
|
|
|
298
341
|
provider();
|
|
299
342
|
const question = choice();
|
|
300
343
|
const decl = ai.decision("triage", {
|
|
301
|
-
|
|
344
|
+
decider: jev(),
|
|
345
|
+
otherwise: "abstain",
|
|
302
346
|
autonomy: { maxError: 0.05, audit: 0 },
|
|
303
347
|
ask: { team: question },
|
|
304
348
|
});
|
|
305
|
-
|
|
349
|
+
setLock({
|
|
306
350
|
decisions: {
|
|
307
351
|
triage: {
|
|
308
352
|
model: "typesafe/jev-1.13.0",
|
|
@@ -340,7 +384,11 @@ describe("fx.decide", () => {
|
|
|
340
384
|
test("the same decision can park twice in one run", async () => {
|
|
341
385
|
provider();
|
|
342
386
|
gate.policy("ops", (ctx) => ctx.operator.id !== null);
|
|
343
|
-
const decl = ai.decision("triage", {
|
|
387
|
+
const decl = ai.decision("triage", {
|
|
388
|
+
decider: jev(),
|
|
389
|
+
otherwise: "ops",
|
|
390
|
+
ask: { team: choice() },
|
|
391
|
+
});
|
|
344
392
|
const store = createMemoryJournalStore();
|
|
345
393
|
const journal = createJournal({ store, now: () => 1_000_000 });
|
|
346
394
|
const session = await journal.start("run", {});
|
|
@@ -413,11 +461,12 @@ describe("fx.decide", () => {
|
|
|
413
461
|
}));
|
|
414
462
|
const question = choice();
|
|
415
463
|
const decl = ai.decision("triage", {
|
|
416
|
-
|
|
464
|
+
decider: jev(),
|
|
465
|
+
otherwise: "abstain",
|
|
417
466
|
autonomy: { maxError: 0.05, audit: 0 },
|
|
418
467
|
ask: { team: question },
|
|
419
468
|
});
|
|
420
|
-
|
|
469
|
+
setLock({
|
|
421
470
|
decisions: {
|
|
422
471
|
triage: {
|
|
423
472
|
model: "typesafe/jev-1.13.0",
|
|
@@ -456,11 +505,12 @@ describe("fx.decide", () => {
|
|
|
456
505
|
const team = choice();
|
|
457
506
|
const urgent = ai.boolean("urgent?");
|
|
458
507
|
const decl = ai.decision("triage", {
|
|
459
|
-
|
|
508
|
+
decider: jev(),
|
|
509
|
+
otherwise: "abstain",
|
|
460
510
|
autonomy: { maxError: 0.05, audit: 0 },
|
|
461
511
|
ask: { team, urgent },
|
|
462
512
|
});
|
|
463
|
-
|
|
513
|
+
setLock({
|
|
464
514
|
decisions: {
|
|
465
515
|
triage: {
|
|
466
516
|
model: "typesafe/jev-1.13.0",
|
|
@@ -498,7 +548,11 @@ describe("fx.decide", () => {
|
|
|
498
548
|
test("a reviewer may resolve a choice as none_of_these", async () => {
|
|
499
549
|
provider();
|
|
500
550
|
gate.policy("ops", () => true);
|
|
501
|
-
const decl = ai.decision("triage", {
|
|
551
|
+
const decl = ai.decision("triage", {
|
|
552
|
+
decider: jev(),
|
|
553
|
+
otherwise: "ops",
|
|
554
|
+
ask: { team: choice() },
|
|
555
|
+
});
|
|
502
556
|
const store = await createPostgresJournalStore({ sql: createPostgresJournalFake() });
|
|
503
557
|
const journal = createJournal({ store, now: () => 1 });
|
|
504
558
|
const session = await journal.start("run", {});
|
|
@@ -553,11 +607,12 @@ describe("fx.decide", () => {
|
|
|
553
607
|
}));
|
|
554
608
|
const question = ai.boolean("urgent?");
|
|
555
609
|
const decl = ai.decision("triage", {
|
|
556
|
-
|
|
610
|
+
decider: jev(),
|
|
611
|
+
otherwise: "abstain",
|
|
557
612
|
autonomy: { maxError: 0.05, audit: 0 },
|
|
558
613
|
ask: { urgent: question },
|
|
559
614
|
});
|
|
560
|
-
|
|
615
|
+
setLock({
|
|
561
616
|
decisions: {
|
|
562
617
|
triage: {
|
|
563
618
|
model: "typesafe/jev-1.13.0",
|
|
@@ -581,7 +636,11 @@ describe("fx.decide", () => {
|
|
|
581
636
|
test("resolve checks the gate, the tenant, and every open question", async () => {
|
|
582
637
|
provider();
|
|
583
638
|
gate.policy("ops", (ctx) => ctx.operator.id === "reviewer");
|
|
584
|
-
const decl = ai.decision("triage", {
|
|
639
|
+
const decl = ai.decision("triage", {
|
|
640
|
+
decider: jev(),
|
|
641
|
+
otherwise: "ops",
|
|
642
|
+
ask: { team: choice() },
|
|
643
|
+
});
|
|
585
644
|
const store = await createPostgresJournalStore({ sql: createPostgresJournalFake() });
|
|
586
645
|
const session = await createJournal({ store, now: () => 1 }).start("run", {});
|
|
587
646
|
const fx = createFx({
|
|
@@ -636,7 +695,11 @@ describe("fx.decide", () => {
|
|
|
636
695
|
|
|
637
696
|
test("pending review stamps the tenant", async () => {
|
|
638
697
|
provider();
|
|
639
|
-
const decl = ai.decision("triage", {
|
|
698
|
+
const decl = ai.decision("triage", {
|
|
699
|
+
decider: jev(),
|
|
700
|
+
otherwise: "ops",
|
|
701
|
+
ask: { team: choice() },
|
|
702
|
+
});
|
|
640
703
|
const store = createMemoryJournalStore();
|
|
641
704
|
const session = await createJournal({ store, now: () => 1 }).start("run", {});
|
|
642
705
|
const fx = createFx({
|
|
@@ -683,12 +746,12 @@ describe("fx.decide", () => {
|
|
|
683
746
|
try {
|
|
684
747
|
const question = choice();
|
|
685
748
|
const decl = ai.decision("triage", {
|
|
686
|
-
|
|
687
|
-
|
|
749
|
+
decider: jev(),
|
|
750
|
+
otherwise: "abstain",
|
|
688
751
|
autonomy: { maxError: 0.05, audit: 0 },
|
|
689
752
|
ask: { team: question },
|
|
690
753
|
});
|
|
691
|
-
|
|
754
|
+
setLock({
|
|
692
755
|
decisions: {
|
|
693
756
|
triage: {
|
|
694
757
|
model: "typesafe/jev-1.13.0",
|
|
@@ -743,11 +806,12 @@ describe("fx.decide", () => {
|
|
|
743
806
|
const team = choice();
|
|
744
807
|
const urgent = ai.boolean("urgent");
|
|
745
808
|
const decl = ai.decision("triage", {
|
|
746
|
-
|
|
809
|
+
decider: jev(),
|
|
810
|
+
otherwise: "abstain",
|
|
747
811
|
autonomy: { maxError: 0.05, audit: 1 },
|
|
748
812
|
ask: { team, urgent },
|
|
749
813
|
});
|
|
750
|
-
|
|
814
|
+
setLock({
|
|
751
815
|
decisions: {
|
|
752
816
|
triage: {
|
|
753
817
|
model: "typesafe/jev-1.13.0",
|
|
@@ -824,7 +888,11 @@ describe("fx.decide", () => {
|
|
|
824
888
|
test("an operator resolves another tenant and a tenant reviewer cannot", async () => {
|
|
825
889
|
provider();
|
|
826
890
|
gate.policy("ops", (ctx) => ctx.auth.userId === "ada" && ctx.operator.id === "reviewer");
|
|
827
|
-
const decl = ai.decision("triage", {
|
|
891
|
+
const decl = ai.decision("triage", {
|
|
892
|
+
decider: jev(),
|
|
893
|
+
otherwise: "ops",
|
|
894
|
+
ask: { team: choice() },
|
|
895
|
+
});
|
|
828
896
|
const store = await createPostgresJournalStore({ sql: createPostgresJournalFake() });
|
|
829
897
|
const session = await createJournal({ store, now: () => 1 }).start("run", {});
|
|
830
898
|
const fx = createFx({
|
|
@@ -872,8 +940,12 @@ describe("fx.decide", () => {
|
|
|
872
940
|
throw new DecisionOutageError("down");
|
|
873
941
|
});
|
|
874
942
|
const question = choice();
|
|
875
|
-
const decl = ai.decision("triage", {
|
|
876
|
-
|
|
943
|
+
const decl = ai.decision("triage", {
|
|
944
|
+
decider: jev(),
|
|
945
|
+
otherwise: "ops",
|
|
946
|
+
ask: { team: question },
|
|
947
|
+
});
|
|
948
|
+
setLock({
|
|
877
949
|
decisions: {
|
|
878
950
|
triage: {
|
|
879
951
|
model: "typesafe/jev-1.13.0",
|
|
@@ -908,9 +980,9 @@ describe("fx.decide", () => {
|
|
|
908
980
|
(entry) => entry.kind === "step" && entry.name.startsWith("ai-decision:"),
|
|
909
981
|
);
|
|
910
982
|
expect(
|
|
911
|
-
(step && step.kind === "step" ? step.value : undefined) as {
|
|
983
|
+
(step && step.kind === "step" ? step.value : undefined) as { why?: string },
|
|
912
984
|
).toMatchObject({
|
|
913
|
-
|
|
985
|
+
why: "outage",
|
|
914
986
|
});
|
|
915
987
|
});
|
|
916
988
|
|
|
@@ -930,7 +1002,8 @@ describe("fx.decide", () => {
|
|
|
930
1002
|
usage: {},
|
|
931
1003
|
}));
|
|
932
1004
|
const decl = ai.decision("severity", {
|
|
933
|
-
|
|
1005
|
+
decider: jev(),
|
|
1006
|
+
otherwise: "abstain",
|
|
934
1007
|
ask: { severity: ai.score("How severe is this?", ["low", "medium", "high"]) },
|
|
935
1008
|
});
|
|
936
1009
|
const fx = createFx({ flow: "run", effects: { decides: ["severity"] } });
|
|
@@ -942,7 +1015,11 @@ describe("fx.decide", () => {
|
|
|
942
1015
|
});
|
|
943
1016
|
|
|
944
1017
|
test("a missing secret is a config error", async () => {
|
|
945
|
-
const decl = ai.decision("triage", {
|
|
1018
|
+
const decl = ai.decision("triage", {
|
|
1019
|
+
decider: jev(),
|
|
1020
|
+
otherwise: "abstain",
|
|
1021
|
+
ask: { team: choice() },
|
|
1022
|
+
});
|
|
946
1023
|
const fx = createFx({
|
|
947
1024
|
flow: "run",
|
|
948
1025
|
effects: { decides: ["triage"], secrets: ["OPENROUTER_API_KEY"] },
|