jev-devtools 0.2.0 → 0.4.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.
- package/README.md +50 -6
- package/lib/flow/index.d.ts +143 -0
- package/lib/flow/index.js +1029 -0
- package/lib/flow/runtime.d.ts +5 -0
- package/lib/flow/runtime.js +81 -0
- package/lib/server/flows.d.ts +28 -0
- package/lib/server/flows.js +187 -0
- package/lib/server/index.d.ts +14 -1
- package/lib/server/index.js +100 -1
- package/lib/server/recorder.d.ts +4 -1
- package/lib/server/recorder.js +19 -3
- package/lib/src/ChoiceProbabilities.js +1 -1
- package/lib/src/ConnectedInspector.js +13 -4
- package/lib/src/DiagramContent.d.ts +6 -0
- package/lib/src/DiagramContent.js +32 -0
- package/lib/src/FlowDetailsPanel.d.ts +8 -0
- package/lib/src/FlowDetailsPanel.js +79 -0
- package/lib/src/FlowOverview.d.ts +8 -0
- package/lib/src/FlowOverview.js +440 -0
- package/lib/src/JevDevtools.d.ts +6 -1
- package/lib/src/JevDevtools.js +18 -4
- package/lib/src/TraceDiagram.d.ts +4 -1
- package/lib/src/TraceDiagram.js +30 -12
- package/lib/src/condition-inputs.d.ts +9 -0
- package/lib/src/condition-inputs.js +109 -0
- package/lib/src/flow-condition.d.ts +4 -0
- package/lib/src/flow-condition.js +48 -0
- package/lib/src/flow-model.d.ts +7 -2
- package/lib/src/flow-model.js +2 -11
- package/lib/src/flow-protocol.d.ts +93 -0
- package/lib/src/flow-protocol.js +1 -0
- package/lib/src/flow-stream.d.ts +8 -0
- package/lib/src/flow-stream.js +80 -0
- package/lib/src/inspector-styles.js +1 -1
- package/lib/src/inspector.css +746 -8
- package/lib/src/protocol.d.ts +5 -0
- package/lib/src/useDiagramViewport.d.ts +5 -1
- package/lib/src/useDiagramViewport.js +31 -9
- package/package.json +11 -4
- package/skills/setup-jev-devtools/SKILL.md +8 -4
- package/skills/setup-jev-devtools/references/flows.md +227 -0
- package/skills/setup-jev-devtools/references/nextjs.md +3 -1
- package/skills/setup-jev-devtools/references/react-node.md +2 -2
- package/skills/setup-jev-devtools/references/verify.md +1 -1
package/lib/src/protocol.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { type Point } from "./diagram-camera.js";
|
|
2
2
|
/** Lab-style camera. Pointer frames update the transform without rerendering cards. */
|
|
3
|
-
export declare function useDiagramViewport(width: number, height: number, scene: string): {
|
|
3
|
+
export declare function useDiagramViewport(width: number, height: number, scene: string, origin?: Point): {
|
|
4
4
|
viewport: import("react").RefObject<HTMLDivElement | null>;
|
|
5
5
|
canvas: import("react").RefObject<HTMLDivElement | null>;
|
|
6
6
|
zoom: number;
|
|
7
7
|
zoomBy: (factor: number, point?: Point) => void;
|
|
8
8
|
fit: () => void;
|
|
9
9
|
preserveCamera: () => void;
|
|
10
|
+
focusArea: (area: Point & {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
}) => void;
|
|
10
14
|
};
|
|
@@ -1,16 +1,16 @@
|
|
|
1
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
|
-
export function useDiagramViewport(width, height, scene) {
|
|
4
|
+
export function useDiagramViewport(width, height, scene, origin = { x: 0, y: 0 }) {
|
|
5
5
|
const viewport = useRef(null);
|
|
6
6
|
const canvas = useRef(null);
|
|
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 });
|
|
10
|
+
const bounds = useRef({ width, height, ...origin });
|
|
11
11
|
useLayoutEffect(() => {
|
|
12
|
-
bounds.current = { width, height };
|
|
13
|
-
}, [width, height]);
|
|
12
|
+
bounds.current = { width, height, x: origin.x, y: origin.y };
|
|
13
|
+
}, [width, height, origin.x, origin.y]);
|
|
14
14
|
const frame = useRef(null);
|
|
15
15
|
const [zoom, setZoom] = useState(100);
|
|
16
16
|
const paint = useCallback(() => {
|
|
@@ -23,7 +23,12 @@ export function useDiagramViewport(width, height, scene) {
|
|
|
23
23
|
canvas.current.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;
|
|
24
24
|
if (viewport.current) {
|
|
25
25
|
viewport.current.style.backgroundPosition = `${x}px ${y}px`;
|
|
26
|
-
|
|
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)))));
|
|
27
32
|
}
|
|
28
33
|
setZoom(Math.round(scale * 100));
|
|
29
34
|
});
|
|
@@ -32,16 +37,31 @@ export function useDiagramViewport(width, height, scene) {
|
|
|
32
37
|
fitted.current = true;
|
|
33
38
|
if (size.current.width <= 0 || size.current.height <= 0)
|
|
34
39
|
return;
|
|
35
|
-
|
|
40
|
+
const fittedCamera = fitCamera(size.current, bounds.current);
|
|
41
|
+
camera.current = {
|
|
42
|
+
...fittedCamera,
|
|
43
|
+
x: fittedCamera.x - bounds.current.x * fittedCamera.scale,
|
|
44
|
+
y: fittedCamera.y - bounds.current.y * fittedCamera.scale,
|
|
45
|
+
};
|
|
36
46
|
paint();
|
|
37
47
|
}, [paint]);
|
|
38
48
|
const preserveCamera = useCallback(() => {
|
|
39
49
|
fitted.current = false;
|
|
40
50
|
}, []);
|
|
51
|
+
const focusArea = useCallback((area) => {
|
|
52
|
+
fitted.current = false;
|
|
53
|
+
const focused = fitCamera(size.current, area);
|
|
54
|
+
camera.current = {
|
|
55
|
+
...focused,
|
|
56
|
+
x: focused.x - area.x * focused.scale,
|
|
57
|
+
y: focused.y - area.y * focused.scale,
|
|
58
|
+
};
|
|
59
|
+
paint();
|
|
60
|
+
}, [paint]);
|
|
41
61
|
useEffect(() => {
|
|
42
62
|
if (fitted.current)
|
|
43
63
|
fit();
|
|
44
|
-
}, [width, height, fit]);
|
|
64
|
+
}, [width, height, origin.x, origin.y, fit]);
|
|
45
65
|
const zoomBy = useCallback((factor, point) => {
|
|
46
66
|
fitted.current = false;
|
|
47
67
|
camera.current = zoomCamera(camera.current, factor, point ?? { x: size.current.width / 2, y: size.current.height / 2 });
|
|
@@ -76,7 +96,7 @@ export function useDiagramViewport(width, height, scene) {
|
|
|
76
96
|
const down = (event) => {
|
|
77
97
|
if (event.button !== 0 ||
|
|
78
98
|
pointers.size >= 2 ||
|
|
79
|
-
event.target.closest("button, input, select, textarea, a, summary"))
|
|
99
|
+
event.target.closest("button, input, select, textarea, a, summary, [data-diagram-scroll]"))
|
|
80
100
|
return;
|
|
81
101
|
event.preventDefault();
|
|
82
102
|
el.focus({ preventScroll: true });
|
|
@@ -113,6 +133,8 @@ export function useDiagramViewport(width, height, scene) {
|
|
|
113
133
|
delete el.dataset.panning;
|
|
114
134
|
};
|
|
115
135
|
const wheel = (event) => {
|
|
136
|
+
if (event.target.closest("[data-diagram-scroll]"))
|
|
137
|
+
return;
|
|
116
138
|
event.preventDefault();
|
|
117
139
|
const units = event.deltaMode === 1
|
|
118
140
|
? 16
|
|
@@ -179,5 +201,5 @@ export function useDiagramViewport(width, height, scene) {
|
|
|
179
201
|
frame.current = null;
|
|
180
202
|
};
|
|
181
203
|
}, [fit, zoomBy, paint, scene]);
|
|
182
|
-
return { viewport, canvas, zoom, zoomBy, fit, preserveCamera };
|
|
204
|
+
return { viewport, canvas, zoom, zoomBy, fit, preserveCamera, focusArea };
|
|
183
205
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jev-devtools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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"
|
|
10
|
-
"@typesafe-ai/sdk": "0.7.0"
|
|
9
|
+
"@tanstack/react-virtual": "3.14.22"
|
|
11
10
|
},
|
|
12
11
|
"description": "A React drawer and Node server adapter for inspecting TypeSafe Jev requests.",
|
|
13
12
|
"exports": {
|
|
@@ -19,7 +18,11 @@
|
|
|
19
18
|
"types": "./lib/server/index.d.ts",
|
|
20
19
|
"import": "./lib/server/index.js"
|
|
21
20
|
},
|
|
22
|
-
"./styles.css": "./lib/src/inspector.css"
|
|
21
|
+
"./styles.css": "./lib/src/inspector.css",
|
|
22
|
+
"./flow": {
|
|
23
|
+
"types": "./lib/flow/index.d.ts",
|
|
24
|
+
"import": "./lib/flow/index.js"
|
|
25
|
+
}
|
|
23
26
|
},
|
|
24
27
|
"files": [
|
|
25
28
|
"lib",
|
|
@@ -32,9 +35,13 @@
|
|
|
32
35
|
"**/*.css"
|
|
33
36
|
],
|
|
34
37
|
"peerDependencies": {
|
|
38
|
+
"@typesafe-ai/sdk": ">=0.7.0 <0.8.0",
|
|
35
39
|
"react": ">=18 <20",
|
|
36
40
|
"react-dom": ">=18 <20"
|
|
37
41
|
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@typesafe-ai/sdk": "0.7.0"
|
|
44
|
+
},
|
|
38
45
|
"license": "MIT",
|
|
39
46
|
"repository": {
|
|
40
47
|
"type": "git",
|
|
@@ -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 tasks, branches, collections and loops, or troubleshoot its setup.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Set up Jev DevTools
|
|
@@ -15,16 +15,20 @@ Inspect the app's package manager/lockfile, React and Node versions, root compon
|
|
|
15
15
|
- **React with a Node backend, including Vite:** read [references/react-node.md](references/react-node.md). A browser-only app needs a Node server for Jev; the Vite development-server option is local only.
|
|
16
16
|
- **Other Node frameworks:** use the Web Request/Response or middleware API described in the React/Node reference and verify that framework's routing. Do not assume the Next.js recipe applies to Pages Router. Edge runtimes and cross-process/serverless history aggregation are unsupported.
|
|
17
17
|
|
|
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.
|
|
18
|
+
Install `jev-devtools` using the app's existing package manager (npm: `npm install jev-devtools @typesafe-ai/sdk`; pnpm: `pnpm add jev-devtools @typesafe-ai/sdk`; Yarn: `yarn add jev-devtools @typesafe-ai/sdk`). `@typesafe-ai/sdk` is a peer dependency: keep the app's existing SDK version when it satisfies the peer range. 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
|
+
|
|
20
|
+
## Optional connected flow API (0.4.0)
|
|
21
|
+
|
|
22
|
+
When asked to build or inspect connected calls, read [references/flows.md](references/flows.md) after the framework recipe. It covers the 0.4.0 API: requests, server function tasks, runtime request factories, parallel checks, terminal branches, returning conditionals, bounded collections, and stateful loops. It includes sample verification, scope/execution rules, and privacy limits. For an older installation, use its bundled recipe and declarations rather than assuming these operations exist. 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.
|
|
19
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
|
|
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.
|
|
26
30
|
- Keep keys in server environment variables, never `VITE_*` or `NEXT_PUBLIC_*`. Use the host's environment loader and an ignored local env file. If a key is unavailable, complete setup and use the clearly labeled sample verification in [references/verify.md](references/verify.md); report that live inference remains unverified.
|
|
27
|
-
- Preserve authentication and project redaction. Shared/non-loopback access requires a real `authorize(request)` check; do not bypass it with `() => true`. Keep local defaults unless shared access is requested. Separate processes require a different architecture, not extra adapter instances.
|
|
31
|
+
- Preserve authentication and project redaction. Shared/non-loopback access requires a real `authorize(request)` check; do not bypass it with `() => true`. Keep local defaults unless shared access is requested. Without `authorize`, Node middleware accepts only loopback connections; Web Request routes can't check the connection, so their dev server must listen on `127.0.0.1` or set a real `authorize`. Separate processes require a different architecture, not extra adapter instances.
|
|
28
32
|
|
|
29
33
|
## Finish and verify
|
|
30
34
|
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Build and inspect a Jev flow
|
|
2
|
+
|
|
3
|
+
For `jev-devtools` **0.4.0**. 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, run server function steps, or visualize parallel checks, collections, and loops. 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. A named output returns data and does not execute an action. Existing business actions can remain in the application after the returned outcome; use `task()` when the user wants application-owned server work included in the flow. Preserve authorization, idempotency, input validation, and error handling. Cancellation cannot undo side effects.
|
|
11
|
+
|
|
12
|
+
If importing question helpers as below, install `@typesafe-ai/sdk` as a direct dependency in the server workspace using its package manager. Version 0.4.0 requires SDK `>=0.7.0 <0.8.0` and is verified with 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.jev(id, { input, request }, display?)` | Resolve input references, then synchronously build the SDK request at execution time. Use a preceding task for asynchronous preparation. |
|
|
93
|
+
| `flow.task(id, { input, run, timeoutMs? }, display?)` | Run server code with resolved JSON-compatible input and return a typed result reference. Cancellation is cooperative; no automatic task retry. |
|
|
94
|
+
| `flow.if(id, { when, then, else }, display?)` | Only the selected path executes. Each arm returns an expression; the merged result can feed shared downstream steps. |
|
|
95
|
+
| `flow.map(id, { items, input, concurrency?, maxItems }, buildItem, display?)` | Run a bounded item program over an array; preserve result order. Pass outer values through `input`; return an expression from the item builder. |
|
|
96
|
+
| `flow.loop(id, { initial, input, while, maxIterations }, buildIteration, display?)` | Check a state expression before each iteration; return a same-type state expression from the body. Reject unfinished limit exhaustion with `FlowLoopLimitError`. |
|
|
97
|
+
| `flow.input.get('field')` | A typed expression referencing the run input, not a value available during definition. |
|
|
98
|
+
| `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)`. |
|
|
99
|
+
| `value.eq(other)`, `.lt(number)`, `.gt(number)`, `.gte(number)` | Matching scalar equality or finite numeric comparisons. Noul values are in 0–1; use `0.9`, not `90`. There is no `.lte()` method. |
|
|
100
|
+
| `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. |
|
|
101
|
+
| `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`. Use `flow.if()` for value-returning paths with common continuation. |
|
|
102
|
+
| `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. |
|
|
103
|
+
| `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. |
|
|
104
|
+
| `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. |
|
|
105
|
+
|
|
106
|
+
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 including collection/iteration templates, 16 nested blocks, and 1,024 expanded steps per run. Version 0.4.0 includes tasks, factories, maps, returning conditionals and loops as described below. 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.
|
|
107
|
+
|
|
108
|
+
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.
|
|
109
|
+
|
|
110
|
+
## Diagram context and privacy
|
|
111
|
+
|
|
112
|
+
Every builder method accepts a final `{ title, description }` argument (`FlowDisplay`): third for `jev`, `task`, `if`, `branch`, `parallel`, and `output`; fourth for `map` and `loop`. 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.
|
|
113
|
+
|
|
114
|
+
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.
|
|
115
|
+
|
|
116
|
+
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: select a card, then open **Show details** in the bottom bar. This panel starts collapsed and supports vertical pointer/keyboard resizing; selection changes preserve its size.
|
|
117
|
+
|
|
118
|
+
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.
|
|
119
|
+
|
|
120
|
+
## Verify without inference
|
|
121
|
+
|
|
122
|
+
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:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import * as zlib from 'node:zlib';
|
|
126
|
+
|
|
127
|
+
const sampleJev = inspector.createClient({
|
|
128
|
+
apiKey: 'sample-only',
|
|
129
|
+
logLevel: 'off',
|
|
130
|
+
fetch: async (_url, init) => {
|
|
131
|
+
const bytes = new Uint8Array(await new Response(init?.body).arrayBuffer());
|
|
132
|
+
const encoding = new Headers(init?.headers).get('content-encoding');
|
|
133
|
+
const body = JSON.parse(
|
|
134
|
+
encoding === 'zstd' ? zlib.zstdDecompressSync(bytes).toString()
|
|
135
|
+
: encoding === 'gzip' ? zlib.gunzipSync(bytes).toString()
|
|
136
|
+
: new TextDecoder().decode(bytes),
|
|
137
|
+
);
|
|
138
|
+
const answers = Object.fromEntries(Object.keys(body.questions).map((name) => [
|
|
139
|
+
name, { type: 'noul', noul: name === 'risky' ? 0.1 : 0.94 },
|
|
140
|
+
]));
|
|
141
|
+
return Response.json({ model: 'jev-sample', answers });
|
|
142
|
+
},
|
|
143
|
+
}, { label: 'Flow setup verification', source: 'sample' });
|
|
144
|
+
|
|
145
|
+
const result = await reviewFlow.run({ message: 'Sample ticket' }, {
|
|
146
|
+
client: sampleJev,
|
|
147
|
+
devtools: inspector,
|
|
148
|
+
});
|
|
149
|
+
// Expect outcome 'ready', value.readiness 0.94, and three captured calls.
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
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.
|
|
153
|
+
|
|
154
|
+
Check these observable results:
|
|
155
|
+
|
|
156
|
+
- 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.
|
|
157
|
+
- 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.
|
|
158
|
+
- 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.
|
|
159
|
+
- 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.
|
|
160
|
+
|
|
161
|
+
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.
|
|
162
|
+
|
|
163
|
+
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.
|
|
164
|
+
|
|
165
|
+
## Function steps and runtime requests (0.4.0)
|
|
166
|
+
|
|
167
|
+
Use `task()` for application-owned server work and the request-factory overload of `jev()` when later questions depend on earlier results. Both are part of 0.4.0.
|
|
168
|
+
|
|
169
|
+
- `flow.task(id, { input, run, timeoutMs? }, display?)`: resolve nested input references, run server code, and return a typed reference. `run(input, { signal, runId, stepId, executionId })` may return a value or Promise. Inputs/outputs must be plain JSON-compatible values; return null rather than undefined. No implicit retry.
|
|
170
|
+
- `flow.jev(id, { input, request: resolved => ({ state, questions, ...sdkOptions }) }, display?)`: construct a synchronous SDK request at execution time. Do asynchronous work in a preceding task. Keep Jev calls here to retain full linked diagrams. Never return unresolved flow references or a Promise from the factory.
|
|
171
|
+
- Every Jev step also has `.answers`, a typed reference to the complete answer map. Pass it to a task when a request factory generates question names dynamically; do not create new `.answer()` references inside a runtime callback.
|
|
172
|
+
- Neither callback executes at definition time. Runtime input snapshots protect previous values from mutation. Task timeouts/cancellation stop downstream work and signal the callback, but cannot undo side effects or forcibly interrupt synchronous code.
|
|
173
|
+
- Captures show function inputs/outputs under `steps.<id>.data.input/output` redaction paths, with warnings for truncation. Only error names are exposed. Definition descriptions remain static public-safe metadata. Remote function calls are ordinary server fetches, not code serialized to another machine.
|
|
174
|
+
- For verification, follow actual answers into a task and a subsequent generated request; confirm the latter's criteria and trace link, then inspect redacted task data. Use the synthetic playground Functions scenario for a no-key example.
|
|
175
|
+
- Use `map()` for bounded collections and `loop()` for stateful iteration. There is no `flow.repeat`, distributed worker API, or durable job store.
|
|
176
|
+
|
|
177
|
+
## Dynamic collections: `flow.map()` (0.4.0)
|
|
178
|
+
|
|
179
|
+
`flow.map(id, { items, input, concurrency?, maxItems }, buildItem, display?)` runs a bounded item program over an array; concurrency defaults to 4. Its builder receives `item.input` shaped as `{ item, index, context }`; `context` resolves the explicit `input` option. Return a typed expression from the item builder. Use `if()` for returning paths or tasks for deterministic value-producing routing; terminal output/branch calls, loops inside maps, and nested maps are unsupported. Pass outer references through `input` rather than capturing them directly in the item builder. Results preserve array order. An empty array makes no calls. Concurrency is 1–16 per map, maxItems is required (1–64), definitions including templates are capped at 256 nodes and execution at 1,024 expanded nodes. Exceeding bounds fails without truncation. Failures abort siblings and block queued work; no automatic task retries or partial results. Remote cancellation remains cooperative.
|
|
180
|
+
|
|
181
|
+
Choose an item in the collection card, then **View Flow** to expand its full request/function diagram directly underneath on the same canvas. Changing the selector replaces the inline item flow; **Hide Flow** collapses it. Item values use bounded/redacted capture; don't put runtime data in structural titles. For an executable sample, see `cookbooks/rag-passages/README.md` in the repository checkout. Do not promise embedding search or LLM generation in that sample: it uses fixed retrieved passages, synthetic Jev scores and a deterministic excerpt preview.
|
|
182
|
+
|
|
183
|
+
## Returning conditionals: `flow.if()` (0.4.0)
|
|
184
|
+
|
|
185
|
+
Version 0.4.0 includes `flow.if()` and strict numeric `.gt()` comparisons. Use `flow.if(id, { when, then, else }, display?)` for a conditional whose value feeds shared downstream steps. Each arm returns a `FlowExpression`; only the selected arm executes. The result type is the union of both values. An arm may return an earlier expression without adding a task. `output()` and terminal `branch()` arms are rejected. Scope-local refs must not leak; consume the returned conditional reference. Conditionals may nest and run inside parallel lanes or map items.
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const record = flow.if("escalate", {
|
|
189
|
+
when: flags.get("maxFieldProbability").gt(0.7),
|
|
190
|
+
then: path => path.task("strong-extractor", {
|
|
191
|
+
input: document,
|
|
192
|
+
run: (text, { signal }) => strongExtractor(text, signal),
|
|
193
|
+
}),
|
|
194
|
+
else: () => cheapRecord,
|
|
195
|
+
}, { title: "Escalate a flagged field" });
|
|
196
|
+
// `record` is available to shared tasks/requests here; output ends the flow.
|
|
197
|
+
return flow.output("record", record);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
This is a fragment inside a definition with prior `flags`, `document`, `cheapRecord` expressions and an application-owned `strongExtractor`. Do not execute application side effects while defining either arm. Runtime callbacks stay on Node; this API is not remote code execution or a durable runner. Pass provider credentials server-side and propagate the signal. Failure/cancellation blocks the merge and continuation, without executing the other arm as a fallback.
|
|
201
|
+
|
|
202
|
+
The drawer generates a Selected result merge, highlights only its selected incoming path, and captures its value with normal redaction. Generated merge IDs are not an application contract; merge nodes count toward existing limits. Use a matching server/drawer build for the new `if`/`merge` node kinds. Check both paths, a boundary and cancellation with samples before live calls.
|
|
203
|
+
|
|
204
|
+
The repository's `cookbooks/sde-cascade` provides a complete local example at `/?cookbook=sde-cascade`: cheap extraction → runtime field verification → **any per-field probability > 0.70** → stronger extractor or keep cheap → shared comparison. The overall judge is displayed but excluded from the gate. Empty fields receive only `absence_wrong`. All four cases use synthetic probabilities and local extractor fixtures; no real provider performance or original evaluation is reproduced. Do not turn on live extraction just by adding an API key.
|
|
205
|
+
|
|
206
|
+
## Bounded loops: `flow.loop()` (0.4.0)
|
|
207
|
+
|
|
208
|
+
Use `flow.loop()` to carry JSON-compatible state across bounded iterations. Syntax:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
const final = flow.loop("repeat", {
|
|
212
|
+
initial: { count: 0 }, input: { increment: 1 },
|
|
213
|
+
while: state => state.get("count").lt(3), maxIterations: 3,
|
|
214
|
+
}, iteration => iteration.task("advance", {
|
|
215
|
+
input: iteration.input,
|
|
216
|
+
run: ({ state, context }) => ({ count: state.count + context.increment }),
|
|
217
|
+
}), { title: "Count up to three" });
|
|
218
|
+
return flow.output("done", final);
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The predicate/body callbacks build once and must not perform side effects. `while` returns an expression of its own state; pass needed outer values into the initial state. It is checked before every iteration, including the first and after the last allowed body. Initially false means zero iterations. `iteration.input` provides `{ state, index, context }`, with zero-based index and context from explicit `input`. The body returns the same state type as an expression; state/context must be JSON-compatible. Use tasks to compute application predicates and `if()` for returning paths. No outer-scope references or terminal outputs inside the body.
|
|
222
|
+
|
|
223
|
+
Require `maxIterations` (1–64). If the condition is still true at the limit, `FlowLoopLimitError` rejects the run, records an incomplete loop and blocks continuation. Never present the last state as a completed classification. Preserve completed iteration details on errors/cancellation. There is no durable execution, resume or automatic task retry.
|
|
224
|
+
|
|
225
|
+
Loops may contain one level of map, plus tasks, requests, conditions and parallel lanes. Nested loops, loops in maps and nested maps remain unsupported. All template nodes count toward the 256-node definition cap; the root execution shares a 1,024-expanded-step budget across iterations and collections. A fresh per-iteration budget would bypass that guarantee. Use a matching server and drawer. Loop cards reuse inline expansion/navigation: select an iteration → View Flow → select a nested frontier item if present. Input/latest state and child outcomes use normal redaction and capture limits.
|
|
226
|
+
|
|
227
|
+
`cookbooks/hierarchical-classification` is an executable sample at `/?cookbook=hierarchical-classification`. It follows the official executable algorithm’s **separate concurrent requests per frontier node**, despite introductory prose describing batching. Preserve geometric-mean decision-edge scores, epsilon 1e-9, single-child inference bypass, completed-leaf retention and top-k pruning. Samples are a small authored taxonomy and synthetic probabilities, not a replay of official benchmark data. Verify recovery, no-request paths, an initial leaf and intentional depth exhaustion before considering live execution.
|
|
@@ -34,6 +34,8 @@ Use one stable, app-specific key. The first options factory wins; restart after
|
|
|
34
34
|
|
|
35
35
|
Set the exact browser origin and API key in the existing server environment (normally an ignored `.env.local`). Never use `NEXT_PUBLIC_` for credentials. SDK client construction requires a key even when capture is disabled. When no key is available, use the sample client in [verify.md](verify.md) for verification instead of constructing a live client with an undefined key.
|
|
36
36
|
|
|
37
|
+
`next dev` listens on `0.0.0.0` by default, and Route Handlers can't see the connection's address. Recommend `--hostname 127.0.0.1` in the dev script, or a real `authorize` check, and report which one the app uses.
|
|
38
|
+
|
|
37
39
|
Import this `jev` in existing Route Handlers, Server Actions, and Server Components that call `systemOne(...)`. Keep their inputs and behavior; calls using other clients are not captured.
|
|
38
40
|
|
|
39
41
|
## Inspector route
|
|
@@ -49,7 +51,7 @@ export const GET = (request: Request) => inspector.handleNodeRoute(request);
|
|
|
49
51
|
export const DELETE = (request: Request) => inspector.handleNodeRoute(request);
|
|
50
52
|
```
|
|
51
53
|
|
|
52
|
-
Use the **optional** catch-all `[[...path]]`, which covers config at the base path as well as `/events
|
|
54
|
+
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
55
|
|
|
54
56
|
## Root client boundary and CSS
|
|
55
57
|
|
|
@@ -41,7 +41,7 @@ app.use(inspector.middleware);
|
|
|
41
41
|
// Existing inference handlers import jev from './jev' and call systemOne as before.
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
Mount at the application root, before fallback routes. Do not mount with `app.use('/api/jev-devtools', ...)`: Express strips the prefix the adapter needs. Dispose the adapter during server shutdown, not after each request. If the framework reloads server modules, use `getJevDevtools('your-app', () => options)` instead and restart after options change.
|
|
44
|
+
Mount at the application root, before fallback routes. Do not mount with `app.use('/api/jev-devtools', ...)`: Express strips the prefix the adapter needs. Dispose the adapter during server shutdown, not after each request. If the framework reloads server modules, use `getJevDevtools('your-app', () => options)` instead and restart after options change. Without `authorize`, the middleware returns 403 to non-loopback connections, including a host browser reaching a container through a published port; those setups need a real `authorize` check. So does a proxy or tunnel reachable from the network, because it connects to the backend over loopback.
|
|
45
45
|
|
|
46
46
|
### Separate Vite frontend and backend ports
|
|
47
47
|
|
|
@@ -61,7 +61,7 @@ server: {
|
|
|
61
61
|
}
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
Set the adapter origin to the **frontend** origin. Preserve Host and the complete path; no prefix rewrite. Do not buffer or cache `/events`. A broader existing `/api` proxy may suffice if it obeys these rules. The React component must still use the relative same-origin endpoint. See [Vite proxy options](https://vite.dev/config/server-options.html#server-proxy).
|
|
64
|
+
Set the adapter origin to the **frontend** origin. Preserve Host and the complete path; no prefix rewrite. The backend sees the proxy's loopback connection, so keep Vite on its default `localhost` host or set `authorize`. Do not buffer or cache `/events`. A broader existing `/api` proxy may suffice if it obeys these rules. The React component must still use the relative same-origin endpoint. See [Vite proxy options](https://vite.dev/config/server-options.html#server-proxy).
|
|
65
65
|
|
|
66
66
|
## Vite app without a backend: local sample setup
|
|
67
67
|
|
|
@@ -78,6 +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
|
|
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
82
|
|
|
83
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.
|