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,369 @@
1
+ /**
2
+ * Observability AI pane — agent runs, the approval queue, and a live follow.
3
+ */
4
+
5
+ import { useCallback, useEffect, useState, type JSX } from "react";
6
+ import {
7
+ agentApprovalApprove,
8
+ agentApprovalDeny,
9
+ agentApprovalsList,
10
+ agentRunsList,
11
+ type AgentApprovalRow,
12
+ type AgentRunRow,
13
+ } from "@/client.ts";
14
+ import {
15
+ EXPLORER_COUNT_CLASS,
16
+ EXPLORER_ROW_CLASS,
17
+ EXPLORER_STRIP_CLASS,
18
+ SECTION_HEAD_CLASS,
19
+ } from "@/components/explorer/explorer-chrome.ts";
20
+
21
+ /** One SSE frame from `GET /agent/runs/:runId/events`. */
22
+ export interface AgentFollowFrame {
23
+ readonly id?: string;
24
+ readonly data: string;
25
+ }
26
+
27
+ /**
28
+ * Split an SSE buffer into frames. The remainder stays for the next chunk.
29
+ *
30
+ * @param buffer - Text received so far
31
+ */
32
+ export function parseAgentEventFrames(buffer: string): {
33
+ readonly frames: readonly AgentFollowFrame[];
34
+ readonly rest: string;
35
+ } {
36
+ const frames: AgentFollowFrame[] = [];
37
+ let rest = buffer;
38
+ for (;;) {
39
+ const split = rest.indexOf("\n\n");
40
+ if (split < 0) break;
41
+ const block = rest.slice(0, split);
42
+ rest = rest.slice(split + 2);
43
+ let id: string | undefined;
44
+ const data: string[] = [];
45
+ for (const line of block.split("\n")) {
46
+ if (line.startsWith("id:")) id = line.slice(3).trim();
47
+ else if (line.startsWith("data:")) data.push(line.slice(5).trim());
48
+ }
49
+ if (data.length === 0) continue;
50
+ frames.push({ ...(id !== undefined ? { id } : {}), data: data.join("\n") });
51
+ }
52
+ return { frames, rest };
53
+ }
54
+
55
+ /**
56
+ * Edited args. Empty means "leave the original". Any other text must be JSON.
57
+ *
58
+ * @param text - Textarea value
59
+ */
60
+ export function parseApprovalArgs(
61
+ text: string,
62
+ ): { readonly ok: true; readonly value?: unknown } | { readonly ok: false } {
63
+ const trimmed = text.trim();
64
+ if (trimmed.length === 0) return { ok: true };
65
+ try {
66
+ return { ok: true, value: JSON.parse(trimmed) as unknown };
67
+ } catch {
68
+ return { ok: false };
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Format a pending age in seconds or minutes.
74
+ *
75
+ * @param ageMs - Milliseconds since the approval was requested
76
+ */
77
+ function formatAge(ageMs: number): string {
78
+ const seconds = Math.max(0, Math.round(ageMs / 1000));
79
+ if (seconds < 60) return `${seconds}s`;
80
+ return `${Math.floor(seconds / 60)}m`;
81
+ }
82
+
83
+ function tokenLabel(run: AgentRunRow): string {
84
+ if (run.inputTokens === undefined && run.outputTokens === undefined) return "";
85
+ return `${run.inputTokens ?? 0}/${run.outputTokens ?? 0}`;
86
+ }
87
+
88
+ /**
89
+ * Agent runs, run detail, follow, and the approvals queue.
90
+ *
91
+ * @param props - Selected run from the URL
92
+ */
93
+ export function AiRunsPanel({
94
+ agentRunId,
95
+ query,
96
+ onSelectRun,
97
+ }: {
98
+ readonly agentRunId: string | null;
99
+ readonly query: string;
100
+ readonly onSelectRun: (id: string | null) => void;
101
+ }): JSX.Element {
102
+ const [runs, setRuns] = useState<readonly AgentRunRow[]>([]);
103
+ const [rows, setRows] = useState<readonly AgentApprovalRow[]>([]);
104
+ const [notice, setNotice] = useState<string | null>(null);
105
+ const [argsText, setArgsText] = useState<Record<string, string>>({});
106
+ const [reasons, setReasons] = useState<Record<string, string>>({});
107
+ const [follow, setFollow] = useState<readonly string[]>([]);
108
+ const [following, setFollowing] = useState(false);
109
+
110
+ const reload = useCallback(async () => {
111
+ const list = await agentRunsList();
112
+ if (list.data) setRuns(list.data.runs);
113
+ const queue = await agentApprovalsList();
114
+ if (queue.data) setRows(queue.data.rows);
115
+ }, []);
116
+
117
+ useEffect(() => {
118
+ void reload();
119
+ }, [reload]);
120
+
121
+ const selected = runs.find((run) => run.id === agentRunId) ?? null;
122
+
123
+ useEffect(() => {
124
+ if (!following || !agentRunId) return;
125
+ const controller = new AbortController();
126
+ let lastId = "0";
127
+ let cancelled = false;
128
+ const run = async (): Promise<void> => {
129
+ const res = await fetch(`/agent/runs/${agentRunId}/events`, {
130
+ headers: { "last-event-id": lastId, accept: "text/event-stream" },
131
+ signal: controller.signal,
132
+ });
133
+ if (!res.ok || !res.body) {
134
+ setNotice(String(res.status));
135
+ return;
136
+ }
137
+ const reader = res.body.getReader();
138
+ const decoder = new TextDecoder();
139
+ let buffer = "";
140
+ for (;;) {
141
+ const chunk = await reader.read();
142
+ if (chunk.done || cancelled) return;
143
+ buffer += decoder.decode(chunk.value, { stream: true });
144
+ const parsed = parseAgentEventFrames(buffer);
145
+ buffer = parsed.rest;
146
+ for (const frame of parsed.frames) {
147
+ if (frame.id) lastId = frame.id;
148
+ setFollow((current) => [...current, frame.data]);
149
+ }
150
+ }
151
+ };
152
+ void run().catch((err: unknown) => {
153
+ if (!cancelled) setNotice(err instanceof Error ? err.message : String(err));
154
+ });
155
+ return () => {
156
+ cancelled = true;
157
+ controller.abort();
158
+ };
159
+ }, [following, agentRunId]);
160
+
161
+ async function approve(row: AgentApprovalRow): Promise<void> {
162
+ const parsed = parseApprovalArgs(argsText[row.id] ?? "");
163
+ if (!parsed.ok) {
164
+ setNotice("ValidationError");
165
+ return;
166
+ }
167
+ const result = await agentApprovalApprove({
168
+ id: row.id,
169
+ ...(parsed.value !== undefined ? { args: parsed.value } : {}),
170
+ });
171
+ if (result.error?.code === "Conflict") setNotice("Conflict");
172
+ else if (result.error?.code === "JournalLeaseBusy") setNotice("JournalLeaseBusy");
173
+ else if (result.error) setNotice(result.error.code);
174
+ else setNotice(null);
175
+ await reload();
176
+ }
177
+
178
+ async function deny(row: AgentApprovalRow): Promise<void> {
179
+ const result = await agentApprovalDeny({
180
+ id: row.id,
181
+ ...(reasons[row.id] ? { reason: reasons[row.id] } : {}),
182
+ });
183
+ if (result.error?.code === "Conflict") setNotice("Conflict");
184
+ else if (result.error?.code === "JournalLeaseBusy") setNotice("JournalLeaseBusy");
185
+ else if (result.error) setNotice(result.error.code);
186
+ else setNotice(null);
187
+ await reload();
188
+ }
189
+
190
+ return (
191
+ <div className="flex min-h-0 flex-1 flex-col overflow-auto" data-slot="observability-ai-runs">
192
+ <div className={EXPLORER_STRIP_CLASS}>
193
+ <span className={`${SECTION_HEAD_CLASS} px-2.5`}>Agent runs</span>
194
+ <span className={EXPLORER_COUNT_CLASS}>{runs.length}</span>
195
+ </div>
196
+ <ul>
197
+ {runs
198
+ .filter(
199
+ (run) => query.length === 0 || run.agent.includes(query) || run.id.includes(query),
200
+ )
201
+ .map((run) => (
202
+ <li key={run.id}>
203
+ <button
204
+ type="button"
205
+ className={EXPLORER_ROW_CLASS}
206
+ data-slot="agent-run-row"
207
+ data-status={run.status}
208
+ data-selected={run.id === agentRunId ? "true" : "false"}
209
+ onClick={() => onSelectRun(run.id === agentRunId ? null : run.id)}
210
+ >
211
+ <span className="min-w-0 flex-1 truncate">{run.agent}</span>
212
+ <span className={EXPLORER_COUNT_CLASS}>{run.status}</span>
213
+ <span className={EXPLORER_COUNT_CLASS} data-slot="agent-run-stop">
214
+ {run.stopReason ?? "—"}
215
+ {run.error ? ` · ${run.error}` : ""}
216
+ </span>
217
+ <span className={EXPLORER_COUNT_CLASS}>{run.steps}</span>
218
+ <span className={EXPLORER_COUNT_CLASS}>{run.cost}</span>
219
+ <span className={EXPLORER_COUNT_CLASS}>{tokenLabel(run)}</span>
220
+ <span className="truncate font-mono text-[10px] text-muted-foreground">
221
+ {run.threadId ?? ""}
222
+ </span>
223
+ </button>
224
+ </li>
225
+ ))}
226
+ </ul>
227
+ {selected ? (
228
+ <div data-slot="agent-run-detail">
229
+ <div className={EXPLORER_STRIP_CLASS}>
230
+ <span className={`${SECTION_HEAD_CLASS} px-2.5`}>{selected.agent}</span>
231
+ <button
232
+ type="button"
233
+ className={`${SECTION_HEAD_CLASS} px-2.5`}
234
+ data-slot="agent-run-follow"
235
+ onClick={() => {
236
+ setFollow([]);
237
+ setFollowing(true);
238
+ }}
239
+ >
240
+ Follow
241
+ </button>
242
+ <a href="#agent-approvals" className="px-2.5 text-xs underline">
243
+ Approvals
244
+ </a>
245
+ </div>
246
+ <ul>
247
+ {selected.trail.map((step, index) => (
248
+ <li
249
+ key={`${step.tool}:${index}`}
250
+ className={EXPLORER_ROW_CLASS}
251
+ data-slot="agent-trail-step"
252
+ >
253
+ <span className="min-w-0 flex-1 truncate">{step.tool}</span>
254
+ <span className={EXPLORER_COUNT_CLASS}>{step.status}</span>
255
+ {step.approver ? (
256
+ <span className={EXPLORER_COUNT_CLASS} data-slot="agent-trail-approver">
257
+ {step.approver}
258
+ </span>
259
+ ) : null}
260
+ {step.denial ? (
261
+ <span className={EXPLORER_COUNT_CLASS}>{step.denial.reason}</span>
262
+ ) : null}
263
+ </li>
264
+ ))}
265
+ </ul>
266
+ {selected.repairs.length > 0 ? (
267
+ <ul>
268
+ {selected.repairs.map((repair) => (
269
+ <li
270
+ key={`${repair.prompt}:${repair.at}`}
271
+ className={EXPLORER_ROW_CLASS}
272
+ data-slot="agent-repair"
273
+ >
274
+ <span className="min-w-0 flex-1 truncate">{repair.prompt}</span>
275
+ <span className={EXPLORER_COUNT_CLASS}>{repair.attempts}</span>
276
+ </li>
277
+ ))}
278
+ </ul>
279
+ ) : null}
280
+ {selected.children.length > 0 ? (
281
+ <ul data-slot="agent-subagents">
282
+ {selected.children.map((child) => (
283
+ <li key={child.id}>
284
+ <button
285
+ type="button"
286
+ className={EXPLORER_ROW_CLASS}
287
+ data-parent={child.parentRunId ?? ""}
288
+ onClick={() => onSelectRun(child.id)}
289
+ >
290
+ <span className="min-w-0 flex-1 truncate">{child.agent}</span>
291
+ <span className={EXPLORER_COUNT_CLASS}>{child.status}</span>
292
+ </button>
293
+ </li>
294
+ ))}
295
+ </ul>
296
+ ) : null}
297
+ {following ? (
298
+ <ul data-slot="agent-follow">
299
+ {follow.map((line, index) => (
300
+ <li key={index} className={EXPLORER_ROW_CLASS} data-slot="agent-follow-event">
301
+ <span className="min-w-0 flex-1 truncate font-mono text-[10px]">{line}</span>
302
+ </li>
303
+ ))}
304
+ </ul>
305
+ ) : null}
306
+ </div>
307
+ ) : null}
308
+ <div className={EXPLORER_STRIP_CLASS} id="agent-approvals">
309
+ <span className={`${SECTION_HEAD_CLASS} px-2.5`}>Approvals</span>
310
+ </div>
311
+ {notice ? (
312
+ <p className="px-2.5 py-1 text-xs" data-slot="approval-notice">
313
+ {notice}
314
+ </p>
315
+ ) : null}
316
+ <ul>
317
+ {rows.map((row) => (
318
+ <li key={row.id} className={EXPLORER_ROW_CLASS} data-slot="approval-row">
319
+ <span className="min-w-0 flex-1 truncate">
320
+ {row.agent} · {row.tool}
321
+ </span>
322
+ <span className={EXPLORER_COUNT_CLASS} data-slot="approval-tenant">
323
+ {row.tenant ?? "—"}
324
+ </span>
325
+ <span className={EXPLORER_COUNT_CLASS} data-slot="approval-age">
326
+ {formatAge(row.ageMs)}
327
+ </span>
328
+ <span className={EXPLORER_COUNT_CLASS}>{row.gate}</span>
329
+ <form
330
+ className="flex items-center gap-2"
331
+ onSubmit={(event) => {
332
+ event.preventDefault();
333
+ void approve(row);
334
+ }}
335
+ >
336
+ <input
337
+ aria-label="Edited args"
338
+ value={argsText[row.id] ?? ""}
339
+ placeholder={JSON.stringify(row.args)}
340
+ onChange={(event) => {
341
+ const next = event.target.value;
342
+ setArgsText((current) => ({ ...current, [row.id]: next }));
343
+ }}
344
+ />
345
+ <button type="submit">Approve</button>
346
+ </form>
347
+ <form
348
+ className="flex items-center gap-2"
349
+ onSubmit={(event) => {
350
+ event.preventDefault();
351
+ void deny(row);
352
+ }}
353
+ >
354
+ <input
355
+ aria-label="Deny reason"
356
+ value={reasons[row.id] ?? ""}
357
+ onChange={(event) => {
358
+ const next = event.target.value;
359
+ setReasons((current) => ({ ...current, [row.id]: next }));
360
+ }}
361
+ />
362
+ <button type="submit">Deny</button>
363
+ </form>
364
+ </li>
365
+ ))}
366
+ </ul>
367
+ </div>
368
+ );
369
+ }
@@ -26,6 +26,7 @@ import { useClockList } from "./data/use-clock-list.ts";
26
26
  import { useInstancesList } from "./data/use-instances-list.ts";
27
27
  import { useSignalsList } from "./data/use-signals-list.ts";
28
28
  import { AiRail } from "./detail/ai-rail.tsx";
29
+ import { AiRunsPanel } from "./detail/ai-runs.tsx";
29
30
  import { InstanceFleetSheet } from "./detail/instance-fleet-sheet.tsx";
30
31
  import { MetricsPanel } from "./detail/metrics-panel.tsx";
31
32
  import { RunsQueryPanel } from "./query/runs-query-panel.tsx";
@@ -57,11 +58,13 @@ export function ObservabilityPage(): JSX.Element {
57
58
  selectedErrorKey,
58
59
  query,
59
60
  view,
61
+ agentRunId,
60
62
  setSelectedRun,
61
63
  setWindow,
62
64
  setSelectedError,
63
65
  setQuery,
64
66
  setView,
67
+ setAgentRun,
65
68
  } = useObservabilitySelection();
66
69
  const [fleetOpen, setFleetOpen] = useState(false);
67
70
  const start = useExplorerStartPanel();
@@ -122,6 +125,10 @@ export function ObservabilityPage(): JSX.Element {
122
125
  () => (selectedRunId ? (buffer.find((run) => run.id === selectedRunId) ?? null) : null),
123
126
  [buffer, selectedRunId],
124
127
  );
128
+ const navigationRuns = useMemo(
129
+ () => [...buffer].sort((a, b) => b.startedAt - a.startedAt || a.id.localeCompare(b.id)),
130
+ [buffer],
131
+ );
125
132
 
126
133
  return (
127
134
  <div className={EXPLORER_PAGE_CLASS} data-slot="observability-page">
@@ -198,9 +205,27 @@ export function ObservabilityPage(): JSX.Element {
198
205
  >
199
206
  SQL
200
207
  </button>
208
+ <button
209
+ type="button"
210
+ role="tab"
211
+ aria-selected={view === "ai"}
212
+ className={cn(
213
+ EXPLORER_STRIP_TOKEN_CLASS,
214
+ "font-semibold tracking-[0.08em] uppercase",
215
+ view === "ai"
216
+ ? EXPLORER_STRIP_TOKEN_ACTIVE_CLASS
217
+ : EXPLORER_STRIP_TOKEN_IDLE_CLASS,
218
+ )}
219
+ onClick={() => setView("ai")}
220
+ data-slot="observability-view-ai"
221
+ >
222
+ AI
223
+ </button>
201
224
  </div>
202
225
  {view === "query" ? (
203
226
  <RunsQueryPanel />
227
+ ) : view === "ai" ? (
228
+ <AiRunsPanel agentRunId={agentRunId} query={query} onSelectRun={setAgentRun} />
204
229
  ) : (
205
230
  <>
206
231
  <AiRail ai={ai.data} asks={asks} tokens={tokens} />
@@ -210,7 +235,12 @@ export function ObservabilityPage(): JSX.Element {
210
235
  </div>
211
236
  </ResizablePanel>
212
237
  </ResizablePanelGroup>
213
- <TraceDetailSheet run={selectedRun} onClose={() => setSelectedRun(null)} />
238
+ <TraceDetailSheet
239
+ run={selectedRun}
240
+ onClose={() => setSelectedRun(null)}
241
+ navigationRuns={navigationRuns}
242
+ onSelectRun={setSelectedRun}
243
+ />
214
244
  <InstanceFleetSheet fleet={fleet.data} open={fleetOpen} onClose={() => setFleetOpen(false)} />
215
245
  </div>
216
246
  );
@@ -11,7 +11,7 @@ import {
11
11
  } from "../lib/window-stats.ts";
12
12
 
13
13
  /** Right-pane mode on `/observability`. */
14
- export type ObservabilityView = "metrics" | "query";
14
+ export type ObservabilityView = "metrics" | "query" | "ai";
15
15
 
16
16
  /** Search params for `/observability`. */
17
17
  export interface ObservabilitySearch {
@@ -20,6 +20,7 @@ export interface ObservabilitySearch {
20
20
  readonly error?: string;
21
21
  readonly q?: string;
22
22
  readonly view?: ObservabilityView;
23
+ readonly agentRun?: string;
23
24
  }
24
25
 
25
26
  /**
@@ -33,13 +34,16 @@ export function validateObservabilitySearch(search: Record<string, unknown>): Ob
33
34
  const error =
34
35
  typeof search.error === "string" && search.error.length > 0 ? search.error : undefined;
35
36
  const q = typeof search.q === "string" && search.q.length > 0 ? search.q : undefined;
36
- const view = search.view === "query" ? "query" : undefined;
37
+ const view = search.view === "query" || search.view === "ai" ? search.view : undefined;
38
+ const agentRun =
39
+ typeof search.agentRun === "string" && search.agentRun.length > 0 ? search.agentRun : undefined;
37
40
  return {
38
41
  ...(run !== undefined ? { run } : {}),
39
42
  ...(window !== DEFAULT_OBSERVABILITY_WINDOW ? { window } : {}),
40
43
  ...(error !== undefined ? { error } : {}),
41
44
  ...(q !== undefined ? { q } : {}),
42
45
  ...(view !== undefined ? { view } : {}),
46
+ ...(agentRun !== undefined ? { agentRun } : {}),
43
47
  };
44
48
  }
45
49
 
@@ -54,7 +58,9 @@ export function useObservabilitySelection() {
54
58
  const window = parseObservabilityWindow(search.window);
55
59
  const selectedErrorKey = typeof search.error === "string" ? search.error : null;
56
60
  const query = typeof search.q === "string" ? search.q : "";
57
- const view: ObservabilityView = search.view === "query" ? "query" : "metrics";
61
+ const view: ObservabilityView =
62
+ search.view === "query" || search.view === "ai" ? search.view : "metrics";
63
+ const agentRunId = typeof search.agentRun === "string" ? search.agentRun : null;
58
64
 
59
65
  const patch = useCallback(
60
66
  (next: Partial<ObservabilitySearch>) => {
@@ -106,16 +112,25 @@ export function useObservabilitySelection() {
106
112
  [patch],
107
113
  );
108
114
 
115
+ const setAgentRun = useCallback(
116
+ (run: string | null) => {
117
+ patch({ agentRun: run ?? undefined, view: "ai" });
118
+ },
119
+ [patch],
120
+ );
121
+
109
122
  return {
110
123
  selectedRunId,
111
124
  window,
112
125
  selectedErrorKey,
113
126
  query,
114
127
  view,
128
+ agentRunId,
115
129
  setSelectedRun,
116
130
  setWindow,
117
131
  setSelectedError,
118
132
  setQuery,
119
133
  setView,
134
+ setAgentRun,
120
135
  };
121
136
  }
@@ -214,7 +214,18 @@ function formatPct(value: number | null): string {
214
214
 
215
215
  function errorMessage(error: unknown): string | null {
216
216
  if (!error) return null;
217
- return error instanceof Error ? error.message : String(error);
217
+ const text =
218
+ error instanceof Error
219
+ ? error.message
220
+ : typeof error === "object" && error !== null && "message" in error
221
+ ? typeof (error as { message?: unknown }).message === "string"
222
+ ? (error as { message: string }).message
223
+ : null
224
+ : typeof error === "string"
225
+ ? error
226
+ : null;
227
+ if (!text || text.trim().length === 0 || text === "[object Object]") return null;
228
+ return text;
218
229
  }
219
230
 
220
231
  function errorCode(error: unknown): string | null {
@@ -433,7 +433,20 @@ function fmtCost(value: unknown): string {
433
433
 
434
434
  function errorMessage(error: unknown): string | null {
435
435
  if (!error) return null;
436
- return error instanceof Error ? error.message : String(error);
436
+ const text =
437
+ error instanceof Error
438
+ ? error.message
439
+ : typeof error === "object" && error !== null && "message" in error
440
+ ? stringField(error.message)
441
+ : typeof error === "string"
442
+ ? error
443
+ : null;
444
+ if (!text || text.trim().length === 0 || text === "[object Object]") return null;
445
+ return text;
446
+ }
447
+
448
+ function stringField(value: unknown): string | null {
449
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
437
450
  }
438
451
 
439
452
  function errorCode(error: unknown): string | null {
@@ -955,7 +955,7 @@ function ResponseStatus({
955
955
  }
956
956
 
957
957
  /**
958
- * Flush invoke dock — toolbar + scrollable body (Units workbench).
958
+ * Flush invoke rail — toolbar + scrollable body (Units workbench).
959
959
  */
960
960
  function CallDock({
961
961
  title,
@@ -48,18 +48,21 @@ export interface FlowContractPanelProps {
48
48
  readonly runs?: readonly RunRow[];
49
49
  /** Start-panel collapse control for the Flows catalog. */
50
50
  readonly leading?: ReactNode;
51
+ /** End-rail collapse control for Call API. */
52
+ readonly trailing?: ReactNode;
51
53
  }
52
54
 
53
55
  /**
54
- * Top inspector pane for a selected flow.
56
+ * Contract pane for a selected flow — Call API sits in the rail beside it.
55
57
  *
56
- * @param props - Flow row + Manifest + optional runs buffer + tree toggle
58
+ * @param props - Flow row + Manifest + optional runs buffer + panel toggles
57
59
  */
58
60
  export function FlowContractPanel({
59
61
  row,
60
62
  manifest,
61
63
  runs,
62
64
  leading,
65
+ trailing,
63
66
  }: FlowContractPanelProps): JSX.Element {
64
67
  const kind = flowTriggerKind(row.flow.trigger);
65
68
  const trigger = flowTriggerSpec(row.flow.trigger);
@@ -95,6 +98,7 @@ export function FlowContractPanel({
95
98
  <DetailHeader
96
99
  dataSlot="endpoint-header"
97
100
  leading={leading}
101
+ trailing={trailing}
98
102
  icon={
99
103
  <span data-slot="trigger-kind-icon" data-kind={kind}>
100
104
  <HugeiconsIcon icon={trigger.icon} className="size-4" />