pipeai 0.8.2 → 0.9.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 +101 -12
- package/dist/index.cjs +1108 -993
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +503 -194
- package/dist/index.d.ts +503 -194
- package/dist/index.js +1106 -990
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A typed multi-agent workflow pipeline built on top of the [Vercel AI SDK v6](htt
|
|
|
4
4
|
|
|
5
5
|
Agents are pure AI SDK wrappers that return native `GenerateTextResult` / `StreamTextResult`. Workflows chain agents into pipelines with automatic stream merging, deterministic agent routing, and typed output extraction.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
It's a lean composition layer over the Vercel AI SDK — not a framework to learn around. Agents return native AI SDK results and workflows merge native streams, so anything you can do with the AI SDK still works underneath; pipeai stays out of the way and stays fully compatible.
|
|
8
8
|
|
|
9
9
|
## Core Concepts
|
|
10
10
|
|
|
@@ -366,6 +366,31 @@ const pipeline = Workflow.create<Ctx>()
|
|
|
366
366
|
|
|
367
367
|
Nested workflows can be arbitrarily deep — a workflow step can contain another workflow that itself contains nested workflows.
|
|
368
368
|
|
|
369
|
+
### Conditional steps via `when` / `otherwise`
|
|
370
|
+
|
|
371
|
+
Any `step` form — agent, inline `step(id, fn)`, or nested `step(workflow)` — accepts a `when` predicate. When it returns false the step is **skipped** and its body never runs:
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
const pipeline = Workflow.create<Ctx, Input>()
|
|
375
|
+
// skip → passthrough: input is forwarded unchanged
|
|
376
|
+
.step(enrichAgent, { when: ({ input }) => input.needsEnrichment })
|
|
377
|
+
// skip → `otherwise` produces the value
|
|
378
|
+
.step("search", runSearch, {
|
|
379
|
+
when: ({ input }) => input.intent === "search",
|
|
380
|
+
otherwise: ({ input }) => ({ ...input, results: [] }),
|
|
381
|
+
})
|
|
382
|
+
// conditionally run a whole sub-pipeline
|
|
383
|
+
.step(productPipeline, { when: ({ input }) => input.intent === "product" });
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
The output type reflects what can actually happen — this is deliberate, so a skipped step can't be mistaken for one that always ran:
|
|
387
|
+
|
|
388
|
+
- **`when` + `otherwise`** → output stays `TNextOutput` (`otherwise` returns it).
|
|
389
|
+
- **`when` without `otherwise`** → output widens to `TOutput | TNextOutput` (skip passes the input through). For same-shape tap/enrich steps the union collapses to a single type; when the shapes differ and you'd rather keep a single type, supply `otherwise` to produce a default.
|
|
390
|
+
- **no `when`** → `TNextOutput`, exactly as before.
|
|
391
|
+
|
|
392
|
+
`when` / `otherwise` throwing propagates as a normal step error (a downstream `.catch()` can observe it). A skipped step still fires the `onStepStart` / `onStepFinish` observability events with its passthrough/`otherwise` value as `output`.
|
|
393
|
+
|
|
369
394
|
### Predicate branching via `branch()`
|
|
370
395
|
|
|
371
396
|
Route to different agents based on runtime conditions. The first matching `when` wins. A case without `when` acts as the default:
|
|
@@ -494,7 +519,7 @@ const pipeline = Workflow.create<Ctx>()
|
|
|
494
519
|
|
|
495
520
|
### Array iteration via `foreach()`
|
|
496
521
|
|
|
497
|
-
`foreach()` maps each element of an array output through an agent or workflow.
|
|
522
|
+
`foreach()` maps each element of an array output through an agent or workflow. By default items run in generate mode — `foreach` never auto-merges, since merging N concurrent streams would interleave into a garbled message (see [Streaming items](#streaming-foreach--parallel-items) to opt in via `handleStream`):
|
|
498
523
|
|
|
499
524
|
```ts
|
|
500
525
|
const summarizer = new Agent<Ctx, string, string>({
|
|
@@ -511,14 +536,16 @@ const pipeline = Workflow.create<Ctx>()
|
|
|
511
536
|
.step("combine", ({ input }) => input.join("\n\n"));
|
|
512
537
|
```
|
|
513
538
|
|
|
514
|
-
|
|
539
|
+
By default `foreach` is **unbounded** — every item runs concurrently. Pass `concurrency` to throttle (e.g. against provider rate limits):
|
|
515
540
|
|
|
516
541
|
```ts
|
|
517
|
-
//
|
|
542
|
+
// Cap at 3 items in flight; the next launches as soon as one finishes.
|
|
518
543
|
.foreach(summarizer, { concurrency: 3 })
|
|
519
544
|
```
|
|
520
545
|
|
|
521
|
-
`concurrency` is the **maximum number of items in flight at any moment** — backed by a
|
|
546
|
+
`concurrency` is the **maximum number of items in flight at any moment** — backed by a worker pool. There's no lockstep batching: a slow item never blocks a finished slot from picking up the next pending one.
|
|
547
|
+
|
|
548
|
+
> **Rate-limit hazard:** the unbounded default fires all items at once. For large arrays against a rate-limited provider, set an explicit `concurrency`.
|
|
522
549
|
|
|
523
550
|
Works with nested workflows too:
|
|
524
551
|
|
|
@@ -532,6 +559,59 @@ const pipeline = Workflow.create<Ctx>()
|
|
|
532
559
|
.foreach(processItem, { concurrency: 5 });
|
|
533
560
|
```
|
|
534
561
|
|
|
562
|
+
#### Per-item pipelines: the builder-callback form
|
|
563
|
+
|
|
564
|
+
Each item runs its **entire** sub-workflow as one independent unit, so item 0 can be at the
|
|
565
|
+
last step while item 1 is still at the first — true per-item pipeline parallelism, with the only
|
|
566
|
+
barrier at the end (collecting the `Result[]`). When the per-item path is specific to this
|
|
567
|
+
`foreach`, you don't need to declare a separate named workflow: pass a **builder callback** and
|
|
568
|
+
the element type is inferred for you.
|
|
569
|
+
|
|
570
|
+
```ts
|
|
571
|
+
const pipeline = Workflow.create<Ctx>()
|
|
572
|
+
.step("fetch-items", async ({ ctx }) => ctx.db.items.getAll()) // Item[]
|
|
573
|
+
.foreach(
|
|
574
|
+
item => item // `item` is a sub-builder seeded with the element type
|
|
575
|
+
.step("normalize", ({ input }) => normalize(input))
|
|
576
|
+
.step(analyzeAgent)
|
|
577
|
+
.step(enrichAgent),
|
|
578
|
+
{ concurrency: 5 }, // up to 5 items running their full path at once
|
|
579
|
+
);
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
This is exactly equivalent to passing the pre-built `processItem` workflow above — same
|
|
583
|
+
concurrency, same collect-at-end semantics — it just saves the `Workflow.create<Ctx, Item>()`
|
|
584
|
+
boilerplate and infers the item type from the array. All `foreach` options (`concurrency`,
|
|
585
|
+
`onError`, `id`) apply unchanged. A gate inside the per-item path is forbidden, same as any
|
|
586
|
+
`foreach` body.
|
|
587
|
+
|
|
588
|
+
#### Streaming `foreach` / `parallel` items
|
|
589
|
+
|
|
590
|
+
When the workflow is run with `.stream(...)`, pass `handleStream` to `foreach` or `parallel` to run each **agent** item/branch in stream mode and control how it surfaces to the writer — the same hook as a single `.step(agent)`, plus an `itemIndex`:
|
|
591
|
+
|
|
592
|
+
```ts
|
|
593
|
+
// foreach: itemIndex is the numeric item index
|
|
594
|
+
.foreach(summarizer, {
|
|
595
|
+
handleStream: ({ result, writer, input, itemIndex }) => {
|
|
596
|
+
writer.write({ type: "data-item-start", data: { itemIndex } });
|
|
597
|
+
writer.merge(result.toUIMessageStream());
|
|
598
|
+
},
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
// parallel record form: itemIndex is the branch key
|
|
602
|
+
.parallel({ summary: summarizer, sentiment: classifier }, {
|
|
603
|
+
handleStream: ({ result, writer, itemIndex }) => {
|
|
604
|
+
if (itemIndex === "summary") writer.merge(result.toUIMessageStream());
|
|
605
|
+
},
|
|
606
|
+
})
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
- **No `handleStream`** → agent items run in generate mode (no auto-merge). `foreach`/`parallel` never auto-merge; you opt into surfacing explicitly.
|
|
610
|
+
- **`SealedWorkflow` items/branches** stream transitively via their own steps when the parent streams — `handleStream` is not called for them.
|
|
611
|
+
- `itemIndex`: `number` for `foreach` and tuple `parallel`; the key (`string`) for record `parallel`. `branch` threads the matched key (select) / case index (predicate) into its existing `handleStream`.
|
|
612
|
+
- Both default to unbounded concurrency, so streamed parts **interleave** (id-keyed, non-corrupting, but nondeterministic order). Set `concurrency: 1` if you want each item/branch to stream sequentially in order.
|
|
613
|
+
- Generate-mode runs (`.generate(...)`) never call `handleStream`.
|
|
614
|
+
|
|
535
615
|
#### Per-item error recovery via `onError`
|
|
536
616
|
|
|
537
617
|
By default a single item's failure aborts the whole `foreach`. Pass an `onError` handler to recover individual items — return a substitute value, return `Workflow.SKIP` to drop the failed index from the output array, or rethrow to abort the step (the throw is catchable by a downstream `.catch()`):
|
|
@@ -572,7 +652,9 @@ const pipeline = Workflow.create<Ctx, string>()
|
|
|
572
652
|
.parallel([researcher, critic] as const);
|
|
573
653
|
```
|
|
574
654
|
|
|
575
|
-
The same input (`state.output`) is fed to each branch.
|
|
655
|
+
The same input (`state.output`) is fed to each branch. By default `parallel` is **unbounded** — all branches run concurrently. Pass an explicit `concurrency` to throttle against rate-limit pressure.
|
|
656
|
+
|
|
657
|
+
Like `foreach`, `parallel` runs agent branches in generate mode and never auto-merges; pass `handleStream` to surface branch streams in a `.stream(...)` run — see [Streaming items](#streaming-foreach--parallel-items).
|
|
576
658
|
|
|
577
659
|
```ts
|
|
578
660
|
.parallel({ a, b, c, d, e, f, g, h }, { concurrency: 3 }) // explicit override
|
|
@@ -599,7 +681,7 @@ The same input (`state.output`) is fed to each branch. Default concurrency is `m
|
|
|
599
681
|
|
|
600
682
|
Gates inside parallel branches throw `NestedGateUnsupportedError`, same as `foreach` concurrent. The lowest-index suspending branch wins the marker; others contribute to `siblingSuspensions`. Multi-branch suspension semantics are finalized in F0.6 alongside `cancelOnFirstSuspend` — until then, all branches run to completion (or sibling-failure) before the marker reaches the caller.
|
|
601
683
|
|
|
602
|
-
> **Rate-limit hazard:** `parallel`'s default
|
|
684
|
+
> **Rate-limit hazard:** `parallel`'s unbounded default fires all branches at once. With many branches on a rate-limited provider, set an explicit `concurrency`. Symptoms of overflow: 429s and stair-stepped latency.
|
|
603
685
|
|
|
604
686
|
> **Concurrent ctx-mutation hazard:** branches share the `ctx` object by reference. Treat `ctx` as immutable inside parallel branches.
|
|
605
687
|
|
|
@@ -636,6 +718,8 @@ Use `while` for the opposite condition (repeat while true, stop when false):
|
|
|
636
718
|
|
|
637
719
|
The `until` and `while` options are mutually exclusive — TypeScript enforces this at compile time.
|
|
638
720
|
|
|
721
|
+
Both forms are **do-while**: the body always runs at least once, then the predicate is checked against its `output`. So `while: () => false` still runs the body once — it is not a pre-check.
|
|
722
|
+
|
|
639
723
|
When `maxIterations` is exceeded, a `WorkflowLoopError` is thrown — catchable by `.catch()`:
|
|
640
724
|
|
|
641
725
|
```ts
|
|
@@ -692,7 +776,7 @@ const { stream, output } = pipeline.stream(ctx, initialInput, {
|
|
|
692
776
|
| `.step(id, fn)` | Transform the output. `fn` receives `{ ctx, input }` and returns the new output. |
|
|
693
777
|
| `.branch([...cases])` | Predicate routing. First `when` match wins; case without `when` is default. |
|
|
694
778
|
| `.branch({ select, agents })` | Key routing. `select` returns a key, runs the matching agent. |
|
|
695
|
-
| `.foreach(target, opts?)` | Map each array element through an agent or workflow. `opts.concurrency` is the max items in flight (default:
|
|
779
|
+
| `.foreach(target, opts?)` | Map each array element through an agent or workflow. `opts.concurrency` is the max items in flight (default: unbounded). `opts.onError` recovers per-item failures; return `Workflow.SKIP` to drop an index. |
|
|
696
780
|
| `.repeat(target, opts)` | Loop an agent or workflow. Use `{ until }` or `{ while }` (mutually exclusive). `maxIterations` defaults to 10. |
|
|
697
781
|
| `.gate(id, opts?)` | Human-in-the-loop suspension point. Returns a result with `status: "suspended"` carrying a serializable snapshot. Resume via `loadState(gateId, snapshot)`. |
|
|
698
782
|
| `.catch(id, fn)` | Handle errors. `fn` receives `{ error, ctx, lastOutput, stepId }` and returns a recovery value. Bypassed on suspension. |
|
|
@@ -971,9 +1055,9 @@ const final = await resumed.generate(ctx); // no response arg — state is see
|
|
|
971
1055
|
|
|
972
1056
|
### Cadence
|
|
973
1057
|
|
|
974
|
-
- `checkpointEvery: N` — fire every N executable steps. Defaults to `max(1, ceil(
|
|
1058
|
+
- `checkpointEvery: N` — fire every N executable steps. Defaults to `max(1, ceil(stepCount / 4))` — 4 checkpoints across the run, floor of every step on tiny pipelines.
|
|
975
1059
|
- `checkpointWhen({ stepIndex, stepId, ctx }) => boolean` — predicate variant. Mutually exclusive with `checkpointEvery`.
|
|
976
|
-
- `.catch()`
|
|
1060
|
+
- The default-cadence denominator counts only checkpointable steps (`step` / `branch` / `foreach` / `repeat` / `parallel` / nested). `gate` nodes suspend or skip and never checkpoint, and `.catch()` / `.finally()` are cleanup — none of them count, so adding them doesn't shift the cadence.
|
|
977
1061
|
|
|
978
1062
|
### Timeout via `AbortSignal`
|
|
979
1063
|
|
|
@@ -1068,9 +1152,12 @@ Pass an `observability` object to `Workflow.create()` to receive lifecycle event
|
|
|
1068
1152
|
```ts
|
|
1069
1153
|
import { Workflow, type WorkflowObservability } from "pipeai";
|
|
1070
1154
|
|
|
1071
|
-
|
|
1155
|
+
// `WorkflowObservability<Ctx>` types `ctx` in every hook as your context.
|
|
1156
|
+
// It defaults to `unknown`, so the bare `WorkflowObservability` form still
|
|
1157
|
+
// works for context-agnostic hooks.
|
|
1158
|
+
const obs: WorkflowObservability<Ctx> = {
|
|
1072
1159
|
onStepStart: ({ stepId, type, ctx, input }) => {
|
|
1073
|
-
console.log(`step ${stepId} (${type}) starting`);
|
|
1160
|
+
console.log(`[${ctx.requestId}] step ${stepId} (${type}) starting`);
|
|
1074
1161
|
},
|
|
1075
1162
|
onStepFinish: ({ stepId, type, output, durationMs, suspended }) => {
|
|
1076
1163
|
console.log(`step ${stepId} (${type}) finished in ${durationMs}ms, suspended=${suspended}`);
|
|
@@ -1085,6 +1172,8 @@ const pipeline = Workflow.create<Ctx, string>({ observability: obs })
|
|
|
1085
1172
|
.step("respond", responder);
|
|
1086
1173
|
```
|
|
1087
1174
|
|
|
1175
|
+
`ctx` is typed as the workflow's context: pass `WorkflowObservability<Ctx>` (or just inline the object into `Workflow.create<Ctx>({ observability: { ... } })` and let `Ctx` flow in). The `input` / `output` fields stay `unknown` — they differ at every step in the chain, so only `ctx` (constant across the run) can be typed.
|
|
1176
|
+
|
|
1088
1177
|
The hooks are threaded through every builder return, so any chain following `Workflow.create({ observability })` keeps the same hooks. `ResumedWorkflow` (gate resume via `loadState`) and `CheckpointResumedWorkflow` (checkpoint resume via `resumeFrom`) ALSO inherit it — events fire on resumed runs without re-wiring.
|
|
1089
1178
|
|
|
1090
1179
|
### Per-node firing rules
|