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,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in OpenAI decisions smoke check.
|
|
3
|
+
*
|
|
4
|
+
* Skipped unless `OPENAI_API_KEY` is set. `bun test` ignores `*.live.test.ts`.
|
|
5
|
+
* The key is never printed, logged, or written.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
9
|
+
import { ai, resetAiDecls } from "../../ai.ts";
|
|
10
|
+
import { createFx } from "../../../kernel/fx.ts";
|
|
11
|
+
import { resetDecisionCertificates } from "./certificate.ts";
|
|
12
|
+
import { resetDecisionProvider } from "../../../kernel/fx-decide.ts";
|
|
13
|
+
|
|
14
|
+
const apiKey = process.env["CI"] === "true" ? undefined : process.env["OPENAI_API_KEY"];
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
resetAiDecls();
|
|
18
|
+
resetDecisionCertificates();
|
|
19
|
+
resetDecisionProvider();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe.skipIf(!apiKey)("openai decisions live", () => {
|
|
23
|
+
test("fx.decide posts to /v1/decisions", async () => {
|
|
24
|
+
const decl = ai.decision("triage-openai", {
|
|
25
|
+
decider: ai.decider("luna", { provider: "openai", model: "gpt-6-luna" }),
|
|
26
|
+
otherwise: "abstain",
|
|
27
|
+
ask: {
|
|
28
|
+
urgent: ai.boolean("Does this need a person today?"),
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
const fx = createFx({
|
|
32
|
+
flow: "triage.openai",
|
|
33
|
+
effects: { decides: ["triage-openai"], secrets: ["OPENAI_API_KEY"] },
|
|
34
|
+
secrets: { OPENAI_API_KEY: apiKey ?? "" },
|
|
35
|
+
});
|
|
36
|
+
const result = (await fx.decide(decl, "Checkout is blank after Pay.")) as {
|
|
37
|
+
$: { urgent: { by: string }; meta: { model: string } };
|
|
38
|
+
};
|
|
39
|
+
expect(result.$.urgent.by).toBe("luna");
|
|
40
|
+
expect(result.$.meta.model.length).toBeGreaterThan(0);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -41,7 +41,11 @@ afterEach(() => {
|
|
|
41
41
|
|
|
42
42
|
function decision() {
|
|
43
43
|
return ai.decision("triage-live", {
|
|
44
|
-
|
|
44
|
+
decider: ai.decider("jev", {
|
|
45
|
+
provider: "openrouter",
|
|
46
|
+
model: "typesafe/jev-1.13",
|
|
47
|
+
}),
|
|
48
|
+
otherwise: "abstain",
|
|
45
49
|
ask: {
|
|
46
50
|
team: ai.choice("Which team should own this ticket?", {
|
|
47
51
|
billing: "Payments and refunds",
|
|
@@ -40,7 +40,7 @@ export interface DecisionUsage {
|
|
|
40
40
|
export interface DecisionResponse {
|
|
41
41
|
readonly model: string;
|
|
42
42
|
readonly provider?: string;
|
|
43
|
-
readonly answers: Readonly<Record<string, unknown
|
|
43
|
+
readonly answers: Readonly<Record<string, unknown>> | readonly unknown[];
|
|
44
44
|
readonly usage: DecisionUsage;
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -90,6 +90,27 @@ export class DecisionConfigError extends Error {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/** State is larger than the decider's max context. Not an outage. */
|
|
94
|
+
export class DecisionInputTooLarge extends Error {
|
|
95
|
+
readonly decider: string;
|
|
96
|
+
readonly tokens: number;
|
|
97
|
+
readonly maxContext: number;
|
|
98
|
+
/**
|
|
99
|
+
* @param decider - Decider name
|
|
100
|
+
* @param tokens - Estimated input tokens
|
|
101
|
+
* @param maxContext - Capability limit
|
|
102
|
+
*/
|
|
103
|
+
constructor(decider: string, tokens: number, maxContext: number) {
|
|
104
|
+
super(
|
|
105
|
+
`fx.decide: decider "${decider}" state is ${tokens} tokens; max context is ${maxContext}`,
|
|
106
|
+
);
|
|
107
|
+
this.name = "DecisionInputTooLarge";
|
|
108
|
+
this.decider = decider;
|
|
109
|
+
this.tokens = tokens;
|
|
110
|
+
this.maxContext = maxContext;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
93
114
|
/** The breaker is open. Callers take the non-auto path. */
|
|
94
115
|
export class DecisionOutageError extends Error {
|
|
95
116
|
/**
|
|
@@ -6,12 +6,20 @@
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
aiAgentRegistry,
|
|
9
|
+
aiDeciderRegistry,
|
|
9
10
|
aiDecisionRegistry,
|
|
10
11
|
aiEmbedRegistry,
|
|
11
12
|
aiMcpServerRegistry,
|
|
12
13
|
aiModelRegistry,
|
|
13
14
|
aiPromptRegistry,
|
|
14
15
|
} from "../../kernel/element-registries.ts";
|
|
16
|
+
import {
|
|
17
|
+
DECIDER_PRESETS,
|
|
18
|
+
resolveDecider,
|
|
19
|
+
type DeciderCapabilities,
|
|
20
|
+
type DeciderProtocol,
|
|
21
|
+
type DeciderPinning,
|
|
22
|
+
} from "./deciders/presets.ts";
|
|
15
23
|
import { mcpToolRef, type McpToolRef } from "../../manifest/mcp-ref.ts";
|
|
16
24
|
import type { VaultSecretDecl } from "../vault/declare.ts";
|
|
17
25
|
import { formatAiProviderTier2Warn, resolveAiModelBaseUrl } from "./providers.ts";
|
|
@@ -131,19 +139,78 @@ export interface AiDecisionAutonomy {
|
|
|
131
139
|
readonly risk?: number;
|
|
132
140
|
}
|
|
133
141
|
|
|
134
|
-
/**
|
|
135
|
-
export
|
|
136
|
-
|
|
142
|
+
/** Questions a capability row allows. A `false` flag removes that kind. */
|
|
143
|
+
export type AskForCapabilities<C> = Readonly<
|
|
144
|
+
Record<
|
|
145
|
+
string,
|
|
146
|
+
| (C extends { readonly boolean: true } ? AiBooleanQuestion : never)
|
|
147
|
+
| (C extends { readonly choice: true } ? AiChoiceQuestion : never)
|
|
148
|
+
| (C extends { readonly score: true } ? AiScoreQuestion : never)
|
|
149
|
+
>
|
|
150
|
+
>;
|
|
151
|
+
|
|
152
|
+
/** Options for a preset host. URL, protocol, secret, and capabilities are filled in. */
|
|
153
|
+
export interface AiDeciderPresetOptions {
|
|
154
|
+
readonly provider: "openrouter" | "openai";
|
|
155
|
+
readonly model: string;
|
|
156
|
+
readonly secret?: string;
|
|
157
|
+
readonly region?: string;
|
|
158
|
+
readonly zdr?: boolean;
|
|
159
|
+
readonly timeout?: AiTimeout;
|
|
160
|
+
readonly concurrency?: number;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Options for a host that is not a preset. */
|
|
164
|
+
export interface AiDeciderCustomOptions<C extends DeciderCapabilities = DeciderCapabilities> {
|
|
165
|
+
readonly provider?: string;
|
|
166
|
+
readonly driverId: DeciderProtocol;
|
|
167
|
+
readonly baseUrl: string;
|
|
168
|
+
readonly model: string;
|
|
169
|
+
readonly secret: string;
|
|
170
|
+
readonly capabilities: C;
|
|
171
|
+
readonly region?: string;
|
|
172
|
+
readonly zdr?: boolean;
|
|
173
|
+
readonly timeout?: AiTimeout;
|
|
174
|
+
readonly concurrency?: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Options for {@link ai.decider}. */
|
|
178
|
+
export type AiDeciderOptions<C extends DeciderCapabilities = DeciderCapabilities> =
|
|
179
|
+
| AiDeciderPresetOptions
|
|
180
|
+
| AiDeciderCustomOptions<C>;
|
|
181
|
+
|
|
182
|
+
/** Declared decider. One model, one breaker, one certificate. */
|
|
183
|
+
export interface AiDeciderDecl<C extends DeciderCapabilities = DeciderCapabilities> {
|
|
184
|
+
readonly kind: "decider";
|
|
185
|
+
readonly name: string;
|
|
186
|
+
readonly provider?: string;
|
|
187
|
+
readonly protocol: DeciderProtocol;
|
|
188
|
+
readonly baseUrl: string;
|
|
189
|
+
readonly model: string;
|
|
190
|
+
readonly secret: string;
|
|
191
|
+
readonly pinning: DeciderPinning;
|
|
192
|
+
readonly capabilities: C;
|
|
193
|
+
readonly region?: string;
|
|
194
|
+
readonly regionStatus?: "declared";
|
|
195
|
+
readonly zdr?: boolean;
|
|
196
|
+
readonly zdrStatus?: "declared";
|
|
197
|
+
readonly timeout?: AiTimeout;
|
|
198
|
+
readonly concurrency?: number;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Options for {@link ai.decision}. `decider` and `otherwise` are required. */
|
|
202
|
+
export interface AiDecisionOptions<C extends DeciderCapabilities = DeciderCapabilities> {
|
|
203
|
+
readonly decider: AiDeciderDecl<C>;
|
|
204
|
+
readonly backup?: readonly AiDeciderDecl[];
|
|
205
|
+
/** A gate name parks. `"abstain"` returns null and does not park. */
|
|
206
|
+
readonly otherwise: { readonly name: string } | string;
|
|
137
207
|
readonly in?: unknown;
|
|
138
|
-
readonly ask:
|
|
208
|
+
readonly ask: AskForCapabilities<C>;
|
|
139
209
|
readonly autonomy?: AiDecisionAutonomy;
|
|
140
|
-
readonly review?: { readonly name: string } | string;
|
|
141
|
-
readonly onUncertain?: "abstain";
|
|
142
210
|
readonly locale?: (input: unknown) => string | undefined;
|
|
143
211
|
readonly evals?: string;
|
|
144
|
-
/**
|
|
145
|
-
readonly
|
|
146
|
-
readonly timeout?: AiTimeout;
|
|
212
|
+
/** Reserved. Using it fails with a message that says it is planned. */
|
|
213
|
+
readonly shadow?: never;
|
|
147
214
|
}
|
|
148
215
|
|
|
149
216
|
/** Declared decision handle. */
|
|
@@ -152,12 +219,14 @@ export interface AiDecisionDecl {
|
|
|
152
219
|
readonly name: string;
|
|
153
220
|
readonly ask: Readonly<Record<string, AiDecisionQuestion>>;
|
|
154
221
|
readonly mode: "review" | "abstain";
|
|
222
|
+
/** Gate name when `otherwise` is a gate. */
|
|
155
223
|
readonly review?: string;
|
|
156
|
-
readonly
|
|
157
|
-
readonly
|
|
224
|
+
readonly decider: string;
|
|
225
|
+
readonly backup: readonly string[];
|
|
226
|
+
readonly otherwise: string;
|
|
227
|
+
readonly secrets: readonly string[];
|
|
158
228
|
readonly autonomy?: AiDecisionAutonomy;
|
|
159
229
|
readonly evals?: string;
|
|
160
|
-
readonly timeout?: AiTimeout;
|
|
161
230
|
readonly locale?: (input: unknown) => string | undefined;
|
|
162
231
|
/** Declared `in` schema. Label export keeps only these fields. */
|
|
163
232
|
readonly inputSchema?: unknown;
|
|
@@ -325,24 +394,45 @@ export interface AiNamespace {
|
|
|
325
394
|
*/
|
|
326
395
|
agent(name: string, options?: AiAgentOptions): AiAgentDecl;
|
|
327
396
|
/**
|
|
328
|
-
* Declare
|
|
397
|
+
* Declare one model that can answer a decision.
|
|
398
|
+
*
|
|
399
|
+
* @param name - Decider id
|
|
400
|
+
* @param options - Preset provider, or driverId + baseUrl + capabilities
|
|
401
|
+
*/
|
|
402
|
+
decider(
|
|
403
|
+
name: string,
|
|
404
|
+
options: AiDeciderPresetOptions & { readonly provider: "openrouter" },
|
|
405
|
+
): AiDeciderDecl<(typeof DECIDER_PRESETS)["openrouter"]["capabilities"]>;
|
|
406
|
+
decider(
|
|
407
|
+
name: string,
|
|
408
|
+
options: AiDeciderPresetOptions & { readonly provider: "openai" },
|
|
409
|
+
): AiDeciderDecl<(typeof DECIDER_PRESETS)["openai"]["capabilities"]>;
|
|
410
|
+
decider<const C extends DeciderCapabilities>(
|
|
411
|
+
name: string,
|
|
412
|
+
options: AiDeciderCustomOptions<C>,
|
|
413
|
+
): AiDeciderDecl<C>;
|
|
414
|
+
/**
|
|
415
|
+
* Declare a decision. `decider` and `otherwise` are required.
|
|
329
416
|
*
|
|
330
417
|
* @param name - Decision id
|
|
331
|
-
* @param options -
|
|
418
|
+
* @param options - Decider, fallback, and questions
|
|
332
419
|
*/
|
|
333
|
-
decision
|
|
420
|
+
decision<C extends DeciderCapabilities>(
|
|
421
|
+
name: string,
|
|
422
|
+
options: AiDecisionOptions<C>,
|
|
423
|
+
): AiDecisionDecl;
|
|
334
424
|
/**
|
|
335
|
-
* A choice question. The answer union
|
|
425
|
+
* A choice question. The answer union includes author keys and `none_of_these`.
|
|
336
426
|
*
|
|
337
427
|
* @param instructions - What to decide
|
|
338
|
-
* @param options -
|
|
428
|
+
* @param options - Author options. `none_of_these` is injected.
|
|
339
429
|
*/
|
|
340
430
|
choice(instructions: string, options: Readonly<Record<string, string | null>>): AiChoiceQuestion;
|
|
341
431
|
/**
|
|
342
432
|
* An ordered score question.
|
|
343
433
|
*
|
|
344
434
|
* @param instructions - What to rate
|
|
345
|
-
* @param levels -
|
|
435
|
+
* @param levels - Level descriptions, low to high
|
|
346
436
|
*/
|
|
347
437
|
score(instructions: string, levels: readonly string[]): AiScoreQuestion;
|
|
348
438
|
/**
|
|
@@ -372,6 +462,7 @@ export function listAiDecls(): {
|
|
|
372
462
|
readonly prompts: readonly AiPromptDecl[];
|
|
373
463
|
readonly embeds: readonly AiEmbedDecl[];
|
|
374
464
|
readonly agents: readonly AiAgentDecl[];
|
|
465
|
+
readonly deciders: readonly AiDeciderDecl[];
|
|
375
466
|
readonly decisions: readonly AiDecisionDecl[];
|
|
376
467
|
readonly mcpServers: readonly AiMcpServerDecl[];
|
|
377
468
|
} {
|
|
@@ -380,6 +471,7 @@ export function listAiDecls(): {
|
|
|
380
471
|
prompts: aiPromptRegistry.slice(),
|
|
381
472
|
embeds: aiEmbedRegistry.slice(),
|
|
382
473
|
agents: aiAgentRegistry.slice(),
|
|
474
|
+
deciders: aiDeciderRegistry.slice(),
|
|
383
475
|
decisions: aiDecisionRegistry.slice(),
|
|
384
476
|
mcpServers: aiMcpServerRegistry.slice(),
|
|
385
477
|
};
|
|
@@ -393,6 +485,7 @@ export function resetAiDecls(): void {
|
|
|
393
485
|
aiPromptRegistry.length = 0;
|
|
394
486
|
aiEmbedRegistry.length = 0;
|
|
395
487
|
aiAgentRegistry.length = 0;
|
|
488
|
+
aiDeciderRegistry.length = 0;
|
|
396
489
|
aiDecisionRegistry.length = 0;
|
|
397
490
|
aiMcpServerRegistry.length = 0;
|
|
398
491
|
}
|
|
@@ -400,7 +493,7 @@ export function resetAiDecls(): void {
|
|
|
400
493
|
/**
|
|
401
494
|
* AI element namespace.
|
|
402
495
|
*/
|
|
403
|
-
export const ai
|
|
496
|
+
export const ai = {
|
|
404
497
|
/**
|
|
405
498
|
* Declare a model binding.
|
|
406
499
|
*
|
|
@@ -510,6 +603,12 @@ export const ai: AiNamespace = {
|
|
|
510
603
|
return decl;
|
|
511
604
|
},
|
|
512
605
|
|
|
606
|
+
decider(name: string, options: AiDeciderOptions): AiDeciderDecl {
|
|
607
|
+
const decl = buildDeciderDecl(name, options);
|
|
608
|
+
aiDeciderRegistry.push(decl);
|
|
609
|
+
return decl;
|
|
610
|
+
},
|
|
611
|
+
|
|
513
612
|
decision(name: string, options: AiDecisionOptions): AiDecisionDecl {
|
|
514
613
|
const decl = buildDecisionDecl(name, options);
|
|
515
614
|
aiDecisionRegistry.push(decl);
|
|
@@ -571,7 +670,25 @@ export const ai: AiNamespace = {
|
|
|
571
670
|
aiMcpServerRegistry.push(decl);
|
|
572
671
|
return decl;
|
|
573
672
|
},
|
|
574
|
-
};
|
|
673
|
+
} as AiNamespace;
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Validate a decision and return the registered shape.
|
|
677
|
+
*
|
|
678
|
+
* @param name - Decision id
|
|
679
|
+
* @param options - Author options
|
|
680
|
+
*/
|
|
681
|
+
/**
|
|
682
|
+
* Validate a decider and return the registered shape.
|
|
683
|
+
*
|
|
684
|
+
* @param name - Decider id
|
|
685
|
+
* @param options - Author options
|
|
686
|
+
*/
|
|
687
|
+
export function buildDeciderDecl(name: string, options: AiDeciderOptions): AiDeciderDecl {
|
|
688
|
+
if (!name) throw new TypeError("ai.decider: name is required");
|
|
689
|
+
const resolved = resolveDecider(name, options);
|
|
690
|
+
return { kind: "decider", name, ...resolved };
|
|
691
|
+
}
|
|
575
692
|
|
|
576
693
|
/**
|
|
577
694
|
* Validate a decision and return the registered shape.
|
|
@@ -581,59 +698,99 @@ export const ai: AiNamespace = {
|
|
|
581
698
|
*/
|
|
582
699
|
export function buildDecisionDecl(name: string, options: AiDecisionOptions): AiDecisionDecl {
|
|
583
700
|
if (!name) throw new TypeError("ai.decision: name is required");
|
|
701
|
+
if ("shadow" in options && options.shadow !== undefined) {
|
|
702
|
+
throw new TypeError(`ai.decision("${name}"): shadow is planned`);
|
|
703
|
+
}
|
|
704
|
+
if (!options.decider || options.decider.kind !== "decider") {
|
|
705
|
+
throw new TypeError(`ai.decision("${name}"): decider is required`);
|
|
706
|
+
}
|
|
707
|
+
if (options.otherwise === undefined) {
|
|
708
|
+
throw new TypeError(`ai.decision("${name}"): otherwise is required`);
|
|
709
|
+
}
|
|
584
710
|
const keys = Object.keys(options.ask ?? {});
|
|
585
711
|
if (keys.length === 0) throw new TypeError(`ai.decision("${name}"): ask is empty`);
|
|
586
|
-
const hasReview = options.review !== undefined;
|
|
587
|
-
const abstain = options.onUncertain === "abstain";
|
|
588
|
-
if (hasReview === abstain) {
|
|
589
|
-
throw new TypeError(
|
|
590
|
-
`ai.decision("${name}"): declare exactly one of review or onUncertain: "abstain"`,
|
|
591
|
-
);
|
|
592
|
-
}
|
|
593
712
|
if (options.autonomy !== undefined && options.autonomy.audit === undefined) {
|
|
594
713
|
throw new TypeError(`ai.decision("${name}"): autonomy requires audit`);
|
|
595
714
|
}
|
|
715
|
+
const chain = [options.decider, ...(options.backup ?? [])];
|
|
596
716
|
for (const key of keys) {
|
|
597
717
|
if (key === "meta" || key === "$") {
|
|
598
718
|
throw new TypeError(`ai.decision("${name}"): question id "${key}" is reserved`);
|
|
599
719
|
}
|
|
600
720
|
const question = options.ask[key];
|
|
601
721
|
if (!question) continue;
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if (count > 254) {
|
|
605
|
-
throw new TypeError(
|
|
606
|
-
`ai.decision("${name}"): choice "${key}" has ${count} options; max 254`,
|
|
607
|
-
);
|
|
608
|
-
}
|
|
609
|
-
if (Object.prototype.hasOwnProperty.call(question.options, "none_of_these")) {
|
|
610
|
-
throw new TypeError(`ai.decision("${name}"): choice "${key}" must not set none_of_these`);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
if (question.kind === "score") {
|
|
614
|
-
const count = question.levels.length;
|
|
615
|
-
if (count < 2 || count > 10) {
|
|
616
|
-
throw new TypeError(`ai.decision("${name}"): score "${key}" needs 2–10 levels`);
|
|
617
|
-
}
|
|
722
|
+
for (const decider of chain) {
|
|
723
|
+
assertQuestionFits(name, key, question, decider);
|
|
618
724
|
}
|
|
619
725
|
}
|
|
620
|
-
const
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
726
|
+
const abstain = options.otherwise === "abstain";
|
|
727
|
+
const review = abstain
|
|
728
|
+
? undefined
|
|
729
|
+
: typeof options.otherwise === "string"
|
|
730
|
+
? options.otherwise
|
|
731
|
+
: options.otherwise.name;
|
|
732
|
+
const secrets = [...new Set(chain.map((decider) => decider.secret))].sort();
|
|
625
733
|
return {
|
|
626
734
|
kind: "decision",
|
|
627
735
|
name,
|
|
628
736
|
ask: options.ask,
|
|
629
737
|
mode: abstain ? "abstain" : "review",
|
|
630
738
|
...(review !== undefined ? { review } : {}),
|
|
631
|
-
|
|
632
|
-
|
|
739
|
+
decider: options.decider.name,
|
|
740
|
+
backup: (options.backup ?? []).map((decider) => decider.name),
|
|
741
|
+
otherwise: abstain ? "abstain" : (review ?? ""),
|
|
742
|
+
secrets,
|
|
633
743
|
...(options.autonomy !== undefined ? { autonomy: options.autonomy } : {}),
|
|
634
744
|
...(options.evals !== undefined ? { evals: options.evals } : {}),
|
|
635
|
-
...(options.timeout !== undefined ? { timeout: options.timeout } : {}),
|
|
636
745
|
...(options.locale !== undefined ? { locale: options.locale } : {}),
|
|
637
746
|
...(options.in !== undefined ? { inputSchema: options.in } : {}),
|
|
638
747
|
};
|
|
639
748
|
}
|
|
749
|
+
|
|
750
|
+
function assertQuestionFits(
|
|
751
|
+
decision: string,
|
|
752
|
+
key: string,
|
|
753
|
+
question: AiDecisionQuestion,
|
|
754
|
+
decider: AiDeciderDecl,
|
|
755
|
+
): void {
|
|
756
|
+
const caps = decider.capabilities;
|
|
757
|
+
if (question.kind === "boolean" && !caps.boolean) {
|
|
758
|
+
throw new TypeError(
|
|
759
|
+
`ai.decision("${decision}"): decider "${decider.name}" cannot answer boolean "${key}"`,
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
if (question.kind === "choice") {
|
|
763
|
+
if (!caps.choice) {
|
|
764
|
+
throw new TypeError(
|
|
765
|
+
`ai.decision("${decision}"): decider "${decider.name}" cannot answer choice "${key}"`,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
const count = Object.keys(question.options).length;
|
|
769
|
+
if (typeof caps.maxChoices === "number" && count > caps.maxChoices) {
|
|
770
|
+
throw new TypeError(
|
|
771
|
+
`ai.decision("${decision}"): choice "${key}" has ${count} options; decider "${decider.name}" allows ${caps.maxChoices}`,
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
if (Object.prototype.hasOwnProperty.call(question.options, "none_of_these")) {
|
|
775
|
+
throw new TypeError(`ai.decision("${decision}"): choice "${key}" must not set none_of_these`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (question.kind === "score") {
|
|
779
|
+
if (!caps.score) {
|
|
780
|
+
throw new TypeError(
|
|
781
|
+
`ai.decision("${decision}"): decider "${decider.name}" cannot answer score "${key}"`,
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
const count = question.levels.length;
|
|
785
|
+
if (typeof caps.minLevels === "number" && count < caps.minLevels) {
|
|
786
|
+
throw new TypeError(
|
|
787
|
+
`ai.decision("${decision}"): score "${key}" has ${count} levels; decider "${decider.name}" needs at least ${caps.minLevels}`,
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
if (typeof caps.maxLevels === "number" && count > caps.maxLevels) {
|
|
791
|
+
throw new TypeError(
|
|
792
|
+
`ai.decision("${decision}"): score "${key}" has ${count} levels; decider "${decider.name}" allows ${caps.maxLevels}`,
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
package/src/elements/ai.ts
CHANGED
package/src/index.ts
CHANGED
package/src/kernel/app.ts
CHANGED
|
@@ -1704,6 +1704,14 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1704
1704
|
"oke boot: decisions are declared but rootDir and OKE_ROOT_DIR are unset, so the decision lockfile cannot load.",
|
|
1705
1705
|
);
|
|
1706
1706
|
}
|
|
1707
|
+
const { resolveDriverId } = await import("../config/index.ts");
|
|
1708
|
+
const { setDecisionTransport } = loadFxDecideModule();
|
|
1709
|
+
const decideDriver = resolveDriverId(
|
|
1710
|
+
(overrides?.config ?? options.config)?.drivers?.decide,
|
|
1711
|
+
bootEnv,
|
|
1712
|
+
{ test: "mock" },
|
|
1713
|
+
);
|
|
1714
|
+
setDecisionTransport(decideDriver === "mock" ? "mock" : "live");
|
|
1707
1715
|
await loadDecisionLockfile(root);
|
|
1708
1716
|
setDeclaredDriftDecisions(Object.keys(declaredDecisions));
|
|
1709
1717
|
if (result.journal && result.journal.store.decisions) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle entry for the reported decision-module gzip row.
|
|
3
|
+
* Not on the kernel edge graph.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { runFxDecide } from "./fx-decide.ts";
|
|
7
|
+
|
|
8
|
+
/** Anchor so minify keeps the decision module. */
|
|
9
|
+
export function __okeDecisionBudgetAnchor(): typeof runFxDecide {
|
|
10
|
+
return runFxDecide;
|
|
11
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import type {
|
|
18
18
|
AiAgentDecl,
|
|
19
|
+
AiDeciderDecl,
|
|
19
20
|
AiDecisionDecl,
|
|
20
21
|
AiEmbedDecl,
|
|
21
22
|
AiMcpServerDecl,
|
|
@@ -53,5 +54,7 @@ export const aiEmbedRegistry: AiEmbedDecl[] = [];
|
|
|
53
54
|
export const aiAgentRegistry: AiAgentDecl[] = [];
|
|
54
55
|
/** `ai.mcpServer` declarations since the last reset. */
|
|
55
56
|
export const aiMcpServerRegistry: AiMcpServerDecl[] = [];
|
|
57
|
+
/** `ai.decider` declarations since the last reset. */
|
|
58
|
+
export const aiDeciderRegistry: AiDeciderDecl[] = [];
|
|
56
59
|
/** `ai.decision` declarations since the last reset. */
|
|
57
60
|
export const aiDecisionRegistry: AiDecisionDecl[] = [];
|