okengine 0.13.0 → 0.15.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 (277) hide show
  1. package/manifest.v1.schema.json +7 -1
  2. package/package.json +4 -5
  3. package/site/content/docs/ai/mcp.mdx +27 -2
  4. package/site/content/docs/elements/ai.mdx +59 -11
  5. package/site/content/docs/elements/channel.mdx +1 -0
  6. package/site/content/docs/elements/clock.mdx +30 -12
  7. package/site/content/docs/elements/flow.mdx +9 -3
  8. package/site/content/docs/elements/gate.mdx +25 -22
  9. package/site/content/docs/elements/signal.mdx +71 -25
  10. package/site/content/docs/elements/store.mdx +87 -28
  11. package/site/content/docs/elements/vault.mdx +1 -0
  12. package/site/content/docs/get-started/basic-usage.mdx +22 -13
  13. package/site/content/docs/get-started/index.mdx +5 -0
  14. package/site/content/docs/get-started/installation.mdx +2 -2
  15. package/site/content/docs/get-started/introduction.mdx +11 -3
  16. package/site/content/docs/get-started/meta.json +1 -1
  17. package/site/content/docs/get-started/project-structure.mdx +932 -0
  18. package/site/content/docs/reference/cli.md +13 -10
  19. package/site/content/docs/reference/client.mdx +25 -21
  20. package/site/content/docs/reference/fx.mdx +27 -22
  21. package/site/content/docs/reference/security.md +1 -1
  22. package/src/auth/bindings.ts +14 -5
  23. package/src/auth/cross-plane.ts +10 -3
  24. package/src/cli/ai-setup/ai-setup.test.ts +62 -0
  25. package/src/cli/ai-setup/apply.ts +32 -1
  26. package/src/cli/ai-setup/recommend.test.ts +25 -0
  27. package/src/cli/ai-setup/recommend.ts +8 -3
  28. package/src/cli/attach-host-console.test.ts +3 -2
  29. package/src/cli/attach-host-console.ts +3 -0
  30. package/src/cli/build.test.ts +4 -2
  31. package/src/cli/db-seed.ts +48 -15
  32. package/src/cli/db.test.ts +104 -0
  33. package/src/cli/db.ts +122 -10
  34. package/src/cli/dev-controls.test.ts +20 -0
  35. package/src/cli/dev-controls.ts +13 -0
  36. package/src/cli/dev.test.ts +15 -4
  37. package/src/cli/dev.ts +102 -29
  38. package/src/cli/ensure-drizzle-config.ts +2 -1
  39. package/src/cli/tui/DevLive.tsx +12 -2
  40. package/src/cli/tui/keys.test.ts +2 -1
  41. package/src/cli/tui/keys.ts +3 -2
  42. package/src/compiler/aot.test.ts +1 -1
  43. package/src/compiler/effects-infer.ts +58 -3
  44. package/src/compiler/extract.test.ts +458 -29
  45. package/src/compiler/extract.ts +387 -69
  46. package/src/compiler/flow-path.test.ts +96 -0
  47. package/src/compiler/flow-path.ts +173 -0
  48. package/src/compiler/generate-adopt.test.ts +112 -3
  49. package/src/compiler/generate-adopt.ts +280 -26
  50. package/src/compiler/response.ts +45 -1
  51. package/src/compiler/schema-from-ast.ts +609 -0
  52. package/src/console/server/ai.ts +5 -2
  53. package/src/console/server/bind.ts +2 -2
  54. package/src/console/server/console.test.ts +3 -0
  55. package/src/console/server/flows.ts +121 -40
  56. package/src/console/server/invoke-user-flow.test.ts +3 -1
  57. package/src/console/server/invoke-user-flow.ts +17 -2
  58. package/src/console/server/iso-at.test.ts +9 -0
  59. package/src/console/server/iso-at.ts +11 -0
  60. package/src/console/server/security-headers.ts +6 -3
  61. package/src/console/server/serve.ts +17 -2
  62. package/src/console/server/spa-proxy.test.ts +1 -0
  63. package/src/console/server/sql-catalog.ts +147 -7
  64. package/src/console/server/sql-rls.test.ts +16 -0
  65. package/src/console/server/state.ts +8 -0
  66. package/src/console/server/store.test.ts +7 -4
  67. package/src/console/server/store.ts +142 -58
  68. package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
  69. package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +1 -0
  70. package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
  71. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
  72. package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
  73. package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
  74. package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
  75. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
  76. package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
  77. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
  78. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
  79. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
  80. package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
  81. package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +4 -0
  82. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
  83. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
  84. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
  85. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
  86. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
  87. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
  88. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
  89. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
  90. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
  91. package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
  92. package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
  93. package/src/console/ui-next/dist/index.html +6 -6
  94. package/src/console/ui-next/seed-invoke-host.ts +2 -0
  95. package/src/console/ui-next/src/client.ts +35 -7
  96. package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
  97. package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
  98. package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
  99. package/src/console/ui-next/src/features/flows/graph/build-flow-graph.test.ts +18 -0
  100. package/src/console/ui-next/src/features/flows/graph/build-flow-graph.ts +14 -1
  101. package/src/console/ui-next/src/features/flows/graph/neighborhood.test.ts +17 -0
  102. package/src/console/ui-next/src/features/flows/graph/neighborhood.ts +16 -3
  103. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +3 -1
  104. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  105. package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
  106. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +9 -3
  107. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +29 -2
  108. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
  109. package/src/console/ui-next/src/features/observability/lib/ask-count.test.ts +25 -0
  110. package/src/console/ui-next/src/features/observability/lib/ask-count.ts +4 -1
  111. package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
  112. package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
  113. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
  114. package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
  115. package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
  116. package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
  117. package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
  118. package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
  119. package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
  120. package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
  121. package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
  122. package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
  123. package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
  124. package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
  125. package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
  126. package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
  127. package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
  128. package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
  129. package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
  130. package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
  131. package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
  132. package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
  133. package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
  134. package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
  135. package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
  136. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
  137. package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +12 -4
  138. package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
  139. package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
  140. package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
  141. package/src/docker/compose-health.test.ts +27 -0
  142. package/src/docker/compose-health.ts +19 -3
  143. package/src/docker/docker.test.ts +4 -1
  144. package/src/docker/dockerfile.ts +1 -1
  145. package/src/docker/recipes/pgdog.ts +2 -0
  146. package/src/drivers/ai-anthropic.ts +5 -0
  147. package/src/drivers/ai-ollama.ts +49 -30
  148. package/src/drivers/ai-openai-compatible.ts +57 -46
  149. package/src/drivers/ai-providers.test.ts +3 -0
  150. package/src/drivers/bun-native-completeness.test.ts +7 -9
  151. package/src/drivers/clock-postgres.ts +13 -9
  152. package/src/drivers/instances-postgres.ts +4 -6
  153. package/src/drivers/journal-postgres.ts +13 -9
  154. package/src/drivers/pg-rls.test.ts +87 -0
  155. package/src/drivers/pg-rls.ts +233 -7
  156. package/src/drivers/pglite.ts +26 -3
  157. package/src/drivers/postgres.test.ts +23 -0
  158. package/src/drivers/postgres.ts +133 -9
  159. package/src/drivers/redis.ts +11 -4
  160. package/src/drivers/signal-redis.ts +24 -14
  161. package/src/drivers/signal-types.ts +2 -1
  162. package/src/drivers/types.ts +10 -0
  163. package/src/elements/ai/declare.ts +109 -0
  164. package/src/elements/ai/errors.test.ts +5 -1
  165. package/src/elements/ai/errors.ts +30 -2
  166. package/src/elements/ai/eval.ts +4 -6
  167. package/src/elements/ai/mcp-client.test.ts +206 -0
  168. package/src/elements/ai/mcp-client.ts +362 -0
  169. package/src/elements/ai/mcp-http.ts +159 -0
  170. package/src/elements/ai/mcp-mock.ts +134 -0
  171. package/src/elements/ai/mcp-protocol.ts +234 -0
  172. package/src/elements/ai/mcp-stdio.test.ts +50 -0
  173. package/src/elements/ai/mcp-stdio.ts +212 -0
  174. package/src/elements/ai/mcp-transport.ts +70 -0
  175. package/src/elements/ai/runtime.ts +159 -29
  176. package/src/elements/ai.test.ts +139 -0
  177. package/src/elements/ai.ts +16 -0
  178. package/src/elements/clock/declare.ts +26 -1
  179. package/src/elements/clock/health.test.ts +43 -0
  180. package/src/elements/clock/runtime.ts +55 -2
  181. package/src/elements/clock/schedule.ts +71 -142
  182. package/src/elements/clock.test.ts +9 -40
  183. package/src/elements/clock.ts +6 -1
  184. package/src/elements/gate/boot.ts +5 -5
  185. package/src/elements/gate/declare.ts +36 -4
  186. package/src/elements/gate/flatten.ts +1 -1
  187. package/src/elements/gate/runtime.ts +1 -1
  188. package/src/elements/index.ts +2 -0
  189. package/src/elements/signal/declare.ts +2 -1
  190. package/src/elements/signal/runtime.ts +16 -1
  191. package/src/elements/signal.ts +1 -0
  192. package/src/elements/store/cache.test.ts +2 -0
  193. package/src/elements/store/cache.ts +3 -3
  194. package/src/elements/store/declare.ts +9 -6
  195. package/src/elements/store/emit-drizzle.ts +44 -8
  196. package/src/elements/store/resource.test.ts +1 -1
  197. package/src/elements/store/rls-identity.test.ts +95 -0
  198. package/src/elements/store/rls-identity.ts +116 -0
  199. package/src/elements/store/runtime.ts +4 -0
  200. package/src/elements/store/schema-decl.test.ts +37 -0
  201. package/src/elements/store/schema-decl.ts +161 -0
  202. package/src/elements/store/seed.ts +1 -1
  203. package/src/elements/store/sql-condition.test.ts +12 -0
  204. package/src/elements/store/sql-condition.ts +26 -2
  205. package/src/elements/store/sql-rls-isolation.test.ts +165 -0
  206. package/src/elements/store/sql-rls-stamp.test.ts +117 -0
  207. package/src/elements/store/sql-session.ts +81 -3
  208. package/src/elements/store/upsert-app.test.ts +1 -1
  209. package/src/elements/store.ts +14 -0
  210. package/src/elements/vault/builtin-adapter.ts +17 -0
  211. package/src/full.ts +2 -0
  212. package/src/http.ts +20 -3
  213. package/src/index.ts +14 -1
  214. package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
  215. package/src/kernel/adopt-routes.ts +30 -2
  216. package/src/kernel/app.ts +227 -77
  217. package/src/kernel/auto-cache.test.ts +8 -8
  218. package/src/kernel/auto-registry.test.ts +54 -9
  219. package/src/kernel/boot-bind/ai.ts +24 -0
  220. package/src/kernel/boot-bind/clock.ts +2 -0
  221. package/src/kernel/boot.test.ts +4 -4
  222. package/src/kernel/boot.ts +5 -3
  223. package/src/kernel/effects-stamping.test.ts +3 -3
  224. package/src/kernel/element-registries.ts +15 -5
  225. package/src/kernel/errors.ts +24 -0
  226. package/src/kernel/flow-units.ts +64 -0
  227. package/src/kernel/flow.ts +29 -17
  228. package/src/kernel/fx-dead-letters.test.ts +77 -0
  229. package/src/kernel/fx.test.ts +22 -0
  230. package/src/kernel/fx.ts +135 -6
  231. package/src/kernel/horizontal-child.ts +4 -4
  232. package/src/kernel/http-path-pending.ts +20 -0
  233. package/src/kernel/http-resource.ts +4 -0
  234. package/src/kernel/http-stream.test.ts +174 -0
  235. package/src/kernel/index.ts +2 -0
  236. package/src/kernel/pipeline.test.ts +2 -2
  237. package/src/kernel/plugin-elements.test.ts +1 -1
  238. package/src/kernel/plugin-needs.test.ts +1 -1
  239. package/src/kernel/ready.test.ts +2 -2
  240. package/src/kernel/resource-mount.test.ts +14 -0
  241. package/src/kernel/run-duration.test.ts +2 -2
  242. package/src/kernel/stamp-http.test.ts +79 -0
  243. package/src/kernel/stamp-http.ts +75 -0
  244. package/src/kernel/triggers.ts +52 -27
  245. package/src/manifest/mcp-ref.ts +88 -0
  246. package/src/manifest/types.ts +41 -2
  247. package/src/manifest/validate.test.ts +20 -0
  248. package/src/mcp/docs-server.ts +1 -1
  249. package/src/mcp/server.ts +1 -1
  250. package/src/plugins/compression.test.ts +21 -0
  251. package/src/plugins/compression.ts +1 -0
  252. package/src/release/measure.ts +2 -2
  253. package/src/runtime/bun.ts +41 -4
  254. package/src/runtime/json-code-block.test.ts +2 -2
  255. package/src/runtime/serve.test.ts +4 -4
  256. package/src/term.test.ts +52 -4
  257. package/src/term.ts +125 -17
  258. package/src/test/reset-element-registries.ts +10 -3
  259. package/src/cli/ask-seed.test.ts +0 -96
  260. package/src/cli/ask-seed.ts +0 -82
  261. package/src/console/ui-next/dist/assets/cache-glyph-CLPBqZeb.js +0 -1
  262. package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
  263. package/src/console/ui-next/dist/assets/flows-page-Dg8CTE29.js +0 -1
  264. package/src/console/ui-next/dist/assets/http-method-BJ92Z_ke.js +0 -1
  265. package/src/console/ui-next/dist/assets/index-Bp-R7jtM.js +0 -66
  266. package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
  267. package/src/console/ui-next/dist/assets/observability-page-DAnpEaq1.js +0 -4
  268. package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
  269. package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
  270. package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
  271. package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
  272. package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
  273. package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
  274. package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
  275. package/src/console/ui-next/dist/assets/trace-detail-sheet-D16lWQMt.js +0 -2
  276. package/src/console/ui-next/dist/assets/units-page-BtQ0bqMe.js +0 -1
  277. package/src/console/ui-next/dist/assets/vault-page-Ca-MvcmJ.js +0 -2
@@ -8,11 +8,26 @@
8
8
  */
9
9
 
10
10
  import type { AiDriver, AiMessage, AiModelClient, AiToolDef } from "../../drivers/ai-types.ts";
11
+ import { currentAbortSignal, withAbortSignal } from "../../kernel/abort-scope.ts";
12
+ import {
13
+ mcpCapabilityRefFromName,
14
+ mcpModelToolName,
15
+ parseMcpToolRef,
16
+ } from "../../manifest/mcp-ref.ts";
17
+ import { createMcpClient, type McpClient } from "./mcp-client.ts";
18
+ import type { McpTransport } from "./mcp-transport.ts";
11
19
  import type { IndexStore } from "../../drivers/types.ts";
12
20
  import { maskRedactedDeep } from "../../kernel/redacted.ts";
13
21
  import type { GatePolicyContext } from "../gate/declare.ts";
14
22
  import type { GateRuntime } from "../gate/runtime.ts";
15
- import type { AiAgentDecl, AiEmbedDecl, AiModelDecl, AiPromptDecl, AiTimeout } from "./declare.ts";
23
+ import type {
24
+ AiAgentDecl,
25
+ AiEmbedDecl,
26
+ AiMcpServerDecl,
27
+ AiModelDecl,
28
+ AiPromptDecl,
29
+ AiTimeout,
30
+ } from "./declare.ts";
16
31
  import {
17
32
  isRetryableAiError,
18
33
  mergeAskAbortSignal,
@@ -102,7 +117,7 @@ export interface AiFallbackAttempt {
102
117
  }
103
118
 
104
119
  /** Ask outcome class — schema failure is not a provider error. */
105
- export type AiAskOutcome = "ok" | "provider_error" | "schema_invalid";
120
+ export type AiAskOutcome = "ok" | "provider_error" | "schema_invalid" | "budget_exceeded";
106
121
 
107
122
  /** Journal entry for a nondeterministic ask. */
108
123
  export interface AiJournalEntry {
@@ -171,6 +186,11 @@ export interface CreateAiRuntimeOptions {
171
186
  readonly clients?: Readonly<Record<string, AiModelClient>>;
172
187
  /** Default driver when a model has no client (usually mock in dev). */
173
188
  readonly defaultDriver?: AiDriver;
189
+ /**
190
+ * Protocol drivers keyed by id — used when a logical model sets
191
+ * {@link AiModelDecl.driverId} instead of the app default.
192
+ */
193
+ readonly drivers?: Readonly<Record<string, AiDriver>>;
174
194
  /** Gate runtime for agent tool calls. */
175
195
  readonly gates?: GateRuntime;
176
196
  /**
@@ -208,6 +228,12 @@ export interface CreateAiRuntimeOptions {
208
228
  * Nondeterministic contract.
209
229
  */
210
230
  readonly forceJournal?: boolean;
231
+ /** Declared external MCP servers. */
232
+ readonly mcpServers?: readonly AiMcpServerDecl[];
233
+ /** Resolve a vault secret by contract name (MCP bearer). */
234
+ readonly resolveSecret?: (name: string) => string | Promise<string>;
235
+ /** Injected MCP transports keyed by server name (tests). */
236
+ readonly mcpTransports?: Readonly<Record<string, McpTransport>>;
211
237
  }
212
238
 
213
239
  /** Ask options. */
@@ -242,6 +268,8 @@ export interface AiStreamOptions {
242
268
  readonly prompt?: string;
243
269
  readonly data?: unknown;
244
270
  readonly signal?: AbortSignal;
271
+ /** Ordered fallback models after the primary `stream(model)` name. */
272
+ readonly via?: readonly string[];
245
273
  }
246
274
 
247
275
  /** AI runtime surface. */
@@ -291,6 +319,14 @@ export interface AiRuntime {
291
319
  * @param options - Prompt / data / signal
292
320
  */
293
321
  stream(model: string, options?: AiStreamOptions): AsyncIterable<string>;
322
+ /**
323
+ * Call an allowlisted MCP tool (`mcp:<server>/<tool>`).
324
+ *
325
+ * @param ref - Capability ref
326
+ * @param input - Tool arguments
327
+ * @param signal - Cancel
328
+ */
329
+ callMcp(ref: string, input: unknown, signal?: AbortSignal): Promise<unknown>;
294
330
  /**
295
331
  * Embed text into the configured index store.
296
332
  *
@@ -341,6 +377,11 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
341
377
  for (const m of options.models ?? []) models.set(m.name, m);
342
378
 
343
379
  const clients = new Map<string, AiModelClient>(Object.entries(options.clients ?? {}));
380
+ const mcpClient: McpClient = createMcpClient({
381
+ servers: options.mcpServers,
382
+ ...(options.resolveSecret !== undefined ? { resolveSecret: options.resolveSecret } : {}),
383
+ ...(options.mcpTransports !== undefined ? { transports: options.mcpTransports } : {}),
384
+ });
344
385
  const denials: AgentDenial[] = [];
345
386
  const agentRuns: AgentRunRecord[] = [];
346
387
  const journal: AiJournalEntry[] = [];
@@ -351,11 +392,18 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
351
392
  async function clientFor(name: string): Promise<AiModelClient> {
352
393
  const existing = clients.get(name);
353
394
  if (existing) return existing;
354
- if (!options.defaultDriver) {
355
- throw new Error(`ai: no client for model "${name}" and no defaultDriver`);
356
- }
357
395
  const model = models.get(name);
358
- const opened = await options.defaultDriver.open({
396
+ const driver =
397
+ (model?.driverId !== undefined ? options.drivers?.[model.driverId] : undefined) ??
398
+ options.defaultDriver;
399
+ if (!driver) {
400
+ throw new Error(
401
+ model?.driverId
402
+ ? `ai: no driver "${model.driverId}" for model "${name}" and no defaultDriver`
403
+ : `ai: no client for model "${name}" and no defaultDriver`,
404
+ );
405
+ }
406
+ const opened = await driver.open({
359
407
  model: model?.model ?? name,
360
408
  ...(model?.baseUrl !== undefined ? { baseUrl: model.baseUrl } : {}),
361
409
  ...(model?.apiKey !== undefined ? { apiKey: model.apiKey } : {}),
@@ -373,12 +421,26 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
373
421
  return options.effectsForFlow?.(tool) ?? [];
374
422
  }
375
423
 
376
- function toolDefsFor(toolNames: readonly string[]): AiToolDef[] {
377
- return toolNames.map((name) => ({
378
- name,
379
- description: `Flow tool: ${name}`,
380
- parameters: options.toolSchemaForFlow?.(name) ?? { type: "object", properties: {} },
381
- }));
424
+ async function toolDefsFor(toolNames: readonly string[]): Promise<AiToolDef[]> {
425
+ const defs: AiToolDef[] = [];
426
+ for (const name of toolNames) {
427
+ const mcp = parseMcpToolRef(name);
428
+ if (mcp) {
429
+ const listed = await mcpClient.listedTool(mcp);
430
+ defs.push({
431
+ name: mcpModelToolName(mcp.server, mcp.tool),
432
+ description: listed?.description ?? `MCP tool: ${mcp.server}/${mcp.tool}`,
433
+ parameters: listed?.inputSchema ?? { type: "object", properties: {} },
434
+ });
435
+ continue;
436
+ }
437
+ defs.push({
438
+ name,
439
+ description: `Flow tool: ${name}`,
440
+ parameters: options.toolSchemaForFlow?.(name) ?? { type: "object", properties: {} },
441
+ });
442
+ }
443
+ return defs;
382
444
  }
383
445
 
384
446
  async function dispatchTool(opts: {
@@ -392,6 +454,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
392
454
  readonly meta?: GatePolicyContext["meta"];
393
455
  readonly trail: AgentToolStep[];
394
456
  readonly runDenials: AgentDenial[];
457
+ readonly signal?: AbortSignal;
395
458
  }): Promise<unknown> {
396
459
  const {
397
460
  tool,
@@ -404,10 +467,12 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
404
467
  meta,
405
468
  trail,
406
469
  runDenials,
470
+ signal,
407
471
  } = opts;
408
- const effects = effectsFor(tool);
472
+ const capability = mcpCapabilityRefFromName(tool) ?? tool;
473
+ const effects = effectsFor(capability);
409
474
 
410
- if (!allowedTools.has(tool)) {
475
+ if (!allowedTools.has(tool) && !allowedTools.has(capability)) {
411
476
  const denial: AgentDenial = {
412
477
  agent: agentLabel,
413
478
  tool,
@@ -421,7 +486,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
421
486
  throw new Error(`ai: model requested unknown tool "${tool}"`);
422
487
  }
423
488
 
424
- const requiredGates = options.gatesForFlow?.(tool) ?? [];
489
+ const requiredGates = options.gatesForFlow?.(capability) ?? [];
425
490
  if (requiredGates.length > 0 && options.gates) {
426
491
  const ctx: GatePolicyContext = {
427
492
  auth: auth ?? { userId: null, scopes: new Set() },
@@ -460,8 +525,9 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
460
525
  return { error: denial.reason, denied: true };
461
526
  }
462
527
 
463
- const output = await invoke(tool, args);
464
- trail.push({ tool, status: "ok", effects, at: now() });
528
+ const invokeSignal = signal ?? currentAbortSignal();
529
+ const output = await withAbortSignal(invokeSignal, () => invoke(capability, args));
530
+ trail.push({ tool: capability, status: "ok", effects, at: now() });
465
531
  return output;
466
532
  }
467
533
 
@@ -491,8 +557,12 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
491
557
  readonly outputTokens?: number;
492
558
  }> {
493
559
  const messages = [...opts.messages];
494
- const defs = toolDefsFor(opts.tools);
560
+ const defs = await toolDefsFor(opts.tools);
495
561
  const allowed = new Set(opts.tools);
562
+ for (const name of opts.tools) {
563
+ const mcp = parseMcpToolRef(name);
564
+ if (mcp) allowed.add(mcpModelToolName(mcp.server, mcp.tool));
565
+ }
496
566
  const trail: AgentToolStep[] = [];
497
567
  const runDenials: AgentDenial[] = [];
498
568
  let steps = 0;
@@ -551,6 +621,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
551
621
  meta: opts.meta,
552
622
  trail,
553
623
  runDenials,
624
+ ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
554
625
  });
555
626
  lastToolResult = toolResult;
556
627
  messages.push({
@@ -594,7 +665,10 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
594
665
  const version = decl.version;
595
666
  const started = now();
596
667
  const tools = opts?.tools ?? [];
597
- const signal = mergeAskAbortSignal(resolveTimeoutMs(opts?.timeout ?? decl.timeout));
668
+ const signal = mergeAskAbortSignal(
669
+ resolveTimeoutMs(opts?.timeout ?? decl.timeout),
670
+ currentAbortSignal(),
671
+ );
598
672
 
599
673
  // Replay from journal when input matches (nondeterministic contract)
600
674
  if (journalingForced && tools.length === 0) {
@@ -628,6 +702,28 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
628
702
  });
629
703
  };
630
704
 
705
+ const assertAskBudget = (spent: number): void => {
706
+ const cap = decl.budget?.maxCostPerCall;
707
+ if (cap === undefined || spent <= cap) return;
708
+ const message = `ai: prompt "${prompt}" exceeded maxCostPerCall ${cap}`;
709
+ if (journalingForced) {
710
+ pushJournal({
711
+ prompt,
712
+ ...(version !== undefined ? { version } : {}),
713
+ input,
714
+ output: { error: message },
715
+ attempts: [...attempts],
716
+ outcome: "budget_exceeded",
717
+ cost: spent,
718
+ latencyMs: Math.max(0, now() - started),
719
+ at: now(),
720
+ });
721
+ }
722
+ const err = new Error(message);
723
+ err.name = "AiBudgetExceededError";
724
+ throw err;
725
+ };
726
+
631
727
  for (const modelName of via) {
632
728
  let sameModelTries = 0;
633
729
  let advance = true;
@@ -656,6 +752,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
656
752
  attemptCost = loop.cost;
657
753
  totalCost += attemptCost;
658
754
  addUsageTokens(totalTokens, loop);
755
+ assertAskBudget(totalCost);
659
756
  if (loop.denials.length > 0 && loop.trail.every((t) => t.status === "denied")) {
660
757
  throw new Error(
661
758
  `ai: all tool calls denied for prompt "${prompt}": ${loop.denials[0]?.reason}`,
@@ -671,6 +768,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
671
768
  attemptCost = result.usage?.cost ?? 0;
672
769
  totalCost += attemptCost;
673
770
  addUsageTokens(totalTokens, result.usage);
771
+ assertAskBudget(totalCost);
674
772
  // Prefer assistant text — `raw` is often the transport envelope
675
773
  // (OpenAI chat.completion object), which must not shadow the content.
676
774
  raw =
@@ -815,7 +913,14 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
815
913
  auth: runOpts.auth,
816
914
  operator: runOpts.operator,
817
915
  meta: runOpts.meta,
916
+ signal: currentAbortSignal(),
818
917
  });
918
+ const runCap = decl.budget?.maxCostPerRun;
919
+ if (runCap !== undefined && loop.cost > runCap) {
920
+ const err = new Error(`ai: agent "${agent}" exceeded maxCostPerRun ${runCap}`);
921
+ err.name = "AiBudgetExceededError";
922
+ throw err;
923
+ }
819
924
 
820
925
  const record: AgentRunRecord = {
821
926
  id: `agent-run-${++runSeq}`,
@@ -842,21 +947,46 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
842
947
  },
843
948
 
844
949
  async *stream(model, streamOpts) {
845
- const client = await clientFor(model);
846
- if (!client.stream) {
847
- throw new Error(`ai: model "${model}" (driver ${client.driverId}) does not support stream`);
848
- }
950
+ const via = [model, ...(streamOpts?.via ?? []).filter((name) => name !== model)];
849
951
  const content =
850
952
  streamOpts?.data !== undefined
851
953
  ? promptContentFromInput(streamOpts.data)
852
954
  : promptContentFromInput(streamOpts?.prompt ?? "");
853
- for await (const chunk of client.stream({
854
- model: wireModel(model, client),
855
- messages: [{ role: "user", content }],
856
- signal: streamOpts?.signal,
857
- })) {
858
- if (chunk.text) yield chunk.text;
955
+ let lastError: unknown;
956
+ for (const modelName of via) {
957
+ let yielded = false;
958
+ try {
959
+ const client = await clientFor(modelName);
960
+ if (!client.stream) {
961
+ throw new Error(
962
+ `ai: model "${modelName}" (driver ${client.driverId}) does not support stream`,
963
+ );
964
+ }
965
+ for await (const chunk of client.stream({
966
+ model: wireModel(modelName, client),
967
+ messages: [{ role: "user", content }],
968
+ signal: streamOpts?.signal,
969
+ })) {
970
+ if (chunk.text) {
971
+ yielded = true;
972
+ yield chunk.text;
973
+ }
974
+ }
975
+ return;
976
+ } catch (err) {
977
+ lastError = err;
978
+ if (yielded || !isRetryableAiError(err)) {
979
+ throw err instanceof Error ? err : new Error(String(err));
980
+ }
981
+ }
859
982
  }
983
+ throw lastError instanceof Error
984
+ ? lastError
985
+ : new Error(`ai: all models failed to stream: ${String(lastError)}`);
986
+ },
987
+
988
+ async callMcp(ref, input, signal) {
989
+ return mcpClient.call(ref, input, signal ?? currentAbortSignal());
860
990
  },
861
991
 
862
992
  async embed(embedName, id, text) {
@@ -51,6 +51,21 @@ describe("ai declaration", () => {
51
51
  });
52
52
  expect(embed.into).toBe("kb");
53
53
  });
54
+
55
+ test("mcpServer requires allowlist and .tool() NamedRef", () => {
56
+ const github = ai.mcpServer("github", {
57
+ url: "https://mcp.example/github",
58
+ tools: ["create_issue"],
59
+ });
60
+ expect(github.kind).toBe("mcp-server");
61
+ expect(github.tools).toEqual(["create_issue"]);
62
+ expect(github.tool("create_issue")).toEqual({ name: "mcp:github/create_issue" });
63
+ expect(() => github.tool("delete_repo")).toThrow(/not in the allowlist/);
64
+ expect(() => ai.mcpServer("broken", { url: "https://x" } as never)).toThrow(/allowlist/);
65
+ expect(() => ai.mcpServer("both", { url: "https://x", command: "npx", tools: ["a"] })).toThrow(
66
+ /exactly one/,
67
+ );
68
+ });
54
69
  });
55
70
 
56
71
  describe("agent gate denial is recorded", () => {
@@ -576,3 +591,127 @@ describe("ask journal tokens", () => {
576
591
  expect(entry.cost).toBe(0);
577
592
  });
578
593
  });
594
+
595
+ describe("ask budget and cancel", () => {
596
+ test("maxCostPerCall stops a successful over-budget ask", async () => {
597
+ const smart = ai.model("smart", { provider: "mock" });
598
+ const prompt = smart.prompt("pricey", { budget: { maxCostPerCall: 0.01 } });
599
+ const runtime = createAiRuntime({
600
+ models: [smart],
601
+ prompts: [prompt],
602
+ clients: {
603
+ smart: {
604
+ driverId: "mock",
605
+ model: "smart",
606
+ async complete() {
607
+ return {
608
+ text: "{}",
609
+ raw: {},
610
+ model: "smart",
611
+ driverId: "mock",
612
+ usage: { cost: 0.5 },
613
+ };
614
+ },
615
+ },
616
+ },
617
+ });
618
+ await expect(runtime.ask("pricey", {})).rejects.toThrow(/maxCostPerCall/);
619
+ expect(runtime.journal[0]!.outcome).toBe("budget_exceeded");
620
+ });
621
+
622
+ test("disconnect AbortError does not advance via", async () => {
623
+ let localCalls = 0;
624
+ const smart = ai.model("smart", { provider: "mock" });
625
+ const local = ai.model("local", { provider: "mock" });
626
+ const prompt = smart.prompt("cancel-me", { via: ["smart", "local"] });
627
+ const runtime = createAiRuntime({
628
+ models: [smart, local],
629
+ prompts: [prompt],
630
+ clients: {
631
+ smart: {
632
+ driverId: "mock",
633
+ model: "smart",
634
+ async complete() {
635
+ const err = new Error("aborted");
636
+ err.name = "AbortError";
637
+ throw err;
638
+ },
639
+ },
640
+ local: {
641
+ driverId: "mock",
642
+ model: "local",
643
+ async complete() {
644
+ localCalls++;
645
+ return { text: "{}", raw: {}, model: "local", driverId: "mock" };
646
+ },
647
+ },
648
+ },
649
+ });
650
+ await expect(runtime.ask("cancel-me", {})).rejects.toThrow(/aborted/);
651
+ expect(localCalls).toBe(0);
652
+ });
653
+
654
+ test("driverId opens the matching protocol driver", async () => {
655
+ let opened = "";
656
+ const local = ai.model("local", { driverId: "ollama", model: "llama" });
657
+ const prompt = local.prompt("ping");
658
+ const runtime = createAiRuntime({
659
+ models: [local],
660
+ prompts: [prompt],
661
+ drivers: {
662
+ ollama: {
663
+ id: "ollama",
664
+ async open() {
665
+ opened = "ollama";
666
+ return {
667
+ driverId: "ollama",
668
+ model: "llama",
669
+ async complete() {
670
+ return { text: "{}", raw: {}, model: "llama", driverId: "ollama" };
671
+ },
672
+ };
673
+ },
674
+ },
675
+ },
676
+ });
677
+ await runtime.ask("ping", {});
678
+ expect(opened).toBe("ollama");
679
+ });
680
+ });
681
+
682
+ describe("stream via", () => {
683
+ test("advances to the next model when the first stream fails before a chunk", async () => {
684
+ const smart = ai.model("smart");
685
+ const local = ai.model("local");
686
+ const runtime = createAiRuntime({
687
+ models: [smart, local],
688
+ clients: {
689
+ smart: {
690
+ driverId: "mock",
691
+ model: "smart",
692
+ async complete() {
693
+ return { text: "", model: "smart", driverId: "mock" };
694
+ },
695
+ async *stream() {
696
+ throw new Error("smart stream down");
697
+ },
698
+ },
699
+ local: {
700
+ driverId: "mock",
701
+ model: "local",
702
+ async complete() {
703
+ return { text: "", model: "local", driverId: "mock" };
704
+ },
705
+ async *stream() {
706
+ yield { text: "ok" };
707
+ },
708
+ },
709
+ },
710
+ });
711
+ const parts: string[] = [];
712
+ for await (const c of runtime.stream("smart", { prompt: "hi", via: ["local"] })) {
713
+ parts.push(c);
714
+ }
715
+ expect(parts.join("")).toBe("ok");
716
+ });
717
+ });
@@ -19,6 +19,10 @@ export type {
19
19
  AiBudgetDecl,
20
20
  AiEmbedDecl,
21
21
  AiEmbedOptions,
22
+ AiMcpServerAuthOptions,
23
+ AiMcpServerDecl,
24
+ AiMcpServerOptions,
25
+ AiMcpToolRef,
22
26
  AiModelDecl,
23
27
  AiModelOptions,
24
28
  AiPromptDecl,
@@ -58,6 +62,18 @@ export type {
58
62
  CreateAiRuntimeOptions,
59
63
  } from "./ai/runtime.ts";
60
64
 
65
+ export { createMcpClient } from "./ai/mcp-client.ts";
66
+ export type { CreateMcpClientOptions, McpClient, McpResolvedTool } from "./ai/mcp-client.ts";
67
+ export { createMockMcpTransport } from "./ai/mcp-mock.ts";
68
+ export type { CreateMockMcpTransportOptions, MockMcpTool } from "./ai/mcp-mock.ts";
69
+ export {
70
+ MCP_CLIENT_PROTOCOL_VERSION,
71
+ MCP_LEGACY_PROTOCOL_VERSION,
72
+ mcpClientMeta,
73
+ parseJsonRpcResponse,
74
+ } from "./ai/mcp-protocol.ts";
75
+ export type { McpListedTool, McpProtocolEra } from "./ai/mcp-protocol.ts";
76
+
61
77
  export { AI_RATE_PRESETS, aiRateGate, createAiRateGates } from "./ai/rate.ts";
62
78
  export type { AiRatePreset } from "./ai/rate.ts";
63
79
 
@@ -5,6 +5,8 @@
5
5
  * the effective state from the Store (console §5), never the code directly.
6
6
  */
7
7
 
8
+ import { clockRegistry } from "../../kernel/element-registries.ts";
9
+
8
10
  /** Options for {@link clock}. */
9
11
  export interface ClockOptions {
10
12
  /** Cron expression (`m h dom mon dow`). */
@@ -40,6 +42,27 @@ export interface ClockDecl {
40
42
  readonly description?: string;
41
43
  }
42
44
 
45
+ /**
46
+ * `clock()` pushes into the shared {@link clockRegistry}
47
+ * (`src/kernel/element-registries.ts`) so {@link oke} can auto-populate
48
+ * `clocks` with zero explicit array — mirrors the {@link on} trigger-drain
49
+ * registry (`src/kernel/on.ts`).
50
+ *
51
+ * Snapshot of every clock declared since the last reset.
52
+ */
53
+ export function listClocks(): readonly ClockDecl[] {
54
+ return clockRegistry.slice();
55
+ }
56
+
57
+ /**
58
+ * Clear the clock registry (tests / fresh app adopt).
59
+ *
60
+ * @internal
61
+ */
62
+ export function resetClocks(): void {
63
+ clockRegistry.length = 0;
64
+ }
65
+
43
66
  /**
44
67
  * Declare a named clock / cron schedule.
45
68
  *
@@ -50,7 +73,7 @@ export function clock(name: string, options: ClockOptions = {}): ClockDecl {
50
73
  if (!options.cron && !options.every) {
51
74
  throw new TypeError(`clock("${name}"): require cron or every`);
52
75
  }
53
- return {
76
+ const decl: ClockDecl = {
54
77
  name,
55
78
  cron: options.cron,
56
79
  every: options.every,
@@ -58,4 +81,6 @@ export function clock(name: string, options: ClockOptions = {}): ClockDecl {
58
81
  overridable: options.overridable ?? false,
59
82
  ...(options.description !== undefined ? { description: options.description } : {}),
60
83
  };
84
+ clockRegistry.push(decl);
85
+ return decl;
61
86
  }
@@ -100,4 +100,47 @@ describe("nextOccurrences", () => {
100
100
  expect(fires.length).toBeGreaterThanOrEqual(1);
101
101
  expect(fires.every((t) => t >= from && t < until)).toBe(true);
102
102
  });
103
+
104
+ test("*/15 and @hourly via Bun.cron.parse", () => {
105
+ const from = Date.UTC(2024, 0, 1, 0, 0, 0);
106
+ const until = from + 60 * 60_000;
107
+ const quarters = nextOccurrences(
108
+ row({
109
+ name: "quarters",
110
+ effectiveCron: "*/15 * * * *",
111
+ timezone: "UTC",
112
+ }),
113
+ from,
114
+ until,
115
+ );
116
+ expect(quarters).toEqual([from, from + 15 * 60_000, from + 30 * 60_000, from + 45 * 60_000]);
117
+
118
+ const hours = nextOccurrences(
119
+ row({
120
+ name: "hourly",
121
+ effectiveCron: "@hourly",
122
+ timezone: "UTC",
123
+ }),
124
+ from,
125
+ from + 3 * 3_600_000,
126
+ );
127
+ expect(hours).toEqual([from, from + 3_600_000, from + 2 * 3_600_000]);
128
+ });
129
+
130
+ test("cron timezone is the declared IANA zone", () => {
131
+ const from = Date.UTC(2024, 0, 1, 0, 0, 0);
132
+ const until = from + 36 * 3_600_000;
133
+ const fires = nextOccurrences(
134
+ row({
135
+ name: "ny",
136
+ effectiveCron: "0 9 * * *",
137
+ timezone: "America/New_York",
138
+ }),
139
+ from,
140
+ until,
141
+ );
142
+ expect(fires).toHaveLength(1);
143
+ // 09:00 America/New_York on 2024-01-01 is 14:00 UTC (EST).
144
+ expect(fires[0]).toBe(Date.UTC(2024, 0, 1, 14, 0, 0));
145
+ });
103
146
  });