File size: 12,831 Bytes
116524e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
# API Reference

Complete reference for all public classes, methods, and enums in the pipeline engine.

---

## `pipeline.context`

### `StepContext`

Frozen dataclass passed from step to step. The pipeline engine only reads `sample` and `metadata` β€” domain-specific fields are added by subclassing.

```python

@dataclass(frozen=True)

class StepContext:

    sample: Any = None

    metadata: MappingProxyType = field(

        default_factory=lambda: MappingProxyType({})

    )

```

| Method | Signature | Description |
|--------|-----------|-------------|
| `replace` | `(**changes: Any) -> StepContext` | Return a new context with the given fields replaced. Uses `dataclasses.replace` internally. |

**Behavior:**

- `metadata` is auto-coerced from `dict` to `MappingProxyType` in `__post_init__`
- Subclasses inherit `.replace()` β€” it works on all fields including subclass-defined ones

---

## `pipeline.protocol`

### `StepProtocol`

Structural protocol that every step (and Pipeline/Branch) must satisfy.

```python

@runtime_checkable

class StepProtocol(Protocol):

    requires: AbstractSet[str]

    provides: AbstractSet[str]



    def __call__(self, ctx: StepContext) -> StepContext: ...

```

| Attribute | Type | Description |
|-----------|------|-------------|
| `requires` | `AbstractSet[str]` | Metadata keys the step reads |
| `provides` | `AbstractSet[str]` | Metadata keys the step writes |
| `__call__` | `(StepContext) -> StepContext` | Execute the step |

**Notes:**

- `AbstractSet[str]` accepts both `set` and `frozenset`
- `@runtime_checkable` enables `isinstance(step, StepProtocol)` checks

---

### `SampleResult`

Outcome for one sample after the pipeline has run.

```python

@dataclass

class SampleResult:

    sample: Any

    output: StepContext | None

    error: Exception | None

    failed_at: str | None

    cause: Exception | None = None

```

| Field | Type | Description |
|-------|------|-------------|
| `sample` | `Any` | The original input sample |
| `output` | `StepContext \| None` | Final context (`None` if any step failed) |
| `error` | `Exception \| None` | The exception (`None` if succeeded) |
| `failed_at` | `str \| None` | Class name of the step that raised (`None` if succeeded) |
| `cause` | `Exception \| None` | Inner exception for `BranchError` failures (default `None`) |

**Notes:**

- Mutable β€” background threads update it in-place when background steps complete
- For background steps, `output`/`error` may be `None` until `wait_for_background()` completes

---

## `pipeline.pipeline`

### `Pipeline`

Ordered sequence of steps. Satisfies `StepProtocol` β€” can be nested inside other pipelines.

#### Constructor

```python

Pipeline(steps: list | None = None, hooks: list[PipelineHook] | None = None)

```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `steps` | `list \| None` | `None` | Optional initial list of steps |
| `hooks` | `list[PipelineHook] \| None` | `None` | Observation-only hooks fired around each foreground step |

Validates step ordering and infers contracts at construction time.

#### Attributes

| Attribute | Type | Description |
|-----------|------|-------------|
| `requires` | `frozenset[str]` | Fields the pipeline needs from external context (auto-inferred) |
| `provides` | `frozenset[str]` | Fields the pipeline writes (auto-inferred, union of all steps) |

#### Methods

##### `then`

```python

def then(self, step: object) -> Pipeline

```

Append a step and return `self` for chaining. Validates ordering immediately.

| Parameter | Type | Description |
|-----------|------|-------------|
| `step` | `object` | Any object satisfying `StepProtocol` |

**Returns:** `self` (for method chaining)

**Raises:** `PipelineOrderError` if the step requires a field produced by a later step

---

##### `branch`

```python

def branch(

    self,

    *pipelines: object,

    merge: MergeStrategy | Callable = MergeStrategy.RAISE_ON_CONFLICT,

) -> Pipeline

```

Append a `Branch` step and return `self` for chaining. Shorthand for `.then(Branch(*pipelines, merge=merge))`.

**Returns:** `self` (for method chaining)

---

##### `run`

```python

def run(

    self,

    contexts: Iterable[StepContext],

    workers: int = 1,

    on_sample_done: Callable[[SampleResult], None] | None = None,

    cancel_token: CancellationToken | None = None,

) -> list[SampleResult]

```

Process contexts through the pipeline (sync entry point).

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `contexts` | `Iterable[StepContext]` | β€” | Input contexts to process |
| `workers` | `int` | `1` | Max concurrent samples in foreground steps |
| `on_sample_done` | `Callable \| None` | `None` | Callback after each sample's foreground steps complete (or fail). Must not block. |
| `cancel_token` | `CancellationToken \| None` | `None` | Cancellation signal. Checked before each step and each new sample. Pass a fresh token per invocation. |

**Returns:** `list[SampleResult]` β€” one result per input context

**Notes:** Calls `asyncio.run(self.run_async(...))` internally. For background steps, call `wait_for_background()` after this returns. When `cancel_token` is provided, also sets `cancel_token_var` so code inside steps (e.g. LLM clients) can read it.

---

##### `run_async`



```python

async def run_async(
    self,

    contexts: Iterable[StepContext],

    workers: int = 1,

    on_sample_done: Callable[[SampleResult], None] | None = None,

    cancel_token: CancellationToken | None = None,

) -> list[SampleResult]

```


Async entry point. Use `await pipe.run_async(contexts)` from coroutine contexts.

Same parameters and return type as `run()`.

---

##### `__call__`

```python

def __call__(self, ctx: StepContext) -> StepContext

```

Run all steps sequentially on a single context. Used when the pipeline is nested as a step inside another pipeline.

**Notes:** `async_boundary` markers are ignored in this mode β€” all steps run to completion.

---

##### `wait_for_background`

```python

def wait_for_background(self, timeout: float | None = None) -> None

```

Block until all background tasks complete.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `timeout` | `float \| None` | `None` | Max seconds to wait. `None` = wait indefinitely. |

**Raises:** `TimeoutError` if timeout elapses before completion

---

##### `background_stats`



```python

def background_stats(self) -> dict[str, int]
```



Return a snapshot of background task progress. Thread-safe.



**Returns:** `{"active": int, "completed": int}`



---



## `pipeline.branch`



### `MergeStrategy`



Enum of built-in merge strategies for `Branch` outputs.



```python

class MergeStrategy(Enum):

    RAISE_ON_CONFLICT = "raise_on_conflict"

    LAST_WRITE_WINS = "last_write_wins"

    NAMESPACED = "namespaced"

```

| Value | Behavior |
|-------|----------|
| `RAISE_ON_CONFLICT` | Raises `ValueError` if two branches write different values to the same named field. Metadata merges with last-writer-wins. |
| `LAST_WRITE_WINS` | Last branch's value wins for every conflicting field. |
| `NAMESPACED` | Each branch's output stored at `metadata["branch_N"]`. No conflict possible. |

---

### `Branch`

Runs multiple pipelines in parallel, then merges their outputs. Satisfies `StepProtocol`.

#### Constructor

```python

Branch(

    *pipelines: object,

    merge: MergeStrategy | Callable = MergeStrategy.RAISE_ON_CONFLICT,

)

```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `*pipelines` | `object` | β€” | Child pipelines to run in parallel (at least one required) |
| `merge` | `MergeStrategy \| Callable` | `RAISE_ON_CONFLICT` | Merge strategy or custom `fn(list[StepContext]) -> StepContext` |

**Raises:** `ValueError` if no pipelines are provided

#### Attributes

| Attribute | Type | Description |
|-----------|------|-------------|
| `requires` | `frozenset[str]` | Union of all children's requires |
| `provides` | `frozenset[str]` | Union of all children's provides |
| `pipelines` | `list` | The child pipelines |

#### Methods

##### `__call__`

```python

def __call__(self, ctx: StepContext) -> StepContext

```

Sync fan-out via `ThreadPoolExecutor`. All branches run to completion before any failure is raised.

**Raises:** `BranchError` if any branch fails

---

##### `__call_async__`



```python

async def __call_async__(self, ctx: StepContext) -> StepContext
```



Async fan-out via `asyncio.gather`. Sync children are wrapped with `asyncio.to_thread`.



**Raises:** `BranchError` if any branch fails



---



## `pipeline.protocol`  β€” Hooks



### `PipelineHook`



Observation-only protocol fired around each foreground step. Hooks cannot modify context β€” both methods return `None`.



```python

@runtime_checkable

class PipelineHook(Protocol):

    def before_step(self, step_name: str, ctx: StepContext) -> None: ...

    def after_step(self, step_name: str, ctx: StepContext) -> None: ...

```

| Method | Parameters | Description |
|--------|-----------|-------------|
| `before_step` | `step_name: str, ctx: StepContext` | Called before each foreground step executes |
| `after_step` | `step_name: str, ctx: StepContext` | Called after each foreground step completes |

**Notes:**

- `step_name` is `type(step).__name__` β€” hooks know what ran but cannot inspect or mutate the step instance
- Hooks fire for foreground steps only β€” background steps (after `async_boundary`) do not trigger hooks
- If a hook raises, the pipeline logs the error and continues β€” a broken hook never kills the pipeline
- For `Branch` steps, hooks fire once for `"Branch"` as a whole, not for inner steps

---

## `pipeline.errors`

### `CancellationToken`

Thread-safe cancellation signal. Create a fresh token per `run()` invocation.

```python

class CancellationToken:

    def cancel(self) -> None: ...



    @property

    def is_cancelled(self) -> bool: ...

```

| Method / Property | Description |
|-------------------|-------------|
| `cancel()` | Signal cancellation. Thread-safe, idempotent. |
| `is_cancelled` | `True` after `cancel()` has been called. |

---

### `cancel_token_var`

`ContextVar` set by `Pipeline.run_async()` so code inside steps (e.g. LLM clients) can read the current cancel token without parameter changes.

```python

cancel_token_var: ContextVar[CancellationToken | None]  # default: None

```

**Notes:**

- Set before steps run, reset after `run_async()` completes
- `asyncio.to_thread()` copies contextvars automatically β€” visible in sync steps too
- Read with `cancel_token_var.get(None)` β€” returns `None` when no pipeline is running

---

### `PipelineCancelled`

```python

class PipelineCancelled(Exception): ...

```

A `cancel_token` was triggered. Surfaces in `SampleResult.error` β€” never propagated to the caller of `run()`. Callers check `isinstance(result.error, PipelineCancelled)` to distinguish cancellation from step failures.

---

### `PipelineOrderError`

```python

class PipelineOrderError(Exception): ...

```

A step requires a field that no earlier step provides (but a later step does). Raised at **construction time**.

---

### `PipelineConfigError`

```python

class PipelineConfigError(Exception): ...

```

Invalid pipeline wiring. Raised at **construction time**. Examples:

- More than one `async_boundary = True` step in the same pipeline
- An `async_boundary = True` step inside a `Branch` child

---

### `BranchError`

```python

class BranchError(Exception):

    failures: list[BaseException]

```

One or more branch pipelines failed. All branches always run to completion before this is raised. Raised at **runtime**.

| Attribute | Type | Description |
|-----------|------|-------------|
| `failures` | `list[BaseException]` | One exception per failed branch |

---

## Step class attributes

Optional attributes a step class can declare to control pipeline behavior:

| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| `requires` | `set[str] \| frozenset[str]` | *(required)* | Metadata keys the step reads |
| `provides` | `set[str] \| frozenset[str]` | *(required)* | Metadata keys the step writes |
| `async_boundary` | `bool` | `False` | Marks the foreground/background split point |
| `max_workers` | `int` | `1` | Max concurrent background threads for this step class |