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
package/src/compiler/extract.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { SearchConfigError } from "../elements/store/search-errors.ts";
|
|
|
28
28
|
import type {
|
|
29
29
|
Ai,
|
|
30
30
|
AiAgent,
|
|
31
|
+
AiDecision,
|
|
31
32
|
AiMcpServer,
|
|
32
33
|
AiModel,
|
|
33
34
|
AiPrompt,
|
|
@@ -257,11 +258,18 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
|
|
|
257
258
|
if (Object.keys(gates).length > 0) manifest.gates = gates;
|
|
258
259
|
if (Object.keys(vault).length > 0) manifest.vault = vault;
|
|
259
260
|
if (Object.keys(channels).length > 0) manifest.channels = channels;
|
|
260
|
-
if (
|
|
261
|
+
if (
|
|
262
|
+
scope.ai.models ||
|
|
263
|
+
scope.ai.prompts ||
|
|
264
|
+
scope.ai.agents ||
|
|
265
|
+
scope.ai.decisions ||
|
|
266
|
+
scope.ai.mcpServers
|
|
267
|
+
) {
|
|
261
268
|
manifest.ai = {
|
|
262
269
|
...(scope.ai.models ? { models: sortRecord(scope.ai.models) } : {}),
|
|
263
270
|
...(scope.ai.prompts ? { prompts: sortRecord(scope.ai.prompts) } : {}),
|
|
264
271
|
...(scope.ai.agents ? { agents: sortRecord(scope.ai.agents) } : {}),
|
|
272
|
+
...(scope.ai.decisions ? { decisions: sortRecord(scope.ai.decisions) } : {}),
|
|
265
273
|
...(scope.ai.mcpServers ? { mcpServers: sortRecord(scope.ai.mcpServers) } : {}),
|
|
266
274
|
};
|
|
267
275
|
}
|
|
@@ -1426,7 +1434,11 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
|
|
|
1426
1434
|
}
|
|
1427
1435
|
|
|
1428
1436
|
if (obj === "ai" && prop === "agent") {
|
|
1429
|
-
collectAgent(call, scope);
|
|
1437
|
+
collectAgent(call, program, scope);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
if (obj === "ai" && prop === "decision") {
|
|
1441
|
+
collectDecision(call, program, scope);
|
|
1430
1442
|
}
|
|
1431
1443
|
|
|
1432
1444
|
if (obj === "ai" && prop === "mcpServer") {
|
|
@@ -1494,6 +1506,7 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
|
|
|
1494
1506
|
: {}),
|
|
1495
1507
|
...(via && via.length > 0 ? { via } : {}),
|
|
1496
1508
|
...(timeout !== undefined ? { timeout } : {}),
|
|
1509
|
+
...(numberProp(opts, "repair") === 1 ? { repair: 1 as const } : {}),
|
|
1497
1510
|
};
|
|
1498
1511
|
scope.ai.prompts = scope.ai.prompts ?? {};
|
|
1499
1512
|
scope.ai.prompts[promptName] = prompt;
|
|
@@ -1648,27 +1661,267 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
|
|
|
1648
1661
|
}
|
|
1649
1662
|
}
|
|
1650
1663
|
|
|
1651
|
-
function
|
|
1664
|
+
function agentToolEntry(
|
|
1665
|
+
el: AstNode,
|
|
1666
|
+
scope: ProjectScope,
|
|
1667
|
+
): { name: string; approval: boolean } | undefined {
|
|
1668
|
+
if (el.type === "ObjectExpression") {
|
|
1669
|
+
const nameNode = objectProp(el, "name");
|
|
1670
|
+
const raw =
|
|
1671
|
+
(nameNode ? resolveCallTarget(nameNode, scope.bindings) : undefined) ??
|
|
1672
|
+
stringArg(nameNode) ??
|
|
1673
|
+
identifierName(nameNode);
|
|
1674
|
+
if (!raw) return undefined;
|
|
1675
|
+
const name = scope.flowExports.get(raw) ?? scope.bindings.get(raw)?.ref ?? raw;
|
|
1676
|
+
const approvalNode = objectProp(el, "approval");
|
|
1677
|
+
return { name, approval: approvalNode !== undefined && boolProp(el, "approval") !== false };
|
|
1678
|
+
}
|
|
1679
|
+
const resolved = resolveCallTarget(el, scope.bindings);
|
|
1680
|
+
const raw = resolved ?? identifierName(el) ?? stringArg(el);
|
|
1681
|
+
if (!raw) return undefined;
|
|
1682
|
+
const name = scope.flowExports.get(raw) ?? scope.bindings.get(raw)?.ref ?? raw;
|
|
1683
|
+
return { name, approval: false };
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
function assertDecisionPlacement(flowName: string, flow: Flow, scope: ProjectScope): void {
|
|
1687
|
+
const names = flow.effects?.decides ?? [];
|
|
1688
|
+
if (names.length === 0) return;
|
|
1689
|
+
const http = flow.trigger !== undefined && "http" in flow.trigger;
|
|
1690
|
+
for (const decisionName of names) {
|
|
1691
|
+
const decision = scope.ai.decisions?.[decisionName];
|
|
1692
|
+
if (!decision || decision.mode !== "review") continue;
|
|
1693
|
+
if (http) {
|
|
1694
|
+
throw new Error(
|
|
1695
|
+
`ai.decision("${decisionName}") review cannot run in HTTP flow "${flowName}" — a park answers 204. fx.emit a signal and decide in a consumer.`,
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
if (flow.durable !== true) {
|
|
1699
|
+
throw new Error(
|
|
1700
|
+
`ai.decision("${decisionName}"): flow "${flowName}" must set durable: true to review`,
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function decisionReviewName(node: AstNode | undefined, scope: ProjectScope): string {
|
|
1707
|
+
const literal = stringArg(node);
|
|
1708
|
+
if (literal) return literal;
|
|
1709
|
+
if (node?.type === "Identifier") {
|
|
1710
|
+
const name = (node as Identifier).name;
|
|
1711
|
+
return scope.gateIds.get(name) ?? name;
|
|
1712
|
+
}
|
|
1713
|
+
if (node?.type === "CallExpression") {
|
|
1714
|
+
const arg = stringArg((node as CallExpression).arguments[0]);
|
|
1715
|
+
if (arg) return arg;
|
|
1716
|
+
}
|
|
1717
|
+
if (node?.type === "MemberExpression") {
|
|
1718
|
+
const member = node as AstNode & { property: AstNode; computed?: boolean };
|
|
1719
|
+
if (member.property.type === "Identifier" && member.computed !== true) {
|
|
1720
|
+
return (member.property as Identifier).name;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
throw new Error("ai.decision: review gate name could not be resolved");
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function assertDecisionQuestion(
|
|
1727
|
+
decisionName: string,
|
|
1728
|
+
key: string,
|
|
1729
|
+
value: AstNode | undefined,
|
|
1730
|
+
program: AstNode,
|
|
1731
|
+
): void {
|
|
1732
|
+
const resolved = resolveDecisionExpr(value, program, decisionName, key);
|
|
1733
|
+
if (!resolved || resolved.type !== "CallExpression") {
|
|
1734
|
+
throw new Error(
|
|
1735
|
+
`ai.decision("${decisionName}"): question "${key}" must be ai.choice, ai.score, or ai.boolean`,
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
const callee = (resolved as CallExpression).callee;
|
|
1739
|
+
const member = callee as AstNode & { property?: AstNode };
|
|
1740
|
+
const fn =
|
|
1741
|
+
callee.type === "MemberExpression" && member.property?.type === "Identifier"
|
|
1742
|
+
? (member.property as Identifier).name
|
|
1743
|
+
: undefined;
|
|
1744
|
+
const args = (resolved as CallExpression).arguments;
|
|
1745
|
+
if (fn === "choice") {
|
|
1746
|
+
const options = args[1];
|
|
1747
|
+
if (!options || options.type !== "ObjectExpression") {
|
|
1748
|
+
throw new Error(
|
|
1749
|
+
`ai.decision("${decisionName}"): choice "${key}" options must be an object literal`,
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
const keys = objectProperties(options)
|
|
1753
|
+
.map((prop) => propKey(prop))
|
|
1754
|
+
.filter((name): name is string => typeof name === "string");
|
|
1755
|
+
if (keys.includes("none_of_these")) {
|
|
1756
|
+
throw new Error(`ai.decision("${decisionName}"): choice "${key}" must not set none_of_these`);
|
|
1757
|
+
}
|
|
1758
|
+
if (keys.length > 254) {
|
|
1759
|
+
throw new Error(
|
|
1760
|
+
`ai.decision("${decisionName}"): choice "${key}" has ${keys.length} options; max 254`,
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
if (fn === "score") {
|
|
1765
|
+
const levels = resolveScoreLevels(args[1], program, decisionName, key);
|
|
1766
|
+
const count = ((levels as AstNode & { elements?: unknown[] }).elements ?? []).length;
|
|
1767
|
+
if (count < 2 || count > 10) {
|
|
1768
|
+
throw new Error(`ai.decision("${decisionName}"): score "${key}" needs 2–10 levels`);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function resolveDecisionExpr(
|
|
1774
|
+
value: AstNode | undefined,
|
|
1775
|
+
program: AstNode,
|
|
1776
|
+
decisionName: string,
|
|
1777
|
+
key: string,
|
|
1778
|
+
): AstNode | undefined {
|
|
1779
|
+
if (!value) return undefined;
|
|
1780
|
+
if (value.type !== "Identifier") return value;
|
|
1781
|
+
const name = (value as Identifier).name;
|
|
1782
|
+
const init = sameFileConstInit(name, program);
|
|
1783
|
+
if (!init) {
|
|
1784
|
+
throw new Error(
|
|
1785
|
+
`ai.decision("${decisionName}"): question "${key}" is "${name}", which is not a same-file const`,
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
return init;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
function resolveScoreLevels(
|
|
1792
|
+
levels: AstNode | undefined,
|
|
1793
|
+
program: AstNode,
|
|
1794
|
+
decisionName: string,
|
|
1795
|
+
key: string,
|
|
1796
|
+
): AstNode {
|
|
1797
|
+
const resolved =
|
|
1798
|
+
levels?.type === "Identifier"
|
|
1799
|
+
? sameFileConstInit((levels as Identifier).name, program)
|
|
1800
|
+
: levels;
|
|
1801
|
+
if (!resolved || resolved.type !== "ArrayExpression") {
|
|
1802
|
+
const label = levels?.type === "Identifier" ? (levels as Identifier).name : "the levels";
|
|
1803
|
+
throw new Error(
|
|
1804
|
+
`ai.decision("${decisionName}"): score "${key}" levels (${label}) must be an array literal or a same-file const`,
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
return resolved;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
function sameFileConstInit(name: string, program: AstNode): AstNode | undefined {
|
|
1811
|
+
let found: AstNode | undefined;
|
|
1812
|
+
walk(program, (node) => {
|
|
1813
|
+
if (found || node.type !== "VariableDeclarator") return;
|
|
1814
|
+
const decl = node as AstNode & { id?: AstNode; init?: AstNode };
|
|
1815
|
+
if (decl.id?.type === "Identifier" && (decl.id as Identifier).name === name) {
|
|
1816
|
+
found = decl.init;
|
|
1817
|
+
}
|
|
1818
|
+
});
|
|
1819
|
+
return found;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
function collectDecision(call: CallExpression, program: AstNode, scope: ProjectScope): void {
|
|
1823
|
+
const decisionName = stringArg(call.arguments[0]);
|
|
1824
|
+
const opts = objectArg(call.arguments[1]);
|
|
1825
|
+
if (!decisionName || !opts) return;
|
|
1826
|
+
const ask = objectProp(opts, "ask");
|
|
1827
|
+
const questionProps = ask?.type === "ObjectExpression" ? objectProperties(ask) : [];
|
|
1828
|
+
const questionNames = questionProps
|
|
1829
|
+
.map((prop) => propKey(prop))
|
|
1830
|
+
.filter((name): name is string => typeof name === "string" && name.length > 0);
|
|
1831
|
+
if (questionNames.length === 0) {
|
|
1832
|
+
throw new Error(`ai.decision("${decisionName}"): ask is empty`);
|
|
1833
|
+
}
|
|
1834
|
+
for (const prop of questionProps) {
|
|
1835
|
+
const key = propKey(prop);
|
|
1836
|
+
if (!key) continue;
|
|
1837
|
+
if (key === "meta" || key === "$") {
|
|
1838
|
+
throw new Error(`ai.decision("${decisionName}"): question id "${key}" is reserved`);
|
|
1839
|
+
}
|
|
1840
|
+
assertDecisionQuestion(
|
|
1841
|
+
decisionName,
|
|
1842
|
+
key,
|
|
1843
|
+
(prop as AstNode & { value?: AstNode }).value,
|
|
1844
|
+
program,
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
if (scope.ai.decisions?.[decisionName]) {
|
|
1848
|
+
throw new Error(`ai.decision("${decisionName}"): duplicate decision name`);
|
|
1849
|
+
}
|
|
1850
|
+
const hasReview = objectProp(opts, "review") !== undefined;
|
|
1851
|
+
const uncertain = stringProp(opts, "onUncertain");
|
|
1852
|
+
const abstain = uncertain === "abstain";
|
|
1853
|
+
if (hasReview === abstain) {
|
|
1854
|
+
throw new Error(
|
|
1855
|
+
`ai.decision("${decisionName}"): declare exactly one of review or onUncertain: "abstain"`,
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1858
|
+
const autonomyNode = objectProp(opts, "autonomy");
|
|
1859
|
+
let autonomy: AiDecision["autonomy"];
|
|
1860
|
+
if (autonomyNode?.type === "ObjectExpression") {
|
|
1861
|
+
if (objectProp(autonomyNode, "audit") === undefined) {
|
|
1862
|
+
throw new Error(`ai.decision("${decisionName}"): autonomy requires audit`);
|
|
1863
|
+
}
|
|
1864
|
+
const maxError = numberProp(autonomyNode, "maxError");
|
|
1865
|
+
const audit = numberProp(autonomyNode, "audit");
|
|
1866
|
+
const risk = numberProp(autonomyNode, "risk");
|
|
1867
|
+
if (maxError === undefined || audit === undefined) {
|
|
1868
|
+
throw new Error(`ai.decision("${decisionName}"): autonomy requires maxError and audit`);
|
|
1869
|
+
}
|
|
1870
|
+
autonomy = { maxError, audit, ...(risk !== undefined ? { risk } : {}) };
|
|
1871
|
+
}
|
|
1872
|
+
const evals = stringProp(opts, "evals");
|
|
1873
|
+
const decision: AiDecision = {
|
|
1874
|
+
mode: abstain ? "abstain" : "review",
|
|
1875
|
+
questions: questionNames,
|
|
1876
|
+
...(hasReview ? { review: decisionReviewName(objectProp(opts, "review"), scope) } : {}),
|
|
1877
|
+
...(stringProp(opts, "model") ? { model: stringProp(opts, "model") } : {}),
|
|
1878
|
+
driverId: stringProp(opts, "driverId") === "typesafe" ? "typesafe" : "openrouter",
|
|
1879
|
+
...(evals !== undefined ? { evals } : {}),
|
|
1880
|
+
...(autonomy !== undefined ? { autonomy } : {}),
|
|
1881
|
+
};
|
|
1882
|
+
scope.ai.decisions = scope.ai.decisions ?? {};
|
|
1883
|
+
scope.ai.decisions[decisionName] = decision;
|
|
1884
|
+
const bindingName = enclosingConstName(call, program);
|
|
1885
|
+
if (bindingName) {
|
|
1886
|
+
scope.bindings.set(bindingName, { kind: "decision", ref: decisionName });
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
function collectAgent(call: CallExpression, program: AstNode, scope: ProjectScope): void {
|
|
1652
1891
|
const agentName = stringArg(call.arguments[0]);
|
|
1653
1892
|
const opts = objectArg(call.arguments[1]);
|
|
1654
1893
|
if (!agentName || !opts) return;
|
|
1655
1894
|
const toolsArr = arrayProp(opts, "tools");
|
|
1656
|
-
const
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1895
|
+
const entries = (toolsArr ?? [])
|
|
1896
|
+
.map((el) => agentToolEntry(el, scope))
|
|
1897
|
+
.filter((entry): entry is { name: string; approval: boolean } => entry !== undefined);
|
|
1898
|
+
const tools = entries.map((entry) => entry.name);
|
|
1899
|
+
const agentCalls = tools.filter(
|
|
1900
|
+
(name) => scope.bindings.get(name)?.kind === "agent" || scope.ai.agents?.[name],
|
|
1901
|
+
);
|
|
1902
|
+
const approvals = Object.fromEntries(
|
|
1903
|
+
entries.filter((entry) => entry.approval).map((entry) => [entry.name, true as const]),
|
|
1904
|
+
);
|
|
1664
1905
|
const agent: AiAgent = {
|
|
1665
|
-
...(tools
|
|
1906
|
+
...(tools.length > 0 ? { tools } : {}),
|
|
1907
|
+
...(Object.keys(approvals).length > 0 ? { approvals } : {}),
|
|
1666
1908
|
...(numberProp(opts, "maxSteps") !== undefined
|
|
1667
1909
|
? { maxSteps: numberProp(opts, "maxSteps") }
|
|
1668
1910
|
: {}),
|
|
1911
|
+
...(numberProp(opts, "maxDepth") !== undefined
|
|
1912
|
+
? { maxDepth: numberProp(opts, "maxDepth") }
|
|
1913
|
+
: {}),
|
|
1669
1914
|
};
|
|
1670
1915
|
scope.ai.agents = scope.ai.agents ?? {};
|
|
1671
1916
|
scope.ai.agents[agentName] = agent;
|
|
1917
|
+
const bindingName = enclosingConstName(call, program);
|
|
1918
|
+
if (bindingName) {
|
|
1919
|
+
scope.bindings.set(bindingName, {
|
|
1920
|
+
kind: "agent",
|
|
1921
|
+
ref: agentName,
|
|
1922
|
+
...(agentCalls.length > 0 ? { agentCalls } : {}),
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1672
1925
|
}
|
|
1673
1926
|
|
|
1674
1927
|
function collectMcpServer(call: CallExpression, program: AstNode, scope: ProjectScope): void {
|
|
@@ -2010,6 +2263,7 @@ function registerFlow(args: {
|
|
|
2010
2263
|
flow.source = `${args.file.path}:${line}`;
|
|
2011
2264
|
|
|
2012
2265
|
if (boolProp(opts, "durable")) flow.durable = true;
|
|
2266
|
+
assertDecisionPlacement(name, flow, args.scope);
|
|
2013
2267
|
if (typeof liveFromTrigger === "string") {
|
|
2014
2268
|
flow.live = liveFromTrigger;
|
|
2015
2269
|
// Manual live surface — `.live(tableBinding)` on the trigger. Synthesize
|
|
@@ -2908,6 +3162,7 @@ function parseEffectsObject(node: AstNode | undefined): Effects | undefined {
|
|
|
2908
3162
|
const secrets = stringArrayProp(node, "secrets");
|
|
2909
3163
|
const calls = stringArrayProp(node, "calls");
|
|
2910
3164
|
const fetches = stringArrayProp(node, "fetches");
|
|
3165
|
+
const decides = stringArrayProp(node, "decides");
|
|
2911
3166
|
if (reads) effects.reads = reads as Effects["reads"];
|
|
2912
3167
|
if (writes) effects.writes = writes as Effects["writes"];
|
|
2913
3168
|
if (emits) effects.emits = emits;
|
|
@@ -2917,6 +3172,7 @@ function parseEffectsObject(node: AstNode | undefined): Effects | undefined {
|
|
|
2917
3172
|
if (secrets) effects.secrets = secrets;
|
|
2918
3173
|
if (calls) effects.calls = calls;
|
|
2919
3174
|
if (fetches) effects.fetches = fetches;
|
|
3175
|
+
if (decides) effects.decides = decides;
|
|
2920
3176
|
return effects;
|
|
2921
3177
|
}
|
|
2922
3178
|
|
package/src/compiler/response.ts
CHANGED
|
@@ -70,6 +70,11 @@ export function encodeFailure(failure: FlowFailure): Response {
|
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/** SSE id reader. Kept off the edge bundle until a stream chunk is encoded. */
|
|
74
|
+
function loadSseId(): typeof import("../kernel/sse-id.ts") {
|
|
75
|
+
return lazyRequire(`${import.meta.dir}/../kernel`, ["sse", "id"].join("-"));
|
|
76
|
+
}
|
|
77
|
+
|
|
73
78
|
function loadFxLiveStream(): typeof import("../kernel/fx-live-stream.ts") {
|
|
74
79
|
return lazyRequire(`${import.meta.dir}/../kernel`, ["fx", "live", "stream"].join("-"));
|
|
75
80
|
}
|
|
@@ -133,6 +138,24 @@ export async function encodeExecuteResult(result: {
|
|
|
133
138
|
return encodeSuccess(result.output);
|
|
134
139
|
}
|
|
135
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Durable park thrown from a streamed agent after the interrupt frame.
|
|
143
|
+
*
|
|
144
|
+
* Duck-typed so this encoder does not import the journal module.
|
|
145
|
+
*
|
|
146
|
+
* @param err - Value thrown by the chunk iterator
|
|
147
|
+
*/
|
|
148
|
+
function isStreamPark(err: unknown): err is { readonly wakeAt: number; readonly label: string } {
|
|
149
|
+
return (
|
|
150
|
+
err instanceof Error &&
|
|
151
|
+
err.name === "JournalSuspend" &&
|
|
152
|
+
"wakeAt" in err &&
|
|
153
|
+
typeof err.wakeAt === "number" &&
|
|
154
|
+
"label" in err &&
|
|
155
|
+
typeof err.label === "string"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
136
159
|
function encodeSseStream(carrier: JsonStreamResult): Response {
|
|
137
160
|
const encoder = new TextEncoder();
|
|
138
161
|
let finalized = false;
|
|
@@ -147,8 +170,12 @@ function encodeSseStream(carrier: JsonStreamResult): Response {
|
|
|
147
170
|
for await (const chunk of carrier.chunks) {
|
|
148
171
|
const frame = loadFxJson().isSseFrame(chunk)
|
|
149
172
|
? chunk
|
|
150
|
-
: { data: chunk as unknown, id:
|
|
173
|
+
: { data: chunk as unknown, id: loadSseId().readSseId(chunk) };
|
|
151
174
|
const lines: string[] = [];
|
|
175
|
+
if ("comment" in frame && frame.comment && frame.data === undefined) {
|
|
176
|
+
controller.enqueue(encoder.encode(`: ${frame.comment}\n\n`));
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
152
179
|
if (frame.id !== undefined) lines.push(`id: ${frame.id}`);
|
|
153
180
|
lines.push(`data: ${JSON.stringify(frame.data)}`);
|
|
154
181
|
controller.enqueue(encoder.encode(`${lines.join("\n")}\n\n`));
|
|
@@ -156,7 +183,13 @@ function encodeSseStream(carrier: JsonStreamResult): Response {
|
|
|
156
183
|
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
157
184
|
controller.close();
|
|
158
185
|
} catch (err) {
|
|
159
|
-
|
|
186
|
+
if (isStreamPark(err)) {
|
|
187
|
+
carrier.parked = { wakeAt: err.wakeAt, label: err.label };
|
|
188
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
189
|
+
controller.close();
|
|
190
|
+
} else {
|
|
191
|
+
controller.error(err);
|
|
192
|
+
}
|
|
160
193
|
} finally {
|
|
161
194
|
await finish();
|
|
162
195
|
}
|
package/src/config/index.ts
CHANGED
|
@@ -508,12 +508,30 @@ export function resolveDriverId(
|
|
|
508
508
|
env: ConfigEnv,
|
|
509
509
|
defaults?: EnvDriverMap,
|
|
510
510
|
): string | undefined {
|
|
511
|
+
return driverRefId(resolveDriverRef(map, env, defaults));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Resolve the rich driver ref for an env (`pool`, `replicas`, `url`).
|
|
516
|
+
*
|
|
517
|
+
* Same merge rules as {@link resolveDriverId}. A string pin has no pool or
|
|
518
|
+
* replicas. With `defaults`, an unset env key keeps that driver's default
|
|
519
|
+
* pin rather than a sibling environment.
|
|
520
|
+
*
|
|
521
|
+
* @param map - Env → driver map (or bare ref)
|
|
522
|
+
* @param env - Active environment
|
|
523
|
+
* @param defaults - Real default map for this specific driver (per-key merge)
|
|
524
|
+
*/
|
|
525
|
+
export function resolveDriverRef(
|
|
526
|
+
map: EnvDriverInput | undefined,
|
|
527
|
+
env: ConfigEnv,
|
|
528
|
+
defaults?: EnvDriverMap,
|
|
529
|
+
): DriverRef | undefined {
|
|
511
530
|
const normalized = normalizeEnvDriverMap(map);
|
|
512
531
|
if (defaults) {
|
|
513
532
|
const merged = mergeEnvDriverMap(normalized, defaults);
|
|
514
|
-
return
|
|
533
|
+
return merged[env];
|
|
515
534
|
}
|
|
516
535
|
if (!normalized) return undefined;
|
|
517
|
-
|
|
518
|
-
return driverRefId(ref);
|
|
536
|
+
return normalized[env] ?? normalized.dev ?? normalized.prod ?? normalized.test;
|
|
519
537
|
}
|