okengine 0.21.1 → 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 +11 -4
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +15 -9
- package/manifest.v1.schema.json +54 -0
- package/package.json +53 -38
- package/site/content/docs/ai/mcp.mdx +13 -9
- package/site/content/docs/ai/skills.mdx +2 -2
- package/site/content/docs/client/calling.mdx +27 -19
- package/site/content/docs/client/index.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/email.mdx +1 -1
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/flow/http.mdx +4 -4
- package/site/content/docs/elements/flow/index.mdx +3 -3
- package/site/content/docs/elements/flow/meta.json +1 -1
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/gate/tenancy.mdx +1 -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/elements/vault/rotation.mdx +15 -0
- package/site/content/docs/plugins/otp.mdx +31 -31
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +1 -1
- 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 +11 -9
- package/site/content/docs/reference/errors.mdx +12 -2
- package/site/content/docs/reference/fx.mdx +35 -21
- package/site/content/docs/reference/idempotency.mdx +192 -0
- package/site/content/docs/reference/index.mdx +6 -1
- package/site/content/docs/reference/meta.json +2 -1
- package/site/content/docs/understand/meta.json +1 -1
- package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
- package/site/content/docs/understand/the-architecture.mdx +14 -12
- package/site/content/docs/understand/try-it.mdx +3 -3
- 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/competitor-mention-removal.test.ts +3 -3
- 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/docker-cli.test.ts +1 -1
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +14 -1
- package/src/cli/load-config.images.test.ts +12 -12
- package/src/cli/load-config.ts +2 -2
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/cli/vault-cmd.test.ts +23 -0
- package/src/cli/vault-cmd.ts +7 -0
- package/src/client/agent.ts +307 -0
- package/src/client/create.ts +18 -1
- package/src/client/transport.test.ts +245 -0
- package/src/client/transport.ts +133 -19
- package/src/client/types.ts +29 -3
- 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 +753 -17
- package/src/compiler/extract.test.ts +37 -0
- package/src/compiler/extract.ts +381 -24
- package/src/compiler/fixtures/skyport.expected.json +24 -0
- package/src/compiler/fx-follow.test.ts +164 -0
- package/src/compiler/fx-index.ts +399 -0
- 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-invoke.test.ts +2 -2
- 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-62Xts8Xi.js +1 -0
- package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.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-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.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-B3GLNVfF.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 +15 -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/docker.test.ts +13 -13
- package/src/docker/images-config.test.ts +12 -12
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/docker/stack-id.test.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 +455 -3
- package/src/drivers/meilisearch.ts +2 -0
- 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/vault-builtin.test.ts +10 -0
- package/src/drivers/vault-builtin.ts +13 -1
- package/src/drivers/vault-remote-bag.ts +5 -1
- package/src/drivers/vault-types.ts +6 -1
- 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/emit-drizzle.ts +2 -2
- package/src/elements/store/live-default.test.ts +2 -0
- package/src/elements/store/live-http.test.ts +1 -0
- package/src/elements/store/resource-list-docs.test.ts +2 -2
- package/src/elements/store/resource.test.ts +3 -3
- package/src/elements/store/schema-decl.test.ts +1 -0
- package/src/elements/store/sql-condition.ts +24 -7
- package/src/elements/store/sql-session.test.ts +51 -0
- package/src/elements/store/sql-session.ts +111 -1
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/vault/audit.test.ts +137 -0
- package/src/elements/vault/audit.ts +106 -13
- package/src/elements/vault/boot-chain.ts +9 -1
- package/src/elements/vault/builtin-adapter.ts +33 -3
- package/src/i18n/catalogs/ar.ts +9 -0
- package/src/i18n/catalogs/en.ts +9 -0
- package/src/index.ts +2 -0
- package/src/kernel/abort-scope.ts +33 -2
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +294 -19
- package/src/kernel/auto-cache.test.ts +6 -6
- 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-bind/vault.ts +1 -0
- package/src/kernel/boot.ts +34 -5
- package/src/kernel/builtin-errors.ts +10 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/client-descriptor.ts +1 -0
- package/src/kernel/concurrency.test.ts +33 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects-stamping.test.ts +2 -2
- 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 +20 -0
- package/src/kernel/errors-text.ts +4 -0
- package/src/kernel/errors.ts +2 -0
- package/src/kernel/external-effects.test.ts +36 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +24 -0
- package/src/kernel/fx-decide.ts +761 -0
- package/src/kernel/fx-fetch.ts +5 -1
- package/src/kernel/fx.test.ts +12 -5
- package/src/kernel/fx.ts +316 -29
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency-store.ts +578 -0
- package/src/kernel/idempotency.test.ts +556 -0
- package/src/kernel/idempotency.ts +304 -0
- package/src/kernel/index.ts +1 -0
- package/src/kernel/journal.ts +160 -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 +37 -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/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/otp.test.ts +65 -1
- package/src/plugins/otp.ts +59 -7
- package/src/release/absolute-regression.test.ts +63 -0
- package/src/release/http-graph.test.ts +27 -0
- package/src/release/http-graph.ts +95 -0
- package/src/release/index.ts +3 -0
- package/src/release/limits.ts +17 -2
- package/src/release/measure.ts +144 -28
- 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/test/create-test-app.test.ts +3 -1
- package/src/test/create-test-app.ts +40 -0
- package/src/test/live-signals.test.ts +1 -0
- package/src/test/provisions.integration.test.ts +1 -0
- package/src/test/tenant-isolation.test.ts +1 -0
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
- package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
- package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
- package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
|
@@ -63,6 +63,7 @@ describe("extractManifest — five trigger types", () => {
|
|
|
63
63
|
http: { method: "POST", path: "/orders" },
|
|
64
64
|
});
|
|
65
65
|
expect(manifest.flows?.["triggers.http"]?.effects?.writes).toEqual(["sql:orders"]);
|
|
66
|
+
expect(manifest.flows?.["triggers.http"]?.idempotency).toEqual({ mode: "auto", ttl: "24h" });
|
|
66
67
|
|
|
67
68
|
expect(manifest.flows?.["triggers.every"]?.trigger).toEqual({
|
|
68
69
|
every: "10m",
|
|
@@ -86,6 +87,7 @@ describe("extractManifest — five trigger types", () => {
|
|
|
86
87
|
true,
|
|
87
88
|
);
|
|
88
89
|
expect(internal?.effects?.reads).toEqual(["sql:links"]);
|
|
90
|
+
expect(internal?.idempotency).toEqual({ mode: "off", ttl: "24h" });
|
|
89
91
|
});
|
|
90
92
|
});
|
|
91
93
|
|
|
@@ -99,9 +101,44 @@ describe("extractManifest — fx.raw", () => {
|
|
|
99
101
|
});
|
|
100
102
|
|
|
101
103
|
expect(manifest.flows?.["raw.unannotated"]?.cache).toBe(false);
|
|
104
|
+
expect(manifest.flows?.["raw.unannotated"]?.usesRaw).toBe(true);
|
|
105
|
+
expect(manifest.flows?.["raw.unannotated"]?.idempotency).toEqual({ mode: "off", ttl: "24h" });
|
|
102
106
|
expect(manifest.flows?.["raw.annotated"]?.cache).toBeUndefined();
|
|
107
|
+
expect(manifest.flows?.["raw.annotated"]?.usesRaw).toBe(true);
|
|
103
108
|
expect(manifest.flows?.["raw.annotated"]?.effects?.reads).toEqual(["sql:orders"]);
|
|
104
109
|
});
|
|
110
|
+
|
|
111
|
+
test("required on a GET flow is an extract error; a bad ttl is an extract error", async () => {
|
|
112
|
+
const requiredGet = `
|
|
113
|
+
import { on, flow, http } from "okengine";
|
|
114
|
+
export const read = on(
|
|
115
|
+
http.get("/read"),
|
|
116
|
+
flow("pay.read", { idempotency: "required", do: async () => ({ ok: true }) }),
|
|
117
|
+
);
|
|
118
|
+
`;
|
|
119
|
+
await expect(extractFromSources({ "src/flows/pay.ts": requiredGet })).rejects.toThrow(
|
|
120
|
+
/header would be ignored/,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const badTtl = `
|
|
124
|
+
import { on, flow, http, store, field } from "okengine";
|
|
125
|
+
export const db = store.sql("db");
|
|
126
|
+
export const payments = store.schema.table("payments", { id: field.text().primaryKey() });
|
|
127
|
+
export const charge = on(
|
|
128
|
+
http.post("/charge"),
|
|
129
|
+
flow("pay.charge", {
|
|
130
|
+
idempotency: { ttl: "nope" },
|
|
131
|
+
do: async (input, fx) => {
|
|
132
|
+
await fx.store(db).insert(payments).values(input);
|
|
133
|
+
return { ok: true };
|
|
134
|
+
},
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
`;
|
|
138
|
+
await expect(extractFromSources({ "src/flows/pay.ts": badTtl })).rejects.toThrow(
|
|
139
|
+
/not a duration/,
|
|
140
|
+
);
|
|
141
|
+
});
|
|
105
142
|
});
|
|
106
143
|
|
|
107
144
|
describe("extractManifest — performance", () => {
|
package/src/compiler/extract.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* from source to `manifest.oke.json`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { readdir } from "node:fs/promises";
|
|
9
10
|
import { parseSync } from "oxc-parser";
|
|
10
11
|
import { join } from "node:path";
|
|
11
12
|
|
|
@@ -15,11 +16,19 @@ import {
|
|
|
15
16
|
getAiProviderEntry,
|
|
16
17
|
} from "../elements/ai/providers.ts";
|
|
17
18
|
import { buildCronExpression, type CronField } from "../elements/clock/cron-fields.ts";
|
|
19
|
+
import {
|
|
20
|
+
hasMutatingEffects,
|
|
21
|
+
idempotencyTtlMs,
|
|
22
|
+
resolveIdempotencyMode,
|
|
23
|
+
ttlOf,
|
|
24
|
+
type FlowIdempotencyOption,
|
|
25
|
+
} from "../kernel/idempotency.ts";
|
|
18
26
|
import { SearchConfigError } from "../elements/store/search-errors.ts";
|
|
19
27
|
|
|
20
28
|
import type {
|
|
21
29
|
Ai,
|
|
22
30
|
AiAgent,
|
|
31
|
+
AiDecision,
|
|
23
32
|
AiMcpServer,
|
|
24
33
|
AiModel,
|
|
25
34
|
AiPrompt,
|
|
@@ -45,6 +54,7 @@ import type {
|
|
|
45
54
|
} from "../manifest/types.ts";
|
|
46
55
|
import { sqlTableRef } from "../manifest/sql-resource.ts";
|
|
47
56
|
import {
|
|
57
|
+
effectsMissingFromDeclaration,
|
|
48
58
|
identifierName,
|
|
49
59
|
inferEffects,
|
|
50
60
|
resolveCallTarget,
|
|
@@ -56,6 +66,7 @@ import {
|
|
|
56
66
|
type InferBinding,
|
|
57
67
|
type Literal,
|
|
58
68
|
} from "./effects-infer.ts";
|
|
69
|
+
import { buildFxIndex, readTsconfigPaths, type FxIndex } from "./fx-index.ts";
|
|
59
70
|
import {
|
|
60
71
|
isFlowsTreeFile,
|
|
61
72
|
isSkippedExtractSource,
|
|
@@ -131,6 +142,8 @@ interface ProjectScope {
|
|
|
131
142
|
/** Store-wide live defaulting — `oke({ store: { live: true } })`. */
|
|
132
143
|
store?: Manifest["store"];
|
|
133
144
|
flows: Record<string, Flow>;
|
|
145
|
+
/** Project functions that can receive `fx`, built once per extract. */
|
|
146
|
+
fxIndex: FxIndex;
|
|
134
147
|
/** Export name → flow id (for agent tools). */
|
|
135
148
|
flowExports: Map<string, string>;
|
|
136
149
|
/** Local binding name → store.resource declaration (for on(http.resource)). */
|
|
@@ -168,6 +181,7 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
|
|
|
168
181
|
|
|
169
182
|
const scope: ProjectScope = {
|
|
170
183
|
app: options.app ?? "app",
|
|
184
|
+
fxIndex: buildFxIndex([]),
|
|
171
185
|
bindings: new Map(),
|
|
172
186
|
signals: {},
|
|
173
187
|
stores: {},
|
|
@@ -198,6 +212,12 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
|
|
|
198
212
|
return { file, program: result.program as unknown as AstNode };
|
|
199
213
|
});
|
|
200
214
|
|
|
215
|
+
const tsPaths = options.rootDir ? await readTsconfigPaths(options.rootDir) : [];
|
|
216
|
+
scope.fxIndex = buildFxIndex(
|
|
217
|
+
parsed.map(({ file, program }) => ({ path: file.path, program })),
|
|
218
|
+
tsPaths,
|
|
219
|
+
);
|
|
220
|
+
|
|
201
221
|
// Pass 1 — declarations (elements, config, named bindings).
|
|
202
222
|
for (const { file, program } of parsed) {
|
|
203
223
|
collectDeclarations(file.path, program, scope);
|
|
@@ -238,11 +258,18 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
|
|
|
238
258
|
if (Object.keys(gates).length > 0) manifest.gates = gates;
|
|
239
259
|
if (Object.keys(vault).length > 0) manifest.vault = vault;
|
|
240
260
|
if (Object.keys(channels).length > 0) manifest.channels = channels;
|
|
241
|
-
if (
|
|
261
|
+
if (
|
|
262
|
+
scope.ai.models ||
|
|
263
|
+
scope.ai.prompts ||
|
|
264
|
+
scope.ai.agents ||
|
|
265
|
+
scope.ai.decisions ||
|
|
266
|
+
scope.ai.mcpServers
|
|
267
|
+
) {
|
|
242
268
|
manifest.ai = {
|
|
243
269
|
...(scope.ai.models ? { models: sortRecord(scope.ai.models) } : {}),
|
|
244
270
|
...(scope.ai.prompts ? { prompts: sortRecord(scope.ai.prompts) } : {}),
|
|
245
271
|
...(scope.ai.agents ? { agents: sortRecord(scope.ai.agents) } : {}),
|
|
272
|
+
...(scope.ai.decisions ? { decisions: sortRecord(scope.ai.decisions) } : {}),
|
|
246
273
|
...(scope.ai.mcpServers ? { mcpServers: sortRecord(scope.ai.mcpServers) } : {}),
|
|
247
274
|
};
|
|
248
275
|
}
|
|
@@ -295,21 +322,50 @@ export async function extractFromSources(
|
|
|
295
322
|
}
|
|
296
323
|
|
|
297
324
|
async function readSources(rootDir: string, pattern: string): Promise<SourceFile[]> {
|
|
298
|
-
// Bun.Glob (global) — bare `import … from "bun"` is rejected by JSR.
|
|
299
|
-
const glob = new Bun.Glob(pattern);
|
|
300
325
|
const files: SourceFile[] = [];
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
326
|
+
if (pattern === "**/*.{ts,tsx}") {
|
|
327
|
+
await walkSources(rootDir, rootDir, files);
|
|
328
|
+
} else {
|
|
329
|
+
// Bun.Glob (global) — bare `import … from "bun"` is rejected by JSR.
|
|
330
|
+
const glob = new Bun.Glob(pattern);
|
|
331
|
+
for await (const path of glob.scan({ cwd: rootDir, onlyFiles: true })) {
|
|
332
|
+
if (isSkippedExtractSource(path)) continue;
|
|
333
|
+
const abs = join(rootDir, path);
|
|
334
|
+
files.push({ path: toPosixPath(path), source: await Bun.file(abs).text() });
|
|
335
|
+
}
|
|
308
336
|
}
|
|
309
337
|
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
310
338
|
return files;
|
|
311
339
|
}
|
|
312
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Walk an app tree without entering `node_modules` or directory symlinks.
|
|
343
|
+
*
|
|
344
|
+
* A template's `node_modules/okengine` link points back at this repo, which
|
|
345
|
+
* contains the template again. Globbing through that link never terminates.
|
|
346
|
+
*/
|
|
347
|
+
async function walkSources(rootDir: string, dir: string, files: SourceFile[]): Promise<void> {
|
|
348
|
+
let entries;
|
|
349
|
+
try {
|
|
350
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
351
|
+
} catch {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
for (const entry of entries) {
|
|
355
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.isSymbolicLink()) continue;
|
|
356
|
+
const abs = join(dir, entry.name);
|
|
357
|
+
if (entry.isDirectory()) {
|
|
358
|
+
await walkSources(rootDir, abs, files);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (!entry.isFile()) continue;
|
|
362
|
+
if (!entry.name.endsWith(".ts") && !entry.name.endsWith(".tsx")) continue;
|
|
363
|
+
const rel = toPosixPath(abs.slice(rootDir.length + 1));
|
|
364
|
+
if (isSkippedExtractSource(rel)) continue;
|
|
365
|
+
files.push({ path: rel, source: await Bun.file(abs).text() });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
313
369
|
function collectDeclarations(filePath: string, program: AstNode, scope: ProjectScope): void {
|
|
314
370
|
walk(program, (node) => {
|
|
315
371
|
if (node.type === "CallExpression") {
|
|
@@ -1378,7 +1434,11 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
|
|
|
1378
1434
|
}
|
|
1379
1435
|
|
|
1380
1436
|
if (obj === "ai" && prop === "agent") {
|
|
1381
|
-
collectAgent(call, scope);
|
|
1437
|
+
collectAgent(call, program, scope);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
if (obj === "ai" && prop === "decision") {
|
|
1441
|
+
collectDecision(call, program, scope);
|
|
1382
1442
|
}
|
|
1383
1443
|
|
|
1384
1444
|
if (obj === "ai" && prop === "mcpServer") {
|
|
@@ -1446,6 +1506,7 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
|
|
|
1446
1506
|
: {}),
|
|
1447
1507
|
...(via && via.length > 0 ? { via } : {}),
|
|
1448
1508
|
...(timeout !== undefined ? { timeout } : {}),
|
|
1509
|
+
...(numberProp(opts, "repair") === 1 ? { repair: 1 as const } : {}),
|
|
1449
1510
|
};
|
|
1450
1511
|
scope.ai.prompts = scope.ai.prompts ?? {};
|
|
1451
1512
|
scope.ai.prompts[promptName] = prompt;
|
|
@@ -1600,27 +1661,267 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
|
|
|
1600
1661
|
}
|
|
1601
1662
|
}
|
|
1602
1663
|
|
|
1603
|
-
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 {
|
|
1604
1891
|
const agentName = stringArg(call.arguments[0]);
|
|
1605
1892
|
const opts = objectArg(call.arguments[1]);
|
|
1606
1893
|
if (!agentName || !opts) return;
|
|
1607
1894
|
const toolsArr = arrayProp(opts, "tools");
|
|
1608
|
-
const
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
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
|
+
);
|
|
1616
1905
|
const agent: AiAgent = {
|
|
1617
|
-
...(tools
|
|
1906
|
+
...(tools.length > 0 ? { tools } : {}),
|
|
1907
|
+
...(Object.keys(approvals).length > 0 ? { approvals } : {}),
|
|
1618
1908
|
...(numberProp(opts, "maxSteps") !== undefined
|
|
1619
1909
|
? { maxSteps: numberProp(opts, "maxSteps") }
|
|
1620
1910
|
: {}),
|
|
1911
|
+
...(numberProp(opts, "maxDepth") !== undefined
|
|
1912
|
+
? { maxDepth: numberProp(opts, "maxDepth") }
|
|
1913
|
+
: {}),
|
|
1621
1914
|
};
|
|
1622
1915
|
scope.ai.agents = scope.ai.agents ?? {};
|
|
1623
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
|
+
}
|
|
1624
1925
|
}
|
|
1625
1926
|
|
|
1626
1927
|
function collectMcpServer(call: CallExpression, program: AstNode, scope: ProjectScope): void {
|
|
@@ -1882,13 +2183,17 @@ function registerFlow(args: {
|
|
|
1882
2183
|
}
|
|
1883
2184
|
args.scope.bindings.set(name, { kind: "flow", ref: name });
|
|
1884
2185
|
|
|
1885
|
-
const
|
|
2186
|
+
const effectsNode = objectProp(opts, "effects");
|
|
2187
|
+
const hasExplicitEffects = effectsNode !== undefined;
|
|
1886
2188
|
const doNode = objectProp(opts, "do");
|
|
1887
2189
|
const inferred = doNode
|
|
1888
2190
|
? inferEffects({
|
|
1889
2191
|
doNode,
|
|
1890
2192
|
bindings: args.scope.bindings,
|
|
1891
2193
|
hasExplicitEffects,
|
|
2194
|
+
flowName: name,
|
|
2195
|
+
file: args.file.path,
|
|
2196
|
+
resolveCallee: (callee, file) => args.scope.fxIndex.resolve(file, callee),
|
|
1892
2197
|
})
|
|
1893
2198
|
: {
|
|
1894
2199
|
effects: {} as Effects,
|
|
@@ -1900,8 +2205,16 @@ function registerFlow(args: {
|
|
|
1900
2205
|
};
|
|
1901
2206
|
|
|
1902
2207
|
let effects: Effects | undefined;
|
|
1903
|
-
if (hasExplicitEffects) {
|
|
1904
|
-
effects = parseEffectsObject(
|
|
2208
|
+
if (hasExplicitEffects && effectsNode?.type === "ObjectExpression") {
|
|
2209
|
+
effects = parseEffectsObject(effectsNode);
|
|
2210
|
+
const missing = effectsMissingFromDeclaration(effects ?? {}, inferred.effects);
|
|
2211
|
+
if (missing.length > 0) {
|
|
2212
|
+
throw new Error(
|
|
2213
|
+
`OKE1900: flow "${name}" declares effects that omit ${missing.join(", ")}. An effects block must include every effect inference can see.`,
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
} else if (hasExplicitEffects) {
|
|
2217
|
+
effects = parseEffectsObject(effectsNode);
|
|
1905
2218
|
} else {
|
|
1906
2219
|
effects = inferred.effects;
|
|
1907
2220
|
}
|
|
@@ -1950,6 +2263,7 @@ function registerFlow(args: {
|
|
|
1950
2263
|
flow.source = `${args.file.path}:${line}`;
|
|
1951
2264
|
|
|
1952
2265
|
if (boolProp(opts, "durable")) flow.durable = true;
|
|
2266
|
+
assertDecisionPlacement(name, flow, args.scope);
|
|
1953
2267
|
if (typeof liveFromTrigger === "string") {
|
|
1954
2268
|
flow.live = liveFromTrigger;
|
|
1955
2269
|
// Manual live surface — `.live(tableBinding)` on the trigger. Synthesize
|
|
@@ -2039,6 +2353,24 @@ function registerFlow(args: {
|
|
|
2039
2353
|
flow.cacheKeys = `computed:${effects.reads[0]}/userId`;
|
|
2040
2354
|
}
|
|
2041
2355
|
|
|
2356
|
+
const idempotencyOption = parseIdempotencyOption(opts);
|
|
2357
|
+
const httpMethod = manifestTrigger?.http?.method;
|
|
2358
|
+
const stream = boolProp(opts, "stream") === true;
|
|
2359
|
+
const live = typeof liveFromTrigger === "string";
|
|
2360
|
+
const canHonor =
|
|
2361
|
+
httpMethod !== "GET" && !stream && !live && (inferred.usesRaw || hasMutatingEffects(effects));
|
|
2362
|
+
if (idempotencyRequired(idempotencyOption) && !canHonor) {
|
|
2363
|
+
throw new Error(
|
|
2364
|
+
`flow "${name}" sets idempotency required, but the header would be ignored (read-only, GET, stream, or live).`,
|
|
2365
|
+
);
|
|
2366
|
+
}
|
|
2367
|
+
const ttl = ttlOf(idempotencyOption);
|
|
2368
|
+
if (idempotencyTtlMs(ttl) <= 0) {
|
|
2369
|
+
throw new Error(`flow "${name}" has an idempotency ttl that is not a duration ("${ttl}").`);
|
|
2370
|
+
}
|
|
2371
|
+
flow.idempotency = resolveIdempotencyMode(idempotencyOption, canHonor);
|
|
2372
|
+
if (inferred.usesRaw) flow.usesRaw = true;
|
|
2373
|
+
|
|
2042
2374
|
putFlow(args.scope, name, flow);
|
|
2043
2375
|
}
|
|
2044
2376
|
|
|
@@ -2830,6 +3162,7 @@ function parseEffectsObject(node: AstNode | undefined): Effects | undefined {
|
|
|
2830
3162
|
const secrets = stringArrayProp(node, "secrets");
|
|
2831
3163
|
const calls = stringArrayProp(node, "calls");
|
|
2832
3164
|
const fetches = stringArrayProp(node, "fetches");
|
|
3165
|
+
const decides = stringArrayProp(node, "decides");
|
|
2833
3166
|
if (reads) effects.reads = reads as Effects["reads"];
|
|
2834
3167
|
if (writes) effects.writes = writes as Effects["writes"];
|
|
2835
3168
|
if (emits) effects.emits = emits;
|
|
@@ -2839,6 +3172,7 @@ function parseEffectsObject(node: AstNode | undefined): Effects | undefined {
|
|
|
2839
3172
|
if (secrets) effects.secrets = secrets;
|
|
2840
3173
|
if (calls) effects.calls = calls;
|
|
2841
3174
|
if (fetches) effects.fetches = fetches;
|
|
3175
|
+
if (decides) effects.decides = decides;
|
|
2842
3176
|
return effects;
|
|
2843
3177
|
}
|
|
2844
3178
|
|
|
@@ -3279,6 +3613,29 @@ function numberProp(obj: AstNode | undefined, key: string): number | undefined {
|
|
|
3279
3613
|
return typeof v === "number" ? v : undefined;
|
|
3280
3614
|
}
|
|
3281
3615
|
|
|
3616
|
+
function parseIdempotencyOption(opts: AstNode | undefined): FlowIdempotencyOption | undefined {
|
|
3617
|
+
const node = objectProp(opts, "idempotency");
|
|
3618
|
+
if (!node) return undefined;
|
|
3619
|
+
if (node.type === "Literal") {
|
|
3620
|
+
const value = (node as Literal).value;
|
|
3621
|
+
if (value === false) return false;
|
|
3622
|
+
if (value === "required") return "required";
|
|
3623
|
+
}
|
|
3624
|
+
if (node.type === "ObjectExpression") {
|
|
3625
|
+
const required = boolProp(node, "required");
|
|
3626
|
+
const ttl = stringProp(node, "ttl");
|
|
3627
|
+
return {
|
|
3628
|
+
...(required !== undefined ? { required } : {}),
|
|
3629
|
+
...(ttl !== undefined ? { ttl } : {}),
|
|
3630
|
+
};
|
|
3631
|
+
}
|
|
3632
|
+
return undefined;
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
function idempotencyRequired(option: FlowIdempotencyOption | undefined): boolean {
|
|
3636
|
+
return option === "required" || (typeof option === "object" && option.required === true);
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3282
3639
|
function boolProp(obj: AstNode | undefined, key: string): boolean | undefined {
|
|
3283
3640
|
const node = objectProp(obj, key);
|
|
3284
3641
|
if (!node || node.type !== "Literal") return undefined;
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
"app": "skyport",
|
|
4
4
|
"flows": {
|
|
5
5
|
"bookings.create": {
|
|
6
|
+
"idempotency": {
|
|
7
|
+
"mode": "auto",
|
|
8
|
+
"ttl": "24h"
|
|
9
|
+
},
|
|
6
10
|
"trigger": {
|
|
7
11
|
"http": {
|
|
8
12
|
"method": "POST",
|
|
@@ -29,12 +33,20 @@
|
|
|
29
33
|
"plane": "user"
|
|
30
34
|
},
|
|
31
35
|
"bookings.getBooking": {
|
|
36
|
+
"idempotency": {
|
|
37
|
+
"mode": "off",
|
|
38
|
+
"ttl": "24h"
|
|
39
|
+
},
|
|
32
40
|
"effects": {
|
|
33
41
|
"reads": ["sql:bookings"]
|
|
34
42
|
},
|
|
35
43
|
"source": "src/flows/bookings/index.ts:50"
|
|
36
44
|
},
|
|
37
45
|
"bookings.mine": {
|
|
46
|
+
"idempotency": {
|
|
47
|
+
"mode": "off",
|
|
48
|
+
"ttl": "24h"
|
|
49
|
+
},
|
|
38
50
|
"trigger": {
|
|
39
51
|
"http": {
|
|
40
52
|
"method": "GET",
|
|
@@ -49,6 +61,10 @@
|
|
|
49
61
|
"cacheKeys": "computed:sql:bookings/userId"
|
|
50
62
|
},
|
|
51
63
|
"bookings.refundBooking": {
|
|
64
|
+
"idempotency": {
|
|
65
|
+
"mode": "auto",
|
|
66
|
+
"ttl": "24h"
|
|
67
|
+
},
|
|
52
68
|
"effects": {
|
|
53
69
|
"reads": ["sql:bookings"],
|
|
54
70
|
"writes": ["sql:bookings"]
|
|
@@ -56,6 +72,10 @@
|
|
|
56
72
|
"source": "src/flows/bookings/index.ts:54"
|
|
57
73
|
},
|
|
58
74
|
"payments.chargeBooking": {
|
|
75
|
+
"idempotency": {
|
|
76
|
+
"mode": "off",
|
|
77
|
+
"ttl": "24h"
|
|
78
|
+
},
|
|
59
79
|
"effects": {
|
|
60
80
|
"secrets": ["STRIPE_KEY"]
|
|
61
81
|
},
|
|
@@ -64,6 +84,10 @@
|
|
|
64
84
|
"steps": ["create-intent", "confirm"]
|
|
65
85
|
},
|
|
66
86
|
"support.createTicket": {
|
|
87
|
+
"idempotency": {
|
|
88
|
+
"mode": "auto",
|
|
89
|
+
"ttl": "24h"
|
|
90
|
+
},
|
|
67
91
|
"trigger": {
|
|
68
92
|
"http": {
|
|
69
93
|
"method": "POST",
|