okengine 0.22.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +7 -0
- package/manifest.v1.schema.json +44 -0
- package/package.json +32 -19
- package/site/content/docs/ai/mcp.mdx +14 -10
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/elements/ai/agents.mdx +61 -13
- package/site/content/docs/elements/ai/decide.mdx +536 -0
- package/site/content/docs/elements/ai/events.mdx +373 -0
- package/site/content/docs/elements/ai/index.mdx +9 -3
- package/site/content/docs/elements/ai/meta.json +1 -1
- package/site/content/docs/elements/ai/prompts.mdx +4 -2
- package/site/content/docs/elements/channel/index.mdx +4 -4
- package/site/content/docs/elements/channel/receipts.mdx +7 -5
- package/site/content/docs/elements/channel/sms.mdx +2 -1
- package/site/content/docs/elements/flow/index.mdx +22 -18
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/signal/index.mdx +15 -14
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/plugins/otp.mdx +3 -3
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/postgres.mdx +12 -8
- package/site/content/docs/recipes/redis.mdx +4 -4
- package/site/content/docs/reference/cli.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +9 -7
- package/site/content/docs/reference/fx.mdx +19 -10
- package/src/auth/api-key-sql.test.ts +44 -0
- package/src/auth/api-key-sql.ts +1 -0
- package/src/auth/api-keys.ts +30 -12
- package/src/auth/config.ts +6 -2
- package/src/auth/gate-auth.test.ts +20 -0
- package/src/auth/tenants.ts +17 -0
- package/src/cli/agents-md.test.ts +25 -0
- package/src/cli/ask-vault-gaps.test.ts +38 -1
- package/src/cli/ask-vault-gaps.ts +35 -2
- package/src/cli/decide.ts +174 -0
- package/src/cli/decision-lock-watch.test.ts +45 -0
- package/src/cli/decision-lock-watch.ts +51 -0
- package/src/cli/dev-app-runner.ts +34 -8
- package/src/cli/dev-auth-secret.test.ts +49 -0
- package/src/cli/dev-auth-secret.ts +49 -0
- package/src/cli/dev-hot.test.ts +41 -0
- package/src/cli/dev-hot.ts +48 -0
- package/src/cli/dev.test.ts +1 -1
- package/src/cli/dev.ts +24 -6
- package/src/cli/doctor-diff.test.ts +29 -1
- package/src/cli/doctor-diff.ts +35 -0
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.images.test.ts +2 -2
- package/src/cli/load-config.ts +1 -1
- package/src/cli/manifest-pr-diff.ts +16 -0
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/client/agent.ts +307 -0
- package/src/client/transport.test.ts +20 -0
- package/src/client/transport.ts +2 -1
- package/src/client/types.ts +1 -1
- package/src/client-react/index.ts +7 -1
- package/src/client-react/use-agent-run.test.ts +271 -0
- package/src/client-react/use-agent-run.ts +193 -0
- package/src/compiler/ai-approval.test.ts +36 -0
- package/src/compiler/ai-repair.test.ts +22 -0
- package/src/compiler/decisions.extract.test.ts +164 -0
- package/src/compiler/effects-fetch.test.ts +77 -0
- package/src/compiler/effects-infer.ts +114 -17
- package/src/compiler/effects-join.test.ts +48 -0
- package/src/compiler/extract.test.ts +28 -0
- package/src/compiler/extract.ts +279 -13
- package/src/compiler/fx-index.ts +58 -4
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/index.ts +1 -1
- package/src/console/server/ai-runs-flows.ts +269 -0
- package/src/console/server/ai-runs.test.ts +323 -0
- package/src/console/server/ai-runs.ts +562 -0
- package/src/console/server/ai.ts +12 -1
- package/src/console/server/app.ts +10 -1
- package/src/console/server/decisions-flows.ts +157 -0
- package/src/console/server/decisions.test.ts +66 -0
- package/src/console/server/decisions.ts +229 -0
- package/src/console/server/flows.ts +50 -2
- package/src/console/server/invoke-user-flow.ts +9 -0
- package/src/console/server/runs-ingest.ts +16 -0
- package/src/console/server/serve.ts +5 -0
- package/src/console/server/state.ts +25 -1
- package/src/console/server/store-stats.test.ts +47 -0
- package/src/console/server/store-stats.ts +92 -18
- package/src/console/ui-next/dist/assets/access-page-C_N4qvai.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-Cq1TxniW.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-BaI4uxZz.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DNyK7IIY.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-CRhBJMZR.js} +1 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
- package/src/console/ui-next/dist/assets/decisions-page-D9twj8Xy.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-CsHWlVPU.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-ZaTJAhR9.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-DQ-EMAfz.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-Cn_Fl79s.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-CmRFeWdT.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-Cn6R_dXz.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bx6EZhSe.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DmdiUrfh.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-8P6vGXnn.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-BfGvpFSv.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CWdUHbc0.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-CZOFZzay.js +1 -0
- package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-ki_ishmX.js +2 -0
- package/src/console/ui-next/dist/index.html +4 -3
- package/src/console/ui-next/seed-parked-approval.ts +126 -0
- package/src/console/ui-next/src/client.ts +224 -1
- package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
- package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
- package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
- package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
- package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
- package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
- package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
- package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
- package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
- package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
- package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
- package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
- package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
- package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
- package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
- package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
- package/src/console/ui-next/src/router.tsx +17 -0
- package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
- package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
- package/src/console/ui-next/vite.config.ts +9 -0
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/drivers/ai-anthropic.ts +212 -21
- package/src/drivers/ai-mock.ts +34 -1
- package/src/drivers/ai-openai-compatible.ts +67 -26
- package/src/drivers/ai-preconnect.ts +18 -0
- package/src/drivers/ai-stream.test.ts +62 -0
- package/src/drivers/ai-types.ts +20 -0
- package/src/drivers/journal-postgres.test.ts +22 -1
- package/src/drivers/journal-postgres.ts +511 -13
- package/src/drivers/memory-ddl.test.ts +18 -0
- package/src/drivers/memory.ts +21 -1
- package/src/drivers/pg-rls.test.ts +31 -0
- package/src/drivers/pg-rls.ts +15 -0
- package/src/drivers/postgres.test.ts +9 -0
- package/src/drivers/postgres.ts +49 -11
- package/src/drivers/signal-compete.test.ts +234 -0
- package/src/drivers/signal-postgres.ts +39 -19
- package/src/drivers/signal-redis.ts +167 -11
- package/src/drivers/signal-types.ts +12 -0
- package/src/elements/ai/agent-event-slot.ts +27 -0
- package/src/elements/ai/approval-http.ts +207 -0
- package/src/elements/ai/approval.test.ts +741 -0
- package/src/elements/ai/approval.ts +228 -0
- package/src/elements/ai/decisions/bind.ts +163 -0
- package/src/elements/ai/decisions/certificate.test.ts +291 -0
- package/src/elements/ai/decisions/certificate.ts +377 -0
- package/src/elements/ai/decisions/certify.ts +314 -0
- package/src/elements/ai/decisions/decide.test.ts +953 -0
- package/src/elements/ai/decisions/e2e.test.ts +399 -0
- package/src/elements/ai/decisions/export.test.ts +120 -0
- package/src/elements/ai/decisions/export.ts +146 -0
- package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
- package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
- package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
- package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
- package/src/elements/ai/decisions/http.test.ts +154 -0
- package/src/elements/ai/decisions/http.ts +240 -0
- package/src/elements/ai/decisions/labels.test.ts +282 -0
- package/src/elements/ai/decisions/labels.ts +257 -0
- package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
- package/src/elements/ai/decisions/openrouter.ts +36 -0
- package/src/elements/ai/decisions/provider.ts +102 -0
- package/src/elements/ai/decisions/typesafe.ts +36 -0
- package/src/elements/ai/declare.ts +252 -1
- package/src/elements/ai/events.test.ts +379 -0
- package/src/elements/ai/events.ts +138 -0
- package/src/elements/ai/mcp-protocol.ts +5 -6
- package/src/elements/ai/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1187 -105
- package/src/elements/ai/stream-turn.ts +214 -0
- package/src/elements/ai/stream.live.test.ts +76 -0
- package/src/elements/ai/subagent.test.ts +136 -0
- package/src/elements/ai.test.ts +497 -3
- package/src/elements/ai.ts +16 -1
- package/src/elements/channel/runtime.ts +11 -0
- package/src/elements/channel/sql-ledger.ts +251 -0
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/signal/runtime.ts +9 -0
- package/src/elements/signal.test.ts +1 -0
- package/src/elements/store/cache.test.ts +120 -15
- package/src/elements/store/cache.ts +200 -39
- package/src/elements/store/prepare-row.test.ts +6 -2
- package/src/elements/store/sql-session.test.ts +53 -0
- package/src/elements/store/sql-session.ts +206 -10
- package/src/elements/store.ts +1 -0
- package/src/i18n/catalogs/ar.ts +5 -0
- package/src/i18n/catalogs/en.ts +5 -0
- package/src/index.ts +2 -0
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +135 -17
- package/src/kernel/auto-cache.test.ts +228 -0
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/channel.ts +63 -2
- package/src/kernel/boot-bind/honor-config.test.ts +40 -10
- package/src/kernel/boot-bind/signal.ts +61 -25
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot.ts +31 -6
- package/src/kernel/builtin-errors.ts +2 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects.test.ts +2 -1
- package/src/kernel/effects.ts +13 -1
- package/src/kernel/element-registries.ts +3 -0
- package/src/kernel/errors-compiler.ts +11 -0
- package/src/kernel/errors-text.ts +20 -0
- package/src/kernel/errors.ts +10 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +33 -3
- package/src/kernel/fx-call-types.test.ts +32 -0
- package/src/kernel/fx-decide.ts +765 -0
- package/src/kernel/fx-sql-handle.ts +46 -7
- package/src/kernel/fx.test.ts +13 -8
- package/src/kernel/fx.ts +339 -45
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency.test.ts +2 -1
- package/src/kernel/journal.test.ts +114 -0
- package/src/kernel/journal.ts +471 -51
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline-tenant.ts +5 -1
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/pipeline.ts +5 -0
- package/src/kernel/signal-tx.ts +28 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/kernel/store-transaction.test.ts +95 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +29 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/docs-server.ts +19 -2
- package/src/mcp/mcp.test.ts +83 -0
- package/src/mcp/protocol.ts +8 -2
- package/src/mcp/server.ts +23 -2
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/mcp/versions.ts +60 -0
- package/src/plugins/index.ts +1 -0
- package/src/plugins/mena.ts +56 -0
- package/src/release/http-graph.test.ts +2 -0
- package/src/release/http-graph.ts +5 -0
- package/src/release/measure.ts +39 -14
- package/src/runs/collect.ts +4 -1
- package/src/runs/parquet.test.ts +27 -1
- package/src/runs/parquet.ts +19 -0
- package/src/runs/types.ts +31 -0
- package/src/runtime/json-code-block.test.ts +106 -47
- package/src/runtime/json-code-block.ts +1209 -46
- package/src/term.test.ts +13 -0
- package/src/term.ts +81 -49
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
- package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
- package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision labels and the drift flag on the journal driver.
|
|
3
|
+
*
|
|
4
|
+
* A sibling of `oke_idempotency`. Rows are not journal runs. Tables are
|
|
5
|
+
* created only when this store is opened, which boot does when the app
|
|
6
|
+
* declares decisions.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { mkdir } from "node:fs/promises";
|
|
12
|
+
import { dirname } from "node:path";
|
|
13
|
+
import type { DecisionCandidate, DecisionLabel } from "../elements/ai/decisions/certificate.ts";
|
|
14
|
+
|
|
15
|
+
/** SQL surface the postgres journal client already exposes. */
|
|
16
|
+
export interface DecisionLabelSql {
|
|
17
|
+
query(sql: string, params?: readonly unknown[]): Promise<Record<string, unknown>[]>;
|
|
18
|
+
exec(sql: string, params?: readonly unknown[]): Promise<{ changes: number }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Drift for one decision. A later certificate (`certifiedAt` newer) ignores it. */
|
|
22
|
+
export interface DecisionDriftFlag {
|
|
23
|
+
readonly suspended: boolean;
|
|
24
|
+
/** Epoch ms of the certificate that was current when the flag was written. */
|
|
25
|
+
readonly certifiedAt: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @deprecated Use {@link DecisionDriftFlag}. Kept for stored rows. */
|
|
29
|
+
export interface DecisionDriftRecord extends DecisionDriftFlag {}
|
|
30
|
+
|
|
31
|
+
/** Thrown when the postgres label store cannot open or query. */
|
|
32
|
+
export class DecisionLabelStoreError extends Error {
|
|
33
|
+
/**
|
|
34
|
+
* @param message - What failed
|
|
35
|
+
* @param options - Underlying driver error
|
|
36
|
+
*/
|
|
37
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
38
|
+
super(message, options);
|
|
39
|
+
this.name = "DecisionLabelStoreError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Persistence for review labels and the app drift flag. */
|
|
44
|
+
export interface DecisionLabelStore {
|
|
45
|
+
insert(label: DecisionLabel, at: number): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Labels for one decision. Omit `tenant` to read every tenant.
|
|
48
|
+
*
|
|
49
|
+
* @param decision - Decision name
|
|
50
|
+
* @param tenant - Tenant filter
|
|
51
|
+
*/
|
|
52
|
+
list(decision?: string, tenant?: string | null): Promise<DecisionLabel[]>;
|
|
53
|
+
/** Drift flags keyed by decision name. */
|
|
54
|
+
drift(): Promise<Readonly<Record<string, DecisionDriftFlag>>>;
|
|
55
|
+
/**
|
|
56
|
+
* Persist one decision's suspension flag.
|
|
57
|
+
*
|
|
58
|
+
* @param decision - Decision name
|
|
59
|
+
* @param record - Flag and the certificate time it belongs to
|
|
60
|
+
*/
|
|
61
|
+
setDrift(decision: string, record: DecisionDriftFlag): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Store one candidate on this journal. Any instance can read it after a restart.
|
|
64
|
+
*
|
|
65
|
+
* @param decision - Decision name
|
|
66
|
+
* @param entry - Lock entry the clock fitted
|
|
67
|
+
*/
|
|
68
|
+
putCandidate(decision: string, entry: DecisionCandidate): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Candidate last written for this decision.
|
|
71
|
+
*
|
|
72
|
+
* @param decision - Decision name
|
|
73
|
+
*/
|
|
74
|
+
getCandidate(decision: string): Promise<DecisionCandidate | undefined>;
|
|
75
|
+
/** Decision names that have a candidate. */
|
|
76
|
+
listCandidates(): Promise<readonly string[]>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface StoredLabel extends DecisionLabel {
|
|
80
|
+
readonly at: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* In-memory label store.
|
|
85
|
+
*/
|
|
86
|
+
export function createMemoryDecisionLabelStore(): DecisionLabelStore {
|
|
87
|
+
const rows: StoredLabel[] = [];
|
|
88
|
+
const candidates = new Map<string, DecisionCandidate>();
|
|
89
|
+
const driftFlags = new Map<string, DecisionDriftFlag>();
|
|
90
|
+
return {
|
|
91
|
+
async insert(label, at) {
|
|
92
|
+
rows.push({ ...label, at });
|
|
93
|
+
},
|
|
94
|
+
async list(decision, tenant) {
|
|
95
|
+
return rows
|
|
96
|
+
.filter((row) => decision === undefined || row.decision === decision)
|
|
97
|
+
.filter((row) => tenant === undefined || (row.tenant ?? null) === tenant)
|
|
98
|
+
.map(toLabel);
|
|
99
|
+
},
|
|
100
|
+
async drift() {
|
|
101
|
+
return Object.fromEntries(driftFlags);
|
|
102
|
+
},
|
|
103
|
+
async setDrift(decision, next) {
|
|
104
|
+
if (next.suspended) driftFlags.set(decision, next);
|
|
105
|
+
else driftFlags.delete(decision);
|
|
106
|
+
},
|
|
107
|
+
async putCandidate(decision, entry) {
|
|
108
|
+
candidates.set(decision, entry);
|
|
109
|
+
},
|
|
110
|
+
async getCandidate(decision) {
|
|
111
|
+
return candidates.get(decision);
|
|
112
|
+
},
|
|
113
|
+
async listCandidates() {
|
|
114
|
+
return [...candidates.keys()];
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* File-backed label store beside the journal file.
|
|
121
|
+
*
|
|
122
|
+
* @param path - JSON file path
|
|
123
|
+
*/
|
|
124
|
+
export function createFileDecisionLabelStore(path: string): DecisionLabelStore {
|
|
125
|
+
let rows: StoredLabel[] | undefined;
|
|
126
|
+
let candidates: Record<string, DecisionCandidate> = {};
|
|
127
|
+
let driftFlags: Record<string, DecisionDriftFlag> = {};
|
|
128
|
+
const load = async (): Promise<void> => {
|
|
129
|
+
if (rows) return;
|
|
130
|
+
rows = [];
|
|
131
|
+
const file = Bun.file(path);
|
|
132
|
+
if (await file.exists()) {
|
|
133
|
+
const raw = (await file.json()) as {
|
|
134
|
+
rows?: StoredLabel[];
|
|
135
|
+
suspended?: boolean;
|
|
136
|
+
certifiedAt?: number;
|
|
137
|
+
drifts?: Record<string, DecisionDriftFlag>;
|
|
138
|
+
candidates?: Record<string, DecisionCandidate>;
|
|
139
|
+
};
|
|
140
|
+
rows = raw.rows ?? [];
|
|
141
|
+
candidates = raw.candidates ?? {};
|
|
142
|
+
driftFlags = raw.drifts ?? {};
|
|
143
|
+
const legacy =
|
|
144
|
+
driftFlags["*"] ??
|
|
145
|
+
(raw.suspended === true && Object.keys(driftFlags).length === 0
|
|
146
|
+
? { suspended: true as const, certifiedAt: raw.certifiedAt ?? 0 }
|
|
147
|
+
: undefined);
|
|
148
|
+
if (legacy?.suspended) {
|
|
149
|
+
delete driftFlags["*"];
|
|
150
|
+
for (const name of declaredDriftDecisions) {
|
|
151
|
+
driftFlags[name] = { suspended: legacy.suspended, certifiedAt: legacy.certifiedAt };
|
|
152
|
+
}
|
|
153
|
+
await mkdir(dirname(path), { recursive: true });
|
|
154
|
+
await Bun.write(path, JSON.stringify({ rows, candidates, drifts: driftFlags }));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const flush = async (): Promise<void> => {
|
|
159
|
+
await mkdir(dirname(path), { recursive: true });
|
|
160
|
+
await Bun.write(
|
|
161
|
+
path,
|
|
162
|
+
JSON.stringify({
|
|
163
|
+
rows,
|
|
164
|
+
candidates,
|
|
165
|
+
drifts: driftFlags,
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
async insert(label, at) {
|
|
171
|
+
await load();
|
|
172
|
+
rows!.push({ ...label, at });
|
|
173
|
+
await flush();
|
|
174
|
+
},
|
|
175
|
+
async list(decision, tenant) {
|
|
176
|
+
await load();
|
|
177
|
+
return rows!
|
|
178
|
+
.filter((row) => decision === undefined || row.decision === decision)
|
|
179
|
+
.filter((row) => tenant === undefined || (row.tenant ?? null) === tenant)
|
|
180
|
+
.map(toLabel);
|
|
181
|
+
},
|
|
182
|
+
async drift() {
|
|
183
|
+
await load();
|
|
184
|
+
return driftFlags;
|
|
185
|
+
},
|
|
186
|
+
async setDrift(decision, next) {
|
|
187
|
+
await load();
|
|
188
|
+
if (next.suspended) driftFlags[decision] = next;
|
|
189
|
+
else delete driftFlags[decision];
|
|
190
|
+
await flush();
|
|
191
|
+
},
|
|
192
|
+
async putCandidate(decision, entry) {
|
|
193
|
+
await load();
|
|
194
|
+
candidates[decision] = entry;
|
|
195
|
+
await flush();
|
|
196
|
+
},
|
|
197
|
+
async getCandidate(decision) {
|
|
198
|
+
await load();
|
|
199
|
+
return candidates[decision];
|
|
200
|
+
},
|
|
201
|
+
async listCandidates() {
|
|
202
|
+
await load();
|
|
203
|
+
return Object.keys(candidates);
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Decision names an old app-wide drift flag is copied onto. Boot sets this before open. */
|
|
209
|
+
let declaredDriftDecisions: readonly string[] = [];
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Declare which decisions receive a legacy app-wide drift flag.
|
|
213
|
+
*
|
|
214
|
+
* @param names - Decision names from the manifest
|
|
215
|
+
*/
|
|
216
|
+
export function setDeclaredDriftDecisions(names: readonly string[]): void {
|
|
217
|
+
declaredDriftDecisions = names;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Postgres label store on the journal connection. Creates the tables.
|
|
222
|
+
* A failed init or query throws {@link DecisionLabelStoreError}.
|
|
223
|
+
*
|
|
224
|
+
* @param sql - Journal SQL client
|
|
225
|
+
*/
|
|
226
|
+
export async function createPostgresDecisionLabelStore(
|
|
227
|
+
sql: DecisionLabelSql,
|
|
228
|
+
): Promise<DecisionLabelStore> {
|
|
229
|
+
try {
|
|
230
|
+
await sql.exec(`CREATE TABLE IF NOT EXISTS oke_decision_labels (
|
|
231
|
+
decision_id TEXT NOT NULL,
|
|
232
|
+
question TEXT NOT NULL,
|
|
233
|
+
value TEXT NOT NULL,
|
|
234
|
+
propensity DOUBLE PRECISION NOT NULL,
|
|
235
|
+
reviewer TEXT NOT NULL,
|
|
236
|
+
locale TEXT,
|
|
237
|
+
model TEXT,
|
|
238
|
+
tenant TEXT,
|
|
239
|
+
score DOUBLE PRECISION,
|
|
240
|
+
loss INTEGER,
|
|
241
|
+
raw TEXT,
|
|
242
|
+
at BIGINT NOT NULL,
|
|
243
|
+
input TEXT
|
|
244
|
+
)`);
|
|
245
|
+
await sql.exec(`ALTER TABLE oke_decision_labels ADD COLUMN IF NOT EXISTS input TEXT`);
|
|
246
|
+
await sql.exec(`ALTER TABLE oke_decision_labels ADD COLUMN IF NOT EXISTS review_id TEXT`);
|
|
247
|
+
await migrateDecisionDriftTable(sql, declaredDriftDecisions);
|
|
248
|
+
await sql.exec(`CREATE TABLE IF NOT EXISTS oke_decision_candidates (
|
|
249
|
+
decision_id TEXT PRIMARY KEY,
|
|
250
|
+
body TEXT NOT NULL
|
|
251
|
+
)`);
|
|
252
|
+
} catch (cause) {
|
|
253
|
+
throw new DecisionLabelStoreError("decision label store failed to open", { cause });
|
|
254
|
+
}
|
|
255
|
+
const query = async (
|
|
256
|
+
statement: string,
|
|
257
|
+
params?: readonly unknown[],
|
|
258
|
+
): Promise<Record<string, unknown>[]> => {
|
|
259
|
+
try {
|
|
260
|
+
return await sql.query(statement, params);
|
|
261
|
+
} catch (cause) {
|
|
262
|
+
throw new DecisionLabelStoreError("decision label store query failed", { cause });
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
const exec = async (statement: string, params?: readonly unknown[]): Promise<void> => {
|
|
266
|
+
try {
|
|
267
|
+
await sql.exec(statement, params);
|
|
268
|
+
} catch (cause) {
|
|
269
|
+
throw new DecisionLabelStoreError("decision label store query failed", { cause });
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
return {
|
|
273
|
+
async insert(label, at) {
|
|
274
|
+
await exec(
|
|
275
|
+
`INSERT INTO oke_decision_labels
|
|
276
|
+
(decision_id, question, value, propensity, reviewer, locale, model, tenant, score, loss, raw, at, input, review_id)
|
|
277
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
278
|
+
[
|
|
279
|
+
label.decision,
|
|
280
|
+
label.question,
|
|
281
|
+
JSON.stringify(label.value),
|
|
282
|
+
label.propensity,
|
|
283
|
+
label.reviewer,
|
|
284
|
+
label.locale ?? null,
|
|
285
|
+
label.model ?? null,
|
|
286
|
+
label.tenant ?? null,
|
|
287
|
+
label.score ?? null,
|
|
288
|
+
label.loss ?? null,
|
|
289
|
+
label.raw === undefined ? null : JSON.stringify(label.raw),
|
|
290
|
+
at,
|
|
291
|
+
label.input === undefined ? null : JSON.stringify(label.input),
|
|
292
|
+
label.reviewId ?? null,
|
|
293
|
+
],
|
|
294
|
+
);
|
|
295
|
+
},
|
|
296
|
+
async list(decision, tenant) {
|
|
297
|
+
const clauses: string[] = [];
|
|
298
|
+
const args: (string | null)[] = [];
|
|
299
|
+
if (decision !== undefined) {
|
|
300
|
+
clauses.push("decision_id = ?");
|
|
301
|
+
args.push(decision);
|
|
302
|
+
}
|
|
303
|
+
if (tenant !== undefined) {
|
|
304
|
+
clauses.push("tenant IS NOT DISTINCT FROM ?");
|
|
305
|
+
args.push(tenant);
|
|
306
|
+
}
|
|
307
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
308
|
+
const found = await query(
|
|
309
|
+
`SELECT decision_id, question, value, propensity, reviewer, locale, model, tenant, score, loss, raw, at, input, review_id
|
|
310
|
+
FROM oke_decision_labels ${where}`,
|
|
311
|
+
args,
|
|
312
|
+
);
|
|
313
|
+
return found.map((row) => ({
|
|
314
|
+
decision: String(row.decision_id),
|
|
315
|
+
question: String(row.question),
|
|
316
|
+
value: JSON.parse(String(row.value)) as unknown,
|
|
317
|
+
propensity: Number(row.propensity),
|
|
318
|
+
reviewer: String(row.reviewer),
|
|
319
|
+
...(row.locale != null ? { locale: String(row.locale) } : {}),
|
|
320
|
+
...(row.model != null ? { model: String(row.model) } : {}),
|
|
321
|
+
...(row.tenant != null ? { tenant: String(row.tenant) } : {}),
|
|
322
|
+
...(row.score != null ? { score: Number(row.score) } : {}),
|
|
323
|
+
...(row.loss != null ? { loss: Number(row.loss) } : {}),
|
|
324
|
+
...(row.raw != null ? { raw: JSON.parse(String(row.raw)) as unknown } : {}),
|
|
325
|
+
...(row.input != null ? { input: JSON.parse(String(row.input)) as unknown } : {}),
|
|
326
|
+
...(row.review_id != null ? { reviewId: String(row.review_id) } : {}),
|
|
327
|
+
at: Number(row.at),
|
|
328
|
+
}));
|
|
329
|
+
},
|
|
330
|
+
async drift() {
|
|
331
|
+
const rows = await query(
|
|
332
|
+
`SELECT decision_id, suspended, certified_at FROM oke_decision_drift`,
|
|
333
|
+
);
|
|
334
|
+
const flags: Record<string, DecisionDriftFlag> = {};
|
|
335
|
+
for (const row of rows) {
|
|
336
|
+
const name = String(row.decision_id ?? "");
|
|
337
|
+
if (!name) continue;
|
|
338
|
+
flags[name] = {
|
|
339
|
+
suspended: Number(row.suspended) === 1,
|
|
340
|
+
certifiedAt: Number(row.certified_at ?? 0),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return flags;
|
|
344
|
+
},
|
|
345
|
+
async setDrift(decision, next) {
|
|
346
|
+
if (!next.suspended) {
|
|
347
|
+
await exec(`DELETE FROM oke_decision_drift WHERE decision_id = ?`, [decision]);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
await exec(
|
|
351
|
+
`INSERT INTO oke_decision_drift (decision_id, suspended, certified_at) VALUES (?, ?, ?)
|
|
352
|
+
ON CONFLICT (decision_id) DO UPDATE SET suspended = excluded.suspended, certified_at = excluded.certified_at`,
|
|
353
|
+
[decision, 1, next.certifiedAt],
|
|
354
|
+
);
|
|
355
|
+
},
|
|
356
|
+
async putCandidate(decision, entry) {
|
|
357
|
+
await exec(
|
|
358
|
+
`INSERT INTO oke_decision_candidates (decision_id, body) VALUES (?, ?)
|
|
359
|
+
ON CONFLICT (decision_id) DO UPDATE SET body = excluded.body`,
|
|
360
|
+
[decision, JSON.stringify(entry)],
|
|
361
|
+
);
|
|
362
|
+
},
|
|
363
|
+
async getCandidate(decision) {
|
|
364
|
+
const rows = await query(`SELECT body FROM oke_decision_candidates WHERE decision_id = ?`, [
|
|
365
|
+
decision,
|
|
366
|
+
]);
|
|
367
|
+
const body = rows[0]?.body;
|
|
368
|
+
if (typeof body !== "string") return undefined;
|
|
369
|
+
const parsed = JSON.parse(body) as DecisionCandidate;
|
|
370
|
+
if (!parsed || typeof parsed.model !== "string" || !parsed.questions) return undefined;
|
|
371
|
+
return parsed;
|
|
372
|
+
},
|
|
373
|
+
async listCandidates() {
|
|
374
|
+
const rows = await query(`SELECT decision_id FROM oke_decision_candidates`);
|
|
375
|
+
return rows.map((row) => String(row.decision_id));
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function toLabel(row: StoredLabel): DecisionLabel {
|
|
381
|
+
return {
|
|
382
|
+
decision: row.decision,
|
|
383
|
+
question: row.question,
|
|
384
|
+
value: row.value,
|
|
385
|
+
propensity: row.propensity,
|
|
386
|
+
reviewer: row.reviewer,
|
|
387
|
+
...(row.locale !== undefined ? { locale: row.locale } : {}),
|
|
388
|
+
...(row.model !== undefined ? { model: row.model } : {}),
|
|
389
|
+
...(row.tenant !== undefined ? { tenant: row.tenant } : {}),
|
|
390
|
+
...(row.score !== undefined ? { score: row.score } : {}),
|
|
391
|
+
...(row.loss !== undefined ? { loss: row.loss } : {}),
|
|
392
|
+
...(row.raw !== undefined ? { raw: row.raw } : {}),
|
|
393
|
+
...(row.input !== undefined ? { input: row.input } : {}),
|
|
394
|
+
...(row.reviewId !== undefined ? { reviewId: row.reviewId } : {}),
|
|
395
|
+
at: row.at,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Recreate `oke_decision_drift` when it still uses an app-wide `id` row.
|
|
401
|
+
* A suspended flag is copied onto every declared decision.
|
|
402
|
+
*
|
|
403
|
+
* @param sql - Journal SQL client
|
|
404
|
+
* @param names - Declared decision names
|
|
405
|
+
*/
|
|
406
|
+
async function migrateDecisionDriftTable(
|
|
407
|
+
sql: DecisionLabelSql,
|
|
408
|
+
names: readonly string[],
|
|
409
|
+
): Promise<void> {
|
|
410
|
+
const create = `CREATE TABLE IF NOT EXISTS oke_decision_drift (
|
|
411
|
+
decision_id TEXT PRIMARY KEY,
|
|
412
|
+
suspended INTEGER NOT NULL,
|
|
413
|
+
certified_at BIGINT NOT NULL
|
|
414
|
+
)`;
|
|
415
|
+
let columns: string[] = [];
|
|
416
|
+
try {
|
|
417
|
+
const rows = await sql.query(
|
|
418
|
+
`SELECT column_name FROM information_schema.columns WHERE table_name = 'oke_decision_drift'`,
|
|
419
|
+
);
|
|
420
|
+
columns = rows.map((row) => String(row.column_name ?? "").toLowerCase());
|
|
421
|
+
} catch {
|
|
422
|
+
columns = [];
|
|
423
|
+
}
|
|
424
|
+
if (columns.includes("id") && !columns.includes("decision_id")) {
|
|
425
|
+
const old = await sql.query(`SELECT * FROM oke_decision_drift`);
|
|
426
|
+
const flag = old[0];
|
|
427
|
+
await sql.exec(`DROP TABLE oke_decision_drift`);
|
|
428
|
+
await sql.exec(create.replace("IF NOT EXISTS ", ""));
|
|
429
|
+
if (flag && Number(flag.suspended) === 1) {
|
|
430
|
+
const certifiedAt = Number(flag.certified_at ?? 0);
|
|
431
|
+
for (const name of names) {
|
|
432
|
+
await sql.exec(
|
|
433
|
+
`INSERT INTO oke_decision_drift (decision_id, suspended, certified_at) VALUES (?, ?, ?)`,
|
|
434
|
+
[name, 1, certifiedAt],
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
await sql.exec(create);
|
|
441
|
+
}
|
|
@@ -21,9 +21,10 @@ describe("effects — reversibility tiers", () => {
|
|
|
21
21
|
secret: "capability",
|
|
22
22
|
call: "portal",
|
|
23
23
|
fetch: "irreversible",
|
|
24
|
+
decide: "irreversible",
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
expect(EFFECT_KIND_TIERS).toHaveLength(
|
|
27
|
+
expect(EFFECT_KIND_TIERS).toHaveLength(10);
|
|
27
28
|
for (const { kind, reversibility } of EFFECT_KIND_TIERS) {
|
|
28
29
|
expect(reversibility).toBe(expected[kind]);
|
|
29
30
|
expect(reversibilityOf(kind)).toBe(expected[kind]);
|
package/src/kernel/effects.ts
CHANGED
|
@@ -17,7 +17,8 @@ export type EffectKind =
|
|
|
17
17
|
| "embed"
|
|
18
18
|
| "secret"
|
|
19
19
|
| "call"
|
|
20
|
-
| "fetch"
|
|
20
|
+
| "fetch"
|
|
21
|
+
| "decide";
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Reversibility tier — console §9.1 ranking, applied to all effect kinds.
|
|
@@ -65,6 +66,11 @@ export interface EffectEntry {
|
|
|
65
66
|
* Omitted for in-process work (PGlite, mock AI, local channel console).
|
|
66
67
|
*/
|
|
67
68
|
readonly external?: EffectExternal;
|
|
69
|
+
/**
|
|
70
|
+
* Agent run this `fx.run` or nested agent call started.
|
|
71
|
+
* Absent on prompt asks and on calls that are not agents.
|
|
72
|
+
*/
|
|
73
|
+
readonly agentRunId?: string;
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
/** Append-only ledger of effect entries for one flow invocation. */
|
|
@@ -92,6 +98,7 @@ const TIER_BY_KIND: Readonly<Record<EffectKind, ReversibilityTier>> = {
|
|
|
92
98
|
secret: "capability",
|
|
93
99
|
call: "portal",
|
|
94
100
|
fetch: "irreversible",
|
|
101
|
+
decide: "irreversible",
|
|
95
102
|
};
|
|
96
103
|
|
|
97
104
|
/**
|
|
@@ -143,6 +150,7 @@ export async function recordEffect<T>(
|
|
|
143
150
|
externalOf?:
|
|
144
151
|
| EffectExternal
|
|
145
152
|
| ((result: T | undefined, error: unknown) => EffectExternal | undefined),
|
|
153
|
+
stamp?: (result: T | undefined) => { readonly agentRunId?: string } | undefined,
|
|
146
154
|
): Promise<T> {
|
|
147
155
|
const timestamp = now();
|
|
148
156
|
const t0 = performance.now();
|
|
@@ -163,6 +171,7 @@ export async function recordEffect<T>(
|
|
|
163
171
|
timestamp,
|
|
164
172
|
resolveDurationMs(now() - timestamp, performance.now() - t0),
|
|
165
173
|
external,
|
|
174
|
+
stamp?.(result),
|
|
166
175
|
);
|
|
167
176
|
}
|
|
168
177
|
}
|
|
@@ -187,7 +196,9 @@ export function recordObservedEffect(
|
|
|
187
196
|
timestamp: number,
|
|
188
197
|
duration: number,
|
|
189
198
|
external?: EffectExternal,
|
|
199
|
+
stamp?: { readonly agentRunId?: string },
|
|
190
200
|
): void {
|
|
201
|
+
const agentRunId = stamp?.agentRunId;
|
|
191
202
|
ledger.record({
|
|
192
203
|
kind,
|
|
193
204
|
resource,
|
|
@@ -195,6 +206,7 @@ export function recordObservedEffect(
|
|
|
195
206
|
duration,
|
|
196
207
|
reversibility: reversibilityOf(kind),
|
|
197
208
|
...(external !== undefined ? { external } : {}),
|
|
209
|
+
...(agentRunId !== undefined && agentRunId.length > 0 ? { agentRunId } : {}),
|
|
198
210
|
});
|
|
199
211
|
}
|
|
200
212
|
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import type {
|
|
18
18
|
AiAgentDecl,
|
|
19
|
+
AiDecisionDecl,
|
|
19
20
|
AiEmbedDecl,
|
|
20
21
|
AiMcpServerDecl,
|
|
21
22
|
AiModelDecl,
|
|
@@ -52,3 +53,5 @@ export const aiEmbedRegistry: AiEmbedDecl[] = [];
|
|
|
52
53
|
export const aiAgentRegistry: AiAgentDecl[] = [];
|
|
53
54
|
/** `ai.mcpServer` declarations since the last reset. */
|
|
54
55
|
export const aiMcpServerRegistry: AiMcpServerDecl[] = [];
|
|
56
|
+
/** `ai.decision` declarations since the last reset. */
|
|
57
|
+
export const aiDecisionRegistry: AiDecisionDecl[] = [];
|
|
@@ -12,6 +12,17 @@ import type { OkeErrorDefinition } from "./errors.ts";
|
|
|
12
12
|
* A Flow hides `fx` from effect inference, or an explicit `effects` block
|
|
13
13
|
* omits an effect the walk can see.
|
|
14
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* A durable Flow calls `fx.fetch` or `fx.send` outside `fx.step`.
|
|
17
|
+
* Those effects are irreversible and must be an explicit step so replay skips them.
|
|
18
|
+
*/
|
|
19
|
+
export const DURABLE_BARE_EFFECT: OkeErrorDefinition = {
|
|
20
|
+
code: 1901,
|
|
21
|
+
domain: "compiler",
|
|
22
|
+
cause: 'Durable flow "{flow}" calls fx.{effect} outside fx.step.',
|
|
23
|
+
fix: "Wrap fx.fetch and fx.send in fx.step so replay returns the journaled value instead of repeating the call.",
|
|
24
|
+
};
|
|
25
|
+
|
|
15
26
|
export const FX_INFERENCE_OPAQUE: OkeErrorDefinition = {
|
|
16
27
|
code: 1900,
|
|
17
28
|
domain: "compiler",
|
|
@@ -52,6 +52,10 @@ export const OKE_ERROR_TEXT: Readonly<Record<string, OkeErrorText>> = {
|
|
|
52
52
|
cause: 'Flow "{flow}" embeds with "{resource}" without declaring it.',
|
|
53
53
|
fix: 'Add "{resource}" to this flow\'s effects.embeds.',
|
|
54
54
|
},
|
|
55
|
+
UNDECLARED_DECIDE: {
|
|
56
|
+
cause: 'Flow "{flow}" decides "{resource}" without declaring it.',
|
|
57
|
+
fix: 'Add "{resource}" to this flow\'s effects.decides.',
|
|
58
|
+
},
|
|
55
59
|
NO_EFFECTS_DECLARED: {
|
|
56
60
|
cause: 'Flow "{flow}" has no declared effects and no Manifest to derive them from.{extract}',
|
|
57
61
|
fix:
|
|
@@ -80,6 +84,22 @@ export const OKE_ERROR_TEXT: Readonly<Record<string, OkeErrorText>> = {
|
|
|
80
84
|
'Live signal "{signal}" is exposed twice with the same gates ({gates}) and match ({match}).',
|
|
81
85
|
fix: "Use a different gate or path-param filter, or drop the extra route.",
|
|
82
86
|
},
|
|
87
|
+
JOURNAL_STALE_LEASE: {
|
|
88
|
+
cause: 'Journal run "{runId}" rejected a write from an expired lease holder.',
|
|
89
|
+
fix: "Resume the run so the current holder acquires a new lease token. Do not write with a token from a previous holder.",
|
|
90
|
+
},
|
|
91
|
+
JOURNAL_REPLAY_DIVERGENCE: {
|
|
92
|
+
cause: 'Journal run "{runId}" expected {expected} but the code called {actual}.',
|
|
93
|
+
fix: "Keep step names, sleep labels, and effect order stable for in-flight runs. A code change that reorders journal calls cannot resume the old run.",
|
|
94
|
+
},
|
|
95
|
+
JOURNAL_CODE_VERSION: {
|
|
96
|
+
cause: 'Journal run "{runId}" was started on code {expected} and this process is {actual}.',
|
|
97
|
+
fix: "Finish or abandon in-flight runs before deploying a different code version. Do not replay them on the new build.",
|
|
98
|
+
},
|
|
99
|
+
JOURNAL_VALUE_NOT_JSON: {
|
|
100
|
+
cause: "A durable journal entry held a value that is not JSON ({detail}).",
|
|
101
|
+
fix: "Return plain JSON from fx.step. fx.fetch and fx.send must run inside fx.step so the journal can store a JSON snapshot.",
|
|
102
|
+
},
|
|
83
103
|
MCP_TOOL_DUPLICATE: {
|
|
84
104
|
cause: 'MCP tool "{tool}" is bound twice (flow "{flow}").',
|
|
85
105
|
fix: "Give each MCP tool exposure a unique tool name.",
|
package/src/kernel/errors.ts
CHANGED
|
@@ -270,6 +270,8 @@ const OKE_ERROR_ROWS = {
|
|
|
270
270
|
UNDECLARED_FETCH: { code: 1008, domain: "kernel" },
|
|
271
271
|
/** Flow embeds via a model not listed in `effects.embeds`. */
|
|
272
272
|
UNDECLARED_EMBED: { code: 1009, domain: "kernel" },
|
|
273
|
+
/** Flow decides a decision not listed in `effects.decides`. */
|
|
274
|
+
UNDECLARED_DECIDE: { code: 1010, domain: "kernel" },
|
|
273
275
|
/**
|
|
274
276
|
* Flow has no declared `effects` and no Manifest-derived effects were
|
|
275
277
|
* available to stamp at boot (dev+compose / prod — never a silent open token).
|
|
@@ -292,6 +294,14 @@ const OKE_ERROR_ROWS = {
|
|
|
292
294
|
LIVE_EXPOSURE_DUPLICATE: { code: 1050, domain: "kernel" },
|
|
293
295
|
/** Two MCP tool bindings share the same exposed tool name. */
|
|
294
296
|
MCP_TOOL_DUPLICATE: { code: 1060, domain: "kernel" },
|
|
297
|
+
/** A journal write arrived from a holder whose lease token no longer matches. */
|
|
298
|
+
JOURNAL_STALE_LEASE: { code: 1074, domain: "kernel" },
|
|
299
|
+
/** Replay called a different step or effect than the next journal entry. */
|
|
300
|
+
JOURNAL_REPLAY_DIVERGENCE: { code: 1075, domain: "kernel" },
|
|
301
|
+
/** Resume of a run stamped with a different code version. */
|
|
302
|
+
JOURNAL_CODE_VERSION: { code: 1076, domain: "kernel" },
|
|
303
|
+
/** A journaled value could not be stored as JSON. */
|
|
304
|
+
JOURNAL_VALUE_NOT_JSON: { code: 1077, domain: "kernel" },
|
|
295
305
|
/**
|
|
296
306
|
* Emit target has no subscriber (unified-theory §21).
|
|
297
307
|
* Thrown at emit when `optional` is false and nobody is subscribed.
|
package/src/kernel/flow.test.ts
CHANGED
|
@@ -46,9 +46,8 @@ describe("flow — one species", () => {
|
|
|
46
46
|
on(signal, shared);
|
|
47
47
|
|
|
48
48
|
const app = oke({ autoBoot: false, name: "one-species" });
|
|
49
|
-
|
|
50
|
-
expect(
|
|
51
|
-
expect(app.bindings[1]?.flow).toBe(shared);
|
|
49
|
+
const sharedBindings = app.bindings.filter((binding) => binding.flow === shared);
|
|
50
|
+
expect(sharedBindings).toHaveLength(2);
|
|
52
51
|
|
|
53
52
|
const httpResult = await app.execute(shared, { n: 3 }, shared.triggers[0]!);
|
|
54
53
|
const signalResults = await app.dispatchSignal("order-placed", { n: 4 });
|
package/src/kernel/flow.ts
CHANGED
|
@@ -49,9 +49,9 @@ export interface FlowOptions<I = unknown, O = unknown> {
|
|
|
49
49
|
*/
|
|
50
50
|
readonly retry?: FxRetryOptions;
|
|
51
51
|
/**
|
|
52
|
-
* Cache policy.
|
|
53
|
-
*
|
|
54
|
-
*
|
|
52
|
+
* Cache policy. Omitted caches a pure store-read flow. `true` or a duration
|
|
53
|
+
* (`"30s"`) opts one flow in when the app set `cache: { auto: false }`.
|
|
54
|
+
* `false` always disables. The key includes tenant, locale, scopes, and roles.
|
|
55
55
|
*/
|
|
56
56
|
readonly cache?: boolean | string;
|
|
57
57
|
/**
|
|
@@ -124,6 +124,10 @@ export type InferFlowOut<Opts> = Opts extends { readonly do: FlowHandler<infer _
|
|
|
124
124
|
* `FlowDef<…, undefined>` remains assignable into `on()` before rebinding.
|
|
125
125
|
*/
|
|
126
126
|
declare const triggerPhantom: unique symbol;
|
|
127
|
+
/** Covariant input phantom so `fx.call` can read `I` without hitting `do`. */
|
|
128
|
+
declare const inputPhantom: unique symbol;
|
|
129
|
+
/** Covariant output phantom so `fx.call` can read `O` without hitting `do`. */
|
|
130
|
+
declare const outputPhantom: unique symbol;
|
|
127
131
|
|
|
128
132
|
/**
|
|
129
133
|
* A Flow definition — one species, trigger-agnostic until {@link on} binds.
|
|
@@ -151,6 +155,10 @@ export interface FlowDef<
|
|
|
151
155
|
* `$trigger` / `triggers[0]` for the value.
|
|
152
156
|
*/
|
|
153
157
|
readonly [triggerPhantom]?: T;
|
|
158
|
+
/** Type-level input. Not present at runtime. */
|
|
159
|
+
readonly [inputPhantom]?: I;
|
|
160
|
+
/** Type-level output. Not present at runtime. */
|
|
161
|
+
readonly [outputPhantom]?: O;
|
|
154
162
|
/** Stable name (auto-assigned when omitted). */
|
|
155
163
|
readonly name: string;
|
|
156
164
|
/** Unit scope — derived from `name`'s first dot segment (e.g. `"auth.refresh"` → `"auth"`). */
|
|
@@ -255,6 +263,28 @@ export interface FlowDef<
|
|
|
255
263
|
): FlowDef<I, O, E, D & (P extends PluginDef<infer PD> ? PD : Record<string, never>), T>;
|
|
256
264
|
}
|
|
257
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Input type of a Flow handle. `unknown` when `F` is a string name.
|
|
268
|
+
*
|
|
269
|
+
* @typeParam F - Flow handle or other call target
|
|
270
|
+
*/
|
|
271
|
+
export type FlowInput<F> = F extends { readonly [inputPhantom]?: infer I }
|
|
272
|
+
? [I] extends [undefined]
|
|
273
|
+
? unknown
|
|
274
|
+
: Exclude<I, undefined>
|
|
275
|
+
: unknown;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Output type of a Flow handle. `unknown` when `F` is a string name.
|
|
279
|
+
*
|
|
280
|
+
* @typeParam F - Flow handle or other call target
|
|
281
|
+
*/
|
|
282
|
+
export type FlowOutput<F> = F extends { readonly [outputPhantom]?: infer O }
|
|
283
|
+
? [O] extends [undefined]
|
|
284
|
+
? unknown
|
|
285
|
+
: Exclude<O, undefined>
|
|
286
|
+
: unknown;
|
|
287
|
+
|
|
258
288
|
/** Unique brand symbol for {@link FlowDef}. */
|
|
259
289
|
export const flowBrand: unique symbol = Symbol("oke.flow");
|
|
260
290
|
|