@magic-marker/nurt 0.1.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 +551 -0
- package/dist/flow-DqIejS_0.d.ts +306 -0
- package/dist/index.d.ts +136 -0
- package/dist/index.js +996 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.d.ts +123 -0
- package/dist/react/index.js +1234 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
# Nurt
|
|
2
|
+
|
|
3
|
+
> [!CAUTION]
|
|
4
|
+
> This documentation and library is mostly Claude generated. It does have plenty of tests and is used in minimal scope but further cleanup is required at some point.
|
|
5
|
+
|
|
6
|
+
A type-safe, zero-dependency DAG flow execution engine for TypeScript. Nurt lets you define directed acyclic graphs of async steps, execute them with automatic parallelism, and observe progress in real time. It supports dynamic step spawning, nested subgraphs, cross-boundary dependencies, and fine-grained error handling.
|
|
7
|
+
|
|
8
|
+
The name comes from the Polish word for "flow" (nurt).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @marker/nurt
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { flow } from "@marker/nurt";
|
|
20
|
+
|
|
21
|
+
const result = await flow("my-flow")
|
|
22
|
+
.step("fetch", async () => {
|
|
23
|
+
const data = await fetchData();
|
|
24
|
+
return { items: data };
|
|
25
|
+
})
|
|
26
|
+
.step("process", ["fetch"], async (input) => {
|
|
27
|
+
// input.fetch is typed as { items: ... }
|
|
28
|
+
return { count: input.fetch.items.length };
|
|
29
|
+
})
|
|
30
|
+
.step("save", ["process"], async (input) => {
|
|
31
|
+
await saveResult(input.process.count);
|
|
32
|
+
return { saved: true };
|
|
33
|
+
})
|
|
34
|
+
.build()
|
|
35
|
+
.run().result;
|
|
36
|
+
|
|
37
|
+
console.log(result.status); // "success"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Core Concepts
|
|
41
|
+
|
|
42
|
+
### Flow
|
|
43
|
+
|
|
44
|
+
An immutable DAG blueprint created by the builder. Defines steps, their dependencies, and groups. Validated at build time (cycle detection, parent existence). Can spawn multiple concurrent runs.
|
|
45
|
+
|
|
46
|
+
### FlowRun
|
|
47
|
+
|
|
48
|
+
A single execution of a flow. Tracks step statuses, outputs, and timing. Provides hooks for observability and a `snapshot()` method for serialization.
|
|
49
|
+
|
|
50
|
+
### Steps
|
|
51
|
+
|
|
52
|
+
Async functions that receive their parents' outputs as typed input. Steps execute as soon as all their dependencies complete. Multiple independent steps run in parallel automatically.
|
|
53
|
+
|
|
54
|
+
### Groups
|
|
55
|
+
|
|
56
|
+
Typed collection points for dynamically-added members. An arbiter step can decide at runtime which members to spawn into a group. Downstream steps wait for all group members to complete.
|
|
57
|
+
|
|
58
|
+
### Subgraphs
|
|
59
|
+
|
|
60
|
+
Group members can be entire nested flows (DAGs within DAGs). A subgraph member contains its own `Flow` that executes as a child `FlowRun`. Subgraphs can reference steps outside their boundary via `externalDeps`.
|
|
61
|
+
|
|
62
|
+
## API Reference
|
|
63
|
+
|
|
64
|
+
### `flow(name)`
|
|
65
|
+
|
|
66
|
+
Creates a new `FlowBuilder`.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { flow } from "@marker/nurt";
|
|
70
|
+
|
|
71
|
+
const builder = flow("my-flow");
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### FlowBuilder
|
|
75
|
+
|
|
76
|
+
Chainable builder. Each `.step()` call returns a new builder with an expanded type registry.
|
|
77
|
+
|
|
78
|
+
#### `.step(name, handler)` - Root step
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
.step("start", async () => {
|
|
82
|
+
return { documentId: "doc-1" };
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### `.step(name, parents, handler)` - Step with dependencies
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
.step("process", ["start"], async (input) => {
|
|
90
|
+
// input.start is typed from the "start" step's return type
|
|
91
|
+
return { processed: true };
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### `.step(name, parents, options)` - Step with options
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
.step("save", ["process"], {
|
|
99
|
+
execute: async (input) => ({ saved: true }),
|
|
100
|
+
terminal: true, // determines run success/failure
|
|
101
|
+
allowFailures: true, // runs even if parents fail
|
|
102
|
+
transform: (raw) => transform(raw), // transform parent outputs
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### `.group<T>(name, options)` - Declare a group
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
.group<ReviewOutput>("reviews", { dependsOn: ["arbiter"] })
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The group adds `T[]` to the type registry. Downstream steps receive an array of member outputs.
|
|
113
|
+
|
|
114
|
+
#### `.step(name, [group(...)], handler)` - Depend on a group
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { group } from "@marker/nurt";
|
|
118
|
+
|
|
119
|
+
.step("merge", [group("reviews")], async (input) => {
|
|
120
|
+
// input.reviews is ReviewOutput[]
|
|
121
|
+
return { total: input.reviews.length };
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### `.build()` - Create the Flow
|
|
126
|
+
|
|
127
|
+
Validates the DAG (cycle detection, parent existence) and returns an immutable `Flow`.
|
|
128
|
+
|
|
129
|
+
### Flow
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
const myFlow = flow("example").step(...).build();
|
|
133
|
+
|
|
134
|
+
// Create a run
|
|
135
|
+
const run = myFlow.run();
|
|
136
|
+
const run2 = myFlow.run({ failFast: true }); // multiple concurrent runs OK
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### RunOptions
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
interface RunOptions {
|
|
143
|
+
failFast?: boolean; // abort entire run on first error (default: false)
|
|
144
|
+
hooks?: FlowHooks; // lifecycle callbacks
|
|
145
|
+
injectedSteps?: Map<string, unknown>; // pre-resolved step outputs
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### FlowRun
|
|
150
|
+
|
|
151
|
+
The execution instance. Created by `flow.run()`.
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
const run = myFlow.run({ hooks: { ... } });
|
|
155
|
+
|
|
156
|
+
run.runId; // "run-1" (unique per run)
|
|
157
|
+
run.result; // Promise<FlowRunResult> - resolves when done
|
|
158
|
+
run.steps; // readonly StepRecord[] - current state snapshot
|
|
159
|
+
|
|
160
|
+
run.snapshot(); // FlowSnapshot - JSON-serializable state
|
|
161
|
+
run.abort(); // signal all steps to stop
|
|
162
|
+
|
|
163
|
+
// Dynamic group control
|
|
164
|
+
run.spawnGroup("reviews", members); // add members + auto-seal
|
|
165
|
+
run.addGroupMember("reviews", member); // add one member
|
|
166
|
+
run.sealGroup("reviews"); // seal (no more members)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### StepContext
|
|
170
|
+
|
|
171
|
+
Available inside every step handler as the second argument.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
.step("my-step", async (input, ctx) => {
|
|
175
|
+
ctx.runId; // current run ID
|
|
176
|
+
ctx.signal; // AbortSignal (check ctx.signal.aborted)
|
|
177
|
+
ctx.history; // shared History store
|
|
178
|
+
ctx.run; // RunHandle for dynamic control
|
|
179
|
+
|
|
180
|
+
ctx.history.set("key", "value");
|
|
181
|
+
ctx.history.get<string>("key"); // "value"
|
|
182
|
+
|
|
183
|
+
// Spawn group members from inside a step
|
|
184
|
+
ctx.run.spawnGroup("reviews", [
|
|
185
|
+
{ name: "grammar", execute: async () => ({ ... }) },
|
|
186
|
+
]);
|
|
187
|
+
})
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### FlowHooks
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
const run = myFlow.run({
|
|
194
|
+
hooks: {
|
|
195
|
+
onChange: () => {
|
|
196
|
+
// fires on ANY state change (including subgraph events)
|
|
197
|
+
updateUI(run.snapshot());
|
|
198
|
+
},
|
|
199
|
+
onStepStart: (step) => console.log(`started: ${step.name}`),
|
|
200
|
+
onStepComplete: (step) =>
|
|
201
|
+
console.log(`done: ${step.name} in ${step.durationMs}ms`),
|
|
202
|
+
onStepError: (step) => console.log(`failed: ${step.name}: ${step.error}`),
|
|
203
|
+
onStepAdded: (step) => console.log(`dynamic step: ${step.name}`),
|
|
204
|
+
onRunComplete: (result) => console.log(`run ${result.status}`),
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Hooks are isolated from execution -- if a hook throws, the run continues unaffected.
|
|
210
|
+
|
|
211
|
+
### FlowRunResult
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
const result = await run.result;
|
|
215
|
+
|
|
216
|
+
result.runId; // "run-1"
|
|
217
|
+
result.status; // "success" | "error"
|
|
218
|
+
result.startedAt; // timestamp
|
|
219
|
+
result.completedAt; // timestamp
|
|
220
|
+
result.steps; // StepRecord[] with status, output, timing
|
|
221
|
+
result.history; // ReadonlyMap<string, unknown>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### StepRecord
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
interface StepRecord {
|
|
228
|
+
name: string;
|
|
229
|
+
parentNames: string[];
|
|
230
|
+
status: "pending" | "running" | "success" | "error" | "skipped";
|
|
231
|
+
startedAt?: number;
|
|
232
|
+
completedAt?: number;
|
|
233
|
+
durationMs?: number;
|
|
234
|
+
output?: unknown;
|
|
235
|
+
error?: string;
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Patterns
|
|
240
|
+
|
|
241
|
+
### Linear Pipeline
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
const result = await flow("pipeline")
|
|
245
|
+
.step("extract", async () => ({ text: "hello world" }))
|
|
246
|
+
.step("transform", ["extract"], async (input) => ({
|
|
247
|
+
upper: input.extract.text.toUpperCase(),
|
|
248
|
+
}))
|
|
249
|
+
.step("load", ["transform"], async (input) => ({
|
|
250
|
+
saved: true,
|
|
251
|
+
text: input.transform.upper,
|
|
252
|
+
}))
|
|
253
|
+
.build()
|
|
254
|
+
.run().result;
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Parallel Fan-Out / Fan-In
|
|
258
|
+
|
|
259
|
+
Steps with the same parent run in parallel automatically.
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
const result = await flow("parallel")
|
|
263
|
+
.step("start", async () => ({ data: [1, 2, 3] }))
|
|
264
|
+
.step("branch-a", ["start"], async (input) => ({
|
|
265
|
+
sum: input.start.data.reduce((a, b) => a + b, 0),
|
|
266
|
+
}))
|
|
267
|
+
.step("branch-b", ["start"], async (input) => ({
|
|
268
|
+
count: input.start.data.length,
|
|
269
|
+
}))
|
|
270
|
+
.step("merge", ["branch-a", "branch-b"], async (input) => ({
|
|
271
|
+
average: input["branch-a"].sum / input["branch-b"].count,
|
|
272
|
+
}))
|
|
273
|
+
.build()
|
|
274
|
+
.run().result;
|
|
275
|
+
// branch-a and branch-b execute concurrently
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Dynamic Group Spawning (Arbiter Pattern)
|
|
279
|
+
|
|
280
|
+
An arbiter step decides at runtime which members to add to a group.
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
type ReviewOutput = { tool: string; comments: string[] };
|
|
284
|
+
|
|
285
|
+
const reviewFlow = flow("review")
|
|
286
|
+
.step("start", async () => ({ wordCount: 1200 }))
|
|
287
|
+
.step("arbiter", ["start"], async (input, ctx) => {
|
|
288
|
+
const tools =
|
|
289
|
+
input.start.wordCount > 500
|
|
290
|
+
? ["grammar", "tone", "clarity"]
|
|
291
|
+
: ["grammar"];
|
|
292
|
+
|
|
293
|
+
ctx.run.spawnGroup(
|
|
294
|
+
"reviews",
|
|
295
|
+
tools.map((tool) => ({
|
|
296
|
+
name: `review-${tool}`,
|
|
297
|
+
execute: async () => ({
|
|
298
|
+
tool,
|
|
299
|
+
comments: [`Found issue in ${tool}`],
|
|
300
|
+
}),
|
|
301
|
+
})),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
return { selectedTools: tools };
|
|
305
|
+
})
|
|
306
|
+
.group<ReviewOutput>("reviews", { dependsOn: ["arbiter"] })
|
|
307
|
+
.step("synthesize", [group("reviews")], async (input) => ({
|
|
308
|
+
total: input.reviews.flatMap((r) => r.comments).length,
|
|
309
|
+
}))
|
|
310
|
+
.build();
|
|
311
|
+
|
|
312
|
+
const result = await reviewFlow.run().result;
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Subgraph Members (Nested DAGs)
|
|
316
|
+
|
|
317
|
+
A group member can contain an entire flow with branching and parallelism.
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
const analysisFlow = flow("deep-analysis")
|
|
321
|
+
.step("extract", async () => ({ claims: ["A", "B"] }))
|
|
322
|
+
.step("verify", ["extract"], async (input) => ({
|
|
323
|
+
verified: input.extract.claims.length,
|
|
324
|
+
}))
|
|
325
|
+
.step("check-dates", ["extract"], async () => ({
|
|
326
|
+
issues: 0,
|
|
327
|
+
}))
|
|
328
|
+
.step("report", ["verify", "check-dates"], {
|
|
329
|
+
execute: async (input) => ({
|
|
330
|
+
result: `${input.verify.verified} verified, ${input["check-dates"].issues} date issues`,
|
|
331
|
+
}),
|
|
332
|
+
terminal: true,
|
|
333
|
+
})
|
|
334
|
+
.build();
|
|
335
|
+
// DAG: extract -> [verify, check-dates] -> report
|
|
336
|
+
|
|
337
|
+
run.spawnGroup("reviews", [
|
|
338
|
+
{ name: "grammar", execute: async () => ({ ... }) }, // single member
|
|
339
|
+
{ name: "deep-analysis", flow: analysisFlow }, // subgraph member
|
|
340
|
+
]);
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Pipeline Helper
|
|
344
|
+
|
|
345
|
+
Shorthand for linear subgraphs.
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
import { pipeline } from "@marker/nurt";
|
|
349
|
+
|
|
350
|
+
run.spawnGroup("reviews", [
|
|
351
|
+
pipeline("tone-check", [
|
|
352
|
+
{ name: "detect", execute: async () => ({ issues: ["too formal"] }) },
|
|
353
|
+
{ name: "classify", execute: async (input) => ({ severity: "medium" }) },
|
|
354
|
+
{
|
|
355
|
+
name: "suggest",
|
|
356
|
+
execute: async (input) => ({ fix: "use simpler words" }),
|
|
357
|
+
},
|
|
358
|
+
]),
|
|
359
|
+
]);
|
|
360
|
+
// Creates: detect -> classify -> suggest (terminal)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Cross-Boundary Dependencies
|
|
364
|
+
|
|
365
|
+
A subgraph step can depend on a step outside the subgraph via `externalDeps`.
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
const clarityFlow = flow("clarity")
|
|
369
|
+
.step("extract", async () => ({ issues: ["vague intro"] }))
|
|
370
|
+
.step("nlp-data", async () => ({})) // placeholder for external injection
|
|
371
|
+
.step("assess", ["extract"], async (input) => ({ ... }))
|
|
372
|
+
.step("cross-ref", ["nlp-data"], async (input) => ({
|
|
373
|
+
// input["nlp-data"] will contain the NLP step's output
|
|
374
|
+
refs: input["nlp-data"].entities.length,
|
|
375
|
+
}))
|
|
376
|
+
.step("refine", ["assess", "cross-ref"], {
|
|
377
|
+
execute: async (input) => ({ ... }),
|
|
378
|
+
terminal: true,
|
|
379
|
+
})
|
|
380
|
+
.build();
|
|
381
|
+
|
|
382
|
+
// In the parent flow, nlp-process runs at the top level
|
|
383
|
+
// externalDeps maps the subgraph's "nlp-data" step to the parent's "nlp-process" step
|
|
384
|
+
run.spawnGroup("reviews", [
|
|
385
|
+
{
|
|
386
|
+
name: "clarity",
|
|
387
|
+
flow: clarityFlow,
|
|
388
|
+
externalDeps: { "nlp-data": "nlp-process" },
|
|
389
|
+
},
|
|
390
|
+
]);
|
|
391
|
+
// The subgraph waits for "nlp-process" to complete, then injects its output
|
|
392
|
+
// as the pre-resolved "nlp-data" step inside the child run
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Error Handling with allowFailures
|
|
396
|
+
|
|
397
|
+
By default, if a step fails, its dependents are skipped. With `allowFailures: true`, a step runs even if parents failed, receiving `StepResult<T>` wrappers.
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
import type { StepResult } from "@marker/nurt";
|
|
401
|
+
|
|
402
|
+
const result = await flow("resilient")
|
|
403
|
+
.step("risky", async () => {
|
|
404
|
+
throw new Error("network timeout");
|
|
405
|
+
})
|
|
406
|
+
.step("handler", ["risky"], {
|
|
407
|
+
allowFailures: true,
|
|
408
|
+
execute: async (input) => {
|
|
409
|
+
// input.risky is StepResult<T>, not T
|
|
410
|
+
const result = input.risky as StepResult<unknown>;
|
|
411
|
+
if (result.status === "error") {
|
|
412
|
+
return { fallback: true, error: result.error };
|
|
413
|
+
}
|
|
414
|
+
return { fallback: false, value: result.value };
|
|
415
|
+
},
|
|
416
|
+
})
|
|
417
|
+
.build()
|
|
418
|
+
.run().result;
|
|
419
|
+
|
|
420
|
+
// result.steps[0].status = "error"
|
|
421
|
+
// result.steps[1].status = "success" (ran despite parent failure)
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Terminal Steps
|
|
425
|
+
|
|
426
|
+
Terminal steps determine the run's final status. If no steps are marked terminal, all steps are considered.
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
const result = await flow("with-terminal")
|
|
430
|
+
.step("main", async () => ({ data: "ok" }))
|
|
431
|
+
.step("save", ["main"], {
|
|
432
|
+
execute: async () => ({ saved: true }),
|
|
433
|
+
terminal: true,
|
|
434
|
+
})
|
|
435
|
+
.step("notify", async () => {
|
|
436
|
+
throw new Error("email service down");
|
|
437
|
+
})
|
|
438
|
+
.build()
|
|
439
|
+
.run().result;
|
|
440
|
+
|
|
441
|
+
// result.status = "success"
|
|
442
|
+
// Only "save" (terminal) determines status. "notify" failed but doesn't affect it.
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Shared State via History
|
|
446
|
+
|
|
447
|
+
Steps can share data through the `ctx.history` store, accessible across all steps in a run.
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
const result = await flow("with-history")
|
|
451
|
+
.step("producer", async (_, ctx) => {
|
|
452
|
+
ctx.history.set("config", { maxRetries: 3 });
|
|
453
|
+
return { produced: true };
|
|
454
|
+
})
|
|
455
|
+
.step("consumer", ["producer"], async (_, ctx) => {
|
|
456
|
+
const config = ctx.history.get<{ maxRetries: number }>("config");
|
|
457
|
+
return { retries: config?.maxRetries };
|
|
458
|
+
})
|
|
459
|
+
.build()
|
|
460
|
+
.run().result;
|
|
461
|
+
|
|
462
|
+
// result.history.get("config") = { maxRetries: 3 }
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Snapshots for Serialization
|
|
466
|
+
|
|
467
|
+
`run.snapshot()` returns a JSON-serializable representation of the entire flow state, including nested subgraphs. Useful for sending state to a frontend for visualization.
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
const run = myFlow.run({
|
|
471
|
+
hooks: {
|
|
472
|
+
onChange: () => {
|
|
473
|
+
const snapshot = run.snapshot();
|
|
474
|
+
// snapshot.flow.name, snapshot.flow.steps, snapshot.flow.groups
|
|
475
|
+
// snapshot.run.status, snapshot.run.runId
|
|
476
|
+
// Each step has: name, status, output, durationMs, error, parentNames
|
|
477
|
+
// Groups have: members with type, status, subgraph (recursive FlowSnapshot)
|
|
478
|
+
sendToFrontend(JSON.stringify(snapshot));
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Executable Classes
|
|
485
|
+
|
|
486
|
+
Steps can be class instances implementing the `Executable` interface.
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
import type { Executable, StepContext } from "@marker/nurt";
|
|
490
|
+
|
|
491
|
+
class MyTool implements Executable<{ data: string }, { result: number }> {
|
|
492
|
+
async execute(
|
|
493
|
+
input: { data: string },
|
|
494
|
+
ctx: StepContext,
|
|
495
|
+
): Promise<{ result: number }> {
|
|
496
|
+
return { result: input.data.length };
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
flow("with-class")
|
|
501
|
+
.step("start", async () => ({ data: "hello" }))
|
|
502
|
+
.step("tool", ["start"], new MyTool())
|
|
503
|
+
.build();
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
## Error Types
|
|
507
|
+
|
|
508
|
+
| Error | When |
|
|
509
|
+
| -------------------- | --------------------------------------------------------------------- |
|
|
510
|
+
| `CycleDetectedError` | `.build()` detects a cycle in the DAG |
|
|
511
|
+
| `DuplicateStepError` | `.step()` or `.group()` uses an already-registered name |
|
|
512
|
+
| `UnknownParentError` | `.step()` references a parent that doesn't exist |
|
|
513
|
+
| `UnsealedGroupError` | Run completes with a group that was never sealed |
|
|
514
|
+
| `UnfilledSlotError` | A pipeline slot was not provided an implementation |
|
|
515
|
+
| `StepExecutionError` | Wraps an error thrown by a step handler. Has `.stepName` and `.cause` |
|
|
516
|
+
|
|
517
|
+
## Graph Utilities
|
|
518
|
+
|
|
519
|
+
Low-level DAG utilities, useful for custom tooling or analysis.
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
import {
|
|
523
|
+
validateAcyclic,
|
|
524
|
+
topologicalSort,
|
|
525
|
+
getReadySteps,
|
|
526
|
+
getSkippableSteps,
|
|
527
|
+
getReadyWithFailures,
|
|
528
|
+
} from "@marker/nurt";
|
|
529
|
+
|
|
530
|
+
const nodes = [
|
|
531
|
+
{ name: "a", parentNames: [] },
|
|
532
|
+
{ name: "b", parentNames: ["a"] },
|
|
533
|
+
{ name: "c", parentNames: ["a"] },
|
|
534
|
+
{ name: "d", parentNames: ["b", "c"] },
|
|
535
|
+
];
|
|
536
|
+
|
|
537
|
+
validateAcyclic(nodes); // throws CycleDetectedError if cyclic
|
|
538
|
+
topologicalSort(nodes); // ["a", "b", "c", "d"]
|
|
539
|
+
|
|
540
|
+
const statuses = new Map([
|
|
541
|
+
["a", "success"],
|
|
542
|
+
["b", "success"],
|
|
543
|
+
["c", "running"],
|
|
544
|
+
["d", "pending"],
|
|
545
|
+
]);
|
|
546
|
+
getReadySteps(nodes, statuses); // [] (c still running, d waits)
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## License
|
|
550
|
+
|
|
551
|
+
MIT
|