okengine 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +7 -0
- package/manifest.v1.schema.json +44 -0
- package/package.json +32 -19
- package/site/content/docs/ai/mcp.mdx +13 -9
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/elements/ai/agents.mdx +61 -13
- package/site/content/docs/elements/ai/decide.mdx +536 -0
- package/site/content/docs/elements/ai/events.mdx +373 -0
- package/site/content/docs/elements/ai/index.mdx +9 -3
- package/site/content/docs/elements/ai/meta.json +1 -1
- package/site/content/docs/elements/ai/prompts.mdx +4 -2
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/postgres.mdx +12 -8
- package/site/content/docs/recipes/redis.mdx +4 -4
- package/site/content/docs/reference/cli.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +9 -7
- package/site/content/docs/reference/fx.mdx +7 -6
- package/src/auth/api-key-sql.test.ts +44 -0
- package/src/auth/api-key-sql.ts +1 -0
- package/src/auth/api-keys.ts +30 -12
- package/src/auth/config.ts +6 -2
- package/src/auth/gate-auth.test.ts +20 -0
- package/src/cli/agents-md.test.ts +25 -0
- package/src/cli/ask-vault-gaps.test.ts +38 -1
- package/src/cli/ask-vault-gaps.ts +35 -2
- package/src/cli/decide.ts +174 -0
- package/src/cli/decision-lock-watch.test.ts +45 -0
- package/src/cli/decision-lock-watch.ts +51 -0
- package/src/cli/dev-app-runner.ts +34 -8
- package/src/cli/dev-auth-secret.test.ts +49 -0
- package/src/cli/dev-auth-secret.ts +49 -0
- package/src/cli/dev-hot.test.ts +41 -0
- package/src/cli/dev-hot.ts +48 -0
- package/src/cli/dev.test.ts +1 -1
- package/src/cli/dev.ts +24 -6
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.images.test.ts +2 -2
- package/src/cli/load-config.ts +1 -1
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/client/agent.ts +307 -0
- package/src/client/transport.test.ts +20 -0
- package/src/client/transport.ts +2 -1
- package/src/client/types.ts +1 -1
- package/src/client-react/index.ts +7 -1
- package/src/client-react/use-agent-run.test.ts +271 -0
- package/src/client-react/use-agent-run.ts +193 -0
- package/src/compiler/ai-approval.test.ts +36 -0
- package/src/compiler/ai-repair.test.ts +22 -0
- package/src/compiler/decisions.extract.test.ts +164 -0
- package/src/compiler/effects-infer.ts +27 -1
- package/src/compiler/extract.ts +268 -12
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/server/ai-runs-flows.ts +269 -0
- package/src/console/server/ai-runs.test.ts +323 -0
- package/src/console/server/ai-runs.ts +562 -0
- package/src/console/server/ai.ts +12 -1
- package/src/console/server/app.ts +10 -1
- package/src/console/server/decisions-flows.ts +157 -0
- package/src/console/server/decisions.test.ts +66 -0
- package/src/console/server/decisions.ts +229 -0
- package/src/console/server/flows.ts +50 -2
- package/src/console/server/invoke-user-flow.ts +9 -0
- package/src/console/server/runs-ingest.ts +16 -0
- package/src/console/server/serve.ts +5 -0
- package/src/console/server/state.ts +25 -1
- package/src/console/server/store-stats.test.ts +47 -0
- package/src/console/server/store-stats.ts +92 -18
- package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-62Xts8Xi.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-DyeoKHKA.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DvfpLXsU.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-BY03SeCg.js} +1 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
- package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-D7nNzpgB.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-DdL19zzh.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bb3FWUu9.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
- package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
- package/src/console/ui-next/dist/index.html +4 -3
- package/src/console/ui-next/seed-parked-approval.ts +126 -0
- package/src/console/ui-next/src/client.ts +224 -1
- package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
- package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
- package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
- package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
- package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
- package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
- package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
- package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
- package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
- package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
- package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
- package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
- package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
- package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
- package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
- package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
- package/src/console/ui-next/src/router.tsx +17 -0
- package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
- package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
- package/src/console/ui-next/vite.config.ts +9 -0
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/drivers/ai-anthropic.ts +212 -21
- package/src/drivers/ai-mock.ts +34 -1
- package/src/drivers/ai-openai-compatible.ts +67 -26
- package/src/drivers/ai-preconnect.ts +18 -0
- package/src/drivers/ai-stream.test.ts +62 -0
- package/src/drivers/ai-types.ts +20 -0
- package/src/drivers/journal-postgres.ts +289 -2
- package/src/drivers/memory-ddl.test.ts +18 -0
- package/src/drivers/memory.ts +21 -1
- package/src/drivers/pg-rls.test.ts +31 -0
- package/src/drivers/pg-rls.ts +15 -0
- package/src/drivers/postgres.test.ts +9 -0
- package/src/drivers/postgres.ts +49 -11
- package/src/elements/ai/agent-event-slot.ts +27 -0
- package/src/elements/ai/approval-http.ts +207 -0
- package/src/elements/ai/approval.test.ts +741 -0
- package/src/elements/ai/approval.ts +228 -0
- package/src/elements/ai/decisions/bind.ts +163 -0
- package/src/elements/ai/decisions/certificate.test.ts +291 -0
- package/src/elements/ai/decisions/certificate.ts +377 -0
- package/src/elements/ai/decisions/certify.ts +314 -0
- package/src/elements/ai/decisions/decide.test.ts +953 -0
- package/src/elements/ai/decisions/e2e.test.ts +399 -0
- package/src/elements/ai/decisions/export.test.ts +120 -0
- package/src/elements/ai/decisions/export.ts +146 -0
- package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
- package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
- package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
- package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
- package/src/elements/ai/decisions/http.test.ts +154 -0
- package/src/elements/ai/decisions/http.ts +240 -0
- package/src/elements/ai/decisions/labels.test.ts +282 -0
- package/src/elements/ai/decisions/labels.ts +257 -0
- package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
- package/src/elements/ai/decisions/openrouter.ts +36 -0
- package/src/elements/ai/decisions/provider.ts +102 -0
- package/src/elements/ai/decisions/typesafe.ts +36 -0
- package/src/elements/ai/declare.ts +252 -1
- package/src/elements/ai/events.test.ts +379 -0
- package/src/elements/ai/events.ts +138 -0
- package/src/elements/ai/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1186 -105
- package/src/elements/ai/stream-turn.ts +214 -0
- package/src/elements/ai/stream.live.test.ts +76 -0
- package/src/elements/ai/subagent.test.ts +136 -0
- package/src/elements/ai.test.ts +497 -3
- package/src/elements/ai.ts +16 -1
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/store/sql-session.test.ts +51 -0
- package/src/elements/store/sql-session.ts +111 -1
- package/src/i18n/catalogs/ar.ts +5 -0
- package/src/i18n/catalogs/en.ts +5 -0
- package/src/index.ts +2 -0
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +103 -1
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot.ts +15 -4
- package/src/kernel/builtin-errors.ts +2 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects.test.ts +2 -1
- package/src/kernel/effects.ts +13 -1
- package/src/kernel/element-registries.ts +3 -0
- package/src/kernel/errors-text.ts +4 -0
- package/src/kernel/errors.ts +2 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/fx-decide.ts +761 -0
- package/src/kernel/fx.test.ts +12 -5
- package/src/kernel/fx.ts +312 -29
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/journal.ts +104 -0
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +29 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/release/http-graph.test.ts +2 -0
- package/src/release/http-graph.ts +5 -0
- package/src/release/measure.ts +39 -14
- package/src/runs/collect.ts +4 -1
- package/src/runs/parquet.test.ts +27 -1
- package/src/runs/parquet.ts +19 -0
- package/src/runs/types.ts +31 -0
- package/src/runtime/json-code-block.test.ts +106 -47
- package/src/runtime/json-code-block.ts +1209 -46
- package/src/term.test.ts +13 -0
- package/src/term.ts +81 -49
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
- package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
- package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
|
@@ -0,0 +1,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[] = [];
|
|
@@ -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:
|
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).
|
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 });
|