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
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Operator routes for the decisions list and the review queue.
3
+ */
4
+
5
+ import { z } from "zod";
6
+ import { journalLeaseBusyResponse } from "../../elements/ai/approval.ts";
7
+ import { resolveDecisionReview } from "../../kernel/fx-decide.ts";
8
+ import { fail, flow, http, type Binding } from "../../kernel/index.ts";
9
+ import { bindHttp } from "./bind.ts";
10
+ import type { ConsoleState } from "./state.ts";
11
+ import { loadDecisionQueue, projectDecisionList, candidateMetrics } from "./decisions.ts";
12
+
13
+ const AuthFailed = z.object({});
14
+
15
+ const DecisionListOut = z.object({
16
+ decisions: z.array(
17
+ z.object({
18
+ name: z.string(),
19
+ state: z.enum(["learning", "candidate", "certified", "suspended"]),
20
+ mode: z.enum(["review", "abstain"]),
21
+ model: z.string().optional(),
22
+ metrics: z.record(z.string(), z.number()).optional(),
23
+ promote: z.string().optional(),
24
+ }),
25
+ ),
26
+ suspended: z.array(z.string()),
27
+ failures: z.array(
28
+ z.object({
29
+ decision: z.string(),
30
+ question: z.string(),
31
+ message: z.string(),
32
+ at: z.number(),
33
+ }),
34
+ ),
35
+ });
36
+
37
+ const DecisionQueueOut = z.object({
38
+ rows: z.array(
39
+ z.object({
40
+ id: z.string(),
41
+ decision: z.string(),
42
+ requestedAt: z.number(),
43
+ ageMs: z.number(),
44
+ labelOnly: z.boolean(),
45
+ status: z.enum(["pending", "reviewed"]),
46
+ questions: z.array(
47
+ z.object({
48
+ id: z.string(),
49
+ kind: z.enum(["boolean", "choice", "score"]),
50
+ options: z.array(z.string()).optional(),
51
+ levels: z.array(z.string()).optional(),
52
+ }),
53
+ ),
54
+ }),
55
+ ),
56
+ });
57
+
58
+ const DecisionResolveIn = z.object({
59
+ id: z.string().min(1),
60
+ values: z.record(z.string(), z.unknown()),
61
+ labelOnly: z.boolean().optional(),
62
+ });
63
+
64
+ const DecisionResolveOut = z.object({ ok: z.literal(true) });
65
+
66
+ /**
67
+ * List, queue, and resolve bindings.
68
+ *
69
+ * @param state - Console state
70
+ */
71
+ export function decisionConsoleBindings(state: ConsoleState): Binding[] {
72
+ const list = flow("console.decisions.list", {
73
+ plane: "operator",
74
+ do: async (_input, fx) => {
75
+ if (!fx.operator.id) return fail("AuthFailed", {});
76
+ const { decisionDriftNames, getDecisionLock } =
77
+ await import("../../elements/ai/decisions/certificate.ts");
78
+ const { decisionLabelWriteFailures, listDecisionCandidates, loadDecisionCandidate } =
79
+ await import("../../elements/ai/decisions/labels.ts");
80
+ const names = await listDecisionCandidates();
81
+ const fitted: Record<string, Record<string, number>> = {};
82
+ for (const name of names) {
83
+ const metrics = candidateMetrics(await loadDecisionCandidate(name));
84
+ if (metrics) fitted[name] = metrics;
85
+ }
86
+ return {
87
+ decisions: projectDecisionList(state.manifest, getDecisionLock(), new Set(names), fitted),
88
+ suspended: decisionDriftNames(),
89
+ failures: decisionLabelWriteFailures(),
90
+ };
91
+ },
92
+ });
93
+ const queue = flow("console.decisions.queue", {
94
+ plane: "operator",
95
+ do: async (_input, fx) => {
96
+ if (!fx.operator.id) return fail("AuthFailed", {});
97
+ return { rows: await loadDecisionQueue(state.journalStore, Date.now()) };
98
+ },
99
+ });
100
+ const resolve = flow("console.decisions.resolve", {
101
+ plane: "operator",
102
+ do: async (input: { id: string; values: Record<string, unknown>; labelOnly?: boolean }, fx) => {
103
+ const reviewer = fx.operator.id;
104
+ if (!reviewer) return fail("Unauthorized", {});
105
+ const store = state.journalStore;
106
+ if (!store) return fx.fail.notFound();
107
+ const result = await resolveDecisionReview(
108
+ store,
109
+ input.id,
110
+ {
111
+ values: input.values,
112
+ reviewer,
113
+ tenantId: fx.tenant.id,
114
+ plane: "operator",
115
+ auth: {
116
+ userId: fx.auth.userId,
117
+ scopes: fx.auth.scopes,
118
+ verified: fx.auth.verified,
119
+ },
120
+ },
121
+ Date.now,
122
+ input.labelOnly === true,
123
+ );
124
+ if (result.ok) return { ok: true as const };
125
+ if (result.status === 409 && result.reason === "lease") {
126
+ return journalLeaseBusyResponse(result.retryAfterSeconds);
127
+ }
128
+ if (result.status === 422) {
129
+ return fail("ValidationError", {
130
+ issues: [{ message: "review values do not match the open questions", path: ["values"] }],
131
+ });
132
+ }
133
+ if (result.status === 503) return fx.fail.serviceUnavailable();
134
+ if (result.status === 403) return fx.fail.forbidden();
135
+ if (result.status === 409) return fx.fail.conflict();
136
+ return fx.fail.notFound();
137
+ },
138
+ });
139
+ return [
140
+ bindHttp(
141
+ http.get("/console/decisions", { out: DecisionListOut, errors: { AuthFailed } }),
142
+ list,
143
+ ),
144
+ bindHttp(
145
+ http.get("/console/decisions/queue", { out: DecisionQueueOut, errors: { AuthFailed } }),
146
+ queue,
147
+ ),
148
+ bindHttp(
149
+ http.post("/console/decisions/resolve", {
150
+ in: DecisionResolveIn,
151
+ out: DecisionResolveOut,
152
+ errors: { AuthFailed },
153
+ }),
154
+ resolve,
155
+ ),
156
+ ];
157
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Decision list state and pending age.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import type { JournalRun } from "../../kernel/journal.ts";
7
+ import { projectDecisionList, projectDecisionQueue } from "./decisions.ts";
8
+ import {
9
+ setDecisionDrift,
10
+ setDecisionLock,
11
+ resetDecisionCertificates,
12
+ } from "../../elements/ai/decisions/certificate.ts";
13
+ import type { Manifest } from "../../manifest/types.ts";
14
+
15
+ const manifest = {
16
+ ai: {
17
+ decisions: {
18
+ triage: { mode: "review" as const, questions: ["team"] },
19
+ route: { mode: "review" as const, questions: ["team"] },
20
+ },
21
+ },
22
+ } as unknown as Manifest;
23
+
24
+ describe("decision console projection", () => {
25
+ test("lockfile and drift set the list state", () => {
26
+ resetDecisionCertificates();
27
+ expect(projectDecisionList(manifest)[0]?.state).toBe("learning");
28
+ setDecisionLock({
29
+ decisions: {
30
+ triage: { model: "typesafe/jev-1.13.0", questions: {} },
31
+ },
32
+ });
33
+ expect(projectDecisionList(manifest).find((row) => row.name === "triage")?.state).toBe(
34
+ "certified",
35
+ );
36
+ setDecisionDrift("triage", true);
37
+ const rows = projectDecisionList(manifest, undefined, new Set(["route"]), {
38
+ route: { labels: 12, errors: 0 },
39
+ });
40
+ expect(rows.find((row) => row.name === "triage")?.state).toBe("suspended");
41
+ const route = rows.find((row) => row.name === "route");
42
+ expect(route?.state).toBe("candidate");
43
+ expect(route?.metrics).toEqual({ labels: 12, errors: 0 });
44
+ expect(route?.promote).toBe("oke decide promote route");
45
+ resetDecisionCertificates();
46
+ });
47
+
48
+ test("queue age is now minus requestedAt", () => {
49
+ const run = {
50
+ id: "run",
51
+ flow: "run",
52
+ status: "sleeping",
53
+ entries: [
54
+ {
55
+ kind: "step" as const,
56
+ name: "ai-decision:abc",
57
+ at: 1_000,
58
+ value: { status: "pending", requestedAt: 1_000 },
59
+ },
60
+ ],
61
+ } as JournalRun;
62
+ const [row] = projectDecisionQueue([run], 61_000);
63
+ expect(row?.ageMs).toBe(60_000);
64
+ expect(row?.labelOnly).toBe(false);
65
+ });
66
+ });
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Console projection for decisions: lockfile state and the review queue.
3
+ */
4
+
5
+ import type { JournalRun, JournalStore } from "../../kernel/journal.ts";
6
+ import type { Manifest } from "../../manifest/types.ts";
7
+ import { aiDecisionRegistry } from "../../kernel/element-registries.ts";
8
+ import {
9
+ decisionDriftNames,
10
+ decisionDriftSuspended,
11
+ getDecisionLock,
12
+ type DecisionCandidate,
13
+ type DecisionLockfile,
14
+ } from "../../elements/ai/decisions/certificate.ts";
15
+
16
+ /** List state for one decision. */
17
+ export type DecisionListState = "learning" | "candidate" | "certified" | "suspended";
18
+
19
+ /** One decision row. */
20
+ export interface DecisionListRow {
21
+ readonly name: string;
22
+ readonly state: DecisionListState;
23
+ readonly mode: "review" | "abstain";
24
+ readonly model?: string;
25
+ /** Candidate fit, when one is stored and the decision is not certified. */
26
+ readonly metrics?: Readonly<Record<string, number>>;
27
+ /** Command that writes this decision's candidate into the lockfile. */
28
+ readonly promote?: string;
29
+ }
30
+
31
+ /** One question the resolve form can answer. */
32
+ export interface DecisionFormQuestion {
33
+ readonly id: string;
34
+ readonly kind: "boolean" | "choice" | "score";
35
+ readonly options?: readonly string[];
36
+ readonly levels?: readonly string[];
37
+ }
38
+
39
+ /** One parked or audit review. */
40
+ export interface DecisionQueueRow {
41
+ readonly id: string;
42
+ readonly decision: string;
43
+ readonly requestedAt: number;
44
+ readonly ageMs: number;
45
+ readonly labelOnly: boolean;
46
+ readonly status: "pending" | "reviewed";
47
+ readonly questions: readonly DecisionFormQuestion[];
48
+ }
49
+
50
+ /**
51
+ * List state from the lockfile, the candidate, and the app drift flag.
52
+ *
53
+ * @param manifest - App manifest
54
+ * @param lock - Lockfile, when one is loaded
55
+ */
56
+ export function projectDecisionList(
57
+ manifest: Manifest | null | undefined,
58
+ lock: DecisionLockfile | undefined = getDecisionLock(),
59
+ candidates: ReadonlySet<string> = new Set(),
60
+ metrics: Readonly<Record<string, Readonly<Record<string, number>>>> = {},
61
+ ): DecisionListRow[] {
62
+ const decisions = manifest?.ai?.decisions ?? {};
63
+ return Object.entries(decisions).map(([name, decision]) => {
64
+ const certified = lock?.decisions[name] !== undefined;
65
+ const candidate = candidates.has(name);
66
+ const suspended = decisionDriftSuspended(name);
67
+ const state: DecisionListState = suspended
68
+ ? "suspended"
69
+ : certified
70
+ ? "certified"
71
+ : candidate
72
+ ? "candidate"
73
+ : "learning";
74
+ const fitted = metrics[name];
75
+ return {
76
+ name,
77
+ state,
78
+ mode: decision.mode,
79
+ ...(decision.model !== undefined ? { model: decision.model } : {}),
80
+ ...(state === "candidate" && fitted ? { metrics: fitted } : {}),
81
+ ...(state === "candidate" ? { promote: `oke decide promote ${name}` } : {}),
82
+ };
83
+ });
84
+ }
85
+
86
+ /**
87
+ * First slice metrics on a stored candidate.
88
+ *
89
+ * @param entry - Candidate body
90
+ */
91
+ export function candidateMetrics(
92
+ entry: DecisionCandidate | undefined,
93
+ ): Record<string, number> | undefined {
94
+ if (!entry) return undefined;
95
+ for (const slices of Object.values(entry.questions)) {
96
+ for (const slice of Object.values(slices)) {
97
+ if (slice.metrics) return { ...slice.metrics };
98
+ }
99
+ }
100
+ return undefined;
101
+ }
102
+
103
+ /**
104
+ * Pending and reviewed decision steps, newest age first.
105
+ *
106
+ * @param runs - Journal runs
107
+ * @param now - Clock
108
+ */
109
+ export function projectDecisionQueue(
110
+ runs: readonly JournalRun[],
111
+ now: number,
112
+ tenantId?: string | null,
113
+ ): DecisionQueueRow[] {
114
+ const rows: DecisionQueueRow[] = [];
115
+ for (const run of runs) {
116
+ for (const entry of run.entries) {
117
+ if (entry.kind !== "step") continue;
118
+ const labelOnly = entry.name.startsWith("ai-decision-label:");
119
+ const parked = entry.name.startsWith("ai-decision:");
120
+ if (!labelOnly && !parked) continue;
121
+ const id = entry.name.slice(labelOnly ? "ai-decision-label:".length : "ai-decision:".length);
122
+ const value = entry.value as {
123
+ status?: "pending" | "reviewed";
124
+ requestedAt?: number;
125
+ tenant?: string | null;
126
+ open?: readonly string[];
127
+ };
128
+ if (tenantId !== undefined && (value.tenant ?? null) !== tenantId) continue;
129
+ const requestedAt = value.requestedAt ?? entry.at;
130
+ rows.push({
131
+ id,
132
+ decision: decisionNameFromId(id),
133
+ requestedAt,
134
+ ageMs: Math.max(0, now - requestedAt),
135
+ labelOnly,
136
+ status: value.status ?? "pending",
137
+ questions: formQuestions(decisionNameFromId(id), value.open),
138
+ });
139
+ }
140
+ }
141
+ return rows.sort((a, b) => b.ageMs - a.ageMs);
142
+ }
143
+
144
+ /** One decision on the read-only MCP list. */
145
+ export interface McpDecisionRow {
146
+ readonly name: string;
147
+ readonly state: DecisionListState;
148
+ readonly pending: number;
149
+ readonly metrics?: Readonly<Record<string, number>>;
150
+ readonly drift: boolean;
151
+ }
152
+
153
+ /**
154
+ * Decision rows for `oke.decisions.list`: lockfile state, pending reviews,
155
+ * certificate or candidate metrics, and drift.
156
+ *
157
+ * @param manifest - App manifest
158
+ * @param pendingByName - Pending review counts
159
+ * @param metrics - Candidate or certificate metrics
160
+ */
161
+ export function projectMcpDecisions(
162
+ manifest: Manifest | null | undefined,
163
+ pendingByName: Readonly<Record<string, number>> = {},
164
+ metrics: Readonly<Record<string, Readonly<Record<string, number>>>> = {},
165
+ ): McpDecisionRow[] {
166
+ const drifted = new Set(decisionDriftNames());
167
+ return projectDecisionList(
168
+ manifest,
169
+ getDecisionLock(),
170
+ new Set(Object.keys(metrics)),
171
+ metrics,
172
+ ).map((row) => ({
173
+ name: row.name,
174
+ state: row.state,
175
+ pending: pendingByName[row.name] ?? 0,
176
+ ...((row.metrics ?? metrics[row.name]) ? { metrics: row.metrics ?? metrics[row.name] } : {}),
177
+ drift: drifted.has(row.name) || row.state === "suspended",
178
+ }));
179
+ }
180
+
181
+ /**
182
+ * Queue from a journal store.
183
+ *
184
+ * @param store - Durable journal, when the console has one
185
+ * @param now - Clock
186
+ */
187
+ export async function loadDecisionQueue(
188
+ store: JournalStore | null | undefined,
189
+ now: number,
190
+ tenantId?: string | null,
191
+ ): Promise<DecisionQueueRow[]> {
192
+ if (!store) return [];
193
+ return projectDecisionQueue(await store.list(), now, tenantId);
194
+ }
195
+
196
+ function decisionNameFromId(id: string): string {
197
+ try {
198
+ const decoded = Buffer.from(id, "base64url").toString("utf8");
199
+ const first = decoded.indexOf(".");
200
+ const second = first >= 0 ? decoded.indexOf(".", first + 1) : -1;
201
+ if (second > first) return decoded.slice(second + 1);
202
+ return first > 0 ? decoded.slice(first + 1) : id;
203
+ } catch {
204
+ return id;
205
+ }
206
+ }
207
+
208
+ function formQuestions(name: string, open: readonly string[] | undefined): DecisionFormQuestion[] {
209
+ const decl = aiDecisionRegistry.find((item) => item.name === name);
210
+ if (!decl) return [];
211
+ const ids = open && open.length > 0 ? open : Object.keys(decl.ask);
212
+ const questions: DecisionFormQuestion[] = [];
213
+ for (const id of ids) {
214
+ const question = decl.ask[id];
215
+ if (!question) continue;
216
+ if (question.kind === "choice") {
217
+ questions.push({
218
+ id,
219
+ kind: "choice",
220
+ options: [...Object.keys(question.options), "none_of_these"],
221
+ });
222
+ } else if (question.kind === "score") {
223
+ questions.push({ id, kind: "score", levels: [...question.levels] });
224
+ } else {
225
+ questions.push({ id, kind: "boolean" });
226
+ }
227
+ }
228
+ return questions;
229
+ }
@@ -16,6 +16,8 @@ import {
16
16
  } from "../../auth/index.ts";
17
17
  import { DryRunWriteIsolationError } from "../../kernel/dry-run.ts";
18
18
  import { fail, flow, http, type AnyFlowDef, type Binding } from "../../kernel/index.ts";
19
+ import { redactHttpFrame } from "../../kernel/http-frame.ts";
20
+ import { lazyRequire } from "../../kernel/lazy-require.ts";
19
21
  import type { Flow as ManifestFlow, ResourceRef } from "../../manifest/types.ts";
20
22
  import { eventHasIrreversible, runReplay } from "../../cli/replay.ts";
21
23
  import { createRunsRuntime } from "../../runs/runtime.ts";
@@ -44,6 +46,7 @@ import {
44
46
  STORE_SQL_LOCKS_POLL_MS,
45
47
  STORE_SQL_STATS_PII_GAP,
46
48
  StoreSqlStatsError,
49
+ engineErrorText,
47
50
  } from "./store-stats.ts";
48
51
  import {
49
52
  STORE_KV_STATS_SERVER_WIDE_GAP,
@@ -92,7 +95,18 @@ const ManifestOut = z.object({
92
95
  });
93
96
 
94
97
  const EffectEntryOut = z.object({
95
- kind: z.enum(["read", "write", "emit", "send", "ask", "embed", "secret", "call", "fetch"]),
98
+ kind: z.enum([
99
+ "read",
100
+ "write",
101
+ "emit",
102
+ "send",
103
+ "ask",
104
+ "embed",
105
+ "secret",
106
+ "call",
107
+ "fetch",
108
+ "decide",
109
+ ]),
96
110
  resource: z.string(),
97
111
  timestamp: z.number(),
98
112
  duration: z.number(),
@@ -104,6 +118,7 @@ const EffectEntryOut = z.object({
104
118
  kind: z.enum(["third-party", "infrastructure"]).optional(),
105
119
  })
106
120
  .optional(),
121
+ agentRunId: z.string().optional(),
107
122
  });
108
123
 
109
124
  const LogLineOut = z.object({
@@ -150,6 +165,26 @@ const RunsListOut = z.object({
150
165
  input: z.unknown().nullable(),
151
166
  /** Flow return value snapshot — null when absent (failures / legacy). */
152
167
  output: z.unknown().nullable(),
168
+ /**
169
+ * HTTP wire frame — null for non-HTTP runs and rows recorded before
170
+ * the frame existed.
171
+ */
172
+ http: z
173
+ .object({
174
+ request: z.object({
175
+ method: z.string(),
176
+ path: z.string(),
177
+ query: z.record(z.string(), z.string()),
178
+ headers: z.record(z.string(), z.string()),
179
+ }),
180
+ response: z
181
+ .object({
182
+ status: z.number(),
183
+ headers: z.record(z.string(), z.string()),
184
+ })
185
+ .optional(),
186
+ })
187
+ .nullable(),
153
188
  }),
154
189
  ),
155
190
  });
@@ -891,6 +926,8 @@ const AiListOut = z.object({
891
926
  agent: z.string(),
892
927
  message: z.string(),
893
928
  ok: z.boolean(),
929
+ stopReason: z.enum(["completed", "max_steps", "budget", "denied", "aborted", "error"]),
930
+ error: z.string().optional(),
894
931
  steps: z.number(),
895
932
  cost: z.number(),
896
933
  at: z.number(),
@@ -910,6 +947,7 @@ const AiListOut = z.object({
910
947
  "secret",
911
948
  "call",
912
949
  "fetch",
950
+ "decide",
913
951
  ]),
914
952
  resource: z.string(),
915
953
  }),
@@ -2356,6 +2394,14 @@ export function createConsoleBindings(state: ConsoleState): {
2356
2394
  }),
2357
2395
  channelSendTest,
2358
2396
  ),
2397
+ ...lazyRequire<typeof import("./decisions-flows.ts")>(
2398
+ import.meta.dir,
2399
+ ["decisions", "flows"].join("-"),
2400
+ ).decisionConsoleBindings(state),
2401
+ ...lazyRequire<typeof import("./ai-runs-flows.ts")>(
2402
+ import.meta.dir,
2403
+ ["ai", "runs", "flows"].join("-"),
2404
+ ).agentConsoleBindings(state),
2359
2405
  ];
2360
2406
 
2361
2407
  return {
@@ -2657,6 +2703,7 @@ export function projectRun(r: WideEvent, piiFields: ReadonlySet<string> = new Se
2657
2703
  duration: e.duration,
2658
2704
  reversibility: e.reversibility,
2659
2705
  ...(e.external !== undefined ? { external: e.external } : {}),
2706
+ ...(e.agentRunId !== undefined ? { agentRunId: e.agentRunId } : {}),
2660
2707
  })),
2661
2708
  logs: masked.logs.map((line) => ({
2662
2709
  level: line.level,
@@ -2667,6 +2714,7 @@ export function projectRun(r: WideEvent, piiFields: ReadonlySet<string> = new Se
2667
2714
  dimensions,
2668
2715
  input: masked.input === undefined ? null : masked.input,
2669
2716
  output: masked.output === undefined ? null : masked.output,
2717
+ http: r.http ? redactHttpFrame(r.http) : null,
2670
2718
  };
2671
2719
  }
2672
2720
 
@@ -3489,7 +3537,7 @@ function failStoreSqlStats(err: unknown) {
3489
3537
  return fail("PgStatStatementsUnsupported", { reason: err.message });
3490
3538
  }
3491
3539
  return fail("PgStatStatementsUnsupported", {
3492
- reason: err instanceof Error ? err.message : String(err),
3540
+ reason: engineErrorText(err),
3493
3541
  });
3494
3542
  }
3495
3543
 
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { userPrincipal, type UserPrincipal } from "../../auth/planes.ts";
10
10
  import { assembleInput } from "../../compiler/http-parse.ts";
11
+ import { requestForHttpInvoke } from "../../kernel/http-frame.ts";
11
12
  import type { ExecuteResult, OkeApp } from "../../kernel/app.ts";
12
13
  import { httpStatusForFailure } from "../../kernel/builtin-errors.ts";
13
14
  import { fail, OkeError } from "../../kernel/errors.ts";
@@ -357,9 +358,17 @@ export function bindHostInvokeUserFlow(app: InvokeHostApp): ConsoleInvokeUserFlo
357
358
  });
358
359
 
359
360
  const runId = okid();
361
+ const request =
362
+ trigger.kind === "http"
363
+ ? requestForHttpInvoke(trigger, {
364
+ ...(input.pathParams !== undefined ? { pathParams: input.pathParams } : {}),
365
+ input: assembled,
366
+ })
367
+ : undefined;
360
368
  const result = await app.execute(flowDef, assembled, trigger, {
361
369
  trustedInvoke: true,
362
370
  runId,
371
+ ...(request !== undefined ? { request } : {}),
363
372
  ...(input.bypassGates === true ? { bypassGates: true } : {}),
364
373
  ...(input.revealPii === true ? { revealPii: true } : {}),
365
374
  ...(input.rls ? { rls: input.rls } : {}),
@@ -45,6 +45,22 @@ const WideEventIngestSchema = z
45
45
  .optional(),
46
46
  input: z.unknown().optional(),
47
47
  output: z.unknown().optional(),
48
+ http: z
49
+ .object({
50
+ request: z.object({
51
+ method: z.string(),
52
+ path: z.string(),
53
+ query: z.record(z.string(), z.string()),
54
+ headers: z.record(z.string(), z.string()),
55
+ }),
56
+ response: z
57
+ .object({
58
+ status: z.number(),
59
+ headers: z.record(z.string(), z.string()),
60
+ })
61
+ .optional(),
62
+ })
63
+ .optional(),
48
64
  effects: z.array(z.unknown()),
49
65
  logs: z.array(z.unknown()),
50
66
  durationMs: z.number(),
@@ -42,6 +42,7 @@ export const CONSOLE_SPA_PATHS = [
42
42
  "/",
43
43
  "/overview",
44
44
  "/flows",
45
+ "/flows/decisions",
45
46
  "/store",
46
47
  "/vault",
47
48
  "/observability",
@@ -201,6 +202,10 @@ export async function serveConsole(
201
202
 
202
203
  const authed = withCookieAuth(request);
203
204
 
205
+ if (url.pathname.startsWith("/agent/") && handle.state.forwardAgent) {
206
+ return handle.state.forwardAgent(request);
207
+ }
208
+
204
209
  if (url.pathname.startsWith("/console/")) {
205
210
  const response = await runWithDevSurface("Console", () => handle.app.fetch(authed));
206
211
  const withCookies = await attachSessionCookies(authed, response);
@@ -349,6 +349,15 @@ export interface ConsoleState {
349
349
  vaultLayerSeed: VaultLayerSeed | null;
350
350
  /** Durable journal for rotation blast radius + Clock waiting-on. */
351
351
  journalStore: JournalStore | null;
352
+ /**
353
+ * Called after a tool approval is written. The seeded app resumes the
354
+ * parked run so the follow log can emit `RUN_FINISHED`.
355
+ */
356
+ afterAgentApproval?: () => Promise<void>;
357
+ /**
358
+ * Forwards `GET /agent/runs/:runId/events` to the app that owns the log.
359
+ */
360
+ forwardAgent?: (request: Request) => Promise<Response>;
352
361
  /** Host fleet registry (read-only). Null until bound or confirmed absent. */
353
362
  instanceStore: InstanceStore | null;
354
363
  /** Host `oke_crons` for fleet lease join (not Console's memory clock). */
@@ -525,7 +534,17 @@ export interface ConsoleLiveRun {
525
534
 
526
535
  /** Effect entry on a live run row. */
527
536
  export interface ConsoleLiveRunEffect {
528
- readonly kind: "read" | "write" | "emit" | "send" | "ask" | "embed" | "secret" | "call" | "fetch";
537
+ readonly kind:
538
+ | "read"
539
+ | "write"
540
+ | "emit"
541
+ | "send"
542
+ | "ask"
543
+ | "embed"
544
+ | "secret"
545
+ | "call"
546
+ | "fetch"
547
+ | "decide";
529
548
  readonly resource: string;
530
549
  readonly timestamp: number;
531
550
  readonly duration: number;
@@ -686,6 +705,11 @@ export function createConsoleState(options: CreateConsoleStateOptions = {}): Con
686
705
  seedAccessDemoData(state.apiKeys, state.invites, state.now);
687
706
  accessSeeded = true;
688
707
  }
708
+ try {
709
+ await state.apiKeys.reload?.();
710
+ } catch {
711
+ // Keep the in-memory keys when the table read fails.
712
+ }
689
713
  return access.projectAccessPanel({
690
714
  manifest: state.manifest,
691
715
  roles: state.roles,