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
@@ -483,9 +483,7 @@ describe("fx — wholesale swap", () => {
483
483
  async deliverOtp(opts) {
484
484
  return { ok: true as const, channel: opts.only ?? opts.channels[0] ?? "email" };
485
485
  },
486
- async ask() {
487
- return {};
488
- },
486
+ ask: (async () => ({})) as unknown as Fx["ask"],
489
487
  async embed() {
490
488
  return [];
491
489
  },
@@ -495,9 +493,18 @@ describe("fx — wholesale swap", () => {
495
493
  async fetch() {
496
494
  return new Response(null, { status: 204 });
497
495
  },
498
- async run() {
499
- return {};
496
+ async decide<T>(): Promise<T> {
497
+ return {} as T;
498
+ },
499
+ agent: {
500
+ async approve() {
501
+ return { ok: true as const };
502
+ },
503
+ async deny() {
504
+ return { ok: true as const };
505
+ },
500
506
  },
507
+ run: (async () => ({})) as unknown as Fx["run"],
501
508
  async *stream() {
502
509
  /* empty */
503
510
  },
package/src/kernel/fx.ts CHANGED
@@ -38,7 +38,10 @@ import type { SignalDecl, SignalRuntime } from "../elements/signal.ts";
38
38
  import type { DeadLetter, SignalEmitOptions } from "../drivers/signal-types.ts";
39
39
  import type { VaultActor, VaultAdapter, VaultRuntime } from "../elements/vault.ts";
40
40
  import type { ChannelRuntime } from "../elements/channel.ts";
41
+ import type { AgentApprovalResolveResult } from "../elements/ai/approval.ts";
41
42
  import type { AiRuntime } from "../elements/ai.ts";
43
+ import type { AgUiEvent } from "../elements/ai/events.ts";
44
+ import type { AiMessage } from "../drivers/ai-types.ts";
42
45
  import { parseDurationMs } from "../elements/clock/duration.ts";
43
46
  import type { ApiKeyStore } from "../auth/api-keys.ts";
44
47
  import type { FxAuthIdentity, FxAuthKeyMethods } from "./fx-auth-keys.ts";
@@ -62,14 +65,14 @@ import {
62
65
  touchDryRunStore,
63
66
  } from "./dry-run.ts";
64
67
  import type { FailFn } from "./errors.ts";
65
- import { abortError, currentAbortSignal, linkAbort } from "./abort-scope.ts";
68
+ import { abortError, currentAbortSignal, linkAbort, withAbortSignal } from "./abort-scope.ts";
66
69
  import type { FxRetryOptions, FxThunk } from "./concurrency.ts";
67
70
  import { maskRedactedDeep, Redacted } from "./redacted.ts";
68
71
  import type { JournalSession, JournalStepOptions } from "./journal.ts";
69
72
 
70
73
  /** Options for {@link Fx.step} — see {@link JournalStepOptions}. */
71
74
  export type StepOptions<T> = JournalStepOptions<T>;
72
- import type { RunTelemetry } from "./run-telemetry.ts";
75
+ import { type RunTelemetry } from "./run-telemetry.ts";
73
76
  import type { RunsRuntime } from "../runs/runtime.ts";
74
77
  import type { RunsRow, WideEvent } from "../runs/types.ts";
75
78
  import type { RunWindowStats, SloBreach } from "../runs/window.ts";
@@ -114,7 +117,33 @@ function loadFxLiveStream(): typeof import("./fx-live-stream.ts") {
114
117
  return lazyRequire(import.meta.dir, ["fx", "live", "stream"].join("-"));
115
118
  }
116
119
 
120
+ /**
121
+ * Split `fx.run` input into one message or a history.
122
+ *
123
+ * @param input - String, `{ message }`, or `{ messages }`
124
+ */
125
+ function agentTurn(input: unknown): { message?: string; messages?: readonly AiMessage[] } {
126
+ if (typeof input === "string") return { message: input };
127
+ if (input && typeof input === "object") {
128
+ const record = input as { message?: unknown; messages?: unknown };
129
+ const hasMessage = "message" in record && record.message !== undefined;
130
+ const hasMessages = "messages" in record && record.messages !== undefined;
131
+ if (hasMessage && hasMessages) {
132
+ throw new TypeError("fx.run: pass message or messages, not both");
133
+ }
134
+ if (hasMessages && Array.isArray(record.messages)) {
135
+ return { messages: record.messages as readonly AiMessage[] };
136
+ }
137
+ if (hasMessage) return { message: String(record.message) };
138
+ }
139
+ return { message: JSON.stringify(input ?? {}) };
140
+ }
141
+
117
142
  /** `fx.fetch` — keep host parsing + dry-run stub off cold edge / Store-only graphs. */
143
+ function loadFxDecide(): typeof import("./fx-decide.ts") {
144
+ return lazyRequire(import.meta.dir, ["fx", "decide"].join("-"));
145
+ }
146
+
118
147
  function loadFxFetch(): typeof import("./fx-fetch.ts") {
119
148
  return lazyRequire(import.meta.dir, ["fx", "fetch"].join("-"));
120
149
  }
@@ -394,6 +423,8 @@ export interface FxAskOptions {
394
423
  readonly tools?: readonly NamedRef[];
395
424
  /** Bound on tool invocations (default 6). */
396
425
  readonly maxSteps?: number;
426
+ /** Yield AG-UI events instead of resolving to the validated object. */
427
+ readonly stream?: boolean;
397
428
  }
398
429
 
399
430
  /** Options for {@link Fx.search}. */
@@ -406,11 +437,13 @@ export type { JsonResult, JsonStreamResult };
406
437
 
407
438
  const sseFrameBrand: unique symbol = Symbol.for("oke.sse.frame");
408
439
 
409
- /** One SSE frame — optional `id:` plus JSON `data:`. */
440
+ /** One SSE frame — optional `id:` plus JSON `data:`, or a comment line. */
410
441
  export interface SseFrame {
411
442
  readonly [sseFrameBrand]: true;
412
443
  readonly data: unknown;
413
444
  readonly id?: string;
445
+ /** Comment body. When set and `data` is undefined, the encoder emits `: comment`. */
446
+ readonly comment?: string;
414
447
  }
415
448
 
416
449
  /**
@@ -423,6 +456,15 @@ export function sseFrame(data: unknown, id?: string): SseFrame {
423
456
  return id !== undefined ? { [sseFrameBrand]: true, data, id } : { [sseFrameBrand]: true, data };
424
457
  }
425
458
 
459
+ /**
460
+ * SSE comment (`: keepalive`). Keeps a followed run from being idle-closed.
461
+ *
462
+ * @param text - Comment text
463
+ */
464
+ export function sseComment(text: string): SseFrame {
465
+ return { [sseFrameBrand]: true, data: undefined, comment: text };
466
+ }
467
+
426
468
  /**
427
469
  * True when `value` is a branded SSE frame.
428
470
  *
@@ -781,6 +823,11 @@ export interface Fx {
781
823
  * @param opts - Model routing opts
782
824
  */
783
825
  ask(prompt: NamedRef, input?: unknown, opts?: FxAskOptions): Promise<Record<string, unknown>>;
826
+ ask(
827
+ prompt: NamedRef,
828
+ input: unknown,
829
+ opts: FxAskOptions & { readonly stream: true },
830
+ ): AsyncIterable<AgUiEvent>;
784
831
  /**
785
832
  * Embed text via a named model (records `embed` — distinct from `ask`).
786
833
  * Returns the vector; does not write an index.
@@ -808,13 +855,40 @@ export interface Fx {
808
855
  * @param init - Standard `RequestInit`
809
856
  */
810
857
  fetch(url: string | URL, init?: RequestInit): Promise<Response>;
858
+ /**
859
+ * Run one declared decision. `$` carries `how` and `audited`.
860
+ * Abstain mode makes each question value `T | null`.
861
+ */
862
+ decide<T>(decision: { name: string }, input: unknown): Promise<T>;
863
+ /**
864
+ * Resolve a pending agent tool approval (records nothing until the tool runs).
865
+ *
866
+ * First resolution wins. A later call returns a conflict failure.
867
+ */
868
+ readonly agent: {
869
+ /**
870
+ * @param id - Approval id from the interrupt
871
+ * @param opts - Optional replacement args
872
+ */
873
+ approve(id: string, opts?: { readonly args?: unknown }): Promise<AgentApprovalResolveResult>;
874
+ /**
875
+ * @param id - Approval id from the interrupt
876
+ * @param opts - Denial reason fed back to the model
877
+ */
878
+ deny(id: string, opts?: { readonly reason?: string }): Promise<AgentApprovalResolveResult>;
879
+ };
811
880
  /**
812
881
  * Run a bounded AI agent (records `ask`).
813
882
  *
814
883
  * @param agent - Agent name or handle
815
- * @param input - Agent input (`{ message }` or string)
884
+ * @param input - Agent input (`{ message }`, `{ messages }`, or string)
816
885
  */
817
886
  run(agent: NamedRef, input?: unknown): Promise<unknown>;
887
+ run(
888
+ agent: NamedRef,
889
+ input: unknown,
890
+ opts: { readonly stream: true; readonly threadId?: string },
891
+ ): AsyncIterable<AgUiEvent>;
818
892
  /**
819
893
  * Stream model tokens (records `ask`). Returns an async iterable of chunks.
820
894
  *
@@ -844,6 +918,8 @@ export interface Fx {
844
918
  t(key: AppMessageKey, values?: MessageValues): string;
845
919
  /** Active locale for {@link Fx.t} and default channel sends. */
846
920
  readonly locale: string;
921
+ /** HTTP `Last-Event-ID` on this request, when the client sent one. */
922
+ readonly lastEventId?: string;
847
923
  /** Generate a unique id (OKID). */
848
924
  id(): string;
849
925
  /** User-plane auth principal. */
@@ -1200,11 +1276,12 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1200
1276
  externalOf?:
1201
1277
  | EffectExternal
1202
1278
  | ((result: T | undefined, error: unknown) => EffectExternal | undefined),
1279
+ stamp?: (result: T | undefined) => { readonly agentRunId?: string } | undefined,
1203
1280
  ): Promise<T> {
1204
1281
  const signal = currentAbortSignal();
1205
1282
  if (signal.aborted) throw abortError(signal.reason);
1206
1283
  capability.assert(kind, resource);
1207
- const execute = () => recordEffect(ledger, kind, resource, now, body, externalOf);
1284
+ const execute = () => recordEffect(ledger, kind, resource, now, body, externalOf, stamp);
1208
1285
  if (journal) {
1209
1286
  return journal.effect(kind, resource, execute);
1210
1287
  }
@@ -1422,6 +1499,12 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1422
1499
 
1423
1500
  const aiDisablesCache = options.aiRuntime?.autoCacheDisabled === true;
1424
1501
  const telemetry = options.runTelemetry;
1502
+ if (telemetry) {
1503
+ lazyRequire<{ bindDecisionLabelTelemetry(telemetry: RunTelemetry): void }>(
1504
+ `${import.meta.dir}/../elements/ai/decisions`,
1505
+ ["lab", "els"].join(""),
1506
+ ).bindDecisionLabelTelemetry(telemetry);
1507
+ }
1425
1508
 
1426
1509
  const cache: FxCache = {
1427
1510
  async get<T = unknown>(key: string): Promise<T | undefined> {
@@ -1663,6 +1746,37 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1663
1746
  },
1664
1747
  };
1665
1748
 
1749
+ async function resolveAgentDecision(
1750
+ id: string,
1751
+ decision: {
1752
+ readonly decision: "approve" | "deny";
1753
+ readonly args?: unknown;
1754
+ readonly reason?: string;
1755
+ },
1756
+ ): Promise<AgentApprovalResolveResult> {
1757
+ const runtime = options.aiRuntime;
1758
+ if (!runtime?.resolveApproval) return { ok: false, status: 404 };
1759
+ const approver = auth.userId ?? operator.id;
1760
+ return runtime.resolveApproval(
1761
+ id,
1762
+ {
1763
+ decision: decision.decision,
1764
+ tenant: tenant.id,
1765
+ ...(decision.args !== undefined ? { args: decision.args } : {}),
1766
+ ...(decision.reason !== undefined ? { reason: decision.reason } : {}),
1767
+ ...(approver ? { approver } : {}),
1768
+ },
1769
+ {
1770
+ auth: {
1771
+ userId: auth.userId,
1772
+ scopes: auth.scopes,
1773
+ verified: auth.verified,
1774
+ },
1775
+ operator: { id: operator.id },
1776
+ },
1777
+ );
1778
+ }
1779
+
1666
1780
  const fx: Fx = {
1667
1781
  store: storeHandle,
1668
1782
  runs: runsSurface,
@@ -1815,8 +1929,37 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1815
1929
  return { ok: true as const, channel: result.channel };
1816
1930
  });
1817
1931
  },
1818
- ask(prompt, input, opts) {
1932
+ ask: function (prompt, input, opts) {
1819
1933
  const name = resolveName(prompt);
1934
+ if (opts?.stream) {
1935
+ return (async function* () {
1936
+ await gated("ask", name, async () => undefined);
1937
+ if (isDryRun()) {
1938
+ recordWouldHaveFired("ask", name);
1939
+ return;
1940
+ }
1941
+ if (!options.aiRuntime) {
1942
+ throw new Error(`fx.ask: AI runtime is not configured for prompt "${name}"`);
1943
+ }
1944
+ const local = new AbortController();
1945
+ const unlink = linkAbort(currentAbortSignal(), local);
1946
+ try {
1947
+ const events = await withAbortSignal(local.signal, () =>
1948
+ options.aiRuntime!.streamAsk(name, input, {
1949
+ via: opts.via?.map(resolveName),
1950
+ ...(opts.timeout !== undefined ? { timeout: opts.timeout } : {}),
1951
+ tools: opts.tools?.map(resolveName),
1952
+ maxSteps: opts.maxSteps,
1953
+ callTool: (tool, toolInput) => fx.call(tool, toolInput),
1954
+ }),
1955
+ );
1956
+ yield* events;
1957
+ } finally {
1958
+ unlink();
1959
+ if (!local.signal.aborted) local.abort();
1960
+ }
1961
+ })();
1962
+ }
1820
1963
  return gated(
1821
1964
  "ask",
1822
1965
  name,
@@ -1844,7 +1987,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1844
1987
  },
1845
1988
  () => options.aiRuntime?.lastExternal,
1846
1989
  );
1847
- },
1990
+ } as Fx["ask"],
1848
1991
  embed(model, text) {
1849
1992
  const name = resolveName(model);
1850
1993
  return gated(
@@ -1890,29 +2033,158 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1890
2033
  fetch(url, init) {
1891
2034
  return loadFxFetch().runFxFetch(gated, url, init);
1892
2035
  },
1893
- run(agent, input) {
2036
+ decide<T>(decision: { name: string }, input: unknown) {
2037
+ return loadFxDecide().runFxDecide({
2038
+ gated,
2039
+ journal: options.journal,
2040
+ signal: currentAbortSignal(),
2041
+ now,
2042
+ runId: options.runId,
2043
+ tenantId: tenant.id,
2044
+ getSecret: async (secretName) => {
2045
+ const value = await gatedSecret(secretName, async () => secrets[secretName]);
2046
+ return typeof value === "string" && value.length > 0 ? value : undefined;
2047
+ },
2048
+ decision,
2049
+ input,
2050
+ }) as Promise<T>;
2051
+ },
2052
+ agent: {
2053
+ approve(id, opts) {
2054
+ return resolveAgentDecision(id, { decision: "approve", args: opts?.args });
2055
+ },
2056
+ deny(id, opts) {
2057
+ return resolveAgentDecision(id, { decision: "deny", reason: opts?.reason });
2058
+ },
2059
+ },
2060
+ run: function (agent, input, opts?: { readonly stream?: boolean; readonly threadId?: string }) {
1894
2061
  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: {
2062
+ const turn = agentTurn(input);
2063
+ if (opts?.stream) {
2064
+ let inner: (AsyncIterable<AgUiEvent> & { readonly result?: Promise<unknown> }) | undefined;
2065
+ let heldRunId: string | undefined;
2066
+ let markRun: () => void = () => undefined;
2067
+ const heldReady = new Promise<void>((resolve) => {
2068
+ markRun = resolve;
2069
+ });
2070
+ const gen = (async function* () {
2071
+ if (!options.aiRuntime) {
2072
+ await gated("ask", name, async () => {
2073
+ throw new Error(`fx.run: AI runtime is not configured for agent "${name}"`);
2074
+ });
2075
+ return;
2076
+ }
2077
+ const local = new AbortController();
2078
+ const unlink = linkAbort(currentAbortSignal(), local);
2079
+ try {
2080
+ await withAbortSignal(local.signal, async () => {
2081
+ // The inner tool loop journals each step. Recording the whole
2082
+ // stream as one ask effect would replay the run id and skip
2083
+ // the parked tool.
2084
+ capability.assert("ask", name);
2085
+ const events = await options.aiRuntime!.streamAgent(name, {
2086
+ ...turn,
2087
+ ...(opts.threadId !== undefined ? { threadId: opts.threadId } : {}),
2088
+ gates: (options.rlsGateNames ?? []).filter((gate) => gate !== "public"),
2089
+ userId: auth.userId,
2090
+ operatorId: operator.id,
2091
+ ...(options.journal ? { journal: options.journal } : {}),
2092
+ ...(options.flow !== undefined ? { flow: options.flow } : {}),
2093
+ tenantId: tenant.id,
2094
+ auth: {
2095
+ userId: auth.userId,
2096
+ scopes: auth.scopes,
2097
+ verified: auth.verified,
2098
+ },
2099
+ callTool: (tool, toolInput) => fx.call(tool, toolInput),
2100
+ recordCall: (tool, runId) => {
2101
+ void gated(
2102
+ "call",
2103
+ tool,
2104
+ async () => runId,
2105
+ undefined,
2106
+ (id) =>
2107
+ typeof id === "string" && id.length > 0 ? { agentRunId: id } : undefined,
2108
+ );
2109
+ },
2110
+ onRunId(id) {
2111
+ heldRunId = id;
2112
+ markRun();
2113
+ },
2114
+ });
2115
+ inner = events as AsyncIterable<AgUiEvent> & {
2116
+ readonly result?: Promise<unknown>;
2117
+ };
2118
+ const settled = inner.result?.then(
2119
+ () => undefined,
2120
+ () => undefined,
2121
+ );
2122
+ await Promise.race([heldReady, settled ?? Promise.resolve()]);
2123
+ await recordEffect(
2124
+ ledger,
2125
+ "ask",
2126
+ name,
2127
+ now,
2128
+ async () => heldRunId,
2129
+ undefined,
2130
+ (id) => (typeof id === "string" && id.length > 0 ? { agentRunId: id } : undefined),
2131
+ );
2132
+ });
2133
+ if (inner) yield* inner;
2134
+ } finally {
2135
+ unlink();
2136
+ if (!local.signal.aborted) local.abort();
2137
+ }
2138
+ })();
2139
+ Object.defineProperty(gen, "result", {
2140
+ enumerable: true,
2141
+ get() {
2142
+ return inner?.result;
2143
+ },
2144
+ });
2145
+ return gen;
2146
+ }
2147
+ return gated(
2148
+ "ask",
2149
+ name,
2150
+ async () => {
2151
+ if (options.aiRuntime) {
2152
+ return options.aiRuntime.runAgent(name, {
2153
+ ...turn,
2154
+ gates: (options.rlsGateNames ?? []).filter((gate) => gate !== "public"),
1906
2155
  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
- },
2156
+ operatorId: operator.id,
2157
+ ...(options.journal ? { journal: options.journal } : {}),
2158
+ ...(options.flow !== undefined ? { flow: options.flow } : {}),
2159
+ tenantId: tenant.id,
2160
+ auth: {
2161
+ userId: auth.userId,
2162
+ scopes: auth.scopes,
2163
+ verified: auth.verified,
2164
+ },
2165
+ callTool: (tool, toolInput) => fx.call(tool, toolInput),
2166
+ recordCall: (tool, runId) => {
2167
+ void gated(
2168
+ "call",
2169
+ tool,
2170
+ async () => runId,
2171
+ undefined,
2172
+ (id) =>
2173
+ typeof id === "string" && id.length > 0 ? { agentRunId: id } : undefined,
2174
+ );
2175
+ },
2176
+ });
2177
+ }
2178
+ return { ok: true, steps: 0, denials: [], output: input };
2179
+ },
2180
+ undefined,
2181
+ (result) => {
2182
+ if (!result || typeof result !== "object" || !("runId" in result)) return undefined;
2183
+ const id = (result as { runId?: unknown }).runId;
2184
+ return typeof id === "string" && id.length > 0 ? { agentRunId: id } : undefined;
2185
+ },
2186
+ );
2187
+ } as Fx["run"],
1916
2188
  stream(model, opts) {
1917
2189
  const name = resolveName(model);
1918
2190
  const chunks = (async function* () {
@@ -1957,6 +2229,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1957
2229
  });
1958
2230
  },
1959
2231
  locale,
2232
+ lastEventId: options.lastEventId,
1960
2233
  id() {
1961
2234
  return okid();
1962
2235
  },
@@ -1983,12 +2256,22 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1983
2256
  with: jsonWith,
1984
2257
  withQuery: jsonWithQuery,
1985
2258
  stream(chunks) {
1986
- return {
2259
+ const carrier = {
1987
2260
  [jsonResultBrand]: true,
1988
2261
  kind: "stream" as const,
1989
2262
  status: 200 as const,
1990
2263
  chunks,
1991
2264
  };
2265
+ Object.defineProperty(carrier, "result", {
2266
+ enumerable: true,
2267
+ get() {
2268
+ if (chunks && typeof chunks === "object" && "result" in chunks) {
2269
+ return (chunks as { readonly result?: Promise<unknown> }).result;
2270
+ }
2271
+ return undefined;
2272
+ },
2273
+ });
2274
+ return carrier as JsonStreamResult;
1992
2275
  },
1993
2276
  },
1994
2277
  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
+ });