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.
Files changed (127) hide show
  1. package/manifest.v1.schema.json +7 -1
  2. package/package.json +2 -2
  3. package/site/content/docs/ai/mcp.mdx +27 -2
  4. package/site/content/docs/elements/ai.mdx +58 -11
  5. package/site/content/docs/elements/clock.mdx +27 -12
  6. package/site/content/docs/elements/flow.mdx +6 -3
  7. package/site/content/docs/elements/signal.mdx +71 -25
  8. package/site/content/docs/elements/store.mdx +21 -1
  9. package/site/content/docs/get-started/installation.mdx +2 -2
  10. package/site/content/docs/providers/index.mdx +1 -0
  11. package/site/content/docs/recipes/dragonfly.mdx +4 -3
  12. package/site/content/docs/recipes/redis.mdx +4 -5
  13. package/site/content/docs/recipes/valkey.mdx +5 -3
  14. package/site/content/docs/reference/configuration.mdx +3 -1
  15. package/site/content/docs/reference/environment-variables.mdx +5 -5
  16. package/site/content/docs/reference/fx.mdx +27 -22
  17. package/src/cli/ai-setup/recommend.test.ts +25 -0
  18. package/src/cli/ai-setup/recommend.ts +8 -3
  19. package/src/cli/load-config.images.test.ts +1 -0
  20. package/src/compiler/effects-infer.ts +58 -3
  21. package/src/compiler/extract.test.ts +68 -1
  22. package/src/compiler/extract.ts +70 -2
  23. package/src/compiler/response.ts +45 -1
  24. package/src/console/server/ai.ts +5 -2
  25. package/src/console/server/flows.ts +1 -0
  26. package/src/console/server/serve.ts +17 -2
  27. package/src/console/ui-next/dist/assets/cache-glyph-BanhLsEY.js +1 -0
  28. package/src/console/ui-next/dist/assets/flows-page-DxDsOd4f.js +1 -0
  29. package/src/console/ui-next/dist/assets/http-method-CJCBYL2j.js +1 -0
  30. package/src/console/ui-next/dist/assets/{index-DX248G39.js → index-Ce6WKWKM.js} +3 -3
  31. package/src/console/ui-next/dist/assets/observability-page-BEZDzyYh.js +4 -0
  32. package/src/console/ui-next/dist/assets/trace-detail-sheet-DFLFfUUX.js +2 -0
  33. package/src/console/ui-next/dist/assets/units-page-C0gW6Kdo.js +1 -0
  34. package/src/console/ui-next/dist/assets/{vault-page-CSOYMHhO.js → vault-page-DuKzqwzW.js} +1 -1
  35. package/src/console/ui-next/dist/index.html +1 -1
  36. package/src/console/ui-next/seed-invoke-host.ts +2 -0
  37. package/src/console/ui-next/src/features/flows/graph/build-flow-graph.test.ts +18 -0
  38. package/src/console/ui-next/src/features/flows/graph/build-flow-graph.ts +14 -1
  39. package/src/console/ui-next/src/features/flows/graph/neighborhood.test.ts +17 -0
  40. package/src/console/ui-next/src/features/flows/graph/neighborhood.ts +16 -3
  41. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +3 -1
  42. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  43. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +9 -3
  44. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +10 -1
  45. package/src/console/ui-next/src/features/observability/lib/ask-count.test.ts +25 -0
  46. package/src/console/ui-next/src/features/observability/lib/ask-count.ts +4 -1
  47. package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +12 -4
  48. package/src/docker/docker.test.ts +1 -1
  49. package/src/docker/dockerfile.ts +1 -1
  50. package/src/docker/helpers.ts +28 -0
  51. package/src/docker/recipes/dragonfly.ts +3 -6
  52. package/src/docker/recipes/redis.ts +2 -6
  53. package/src/docker/recipes/valkey.ts +2 -6
  54. package/src/drivers/ai-anthropic.ts +5 -0
  55. package/src/drivers/ai-ollama.ts +49 -30
  56. package/src/drivers/ai-openai-compatible.ts +57 -46
  57. package/src/drivers/ai-providers.test.ts +3 -0
  58. package/src/drivers/bun-native-completeness.test.ts +7 -9
  59. package/src/drivers/redis.ts +11 -4
  60. package/src/drivers/signal-redis.ts +24 -14
  61. package/src/drivers/signal-types.ts +2 -1
  62. package/src/drivers/types.ts +1 -1
  63. package/src/elements/ai/declare.ts +109 -0
  64. package/src/elements/ai/errors.test.ts +5 -1
  65. package/src/elements/ai/errors.ts +30 -2
  66. package/src/elements/ai/eval.ts +4 -6
  67. package/src/elements/ai/mcp-client.test.ts +206 -0
  68. package/src/elements/ai/mcp-client.ts +362 -0
  69. package/src/elements/ai/mcp-http.ts +159 -0
  70. package/src/elements/ai/mcp-mock.ts +134 -0
  71. package/src/elements/ai/mcp-protocol.ts +234 -0
  72. package/src/elements/ai/mcp-stdio.test.ts +50 -0
  73. package/src/elements/ai/mcp-stdio.ts +212 -0
  74. package/src/elements/ai/mcp-transport.ts +70 -0
  75. package/src/elements/ai/runtime.ts +159 -29
  76. package/src/elements/ai.test.ts +139 -0
  77. package/src/elements/ai.ts +16 -0
  78. package/src/elements/clock/health.test.ts +43 -0
  79. package/src/elements/clock/runtime.ts +55 -2
  80. package/src/elements/clock/schedule.ts +71 -142
  81. package/src/elements/clock.test.ts +9 -40
  82. package/src/elements/clock.ts +6 -1
  83. package/src/elements/gate/runtime.ts +3 -3
  84. package/src/elements/index.ts +2 -0
  85. package/src/elements/signal/declare.ts +2 -1
  86. package/src/elements/signal/runtime.ts +16 -1
  87. package/src/elements/signal.ts +1 -0
  88. package/src/elements/store/cache.test.ts +2 -0
  89. package/src/elements/store/cache.ts +3 -3
  90. package/src/elements/store/declare.ts +7 -0
  91. package/src/elements/store/kv-sql.test.ts +86 -0
  92. package/src/elements/store/kv-sql.ts +178 -0
  93. package/src/elements/store/runtime.ts +35 -9
  94. package/src/elements/store.test.ts +2 -0
  95. package/src/elements/vault/builtin-adapter.ts +17 -0
  96. package/src/full.ts +2 -0
  97. package/src/index.ts +4 -0
  98. package/src/kernel/app.ts +97 -64
  99. package/src/kernel/auto-registry.test.ts +5 -0
  100. package/src/kernel/boot-bind/ai.ts +24 -0
  101. package/src/kernel/boot-bind/clock.ts +2 -0
  102. package/src/kernel/boot-bind/store.test.ts +150 -1
  103. package/src/kernel/boot-bind/store.ts +66 -1
  104. package/src/kernel/boot.ts +3 -1
  105. package/src/kernel/element-registries.ts +4 -1
  106. package/src/kernel/fx-dead-letters.test.ts +77 -0
  107. package/src/kernel/fx.test.ts +22 -0
  108. package/src/kernel/fx.ts +112 -6
  109. package/src/kernel/http-stream.test.ts +174 -0
  110. package/src/kernel/index.ts +2 -0
  111. package/src/manifest/diff.test.ts +13 -0
  112. package/src/manifest/diff.ts +15 -0
  113. package/src/manifest/mcp-ref.ts +88 -0
  114. package/src/manifest/types.ts +33 -2
  115. package/src/manifest/validate.test.ts +20 -0
  116. package/src/mcp/docs-server.ts +1 -1
  117. package/src/mcp/server.ts +1 -1
  118. package/src/plugins/compression.test.ts +21 -0
  119. package/src/plugins/compression.ts +1 -0
  120. package/src/runtime/bun.ts +41 -4
  121. package/src/test/reset-element-registries.ts +2 -0
  122. package/src/console/ui-next/dist/assets/cache-glyph-CLPBqZeb.js +0 -1
  123. package/src/console/ui-next/dist/assets/flows-page-C_Tas1E1.js +0 -1
  124. package/src/console/ui-next/dist/assets/http-method-BJ92Z_ke.js +0 -1
  125. package/src/console/ui-next/dist/assets/observability-page-BwVS5Jvm.js +0 -4
  126. package/src/console/ui-next/dist/assets/trace-detail-sheet-D16lWQMt.js +0 -2
  127. 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 | Default when unset |
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 | config map |
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 | 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) |
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? })` | `ask` | `AsyncIterable<string>` — real driver stream; cancels via ambient `fx.signal` |
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.sleep(label, duration)` | Durable sleep in `durable` flows; immediate otherwise |
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
- if (value.length === width) return value;
199
- if (value.length < width) return value.padEnd(width);
200
- return `${value.slice(0, Math.max(0, width - 1))}…`;
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
  /**
@@ -132,6 +132,7 @@ describe("resolveImages", () => {
132
132
  });
133
133
  expect(images["store.sql"]).toBe("postgres:18-alpine");
134
134
  expect(images["store.kv"]).toBe("redis:8-alpine");
135
+ expect(images["store.kv.durable"]).toBeUndefined();
135
136
  });
136
137
  });
137
138
 
@@ -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 = resolveNamed(call.arguments[0], options.bindings, "flow");
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 = resolveNamed(el, bindings, "flow");
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
- expect(elapsed).toBeLessThan(2000);
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
+ });
@@ -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) => identifierName(el) ?? stringArg(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
 
@@ -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
+ }
@@ -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 + acc.outcomes.provider_error + acc.outcomes.schema_invalid;
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,
@@ -834,6 +834,7 @@ const AiListOut = z.object({
834
834
  ok: z.number(),
835
835
  provider_error: z.number(),
836
836
  schema_invalid: z.number(),
837
+ budget_exceeded: z.number(),
837
838
  }),
838
839
  }),
839
840
  ),
@@ -244,7 +244,12 @@ export async function serveConsole(
244
244
  new Response("WebSocket upgrade failed", { status: 400 }),
245
245
  );
246
246
  }
247
- return fetchHandler(req);
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
- server.stop(closeActive);
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};