okengine 0.22.0 → 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 (262) 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 +13 -9
  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/gate/auth.mdx +4 -1
  13. package/site/content/docs/elements/store/index.mdx +1 -1
  14. package/site/content/docs/elements/store/kv.mdx +5 -5
  15. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  16. package/site/content/docs/recipes/index.mdx +6 -6
  17. package/site/content/docs/recipes/postgres.mdx +12 -8
  18. package/site/content/docs/recipes/redis.mdx +4 -4
  19. package/site/content/docs/reference/cli.mdx +2 -1
  20. package/site/content/docs/reference/configuration.mdx +9 -7
  21. package/site/content/docs/reference/fx.mdx +7 -6
  22. package/src/auth/api-key-sql.test.ts +44 -0
  23. package/src/auth/api-key-sql.ts +1 -0
  24. package/src/auth/api-keys.ts +30 -12
  25. package/src/auth/config.ts +6 -2
  26. package/src/auth/gate-auth.test.ts +20 -0
  27. package/src/cli/agents-md.test.ts +25 -0
  28. package/src/cli/ask-vault-gaps.test.ts +38 -1
  29. package/src/cli/ask-vault-gaps.ts +35 -2
  30. package/src/cli/decide.ts +174 -0
  31. package/src/cli/decision-lock-watch.test.ts +45 -0
  32. package/src/cli/decision-lock-watch.ts +51 -0
  33. package/src/cli/dev-app-runner.ts +34 -8
  34. package/src/cli/dev-auth-secret.test.ts +49 -0
  35. package/src/cli/dev-auth-secret.ts +49 -0
  36. package/src/cli/dev-hot.test.ts +41 -0
  37. package/src/cli/dev-hot.ts +48 -0
  38. package/src/cli/dev.test.ts +1 -1
  39. package/src/cli/dev.ts +24 -6
  40. package/src/cli/eval.ts +142 -1
  41. package/src/cli/index.ts +5 -0
  42. package/src/cli/load-config.images.test.ts +2 -2
  43. package/src/cli/load-config.ts +1 -1
  44. package/src/cli/mcp-from-console.ts +48 -0
  45. package/src/cli/registry.ts +35 -1
  46. package/src/client/agent.ts +307 -0
  47. package/src/client/transport.test.ts +20 -0
  48. package/src/client/transport.ts +2 -1
  49. package/src/client/types.ts +1 -1
  50. package/src/client-react/index.ts +7 -1
  51. package/src/client-react/use-agent-run.test.ts +271 -0
  52. package/src/client-react/use-agent-run.ts +193 -0
  53. package/src/compiler/ai-approval.test.ts +36 -0
  54. package/src/compiler/ai-repair.test.ts +22 -0
  55. package/src/compiler/decisions.extract.test.ts +164 -0
  56. package/src/compiler/effects-infer.ts +27 -1
  57. package/src/compiler/extract.ts +268 -12
  58. package/src/compiler/response.ts +35 -2
  59. package/src/config/index.ts +21 -3
  60. package/src/console/server/ai-runs-flows.ts +269 -0
  61. package/src/console/server/ai-runs.test.ts +323 -0
  62. package/src/console/server/ai-runs.ts +562 -0
  63. package/src/console/server/ai.ts +12 -1
  64. package/src/console/server/app.ts +10 -1
  65. package/src/console/server/decisions-flows.ts +157 -0
  66. package/src/console/server/decisions.test.ts +66 -0
  67. package/src/console/server/decisions.ts +229 -0
  68. package/src/console/server/flows.ts +50 -2
  69. package/src/console/server/invoke-user-flow.ts +9 -0
  70. package/src/console/server/runs-ingest.ts +16 -0
  71. package/src/console/server/serve.ts +5 -0
  72. package/src/console/server/state.ts +25 -1
  73. package/src/console/server/store-stats.test.ts +47 -0
  74. package/src/console/server/store-stats.ts +92 -18
  75. package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
  76. package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-62Xts8Xi.js} +1 -1
  77. package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-DyeoKHKA.js} +1 -1
  78. package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DvfpLXsU.js} +1 -1
  79. package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-BY03SeCg.js} +1 -1
  80. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  81. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  82. package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
  83. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  84. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  85. package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
  86. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  87. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  88. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  89. package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
  90. package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-D7nNzpgB.js} +1 -1
  91. package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-DdL19zzh.js} +1 -1
  92. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  93. package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
  94. package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
  95. package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bb3FWUu9.js} +7 -7
  96. package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
  97. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  98. package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
  99. package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
  100. package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
  101. package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
  102. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  103. package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
  104. package/src/console/ui-next/dist/index.html +4 -3
  105. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  106. package/src/console/ui-next/src/client.ts +224 -1
  107. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  108. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  109. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  110. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  111. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  112. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  113. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  114. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  115. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  116. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  117. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  118. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  119. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  120. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  121. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  122. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  123. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  124. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  125. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  126. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  127. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  128. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  129. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  130. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  131. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  132. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  133. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  134. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  135. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  136. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  137. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
  138. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  139. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  140. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  141. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  142. package/src/console/ui-next/src/router.tsx +17 -0
  143. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  144. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  145. package/src/console/ui-next/vite.config.ts +9 -0
  146. package/src/docker/recipes/dragonfly.ts +3 -2
  147. package/src/docker/recipes/redis.ts +1 -1
  148. package/src/drivers/ai-anthropic.ts +212 -21
  149. package/src/drivers/ai-mock.ts +34 -1
  150. package/src/drivers/ai-openai-compatible.ts +67 -26
  151. package/src/drivers/ai-preconnect.ts +18 -0
  152. package/src/drivers/ai-stream.test.ts +62 -0
  153. package/src/drivers/ai-types.ts +20 -0
  154. package/src/drivers/journal-postgres.ts +289 -2
  155. package/src/drivers/memory-ddl.test.ts +18 -0
  156. package/src/drivers/memory.ts +21 -1
  157. package/src/drivers/pg-rls.test.ts +31 -0
  158. package/src/drivers/pg-rls.ts +15 -0
  159. package/src/drivers/postgres.test.ts +9 -0
  160. package/src/drivers/postgres.ts +49 -11
  161. package/src/elements/ai/agent-event-slot.ts +27 -0
  162. package/src/elements/ai/approval-http.ts +207 -0
  163. package/src/elements/ai/approval.test.ts +741 -0
  164. package/src/elements/ai/approval.ts +228 -0
  165. package/src/elements/ai/decisions/bind.ts +163 -0
  166. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  167. package/src/elements/ai/decisions/certificate.ts +377 -0
  168. package/src/elements/ai/decisions/certify.ts +314 -0
  169. package/src/elements/ai/decisions/decide.test.ts +953 -0
  170. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  171. package/src/elements/ai/decisions/export.test.ts +120 -0
  172. package/src/elements/ai/decisions/export.ts +146 -0
  173. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  174. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  175. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  176. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  177. package/src/elements/ai/decisions/http.test.ts +154 -0
  178. package/src/elements/ai/decisions/http.ts +240 -0
  179. package/src/elements/ai/decisions/labels.test.ts +282 -0
  180. package/src/elements/ai/decisions/labels.ts +257 -0
  181. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  182. package/src/elements/ai/decisions/openrouter.ts +36 -0
  183. package/src/elements/ai/decisions/provider.ts +102 -0
  184. package/src/elements/ai/decisions/typesafe.ts +36 -0
  185. package/src/elements/ai/declare.ts +252 -1
  186. package/src/elements/ai/events.test.ts +379 -0
  187. package/src/elements/ai/events.ts +138 -0
  188. package/src/elements/ai/run-events.test.ts +461 -0
  189. package/src/elements/ai/run-events.ts +522 -0
  190. package/src/elements/ai/runtime.ts +1186 -105
  191. package/src/elements/ai/stream-turn.ts +214 -0
  192. package/src/elements/ai/stream.live.test.ts +76 -0
  193. package/src/elements/ai/subagent.test.ts +136 -0
  194. package/src/elements/ai.test.ts +497 -3
  195. package/src/elements/ai.ts +16 -1
  196. package/src/elements/clock/durable.ts +27 -1
  197. package/src/elements/store/sql-session.test.ts +51 -0
  198. package/src/elements/store/sql-session.ts +111 -1
  199. package/src/i18n/catalogs/ar.ts +5 -0
  200. package/src/i18n/catalogs/en.ts +5 -0
  201. package/src/index.ts +2 -0
  202. package/src/kernel/agent-event-store.ts +448 -0
  203. package/src/kernel/app.ts +103 -1
  204. package/src/kernel/boot-bind/ai.ts +3 -0
  205. package/src/kernel/boot-bind/store.test.ts +26 -0
  206. package/src/kernel/boot-bind/store.ts +82 -6
  207. package/src/kernel/boot.ts +15 -4
  208. package/src/kernel/builtin-errors.ts +2 -0
  209. package/src/kernel/capability.ts +3 -0
  210. package/src/kernel/decision-label-store.ts +441 -0
  211. package/src/kernel/effects.test.ts +2 -1
  212. package/src/kernel/effects.ts +13 -1
  213. package/src/kernel/element-registries.ts +3 -0
  214. package/src/kernel/errors-text.ts +4 -0
  215. package/src/kernel/errors.ts +2 -0
  216. package/src/kernel/flow.test.ts +2 -3
  217. package/src/kernel/fx-decide.ts +761 -0
  218. package/src/kernel/fx.test.ts +12 -5
  219. package/src/kernel/fx.ts +312 -29
  220. package/src/kernel/http-frame.test.ts +94 -0
  221. package/src/kernel/http-frame.ts +203 -0
  222. package/src/kernel/journal.ts +104 -0
  223. package/src/kernel/json-result.ts +10 -0
  224. package/src/kernel/pipeline.test.ts +45 -0
  225. package/src/kernel/sse-id.ts +32 -0
  226. package/src/manifest/diff.ts +1 -0
  227. package/src/manifest/types.ts +29 -2
  228. package/src/mcp/ai-tools.test.ts +202 -0
  229. package/src/mcp/authorization.ts +66 -0
  230. package/src/mcp/session.ts +3 -0
  231. package/src/mcp/tools.ts +168 -0
  232. package/src/release/http-graph.test.ts +2 -0
  233. package/src/release/http-graph.ts +5 -0
  234. package/src/release/measure.ts +39 -14
  235. package/src/runs/collect.ts +4 -1
  236. package/src/runs/parquet.test.ts +27 -1
  237. package/src/runs/parquet.ts +19 -0
  238. package/src/runs/types.ts +31 -0
  239. package/src/runtime/json-code-block.test.ts +106 -47
  240. package/src/runtime/json-code-block.ts +1209 -46
  241. package/src/term.test.ts +13 -0
  242. package/src/term.ts +81 -49
  243. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  244. package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
  245. package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
  246. package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
  247. package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
  248. package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
  249. package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
  250. package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
  251. package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
  252. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  253. package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
  254. package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
  255. package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
  256. package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
  257. package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
  258. package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
  259. package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
  260. package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
  261. package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
  262. package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
@@ -28,6 +28,7 @@ import { SearchConfigError } from "../elements/store/search-errors.ts";
28
28
  import type {
29
29
  Ai,
30
30
  AiAgent,
31
+ AiDecision,
31
32
  AiMcpServer,
32
33
  AiModel,
33
34
  AiPrompt,
@@ -257,11 +258,18 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
257
258
  if (Object.keys(gates).length > 0) manifest.gates = gates;
258
259
  if (Object.keys(vault).length > 0) manifest.vault = vault;
259
260
  if (Object.keys(channels).length > 0) manifest.channels = channels;
260
- if (scope.ai.models || scope.ai.prompts || scope.ai.agents || scope.ai.mcpServers) {
261
+ if (
262
+ scope.ai.models ||
263
+ scope.ai.prompts ||
264
+ scope.ai.agents ||
265
+ scope.ai.decisions ||
266
+ scope.ai.mcpServers
267
+ ) {
261
268
  manifest.ai = {
262
269
  ...(scope.ai.models ? { models: sortRecord(scope.ai.models) } : {}),
263
270
  ...(scope.ai.prompts ? { prompts: sortRecord(scope.ai.prompts) } : {}),
264
271
  ...(scope.ai.agents ? { agents: sortRecord(scope.ai.agents) } : {}),
272
+ ...(scope.ai.decisions ? { decisions: sortRecord(scope.ai.decisions) } : {}),
265
273
  ...(scope.ai.mcpServers ? { mcpServers: sortRecord(scope.ai.mcpServers) } : {}),
266
274
  };
267
275
  }
@@ -1426,7 +1434,11 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
1426
1434
  }
1427
1435
 
1428
1436
  if (obj === "ai" && prop === "agent") {
1429
- collectAgent(call, scope);
1437
+ collectAgent(call, program, scope);
1438
+ }
1439
+
1440
+ if (obj === "ai" && prop === "decision") {
1441
+ collectDecision(call, program, scope);
1430
1442
  }
1431
1443
 
1432
1444
  if (obj === "ai" && prop === "mcpServer") {
@@ -1494,6 +1506,7 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
1494
1506
  : {}),
1495
1507
  ...(via && via.length > 0 ? { via } : {}),
1496
1508
  ...(timeout !== undefined ? { timeout } : {}),
1509
+ ...(numberProp(opts, "repair") === 1 ? { repair: 1 as const } : {}),
1497
1510
  };
1498
1511
  scope.ai.prompts = scope.ai.prompts ?? {};
1499
1512
  scope.ai.prompts[promptName] = prompt;
@@ -1648,27 +1661,267 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
1648
1661
  }
1649
1662
  }
1650
1663
 
1651
- function collectAgent(call: CallExpression, scope: ProjectScope): void {
1664
+ function agentToolEntry(
1665
+ el: AstNode,
1666
+ scope: ProjectScope,
1667
+ ): { name: string; approval: boolean } | undefined {
1668
+ if (el.type === "ObjectExpression") {
1669
+ const nameNode = objectProp(el, "name");
1670
+ const raw =
1671
+ (nameNode ? resolveCallTarget(nameNode, scope.bindings) : undefined) ??
1672
+ stringArg(nameNode) ??
1673
+ identifierName(nameNode);
1674
+ if (!raw) return undefined;
1675
+ const name = scope.flowExports.get(raw) ?? scope.bindings.get(raw)?.ref ?? raw;
1676
+ const approvalNode = objectProp(el, "approval");
1677
+ return { name, approval: approvalNode !== undefined && boolProp(el, "approval") !== false };
1678
+ }
1679
+ const resolved = resolveCallTarget(el, scope.bindings);
1680
+ const raw = resolved ?? identifierName(el) ?? stringArg(el);
1681
+ if (!raw) return undefined;
1682
+ const name = scope.flowExports.get(raw) ?? scope.bindings.get(raw)?.ref ?? raw;
1683
+ return { name, approval: false };
1684
+ }
1685
+
1686
+ function assertDecisionPlacement(flowName: string, flow: Flow, scope: ProjectScope): void {
1687
+ const names = flow.effects?.decides ?? [];
1688
+ if (names.length === 0) return;
1689
+ const http = flow.trigger !== undefined && "http" in flow.trigger;
1690
+ for (const decisionName of names) {
1691
+ const decision = scope.ai.decisions?.[decisionName];
1692
+ if (!decision || decision.mode !== "review") continue;
1693
+ if (http) {
1694
+ throw new Error(
1695
+ `ai.decision("${decisionName}") review cannot run in HTTP flow "${flowName}" — a park answers 204. fx.emit a signal and decide in a consumer.`,
1696
+ );
1697
+ }
1698
+ if (flow.durable !== true) {
1699
+ throw new Error(
1700
+ `ai.decision("${decisionName}"): flow "${flowName}" must set durable: true to review`,
1701
+ );
1702
+ }
1703
+ }
1704
+ }
1705
+
1706
+ function decisionReviewName(node: AstNode | undefined, scope: ProjectScope): string {
1707
+ const literal = stringArg(node);
1708
+ if (literal) return literal;
1709
+ if (node?.type === "Identifier") {
1710
+ const name = (node as Identifier).name;
1711
+ return scope.gateIds.get(name) ?? name;
1712
+ }
1713
+ if (node?.type === "CallExpression") {
1714
+ const arg = stringArg((node as CallExpression).arguments[0]);
1715
+ if (arg) return arg;
1716
+ }
1717
+ if (node?.type === "MemberExpression") {
1718
+ const member = node as AstNode & { property: AstNode; computed?: boolean };
1719
+ if (member.property.type === "Identifier" && member.computed !== true) {
1720
+ return (member.property as Identifier).name;
1721
+ }
1722
+ }
1723
+ throw new Error("ai.decision: review gate name could not be resolved");
1724
+ }
1725
+
1726
+ function assertDecisionQuestion(
1727
+ decisionName: string,
1728
+ key: string,
1729
+ value: AstNode | undefined,
1730
+ program: AstNode,
1731
+ ): void {
1732
+ const resolved = resolveDecisionExpr(value, program, decisionName, key);
1733
+ if (!resolved || resolved.type !== "CallExpression") {
1734
+ throw new Error(
1735
+ `ai.decision("${decisionName}"): question "${key}" must be ai.choice, ai.score, or ai.boolean`,
1736
+ );
1737
+ }
1738
+ const callee = (resolved as CallExpression).callee;
1739
+ const member = callee as AstNode & { property?: AstNode };
1740
+ const fn =
1741
+ callee.type === "MemberExpression" && member.property?.type === "Identifier"
1742
+ ? (member.property as Identifier).name
1743
+ : undefined;
1744
+ const args = (resolved as CallExpression).arguments;
1745
+ if (fn === "choice") {
1746
+ const options = args[1];
1747
+ if (!options || options.type !== "ObjectExpression") {
1748
+ throw new Error(
1749
+ `ai.decision("${decisionName}"): choice "${key}" options must be an object literal`,
1750
+ );
1751
+ }
1752
+ const keys = objectProperties(options)
1753
+ .map((prop) => propKey(prop))
1754
+ .filter((name): name is string => typeof name === "string");
1755
+ if (keys.includes("none_of_these")) {
1756
+ throw new Error(`ai.decision("${decisionName}"): choice "${key}" must not set none_of_these`);
1757
+ }
1758
+ if (keys.length > 254) {
1759
+ throw new Error(
1760
+ `ai.decision("${decisionName}"): choice "${key}" has ${keys.length} options; max 254`,
1761
+ );
1762
+ }
1763
+ }
1764
+ if (fn === "score") {
1765
+ const levels = resolveScoreLevels(args[1], program, decisionName, key);
1766
+ const count = ((levels as AstNode & { elements?: unknown[] }).elements ?? []).length;
1767
+ if (count < 2 || count > 10) {
1768
+ throw new Error(`ai.decision("${decisionName}"): score "${key}" needs 2–10 levels`);
1769
+ }
1770
+ }
1771
+ }
1772
+
1773
+ function resolveDecisionExpr(
1774
+ value: AstNode | undefined,
1775
+ program: AstNode,
1776
+ decisionName: string,
1777
+ key: string,
1778
+ ): AstNode | undefined {
1779
+ if (!value) return undefined;
1780
+ if (value.type !== "Identifier") return value;
1781
+ const name = (value as Identifier).name;
1782
+ const init = sameFileConstInit(name, program);
1783
+ if (!init) {
1784
+ throw new Error(
1785
+ `ai.decision("${decisionName}"): question "${key}" is "${name}", which is not a same-file const`,
1786
+ );
1787
+ }
1788
+ return init;
1789
+ }
1790
+
1791
+ function resolveScoreLevels(
1792
+ levels: AstNode | undefined,
1793
+ program: AstNode,
1794
+ decisionName: string,
1795
+ key: string,
1796
+ ): AstNode {
1797
+ const resolved =
1798
+ levels?.type === "Identifier"
1799
+ ? sameFileConstInit((levels as Identifier).name, program)
1800
+ : levels;
1801
+ if (!resolved || resolved.type !== "ArrayExpression") {
1802
+ const label = levels?.type === "Identifier" ? (levels as Identifier).name : "the levels";
1803
+ throw new Error(
1804
+ `ai.decision("${decisionName}"): score "${key}" levels (${label}) must be an array literal or a same-file const`,
1805
+ );
1806
+ }
1807
+ return resolved;
1808
+ }
1809
+
1810
+ function sameFileConstInit(name: string, program: AstNode): AstNode | undefined {
1811
+ let found: AstNode | undefined;
1812
+ walk(program, (node) => {
1813
+ if (found || node.type !== "VariableDeclarator") return;
1814
+ const decl = node as AstNode & { id?: AstNode; init?: AstNode };
1815
+ if (decl.id?.type === "Identifier" && (decl.id as Identifier).name === name) {
1816
+ found = decl.init;
1817
+ }
1818
+ });
1819
+ return found;
1820
+ }
1821
+
1822
+ function collectDecision(call: CallExpression, program: AstNode, scope: ProjectScope): void {
1823
+ const decisionName = stringArg(call.arguments[0]);
1824
+ const opts = objectArg(call.arguments[1]);
1825
+ if (!decisionName || !opts) return;
1826
+ const ask = objectProp(opts, "ask");
1827
+ const questionProps = ask?.type === "ObjectExpression" ? objectProperties(ask) : [];
1828
+ const questionNames = questionProps
1829
+ .map((prop) => propKey(prop))
1830
+ .filter((name): name is string => typeof name === "string" && name.length > 0);
1831
+ if (questionNames.length === 0) {
1832
+ throw new Error(`ai.decision("${decisionName}"): ask is empty`);
1833
+ }
1834
+ for (const prop of questionProps) {
1835
+ const key = propKey(prop);
1836
+ if (!key) continue;
1837
+ if (key === "meta" || key === "$") {
1838
+ throw new Error(`ai.decision("${decisionName}"): question id "${key}" is reserved`);
1839
+ }
1840
+ assertDecisionQuestion(
1841
+ decisionName,
1842
+ key,
1843
+ (prop as AstNode & { value?: AstNode }).value,
1844
+ program,
1845
+ );
1846
+ }
1847
+ if (scope.ai.decisions?.[decisionName]) {
1848
+ throw new Error(`ai.decision("${decisionName}"): duplicate decision name`);
1849
+ }
1850
+ const hasReview = objectProp(opts, "review") !== undefined;
1851
+ const uncertain = stringProp(opts, "onUncertain");
1852
+ const abstain = uncertain === "abstain";
1853
+ if (hasReview === abstain) {
1854
+ throw new Error(
1855
+ `ai.decision("${decisionName}"): declare exactly one of review or onUncertain: "abstain"`,
1856
+ );
1857
+ }
1858
+ const autonomyNode = objectProp(opts, "autonomy");
1859
+ let autonomy: AiDecision["autonomy"];
1860
+ if (autonomyNode?.type === "ObjectExpression") {
1861
+ if (objectProp(autonomyNode, "audit") === undefined) {
1862
+ throw new Error(`ai.decision("${decisionName}"): autonomy requires audit`);
1863
+ }
1864
+ const maxError = numberProp(autonomyNode, "maxError");
1865
+ const audit = numberProp(autonomyNode, "audit");
1866
+ const risk = numberProp(autonomyNode, "risk");
1867
+ if (maxError === undefined || audit === undefined) {
1868
+ throw new Error(`ai.decision("${decisionName}"): autonomy requires maxError and audit`);
1869
+ }
1870
+ autonomy = { maxError, audit, ...(risk !== undefined ? { risk } : {}) };
1871
+ }
1872
+ const evals = stringProp(opts, "evals");
1873
+ const decision: AiDecision = {
1874
+ mode: abstain ? "abstain" : "review",
1875
+ questions: questionNames,
1876
+ ...(hasReview ? { review: decisionReviewName(objectProp(opts, "review"), scope) } : {}),
1877
+ ...(stringProp(opts, "model") ? { model: stringProp(opts, "model") } : {}),
1878
+ driverId: stringProp(opts, "driverId") === "typesafe" ? "typesafe" : "openrouter",
1879
+ ...(evals !== undefined ? { evals } : {}),
1880
+ ...(autonomy !== undefined ? { autonomy } : {}),
1881
+ };
1882
+ scope.ai.decisions = scope.ai.decisions ?? {};
1883
+ scope.ai.decisions[decisionName] = decision;
1884
+ const bindingName = enclosingConstName(call, program);
1885
+ if (bindingName) {
1886
+ scope.bindings.set(bindingName, { kind: "decision", ref: decisionName });
1887
+ }
1888
+ }
1889
+
1890
+ function collectAgent(call: CallExpression, program: AstNode, scope: ProjectScope): void {
1652
1891
  const agentName = stringArg(call.arguments[0]);
1653
1892
  const opts = objectArg(call.arguments[1]);
1654
1893
  if (!agentName || !opts) return;
1655
1894
  const toolsArr = arrayProp(opts, "tools");
1656
- const tools = toolsArr
1657
- ?.map((el) => {
1658
- const resolved = resolveCallTarget(el, scope.bindings);
1659
- if (resolved) return resolved;
1660
- return identifierName(el) ?? stringArg(el);
1661
- })
1662
- .filter((x): x is string => typeof x === "string")
1663
- .map((id) => scope.flowExports.get(id) ?? scope.bindings.get(id)?.ref ?? id);
1895
+ const entries = (toolsArr ?? [])
1896
+ .map((el) => agentToolEntry(el, scope))
1897
+ .filter((entry): entry is { name: string; approval: boolean } => entry !== undefined);
1898
+ const tools = entries.map((entry) => entry.name);
1899
+ const agentCalls = tools.filter(
1900
+ (name) => scope.bindings.get(name)?.kind === "agent" || scope.ai.agents?.[name],
1901
+ );
1902
+ const approvals = Object.fromEntries(
1903
+ entries.filter((entry) => entry.approval).map((entry) => [entry.name, true as const]),
1904
+ );
1664
1905
  const agent: AiAgent = {
1665
- ...(tools && tools.length > 0 ? { tools } : {}),
1906
+ ...(tools.length > 0 ? { tools } : {}),
1907
+ ...(Object.keys(approvals).length > 0 ? { approvals } : {}),
1666
1908
  ...(numberProp(opts, "maxSteps") !== undefined
1667
1909
  ? { maxSteps: numberProp(opts, "maxSteps") }
1668
1910
  : {}),
1911
+ ...(numberProp(opts, "maxDepth") !== undefined
1912
+ ? { maxDepth: numberProp(opts, "maxDepth") }
1913
+ : {}),
1669
1914
  };
1670
1915
  scope.ai.agents = scope.ai.agents ?? {};
1671
1916
  scope.ai.agents[agentName] = agent;
1917
+ const bindingName = enclosingConstName(call, program);
1918
+ if (bindingName) {
1919
+ scope.bindings.set(bindingName, {
1920
+ kind: "agent",
1921
+ ref: agentName,
1922
+ ...(agentCalls.length > 0 ? { agentCalls } : {}),
1923
+ });
1924
+ }
1672
1925
  }
1673
1926
 
1674
1927
  function collectMcpServer(call: CallExpression, program: AstNode, scope: ProjectScope): void {
@@ -2010,6 +2263,7 @@ function registerFlow(args: {
2010
2263
  flow.source = `${args.file.path}:${line}`;
2011
2264
 
2012
2265
  if (boolProp(opts, "durable")) flow.durable = true;
2266
+ assertDecisionPlacement(name, flow, args.scope);
2013
2267
  if (typeof liveFromTrigger === "string") {
2014
2268
  flow.live = liveFromTrigger;
2015
2269
  // Manual live surface — `.live(tableBinding)` on the trigger. Synthesize
@@ -2908,6 +3162,7 @@ function parseEffectsObject(node: AstNode | undefined): Effects | undefined {
2908
3162
  const secrets = stringArrayProp(node, "secrets");
2909
3163
  const calls = stringArrayProp(node, "calls");
2910
3164
  const fetches = stringArrayProp(node, "fetches");
3165
+ const decides = stringArrayProp(node, "decides");
2911
3166
  if (reads) effects.reads = reads as Effects["reads"];
2912
3167
  if (writes) effects.writes = writes as Effects["writes"];
2913
3168
  if (emits) effects.emits = emits;
@@ -2917,6 +3172,7 @@ function parseEffectsObject(node: AstNode | undefined): Effects | undefined {
2917
3172
  if (secrets) effects.secrets = secrets;
2918
3173
  if (calls) effects.calls = calls;
2919
3174
  if (fetches) effects.fetches = fetches;
3175
+ if (decides) effects.decides = decides;
2920
3176
  return effects;
2921
3177
  }
2922
3178
 
@@ -70,6 +70,11 @@ export function encodeFailure(failure: FlowFailure): Response {
70
70
  });
71
71
  }
72
72
 
73
+ /** SSE id reader. Kept off the edge bundle until a stream chunk is encoded. */
74
+ function loadSseId(): typeof import("../kernel/sse-id.ts") {
75
+ return lazyRequire(`${import.meta.dir}/../kernel`, ["sse", "id"].join("-"));
76
+ }
77
+
73
78
  function loadFxLiveStream(): typeof import("../kernel/fx-live-stream.ts") {
74
79
  return lazyRequire(`${import.meta.dir}/../kernel`, ["fx", "live", "stream"].join("-"));
75
80
  }
@@ -133,6 +138,24 @@ export async function encodeExecuteResult(result: {
133
138
  return encodeSuccess(result.output);
134
139
  }
135
140
 
141
+ /**
142
+ * Durable park thrown from a streamed agent after the interrupt frame.
143
+ *
144
+ * Duck-typed so this encoder does not import the journal module.
145
+ *
146
+ * @param err - Value thrown by the chunk iterator
147
+ */
148
+ function isStreamPark(err: unknown): err is { readonly wakeAt: number; readonly label: string } {
149
+ return (
150
+ err instanceof Error &&
151
+ err.name === "JournalSuspend" &&
152
+ "wakeAt" in err &&
153
+ typeof err.wakeAt === "number" &&
154
+ "label" in err &&
155
+ typeof err.label === "string"
156
+ );
157
+ }
158
+
136
159
  function encodeSseStream(carrier: JsonStreamResult): Response {
137
160
  const encoder = new TextEncoder();
138
161
  let finalized = false;
@@ -147,8 +170,12 @@ function encodeSseStream(carrier: JsonStreamResult): Response {
147
170
  for await (const chunk of carrier.chunks) {
148
171
  const frame = loadFxJson().isSseFrame(chunk)
149
172
  ? chunk
150
- : { data: chunk as unknown, id: undefined as string | undefined };
173
+ : { data: chunk as unknown, id: loadSseId().readSseId(chunk) };
151
174
  const lines: string[] = [];
175
+ if ("comment" in frame && frame.comment && frame.data === undefined) {
176
+ controller.enqueue(encoder.encode(`: ${frame.comment}\n\n`));
177
+ continue;
178
+ }
152
179
  if (frame.id !== undefined) lines.push(`id: ${frame.id}`);
153
180
  lines.push(`data: ${JSON.stringify(frame.data)}`);
154
181
  controller.enqueue(encoder.encode(`${lines.join("\n")}\n\n`));
@@ -156,7 +183,13 @@ function encodeSseStream(carrier: JsonStreamResult): Response {
156
183
  controller.enqueue(encoder.encode("data: [DONE]\n\n"));
157
184
  controller.close();
158
185
  } catch (err) {
159
- controller.error(err);
186
+ if (isStreamPark(err)) {
187
+ carrier.parked = { wakeAt: err.wakeAt, label: err.label };
188
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
189
+ controller.close();
190
+ } else {
191
+ controller.error(err);
192
+ }
160
193
  } finally {
161
194
  await finish();
162
195
  }
@@ -508,12 +508,30 @@ export function resolveDriverId(
508
508
  env: ConfigEnv,
509
509
  defaults?: EnvDriverMap,
510
510
  ): string | undefined {
511
+ return driverRefId(resolveDriverRef(map, env, defaults));
512
+ }
513
+
514
+ /**
515
+ * Resolve the rich driver ref for an env (`pool`, `replicas`, `url`).
516
+ *
517
+ * Same merge rules as {@link resolveDriverId}. A string pin has no pool or
518
+ * replicas. With `defaults`, an unset env key keeps that driver's default
519
+ * pin rather than a sibling environment.
520
+ *
521
+ * @param map - Env → driver map (or bare ref)
522
+ * @param env - Active environment
523
+ * @param defaults - Real default map for this specific driver (per-key merge)
524
+ */
525
+ export function resolveDriverRef(
526
+ map: EnvDriverInput | undefined,
527
+ env: ConfigEnv,
528
+ defaults?: EnvDriverMap,
529
+ ): DriverRef | undefined {
511
530
  const normalized = normalizeEnvDriverMap(map);
512
531
  if (defaults) {
513
532
  const merged = mergeEnvDriverMap(normalized, defaults);
514
- return driverRefId(merged[env]);
533
+ return merged[env];
515
534
  }
516
535
  if (!normalized) return undefined;
517
- const ref = normalized[env] ?? normalized.dev ?? normalized.prod ?? normalized.test;
518
- return driverRefId(ref);
536
+ return normalized[env] ?? normalized.dev ?? normalized.prod ?? normalized.test;
519
537
  }