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.
- package/AGENTS.md +7 -0
- package/manifest.v1.schema.json +44 -0
- package/package.json +32 -19
- package/site/content/docs/ai/mcp.mdx +13 -9
- 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/gate/auth.mdx +4 -1
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- 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 +7 -6
- 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/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/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/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-infer.ts +27 -1
- package/src/compiler/extract.ts +268 -12
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- 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-DDKkhT9v.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-62Xts8Xi.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-DyeoKHKA.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DvfpLXsU.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-BY03SeCg.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-CgkKlA56.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-Y6HLjyJ5.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-DLPfNy-_.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-D7nNzpgB.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-DdL19zzh.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bb3FWUu9.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.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-CcE-SXC8.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.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-B54v4Yit.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.ts +289 -2
- 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/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/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1186 -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/clock/durable.ts +27 -1
- package/src/elements/store/sql-session.test.ts +51 -0
- package/src/elements/store/sql-session.ts +111 -1
- 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 +103 -1
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot.ts +15 -4
- 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-text.ts +4 -0
- package/src/kernel/errors.ts +2 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/fx-decide.ts +761 -0
- package/src/kernel/fx.test.ts +12 -5
- package/src/kernel/fx.ts +312 -29
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/journal.ts +104 -0
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/sse-id.ts +32 -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/session.ts +3 -0
- package/src/mcp/tools.ts +168 -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
|
@@ -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
|
/**
|