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.
Files changed (75) hide show
  1. package/AGENTS.md +5 -1
  2. package/manifest.v1.schema.json +53 -3
  3. package/package.json +2 -1
  4. package/site/content/docs/ai/mcp.mdx +1 -1
  5. package/site/content/docs/ai/skills.mdx +1 -1
  6. package/site/content/docs/elements/ai/decide.mdx +95 -417
  7. package/site/content/docs/elements/ai/deciders.mdx +154 -0
  8. package/site/content/docs/elements/ai/index.mdx +2 -1
  9. package/site/content/docs/elements/ai/meta.json +1 -1
  10. package/site/content/docs/reference/cli.mdx +2 -2
  11. package/site/content/docs/reference/configuration.mdx +2 -1
  12. package/site/content/docs/reference/fx.mdx +11 -9
  13. package/src/cli/agents-md.test.ts +8 -2
  14. package/src/cli/decide-certify.test.ts +166 -0
  15. package/src/cli/decide.ts +170 -3
  16. package/src/cli/decision-lock-watch.test.ts +4 -4
  17. package/src/cli/eval.ts +38 -59
  18. package/src/cli/mcp-from-console.ts +1 -1
  19. package/src/cli/registry.ts +45 -9
  20. package/src/compiler/decisions.extract.test.ts +86 -31
  21. package/src/compiler/effects-infer.ts +1 -0
  22. package/src/compiler/extract.ts +239 -24
  23. package/src/config/index.ts +6 -0
  24. package/src/console/server/decisions.test.ts +6 -1
  25. package/src/console/server/decisions.ts +32 -1
  26. package/src/console/ui-next/dist/assets/{access-page-zBMhZnFm.js → access-page-DX2dKLmq.js} +1 -1
  27. package/src/console/ui-next/dist/assets/{agent-disclosure-CBj9f7Ju.js → agent-disclosure-CiRvA3M9.js} +1 -1
  28. package/src/console/ui-next/dist/assets/{cache-glyph-D3MY49h3.js → cache-glyph-CC1yFJ8A.js} +1 -1
  29. package/src/console/ui-next/dist/assets/{call-pii-button-L9JxdBhp.js → call-pii-button-CLXQgIDD.js} +1 -1
  30. package/src/console/ui-next/dist/assets/{collapsible-BvFaX-Zt.js → collapsible-C0AThBZg.js} +1 -1
  31. package/src/console/ui-next/dist/assets/{decisions-page-DgB76m-X.js → decisions-page-Bf3AhTAa.js} +1 -1
  32. package/src/console/ui-next/dist/assets/{duration-tone-l1DSJ2Vz.js → duration-tone-Bptur2gX.js} +1 -1
  33. package/src/console/ui-next/dist/assets/{flows-page-Bb9t1lu6.js → flows-page-DRZVeTZU.js} +1 -1
  34. package/src/console/ui-next/dist/assets/{highlighted-json-DMvsi3Ti.js → highlighted-json-BKf0PjBK.js} +1 -1
  35. package/src/console/ui-next/dist/assets/{http-method-BwhJBTcQ.js → http-method-uhrnWrGC.js} +1 -1
  36. package/src/console/ui-next/dist/assets/{index-RhV2jT_7.js → index-Duxus_sG.js} +3 -3
  37. package/src/console/ui-next/dist/assets/{observability-page-BRfUILBq.js → observability-page-B-QM8X0O.js} +1 -1
  38. package/src/console/ui-next/dist/assets/{replica-lag-BsceAzIG.js → replica-lag-DuqHCANi.js} +1 -1
  39. package/src/console/ui-next/dist/assets/{request-meta-C6lTyCXp.js → request-meta-BTBZ3Ulc.js} +1 -1
  40. package/src/console/ui-next/dist/assets/{store-page-YmE806bJ.js → store-page-fpo7VxYb.js} +1 -1
  41. package/src/console/ui-next/dist/assets/{trace-detail-sheet-Di4irS49.js → trace-detail-sheet-DKtr1-K1.js} +1 -1
  42. package/src/console/ui-next/dist/assets/{tree-expand-toggle-DYeL6Rye.js → tree-expand-toggle-QeTceKKI.js} +1 -1
  43. package/src/console/ui-next/dist/assets/{units-page-07agasRA.js → units-page-ChCp8mOy.js} +1 -1
  44. package/src/console/ui-next/dist/assets/{vault-page-CHBck4n_.js → vault-page-CEYLXhUX.js} +1 -1
  45. package/src/console/ui-next/dist/index.html +1 -1
  46. package/src/console/ui-next/src/client.ts +8 -0
  47. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +16 -0
  48. package/src/elements/ai/deciders/presets.ts +182 -0
  49. package/src/elements/ai/decisions/bind.ts +6 -5
  50. package/src/elements/ai/decisions/catalog.ts +93 -0
  51. package/src/elements/ai/decisions/certificate.test.ts +47 -19
  52. package/src/elements/ai/decisions/certificate.ts +107 -24
  53. package/src/elements/ai/decisions/certify.ts +104 -9
  54. package/src/elements/ai/decisions/codec.test.ts +142 -0
  55. package/src/elements/ai/decisions/codec.ts +302 -0
  56. package/src/elements/ai/decisions/decide.test.ts +108 -31
  57. package/src/elements/ai/decisions/decider-runtime.test.ts +415 -0
  58. package/src/elements/ai/decisions/decider.types.test.ts +62 -0
  59. package/src/elements/ai/decisions/e2e.test.ts +20 -16
  60. package/src/elements/ai/decisions/http.ts +15 -7
  61. package/src/elements/ai/decisions/labels.test.ts +6 -1
  62. package/src/elements/ai/decisions/labels.ts +4 -3
  63. package/src/elements/ai/decisions/openai.live.test.ts +42 -0
  64. package/src/elements/ai/decisions/openrouter.live.test.ts +5 -1
  65. package/src/elements/ai/decisions/provider.ts +22 -1
  66. package/src/elements/ai/declare.ts +208 -51
  67. package/src/elements/ai.ts +1 -0
  68. package/src/index.ts +1 -0
  69. package/src/kernel/app.ts +8 -0
  70. package/src/kernel/decision-budget-entry.ts +11 -0
  71. package/src/kernel/element-registries.ts +3 -0
  72. package/src/kernel/fx-decide.ts +276 -88
  73. package/src/manifest/types.ts +38 -2
  74. package/src/release/measure.ts +47 -8
  75. 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
- onUncertain: "abstain",
115
+ decider: jev(),
116
+ otherwise: "abstain",
78
117
  autonomy: { maxError: 0.05, audit: 0 },
79
118
  ask: { team: question },
80
119
  });
81
- setDecisionLock({
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
- onUncertain: "abstain",
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
- onUncertain: "abstain",
182
+ decider: jev(),
183
+ otherwise: "abstain",
143
184
  autonomy: { maxError: 0.05, audit: 1 },
144
185
  ask: { team: question },
145
186
  });
146
- setDecisionLock({
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
- review: "ops",
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
- review: "ops",
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
- onUncertain: "abstain",
344
+ decider: jev(),
345
+ otherwise: "abstain",
302
346
  autonomy: { maxError: 0.05, audit: 0 },
303
347
  ask: { team: question },
304
348
  });
305
- setDecisionLock({
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", { review: "ops", ask: { team: choice() } });
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
- onUncertain: "abstain",
464
+ decider: jev(),
465
+ otherwise: "abstain",
417
466
  autonomy: { maxError: 0.05, audit: 0 },
418
467
  ask: { team: question },
419
468
  });
420
- setDecisionLock({
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
- onUncertain: "abstain",
508
+ decider: jev(),
509
+ otherwise: "abstain",
460
510
  autonomy: { maxError: 0.05, audit: 0 },
461
511
  ask: { team, urgent },
462
512
  });
463
- setDecisionLock({
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", { review: "ops", ask: { team: choice() } });
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
- onUncertain: "abstain",
610
+ decider: jev(),
611
+ otherwise: "abstain",
557
612
  autonomy: { maxError: 0.05, audit: 0 },
558
613
  ask: { urgent: question },
559
614
  });
560
- setDecisionLock({
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", { review: "ops", ask: { team: choice() } });
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", { review: "ops", ask: { team: choice() } });
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
- onUncertain: "abstain",
687
- model: "typesafe/jev-1.13",
749
+ decider: jev(),
750
+ otherwise: "abstain",
688
751
  autonomy: { maxError: 0.05, audit: 0 },
689
752
  ask: { team: question },
690
753
  });
691
- setDecisionLock({
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
- onUncertain: "abstain",
809
+ decider: jev(),
810
+ otherwise: "abstain",
747
811
  autonomy: { maxError: 0.05, audit: 1 },
748
812
  ask: { team, urgent },
749
813
  });
750
- setDecisionLock({
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", { review: "ops", ask: { team: choice() } });
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", { review: "ops", ask: { team: question } });
876
- setDecisionLock({
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 { reason?: string },
983
+ (step && step.kind === "step" ? step.value : undefined) as { why?: string },
912
984
  ).toMatchObject({
913
- reason: "outage",
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
- onUncertain: "abstain",
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", { onUncertain: "abstain", ask: { team: choice() } });
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"] },