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,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP SSE via `fx.json.stream` + client-disconnect abort of the provider fetch.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { ai } from "../elements/ai.ts";
|
|
7
|
+
import type { AiDriver } from "../drivers/ai-types.ts";
|
|
8
|
+
import { createRunsRuntime } from "../runs/runtime.ts";
|
|
9
|
+
import { createBunRuntime } from "../runtime/bun.ts";
|
|
10
|
+
import type { ServerHandle } from "../runtime/types.ts";
|
|
11
|
+
import { oke } from "./app.ts";
|
|
12
|
+
import { flow, resetFlowSeq } from "./flow.ts";
|
|
13
|
+
import { on, resetBindings } from "./on.ts";
|
|
14
|
+
import { http } from "./triggers.ts";
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
resetBindings();
|
|
18
|
+
resetFlowSeq();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function tokenDriver(chunks: readonly string[], delayMs = 0): AiDriver {
|
|
22
|
+
return {
|
|
23
|
+
id: "mock",
|
|
24
|
+
async open() {
|
|
25
|
+
return {
|
|
26
|
+
driverId: "mock" as const,
|
|
27
|
+
model: "smart",
|
|
28
|
+
async complete() {
|
|
29
|
+
return { text: "", model: "smart", driverId: "mock" as const };
|
|
30
|
+
},
|
|
31
|
+
async *stream() {
|
|
32
|
+
for (const text of chunks) {
|
|
33
|
+
if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
|
|
34
|
+
yield { text };
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hangingDriver(onAbort: () => void): AiDriver {
|
|
43
|
+
return {
|
|
44
|
+
id: "mock",
|
|
45
|
+
async open() {
|
|
46
|
+
return {
|
|
47
|
+
driverId: "mock" as const,
|
|
48
|
+
model: "smart",
|
|
49
|
+
async complete() {
|
|
50
|
+
return { text: "", model: "smart", driverId: "mock" as const };
|
|
51
|
+
},
|
|
52
|
+
async *stream(opts) {
|
|
53
|
+
yield { text: "start" };
|
|
54
|
+
await new Promise<void>((resolve, reject) => {
|
|
55
|
+
const t = setTimeout(resolve, 8_000);
|
|
56
|
+
opts.signal?.addEventListener(
|
|
57
|
+
"abort",
|
|
58
|
+
() => {
|
|
59
|
+
onAbort();
|
|
60
|
+
clearTimeout(t);
|
|
61
|
+
const err = new Error("aborted");
|
|
62
|
+
err.name = "AbortError";
|
|
63
|
+
reject(err);
|
|
64
|
+
},
|
|
65
|
+
{ once: true },
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
yield { text: "never" };
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("fx.json.stream HTTP SSE", () => {
|
|
76
|
+
let handle: ServerHandle | undefined;
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
handle?.stop(true);
|
|
80
|
+
handle = undefined;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("encodes token chunks as SSE and leaves JSON flows buffered", async () => {
|
|
84
|
+
const smart = ai.model("smart");
|
|
85
|
+
on(
|
|
86
|
+
http.get("/chat").gate.public,
|
|
87
|
+
flow("chat.stream", {
|
|
88
|
+
do: (_input, fx) => fx.json.stream(fx.stream(smart, { prompt: "hi" })),
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
on(http.get("/ping").gate.public, flow("ping", { do: () => ({ ok: true as const }) }));
|
|
92
|
+
|
|
93
|
+
const app = oke({
|
|
94
|
+
name: "http-stream",
|
|
95
|
+
ai: { models: [smart], defaultDriver: tokenDriver(["Hel", "lo"]) },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const streamed = await app.fetch(new Request("http://localhost/chat"));
|
|
99
|
+
expect(streamed.headers.get("content-type")).toMatch(/text\/event-stream/);
|
|
100
|
+
const body = await streamed.text();
|
|
101
|
+
expect(body).toContain('data: "Hel"');
|
|
102
|
+
expect(body).toContain('data: "lo"');
|
|
103
|
+
expect(body).toContain("data: [DONE]");
|
|
104
|
+
|
|
105
|
+
const json = await app.fetch(new Request("http://localhost/ping"));
|
|
106
|
+
expect(json.headers.get("content-type")).toMatch(/application\/json/);
|
|
107
|
+
expect(await json.json()).toEqual({ data: { ok: true }, error: null });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("client abort mid-stream aborts the upstream provider fetch", async () => {
|
|
111
|
+
let providerAborted = false;
|
|
112
|
+
const smart = ai.model("smart");
|
|
113
|
+
on(
|
|
114
|
+
http.get("/slow").gate.public,
|
|
115
|
+
flow("chat.slow", {
|
|
116
|
+
do: (_input, fx) => fx.json.stream(fx.stream(smart, { prompt: "long" })),
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const app = oke({
|
|
121
|
+
name: "http-stream-abort",
|
|
122
|
+
ai: { models: [smart], defaultDriver: hangingDriver(() => (providerAborted = true)) },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
handle = createBunRuntime().serve(app, { port: 0, hostname: "127.0.0.1" });
|
|
126
|
+
const ac = new AbortController();
|
|
127
|
+
const res = await fetch(`http://127.0.0.1:${handle.port}/slow`, { signal: ac.signal });
|
|
128
|
+
expect(res.headers.get("content-type")).toMatch(/text\/event-stream/);
|
|
129
|
+
const reader = res.body!.getReader();
|
|
130
|
+
const first = await reader.read();
|
|
131
|
+
expect(first.done).toBe(false);
|
|
132
|
+
ac.abort();
|
|
133
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
134
|
+
expect(providerAborted).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("Traces stay empty until stream close; duration covers the open stream", async () => {
|
|
138
|
+
const runs = createRunsRuntime({ driver: "memory" });
|
|
139
|
+
await runs.open();
|
|
140
|
+
const smart = ai.model("smart");
|
|
141
|
+
on(
|
|
142
|
+
http.get("/chat").gate.public,
|
|
143
|
+
flow("chat.stream", {
|
|
144
|
+
do: (_input, fx) => fx.json.stream(fx.stream(smart, { prompt: "hi" })),
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const app = oke({
|
|
149
|
+
name: "http-stream-traces",
|
|
150
|
+
ai: { models: [smart], defaultDriver: tokenDriver(["Hel", "lo"], 25) },
|
|
151
|
+
runs,
|
|
152
|
+
env: "test",
|
|
153
|
+
});
|
|
154
|
+
await app.boot({ env: "test", runs, startScheduler: false });
|
|
155
|
+
|
|
156
|
+
const streamed = await app.fetch(new Request("http://localhost/chat"));
|
|
157
|
+
expect(streamed.headers.get("content-type")).toMatch(/text\/event-stream/);
|
|
158
|
+
await runs.flush();
|
|
159
|
+
expect(await runs.all()).toHaveLength(0);
|
|
160
|
+
|
|
161
|
+
const body = await streamed.text();
|
|
162
|
+
expect(body).toContain("data: [DONE]");
|
|
163
|
+
|
|
164
|
+
await runs.flush();
|
|
165
|
+
const events = await runs.all();
|
|
166
|
+
expect(events).toHaveLength(1);
|
|
167
|
+
expect(events[0]!.error ?? null).toBeNull();
|
|
168
|
+
expect(events[0]!.output).toEqual({ streamed: true });
|
|
169
|
+
expect(events[0]!.durationMs).toBeGreaterThanOrEqual(40);
|
|
170
|
+
|
|
171
|
+
await app.stop();
|
|
172
|
+
await runs.close();
|
|
173
|
+
});
|
|
174
|
+
});
|
package/src/kernel/index.ts
CHANGED
|
@@ -137,6 +137,7 @@ export {
|
|
|
137
137
|
createFxContext,
|
|
138
138
|
freezePrincipal,
|
|
139
139
|
isJsonResult,
|
|
140
|
+
isJsonStreamResult,
|
|
140
141
|
jsonResultBrand,
|
|
141
142
|
resolveName,
|
|
142
143
|
resolveStoreRef,
|
|
@@ -160,6 +161,7 @@ export {
|
|
|
160
161
|
type FxThunk,
|
|
161
162
|
type JsonPage,
|
|
162
163
|
type JsonResult,
|
|
164
|
+
type JsonStreamResult,
|
|
163
165
|
type NamedRef,
|
|
164
166
|
type StepOptions,
|
|
165
167
|
} from "./fx.ts";
|
|
@@ -309,6 +309,19 @@ describe("diffManifest — additional branches", () => {
|
|
|
309
309
|
expect(hasCategory(result.changes, "contract-breaking", "facet")).toBe(true);
|
|
310
310
|
});
|
|
311
311
|
|
|
312
|
+
test("store.kv durable on is effect-widening; off is contract-breaking", async () => {
|
|
313
|
+
const before = await loadBase();
|
|
314
|
+
before.stores!.db!.facet = "kv";
|
|
315
|
+
before.stores!.db!.namespaces = ["cache"];
|
|
316
|
+
delete before.stores!.db!.tables;
|
|
317
|
+
const on = clone(before);
|
|
318
|
+
on.stores!.db!.durable = true;
|
|
319
|
+
expect(hasCategory(diffManifest(before, on).changes, "effect-widening", "durable")).toBe(true);
|
|
320
|
+
const off = clone(on);
|
|
321
|
+
off.stores!.db!.durable = false;
|
|
322
|
+
expect(hasCategory(diffManifest(on, off).changes, "contract-breaking", "durable")).toBe(true);
|
|
323
|
+
});
|
|
324
|
+
|
|
312
325
|
test("clocks, vault, channels, journeys, drivers, i18n, topology, images", async () => {
|
|
313
326
|
const before = await loadBase();
|
|
314
327
|
const after = clone(before);
|
package/src/manifest/diff.ts
CHANGED
|
@@ -779,6 +779,21 @@ function diffStore(before: Store, after: Store, path: string, out: ManifestChang
|
|
|
779
779
|
diffStringSet(before.namespaces, after.namespaces, `${path}/namespaces`, out);
|
|
780
780
|
diffStringSet(before.buckets, after.buckets, `${path}/buckets`, out);
|
|
781
781
|
diffStringSet(before.indexes, after.indexes, `${path}/indexes`, out);
|
|
782
|
+
if (
|
|
783
|
+
before.durable !== after.durable &&
|
|
784
|
+
(before.durable !== undefined || after.durable !== undefined)
|
|
785
|
+
) {
|
|
786
|
+
out.push(
|
|
787
|
+
change(
|
|
788
|
+
`${path}/durable`,
|
|
789
|
+
after.durable === true ? "effect-widening" : "contract-breaking",
|
|
790
|
+
"changed",
|
|
791
|
+
before.durable,
|
|
792
|
+
after.durable,
|
|
793
|
+
`store durable ${String(before.durable)} → ${String(after.durable)}`,
|
|
794
|
+
),
|
|
795
|
+
);
|
|
796
|
+
}
|
|
782
797
|
if (!deepEqual(before.classifications, after.classifications)) {
|
|
783
798
|
out.push(
|
|
784
799
|
change(
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool resource refs — `mcp:<server>/<tool>` (capability / Manifest)
|
|
3
|
+
* and `<server>__<tool>` (provider-safe model tool name).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Capability / Manifest ref for one declared MCP tool. */
|
|
7
|
+
export type McpToolRef = `mcp:${string}/${string}`;
|
|
8
|
+
|
|
9
|
+
/** Parsed `mcp:<server>/<tool>` ref. */
|
|
10
|
+
export interface ParsedMcpToolRef {
|
|
11
|
+
readonly server: string;
|
|
12
|
+
readonly tool: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build a capability ref.
|
|
17
|
+
*
|
|
18
|
+
* @param server - Declared MCP server name
|
|
19
|
+
* @param tool - Allowlisted tool name on that server
|
|
20
|
+
*/
|
|
21
|
+
export function mcpToolRef(server: string, tool: string): McpToolRef {
|
|
22
|
+
return `mcp:${server}/${tool}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse `mcp:<server>/<tool>`. Null when the prefix or parts are missing.
|
|
27
|
+
*
|
|
28
|
+
* @param ref - Effect / tool resource string
|
|
29
|
+
*/
|
|
30
|
+
export function parseMcpToolRef(ref: string): ParsedMcpToolRef | null {
|
|
31
|
+
if (!ref.startsWith("mcp:")) return null;
|
|
32
|
+
const rest = ref.slice(4);
|
|
33
|
+
const slash = rest.indexOf("/");
|
|
34
|
+
if (slash <= 0 || slash === rest.length - 1) return null;
|
|
35
|
+
return { server: rest.slice(0, slash), tool: rest.slice(slash + 1) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* True when `ref` is a well-formed MCP capability ref.
|
|
40
|
+
*
|
|
41
|
+
* @param ref - Candidate
|
|
42
|
+
*/
|
|
43
|
+
export function isMcpToolRef(ref: string): boolean {
|
|
44
|
+
return parseMcpToolRef(ref) !== null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Graph node id for one MCP server (`mcp:github`).
|
|
49
|
+
*
|
|
50
|
+
* @param server - Declared server name
|
|
51
|
+
*/
|
|
52
|
+
export function mcpServerNodeId(server: string): string {
|
|
53
|
+
return `mcp:${server}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Provider-safe function name (`github__create_issue`).
|
|
58
|
+
*
|
|
59
|
+
* @param server - Declared server name
|
|
60
|
+
* @param tool - MCP tool name
|
|
61
|
+
*/
|
|
62
|
+
export function mcpModelToolName(server: string, tool: string): string {
|
|
63
|
+
return `${server}__${tool}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Inverse of {@link mcpModelToolName}.
|
|
68
|
+
*
|
|
69
|
+
* @param name - Model-facing tool name
|
|
70
|
+
*/
|
|
71
|
+
export function parseMcpModelToolName(name: string): ParsedMcpToolRef | null {
|
|
72
|
+
const sep = name.indexOf("__");
|
|
73
|
+
if (sep <= 0 || sep === name.length - 2) return null;
|
|
74
|
+
return { server: name.slice(0, sep), tool: name.slice(sep + 2) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Capability ref from either `mcp:server/tool` or `server__tool`.
|
|
79
|
+
*
|
|
80
|
+
* @param name - Model-facing or capability name
|
|
81
|
+
*/
|
|
82
|
+
export function mcpCapabilityRefFromName(name: string): McpToolRef | null {
|
|
83
|
+
const fromRef = parseMcpToolRef(name);
|
|
84
|
+
if (fromRef) return mcpToolRef(fromRef.server, fromRef.tool);
|
|
85
|
+
const fromModel = parseMcpModelToolName(name);
|
|
86
|
+
if (fromModel) return mcpToolRef(fromModel.server, fromModel.tool);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
package/src/manifest/types.ts
CHANGED
|
@@ -46,6 +46,12 @@ export type ResourceRef = `${StoreFacet}:${string}`;
|
|
|
46
46
|
*/
|
|
47
47
|
export type RunsResourceRef = "runs";
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Dead-letter read capability for `fx.deadLetters` — not a store facet.
|
|
51
|
+
* Declare on `effects.reads` (never `writes`).
|
|
52
|
+
*/
|
|
53
|
+
export type SignalResourceRef = `signal:${string}`;
|
|
54
|
+
|
|
49
55
|
/** Signal name reference. */
|
|
50
56
|
export type SignalRef = string;
|
|
51
57
|
|
|
@@ -74,8 +80,8 @@ export type JsonSchema = string | Record<string, unknown>;
|
|
|
74
80
|
* `sends` / `asks` are irreversible (asks also nondeterministic + cost).
|
|
75
81
|
*/
|
|
76
82
|
export interface Effects {
|
|
77
|
-
/** Store reads, plus
|
|
78
|
-
reads?: Array<ResourceRef | RunsResourceRef>;
|
|
83
|
+
/** Store reads, plus `"runs"` for `fx.runs` and `signal:name` for `fx.deadLetters`. */
|
|
84
|
+
reads?: Array<ResourceRef | RunsResourceRef | SignalResourceRef>;
|
|
79
85
|
/** Store writes. */
|
|
80
86
|
writes?: ResourceRef[];
|
|
81
87
|
/** Emitted signals. */
|
|
@@ -234,6 +240,11 @@ export interface Store {
|
|
|
234
240
|
buckets?: string[];
|
|
235
241
|
indexes?: string[];
|
|
236
242
|
classifications?: Record<string, ClassificationValue>;
|
|
243
|
+
/**
|
|
244
|
+
* KV only — namespace persists in SQL (`oke_kv`), not the cache Redis.
|
|
245
|
+
* Distinct from Flow `durable` (journaled steps).
|
|
246
|
+
*/
|
|
247
|
+
durable?: boolean;
|
|
237
248
|
}
|
|
238
249
|
|
|
239
250
|
/** Named clock / schedule. */
|
|
@@ -292,6 +303,8 @@ export interface AiModel {
|
|
|
292
303
|
provider?: string;
|
|
293
304
|
tier?: string;
|
|
294
305
|
model?: string;
|
|
306
|
+
/** Protocol driver override for this logical binding. */
|
|
307
|
+
driverId?: string;
|
|
295
308
|
}
|
|
296
309
|
|
|
297
310
|
/** Prompt / agent budget. */
|
|
@@ -322,11 +335,29 @@ export interface AiAgent {
|
|
|
322
335
|
budget?: AiBudget;
|
|
323
336
|
}
|
|
324
337
|
|
|
338
|
+
/**
|
|
339
|
+
* External MCP server the app consumes — tools join the existing `fx.call`
|
|
340
|
+
* / `toolLoop` path as `mcp:<server>/<tool>` capability refs.
|
|
341
|
+
*/
|
|
342
|
+
export interface AiMcpServer {
|
|
343
|
+
/** Streamable HTTP endpoint. */
|
|
344
|
+
url?: string;
|
|
345
|
+
/** stdio executable (no shell string). */
|
|
346
|
+
command?: string;
|
|
347
|
+
/** Arguments for {@link command}. */
|
|
348
|
+
args?: string[];
|
|
349
|
+
/** Bearer secret contract name (never the value). */
|
|
350
|
+
auth?: SecretRef;
|
|
351
|
+
/** Required allowlist — never trust whatever `tools/list` exposes. */
|
|
352
|
+
tools: string[];
|
|
353
|
+
}
|
|
354
|
+
|
|
325
355
|
/** AI element catalogue. */
|
|
326
356
|
export interface Ai {
|
|
327
357
|
models?: Record<string, AiModel>;
|
|
328
358
|
prompts?: Record<string, AiPrompt>;
|
|
329
359
|
agents?: Record<string, AiAgent>;
|
|
360
|
+
mcpServers?: Record<string, AiMcpServer>;
|
|
330
361
|
}
|
|
331
362
|
|
|
332
363
|
/** Per-table metadata on a Manifest {@link Plugin}. */
|
|
@@ -75,6 +75,26 @@ describe("validateManifest", () => {
|
|
|
75
75
|
expect(result.ok).toBe(false);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
test("accepts signal resource refs on reads only", async () => {
|
|
79
|
+
const reads = await validateManifest({
|
|
80
|
+
oke: "1.0",
|
|
81
|
+
app: "x",
|
|
82
|
+
flows: {
|
|
83
|
+
f: { effects: { reads: ["signal:notify"] } },
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
expect(reads.ok).toBe(true);
|
|
87
|
+
|
|
88
|
+
const writes = await validateManifest({
|
|
89
|
+
oke: "1.0",
|
|
90
|
+
app: "x",
|
|
91
|
+
flows: {
|
|
92
|
+
f: { effects: { writes: ["signal:notify"] } },
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
expect(writes.ok).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
78
98
|
test("parseManifest throws ManifestValidationError on invalid JSON", async () => {
|
|
79
99
|
expect(parseManifest("{")).rejects.toBeInstanceOf(ManifestValidationError);
|
|
80
100
|
});
|
package/src/mcp/docs-server.ts
CHANGED
package/src/mcp/server.ts
CHANGED
|
@@ -122,4 +122,25 @@ describe("compression plugin", () => {
|
|
|
122
122
|
);
|
|
123
123
|
expect(nt.headers.get("content-encoding")).toBeNull();
|
|
124
124
|
});
|
|
125
|
+
|
|
126
|
+
test("skips text/event-stream even when Accept-Encoding is gzip", async () => {
|
|
127
|
+
on(
|
|
128
|
+
http.get("/sse"),
|
|
129
|
+
flow("sse.get", {
|
|
130
|
+
do: (_input, fx) =>
|
|
131
|
+
fx.json.stream(
|
|
132
|
+
(async function* () {
|
|
133
|
+
yield "hello";
|
|
134
|
+
})(),
|
|
135
|
+
),
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
const app = oke({ autoBoot: false, name: "zip-sse" }).plug(compression({ minSize: 0 }));
|
|
139
|
+
const res = await app.fetch(
|
|
140
|
+
new Request("http://localhost/sse", { headers: { "accept-encoding": "gzip" } }),
|
|
141
|
+
);
|
|
142
|
+
expect(res.headers.get("content-type")).toMatch(/text\/event-stream/);
|
|
143
|
+
expect(res.headers.get("content-encoding")).toBeNull();
|
|
144
|
+
expect(await res.text()).toContain('data: "hello"');
|
|
145
|
+
});
|
|
125
146
|
});
|
|
@@ -73,6 +73,7 @@ export function compression(
|
|
|
73
73
|
if (cacheControl !== null && /\bno-transform\b/i.test(cacheControl)) return;
|
|
74
74
|
|
|
75
75
|
const contentType = headers.get("content-type") ?? "";
|
|
76
|
+
if (/^text\/event-stream\b/i.test(contentType.split(";")[0]?.trim() ?? "")) return;
|
|
76
77
|
if (!match.test(contentType)) return;
|
|
77
78
|
|
|
78
79
|
const body = await ctx.response.arrayBuffer();
|
package/src/runtime/bun.ts
CHANGED
|
@@ -81,7 +81,29 @@ export function isBunNativeMethod(method: string): boolean {
|
|
|
81
81
|
);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
/** Bun.serve `fetch` / route handler — second arg is the live server. */
|
|
85
|
+
type ServeFetch = (
|
|
86
|
+
req: Request,
|
|
87
|
+
server: { timeout(request: Request, seconds: number): void },
|
|
88
|
+
) => Response | Promise<Response>;
|
|
89
|
+
|
|
90
|
+
type MethodHandlers = Partial<Record<string, ServeFetch>>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Disable the 10s idle timeout for SSE so a quiet token stream is not reset.
|
|
94
|
+
*
|
|
95
|
+
* @param inner - App fetch
|
|
96
|
+
*/
|
|
97
|
+
function holdSseIdle(inner: (request: Request) => Promise<Response>): ServeFetch {
|
|
98
|
+
return async (req, server) => {
|
|
99
|
+
const res = await inner(req);
|
|
100
|
+
const ct = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
|
|
101
|
+
if (/^text\/event-stream$/i.test(ct)) {
|
|
102
|
+
server.timeout(req, 0);
|
|
103
|
+
}
|
|
104
|
+
return res;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
85
107
|
|
|
86
108
|
/**
|
|
87
109
|
* Build Bun.serve `routes` from app HTTP bindings.
|
|
@@ -103,7 +125,7 @@ export function buildBunRoutes(
|
|
|
103
125
|
if (typeof path !== "string" || typeof method !== "string") continue;
|
|
104
126
|
if (!isBunNativePath(path) || !isBunNativeMethod(method)) continue;
|
|
105
127
|
const methods = routes[path] ?? (routes[path] = {});
|
|
106
|
-
methods[method] = (
|
|
128
|
+
methods[method] = holdSseIdle(fetchHandler);
|
|
107
129
|
}
|
|
108
130
|
return routes;
|
|
109
131
|
}
|
|
@@ -128,10 +150,15 @@ export function serveBunHttp(options: {
|
|
|
128
150
|
readonly routes?: Record<string, MethodHandlers>;
|
|
129
151
|
readonly id?: string;
|
|
130
152
|
}): ReturnType<typeof Bun.serve> {
|
|
153
|
+
const fetch = holdSseIdle(
|
|
154
|
+
typeof options.fetch === "function"
|
|
155
|
+
? (req: Request) => Promise.resolve(options.fetch(req))
|
|
156
|
+
: options.fetch,
|
|
157
|
+
);
|
|
131
158
|
const base = {
|
|
132
159
|
port: options.port,
|
|
133
160
|
hostname: options.hostname,
|
|
134
|
-
fetch
|
|
161
|
+
fetch,
|
|
135
162
|
...(options.id !== undefined ? { id: options.id } : {}),
|
|
136
163
|
};
|
|
137
164
|
const table = options.routes;
|
|
@@ -160,6 +187,15 @@ function listenBun(app: FetchApp, options?: ServeOptions): ServerHandle {
|
|
|
160
187
|
...(options?.id !== undefined ? { id: options.id } : {}),
|
|
161
188
|
});
|
|
162
189
|
|
|
190
|
+
const closeIdle = (
|
|
191
|
+
server as typeof server & { closeIdleConnections(): void }
|
|
192
|
+
).closeIdleConnections.bind(server);
|
|
193
|
+
const onPressure = (): void => {
|
|
194
|
+
closeIdle();
|
|
195
|
+
};
|
|
196
|
+
const proc = process as NodeJS.EventEmitter;
|
|
197
|
+
proc.on("memoryPressure", onPressure);
|
|
198
|
+
|
|
163
199
|
const boundPort = server.port ?? port;
|
|
164
200
|
const boundHost = server.hostname ?? hostname;
|
|
165
201
|
const url = new URL(`http://${formatHostForUrl(boundHost)}:${boundPort}/`);
|
|
@@ -170,7 +206,8 @@ function listenBun(app: FetchApp, options?: ServeOptions): ServerHandle {
|
|
|
170
206
|
hostname: boundHost,
|
|
171
207
|
fetch: fetchHandler,
|
|
172
208
|
stop(closeActiveConnections = false) {
|
|
173
|
-
|
|
209
|
+
proc.off("memoryPressure", onPressure);
|
|
210
|
+
return server.stop(closeActiveConnections);
|
|
174
211
|
},
|
|
175
212
|
};
|
|
176
213
|
}
|
|
@@ -19,6 +19,7 @@ import { afterEach } from "bun:test";
|
|
|
19
19
|
import {
|
|
20
20
|
aiAgentRegistry,
|
|
21
21
|
aiEmbedRegistry,
|
|
22
|
+
aiMcpServerRegistry,
|
|
22
23
|
aiModelRegistry,
|
|
23
24
|
aiPromptRegistry,
|
|
24
25
|
channelTemplateRegistry,
|
|
@@ -38,4 +39,5 @@ afterEach(() => {
|
|
|
38
39
|
aiPromptRegistry.length = 0;
|
|
39
40
|
aiEmbedRegistry.length = 0;
|
|
40
41
|
aiAgentRegistry.length = 0;
|
|
42
|
+
aiMcpServerRegistry.length = 0;
|
|
41
43
|
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{Cr as e,F as t,L as n,P as r,wr as i}from"./shortcut-keys-DO4IsVqv.js";import{A as a,f as o}from"./xyflow-D7n4g6go.js";import{t as s}from"./dagre.esm-ZwcdTuZZ.js";import{C as c,S as l,_ as u,o as d,v as f}from"./http-method-BJ92Z_ke.js";function p(e){let t=e.indexOf(`.`);return t===-1?e:e.slice(0,t)}function m(e){let t=e.indexOf(`.`);return t===-1?e:e.slice(t+1)}function h(e){let t=e?.flows??{},n=new Map;for(let e of Object.keys(t)){let r=t[e]?.effects?.calls??[];for(let t of r){let r=n.get(t)??[];r.push(e),n.set(t,r)}}for(let[e,t]of n)t.sort((e,t)=>e.localeCompare(t)),n.set(e,t);return n}function g(e,t){return h(e).get(t)??[]}function _(e){return e.startsWith(`sql:`)?{id:e,kind:`store`,label:e.slice(4),facet:`sql`}:e.startsWith(`kv:`)?{id:e,kind:`store`,label:e.slice(3),facet:`kv`}:e.startsWith(`files:`)?{id:e,kind:`store`,label:e.slice(6),facet:`files`}:e.startsWith(`index:`)?{id:e,kind:`store`,label:e.slice(6),facet:`index`}:e.startsWith(`signal:`)?{id:e,kind:`signal`,label:e.slice(7)}:e.startsWith(`ai:`)?{id:e,kind:`ai`,label:e.slice(3)}:e.startsWith(`vault:`)?{id:e,kind:`vault`,label:e.slice(6)}:e.startsWith(`channel:`)?{id:e,kind:`channel`,label:e.slice(8)}:e.startsWith(`gate:`)?{id:e,kind:`gate`,label:e.slice(5)}:e.startsWith(`clock:`)?{id:e,kind:`clock`,label:e.slice(6)}:null}function v(e,t,n,r,i=!1){let a=u[r];return{id:e,source:t,target:n,type:`smoothstep`,animated:i,data:{kind:r},style:{stroke:a,strokeWidth:r===`calls`?2.5:2.25},pathOptions:{borderRadius:28},zIndex:f.edge,markerEnd:{type:o.ArrowClosed,width:14,height:14,color:a}}}function y(e){let t=e?.flows??{},n=new Set(Object.keys(t)),r=[],i=new Map;for(let e of[...n].sort()){let t=p(e),n=i.get(t)??[];n.push(e),i.set(t,n)}let a=new Map,o=new Map;for(let e of n){let n=t[e];if(!n)continue;if(n.trigger?.signal){let t=`signal:${n.trigger.signal}`,i=_(t);i&&a.set(t,i),r.push(v(`e:${t}->flow:${e}`,t,`flow:${e}`,`trigger`))}if(n.trigger?.cron||n.trigger?.every){let t=`clock:${e}`,i=n.trigger.cron??n.trigger.every??e;a.set(t,{id:t,kind:`clock`,label:i,facet:n.trigger.cron?`cron`:`every`}),r.push(v(`e:${t}->flow:${e}`,t,`flow:${e}`,`trigger`))}for(let t of n.gates??[]){let n=`gate:${t}`;a.set(n,{id:n,kind:`gate`,label:t}),o.set(n,(o.get(n)??0)+1),r.push(v(`e:flow:${e}-gates->${n}`,`flow:${e}`,n,`gates`))}let i=n.effects;if(i){for(let t of i.reads??[]){let n=_(t);n&&(a.set(t,n),o.set(t,(o.get(t)??0)+1),r.push(v(`e:flow:${e}-reads->${t}`,`flow:${e}`,t,`reads`)))}for(let t of i.writes??[]){let n=_(t);n&&(a.set(t,n),o.set(t,(o.get(t)??0)+1),r.push(v(`e:flow:${e}-writes->${t}`,`flow:${e}`,t,`writes`)))}for(let t of i.emits??[]){let n=`signal:${t}`,i=_(n);i&&(a.set(n,i),o.set(n,(o.get(n)??0)+1),r.push(v(`e:flow:${e}->${n}`,`flow:${e}`,n,`emits`)))}for(let t of i.asks??[]){let n=`ai:${t}`,i=_(n);i&&(a.set(n,i),o.set(n,(o.get(n)??0)+1),r.push(v(`e:flow:${e}->${n}`,`flow:${e}`,n,`asks`)))}for(let t of i.sends??[]){let n=`channel:${t}`,i=_(n);i&&(a.set(n,i),o.set(n,(o.get(n)??0)+1),r.push(v(`e:flow:${e}->${n}`,`flow:${e}`,n,`sends`)))}for(let t of i.secrets??[]){let n=`vault:${t}`,i=_(n);i&&(a.set(n,i),o.set(n,(o.get(n)??0)+1),r.push(v(`e:flow:${e}->${n}`,`flow:${e}`,n,`secrets`)))}for(let t of i.calls??[])r.push(v(`e:flow:${e}->flow:${t}`,`flow:${e}`,`flow:${t}`,`calls`,!0))}}return{nodes:b(i,t,a,o),edges:r,flowIds:n}}function b(e,t,n,r){let i=new s.graphlib.Graph({compound:!0});i.setGraph({rankdir:`LR`,align:`UL`,nodesep:16,ranksep:88,edgesep:12,marginx:8,marginy:8}),i.setDefaultEdgeLabel(()=>({}));for(let[t,n]of[...e.entries()].sort()){i.setNode(`unit:${t}`,{});for(let e of n)i.setNode(`flow:${e}`,{...l.flow}),i.setParent(`flow:${e}`,`unit:${t}`)}for(let e of n.values())i.setNode(e.id,{...l[e.kind]});let a=new Set;for(let n of e.values())for(let e of n){let n=t[e];if(!n)continue;let r=(e,t)=>{let n=`${e}->${t}`;a.has(n)||!i.hasNode(e)||!i.hasNode(t)||(a.add(n),i.setEdge(e,t))};n.trigger?.signal&&r(`signal:${n.trigger.signal}`,`flow:${e}`),(n.trigger?.cron||n.trigger?.every)&&r(`clock:${e}`,`flow:${e}`);for(let t of n.gates??[])r(`flow:${e}`,`gate:${t}`);let o=n.effects;if(o){for(let t of o.reads??[])r(`flow:${e}`,t);for(let t of o.writes??[])r(`flow:${e}`,t);for(let t of o.emits??[])r(`flow:${e}`,`signal:${t}`);for(let t of o.asks??[])r(`flow:${e}`,`ai:${t}`);for(let t of o.sends??[])r(`flow:${e}`,`channel:${t}`);for(let t of o.secrets??[])r(`flow:${e}`,`vault:${t}`);for(let t of o.calls??[])r(`flow:${e}`,`flow:${t}`)}}s.layout(i);let o=[],{headerH:u,padX:d,padBottom:p}=c;for(let[n,r]of[...e.entries()].sort()){let e=`unit:${n}`,a=i.node(e);if(!a)continue;let s=a.x??0,c=a.y??0,h=r.length*l.flow.height+Math.max(0,r.length-1)*10,g=l.flow.width+d*2,_=u+h+p,v=s-g/2,y=c-_/2;o.push({id:e,type:`unit`,position:{x:v,y},data:{kind:`unit`,label:n,refId:e,badge:String(r.length)},selectable:!1,draggable:!1,zIndex:f.unit,width:g,height:_,style:{width:g,height:_}}),r.forEach((r,i)=>{let a=t[r];a&&o.push({id:`flow:${r}`,type:`flow`,position:{x:d,y:u+i*(l.flow.height+10)},parentId:e,extent:`parent`,zIndex:f.leaf,data:{kind:`flow`,label:m(r),refId:r,unit:n,plane:a.plane??`user`,badge:a.plane??`user`},draggable:!1,width:l.flow.width,height:l.flow.height,style:{width:l.flow.width,height:l.flow.height}})})}let h=[...n.values()].sort((e,t)=>e.kind.localeCompare(t.kind)||e.label.localeCompare(t.label));for(let e of h){let t=i.node(e.id),n=l[e.kind],a=r.get(e.id)??0;o.push({id:e.id,type:e.kind,position:{x:(t?.x??0)-n.width/2,y:(t?.y??0)-n.height/2},data:{kind:e.kind,label:e.label,refId:e.id,...e.facet===void 0?{}:{facet:e.facet},badge:e.facet??(a>0?String(a):e.kind)},draggable:!1,zIndex:f.leaf,width:n.width,height:n.height,style:{width:n.width,height:n.height}})}return o}function x(e,t,n,r={}){let i=t.size>0||n.size>0;return e.map(e=>{if(e.data.kind===`unit`||e.data.kind===`law`)return e;let a=e.data.kind===`flow`?t.has(e.data.refId):n.has(e.id);return{...e,data:{...e.data,highlighted:a,dimmed:i&&!a,active:r.activeNodeId!=null&&e.id===r.activeNodeId,focused:r.focusedNodeId!=null&&e.id===r.focusedNodeId}}})}function S(e,t){let n=new Map(t.map(e=>[e.id,e]));return t.some(e=>e.data.highlighted===!0)?e.map(e=>{let t=n.get(e.source),r=n.get(e.target),i=t?.data.highlighted===!0&&r?.data.highlighted===!0,a=u[e.data?.kind??`reads`];return{...e,animated:i?!0:e.animated,style:{...e.style,stroke:a,opacity:i?1:.32,strokeWidth:i?2.75:1.75},markerEnd:e.markerEnd&&typeof e.markerEnd==`object`?{...e.markerEnd,color:a}:e.markerEnd}}):e.map(e=>({...e,style:{...e.style,opacity:1}}))}var C=100;function w(e,t){return e.filter(e=>t.has(e.flow)).sort((e,t)=>t.startedAt-e.startedAt).slice(0,C)}var T=a();function E({cache:a,dataSlot:o}){let s=d(a);return(0,T.jsxs)(r,{children:[(0,T.jsx)(n,{render:t=>(0,T.jsx)(`span`,{...t,className:e(`flex w-4 shrink-0 items-center justify-center`,s.className),"data-slot":o,"data-cache":a,"aria-label":s.label,children:(0,T.jsx)(i,{icon:s.icon,className:`size-3`,"aria-hidden":!0})})}),(0,T.jsx)(t,{side:`top`,children:s.label})]})}export{S as a,p as c,x as i,w as n,y as o,m as r,g as s,E as t};
|