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
|
@@ -33,11 +33,11 @@ OKE reads environment variables at boot for connection detail and secrets — ne
|
|
|
33
33
|
|
|
34
34
|
## KV store
|
|
35
35
|
|
|
36
|
-
| Variable | Used for
|
|
37
|
-
| ------------------ |
|
|
38
|
-
| `REDIS_URL` | Redis connection | driver default |
|
|
39
|
-
| `OKE_STORE_KV_URL` | Explicit KV URL override | — |
|
|
40
|
-
| `OKE_KV_DRIVER` | Force the kv driver id
|
|
36
|
+
| Variable | Used for | Default when unset |
|
|
37
|
+
| ------------------ | ------------------------------ | ------------------ |
|
|
38
|
+
| `REDIS_URL` | Cache Redis connection | driver default |
|
|
39
|
+
| `OKE_STORE_KV_URL` | Explicit cache KV URL override | — |
|
|
40
|
+
| `OKE_KV_DRIVER` | Force the kv driver id | config map |
|
|
41
41
|
|
|
42
42
|
## Files store
|
|
43
43
|
|
|
@@ -33,9 +33,10 @@ See [Store](/docs/elements/store) for the query-builder surface.
|
|
|
33
33
|
|
|
34
34
|
## Signals
|
|
35
35
|
|
|
36
|
-
| Signature | Records
|
|
37
|
-
| ------------------------------------- |
|
|
38
|
-
| `fx.emit(signal, payload?, { key? })` | `emit`
|
|
36
|
+
| Signature | Records | Notes |
|
|
37
|
+
| ------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
38
|
+
| `fx.emit(signal, payload?, { key? })` | `emit` | Commits the signal outbox when the call resolves; optional `key` serializes `once` per key; stamps producer run id as `parentRunId` for trace chains; throws **OKE1042** (orphan) or **OKE1043** (schema) |
|
|
39
|
+
| `fx.deadLetters(signal)` | `read` `signal:<name>` | Dead-lettered messages for that signal. Payload typed from `SignalDecl<T>`. Page with `fx.json.withQuery`. Cross-signal throws **OKE1001**. |
|
|
39
40
|
|
|
40
41
|
## Runs (observability read)
|
|
41
42
|
|
|
@@ -145,12 +146,12 @@ contact a provider. Channel bodies use `{{field}}` catalogs — not ICU (see [Ch
|
|
|
145
146
|
|
|
146
147
|
## AI
|
|
147
148
|
|
|
148
|
-
| Signature | Records | Returns
|
|
149
|
-
| ----------------------------------------------------- | ------------------------- |
|
|
150
|
-
| `fx.ask(prompt, input?, { via?, tools?, maxSteps? })` | `ask` (+ `call` per tool) | Object validated against the prompt's `out`
|
|
151
|
-
| `fx.run(agent, input?)` | `ask` | Agent result
|
|
152
|
-
| `fx.stream(model, { prompt?, data? })`
|
|
153
|
-
| `fx.search(embed, query, { topK? })` | `read` | Matches from the index/embed
|
|
149
|
+
| Signature | Records | Returns |
|
|
150
|
+
| ----------------------------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
151
|
+
| `fx.ask(prompt, input?, { via?, tools?, maxSteps? })` | `ask` (+ `call` per tool) | Object validated against the prompt's `out` |
|
|
152
|
+
| `fx.run(agent, input?)` | `ask` | Agent result |
|
|
153
|
+
| `fx.stream(model, { prompt?, data?, via? })` | `ask` | `AsyncIterable<string>` — real driver stream; cancels via ambient `fx.signal` (HTTP disconnect included) |
|
|
154
|
+
| `fx.search(embed, query, { topK? })` | `read` | Matches from the index/embed |
|
|
154
155
|
|
|
155
156
|
AI calls are nondeterministic: journaling is forced on and auto-cache disabled around them. `tools` are Flow refs — each model tool call goes through `fx.call` (same capability and Runs path).
|
|
156
157
|
|
|
@@ -169,12 +170,15 @@ AI calls are nondeterministic: journaling is forced on and auto-cache disabled a
|
|
|
169
170
|
|
|
170
171
|
## Clock
|
|
171
172
|
|
|
172
|
-
| Signature | Notes
|
|
173
|
-
| --------------------------------- |
|
|
174
|
-
| `fx.clock.now()` | Epoch-ms, injectable — the only legal "now"
|
|
175
|
-
| `fx.clock.
|
|
173
|
+
| Signature | Notes |
|
|
174
|
+
| --------------------------------- | ------------------------------------------------------------------- |
|
|
175
|
+
| `fx.clock.now()` | Epoch-ms, injectable — the only legal "now" |
|
|
176
|
+
| `fx.clock.ago(duration)` | Instant before now (`"30d"` → now − 30 days) |
|
|
177
|
+
| `fx.clock.fromNow(duration)` | Instant after now (`"14d"` → now + 14 days) |
|
|
178
|
+
| `fx.clock.duration(duration)` | Span in ms — offset a stored instant (`createdAt + duration("7d")`) |
|
|
179
|
+
| `fx.clock.sleep(label, duration)` | Durable sleep in `durable` flows; immediate otherwise |
|
|
176
180
|
|
|
177
|
-
Durations: `"200ms"` · `"30s"` · `"2m"` · `"1h"` · `"7d"`.
|
|
181
|
+
Durations: `"200ms"` · `"30s"` · `"2m"` · `"1h"` · `"7d"`. A `"d"` is 86_400_000 ms, not a calendar day. Unknown strings parse as `0`.
|
|
178
182
|
|
|
179
183
|
## Cache
|
|
180
184
|
|
|
@@ -192,15 +196,16 @@ keys. Use `cache: false` to opt out, or `cache: "30s"` for a TTL.
|
|
|
192
196
|
|
|
193
197
|
## Responses
|
|
194
198
|
|
|
195
|
-
| Helper | Status | Body
|
|
196
|
-
| ----------------------------------------- | ------ |
|
|
197
|
-
| `fx.json.ok(value, { meta? })` | 200 | `{ data, meta?, error: null }`
|
|
198
|
-
| `fx.json.create(value)` | 201 | `{ data, error: null }`
|
|
199
|
-
| `fx.json.empty()` | 204 | no body
|
|
200
|
-
| `fx.json.with(page)` / `with(data, meta)` | 200 | `{ data, meta, error: null }` — already-built pager
|
|
201
|
-
| `fx.json.withQuery(rows, input, spec?)` | 200 | In-memory list page — zero-config `q` / auto-eq / PostgREST
|
|
199
|
+
| Helper | Status | Body |
|
|
200
|
+
| ----------------------------------------- | ------ | -------------------------------------------------------------- |
|
|
201
|
+
| `fx.json.ok(value, { meta? })` | 200 | `{ data, meta?, error: null }` |
|
|
202
|
+
| `fx.json.create(value)` | 201 | `{ data, error: null }` |
|
|
203
|
+
| `fx.json.empty()` | 204 | no body |
|
|
204
|
+
| `fx.json.with(page)` / `with(data, meta)` | 200 | `{ data, meta, error: null }` — already-built pager |
|
|
205
|
+
| `fx.json.withQuery(rows, input, spec?)` | 200 | In-memory list page — zero-config `q` / auto-eq / PostgREST |
|
|
206
|
+
| `fx.json.stream(chunks)` | 200 | `text/event-stream` — JSON `data:` frames, then `data: [DONE]` |
|
|
202
207
|
|
|
203
|
-
Returning a plain value instead answers 200 with `{ data: value, error: null }` — the helpers exist for status and `meta` control.
|
|
208
|
+
Returning a plain value instead answers 200 with `{ data: value, error: null }` — the helpers exist for status and `meta` control. Pass `fx.stream(...)` into `fx.json.stream` to reach the HTTP client token-by-token.
|
|
204
209
|
|
|
205
210
|
## Logging, i18n, ids
|
|
206
211
|
|
|
@@ -197,4 +197,29 @@ describe("formatModelRow", () => {
|
|
|
197
197
|
expect(row).toContain("text · code");
|
|
198
198
|
expect(row).not.toContain(" · ≈");
|
|
199
199
|
});
|
|
200
|
+
|
|
201
|
+
test("Arabic and CJK labels keep Caps aligned", () => {
|
|
202
|
+
const header = formatModelTableHeader();
|
|
203
|
+
const capsWidth = Bun.stringWidth(header.slice(0, header.indexOf("Caps")));
|
|
204
|
+
const ar = formatModelRow({
|
|
205
|
+
id: "ar",
|
|
206
|
+
label: "مرحبا بالعالم",
|
|
207
|
+
hint: "",
|
|
208
|
+
role: "chat",
|
|
209
|
+
ramGb: 8,
|
|
210
|
+
tier: "fast",
|
|
211
|
+
modalities: ["text"],
|
|
212
|
+
});
|
|
213
|
+
const cjk = formatModelRow({
|
|
214
|
+
id: "cjk",
|
|
215
|
+
label: "你好世界模型",
|
|
216
|
+
hint: "",
|
|
217
|
+
role: "chat",
|
|
218
|
+
ramGb: 8,
|
|
219
|
+
tier: "fast",
|
|
220
|
+
modalities: ["text"],
|
|
221
|
+
});
|
|
222
|
+
expect(Bun.stringWidth(ar.slice(0, ar.indexOf("text")))).toBe(capsWidth);
|
|
223
|
+
expect(Bun.stringWidth(cjk.slice(0, cjk.indexOf("text")))).toBe(capsWidth);
|
|
224
|
+
});
|
|
200
225
|
});
|
|
@@ -195,9 +195,14 @@ const MODEL_COL_RAM = 7;
|
|
|
195
195
|
* @param width - Fixed width
|
|
196
196
|
*/
|
|
197
197
|
function clipPad(value: string, width: number): string {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
const bunAnsi = Bun as typeof Bun & {
|
|
199
|
+
stringWidth(text: string): number;
|
|
200
|
+
sliceAnsi(text: string, start: number, end: number, omission?: string): string;
|
|
201
|
+
};
|
|
202
|
+
const display = bunAnsi.stringWidth(value);
|
|
203
|
+
if (display === width) return value;
|
|
204
|
+
if (display < width) return value + " ".repeat(width - display);
|
|
205
|
+
return bunAnsi.sliceAnsi(value, 0, width, "…");
|
|
201
206
|
}
|
|
202
207
|
|
|
203
208
|
/**
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
ResourceRef,
|
|
13
13
|
SecretRef,
|
|
14
14
|
SignalRef,
|
|
15
|
+
SignalResourceRef,
|
|
15
16
|
TemplateRef,
|
|
16
17
|
FlowRef,
|
|
17
18
|
} from "../manifest/types.ts";
|
|
@@ -56,6 +57,8 @@ export interface InferBinding {
|
|
|
56
57
|
| "flow"
|
|
57
58
|
| "embed"
|
|
58
59
|
| "table"
|
|
60
|
+
| "mcp-server"
|
|
61
|
+
| "mcp-tool"
|
|
59
62
|
| "unknown";
|
|
60
63
|
/** Resolved resource / name. */
|
|
61
64
|
readonly ref: string;
|
|
@@ -142,7 +145,7 @@ const TABLE_ARG_METHODS = new Set([
|
|
|
142
145
|
* @param options - Handler AST, bindings, and annotation flag
|
|
143
146
|
*/
|
|
144
147
|
export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
145
|
-
const reads = new Set<ResourceRef>();
|
|
148
|
+
const reads = new Set<ResourceRef | SignalResourceRef>();
|
|
146
149
|
const writes = new Set<ResourceRef>();
|
|
147
150
|
const emits = new Set<SignalRef>();
|
|
148
151
|
const sends = new Set<TemplateRef>();
|
|
@@ -173,6 +176,12 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
|
173
176
|
continue;
|
|
174
177
|
}
|
|
175
178
|
|
|
179
|
+
if (chain.rootMethod === "deadLetters" && call === chain.rootCall) {
|
|
180
|
+
const ref = resolveNamed(call.arguments[0], options.bindings, "signal");
|
|
181
|
+
if (ref) reads.add(`signal:${ref}`);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
176
185
|
if (chain.rootMethod === "send" && call === chain.rootCall) {
|
|
177
186
|
const ref = resolveNamed(call.arguments[0], options.bindings, "template");
|
|
178
187
|
if (ref) sends.add(ref);
|
|
@@ -203,7 +212,7 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
|
203
212
|
}
|
|
204
213
|
|
|
205
214
|
if (chain.rootMethod === "call" && call === chain.rootCall) {
|
|
206
|
-
const ref =
|
|
215
|
+
const ref = resolveCallTarget(call.arguments[0], options.bindings);
|
|
207
216
|
if (ref) calls.add(ref);
|
|
208
217
|
continue;
|
|
209
218
|
}
|
|
@@ -541,12 +550,58 @@ function toolsFromAskOptions(
|
|
|
541
550
|
);
|
|
542
551
|
const out: FlowRef[] = [];
|
|
543
552
|
for (const el of els) {
|
|
544
|
-
const ref =
|
|
553
|
+
const ref = resolveCallTarget(el, bindings);
|
|
545
554
|
if (ref) out.push(ref as FlowRef);
|
|
546
555
|
}
|
|
547
556
|
return out;
|
|
548
557
|
}
|
|
549
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Resolve `fx.call` / ask-tools: flow name, `mcp:` ref, or `server.tool("x")`.
|
|
561
|
+
*
|
|
562
|
+
* @param node - Argument AST
|
|
563
|
+
* @param bindings - Known bindings
|
|
564
|
+
*/
|
|
565
|
+
export function resolveCallTarget(
|
|
566
|
+
node: AstNode | undefined,
|
|
567
|
+
bindings: ReadonlyMap<string, InferBinding>,
|
|
568
|
+
): string | undefined {
|
|
569
|
+
const mcp = resolveMcpToolExpr(node, bindings);
|
|
570
|
+
if (mcp) return mcp;
|
|
571
|
+
return resolveNamed(node, bindings, "flow");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Resolve `server.tool("name")` or a bound `mcp-tool` identifier.
|
|
576
|
+
*
|
|
577
|
+
* @param node - AST
|
|
578
|
+
* @param bindings - Known bindings
|
|
579
|
+
*/
|
|
580
|
+
export function resolveMcpToolExpr(
|
|
581
|
+
node: AstNode | undefined,
|
|
582
|
+
bindings: ReadonlyMap<string, InferBinding>,
|
|
583
|
+
): string | undefined {
|
|
584
|
+
if (!node) return undefined;
|
|
585
|
+
if (node.type === "CallExpression") {
|
|
586
|
+
const callee = (node as CallExpression).callee;
|
|
587
|
+
if (callee.type === "MemberExpression") {
|
|
588
|
+
const member = callee as AstNode & { object: AstNode; property: AstNode };
|
|
589
|
+
const obj = identifierName(member.object);
|
|
590
|
+
const prop = identifierName(member.property);
|
|
591
|
+
const tool = stringArg((node as CallExpression).arguments[0]);
|
|
592
|
+
if (obj && prop === "tool" && tool) {
|
|
593
|
+
const server = bindings.get(obj);
|
|
594
|
+
if (server?.kind === "mcp-server") {
|
|
595
|
+
return `mcp:${server.ref}/${tool}`;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const binding = resolveBinding(node, bindings);
|
|
601
|
+
if (binding?.kind === "mcp-tool") return binding.ref;
|
|
602
|
+
return undefined;
|
|
603
|
+
}
|
|
604
|
+
|
|
550
605
|
/**
|
|
551
606
|
* String literal argument.
|
|
552
607
|
*
|
|
@@ -135,12 +135,15 @@ export const flow_${i} = on(
|
|
|
135
135
|
}
|
|
136
136
|
sources["src/flows/all.ts"] = parts.join("\n");
|
|
137
137
|
|
|
138
|
+
await extractFromSources(sources);
|
|
138
139
|
const start = performance.now();
|
|
139
140
|
const manifest = await extractFromSources(sources);
|
|
140
141
|
const elapsed = performance.now() - start;
|
|
141
142
|
|
|
142
143
|
expect(Object.keys(manifest.flows ?? {}).length).toBe(200);
|
|
143
|
-
|
|
144
|
+
// Local ~400ms after warmup. GHA shared runners measured ~2.9s cold.
|
|
145
|
+
const budgetMs = process.env.CI ? 4_000 : 2_000;
|
|
146
|
+
expect(elapsed).toBeLessThan(budgetMs);
|
|
144
147
|
});
|
|
145
148
|
});
|
|
146
149
|
|
|
@@ -293,6 +296,7 @@ export const stripeKey = vault.secret("STRIPE_KEY", {
|
|
|
293
296
|
expect(manifest.stores?.embeddings?.description).toBe("Document embeddings");
|
|
294
297
|
expect(manifest.stores?.sessions?.description).toBe("Session cache");
|
|
295
298
|
expect(manifest.stores?.sessions?.namespaces).toEqual(["sessions"]);
|
|
299
|
+
expect(manifest.stores?.sessions?.durable).toBeUndefined();
|
|
296
300
|
expect(manifest.signals?.["order-placed"]?.description).toBe("Order placed event");
|
|
297
301
|
expect(manifest.channels?.["booking-confirmed"]?.description).toBe(
|
|
298
302
|
"Booking confirmation email",
|
|
@@ -301,6 +305,19 @@ export const stripeKey = vault.secret("STRIPE_KEY", {
|
|
|
301
305
|
expect(manifest.gates?.member?.description).toBe("Verified members only");
|
|
302
306
|
expect(manifest.vault?.STRIPE_KEY?.description).toBe("Payments gateway key");
|
|
303
307
|
});
|
|
308
|
+
|
|
309
|
+
test("extracts store.kv durable onto the Manifest store", async () => {
|
|
310
|
+
const manifest = await extractFromSources({
|
|
311
|
+
"src/kv.ts": `
|
|
312
|
+
import { store } from "okengine";
|
|
313
|
+
export const sessions = store.kv("sessions", { description: "Session cache" });
|
|
314
|
+
export const ledger = store.kv("ledger", { durable: true, description: "Idempotency keys" });
|
|
315
|
+
`,
|
|
316
|
+
});
|
|
317
|
+
expect(manifest.stores?.sessions?.durable).toBeUndefined();
|
|
318
|
+
expect(manifest.stores?.ledger?.durable).toBe(true);
|
|
319
|
+
expect(manifest.stores?.ledger?.namespaces).toEqual(["ledger"]);
|
|
320
|
+
});
|
|
304
321
|
});
|
|
305
322
|
|
|
306
323
|
describe("extractManifest — bindNamedTableCrud(...)", () => {
|
|
@@ -584,6 +601,25 @@ export const expire = on(
|
|
|
584
601
|
});
|
|
585
602
|
});
|
|
586
603
|
|
|
604
|
+
describe("extractManifest — fx.deadLetters", () => {
|
|
605
|
+
test("fx.deadLetters(signal) infers reads: [signal:<name>]", async () => {
|
|
606
|
+
const source = `
|
|
607
|
+
import { on, flow, http, signal } from "okengine";
|
|
608
|
+
|
|
609
|
+
export const notify = signal("notify", { delivery: "once" });
|
|
610
|
+
|
|
611
|
+
export const failed = on(
|
|
612
|
+
http.get("/notifications/failed").gate.public,
|
|
613
|
+
flow("notifications.failed", {
|
|
614
|
+
do: async (input, fx) => fx.json.withQuery(await fx.deadLetters(notify), input),
|
|
615
|
+
}),
|
|
616
|
+
);
|
|
617
|
+
`;
|
|
618
|
+
const manifest = await extractFromSources({ "src/flows/failed.ts": source });
|
|
619
|
+
expect(manifest.flows?.["notifications.failed"]?.effects?.reads).toEqual(["signal:notify"]);
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
|
|
587
623
|
describe("extractManifest — channel medium binder aliasing", () => {
|
|
588
624
|
test("mail.template(...) resolves through `const mail = channel.email(...)`", async () => {
|
|
589
625
|
const source = `
|
|
@@ -762,3 +798,34 @@ export const create = on(
|
|
|
762
798
|
expect(manifest.gates?.WRITE).toBeUndefined();
|
|
763
799
|
});
|
|
764
800
|
});
|
|
801
|
+
|
|
802
|
+
describe("extractManifest — ai.mcpServer", () => {
|
|
803
|
+
test("stamps mcpServers and infers mcp: calls from .tool()", async () => {
|
|
804
|
+
const source = `
|
|
805
|
+
import { ai, flow, on, http, vault } from "okengine";
|
|
806
|
+
|
|
807
|
+
export const token = vault.secret("GITHUB_TOKEN");
|
|
808
|
+
export const github = ai.mcpServer("github", {
|
|
809
|
+
url: "https://mcp.example/github",
|
|
810
|
+
auth: { bearer: token },
|
|
811
|
+
tools: ["create_issue"],
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
export const triage = on(
|
|
815
|
+
http.post("/triage"),
|
|
816
|
+
flow("support.triage", {
|
|
817
|
+
do: async (input, fx) => {
|
|
818
|
+
await fx.ask("ticket-triage", input, { tools: [github.tool("create_issue")] });
|
|
819
|
+
},
|
|
820
|
+
}),
|
|
821
|
+
);
|
|
822
|
+
`;
|
|
823
|
+
const manifest = await extractFromSources({ "src/ai.ts": source });
|
|
824
|
+
expect(manifest.ai?.mcpServers?.github).toEqual({
|
|
825
|
+
url: "https://mcp.example/github",
|
|
826
|
+
auth: "GITHUB_TOKEN",
|
|
827
|
+
tools: ["create_issue"],
|
|
828
|
+
});
|
|
829
|
+
expect(manifest.flows?.["support.triage"]?.effects?.calls).toEqual(["mcp:github/create_issue"]);
|
|
830
|
+
});
|
|
831
|
+
});
|
package/src/compiler/extract.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { parseSync } from "oxc-parser";
|
|
|
11
11
|
import type {
|
|
12
12
|
Ai,
|
|
13
13
|
AiAgent,
|
|
14
|
+
AiMcpServer,
|
|
14
15
|
AiModel,
|
|
15
16
|
AiPrompt,
|
|
16
17
|
Channel,
|
|
@@ -35,6 +36,7 @@ import { sqlTableRef } from "../manifest/sql-resource.ts";
|
|
|
35
36
|
import {
|
|
36
37
|
identifierName,
|
|
37
38
|
inferEffects,
|
|
39
|
+
resolveCallTarget,
|
|
38
40
|
stringArg,
|
|
39
41
|
walk,
|
|
40
42
|
type AstNode,
|
|
@@ -177,11 +179,12 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
|
|
|
177
179
|
if (Object.keys(gates).length > 0) manifest.gates = gates;
|
|
178
180
|
if (Object.keys(vault).length > 0) manifest.vault = vault;
|
|
179
181
|
if (Object.keys(channels).length > 0) manifest.channels = channels;
|
|
180
|
-
if (scope.ai.models || scope.ai.prompts || scope.ai.agents) {
|
|
182
|
+
if (scope.ai.models || scope.ai.prompts || scope.ai.agents || scope.ai.mcpServers) {
|
|
181
183
|
manifest.ai = {
|
|
182
184
|
...(scope.ai.models ? { models: sortRecord(scope.ai.models) } : {}),
|
|
183
185
|
...(scope.ai.prompts ? { prompts: sortRecord(scope.ai.prompts) } : {}),
|
|
184
186
|
...(scope.ai.agents ? { agents: sortRecord(scope.ai.agents) } : {}),
|
|
187
|
+
...(scope.ai.mcpServers ? { mcpServers: sortRecord(scope.ai.mcpServers) } : {}),
|
|
185
188
|
};
|
|
186
189
|
}
|
|
187
190
|
if (Object.keys(journeys).length > 0) manifest.journeys = journeys;
|
|
@@ -301,6 +304,13 @@ function finalizeRefs(scope: ProjectScope): void {
|
|
|
301
304
|
);
|
|
302
305
|
}
|
|
303
306
|
}
|
|
307
|
+
if (scope.ai.mcpServers) {
|
|
308
|
+
for (const server of Object.values(scope.ai.mcpServers)) {
|
|
309
|
+
if (!server.auth) continue;
|
|
310
|
+
const binding = scope.bindings.get(server.auth);
|
|
311
|
+
if (binding?.kind === "secret") server.auth = binding.ref;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
304
314
|
for (const journey of Object.values(scope.journeys)) {
|
|
305
315
|
if (!journey.flows) continue;
|
|
306
316
|
journey.flows = journey.flows.map(
|
|
@@ -650,6 +660,7 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
|
|
|
650
660
|
const namespaces = new Set(storeEntry.namespaces ?? []);
|
|
651
661
|
namespaces.add(storeName);
|
|
652
662
|
storeEntry.namespaces = [...namespaces].sort();
|
|
663
|
+
if (boolProp(storeOpts, "durable")) storeEntry.durable = true;
|
|
653
664
|
} else if (facet === "files") {
|
|
654
665
|
const buckets = new Set(storeEntry.buckets ?? []);
|
|
655
666
|
buckets.add(storeName);
|
|
@@ -815,6 +826,7 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
|
|
|
815
826
|
const model: AiModel = {
|
|
816
827
|
...(stringProp(opts, "provider") ? { provider: stringProp(opts, "provider") } : {}),
|
|
817
828
|
...(stringProp(opts, "tier") ? { tier: stringProp(opts, "tier") } : {}),
|
|
829
|
+
...(stringProp(opts, "driverId") ? { driverId: stringProp(opts, "driverId") } : {}),
|
|
818
830
|
};
|
|
819
831
|
scope.ai.models = scope.ai.models ?? {};
|
|
820
832
|
scope.ai.models[modelName] = model;
|
|
@@ -832,6 +844,24 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
|
|
|
832
844
|
collectAgent(call, scope);
|
|
833
845
|
}
|
|
834
846
|
|
|
847
|
+
if (obj === "ai" && prop === "mcpServer") {
|
|
848
|
+
collectMcpServer(call, program, scope);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (prop === "tool" && obj) {
|
|
852
|
+
const toolName = stringArg(call.arguments[0]);
|
|
853
|
+
const server = scope.bindings.get(obj);
|
|
854
|
+
if (server?.kind === "mcp-server" && toolName) {
|
|
855
|
+
const bindingName = enclosingConstName(call, program);
|
|
856
|
+
if (bindingName) {
|
|
857
|
+
scope.bindings.set(bindingName, {
|
|
858
|
+
kind: "mcp-tool",
|
|
859
|
+
ref: `mcp:${server.ref}/${toolName}`,
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
835
865
|
if (obj === "ai" && prop === "embed") {
|
|
836
866
|
const embedName = stringArg(call.arguments[0]);
|
|
837
867
|
if (embedName) {
|
|
@@ -1025,7 +1055,11 @@ function collectAgent(call: CallExpression, scope: ProjectScope): void {
|
|
|
1025
1055
|
if (!agentName || !opts) return;
|
|
1026
1056
|
const toolsArr = arrayProp(opts, "tools");
|
|
1027
1057
|
const tools = toolsArr
|
|
1028
|
-
?.map((el) =>
|
|
1058
|
+
?.map((el) => {
|
|
1059
|
+
const resolved = resolveCallTarget(el, scope.bindings);
|
|
1060
|
+
if (resolved) return resolved;
|
|
1061
|
+
return identifierName(el) ?? stringArg(el);
|
|
1062
|
+
})
|
|
1029
1063
|
.filter((x): x is string => typeof x === "string")
|
|
1030
1064
|
.map((id) => scope.flowExports.get(id) ?? scope.bindings.get(id)?.ref ?? id);
|
|
1031
1065
|
const agent: AiAgent = {
|
|
@@ -1038,6 +1072,40 @@ function collectAgent(call: CallExpression, scope: ProjectScope): void {
|
|
|
1038
1072
|
scope.ai.agents[agentName] = agent;
|
|
1039
1073
|
}
|
|
1040
1074
|
|
|
1075
|
+
function collectMcpServer(call: CallExpression, program: AstNode, scope: ProjectScope): void {
|
|
1076
|
+
const serverName = stringArg(call.arguments[0]);
|
|
1077
|
+
const opts = objectArg(call.arguments[1]);
|
|
1078
|
+
if (!serverName || !opts) return;
|
|
1079
|
+
const tools = stringArrayProp(opts, "tools") ?? [];
|
|
1080
|
+
const url = stringProp(opts, "url");
|
|
1081
|
+
const command = stringProp(opts, "command");
|
|
1082
|
+
const args = stringArrayProp(opts, "args");
|
|
1083
|
+
const authObj = objectProp(opts, "auth");
|
|
1084
|
+
const bearerNode = authObj ? objectProp(authObj, "bearer") : undefined;
|
|
1085
|
+
const bearerLit = stringArg(bearerNode);
|
|
1086
|
+
const bearerId = identifierName(bearerNode);
|
|
1087
|
+
const auth =
|
|
1088
|
+
bearerLit ??
|
|
1089
|
+
(bearerId
|
|
1090
|
+
? scope.bindings.get(bearerId)?.kind === "secret"
|
|
1091
|
+
? scope.bindings.get(bearerId)?.ref
|
|
1092
|
+
: bearerId
|
|
1093
|
+
: undefined);
|
|
1094
|
+
const server: AiMcpServer = {
|
|
1095
|
+
tools,
|
|
1096
|
+
...(url ? { url } : {}),
|
|
1097
|
+
...(command ? { command } : {}),
|
|
1098
|
+
...(args && args.length > 0 ? { args } : {}),
|
|
1099
|
+
...(auth ? { auth } : {}),
|
|
1100
|
+
};
|
|
1101
|
+
scope.ai.mcpServers = scope.ai.mcpServers ?? {};
|
|
1102
|
+
scope.ai.mcpServers[serverName] = server;
|
|
1103
|
+
const bindingName = enclosingConstName(call, program);
|
|
1104
|
+
if (bindingName) {
|
|
1105
|
+
scope.bindings.set(bindingName, { kind: "mcp-server", ref: serverName });
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1041
1109
|
function collectConfig(opts: AstNode | undefined, scope: ProjectScope): void {
|
|
1042
1110
|
if (!opts) return;
|
|
1043
1111
|
|
package/src/compiler/response.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { FlowFailure } from "../kernel/errors.ts";
|
|
8
|
-
import { isJsonResult } from "../kernel/fx.ts";
|
|
8
|
+
import { isJsonResult, isJsonStreamResult, type JsonStreamResult } from "../kernel/fx.ts";
|
|
9
9
|
import { isFlowFailure } from "../kernel/hooks.ts";
|
|
10
10
|
import { VALIDATION_ERROR_CODE } from "../validation/standard-schema.ts";
|
|
11
11
|
|
|
@@ -53,6 +53,9 @@ export function statusForFailure(failure: FlowFailure): number {
|
|
|
53
53
|
* @param output - Handler output (`undefined` → 204)
|
|
54
54
|
*/
|
|
55
55
|
export function encodeSuccess(output: unknown): Response {
|
|
56
|
+
if (isJsonStreamResult(output)) {
|
|
57
|
+
return encodeSseStream(output);
|
|
58
|
+
}
|
|
56
59
|
if (isJsonResult(output)) {
|
|
57
60
|
if (output.status === 204) {
|
|
58
61
|
return new Response(null, { status: 204 });
|
|
@@ -110,3 +113,44 @@ export function encodeExecuteResult(result: {
|
|
|
110
113
|
}
|
|
111
114
|
return encodeSuccess(result.output);
|
|
112
115
|
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Encode {@link JsonStreamResult} as SSE (`text/event-stream`).
|
|
119
|
+
*
|
|
120
|
+
* @param carrier - Stream carrier from `fx.json.stream`
|
|
121
|
+
*/
|
|
122
|
+
function encodeSseStream(carrier: JsonStreamResult): Response {
|
|
123
|
+
const encoder = new TextEncoder();
|
|
124
|
+
let finalized = false;
|
|
125
|
+
const finish = async (): Promise<void> => {
|
|
126
|
+
if (finalized) return;
|
|
127
|
+
finalized = true;
|
|
128
|
+
await carrier.finalize?.();
|
|
129
|
+
};
|
|
130
|
+
const body = new ReadableStream<Uint8Array>({
|
|
131
|
+
async start(controller) {
|
|
132
|
+
try {
|
|
133
|
+
for await (const chunk of carrier.chunks) {
|
|
134
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
|
|
135
|
+
}
|
|
136
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
137
|
+
controller.close();
|
|
138
|
+
} catch (err) {
|
|
139
|
+
controller.error(err);
|
|
140
|
+
} finally {
|
|
141
|
+
await finish();
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
cancel() {
|
|
145
|
+
void finish();
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
return new Response(body, {
|
|
149
|
+
status: 200,
|
|
150
|
+
headers: {
|
|
151
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
152
|
+
"cache-control": "no-cache",
|
|
153
|
+
connection: "keep-alive",
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
package/src/console/server/ai.ts
CHANGED
|
@@ -459,7 +459,7 @@ function buildVersionMetrics(input: {
|
|
|
459
459
|
costs: [],
|
|
460
460
|
latencies: [],
|
|
461
461
|
evals: [],
|
|
462
|
-
outcomes: { ok: 0, provider_error: 0, schema_invalid: 0 },
|
|
462
|
+
outcomes: { ok: 0, provider_error: 0, schema_invalid: 0, budget_exceeded: 0 },
|
|
463
463
|
overBudget: 0,
|
|
464
464
|
budget: meta?.budget ?? null,
|
|
465
465
|
};
|
|
@@ -514,7 +514,10 @@ function buildVersionMetrics(input: {
|
|
|
514
514
|
const out: PromptVersionMetrics[] = [];
|
|
515
515
|
for (const acc of map.values()) {
|
|
516
516
|
const totalOutcomes =
|
|
517
|
-
acc.outcomes.ok +
|
|
517
|
+
acc.outcomes.ok +
|
|
518
|
+
acc.outcomes.provider_error +
|
|
519
|
+
acc.outcomes.schema_invalid +
|
|
520
|
+
acc.outcomes.budget_exceeded;
|
|
518
521
|
out.push({
|
|
519
522
|
prompt: acc.prompt,
|
|
520
523
|
version: acc.version,
|
|
@@ -244,7 +244,12 @@ export async function serveConsole(
|
|
|
244
244
|
new Response("WebSocket upgrade failed", { status: 400 }),
|
|
245
245
|
);
|
|
246
246
|
}
|
|
247
|
-
return
|
|
247
|
+
return (async () => {
|
|
248
|
+
const res = await fetchHandler(req);
|
|
249
|
+
const ct = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
|
|
250
|
+
if (/^text\/event-stream$/i.test(ct)) srv.timeout(req, 0);
|
|
251
|
+
return res;
|
|
252
|
+
})();
|
|
248
253
|
},
|
|
249
254
|
websocket: {
|
|
250
255
|
open: live.open,
|
|
@@ -259,6 +264,15 @@ export async function serveConsole(
|
|
|
259
264
|
boundHost.includes(":") && !boundHost.startsWith("[") ? `[${boundHost}]` : boundHost;
|
|
260
265
|
const url = new URL(`http://${hostForUrl}:${boundPort}/`);
|
|
261
266
|
|
|
267
|
+
const closeIdle = (
|
|
268
|
+
server as typeof server & { closeIdleConnections(): void }
|
|
269
|
+
).closeIdleConnections.bind(server);
|
|
270
|
+
const onPressure = (): void => {
|
|
271
|
+
closeIdle();
|
|
272
|
+
};
|
|
273
|
+
const proc = process as NodeJS.EventEmitter;
|
|
274
|
+
proc.on("memoryPressure", onPressure);
|
|
275
|
+
|
|
262
276
|
return {
|
|
263
277
|
console: handle,
|
|
264
278
|
url,
|
|
@@ -266,9 +280,10 @@ export async function serveConsole(
|
|
|
266
280
|
hostname: boundHost,
|
|
267
281
|
fetch: fetchHandler,
|
|
268
282
|
stop(closeActive = false) {
|
|
269
|
-
|
|
283
|
+
proc.off("memoryPressure", onPressure);
|
|
270
284
|
void handle.app.stop();
|
|
271
285
|
void persistence?.close();
|
|
286
|
+
return server.stop(closeActive);
|
|
272
287
|
},
|
|
273
288
|
};
|
|
274
289
|
}
|
|
@@ -0,0 +1 @@
|
|
|
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{D as c,E as l,T as u,b as d,o as f,w as p,y as m}from"./http-method-CJCBYL2j.js";function h(e){let t=e.indexOf(`.`);return t===-1?e:e.slice(0,t)}function g(e){let t=e.indexOf(`.`);return t===-1?e:e.slice(t+1)}function _(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 v(e,t){return _(e).get(t)??[]}function y(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 b(e,t,n,r,i=!1){let a=m[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:d.edge,markerEnd:{type:o.ArrowClosed,width:14,height:14,color:a}}}function x(e){let t=e?.flows??{},n=new Set(Object.keys(t)),r=[],i=new Map;for(let e of[...n].sort()){let t=h(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=y(t);i&&a.set(t,i),r.push(b(`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(b(`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(b(`e:flow:${e}-gates->${n}`,`flow:${e}`,n,`gates`))}let i=n.effects;if(i){for(let t of i.reads??[]){let n=y(t);n&&(a.set(t,n),o.set(t,(o.get(t)??0)+1),r.push(b(`e:flow:${e}-reads->${t}`,`flow:${e}`,t,`reads`)))}for(let t of i.writes??[]){let n=y(t);n&&(a.set(t,n),o.set(t,(o.get(t)??0)+1),r.push(b(`e:flow:${e}-writes->${t}`,`flow:${e}`,t,`writes`)))}for(let t of i.emits??[]){let n=`signal:${t}`,i=y(n);i&&(a.set(n,i),o.set(n,(o.get(n)??0)+1),r.push(b(`e:flow:${e}->${n}`,`flow:${e}`,n,`emits`)))}for(let t of i.asks??[]){let n=`ai:${t}`,i=y(n);i&&(a.set(n,i),o.set(n,(o.get(n)??0)+1),r.push(b(`e:flow:${e}->${n}`,`flow:${e}`,n,`asks`)))}for(let t of i.sends??[]){let n=`channel:${t}`,i=y(n);i&&(a.set(n,i),o.set(n,(o.get(n)??0)+1),r.push(b(`e:flow:${e}->${n}`,`flow:${e}`,n,`sends`)))}for(let t of i.secrets??[]){let n=`vault:${t}`,i=y(n);i&&(a.set(n,i),o.set(n,(o.get(n)??0)+1),r.push(b(`e:flow:${e}->${n}`,`flow:${e}`,n,`secrets`)))}for(let t of i.calls??[]){let n=c(t);if(n){let t=l(n.server);a.set(t,{id:t,kind:`ai`,label:n.server}),o.set(t,(o.get(t)??0)+1),r.push(b(`e:flow:${e}->${t}`,`flow:${e}`,t,`calls`,!0));continue}r.push(b(`e:flow:${e}->flow:${t}`,`flow:${e}`,`flow:${t}`,`calls`,!0))}}}return{nodes:S(i,t,a,o),edges:r,flowIds:n}}function S(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}`,{...p.flow}),i.setParent(`flow:${e}`,`unit:${t}`)}for(let e of n.values())i.setNode(e.id,{...p[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??[]){let n=c(t);n?r(`flow:${e}`,l(n.server)):r(`flow:${e}`,`flow:${t}`)}}}s.layout(i);let o=[],{headerH:f,padX:m,padBottom:h}=u;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,l=r.length*p.flow.height+Math.max(0,r.length-1)*10,u=p.flow.width+m*2,_=f+l+h,v=s-u/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:d.unit,width:u,height:_,style:{width:u,height:_}}),r.forEach((r,i)=>{let a=t[r];a&&o.push({id:`flow:${r}`,type:`flow`,position:{x:m,y:f+i*(p.flow.height+10)},parentId:e,extent:`parent`,zIndex:d.leaf,data:{kind:`flow`,label:g(r),refId:r,unit:n,plane:a.plane??`user`,badge:a.plane??`user`},draggable:!1,width:p.flow.width,height:p.flow.height,style:{width:p.flow.width,height:p.flow.height}})})}let _=[...n.values()].sort((e,t)=>e.kind.localeCompare(t.kind)||e.label.localeCompare(t.label));for(let e of _){let t=i.node(e.id),n=p[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:d.leaf,width:n.width,height:n.height,style:{width:n.width,height:n.height}})}return o}function C(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 w(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=m[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 T=100;function E(e,t){return e.filter(e=>t.has(e.flow)).sort((e,t)=>t.startedAt-e.startedAt).slice(0,T)}var D=a();function O({cache:a,dataSlot:o}){let s=f(a);return(0,D.jsxs)(r,{children:[(0,D.jsx)(n,{render:t=>(0,D.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,D.jsx)(i,{icon:s.icon,className:`size-3`,"aria-hidden":!0})})}),(0,D.jsx)(t,{side:`top`,children:s.label})]})}export{w as a,h as c,C as i,E as n,x as o,g as r,v as s,O as t};
|