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
@@ -55,6 +55,41 @@ export interface DoctorDiffResult {
55
55
  readonly allChanges: readonly ManifestChange[];
56
56
  }
57
57
 
58
+ const COMMENT_CATEGORIES = ["contract-breaking", "permission-widening", "effect-widening"] as const;
59
+
60
+ const CATEGORY_LABEL: Record<(typeof COMMENT_CATEGORIES)[number], string> = {
61
+ "contract-breaking": "Contract breaks",
62
+ "permission-widening": "Permission widening",
63
+ "effect-widening": "Effect widening",
64
+ };
65
+
66
+ /**
67
+ * Plain-language pull-request comment for the three behavioural categories.
68
+ * Widening is informational. Contract breaks are the lines that fail CI
69
+ * when they are undeclared.
70
+ *
71
+ * @param changes - {@link diffManifest} changes
72
+ */
73
+ export function formatManifestDiffComment(changes: readonly ManifestChange[]): string {
74
+ const lines = ["## Manifest diff", ""];
75
+ for (const category of COMMENT_CATEGORIES) {
76
+ const rows = changes.filter((change) => change.category === category);
77
+ lines.push(`### ${CATEGORY_LABEL[category]}`);
78
+ if (rows.length === 0) {
79
+ lines.push("None.");
80
+ } else {
81
+ for (const row of rows) {
82
+ lines.push(`- \`${row.path}\` — ${row.summary}`);
83
+ }
84
+ }
85
+ lines.push("");
86
+ }
87
+ lines.push(
88
+ "Undeclared contract breaks fail the check. Permission and effect widening are comments only. Acknowledge an intentional break with `breaking: true` on the HTTP, call, MCP, or resource contract.",
89
+ );
90
+ return lines.join("\n");
91
+ }
92
+
58
93
  /**
59
94
  * Diff two manifests and fail on undeclared contract breaks.
60
95
  *
package/src/cli/eval.ts CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { resolve } from "node:path";
6
6
  import { parseEvalJsonl, runPromptEvals, type EvalCase } from "../elements/ai/eval.ts";
7
+ import type { DecisionResponse } from "../elements/ai/decisions/provider.ts";
7
8
  import type { Manifest } from "../manifest/types.ts";
8
9
 
9
10
  /** Options for {@link runOkeEval}. */
@@ -96,17 +97,157 @@ export async function runOkeEval(options: OkeEvalOptions = {}): Promise<number>
96
97
  */
97
98
  export async function evalCli(args: string[]): Promise<number> {
98
99
  let manifestPath: string | undefined;
100
+ let certify = false;
99
101
  for (let i = 0; i < args.length; i++) {
100
102
  const a = args[i]!;
101
103
  if (a === "--manifest" || a === "-m") {
102
104
  manifestPath = args[++i];
105
+ } else if (a === "--certify") {
106
+ certify = true;
103
107
  } else if (a === "--help" || a === "-h") {
104
- console.log(`oke eval [--manifest oke.manifest.json]
108
+ console.log(`oke eval [--manifest oke.manifest.json] [--certify]
105
109
 
106
110
  Run prompt eval sets declared in the Manifest. Fails CI on regression.
111
+ --certify builds a decision certificate from each decision seed file.
107
112
  `);
108
113
  return 0;
109
114
  }
110
115
  }
116
+ if (certify) return runOkeCertify({ manifestPath });
111
117
  return runOkeEval({ manifestPath });
112
118
  }
119
+
120
+ /**
121
+ * Build certificates from decision seed files. Prompt evals are not run.
122
+ *
123
+ * @param options - Manifest path
124
+ */
125
+ export async function runOkeCertify(
126
+ options: {
127
+ readonly manifestPath?: string;
128
+ readonly manifest?: Manifest;
129
+ readonly root?: string;
130
+ readonly evaluate?: (input: unknown) => Promise<DecisionResponse>;
131
+ } = {},
132
+ ): Promise<number> {
133
+ const { certifySeed } = await import("../elements/ai/decisions/certify.ts");
134
+ const { aiDecisionRegistry } = await import("../kernel/element-registries.ts");
135
+ const { DECISION_LOCK_FILENAME, parseDecisionLockfile } =
136
+ await import("../elements/ai/decisions/certificate.ts");
137
+ const { resolveStartEntry } = await import("./start.ts");
138
+ const root = resolve(options.root ?? process.env["OKE_ROOT_DIR"] ?? ".");
139
+ process.env["OKE_ROOT_DIR"] ??= root;
140
+ let manifest = options.manifest;
141
+ if (!manifest) {
142
+ try {
143
+ const entry = await resolveStartEntry(root);
144
+ await import(entry);
145
+ } catch (err) {
146
+ console.error(
147
+ `oke eval: failed to load the app: ${err instanceof Error ? err.message : String(err)}`,
148
+ );
149
+ return 1;
150
+ }
151
+ const path = resolve(root, options.manifestPath ?? "oke.manifest.json");
152
+ const file = Bun.file(path);
153
+ if (!(await file.exists())) {
154
+ console.error(`oke eval: manifest not found: ${path}`);
155
+ return 1;
156
+ }
157
+ manifest = (await file.json()) as Manifest;
158
+ }
159
+ const decisions = manifest.ai?.decisions ?? {};
160
+ const names = Object.keys(decisions);
161
+ if (names.length === 0) {
162
+ process.stdout.write("oke eval: no decisions declared\n");
163
+ return 0;
164
+ }
165
+ const injected = options.evaluate;
166
+ const lockPath = resolve(root, DECISION_LOCK_FILENAME);
167
+ const existingFile = Bun.file(lockPath);
168
+ const current = (await existingFile.exists())
169
+ ? parseDecisionLockfile(await existingFile.json())
170
+ : undefined;
171
+ const next = { decisions: { ...(current?.decisions ?? {}) } };
172
+ for (const name of names) {
173
+ const decision = decisions[name];
174
+ if (!decision?.evals) {
175
+ process.stdout.write(`oke eval: skip ${name} (no evals)\n`);
176
+ continue;
177
+ }
178
+ const decl = aiDecisionRegistry.find((item) => item.name === name);
179
+ if (!decl) {
180
+ console.error(`oke eval: decision "${name}" is not loaded`);
181
+ return 1;
182
+ }
183
+ const text = await Bun.file(resolve(root, decision.evals)).text();
184
+ const evaluate = injected ?? (await providerForDecision(decl));
185
+ next.decisions[name] = await certifySeed({
186
+ model: decl.model ?? decision.model ?? "",
187
+ maxError: decision.autonomy?.maxError ?? 0.05,
188
+ delta: decision.autonomy?.risk ?? decl.autonomy?.risk ?? 0.1,
189
+ jsonl: text,
190
+ ask: decl.ask,
191
+ evaluate,
192
+ });
193
+ }
194
+ await Bun.write(lockPath, `${JSON.stringify(next, null, 2)}\n`);
195
+ const { setDecisionDrift } = await import("../elements/ai/decisions/certificate.ts");
196
+ const { persistDecisionDrift } = await import("../elements/ai/decisions/labels.ts");
197
+ for (const name of names) {
198
+ setDecisionDrift(name, false);
199
+ persistDecisionDrift(name, false);
200
+ }
201
+ return 0;
202
+ }
203
+
204
+ async function providerForDecision(
205
+ decl: import("../elements/ai/declare.ts").AiDecisionDecl,
206
+ ): Promise<(input: unknown) => Promise<DecisionResponse>> {
207
+ const typesafe = decl.driverId === "typesafe";
208
+ const keyName = typesafe ? "TYPESAFE_API_KEY" : "OPENROUTER_API_KEY";
209
+ const apiKey = process.env[keyName];
210
+ if (!apiKey) throw new Error(`oke eval: secret "${keyName}" is not configured`);
211
+ const { createTypesafeDecisionProvider, TYPESAFE_JEV_MODEL } =
212
+ await import("../elements/ai/decisions/typesafe.ts");
213
+ const { createOpenRouterDecisionProvider, OPENROUTER_JEV_MODEL } =
214
+ await import("../elements/ai/decisions/openrouter.ts");
215
+ const provider = typesafe
216
+ ? createTypesafeDecisionProvider(apiKey)
217
+ : createOpenRouterDecisionProvider(apiKey);
218
+ const model = decl.model ?? (typesafe ? TYPESAFE_JEV_MODEL : OPENROUTER_JEV_MODEL);
219
+ return (input) =>
220
+ provider.evaluate({
221
+ model,
222
+ state: input,
223
+ questions: wireFromDecl(decl),
224
+ });
225
+ }
226
+
227
+ function wireFromDecl(
228
+ decl: import("../elements/ai/declare.ts").AiDecisionDecl,
229
+ ): import("../elements/ai/decisions/provider.ts").DecisionRequest["questions"] {
230
+ const questions: Record<string, import("../elements/ai/decisions/provider.ts").WireQuestion> = {};
231
+ for (const [id, question] of Object.entries(decl.ask)) {
232
+ if (question.kind === "choice") {
233
+ questions[id] = {
234
+ type: "choice",
235
+ instructions: question.instructions,
236
+ criteria: { ...question.options, none_of_these: null },
237
+ };
238
+ } else if (question.kind === "score") {
239
+ questions[id] = {
240
+ type: "score",
241
+ instructions: question.instructions,
242
+ criteria: question.levels,
243
+ };
244
+ } else {
245
+ questions[id] = {
246
+ type: "noul",
247
+ instructions: question.instructions,
248
+ ...(question.criteria !== undefined ? { criteria: question.criteria } : {}),
249
+ };
250
+ }
251
+ }
252
+ return questions;
253
+ }
package/src/cli/index.ts CHANGED
@@ -16,6 +16,7 @@ import { dbCli } from "./db.ts";
16
16
  import { devCli } from "./dev.ts";
17
17
  import { doctorCli } from "./doctor.ts";
18
18
  import { dockerCli } from "./docker.ts";
19
+ import { decideCli } from "./decide.ts";
19
20
  import { evalCli } from "./eval.ts";
20
21
  import { EXIT_CODE_HELP, EXIT_OK, EXIT_USAGE } from "./exit.ts";
21
22
  import { gatesListCli } from "./gates-list.ts";
@@ -117,6 +118,10 @@ if (cmd === "eval") {
117
118
  process.exit(await evalCli(sub ? [sub, ...rest] : rest));
118
119
  }
119
120
 
121
+ if (cmd === "decide") {
122
+ process.exit(await decideCli(sub ? [sub, ...rest] : rest));
123
+ }
124
+
120
125
  if (cmd === "ai") {
121
126
  process.exit(await aiCli(sub ? [sub, ...rest] : rest));
122
127
  }
@@ -54,7 +54,7 @@ describe("defaultImagesFromConfig", () => {
54
54
  expect(images).toEqual({
55
55
  "store.sql": "postgres:18-alpine",
56
56
  pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.59",
57
- "store.kv": "redis:8-alpine",
57
+ "store.kv": "docker.dragonflydb.io/dragonflydb/dragonfly:v2.0.0",
58
58
  });
59
59
  });
60
60
 
@@ -129,7 +129,7 @@ describe("resolveImages", () => {
129
129
  },
130
130
  });
131
131
  expect(images["store.sql"]).toBe("postgres:18-alpine");
132
- expect(images["store.kv"]).toBe("redis:8-alpine");
132
+ expect(images["store.kv"]).toBe("docker.dragonflydb.io/dragonflydb/dragonfly:v2.0.0");
133
133
  expect(images["store.kv.durable"]).toBeUndefined();
134
134
  });
135
135
  });
@@ -16,7 +16,7 @@ import type { Manifest } from "../manifest/types.ts";
16
16
  const DEFAULT_SQL_IMAGE = "postgres:18-alpine";
17
17
  const DEFAULT_PGVECTOR_IMAGE = "pgvector/pgvector:pg17";
18
18
  const DEFAULT_PGDOG_IMAGE = "ghcr.io/pgdogdev/pgdog:v0.1.59";
19
- const DEFAULT_KV_IMAGE = "redis:8-alpine";
19
+ const DEFAULT_KV_IMAGE = "docker.dragonflydb.io/dragonflydb/dragonfly:v2.0.0";
20
20
 
21
21
  /**
22
22
  * Prefer `dev` then `prod` for a driver map (compose infra ≈ production).
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Pull-request manifest diff.
3
+ *
4
+ * Writes the comment body, then exits non-zero only for undeclared
5
+ * contract breaks. Widening changes stay in the comment.
6
+ *
7
+ * bun src/cli/manifest-pr-diff.ts comment.md
8
+ */
9
+
10
+ import { writeFileSync } from "node:fs";
11
+ import { formatManifestDiffComment, runDoctorDiff } from "./doctor-diff.ts";
12
+
13
+ const out = process.argv[2] ?? "manifest-diff.md";
14
+ const result = await runDoctorDiff({ write: () => {} });
15
+ writeFileSync(out, formatManifestDiffComment(result.allChanges));
16
+ process.exit(result.code);
@@ -22,6 +22,10 @@ export interface McpConsoleSurface {
22
22
  readonly cwd: string;
23
23
  /** Clock shared with Console. */
24
24
  readonly now: () => number;
25
+ /** Journal that holds agent events and parked approvals. */
26
+ readonly journalStore?: import("../kernel/journal.ts").JournalStore | null;
27
+ /** In-process agent ledger. The follow log is the durable source. */
28
+ readonly aiRuntime?: import("../elements/ai.ts").AiRuntime | null;
25
29
  }
26
30
 
27
31
  /**
@@ -33,6 +37,50 @@ export function mcpContextFromConsole(state: McpConsoleSurface): McpContext {
33
37
  return {
34
38
  getManifest: () => state.manifest,
35
39
  listRuns: () => state.listRuns(),
40
+ listAgentRuns: async () => {
41
+ const { loadConsoleAgentRuns } = await import("../console/server/ai-runs.ts");
42
+ return loadConsoleAgentRuns({
43
+ runtime: state.aiRuntime ?? null,
44
+ store: state.journalStore ?? null,
45
+ now: state.now(),
46
+ });
47
+ },
48
+ getAgentRun: async (runId) => {
49
+ const { loadConsoleAgentRun } = await import("../console/server/ai-runs.ts");
50
+ const run = await loadConsoleAgentRun(
51
+ {
52
+ runtime: state.aiRuntime ?? null,
53
+ store: state.journalStore ?? null,
54
+ now: state.now(),
55
+ },
56
+ runId,
57
+ );
58
+ if (!run) return undefined;
59
+ return { run, events: run.events };
60
+ },
61
+ listApprovals: async () => {
62
+ const { loadApprovalQueue } = await import("../console/server/ai-runs.ts");
63
+ return loadApprovalQueue(state.journalStore ?? null, state.now());
64
+ },
65
+ listDecisions: async (tenant) => {
66
+ const { loadDecisionQueue, projectMcpDecisions, candidateMetrics } =
67
+ await import("../console/server/decisions.ts");
68
+ const { listDecisionCandidates, loadDecisionCandidate } =
69
+ await import("../elements/ai/decisions/labels.ts");
70
+ const names = await listDecisionCandidates();
71
+ const fitted: Record<string, Record<string, number>> = {};
72
+ for (const name of names) {
73
+ const metrics = candidateMetrics(await loadDecisionCandidate(name));
74
+ if (metrics) fitted[name] = metrics;
75
+ }
76
+ const queue = await loadDecisionQueue(state.journalStore ?? null, state.now(), tenant);
77
+ const pending: Record<string, number> = {};
78
+ for (const row of queue) {
79
+ if (row.status !== "pending" || row.labelOnly) continue;
80
+ pending[row.decision] = (pending[row.decision] ?? 0) + 1;
81
+ }
82
+ return projectMcpDecisions(state.manifest, pending, fitted);
83
+ },
36
84
  proposeStructural: async (input) =>
37
85
  emitStructuralDiff({
38
86
  cwd: state.cwd,
@@ -550,7 +550,41 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
550
550
  name: "eval",
551
551
  summary: "prompt eval sets (CI gate)",
552
552
  leaf: true,
553
- flags: [MANIFEST, HELP],
553
+ flags: [
554
+ MANIFEST,
555
+ {
556
+ long: "--certify",
557
+ takesValue: false,
558
+ summary: "Build a decision certificate from the seed file",
559
+ },
560
+ HELP,
561
+ ],
562
+ },
563
+ {
564
+ name: "decide",
565
+ summary: "promote a decision certificate",
566
+ subcommands: [
567
+ {
568
+ name: "promote",
569
+ summary: "Fetch the operator candidate and write the app lockfile",
570
+ positionals: "<name>",
571
+ flags: [
572
+ {
573
+ long: "--origin",
574
+ takesValue: true,
575
+ valueName: "url",
576
+ summary: "App origin that serves the candidate",
577
+ },
578
+ {
579
+ long: "--lock",
580
+ takesValue: true,
581
+ valueName: "path",
582
+ summary: "Lockfile path (default oke-decisions.lock.json)",
583
+ },
584
+ HELP,
585
+ ],
586
+ },
587
+ ],
554
588
  },
555
589
  {
556
590
  name: "ai",
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Parse AG-UI agent events from JSON or SSE.
3
+ *
4
+ * Import from `okengine/client/agent`. This stays off the core `okengine/client`
5
+ * graph. Wire `CUSTOM` events stay `CUSTOM`. The three OKE subagent names are
6
+ * also returned as typed variants.
7
+ */
8
+
9
+ import type { AgUiEvent } from "../elements/ai/events.ts";
10
+
11
+ /** Typed subagent notice parsed from `CUSTOM`. */
12
+ export type ParsedSubagentEvent =
13
+ | {
14
+ readonly type: "subagent.started";
15
+ readonly runId: string;
16
+ readonly parentToolCallId?: string;
17
+ }
18
+ | {
19
+ readonly type: "subagent.finished";
20
+ readonly runId: string;
21
+ readonly parentToolCallId?: string;
22
+ }
23
+ | {
24
+ readonly type: "subagent.error";
25
+ readonly runId: string;
26
+ readonly parentToolCallId?: string;
27
+ readonly message?: string;
28
+ };
29
+
30
+ /** One event from {@link parseAgentEvent}. */
31
+ export type ParsedAgentEvent = AgUiEvent | ParsedSubagentEvent;
32
+
33
+ const SUBAGENT_NAMES = {
34
+ "oke.subagent.started": "subagent.started",
35
+ "oke.subagent.finished": "subagent.finished",
36
+ "oke.subagent.error": "subagent.error",
37
+ } as const;
38
+
39
+ /**
40
+ * Parse one JSON event. Unknown objects return undefined.
41
+ *
42
+ * @param data - Parsed JSON value
43
+ */
44
+ export function parseAgentEvent(data: unknown): ParsedAgentEvent | undefined {
45
+ if (typeof data !== "object" || data === null || !("type" in data)) return undefined;
46
+ const event = data as AgUiEvent;
47
+ if (event.type !== "CUSTOM") return event;
48
+ const kind = SUBAGENT_NAMES[event.name as keyof typeof SUBAGENT_NAMES];
49
+ if (!kind) return event;
50
+ const value =
51
+ typeof event.value === "object" && event.value !== null
52
+ ? (event.value as { runId?: unknown; parentToolCallId?: unknown; message?: unknown })
53
+ : {};
54
+ const runId = typeof value.runId === "string" ? value.runId : "";
55
+ const parentToolCallId =
56
+ typeof value.parentToolCallId === "string" ? value.parentToolCallId : undefined;
57
+ if (kind === "subagent.error") {
58
+ return {
59
+ type: kind,
60
+ runId,
61
+ ...(parentToolCallId !== undefined ? { parentToolCallId } : {}),
62
+ ...(typeof value.message === "string" ? { message: value.message } : {}),
63
+ };
64
+ }
65
+ return {
66
+ type: kind,
67
+ runId,
68
+ ...(parentToolCallId !== undefined ? { parentToolCallId } : {}),
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Read `data:` frames from an SSE response body.
74
+ *
75
+ * Pass a URL to follow a run: the generator reconnects and sends
76
+ * `Last-Event-ID` from the last frame it yielded.
77
+ *
78
+ * @param source - One response, or the follow URL
79
+ * @param init - Fetch, abort, and a starting event id
80
+ */
81
+ export async function* readAgentEvents(
82
+ source: Response | string | URL,
83
+ init?: AgentFollowInit,
84
+ ): AsyncIterable<ParsedAgentEvent> {
85
+ if (source instanceof Response) {
86
+ yield* readAgentEventResponse(source, init);
87
+ return;
88
+ }
89
+ const fetcher = init?.fetch ?? fetch;
90
+ let lastEventId = init?.lastEventId;
91
+ let delayMs = 250;
92
+ for (;;) {
93
+ if (init?.signal?.aborted) return;
94
+ const headers = new Headers(init?.headers);
95
+ if (lastEventId) headers.set("last-event-id", lastEventId);
96
+ let response: Response;
97
+ try {
98
+ response = await fetcher(source, { headers, signal: init?.signal });
99
+ } catch (err) {
100
+ if (init?.signal?.aborted) return;
101
+ init?.trace?.(err instanceof Error ? err.message : "agent events: network error");
102
+ await wait(delayMs, init?.signal);
103
+ delayMs = Math.min(delayMs * 2, 10_000);
104
+ continue;
105
+ }
106
+ delayMs = 250;
107
+ if (!response.ok) throw new Error(`agent events: HTTP ${response.status}`);
108
+ let sawFrame = false;
109
+ try {
110
+ for await (const frame of readAgentEventFrames(response, init?.trace)) {
111
+ sawFrame = true;
112
+ if (frame.id) {
113
+ lastEventId = frame.id;
114
+ init?.onId?.(frame.id);
115
+ }
116
+ if (frame.event) yield frame.event;
117
+ if (frame.event?.type === "RUN_ERROR") return;
118
+ if (frame.event?.type === "RUN_FINISHED" && frame.event.outcome?.type !== "interrupt")
119
+ return;
120
+ }
121
+ } catch (err) {
122
+ if (init?.signal?.aborted) return;
123
+ init?.trace?.(err instanceof Error ? err.message : "agent events: network error");
124
+ await wait(delayMs, init?.signal);
125
+ delayMs = Math.min(delayMs * 2, 10_000);
126
+ continue;
127
+ }
128
+ if (!sawFrame || init?.signal?.aborted) return;
129
+ }
130
+ }
131
+
132
+ /** Options for {@link approve} and {@link deny}. */
133
+ export interface AgentDecisionInit {
134
+ readonly fetch?: typeof fetch;
135
+ /** Cap on lease retries. Default 5. */
136
+ readonly attempts?: number;
137
+ readonly headers?: Record<string, string>;
138
+ }
139
+
140
+ /**
141
+ * Approve a parked tool. Retries `JournalLeaseBusy`. Throws on `Conflict`.
142
+ *
143
+ * @param url - Approve route (`/agent/approvals/approve`)
144
+ * @param id - Approval id
145
+ * @param args - Replacement tool input
146
+ * @param init - Fetch override
147
+ */
148
+ export async function approve(
149
+ url: string,
150
+ id: string,
151
+ args?: unknown,
152
+ init?: AgentDecisionInit,
153
+ ): Promise<void> {
154
+ await postDecision(url, { id, ...(args !== undefined ? { args } : {}) }, init);
155
+ }
156
+
157
+ /**
158
+ * Deny a parked tool. Retries `JournalLeaseBusy`. Throws on `Conflict`.
159
+ *
160
+ * @param url - Deny route (`/agent/approvals/deny`)
161
+ * @param id - Approval id
162
+ * @param reason - Text the model sees
163
+ * @param init - Fetch override
164
+ */
165
+ export async function deny(
166
+ url: string,
167
+ id: string,
168
+ reason?: string,
169
+ init?: AgentDecisionInit,
170
+ ): Promise<void> {
171
+ await postDecision(url, { id, ...(reason !== undefined ? { reason } : {}) }, init);
172
+ }
173
+
174
+ /** Follow options for {@link readAgentEvents}. */
175
+ export interface AgentFollowInit {
176
+ readonly fetch?: typeof fetch;
177
+ readonly headers?: Record<string, string>;
178
+ readonly signal?: AbortSignal;
179
+ readonly lastEventId?: string;
180
+ /** Called with each SSE id as it is read. */
181
+ onId?(id: string): void;
182
+ /** Called when a frame cannot be parsed. The frame is skipped. */
183
+ trace?(message: string): void;
184
+ }
185
+
186
+ async function* readAgentEventResponse(
187
+ response: Response,
188
+ init?: AgentFollowInit,
189
+ ): AsyncIterable<ParsedAgentEvent> {
190
+ for await (const frame of readAgentEventFrames(response, init?.trace)) {
191
+ if (frame.id) init?.onId?.(frame.id);
192
+ if (frame.event) yield frame.event;
193
+ }
194
+ }
195
+
196
+ async function* readAgentEventFrames(
197
+ response: Response,
198
+ trace?: (message: string) => void,
199
+ ): AsyncIterable<{ id?: string; event?: ParsedAgentEvent }> {
200
+ const body = response.body;
201
+ if (!body) return;
202
+ const reader = body.getReader();
203
+ const decoder = new TextDecoder();
204
+ let buffer = "";
205
+ for (;;) {
206
+ const chunk = await reader.read();
207
+ if (chunk.done) break;
208
+ buffer += decoder.decode(chunk.value, { stream: true });
209
+ const frames = buffer.split("\n\n");
210
+ buffer = frames.pop() ?? "";
211
+ for (const frame of frames) {
212
+ const lines = frame.split("\n");
213
+ const idLine = lines.find((line) => line.startsWith("id:"));
214
+ const data = lines
215
+ .filter((line) => line.startsWith("data:"))
216
+ .map((line) => line.slice(5).trim())
217
+ .join("\n");
218
+ if (!data || data === "[DONE]") continue;
219
+ let parsed: ParsedAgentEvent | undefined;
220
+ try {
221
+ parsed = parseAgentEvent(JSON.parse(data) as unknown);
222
+ } catch (err) {
223
+ trace?.(err instanceof Error ? err.message : "agent events: malformed frame");
224
+ continue;
225
+ }
226
+ yield {
227
+ ...(idLine ? { id: idLine.slice(3).trim() } : {}),
228
+ ...(parsed ? { event: parsed } : {}),
229
+ };
230
+ }
231
+ }
232
+ }
233
+
234
+ async function postDecision(
235
+ url: string,
236
+ body: { id: string; args?: unknown; reason?: string },
237
+ init?: AgentDecisionInit,
238
+ ): Promise<void> {
239
+ const fetcher = init?.fetch ?? fetch;
240
+ const attempts = init?.attempts ?? 5;
241
+ for (let attempt = 0; attempt < attempts; attempt++) {
242
+ const response = await fetcher(url, {
243
+ method: "POST",
244
+ headers: { "content-type": "application/json", ...headersOf(init?.headers) },
245
+ body: JSON.stringify(body),
246
+ });
247
+ if (response.ok) return;
248
+ const payload = (await response.json().catch(() => ({}))) as {
249
+ error?: { code?: string };
250
+ };
251
+ const code = payload.error?.code;
252
+ if (response.status === 409 && code === "JournalLeaseBusy") {
253
+ const retryAfter = retryAfterMs(response.headers.get("retry-after"));
254
+ await new Promise((resolve) => setTimeout(resolve, retryAfter));
255
+ continue;
256
+ }
257
+ if (response.status === 409 && code === "Conflict") {
258
+ const error = new Error("That value is already in use.");
259
+ error.name = "Conflict";
260
+ throw error;
261
+ }
262
+ const error = new Error(code ?? `agent decision: HTTP ${response.status}`);
263
+ error.name = code ?? "Error";
264
+ throw error;
265
+ }
266
+ const error = new Error("This run is locked by another worker. Retry after the given delay.");
267
+ error.name = "JournalLeaseBusy";
268
+ throw error;
269
+ }
270
+
271
+ /**
272
+ * `Retry-After` as delta-seconds or an HTTP-date.
273
+ *
274
+ * @param header - Header value
275
+ */
276
+ function retryAfterMs(header: string | null): number {
277
+ if (!header) return 0;
278
+ const seconds = Number(header);
279
+ if (Number.isFinite(seconds)) return Math.max(0, seconds) * 1000;
280
+ const at = Date.parse(header);
281
+ if (Number.isNaN(at)) return 0;
282
+ return Math.max(0, at - Date.now());
283
+ }
284
+
285
+ function headersOf(headers: Record<string, string> | undefined): Record<string, string> {
286
+ return headers ?? {};
287
+ }
288
+
289
+ /**
290
+ * Wait, or return early when `signal` aborts.
291
+ *
292
+ * @param ms - Delay
293
+ * @param signal - Abort
294
+ */
295
+ function wait(ms: number, signal?: AbortSignal): Promise<void> {
296
+ return new Promise((resolve) => {
297
+ const timer = setTimeout(resolve, ms);
298
+ signal?.addEventListener(
299
+ "abort",
300
+ () => {
301
+ clearTimeout(timer);
302
+ resolve();
303
+ },
304
+ { once: true },
305
+ );
306
+ });
307
+ }