okengine 0.22.0 → 0.23.1

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 (312) hide show
  1. package/AGENTS.md +7 -0
  2. package/manifest.v1.schema.json +44 -0
  3. package/package.json +32 -19
  4. package/site/content/docs/ai/mcp.mdx +14 -10
  5. package/site/content/docs/ai/skills.mdx +1 -1
  6. package/site/content/docs/elements/ai/agents.mdx +61 -13
  7. package/site/content/docs/elements/ai/decide.mdx +536 -0
  8. package/site/content/docs/elements/ai/events.mdx +373 -0
  9. package/site/content/docs/elements/ai/index.mdx +9 -3
  10. package/site/content/docs/elements/ai/meta.json +1 -1
  11. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  12. package/site/content/docs/elements/channel/index.mdx +4 -4
  13. package/site/content/docs/elements/channel/receipts.mdx +7 -5
  14. package/site/content/docs/elements/channel/sms.mdx +2 -1
  15. package/site/content/docs/elements/flow/index.mdx +22 -18
  16. package/site/content/docs/elements/gate/auth.mdx +4 -1
  17. package/site/content/docs/elements/signal/index.mdx +15 -14
  18. package/site/content/docs/elements/store/index.mdx +1 -1
  19. package/site/content/docs/elements/store/kv.mdx +5 -5
  20. package/site/content/docs/plugins/otp.mdx +3 -3
  21. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  22. package/site/content/docs/recipes/index.mdx +6 -6
  23. package/site/content/docs/recipes/postgres.mdx +12 -8
  24. package/site/content/docs/recipes/redis.mdx +4 -4
  25. package/site/content/docs/reference/cli.mdx +2 -1
  26. package/site/content/docs/reference/configuration.mdx +9 -7
  27. package/site/content/docs/reference/fx.mdx +19 -10
  28. package/src/auth/api-key-sql.test.ts +44 -0
  29. package/src/auth/api-key-sql.ts +1 -0
  30. package/src/auth/api-keys.ts +30 -12
  31. package/src/auth/config.ts +6 -2
  32. package/src/auth/gate-auth.test.ts +20 -0
  33. package/src/auth/tenants.ts +17 -0
  34. package/src/cli/agents-md.test.ts +25 -0
  35. package/src/cli/ask-vault-gaps.test.ts +38 -1
  36. package/src/cli/ask-vault-gaps.ts +35 -2
  37. package/src/cli/decide.ts +174 -0
  38. package/src/cli/decision-lock-watch.test.ts +45 -0
  39. package/src/cli/decision-lock-watch.ts +51 -0
  40. package/src/cli/dev-app-runner.ts +34 -8
  41. package/src/cli/dev-auth-secret.test.ts +49 -0
  42. package/src/cli/dev-auth-secret.ts +49 -0
  43. package/src/cli/dev-hot.test.ts +41 -0
  44. package/src/cli/dev-hot.ts +48 -0
  45. package/src/cli/dev.test.ts +1 -1
  46. package/src/cli/dev.ts +24 -6
  47. package/src/cli/doctor-diff.test.ts +29 -1
  48. package/src/cli/doctor-diff.ts +35 -0
  49. package/src/cli/eval.ts +142 -1
  50. package/src/cli/index.ts +5 -0
  51. package/src/cli/load-config.images.test.ts +2 -2
  52. package/src/cli/load-config.ts +1 -1
  53. package/src/cli/manifest-pr-diff.ts +16 -0
  54. package/src/cli/mcp-from-console.ts +48 -0
  55. package/src/cli/registry.ts +35 -1
  56. package/src/client/agent.ts +307 -0
  57. package/src/client/transport.test.ts +20 -0
  58. package/src/client/transport.ts +2 -1
  59. package/src/client/types.ts +1 -1
  60. package/src/client-react/index.ts +7 -1
  61. package/src/client-react/use-agent-run.test.ts +271 -0
  62. package/src/client-react/use-agent-run.ts +193 -0
  63. package/src/compiler/ai-approval.test.ts +36 -0
  64. package/src/compiler/ai-repair.test.ts +22 -0
  65. package/src/compiler/decisions.extract.test.ts +164 -0
  66. package/src/compiler/effects-fetch.test.ts +77 -0
  67. package/src/compiler/effects-infer.ts +114 -17
  68. package/src/compiler/effects-join.test.ts +48 -0
  69. package/src/compiler/extract.test.ts +28 -0
  70. package/src/compiler/extract.ts +279 -13
  71. package/src/compiler/fx-index.ts +58 -4
  72. package/src/compiler/response.ts +35 -2
  73. package/src/config/index.ts +21 -3
  74. package/src/console/index.ts +1 -1
  75. package/src/console/server/ai-runs-flows.ts +269 -0
  76. package/src/console/server/ai-runs.test.ts +323 -0
  77. package/src/console/server/ai-runs.ts +562 -0
  78. package/src/console/server/ai.ts +12 -1
  79. package/src/console/server/app.ts +10 -1
  80. package/src/console/server/decisions-flows.ts +157 -0
  81. package/src/console/server/decisions.test.ts +66 -0
  82. package/src/console/server/decisions.ts +229 -0
  83. package/src/console/server/flows.ts +50 -2
  84. package/src/console/server/invoke-user-flow.ts +9 -0
  85. package/src/console/server/runs-ingest.ts +16 -0
  86. package/src/console/server/serve.ts +5 -0
  87. package/src/console/server/state.ts +25 -1
  88. package/src/console/server/store-stats.test.ts +47 -0
  89. package/src/console/server/store-stats.ts +92 -18
  90. package/src/console/ui-next/dist/assets/access-page-C_N4qvai.js +4 -0
  91. package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-Cq1TxniW.js} +1 -1
  92. package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-BaI4uxZz.js} +1 -1
  93. package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DNyK7IIY.js} +1 -1
  94. package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-CRhBJMZR.js} +1 -1
  95. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  96. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  97. package/src/console/ui-next/dist/assets/decisions-page-D9twj8Xy.js +1 -0
  98. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  99. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  100. package/src/console/ui-next/dist/assets/duration-tone-CsHWlVPU.js +9 -0
  101. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  102. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  103. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  104. package/src/console/ui-next/dist/assets/flows-page-ZaTJAhR9.js +1 -0
  105. package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-DQ-EMAfz.js} +1 -1
  106. package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-Cn_Fl79s.js} +1 -1
  107. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  108. package/src/console/ui-next/dist/assets/index-CmRFeWdT.js +58 -0
  109. package/src/console/ui-next/dist/assets/observability-page-Cn6R_dXz.js +8 -0
  110. package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bx6EZhSe.js} +7 -7
  111. package/src/console/ui-next/dist/assets/request-meta-DmdiUrfh.js +1 -0
  112. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  113. package/src/console/ui-next/dist/assets/store-page-8P6vGXnn.js +41 -0
  114. package/src/console/ui-next/dist/assets/trace-detail-sheet-BfGvpFSv.js +2 -0
  115. package/src/console/ui-next/dist/assets/tree-expand-toggle-CWdUHbc0.js +55 -0
  116. package/src/console/ui-next/dist/assets/units-page-CZOFZzay.js +1 -0
  117. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  118. package/src/console/ui-next/dist/assets/vault-page-ki_ishmX.js +2 -0
  119. package/src/console/ui-next/dist/index.html +4 -3
  120. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  121. package/src/console/ui-next/src/client.ts +224 -1
  122. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  123. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  124. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  125. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  126. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  127. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  128. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  129. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  130. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  131. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  132. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  133. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  134. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  135. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  136. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  137. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  138. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  139. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  140. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  141. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  142. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  143. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  144. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  145. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  146. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  147. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  148. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  149. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  150. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  151. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  152. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
  153. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  154. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  155. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  156. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  157. package/src/console/ui-next/src/router.tsx +17 -0
  158. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  159. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  160. package/src/console/ui-next/vite.config.ts +9 -0
  161. package/src/docker/recipes/dragonfly.ts +3 -2
  162. package/src/docker/recipes/redis.ts +1 -1
  163. package/src/drivers/ai-anthropic.ts +212 -21
  164. package/src/drivers/ai-mock.ts +34 -1
  165. package/src/drivers/ai-openai-compatible.ts +67 -26
  166. package/src/drivers/ai-preconnect.ts +18 -0
  167. package/src/drivers/ai-stream.test.ts +62 -0
  168. package/src/drivers/ai-types.ts +20 -0
  169. package/src/drivers/journal-postgres.test.ts +22 -1
  170. package/src/drivers/journal-postgres.ts +511 -13
  171. package/src/drivers/memory-ddl.test.ts +18 -0
  172. package/src/drivers/memory.ts +21 -1
  173. package/src/drivers/pg-rls.test.ts +31 -0
  174. package/src/drivers/pg-rls.ts +15 -0
  175. package/src/drivers/postgres.test.ts +9 -0
  176. package/src/drivers/postgres.ts +49 -11
  177. package/src/drivers/signal-compete.test.ts +234 -0
  178. package/src/drivers/signal-postgres.ts +39 -19
  179. package/src/drivers/signal-redis.ts +167 -11
  180. package/src/drivers/signal-types.ts +12 -0
  181. package/src/elements/ai/agent-event-slot.ts +27 -0
  182. package/src/elements/ai/approval-http.ts +207 -0
  183. package/src/elements/ai/approval.test.ts +741 -0
  184. package/src/elements/ai/approval.ts +228 -0
  185. package/src/elements/ai/decisions/bind.ts +163 -0
  186. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  187. package/src/elements/ai/decisions/certificate.ts +377 -0
  188. package/src/elements/ai/decisions/certify.ts +314 -0
  189. package/src/elements/ai/decisions/decide.test.ts +953 -0
  190. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  191. package/src/elements/ai/decisions/export.test.ts +120 -0
  192. package/src/elements/ai/decisions/export.ts +146 -0
  193. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  194. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  195. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  196. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  197. package/src/elements/ai/decisions/http.test.ts +154 -0
  198. package/src/elements/ai/decisions/http.ts +240 -0
  199. package/src/elements/ai/decisions/labels.test.ts +282 -0
  200. package/src/elements/ai/decisions/labels.ts +257 -0
  201. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  202. package/src/elements/ai/decisions/openrouter.ts +36 -0
  203. package/src/elements/ai/decisions/provider.ts +102 -0
  204. package/src/elements/ai/decisions/typesafe.ts +36 -0
  205. package/src/elements/ai/declare.ts +252 -1
  206. package/src/elements/ai/events.test.ts +379 -0
  207. package/src/elements/ai/events.ts +138 -0
  208. package/src/elements/ai/mcp-protocol.ts +5 -6
  209. package/src/elements/ai/run-events.test.ts +461 -0
  210. package/src/elements/ai/run-events.ts +522 -0
  211. package/src/elements/ai/runtime.ts +1187 -105
  212. package/src/elements/ai/stream-turn.ts +214 -0
  213. package/src/elements/ai/stream.live.test.ts +76 -0
  214. package/src/elements/ai/subagent.test.ts +136 -0
  215. package/src/elements/ai.test.ts +497 -3
  216. package/src/elements/ai.ts +16 -1
  217. package/src/elements/channel/runtime.ts +11 -0
  218. package/src/elements/channel/sql-ledger.ts +251 -0
  219. package/src/elements/clock/durable.ts +27 -1
  220. package/src/elements/signal/runtime.ts +9 -0
  221. package/src/elements/signal.test.ts +1 -0
  222. package/src/elements/store/cache.test.ts +120 -15
  223. package/src/elements/store/cache.ts +200 -39
  224. package/src/elements/store/prepare-row.test.ts +6 -2
  225. package/src/elements/store/sql-session.test.ts +53 -0
  226. package/src/elements/store/sql-session.ts +206 -10
  227. package/src/elements/store.ts +1 -0
  228. package/src/i18n/catalogs/ar.ts +5 -0
  229. package/src/i18n/catalogs/en.ts +5 -0
  230. package/src/index.ts +2 -0
  231. package/src/kernel/agent-event-store.ts +448 -0
  232. package/src/kernel/app.ts +135 -17
  233. package/src/kernel/auto-cache.test.ts +228 -0
  234. package/src/kernel/boot-bind/ai.ts +3 -0
  235. package/src/kernel/boot-bind/channel.ts +63 -2
  236. package/src/kernel/boot-bind/honor-config.test.ts +40 -10
  237. package/src/kernel/boot-bind/signal.ts +61 -25
  238. package/src/kernel/boot-bind/store.test.ts +26 -0
  239. package/src/kernel/boot-bind/store.ts +82 -6
  240. package/src/kernel/boot.ts +31 -6
  241. package/src/kernel/builtin-errors.ts +2 -0
  242. package/src/kernel/capability.ts +3 -0
  243. package/src/kernel/decision-label-store.ts +441 -0
  244. package/src/kernel/effects.test.ts +2 -1
  245. package/src/kernel/effects.ts +13 -1
  246. package/src/kernel/element-registries.ts +3 -0
  247. package/src/kernel/errors-compiler.ts +11 -0
  248. package/src/kernel/errors-text.ts +20 -0
  249. package/src/kernel/errors.ts +10 -0
  250. package/src/kernel/flow.test.ts +2 -3
  251. package/src/kernel/flow.ts +33 -3
  252. package/src/kernel/fx-call-types.test.ts +32 -0
  253. package/src/kernel/fx-decide.ts +765 -0
  254. package/src/kernel/fx-sql-handle.ts +46 -7
  255. package/src/kernel/fx.test.ts +13 -8
  256. package/src/kernel/fx.ts +339 -45
  257. package/src/kernel/http-frame.test.ts +94 -0
  258. package/src/kernel/http-frame.ts +203 -0
  259. package/src/kernel/idempotency.test.ts +2 -1
  260. package/src/kernel/journal.test.ts +114 -0
  261. package/src/kernel/journal.ts +471 -51
  262. package/src/kernel/json-result.ts +10 -0
  263. package/src/kernel/pipeline-tenant.ts +5 -1
  264. package/src/kernel/pipeline.test.ts +45 -0
  265. package/src/kernel/pipeline.ts +5 -0
  266. package/src/kernel/signal-tx.ts +28 -0
  267. package/src/kernel/sse-id.ts +32 -0
  268. package/src/kernel/store-transaction.test.ts +95 -0
  269. package/src/manifest/diff.ts +1 -0
  270. package/src/manifest/types.ts +29 -2
  271. package/src/mcp/ai-tools.test.ts +202 -0
  272. package/src/mcp/authorization.ts +66 -0
  273. package/src/mcp/docs-server.ts +19 -2
  274. package/src/mcp/mcp.test.ts +83 -0
  275. package/src/mcp/protocol.ts +8 -2
  276. package/src/mcp/server.ts +23 -2
  277. package/src/mcp/session.ts +3 -0
  278. package/src/mcp/tools.ts +168 -0
  279. package/src/mcp/versions.ts +60 -0
  280. package/src/plugins/index.ts +1 -0
  281. package/src/plugins/mena.ts +56 -0
  282. package/src/release/http-graph.test.ts +2 -0
  283. package/src/release/http-graph.ts +5 -0
  284. package/src/release/measure.ts +39 -14
  285. package/src/runs/collect.ts +4 -1
  286. package/src/runs/parquet.test.ts +27 -1
  287. package/src/runs/parquet.ts +19 -0
  288. package/src/runs/types.ts +31 -0
  289. package/src/runtime/json-code-block.test.ts +106 -47
  290. package/src/runtime/json-code-block.ts +1209 -46
  291. package/src/term.test.ts +13 -0
  292. package/src/term.ts +81 -49
  293. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  294. package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
  295. package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
  296. package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
  297. package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
  298. package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
  299. package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
  300. package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
  301. package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
  302. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  303. package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
  304. package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
  305. package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
  306. package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
  307. package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
  308. package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
  309. package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
  310. package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
  311. package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
  312. package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
@@ -10,9 +10,12 @@
10
10
 
11
11
  import { describe, expect, test } from "bun:test";
12
12
  import { createMockAiDriver, memoryIndexDriver, mockAiDriver } from "../drivers/index.ts";
13
+ import { withAbortSignal } from "../kernel/abort-scope.ts";
13
14
  import { createFx } from "../kernel/fx.ts";
15
+ import { createJournal, createMemoryJournalStore } from "../kernel/journal.ts";
14
16
  import {
15
17
  ai,
18
+ AI_OBSERVABILITY_LIMIT,
16
19
  AiPiiBuildError,
17
20
  AiSchemaValidationError,
18
21
  assertAllowPiiForAsk,
@@ -132,7 +135,8 @@ describe("agent gate denial is recorded", () => {
132
135
  message: "status?",
133
136
  auth: { userId: "u1", scopes: new Set(), verified: true },
134
137
  });
135
- expect(result.ok).toBe(true);
138
+ expect(result.stopReason).toBe("max_steps");
139
+ expect(result.ok).toBe(false);
136
140
  expect(result.denials).toHaveLength(0);
137
141
  expect(result.output).toEqual({ booking: "B1" });
138
142
  });
@@ -189,8 +193,260 @@ describe("pii to third-party model fails the build", () => {
189
193
  });
190
194
  });
191
195
 
196
+ describe("agent maxCostPerRun", () => {
197
+ test("a model call over the cap is the last call and the run resolves", async () => {
198
+ let calls = 0;
199
+ const agent = ai.agent("budget-agent", {
200
+ model: "smart",
201
+ tools: ["orders.get"],
202
+ maxSteps: 4,
203
+ budget: { maxCostPerRun: 0.5 },
204
+ });
205
+ const called: string[] = [];
206
+ const runtime = createAiRuntime({
207
+ models: [ai.model("smart")],
208
+ agents: [agent],
209
+ clients: {
210
+ smart: {
211
+ driverId: "mock",
212
+ model: "smart",
213
+ async complete() {
214
+ calls++;
215
+ return {
216
+ text: "",
217
+ raw: {},
218
+ model: "smart",
219
+ driverId: "mock",
220
+ usage: { cost: 0.8 },
221
+ toolCalls: [{ id: "c1", name: "orders.get", arguments: {} }],
222
+ };
223
+ },
224
+ },
225
+ },
226
+ callFlow: async (name) => {
227
+ called.push(name);
228
+ return { ok: true };
229
+ },
230
+ });
231
+ const result = await runtime.runAgent("budget-agent", { message: "look up" });
232
+ expect(calls).toBe(1);
233
+ expect(called).toEqual([]);
234
+ expect(result.ok).toBe(false);
235
+ expect(result.stopReason).toBe("budget");
236
+ expect(result.cost).toBe(0.8);
237
+ });
238
+
239
+ test("tools run while under the cap, and the loop stops once a later call crosses it", async () => {
240
+ let calls = 0;
241
+ const agent = ai.agent("budget-round", {
242
+ model: "smart",
243
+ tools: ["orders.get"],
244
+ maxSteps: 4,
245
+ budget: { maxCostPerRun: 0.5 },
246
+ });
247
+ const called: string[] = [];
248
+ const runtime = createAiRuntime({
249
+ models: [ai.model("smart")],
250
+ agents: [agent],
251
+ clients: {
252
+ smart: {
253
+ driverId: "mock",
254
+ model: "smart",
255
+ async complete() {
256
+ calls++;
257
+ return {
258
+ text: calls === 1 ? "" : "done",
259
+ raw: {},
260
+ model: "smart",
261
+ driverId: "mock",
262
+ usage: { cost: calls === 1 ? 0.4 : 0.4 },
263
+ ...(calls === 1
264
+ ? { toolCalls: [{ id: "c1", name: "orders.get", arguments: {} }] }
265
+ : {}),
266
+ };
267
+ },
268
+ },
269
+ },
270
+ callFlow: async (name) => {
271
+ called.push(name);
272
+ return { ok: true };
273
+ },
274
+ });
275
+ const result = await runtime.runAgent("budget-round", { message: "look up" });
276
+ expect(called).toEqual(["orders.get"]);
277
+ expect(calls).toBe(2);
278
+ expect(result.ok).toBe(false);
279
+ expect(result.stopReason).toBe("budget");
280
+ expect(result.cost).toBeCloseTo(0.8);
281
+ });
282
+
283
+ test("maxSteps is not a normal completion", async () => {
284
+ const agent = ai.agent("step-cap", {
285
+ model: "smart",
286
+ tools: ["orders.get"],
287
+ maxSteps: 1,
288
+ });
289
+ const runtime = createAiRuntime({
290
+ models: [ai.model("smart")],
291
+ agents: [agent],
292
+ clients: {
293
+ smart: {
294
+ driverId: "mock",
295
+ model: "smart",
296
+ async complete() {
297
+ return {
298
+ text: "",
299
+ raw: {},
300
+ model: "smart",
301
+ driverId: "mock",
302
+ toolCalls: [{ id: "c1", name: "orders.get", arguments: {} }],
303
+ };
304
+ },
305
+ },
306
+ },
307
+ callFlow: async () => ({ ok: true }),
308
+ });
309
+ const result = await runtime.runAgent("step-cap", { message: "go" });
310
+ expect(result.ok).toBe(false);
311
+ expect(result.stopReason).toBe("max_steps");
312
+ expect(result.steps).toBe(1);
313
+ });
314
+
315
+ test("a denial fed back to the model does not terminate as denied", async () => {
316
+ let calls = 0;
317
+ const member = gate.policy("member", () => false);
318
+ const agent = ai.agent("deny-continue", {
319
+ model: "smart",
320
+ tools: ["orders.refund"],
321
+ maxSteps: 2,
322
+ });
323
+ const runtime = createAiRuntime({
324
+ models: [ai.model("smart")],
325
+ agents: [agent],
326
+ gates: createGateRuntime({ gates: [member] }),
327
+ gatesForFlow: () => ["member"],
328
+ clients: {
329
+ smart: {
330
+ driverId: "mock",
331
+ model: "smart",
332
+ async complete() {
333
+ calls++;
334
+ if (calls === 1) {
335
+ return {
336
+ text: "",
337
+ raw: {},
338
+ model: "smart",
339
+ driverId: "mock",
340
+ toolCalls: [{ id: "c1", name: "orders.refund", arguments: {} }],
341
+ };
342
+ }
343
+ return { text: "done", raw: { done: true }, model: "smart", driverId: "mock" };
344
+ },
345
+ },
346
+ },
347
+ callFlow: async () => ({ ok: true }),
348
+ });
349
+ const result = await runtime.runAgent("deny-continue", {
350
+ message: "refund",
351
+ auth: { userId: "u1", scopes: new Set(), verified: false },
352
+ });
353
+ expect(calls).toBe(2);
354
+ expect(result.stopReason).toBe("completed");
355
+ expect(result.denials).toHaveLength(1);
356
+ expect(result.ok).toBe(false);
357
+ });
358
+
359
+ test("an unknown tool terminates the run as denied", async () => {
360
+ const agent = ai.agent("unknown-tool", {
361
+ model: "smart",
362
+ tools: ["orders.get"],
363
+ maxSteps: 2,
364
+ });
365
+ const runtime = createAiRuntime({
366
+ models: [ai.model("smart")],
367
+ agents: [agent],
368
+ clients: {
369
+ smart: {
370
+ driverId: "mock",
371
+ model: "smart",
372
+ async complete() {
373
+ return {
374
+ text: "",
375
+ raw: {},
376
+ model: "smart",
377
+ driverId: "mock",
378
+ toolCalls: [{ id: "c1", name: "nope", arguments: {} }],
379
+ };
380
+ },
381
+ },
382
+ },
383
+ callFlow: async () => ({ ok: true }),
384
+ });
385
+ const result = await runtime.runAgent("unknown-tool", { message: "go" });
386
+ expect(result.stopReason).toBe("denied");
387
+ expect(result.ok).toBe(false);
388
+ expect(runtime.agentRuns[0]?.stopReason).toBe("denied");
389
+ });
390
+
391
+ test("a thrown tool error is recorded as error and rethrown", async () => {
392
+ const agent = ai.agent("tool-error", {
393
+ model: "smart",
394
+ tools: ["orders.get"],
395
+ maxSteps: 2,
396
+ });
397
+ const runtime = createAiRuntime({
398
+ models: [ai.model("smart")],
399
+ agents: [agent],
400
+ clients: {
401
+ smart: {
402
+ driverId: "mock",
403
+ model: "smart",
404
+ async complete() {
405
+ return {
406
+ text: "",
407
+ raw: {},
408
+ model: "smart",
409
+ driverId: "mock",
410
+ toolCalls: [{ id: "c1", name: "orders.get", arguments: {} }],
411
+ };
412
+ },
413
+ },
414
+ },
415
+ callFlow: async () => {
416
+ throw new Error("store down");
417
+ },
418
+ });
419
+ await expect(runtime.runAgent("tool-error", { message: "go" })).rejects.toThrow("store down");
420
+ expect(runtime.agentRuns[0]?.stopReason).toBe("error");
421
+ expect(runtime.agentRuns[0]?.error).toBe("store down");
422
+ });
423
+
424
+ test("an aborted run is recorded and then rejected", async () => {
425
+ const agent = ai.agent("abort-agent", { model: "smart", tools: [], maxSteps: 2 });
426
+ const runtime = createAiRuntime({
427
+ models: [ai.model("smart")],
428
+ agents: [agent],
429
+ clients: {
430
+ smart: {
431
+ driverId: "mock",
432
+ model: "smart",
433
+ async complete() {
434
+ return { text: "ok", raw: {}, model: "smart", driverId: "mock" };
435
+ },
436
+ },
437
+ },
438
+ });
439
+ const ctrl = new AbortController();
440
+ ctrl.abort();
441
+ await expect(
442
+ withAbortSignal(ctrl.signal, () => runtime.runAgent("abort-agent", { message: "go" })),
443
+ ).rejects.toThrow(/aborted/);
444
+ expect(runtime.agentRuns[0]?.stopReason).toBe("aborted");
445
+ });
446
+ });
447
+
192
448
  describe("journaling forced · auto-cache disabled", () => {
193
- test("ask is journaled and replayed without re-calling the model", async () => {
449
+ test("identical asks both reach the model and both are journaled", async () => {
194
450
  let calls = 0;
195
451
  const driver = createMockAiDriver({
196
452
  "*": { urgency: "high", team: "ops" },
@@ -219,8 +475,81 @@ describe("journaling forced · auto-cache disabled", () => {
219
475
  const a = await runtime.ask("ticket-triage", { subject: "x" });
220
476
  const b = await runtime.ask("ticket-triage", { subject: "x" });
221
477
  expect(a).toEqual(b);
478
+ expect(calls).toBe(2);
479
+ expect(runtime.journal).toHaveLength(2);
480
+ });
481
+
482
+ test("ask journal and agent runs keep only the newest observability rows", async () => {
483
+ const smart = ai.model("ring-smart", { provider: "mock" });
484
+ const prompt = smart.prompt("ring-prompt", { version: 1 });
485
+ const runtime = createAiRuntime({
486
+ models: [smart],
487
+ prompts: [prompt],
488
+ agents: [
489
+ ai.agent("ring-agent", {
490
+ model: "ring-smart",
491
+ tools: [],
492
+ maxSteps: 1,
493
+ }),
494
+ ],
495
+ defaultDriver: createMockAiDriver({
496
+ "*": { ok: true },
497
+ }),
498
+ });
499
+
500
+ for (let i = 0; i < AI_OBSERVABILITY_LIMIT + 1; i++) {
501
+ await runtime.ask("ring-prompt", { n: i });
502
+ await runtime.runAgent("ring-agent", { message: `m-${i}` });
503
+ }
504
+ expect(runtime.journal).toHaveLength(AI_OBSERVABILITY_LIMIT);
505
+ expect(runtime.agentRuns).toHaveLength(AI_OBSERVABILITY_LIMIT);
506
+ expect(runtime.journal[0]?.input).toEqual({ n: 1 });
507
+ expect(runtime.journal.at(-1)?.input).toEqual({ n: AI_OBSERVABILITY_LIMIT });
508
+ expect(runtime.agentRuns[0]?.message).toBe("m-1");
509
+ expect(runtime.agentRuns.at(-1)?.message).toBe(`m-${AI_OBSERVABILITY_LIMIT}`);
510
+ });
511
+
512
+ test("a durable journal replays fx.ask without a second model call", async () => {
513
+ let calls = 0;
514
+ const driver = createMockAiDriver({
515
+ "*": { urgency: "high" },
516
+ });
517
+ const base = await driver.open({ model: "mock" });
518
+ const counting = {
519
+ ...base,
520
+ async complete(opts: Parameters<typeof base.complete>[0]) {
521
+ calls++;
522
+ return base.complete(opts);
523
+ },
524
+ };
525
+ const smart = ai.model("durable-smart", { provider: "mock" });
526
+ const triage = smart.prompt("durable-triage", { version: 1 });
527
+ const runtime = createAiRuntime({
528
+ models: [smart],
529
+ prompts: [triage],
530
+ clients: { "durable-smart": counting },
531
+ });
532
+ const journal = createJournal({ store: createMemoryJournalStore() });
533
+ const session = await journal.start("support.triage", { subject: "x" });
534
+ const fx = createFx({
535
+ flow: "support.triage",
536
+ effects: { asks: ["durable-triage"] },
537
+ aiRuntime: runtime,
538
+ journal: session,
539
+ });
540
+ const first = await fx.ask("durable-triage", { subject: "x" });
222
541
  expect(calls).toBe(1);
223
- expect(runtime.journal).toHaveLength(1);
542
+
543
+ const resumed = await journal.resume(session.runId);
544
+ const again = createFx({
545
+ flow: "support.triage",
546
+ effects: { asks: ["durable-triage"] },
547
+ aiRuntime: runtime,
548
+ journal: resumed,
549
+ });
550
+ const second = await again.ask("durable-triage", { subject: "x" });
551
+ expect(calls).toBe(1);
552
+ expect(second).toEqual(first);
224
553
  });
225
554
 
226
555
  test("fx.cache is disabled when aiRuntime is bound", async () => {
@@ -489,6 +818,171 @@ describe("schema-validation is its own class", () => {
489
818
  expect(runtime.journal[0]!.outcome).toBe("schema_invalid");
490
819
  expect(runtime.journal[0]!.outcome).not.toBe("provider_error");
491
820
  });
821
+
822
+ test("repair: 0 throws on the first mismatch", async () => {
823
+ let calls = 0;
824
+ const smart = ai.model("smart", { provider: "mock" });
825
+ const triage = smart.prompt("ticket-triage-norepair", {
826
+ repair: 0,
827
+ out: {
828
+ type: "object",
829
+ properties: { urgency: { type: "string" }, team: { type: "string" } },
830
+ required: ["urgency", "team"],
831
+ },
832
+ });
833
+ const runtime = createAiRuntime({
834
+ models: [smart],
835
+ prompts: [triage],
836
+ clients: {
837
+ smart: {
838
+ driverId: "mock" as const,
839
+ model: "smart",
840
+ async complete() {
841
+ calls++;
842
+ return {
843
+ text: JSON.stringify({ urgency: "high" }),
844
+ model: "smart",
845
+ driverId: "mock" as const,
846
+ };
847
+ },
848
+ },
849
+ },
850
+ });
851
+ await expect(runtime.ask("ticket-triage-norepair", { subject: "x" })).rejects.toBeInstanceOf(
852
+ AiSchemaValidationError,
853
+ );
854
+ expect(calls).toBe(1);
855
+ });
856
+
857
+ test("repair: 1 follows up once and journals both attempts", async () => {
858
+ let calls = 0;
859
+ const smart = ai.model("smart", { provider: "mock" });
860
+ const triage = smart.prompt("ticket-triage-repair", {
861
+ repair: 1,
862
+ out: {
863
+ type: "object",
864
+ properties: { urgency: { type: "string" }, team: { type: "string" } },
865
+ required: ["urgency", "team"],
866
+ },
867
+ });
868
+ const runtime = createAiRuntime({
869
+ models: [smart],
870
+ prompts: [triage],
871
+ clients: {
872
+ smart: {
873
+ driverId: "mock" as const,
874
+ model: "smart",
875
+ async complete(opts: { messages: readonly { role: string; content: string }[] }) {
876
+ calls++;
877
+ const last = opts.messages.at(-1)?.content ?? "";
878
+ if (calls === 1) {
879
+ return {
880
+ text: JSON.stringify({ urgency: "high" }),
881
+ model: "smart",
882
+ driverId: "mock" as const,
883
+ usage: { cost: 0.01 },
884
+ };
885
+ }
886
+ expect(opts.messages).toHaveLength(2);
887
+ expect(opts.messages[0]?.role).toBe("user");
888
+ expect(last).toContain("Schema mismatch");
889
+ return {
890
+ text: JSON.stringify({ urgency: "high", team: "ops" }),
891
+ model: "smart",
892
+ driverId: "mock" as const,
893
+ usage: { cost: 0.01 },
894
+ };
895
+ },
896
+ },
897
+ },
898
+ });
899
+ const output = await runtime.ask("ticket-triage-repair", { subject: "x" });
900
+ expect(output.team).toBe("ops");
901
+ expect(calls).toBe(2);
902
+ expect(runtime.journal.map((entry) => entry.outcome)).toEqual(["schema_invalid", "ok"]);
903
+ });
904
+
905
+ test("a repair that exceeds maxCostPerCall throws and does not return output", async () => {
906
+ let calls = 0;
907
+ const smart = ai.model("smart", { provider: "mock" });
908
+ const triage = smart.prompt("ticket-triage-repair-cap", {
909
+ repair: 1,
910
+ budget: { maxCostPerCall: 0.02 },
911
+ out: {
912
+ type: "object",
913
+ properties: { urgency: { type: "string" }, team: { type: "string" } },
914
+ required: ["urgency", "team"],
915
+ },
916
+ });
917
+ const runtime = createAiRuntime({
918
+ models: [smart],
919
+ prompts: [triage],
920
+ clients: {
921
+ smart: {
922
+ driverId: "mock" as const,
923
+ model: "smart",
924
+ async complete() {
925
+ calls++;
926
+ return {
927
+ text: JSON.stringify(
928
+ calls === 1 ? { urgency: "high" } : { urgency: "high", team: "ops" },
929
+ ),
930
+ model: "smart",
931
+ driverId: "mock" as const,
932
+ usage: { cost: calls === 1 ? 0.01 : 0.05 },
933
+ };
934
+ },
935
+ },
936
+ },
937
+ });
938
+ await expect(runtime.ask("ticket-triage-repair-cap", { subject: "x" })).rejects.toThrow(
939
+ /exceeded maxCostPerCall/,
940
+ );
941
+ expect(calls).toBe(2);
942
+ expect(runtime.journal.map((entry) => entry.outcome)).toEqual([
943
+ "schema_invalid",
944
+ "budget_exceeded",
945
+ ]);
946
+ });
947
+
948
+ test("a second mismatch after repair still throws", async () => {
949
+ let calls = 0;
950
+ const smart = ai.model("smart", { provider: "mock" });
951
+ const triage = smart.prompt("ticket-triage-repair-fail", {
952
+ repair: 1,
953
+ out: {
954
+ type: "object",
955
+ properties: { urgency: { type: "string" }, team: { type: "string" } },
956
+ required: ["urgency", "team"],
957
+ },
958
+ });
959
+ const runtime = createAiRuntime({
960
+ models: [smart],
961
+ prompts: [triage],
962
+ clients: {
963
+ smart: {
964
+ driverId: "mock" as const,
965
+ model: "smart",
966
+ async complete() {
967
+ calls++;
968
+ return {
969
+ text: JSON.stringify({ urgency: "high" }),
970
+ model: "smart",
971
+ driverId: "mock" as const,
972
+ };
973
+ },
974
+ },
975
+ },
976
+ });
977
+ await expect(runtime.ask("ticket-triage-repair-fail", { subject: "x" })).rejects.toBeInstanceOf(
978
+ AiSchemaValidationError,
979
+ );
980
+ expect(calls).toBe(2);
981
+ expect(runtime.journal.map((entry) => entry.outcome)).toEqual([
982
+ "schema_invalid",
983
+ "schema_invalid",
984
+ ]);
985
+ });
492
986
  });
493
987
 
494
988
  describe("agent tool trail carries effects; denials are not errors", () => {
@@ -8,14 +8,24 @@
8
8
  * Prompts are versioned artifacts with validated output shapes and eval sets
9
9
  * (`oke eval` gates CI). Agents' tools are the app's own flows (gates apply).
10
10
  * PII fields cannot reach a third-party model without explicit `allowPii`.
11
- * Nondeterministic ⇒ journaling forced, auto-cache disabled.
11
+ * Nondeterministic ⇒ journaling forced for Console, auto-cache disabled.
12
+ * Replay belongs to a durable run's journal, not this process-wide log.
12
13
  * @module
13
14
  */
14
15
 
15
16
  export { ai, listAiDecls, resetAiDecls } from "./ai/declare.ts";
16
17
  export type {
17
18
  AiAgentDecl,
19
+ AiBooleanQuestion,
20
+ AiChoiceQuestion,
21
+ AiDecisionDecl,
22
+ AiDecisionOptions,
23
+ AiDecisionQuestion,
24
+ AiScoreQuestion,
25
+ AiAgentApprovalDecl,
18
26
  AiAgentOptions,
27
+ AiAgentToolOptions,
28
+ AiToolApproval,
19
29
  AiBudgetDecl,
20
30
  AiEmbedDecl,
21
31
  AiEmbedOptions,
@@ -39,16 +49,21 @@ export {
39
49
  } from "./ai/errors.ts";
40
50
  export type { AiErrorFields } from "./ai/errors.ts";
41
51
 
52
+ export type { AgentEventEmit, AgUiEvent, AgUiUsage } from "./ai/events.ts";
53
+ export { createEventQueue, emitAssistantText } from "./ai/events.ts";
54
+
42
55
  export {
43
56
  createAiRuntime,
44
57
  parsePromptRef,
45
58
  AiSchemaValidationError,
46
59
  promptContentFromInput,
47
60
  AI_DEFAULT_MAX_STEPS,
61
+ AI_OBSERVABILITY_LIMIT,
48
62
  } from "./ai/runtime.ts";
49
63
  export type {
50
64
  AgentDenial,
51
65
  AgentRunRecord,
66
+ AgentStopReason,
52
67
  AgentToolEffect,
53
68
  AgentToolStep,
54
69
  AiAgentRunOptions,
@@ -105,6 +105,11 @@ export interface CreateChannelRuntimeOptions {
105
105
  readonly retry?: boolean;
106
106
  /** Injectable clock. */
107
107
  readonly now?: () => number;
108
+ /**
109
+ * Reload durable consent / suppression / receipts.
110
+ * Boot calls this from the scheduler when a SQL ledger is bound.
111
+ */
112
+ readonly refresh?: () => Promise<void>;
108
113
  }
109
114
 
110
115
  /** Send options for {@link ChannelRuntime.send}. */
@@ -127,6 +132,11 @@ export interface ChannelRuntime {
127
132
  readonly costs: MediumCosts;
128
133
  /** Bound driver chain (email + SMS + …). */
129
134
  readonly drivers: readonly ChannelDriver[];
135
+ /**
136
+ * Reload a shared consent / suppression / receipt ledger.
137
+ * Memory stores leave this unset.
138
+ */
139
+ refresh?(): Promise<void>;
130
140
  /**
131
141
  * Send a template through the driver chain.
132
142
  *
@@ -728,6 +738,7 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
728
738
  receipts,
729
739
  costs,
730
740
  drivers,
741
+ ...(options.refresh ? { refresh: options.refresh } : {}),
731
742
  async sendOtp(opts) {
732
743
  const { otp } = otpSmsDriver();
733
744
  return otp.sendOtp(opts);