Skip to main content
A module wraps one or more predictors into a prompting strategy. ChainOfThought makes the LM reason before answering. You swap strategies by changing a type — everything else stays the same.

The idea

A Predict<S> calls the LM directly against your signature. A module adds behavior around that call — extra output fields, retry loops, tool use — without changing your signature definition.
Both return your answer field. ChainOfThought adds reasoning on top. Your signature didn’t change. The prompting strategy did.

How augmented output works

ChainOfThought returns WithReasoning<QAOutput>, not bare QAOutput. But you rarely write that type — inference handles it:
Rust’s Deref coercion makes the wrapper transparent. result.answer resolves automatically. Your IDE shows both reasoning and answer in autocomplete. When you do need to name the type (function signatures, struct fields):
WithReasoning<QAOutput> reads as English: “QA output, with reasoning.”

ChainOfThought

Prepends a reasoning field to the output. The LM thinks step-by-step before producing your output fields.

With instruction override

With demos

Demos for ChainOfThought include reasoning — they’re Example<Augmented<S, Reasoning>>. The reasoning field shows the LM what good chain-of-thought looks like.
WithReasoning has two fields: reasoning: String and inner: O (your output type). The Deref to O is just for ergonomic field access — when constructing, you build both parts explicitly. In practice you rarely write demos by hand. Optimizers generate them automatically.

Custom modules

Define a struct, derive Facet, implement Module.
Usage:
#[derive(facet::Facet)] on the struct is what makes optimizer discovery work — the framework finds retrieve and answer’s inner predictor automatically without annotations. See Optimization for details.

call vs forward

call is the user-facing entry point. forward is the implementation hook you override. call currently delegates to forward — the split exists so hooks, tracing, and usage tracking can wrap call without breaking module implementations.

Output transforms without impl Module

For simple post-processing, use .map() instead of writing a full module:
.and_then() for fallible transforms that return Result<T, PredictError>. Combinators preserve optimizer discovery — the framework sees through .map() and .and_then() to find the Predict leaves inside.

Batch calls

Run a module over many inputs concurrently:
The third argument is max concurrency. Each result is independent — one failure doesn’t stop the others. Shows a progress bar on stderr.

Swapping strategies

Modules are interchangeable when they share the same input type. Change a type annotation, the compiler tells you what else to update:
Changing the strategy may change the output type — Predict<QA> returns QAOutput, ChainOfThought<QA> returns WithReasoning<QAOutput>. The compiler catches every downstream breakage. No runtime surprises. For generic pipelines that accept any strategy:

Where it fits

A Module doesn’t call the LM directly. It orchestrates one or more Predict instances that do. The optimizer reaches through the module to find and tune those Predict leaves. Your module’s forward logic stays the same — the optimizer changes what the LM sees (demos, instructions), not how your code runs.