okengine 0.22.0 → 0.23.1
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.
- package/AGENTS.md +7 -0
- package/manifest.v1.schema.json +44 -0
- package/package.json +32 -19
- package/site/content/docs/ai/mcp.mdx +14 -10
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/elements/ai/agents.mdx +61 -13
- package/site/content/docs/elements/ai/decide.mdx +536 -0
- package/site/content/docs/elements/ai/events.mdx +373 -0
- package/site/content/docs/elements/ai/index.mdx +9 -3
- package/site/content/docs/elements/ai/meta.json +1 -1
- package/site/content/docs/elements/ai/prompts.mdx +4 -2
- package/site/content/docs/elements/channel/index.mdx +4 -4
- package/site/content/docs/elements/channel/receipts.mdx +7 -5
- package/site/content/docs/elements/channel/sms.mdx +2 -1
- package/site/content/docs/elements/flow/index.mdx +22 -18
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/signal/index.mdx +15 -14
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/plugins/otp.mdx +3 -3
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/postgres.mdx +12 -8
- package/site/content/docs/recipes/redis.mdx +4 -4
- package/site/content/docs/reference/cli.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +9 -7
- package/site/content/docs/reference/fx.mdx +19 -10
- package/src/auth/api-key-sql.test.ts +44 -0
- package/src/auth/api-key-sql.ts +1 -0
- package/src/auth/api-keys.ts +30 -12
- package/src/auth/config.ts +6 -2
- package/src/auth/gate-auth.test.ts +20 -0
- package/src/auth/tenants.ts +17 -0
- package/src/cli/agents-md.test.ts +25 -0
- package/src/cli/ask-vault-gaps.test.ts +38 -1
- package/src/cli/ask-vault-gaps.ts +35 -2
- package/src/cli/decide.ts +174 -0
- package/src/cli/decision-lock-watch.test.ts +45 -0
- package/src/cli/decision-lock-watch.ts +51 -0
- package/src/cli/dev-app-runner.ts +34 -8
- package/src/cli/dev-auth-secret.test.ts +49 -0
- package/src/cli/dev-auth-secret.ts +49 -0
- package/src/cli/dev-hot.test.ts +41 -0
- package/src/cli/dev-hot.ts +48 -0
- package/src/cli/dev.test.ts +1 -1
- package/src/cli/dev.ts +24 -6
- package/src/cli/doctor-diff.test.ts +29 -1
- package/src/cli/doctor-diff.ts +35 -0
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.images.test.ts +2 -2
- package/src/cli/load-config.ts +1 -1
- package/src/cli/manifest-pr-diff.ts +16 -0
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/client/agent.ts +307 -0
- package/src/client/transport.test.ts +20 -0
- package/src/client/transport.ts +2 -1
- package/src/client/types.ts +1 -1
- package/src/client-react/index.ts +7 -1
- package/src/client-react/use-agent-run.test.ts +271 -0
- package/src/client-react/use-agent-run.ts +193 -0
- package/src/compiler/ai-approval.test.ts +36 -0
- package/src/compiler/ai-repair.test.ts +22 -0
- package/src/compiler/decisions.extract.test.ts +164 -0
- package/src/compiler/effects-fetch.test.ts +77 -0
- package/src/compiler/effects-infer.ts +114 -17
- package/src/compiler/effects-join.test.ts +48 -0
- package/src/compiler/extract.test.ts +28 -0
- package/src/compiler/extract.ts +279 -13
- package/src/compiler/fx-index.ts +58 -4
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/index.ts +1 -1
- package/src/console/server/ai-runs-flows.ts +269 -0
- package/src/console/server/ai-runs.test.ts +323 -0
- package/src/console/server/ai-runs.ts +562 -0
- package/src/console/server/ai.ts +12 -1
- package/src/console/server/app.ts +10 -1
- package/src/console/server/decisions-flows.ts +157 -0
- package/src/console/server/decisions.test.ts +66 -0
- package/src/console/server/decisions.ts +229 -0
- package/src/console/server/flows.ts +50 -2
- package/src/console/server/invoke-user-flow.ts +9 -0
- package/src/console/server/runs-ingest.ts +16 -0
- package/src/console/server/serve.ts +5 -0
- package/src/console/server/state.ts +25 -1
- package/src/console/server/store-stats.test.ts +47 -0
- package/src/console/server/store-stats.ts +92 -18
- package/src/console/ui-next/dist/assets/access-page-C_N4qvai.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-Cq1TxniW.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-BaI4uxZz.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DNyK7IIY.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-CRhBJMZR.js} +1 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
- package/src/console/ui-next/dist/assets/decisions-page-D9twj8Xy.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-CsHWlVPU.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-ZaTJAhR9.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-DQ-EMAfz.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-Cn_Fl79s.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-CmRFeWdT.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-Cn6R_dXz.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bx6EZhSe.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DmdiUrfh.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-8P6vGXnn.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-BfGvpFSv.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CWdUHbc0.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-CZOFZzay.js +1 -0
- package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-ki_ishmX.js +2 -0
- package/src/console/ui-next/dist/index.html +4 -3
- package/src/console/ui-next/seed-parked-approval.ts +126 -0
- package/src/console/ui-next/src/client.ts +224 -1
- package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
- package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
- package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
- package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
- package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
- package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
- package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
- package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
- package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
- package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
- package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
- package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
- package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
- package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
- package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
- package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
- package/src/console/ui-next/src/router.tsx +17 -0
- package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
- package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
- package/src/console/ui-next/vite.config.ts +9 -0
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/drivers/ai-anthropic.ts +212 -21
- package/src/drivers/ai-mock.ts +34 -1
- package/src/drivers/ai-openai-compatible.ts +67 -26
- package/src/drivers/ai-preconnect.ts +18 -0
- package/src/drivers/ai-stream.test.ts +62 -0
- package/src/drivers/ai-types.ts +20 -0
- package/src/drivers/journal-postgres.test.ts +22 -1
- package/src/drivers/journal-postgres.ts +511 -13
- package/src/drivers/memory-ddl.test.ts +18 -0
- package/src/drivers/memory.ts +21 -1
- package/src/drivers/pg-rls.test.ts +31 -0
- package/src/drivers/pg-rls.ts +15 -0
- package/src/drivers/postgres.test.ts +9 -0
- package/src/drivers/postgres.ts +49 -11
- package/src/drivers/signal-compete.test.ts +234 -0
- package/src/drivers/signal-postgres.ts +39 -19
- package/src/drivers/signal-redis.ts +167 -11
- package/src/drivers/signal-types.ts +12 -0
- package/src/elements/ai/agent-event-slot.ts +27 -0
- package/src/elements/ai/approval-http.ts +207 -0
- package/src/elements/ai/approval.test.ts +741 -0
- package/src/elements/ai/approval.ts +228 -0
- package/src/elements/ai/decisions/bind.ts +163 -0
- package/src/elements/ai/decisions/certificate.test.ts +291 -0
- package/src/elements/ai/decisions/certificate.ts +377 -0
- package/src/elements/ai/decisions/certify.ts +314 -0
- package/src/elements/ai/decisions/decide.test.ts +953 -0
- package/src/elements/ai/decisions/e2e.test.ts +399 -0
- package/src/elements/ai/decisions/export.test.ts +120 -0
- package/src/elements/ai/decisions/export.ts +146 -0
- package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
- package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
- package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
- package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
- package/src/elements/ai/decisions/http.test.ts +154 -0
- package/src/elements/ai/decisions/http.ts +240 -0
- package/src/elements/ai/decisions/labels.test.ts +282 -0
- package/src/elements/ai/decisions/labels.ts +257 -0
- package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
- package/src/elements/ai/decisions/openrouter.ts +36 -0
- package/src/elements/ai/decisions/provider.ts +102 -0
- package/src/elements/ai/decisions/typesafe.ts +36 -0
- package/src/elements/ai/declare.ts +252 -1
- package/src/elements/ai/events.test.ts +379 -0
- package/src/elements/ai/events.ts +138 -0
- package/src/elements/ai/mcp-protocol.ts +5 -6
- package/src/elements/ai/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1187 -105
- package/src/elements/ai/stream-turn.ts +214 -0
- package/src/elements/ai/stream.live.test.ts +76 -0
- package/src/elements/ai/subagent.test.ts +136 -0
- package/src/elements/ai.test.ts +497 -3
- package/src/elements/ai.ts +16 -1
- package/src/elements/channel/runtime.ts +11 -0
- package/src/elements/channel/sql-ledger.ts +251 -0
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/signal/runtime.ts +9 -0
- package/src/elements/signal.test.ts +1 -0
- package/src/elements/store/cache.test.ts +120 -15
- package/src/elements/store/cache.ts +200 -39
- package/src/elements/store/prepare-row.test.ts +6 -2
- package/src/elements/store/sql-session.test.ts +53 -0
- package/src/elements/store/sql-session.ts +206 -10
- package/src/elements/store.ts +1 -0
- package/src/i18n/catalogs/ar.ts +5 -0
- package/src/i18n/catalogs/en.ts +5 -0
- package/src/index.ts +2 -0
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +135 -17
- package/src/kernel/auto-cache.test.ts +228 -0
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/channel.ts +63 -2
- package/src/kernel/boot-bind/honor-config.test.ts +40 -10
- package/src/kernel/boot-bind/signal.ts +61 -25
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot.ts +31 -6
- package/src/kernel/builtin-errors.ts +2 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects.test.ts +2 -1
- package/src/kernel/effects.ts +13 -1
- package/src/kernel/element-registries.ts +3 -0
- package/src/kernel/errors-compiler.ts +11 -0
- package/src/kernel/errors-text.ts +20 -0
- package/src/kernel/errors.ts +10 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +33 -3
- package/src/kernel/fx-call-types.test.ts +32 -0
- package/src/kernel/fx-decide.ts +765 -0
- package/src/kernel/fx-sql-handle.ts +46 -7
- package/src/kernel/fx.test.ts +13 -8
- package/src/kernel/fx.ts +339 -45
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency.test.ts +2 -1
- package/src/kernel/journal.test.ts +114 -0
- package/src/kernel/journal.ts +471 -51
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline-tenant.ts +5 -1
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/pipeline.ts +5 -0
- package/src/kernel/signal-tx.ts +28 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/kernel/store-transaction.test.ts +95 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +29 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/docs-server.ts +19 -2
- package/src/mcp/mcp.test.ts +83 -0
- package/src/mcp/protocol.ts +8 -2
- package/src/mcp/server.ts +23 -2
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/mcp/versions.ts +60 -0
- package/src/plugins/index.ts +1 -0
- package/src/plugins/mena.ts +56 -0
- package/src/release/http-graph.test.ts +2 -0
- package/src/release/http-graph.ts +5 -0
- package/src/release/measure.ts +39 -14
- package/src/runs/collect.ts +4 -1
- package/src/runs/parquet.test.ts +27 -1
- package/src/runs/parquet.ts +19 -0
- package/src/runs/types.ts +31 -0
- package/src/runtime/json-code-block.test.ts +106 -47
- package/src/runtime/json-code-block.ts +1209 -46
- package/src/term.test.ts +13 -0
- package/src/term.ts +81 -49
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
- package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
- package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
|
@@ -246,6 +246,26 @@ describe("transport — retry", () => {
|
|
|
246
246
|
expect(n).toBe(3);
|
|
247
247
|
});
|
|
248
248
|
|
|
249
|
+
test("JournalLeaseBusy waits for Retry-After", async () => {
|
|
250
|
+
let n = 0;
|
|
251
|
+
const api = createClient<PingApp>("http://app.test", {
|
|
252
|
+
retry: { retries: 1, delay: 1, backoff: 1 },
|
|
253
|
+
fetch: async () => {
|
|
254
|
+
n += 1;
|
|
255
|
+
if (n === 1) {
|
|
256
|
+
return Response.json(
|
|
257
|
+
{ data: null, error: { code: "JournalLeaseBusy", data: { retryAfter: 1 } } },
|
|
258
|
+
{ status: 409, headers: { "retry-after": "0.01" } },
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return Response.json({ data: { ok: true }, error: null });
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
const result = await api.sys.ping();
|
|
265
|
+
expect(result.error).toBeNull();
|
|
266
|
+
expect(n).toBe(2);
|
|
267
|
+
});
|
|
268
|
+
|
|
249
269
|
test("other 409 envelopes are not retried", async () => {
|
|
250
270
|
let n = 0;
|
|
251
271
|
const api = createClient<PingApp>("http://app.test", {
|
package/src/client/transport.ts
CHANGED
|
@@ -104,7 +104,8 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
|
|
|
104
104
|
const seconds = Number(res.headers.get("retry-after"));
|
|
105
105
|
const wait = seconds > 0 ? seconds * 1000 : delay;
|
|
106
106
|
const structured = await decodeIfEnvelope(res);
|
|
107
|
-
|
|
107
|
+
const code = structured?.error?.code;
|
|
108
|
+
if (code === "IdempotencyInProgress" || code === "JournalLeaseBusy") {
|
|
108
109
|
await sleep(wait);
|
|
109
110
|
delay *= backoff;
|
|
110
111
|
attempt += 1;
|
package/src/client/types.ts
CHANGED
|
@@ -264,7 +264,7 @@ export interface ClientOptions {
|
|
|
264
264
|
* A call retries when it is `GET` or `QUERY`, or when it carries an
|
|
265
265
|
* idempotency key. Keys are sent automatically on every non-GET call.
|
|
266
266
|
* `idempotencyKey: false` runs once unless that call passes `{ retry: true }`.
|
|
267
|
-
* `409 IdempotencyInProgress`
|
|
267
|
+
* `409 IdempotencyInProgress` and `409 JournalLeaseBusy` wait for `Retry-After` and count as an attempt.
|
|
268
268
|
*/
|
|
269
269
|
readonly retry?: {
|
|
270
270
|
/** Extra attempts after the first (default 0). */
|
|
@@ -11,9 +11,15 @@ export {
|
|
|
11
11
|
type ResourceStreamOptions,
|
|
12
12
|
type ResourceStreamStop,
|
|
13
13
|
} from "./live-resource.ts";
|
|
14
|
-
export
|
|
14
|
+
export {
|
|
15
|
+
useAgentRun,
|
|
16
|
+
type AgentPendingApproval,
|
|
17
|
+
type AgentRunState,
|
|
18
|
+
type UseAgentRunOptions,
|
|
19
|
+
} from "./use-agent-run.ts";
|
|
15
20
|
export {
|
|
16
21
|
useLiveQuery,
|
|
22
|
+
type LiveRouteContract,
|
|
17
23
|
type UseLiveQueryOptions,
|
|
18
24
|
type UseLiveQueryState,
|
|
19
25
|
} from "./use-live-query.ts";
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAgentRun keeps one follow and does not replay text.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { Window } from "happy-dom";
|
|
7
|
+
import { act, createElement, useEffect } from "react";
|
|
8
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
9
|
+
import type { AiMessage } from "../drivers/ai-types.ts";
|
|
10
|
+
import { ai, createAiRuntime } from "../elements/ai.ts";
|
|
11
|
+
import { createGateRuntime, gate } from "../elements/gate.ts";
|
|
12
|
+
import { oke } from "../kernel/app.ts";
|
|
13
|
+
import { flow, resetFlowSeq, type AnyFlowDef } from "../kernel/flow.ts";
|
|
14
|
+
import { createMemoryJournalStore } from "../kernel/journal.ts";
|
|
15
|
+
import { resetBindings, type Binding } from "../kernel/on.ts";
|
|
16
|
+
import { http } from "../kernel/triggers.ts";
|
|
17
|
+
import { useAgentRun, type AgentRunState } from "./use-agent-run.ts";
|
|
18
|
+
|
|
19
|
+
function frames(rows: readonly { id: string; event: unknown }[]): string {
|
|
20
|
+
return rows.map((row) => `id: ${row.id}\ndata: ${JSON.stringify(row.event)}\n\n`).join("");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sse(body: string): Response {
|
|
24
|
+
return new Response(body, { status: 200, headers: { "content-type": "text/event-stream" } });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("useAgentRun", () => {
|
|
28
|
+
let happy: Window;
|
|
29
|
+
let root: Root | undefined;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
33
|
+
happy = new Window({ url: "http://app.test/" });
|
|
34
|
+
Object.defineProperty(globalThis, "window", {
|
|
35
|
+
value: happy,
|
|
36
|
+
configurable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
});
|
|
39
|
+
Object.defineProperty(globalThis, "document", {
|
|
40
|
+
value: happy.document,
|
|
41
|
+
configurable: true,
|
|
42
|
+
writable: true,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
root?.unmount();
|
|
48
|
+
happy.close();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("text is not duplicated and approve does not start a second follow", async () => {
|
|
52
|
+
const encoder = new TextEncoder();
|
|
53
|
+
let follow: ReadableStreamDefaultController<Uint8Array> | undefined;
|
|
54
|
+
let lastEventId: string | null = null;
|
|
55
|
+
let follows = 0;
|
|
56
|
+
const fetchFn = Object.assign(
|
|
57
|
+
async (input: string | URL | Request, init?: RequestInit) => {
|
|
58
|
+
const url = String(input);
|
|
59
|
+
if (url.endsWith("/send")) {
|
|
60
|
+
return sse(
|
|
61
|
+
frames([
|
|
62
|
+
{ id: "1", event: { type: "RUN_STARTED", threadId: "t", runId: "run-1" } },
|
|
63
|
+
{ id: "2", event: { type: "TEXT_MESSAGE_CONTENT", messageId: "m", delta: "Hel" } },
|
|
64
|
+
{
|
|
65
|
+
id: "3",
|
|
66
|
+
event: {
|
|
67
|
+
type: "RUN_FINISHED",
|
|
68
|
+
threadId: "t",
|
|
69
|
+
runId: "run-1",
|
|
70
|
+
outcome: {
|
|
71
|
+
type: "interrupt",
|
|
72
|
+
interrupts: [{ id: "ap-1", reason: "approval", payload: { tool: "pay" } }],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
]),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (url.endsWith("/events")) {
|
|
80
|
+
follows += 1;
|
|
81
|
+
lastEventId = new Headers(init?.headers).get("last-event-id");
|
|
82
|
+
return new Response(
|
|
83
|
+
new ReadableStream({
|
|
84
|
+
start(controller) {
|
|
85
|
+
follow = controller;
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
{ status: 200, headers: { "content-type": "text/event-stream" } },
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
follow?.enqueue(
|
|
92
|
+
encoder.encode(
|
|
93
|
+
frames([
|
|
94
|
+
{ id: "4", event: { type: "TEXT_MESSAGE_CONTENT", messageId: "m", delta: "lo" } },
|
|
95
|
+
{
|
|
96
|
+
id: "5",
|
|
97
|
+
event: {
|
|
98
|
+
type: "RUN_FINISHED",
|
|
99
|
+
threadId: "t",
|
|
100
|
+
runId: "run-1",
|
|
101
|
+
result: { cost: 0, stopReason: "completed", output: "Hello" },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
]),
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
follow?.close();
|
|
108
|
+
return new Response(JSON.stringify({ data: { ok: true }, error: null }), { status: 200 });
|
|
109
|
+
},
|
|
110
|
+
{ preconnect: () => undefined },
|
|
111
|
+
) as typeof fetch;
|
|
112
|
+
|
|
113
|
+
let view: AgentRunState | undefined;
|
|
114
|
+
function Probe(): null {
|
|
115
|
+
const state = useAgentRun({
|
|
116
|
+
sendUrl: "http://app.test/send",
|
|
117
|
+
approveUrl: "http://app.test/approve",
|
|
118
|
+
denyUrl: "http://app.test/deny",
|
|
119
|
+
followUrl: (runId) => `http://app.test/runs/${runId}/events`,
|
|
120
|
+
fetch: fetchFn,
|
|
121
|
+
});
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
view = state;
|
|
124
|
+
});
|
|
125
|
+
view = state;
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
root = createRoot(happy.document.createElement("div"));
|
|
130
|
+
await act(async () => {
|
|
131
|
+
root?.render(createElement(Probe));
|
|
132
|
+
});
|
|
133
|
+
await act(async () => {
|
|
134
|
+
await view?.send("hi");
|
|
135
|
+
});
|
|
136
|
+
expect(view?.status).toBe("approval");
|
|
137
|
+
expect(view?.text).toBe("Hel");
|
|
138
|
+
await act(async () => {
|
|
139
|
+
await view?.approve();
|
|
140
|
+
});
|
|
141
|
+
const start = Date.now();
|
|
142
|
+
while (view?.text !== "Hello") {
|
|
143
|
+
if (Date.now() - start > 1_000) throw new Error(`text stayed ${view?.text}`);
|
|
144
|
+
await act(async () => {
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
expect(view?.text).toBe("Hello");
|
|
149
|
+
expect(follows).toBe(1);
|
|
150
|
+
if (lastEventId !== "3") throw new Error(`last event id ${String(lastEventId)}`);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("a booted app shows the model text once across a real approval", async () => {
|
|
154
|
+
const calls: unknown[] = [];
|
|
155
|
+
const store = createMemoryJournalStore();
|
|
156
|
+
const ops = gate.policy("ops", () => true);
|
|
157
|
+
const runtime = createAiRuntime({
|
|
158
|
+
journalStore: store,
|
|
159
|
+
models: [ai.model("smart")],
|
|
160
|
+
gates: createGateRuntime({ gates: [ops] }),
|
|
161
|
+
agents: [
|
|
162
|
+
ai.agent("support", {
|
|
163
|
+
model: "smart",
|
|
164
|
+
maxSteps: 4,
|
|
165
|
+
tools: [{ name: "refund", approval: true, gate: "ops", timeout: "1h" }],
|
|
166
|
+
}),
|
|
167
|
+
],
|
|
168
|
+
clients: {
|
|
169
|
+
smart: {
|
|
170
|
+
driverId: "mock",
|
|
171
|
+
model: "smart",
|
|
172
|
+
async complete() {
|
|
173
|
+
return { text: "", raw: {}, model: "smart", driverId: "mock" as const };
|
|
174
|
+
},
|
|
175
|
+
async *stream(opts: { messages: readonly AiMessage[] }) {
|
|
176
|
+
const answered = opts.messages.some((message) => message.role === "tool");
|
|
177
|
+
if (!answered) {
|
|
178
|
+
yield { text: "Hello" };
|
|
179
|
+
yield {
|
|
180
|
+
text: "",
|
|
181
|
+
toolCall: { index: 0, id: "tc1", name: "refund", argumentsDelta: "{}" },
|
|
182
|
+
};
|
|
183
|
+
yield { text: "", done: true as const };
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
yield { text: "!" };
|
|
187
|
+
yield { text: "", done: true as const };
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
const refund = flow("refund", {
|
|
193
|
+
do: async (input) => {
|
|
194
|
+
calls.push(input);
|
|
195
|
+
return { refunded: true };
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const assist: Binding = {
|
|
199
|
+
trigger: http.post("/assist"),
|
|
200
|
+
flow: flow("assist", {
|
|
201
|
+
durable: true,
|
|
202
|
+
effects: { asks: ["support"], calls: ["refund"] },
|
|
203
|
+
do: (_input, fx) =>
|
|
204
|
+
fx.json.stream(fx.run("support", { message: "refund" }, { stream: true })),
|
|
205
|
+
}) as AnyFlowDef,
|
|
206
|
+
};
|
|
207
|
+
resetBindings();
|
|
208
|
+
const app = oke({
|
|
209
|
+
name: "use-agent-run",
|
|
210
|
+
env: "test",
|
|
211
|
+
startScheduler: false,
|
|
212
|
+
registry: "ignore",
|
|
213
|
+
gate: { unguardedHttp: "allow", policies: [ops] },
|
|
214
|
+
bindings: [assist, { trigger: http.post("/refund"), flow: refund as AnyFlowDef }],
|
|
215
|
+
elements: {
|
|
216
|
+
journal: { store, instanceId: "use-agent-run", leaseMs: 30_000, driverId: "memory" },
|
|
217
|
+
ai: runtime,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
await app.boot({ env: "test" });
|
|
221
|
+
const fetchFn = Object.assign(
|
|
222
|
+
async (input: string | URL | Request, init?: RequestInit) => {
|
|
223
|
+
const request = input instanceof Request ? input : new Request(String(input), init);
|
|
224
|
+
return app.fetch(request);
|
|
225
|
+
},
|
|
226
|
+
{ preconnect: () => undefined },
|
|
227
|
+
) as typeof fetch;
|
|
228
|
+
|
|
229
|
+
let view: AgentRunState | undefined;
|
|
230
|
+
function Probe(): null {
|
|
231
|
+
const state = useAgentRun({
|
|
232
|
+
sendUrl: "http://localhost/assist",
|
|
233
|
+
approveUrl: "http://localhost/agent/approvals/approve",
|
|
234
|
+
denyUrl: "http://localhost/agent/approvals/deny",
|
|
235
|
+
followUrl: (runId) => `http://localhost/agent/runs/${runId}/events`,
|
|
236
|
+
fetch: fetchFn,
|
|
237
|
+
});
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
view = state;
|
|
240
|
+
});
|
|
241
|
+
view = state;
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
root = createRoot(happy.document.createElement("div"));
|
|
246
|
+
await act(async () => {
|
|
247
|
+
root?.render(createElement(Probe));
|
|
248
|
+
});
|
|
249
|
+
await act(async () => {
|
|
250
|
+
await view?.send("refund");
|
|
251
|
+
});
|
|
252
|
+
expect(view?.status).toBe("approval");
|
|
253
|
+
expect(view?.text).toBe("Hello");
|
|
254
|
+
await act(async () => {
|
|
255
|
+
await view?.approve();
|
|
256
|
+
});
|
|
257
|
+
await app.resumeDurable(Date.now() + 1000);
|
|
258
|
+
const start = Date.now();
|
|
259
|
+
while (view?.text !== "Hello!") {
|
|
260
|
+
if (Date.now() - start > 3_000) throw new Error(`text stayed ${view?.text}`);
|
|
261
|
+
await act(async () => {
|
|
262
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
expect(view?.text).toBe("Hello!");
|
|
266
|
+
expect(calls).toEqual([{}]);
|
|
267
|
+
await app.bootResult?.close();
|
|
268
|
+
resetBindings();
|
|
269
|
+
resetFlowSeq();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
@@ -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
|
+
});
|