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
@@ -9,6 +9,19 @@
9
9
 
10
10
  import type { AiDriver, AiMessage, AiModelClient, AiToolDef } from "../../drivers/ai-types.ts";
11
11
  import { currentAbortSignal, withAbortSignal } from "../../kernel/abort-scope.ts";
12
+ import { hasJournalLease, type JournalSession, type JournalStore } from "../../kernel/journal.ts";
13
+ import { isJournalSuspend } from "../../kernel/journal-suspend.ts";
14
+ import {
15
+ AiDurableRequiredError,
16
+ approvalId,
17
+ approvalStepName,
18
+ approvalTimeoutMs,
19
+ readAgentApproval,
20
+ resolveAgentApproval,
21
+ type AgentApprovalDecision,
22
+ type AgentApprovalRecord,
23
+ type AgentApprovalResolveResult,
24
+ } from "./approval.ts";
12
25
  import {
13
26
  mcpCapabilityRefFromName,
14
27
  mcpModelToolName,
@@ -18,6 +31,7 @@ import { createMcpClient, type McpClient } from "./mcp-client.ts";
18
31
  import type { McpTransport } from "./mcp-transport.ts";
19
32
  import type { IndexStore } from "../../drivers/types.ts";
20
33
  import { maskRedactedDeep } from "../../kernel/redacted.ts";
34
+ import { lazyRequire } from "../../kernel/lazy-require.ts";
21
35
  import type { GatePolicyContext } from "../gate/declare.ts";
22
36
  import type { GateRuntime } from "../gate/runtime.ts";
23
37
  import type {
@@ -28,12 +42,20 @@ import type {
28
42
  AiPromptDecl,
29
43
  AiTimeout,
30
44
  } from "./declare.ts";
45
+ import type { AgentEventLog, AgentRunHeader } from "./run-events.ts";
46
+ import { setAgentEventLog } from "./agent-event-slot.ts";
31
47
  import {
32
48
  isRetryableAiError,
33
49
  mergeAskAbortSignal,
34
50
  outExpectsVia,
35
51
  resolveTimeoutMs,
36
52
  } from "./errors.ts";
53
+ import {
54
+ createEventQueue,
55
+ emitAssistantText,
56
+ type AgentEventEmit,
57
+ type AgUiEvent,
58
+ } from "./events.ts";
37
59
  import {
38
60
  AiSchemaValidationError,
39
61
  coerceModelObject,
@@ -49,6 +71,78 @@ const AI_SAME_MODEL_RETRY_BACKOFF_MS = 250;
49
71
  /** Default bound for tool / agent loops. */
50
72
  export const AI_DEFAULT_MAX_STEPS = 6;
51
73
 
74
+ /** Model-turn reader. Stream parsing stays off the AI barrel until a turn runs. */
75
+ function loadStreamTurn(): typeof import("./stream-turn.ts") {
76
+ return lazyRequire(import.meta.dir, ["stream", "turn"].join("-"));
77
+ }
78
+
79
+ /** Follow log. A static import pulls the journal event store onto every AI import. */
80
+ function loadRunEvents(): typeof import("./run-events.ts") {
81
+ return lazyRequire(import.meta.dir, ["run", "events"].join("-"));
82
+ }
83
+
84
+ /** Approval HTTP routes. Bound when an AI runtime is created, not at import. */
85
+ function loadApprovalHttp(): typeof import("./approval-http.ts") {
86
+ return lazyRequire(import.meta.dir, ["approval", "http"].join("-"));
87
+ }
88
+
89
+ /** Run ids. The extended okid table stays off this graph. */
90
+ function loadOkid(): typeof import("../../okid.ts") {
91
+ return lazyRequire(`${import.meta.dir}/../..`, ["ok", "id"].join(""));
92
+ }
93
+
94
+ /** SSE id stamp for a followed event. */
95
+ function loadSseId(): typeof import("../../kernel/sse-id.ts") {
96
+ return lazyRequire(`${import.meta.dir}/../../kernel`, ["sse", "id"].join("-"));
97
+ }
98
+
99
+ /** Thrown inside the tool loop when a deny or abort ends the run. */
100
+ class AgentLoopHalt extends Error {
101
+ readonly stopReason: "aborted" | "denied" | "error";
102
+ override readonly cause: unknown;
103
+ readonly trail: readonly AgentToolStep[];
104
+ readonly denials: readonly AgentDenial[];
105
+ readonly steps: number;
106
+ readonly cost: number;
107
+ readonly output: unknown;
108
+
109
+ constructor(
110
+ stopReason: "aborted" | "denied" | "error",
111
+ cause: unknown,
112
+ partial: {
113
+ readonly trail: readonly AgentToolStep[];
114
+ readonly denials: readonly AgentDenial[];
115
+ readonly steps: number;
116
+ readonly cost: number;
117
+ readonly output: unknown;
118
+ },
119
+ ) {
120
+ super(cause instanceof Error ? cause.message : String(cause));
121
+ this.name = stopReason === "aborted" ? "AbortError" : "AgentLoopHalt";
122
+ this.stopReason = stopReason;
123
+ this.cause = cause;
124
+ this.trail = partial.trail;
125
+ this.denials = partial.denials;
126
+ this.steps = partial.steps;
127
+ this.cost = partial.cost;
128
+ this.output = partial.output;
129
+ }
130
+ }
131
+
132
+ /** Console observability cap for ask journal entries and agent run records. */
133
+ export const AI_OBSERVABILITY_LIMIT = 500;
134
+
135
+ /**
136
+ * Keep the newest entries. Older rows are observability, not storage.
137
+ *
138
+ * @param buf - Mutable ring
139
+ * @param item - Entry to append
140
+ */
141
+ function pushObservability<T>(buf: T[], item: T): void {
142
+ buf.push(item);
143
+ if (buf.length > AI_OBSERVABILITY_LIMIT) buf.shift();
144
+ }
145
+
52
146
  /**
53
147
  * Split `name` / `name@version` the same way capability pins do.
54
148
  *
@@ -65,6 +159,9 @@ export function parsePromptRef(ref: string): {
65
159
  return { name: ref.slice(0, at), version: Number(tail) };
66
160
  }
67
161
 
162
+ /** Why an agent loop stopped. A fed-back denial is not `denied`. */
163
+ export type AgentStopReason = "completed" | "max_steps" | "budget" | "denied" | "aborted" | "error";
164
+
68
165
  /** Recorded agent tool denial (containment proof — not an error). */
69
166
  export interface AgentDenial {
70
167
  readonly agent: string;
@@ -90,6 +187,8 @@ export interface AgentToolStep {
90
187
  readonly effects: readonly AgentToolEffect[];
91
188
  readonly denial?: AgentDenial;
92
189
  readonly at: number;
190
+ /** Who approved this tool, when a human resolution ran it. */
191
+ readonly approver?: string;
93
192
  }
94
193
 
95
194
  /** Full agent run recorded on the denial / trail ledger. */
@@ -98,12 +197,25 @@ export interface AgentRunRecord {
98
197
  readonly agent: string;
99
198
  readonly message: string;
100
199
  readonly ok: boolean;
200
+ readonly stopReason: AgentStopReason;
101
201
  readonly steps: number;
102
202
  readonly trail: readonly AgentToolStep[];
103
203
  readonly denials: readonly AgentDenial[];
104
204
  readonly output?: unknown;
105
205
  readonly at: number;
106
206
  readonly cost: number;
207
+ /** Parent agent run, when this run is a nested tool. */
208
+ readonly parentRunId?: string;
209
+ /** Message when {@link stopReason} is `error`. */
210
+ readonly error?: string;
211
+ /** AG-UI thread, when the run opened a follow log. */
212
+ readonly threadId?: string;
213
+ /** Epoch ms the run finished. Absent while it is still open. */
214
+ readonly finishedAt?: number;
215
+ /** Driver-reported input tokens. Omitted when the driver did not supply them. */
216
+ readonly inputTokens?: number;
217
+ /** Driver-reported output tokens. Omitted when the driver did not supply them. */
218
+ readonly outputTokens?: number;
107
219
  }
108
220
 
109
221
  /** Fallback attempt for model routing (`via` chains). */
@@ -201,6 +313,10 @@ export interface CreateAiRuntimeOptions {
201
313
  * @param input - Tool input
202
314
  */
203
315
  readonly callFlow?: (name: string, input: unknown) => Promise<unknown>;
316
+ /** Journal store that holds pending tool approvals. */
317
+ readonly journalStore?: JournalStore;
318
+ /** Event log for follow / resume. Defaults to an in-memory log. */
319
+ readonly eventLog?: AgentEventLog;
204
320
  /**
205
321
  * Resolve gates required for a tool flow.
206
322
  *
@@ -251,16 +367,46 @@ export interface AiAskOptions {
251
367
  * Falls back to runtime `callFlow` when omitted.
252
368
  */
253
369
  readonly callTool?: (name: string, input: unknown) => Promise<unknown>;
370
+ /** Yield AG-UI events instead of returning the validated object. */
371
+ readonly stream?: boolean;
254
372
  }
255
373
 
256
374
  /** Agent run options. */
257
375
  export interface AiAgentRunOptions {
258
- readonly message: string;
376
+ /** Single user turn. Mutually exclusive with {@link messages}. */
377
+ readonly message?: string;
378
+ /** Prior user, assistant, and tool turns. Mutually exclusive with {@link message}. */
379
+ readonly messages?: readonly AiMessage[];
259
380
  readonly auth?: GatePolicyContext["auth"];
260
381
  readonly operator?: GatePolicyContext["operator"];
261
382
  readonly meta?: GatePolicyContext["meta"];
262
383
  /** Host-flow dispatch — must be `fx.call` when wired from fx.run. */
263
384
  readonly callTool?: (name: string, input: unknown) => Promise<unknown>;
385
+ /** Durable journal when the calling Flow is `durable: true`. */
386
+ readonly journal?: JournalSession;
387
+ /** Calling flow name, used when approval requires durability. */
388
+ readonly flow?: string;
389
+ readonly tenantId?: string | null;
390
+ /** This run's id. Nested tools stamp it as `parentRunId`. */
391
+ readonly runId?: string;
392
+ /** Parent agent run id. */
393
+ readonly parentRunId?: string;
394
+ /** 1-based nest level. Default 1. */
395
+ readonly depth?: number;
396
+ /** Cost cap for this invocation. Wins over the agent's own budget. */
397
+ readonly maxCostPerRun?: number;
398
+ /** Record a `call` effect on the host ledger when a child agent starts. */
399
+ readonly recordCall?: (name: string, runId?: string) => void | Promise<void>;
400
+ /** Fires once the agent run id exists, including streamed runs. */
401
+ readonly onRunId?: (runId: string) => void;
402
+ /** AG-UI thread. Defaults to a unique id. */
403
+ readonly threadId?: string;
404
+ /** Non-public gates on the calling Flow. */
405
+ readonly gates?: readonly string[];
406
+ /** Starting `auth.userId`. */
407
+ readonly userId?: string | null;
408
+ /** Starting operator id. */
409
+ readonly operatorId?: string | null;
264
410
  }
265
411
 
266
412
  /** Stream options. */
@@ -285,7 +431,7 @@ export interface AiRuntime {
285
431
  readonly denials: readonly AgentDenial[];
286
432
  /** Agent runs with full tool trails. */
287
433
  readonly agentRuns: readonly AgentRunRecord[];
288
- /** Journal of ask results (replay without re-calling the model). */
434
+ /** Ask results kept for Console (last {@link AI_OBSERVABILITY_LIMIT}). Not a replay cache. */
289
435
  readonly journal: readonly AiJournalEntry[];
290
436
  /**
291
437
  * Egress identity from the most recent ask / embed / stream complete.
@@ -300,6 +446,14 @@ export interface AiRuntime {
300
446
  * @param opts - via / tools / callTool / allowPii
301
447
  */
302
448
  ask(prompt: string, input?: unknown, opts?: AiAskOptions): Promise<Record<string, unknown>>;
449
+ /**
450
+ * Ask and yield AG-UI events. Tool-less asks still throw on budget inside the stream.
451
+ *
452
+ * @param prompt - Prompt name
453
+ * @param input - Prompt input
454
+ * @param opts - via / tools / callTool
455
+ */
456
+ streamAsk(prompt: string, input?: unknown, opts?: AiAskOptions): AsyncIterable<AgUiEvent>;
303
457
  /**
304
458
  * Run a bounded agent; tool calls that fail gates are denied + recorded.
305
459
  *
@@ -311,12 +465,22 @@ export interface AiRuntime {
311
465
  options: AiAgentRunOptions,
312
466
  ): Promise<{
313
467
  readonly ok: boolean;
468
+ readonly stopReason: AgentStopReason;
314
469
  readonly steps: number;
315
470
  readonly denials: readonly AgentDenial[];
316
471
  readonly trail: readonly AgentToolStep[];
317
472
  readonly output?: unknown;
318
473
  readonly cost: number;
474
+ readonly inputTokens?: number;
475
+ readonly outputTokens?: number;
319
476
  }>;
477
+ /**
478
+ * Run an agent and yield AG-UI events.
479
+ *
480
+ * @param agent - Agent name
481
+ * @param options - Message + auth context
482
+ */
483
+ streamAgent(agent: string, options: AiAgentRunOptions): AsyncIterable<AgUiEvent>;
320
484
  /**
321
485
  * Stream model tokens (real driver stream; fails loud if unsupported).
322
486
  *
@@ -348,6 +512,18 @@ export interface AiRuntime {
348
512
  * @param text - Text to embed
349
513
  */
350
514
  embedVector(model: string, text: string): Promise<readonly number[]>;
515
+ /**
516
+ * Approve or deny a pending tool. First resolution wins.
517
+ *
518
+ * @param id - Approval id
519
+ * @param decision - Approve or deny
520
+ * @param ctx - Gate context for the tool's gate
521
+ */
522
+ resolveApproval(
523
+ id: string,
524
+ decision: AgentApprovalDecision,
525
+ ctx: GatePolicyContext,
526
+ ): Promise<AgentApprovalResolveResult>;
351
527
  }
352
528
 
353
529
  /**
@@ -362,11 +538,79 @@ export function promptContentFromInput(input: unknown): string {
362
538
  }
363
539
 
364
540
  /**
365
- * User message plus a JSON-only contract when `out` is declared.
541
+ * Initial model messages for an agent run.
542
+ *
543
+ * @param runOpts - Single message or a history
544
+ */
545
+ function agentMessages(runOpts: AiAgentRunOptions): AiMessage[] {
546
+ if (runOpts.message !== undefined && runOpts.messages !== undefined) {
547
+ throw new TypeError("fx.run: pass message or messages, not both");
548
+ }
549
+ if (runOpts.messages !== undefined) return [...runOpts.messages];
550
+ return [{ role: "user", content: promptContentFromInput(runOpts.message ?? "") }];
551
+ }
552
+
553
+ const agentRunSlots = new WeakMap<JournalSession, { epoch: number; slot: number }>();
554
+
555
+ /**
556
+ * Run id for one agent invocation. A durable Flow journals the id so two
557
+ * streamed runs in the same Flow do not share a log, and a replay keeps it.
558
+ *
559
+ * @param agent - Agent name
560
+ * @param runOpts - Run options
561
+ */
562
+ async function allocateAgentRunId(agent: string, runOpts: AiAgentRunOptions): Promise<string> {
563
+ if (runOpts.runId) return runOpts.runId;
564
+ const journal = runOpts.journal;
565
+ if (!journal) return loadOkid().okid();
566
+ const epoch = journal.epoch;
567
+ const prev = agentRunSlots.get(journal);
568
+ const next = prev && prev.epoch === epoch ? prev.slot + 1 : 1;
569
+ agentRunSlots.set(journal, { epoch, slot: next });
570
+ const stored = await journal.effect("ask", `oke.agent.run.${agent}.${next}`, () =>
571
+ loadOkid().okid(),
572
+ );
573
+ return typeof stored === "string" ? stored : loadOkid().okid();
574
+ }
575
+
576
+ /**
577
+ * Principal and gates stored on the follow log.
578
+ *
579
+ * @param runId - Agent run id
580
+ * @param threadId - AG-UI thread
581
+ * @param runOpts - Run options
582
+ */
583
+ function agentLogHeader(
584
+ runId: string,
585
+ threadId: string,
586
+ agent: string,
587
+ runOpts: AiAgentRunOptions,
588
+ ): AgentRunHeader {
589
+ return {
590
+ runId,
591
+ threadId,
592
+ agent,
593
+ ...(runOpts.parentRunId !== undefined ? { parentRunId: runOpts.parentRunId } : {}),
594
+ tenant: runOpts.tenantId ?? null,
595
+ gates: runOpts.gates ?? [],
596
+ userId: runOpts.userId ?? runOpts.auth?.userId ?? null,
597
+ operatorId: runOpts.operatorId ?? runOpts.operator?.id ?? null,
598
+ };
599
+ }
600
+
601
+ /**
602
+ * Ledger label for a run that may be a history rather than one string.
366
603
  *
367
- * @param input - Ask input
368
- * @param out - Prompt output schema
604
+ * @param runOpts - Single message or a history
369
605
  */
606
+ function agentMessageLabel(runOpts: AiAgentRunOptions): string {
607
+ if (typeof runOpts.message === "string") return runOpts.message;
608
+ const lastUser = [...(runOpts.messages ?? [])]
609
+ .reverse()
610
+ .find((message) => message.role === "user");
611
+ return lastUser?.content ?? "";
612
+ }
613
+
370
614
  function askUserContent(input: unknown, out: unknown): string {
371
615
  const base = promptContentFromInput(input);
372
616
  const schema = promptOutJsonSchema(out);
@@ -390,6 +634,23 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
390
634
  for (const m of options.models ?? []) models.set(m.name, m);
391
635
 
392
636
  const clients = new Map<string, AiModelClient>(Object.entries(options.clients ?? {}));
637
+ const eventStore = options.journalStore?.agentEvents;
638
+ const eventLog =
639
+ options.eventLog ??
640
+ loadRunEvents().createMemoryAgentEventLog(eventStore, {
641
+ claim: async (runId) => {
642
+ const journal = options.journalStore;
643
+ if (journal && hasJournalLease(journal)) {
644
+ const row = await journal.get(runId);
645
+ if (row) {
646
+ return journal.acquireLease(runId, "agent-events", Date.now(), 30_000);
647
+ }
648
+ }
649
+ return (await eventStore?.claim(runId)) ?? true;
650
+ },
651
+ });
652
+ setAgentEventLog(eventLog);
653
+ loadApprovalHttp().setAgentFollowGates(options.gates);
393
654
  const mcpClient: McpClient = createMcpClient({
394
655
  servers: options.mcpServers,
395
656
  ...(options.resolveSecret !== undefined ? { resolveSecret: options.resolveSecret } : {}),
@@ -399,6 +660,27 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
399
660
  const agentRuns: AgentRunRecord[] = [];
400
661
  const journal: AiJournalEntry[] = [];
401
662
  const now = options.now ?? (() => Date.now());
663
+ const noteAppendFailure = (
664
+ runId: string,
665
+ agentName: string,
666
+ label: string,
667
+ err: unknown,
668
+ ): void => {
669
+ const message = err instanceof Error ? err.message : String(err);
670
+ pushObservability(agentRuns, {
671
+ id: runId,
672
+ agent: agentName,
673
+ message: label,
674
+ ok: false,
675
+ stopReason: "error",
676
+ error: message,
677
+ steps: 0,
678
+ trail: [],
679
+ denials: [],
680
+ at: now(),
681
+ cost: 0,
682
+ });
683
+ };
402
684
  const journalingForced = options.forceJournal !== false;
403
685
  let runSeq = 0;
404
686
  /** Mutable egress stamp shared by ask / embed / toolLoop. */
@@ -485,6 +767,8 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
485
767
  return defs;
486
768
  }
487
769
 
770
+ let self: AiRuntime;
771
+
488
772
  async function dispatchTool(opts: {
489
773
  readonly tool: string;
490
774
  readonly args: unknown;
@@ -497,6 +781,21 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
497
781
  readonly trail: AgentToolStep[];
498
782
  readonly runDenials: AgentDenial[];
499
783
  readonly signal?: AbortSignal;
784
+ readonly callId?: string;
785
+ readonly step?: number;
786
+ readonly index?: number;
787
+ readonly threadId?: string;
788
+ readonly journal?: JournalSession;
789
+ readonly flow?: string;
790
+ readonly tenantId?: string | null;
791
+ readonly approvals?: AiAgentDecl["approvals"];
792
+ readonly emit?: AgentEventEmit;
793
+ readonly runId?: string;
794
+ readonly depth?: number;
795
+ readonly maxCostPerRun?: number;
796
+ readonly spent?: number;
797
+ readonly recordCall?: (name: string, runId?: string) => void | Promise<void>;
798
+ readonly spend?: { cost: number; inputTokens?: number; outputTokens?: number };
500
799
  }): Promise<unknown> {
501
800
  const {
502
801
  tool,
@@ -525,7 +824,9 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
525
824
  runDenials.push(denial);
526
825
  denials.push(denial);
527
826
  trail.push({ tool, status: "denied", effects, denial, at: denial.at });
528
- throw new Error(`ai: model requested unknown tool "${tool}"`);
827
+ const unknown = new Error(`ai: model requested unknown tool "${tool}"`);
828
+ unknown.name = "AgentDenied";
829
+ throw unknown;
529
830
  }
530
831
 
531
832
  const requiredGates = options.gatesForFlow?.(capability) ?? [];
@@ -552,6 +853,152 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
552
853
  }
553
854
  }
554
855
 
856
+ const approval = opts.approvals?.[capability] ?? opts.approvals?.[tool];
857
+ const needsApproval =
858
+ approval !== undefined &&
859
+ (approval.when === undefined || approval.when(args, { auth, tenant: opts.tenantId ?? null }));
860
+ if (needsApproval) {
861
+ if (!opts.journal) {
862
+ throw new AiDurableRequiredError(opts.flow ?? "(unknown)", agentLabel);
863
+ }
864
+ const toolCallId =
865
+ opts.callId && opts.callId.length > 0
866
+ ? opts.callId
867
+ : `${agentLabel}:${opts.step ?? 0}:${opts.index ?? 0}`;
868
+ const id = approvalId(opts.journal.runId, toolCallId);
869
+ const record: AgentApprovalRecord = {
870
+ status: "pending",
871
+ agent: agentLabel,
872
+ tool: capability,
873
+ args,
874
+ gate: approval.gate,
875
+ tenant: opts.tenantId ?? null,
876
+ requestedAt: now(),
877
+ };
878
+ const stored = (await opts.journal.step(
879
+ approvalStepName(id),
880
+ () => record,
881
+ )) as AgentApprovalRecord;
882
+ let decision = stored;
883
+ if (decision.status === "pending") {
884
+ opts.emit?.({
885
+ type: "RUN_FINISHED",
886
+ threadId: opts.threadId ?? opts.runId ?? opts.journal.runId,
887
+ runId: opts.runId ?? opts.journal.runId,
888
+ outcome: {
889
+ type: "interrupt",
890
+ interrupts: [{ id, reason: "approval", payload: { tool: capability, args } }],
891
+ },
892
+ });
893
+ await opts.journal.sleep(approvalStepName(id), approval.timeout, () =>
894
+ approvalTimeoutMs(approval.timeout),
895
+ );
896
+ const store = options.journalStore;
897
+ if (!store) throw new Error("ai: approval resume requires a journal store");
898
+ decision = (await readAgentApproval(store, id)) ?? decision;
899
+ if (decision.status === "pending") {
900
+ const wrote = await resolveAgentApproval(
901
+ store,
902
+ id,
903
+ { decision: "deny", reason: "timeout", tenant: decision.tenant },
904
+ now,
905
+ opts.journal.run.lockedBy,
906
+ );
907
+ decision = wrote.ok
908
+ ? { ...decision, status: "denied", reason: "timeout" }
909
+ : ((await readAgentApproval(store, id)) ?? decision);
910
+ }
911
+ }
912
+ if (decision.status === "denied") {
913
+ const denial: AgentDenial = {
914
+ agent: agentLabel,
915
+ tool: capability,
916
+ gate: approval.gate,
917
+ reason: decision.reason ?? "denied",
918
+ at: now(),
919
+ };
920
+ runDenials.push(denial);
921
+ denials.push(denial);
922
+ trail.push({ tool: capability, status: "denied", effects, denial, at: denial.at });
923
+ return { denied: true, reason: denial.reason };
924
+ }
925
+ const toolArgs = decision.editedArgs !== undefined ? decision.editedArgs : args;
926
+ const output = await opts.journal.effect("call", approvalStepName(id), () => {
927
+ const call = callTool ?? options.callFlow;
928
+ if (!call) throw new Error("callFlow not configured");
929
+ return withAbortSignal(signal ?? currentAbortSignal(), () => call(capability, toolArgs));
930
+ });
931
+ trail.push({
932
+ tool: capability,
933
+ status: "ok",
934
+ effects,
935
+ at: now(),
936
+ ...(decision.approver !== undefined ? { approver: decision.approver } : {}),
937
+ });
938
+ return output;
939
+ }
940
+
941
+ const childDecl = agents.get(capability) ?? agents.get(tool);
942
+ if (childDecl && self) {
943
+ const depth = opts.depth ?? 1;
944
+ const parentLimit = agents.get(agentLabel)?.maxDepth ?? 3;
945
+ if (depth >= parentLimit) {
946
+ return { error: `ai: agent "${agentLabel}" is nested past maxDepth ${parentLimit}` };
947
+ }
948
+ const childId = loadOkid().okid();
949
+ opts.emit?.({
950
+ type: "CUSTOM",
951
+ name: "oke.subagent.started",
952
+ value: { runId: childId, parentToolCallId: opts.callId },
953
+ });
954
+ await opts.recordCall?.(childDecl.name, childId);
955
+ const parentRemaining =
956
+ opts.maxCostPerRun !== undefined ? opts.maxCostPerRun - (opts.spent ?? 0) : undefined;
957
+ const childCap = childDecl.budget?.maxCostPerRun;
958
+ const cap =
959
+ parentRemaining !== undefined
960
+ ? childCap !== undefined
961
+ ? Math.min(childCap, parentRemaining)
962
+ : parentRemaining
963
+ : childCap;
964
+ try {
965
+ const child = await self.runAgent(childDecl.name, {
966
+ message: typeof args === "string" ? args : JSON.stringify(args ?? {}),
967
+ runId: childId,
968
+ parentRunId: opts.runId,
969
+ depth: depth + 1,
970
+ ...(cap !== undefined ? { maxCostPerRun: cap } : {}),
971
+ ...(callTool !== undefined ? { callTool } : {}),
972
+ ...(auth !== undefined ? { auth } : {}),
973
+ ...(operator !== undefined ? { operator } : {}),
974
+ ...(meta !== undefined ? { meta } : {}),
975
+ ...(opts.journal !== undefined ? { journal: opts.journal } : {}),
976
+ ...(opts.flow !== undefined ? { flow: opts.flow } : {}),
977
+ ...(opts.tenantId !== undefined ? { tenantId: opts.tenantId } : {}),
978
+ ...(opts.recordCall !== undefined ? { recordCall: opts.recordCall } : {}),
979
+ });
980
+ if (opts.spend) {
981
+ opts.spend.cost += child.cost;
982
+ opts.spend.inputTokens = (opts.spend.inputTokens ?? 0) + (child.inputTokens ?? 0);
983
+ opts.spend.outputTokens = (opts.spend.outputTokens ?? 0) + (child.outputTokens ?? 0);
984
+ }
985
+ opts.emit?.({
986
+ type: "CUSTOM",
987
+ name: "oke.subagent.finished",
988
+ value: { runId: childId, parentToolCallId: opts.callId },
989
+ });
990
+ trail.push({ tool: capability, status: "ok", effects, at: now() });
991
+ return child.output ?? child;
992
+ } catch (err) {
993
+ opts.emit?.({
994
+ type: "CUSTOM",
995
+ name: "oke.subagent.error",
996
+ value: { runId: childId, parentToolCallId: opts.callId },
997
+ });
998
+ throw err;
999
+ }
1000
+ }
1001
+
555
1002
  const invoke = callTool ?? options.callFlow;
556
1003
  if (!invoke) {
557
1004
  const denial: AgentDenial = {
@@ -579,6 +1026,8 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
579
1026
  readonly messages: AiMessage[];
580
1027
  readonly tools: readonly string[];
581
1028
  readonly maxSteps: number;
1029
+ /** Stop before the next model call once accumulated cost reaches this cap. */
1030
+ readonly maxCostPerRun?: number;
582
1031
  readonly agentLabel: string;
583
1032
  readonly responseFormat?: unknown;
584
1033
  readonly signal?: AbortSignal;
@@ -586,6 +1035,15 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
586
1035
  readonly auth?: GatePolicyContext["auth"];
587
1036
  readonly operator?: GatePolicyContext["operator"];
588
1037
  readonly meta?: GatePolicyContext["meta"];
1038
+ readonly emit?: AgentEventEmit;
1039
+ readonly journal?: JournalSession;
1040
+ readonly flow?: string;
1041
+ readonly tenantId?: string | null;
1042
+ readonly approvals?: AiAgentDecl["approvals"];
1043
+ readonly runId?: string;
1044
+ readonly threadId?: string;
1045
+ readonly depth?: number;
1046
+ readonly recordCall?: (name: string, runId?: string) => void | Promise<void>;
589
1047
  }): Promise<{
590
1048
  readonly output: unknown;
591
1049
  readonly text: string;
@@ -595,6 +1053,8 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
595
1053
  readonly denials: AgentDenial[];
596
1054
  readonly steps: number;
597
1055
  readonly cost: number;
1056
+ readonly budgetExceeded: boolean;
1057
+ readonly stopReason: AgentStopReason;
598
1058
  readonly inputTokens?: number;
599
1059
  readonly outputTokens?: number;
600
1060
  }> {
@@ -609,20 +1069,86 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
609
1069
  const runDenials: AgentDenial[] = [];
610
1070
  let steps = 0;
611
1071
  let cost = 0;
1072
+ let budgetExceeded = false;
1073
+ let stopReason: AgentStopReason = "max_steps";
612
1074
  const tokens: { inputTokens?: number; outputTokens?: number } = {};
613
1075
  let lastText = "";
614
1076
  let lastRaw: unknown = {};
615
1077
  let lastToolResult: unknown;
1078
+ let messageSeq = 0;
1079
+
1080
+ const capHit = (): boolean => opts.maxCostPerRun !== undefined && cost >= opts.maxCostPerRun;
1081
+
1082
+ const finish = () => ({
1083
+ output: lastToolResult !== undefined ? lastToolResult : lastRaw,
1084
+ text: lastText,
1085
+ raw: lastRaw,
1086
+ lastToolResult,
1087
+ trail,
1088
+ denials: runDenials,
1089
+ steps,
1090
+ cost,
1091
+ budgetExceeded,
1092
+ stopReason,
1093
+ ...tokenFields(tokens),
1094
+ });
616
1095
 
617
1096
  const providerModel = wireModel(opts.modelName, opts.client);
618
1097
  while (steps < opts.maxSteps) {
619
- const result = await opts.client.complete({
620
- model: providerModel,
621
- messages,
622
- tools: defs.length > 0 ? defs : undefined,
623
- responseFormat: opts.responseFormat,
624
- ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
625
- });
1098
+ if (opts.signal?.aborted) {
1099
+ stopReason = "aborted";
1100
+ throw new AgentLoopHalt("aborted", new Error("aborted"), {
1101
+ trail,
1102
+ denials: runDenials,
1103
+ steps,
1104
+ cost,
1105
+ output: lastToolResult !== undefined ? lastToolResult : lastRaw,
1106
+ });
1107
+ }
1108
+ if (capHit()) {
1109
+ budgetExceeded = true;
1110
+ stopReason = "budget";
1111
+ return finish();
1112
+ }
1113
+ const stepName = `step-${steps + 1}`;
1114
+ opts.emit?.({ type: "STEP_STARTED", stepName });
1115
+ let result: Awaited<ReturnType<AiModelClient["complete"]>>;
1116
+ let streamedLive = false;
1117
+ let executed = false;
1118
+ try {
1119
+ const produce = async () => {
1120
+ executed = true;
1121
+ const turn = await loadStreamTurn().readModelTurn(
1122
+ opts.client,
1123
+ {
1124
+ model: providerModel,
1125
+ messages,
1126
+ tools: defs.length > 0 ? defs : undefined,
1127
+ responseFormat: opts.responseFormat,
1128
+ ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
1129
+ },
1130
+ opts.emit,
1131
+ () => `m-${++messageSeq}`,
1132
+ );
1133
+ streamedLive = turn.streamed;
1134
+ return turn.result;
1135
+ };
1136
+ result = opts.journal
1137
+ ? await opts.journal.effect("ask", `${opts.agentLabel}:${steps}`, produce)
1138
+ : await produce();
1139
+ } catch (err) {
1140
+ if (err instanceof AgentLoopHalt) throw err;
1141
+ if (err instanceof Error && err.name === "AbortError") {
1142
+ throw new AgentLoopHalt("aborted", err, {
1143
+ trail,
1144
+ denials: runDenials,
1145
+ steps,
1146
+ cost,
1147
+ output: lastToolResult !== undefined ? lastToolResult : lastRaw,
1148
+ });
1149
+ }
1150
+ throw err;
1151
+ }
626
1152
  if (result.external !== undefined) {
627
1153
  egress.lastExternal = result.external;
628
1154
  }
@@ -630,20 +1156,24 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
630
1156
  addUsageTokens(tokens, result.usage);
631
1157
  lastText = result.text;
632
1158
  lastRaw = result.raw !== undefined ? result.raw : result.text;
1159
+ const replayed = opts.journal !== undefined && !executed;
1160
+ const messageId = streamedLive || replayed ? "" : `m-${++messageSeq}`;
1161
+ const emittedText =
1162
+ streamedLive || replayed
1163
+ ? false
1164
+ : emitAssistantText(opts.emit ?? (() => undefined), messageId, result.text);
1165
+ if (capHit()) {
1166
+ budgetExceeded = true;
1167
+ stopReason = "budget";
1168
+ opts.emit?.({ type: "STEP_FINISHED", stepName });
1169
+ return finish();
1170
+ }
633
1171
 
634
1172
  const toolCalls = result.toolCalls;
635
1173
  if (!toolCalls || toolCalls.length === 0) {
636
- return {
637
- output: lastToolResult !== undefined ? lastToolResult : lastRaw,
638
- text: lastText,
639
- raw: lastRaw,
640
- lastToolResult,
641
- trail,
642
- denials: runDenials,
643
- steps,
644
- cost,
645
- ...tokenFields(tokens),
646
- };
1174
+ opts.emit?.({ type: "STEP_FINISHED", stepName });
1175
+ stopReason = "completed";
1176
+ return finish();
647
1177
  }
648
1178
 
649
1179
  messages.push({
@@ -652,43 +1182,111 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
652
1182
  toolCalls,
653
1183
  });
654
1184
 
655
- for (const tc of toolCalls) {
1185
+ for (const [index, tc] of toolCalls.entries()) {
656
1186
  if (steps >= opts.maxSteps) break;
657
1187
  steps++;
658
- const toolResult = await dispatchTool({
659
- tool: tc.name,
660
- args: tc.arguments,
661
- agentLabel: opts.agentLabel,
662
- allowedTools: allowed,
663
- callTool: opts.callTool,
664
- auth: opts.auth,
665
- operator: opts.operator,
666
- meta: opts.meta,
667
- trail,
668
- runDenials,
669
- ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
670
- });
1188
+ const callId = tc.id.length > 0 ? tc.id : `${opts.agentLabel}:${steps}:${index}`;
1189
+ if (!streamedLive && !replayed) {
1190
+ opts.emit?.({
1191
+ type: "TOOL_CALL_START",
1192
+ toolCallId: callId,
1193
+ toolCallName: tc.name,
1194
+ ...(emittedText ? { parentMessageId: messageId } : {}),
1195
+ });
1196
+ opts.emit?.({
1197
+ type: "TOOL_CALL_ARGS",
1198
+ toolCallId: callId,
1199
+ delta: JSON.stringify(tc.arguments ?? {}),
1200
+ });
1201
+ opts.emit?.({ type: "TOOL_CALL_END", toolCallId: callId });
1202
+ }
1203
+ let toolResult: unknown;
1204
+ const spend = { cost: 0, inputTokens: 0, outputTokens: 0 };
1205
+ try {
1206
+ toolResult = await dispatchTool({
1207
+ tool: tc.name,
1208
+ args: tc.arguments,
1209
+ agentLabel: opts.agentLabel,
1210
+ allowedTools: allowed,
1211
+ callTool: opts.callTool,
1212
+ auth: opts.auth,
1213
+ operator: opts.operator,
1214
+ meta: opts.meta,
1215
+ trail,
1216
+ runDenials,
1217
+ callId,
1218
+ step: steps,
1219
+ index,
1220
+ ...(opts.threadId !== undefined ? { threadId: opts.threadId } : {}),
1221
+ ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
1222
+ ...(opts.journal !== undefined ? { journal: opts.journal } : {}),
1223
+ ...(opts.flow !== undefined ? { flow: opts.flow } : {}),
1224
+ ...(opts.tenantId !== undefined ? { tenantId: opts.tenantId } : {}),
1225
+ ...(opts.approvals !== undefined ? { approvals: opts.approvals } : {}),
1226
+ ...(opts.emit !== undefined ? { emit: opts.emit } : {}),
1227
+ ...(opts.runId !== undefined ? { runId: opts.runId } : {}),
1228
+ ...(opts.depth !== undefined ? { depth: opts.depth } : {}),
1229
+ ...(opts.maxCostPerRun !== undefined ? { maxCostPerRun: opts.maxCostPerRun } : {}),
1230
+ spent: cost,
1231
+ ...(opts.recordCall !== undefined ? { recordCall: opts.recordCall } : {}),
1232
+ spend,
1233
+ });
1234
+ } catch (err) {
1235
+ if (err instanceof AgentLoopHalt || isJournalSuspend(err)) throw err;
1236
+ if (err instanceof AiDurableRequiredError) throw err;
1237
+ if (err instanceof Error && err.name === "AgentDenied") {
1238
+ throw new AgentLoopHalt("denied", err, {
1239
+ trail,
1240
+ denials: runDenials,
1241
+ steps,
1242
+ cost,
1243
+ output: lastToolResult !== undefined ? lastToolResult : lastRaw,
1244
+ });
1245
+ }
1246
+ if (err instanceof Error && err.name === "AbortError") {
1247
+ throw new AgentLoopHalt("aborted", err, {
1248
+ trail,
1249
+ denials: runDenials,
1250
+ steps,
1251
+ cost,
1252
+ output: lastToolResult !== undefined ? lastToolResult : lastRaw,
1253
+ });
1254
+ }
1255
+ throw new AgentLoopHalt("error", err, {
1256
+ trail,
1257
+ denials: runDenials,
1258
+ steps,
1259
+ cost,
1260
+ output: lastToolResult !== undefined ? lastToolResult : lastRaw,
1261
+ });
1262
+ }
671
1263
  lastToolResult = toolResult;
1264
+ cost += spend.cost;
1265
+ addUsageTokens(tokens, spend);
1266
+ opts.emit?.({
1267
+ type: "TOOL_CALL_RESULT",
1268
+ messageId: `m-${++messageSeq}`,
1269
+ toolCallId: callId,
1270
+ content: typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult ?? null),
1271
+ role: "tool",
1272
+ });
672
1273
  messages.push({
673
1274
  role: "tool",
674
1275
  content: typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult ?? null),
675
- toolCallId: tc.id,
1276
+ toolCallId: callId,
676
1277
  name: tc.name,
677
1278
  });
1279
+ if (capHit()) {
1280
+ budgetExceeded = true;
1281
+ stopReason = "budget";
1282
+ opts.emit?.({ type: "STEP_FINISHED", stepName });
1283
+ return finish();
1284
+ }
678
1285
  }
1286
+ opts.emit?.({ type: "STEP_FINISHED", stepName });
679
1287
  }
680
1288
 
681
- return {
682
- output: lastToolResult !== undefined ? lastToolResult : lastRaw,
683
- text: lastText,
684
- raw: lastRaw,
685
- lastToolResult,
686
- trail,
687
- denials: runDenials,
688
- steps,
689
- cost,
690
- ...tokenFields(tokens),
691
- };
1289
+ return finish();
692
1290
  }
693
1291
 
694
1292
  const runtime: AiRuntime = {
@@ -721,21 +1319,6 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
721
1319
  currentAbortSignal(),
722
1320
  );
723
1321
 
724
- // Replay from journal when input matches (nondeterministic contract)
725
- if (journalingForced && tools.length === 0) {
726
- const hit = [...journal]
727
- .reverse()
728
- .find(
729
- (e) =>
730
- e.prompt === prompt &&
731
- e.outcome === "ok" &&
732
- JSON.stringify(e.input) === JSON.stringify(input),
733
- );
734
- if (hit) {
735
- return hit.output as Record<string, unknown>;
736
- }
737
- }
738
-
739
1322
  const via =
740
1323
  opts?.via ?? decl.via ?? (decl.model ? [decl.model] : [...models.keys()].slice(0, 1));
741
1324
  const attempts: AiFallbackAttempt[] = [];
@@ -747,7 +1330,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
747
1330
  const responseFormat = promptResponseFormat(prompt, decl.out);
748
1331
 
749
1332
  const pushJournal = (entry: Omit<AiJournalEntry, "inputTokens" | "outputTokens">): void => {
750
- journal.push({
1333
+ pushObservability(journal, {
751
1334
  ...entry,
752
1335
  ...tokenFields(totalTokens),
753
1336
  });
@@ -777,6 +1360,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
777
1360
 
778
1361
  for (const modelName of via) {
779
1362
  let sameModelTries = 0;
1363
+ let repaired = false;
780
1364
  let advance = true;
781
1365
  while (sameModelTries < 2 && advance) {
782
1366
  sameModelTries++;
@@ -785,6 +1369,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
785
1369
  const client = await clientFor(modelName);
786
1370
  let raw: unknown;
787
1371
  let attemptCost = 0;
1372
+ const sent: AiMessage[] = [{ role: "user", content: userContent }];
788
1373
 
789
1374
  if (tools.length > 0) {
790
1375
  const loop = await toolLoop({
@@ -812,7 +1397,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
812
1397
  } else {
813
1398
  const result = await client.complete({
814
1399
  model: wireModel(modelName, client),
815
- messages: [{ role: "user", content: userContent }],
1400
+ messages: sent,
816
1401
  ...(responseFormat !== undefined ? { responseFormat } : {}),
817
1402
  ...(signal !== undefined ? { signal } : {}),
818
1403
  });
@@ -888,12 +1473,83 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
888
1473
  at: now(),
889
1474
  });
890
1475
  }
1476
+ if (decl.repair === 1 && !repaired) {
1477
+ repaired = true;
1478
+ const follow = await client.complete({
1479
+ model: wireModel(modelName, client),
1480
+ messages: [
1481
+ ...sent,
1482
+ {
1483
+ role: "user",
1484
+ content: `Schema mismatch: ${err.message}. Reply with JSON only.`,
1485
+ },
1486
+ ],
1487
+ ...(responseFormat !== undefined ? { responseFormat } : {}),
1488
+ ...(signal !== undefined ? { signal } : {}),
1489
+ });
1490
+ const repairCost = follow.usage?.cost ?? 0;
1491
+ totalCost += repairCost;
1492
+ addUsageTokens(totalTokens, follow.usage);
1493
+ attempts.push({
1494
+ model: modelName,
1495
+ ok: true,
1496
+ cost: repairCost,
1497
+ latencyMs: Math.max(0, now() - attemptStart),
1498
+ at: now(),
1499
+ });
1500
+ assertAskBudget(totalCost);
1501
+ let repairRaw: unknown =
1502
+ typeof follow.text === "string" && follow.text.length > 0
1503
+ ? follow.text
1504
+ : follow.raw !== undefined
1505
+ ? follow.raw
1506
+ : follow.text;
1507
+ try {
1508
+ const coerced = coerceModelObject(repairRaw);
1509
+ const prepared = outExpectsVia(decl.out)
1510
+ ? { ...coerced, via: modelName }
1511
+ : coerced;
1512
+ const validated = validatePromptOut(prompt, version, decl.out, prepared);
1513
+ const output = { ...validated, via: modelName };
1514
+ if (journalingForced) {
1515
+ pushJournal({
1516
+ prompt,
1517
+ ...(version !== undefined ? { version } : {}),
1518
+ input,
1519
+ output,
1520
+ attempts: [...attempts],
1521
+ outcome: "ok",
1522
+ cost: totalCost,
1523
+ latencyMs: Math.max(0, now() - started),
1524
+ at: now(),
1525
+ });
1526
+ }
1527
+ return output;
1528
+ } catch (again) {
1529
+ if (again instanceof AiSchemaValidationError && journalingForced) {
1530
+ pushJournal({
1531
+ prompt,
1532
+ ...(version !== undefined ? { version } : {}),
1533
+ input,
1534
+ output: coerceModelObject(repairRaw),
1535
+ attempts: [...attempts],
1536
+ outcome: "schema_invalid",
1537
+ cost: totalCost,
1538
+ latencyMs: Math.max(0, now() - started),
1539
+ schemaMismatch: again.mismatch,
1540
+ at: now(),
1541
+ });
1542
+ }
1543
+ throw again;
1544
+ }
1545
+ }
891
1546
  throw err;
892
1547
  }
893
1548
  throw err;
894
1549
  }
895
1550
  } catch (err) {
896
1551
  if (err instanceof AiSchemaValidationError) throw err;
1552
+ if (err instanceof Error && err.name === "AiBudgetExceededError") throw err;
897
1553
  lastError = err instanceof Error ? err.message : String(err);
898
1554
  attempts.push({
899
1555
  model: modelName,
@@ -955,49 +1611,456 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
955
1611
  const modelName = decl.model ?? [...models.keys()][0] ?? "mock";
956
1612
  const client = await clientFor(modelName);
957
1613
  const started = now();
1614
+ const runId = await allocateAgentRunId(agent, runOpts);
1615
+ runOpts.onRunId?.(runId);
1616
+ const threadId = runOpts.threadId ?? loadOkid().okid();
1617
+ const logHeader = agentLogHeader(runId, threadId, agent, runOpts);
1618
+ const safeAppend = async (event: AgUiEvent): Promise<void> => {
1619
+ try {
1620
+ await eventLog.append(runId, event, now());
1621
+ } catch (err) {
1622
+ noteAppendFailure(runId, agent, agentMessageLabel(runOpts), err);
1623
+ }
1624
+ };
1625
+ if (runOpts.journal) {
1626
+ await eventLog.open(logHeader);
1627
+ await safeAppend({ type: "RUN_STARTED", threadId, runId });
1628
+ }
958
1629
 
959
- const loop = await toolLoop({
960
- client,
961
- modelName,
962
- messages: [{ role: "user", content: promptContentFromInput(runOpts.message) }],
963
- tools: decl.tools,
964
- maxSteps,
965
- agentLabel: agent,
966
- callTool: runOpts.callTool,
967
- auth: runOpts.auth,
968
- operator: runOpts.operator,
969
- meta: runOpts.meta,
970
- signal: currentAbortSignal(),
971
- });
972
- const runCap = decl.budget?.maxCostPerRun;
973
- if (runCap !== undefined && loop.cost > runCap) {
974
- const err = new Error(`ai: agent "${agent}" exceeded maxCostPerRun ${runCap}`);
975
- err.name = "AiBudgetExceededError";
1630
+ const remember = (partial: {
1631
+ readonly ok: boolean;
1632
+ readonly stopReason: AgentStopReason;
1633
+ readonly steps: number;
1634
+ readonly trail: readonly AgentToolStep[];
1635
+ readonly denials: readonly AgentDenial[];
1636
+ readonly output: unknown;
1637
+ readonly cost: number;
1638
+ readonly error?: string;
1639
+ }) => {
1640
+ const record: AgentRunRecord = {
1641
+ id: runId,
1642
+ agent,
1643
+ message: agentMessageLabel(runOpts),
1644
+ ...(runOpts.parentRunId !== undefined ? { parentRunId: runOpts.parentRunId } : {}),
1645
+ ok: partial.ok,
1646
+ stopReason: partial.stopReason,
1647
+ ...(partial.error !== undefined ? { error: partial.error } : {}),
1648
+ steps: partial.steps,
1649
+ trail: partial.trail,
1650
+ denials: partial.denials,
1651
+ output: partial.output,
1652
+ at: started,
1653
+ finishedAt: now(),
1654
+ threadId,
1655
+ cost: partial.cost,
1656
+ ...(loopTokens.inputTokens !== undefined ? { inputTokens: loopTokens.inputTokens } : {}),
1657
+ ...(loopTokens.outputTokens !== undefined
1658
+ ? { outputTokens: loopTokens.outputTokens }
1659
+ : {}),
1660
+ };
1661
+ pushObservability(agentRuns, record);
1662
+ return {
1663
+ runId,
1664
+ ok: record.ok,
1665
+ stopReason: record.stopReason,
1666
+ steps: record.steps,
1667
+ denials: record.denials,
1668
+ trail: record.trail,
1669
+ output: record.output,
1670
+ cost: record.cost,
1671
+ ...(loopTokens.inputTokens !== undefined ? { inputTokens: loopTokens.inputTokens } : {}),
1672
+ ...(loopTokens.outputTokens !== undefined
1673
+ ? { outputTokens: loopTokens.outputTokens }
1674
+ : {}),
1675
+ };
1676
+ };
1677
+ let loopTokens: { inputTokens?: number; outputTokens?: number } = {};
1678
+ let appendChain: Promise<unknown> = Promise.resolve();
1679
+
1680
+ try {
1681
+ const loop = await toolLoop({
1682
+ client,
1683
+ modelName,
1684
+ messages: agentMessages(runOpts),
1685
+ tools: decl.tools,
1686
+ maxSteps,
1687
+ ...(runOpts.maxCostPerRun !== undefined
1688
+ ? { maxCostPerRun: runOpts.maxCostPerRun }
1689
+ : decl.budget?.maxCostPerRun !== undefined
1690
+ ? { maxCostPerRun: decl.budget.maxCostPerRun }
1691
+ : {}),
1692
+ agentLabel: agent,
1693
+ runId,
1694
+ depth: runOpts.depth ?? 1,
1695
+ ...(runOpts.recordCall !== undefined ? { recordCall: runOpts.recordCall } : {}),
1696
+ callTool: runOpts.callTool,
1697
+ auth: runOpts.auth,
1698
+ operator: runOpts.operator,
1699
+ meta: runOpts.meta,
1700
+ signal: currentAbortSignal(),
1701
+ threadId,
1702
+ ...(runOpts.journal !== undefined
1703
+ ? {
1704
+ emit: (event: AgUiEvent) => {
1705
+ appendChain = appendChain.then(() => safeAppend(event));
1706
+ },
1707
+ }
1708
+ : {}),
1709
+ ...(runOpts.journal !== undefined ? { journal: runOpts.journal } : {}),
1710
+ ...(runOpts.flow !== undefined ? { flow: runOpts.flow } : {}),
1711
+ ...(runOpts.tenantId !== undefined ? { tenantId: runOpts.tenantId } : {}),
1712
+ ...(decl.approvals !== undefined ? { approvals: decl.approvals } : {}),
1713
+ });
1714
+ await appendChain;
1715
+ loopTokens = tokenFields(loop);
1716
+ const settled = remember({
1717
+ ok: loop.stopReason === "completed" && loop.denials.length === 0,
1718
+ stopReason: loop.stopReason,
1719
+ steps: loop.steps,
1720
+ trail: loop.trail,
1721
+ denials: loop.denials,
1722
+ output: loop.output,
1723
+ cost: loop.cost,
1724
+ });
1725
+ if (runOpts.journal) {
1726
+ await safeAppend({
1727
+ type: "RUN_FINISHED",
1728
+ threadId,
1729
+ runId,
1730
+ result: { cost: loop.cost, stopReason: loop.stopReason, output: loop.output },
1731
+ ...(loopTokens.inputTokens !== undefined || loopTokens.outputTokens !== undefined
1732
+ ? { usage: [tokenFields(loopTokens)] }
1733
+ : {}),
1734
+ });
1735
+ }
1736
+ return settled;
1737
+ } catch (err) {
1738
+ if (err instanceof AgentLoopHalt) {
1739
+ const result = remember({
1740
+ ok: false,
1741
+ stopReason: err.stopReason,
1742
+ steps: err.steps,
1743
+ trail: err.trail,
1744
+ denials: err.denials,
1745
+ output: err.output,
1746
+ cost: err.cost,
1747
+ ...(err.stopReason === "error"
1748
+ ? { error: err.cause instanceof Error ? err.cause.message : String(err.cause) }
1749
+ : {}),
1750
+ });
1751
+ if (err.stopReason === "aborted" || err.stopReason === "error") {
1752
+ if (runOpts.journal) {
1753
+ const cause = err.cause instanceof Error ? err.cause : undefined;
1754
+ await safeAppend({
1755
+ type: "RUN_ERROR",
1756
+ message: cause?.message ?? String(err.cause),
1757
+ ...(cause && cause.name !== "Error" ? { code: cause.name } : {}),
1758
+ });
1759
+ }
1760
+ throw err.cause;
1761
+ }
1762
+ if (runOpts.journal) {
1763
+ await safeAppend({
1764
+ type: "RUN_FINISHED",
1765
+ threadId,
1766
+ runId,
1767
+ result: {
1768
+ cost: err.cost,
1769
+ stopReason: err.stopReason,
1770
+ output: err.output,
1771
+ },
1772
+ ...(loopTokens.inputTokens !== undefined || loopTokens.outputTokens !== undefined
1773
+ ? { usage: [tokenFields(loopTokens)] }
1774
+ : {}),
1775
+ });
1776
+ }
1777
+ return result;
1778
+ }
1779
+ if (runOpts.journal) {
1780
+ const error = err instanceof Error ? err : undefined;
1781
+ await safeAppend({
1782
+ type: "RUN_ERROR",
1783
+ message: error?.message ?? String(err),
1784
+ ...(error && error.name !== "Error" ? { code: error.name } : {}),
1785
+ });
1786
+ }
976
1787
  throw err;
977
1788
  }
1789
+ },
978
1790
 
979
- const record: AgentRunRecord = {
980
- id: `agent-run-${++runSeq}`,
981
- agent,
982
- message: runOpts.message,
983
- ok: loop.denials.length === 0,
984
- steps: loop.steps,
985
- trail: loop.trail,
986
- denials: loop.denials,
987
- output: loop.output,
988
- at: started,
989
- cost: loop.cost,
990
- };
991
- agentRuns.push(record);
992
-
993
- return {
994
- ok: record.ok,
995
- steps: loop.steps,
996
- denials: loop.denials,
997
- trail: loop.trail,
998
- output: loop.output,
999
- cost: record.cost,
1791
+ streamAsk(prompt, input, opts) {
1792
+ const queue = createEventQueue();
1793
+ const runId = `ask-${++runSeq}`;
1794
+ const signal = currentAbortSignal();
1795
+ void (async () => {
1796
+ try {
1797
+ queue.emit({ type: "RUN_STARTED", threadId: "default", runId });
1798
+ const tools = opts?.tools ?? [];
1799
+ if (tools.length > 0) {
1800
+ const pin = parsePromptRef(prompt);
1801
+ const decl = prompts.get(pin.name) ?? prompts.get(prompt);
1802
+ if (!decl) throw new Error(`ai: unknown prompt "${prompt}"`);
1803
+ const via =
1804
+ opts?.via ?? decl.via ?? (decl.model ? [decl.model] : [...models.keys()].slice(0, 1));
1805
+ const modelName = via[0];
1806
+ if (!modelName) throw new Error(`ai: no model for prompt "${prompt}"`);
1807
+ const client = await clientFor(modelName);
1808
+ const loop = await toolLoop({
1809
+ client,
1810
+ modelName,
1811
+ messages: [{ role: "user", content: askUserContent(input, decl.out) }],
1812
+ tools,
1813
+ maxSteps: opts?.maxSteps ?? AI_DEFAULT_MAX_STEPS,
1814
+ agentLabel: prompt,
1815
+ callTool: opts?.callTool,
1816
+ emit: queue.emit,
1817
+ signal,
1818
+ });
1819
+ queue.emit({
1820
+ type: "RUN_FINISHED",
1821
+ threadId: "default",
1822
+ runId,
1823
+ result: { cost: loop.cost, stopReason: loop.stopReason, output: loop.output },
1824
+ ...(tokenFields(loop).inputTokens !== undefined ||
1825
+ tokenFields(loop).outputTokens !== undefined
1826
+ ? { usage: [tokenFields(loop)] }
1827
+ : {}),
1828
+ });
1829
+ } else {
1830
+ const output = await runtime.ask(prompt, input, opts);
1831
+ emitAssistantText(queue.emit, "m-1", JSON.stringify(output));
1832
+ const spent = journal.at(-1)?.cost ?? 0;
1833
+ queue.emit({
1834
+ type: "RUN_FINISHED",
1835
+ threadId: "default",
1836
+ runId,
1837
+ result: { cost: spent, stopReason: "completed", output },
1838
+ });
1839
+ }
1840
+ queue.finish();
1841
+ } catch (err) {
1842
+ queue.emit({
1843
+ type: "RUN_ERROR",
1844
+ message: err instanceof Error ? err.message : String(err),
1845
+ ...(err instanceof Error && err.name !== "Error" ? { code: err.name } : {}),
1846
+ });
1847
+ queue.finish();
1848
+ }
1849
+ })();
1850
+ return queue.events;
1851
+ },
1852
+
1853
+ streamAgent(agent, runOpts) {
1854
+ const queue = createEventQueue();
1855
+ const signal = currentAbortSignal();
1856
+ const threadId = runOpts.threadId ?? loadOkid().okid();
1857
+ let skipLoggedStart = false;
1858
+ let runId = runOpts.runId ?? "";
1859
+ let appendChain: Promise<unknown> = Promise.resolve();
1860
+ const emit: AgentEventEmit = (event) => {
1861
+ if (!runOpts.journal) {
1862
+ queue.emit(event);
1863
+ return;
1864
+ }
1865
+ if (skipLoggedStart && event.type === "RUN_STARTED") {
1866
+ queue.emit(event);
1867
+ return;
1868
+ }
1869
+ appendChain = appendChain.then(async () => {
1870
+ let seq: number | undefined;
1871
+ try {
1872
+ seq = await eventLog.append(runId, event, now());
1873
+ } catch (err) {
1874
+ noteAppendFailure(runId, agent, agentMessageLabel(runOpts), err);
1875
+ }
1876
+ queue.emit(seq !== undefined ? loadSseId().withSseId(event, String(seq)) : event);
1877
+ });
1000
1878
  };
1879
+ let resolveResult: (value: unknown) => void = () => undefined;
1880
+ let rejectResult: (err: unknown) => void = () => undefined;
1881
+ const result = new Promise<unknown>((resolve, reject) => {
1882
+ resolveResult = resolve;
1883
+ rejectResult = reject;
1884
+ });
1885
+ // HTTP drains the iterator and may never await `result`.
1886
+ void result.catch(() => undefined);
1887
+ void (async () => {
1888
+ let closed = false;
1889
+ const close = (err?: unknown): void => {
1890
+ if (closed) return;
1891
+ closed = true;
1892
+ queue.finish(err);
1893
+ };
1894
+ try {
1895
+ if (runOpts.journal) {
1896
+ runId = await allocateAgentRunId(agent, runOpts);
1897
+ runOpts.onRunId?.(runId);
1898
+ await eventLog.open(agentLogHeader(runId, threadId, agent, runOpts));
1899
+ const prior = await eventLog.read(runId, 0);
1900
+ skipLoggedStart = prior.some((row) => row.event.type === "RUN_STARTED");
1901
+ } else {
1902
+ runId = runOpts.runId ?? loadOkid().okid();
1903
+ runOpts.onRunId?.(runId);
1904
+ }
1905
+ emit({ type: "RUN_STARTED", threadId, runId });
1906
+ const decl = agents.get(agent);
1907
+ if (!decl) throw new Error(`ai: unknown agent "${agent}"`);
1908
+ const maxSteps = decl.maxSteps ?? AI_DEFAULT_MAX_STEPS;
1909
+ const modelName = decl.model ?? [...models.keys()][0] ?? "mock";
1910
+ const client = await clientFor(modelName);
1911
+ const started = now();
1912
+ const loop = await toolLoop({
1913
+ client,
1914
+ modelName,
1915
+ messages: agentMessages(runOpts),
1916
+ tools: decl.tools,
1917
+ maxSteps,
1918
+ ...(runOpts.maxCostPerRun !== undefined
1919
+ ? { maxCostPerRun: runOpts.maxCostPerRun }
1920
+ : decl.budget?.maxCostPerRun !== undefined
1921
+ ? { maxCostPerRun: decl.budget.maxCostPerRun }
1922
+ : {}),
1923
+ agentLabel: agent,
1924
+ runId,
1925
+ depth: runOpts.depth ?? 1,
1926
+ ...(runOpts.recordCall !== undefined ? { recordCall: runOpts.recordCall } : {}),
1927
+ callTool: runOpts.callTool,
1928
+ auth: runOpts.auth,
1929
+ operator: runOpts.operator,
1930
+ meta: runOpts.meta,
1931
+ emit,
1932
+ signal,
1933
+ threadId,
1934
+ ...(runOpts.journal !== undefined ? { journal: runOpts.journal } : {}),
1935
+ ...(runOpts.flow !== undefined ? { flow: runOpts.flow } : {}),
1936
+ ...(runOpts.tenantId !== undefined ? { tenantId: runOpts.tenantId } : {}),
1937
+ ...(decl.approvals !== undefined ? { approvals: decl.approvals } : {}),
1938
+ });
1939
+ const record: AgentRunRecord = {
1940
+ id: runId,
1941
+ agent,
1942
+ message: agentMessageLabel(runOpts),
1943
+ ...(runOpts.parentRunId !== undefined ? { parentRunId: runOpts.parentRunId } : {}),
1944
+ ok: loop.stopReason === "completed" && loop.denials.length === 0,
1945
+ stopReason: loop.stopReason,
1946
+ steps: loop.steps,
1947
+ trail: loop.trail,
1948
+ denials: loop.denials,
1949
+ output: loop.output,
1950
+ at: started,
1951
+ cost: loop.cost,
1952
+ };
1953
+ pushObservability(agentRuns, record);
1954
+ const usage = tokenFields(loop);
1955
+ const settled = {
1956
+ ok: record.ok,
1957
+ stopReason: record.stopReason,
1958
+ steps: record.steps,
1959
+ denials: record.denials,
1960
+ trail: record.trail,
1961
+ output: record.output,
1962
+ cost: record.cost,
1963
+ ...(usage.inputTokens !== undefined ? { inputTokens: usage.inputTokens } : {}),
1964
+ ...(usage.outputTokens !== undefined ? { outputTokens: usage.outputTokens } : {}),
1965
+ };
1966
+ resolveResult(settled);
1967
+ await appendChain;
1968
+ emit({
1969
+ type: "RUN_FINISHED",
1970
+ threadId,
1971
+ runId,
1972
+ result: { cost: loop.cost, stopReason: loop.stopReason, output: loop.output },
1973
+ ...(usage.inputTokens !== undefined || usage.outputTokens !== undefined
1974
+ ? { usage: [usage] }
1975
+ : {}),
1976
+ });
1977
+ await appendChain;
1978
+ close();
1979
+ } catch (err) {
1980
+ if (isJournalSuspend(err)) {
1981
+ await appendChain;
1982
+ rejectResult(err);
1983
+ close(err);
1984
+ return;
1985
+ }
1986
+ if (err instanceof AiDurableRequiredError) {
1987
+ emit({
1988
+ type: "RUN_ERROR",
1989
+ message: err.message,
1990
+ code: err.name,
1991
+ });
1992
+ await appendChain;
1993
+ rejectResult(err);
1994
+ close();
1995
+ return;
1996
+ }
1997
+ if (err instanceof AgentLoopHalt) {
1998
+ const message = err.cause instanceof Error ? err.cause.message : String(err.cause);
1999
+ pushObservability(agentRuns, {
2000
+ id: runId,
2001
+ agent,
2002
+ message: agentMessageLabel(runOpts),
2003
+ ok: false,
2004
+ stopReason: err.stopReason,
2005
+ ...(err.stopReason === "error" ? { error: message } : {}),
2006
+ steps: err.steps,
2007
+ trail: err.trail,
2008
+ denials: err.denials,
2009
+ output: err.output,
2010
+ at: now(),
2011
+ cost: err.cost,
2012
+ });
2013
+ if (err.stopReason === "error") {
2014
+ const settled = {
2015
+ ok: false,
2016
+ stopReason: err.stopReason,
2017
+ error: message,
2018
+ steps: err.steps,
2019
+ denials: err.denials,
2020
+ trail: err.trail,
2021
+ output: err.output,
2022
+ cost: err.cost,
2023
+ };
2024
+ resolveResult(settled);
2025
+ await appendChain;
2026
+ emit({
2027
+ type: "RUN_FINISHED",
2028
+ threadId,
2029
+ runId,
2030
+ result: {
2031
+ cost: err.cost,
2032
+ stopReason: "error",
2033
+ output: err.output,
2034
+ error: message,
2035
+ },
2036
+ });
2037
+ await appendChain;
2038
+ close();
2039
+ return;
2040
+ }
2041
+ }
2042
+ const message = err instanceof Error ? err.message : String(err);
2043
+ rejectResult(err);
2044
+ emit({
2045
+ type: "RUN_ERROR",
2046
+ message,
2047
+ ...(err instanceof Error && err.name !== "Error" ? { code: err.name } : {}),
2048
+ });
2049
+ await appendChain;
2050
+ close();
2051
+ } finally {
2052
+ try {
2053
+ await appendChain;
2054
+ } catch (err) {
2055
+ noteAppendFailure(runId, agent, agentMessageLabel(runOpts), err);
2056
+ }
2057
+ if (!closed) {
2058
+ queue.emit({ type: "RUN_ERROR", message: "agent run ended" });
2059
+ close();
2060
+ }
2061
+ }
2062
+ })();
2063
+ return Object.assign(queue.events, { result });
1001
2064
  },
1002
2065
 
1003
2066
  async *stream(model, streamOpts) {
@@ -1062,6 +2125,23 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
1062
2125
  await index.upsert(id, vector, { text });
1063
2126
  },
1064
2127
 
2128
+ async resolveApproval(id, decision, ctx) {
2129
+ const store = options.journalStore;
2130
+ if (!store) return { ok: false, status: 404 };
2131
+ const pending = await readAgentApproval(store, id);
2132
+ if (!pending) return { ok: false, status: 404 };
2133
+ if ((decision.tenant ?? null) !== (pending.tenant ?? null)) {
2134
+ return { ok: false, status: 404 };
2135
+ }
2136
+ if (options.gates) {
2137
+ const allowed = await options.gates.allow([pending.gate], ctx);
2138
+ if (!allowed) return { ok: false, status: 403 };
2139
+ } else if (pending.gate !== "public") {
2140
+ return { ok: false, status: 403 };
2141
+ }
2142
+ return resolveAgentApproval(store, id, decision, now);
2143
+ },
2144
+
1065
2145
  async embedVector(modelName, text) {
1066
2146
  const client = await clientFor(modelName);
1067
2147
  if (!client.embed) {
@@ -1076,6 +2156,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
1076
2156
  return vector;
1077
2157
  },
1078
2158
  };
2159
+ self = runtime;
1079
2160
  return runtime;
1080
2161
  }
1081
2162