okengine 0.13.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 +1 -1
- package/site/content/docs/get-started/installation.mdx +2 -2
- 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/compiler/effects-infer.ts +58 -3
- package/src/compiler/extract.test.ts +54 -1
- package/src/compiler/extract.ts +69 -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-Bp-R7jtM.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-Ca-MvcmJ.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/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/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/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/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.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/mcp-ref.ts +88 -0
- package/src/manifest/types.ts +28 -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-Dg8CTE29.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-DAnpEaq1.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-BtQ0bqMe.js +0 -1
package/src/elements/index.ts
CHANGED
|
@@ -34,7 +34,8 @@ export interface SignalOptions {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Declared signal handle — usable as `on(signal, flow)
|
|
37
|
+
* Declared signal handle — usable as `on(signal, flow)`, `fx.emit(signal, …)`,
|
|
38
|
+
* and `fx.deadLetters(signal)`.
|
|
38
39
|
*/
|
|
39
40
|
export interface SignalDecl<T = unknown> {
|
|
40
41
|
/** Signal name (manifest key). */
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
* emit / subscribe / live / drain for `fx` and tests.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
DeadLetter,
|
|
8
|
+
SignalBus,
|
|
9
|
+
SignalDriver,
|
|
10
|
+
SignalEmitOptions,
|
|
11
|
+
} from "../../drivers/signal-types.ts";
|
|
7
12
|
import type { SignalDecl } from "./declare.ts";
|
|
8
13
|
|
|
9
14
|
/** Options for {@link createSignalRuntime}. */
|
|
@@ -48,6 +53,12 @@ export interface SignalRuntime {
|
|
|
48
53
|
* @param options - Optional emit options (`key` for per-key once ordering)
|
|
49
54
|
*/
|
|
50
55
|
emit(name: string, payload?: unknown, options?: SignalEmitOptions): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Dead-lettered messages for one signal (auto-starts).
|
|
58
|
+
*
|
|
59
|
+
* @param name - Signal name
|
|
60
|
+
*/
|
|
61
|
+
deadLetters(name: string): Promise<readonly DeadLetter[]>;
|
|
51
62
|
/** Close the bus. */
|
|
52
63
|
close(): Promise<void>;
|
|
53
64
|
}
|
|
@@ -87,6 +98,10 @@ export function createSignalRuntime(options: CreateSignalRuntimeOptions): Signal
|
|
|
87
98
|
const b = await this.start();
|
|
88
99
|
await b.emit(name, payload, options);
|
|
89
100
|
},
|
|
101
|
+
async deadLetters(name) {
|
|
102
|
+
const b = await this.start();
|
|
103
|
+
return b.deadLetters(name);
|
|
104
|
+
},
|
|
90
105
|
async close() {
|
|
91
106
|
if (bus) {
|
|
92
107
|
await bus.close();
|
package/src/elements/signal.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
export { signal } from "./signal/declare.ts";
|
|
10
10
|
export type { SignalDecl, SignalOptions } from "./signal/declare.ts";
|
|
11
|
+
export type { DeadLetter, SignalFailureReason } from "../drivers/signal-types.ts";
|
|
11
12
|
|
|
12
13
|
export { createSignalRuntime } from "./signal/runtime.ts";
|
|
13
14
|
export type { CreateSignalRuntimeOptions, SignalRuntime } from "./signal/runtime.ts";
|
|
@@ -92,6 +92,7 @@ describe("autoCacheEligible", () => {
|
|
|
92
92
|
false,
|
|
93
93
|
);
|
|
94
94
|
expect(autoCacheEligible({ effects: { reads: ["runs"] } })).toBe(false);
|
|
95
|
+
expect(autoCacheEligible({ effects: { reads: ["signal:notify"] } })).toBe(false);
|
|
95
96
|
expect(autoCacheEligible({ effects: {} })).toBe(false);
|
|
96
97
|
});
|
|
97
98
|
|
|
@@ -112,6 +113,7 @@ describe("autoCacheEligible", () => {
|
|
|
112
113
|
{ kind: "read", resource: "sql:views" },
|
|
113
114
|
{ kind: "read", resource: "sql:views" },
|
|
114
115
|
{ kind: "read", resource: "runs" },
|
|
116
|
+
{ kind: "read", resource: "signal:notify" },
|
|
115
117
|
{ kind: "write", resource: "sql:views" },
|
|
116
118
|
{ kind: "ask", resource: "summarize" },
|
|
117
119
|
{ kind: "emit", resource: "view-changed" },
|
|
@@ -53,7 +53,7 @@ export function tier1KeysForReads(
|
|
|
53
53
|
effects: Effects,
|
|
54
54
|
dimsByResource?: Readonly<Record<string, readonly string[]>>,
|
|
55
55
|
): string[] {
|
|
56
|
-
const reads = (effects.reads ?? []).filter((r): r is ResourceRef => r
|
|
56
|
+
const reads = (effects.reads ?? []).filter((r): r is ResourceRef => isStoreResourceRef(r));
|
|
57
57
|
return reads.map((resource) => computedCacheKey(resource, dimsByResource?.[resource]));
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -223,7 +223,7 @@ export function resolveCacheEffects(
|
|
|
223
223
|
declared: Effects | undefined,
|
|
224
224
|
learnedReads: readonly ResourceRef[] | undefined,
|
|
225
225
|
): Effects {
|
|
226
|
-
const declaredReads = (declared?.reads ?? []).filter((r) => r
|
|
226
|
+
const declaredReads = (declared?.reads ?? []).filter((r) => isStoreResourceRef(r));
|
|
227
227
|
if (declaredReads.length > 0 || (declared?.writes?.length ?? 0) > 0) {
|
|
228
228
|
return declared ?? {};
|
|
229
229
|
}
|
|
@@ -275,7 +275,7 @@ export function autoCacheEligible(options: {
|
|
|
275
275
|
const effects = options.effects ?? {};
|
|
276
276
|
if ((effects.asks?.length ?? 0) > 0) return false;
|
|
277
277
|
if ((effects.writes?.length ?? 0) > 0) return false;
|
|
278
|
-
const reads = (effects.reads ?? []).filter((r) => r
|
|
278
|
+
const reads = (effects.reads ?? []).filter((r) => isStoreResourceRef(r));
|
|
279
279
|
return reads.length > 0;
|
|
280
280
|
}
|
|
281
281
|
|
|
@@ -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 {
|
package/src/kernel/boot.ts
CHANGED
|
@@ -550,7 +550,7 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
550
550
|
// 6. AI
|
|
551
551
|
let ai = pre.ai;
|
|
552
552
|
if (needs.ai && !ai) {
|
|
553
|
-
ai = aiBind!.bindAi(options, gate, now, env, docker);
|
|
553
|
+
ai = aiBind!.bindAi(options, gate, now, env, docker, vault);
|
|
554
554
|
}
|
|
555
555
|
|
|
556
556
|
// 7. Runs
|
|
@@ -607,12 +607,14 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
607
607
|
clearInterval(schedulerTimer);
|
|
608
608
|
schedulerTimer = undefined;
|
|
609
609
|
}
|
|
610
|
+
clock?.stopWakes();
|
|
610
611
|
},
|
|
611
612
|
async close() {
|
|
612
613
|
if (schedulerTimer !== undefined) {
|
|
613
614
|
clearInterval(schedulerTimer);
|
|
614
615
|
schedulerTimer = undefined;
|
|
615
616
|
}
|
|
617
|
+
clock?.stopWakes();
|
|
616
618
|
await signal?.close();
|
|
617
619
|
await vault?.close();
|
|
618
620
|
await runs?.flush();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Module-evaluation registries for `store.sql` / `store.files`, `vault.secret`,
|
|
3
3
|
* `vault.env.required`, `signal()`, `channel.<medium>().template()`, and
|
|
4
|
-
* `ai.model` / `.prompt` / `ai.embed` / `ai.agent` — the plain arrays behind
|
|
4
|
+
* `ai.model` / `.prompt` / `ai.embed` / `ai.agent` / `ai.mcpServer` — the plain arrays behind
|
|
5
5
|
* each declare module's `listX()` / `resetX()` pair (mirrors `on.ts`'s
|
|
6
6
|
* trigger-drain `bindings` array).
|
|
7
7
|
*
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import type {
|
|
17
17
|
AiAgentDecl,
|
|
18
18
|
AiEmbedDecl,
|
|
19
|
+
AiMcpServerDecl,
|
|
19
20
|
AiModelDecl,
|
|
20
21
|
AiPromptDecl,
|
|
21
22
|
} from "../elements/ai/declare.ts";
|
|
@@ -42,3 +43,5 @@ export const aiPromptRegistry: AiPromptDecl[] = [];
|
|
|
42
43
|
export const aiEmbedRegistry: AiEmbedDecl[] = [];
|
|
43
44
|
/** `ai.agent` declarations since the last reset. */
|
|
44
45
|
export const aiAgentRegistry: AiAgentDecl[] = [];
|
|
46
|
+
/** `ai.mcpServer` declarations since the last reset. */
|
|
47
|
+
export const aiMcpServerRegistry: AiMcpServerDecl[] = [];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { memorySignalDriver } from "../drivers/index.ts";
|
|
3
|
+
import { createSignalRuntime, signal, type SignalDecl } from "../elements/signal.ts";
|
|
4
|
+
import { OkeError } from "./errors.ts";
|
|
5
|
+
import { createFx, createFxContext, signalReadRef } from "./fx.ts";
|
|
6
|
+
|
|
7
|
+
describe("fx.deadLetters", () => {
|
|
8
|
+
test("returns dead-lettered messages for a declared signal read", async () => {
|
|
9
|
+
const notify = signal("notify", { delivery: "once", retries: 0, deadLetter: true });
|
|
10
|
+
const runtime = openRuntime(notify);
|
|
11
|
+
const bus = await runtime.start();
|
|
12
|
+
await bus.subscribe("notify", "c1", async () => {
|
|
13
|
+
throw new Error("smtp-down");
|
|
14
|
+
});
|
|
15
|
+
await runtime.emit("notify", { orderId: "ord_1" });
|
|
16
|
+
await bus.drain();
|
|
17
|
+
|
|
18
|
+
const { fx, ledger } = createFxContext({
|
|
19
|
+
flow: "notifications.failed",
|
|
20
|
+
effects: { reads: [signalReadRef("notify")] },
|
|
21
|
+
signalRuntime: runtime,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const dead = await fx.deadLetters(notify);
|
|
25
|
+
expect(dead).toHaveLength(1);
|
|
26
|
+
expect(dead[0]!.payload).toEqual({ orderId: "ord_1" });
|
|
27
|
+
expect(dead[0]!.status).toBe("dead");
|
|
28
|
+
expect(dead[0]!.attempts).toBe(1);
|
|
29
|
+
expect(dead[0]!.failures[0]!.message).toBe("smtp-down");
|
|
30
|
+
expect(ledger.entries.map((e) => `${e.kind}:${e.resource}`)).toEqual(["read:signal:notify"]);
|
|
31
|
+
|
|
32
|
+
await runtime.close();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("undeclared and cross-signal reads throw OKE1001", async () => {
|
|
36
|
+
const notify = signal("notify", { delivery: "once", retries: 0, deadLetter: true });
|
|
37
|
+
const other = signal("other", { delivery: "once", retries: 0, deadLetter: true });
|
|
38
|
+
const runtime = openRuntime(notify);
|
|
39
|
+
const fx = createFx({
|
|
40
|
+
flow: "notifications.failed",
|
|
41
|
+
effects: { reads: [signalReadRef("notify")] },
|
|
42
|
+
signalRuntime: runtime,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
let err: unknown;
|
|
46
|
+
try {
|
|
47
|
+
await fx.deadLetters(other);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
err = e;
|
|
50
|
+
}
|
|
51
|
+
expect(err).toBeInstanceOf(OkeError);
|
|
52
|
+
const oke = err as OkeError;
|
|
53
|
+
expect(oke.code).toBe(1001);
|
|
54
|
+
expect(oke.causeText).toBe(
|
|
55
|
+
'Flow "notifications.failed" reads "signal:other" without declaring it.',
|
|
56
|
+
);
|
|
57
|
+
expect(oke.fix).toBe('Add "signal:other" to this flow\'s effects.reads.');
|
|
58
|
+
|
|
59
|
+
await runtime.close();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("throws when no signal runtime is bound", async () => {
|
|
63
|
+
const fx = createFx({
|
|
64
|
+
flow: "notifications.failed",
|
|
65
|
+
effects: { reads: [signalReadRef("notify")] },
|
|
66
|
+
});
|
|
67
|
+
await expect(fx.deadLetters("notify")).rejects.toThrow(
|
|
68
|
+
"fx.deadLetters requires a bound signal runtime",
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
function openRuntime(decl: SignalDecl) {
|
|
74
|
+
const runtime = createSignalRuntime({ driver: memorySignalDriver });
|
|
75
|
+
runtime.register(decl);
|
|
76
|
+
return runtime;
|
|
77
|
+
}
|