jev-devtools 0.1.3 → 0.3.0

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 (52) hide show
  1. package/README.md +50 -4
  2. package/THIRD_PARTY_NOTICES.txt +27 -0
  3. package/lib/flow/index.d.ts +82 -0
  4. package/lib/flow/index.js +588 -0
  5. package/lib/server/flows.d.ts +25 -0
  6. package/lib/server/flows.js +115 -0
  7. package/lib/server/index.d.ts +8 -0
  8. package/lib/server/index.js +163 -9
  9. package/lib/server/recorder.d.ts +12 -2
  10. package/lib/server/recorder.js +47 -9
  11. package/lib/src/ChoiceProbabilities.d.ts +4 -0
  12. package/lib/src/ChoiceProbabilities.js +19 -0
  13. package/lib/src/ConnectedInspector.js +46 -29
  14. package/lib/src/DiagramContent.d.ts +6 -0
  15. package/lib/src/DiagramContent.js +32 -0
  16. package/lib/src/FlowOverview.d.ts +7 -0
  17. package/lib/src/FlowOverview.js +228 -0
  18. package/lib/src/JevDevtools.d.ts +6 -1
  19. package/lib/src/JevDevtools.js +52 -48
  20. package/lib/src/JevDrawer.d.ts +3 -2
  21. package/lib/src/JevDrawer.js +12 -3
  22. package/lib/src/Questions.d.ts +5 -0
  23. package/lib/src/Questions.js +174 -0
  24. package/lib/src/TraceContent.d.ts +11 -0
  25. package/lib/src/TraceContent.js +32 -0
  26. package/lib/src/TraceDiagram.d.ts +4 -1
  27. package/lib/src/TraceDiagram.js +111 -25
  28. package/lib/src/condition-inputs.d.ts +9 -0
  29. package/lib/src/condition-inputs.js +100 -0
  30. package/lib/src/flow-condition.d.ts +4 -0
  31. package/lib/src/flow-condition.js +47 -0
  32. package/lib/src/flow-model.d.ts +7 -2
  33. package/lib/src/flow-model.js +3 -12
  34. package/lib/src/flow-protocol.d.ts +68 -0
  35. package/lib/src/flow-protocol.js +1 -0
  36. package/lib/src/flow-stream.d.ts +8 -0
  37. package/lib/src/flow-stream.js +80 -0
  38. package/lib/src/index.d.ts +1 -1
  39. package/lib/src/inspector-styles.js +1 -1
  40. package/lib/src/inspector.css +725 -8
  41. package/lib/src/protocol.d.ts +18 -0
  42. package/lib/src/trace-store.d.ts +6 -0
  43. package/lib/src/trace-store.js +66 -0
  44. package/lib/src/trace-stream.d.ts +2 -0
  45. package/lib/src/trace-stream.js +65 -0
  46. package/lib/src/useDiagramViewport.d.ts +5 -0
  47. package/lib/src/useDiagramViewport.js +34 -6
  48. package/package.json +7 -2
  49. package/skills/setup-jev-devtools/SKILL.md +6 -2
  50. package/skills/setup-jev-devtools/references/flows.md +150 -0
  51. package/skills/setup-jev-devtools/references/nextjs.md +1 -1
  52. package/skills/setup-jev-devtools/references/verify.md +3 -1
@@ -25,6 +25,11 @@ export type Trace = {
25
25
  version: 1;
26
26
  id: string;
27
27
  session: string;
28
+ flow?: {
29
+ runId: string;
30
+ stepId: string;
31
+ executionId: string;
32
+ };
28
33
  label: string;
29
34
  source: "sample" | "live";
30
35
  startedAt: number;
@@ -62,3 +67,16 @@ export type Snapshot = {
62
67
  version: 1;
63
68
  traces: Trace[];
64
69
  };
70
+ export type StreamSnapshot = {
71
+ version: 2;
72
+ revision: number;
73
+ traces: Trace[];
74
+ };
75
+ export type TraceDelta = {
76
+ version: 2;
77
+ base: number;
78
+ revision: number;
79
+ upserts: Trace[];
80
+ /** Authoritative newest-first retained IDs. Omitted IDs are removed. */
81
+ order: string[];
82
+ };
@@ -0,0 +1,6 @@
1
+ import type { Trace } from "./protocol.js";
2
+ export declare class TraceStore {
3
+ private current;
4
+ private revision;
5
+ apply(event: "snapshot" | "delta", input: unknown): Trace[];
6
+ }
@@ -0,0 +1,66 @@
1
+ const revision = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
2
+ function traces(value) {
3
+ if (!Array.isArray(value) || value.length > 200)
4
+ throw new Error("Invalid traces");
5
+ const ids = new Set();
6
+ for (const trace of value) {
7
+ if (!trace ||
8
+ trace.version !== 1 ||
9
+ typeof trace.id !== "string" ||
10
+ !trace.id ||
11
+ ids.has(trace.id))
12
+ throw new Error("Invalid trace ID");
13
+ ids.add(trace.id);
14
+ }
15
+ return value;
16
+ }
17
+ // Owned by one EventSource connection. Unchanged trace objects keep their
18
+ // identity; validate the complete update before replacing any current state.
19
+ export class TraceStore {
20
+ current = [];
21
+ revision;
22
+ apply(event, input) {
23
+ if (!input || typeof input !== "object")
24
+ throw new Error("Invalid update");
25
+ const data = input;
26
+ if (event === "snapshot") {
27
+ if (data.version !== 1 && data.version !== 2)
28
+ throw new Error("Unsupported stream");
29
+ if (data.version === 2 && !revision(data.revision))
30
+ throw new Error("Invalid revision");
31
+ const next = traces(data.traces);
32
+ this.current = next;
33
+ this.revision =
34
+ data.version === 2 ? data.revision : undefined;
35
+ return next;
36
+ }
37
+ if (data.version !== 2 ||
38
+ this.revision === undefined ||
39
+ data.base !== this.revision ||
40
+ !revision(data.revision) ||
41
+ data.revision <= this.revision)
42
+ throw new Error("Inspector stream needs a fresh snapshot");
43
+ if (!Array.isArray(data.order) ||
44
+ data.order.length > 200 ||
45
+ data.order.some((id) => typeof id !== "string") ||
46
+ new Set(data.order).size !== data.order.length)
47
+ throw new Error("Invalid retained order");
48
+ const order = data.order;
49
+ const retained = new Set(order);
50
+ const upserts = traces(data.upserts);
51
+ if (upserts.some((t) => !retained.has(t.id)))
52
+ throw new Error("Unexpected upsert");
53
+ const byId = new Map(this.current.map((t) => [t.id, t]));
54
+ for (const trace of upserts)
55
+ byId.set(trace.id, trace);
56
+ const next = order.map((id) => {
57
+ const trace = byId.get(id);
58
+ if (!trace)
59
+ throw new Error("Missing retained trace");
60
+ return trace;
61
+ });
62
+ this.current = next;
63
+ this.revision = data.revision;
64
+ return next;
65
+ }
66
+ }
@@ -0,0 +1,2 @@
1
+ import type { Trace } from "./protocol.js";
2
+ export declare function connectTraceStream(url: string, onTraces: (traces: Trace[]) => void, onConnection: (status: string) => void): () => void;
@@ -0,0 +1,65 @@
1
+ import { TraceStore } from "./trace-store.js";
2
+ export function connectTraceStream(url, onTraces, onConnection) {
3
+ let alive = true, events, retry, frame, fallback, pending;
4
+ const cancelPublish = () => {
5
+ if (frame !== undefined)
6
+ cancelAnimationFrame(frame);
7
+ clearTimeout(fallback);
8
+ frame = undefined;
9
+ fallback = undefined;
10
+ pending = undefined;
11
+ };
12
+ const publish = () => {
13
+ const next = pending;
14
+ cancelPublish();
15
+ if (alive && next)
16
+ onTraces(next);
17
+ };
18
+ const schedule = (next) => {
19
+ pending = next;
20
+ if (frame !== undefined)
21
+ return;
22
+ // Merge every protocol event, but render only the newest state per frame.
23
+ // Background tabs may suspend rAF; keep delivery bounded there as well.
24
+ frame = requestAnimationFrame(publish);
25
+ fallback = setTimeout(publish, 100);
26
+ };
27
+ const connect = () => {
28
+ if (!alive)
29
+ return;
30
+ // Native reconnects receive a full snapshot too. After an invalid delta,
31
+ // discard the cursor and reconnect rather than applying partial history.
32
+ const store = new TraceStore();
33
+ const source = new EventSource(url + "/events?protocol=2");
34
+ events = source;
35
+ const receive = (kind, event) => {
36
+ if (!alive || events !== source)
37
+ return;
38
+ try {
39
+ const next = store.apply(kind, JSON.parse(event.data));
40
+ schedule(next);
41
+ onConnection("connected");
42
+ }
43
+ catch {
44
+ cancelPublish();
45
+ source.close();
46
+ events = undefined;
47
+ onConnection("resynchronizing");
48
+ retry = setTimeout(connect, 1000);
49
+ }
50
+ };
51
+ source.addEventListener("snapshot", (event) => receive("snapshot", event));
52
+ source.addEventListener("delta", (event) => receive("delta", event));
53
+ source.onerror = () => {
54
+ if (alive && events === source)
55
+ onConnection("reconnecting");
56
+ };
57
+ };
58
+ connect();
59
+ return () => {
60
+ alive = false;
61
+ clearTimeout(retry);
62
+ cancelPublish();
63
+ events?.close();
64
+ };
65
+ }
@@ -6,4 +6,9 @@ export declare function useDiagramViewport(width: number, height: number, scene:
6
6
  zoom: number;
7
7
  zoomBy: (factor: number, point?: Point) => void;
8
8
  fit: () => void;
9
+ preserveCamera: () => void;
10
+ focusArea: (area: Point & {
11
+ width: number;
12
+ height: number;
13
+ }) => void;
9
14
  };
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState, } from "react";
2
2
  import { fitCamera, zoomCamera, } from "./diagram-camera.js";
3
3
  /** Lab-style camera. Pointer frames update the transform without rerendering cards. */
4
4
  export function useDiagramViewport(width, height, scene) {
@@ -7,6 +7,10 @@ export function useDiagramViewport(width, height, scene) {
7
7
  const camera = useRef({ x: 0, y: 0, scale: 1 });
8
8
  const size = useRef({ width: 0, height: 0 });
9
9
  const fitted = useRef(true);
10
+ const bounds = useRef({ width, height });
11
+ useLayoutEffect(() => {
12
+ bounds.current = { width, height };
13
+ }, [width, height]);
10
14
  const frame = useRef(null);
11
15
  const [zoom, setZoom] = useState(100);
12
16
  const paint = useCallback(() => {
@@ -19,7 +23,12 @@ export function useDiagramViewport(width, height, scene) {
19
23
  canvas.current.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;
20
24
  if (viewport.current) {
21
25
  viewport.current.style.backgroundPosition = `${x}px ${y}px`;
22
- viewport.current.style.backgroundSize = `${20 * scale}px ${20 * scale}px`;
26
+ // Thin out the decorative grid at overview zooms instead of packing
27
+ // more dots behind an increasingly small diagram.
28
+ const grid = 24 * Math.max(scale, 1 / Math.sqrt(scale));
29
+ viewport.current.style.backgroundSize = `${grid}px ${grid}px`;
30
+ viewport.current.style.setProperty("--diagram-grid-opacity", String(0.45 * Math.min(1, Math.sqrt(scale))));
31
+ viewport.current.style.setProperty("--diagram-grid-weight", String(Math.max(0.5, Math.min(0.75, Math.sqrt(scale)))));
23
32
  }
24
33
  setZoom(Math.round(scale * 100));
25
34
  });
@@ -28,9 +37,26 @@ export function useDiagramViewport(width, height, scene) {
28
37
  fitted.current = true;
29
38
  if (size.current.width <= 0 || size.current.height <= 0)
30
39
  return;
31
- camera.current = fitCamera(size.current, { width, height });
40
+ camera.current = fitCamera(size.current, bounds.current);
32
41
  paint();
33
- }, [width, height, paint]);
42
+ }, [paint]);
43
+ const preserveCamera = useCallback(() => {
44
+ fitted.current = false;
45
+ }, []);
46
+ const focusArea = useCallback((area) => {
47
+ fitted.current = false;
48
+ const focused = fitCamera(size.current, area);
49
+ camera.current = {
50
+ ...focused,
51
+ x: focused.x - area.x * focused.scale,
52
+ y: focused.y - area.y * focused.scale,
53
+ };
54
+ paint();
55
+ }, [paint]);
56
+ useEffect(() => {
57
+ if (fitted.current)
58
+ fit();
59
+ }, [width, height, fit]);
34
60
  const zoomBy = useCallback((factor, point) => {
35
61
  fitted.current = false;
36
62
  camera.current = zoomCamera(camera.current, factor, point ?? { x: size.current.width / 2, y: size.current.height / 2 });
@@ -65,7 +91,7 @@ export function useDiagramViewport(width, height, scene) {
65
91
  const down = (event) => {
66
92
  if (event.button !== 0 ||
67
93
  pointers.size >= 2 ||
68
- event.target.closest("button, input, select, textarea, a, summary"))
94
+ event.target.closest("button, input, select, textarea, a, summary, [data-diagram-scroll]"))
69
95
  return;
70
96
  event.preventDefault();
71
97
  el.focus({ preventScroll: true });
@@ -102,6 +128,8 @@ export function useDiagramViewport(width, height, scene) {
102
128
  delete el.dataset.panning;
103
129
  };
104
130
  const wheel = (event) => {
131
+ if (event.target.closest("[data-diagram-scroll]"))
132
+ return;
105
133
  event.preventDefault();
106
134
  const units = event.deltaMode === 1
107
135
  ? 16
@@ -168,5 +196,5 @@ export function useDiagramViewport(width, height, scene) {
168
196
  frame.current = null;
169
197
  };
170
198
  }, [fit, zoomBy, paint, scene]);
171
- return { viewport, canvas, zoom, zoomBy, fit };
199
+ return { viewport, canvas, zoom, zoomBy, fit, preserveCamera, focusArea };
172
200
  }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "jev-devtools",
3
- "version": "0.1.3",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.12"
7
7
  },
8
8
  "dependencies": {
9
+ "@tanstack/react-virtual": "3.14.22",
9
10
  "@typesafe-ai/sdk": "0.7.0"
10
11
  },
11
12
  "description": "A React drawer and Node server adapter for inspecting TypeSafe Jev requests.",
@@ -18,7 +19,11 @@
18
19
  "types": "./lib/server/index.d.ts",
19
20
  "import": "./lib/server/index.js"
20
21
  },
21
- "./styles.css": "./lib/src/inspector.css"
22
+ "./styles.css": "./lib/src/inspector.css",
23
+ "./flow": {
24
+ "types": "./lib/flow/index.d.ts",
25
+ "import": "./lib/flow/index.js"
26
+ }
22
27
  },
23
28
  "files": [
24
29
  "lib",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: setup-jev-devtools
3
- description: Install and verify Jev DevTools in an existing React application with a Node backend or Next.js App Router. Use when asked to add the Jev request inspector, wire capture, or troubleshoot its setup.
3
+ description: Install and verify Jev DevTools in an existing React application with a Node backend or Next.js App Router. Use when asked to add the Jev request inspector, wire capture, build connected Jev flows with branches and parallel checks, or troubleshoot its setup.
4
4
  ---
5
5
 
6
6
  # Set up Jev DevTools
@@ -17,9 +17,13 @@ Inspect the app's package manager/lockfile, React and Node versions, root compon
17
17
 
18
18
  Install `jev-devtools` using the app's existing package manager (npm: `npm install jev-devtools`; pnpm: `pnpm add jev-devtools`; Yarn: `yarn add jev-devtools`). Install in the workspace(s) that actually import it. It is a regular dependency when application modules import it, even though capture/display are development-only. React 18/19 and Node 22.12+ are required. Do not replace the app's framework or upgrade unrelated dependencies to force compatibility.
19
19
 
20
+ ## Optional connected flow API (0.3.0+)
21
+
22
+ When asked to build or inspect connected calls, read [references/flows.md](references/flows.md) after the framework recipe. It includes an executable parallel-checks/branch example, step titles/descriptions, scope and execution rules, sample verification, and privacy limits. Preserve existing application policies; do not convert unrelated calls just to install the drawer. Flow execution is server-only and continues even with the inspector disabled.
23
+
20
24
  ## Integration invariants
21
25
 
22
- - Use only public imports: `jev-devtools`, `jev-devtools/styles.css`, and server-only `jev-devtools/server`. No provider, TanStack, Tailwind, or shadcn installation is needed.
26
+ - Use only public imports: `jev-devtools`, `jev-devtools/styles.css`, and server-only `jev-devtools/server` and `jev-devtools/flow`. No provider, TanStack DevTools shell, Tailwind, or shadcn setup is needed; TanStack Virtual is an internal dependency.
23
27
  - Mount `<JevDevtools />` once with an explicit development flag and import the CSS. Server capture must also be explicitly enabled for development; both default to off.
24
28
  - Create a shared adapter; the inspector routes and application client must use it in the same Node realm/process. Replace existing `new TypeSafeClient(options)` construction with `inspector.createClient(options)` while preserving keys, transport options, retries, and call semantics. Keep existing `systemOne(...)` inputs and application behavior.
25
29
  - Browser and server `endpoint` paths must agree (default `/api/jev-devtools`). Route the base path and its subpaths. The server `origin` must exactly match the browser scheme, hostname, and port. A browser endpoint is a same-origin path, not an absolute backend URL.
@@ -0,0 +1,150 @@
1
+ # Build and inspect a Jev flow
2
+
3
+ For `jev-devtools` 0.3.0 and later. Read the installed package version and `lib/flow/index.d.ts` first; online `main` may describe newer APIs. Use this recipe when the user asks to connect multiple Jev requests, inspect branches, or visualize parallel checks. Adding the inspector alone does not require converting existing calls to flows.
4
+
5
+ ## Plan the integration
6
+
7
+ 1. Complete the existing [React/Node](react-node.md) or [Next.js](nextjs.md) setup. Reuse its shared inspector and instrumented client; do not create a second recorder for flows.
8
+ 2. Identify the input shape, questions, execution order, decision thresholds, parallel dependencies, and named outcomes in the user's application. Preserve existing policies. If a threshold or action is unspecified, ask rather than inventing it.
9
+ 3. Import `defineJevFlow` (and `all`, `any`, `not` when needed) from **server-only** `jev-devtools/flow`. Define the graph once at module scope and call `run()` from the existing Node handler/action. In Next.js, mark the module `server-only` and keep the executing route/segment on the Node runtime. No new client provider or root component is needed.
10
+ 4. Keep business actions such as refunds, notifications, and database writes in the application after the returned outcome. A named output does not execute an action. Preserve authorization, idempotency, input validation, and error handling around those actions.
11
+
12
+ If importing question helpers as below, install `@typesafe-ai/sdk` as a direct dependency in the server workspace using its package manager. The 0.3.0 release is verified with SDK 0.7.0; preserve a compatible existing version rather than silently changing SDK semantics. The flow also accepts ordinary SDK question objects.
13
+
14
+ ## Complete example: parallel checks, one decision, two outcomes
15
+
16
+ The 90% threshold here is an illustrative application policy, not a Jev default. The example's `inspector` and `jev` come from the shared server setup above.
17
+
18
+ ```ts
19
+ import { noul } from '@typesafe-ai/sdk';
20
+ import { defineJevFlow } from 'jev-devtools/flow';
21
+
22
+ type Ticket = { message: string };
23
+
24
+ export const reviewFlow = defineJevFlow<Ticket>('ticket-review', (flow) => {
25
+ const checks = flow.parallel('checks', {
26
+ eligibility: (lane) => lane.jev('eligibility', {
27
+ state: flow.input,
28
+ questions: { eligible: noul('Is this ticket eligible for review?') },
29
+ }, {
30
+ title: 'Check eligibility',
31
+ description: 'Evaluate whether this ticket can enter the review process.',
32
+ }).answer('eligible').noul,
33
+ risk: (lane) => lane.jev('risk', {
34
+ state: flow.input,
35
+ questions: { risky: noul('Does this ticket need extra risk review?') },
36
+ }, {
37
+ title: 'Check risk',
38
+ description: 'Evaluate risk independently of eligibility.',
39
+ }).answer('risky').noul,
40
+ }, {
41
+ title: 'Wait for both checks',
42
+ description: 'Continue after eligibility and risk have both succeeded.',
43
+ });
44
+
45
+ const decision = flow.jev('decision', {
46
+ state: { ticket: flow.input, eligibility: checks.eligibility, risk: checks.risk },
47
+ questions: { ready: noul('Is this ticket ready for approval?') },
48
+ }, {
49
+ title: 'Assess readiness',
50
+ description: 'Ask Jev to assess the ticket using both completed checks.',
51
+ });
52
+
53
+ return flow.branch('readiness', {
54
+ when: decision.answer('ready').noul.gte(0.9),
55
+ then: (path) => path.output('ready', {
56
+ readiness: decision.answer('ready').noul,
57
+ }, {
58
+ title: 'Ready for approval',
59
+ description: 'Return readiness to the application. No approval is executed here.',
60
+ }),
61
+ else: (path) => path.output('manual-review', undefined, {
62
+ title: 'Needs manual review',
63
+ description: 'Readiness is below the application threshold.',
64
+ }),
65
+ }, {
66
+ title: 'Require at least 90% readiness',
67
+ description: 'Use the returned readiness probability to choose a route.',
68
+ });
69
+ });
70
+ ```
71
+
72
+ Invoke from your existing server handler after validating the input:
73
+
74
+ ```ts
75
+ const result = await reviewFlow.run({ message: 'I was charged twice.' }, {
76
+ client: jev, // Created by this same inspector.
77
+ devtools: inspector,
78
+ signal: request.signal, // Optional AbortSignal when the framework provides one.
79
+ });
80
+ // { runId, outcome: 'ready', value: { readiness: 0.94 } }
81
+ // Or { runId, outcome: 'manual-review', value: undefined }.
82
+ ```
83
+
84
+ For custom server-derived sessions, use `await inspector.run(request, () => reviewFlow.run(input, { client: jev, devtools: inspector, signal: request.signal }))`. This ensures the run and its calls use the same session as the authorized inspector connection. Do not accept an arbitrary client-supplied session ID.
85
+
86
+ ## API rules that affect generated code
87
+
88
+ | API | Contract |
89
+ | --- | --- |
90
+ | `defineJevFlow<Input>(name, build)` | Synchronous definition; no requests run while building. Return `flow.output(...)` or `flow.branch(...)`. Each `run` is independent. |
91
+ | `flow.jev(id, request, display?)` | One SDK `systemOne` call when reached. Sequential within its block. Use normal SDK questions/options/assets; references resolve in state. |
92
+ | `flow.input.get('field')` | A typed expression referencing the run input, not a value available during definition. |
93
+ | `result.answer('name')` | Typed answer fields: `.choice`, `.probabilities.get('label')`, `.confidence`, `.noul`, `.score`, `.legend`, or `.box` as supported by that question's SDK result. Select nested fields with `.get(key)`. |
94
+ | `value.eq(other)`, `.lt(number)`, `.gte(number)` | Matching scalar equality or finite numeric comparisons. Noul values are in 0–1; use `0.9`, not `90`. There is no `.gt()` or `.lte()` method. |
95
+ | `all(...)`, `any(...)`, `not(...)` | Compose Boolean expressions. Every operand evaluates; missing values are errors even in an otherwise short-circuited expression. Do not use JavaScript `if`, `&&`, or `>=` on expression objects. |
96
+ | `flow.branch(id, { when, then, else }, display?)` | Terminal in that block. Both callbacks define paths; only one executes. Each callback must return its own builder's `output` or nested `branch`. No common continuation after a branch in v1. |
97
+ | `flow.parallel(id, lanes, display?)` | Named lanes run concurrently. Each lane returns an expression. Use the resulting named references after all lanes succeed. Lane-local references cannot escape directly or cross into a sibling lane. |
98
+ | `flow.output(name, value?, display?)` | Terminal named result, optionally containing nested resolved expressions. Output names are globally unique node IDs. For metadata without a value, pass `undefined` second. |
99
+ | `definition.run(input, { client, devtools?, signal? })` | Returns `{ runId, outcome: string, value?: unknown }`. Narrow/validate values in application code; outcomes are not inferred as a discriminated union. A normal SDK client works without recording. |
100
+
101
+ Use the callback's current builder (`lane` or `path`) to add steps; do not call the outer builder inside it. References must belong to the same definition and be available on the current path. Reusing an earlier parent reference is allowed. IDs must be unique across all paths/lanes: 1–80 letters, digits, `_`, or `-`, starting with a letter or digit. Maximum 256 nodes and 16 nested blocks; no loops or arbitrary execution callbacks. Plain templates are copied when defining; treat SDK assets as immutable. Define once to avoid a new definition identity on every request; hot reload can still create a new identity.
102
+
103
+ SDK retries remain within each request; the flow adds no retries. A failed step blocks dependents and rejects the run. A failed lane aborts siblings and waits for them to settle. External cancellation propagates to SDK signals; already completed calls stay completed. Handle rejection in the app; do not retry a whole flow automatically or imply durable/resumable execution.
104
+
105
+ ## Diagram context and privacy
106
+
107
+ Every builder method accepts a third `{ title, description }` argument (`FlowDisplay`). Title: 1–120 characters; description: 1–600; supplied text must be nonblank. Titles change display text, not IDs, outcomes, questions, or execution. These are static developer-authored explanations, not model reasoning.
108
+
109
+ Titles/descriptions, flow names, step IDs, lane names and outcome names are structural metadata and are **not request-redacted**. Keep them static and public-safe. Request state/answers and output values use capture limits/redaction. Rule literals also use the configured redaction callback with paths beginning `['conditions', stepId]` and ending in `'value'`; preserve the app's redaction policy. Redaction never changes execution.
110
+
111
+ The condition card separates incoming captured answers from requirements, e.g. **Ready: 94% → At least 90%**. Its result comes from the actual recorded decision, not a fresh evaluation of redacted data. Evicted/missing answers and flow-input/parallel aggregate references show **Not captured**. Do not promise that every runtime value is retained. Full technical rules appear in selected-step details.
112
+
113
+ History is bounded in process (default 50 runs, maximum 200, independently from request history). Capture and display default off; disabled recording does not disable flow execution. Keep flows server-side in production too if application behavior depends on them. This is not a persistent or distributed workflow engine, and large flow canvases are not virtualized.
114
+
115
+ ## Verify without inference
116
+
117
+ Use a **separate, visibly labeled sample client** from the same inspector. Do not replace the user's live client or routes. With the `reviewFlow` above, this fixture exercises the actual SDK and flow executor:
118
+
119
+ ```ts
120
+ const sampleJev = inspector.createClient({
121
+ apiKey: 'sample-only',
122
+ logLevel: 'off',
123
+ fetch: async (_url, init) => {
124
+ const body = JSON.parse(String(init?.body));
125
+ const answers = Object.fromEntries(Object.keys(body.questions).map((name) => [
126
+ name, { type: 'noul', noul: name === 'risky' ? 0.1 : 0.94 },
127
+ ]));
128
+ return Response.json({ model: 'jev-sample', answers });
129
+ },
130
+ }, { label: 'Flow setup verification', source: 'sample' });
131
+
132
+ const result = await reviewFlow.run({ message: 'Sample ticket' }, {
133
+ client: sampleJev,
134
+ devtools: inspector,
135
+ });
136
+ // Expect outcome 'ready', value.readiness 0.94, and three captured calls.
137
+ ```
138
+
139
+ Run this inside the application process serving the inspector, using a temporary development-only handler or the app's existing harness. A separate CLI has separate history. Mount the same root drawer; the existing base route must forward `/flows/events` as well as `/events` and `/clear`. The Next optional catch-all already does this.
140
+
141
+ Check these observable results:
142
+
143
+ - The Flows view shows two parallel requests, their join, one downstream readiness request, a condition and two possible outcomes. Three real SDK calls use the local fixture; no inference network call occurs.
144
+ - The join waits for both lanes. The chosen output is `ready`; `manual-review` is marked untaken. Incoming readiness is 94%, the requirement is at least 90%, and each request opens its captured details.
145
+ - Change only the fixture's `ready` probability to `0.4`: the other outcome executes. Missing values must not silently become zero. Use the app's error/cancellation harness if it has one.
146
+ - App typecheck/build passes, `/flow` and `/server` stay out of client bundles, and production disables inspector endpoints/display while the application's flow still executes. Do not call live inference just to verify this.
147
+
148
+ If the flow executes but details are absent, check: `devtools` passed to `run`, `client` from that same adapter, same process/session, capture enabled, `/flows/events` forwarded, and histories not cleared/evicted. A plain SDK client can produce flow states but cannot link request details. See [verify.md](verify.md) for general endpoint checks.
149
+
150
+ Remove temporary verification routes unless requested as a demo. Report the installed version, integration location, outcomes observed, whether browser checks were performed, and that responses were samples. Never equate sample verification with live-model correctness.
@@ -49,7 +49,7 @@ export const GET = (request: Request) => inspector.handleNodeRoute(request);
49
49
  export const DELETE = (request: Request) => inspector.handleNodeRoute(request);
50
50
  ```
51
51
 
52
- Use the **optional** catch-all `[[...path]]`, which covers config at the base path as well as `/events` and `/clear`. Keep streams out of caches/proxy buffering. Match Host to the configured origin; `localhost` and `127.0.0.1` differ. For Next `basePath` or a custom endpoint, align server `endpoint`, client `endpoint`, and the externally visible route path.
52
+ Use the **optional** catch-all `[[...path]]`, which covers config at the base path as well as `/events`, `/flows/events`, and `/clear`. Keep streams out of caches/proxy buffering. Match Host to the configured origin; `localhost` and `127.0.0.1` differ. For Next `basePath` or a custom endpoint, align server `endpoint`, client `endpoint`, and the externally visible route path.
53
53
 
54
54
  ## Root client boundary and CSS
55
55
 
@@ -78,4 +78,6 @@ Report HTTP/capture checks separately from visual browser checks, and sample ver
78
78
  | Server import/build errors | Keep `/server` outside client modules. In Next externalize the SDK only. Use a supported Node runtime. |
79
79
  | Production 404 | Expected when disabled; app inference remains independent. |
80
80
 
81
- Capture is bounded to 50 calls in memory by default. Built-in redaction has limits; arbitrary secrets in prose are not detected. Diagrams show captured inputs/outputs, not hidden model reasoning or application routing decisions.
81
+ Capture is bounded to 50 calls in memory by default. Built-in redaction has limits; arbitrary secrets in prose are not detected. Diagrams show captured inputs/outputs, not hidden model reasoning. Routing decisions are recorded only for explicitly defined flows; see [flows.md](flows.md) for flow verification.
82
+
83
+ The current browser requests `/events?protocol=2`: the first event is a full version 2 `snapshot`, followed by `delta` events containing changed traces and the retained ID order. The unqualified `/events` check above intentionally remains compatible with legacy clients. Reconnecting always supplies a fresh snapshot.