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,193 @@
1
+ /**
2
+ * React hook for one agent run: send, stream, approve, and follow the resume.
3
+ *
4
+ * Import from `okengine/client-react`. This file does not import `okengine/client`.
5
+ */
6
+
7
+ import { useCallback, useEffect, useRef, useState } from "react";
8
+ import { approve, deny, readAgentEvents, type ParsedAgentEvent } from "../client/agent.ts";
9
+
10
+ /** Pending tool approval from a `RUN_FINISHED` interrupt. */
11
+ export interface AgentPendingApproval {
12
+ readonly id: string;
13
+ readonly tool?: string;
14
+ readonly args?: unknown;
15
+ }
16
+
17
+ /** State returned by {@link useAgentRun}. */
18
+ export interface AgentRunState {
19
+ readonly events: readonly ParsedAgentEvent[];
20
+ readonly text: string;
21
+ readonly pending: AgentPendingApproval | null;
22
+ readonly status: "idle" | "streaming" | "approval" | "done" | "error";
23
+ send(message: string): Promise<void>;
24
+ approve(args?: unknown): Promise<void>;
25
+ deny(reason?: string): Promise<void>;
26
+ }
27
+
28
+ /** Routes the hook calls. */
29
+ export interface UseAgentRunOptions {
30
+ /** POST that starts `fx.run(..., { stream: true })`. */
31
+ readonly sendUrl: string;
32
+ /** POST `/agent/approvals/approve`. */
33
+ readonly approveUrl: string;
34
+ /** POST `/agent/approvals/deny`. */
35
+ readonly denyUrl: string;
36
+ /** Follow URL for a run id. */
37
+ followUrl(runId: string): string;
38
+ readonly fetch?: typeof fetch;
39
+ readonly headers?: Record<string, string>;
40
+ }
41
+
42
+ /**
43
+ * Send one message and keep the events, the text so far, and a pending approval.
44
+ *
45
+ * After an interrupt, the hook follows once with `Last-Event-ID`. Approve and
46
+ * deny post the decision and leave that follow in place. Unmount aborts it.
47
+ *
48
+ * @param options - Send, approve, deny, and follow URLs
49
+ */
50
+ export function useAgentRun(options: UseAgentRunOptions): AgentRunState {
51
+ const [events, setEvents] = useState<ParsedAgentEvent[]>([]);
52
+ const [text, setText] = useState("");
53
+ const [pending, setPending] = useState<AgentPendingApproval | null>(null);
54
+ const [status, setStatus] = useState<AgentRunState["status"]>("idle");
55
+ const lastId = useRef<string | undefined>(undefined);
56
+ const runIdRef = useRef<string | undefined>(undefined);
57
+ const seenInterrupt = useRef<string | undefined>(undefined);
58
+ const followedRun = useRef<string | undefined>(undefined);
59
+ const followTask = useRef<Promise<void> | undefined>(undefined);
60
+ const abortRef = useRef<AbortController | undefined>(undefined);
61
+
62
+ useEffect(() => {
63
+ const controller = new AbortController();
64
+ abortRef.current = controller;
65
+ return () => {
66
+ controller.abort();
67
+ abortRef.current = undefined;
68
+ };
69
+ }, []);
70
+
71
+ const consume = useCallback(
72
+ async (source: Response | string, lastEventId?: string): Promise<string | undefined> => {
73
+ let followed: string | undefined;
74
+ for await (const event of readAgentEvents(source, {
75
+ fetch: options.fetch,
76
+ headers: options.headers,
77
+ signal: abortRef.current?.signal,
78
+ ...(lastEventId !== undefined ? { lastEventId } : {}),
79
+ onId(id) {
80
+ lastId.current = id;
81
+ },
82
+ })) {
83
+ if (
84
+ event.type === "RUN_FINISHED" &&
85
+ event.outcome?.type === "interrupt" &&
86
+ seenInterrupt.current === event.outcome.interrupts[0]?.id
87
+ ) {
88
+ continue;
89
+ }
90
+ setEvents((prev) => [...prev, event]);
91
+ if (event.type === "TEXT_MESSAGE_CONTENT") {
92
+ setText((prev) => prev + event.delta);
93
+ }
94
+ if (event.type === "RUN_STARTED" && "runId" in event) {
95
+ runIdRef.current = event.runId;
96
+ followed = event.runId;
97
+ }
98
+ if (event.type === "RUN_FINISHED" && event.outcome?.type === "interrupt") {
99
+ const first = event.outcome.interrupts[0];
100
+ const payload = first?.payload as { tool?: string; args?: unknown } | undefined;
101
+ if (first) {
102
+ seenInterrupt.current = first.id;
103
+ setPending({
104
+ id: first.id,
105
+ ...(payload?.tool !== undefined ? { tool: payload.tool } : {}),
106
+ ...(payload?.args !== undefined ? { args: payload.args } : {}),
107
+ });
108
+ setStatus("approval");
109
+ }
110
+ runIdRef.current = event.runId;
111
+ return event.runId;
112
+ }
113
+ if (event.type === "RUN_FINISHED" || event.type === "RUN_ERROR") {
114
+ setPending(null);
115
+ setStatus(event.type === "RUN_ERROR" ? "error" : "done");
116
+ if (event.type === "RUN_FINISHED") runIdRef.current = event.runId;
117
+ return undefined;
118
+ }
119
+ }
120
+ return followed;
121
+ },
122
+ [options.fetch, options.headers],
123
+ );
124
+
125
+ const followUntilDone = useCallback(
126
+ async (runId: string) => {
127
+ if (followedRun.current === runId) return;
128
+ followedRun.current = runId;
129
+ await consume(options.followUrl(runId), lastId.current);
130
+ },
131
+ [consume, options.followUrl],
132
+ );
133
+
134
+ const send = useCallback(
135
+ async (message: string) => {
136
+ setStatus("streaming");
137
+ setText("");
138
+ setEvents([]);
139
+ setPending(null);
140
+ lastId.current = undefined;
141
+ seenInterrupt.current = undefined;
142
+ followedRun.current = undefined;
143
+ const fetcher = options.fetch ?? fetch;
144
+ const response = await fetcher(options.sendUrl, {
145
+ method: "POST",
146
+ headers: { "content-type": "application/json", ...headersOf(options.headers) },
147
+ body: JSON.stringify({ message }),
148
+ signal: abortRef.current?.signal,
149
+ });
150
+ const runId = await consume(response);
151
+ if (runId) followTask.current = followUntilDone(runId);
152
+ },
153
+ [consume, followUntilDone, options.fetch, options.headers, options.sendUrl],
154
+ );
155
+
156
+ const approvePending = useCallback(
157
+ async (args?: unknown) => {
158
+ if (!pending) return;
159
+ await approve(options.approveUrl, pending.id, args, {
160
+ fetch: options.fetch,
161
+ headers: options.headers,
162
+ });
163
+ setStatus("streaming");
164
+ },
165
+ [options.approveUrl, options.fetch, options.headers, pending],
166
+ );
167
+
168
+ const denyPending = useCallback(
169
+ async (reason?: string) => {
170
+ if (!pending) return;
171
+ await deny(options.denyUrl, pending.id, reason, {
172
+ fetch: options.fetch,
173
+ headers: options.headers,
174
+ });
175
+ setStatus("streaming");
176
+ },
177
+ [options.denyUrl, options.fetch, options.headers, pending],
178
+ );
179
+
180
+ return {
181
+ events,
182
+ text,
183
+ pending,
184
+ status,
185
+ send,
186
+ approve: approvePending,
187
+ deny: denyPending,
188
+ };
189
+ }
190
+
191
+ function headersOf(headers: Record<string, string> | undefined): Record<string, string> {
192
+ return headers ?? {};
193
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Agent tool approval is a Manifest flag. The predicate stays in author code.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { extractFromSources } from "./extract.ts";
7
+
8
+ describe("extract agent approval", () => {
9
+ test("approval: true is recorded and a plain tool is not", async () => {
10
+ const manifest = await extractFromSources({
11
+ "src/flows/assist.ts": `
12
+ import { ai } from "okengine";
13
+ export const support = ai.agent("support", {
14
+ tools: [{ name: "billing.refund", approval: true, gate: "ops" }, "billing.lookup"],
15
+ });
16
+ `,
17
+ });
18
+ expect(manifest.ai?.agents?.support?.tools).toEqual(["billing.refund", "billing.lookup"]);
19
+ expect(manifest.ai?.agents?.support?.approvals).toEqual({ "billing.refund": true });
20
+ });
21
+
22
+ test("fx.run asks the agent and an agent tool is a call", async () => {
23
+ const manifest = await extractFromSources({
24
+ "src/flows/assist.ts": `
25
+ import { ai, flow, on, http } from "okengine";
26
+ const lookup = ai.agent("lookup", { tools: [] });
27
+ const support = ai.agent("support", { tools: [lookup] });
28
+ export const assist = on(http.post("/assist"), flow("assist", {
29
+ do: async (_input, fx) => fx.run(support, { message: "hi" }),
30
+ }));
31
+ `,
32
+ });
33
+ expect(manifest.flows?.assist?.effects?.asks).toContain("support");
34
+ expect(manifest.flows?.assist?.effects?.calls).toContain("lookup");
35
+ });
36
+ });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Prompt `repair` is copied onto the Manifest the same way as `budget`.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { extractFromSources } from "./extract.ts";
7
+
8
+ describe("extract prompt repair", () => {
9
+ test("repair: 1 is recorded and repair: 0 is omitted", async () => {
10
+ const manifest = await extractFromSources({
11
+ "src/flows/triage.ts": `
12
+ import { ai } from "okengine";
13
+ export const smart = ai.model("smart", { provider: "mock" });
14
+ smart.prompt("ticket-triage", { repair: 1, budget: { maxCostPerCall: 0.02 } });
15
+ smart.prompt("plain", { repair: 0 });
16
+ `,
17
+ });
18
+ expect(manifest.ai?.prompts?.["ticket-triage"]?.repair).toBe(1);
19
+ expect(manifest.ai?.prompts?.["ticket-triage"]?.budget?.maxCostPerCall).toBe(0.02);
20
+ expect(manifest.ai?.prompts?.plain?.repair).toBeUndefined();
21
+ });
22
+ });
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Decision extraction: exclusive modes, and review cannot sit on HTTP.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { extractFromSources } from "./extract.ts";
7
+
8
+ const header = `const triage = ai.decision("triage", {\n`;
9
+
10
+ describe("ai.decision extract", () => {
11
+ test("review and abstain together fail to compile", async () => {
12
+ await expect(
13
+ extractFromSources({
14
+ "src/flows/run.ts": `${header} review: "ops",\n onUncertain: "abstain",\n ask: { team: ai.choice("which", { a: "A" }) },\n});\n`,
15
+ }),
16
+ ).rejects.toThrow(/exactly one of review/);
17
+ });
18
+
19
+ test("an HTTP trigger that reviews fails to compile", async () => {
20
+ await expect(
21
+ extractFromSources({
22
+ "src/flows/run.ts": `
23
+ const triage = ai.decision("triage", {
24
+ review: "ops",
25
+ ask: { team: ai.choice("which", { a: "A" }) },
26
+ });
27
+ on(http.post("/t"), flow("run", {
28
+ durable: true,
29
+ do: async (input, fx) => fx.decide(triage, input),
30
+ }));
31
+ `,
32
+ }),
33
+ ).rejects.toThrow(/fx\.emit/);
34
+ });
35
+
36
+ test("choice, score, and duplicate names fail at compile time", async () => {
37
+ const many = Array.from({ length: 255 }, (_, i) => `o${i}: "x"`).join(", ");
38
+ await expect(
39
+ extractFromSources({
40
+ "src/flows/run.ts": `${header} onUncertain: "abstain",\n ask: { team: ai.choice("which", { ${many} }) },\n});\n`,
41
+ }),
42
+ ).rejects.toThrow(/max 254/);
43
+ await expect(
44
+ extractFromSources({
45
+ "src/flows/run.ts": `${header} onUncertain: "abstain",\n ask: { team: ai.choice("which", { none_of_these: null }) },\n});\n`,
46
+ }),
47
+ ).rejects.toThrow(/none_of_these/);
48
+ await expect(
49
+ extractFromSources({
50
+ "src/flows/run.ts": `${header} onUncertain: "abstain",\n ask: { rank: ai.score("rank", ["only"]) },\n});\n`,
51
+ }),
52
+ ).rejects.toThrow(/2–10 levels/);
53
+ await expect(
54
+ extractFromSources({
55
+ "src/flows/a.ts": `${header} onUncertain: "abstain",\n ask: { team: ai.choice("which", { a: "A", b: "B" }) },\n});\n`,
56
+ "src/flows/b.ts": `${header} onUncertain: "abstain",\n ask: { team: ai.choice("which", { a: "A", b: "B" }) },\n});\n`,
57
+ }),
58
+ ).rejects.toThrow(/duplicate decision name/);
59
+ });
60
+
61
+ test("review records the gate name", async () => {
62
+ const manifest = await extractFromSources({
63
+ "src/flows/run.ts": `
64
+ const ops = gate.policy("ops", () => true);
65
+ const triage = ai.decision("triage", {
66
+ review: ops,
67
+ ask: { team: ai.choice("which", { a: "A", b: "B" }) },
68
+ });
69
+ on(signal.once("job"), flow("run", {
70
+ durable: true,
71
+ do: async (input, fx) => fx.decide(triage, input),
72
+ }));
73
+ `,
74
+ });
75
+ expect(manifest.ai?.decisions?.triage?.review).toBe("ops");
76
+ });
77
+
78
+ test("review resolves a variable to the gate's declared name", async () => {
79
+ const manifest = await extractFromSources({
80
+ "src/flows/run.ts": `
81
+ const someVar = gate.policy("ops", () => true);
82
+ const triage = ai.decision("triage", {
83
+ review: someVar,
84
+ ask: { team: ai.choice("which", { a: "A", b: "B" }) },
85
+ });
86
+ on(signal.once("job"), flow("run", {
87
+ durable: true,
88
+ do: async (input, fx) => fx.decide(triage, input),
89
+ }));
90
+ `,
91
+ });
92
+ expect(manifest.ai?.decisions?.triage?.review).toBe("ops");
93
+ });
94
+
95
+ test("autonomy and evals survive extraction", async () => {
96
+ const manifest = await extractFromSources({
97
+ "src/flows/run.ts": `
98
+ const triage = ai.decision("triage", {
99
+ onUncertain: "abstain",
100
+ evals: "evals/triage.jsonl",
101
+ autonomy: { maxError: 0.05, audit: 0.1, risk: 0.1 },
102
+ ask: { team: ai.choice("which", { a: "A", b: "B" }) },
103
+ });
104
+ `,
105
+ });
106
+ expect(manifest.ai?.decisions?.triage?.evals).toBe("evals/triage.jsonl");
107
+ expect(manifest.ai?.decisions?.triage?.autonomy).toEqual({
108
+ maxError: 0.05,
109
+ audit: 0.1,
110
+ risk: 0.1,
111
+ });
112
+ });
113
+
114
+ test("choice options passed through a variable fail to compile", async () => {
115
+ await expect(
116
+ extractFromSources({
117
+ "src/flows/run.ts": `
118
+ const options = { a: "A", b: "B" };
119
+ ai.decision("triage", {
120
+ onUncertain: "abstain",
121
+ ask: { team: ai.choice("which", options) },
122
+ });
123
+ `,
124
+ }),
125
+ ).rejects.toThrow(/object literal/);
126
+ });
127
+
128
+ test("score levels and a whole question resolve a same-file const", async () => {
129
+ const manifest = await extractFromSources({
130
+ "src/flows/run.ts": `
131
+ const levels = ["low", "high"];
132
+ const team = ai.choice("which", { a: "A", b: "B" });
133
+ ai.decision("triage", {
134
+ onUncertain: "abstain",
135
+ ask: { team, rank: ai.score("rank", levels) },
136
+ });
137
+ `,
138
+ });
139
+ expect(manifest.ai?.decisions?.triage?.questions).toEqual(["team", "rank"]);
140
+ });
141
+
142
+ test("an unresolved score or question fails to compile", async () => {
143
+ await expect(
144
+ extractFromSources({
145
+ "src/flows/run.ts": `
146
+ ai.decision("triage", {
147
+ onUncertain: "abstain",
148
+ ask: { rank: ai.score("rank", missing) },
149
+ });
150
+ `,
151
+ }),
152
+ ).rejects.toThrow(/same-file const/);
153
+ await expect(
154
+ extractFromSources({
155
+ "src/flows/run.ts": `
156
+ ai.decision("triage", {
157
+ onUncertain: "abstain",
158
+ ask: { team: missingQuestion },
159
+ });
160
+ `,
161
+ }),
162
+ ).rejects.toThrow(/not a same-file const/);
163
+ });
164
+ });
@@ -57,10 +57,12 @@ export interface InferBinding {
57
57
  | "signal"
58
58
  | "clock"
59
59
  | "prompt"
60
+ | "agent"
60
61
  | "secret"
61
62
  | "template"
62
63
  | "flow"
63
64
  | "embed"
65
+ | "decision"
64
66
  | "table"
65
67
  | "mcp-server"
66
68
  | "mcp-tool"
@@ -71,6 +73,8 @@ export interface InferBinding {
71
73
  readonly facet?: "sql" | "kv" | "files" | "index";
72
74
  /** Prompt version when kind is `prompt`. */
73
75
  readonly version?: number;
76
+ /** Nested agent tools when kind is `agent`. */
77
+ readonly agentCalls?: readonly string[];
74
78
  }
75
79
 
76
80
  /** What a callee that is passed `fx` resolves to. */
@@ -190,6 +194,7 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
190
194
  const secrets = new Set<SecretRef>();
191
195
  const calls = new Set<FlowRef>();
192
196
  const fetches = new Set<FetchHostRef>();
197
+ const decides = new Set<string>();
193
198
  const steps: string[] = [];
194
199
  let usesRaw = false;
195
200
 
@@ -237,6 +242,17 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
237
242
  continue;
238
243
  }
239
244
 
245
+ if (chain.rootMethod === "run" && call === chain.rootCall) {
246
+ const binding = resolveBinding(call.arguments[0], options.bindings);
247
+ const name =
248
+ stringArg(call.arguments[0]) ?? (binding?.kind === "agent" ? binding.ref : undefined);
249
+ if (name) asks.add(name);
250
+ if (binding?.kind === "agent") {
251
+ for (const child of binding.agentCalls ?? []) calls.add(child);
252
+ }
253
+ continue;
254
+ }
255
+
240
256
  if (chain.rootMethod === "ask" && call === chain.rootCall) {
241
257
  const ref = resolvePrompt(call.arguments[0], options.bindings);
242
258
  if (ref) asks.add(ref);
@@ -268,6 +284,14 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
268
284
  continue;
269
285
  }
270
286
 
287
+ if (chain.rootMethod === "decide" && call === chain.rootCall) {
288
+ const binding = resolveBinding(call.arguments[0], options.bindings);
289
+ const name =
290
+ stringArg(call.arguments[0]) ?? (binding?.kind === "decision" ? binding.ref : undefined);
291
+ if (name) decides.add(name);
292
+ continue;
293
+ }
294
+
271
295
  if (chain.rootMethod === "fetch" && call === chain.rootCall) {
272
296
  // fx.fetch(url) — host becomes effects.fetches entry.
273
297
  const lit = stringArg(call.arguments[0]);
@@ -374,13 +398,14 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
374
398
  if (secrets.size > 0) effects.secrets = sortUnique([...secrets]);
375
399
  if (calls.size > 0) effects.calls = sortUnique([...calls]);
376
400
  if (fetches.size > 0) effects.fetches = sortUnique([...fetches]);
401
+ if (decides.size > 0) effects.decides = sortUnique([...decides]);
377
402
 
378
403
  return {
379
404
  effects,
380
405
  steps,
381
406
  usesRaw,
382
407
  cacheIneligible: usesRaw && !options.hasExplicitEffects,
383
- nondeterministic: asks.size > 0 || embeds.size > 0,
408
+ nondeterministic: asks.size > 0 || embeds.size > 0 || decides.size > 0,
384
409
  readsUserId: userIdRoots.some((node) => containsAuthUserId(node)),
385
410
  };
386
411
  }
@@ -395,6 +420,7 @@ const EFFECT_KEYS = [
395
420
  "secrets",
396
421
  "calls",
397
422
  "fetches",
423
+ "decides",
398
424
  ] as const satisfies readonly (keyof Effects)[];
399
425
 
400
426
  /**