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
|
@@ -20,7 +20,7 @@ describe("decision lockfile watch", () => {
|
|
|
20
20
|
const path = join(root, DECISION_LOCK_FILENAME);
|
|
21
21
|
await Bun.write(
|
|
22
22
|
path,
|
|
23
|
-
`${JSON.stringify({ decisions: { triage: { model: "m", questions: {} } } })}\n`,
|
|
23
|
+
`${JSON.stringify({ version: 2, decisions: { triage: { deciders: { jev: { model: "m", pinned: true, questions: {} } } } } })}\n`,
|
|
24
24
|
);
|
|
25
25
|
let listener: ((event: "rename" | "change", filename: string | null) => void) | undefined;
|
|
26
26
|
const watch = watchDecisionLockfile(root, (_path, _options, next) => {
|
|
@@ -29,15 +29,15 @@ describe("decision lockfile watch", () => {
|
|
|
29
29
|
});
|
|
30
30
|
listener?.("change", DECISION_LOCK_FILENAME);
|
|
31
31
|
await watch.flushed();
|
|
32
|
-
expect(getDecisionLock()?.decisions.triage?.model).toBe("m");
|
|
32
|
+
expect(getDecisionLock()?.decisions.triage?.deciders.jev?.model).toBe("m");
|
|
33
33
|
|
|
34
34
|
await Bun.write(
|
|
35
35
|
path,
|
|
36
|
-
`${JSON.stringify({ decisions: { route: { model: "n", questions: {} } } })}\n`,
|
|
36
|
+
`${JSON.stringify({ version: 2, decisions: { route: { deciders: { jev: { model: "n", pinned: true, questions: {} } } } } })}\n`,
|
|
37
37
|
);
|
|
38
38
|
listener?.("change", DECISION_LOCK_FILENAME);
|
|
39
39
|
await watch.flushed();
|
|
40
|
-
expect(getDecisionLock()?.decisions.route?.model).toBe("n");
|
|
40
|
+
expect(getDecisionLock()?.decisions.route?.deciders.jev?.model).toBe("n");
|
|
41
41
|
expect(getDecisionLock()?.decisions.triage).toBeUndefined();
|
|
42
42
|
watch.close();
|
|
43
43
|
resetDecisionCertificates();
|
package/src/cli/eval.ts
CHANGED
|
@@ -97,23 +97,19 @@ export async function runOkeEval(options: OkeEvalOptions = {}): Promise<number>
|
|
|
97
97
|
*/
|
|
98
98
|
export async function evalCli(args: string[]): Promise<number> {
|
|
99
99
|
let manifestPath: string | undefined;
|
|
100
|
-
let certify = false;
|
|
101
100
|
for (let i = 0; i < args.length; i++) {
|
|
102
101
|
const a = args[i]!;
|
|
103
102
|
if (a === "--manifest" || a === "-m") {
|
|
104
103
|
manifestPath = args[++i];
|
|
105
|
-
} else if (a === "--certify") {
|
|
106
|
-
certify = true;
|
|
107
104
|
} else if (a === "--help" || a === "-h") {
|
|
108
|
-
console.log(`oke eval [--manifest oke.manifest.json]
|
|
105
|
+
console.log(`oke eval [--manifest oke.manifest.json]
|
|
109
106
|
|
|
110
107
|
Run prompt eval sets declared in the Manifest. Fails CI on regression.
|
|
111
|
-
|
|
108
|
+
Decision certificates are \`oke decide certify\`.
|
|
112
109
|
`);
|
|
113
110
|
return 0;
|
|
114
111
|
}
|
|
115
112
|
}
|
|
116
|
-
if (certify) return runOkeCertify({ manifestPath });
|
|
117
113
|
return runOkeEval({ manifestPath });
|
|
118
114
|
}
|
|
119
115
|
|
|
@@ -131,7 +127,7 @@ export async function runOkeCertify(
|
|
|
131
127
|
} = {},
|
|
132
128
|
): Promise<number> {
|
|
133
129
|
const { certifySeed } = await import("../elements/ai/decisions/certify.ts");
|
|
134
|
-
const { aiDecisionRegistry } = await import("../kernel/element-registries.ts");
|
|
130
|
+
const { aiDeciderRegistry, aiDecisionRegistry } = await import("../kernel/element-registries.ts");
|
|
135
131
|
const { DECISION_LOCK_FILENAME, parseDecisionLockfile } =
|
|
136
132
|
await import("../elements/ai/decisions/certificate.ts");
|
|
137
133
|
const { resolveStartEntry } = await import("./start.ts");
|
|
@@ -168,28 +164,39 @@ export async function runOkeCertify(
|
|
|
168
164
|
const current = (await existingFile.exists())
|
|
169
165
|
? parseDecisionLockfile(await existingFile.json())
|
|
170
166
|
: undefined;
|
|
171
|
-
const next
|
|
167
|
+
const next: {
|
|
168
|
+
version: 2;
|
|
169
|
+
decisions: Record<string, { deciders: Record<string, unknown> }>;
|
|
170
|
+
} = { version: 2, decisions: { ...(current?.decisions ?? {}) } };
|
|
172
171
|
for (const name of names) {
|
|
173
172
|
const decision = decisions[name];
|
|
174
173
|
if (!decision?.evals) {
|
|
175
|
-
process.stdout.write(`oke
|
|
174
|
+
process.stdout.write(`oke decide certify: skip ${name} (no evals)\n`);
|
|
176
175
|
continue;
|
|
177
176
|
}
|
|
178
177
|
const decl = aiDecisionRegistry.find((item) => item.name === name);
|
|
179
178
|
if (!decl) {
|
|
180
|
-
console.error(`oke
|
|
179
|
+
console.error(`oke decide certify: decision "${name}" is not loaded`);
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
const decider = aiDeciderRegistry.find((item) => item.name === decl.decider);
|
|
183
|
+
if (!decider) {
|
|
184
|
+
console.error(`oke decide certify: decider "${decl.decider}" is not loaded`);
|
|
181
185
|
return 1;
|
|
182
186
|
}
|
|
183
187
|
const text = await Bun.file(resolve(root, decision.evals)).text();
|
|
184
|
-
const evaluate = injected ?? (await
|
|
185
|
-
|
|
186
|
-
model:
|
|
188
|
+
const evaluate = injected ?? (await providerForDecider(decl, decider));
|
|
189
|
+
const result = await certifySeed({
|
|
190
|
+
model: decider.model,
|
|
191
|
+
pinned: decider.pinning !== "alias",
|
|
187
192
|
maxError: decision.autonomy?.maxError ?? 0.05,
|
|
188
193
|
delta: decision.autonomy?.risk ?? decl.autonomy?.risk ?? 0.1,
|
|
189
194
|
jsonl: text,
|
|
190
195
|
ask: decl.ask,
|
|
191
196
|
evaluate,
|
|
192
197
|
});
|
|
198
|
+
const prev = next.decisions[name]?.deciders ?? {};
|
|
199
|
+
next.decisions[name] = { deciders: { ...prev, [decider.name]: result.cert } };
|
|
193
200
|
}
|
|
194
201
|
await Bun.write(lockPath, `${JSON.stringify(next, null, 2)}\n`);
|
|
195
202
|
const { setDecisionDrift } = await import("../elements/ai/decisions/certificate.ts");
|
|
@@ -201,53 +208,25 @@ export async function runOkeCertify(
|
|
|
201
208
|
return 0;
|
|
202
209
|
}
|
|
203
210
|
|
|
204
|
-
async function
|
|
211
|
+
async function providerForDecider(
|
|
205
212
|
decl: import("../elements/ai/declare.ts").AiDecisionDecl,
|
|
213
|
+
decider: import("../elements/ai/declare.ts").AiDeciderDecl,
|
|
206
214
|
): Promise<(input: unknown) => Promise<DecisionResponse>> {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
await
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
model,
|
|
222
|
-
state: input,
|
|
223
|
-
questions: wireFromDecl(decl),
|
|
215
|
+
const apiKey = process.env[decider.secret];
|
|
216
|
+
if (!apiKey) throw new Error(`oke decide certify: secret "${decider.secret}" is not configured`);
|
|
217
|
+
return async (input) => {
|
|
218
|
+
const { decisionHttp } = await import("../elements/ai/decisions/http.ts");
|
|
219
|
+
const { encodeDecisionRequest, normalizeDecisionResponse, wireQuestions } =
|
|
220
|
+
await import("../elements/ai/decisions/codec.ts");
|
|
221
|
+
const request = { model: decider.model, state: input, questions: wireQuestions(decl.ask) };
|
|
222
|
+
const raw = await decisionHttp({
|
|
223
|
+
url: decider.baseUrl,
|
|
224
|
+
apiKey,
|
|
225
|
+
request,
|
|
226
|
+
body: encodeDecisionRequest(decider.protocol, request),
|
|
227
|
+
timeoutMs: 30_000,
|
|
228
|
+
breakerKey: decider.name,
|
|
224
229
|
});
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
function wireFromDecl(
|
|
228
|
-
decl: import("../elements/ai/declare.ts").AiDecisionDecl,
|
|
229
|
-
): import("../elements/ai/decisions/provider.ts").DecisionRequest["questions"] {
|
|
230
|
-
const questions: Record<string, import("../elements/ai/decisions/provider.ts").WireQuestion> = {};
|
|
231
|
-
for (const [id, question] of Object.entries(decl.ask)) {
|
|
232
|
-
if (question.kind === "choice") {
|
|
233
|
-
questions[id] = {
|
|
234
|
-
type: "choice",
|
|
235
|
-
instructions: question.instructions,
|
|
236
|
-
criteria: { ...question.options, none_of_these: null },
|
|
237
|
-
};
|
|
238
|
-
} else if (question.kind === "score") {
|
|
239
|
-
questions[id] = {
|
|
240
|
-
type: "score",
|
|
241
|
-
instructions: question.instructions,
|
|
242
|
-
criteria: question.levels,
|
|
243
|
-
};
|
|
244
|
-
} else {
|
|
245
|
-
questions[id] = {
|
|
246
|
-
type: "noul",
|
|
247
|
-
instructions: question.instructions,
|
|
248
|
-
...(question.criteria !== undefined ? { criteria: question.criteria } : {}),
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return questions;
|
|
230
|
+
return normalizeDecisionResponse(decider.protocol, raw);
|
|
231
|
+
};
|
|
253
232
|
}
|
|
@@ -79,7 +79,7 @@ export function mcpContextFromConsole(state: McpConsoleSurface): McpContext {
|
|
|
79
79
|
if (row.status !== "pending" || row.labelOnly) continue;
|
|
80
80
|
pending[row.decision] = (pending[row.decision] ?? 0) + 1;
|
|
81
81
|
}
|
|
82
|
-
return projectMcpDecisions(state.manifest, pending, fitted);
|
|
82
|
+
return projectMcpDecisions(state.manifest, pending, fitted, queue);
|
|
83
83
|
},
|
|
84
84
|
proposeStructural: async (input) =>
|
|
85
85
|
emitStructuralDiff({
|
package/src/cli/registry.ts
CHANGED
|
@@ -550,15 +550,7 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
|
|
|
550
550
|
name: "eval",
|
|
551
551
|
summary: "prompt eval sets (CI gate)",
|
|
552
552
|
leaf: true,
|
|
553
|
-
flags: [
|
|
554
|
-
MANIFEST,
|
|
555
|
-
{
|
|
556
|
-
long: "--certify",
|
|
557
|
-
takesValue: false,
|
|
558
|
-
summary: "Build a decision certificate from the seed file",
|
|
559
|
-
},
|
|
560
|
-
HELP,
|
|
561
|
-
],
|
|
553
|
+
flags: [MANIFEST, HELP],
|
|
562
554
|
},
|
|
563
555
|
{
|
|
564
556
|
name: "decide",
|
|
@@ -584,6 +576,50 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
|
|
|
584
576
|
HELP,
|
|
585
577
|
],
|
|
586
578
|
},
|
|
579
|
+
{
|
|
580
|
+
name: "labels",
|
|
581
|
+
summary: "Export reviewed labels as seed JSONL",
|
|
582
|
+
positionals: "<name>",
|
|
583
|
+
flags: [
|
|
584
|
+
{
|
|
585
|
+
long: "--export",
|
|
586
|
+
takesValue: false,
|
|
587
|
+
summary: "Write the JSONL file",
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
long: "--out",
|
|
591
|
+
takesValue: true,
|
|
592
|
+
valueName: "file",
|
|
593
|
+
summary: "Output path",
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
long: "--origin",
|
|
597
|
+
takesValue: true,
|
|
598
|
+
valueName: "url",
|
|
599
|
+
summary: "App origin",
|
|
600
|
+
},
|
|
601
|
+
HELP,
|
|
602
|
+
],
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
name: "certify",
|
|
606
|
+
summary: "Write one certificate per decider",
|
|
607
|
+
positionals: "<name>",
|
|
608
|
+
flags: [
|
|
609
|
+
{
|
|
610
|
+
long: "--deciders",
|
|
611
|
+
takesValue: true,
|
|
612
|
+
valueName: "a,b",
|
|
613
|
+
summary: "Decider names",
|
|
614
|
+
},
|
|
615
|
+
HELP,
|
|
616
|
+
],
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
name: "models",
|
|
620
|
+
summary: "List decision models",
|
|
621
|
+
flags: [HELP],
|
|
622
|
+
},
|
|
587
623
|
],
|
|
588
624
|
},
|
|
589
625
|
{
|
|
@@ -1,27 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Decision extraction:
|
|
2
|
+
* Decision extraction: decider, otherwise, and capability limits.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
6
|
import { extractFromSources } from "./extract.ts";
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const decider = `const jev = ai.decider("jev", { provider: "openrouter", model: "typesafe/jev-1.13" });\n`;
|
|
9
9
|
|
|
10
10
|
describe("ai.decision extract", () => {
|
|
11
|
-
test("
|
|
11
|
+
test("a decision without otherwise fails to compile", async () => {
|
|
12
12
|
await expect(
|
|
13
13
|
extractFromSources({
|
|
14
|
-
"src/flows/run.ts": `${
|
|
14
|
+
"src/flows/run.ts": `${decider}ai.decision("triage", {\n decider: jev,\n ask: { team: ai.choice("which", { a: "A" }) },\n});\n`,
|
|
15
15
|
}),
|
|
16
|
-
).rejects.toThrow(/
|
|
16
|
+
).rejects.toThrow(/otherwise is required/);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
test("
|
|
19
|
+
test("shadow fails to compile as planned", async () => {
|
|
20
|
+
await expect(
|
|
21
|
+
extractFromSources({
|
|
22
|
+
"src/flows/run.ts": `${decider}ai.decision("triage", {\n decider: jev,\n otherwise: "abstain",\n shadow: "other",\n ask: { team: ai.choice("which", { a: "A" }) },\n});\n`,
|
|
23
|
+
}),
|
|
24
|
+
).rejects.toThrow(/planned/);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("an HTTP trigger that parks fails to compile", async () => {
|
|
20
28
|
await expect(
|
|
21
29
|
extractFromSources({
|
|
22
30
|
"src/flows/run.ts": `
|
|
31
|
+
const jev = ai.decider("jev", { provider: "openrouter", model: "m" });
|
|
23
32
|
const triage = ai.decision("triage", {
|
|
24
|
-
|
|
33
|
+
decider: jev,
|
|
34
|
+
otherwise: "ops",
|
|
25
35
|
ask: { team: ai.choice("which", { a: "A" }) },
|
|
26
36
|
});
|
|
27
37
|
on(http.post("/t"), flow("run", {
|
|
@@ -33,38 +43,52 @@ describe("ai.decision extract", () => {
|
|
|
33
43
|
).rejects.toThrow(/fx\.emit/);
|
|
34
44
|
});
|
|
35
45
|
|
|
36
|
-
test("
|
|
37
|
-
const many = Array.from({ length: 255 }, (_, i) => `o${i}: "x"`).join(", ");
|
|
46
|
+
test("a custom decider's max choices fails at compile time", async () => {
|
|
38
47
|
await expect(
|
|
39
48
|
extractFromSources({
|
|
40
|
-
"src/flows/run.ts":
|
|
49
|
+
"src/flows/run.ts": `
|
|
50
|
+
const host = ai.decider("host", {
|
|
51
|
+
driverId: "systemone",
|
|
52
|
+
baseUrl: "https://example.test/decisions",
|
|
53
|
+
model: "m",
|
|
54
|
+
secret: "HOST_KEY",
|
|
55
|
+
capabilities: { boolean: true, choice: true, score: true, refusal: false, maxChoices: 1 },
|
|
56
|
+
});
|
|
57
|
+
ai.decision("triage", {
|
|
58
|
+
decider: host,
|
|
59
|
+
otherwise: "abstain",
|
|
60
|
+
ask: { team: ai.choice("which", { a: "A", b: "B" }) },
|
|
61
|
+
});
|
|
62
|
+
`,
|
|
41
63
|
}),
|
|
42
|
-
).rejects.toThrow(/
|
|
64
|
+
).rejects.toThrow(/allows 1/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("author none_of_these fails to compile", async () => {
|
|
43
68
|
await expect(
|
|
44
69
|
extractFromSources({
|
|
45
|
-
"src/flows/run.ts": `${
|
|
70
|
+
"src/flows/run.ts": `${decider}ai.decision("triage", {\n decider: jev,\n otherwise: "abstain",\n ask: { team: ai.choice("which", { none_of_these: null }) },\n});\n`,
|
|
46
71
|
}),
|
|
47
72
|
).rejects.toThrow(/none_of_these/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("duplicate names fail to compile", async () => {
|
|
48
76
|
await expect(
|
|
49
77
|
extractFromSources({
|
|
50
|
-
"src/flows/
|
|
51
|
-
|
|
52
|
-
).rejects.toThrow(/2–10 levels/);
|
|
53
|
-
await expect(
|
|
54
|
-
extractFromSources({
|
|
55
|
-
"src/flows/a.ts": `${header} onUncertain: "abstain",\n ask: { team: ai.choice("which", { a: "A", b: "B" }) },\n});\n`,
|
|
56
|
-
"src/flows/b.ts": `${header} onUncertain: "abstain",\n ask: { team: ai.choice("which", { a: "A", b: "B" }) },\n});\n`,
|
|
78
|
+
"src/flows/a.ts": `${decider}ai.decision("triage", {\n decider: jev,\n otherwise: "abstain",\n ask: { team: ai.choice("which", { a: "A", b: "B" }) },\n});\n`,
|
|
79
|
+
"src/flows/b.ts": `${decider}ai.decision("triage", {\n decider: jev,\n otherwise: "abstain",\n ask: { team: ai.choice("which", { a: "A", b: "B" }) },\n});\n`,
|
|
57
80
|
}),
|
|
58
|
-
).rejects.toThrow(/duplicate
|
|
81
|
+
).rejects.toThrow(/duplicate/);
|
|
59
82
|
});
|
|
60
83
|
|
|
61
|
-
test("
|
|
84
|
+
test("otherwise resolves a gate and a durable flow parks", async () => {
|
|
62
85
|
const manifest = await extractFromSources({
|
|
63
86
|
"src/flows/run.ts": `
|
|
64
|
-
const
|
|
87
|
+
const jev = ai.decider("jev", { provider: "openrouter", model: "m" });
|
|
65
88
|
const triage = ai.decision("triage", {
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
decider: jev,
|
|
90
|
+
otherwise: "ops",
|
|
91
|
+
ask: { team: ai.choice("which", { a: "A" }) },
|
|
68
92
|
});
|
|
69
93
|
on(signal.once("job"), flow("run", {
|
|
70
94
|
durable: true,
|
|
@@ -72,15 +96,20 @@ describe("ai.decision extract", () => {
|
|
|
72
96
|
}));
|
|
73
97
|
`,
|
|
74
98
|
});
|
|
99
|
+
expect(manifest.ai?.decisions?.triage?.otherwise).toBe("ops");
|
|
75
100
|
expect(manifest.ai?.decisions?.triage?.review).toBe("ops");
|
|
101
|
+
expect(manifest.flows?.run?.effects?.secrets).toEqual(["OPENROUTER_API_KEY"]);
|
|
102
|
+
expect(manifest.ai?.deciders?.jev?.pinning).toBe("dated");
|
|
76
103
|
});
|
|
77
104
|
|
|
78
|
-
test("
|
|
105
|
+
test("otherwise resolves a variable to the gate's declared name", async () => {
|
|
79
106
|
const manifest = await extractFromSources({
|
|
80
107
|
"src/flows/run.ts": `
|
|
108
|
+
const jev = ai.decider("jev", { provider: "openrouter", model: "m" });
|
|
81
109
|
const someVar = gate.policy("ops", () => true);
|
|
82
110
|
const triage = ai.decision("triage", {
|
|
83
|
-
|
|
111
|
+
decider: jev,
|
|
112
|
+
otherwise: someVar,
|
|
84
113
|
ask: { team: ai.choice("which", { a: "A", b: "B" }) },
|
|
85
114
|
});
|
|
86
115
|
on(signal.once("job"), flow("run", {
|
|
@@ -95,8 +124,10 @@ describe("ai.decision extract", () => {
|
|
|
95
124
|
test("autonomy and evals survive extraction", async () => {
|
|
96
125
|
const manifest = await extractFromSources({
|
|
97
126
|
"src/flows/run.ts": `
|
|
127
|
+
const jev = ai.decider("jev", { provider: "openrouter", model: "m", region: "eu" });
|
|
98
128
|
const triage = ai.decision("triage", {
|
|
99
|
-
|
|
129
|
+
decider: jev,
|
|
130
|
+
otherwise: "abstain",
|
|
100
131
|
evals: "evals/triage.jsonl",
|
|
101
132
|
autonomy: { maxError: 0.05, audit: 0.1, risk: 0.1 },
|
|
102
133
|
ask: { team: ai.choice("which", { a: "A", b: "B" }) },
|
|
@@ -109,15 +140,19 @@ describe("ai.decision extract", () => {
|
|
|
109
140
|
audit: 0.1,
|
|
110
141
|
risk: 0.1,
|
|
111
142
|
});
|
|
143
|
+
expect(manifest.ai?.deciders?.jev?.region).toBe("eu");
|
|
144
|
+
expect(manifest.ai?.deciders?.jev?.regionStatus).toBe("declared");
|
|
112
145
|
});
|
|
113
146
|
|
|
114
147
|
test("choice options passed through a variable fail to compile", async () => {
|
|
115
148
|
await expect(
|
|
116
149
|
extractFromSources({
|
|
117
150
|
"src/flows/run.ts": `
|
|
151
|
+
const jev = ai.decider("jev", { provider: "openrouter", model: "m" });
|
|
118
152
|
const options = { a: "A", b: "B" };
|
|
119
153
|
ai.decision("triage", {
|
|
120
|
-
|
|
154
|
+
decider: jev,
|
|
155
|
+
otherwise: "abstain",
|
|
121
156
|
ask: { team: ai.choice("which", options) },
|
|
122
157
|
});
|
|
123
158
|
`,
|
|
@@ -128,10 +163,12 @@ describe("ai.decision extract", () => {
|
|
|
128
163
|
test("score levels and a whole question resolve a same-file const", async () => {
|
|
129
164
|
const manifest = await extractFromSources({
|
|
130
165
|
"src/flows/run.ts": `
|
|
166
|
+
const jev = ai.decider("jev", { provider: "openrouter", model: "m" });
|
|
131
167
|
const levels = ["low", "high"];
|
|
132
168
|
const team = ai.choice("which", { a: "A", b: "B" });
|
|
133
169
|
ai.decision("triage", {
|
|
134
|
-
|
|
170
|
+
decider: jev,
|
|
171
|
+
otherwise: "abstain",
|
|
135
172
|
ask: { team, rank: ai.score("rank", levels) },
|
|
136
173
|
});
|
|
137
174
|
`,
|
|
@@ -143,8 +180,10 @@ describe("ai.decision extract", () => {
|
|
|
143
180
|
await expect(
|
|
144
181
|
extractFromSources({
|
|
145
182
|
"src/flows/run.ts": `
|
|
183
|
+
const jev = ai.decider("jev", { provider: "openrouter", model: "m" });
|
|
146
184
|
ai.decision("triage", {
|
|
147
|
-
|
|
185
|
+
decider: jev,
|
|
186
|
+
otherwise: "abstain",
|
|
148
187
|
ask: { rank: ai.score("rank", missing) },
|
|
149
188
|
});
|
|
150
189
|
`,
|
|
@@ -153,12 +192,28 @@ describe("ai.decision extract", () => {
|
|
|
153
192
|
await expect(
|
|
154
193
|
extractFromSources({
|
|
155
194
|
"src/flows/run.ts": `
|
|
195
|
+
const jev = ai.decider("jev", { provider: "openrouter", model: "m" });
|
|
156
196
|
ai.decision("triage", {
|
|
157
|
-
|
|
197
|
+
decider: jev,
|
|
198
|
+
otherwise: "abstain",
|
|
158
199
|
ask: { team: missingQuestion },
|
|
159
200
|
});
|
|
160
201
|
`,
|
|
161
202
|
}),
|
|
162
203
|
).rejects.toThrow(/not a same-file const/);
|
|
163
204
|
});
|
|
205
|
+
|
|
206
|
+
test("a string passed as decider fails to compile", async () => {
|
|
207
|
+
await expect(
|
|
208
|
+
extractFromSources({
|
|
209
|
+
"src/flows/run.ts": `
|
|
210
|
+
ai.decision("triage", {
|
|
211
|
+
decider: "jev",
|
|
212
|
+
otherwise: "abstain",
|
|
213
|
+
ask: { team: ai.choice("which", { a: "A" }) },
|
|
214
|
+
});
|
|
215
|
+
`,
|
|
216
|
+
}),
|
|
217
|
+
).rejects.toThrow(/not a decider/);
|
|
218
|
+
});
|
|
164
219
|
});
|