okengine 0.21.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/AGENTS.md +11 -4
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +15 -9
  5. package/manifest.v1.schema.json +54 -0
  6. package/package.json +53 -38
  7. package/site/content/docs/ai/mcp.mdx +13 -9
  8. package/site/content/docs/ai/skills.mdx +2 -2
  9. package/site/content/docs/client/calling.mdx +27 -19
  10. package/site/content/docs/client/index.mdx +1 -1
  11. package/site/content/docs/elements/ai/agents.mdx +61 -13
  12. package/site/content/docs/elements/ai/decide.mdx +536 -0
  13. package/site/content/docs/elements/ai/events.mdx +373 -0
  14. package/site/content/docs/elements/ai/index.mdx +9 -3
  15. package/site/content/docs/elements/ai/meta.json +1 -1
  16. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  17. package/site/content/docs/elements/channel/email.mdx +1 -1
  18. package/site/content/docs/elements/channel/index.mdx +1 -1
  19. package/site/content/docs/elements/flow/http.mdx +4 -4
  20. package/site/content/docs/elements/flow/index.mdx +3 -3
  21. package/site/content/docs/elements/flow/meta.json +1 -1
  22. package/site/content/docs/elements/gate/auth.mdx +4 -1
  23. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  24. package/site/content/docs/elements/store/index.mdx +1 -1
  25. package/site/content/docs/elements/store/kv.mdx +5 -5
  26. package/site/content/docs/elements/vault/rotation.mdx +15 -0
  27. package/site/content/docs/plugins/otp.mdx +31 -31
  28. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  29. package/site/content/docs/recipes/index.mdx +6 -6
  30. package/site/content/docs/recipes/mailpit.mdx +1 -1
  31. package/site/content/docs/recipes/meilisearch.mdx +1 -1
  32. package/site/content/docs/recipes/pgdog.mdx +1 -1
  33. package/site/content/docs/recipes/postgres.mdx +12 -8
  34. package/site/content/docs/recipes/redis.mdx +4 -4
  35. package/site/content/docs/reference/cli.mdx +2 -1
  36. package/site/content/docs/reference/configuration.mdx +11 -9
  37. package/site/content/docs/reference/errors.mdx +12 -2
  38. package/site/content/docs/reference/fx.mdx +35 -21
  39. package/site/content/docs/reference/idempotency.mdx +192 -0
  40. package/site/content/docs/reference/index.mdx +6 -1
  41. package/site/content/docs/reference/meta.json +2 -1
  42. package/site/content/docs/understand/meta.json +1 -1
  43. package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
  44. package/site/content/docs/understand/the-architecture.mdx +14 -12
  45. package/site/content/docs/understand/try-it.mdx +3 -3
  46. package/src/auth/api-key-sql.test.ts +44 -0
  47. package/src/auth/api-key-sql.ts +1 -0
  48. package/src/auth/api-keys.ts +30 -12
  49. package/src/auth/config.ts +6 -2
  50. package/src/auth/gate-auth.test.ts +20 -0
  51. package/src/cli/agents-md.test.ts +25 -0
  52. package/src/cli/ask-vault-gaps.test.ts +38 -1
  53. package/src/cli/ask-vault-gaps.ts +35 -2
  54. package/src/cli/competitor-mention-removal.test.ts +3 -3
  55. package/src/cli/decide.ts +174 -0
  56. package/src/cli/decision-lock-watch.test.ts +45 -0
  57. package/src/cli/decision-lock-watch.ts +51 -0
  58. package/src/cli/dev-app-runner.ts +34 -8
  59. package/src/cli/dev-auth-secret.test.ts +49 -0
  60. package/src/cli/dev-auth-secret.ts +49 -0
  61. package/src/cli/dev-hot.test.ts +41 -0
  62. package/src/cli/dev-hot.ts +48 -0
  63. package/src/cli/dev.test.ts +1 -1
  64. package/src/cli/dev.ts +24 -6
  65. package/src/cli/docker-cli.test.ts +1 -1
  66. package/src/cli/eval.ts +142 -1
  67. package/src/cli/index.ts +14 -1
  68. package/src/cli/load-config.images.test.ts +12 -12
  69. package/src/cli/load-config.ts +2 -2
  70. package/src/cli/mcp-from-console.ts +48 -0
  71. package/src/cli/registry.ts +35 -1
  72. package/src/cli/vault-cmd.test.ts +23 -0
  73. package/src/cli/vault-cmd.ts +7 -0
  74. package/src/client/agent.ts +307 -0
  75. package/src/client/create.ts +18 -1
  76. package/src/client/transport.test.ts +245 -0
  77. package/src/client/transport.ts +133 -19
  78. package/src/client/types.ts +29 -3
  79. package/src/client-react/index.ts +7 -1
  80. package/src/client-react/use-agent-run.test.ts +271 -0
  81. package/src/client-react/use-agent-run.ts +193 -0
  82. package/src/compiler/ai-approval.test.ts +36 -0
  83. package/src/compiler/ai-repair.test.ts +22 -0
  84. package/src/compiler/decisions.extract.test.ts +164 -0
  85. package/src/compiler/effects-infer.ts +753 -17
  86. package/src/compiler/extract.test.ts +37 -0
  87. package/src/compiler/extract.ts +381 -24
  88. package/src/compiler/fixtures/skyport.expected.json +24 -0
  89. package/src/compiler/fx-follow.test.ts +164 -0
  90. package/src/compiler/fx-index.ts +399 -0
  91. package/src/compiler/response.ts +35 -2
  92. package/src/config/index.ts +21 -3
  93. package/src/console/server/ai-runs-flows.ts +269 -0
  94. package/src/console/server/ai-runs.test.ts +323 -0
  95. package/src/console/server/ai-runs.ts +562 -0
  96. package/src/console/server/ai.ts +12 -1
  97. package/src/console/server/app.ts +10 -1
  98. package/src/console/server/decisions-flows.ts +157 -0
  99. package/src/console/server/decisions.test.ts +66 -0
  100. package/src/console/server/decisions.ts +229 -0
  101. package/src/console/server/flows-invoke.test.ts +2 -2
  102. package/src/console/server/flows.ts +50 -2
  103. package/src/console/server/invoke-user-flow.ts +9 -0
  104. package/src/console/server/runs-ingest.ts +16 -0
  105. package/src/console/server/serve.ts +5 -0
  106. package/src/console/server/state.ts +25 -1
  107. package/src/console/server/store-stats.test.ts +47 -0
  108. package/src/console/server/store-stats.ts +92 -18
  109. package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
  110. package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
  111. package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
  112. package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
  113. package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.js} +1 -1
  114. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  115. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  116. package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
  117. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  118. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  119. package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
  120. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  121. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  122. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  123. package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
  124. package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
  125. package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
  126. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  127. package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
  128. package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
  129. package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
  130. package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
  131. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  132. package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
  133. package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
  134. package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
  135. package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
  136. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  137. package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
  138. package/src/console/ui-next/dist/index.html +4 -3
  139. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  140. package/src/console/ui-next/src/client.ts +224 -1
  141. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  142. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  143. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  144. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  145. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  146. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  147. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  148. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  149. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  150. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  151. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  152. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  153. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  154. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  155. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  156. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  157. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  158. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  159. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  160. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  161. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  162. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  163. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  164. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  165. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  166. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  167. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  168. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  169. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  170. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  171. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +15 -2
  172. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  173. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  174. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  175. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  176. package/src/console/ui-next/src/router.tsx +17 -0
  177. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  178. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  179. package/src/console/ui-next/vite.config.ts +9 -0
  180. package/src/docker/docker.test.ts +13 -13
  181. package/src/docker/images-config.test.ts +12 -12
  182. package/src/docker/recipes/dragonfly.ts +3 -2
  183. package/src/docker/recipes/redis.ts +1 -1
  184. package/src/docker/stack-id.test.ts +1 -1
  185. package/src/drivers/ai-anthropic.ts +212 -21
  186. package/src/drivers/ai-mock.ts +34 -1
  187. package/src/drivers/ai-openai-compatible.ts +67 -26
  188. package/src/drivers/ai-preconnect.ts +18 -0
  189. package/src/drivers/ai-stream.test.ts +62 -0
  190. package/src/drivers/ai-types.ts +20 -0
  191. package/src/drivers/journal-postgres.ts +455 -3
  192. package/src/drivers/meilisearch.ts +2 -0
  193. package/src/drivers/memory-ddl.test.ts +18 -0
  194. package/src/drivers/memory.ts +21 -1
  195. package/src/drivers/pg-rls.test.ts +31 -0
  196. package/src/drivers/pg-rls.ts +15 -0
  197. package/src/drivers/postgres.test.ts +9 -0
  198. package/src/drivers/postgres.ts +49 -11
  199. package/src/drivers/vault-builtin.test.ts +10 -0
  200. package/src/drivers/vault-builtin.ts +13 -1
  201. package/src/drivers/vault-remote-bag.ts +5 -1
  202. package/src/drivers/vault-types.ts +6 -1
  203. package/src/elements/ai/agent-event-slot.ts +27 -0
  204. package/src/elements/ai/approval-http.ts +207 -0
  205. package/src/elements/ai/approval.test.ts +741 -0
  206. package/src/elements/ai/approval.ts +228 -0
  207. package/src/elements/ai/decisions/bind.ts +163 -0
  208. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  209. package/src/elements/ai/decisions/certificate.ts +377 -0
  210. package/src/elements/ai/decisions/certify.ts +314 -0
  211. package/src/elements/ai/decisions/decide.test.ts +953 -0
  212. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  213. package/src/elements/ai/decisions/export.test.ts +120 -0
  214. package/src/elements/ai/decisions/export.ts +146 -0
  215. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  216. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  217. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  218. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  219. package/src/elements/ai/decisions/http.test.ts +154 -0
  220. package/src/elements/ai/decisions/http.ts +240 -0
  221. package/src/elements/ai/decisions/labels.test.ts +282 -0
  222. package/src/elements/ai/decisions/labels.ts +257 -0
  223. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  224. package/src/elements/ai/decisions/openrouter.ts +36 -0
  225. package/src/elements/ai/decisions/provider.ts +102 -0
  226. package/src/elements/ai/decisions/typesafe.ts +36 -0
  227. package/src/elements/ai/declare.ts +252 -1
  228. package/src/elements/ai/events.test.ts +379 -0
  229. package/src/elements/ai/events.ts +138 -0
  230. package/src/elements/ai/run-events.test.ts +461 -0
  231. package/src/elements/ai/run-events.ts +522 -0
  232. package/src/elements/ai/runtime.ts +1186 -105
  233. package/src/elements/ai/stream-turn.ts +214 -0
  234. package/src/elements/ai/stream.live.test.ts +76 -0
  235. package/src/elements/ai/subagent.test.ts +136 -0
  236. package/src/elements/ai.test.ts +497 -3
  237. package/src/elements/ai.ts +16 -1
  238. package/src/elements/clock/durable.ts +27 -1
  239. package/src/elements/store/emit-drizzle.ts +2 -2
  240. package/src/elements/store/live-default.test.ts +2 -0
  241. package/src/elements/store/live-http.test.ts +1 -0
  242. package/src/elements/store/resource-list-docs.test.ts +2 -2
  243. package/src/elements/store/resource.test.ts +3 -3
  244. package/src/elements/store/schema-decl.test.ts +1 -0
  245. package/src/elements/store/sql-condition.ts +24 -7
  246. package/src/elements/store/sql-session.test.ts +51 -0
  247. package/src/elements/store/sql-session.ts +111 -1
  248. package/src/elements/store/upsert-app.test.ts +1 -1
  249. package/src/elements/vault/audit.test.ts +137 -0
  250. package/src/elements/vault/audit.ts +106 -13
  251. package/src/elements/vault/boot-chain.ts +9 -1
  252. package/src/elements/vault/builtin-adapter.ts +33 -3
  253. package/src/i18n/catalogs/ar.ts +9 -0
  254. package/src/i18n/catalogs/en.ts +9 -0
  255. package/src/index.ts +2 -0
  256. package/src/kernel/abort-scope.ts +33 -2
  257. package/src/kernel/agent-event-store.ts +448 -0
  258. package/src/kernel/app.ts +294 -19
  259. package/src/kernel/auto-cache.test.ts +6 -6
  260. package/src/kernel/boot-bind/ai.ts +3 -0
  261. package/src/kernel/boot-bind/store.test.ts +26 -0
  262. package/src/kernel/boot-bind/store.ts +82 -6
  263. package/src/kernel/boot-bind/vault.ts +1 -0
  264. package/src/kernel/boot.ts +34 -5
  265. package/src/kernel/builtin-errors.ts +10 -0
  266. package/src/kernel/capability.ts +3 -0
  267. package/src/kernel/client-descriptor.ts +1 -0
  268. package/src/kernel/concurrency.test.ts +33 -0
  269. package/src/kernel/decision-label-store.ts +441 -0
  270. package/src/kernel/effects-stamping.test.ts +2 -2
  271. package/src/kernel/effects.test.ts +2 -1
  272. package/src/kernel/effects.ts +13 -1
  273. package/src/kernel/element-registries.ts +3 -0
  274. package/src/kernel/errors-compiler.ts +20 -0
  275. package/src/kernel/errors-text.ts +4 -0
  276. package/src/kernel/errors.ts +2 -0
  277. package/src/kernel/external-effects.test.ts +36 -0
  278. package/src/kernel/flow.test.ts +2 -3
  279. package/src/kernel/flow.ts +24 -0
  280. package/src/kernel/fx-decide.ts +761 -0
  281. package/src/kernel/fx-fetch.ts +5 -1
  282. package/src/kernel/fx.test.ts +12 -5
  283. package/src/kernel/fx.ts +316 -29
  284. package/src/kernel/http-frame.test.ts +94 -0
  285. package/src/kernel/http-frame.ts +203 -0
  286. package/src/kernel/idempotency-store.ts +578 -0
  287. package/src/kernel/idempotency.test.ts +556 -0
  288. package/src/kernel/idempotency.ts +304 -0
  289. package/src/kernel/index.ts +1 -0
  290. package/src/kernel/journal.ts +160 -0
  291. package/src/kernel/json-result.ts +10 -0
  292. package/src/kernel/pipeline.test.ts +45 -0
  293. package/src/kernel/sse-id.ts +32 -0
  294. package/src/manifest/diff.ts +1 -0
  295. package/src/manifest/types.ts +37 -2
  296. package/src/mcp/ai-tools.test.ts +202 -0
  297. package/src/mcp/authorization.ts +66 -0
  298. package/src/mcp/session.ts +3 -0
  299. package/src/mcp/tools.ts +168 -0
  300. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  301. package/src/plugins/otp.test.ts +65 -1
  302. package/src/plugins/otp.ts +59 -7
  303. package/src/release/absolute-regression.test.ts +63 -0
  304. package/src/release/http-graph.test.ts +27 -0
  305. package/src/release/http-graph.ts +95 -0
  306. package/src/release/index.ts +3 -0
  307. package/src/release/limits.ts +17 -2
  308. package/src/release/measure.ts +144 -28
  309. package/src/runs/collect.ts +4 -1
  310. package/src/runs/parquet.test.ts +27 -1
  311. package/src/runs/parquet.ts +19 -0
  312. package/src/runs/types.ts +31 -0
  313. package/src/runtime/json-code-block.test.ts +106 -47
  314. package/src/runtime/json-code-block.ts +1209 -46
  315. package/src/term.test.ts +13 -0
  316. package/src/term.ts +81 -49
  317. package/src/test/create-test-app.test.ts +3 -1
  318. package/src/test/create-test-app.ts +40 -0
  319. package/src/test/live-signals.test.ts +1 -0
  320. package/src/test/provisions.integration.test.ts +1 -0
  321. package/src/test/tenant-isolation.test.ts +1 -0
  322. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  323. package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
  324. package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
  325. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
  326. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
  327. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
  328. package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
  329. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
  330. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
  331. package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
  332. package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
  333. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  334. package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
  335. package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
  336. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
  337. package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
  338. package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
  339. package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
  340. package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
  341. package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
  342. package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
@@ -0,0 +1,536 @@
1
+ ---
2
+ title: "Decisions"
3
+ description: "One model call that labels a ticket or request, and waits for a person when it is not allowed to decide alone."
4
+ icon: "Scale"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ A decision picks labels for one piece of work: which queue a ticket belongs in, and whether it needs a person today. One request asks every question. The result records whether the model or a person supplied each value.
9
+
10
+ <Callout title="OpenRouter">
11
+ The OpenRouter decisions endpoint is alpha on the provider side. Score levels and a whole question
12
+ held in a same-file const are checked at compile time.
13
+ </Callout>
14
+
15
+ <Callout title="The one rule">
16
+ Declare exactly one of `review` or `onUncertain: "abstain"`, then call it with `fx.decide`. Only
17
+ `oke-decisions.lock.json` can grant `auto`.
18
+ </Callout>
19
+
20
+ ## Quick start
21
+
22
+ <Steps>
23
+
24
+ <Step>
25
+ ### Declare the questions
26
+
27
+ `review` is a real gate. The people who hold that gate are the ones who may answer when the
28
+ model is not allowed to.
29
+
30
+ ```typescript title="src/core/ai.ts"
31
+ import { ai, gate } from "okengine";
32
+
33
+ export const ops = gate.policy("ops", ({ operator }) => operator.id !== null);
34
+
35
+ export const triage = ai.decision("triage", {
36
+ review: ops,
37
+ ask: {
38
+ team: ai.choice("Which team owns this ticket?", {
39
+ billing: "Billing",
40
+ technical: "Technical",
41
+ }),
42
+ urgent: ai.boolean("Does this need a person today?", {
43
+ true: "A person should see it today",
44
+ false: "It can wait",
45
+ }),
46
+ },
47
+ });
48
+ ```
49
+
50
+ `ai.choice` takes at most 254 options. The model may also answer `none_of_these`, which never returns as `auto`. A reviewer may still submit `none_of_these`; the Flow receives that string. `ai.boolean` takes an optional `{ true, false }` criteria pair. `ai.score` takes 2–10 levels, best to worst. Question ids `meta` and `$` are reserved.
51
+
52
+ </Step>
53
+
54
+ <Step>
55
+ ### Emit, then decide in a consumer
56
+
57
+ A review parks the run until a person answers. An HTTP trigger cannot park: the response would
58
+ be 204 and the caller would see nothing. Accept the ticket on HTTP, emit a signal, and decide
59
+ in a durable consumer.
60
+
61
+ ```typescript title="src/flows/support/open.ts"
62
+ import { on, flow, http, signal } from "okengine";
63
+ import { z } from "zod";
64
+
65
+ export const ticketOpened = signal.broadcast("ticket.opened");
66
+
67
+ export const open = on(
68
+ http.post({ in: z.object({ text: z.string().min(1) }) }).public(),
69
+ flow({
70
+ do: async ({ text }, fx) => {
71
+ await fx.emit(ticketOpened, { text });
72
+ return { accepted: true };
73
+ },
74
+ }),
75
+ );
76
+ ```
77
+
78
+ ```typescript title="src/flows/support/route.ts"
79
+ import { on, flow } from "okengine";
80
+ import { triage } from "@/core/ai";
81
+ import { ticketOpened } from "./open";
82
+
83
+ export const route = on(
84
+ ticketOpened,
85
+ flow({
86
+ durable: true,
87
+ do: async (input, fx) => fx.decide(triage, input),
88
+ }),
89
+ );
90
+ ```
91
+
92
+ `durable: true` is required for `review`. Abstain mode can run without it.
93
+
94
+ </Step>
95
+
96
+ <Step>
97
+ ### Resolve it in Console
98
+
99
+ The park waits until someone who passes the `ops` gate submits every open question. Console on port 6533 does that as an operator: the route sends the signed-in operator and that session's auth scopes into the review gate. The gate must allow that operator. An operator may resolve any tenant.
100
+
101
+ Open `/flows/decisions`, or POST the review id from the queue:
102
+
103
+ ```bash
104
+ curl -X POST http://127.0.0.1:6533/console/decisions/resolve \
105
+ -H "Authorization: $OKE_OPERATOR_AUTHORIZATION" \
106
+ -H "content-type: application/json" \
107
+ -d '{"id":"<review id>","values":{"team":"billing","urgent":false}}'
108
+ ```
109
+
110
+ The consumer then returns. Each question is a top-level field. `$` records how it was produced. `p` is calibrated confidence from 0 to 1. `raw` is the provider distribution before calibration.
111
+
112
+ ```json
113
+ {
114
+ "team": "billing",
115
+ "urgent": false,
116
+ "$": {
117
+ "meta": {
118
+ "model": "typesafe/jev-1.13-20260917",
119
+ "provider": "openrouter",
120
+ "usage": { "inputTokens": 120, "outputTokens": 40 }
121
+ },
122
+ "team": {
123
+ "how": "reviewed",
124
+ "p": 0.42,
125
+ "raw": { "billing": 0.2, "technical": 0.2, "none_of_these": 0.6 }
126
+ },
127
+ "urgent": { "how": "reviewed", "p": 0.51, "raw": { "noul": 0.51 } }
128
+ }
129
+ }
130
+ ```
131
+
132
+ `how: "reviewed"` is the value the gate submitted, including a choice of `none_of_these`. `p` stays the model's confidence. `raw.noul` is the provider's yes/no score for a boolean question.
133
+
134
+ | `how` | Value the Flow sees |
135
+ | ----------- | ----------------------------------------------------------------------------- |
136
+ | `reviewed` | The value the review gate submitted. |
137
+ | `abstained` | `null` on that question only. Certain questions in the same call stay `auto`. |
138
+ | `auto` | The calibrated answer, after a lockfile exists. See Certify and promote. |
139
+
140
+ </Step>
141
+
142
+ </Steps>
143
+
144
+ ## Options
145
+
146
+ <TypeTable
147
+ type={{
148
+ ask: {
149
+ description: "Named questions. One request sends all of them.",
150
+ type: "Record<string, Choice | Score | Boolean>",
151
+ required: true,
152
+ },
153
+ review: {
154
+ description:
155
+ "Gate that may resolve a park. Exclusive with onUncertain. The Flow must be durable. No timeout.",
156
+ type: "Gate | string",
157
+ },
158
+ onUncertain: {
159
+ description: "Only abstain. Returns null and does not park. Exclusive with review.",
160
+ type: '"abstain"',
161
+ },
162
+ autonomy: {
163
+ description: "maxError is required. audit is required, from 0 to 1. risk defaults to 0.1.",
164
+ type: "{ maxError: number; audit: number; risk?: number }",
165
+ },
166
+ model: {
167
+ description:
168
+ "Model id. Default is typesafe/jev-1.13 on OpenRouter, or jev-1.13.0 on TypeSafe.",
169
+ type: "AiModel | string",
170
+ },
171
+ driverId: {
172
+ description: "Which provider to call.",
173
+ type: '"openrouter" | "typesafe"',
174
+ default: "openrouter",
175
+ },
176
+ locale: {
177
+ description: "Slice the certificate by a string from the input. Absent means one slice.",
178
+ type: "(input) => string | undefined",
179
+ },
180
+ evals: { description: "Seed JSONL path. Same role as a prompt evals file.", type: "string" },
181
+ timeout: {
182
+ description: "Provider deadline. A duration string or milliseconds.",
183
+ type: "string | number",
184
+ default: "30s",
185
+ },
186
+ }}
187
+ />
188
+
189
+ ## Lifecycle
190
+
191
+ A decision is in one list state. Drift is one flag per decision. Suspending `triage` leaves `route` alone.
192
+
193
+ ```text
194
+ learning --labels--> candidate ready --promote--> certified
195
+ \ /
196
+ \---- drift ----> suspended
197
+ ```
198
+
199
+ | State | What is true | What moves it |
200
+ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
201
+ | `learning` | No lockfile entry and no candidate. | Reviews and audit labels accumulate. |
202
+ | `candidate ready` | The clock has built a candidate from those labels. The page shows the fit and `oke decide promote <name>`. Counts are not the grant. | Promote writes that slot. |
203
+ | `certified` | `oke-decisions.lock.json` has this decision, and the declaration sets `autonomy`. A lock entry is ignored without `autonomy`. | The runtime may return `auto` under that certificate. |
204
+ | `suspended` | This decision's audit labels since its certificate, inside 7 days, for the pinned model, failed the error cap. The monitor emitted `oke/decision/drift` with that name. | Promote or recertify clears only this decision. A newer `certifiedAt` on the next load ignores the flag. |
205
+
206
+ `oke dev` reloads `oke-decisions.lock.json` when the file changes. Production reads it once, at boot.
207
+
208
+ The runtime never raises a threshold and never invents a certificate. Missing lock, a stale
209
+ question, a model version mismatch, an uncertified locale, low confidence, drift, and an
210
+ outage all take the non-auto path.
211
+
212
+ ## How a person resolves a review
213
+
214
+ Open Console at `/flows/decisions` on port 6533. Each row is `learning`, `candidate ready`, `certified`, or `suspended` for that decision. The queue shows how long each row has been waiting. The resolve form only accepts a choice option, `none_of_these`, or a score level. An audit row submits `labelOnly: true`. A second resolve is `Conflict`. A lease collision retries. A failed label write is listed on the page.
215
+
216
+ An audit row is a label only. The original Flow already returned. A review row is the park:
217
+ submitting it wakes the consumer.
218
+
219
+ The review gate must allow the caller. Console resolve runs on the operator plane: it passes the signed-in operator id and the session auth into the gate, and it may resolve any tenant. A gate that checks `operator.id` matches that call.
220
+
221
+ A review body must include every question that was not `auto`. An audit row (`labelOnly: true`) must include every question, including ones that were already `auto`. A choice may be `none_of_these`. A partial body is 422: `review values do not match the open questions`.
222
+
223
+ ```bash
224
+ curl -X POST http://127.0.0.1:6533/console/decisions/resolve \
225
+ -H "Authorization: $OKE_OPERATOR_AUTHORIZATION" \
226
+ -H "content-type: application/json" \
227
+ -d '{"id":"<audit id>","labelOnly":true,"values":{"team":"billing","urgent":true}}'
228
+ ```
229
+
230
+ | Result | HTTP | What you do |
231
+ | ---------------------------- | --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
232
+ | `{ ok: true }` | 200 | The consumer resumes with `how: "reviewed"`. |
233
+ | Already resolved | 409 `Conflict`. No `Retry-After`. `That value is already in use.` | Stop. |
234
+ | Another worker holds the run | 409 `JournalLeaseBusy` with `Retry-After`. `This run is locked by another worker. Retry after the given delay.` | Retry after the delay. A wait over 60 seconds is 503. |
235
+
236
+ The same 409 split applies to tool approval. Retry only `JournalLeaseBusy`.
237
+
238
+ ## Certify and promote
239
+
240
+ **maxError** is the highest fraction of wrong answers you will accept among the ones the
241
+ model is allowed to return alone. `0.05` means at most 5 in 100 of those automatic answers.
242
+
243
+ **audit** is how often an automatic answer is also queued for a person, from 0 to 1.
244
+ `0.1` queues about one in ten. Those labels are how the next certificate is fit. Omitting
245
+ `audit` when `autonomy` is set fails to compile.
246
+
247
+ **risk** is how sure the statistical test must be before it accepts a threshold. Default
248
+ `0.1`. A smaller risk demands more labels.
249
+
250
+ **Calibration** rescales the provider's raw scores so `p` means a probability. Choice and
251
+ score use a temperature. Yes/no uses a curve fit on the provider score. The lockfile stores
252
+ that fit next to the question.
253
+
254
+ **The test**, once: for each confidence cutoff from 0.50 to 0.99 (50 cutoffs), count mistakes on labels that cleared it.
255
+ A binomial test passes only when those mistakes are few enough to show the error rate is under `maxError`.
256
+ The loosest cutoff that passes is the threshold. If none pass, that question cannot return `auto`.
257
+
258
+ With `risk` at 0.1 and no mistakes, the label count is about `log(0.1 / 50) / log(1 − maxError)`:
259
+
260
+ | `maxError` | Error-free labels |
261
+ | ---------- | ----------------- |
262
+ | `0.1` | ≈ 59 |
263
+ | `0.05` | ≈ 122 |
264
+ | `0.01` | ≈ 619 |
265
+
266
+ A seed file is JSONL. `expect` is the label for each question. `locale` is optional.
267
+
268
+ ```json
269
+ {"id":"t1","input":{"text":"I was charged twice"},"expect":{"team":"billing","urgent":true}}
270
+ {"id":"t2","input":{"text":"The app crashes on save"},"expect":{"team":"technical","urgent":false},"locale":"en"}
271
+ ```
272
+
273
+ Point `evals` at that file, then certify. Run `oke build` first. `oke eval --certify` reads `oke.manifest.json` from the app root. Prompt evals do not run when the flag is present.
274
+ It writes `oke-decisions.lock.json` in that root (`OKE_ROOT_DIR`, or `rootDir`), not in the shell's current directory. Boot reads that same file. Declared decisions without a project root fail at boot.
275
+
276
+ `how: "auto"` is what the Flow sees after that file is loaded and the question clears its threshold:
277
+
278
+ ```json
279
+ { "team": "technical", "$": { "team": { "how": "auto", "p": 0.91, "audited": true } } }
280
+ ```
281
+
282
+ ```bash
283
+ OKE_ROOT_DIR=/srv/app oke eval --certify
284
+ ```
285
+
286
+ The empty-string key is the slice used when `locale` is absent. A locale function gets its
287
+ own key. `hash` is the question text and options. Edit the question and the hash no longer
288
+ matches: the runtime stops returning `auto` until you certify again.
289
+
290
+ ```json
291
+ {
292
+ "decisions": {
293
+ "triage": {
294
+ "model": "typesafe/jev-1.13-20260917",
295
+ "certifiedAt": 1710000000000,
296
+ "questions": {
297
+ "team": {
298
+ "": {
299
+ "hash": "…",
300
+ "calibrator": { "kind": "temperature", "t": 1.2 },
301
+ "threshold": 0.8,
302
+ "metrics": {
303
+ "labels": 122,
304
+ "accepted": 122,
305
+ "errors": 0,
306
+ "maxError": 0.05,
307
+ "delta": 0.1
308
+ }
309
+ }
310
+ }
311
+ }
312
+ }
313
+ }
314
+ }
315
+ ```
316
+
317
+ After production labels exist, the clock stores a candidate on the journal. It survives a restart only when that journal is a file or Postgres. A memory journal drops it when the process exits. Promote fetches it. It does not recompute the certificate. Other decisions already in the file stay. This decision's slot is replaced. Promote clears drift for this decision only.
318
+
319
+ ```bash
320
+ OKE_ORIGIN=http://127.0.0.1:6530 \
321
+ OKE_OPERATOR_AUTHORIZATION='Bearer …' \
322
+ oke decide promote triage \
323
+ --origin "$OKE_ORIGIN" \
324
+ --authorization "$OKE_OPERATOR_AUTHORIZATION" \
325
+ --lock "$OKE_ROOT_DIR/oke-decisions.lock.json"
326
+ ```
327
+
328
+ That GET is `/_oke/decisions/triage/candidate`. The caller must be an operator. A missing candidate is 404. The route exists only when a decision declares `autonomy`.
329
+
330
+ `oke decide labels <name> --export` writes reviewed labels as seed JSONL (`input`, `expect`, `locale`). It is operator-only and limited to the caller's tenant. The file keeps the decision's declared `in` fields. Secret and redacted fields are masked. The command prints that the file contains production data.
331
+
332
+ ```bash
333
+ oke decide labels triage --export --out triage.labels.jsonl
334
+ ```
335
+
336
+ ## Patterns
337
+
338
+ <Tabs items={["Review", "Abstain", "Autonomy", "Locale"]}>
339
+
340
+ <Tab value="Review">
341
+
342
+ Non-auto questions park the consumer. The result is returned only after the gate submits
343
+ every open question. `how` on those questions is `reviewed`.
344
+
345
+ ```typescript
346
+ export const triage = ai.decision("triage", {
347
+ review: ops,
348
+ ask: {
349
+ team: ai.choice("Which team owns this ticket?", {
350
+ billing: "Billing",
351
+ technical: "Technical",
352
+ }),
353
+ },
354
+ });
355
+ ```
356
+
357
+ </Tab>
358
+
359
+ <Tab value="Abstain">
360
+
361
+ Only the uncertain questions return `null` and `how: "abstained"`. A question that cleared its threshold stays `auto`. The Flow is not parked, so it can run on HTTP and without `durable`.
362
+
363
+ ```typescript
364
+ export const triage = ai.decision("triage", {
365
+ onUncertain: "abstain",
366
+ ask: {
367
+ urgent: ai.boolean("Does this need a person today?"),
368
+ },
369
+ });
370
+ ```
371
+
372
+ </Tab>
373
+
374
+ <Tab value="Autonomy">
375
+
376
+ `autonomy` does not replace `review` or `abstain`. It is the budget the lockfile is allowed
377
+ to spend. Without a matching certificate, every question still takes the non-auto path.
378
+
379
+ ```typescript
380
+ export const triage = ai.decision("triage", {
381
+ review: ops,
382
+ autonomy: { maxError: 0.05, audit: 0.1, risk: 0.1 },
383
+ evals: "evals/triage.jsonl",
384
+ ask: {
385
+ team: ai.choice("Which team owns this ticket?", {
386
+ billing: "Billing",
387
+ technical: "Technical",
388
+ }),
389
+ },
390
+ });
391
+ ```
392
+
393
+ </Tab>
394
+
395
+ <Tab value="Locale">
396
+
397
+ Return a string to keep a separate certificate slice. `undefined` uses the default slice.
398
+ An input whose locale has no slice is not `auto`.
399
+
400
+ ```typescript
401
+ export const triage = ai.decision("triage", {
402
+ review: ops,
403
+ locale: (input) => {
404
+ const row = input as { locale?: string };
405
+ return row.locale;
406
+ },
407
+ ask: {
408
+ team: ai.choice("Which team owns this ticket?", {
409
+ billing: "Billing",
410
+ technical: "Technical",
411
+ }),
412
+ },
413
+ });
414
+ ```
415
+
416
+ </Tab>
417
+
418
+ </Tabs>
419
+
420
+ ## Provider
421
+
422
+ The key is a Vault secret. `fx.decide` reads `OPENROUTER_API_KEY`, or `TYPESAFE_API_KEY` when
423
+ `driverId` is `"typesafe"`. Declare that secret on the Flow when you write an `effects` block.
424
+ An empty value throws `DecisionConfigError`.
425
+
426
+ | `driverId` | Default model | Secret |
427
+ | ------------ | ------------------------------------------------------------------------------------------------- | -------------------- |
428
+ | `openrouter` | `typesafe/jev-1.13` (the response `model` is a dated pin, currently `typesafe/jev-1.13-20260917`) | `OPENROUTER_API_KEY` |
429
+ | `typesafe` | `jev-1.13.0` | `TYPESAFE_API_KEY` |
430
+
431
+ The deadline defaults to 30 seconds. `400`, `401`, `402`, `403`, `404`, and `422` throw `DecisionRequestError` and leave the breaker closed.
432
+ `429` and `529` retry up to 3 attempts and honor `Retry-After`. A wait over 60 seconds is an outage.
433
+
434
+ Network errors, timeouts, and HTTP 5xx count toward the breaker. Three failures open it for 30 seconds.
435
+ An open breaker is an outage: review parks, abstain returns `null`.
436
+
437
+ ## Troubleshooting
438
+
439
+ <Accordions>
440
+
441
+ <Accordion title='declare exactly one of review or onUncertain: "abstain"'>
442
+ The decision named both, or neither. Keep one. `autonomy` does not replace either.
443
+ </Accordion>
444
+
445
+ <Accordion title="review cannot run in HTTP flow">
446
+ `ai.decision("triage") review cannot run in HTTP flow "support.open" — a park answers 204. fx.emit
447
+ a signal and decide in a consumer.` Move `fx.decide` to a signal or clock Flow with `durable:
448
+ true`.
449
+ </Accordion>
450
+
451
+ <Accordion title="must set durable: true to review">
452
+ Compile: `ai.decision("triage"): flow "support.route" must set durable: true to review`. Runtime,
453
+ if it still runs: `fx.decide: "triage" review requires a durable journal`. Abstain is the mode
454
+ that can skip `durable`.
455
+ </Accordion>
456
+
457
+ <Accordion title="choice, score, and duplicate names">
458
+ `choice "team" has 255 options; max 254`. `choice "team" must not set none_of_these` — that option
459
+ is added for you. `score "urgency" needs 2–10 levels`. `question id "$" is reserved`. `duplicate
460
+ decision name` when two declarations share a name. `ask is empty` when `ask` has no questions.
461
+ </Accordion>
462
+
463
+ <Accordion title="OKE1010 UNDECLARED_DECIDE">
464
+ Cause: `Flow "support.route" decides "triage" without declaring it.` Add `triage` to
465
+ `effects.decides`, or drop the hand-written `effects` block so inference can see `fx.decide`.
466
+ </Accordion>
467
+
468
+ <Accordion title='fx.decide: secret "OPENROUTER_API_KEY" is not configured'>
469
+ `DecisionConfigError`. The Vault contract is missing or empty. TypeSafe looks for
470
+ `TYPESAFE_API_KEY` instead. This is not an outage, and it does not park.
471
+ </Accordion>
472
+
473
+ <Accordion title="400, 401, 402, 403, 404, or 422 from the provider">
474
+ `DecisionRequestError`. 400 and 422 are a body the provider rejected. 401 is the key. 402 is
475
+ billing. 403 is forbidden. 404 is an unknown model or path. The breaker stays closed. Fix the
476
+ request. Do not retry as if the provider were down.
477
+ </Accordion>
478
+
479
+ <Accordion title="oke boot: decisions are declared but rootDir and OKE_ROOT_DIR are unset">
480
+ `oke boot: decisions are declared but rootDir and OKE_ROOT_DIR are unset, so the decision lockfile
481
+ cannot load.` Boot with `rootDir`, or set `OKE_ROOT_DIR` to the directory that holds
482
+ `oke-decisions.lock.json`.
483
+ </Accordion>
484
+
485
+ <Accordion title="autonomy requires audit">
486
+ `ai.decision("triage"): autonomy requires audit`. `autonomy` without `audit` fails to compile. Set
487
+ `audit` from 0 to 1. `maxError` is required in the same object.
488
+ </Accordion>
489
+
490
+ <Accordion title="options must be an object literal">
491
+ `ai.decision("triage"): choice "team" options must be an object literal`. Write the options
492
+ inline. A variable is invisible to the compiler. The same applies when the review gate name cannot
493
+ be resolved: `ai.decision: review gate name could not be resolved`.
494
+ </Accordion>
495
+
496
+ <Accordion title="The candidate route is missing">
497
+ The candidate route is registered only when some decision declares `autonomy`. Without that block
498
+ there is no lockfile grant to promote, and `GET /_oke/decisions/:name/candidate` is not mounted.
499
+ </Accordion>
500
+
501
+ <Accordion title="Conflict or JournalLeaseBusy">
502
+ A second resolve after the review is stored is `Conflict`. Do not retry it. A lost race is
503
+ `JournalLeaseBusy` with `Retry-After`. Retry only that code. A suggested wait over 60 seconds
504
+ comes back as 503.
505
+ </Accordion>
506
+
507
+ <Accordion title="The certificate went stale after an edit">
508
+ Changing the question text or its options changes the hash. The runtime then refuses `auto`
509
+ (`stale-hash`) until `oke eval --certify` or `oke decide promote` writes a new lock entry. The old
510
+ threshold is not reused.
511
+ </Accordion>
512
+
513
+ </Accordions>
514
+
515
+ ## Learn more
516
+
517
+ - [Events](/docs/elements/ai/events) — tool approval uses the same 409 split
518
+ - [Gate](/docs/elements/gate) — the policy on `review`
519
+ - [fx](/docs/reference/fx) — `fx.decide` effect row
520
+ - [CLI](/docs/reference/cli) — `oke decide promote` and `oke eval --certify`
521
+
522
+ ## Next
523
+
524
+ <Cards>
525
+ <Card
526
+ title="Events"
527
+ description="Stream an agent run as AG-UI events."
528
+ href="/docs/elements/ai/events"
529
+ />
530
+ <Card
531
+ title="Agents"
532
+ description="Bounded tool loops with fx.run."
533
+ href="/docs/elements/ai/agents"
534
+ />
535
+ <Card title="AI" description="Models, prompts, agents, and decisions." href="/docs/elements/ai" />
536
+ </Cards>