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
package/src/kernel/fx.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  type JsonStreamResult,
18
18
  } from "./json-result.ts";
19
19
  import type { Effects, ResourceRef, SignalResourceRef } from "../manifest/types.ts";
20
+ import type { FlowInput, FlowOutput } from "./flow.ts";
20
21
  import { isMcpToolRef } from "../manifest/mcp-ref.ts";
21
22
  import type { QueryPageSpec } from "./list-page.ts";
22
23
  import type {
@@ -36,9 +37,13 @@ import { rlsIdentityFromAuth } from "../elements/store.ts";
36
37
  import type { RlsIdentity } from "../drivers/pg-rls.ts";
37
38
  import type { SignalDecl, SignalRuntime } from "../elements/signal.ts";
38
39
  import type { DeadLetter, SignalEmitOptions } from "../drivers/signal-types.ts";
40
+ import { currentSignalTransaction } from "./signal-tx.ts";
39
41
  import type { VaultActor, VaultAdapter, VaultRuntime } from "../elements/vault.ts";
40
42
  import type { ChannelRuntime } from "../elements/channel.ts";
43
+ import type { AgentApprovalResolveResult } from "../elements/ai/approval.ts";
41
44
  import type { AiRuntime } from "../elements/ai.ts";
45
+ import type { AgUiEvent } from "../elements/ai/events.ts";
46
+ import type { AiMessage } from "../drivers/ai-types.ts";
42
47
  import { parseDurationMs } from "../elements/clock/duration.ts";
43
48
  import type { ApiKeyStore } from "../auth/api-keys.ts";
44
49
  import type { FxAuthIdentity, FxAuthKeyMethods } from "./fx-auth-keys.ts";
@@ -62,14 +67,14 @@ import {
62
67
  touchDryRunStore,
63
68
  } from "./dry-run.ts";
64
69
  import type { FailFn } from "./errors.ts";
65
- import { abortError, currentAbortSignal, linkAbort } from "./abort-scope.ts";
70
+ import { abortError, currentAbortSignal, linkAbort, withAbortSignal } from "./abort-scope.ts";
66
71
  import type { FxRetryOptions, FxThunk } from "./concurrency.ts";
67
72
  import { maskRedactedDeep, Redacted } from "./redacted.ts";
68
73
  import type { JournalSession, JournalStepOptions } from "./journal.ts";
69
74
 
70
75
  /** Options for {@link Fx.step} — see {@link JournalStepOptions}. */
71
76
  export type StepOptions<T> = JournalStepOptions<T>;
72
- import type { RunTelemetry } from "./run-telemetry.ts";
77
+ import { type RunTelemetry } from "./run-telemetry.ts";
73
78
  import type { RunsRuntime } from "../runs/runtime.ts";
74
79
  import type { RunsRow, WideEvent } from "../runs/types.ts";
75
80
  import type { RunWindowStats, SloBreach } from "../runs/window.ts";
@@ -114,7 +119,33 @@ function loadFxLiveStream(): typeof import("./fx-live-stream.ts") {
114
119
  return lazyRequire(import.meta.dir, ["fx", "live", "stream"].join("-"));
115
120
  }
116
121
 
122
+ /**
123
+ * Split `fx.run` input into one message or a history.
124
+ *
125
+ * @param input - String, `{ message }`, or `{ messages }`
126
+ */
127
+ function agentTurn(input: unknown): { message?: string; messages?: readonly AiMessage[] } {
128
+ if (typeof input === "string") return { message: input };
129
+ if (input && typeof input === "object") {
130
+ const record = input as { message?: unknown; messages?: unknown };
131
+ const hasMessage = "message" in record && record.message !== undefined;
132
+ const hasMessages = "messages" in record && record.messages !== undefined;
133
+ if (hasMessage && hasMessages) {
134
+ throw new TypeError("fx.run: pass message or messages, not both");
135
+ }
136
+ if (hasMessages && Array.isArray(record.messages)) {
137
+ return { messages: record.messages as readonly AiMessage[] };
138
+ }
139
+ if (hasMessage) return { message: String(record.message) };
140
+ }
141
+ return { message: JSON.stringify(input ?? {}) };
142
+ }
143
+
117
144
  /** `fx.fetch` — keep host parsing + dry-run stub off cold edge / Store-only graphs. */
145
+ function loadFxDecide(): typeof import("./fx-decide.ts") {
146
+ return lazyRequire(import.meta.dir, ["fx", "decide"].join("-"));
147
+ }
148
+
118
149
  function loadFxFetch(): typeof import("./fx-fetch.ts") {
119
150
  return lazyRequire(import.meta.dir, ["fx", "fetch"].join("-"));
120
151
  }
@@ -143,6 +174,12 @@ export type { FxRetryOptions, FxThunk } from "./concurrency.ts";
143
174
  /** Named ref: plain string or `{ name }` element handle (optional pin). */
144
175
  export type NamedRef = string | { readonly name: string; readonly version?: number };
145
176
 
177
+ /** Input of {@link Fx.call}: the flow's input, or `unknown` for a name. */
178
+ type FxCallInput<F> = FlowInput<F>;
179
+
180
+ /** Result of {@link Fx.call}: the flow's output, or `unknown` for a name. */
181
+ type FxCallResult<F> = Promise<FlowOutput<F>>;
182
+
146
183
  /** Resolve a {@link NamedRef} to its string id (`name@version` when pinned). */
147
184
  export function resolveName(ref: NamedRef): string {
148
185
  if (typeof ref === "string") return ref;
@@ -394,6 +431,8 @@ export interface FxAskOptions {
394
431
  readonly tools?: readonly NamedRef[];
395
432
  /** Bound on tool invocations (default 6). */
396
433
  readonly maxSteps?: number;
434
+ /** Yield AG-UI events instead of resolving to the validated object. */
435
+ readonly stream?: boolean;
397
436
  }
398
437
 
399
438
  /** Options for {@link Fx.search}. */
@@ -406,11 +445,13 @@ export type { JsonResult, JsonStreamResult };
406
445
 
407
446
  const sseFrameBrand: unique symbol = Symbol.for("oke.sse.frame");
408
447
 
409
- /** One SSE frame — optional `id:` plus JSON `data:`. */
448
+ /** One SSE frame — optional `id:` plus JSON `data:`, or a comment line. */
410
449
  export interface SseFrame {
411
450
  readonly [sseFrameBrand]: true;
412
451
  readonly data: unknown;
413
452
  readonly id?: string;
453
+ /** Comment body. When set and `data` is undefined, the encoder emits `: comment`. */
454
+ readonly comment?: string;
414
455
  }
415
456
 
416
457
  /**
@@ -423,6 +464,15 @@ export function sseFrame(data: unknown, id?: string): SseFrame {
423
464
  return id !== undefined ? { [sseFrameBrand]: true, data, id } : { [sseFrameBrand]: true, data };
424
465
  }
425
466
 
467
+ /**
468
+ * SSE comment (`: keepalive`). Keeps a followed run from being idle-closed.
469
+ *
470
+ * @param text - Comment text
471
+ */
472
+ export function sseComment(text: string): SseFrame {
473
+ return { [sseFrameBrand]: true, data: undefined, comment: text };
474
+ }
475
+
426
476
  /**
427
477
  * True when `value` is a branded SSE frame.
428
478
  *
@@ -726,7 +776,7 @@ export interface Fx {
726
776
  * @param flow - Flow name or handle
727
777
  * @param input - Input payload
728
778
  */
729
- call(flow: NamedRef, input?: unknown): Promise<unknown>;
779
+ call<F extends NamedRef>(flow: F, input?: FxCallInput<F>): FxCallResult<F>;
730
780
  /**
731
781
  * Query the Runs wide-event store (records `read` on `"runs"`).
732
782
  * Requires a bound runs runtime and `effects.reads` including `"runs"`.
@@ -748,10 +798,6 @@ export interface Fx {
748
798
  /**
749
799
  * Send a provider-managed SMS OTP (records `send` on `sms-otp`).
750
800
  *
751
- * Vendor extra (Taqnyat Verify API) — requires a bound SMS driver that
752
- * supports provider-managed OTP. Dry-run records would-have-fired without
753
- * contacting the provider.
754
- *
755
801
  * @param opts - Recipient + requestId (+ lang / note / from)
756
802
  */
757
803
  sendOtp(opts: FxSendOtpOptions): Promise<{ ok: true }>;
@@ -764,9 +810,6 @@ export interface Fx {
764
810
  /**
765
811
  * Deliver an app-owned OTP across declared channels (records `send` on `auth-otp`).
766
812
  *
767
- * Tier-2 only — uses Channel `deliverOtp` (sently FallbackTransport). Pass
768
- * `only` for explicit user resend (single channel, no cross-medium failover).
769
- *
770
813
  * @param opts - Channels, templates, addresses, OTP data
771
814
  */
772
815
  deliverOtp(opts: FxDeliverOtpOptions): Promise<{
@@ -781,6 +824,11 @@ export interface Fx {
781
824
  * @param opts - Model routing opts
782
825
  */
783
826
  ask(prompt: NamedRef, input?: unknown, opts?: FxAskOptions): Promise<Record<string, unknown>>;
827
+ ask(
828
+ prompt: NamedRef,
829
+ input: unknown,
830
+ opts: FxAskOptions & { readonly stream: true },
831
+ ): AsyncIterable<AgUiEvent>;
784
832
  /**
785
833
  * Embed text via a named model (records `embed` — distinct from `ask`).
786
834
  * Returns the vector; does not write an index.
@@ -808,13 +856,40 @@ export interface Fx {
808
856
  * @param init - Standard `RequestInit`
809
857
  */
810
858
  fetch(url: string | URL, init?: RequestInit): Promise<Response>;
859
+ /**
860
+ * Run one declared decision. `$` carries `how` and `audited`.
861
+ * Abstain mode makes each question value `T | null`.
862
+ */
863
+ decide<T>(decision: { name: string }, input: unknown): Promise<T>;
864
+ /**
865
+ * Resolve a pending agent tool approval (records nothing until the tool runs).
866
+ *
867
+ * First resolution wins. A later call returns a conflict failure.
868
+ */
869
+ readonly agent: {
870
+ /**
871
+ * @param id - Approval id from the interrupt
872
+ * @param opts - Optional replacement args
873
+ */
874
+ approve(id: string, opts?: { readonly args?: unknown }): Promise<AgentApprovalResolveResult>;
875
+ /**
876
+ * @param id - Approval id from the interrupt
877
+ * @param opts - Denial reason fed back to the model
878
+ */
879
+ deny(id: string, opts?: { readonly reason?: string }): Promise<AgentApprovalResolveResult>;
880
+ };
811
881
  /**
812
882
  * Run a bounded AI agent (records `ask`).
813
883
  *
814
884
  * @param agent - Agent name or handle
815
- * @param input - Agent input (`{ message }` or string)
885
+ * @param input - Agent input (`{ message }`, `{ messages }`, or string)
816
886
  */
817
887
  run(agent: NamedRef, input?: unknown): Promise<unknown>;
888
+ run(
889
+ agent: NamedRef,
890
+ input: unknown,
891
+ opts: { readonly stream: true; readonly threadId?: string },
892
+ ): AsyncIterable<AgUiEvent>;
818
893
  /**
819
894
  * Stream model tokens (records `ask`). Returns an async iterable of chunks.
820
895
  *
@@ -844,6 +919,8 @@ export interface Fx {
844
919
  t(key: AppMessageKey, values?: MessageValues): string;
845
920
  /** Active locale for {@link Fx.t} and default channel sends. */
846
921
  readonly locale: string;
922
+ /** HTTP `Last-Event-ID` on this request, when the client sent one. */
923
+ readonly lastEventId?: string;
847
924
  /** Generate a unique id (OKID). */
848
925
  id(): string;
849
926
  /** User-plane auth principal. */
@@ -1200,11 +1277,12 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1200
1277
  externalOf?:
1201
1278
  | EffectExternal
1202
1279
  | ((result: T | undefined, error: unknown) => EffectExternal | undefined),
1280
+ stamp?: (result: T | undefined) => { readonly agentRunId?: string } | undefined,
1203
1281
  ): Promise<T> {
1204
1282
  const signal = currentAbortSignal();
1205
1283
  if (signal.aborted) throw abortError(signal.reason);
1206
1284
  capability.assert(kind, resource);
1207
- const execute = () => recordEffect(ledger, kind, resource, now, body, externalOf);
1285
+ const execute = () => recordEffect(ledger, kind, resource, now, body, externalOf, stamp);
1208
1286
  if (journal) {
1209
1287
  return journal.effect(kind, resource, execute);
1210
1288
  }
@@ -1330,6 +1408,11 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1330
1408
  gated,
1331
1409
  capability,
1332
1410
  ask: (model, input) => fx.ask(model, input),
1411
+ beginSignal: async () => {
1412
+ const bus = options.signalRuntime?.bus;
1413
+ if (!bus) return undefined;
1414
+ return bus.begin();
1415
+ },
1333
1416
  });
1334
1417
  }
1335
1418
 
@@ -1422,6 +1505,12 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1422
1505
 
1423
1506
  const aiDisablesCache = options.aiRuntime?.autoCacheDisabled === true;
1424
1507
  const telemetry = options.runTelemetry;
1508
+ if (telemetry) {
1509
+ lazyRequire<{ bindDecisionLabelTelemetry(telemetry: RunTelemetry): void }>(
1510
+ `${import.meta.dir}/../elements/ai/decisions`,
1511
+ ["lab", "els"].join(""),
1512
+ ).bindDecisionLabelTelemetry(telemetry);
1513
+ }
1425
1514
 
1426
1515
  const cache: FxCache = {
1427
1516
  async get<T = unknown>(key: string): Promise<T | undefined> {
@@ -1663,19 +1752,55 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1663
1752
  },
1664
1753
  };
1665
1754
 
1755
+ async function resolveAgentDecision(
1756
+ id: string,
1757
+ decision: {
1758
+ readonly decision: "approve" | "deny";
1759
+ readonly args?: unknown;
1760
+ readonly reason?: string;
1761
+ },
1762
+ ): Promise<AgentApprovalResolveResult> {
1763
+ const runtime = options.aiRuntime;
1764
+ if (!runtime?.resolveApproval) return { ok: false, status: 404 };
1765
+ const approver = auth.userId ?? operator.id;
1766
+ return runtime.resolveApproval(
1767
+ id,
1768
+ {
1769
+ decision: decision.decision,
1770
+ tenant: tenant.id,
1771
+ ...(decision.args !== undefined ? { args: decision.args } : {}),
1772
+ ...(decision.reason !== undefined ? { reason: decision.reason } : {}),
1773
+ ...(approver ? { approver } : {}),
1774
+ },
1775
+ {
1776
+ auth: {
1777
+ userId: auth.userId,
1778
+ scopes: auth.scopes,
1779
+ verified: auth.verified,
1780
+ },
1781
+ operator: { id: operator.id },
1782
+ },
1783
+ );
1784
+ }
1785
+
1666
1786
  const fx: Fx = {
1667
1787
  store: storeHandle,
1668
1788
  runs: runsSurface,
1669
1789
  emit(signal: NamedRef, payload?: unknown, emitOptions?: SignalEmitOptions) {
1670
1790
  const name = resolveName(signal);
1671
1791
  return gated("emit", name, async () => {
1792
+ const merged: SignalEmitOptions = {
1793
+ ...emitOptions,
1794
+ ...(options.runId !== undefined && emitOptions?.parentRunId === undefined
1795
+ ? { parentRunId: options.runId }
1796
+ : {}),
1797
+ };
1798
+ const pending = currentSignalTransaction();
1799
+ if (pending) {
1800
+ await pending.emit(name, payload, merged);
1801
+ return;
1802
+ }
1672
1803
  if (options.signalRuntime) {
1673
- const merged: SignalEmitOptions = {
1674
- ...emitOptions,
1675
- ...(options.runId !== undefined && emitOptions?.parentRunId === undefined
1676
- ? { parentRunId: options.runId }
1677
- : {}),
1678
- };
1679
1804
  await options.signalRuntime.emit(name, payload, merged);
1680
1805
  }
1681
1806
  });
@@ -1701,7 +1826,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1701
1826
  signalRuntime: options.signalRuntime,
1702
1827
  }) as JsonStreamResult;
1703
1828
  },
1704
- call(flow, input) {
1829
+ call<F extends NamedRef>(flow: F, input?: FxCallInput<F>): FxCallResult<F> {
1705
1830
  const name = resolveName(flow);
1706
1831
  return gated("call", name, async () => {
1707
1832
  if (isMcpToolRef(name)) {
@@ -1714,7 +1839,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1714
1839
  return options.callHandler(name, input);
1715
1840
  }
1716
1841
  return undefined;
1717
- });
1842
+ }) as FxCallResult<F>;
1718
1843
  },
1719
1844
  clock,
1720
1845
  vault: vaultSurface,
@@ -1815,8 +1940,37 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1815
1940
  return { ok: true as const, channel: result.channel };
1816
1941
  });
1817
1942
  },
1818
- ask(prompt, input, opts) {
1943
+ ask: function (prompt, input, opts) {
1819
1944
  const name = resolveName(prompt);
1945
+ if (opts?.stream) {
1946
+ return (async function* () {
1947
+ await gated("ask", name, async () => undefined);
1948
+ if (isDryRun()) {
1949
+ recordWouldHaveFired("ask", name);
1950
+ return;
1951
+ }
1952
+ if (!options.aiRuntime) {
1953
+ throw new Error(`fx.ask: AI runtime is not configured for prompt "${name}"`);
1954
+ }
1955
+ const local = new AbortController();
1956
+ const unlink = linkAbort(currentAbortSignal(), local);
1957
+ try {
1958
+ const events = await withAbortSignal(local.signal, () =>
1959
+ options.aiRuntime!.streamAsk(name, input, {
1960
+ via: opts.via?.map(resolveName),
1961
+ ...(opts.timeout !== undefined ? { timeout: opts.timeout } : {}),
1962
+ tools: opts.tools?.map(resolveName),
1963
+ maxSteps: opts.maxSteps,
1964
+ callTool: (tool, toolInput) => fx.call(tool, toolInput),
1965
+ }),
1966
+ );
1967
+ yield* events;
1968
+ } finally {
1969
+ unlink();
1970
+ if (!local.signal.aborted) local.abort();
1971
+ }
1972
+ })();
1973
+ }
1820
1974
  return gated(
1821
1975
  "ask",
1822
1976
  name,
@@ -1844,7 +1998,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1844
1998
  },
1845
1999
  () => options.aiRuntime?.lastExternal,
1846
2000
  );
1847
- },
2001
+ } as Fx["ask"],
1848
2002
  embed(model, text) {
1849
2003
  const name = resolveName(model);
1850
2004
  return gated(
@@ -1890,29 +2044,158 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1890
2044
  fetch(url, init) {
1891
2045
  return loadFxFetch().runFxFetch(gated, url, init);
1892
2046
  },
1893
- run(agent, input) {
2047
+ decide<T>(decision: { name: string }, input: unknown) {
2048
+ return loadFxDecide().runFxDecide({
2049
+ gated,
2050
+ journal: options.journal,
2051
+ signal: currentAbortSignal(),
2052
+ now,
2053
+ runId: options.runId,
2054
+ tenantId: tenant.id,
2055
+ getSecret: async (secretName) => {
2056
+ const value = await gatedSecret(secretName, async () => secrets[secretName]);
2057
+ return typeof value === "string" && value.length > 0 ? value : undefined;
2058
+ },
2059
+ decision,
2060
+ input,
2061
+ }) as Promise<T>;
2062
+ },
2063
+ agent: {
2064
+ approve(id, opts) {
2065
+ return resolveAgentDecision(id, { decision: "approve", args: opts?.args });
2066
+ },
2067
+ deny(id, opts) {
2068
+ return resolveAgentDecision(id, { decision: "deny", reason: opts?.reason });
2069
+ },
2070
+ },
2071
+ run: function (agent, input, opts?: { readonly stream?: boolean; readonly threadId?: string }) {
1894
2072
  const name = resolveName(agent);
1895
- return gated("ask", name, async () => {
1896
- if (options.aiRuntime) {
1897
- const message =
1898
- typeof input === "string"
1899
- ? input
1900
- : input && typeof input === "object" && "message" in input
1901
- ? String((input as { message: unknown }).message)
1902
- : JSON.stringify(input ?? {});
1903
- return options.aiRuntime.runAgent(name, {
1904
- message,
1905
- auth: {
2073
+ const turn = agentTurn(input);
2074
+ if (opts?.stream) {
2075
+ let inner: (AsyncIterable<AgUiEvent> & { readonly result?: Promise<unknown> }) | undefined;
2076
+ let heldRunId: string | undefined;
2077
+ let markRun: () => void = () => undefined;
2078
+ const heldReady = new Promise<void>((resolve) => {
2079
+ markRun = resolve;
2080
+ });
2081
+ const gen = (async function* () {
2082
+ if (!options.aiRuntime) {
2083
+ await gated("ask", name, async () => {
2084
+ throw new Error(`fx.run: AI runtime is not configured for agent "${name}"`);
2085
+ });
2086
+ return;
2087
+ }
2088
+ const local = new AbortController();
2089
+ const unlink = linkAbort(currentAbortSignal(), local);
2090
+ try {
2091
+ await withAbortSignal(local.signal, async () => {
2092
+ // The inner tool loop journals each step. Recording the whole
2093
+ // stream as one ask effect would replay the run id and skip
2094
+ // the parked tool.
2095
+ capability.assert("ask", name);
2096
+ const events = await options.aiRuntime!.streamAgent(name, {
2097
+ ...turn,
2098
+ ...(opts.threadId !== undefined ? { threadId: opts.threadId } : {}),
2099
+ gates: (options.rlsGateNames ?? []).filter((gate) => gate !== "public"),
2100
+ userId: auth.userId,
2101
+ operatorId: operator.id,
2102
+ ...(options.journal ? { journal: options.journal } : {}),
2103
+ ...(options.flow !== undefined ? { flow: options.flow } : {}),
2104
+ tenantId: tenant.id,
2105
+ auth: {
2106
+ userId: auth.userId,
2107
+ scopes: auth.scopes,
2108
+ verified: auth.verified,
2109
+ },
2110
+ callTool: (tool, toolInput) => fx.call(tool, toolInput),
2111
+ recordCall: (tool, runId) => {
2112
+ void gated(
2113
+ "call",
2114
+ tool,
2115
+ async () => runId,
2116
+ undefined,
2117
+ (id) =>
2118
+ typeof id === "string" && id.length > 0 ? { agentRunId: id } : undefined,
2119
+ );
2120
+ },
2121
+ onRunId(id) {
2122
+ heldRunId = id;
2123
+ markRun();
2124
+ },
2125
+ });
2126
+ inner = events as AsyncIterable<AgUiEvent> & {
2127
+ readonly result?: Promise<unknown>;
2128
+ };
2129
+ const settled = inner.result?.then(
2130
+ () => undefined,
2131
+ () => undefined,
2132
+ );
2133
+ await Promise.race([heldReady, settled ?? Promise.resolve()]);
2134
+ await recordEffect(
2135
+ ledger,
2136
+ "ask",
2137
+ name,
2138
+ now,
2139
+ async () => heldRunId,
2140
+ undefined,
2141
+ (id) => (typeof id === "string" && id.length > 0 ? { agentRunId: id } : undefined),
2142
+ );
2143
+ });
2144
+ if (inner) yield* inner;
2145
+ } finally {
2146
+ unlink();
2147
+ if (!local.signal.aborted) local.abort();
2148
+ }
2149
+ })();
2150
+ Object.defineProperty(gen, "result", {
2151
+ enumerable: true,
2152
+ get() {
2153
+ return inner?.result;
2154
+ },
2155
+ });
2156
+ return gen;
2157
+ }
2158
+ return gated(
2159
+ "ask",
2160
+ name,
2161
+ async () => {
2162
+ if (options.aiRuntime) {
2163
+ return options.aiRuntime.runAgent(name, {
2164
+ ...turn,
2165
+ gates: (options.rlsGateNames ?? []).filter((gate) => gate !== "public"),
1906
2166
  userId: auth.userId,
1907
- scopes: auth.scopes,
1908
- verified: auth.verified,
1909
- },
1910
- callTool: (tool, toolInput) => fx.call(tool, toolInput),
1911
- });
1912
- }
1913
- return { ok: true, steps: 0, denials: [], output: input };
1914
- });
1915
- },
2167
+ operatorId: operator.id,
2168
+ ...(options.journal ? { journal: options.journal } : {}),
2169
+ ...(options.flow !== undefined ? { flow: options.flow } : {}),
2170
+ tenantId: tenant.id,
2171
+ auth: {
2172
+ userId: auth.userId,
2173
+ scopes: auth.scopes,
2174
+ verified: auth.verified,
2175
+ },
2176
+ callTool: (tool, toolInput) => fx.call(tool, toolInput),
2177
+ recordCall: (tool, runId) => {
2178
+ void gated(
2179
+ "call",
2180
+ tool,
2181
+ async () => runId,
2182
+ undefined,
2183
+ (id) =>
2184
+ typeof id === "string" && id.length > 0 ? { agentRunId: id } : undefined,
2185
+ );
2186
+ },
2187
+ });
2188
+ }
2189
+ return { ok: true, steps: 0, denials: [], output: input };
2190
+ },
2191
+ undefined,
2192
+ (result) => {
2193
+ if (!result || typeof result !== "object" || !("runId" in result)) return undefined;
2194
+ const id = (result as { runId?: unknown }).runId;
2195
+ return typeof id === "string" && id.length > 0 ? { agentRunId: id } : undefined;
2196
+ },
2197
+ );
2198
+ } as Fx["run"],
1916
2199
  stream(model, opts) {
1917
2200
  const name = resolveName(model);
1918
2201
  const chunks = (async function* () {
@@ -1957,6 +2240,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1957
2240
  });
1958
2241
  },
1959
2242
  locale,
2243
+ lastEventId: options.lastEventId,
1960
2244
  id() {
1961
2245
  return okid();
1962
2246
  },
@@ -1983,12 +2267,22 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1983
2267
  with: jsonWith,
1984
2268
  withQuery: jsonWithQuery,
1985
2269
  stream(chunks) {
1986
- return {
2270
+ const carrier = {
1987
2271
  [jsonResultBrand]: true,
1988
2272
  kind: "stream" as const,
1989
2273
  status: 200 as const,
1990
2274
  chunks,
1991
2275
  };
2276
+ Object.defineProperty(carrier, "result", {
2277
+ enumerable: true,
2278
+ get() {
2279
+ if (chunks && typeof chunks === "object" && "result" in chunks) {
2280
+ return (chunks as { readonly result?: Promise<unknown> }).result;
2281
+ }
2282
+ return undefined;
2283
+ },
2284
+ });
2285
+ return carrier as JsonStreamResult;
1992
2286
  },
1993
2287
  },
1994
2288
  async step<T>(name: string, fn: () => T | Promise<T>, opts?: StepOptions<T>): Promise<T> {
@@ -0,0 +1,94 @@
1
+ /**
2
+ * HTTP wire-frame capture redacts credentials and keeps the rest.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import {
7
+ HTTP_FRAME_REDACTED,
8
+ captureHttpFrame,
9
+ redactHttpFrame,
10
+ requestForHttpInvoke,
11
+ } from "./http-frame.ts";
12
+
13
+ describe("requestForHttpInvoke", () => {
14
+ test("GET puts scalar input on the query string and keeps accept", () => {
15
+ const request = requestForHttpInvoke(
16
+ { method: "GET", path: "/tasks/:id" },
17
+ { pathParams: { id: "tsk_1" }, input: { id: "tsk_1", limit: 20 } },
18
+ );
19
+ expect(request.method).toBe("GET");
20
+ expect(new URL(request.url).pathname).toBe("/tasks/tsk_1");
21
+ expect(new URL(request.url).searchParams.get("limit")).toBe("20");
22
+ expect(new URL(request.url).searchParams.has("id")).toBe(false);
23
+ expect(request.headers.get("accept")).toBe("application/json");
24
+ expect(request.headers.get("x-oke-client")).toBe("console");
25
+ expect(request.headers.get("content-type")).toBeNull();
26
+ });
27
+ });
28
+
29
+ describe("captureHttpFrame", () => {
30
+ test("keeps method, path, query, and headers and redacts credentials", () => {
31
+ const request = new Request("http://localhost/tasks?limit=20&token=sekrit", {
32
+ method: "GET",
33
+ headers: {
34
+ accept: "application/json",
35
+ authorization: "Bearer super-secret",
36
+ cookie: "oke_session=abc",
37
+ "x-request-id": "req_1",
38
+ },
39
+ });
40
+ const response = new Response(JSON.stringify({ ok: true }), {
41
+ status: 200,
42
+ headers: {
43
+ "content-type": "application/json",
44
+ "set-cookie": "oke_session=next; HttpOnly",
45
+ },
46
+ });
47
+
48
+ expect(captureHttpFrame(request, response)).toEqual({
49
+ request: {
50
+ method: "GET",
51
+ path: "/tasks",
52
+ query: { limit: "20", token: HTTP_FRAME_REDACTED },
53
+ headers: {
54
+ accept: "application/json",
55
+ authorization: HTTP_FRAME_REDACTED,
56
+ cookie: HTTP_FRAME_REDACTED,
57
+ "x-request-id": "req_1",
58
+ },
59
+ },
60
+ response: {
61
+ status: 200,
62
+ headers: {
63
+ "content-type": "application/json",
64
+ "set-cookie": HTTP_FRAME_REDACTED,
65
+ },
66
+ },
67
+ });
68
+ });
69
+
70
+ test("redactHttpFrame masks a frame that skipped capture", () => {
71
+ expect(
72
+ redactHttpFrame({
73
+ request: {
74
+ method: "POST",
75
+ path: "/tasks",
76
+ query: {},
77
+ headers: { authorization: "Bearer leak", host: "localhost" },
78
+ },
79
+ response: { status: 201, headers: { "x-api-key": "leak" } },
80
+ }),
81
+ ).toEqual({
82
+ request: {
83
+ method: "POST",
84
+ path: "/tasks",
85
+ query: {},
86
+ headers: { authorization: HTTP_FRAME_REDACTED, host: "localhost" },
87
+ },
88
+ response: {
89
+ status: 201,
90
+ headers: { "x-api-key": HTTP_FRAME_REDACTED },
91
+ },
92
+ });
93
+ });
94
+ });