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,461 @@
1
+ /**
2
+ * Coalesced agent event log: cap, truncation, and TTL.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { mkdtemp } from "node:fs/promises";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import {
10
+ AGENT_EVENT_SWEEP_CLAIM_MS,
11
+ AgentEventDuplicateSeqError,
12
+ createFileAgentEventStore,
13
+ createMemoryAgentEventStore,
14
+ createPostgresAgentEventStore,
15
+ type AgentEventStore,
16
+ } from "../../kernel/agent-event-store.ts";
17
+ import { connectPglite } from "../../drivers/pglite.ts";
18
+ import {
19
+ AGENT_EVENT_CAP,
20
+ AGENT_EVENT_TTL_MS,
21
+ AGENT_EVENTS_GAP,
22
+ createMemoryAgentEventLog,
23
+ setAgentEventLog,
24
+ sweepInstalledAgentEvents,
25
+ } from "./run-events.ts";
26
+
27
+ const header = {
28
+ runId: "r1",
29
+ threadId: "t1",
30
+ tenant: null,
31
+ gates: [] as string[],
32
+ userId: null,
33
+ operatorId: null,
34
+ };
35
+
36
+ describe("agent event log", () => {
37
+ test("the default cap is 5000 rows", () => {
38
+ expect(AGENT_EVENT_CAP).toBe(5_000);
39
+ });
40
+
41
+ test("coalesces text deltas and keeps structural events", async () => {
42
+ const log = createMemoryAgentEventLog();
43
+ await log.open({
44
+ runId: "r1",
45
+ threadId: "t1",
46
+ tenant: null,
47
+ gates: [],
48
+ userId: null,
49
+ operatorId: null,
50
+ });
51
+ await log.append("r1", { type: "TEXT_MESSAGE_START", messageId: "m-1", role: "assistant" }, 0);
52
+ await log.append("r1", { type: "TEXT_MESSAGE_CONTENT", messageId: "m-1", delta: "Hel" }, 0);
53
+ await log.append("r1", { type: "TEXT_MESSAGE_CONTENT", messageId: "m-1", delta: "lo" }, 0);
54
+ await log.append("r1", { type: "TEXT_MESSAGE_END", messageId: "m-1" }, 50);
55
+ const rows = await log.read("r1", 0);
56
+ const content = rows.find((row) => row.event.type === "TEXT_MESSAGE_CONTENT");
57
+ expect(content?.event).toMatchObject({ delta: "Hello" });
58
+ expect(rows.some((row) => row.event.type === "TEXT_MESSAGE_END")).toBe(true);
59
+ });
60
+
61
+ test("at the cap, deltas stop and structural events stay", async () => {
62
+ const log = createMemoryAgentEventLog(undefined, { cap: 3 });
63
+ await log.open({
64
+ runId: "r1",
65
+ threadId: "t1",
66
+ tenant: null,
67
+ gates: [],
68
+ userId: null,
69
+ operatorId: null,
70
+ });
71
+ for (let i = 0; i < 3; i++) {
72
+ await log.append("r1", { type: "STEP_STARTED", stepName: `step-${i}` }, i);
73
+ }
74
+ await log.append("r1", { type: "TEXT_MESSAGE_CONTENT", messageId: "m-1", delta: "x" }, 1_000);
75
+ await log.flush("r1", 1_000);
76
+ await log.append("r1", { type: "TEXT_MESSAGE_CONTENT", messageId: "m-1", delta: "y" }, 1_000);
77
+ await log.append("r1", { type: "STEP_STARTED", stepName: "after" }, 1_001);
78
+ await log.append(
79
+ "r1",
80
+ {
81
+ type: "RUN_FINISHED",
82
+ threadId: "t1",
83
+ runId: "r1",
84
+ outcome: {
85
+ type: "interrupt",
86
+ interrupts: [{ id: "ap-1", reason: "approval" }],
87
+ },
88
+ },
89
+ 1_002,
90
+ );
91
+ await log.append(
92
+ "r1",
93
+ {
94
+ type: "RUN_FINISHED",
95
+ threadId: "t1",
96
+ runId: "r1",
97
+ result: { cost: 0, stopReason: "completed", output: "ok" },
98
+ },
99
+ 1_003,
100
+ );
101
+ const rows = await log.read("r1", 0);
102
+ expect(rows.map((row) => row.seq)).toEqual([1, 2, 3, 4, 5, 6, 7]);
103
+ expect(rows.filter((row) => row.event.type === "CUSTOM")).toHaveLength(1);
104
+ expect(rows.some((row) => row.event.type === "TEXT_MESSAGE_CONTENT")).toBe(false);
105
+ expect(
106
+ rows.some((row) => row.event.type === "STEP_STARTED" && row.event.stepName === "after"),
107
+ ).toBe(true);
108
+ expect(rows.at(-2)?.event.type).toBe("RUN_FINISHED");
109
+ expect(rows.at(-1)?.event.type).toBe("RUN_FINISHED");
110
+ });
111
+
112
+ test("sweep deletes a finished run after the TTL", async () => {
113
+ const log = createMemoryAgentEventLog();
114
+ await log.open({
115
+ runId: "r1",
116
+ threadId: "t1",
117
+ tenant: null,
118
+ gates: [],
119
+ userId: null,
120
+ operatorId: null,
121
+ });
122
+ await log.append(
123
+ "r1",
124
+ {
125
+ type: "RUN_FINISHED",
126
+ threadId: "t1",
127
+ runId: "r1",
128
+ result: { cost: 0, stopReason: "completed" },
129
+ },
130
+ 10,
131
+ );
132
+ expect(await log.sweep(10 + AGENT_EVENT_TTL_MS - 1)).toBe(0);
133
+ expect(await log.header("r1")).toBeDefined();
134
+ expect(await log.sweep(10 + AGENT_EVENT_TTL_MS)).toBe(1);
135
+ expect(await log.header("r1")).toBeUndefined();
136
+ });
137
+
138
+ test("sweepInstalledAgentEvents runs the scheduler hook", async () => {
139
+ const log = createMemoryAgentEventLog();
140
+ setAgentEventLog(log);
141
+ await log.open({
142
+ runId: "r1",
143
+ threadId: "t1",
144
+ tenant: null,
145
+ gates: [],
146
+ userId: null,
147
+ operatorId: null,
148
+ });
149
+ await log.append("r1", { type: "RUN_ERROR", message: "nope", code: "Error" }, 10);
150
+ expect((await log.header("r1"))?.finishedAt).toBe(10);
151
+ expect(await sweepInstalledAgentEvents(10 + AGENT_EVENT_TTL_MS)).toBe(1);
152
+ setAgentEventLog(undefined);
153
+ });
154
+
155
+ test("100 aborted follows leave no listeners", async () => {
156
+ const log = createMemoryAgentEventLog();
157
+ await log.open({
158
+ runId: "r1",
159
+ threadId: "t1",
160
+ tenant: null,
161
+ gates: [],
162
+ userId: null,
163
+ operatorId: null,
164
+ });
165
+ const controllers = Array.from({ length: 100 }, () => new AbortController());
166
+ const iters = controllers.map((controller) =>
167
+ log.subscribe("r1", 0, controller.signal)[Symbol.asyncIterator](),
168
+ );
169
+ for (const controller of controllers) controller.abort();
170
+ await Promise.all(iters.map((iter) => iter.next()));
171
+ expect(log.listenerCount("r1")).toBe(0);
172
+ });
173
+
174
+ test("a restarted log resumes the next seq from the store", async () => {
175
+ const store = createMemoryAgentEventStore();
176
+ const first = createMemoryAgentEventLog(store);
177
+ await first.open({
178
+ runId: "r1",
179
+ threadId: "t1",
180
+ tenant: null,
181
+ gates: [],
182
+ userId: null,
183
+ operatorId: null,
184
+ });
185
+ await first.append("r1", { type: "RUN_STARTED", threadId: "t1", runId: "r1" }, 1);
186
+ const second = createMemoryAgentEventLog(store);
187
+ await second.open({
188
+ runId: "r1",
189
+ threadId: "t1",
190
+ tenant: null,
191
+ gates: [],
192
+ userId: null,
193
+ operatorId: null,
194
+ });
195
+ const seq = await second.append(
196
+ "r1",
197
+ {
198
+ type: "RUN_FINISHED",
199
+ threadId: "t1",
200
+ runId: "r1",
201
+ result: { cost: 0, stopReason: "completed" },
202
+ },
203
+ 2,
204
+ );
205
+ expect(seq).toBe(2);
206
+ expect((await second.read("r1", 0)).map((row) => row.seq)).toEqual([1, 2]);
207
+ });
208
+
209
+ test("sweep reads the store, and an abandoned run is closed then removed", async () => {
210
+ const store = createMemoryAgentEventStore();
211
+ const first = createMemoryAgentEventLog(store);
212
+ await first.open({
213
+ runId: "done",
214
+ threadId: "t1",
215
+ tenant: null,
216
+ gates: [],
217
+ userId: null,
218
+ operatorId: null,
219
+ });
220
+ await first.append(
221
+ "done",
222
+ {
223
+ type: "RUN_FINISHED",
224
+ threadId: "t1",
225
+ runId: "done",
226
+ result: { cost: 0, stopReason: "completed" },
227
+ },
228
+ 10,
229
+ );
230
+ await first.open({
231
+ runId: "stuck",
232
+ threadId: "t1",
233
+ tenant: null,
234
+ gates: [],
235
+ userId: null,
236
+ operatorId: null,
237
+ openedAt: 10,
238
+ });
239
+ const second = createMemoryAgentEventLog(store, { maxAgeMs: 50 });
240
+ expect(second.listenerCount("done")).toBe(0);
241
+ expect(await second.sweep(10 + AGENT_EVENT_TTL_MS, AGENT_EVENT_TTL_MS, 50)).toBe(2);
242
+ expect(await second.header("done")).toBeUndefined();
243
+ expect(await second.header("stuck")).toBeUndefined();
244
+ });
245
+
246
+ test("a duplicate seq is an error", async () => {
247
+ const store = createMemoryAgentEventStore();
248
+ await store.writeHeader(header);
249
+ await store.append("r1", { seq: 1, event: { type: "RUN_STARTED" } });
250
+ await expect(
251
+ store.append("r1", { seq: 1, event: { type: "RUN_STARTED" } }),
252
+ ).rejects.toBeInstanceOf(AgentEventDuplicateSeqError);
253
+ });
254
+
255
+ test("a file store appends JSONL under a write lock", async () => {
256
+ const dir = await mkdtemp(join(tmpdir(), "oke-events-"));
257
+ const store = createFileAgentEventStore(dir);
258
+ const log = createMemoryAgentEventLog(store);
259
+ await log.open(header);
260
+ await Promise.all([
261
+ log.append("r1", { type: "STEP_STARTED", stepName: "a" }, 1),
262
+ log.append("r1", { type: "STEP_STARTED", stepName: "b" }, 2),
263
+ ]);
264
+ const text = await Bun.file(join(dir, `${encodeURIComponent("r1")}.jsonl`)).text();
265
+ const lines = text.trim().split("\n");
266
+ expect(lines[0]).toContain('"kind":"header"');
267
+ expect(lines.filter((line) => line.includes('"kind":"row"'))).toHaveLength(2);
268
+ expect((await log.read("r1", 0)).map((row) => row.seq)).toEqual([1, 2]);
269
+ });
270
+
271
+ test("PGlite stores interleaved emits in call order", async () => {
272
+ const sql = await connectPglite({ url: "memory://" });
273
+ try {
274
+ const store = await createPostgresAgentEventStore(sql);
275
+ const log = createMemoryAgentEventLog(store);
276
+ await log.open(header);
277
+ const names: string[] = [];
278
+ const writes = [];
279
+ for (let i = 0; i < 20; i++) {
280
+ const stepName = i % 2 === 0 ? `a-${i}` : `b-${i}`;
281
+ names.push(stepName);
282
+ writes.push(log.append("r1", { type: "STEP_STARTED", stepName }, i));
283
+ }
284
+ await Promise.all(writes);
285
+ const rows = await log.read("r1", 0);
286
+ expect(
287
+ rows.map((row) => (row.event.type === "STEP_STARTED" ? row.event.stepName : "")),
288
+ ).toEqual(names);
289
+ expect(rows.map((row) => row.seq)).toEqual(names.map((_, index) => index + 1));
290
+ } finally {
291
+ await sql.close();
292
+ }
293
+ });
294
+
295
+ test("resume on another log continues seq without rewriting", async () => {
296
+ const sql = await connectPglite({ url: "memory://" });
297
+ try {
298
+ const store = await createPostgresAgentEventStore(sql);
299
+ const first = createMemoryAgentEventLog(store);
300
+ await first.open(header);
301
+ await first.append("r1", { type: "RUN_STARTED", threadId: "t1", runId: "r1" }, 1);
302
+ await first.append(
303
+ "r1",
304
+ {
305
+ type: "RUN_FINISHED",
306
+ threadId: "t1",
307
+ runId: "r1",
308
+ outcome: { type: "interrupt", interrupts: [{ id: "ap-1", reason: "approval" }] },
309
+ },
310
+ 2,
311
+ );
312
+ const second = createMemoryAgentEventLog(store);
313
+ await second.open(header);
314
+ await second.append("r1", { type: "STEP_STARTED", stepName: "after" }, 3);
315
+ await second.append(
316
+ "r1",
317
+ {
318
+ type: "RUN_FINISHED",
319
+ threadId: "t1",
320
+ runId: "r1",
321
+ result: { cost: 0, stopReason: "completed" },
322
+ },
323
+ 4,
324
+ );
325
+ const rows = await store.read("r1");
326
+ expect(rows?.rows.map((row) => row.seq)).toEqual([1, 2, 3, 4]);
327
+ expect(new Set(rows?.rows.map((row) => row.seq)).size).toBe(4);
328
+ } finally {
329
+ await sql.close();
330
+ }
331
+ });
332
+
333
+ test("park on A, resume on B, resume on A stores each event once", async () => {
334
+ const store = createMemoryAgentEventStore();
335
+ const a = createMemoryAgentEventLog(store);
336
+ const b = createMemoryAgentEventLog(store);
337
+ await a.open(header);
338
+ await a.append("r1", { type: "RUN_STARTED", threadId: "t1", runId: "r1" }, 1);
339
+ await a.append(
340
+ "r1",
341
+ {
342
+ type: "RUN_FINISHED",
343
+ threadId: "t1",
344
+ runId: "r1",
345
+ outcome: { type: "interrupt", interrupts: [{ id: "ap-1", reason: "approval" }] },
346
+ },
347
+ 2,
348
+ );
349
+ await b.open(header);
350
+ await b.append("r1", { type: "STEP_STARTED", stepName: "on-b" }, 3);
351
+ await a.open(header);
352
+ await a.append("r1", { type: "STEP_STARTED", stepName: "on-a" }, 4);
353
+ const rows = await store.read("r1");
354
+ expect(rows?.rows.map((row) => row.seq)).toEqual([1, 2, 3, 4]);
355
+ expect(new Set(rows?.rows.map((row) => row.seq)).size).toBe(4);
356
+ });
357
+
358
+ test("a new log keeps the truncation marker and drops later deltas", async () => {
359
+ const store = createMemoryAgentEventStore();
360
+ const first = createMemoryAgentEventLog(store, { cap: 2 });
361
+ await first.open(header);
362
+ await first.append("r1", { type: "STEP_STARTED", stepName: "a" }, 1);
363
+ await first.append("r1", { type: "STEP_STARTED", stepName: "b" }, 2);
364
+ await first.append("r1", { type: "TEXT_MESSAGE_CONTENT", messageId: "m", delta: "x" }, 1_000);
365
+ await first.flush("r1", 1_000);
366
+ const second = createMemoryAgentEventLog(store, { cap: 2 });
367
+ await second.open(header);
368
+ const seq = await second.append(
369
+ "r1",
370
+ { type: "TEXT_MESSAGE_CONTENT", messageId: "m", delta: "y" },
371
+ 2_000,
372
+ );
373
+ await second.flush("r1", 2_000);
374
+ expect(seq).toBeUndefined();
375
+ const rows = await second.read("r1", 0);
376
+ expect(rows.some((row) => row.event.type === "TEXT_MESSAGE_CONTENT")).toBe(false);
377
+ expect(rows.filter((row) => row.event.type === "CUSTOM")).toHaveLength(1);
378
+ });
379
+
380
+ test("two sweeps close an abandoned run once and a duplicate seq is not thrown", async () => {
381
+ const store = createMemoryAgentEventStore();
382
+ const opener = createMemoryAgentEventLog(store);
383
+ await opener.open({ ...header, openedAt: 1 });
384
+ const a = createMemoryAgentEventLog(store, { maxAgeMs: 10 });
385
+ const b = createMemoryAgentEventLog(store, { maxAgeMs: 10 });
386
+ const removed = await Promise.all([
387
+ a.sweep(100, AGENT_EVENT_TTL_MS, 10),
388
+ b.sweep(100, AGENT_EVENT_TTL_MS, 10),
389
+ ]);
390
+ expect(removed.reduce((sum, count) => sum + count, 0)).toBe(1);
391
+ expect(await store.read("r1")).toBeUndefined();
392
+ await expect(b.sweep(100, AGENT_EVENT_TTL_MS, 10)).resolves.toBe(0);
393
+ });
394
+
395
+ test("a failed store write retries once, then the next stored row is a gap", async () => {
396
+ const inner = createMemoryAgentEventStore();
397
+ let failures = 2;
398
+ const store: AgentEventStore = {
399
+ ...inner,
400
+ async append(runId, row) {
401
+ if (failures > 0) {
402
+ failures -= 1;
403
+ throw new Error("store write failed");
404
+ }
405
+ await inner.append(runId, row);
406
+ },
407
+ };
408
+ const log = createMemoryAgentEventLog(store);
409
+ await log.open(header);
410
+ expect(await log.append("r1", { type: "STEP_STARTED", stepName: "lost" }, 1)).toBeUndefined();
411
+ expect(await inner.maxSeq("r1")).toBe(0);
412
+ await log.append("r1", { type: "STEP_STARTED", stepName: "kept" }, 2);
413
+ const rows = await inner.read("r1");
414
+ expect(rows?.rows.map((row) => row.seq)).toEqual([1, 2]);
415
+ expect(rows?.rows[0]?.event).toMatchObject({ type: "CUSTOM", name: AGENT_EVENTS_GAP });
416
+ expect(rows?.rows[1]?.event).toMatchObject({ type: "STEP_STARTED", stepName: "kept" });
417
+ });
418
+
419
+ test("a sweep claim expires so another instance can reclaim the run", async () => {
420
+ const dir = await mkdtemp(join(tmpdir(), "oke-events-claim-"));
421
+ const sql = await connectPglite({ url: "memory://" });
422
+ const stores: AgentEventStore[] = [
423
+ createMemoryAgentEventStore(),
424
+ createFileAgentEventStore(dir),
425
+ await createPostgresAgentEventStore(sql),
426
+ ];
427
+ try {
428
+ for (const store of stores) {
429
+ await store.writeHeader({ ...header, openedAt: 1 });
430
+ expect(await store.claim("r1", 1_000)).toBe(true);
431
+ expect(await store.claim("r1", 1_000 + AGENT_EVENT_SWEEP_CLAIM_MS - 1)).toBe(false);
432
+ expect(await store.claim("r1", 1_000 + AGENT_EVENT_SWEEP_CLAIM_MS)).toBe(true);
433
+ await store.remove("r1");
434
+ }
435
+ } finally {
436
+ await sql.close();
437
+ }
438
+ });
439
+
440
+ test("truncation matches the stored event type, not a payload string", async () => {
441
+ const sql = await connectPglite({ url: "memory://" });
442
+ try {
443
+ const store = await createPostgresAgentEventStore(sql);
444
+ const log = createMemoryAgentEventLog(store);
445
+ await log.open(header);
446
+ await log.append(
447
+ "r1",
448
+ { type: "CUSTOM", name: "note", value: { text: "oke.events.truncated" } },
449
+ 1,
450
+ );
451
+ expect(await store.truncated("r1")).toBe(false);
452
+ const capped = createMemoryAgentEventLog(store, { cap: 1 });
453
+ await capped.open(header);
454
+ await capped.append("r1", { type: "TEXT_MESSAGE_CONTENT", messageId: "m", delta: "x" }, 2);
455
+ await capped.flush("r1", 2);
456
+ expect(await store.truncated("r1")).toBe(true);
457
+ } finally {
458
+ await sql.close();
459
+ }
460
+ });
461
+ });