okengine 0.12.0 → 0.14.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/manifest.v1.schema.json +7 -1
- package/package.json +2 -2
- package/site/content/docs/ai/mcp.mdx +27 -2
- package/site/content/docs/elements/ai.mdx +58 -11
- package/site/content/docs/elements/clock.mdx +27 -12
- package/site/content/docs/elements/flow.mdx +6 -3
- package/site/content/docs/elements/signal.mdx +71 -25
- package/site/content/docs/elements/store.mdx +21 -1
- package/site/content/docs/get-started/installation.mdx +2 -2
- package/site/content/docs/providers/index.mdx +1 -0
- package/site/content/docs/recipes/dragonfly.mdx +4 -3
- package/site/content/docs/recipes/redis.mdx +4 -5
- package/site/content/docs/recipes/valkey.mdx +5 -3
- package/site/content/docs/reference/configuration.mdx +3 -1
- package/site/content/docs/reference/environment-variables.mdx +5 -5
- package/site/content/docs/reference/fx.mdx +27 -22
- package/src/cli/ai-setup/recommend.test.ts +25 -0
- package/src/cli/ai-setup/recommend.ts +8 -3
- package/src/cli/load-config.images.test.ts +1 -0
- package/src/compiler/effects-infer.ts +58 -3
- package/src/compiler/extract.test.ts +68 -1
- package/src/compiler/extract.ts +70 -2
- package/src/compiler/response.ts +45 -1
- package/src/console/server/ai.ts +5 -2
- package/src/console/server/flows.ts +1 -0
- package/src/console/server/serve.ts +17 -2
- package/src/console/ui-next/dist/assets/cache-glyph-BanhLsEY.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-DxDsOd4f.js +1 -0
- package/src/console/ui-next/dist/assets/http-method-CJCBYL2j.js +1 -0
- package/src/console/ui-next/dist/assets/{index-DX248G39.js → index-Ce6WKWKM.js} +3 -3
- package/src/console/ui-next/dist/assets/observability-page-BEZDzyYh.js +4 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-DFLFfUUX.js +2 -0
- package/src/console/ui-next/dist/assets/units-page-C0gW6Kdo.js +1 -0
- package/src/console/ui-next/dist/assets/{vault-page-CSOYMHhO.js → vault-page-DuKzqwzW.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/seed-invoke-host.ts +2 -0
- package/src/console/ui-next/src/features/flows/graph/build-flow-graph.test.ts +18 -0
- package/src/console/ui-next/src/features/flows/graph/build-flow-graph.ts +14 -1
- package/src/console/ui-next/src/features/flows/graph/neighborhood.test.ts +17 -0
- package/src/console/ui-next/src/features/flows/graph/neighborhood.ts +16 -3
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +3 -1
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +9 -3
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +10 -1
- package/src/console/ui-next/src/features/observability/lib/ask-count.test.ts +25 -0
- package/src/console/ui-next/src/features/observability/lib/ask-count.ts +4 -1
- package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +12 -4
- package/src/docker/docker.test.ts +1 -1
- package/src/docker/dockerfile.ts +1 -1
- package/src/docker/helpers.ts +28 -0
- package/src/docker/recipes/dragonfly.ts +3 -6
- package/src/docker/recipes/redis.ts +2 -6
- package/src/docker/recipes/valkey.ts +2 -6
- package/src/drivers/ai-anthropic.ts +5 -0
- package/src/drivers/ai-ollama.ts +49 -30
- package/src/drivers/ai-openai-compatible.ts +57 -46
- package/src/drivers/ai-providers.test.ts +3 -0
- package/src/drivers/bun-native-completeness.test.ts +7 -9
- package/src/drivers/redis.ts +11 -4
- package/src/drivers/signal-redis.ts +24 -14
- package/src/drivers/signal-types.ts +2 -1
- package/src/drivers/types.ts +1 -1
- package/src/elements/ai/declare.ts +109 -0
- package/src/elements/ai/errors.test.ts +5 -1
- package/src/elements/ai/errors.ts +30 -2
- package/src/elements/ai/eval.ts +4 -6
- package/src/elements/ai/mcp-client.test.ts +206 -0
- package/src/elements/ai/mcp-client.ts +362 -0
- package/src/elements/ai/mcp-http.ts +159 -0
- package/src/elements/ai/mcp-mock.ts +134 -0
- package/src/elements/ai/mcp-protocol.ts +234 -0
- package/src/elements/ai/mcp-stdio.test.ts +50 -0
- package/src/elements/ai/mcp-stdio.ts +212 -0
- package/src/elements/ai/mcp-transport.ts +70 -0
- package/src/elements/ai/runtime.ts +159 -29
- package/src/elements/ai.test.ts +139 -0
- package/src/elements/ai.ts +16 -0
- package/src/elements/clock/health.test.ts +43 -0
- package/src/elements/clock/runtime.ts +55 -2
- package/src/elements/clock/schedule.ts +71 -142
- package/src/elements/clock.test.ts +9 -40
- package/src/elements/clock.ts +6 -1
- package/src/elements/gate/runtime.ts +3 -3
- package/src/elements/index.ts +2 -0
- package/src/elements/signal/declare.ts +2 -1
- package/src/elements/signal/runtime.ts +16 -1
- package/src/elements/signal.ts +1 -0
- package/src/elements/store/cache.test.ts +2 -0
- package/src/elements/store/cache.ts +3 -3
- package/src/elements/store/declare.ts +7 -0
- package/src/elements/store/kv-sql.test.ts +86 -0
- package/src/elements/store/kv-sql.ts +178 -0
- package/src/elements/store/runtime.ts +35 -9
- package/src/elements/store.test.ts +2 -0
- package/src/elements/vault/builtin-adapter.ts +17 -0
- package/src/full.ts +2 -0
- package/src/index.ts +4 -0
- package/src/kernel/app.ts +97 -64
- package/src/kernel/auto-registry.test.ts +5 -0
- package/src/kernel/boot-bind/ai.ts +24 -0
- package/src/kernel/boot-bind/clock.ts +2 -0
- package/src/kernel/boot-bind/store.test.ts +150 -1
- package/src/kernel/boot-bind/store.ts +66 -1
- package/src/kernel/boot.ts +3 -1
- package/src/kernel/element-registries.ts +4 -1
- package/src/kernel/fx-dead-letters.test.ts +77 -0
- package/src/kernel/fx.test.ts +22 -0
- package/src/kernel/fx.ts +112 -6
- package/src/kernel/http-stream.test.ts +174 -0
- package/src/kernel/index.ts +2 -0
- package/src/manifest/diff.test.ts +13 -0
- package/src/manifest/diff.ts +15 -0
- package/src/manifest/mcp-ref.ts +88 -0
- package/src/manifest/types.ts +33 -2
- package/src/manifest/validate.test.ts +20 -0
- package/src/mcp/docs-server.ts +1 -1
- package/src/mcp/server.ts +1 -1
- package/src/plugins/compression.test.ts +21 -0
- package/src/plugins/compression.ts +1 -0
- package/src/runtime/bun.ts +41 -4
- package/src/test/reset-element-registries.ts +2 -0
- package/src/console/ui-next/dist/assets/cache-glyph-CLPBqZeb.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-C_Tas1E1.js +0 -1
- package/src/console/ui-next/dist/assets/http-method-BJ92Z_ke.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-BwVS5Jvm.js +0 -4
- package/src/console/ui-next/dist/assets/trace-detail-sheet-D16lWQMt.js +0 -2
- package/src/console/ui-next/dist/assets/units-page-C5KOI7qG.js +0 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL-backed durable KV — `oke_kv` JSONB on the shared store.sql connection.
|
|
3
|
+
*
|
|
4
|
+
* Not a catalog `drivers.store.kv` id. Cache namespaces stay redis/memory.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { KvNamespace, SqlConnection } from "../../drivers/types.ts";
|
|
8
|
+
import { parseTtlMs } from "./cache.ts";
|
|
9
|
+
|
|
10
|
+
/** Engine-owned durable KV table — boot DDL, excluded from `oke db push`. */
|
|
11
|
+
export const OKE_KV_TABLE = "oke_kv";
|
|
12
|
+
|
|
13
|
+
/** Options for {@link openSqlKvNamespace}. */
|
|
14
|
+
export interface OpenSqlKvOptions {
|
|
15
|
+
/** Shared SQL connection (`sharedSqlConn`). */
|
|
16
|
+
readonly conn: SqlConnection;
|
|
17
|
+
/** `store.kv` namespace name. */
|
|
18
|
+
readonly namespace: string;
|
|
19
|
+
/** Injectable clock (epoch ms). */
|
|
20
|
+
readonly now: () => number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** SQL KV handle — {@link KvNamespace} plus {@link SqlKvNamespace.purgeExpired}. */
|
|
24
|
+
export interface SqlKvNamespace extends KvNamespace {
|
|
25
|
+
readonly driverId: "postgres" | "pglite" | "memory";
|
|
26
|
+
/**
|
|
27
|
+
* Delete rows whose `expires_at` has passed.
|
|
28
|
+
*
|
|
29
|
+
* @returns Rows removed
|
|
30
|
+
*/
|
|
31
|
+
purgeExpired(): Promise<number>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ensured = new WeakSet<SqlConnection>();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create `oke_kv` + the expires partial index (idempotent).
|
|
38
|
+
*
|
|
39
|
+
* @param conn - Shared SQL connection
|
|
40
|
+
*/
|
|
41
|
+
export async function ensureOkeKvSchema(conn: SqlConnection): Promise<void> {
|
|
42
|
+
if (ensured.has(conn)) return;
|
|
43
|
+
await conn.exec(`CREATE TABLE IF NOT EXISTS ${OKE_KV_TABLE} (
|
|
44
|
+
namespace TEXT NOT NULL,
|
|
45
|
+
key TEXT NOT NULL,
|
|
46
|
+
value JSONB NOT NULL,
|
|
47
|
+
expires_at BIGINT,
|
|
48
|
+
updated_at BIGINT NOT NULL,
|
|
49
|
+
PRIMARY KEY (namespace, key)
|
|
50
|
+
)`);
|
|
51
|
+
await conn.exec(
|
|
52
|
+
`CREATE INDEX IF NOT EXISTS oke_kv_expires ON ${OKE_KV_TABLE} (expires_at) WHERE expires_at IS NOT NULL`,
|
|
53
|
+
);
|
|
54
|
+
ensured.add(conn);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Open a durable KV namespace on an already-open SQL connection.
|
|
59
|
+
*
|
|
60
|
+
* @param options - Connection / namespace / clock
|
|
61
|
+
*/
|
|
62
|
+
export async function openSqlKvNamespace(options: OpenSqlKvOptions): Promise<SqlKvNamespace> {
|
|
63
|
+
const { conn, namespace, now } = options;
|
|
64
|
+
await ensureOkeKvSchema(conn);
|
|
65
|
+
await purgeExpiredOn(conn, now());
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
driverId: conn.driverId,
|
|
69
|
+
async get(key) {
|
|
70
|
+
const rows = await conn.query(
|
|
71
|
+
`SELECT value FROM ${OKE_KV_TABLE}
|
|
72
|
+
WHERE namespace = ? AND key = ?
|
|
73
|
+
AND (expires_at IS NULL OR expires_at > ?)`,
|
|
74
|
+
[namespace, key, now()],
|
|
75
|
+
);
|
|
76
|
+
const raw = rows[0]?.value;
|
|
77
|
+
if (raw === undefined) return undefined;
|
|
78
|
+
return decodeJsonb(raw);
|
|
79
|
+
},
|
|
80
|
+
async set(key, value, ttl) {
|
|
81
|
+
await purgeExpiredOn(conn, now());
|
|
82
|
+
const t = now();
|
|
83
|
+
const expiresAt = ttlExpiresAt(ttl, t);
|
|
84
|
+
await conn.exec(
|
|
85
|
+
`INSERT INTO ${OKE_KV_TABLE} (namespace, key, value, expires_at, updated_at)
|
|
86
|
+
VALUES (?, ?, CAST(? AS JSONB), ?, ?)
|
|
87
|
+
ON CONFLICT (namespace, key) DO UPDATE SET
|
|
88
|
+
value = EXCLUDED.value,
|
|
89
|
+
expires_at = EXCLUDED.expires_at,
|
|
90
|
+
updated_at = EXCLUDED.updated_at`,
|
|
91
|
+
[namespace, key, JSON.stringify(value), expiresAt, t],
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
async delete(key) {
|
|
95
|
+
const result = await conn.exec(
|
|
96
|
+
`DELETE FROM ${OKE_KV_TABLE} WHERE namespace = ? AND key = ?`,
|
|
97
|
+
[namespace, key],
|
|
98
|
+
);
|
|
99
|
+
return result.changes > 0;
|
|
100
|
+
},
|
|
101
|
+
async list(prefix = "") {
|
|
102
|
+
const rows = await conn.query(
|
|
103
|
+
`SELECT key FROM ${OKE_KV_TABLE}
|
|
104
|
+
WHERE namespace = ?
|
|
105
|
+
AND (expires_at IS NULL OR expires_at > ?)
|
|
106
|
+
AND key LIKE ?
|
|
107
|
+
ORDER BY key`,
|
|
108
|
+
[namespace, now(), `${prefix}%`],
|
|
109
|
+
);
|
|
110
|
+
return rows.map((row) => String(row.key ?? ""));
|
|
111
|
+
},
|
|
112
|
+
async ttlMs(key) {
|
|
113
|
+
const rows = await conn.query(
|
|
114
|
+
`SELECT expires_at FROM ${OKE_KV_TABLE}
|
|
115
|
+
WHERE namespace = ? AND key = ?
|
|
116
|
+
AND (expires_at IS NULL OR expires_at > ?)`,
|
|
117
|
+
[namespace, key, now()],
|
|
118
|
+
);
|
|
119
|
+
const raw = rows[0]?.expires_at;
|
|
120
|
+
if (raw === undefined || raw === null) return null;
|
|
121
|
+
const at = Number(raw);
|
|
122
|
+
if (!Number.isFinite(at)) return null;
|
|
123
|
+
return Math.max(0, at - now());
|
|
124
|
+
},
|
|
125
|
+
async eval() {
|
|
126
|
+
throw new Error("oke store: durable store.kv does not support eval");
|
|
127
|
+
},
|
|
128
|
+
async close() {
|
|
129
|
+
/* Shared connection — StoreRuntime.close owns it. */
|
|
130
|
+
},
|
|
131
|
+
async purgeExpired() {
|
|
132
|
+
return purgeExpiredOn(conn, now());
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Decode a JSONB cell — PGlite/Postgres may return an object or a string.
|
|
139
|
+
*
|
|
140
|
+
* @param raw - Driver cell
|
|
141
|
+
*/
|
|
142
|
+
function decodeJsonb(raw: unknown): unknown {
|
|
143
|
+
if (typeof raw === "string") {
|
|
144
|
+
try {
|
|
145
|
+
return JSON.parse(raw) as unknown;
|
|
146
|
+
} catch {
|
|
147
|
+
return raw;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return raw;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Absolute expiry from a TTL string, or `null` when omitted / unparsed.
|
|
155
|
+
*
|
|
156
|
+
* @param ttl - Duration like `"7d"`
|
|
157
|
+
* @param t - Now (epoch ms)
|
|
158
|
+
*/
|
|
159
|
+
function ttlExpiresAt(ttl: string | undefined, t: number): number | null {
|
|
160
|
+
if (ttl === undefined) return null;
|
|
161
|
+
const ms = parseTtlMs(ttl);
|
|
162
|
+
if (ms <= 0) return null;
|
|
163
|
+
return t + ms;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Delete expired rows across every namespace on this connection.
|
|
168
|
+
*
|
|
169
|
+
* @param conn - Shared SQL connection
|
|
170
|
+
* @param t - Now (epoch ms)
|
|
171
|
+
*/
|
|
172
|
+
async function purgeExpiredOn(conn: SqlConnection, t: number): Promise<number> {
|
|
173
|
+
const result = await conn.exec(
|
|
174
|
+
`DELETE FROM ${OKE_KV_TABLE} WHERE expires_at IS NOT NULL AND expires_at <= ?`,
|
|
175
|
+
[t],
|
|
176
|
+
);
|
|
177
|
+
return result.changes;
|
|
178
|
+
}
|
|
@@ -42,6 +42,7 @@ import type {
|
|
|
42
42
|
SqlStoreDecl,
|
|
43
43
|
StoreDecl,
|
|
44
44
|
} from "./declare.ts";
|
|
45
|
+
import { openSqlKvNamespace } from "./kv-sql.ts";
|
|
45
46
|
import { createGatedFilesStoreHandle, type GatedFilesFxBridge } from "./files-fx.ts";
|
|
46
47
|
import {
|
|
47
48
|
createFilesImagePipeline,
|
|
@@ -98,6 +99,11 @@ export interface CreateStoreRuntimeOptions {
|
|
|
98
99
|
* Boot sets `off` for docker/prod and local+autoPush.
|
|
99
100
|
*/
|
|
100
101
|
readonly domainDdl?: DomainDdlMode;
|
|
102
|
+
/**
|
|
103
|
+
* Shared SQL URL for durable KV (and apps with no `store.sql()` decl).
|
|
104
|
+
* Durable namespaces open via `sharedSqlConn`, never a second Redis.
|
|
105
|
+
*/
|
|
106
|
+
readonly sqlUrl?: string;
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
/** Per-invocation context when opening a handle through the runtime. */
|
|
@@ -118,7 +124,7 @@ export type StoreHandle =
|
|
|
118
124
|
/** KV handle on `fx.store`. */
|
|
119
125
|
export interface KvStoreFxHandle {
|
|
120
126
|
readonly ref: `kv:${string}`;
|
|
121
|
-
readonly driverId: "memory" | "redis";
|
|
127
|
+
readonly driverId: "memory" | "redis" | "postgres" | "pglite";
|
|
122
128
|
get(key: string): Promise<unknown>;
|
|
123
129
|
set(key: string, value: unknown, ttl?: string): Promise<void>;
|
|
124
130
|
delete(key: string): Promise<boolean>;
|
|
@@ -383,17 +389,37 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
|
|
|
383
389
|
return sharedSqlConn(sqlDriver, "primary", { url });
|
|
384
390
|
}
|
|
385
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Durable KV — JSONB table on the shared SQL connection.
|
|
394
|
+
*
|
|
395
|
+
* @param name - Namespace
|
|
396
|
+
*/
|
|
397
|
+
async function openDurableKv(name: string): Promise<KvNamespace> {
|
|
398
|
+
const sqlDriver = options.drivers.sql;
|
|
399
|
+
if (!sqlDriver) {
|
|
400
|
+
throw new Error("oke store: durable store.kv needs a configured sql driver");
|
|
401
|
+
}
|
|
402
|
+
const firstSql = Object.values(options.sql ?? [])[0];
|
|
403
|
+
const url = options.sqlUrl ?? firstSql?.primary.url;
|
|
404
|
+
const conn = await sharedSqlConn(sqlDriver, "primary", { url });
|
|
405
|
+
return openSqlKvNamespace({ conn, namespace: name, now });
|
|
406
|
+
}
|
|
407
|
+
|
|
386
408
|
async function openKv(decl: KvStoreDecl): Promise<KvStoreFxHandle> {
|
|
387
|
-
const driver = options.drivers.kv;
|
|
388
|
-
if (!driver) throw new Error("No kv driver configured");
|
|
389
409
|
let ns = kvNs.get(decl.name);
|
|
390
410
|
if (!ns) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
411
|
+
if (decl.durable === true) {
|
|
412
|
+
ns = await openDurableKv(decl.name);
|
|
413
|
+
} else {
|
|
414
|
+
const driver = options.drivers.kv;
|
|
415
|
+
if (!driver) throw new Error("No kv driver configured");
|
|
416
|
+
const binding = options.kv?.[decl.name] ?? {};
|
|
417
|
+
ns = await driver.open({
|
|
418
|
+
name: decl.name,
|
|
419
|
+
url: binding.url,
|
|
420
|
+
client: binding.client as never,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
397
423
|
kvNs.set(decl.name, ns);
|
|
398
424
|
}
|
|
399
425
|
const driverId = ns.driverId;
|
|
@@ -63,6 +63,8 @@ describe("store facets", () => {
|
|
|
63
63
|
test("declares sql · kv · files · index with resource refs", () => {
|
|
64
64
|
expect(store.sql("notes").ref).toBe("sql:notes");
|
|
65
65
|
expect(store.kv("sessions").ref).toBe("kv:sessions");
|
|
66
|
+
expect(store.kv("ledger", { durable: true }).durable).toBe(true);
|
|
67
|
+
expect(store.kv("sessions").durable).toBeUndefined();
|
|
66
68
|
expect(store.files("uploads").ref).toBe("files:uploads");
|
|
67
69
|
expect(store.index("kb", { dims: 8 }).ref).toBe("index:kb");
|
|
68
70
|
});
|
|
@@ -450,6 +450,23 @@ export function createBuiltinVaultAdapter(opts: CreateBuiltinVaultOptions): Buil
|
|
|
450
450
|
* @param keyRow - That version's live wrapped DEK
|
|
451
451
|
*/
|
|
452
452
|
async function decryptRow(row: SecretRow, keyRow: KeyRow): Promise<string> {
|
|
453
|
+
try {
|
|
454
|
+
return await decryptRowOnce(row, keyRow);
|
|
455
|
+
} catch (err) {
|
|
456
|
+
// Stale key snapshot: rotation committed and dropped `pending` after
|
|
457
|
+
// this read. Reload the wrap and retry once — never return torn bytes.
|
|
458
|
+
if (!(err instanceof VaultError) || err.code !== "INVALID_KEY") throw err;
|
|
459
|
+
return await decryptRowOnce(row, await readKeyRow(row.id));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Unwrap the DEK and decrypt the value ciphertext.
|
|
465
|
+
*
|
|
466
|
+
* @param row - Stored secret version
|
|
467
|
+
* @param keyRow - That version's wrapped DEK
|
|
468
|
+
*/
|
|
469
|
+
async function decryptRowOnce(row: SecretRow, keyRow: KeyRow): Promise<string> {
|
|
453
470
|
const path = row.path;
|
|
454
471
|
const version = toInt(row.version);
|
|
455
472
|
const algorithm = row.algorithm as VaultAlgorithm;
|
package/src/full.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -71,6 +71,8 @@ export {
|
|
|
71
71
|
type SignalDecl,
|
|
72
72
|
type SignalOptions,
|
|
73
73
|
type SignalRuntime,
|
|
74
|
+
type DeadLetter,
|
|
75
|
+
type SignalFailureReason,
|
|
74
76
|
} from "./elements/signal.ts";
|
|
75
77
|
|
|
76
78
|
export {
|
|
@@ -143,6 +145,8 @@ export {
|
|
|
143
145
|
type AiModelDecl,
|
|
144
146
|
type AiPromptDecl,
|
|
145
147
|
type AiAgentDecl,
|
|
148
|
+
type AiMcpServerDecl,
|
|
149
|
+
type AiMcpServerOptions,
|
|
146
150
|
type AiRuntime,
|
|
147
151
|
} from "./elements/ai.ts";
|
|
148
152
|
|
package/src/kernel/app.ts
CHANGED
|
@@ -37,10 +37,12 @@ import { runWithLocale } from "../i18n/locale-context.ts";
|
|
|
37
37
|
import { runDurable } from "../elements/clock/durable.ts";
|
|
38
38
|
import { isFlow, type AnyFlowDef } from "./flow.ts";
|
|
39
39
|
import { failureFromUnknown, runCompensationPhase } from "./compensate.ts";
|
|
40
|
+
import { withAbortSignal } from "./abort-scope.ts";
|
|
40
41
|
import { fxRetry } from "./concurrency.ts";
|
|
41
42
|
import {
|
|
42
43
|
createFxContext,
|
|
43
44
|
freezePrincipal,
|
|
45
|
+
isJsonStreamResult,
|
|
44
46
|
resolveName,
|
|
45
47
|
type CreateFxOptions,
|
|
46
48
|
type Fx,
|
|
@@ -85,6 +87,7 @@ import { listBindings, resetBindings, type Binding } from "./on.ts";
|
|
|
85
87
|
import {
|
|
86
88
|
aiAgentRegistry,
|
|
87
89
|
aiEmbedRegistry,
|
|
90
|
+
aiMcpServerRegistry,
|
|
88
91
|
aiModelRegistry,
|
|
89
92
|
aiPromptRegistry,
|
|
90
93
|
channelTemplateRegistry,
|
|
@@ -531,18 +534,21 @@ function mergeAiOptions(
|
|
|
531
534
|
readonly prompts: NonNullable<BootOptions["ai"]>["prompts"];
|
|
532
535
|
readonly embeds: NonNullable<BootOptions["ai"]>["embeds"];
|
|
533
536
|
readonly agents: NonNullable<BootOptions["ai"]>["agents"];
|
|
537
|
+
readonly mcpServers: NonNullable<BootOptions["ai"]>["mcpServers"];
|
|
534
538
|
},
|
|
535
539
|
): BootOptions["ai"] | undefined {
|
|
536
540
|
const models = mergeUnique(explicit?.models, fromRegistry.models ?? []);
|
|
537
541
|
const prompts = mergeUnique(explicit?.prompts, fromRegistry.prompts ?? []);
|
|
538
542
|
const embeds = mergeUnique(explicit?.embeds, fromRegistry.embeds ?? []);
|
|
539
543
|
const agents = mergeUnique(explicit?.agents, fromRegistry.agents ?? []);
|
|
544
|
+
const mcpServers = mergeUnique(explicit?.mcpServers, fromRegistry.mcpServers ?? []);
|
|
540
545
|
if (
|
|
541
546
|
explicit === undefined &&
|
|
542
547
|
models.length === 0 &&
|
|
543
548
|
prompts.length === 0 &&
|
|
544
549
|
embeds.length === 0 &&
|
|
545
|
-
agents.length === 0
|
|
550
|
+
agents.length === 0 &&
|
|
551
|
+
mcpServers.length === 0
|
|
546
552
|
) {
|
|
547
553
|
return undefined;
|
|
548
554
|
}
|
|
@@ -552,6 +558,7 @@ function mergeAiOptions(
|
|
|
552
558
|
...(prompts.length > 0 ? { prompts } : {}),
|
|
553
559
|
...(embeds.length > 0 ? { embeds } : {}),
|
|
554
560
|
...(agents.length > 0 ? { agents } : {}),
|
|
561
|
+
...(mcpServers.length > 0 ? { mcpServers } : {}),
|
|
555
562
|
};
|
|
556
563
|
}
|
|
557
564
|
|
|
@@ -570,7 +577,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
570
577
|
if (registry === "consume") resetBindings();
|
|
571
578
|
|
|
572
579
|
// Same registry mode drains store.sql/store.files, vault.secret, signal(),
|
|
573
|
-
// channel.<medium>().template(), and ai.model/prompt/embed/agent —
|
|
580
|
+
// channel.<medium>().template(), and ai.model/prompt/embed/agent/mcpServer —
|
|
574
581
|
// module-evaluation registries that mirror `on`'s trigger drain
|
|
575
582
|
// (`listBindings`/`resetBindings` above). Explicit `options.stores` /
|
|
576
583
|
// `secrets` / `signals` / `channel.templates` / `ai` are additive, never
|
|
@@ -588,6 +595,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
588
595
|
aiPrompts: [],
|
|
589
596
|
aiEmbeds: [],
|
|
590
597
|
aiAgents: [],
|
|
598
|
+
aiMcpServers: [],
|
|
591
599
|
}
|
|
592
600
|
: {
|
|
593
601
|
stores: storeRegistry.slice(),
|
|
@@ -599,6 +607,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
599
607
|
aiPrompts: aiPromptRegistry.slice(),
|
|
600
608
|
aiEmbeds: aiEmbedRegistry.slice(),
|
|
601
609
|
aiAgents: aiAgentRegistry.slice(),
|
|
610
|
+
aiMcpServers: aiMcpServerRegistry.slice(),
|
|
602
611
|
};
|
|
603
612
|
if (registry === "consume") {
|
|
604
613
|
storeRegistry.length = 0;
|
|
@@ -610,6 +619,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
610
619
|
aiPromptRegistry.length = 0;
|
|
611
620
|
aiEmbedRegistry.length = 0;
|
|
612
621
|
aiAgentRegistry.length = 0;
|
|
622
|
+
aiMcpServerRegistry.length = 0;
|
|
613
623
|
}
|
|
614
624
|
const effectiveStores = mergeUnique(options.stores, registrySnapshot.stores);
|
|
615
625
|
const effectiveSecrets = mergeUnique(options.secrets, registrySnapshot.secrets);
|
|
@@ -643,6 +653,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
643
653
|
prompts: registrySnapshot.aiPrompts,
|
|
644
654
|
embeds: registrySnapshot.aiEmbeds,
|
|
645
655
|
agents: registrySnapshot.aiAgents,
|
|
656
|
+
mcpServers: registrySnapshot.aiMcpServers,
|
|
646
657
|
});
|
|
647
658
|
|
|
648
659
|
// Resolve Gate bag early so auth HTTP Bindings join `adopted` + the router
|
|
@@ -1149,16 +1160,19 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1149
1160
|
defaultLocale,
|
|
1150
1161
|
);
|
|
1151
1162
|
|
|
1152
|
-
return runWithLocale({ locale: resolvedLocale, defaultLocale }, () =>
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1163
|
+
return runWithLocale({ locale: resolvedLocale, defaultLocale }, () => {
|
|
1164
|
+
const run = () =>
|
|
1165
|
+
executeInLocale({
|
|
1166
|
+
flowDef,
|
|
1167
|
+
input,
|
|
1168
|
+
trigger,
|
|
1169
|
+
extras,
|
|
1170
|
+
resolvedLocale,
|
|
1171
|
+
defaultLocale,
|
|
1172
|
+
});
|
|
1173
|
+
const signal = extras?.request?.signal;
|
|
1174
|
+
return signal ? withAbortSignal(signal, run) : run();
|
|
1175
|
+
});
|
|
1162
1176
|
}
|
|
1163
1177
|
|
|
1164
1178
|
async function executeInLocale(args: {
|
|
@@ -1451,7 +1465,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1451
1465
|
durable: flowDef.durable,
|
|
1452
1466
|
effects: putEffects,
|
|
1453
1467
|
});
|
|
1454
|
-
if (storeAfter && output !== undefined) {
|
|
1468
|
+
if (storeAfter && output !== undefined && !isJsonStreamResult(output)) {
|
|
1455
1469
|
const ttlMs =
|
|
1456
1470
|
typeof flowDef.cache === "string" ? cache.parseTtlMs(flowDef.cache) : undefined;
|
|
1457
1471
|
storeRt.putTier1(
|
|
@@ -1499,60 +1513,79 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1499
1513
|
|
|
1500
1514
|
const endedAt = now();
|
|
1501
1515
|
const durationMs = resolveDurationMs(endedAt - startedAt, performance.now() - startedHr);
|
|
1516
|
+
const streamOut = isJsonStreamResult(result.output) ? result.output : undefined;
|
|
1517
|
+
|
|
1518
|
+
const finalizeRun = async (): Promise<void> => {
|
|
1519
|
+
// Stream rows land in Traces only after close. Measure wall time then
|
|
1520
|
+
// so duration / waterfall cover the open stream, not just TTFB.
|
|
1521
|
+
const recordEndedAt = streamOut ? now() : endedAt;
|
|
1522
|
+
const recordDurationMs = streamOut
|
|
1523
|
+
? resolveDurationMs(recordEndedAt - startedAt, performance.now() - startedHr)
|
|
1524
|
+
: durationMs;
|
|
1525
|
+
if (journalSession) {
|
|
1526
|
+
inflightRuns.delete(journalSession.runId);
|
|
1527
|
+
const sleeping = ctx.state.sleeping as
|
|
1528
|
+
| { readonly wakeAt: number; readonly label: string; readonly runId: string }
|
|
1529
|
+
| undefined;
|
|
1530
|
+
// Lease-capable stores make the shared row the wake schedule; the
|
|
1531
|
+
// in-process map is only for custom stores without the lease surface.
|
|
1532
|
+
if (sleeping && !hasJournalLease(activeJournal().store)) {
|
|
1533
|
+
sleepingRuns.set(sleeping.runId, {
|
|
1534
|
+
flow: flowDef,
|
|
1535
|
+
input: ctx.input,
|
|
1536
|
+
wakeAt: sleeping.wakeAt,
|
|
1537
|
+
});
|
|
1538
|
+
} else if (!sleeping && isTerminalFailure(result)) {
|
|
1539
|
+
const terminalErr = result.failure ?? result.ctx.error;
|
|
1540
|
+
await runCompensationPhase({
|
|
1541
|
+
flow: flowDef,
|
|
1542
|
+
input: ctx.input,
|
|
1543
|
+
session: journalSession,
|
|
1544
|
+
fx,
|
|
1545
|
+
error: terminalErr,
|
|
1546
|
+
});
|
|
1547
|
+
} else if (!sleeping) {
|
|
1548
|
+
await journalSession.commit("completed", {
|
|
1549
|
+
output: streamOut ? { streamed: true } : result.output,
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1502
1553
|
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1554
|
+
const runsRuntime =
|
|
1555
|
+
booted?.runs ?? (isRunsRuntimeLike(options.runs) ? options.runs : undefined);
|
|
1556
|
+
if (runsRuntime) {
|
|
1557
|
+
const archiveCleartext = archiveFromInput(ctx.input, options.archiveInputFields);
|
|
1558
|
+
const replayInput = redactArchivedFields(ctx.input, options.archiveInputFields);
|
|
1559
|
+
const recordFailure =
|
|
1560
|
+
result.failure ??
|
|
1561
|
+
(isTerminalFailure(result) ? failureFromUnknown(result.ctx.error) : undefined);
|
|
1562
|
+
await runsRuntime.record(
|
|
1563
|
+
{
|
|
1564
|
+
flow: flowDef,
|
|
1565
|
+
trigger,
|
|
1566
|
+
fx,
|
|
1567
|
+
ledger,
|
|
1568
|
+
telemetry,
|
|
1569
|
+
startedAt,
|
|
1570
|
+
endedAt: recordEndedAt,
|
|
1571
|
+
durationMs: recordDurationMs,
|
|
1572
|
+
failure: recordFailure,
|
|
1573
|
+
id: runId,
|
|
1574
|
+
input: replayInput,
|
|
1575
|
+
...(result.output !== undefined && !recordFailure
|
|
1576
|
+
? { output: streamOut ? { streamed: true } : result.output }
|
|
1577
|
+
: {}),
|
|
1578
|
+
...(parentId !== undefined ? { parentId } : {}),
|
|
1579
|
+
},
|
|
1580
|
+
archiveCleartext,
|
|
1581
|
+
);
|
|
1527
1582
|
}
|
|
1528
|
-
}
|
|
1583
|
+
};
|
|
1529
1584
|
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
const replayInput = redactArchivedFields(ctx.input, options.archiveInputFields);
|
|
1535
|
-
const recordFailure =
|
|
1536
|
-
result.failure ??
|
|
1537
|
-
(isTerminalFailure(result) ? failureFromUnknown(result.ctx.error) : undefined);
|
|
1538
|
-
await runsRuntime.record(
|
|
1539
|
-
{
|
|
1540
|
-
flow: flowDef,
|
|
1541
|
-
trigger,
|
|
1542
|
-
fx,
|
|
1543
|
-
ledger,
|
|
1544
|
-
telemetry,
|
|
1545
|
-
startedAt,
|
|
1546
|
-
endedAt,
|
|
1547
|
-
durationMs,
|
|
1548
|
-
failure: recordFailure,
|
|
1549
|
-
id: runId,
|
|
1550
|
-
input: replayInput,
|
|
1551
|
-
...(result.output !== undefined && !recordFailure ? { output: result.output } : {}),
|
|
1552
|
-
...(parentId !== undefined ? { parentId } : {}),
|
|
1553
|
-
},
|
|
1554
|
-
archiveCleartext,
|
|
1555
|
-
);
|
|
1585
|
+
if (streamOut && !isTerminalFailure(result)) {
|
|
1586
|
+
streamOut.finalize = finalizeRun;
|
|
1587
|
+
} else {
|
|
1588
|
+
await finalizeRun();
|
|
1556
1589
|
}
|
|
1557
1590
|
|
|
1558
1591
|
return {
|
|
@@ -205,6 +205,10 @@ describe("oke() auto-registry — stores/secrets/signals/channel.templates/ai",
|
|
|
205
205
|
const summarizeNote = smart.prompt("summarize-note", {
|
|
206
206
|
version: 1,
|
|
207
207
|
});
|
|
208
|
+
ai.mcpServer("github", {
|
|
209
|
+
url: "https://mcp.example/github",
|
|
210
|
+
tools: ["create_issue"],
|
|
211
|
+
});
|
|
208
212
|
|
|
209
213
|
const ping = flow("ai.ping", {
|
|
210
214
|
effects: { asks: ["summarize-note"] },
|
|
@@ -221,6 +225,7 @@ describe("oke() auto-registry — stores/secrets/signals/channel.templates/ai",
|
|
|
221
225
|
|
|
222
226
|
expect(app.$options.ai?.models?.some((m) => m.name === "smart")).toBe(true);
|
|
223
227
|
expect(app.$options.ai?.prompts?.some((p) => p.name === "summarize-note")).toBe(true);
|
|
228
|
+
expect(app.$options.ai?.mcpServers?.some((s) => s.name === "github")).toBe(true);
|
|
224
229
|
|
|
225
230
|
const runtime = createAiRuntime({
|
|
226
231
|
models: [smart],
|
|
@@ -18,6 +18,7 @@ import { openaiCompatibleAiDriver } from "../../drivers/ai-openai-compatible.ts"
|
|
|
18
18
|
import type { AiDriver, AiOpenOptions } from "../../drivers/ai-types.ts";
|
|
19
19
|
import { createAiRuntime, type AiRuntime } from "../../elements/ai.ts";
|
|
20
20
|
import type { GateRuntime } from "../../elements/gate.ts";
|
|
21
|
+
import type { VaultRuntime } from "../../elements/vault.ts";
|
|
21
22
|
import type { BootOptions } from "../boot.ts";
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -28,6 +29,7 @@ import type { BootOptions } from "../boot.ts";
|
|
|
28
29
|
* @param now - Clock
|
|
29
30
|
* @param env - Active environment
|
|
30
31
|
* @param docker - Prefer compose AI URL when active
|
|
32
|
+
* @param vault - Optional vault for MCP bearer secrets
|
|
31
33
|
*/
|
|
32
34
|
export function bindAi(
|
|
33
35
|
options: BootOptions,
|
|
@@ -35,13 +37,21 @@ export function bindAi(
|
|
|
35
37
|
now: () => number,
|
|
36
38
|
env: ConfigEnv = "test",
|
|
37
39
|
docker = false,
|
|
40
|
+
vault?: VaultRuntime,
|
|
38
41
|
): AiRuntime {
|
|
39
42
|
const id = resolveAiDriverId(options, env, docker);
|
|
40
43
|
const driver =
|
|
41
44
|
options.ai?.defaultDriver ?? withOpenDefaults(aiDriverFor(id), openDefaultsFor(id, docker));
|
|
42
45
|
return createAiRuntime({
|
|
43
46
|
...(options.ai ?? {}),
|
|
47
|
+
...(vault ? { resolveSecret: (name: string) => vault.read(name) } : undefined),
|
|
44
48
|
defaultDriver: driver,
|
|
49
|
+
drivers: {
|
|
50
|
+
mock: lazyDriver("mock", docker),
|
|
51
|
+
anthropic: lazyDriver("anthropic", docker),
|
|
52
|
+
"openai-compatible": lazyDriver("openai-compatible", docker),
|
|
53
|
+
ollama: lazyDriver("ollama", docker),
|
|
54
|
+
},
|
|
45
55
|
gates: options.ai?.gates ?? gate,
|
|
46
56
|
now,
|
|
47
57
|
});
|
|
@@ -156,6 +166,20 @@ export function aiUrlFor(docker = false): string {
|
|
|
156
166
|
* @param driver - Base driver
|
|
157
167
|
* @param defaults - Env-derived open options
|
|
158
168
|
*/
|
|
169
|
+
/**
|
|
170
|
+
* Open a protocol driver with env defaults only when a model actually uses it.
|
|
171
|
+
*
|
|
172
|
+
* @param id - Driver id
|
|
173
|
+
* @param docker - Docker mode
|
|
174
|
+
*/
|
|
175
|
+
function lazyDriver(id: string, docker: boolean): AiDriver {
|
|
176
|
+
const base = aiDriverFor(id);
|
|
177
|
+
return {
|
|
178
|
+
id: base.id,
|
|
179
|
+
open: (opts = {}) => base.open({ ...openDefaultsFor(id, docker), ...opts }),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
159
183
|
function withOpenDefaults(driver: AiDriver, defaults: AiOpenOptions): AiDriver {
|
|
160
184
|
if (Object.keys(defaults).length === 0) return driver;
|
|
161
185
|
return {
|