jev-devtools 0.5.0 → 0.5.2

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.
@@ -1,6 +1,6 @@
1
1
  # Build and inspect a Jev flow
2
2
 
3
- For `jev-devtools` **0.5.0**. Check the repository changelog for publication status when installing from source. 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.
3
+ For `jev-devtools` **0.5.2**. Check the repository changelog for publication status when installing from source. 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
4
 
5
5
  ## Plan the integration
6
6
 
@@ -9,74 +9,119 @@ For `jev-devtools` **0.5.0**. Check the repository changelog for publication sta
9
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
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
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.5.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.
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.5.2 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
13
 
14
14
  ## Complete example: parallel checks, one decision, two outcomes
15
15
 
16
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
17
 
18
18
  ```ts
19
- import { noul } from '@typesafe-ai/sdk';
20
- import { defineJevFlow } from 'jev-devtools/flow';
19
+ import { noul } from "@typesafe-ai/sdk";
20
+ import { defineJevFlow } from "jev-devtools/flow";
21
21
 
22
22
  type Ticket = { message: string };
23
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
- });
24
+ export const reviewFlow = defineJevFlow<Ticket>("ticket-review", (flow) => {
25
+ const checks = flow.parallel(
26
+ "checks",
27
+ {
28
+ eligibility: (lane) =>
29
+ lane
30
+ .jev(
31
+ "eligibility",
32
+ {
33
+ state: flow.input,
34
+ questions: {
35
+ eligible: noul("Is this ticket eligible for review?"),
36
+ },
37
+ },
38
+ {
39
+ title: "Check eligibility",
40
+ description:
41
+ "Evaluate whether this ticket can enter the review process.",
42
+ },
43
+ )
44
+ .answer("eligible").noul,
45
+ risk: (lane) =>
46
+ lane
47
+ .jev(
48
+ "risk",
49
+ {
50
+ state: flow.input,
51
+ questions: {
52
+ risky: noul("Does this ticket need extra risk review?"),
53
+ },
54
+ },
55
+ {
56
+ title: "Check risk",
57
+ description: "Evaluate risk independently of eligibility.",
58
+ },
59
+ )
60
+ .answer("risky").noul,
61
+ },
62
+ {
63
+ title: "Wait for both checks",
64
+ description: "Continue after eligibility and risk have both succeeded.",
65
+ },
66
+ );
67
+
68
+ const decision = flow.jev(
69
+ "decision",
70
+ {
71
+ state: {
72
+ ticket: flow.input,
73
+ eligibility: checks.eligibility,
74
+ risk: checks.risk,
75
+ },
76
+ questions: { ready: noul("Is this ticket ready for approval?") },
77
+ },
78
+ {
79
+ title: "Assess readiness",
80
+ description: "Ask Jev to assess the ticket using both completed checks.",
81
+ },
82
+ );
83
+
84
+ return flow.branch(
85
+ "readiness",
86
+ {
87
+ when: decision.answer("ready").noul.gte(0.9),
88
+ then: (path) =>
89
+ path.output(
90
+ "ready",
91
+ {
92
+ readiness: decision.answer("ready").noul,
93
+ },
94
+ {
95
+ title: "Ready for approval",
96
+ description:
97
+ "Return readiness to the application. No approval is executed here.",
98
+ },
99
+ ),
100
+ else: (path) =>
101
+ path.output("manual-review", undefined, {
102
+ title: "Needs manual review",
103
+ description: "Readiness is below the application threshold.",
104
+ }),
105
+ },
106
+ {
107
+ title: "Require at least 90% readiness",
108
+ description: "Use the returned readiness probability to choose a route.",
109
+ },
110
+ );
69
111
  });
70
112
  ```
71
113
 
72
114
  Invoke from your existing server handler after validating the input:
73
115
 
74
116
  ```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
- });
117
+ const result = await reviewFlow.run(
118
+ { message: "I was charged twice." },
119
+ {
120
+ client: jev, // Created by this same inspector.
121
+ devtools: inspector,
122
+ signal: request.signal, // Optional AbortSignal when the framework provides one.
123
+ },
124
+ );
80
125
  // { runId, outcome: 'ready', value: { readiness: 0.94 } }
81
126
  // Or { runId, outcome: 'manual-review', value: undefined }.
82
127
  ```
@@ -85,26 +130,28 @@ For custom server-derived sessions, use `await inspector.run(request, () => revi
85
130
 
86
131
  ## API rules that affect generated code
87
132
 
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. |
133
+ | API | Contract |
134
+ | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
135
+ | `defineJevFlow<Input>(name, build)` | Synchronous definition; no requests run while building. Return `flow.output(...)` or `flow.branch(...)`. Each `run` is independent. |
136
+ | `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. |
137
+ | `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. |
138
+ | `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. |
139
+ | `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. |
140
+ | `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. |
141
+ | `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`. |
142
+ | `flow.input.get('field')` | Select an own object field, array index, or array `length` as a typed reference resolved during execution. Primitive properties (string length/indexes included) are unsupported; compute them in a task callback. |
143
+ | `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)`. |
144
+ | `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. |
145
+ | `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. |
146
+ | `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. |
147
+ | `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. |
148
+ | `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. |
149
+ | `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
150
 
106
151
  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
152
 
153
+ **0.5.2:** expressions are bounded during construction to depth 80 (leaf depth 0), 4,096 expanded nodes, and 256 operands per operation. Reusing a child counts its complete size again: eleven `all(e, e)` doublings of Boolean `flow.input` fit (4,095 nodes), but twelve do not. Split larger logic into tasks or smaller steps. Legacy condition descriptions are capped at 4,096 UTF-16 code units including `[condition truncated]`; structured rules keep their 128-node projection budget. Display truncation never changes accepted expression evaluation.
154
+
108
155
  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
156
 
110
157
  ## Diagram context and privacy
@@ -113,6 +160,10 @@ Every builder method accepts a final `{ title, description }` argument (`FlowDis
113
160
 
114
161
  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
162
 
163
+ Outcome `FlowRun.captureWarnings`, rule `FlowNode.conditionCaptureWarnings`, and step `FlowStep.captureWarnings` describe capture limitations independently, including nested runs. Warned captures are unsuitable as complete originals. Missing fields on older captures mean unknown completeness; do not infer control metadata from strings such as `[truncated]`. Strings retain up to 16,000 characters, arrays/general objects 150 entries, within depth-12 and approximate 96,000-character budgets. The drawer displays warnings beside values; execution remains unchanged. Custom redactors should return replacements, not mutate in place, for reliable warning detection. Fixtures only store Jev responses and trace warnings, not outcomes or rules.
164
+
165
+ Structured condition rules also have a 128-node projection budget. Exceeding it retains `conditionCaptureWarnings` before server capture; it does not change execution. Unsupported rule literals likewise carry an explicit warning.
166
+
116
167
  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
168
 
118
169
  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.
@@ -122,30 +173,43 @@ History is bounded in process (default 50 runs, maximum 200, independently from
122
173
  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
174
 
124
175
  ```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 });
176
+ import * as zlib from "node:zlib";
177
+
178
+ const sampleJev = inspector.createClient(
179
+ {
180
+ apiKey: "sample-only",
181
+ logLevel: "off",
182
+ fetch: async (_url, init) => {
183
+ const bytes = new Uint8Array(
184
+ await new Response(init?.body).arrayBuffer(),
185
+ );
186
+ const encoding = new Headers(init?.headers).get("content-encoding");
187
+ const body = JSON.parse(
188
+ encoding === "zstd"
189
+ ? zlib.zstdDecompressSync(bytes).toString()
190
+ : encoding === "gzip"
191
+ ? zlib.gunzipSync(bytes).toString()
192
+ : new TextDecoder().decode(bytes),
193
+ );
194
+ const answers = Object.fromEntries(
195
+ Object.keys(body.questions).map((name) => [
196
+ name,
197
+ { type: "noul", noul: name === "risky" ? 0.1 : 0.94 },
198
+ ]),
199
+ );
200
+ return Response.json({ model: "jev-sample", answers });
201
+ },
142
202
  },
143
- }, { label: 'Flow setup verification', source: 'sample' });
144
-
145
- const result = await reviewFlow.run({ message: 'Sample ticket' }, {
146
- client: sampleJev,
147
- devtools: inspector,
148
- });
203
+ { label: "Flow setup verification", source: "sample" },
204
+ );
205
+
206
+ const result = await reviewFlow.run(
207
+ { message: "Sample ticket" },
208
+ {
209
+ client: sampleJev,
210
+ devtools: inspector,
211
+ },
212
+ );
149
213
  // Expect outcome 'ready', value.readiness 0.94, and three captured calls.
150
214
  ```
151
215
 
@@ -170,7 +234,7 @@ Remove temporary verification routes unless requested as a demo. Report the inst
170
234
 
171
235
  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.
172
236
 
173
- - `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.
237
+ - `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. Task input literals with own symbol keys (including nested objects and arrays) fail during definition; referenced values are checked before the callback runs. No implicit retry.
174
238
  - `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.
175
239
  - 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.
176
240
  - 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.
@@ -180,7 +244,7 @@ Use `task()` for application-owned server work and the request-factory overload
180
244
 
181
245
  ## Dynamic collections: `flow.map()` (0.4.0)
182
246
 
183
- `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.
247
+ `flow.map(id, { items, input, concurrency?, maxItems }, buildItem, display?)` runs a bounded item program over an array; concurrency defaults to 4. Items may be literals, a whole-array reference, or nested object/array templates containing references. Its builder receives `item.input` shaped as `{ item, index, context }`, with item type `I` for whole-array references/scalar arrays or `Resolved<I>` for literal/template items; `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.
184
248
 
185
249
  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.
186
250
 
@@ -189,14 +253,19 @@ Choose an item in the collection card, then **View Flow** to expand its full req
189
253
  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.
190
254
 
191
255
  ```ts
192
- const record = flow.if("escalate", {
193
- when: flags.get("maxFieldProbability").gt(0.7),
194
- then: path => path.task("strong-extractor", {
195
- input: document,
196
- run: (text, { signal }) => strongExtractor(text, signal),
197
- }),
198
- else: () => cheapRecord,
199
- }, { title: "Escalate a flagged field" });
256
+ const record = flow.if(
257
+ "escalate",
258
+ {
259
+ when: flags.get("maxFieldProbability").gt(0.7),
260
+ then: (path) =>
261
+ path.task("strong-extractor", {
262
+ input: document,
263
+ run: (text, { signal }) => strongExtractor(text, signal),
264
+ }),
265
+ else: () => cheapRecord,
266
+ },
267
+ { title: "Escalate a flagged field" },
268
+ );
200
269
  // `record` is available to shared tasks/requests here; output ends the flow.
201
270
  return flow.output("record", record);
202
271
  ```
@@ -209,16 +278,24 @@ The repository's `cookbooks/sde-cascade` provides a complete local example at `/
209
278
 
210
279
  ## Bounded loops: `flow.loop()` (0.4.0)
211
280
 
212
- Use `flow.loop()` to carry JSON-compatible state across bounded iterations. Syntax:
281
+ Use `flow.loop()` to carry JSON-compatible state across bounded iterations. Literal initial state, whole-state references and nested reference templates are supported. Whole-state references and scalar initial values preserve their `S` payload, including generic types. For literal/template state, the gate, iteration, body return and final result use `Resolved<S>`, preserving branded primitives: for `initial: { count: flow.input.get("count") }`, task callbacks receive numeric `state.count` and can return `{ count: state.count + 1 }`. Expression methods belong on builder references, not resolved task values. Syntax:
213
282
 
214
283
  ```ts
215
- const final = flow.loop("repeat", {
216
- initial: { count: 0 }, input: { increment: 1 },
217
- while: state => state.get("count").lt(3), maxIterations: 3,
218
- }, iteration => iteration.task("advance", {
219
- input: iteration.input,
220
- run: ({ state, context }) => ({ count: state.count + context.increment }),
221
- }), { title: "Count up to three" });
284
+ const final = flow.loop(
285
+ "repeat",
286
+ {
287
+ initial: { count: 0 },
288
+ input: { increment: 1 },
289
+ while: (state) => state.get("count").lt(3),
290
+ maxIterations: 3,
291
+ },
292
+ (iteration) =>
293
+ iteration.task("advance", {
294
+ input: iteration.input,
295
+ run: ({ state, context }) => ({ count: state.count + context.increment }),
296
+ }),
297
+ { title: "Count up to three" },
298
+ );
222
299
  return flow.output("done", final);
223
300
  ```
224
301