okengine 0.11.1 → 0.11.2

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 (61) hide show
  1. package/package.json +1 -1
  2. package/site/content/docs/elements/ai.mdx +22 -1
  3. package/site/content/docs/elements/store.mdx +3 -1
  4. package/site/content/docs/elements/vault.mdx +19 -11
  5. package/site/content/docs/get-started/installation.mdx +7 -1
  6. package/site/content/docs/recipes/llama-cpp.mdx +10 -9
  7. package/site/content/docs/reference/cli.md +6 -2
  8. package/site/content/docs/reference/environment-variables.mdx +9 -9
  9. package/src/cli/ai-setup/ai-setup.test.ts +3 -1
  10. package/src/cli/ai-setup/apply.ts +61 -1
  11. package/src/cli/ask-seed.test.ts +4 -3
  12. package/src/cli/ask-seed.ts +5 -6
  13. package/src/cli/client-add.test.ts +2 -1
  14. package/src/cli/dev.test.ts +116 -0
  15. package/src/cli/dev.ts +107 -18
  16. package/src/cli/project-state.test.ts +50 -0
  17. package/src/cli/project-state.ts +123 -0
  18. package/src/cli/vault-cmd.test.ts +47 -18
  19. package/src/cli/vault-cmd.ts +2 -1
  20. package/src/compiler/extract.ts +12 -1
  21. package/src/console/server/console.test.ts +3 -1
  22. package/src/console/server/operator-db.test.ts +48 -17
  23. package/src/console/server/operator-db.ts +5 -1
  24. package/src/docker/derive.ts +24 -3
  25. package/src/docker/docker.test.ts +4 -2
  26. package/src/docker/index.ts +1 -0
  27. package/src/docker/recipes/index.ts +1 -0
  28. package/src/docker/recipes/llama-cpp.ts +20 -4
  29. package/src/drivers/ai-openai-compatible.ts +15 -3
  30. package/src/drivers/vault-builtin.test.ts +50 -42
  31. package/src/elements/ai/declare.ts +73 -3
  32. package/src/elements/ai/errors.test.ts +35 -0
  33. package/src/elements/ai/errors.ts +139 -0
  34. package/src/elements/ai/eval.ts +26 -1
  35. package/src/elements/ai/runtime.ts +140 -80
  36. package/src/elements/ai/tools.test.ts +1 -1
  37. package/src/elements/ai.test.ts +99 -2
  38. package/src/elements/ai.ts +11 -1
  39. package/src/elements/index.ts +2 -0
  40. package/src/elements/store/index-boot.test.ts +23 -6
  41. package/src/elements/store/resource.test.ts +38 -19
  42. package/src/elements/store/sql-session.test.ts +55 -58
  43. package/src/elements/vault/builtin-adapter.test.ts +115 -58
  44. package/src/elements/vault/builtin-adapter.ts +241 -47
  45. package/src/elements/vault/chaos-child.ts +424 -0
  46. package/src/elements/vault/chaos.test.ts +651 -0
  47. package/src/elements/vault/resilience.ts +6 -1
  48. package/src/elements/vault/security-checklist.test.ts +10 -8
  49. package/src/elements/vault/storage.ts +130 -27
  50. package/src/elements/vault/test-helpers.ts +368 -0
  51. package/src/elements/vault.ts +6 -0
  52. package/src/index.ts +2 -0
  53. package/src/kernel/app.ts +83 -10
  54. package/src/kernel/auto-registry.test.ts +52 -1
  55. package/src/kernel/element-registries.ts +19 -4
  56. package/src/kernel/errors.ts +3 -3
  57. package/src/kernel/fx.test.ts +25 -0
  58. package/src/kernel/fx.ts +4 -1
  59. package/src/manifest/types.ts +4 -0
  60. package/src/test/create-test-app.ts +16 -11
  61. package/src/test/reset-element-registries.ts +17 -9
package/src/kernel/app.ts CHANGED
@@ -81,6 +81,10 @@ import { releaseInstanceLeases } from "./graceful-shutdown.ts";
81
81
  import type { JournalRuntime } from "./boot-bind/journal.ts";
82
82
  import { listBindings, resetBindings, type Binding } from "./on.ts";
83
83
  import {
84
+ aiAgentRegistry,
85
+ aiEmbedRegistry,
86
+ aiModelRegistry,
87
+ aiPromptRegistry,
84
88
  channelTemplateRegistry,
85
89
  requiredEnvRegistry,
86
90
  secretRegistry,
@@ -461,6 +465,45 @@ function mergeUnique<T>(explicit: readonly T[] | undefined, fromRegistry: readon
461
465
  return out;
462
466
  }
463
467
 
468
+ /**
469
+ * Merge explicit `oke({ ai })` with auto-drained AI decls. Returns
470
+ * `undefined` when neither side contributes — a defined empty object would
471
+ * flip {@link resolveElementNeeds}'s `ai` flag for every app.
472
+ *
473
+ * @param explicit - Hand-passed AI boot options
474
+ * @param fromRegistry - Models / prompts / embeds / agents from declare registries
475
+ */
476
+ function mergeAiOptions(
477
+ explicit: BootOptions["ai"] | undefined,
478
+ fromRegistry: {
479
+ readonly models: NonNullable<BootOptions["ai"]>["models"];
480
+ readonly prompts: NonNullable<BootOptions["ai"]>["prompts"];
481
+ readonly embeds: NonNullable<BootOptions["ai"]>["embeds"];
482
+ readonly agents: NonNullable<BootOptions["ai"]>["agents"];
483
+ },
484
+ ): BootOptions["ai"] | undefined {
485
+ const models = mergeUnique(explicit?.models, fromRegistry.models ?? []);
486
+ const prompts = mergeUnique(explicit?.prompts, fromRegistry.prompts ?? []);
487
+ const embeds = mergeUnique(explicit?.embeds, fromRegistry.embeds ?? []);
488
+ const agents = mergeUnique(explicit?.agents, fromRegistry.agents ?? []);
489
+ if (
490
+ explicit === undefined &&
491
+ models.length === 0 &&
492
+ prompts.length === 0 &&
493
+ embeds.length === 0 &&
494
+ agents.length === 0
495
+ ) {
496
+ return undefined;
497
+ }
498
+ return {
499
+ ...(explicit ?? {}),
500
+ ...(models.length > 0 ? { models } : {}),
501
+ ...(prompts.length > 0 ? { prompts } : {}),
502
+ ...(embeds.length > 0 ? { embeds } : {}),
503
+ ...(agents.length > 0 ? { agents } : {}),
504
+ };
505
+ }
506
+
464
507
  /**
465
508
  * Create an application. Adopts bindings registered via {@link on}.
466
509
  *
@@ -476,20 +519,35 @@ export function oke(options: OkeOptions): OkeApp {
476
519
  if (registry === "consume") resetBindings();
477
520
 
478
521
  // Same registry mode drains store.sql/store.files, vault.secret, signal(),
479
- // and channel.<medium>().template() module-evaluation registries that
480
- // mirror `on`'s trigger drain (`listBindings`/`resetBindings` above).
481
- // Explicit `options.stores` / `secrets` / `signals` / `channel.templates`
482
- // are additive, never silently ignored deduped by reference so an
483
- // explicitly-passed decl that is also in the registry is not doubled.
522
+ // channel.<medium>().template(), and ai.model/prompt/embed/agent
523
+ // module-evaluation registries that mirror `on`'s trigger drain
524
+ // (`listBindings`/`resetBindings` above). Explicit `options.stores` /
525
+ // `secrets` / `signals` / `channel.templates` / `ai` are additive, never
526
+ // silently ignored deduped by reference so an explicitly-passed decl
527
+ // that is also in the registry is not doubled.
484
528
  const registrySnapshot =
485
529
  registry === "ignore"
486
- ? { stores: [], secrets: [], requiredEnv: [], signals: [], channelTemplates: [] }
530
+ ? {
531
+ stores: [],
532
+ secrets: [],
533
+ requiredEnv: [],
534
+ signals: [],
535
+ channelTemplates: [],
536
+ aiModels: [],
537
+ aiPrompts: [],
538
+ aiEmbeds: [],
539
+ aiAgents: [],
540
+ }
487
541
  : {
488
542
  stores: storeRegistry.slice(),
489
543
  secrets: secretRegistry.slice(),
490
544
  requiredEnv: requiredEnvRegistry.slice(),
491
545
  signals: signalRegistry.slice(),
492
546
  channelTemplates: channelTemplateRegistry.slice(),
547
+ aiModels: aiModelRegistry.slice(),
548
+ aiPrompts: aiPromptRegistry.slice(),
549
+ aiEmbeds: aiEmbedRegistry.slice(),
550
+ aiAgents: aiAgentRegistry.slice(),
493
551
  };
494
552
  if (registry === "consume") {
495
553
  storeRegistry.length = 0;
@@ -497,6 +555,10 @@ export function oke(options: OkeOptions): OkeApp {
497
555
  requiredEnvRegistry.length = 0;
498
556
  signalRegistry.length = 0;
499
557
  channelTemplateRegistry.length = 0;
558
+ aiModelRegistry.length = 0;
559
+ aiPromptRegistry.length = 0;
560
+ aiEmbedRegistry.length = 0;
561
+ aiAgentRegistry.length = 0;
500
562
  }
501
563
  const effectiveStores = mergeUnique(options.stores, registrySnapshot.stores);
502
564
  const effectiveSecrets = mergeUnique(options.secrets, registrySnapshot.secrets);
@@ -523,6 +585,14 @@ export function oke(options: OkeOptions): OkeApp {
523
585
  ...(options.channel ?? {}),
524
586
  templates: mergeUnique(options.channel?.templates, registrySnapshot.channelTemplates),
525
587
  };
588
+ // Same undefined-when-empty rule for AI — a bare `{}` would force the AI
589
+ // runtime open even when the app never declared models / prompts.
590
+ const effectiveAi = mergeAiOptions(options.ai, {
591
+ models: registrySnapshot.aiModels,
592
+ prompts: registrySnapshot.aiPrompts,
593
+ embeds: registrySnapshot.aiEmbeds,
594
+ agents: registrySnapshot.aiAgents,
595
+ });
526
596
 
527
597
  // Resolve Gate bag early so auth HTTP Bindings join `adopted` + the router
528
598
  // before posture audit (same ensureBoot → doBoot path — never a side channel).
@@ -531,8 +601,11 @@ export function oke(options: OkeOptions): OkeApp {
531
601
  env: options.env,
532
602
  });
533
603
 
534
- /** Retained for test harness / boot merges. */
535
- const $options = options;
604
+ /** Retained for test harness / boot merges (includes auto-drained AI decls). */
605
+ const $options: OkeOptions =
606
+ effectiveAi === undefined || options.ai === effectiveAi
607
+ ? options
608
+ : { ...options, ai: effectiveAi };
536
609
 
537
610
  const appHooks: HookMap = {};
538
611
  const unitHooks = new Map<string, HookMap>();
@@ -828,7 +901,7 @@ export function oke(options: OkeOptions): OkeApp {
828
901
  clocks: overrides?.clocks ?? options.clocks,
829
902
  stores: overrides?.stores ?? effectiveStores,
830
903
  channel: overrides?.channel ?? effectiveChannel,
831
- ai: overrides?.ai ?? options.ai,
904
+ ai: overrides?.ai ?? effectiveAi,
832
905
  runs: overrides?.runs ?? options.runs,
833
906
  bindings: adopted,
834
907
  flows: [...flowsByName.values()],
@@ -907,7 +980,7 @@ export function oke(options: OkeOptions): OkeApp {
907
980
  (overrides?.config ?? options.config)?.i18n?.default ??
908
981
  "en",
909
982
  },
910
- ai: overrides?.ai ?? options.ai,
983
+ ai: overrides?.ai ?? effectiveAi,
911
984
  runs: overrides?.runs ?? options.runs,
912
985
  now: overrides?.now ?? options.fx?.now,
913
986
  instanceId: overrides?.instanceId,
@@ -20,7 +20,7 @@ import { flow } from "./flow.ts";
20
20
  import { on, resetBindings } from "./on.ts";
21
21
  import { http } from "./triggers.ts";
22
22
 
23
- describe("oke() auto-registry — stores/secrets/signals/channel.templates", () => {
23
+ describe("oke() auto-registry — stores/secrets/signals/channel.templates/ai", () => {
24
24
  test("before/after: zero explicit arrays boots identically to the explicit-array form", async () => {
25
25
  resetBindings();
26
26
 
@@ -195,4 +195,55 @@ describe("oke() auto-registry — stores/secrets/signals/channel.templates", ()
195
195
  expect(appB.bootResult?.signal).toBeUndefined();
196
196
  expect(appB.bootResult?.channel?.templates.has("leak-template") ?? false).toBe(false);
197
197
  });
198
+
199
+ test("ai.model / .prompt auto-drain into oke options (no explicit oke({ ai }))", async () => {
200
+ const { ai } = await import("../elements/ai/declare.ts");
201
+ const { mockAiDriver } = await import("../drivers/ai-mock.ts");
202
+ const { createAiRuntime } = await import("../elements/ai/runtime.ts");
203
+
204
+ const smart = ai.model("smart", { provider: "mock" });
205
+ const summarizeNote = smart.prompt("summarize-note", {
206
+ version: 1,
207
+ });
208
+
209
+ const ping = flow("ai.ping", {
210
+ effects: { asks: ["summarize-note"] },
211
+ do: async (_input, fx) => fx.ask("summarize-note", { title: "t", body: "b" }),
212
+ });
213
+ on(http.post("/ai-ping"), ping);
214
+
215
+ const app = oke({
216
+ name: "ai-auto-registry",
217
+ autoBoot: false,
218
+ startScheduler: false,
219
+ gate: { unguardedHttp: "allow" },
220
+ });
221
+
222
+ expect(app.$options.ai?.models?.some((m) => m.name === "smart")).toBe(true);
223
+ expect(app.$options.ai?.prompts?.some((p) => p.name === "summarize-note")).toBe(true);
224
+
225
+ const runtime = createAiRuntime({
226
+ models: [smart],
227
+ prompts: [summarizeNote],
228
+ defaultDriver: {
229
+ id: "mock",
230
+ open: () =>
231
+ mockAiDriver.open({
232
+ mockResponses: { "*": { summary: "one-line summary" } },
233
+ }),
234
+ },
235
+ });
236
+ await app.boot({
237
+ env: "test",
238
+ unguardedHttp: "allow",
239
+ elements: { ai: runtime },
240
+ });
241
+
242
+ const res = await app.fetch(
243
+ new Request("http://127.0.0.1/ai-ping", { method: "POST", body: "{}" }),
244
+ );
245
+ expect(res.status).toBe(200);
246
+ const json = (await res.json()) as { data: { summary: string } };
247
+ expect(json.data.summary).toBe("one-line summary");
248
+ });
198
249
  });
@@ -1,17 +1,24 @@
1
1
  /**
2
2
  * Module-evaluation registries for `store.sql` / `store.files`, `vault.secret`,
3
- * `vault.env.required`, `signal()`, and `channel.<medium>().template()` — the
4
- * plain arrays behind each declare module's `listX()` / `resetX()` pair
5
- * (mirrors `on.ts`'s trigger-drain `bindings` array).
3
+ * `vault.env.required`, `signal()`, `channel.<medium>().template()`, and
4
+ * `ai.model` / `.prompt` / `ai.embed` / `ai.agent` — the plain arrays behind
5
+ * each declare module's `listX()` / `resetX()` pair (mirrors `on.ts`'s
6
+ * trigger-drain `bindings` array).
6
7
  *
7
8
  * Lives here, not in the declare modules themselves, so {@link oke} can read
8
9
  * every registry with one lightweight import instead of statically pulling
9
- * in all four element `declare.ts` modules (and every unrelated export they
10
+ * in all element `declare.ts` modules (and every unrelated export they
10
11
  * carry) into every app's bundle — see the `oke()` Store-only bundle-size
11
12
  * budget in `boot.test.ts`. Only type imports cross back to the declare
12
13
  * modules, so this file has zero runtime dependencies of its own.
13
14
  */
14
15
 
16
+ import type {
17
+ AiAgentDecl,
18
+ AiEmbedDecl,
19
+ AiModelDecl,
20
+ AiPromptDecl,
21
+ } from "../elements/ai/declare.ts";
15
22
  import type { StoreDecl } from "../elements/store/declare.ts";
16
23
  import type { VaultSecretDecl } from "../elements/vault/declare.ts";
17
24
  import type { SignalDecl } from "../elements/signal/declare.ts";
@@ -27,3 +34,11 @@ export const requiredEnvRegistry: string[] = [];
27
34
  export const signalRegistry: SignalDecl[] = [];
28
35
  /** Medium-binder `.template()` declarations since the last reset. */
29
36
  export const channelTemplateRegistry: ChannelTemplateDecl[] = [];
37
+ /** `ai.model` declarations since the last reset. */
38
+ export const aiModelRegistry: AiModelDecl[] = [];
39
+ /** `model.prompt` declarations since the last reset. */
40
+ export const aiPromptRegistry: AiPromptDecl[] = [];
41
+ /** `ai.embed` declarations since the last reset. */
42
+ export const aiEmbedRegistry: AiEmbedDecl[] = [];
43
+ /** `ai.agent` declarations since the last reset. */
44
+ export const aiAgentRegistry: AiAgentDecl[] = [];
@@ -144,19 +144,19 @@ export const OKE_ERRORS = {
144
144
  },
145
145
  /**
146
146
  * Flow has no declared `effects` and no Manifest-derived effects were
147
- * available to stamp at boot (docker / prod — never a silent open token).
147
+ * available to stamp at boot (dev+compose / prod — never a silent open token).
148
148
  */
149
149
  NO_EFFECTS_DECLARED: {
150
150
  code: 1008,
151
151
  cause: 'Flow "{flow}" has no declared effects and no Manifest to derive them from.',
152
152
  fix:
153
153
  "Add explicit `effects` to this flow, or boot with a Manifest (`oke build`) / " +
154
- "`rootDir` so effects can be derived. docker/prod refuse an open capability token.",
154
+ "`rootDir` so effects can be derived. dev+compose/prod refuse an open capability token.",
155
155
  },
156
156
  /**
157
157
  * A `src/flows/<unit>` folder exists on disk but no adopted flow carries
158
158
  * that unit — the generated `.adopt()` barrel (`src/flows/generated.ts`)
159
- * is stale or was hand-edited. docker/prod — never a silently-incomplete
159
+ * is stale or was hand-edited. dev+compose / prod — never a silently-incomplete
160
160
  * route table in a deploy-shaped environment.
161
161
  */
162
162
  ADOPT_BARREL_STALE: {
@@ -93,6 +93,31 @@ describe("fx — effect ledger", () => {
93
93
  },
94
94
  ledger,
95
95
  secrets: { STRIPE_KEY: "sk_test" },
96
+ aiRuntime: {
97
+ prompts: new Map(),
98
+ agents: new Map(),
99
+ embeds: new Map(),
100
+ autoCacheDisabled: true,
101
+ journalingForced: false,
102
+ denials: [],
103
+ agentRuns: [],
104
+ journal: [],
105
+ async ask() {
106
+ return { ok: true };
107
+ },
108
+ async runAgent() {
109
+ return { ok: true, steps: 0, denials: [], output: {} };
110
+ },
111
+ async *stream() {
112
+ /* no tokens */
113
+ },
114
+ async search() {
115
+ return [];
116
+ },
117
+ async embed() {
118
+ return { vectors: [] };
119
+ },
120
+ } as never,
96
121
  });
97
122
 
98
123
  await stub(fx, "sql:bookings").get("x");
package/src/kernel/fx.ts CHANGED
@@ -297,6 +297,8 @@ export interface FxDeliverOtpOptions {
297
297
  /** Options for {@link Fx.ask}. */
298
298
  export interface FxAskOptions {
299
299
  readonly via?: readonly NamedRef[];
300
+ /** Per-call deadline — overrides prompt `timeout` (`"30s"` or ms). */
301
+ readonly timeout?: string | number;
300
302
  /** Flow refs offered as tools — each model call goes through `fx.call`. */
301
303
  readonly tools?: readonly NamedRef[];
302
304
  /** Bound on tool invocations (default 6). */
@@ -1587,13 +1589,14 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1587
1589
  if (options.aiRuntime) {
1588
1590
  return options.aiRuntime.ask(name, input, {
1589
1591
  via: opts?.via?.map(resolveName),
1592
+ ...(opts?.timeout !== undefined ? { timeout: opts.timeout } : {}),
1590
1593
  tools: opts?.tools?.map(resolveName),
1591
1594
  maxSteps: opts?.maxSteps,
1592
1595
  // Host fx.call — same capability / ledger / Runs path as any call.
1593
1596
  callTool: (tool, toolInput) => fx.call(tool, toolInput),
1594
1597
  });
1595
1598
  }
1596
- return {};
1599
+ throw new Error(`fx.ask: AI runtime is not configured for prompt "${name}"`);
1597
1600
  });
1598
1601
  },
1599
1602
  search(embed, query, opts) {
@@ -300,6 +300,10 @@ export interface AiPrompt {
300
300
  version?: number;
301
301
  evals?: string;
302
302
  budget?: AiBudget;
303
+ /** Ordered recovery chain of logical model names. */
304
+ via?: string[];
305
+ /** Per-command deadline (`"30s"` or milliseconds). */
306
+ timeout?: string | number;
303
307
  model?: string;
304
308
  in?: JsonSchema;
305
309
  out?: JsonSchema;
@@ -14,9 +14,6 @@
14
14
  * ```
15
15
  */
16
16
 
17
- import { mkdtemp, rm } from "node:fs/promises";
18
- import { tmpdir } from "node:os";
19
- import { join } from "node:path";
20
17
  import {
21
18
  createMockAiDriver,
22
19
  createChannelInbox,
@@ -215,15 +212,26 @@ export async function createTestApp<App extends OkeApp>(
215
212
  return out;
216
213
  };
217
214
 
218
- // Unique on-disk PGLite datadir per harness `memory://` is not reliably
219
- // isolated across boots in one worker (IF NOT EXISTS / row leaks).
220
- const prevPgliteUrl = process.env.OKE_PGLITE_URL;
221
- const pgliteDir = await mkdtemp(join(tmpdir(), "oke-pglite-"));
222
- process.env.OKE_PGLITE_URL = pgliteDir;
215
+ // Harness contract is in-process memory SQL (same as vault/channel/ai/runs).
216
+ // Global `STORE_SQL_DEFAULTS.test` is PGLite for real app boots do not inherit
217
+ // that here, or every insert pays cold WASM+pgvector init (~seconds). Dialects
218
+ // that need PGLite pass `boot.config.drivers.store.sql` explicitly.
219
+ const bootConfig = options.boot?.config;
220
+ const config = {
221
+ ...(bootConfig ?? {}),
222
+ drivers: {
223
+ ...(bootConfig?.drivers ?? {}),
224
+ store: {
225
+ ...(bootConfig?.drivers?.store ?? {}),
226
+ sql: bootConfig?.drivers?.store?.sql ?? { test: "memory" },
227
+ },
228
+ },
229
+ };
223
230
 
224
231
  await app.boot({
225
232
  ...(options.boot ?? {}),
226
233
  env: "test",
234
+ config,
227
235
  // Test-only opt-out (honoured because env is "test" below). Production
228
236
  // boots never skip posture via this flag.
229
237
  unguardedHttp: options.boot?.unguardedHttp ?? appOpts.gate?.unguardedHttp ?? "allow",
@@ -331,9 +339,6 @@ export async function createTestApp<App extends OkeApp>(
331
339
  },
332
340
  async close() {
333
341
  await app.bootResult?.close();
334
- if (prevPgliteUrl === undefined) delete process.env.OKE_PGLITE_URL;
335
- else process.env.OKE_PGLITE_URL = prevPgliteUrl;
336
- await rm(pgliteDir, { recursive: true, force: true }).catch(() => undefined);
337
342
  },
338
343
  };
339
344
 
@@ -1,22 +1,26 @@
1
1
  /**
2
2
  * Global safety net for the `store.sql` / `store.files` / `vault.secret` /
3
- * `signal()` / `channel.<medium>().template()` auto-registries
4
- * (`src/kernel/element-registries.ts`).
3
+ * `signal()` / `channel.<medium>().template()` / `ai.model`·prompt·embed·agent
4
+ * auto-registries (`src/kernel/element-registries.ts`).
5
5
  *
6
6
  * Unlike `on()` bindings — created almost exclusively to wire a real app —
7
- * these four factories are called throughout the suite as bare value
8
- * constructors, completely unrelated to booting an app (hundreds of call
9
- * sites across `src/elements/*.test.ts`). Since the registries are plain
10
- * module-level arrays shared by every test file in one `bun test` process,
11
- * leaving cleanup to per-file discipline (the convention `on.ts` relies on —
12
- * see `resetBindings()` / `registry: "ignore"`) would let stray decls from
13
- * one file silently reach a default-registry `oke()` call in a completely
7
+ * these factories are called throughout the suite as bare value constructors,
8
+ * completely unrelated to booting an app (hundreds of call sites across
9
+ * `src/elements/*.test.ts`). Since the registries are plain module-level
10
+ * arrays shared by every test file in one `bun test` process, leaving cleanup
11
+ * to per-file discipline (the convention `on.ts` relies on — see
12
+ * `resetBindings()` / `registry: "ignore"`) would let stray decls from one
13
+ * file silently reach a default-registry `oke()` call in a completely
14
14
  * unrelated file. Reset after every test, globally, via `bunfig.toml`
15
15
  * `[test].preload`.
16
16
  */
17
17
 
18
18
  import { afterEach } from "bun:test";
19
19
  import {
20
+ aiAgentRegistry,
21
+ aiEmbedRegistry,
22
+ aiModelRegistry,
23
+ aiPromptRegistry,
20
24
  channelTemplateRegistry,
21
25
  requiredEnvRegistry,
22
26
  secretRegistry,
@@ -30,4 +34,8 @@ afterEach(() => {
30
34
  requiredEnvRegistry.length = 0;
31
35
  signalRegistry.length = 0;
32
36
  channelTemplateRegistry.length = 0;
37
+ aiModelRegistry.length = 0;
38
+ aiPromptRegistry.length = 0;
39
+ aiEmbedRegistry.length = 0;
40
+ aiAgentRegistry.length = 0;
33
41
  });