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,441 @@
1
+ /**
2
+ * Decision labels and the drift flag on the journal driver.
3
+ *
4
+ * A sibling of `oke_idempotency`. Rows are not journal runs. Tables are
5
+ * created only when this store is opened, which boot does when the app
6
+ * declares decisions.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import { mkdir } from "node:fs/promises";
12
+ import { dirname } from "node:path";
13
+ import type { DecisionCandidate, DecisionLabel } from "../elements/ai/decisions/certificate.ts";
14
+
15
+ /** SQL surface the postgres journal client already exposes. */
16
+ export interface DecisionLabelSql {
17
+ query(sql: string, params?: readonly unknown[]): Promise<Record<string, unknown>[]>;
18
+ exec(sql: string, params?: readonly unknown[]): Promise<{ changes: number }>;
19
+ }
20
+
21
+ /** Drift for one decision. A later certificate (`certifiedAt` newer) ignores it. */
22
+ export interface DecisionDriftFlag {
23
+ readonly suspended: boolean;
24
+ /** Epoch ms of the certificate that was current when the flag was written. */
25
+ readonly certifiedAt: number;
26
+ }
27
+
28
+ /** @deprecated Use {@link DecisionDriftFlag}. Kept for stored rows. */
29
+ export interface DecisionDriftRecord extends DecisionDriftFlag {}
30
+
31
+ /** Thrown when the postgres label store cannot open or query. */
32
+ export class DecisionLabelStoreError extends Error {
33
+ /**
34
+ * @param message - What failed
35
+ * @param options - Underlying driver error
36
+ */
37
+ constructor(message: string, options?: { cause?: unknown }) {
38
+ super(message, options);
39
+ this.name = "DecisionLabelStoreError";
40
+ }
41
+ }
42
+
43
+ /** Persistence for review labels and the app drift flag. */
44
+ export interface DecisionLabelStore {
45
+ insert(label: DecisionLabel, at: number): Promise<void>;
46
+ /**
47
+ * Labels for one decision. Omit `tenant` to read every tenant.
48
+ *
49
+ * @param decision - Decision name
50
+ * @param tenant - Tenant filter
51
+ */
52
+ list(decision?: string, tenant?: string | null): Promise<DecisionLabel[]>;
53
+ /** Drift flags keyed by decision name. */
54
+ drift(): Promise<Readonly<Record<string, DecisionDriftFlag>>>;
55
+ /**
56
+ * Persist one decision's suspension flag.
57
+ *
58
+ * @param decision - Decision name
59
+ * @param record - Flag and the certificate time it belongs to
60
+ */
61
+ setDrift(decision: string, record: DecisionDriftFlag): Promise<void>;
62
+ /**
63
+ * Store one candidate on this journal. Any instance can read it after a restart.
64
+ *
65
+ * @param decision - Decision name
66
+ * @param entry - Lock entry the clock fitted
67
+ */
68
+ putCandidate(decision: string, entry: DecisionCandidate): Promise<void>;
69
+ /**
70
+ * Candidate last written for this decision.
71
+ *
72
+ * @param decision - Decision name
73
+ */
74
+ getCandidate(decision: string): Promise<DecisionCandidate | undefined>;
75
+ /** Decision names that have a candidate. */
76
+ listCandidates(): Promise<readonly string[]>;
77
+ }
78
+
79
+ interface StoredLabel extends DecisionLabel {
80
+ readonly at: number;
81
+ }
82
+
83
+ /**
84
+ * In-memory label store.
85
+ */
86
+ export function createMemoryDecisionLabelStore(): DecisionLabelStore {
87
+ const rows: StoredLabel[] = [];
88
+ const candidates = new Map<string, DecisionCandidate>();
89
+ const driftFlags = new Map<string, DecisionDriftFlag>();
90
+ return {
91
+ async insert(label, at) {
92
+ rows.push({ ...label, at });
93
+ },
94
+ async list(decision, tenant) {
95
+ return rows
96
+ .filter((row) => decision === undefined || row.decision === decision)
97
+ .filter((row) => tenant === undefined || (row.tenant ?? null) === tenant)
98
+ .map(toLabel);
99
+ },
100
+ async drift() {
101
+ return Object.fromEntries(driftFlags);
102
+ },
103
+ async setDrift(decision, next) {
104
+ if (next.suspended) driftFlags.set(decision, next);
105
+ else driftFlags.delete(decision);
106
+ },
107
+ async putCandidate(decision, entry) {
108
+ candidates.set(decision, entry);
109
+ },
110
+ async getCandidate(decision) {
111
+ return candidates.get(decision);
112
+ },
113
+ async listCandidates() {
114
+ return [...candidates.keys()];
115
+ },
116
+ };
117
+ }
118
+
119
+ /**
120
+ * File-backed label store beside the journal file.
121
+ *
122
+ * @param path - JSON file path
123
+ */
124
+ export function createFileDecisionLabelStore(path: string): DecisionLabelStore {
125
+ let rows: StoredLabel[] | undefined;
126
+ let candidates: Record<string, DecisionCandidate> = {};
127
+ let driftFlags: Record<string, DecisionDriftFlag> = {};
128
+ const load = async (): Promise<void> => {
129
+ if (rows) return;
130
+ rows = [];
131
+ const file = Bun.file(path);
132
+ if (await file.exists()) {
133
+ const raw = (await file.json()) as {
134
+ rows?: StoredLabel[];
135
+ suspended?: boolean;
136
+ certifiedAt?: number;
137
+ drifts?: Record<string, DecisionDriftFlag>;
138
+ candidates?: Record<string, DecisionCandidate>;
139
+ };
140
+ rows = raw.rows ?? [];
141
+ candidates = raw.candidates ?? {};
142
+ driftFlags = raw.drifts ?? {};
143
+ const legacy =
144
+ driftFlags["*"] ??
145
+ (raw.suspended === true && Object.keys(driftFlags).length === 0
146
+ ? { suspended: true as const, certifiedAt: raw.certifiedAt ?? 0 }
147
+ : undefined);
148
+ if (legacy?.suspended) {
149
+ delete driftFlags["*"];
150
+ for (const name of declaredDriftDecisions) {
151
+ driftFlags[name] = { suspended: legacy.suspended, certifiedAt: legacy.certifiedAt };
152
+ }
153
+ await mkdir(dirname(path), { recursive: true });
154
+ await Bun.write(path, JSON.stringify({ rows, candidates, drifts: driftFlags }));
155
+ }
156
+ }
157
+ };
158
+ const flush = async (): Promise<void> => {
159
+ await mkdir(dirname(path), { recursive: true });
160
+ await Bun.write(
161
+ path,
162
+ JSON.stringify({
163
+ rows,
164
+ candidates,
165
+ drifts: driftFlags,
166
+ }),
167
+ );
168
+ };
169
+ return {
170
+ async insert(label, at) {
171
+ await load();
172
+ rows!.push({ ...label, at });
173
+ await flush();
174
+ },
175
+ async list(decision, tenant) {
176
+ await load();
177
+ return rows!
178
+ .filter((row) => decision === undefined || row.decision === decision)
179
+ .filter((row) => tenant === undefined || (row.tenant ?? null) === tenant)
180
+ .map(toLabel);
181
+ },
182
+ async drift() {
183
+ await load();
184
+ return driftFlags;
185
+ },
186
+ async setDrift(decision, next) {
187
+ await load();
188
+ if (next.suspended) driftFlags[decision] = next;
189
+ else delete driftFlags[decision];
190
+ await flush();
191
+ },
192
+ async putCandidate(decision, entry) {
193
+ await load();
194
+ candidates[decision] = entry;
195
+ await flush();
196
+ },
197
+ async getCandidate(decision) {
198
+ await load();
199
+ return candidates[decision];
200
+ },
201
+ async listCandidates() {
202
+ await load();
203
+ return Object.keys(candidates);
204
+ },
205
+ };
206
+ }
207
+
208
+ /** Decision names an old app-wide drift flag is copied onto. Boot sets this before open. */
209
+ let declaredDriftDecisions: readonly string[] = [];
210
+
211
+ /**
212
+ * Declare which decisions receive a legacy app-wide drift flag.
213
+ *
214
+ * @param names - Decision names from the manifest
215
+ */
216
+ export function setDeclaredDriftDecisions(names: readonly string[]): void {
217
+ declaredDriftDecisions = names;
218
+ }
219
+
220
+ /**
221
+ * Postgres label store on the journal connection. Creates the tables.
222
+ * A failed init or query throws {@link DecisionLabelStoreError}.
223
+ *
224
+ * @param sql - Journal SQL client
225
+ */
226
+ export async function createPostgresDecisionLabelStore(
227
+ sql: DecisionLabelSql,
228
+ ): Promise<DecisionLabelStore> {
229
+ try {
230
+ await sql.exec(`CREATE TABLE IF NOT EXISTS oke_decision_labels (
231
+ decision_id TEXT NOT NULL,
232
+ question TEXT NOT NULL,
233
+ value TEXT NOT NULL,
234
+ propensity DOUBLE PRECISION NOT NULL,
235
+ reviewer TEXT NOT NULL,
236
+ locale TEXT,
237
+ model TEXT,
238
+ tenant TEXT,
239
+ score DOUBLE PRECISION,
240
+ loss INTEGER,
241
+ raw TEXT,
242
+ at BIGINT NOT NULL,
243
+ input TEXT
244
+ )`);
245
+ await sql.exec(`ALTER TABLE oke_decision_labels ADD COLUMN IF NOT EXISTS input TEXT`);
246
+ await sql.exec(`ALTER TABLE oke_decision_labels ADD COLUMN IF NOT EXISTS review_id TEXT`);
247
+ await migrateDecisionDriftTable(sql, declaredDriftDecisions);
248
+ await sql.exec(`CREATE TABLE IF NOT EXISTS oke_decision_candidates (
249
+ decision_id TEXT PRIMARY KEY,
250
+ body TEXT NOT NULL
251
+ )`);
252
+ } catch (cause) {
253
+ throw new DecisionLabelStoreError("decision label store failed to open", { cause });
254
+ }
255
+ const query = async (
256
+ statement: string,
257
+ params?: readonly unknown[],
258
+ ): Promise<Record<string, unknown>[]> => {
259
+ try {
260
+ return await sql.query(statement, params);
261
+ } catch (cause) {
262
+ throw new DecisionLabelStoreError("decision label store query failed", { cause });
263
+ }
264
+ };
265
+ const exec = async (statement: string, params?: readonly unknown[]): Promise<void> => {
266
+ try {
267
+ await sql.exec(statement, params);
268
+ } catch (cause) {
269
+ throw new DecisionLabelStoreError("decision label store query failed", { cause });
270
+ }
271
+ };
272
+ return {
273
+ async insert(label, at) {
274
+ await exec(
275
+ `INSERT INTO oke_decision_labels
276
+ (decision_id, question, value, propensity, reviewer, locale, model, tenant, score, loss, raw, at, input, review_id)
277
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
278
+ [
279
+ label.decision,
280
+ label.question,
281
+ JSON.stringify(label.value),
282
+ label.propensity,
283
+ label.reviewer,
284
+ label.locale ?? null,
285
+ label.model ?? null,
286
+ label.tenant ?? null,
287
+ label.score ?? null,
288
+ label.loss ?? null,
289
+ label.raw === undefined ? null : JSON.stringify(label.raw),
290
+ at,
291
+ label.input === undefined ? null : JSON.stringify(label.input),
292
+ label.reviewId ?? null,
293
+ ],
294
+ );
295
+ },
296
+ async list(decision, tenant) {
297
+ const clauses: string[] = [];
298
+ const args: (string | null)[] = [];
299
+ if (decision !== undefined) {
300
+ clauses.push("decision_id = ?");
301
+ args.push(decision);
302
+ }
303
+ if (tenant !== undefined) {
304
+ clauses.push("tenant IS NOT DISTINCT FROM ?");
305
+ args.push(tenant);
306
+ }
307
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
308
+ const found = await query(
309
+ `SELECT decision_id, question, value, propensity, reviewer, locale, model, tenant, score, loss, raw, at, input, review_id
310
+ FROM oke_decision_labels ${where}`,
311
+ args,
312
+ );
313
+ return found.map((row) => ({
314
+ decision: String(row.decision_id),
315
+ question: String(row.question),
316
+ value: JSON.parse(String(row.value)) as unknown,
317
+ propensity: Number(row.propensity),
318
+ reviewer: String(row.reviewer),
319
+ ...(row.locale != null ? { locale: String(row.locale) } : {}),
320
+ ...(row.model != null ? { model: String(row.model) } : {}),
321
+ ...(row.tenant != null ? { tenant: String(row.tenant) } : {}),
322
+ ...(row.score != null ? { score: Number(row.score) } : {}),
323
+ ...(row.loss != null ? { loss: Number(row.loss) } : {}),
324
+ ...(row.raw != null ? { raw: JSON.parse(String(row.raw)) as unknown } : {}),
325
+ ...(row.input != null ? { input: JSON.parse(String(row.input)) as unknown } : {}),
326
+ ...(row.review_id != null ? { reviewId: String(row.review_id) } : {}),
327
+ at: Number(row.at),
328
+ }));
329
+ },
330
+ async drift() {
331
+ const rows = await query(
332
+ `SELECT decision_id, suspended, certified_at FROM oke_decision_drift`,
333
+ );
334
+ const flags: Record<string, DecisionDriftFlag> = {};
335
+ for (const row of rows) {
336
+ const name = String(row.decision_id ?? "");
337
+ if (!name) continue;
338
+ flags[name] = {
339
+ suspended: Number(row.suspended) === 1,
340
+ certifiedAt: Number(row.certified_at ?? 0),
341
+ };
342
+ }
343
+ return flags;
344
+ },
345
+ async setDrift(decision, next) {
346
+ if (!next.suspended) {
347
+ await exec(`DELETE FROM oke_decision_drift WHERE decision_id = ?`, [decision]);
348
+ return;
349
+ }
350
+ await exec(
351
+ `INSERT INTO oke_decision_drift (decision_id, suspended, certified_at) VALUES (?, ?, ?)
352
+ ON CONFLICT (decision_id) DO UPDATE SET suspended = excluded.suspended, certified_at = excluded.certified_at`,
353
+ [decision, 1, next.certifiedAt],
354
+ );
355
+ },
356
+ async putCandidate(decision, entry) {
357
+ await exec(
358
+ `INSERT INTO oke_decision_candidates (decision_id, body) VALUES (?, ?)
359
+ ON CONFLICT (decision_id) DO UPDATE SET body = excluded.body`,
360
+ [decision, JSON.stringify(entry)],
361
+ );
362
+ },
363
+ async getCandidate(decision) {
364
+ const rows = await query(`SELECT body FROM oke_decision_candidates WHERE decision_id = ?`, [
365
+ decision,
366
+ ]);
367
+ const body = rows[0]?.body;
368
+ if (typeof body !== "string") return undefined;
369
+ const parsed = JSON.parse(body) as DecisionCandidate;
370
+ if (!parsed || typeof parsed.model !== "string" || !parsed.questions) return undefined;
371
+ return parsed;
372
+ },
373
+ async listCandidates() {
374
+ const rows = await query(`SELECT decision_id FROM oke_decision_candidates`);
375
+ return rows.map((row) => String(row.decision_id));
376
+ },
377
+ };
378
+ }
379
+
380
+ function toLabel(row: StoredLabel): DecisionLabel {
381
+ return {
382
+ decision: row.decision,
383
+ question: row.question,
384
+ value: row.value,
385
+ propensity: row.propensity,
386
+ reviewer: row.reviewer,
387
+ ...(row.locale !== undefined ? { locale: row.locale } : {}),
388
+ ...(row.model !== undefined ? { model: row.model } : {}),
389
+ ...(row.tenant !== undefined ? { tenant: row.tenant } : {}),
390
+ ...(row.score !== undefined ? { score: row.score } : {}),
391
+ ...(row.loss !== undefined ? { loss: row.loss } : {}),
392
+ ...(row.raw !== undefined ? { raw: row.raw } : {}),
393
+ ...(row.input !== undefined ? { input: row.input } : {}),
394
+ ...(row.reviewId !== undefined ? { reviewId: row.reviewId } : {}),
395
+ at: row.at,
396
+ };
397
+ }
398
+
399
+ /**
400
+ * Recreate `oke_decision_drift` when it still uses an app-wide `id` row.
401
+ * A suspended flag is copied onto every declared decision.
402
+ *
403
+ * @param sql - Journal SQL client
404
+ * @param names - Declared decision names
405
+ */
406
+ async function migrateDecisionDriftTable(
407
+ sql: DecisionLabelSql,
408
+ names: readonly string[],
409
+ ): Promise<void> {
410
+ const create = `CREATE TABLE IF NOT EXISTS oke_decision_drift (
411
+ decision_id TEXT PRIMARY KEY,
412
+ suspended INTEGER NOT NULL,
413
+ certified_at BIGINT NOT NULL
414
+ )`;
415
+ let columns: string[] = [];
416
+ try {
417
+ const rows = await sql.query(
418
+ `SELECT column_name FROM information_schema.columns WHERE table_name = 'oke_decision_drift'`,
419
+ );
420
+ columns = rows.map((row) => String(row.column_name ?? "").toLowerCase());
421
+ } catch {
422
+ columns = [];
423
+ }
424
+ if (columns.includes("id") && !columns.includes("decision_id")) {
425
+ const old = await sql.query(`SELECT * FROM oke_decision_drift`);
426
+ const flag = old[0];
427
+ await sql.exec(`DROP TABLE oke_decision_drift`);
428
+ await sql.exec(create.replace("IF NOT EXISTS ", ""));
429
+ if (flag && Number(flag.suspended) === 1) {
430
+ const certifiedAt = Number(flag.certified_at ?? 0);
431
+ for (const name of names) {
432
+ await sql.exec(
433
+ `INSERT INTO oke_decision_drift (decision_id, suspended, certified_at) VALUES (?, ?, ?)`,
434
+ [name, 1, certifiedAt],
435
+ );
436
+ }
437
+ }
438
+ return;
439
+ }
440
+ await sql.exec(create);
441
+ }
@@ -21,9 +21,10 @@ describe("effects — reversibility tiers", () => {
21
21
  secret: "capability",
22
22
  call: "portal",
23
23
  fetch: "irreversible",
24
+ decide: "irreversible",
24
25
  };
25
26
 
26
- expect(EFFECT_KIND_TIERS).toHaveLength(9);
27
+ expect(EFFECT_KIND_TIERS).toHaveLength(10);
27
28
  for (const { kind, reversibility } of EFFECT_KIND_TIERS) {
28
29
  expect(reversibility).toBe(expected[kind]);
29
30
  expect(reversibilityOf(kind)).toBe(expected[kind]);
@@ -17,7 +17,8 @@ export type EffectKind =
17
17
  | "embed"
18
18
  | "secret"
19
19
  | "call"
20
- | "fetch";
20
+ | "fetch"
21
+ | "decide";
21
22
 
22
23
  /**
23
24
  * Reversibility tier — console §9.1 ranking, applied to all effect kinds.
@@ -65,6 +66,11 @@ export interface EffectEntry {
65
66
  * Omitted for in-process work (PGlite, mock AI, local channel console).
66
67
  */
67
68
  readonly external?: EffectExternal;
69
+ /**
70
+ * Agent run this `fx.run` or nested agent call started.
71
+ * Absent on prompt asks and on calls that are not agents.
72
+ */
73
+ readonly agentRunId?: string;
68
74
  }
69
75
 
70
76
  /** Append-only ledger of effect entries for one flow invocation. */
@@ -92,6 +98,7 @@ const TIER_BY_KIND: Readonly<Record<EffectKind, ReversibilityTier>> = {
92
98
  secret: "capability",
93
99
  call: "portal",
94
100
  fetch: "irreversible",
101
+ decide: "irreversible",
95
102
  };
96
103
 
97
104
  /**
@@ -143,6 +150,7 @@ export async function recordEffect<T>(
143
150
  externalOf?:
144
151
  | EffectExternal
145
152
  | ((result: T | undefined, error: unknown) => EffectExternal | undefined),
153
+ stamp?: (result: T | undefined) => { readonly agentRunId?: string } | undefined,
146
154
  ): Promise<T> {
147
155
  const timestamp = now();
148
156
  const t0 = performance.now();
@@ -163,6 +171,7 @@ export async function recordEffect<T>(
163
171
  timestamp,
164
172
  resolveDurationMs(now() - timestamp, performance.now() - t0),
165
173
  external,
174
+ stamp?.(result),
166
175
  );
167
176
  }
168
177
  }
@@ -187,7 +196,9 @@ export function recordObservedEffect(
187
196
  timestamp: number,
188
197
  duration: number,
189
198
  external?: EffectExternal,
199
+ stamp?: { readonly agentRunId?: string },
190
200
  ): void {
201
+ const agentRunId = stamp?.agentRunId;
191
202
  ledger.record({
192
203
  kind,
193
204
  resource,
@@ -195,6 +206,7 @@ export function recordObservedEffect(
195
206
  duration,
196
207
  reversibility: reversibilityOf(kind),
197
208
  ...(external !== undefined ? { external } : {}),
209
+ ...(agentRunId !== undefined && agentRunId.length > 0 ? { agentRunId } : {}),
198
210
  });
199
211
  }
200
212
 
@@ -16,6 +16,7 @@
16
16
 
17
17
  import type {
18
18
  AiAgentDecl,
19
+ AiDecisionDecl,
19
20
  AiEmbedDecl,
20
21
  AiMcpServerDecl,
21
22
  AiModelDecl,
@@ -52,3 +53,5 @@ export const aiEmbedRegistry: AiEmbedDecl[] = [];
52
53
  export const aiAgentRegistry: AiAgentDecl[] = [];
53
54
  /** `ai.mcpServer` declarations since the last reset. */
54
55
  export const aiMcpServerRegistry: AiMcpServerDecl[] = [];
56
+ /** `ai.decision` declarations since the last reset. */
57
+ export const aiDecisionRegistry: AiDecisionDecl[] = [];
@@ -52,6 +52,10 @@ export const OKE_ERROR_TEXT: Readonly<Record<string, OkeErrorText>> = {
52
52
  cause: 'Flow "{flow}" embeds with "{resource}" without declaring it.',
53
53
  fix: 'Add "{resource}" to this flow\'s effects.embeds.',
54
54
  },
55
+ UNDECLARED_DECIDE: {
56
+ cause: 'Flow "{flow}" decides "{resource}" without declaring it.',
57
+ fix: 'Add "{resource}" to this flow\'s effects.decides.',
58
+ },
55
59
  NO_EFFECTS_DECLARED: {
56
60
  cause: 'Flow "{flow}" has no declared effects and no Manifest to derive them from.{extract}',
57
61
  fix:
@@ -270,6 +270,8 @@ const OKE_ERROR_ROWS = {
270
270
  UNDECLARED_FETCH: { code: 1008, domain: "kernel" },
271
271
  /** Flow embeds via a model not listed in `effects.embeds`. */
272
272
  UNDECLARED_EMBED: { code: 1009, domain: "kernel" },
273
+ /** Flow decides a decision not listed in `effects.decides`. */
274
+ UNDECLARED_DECIDE: { code: 1010, domain: "kernel" },
273
275
  /**
274
276
  * Flow has no declared `effects` and no Manifest-derived effects were
275
277
  * available to stamp at boot (dev+compose / prod — never a silent open token).
@@ -46,9 +46,8 @@ describe("flow — one species", () => {
46
46
  on(signal, shared);
47
47
 
48
48
  const app = oke({ autoBoot: false, name: "one-species" });
49
- expect(app.bindings).toHaveLength(2);
50
- expect(app.bindings[0]?.flow).toBe(shared);
51
- expect(app.bindings[1]?.flow).toBe(shared);
49
+ const sharedBindings = app.bindings.filter((binding) => binding.flow === shared);
50
+ expect(sharedBindings).toHaveLength(2);
52
51
 
53
52
  const httpResult = await app.execute(shared, { n: 3 }, shared.triggers[0]!);
54
53
  const signalResults = await app.dispatchSignal("order-placed", { n: 4 });