ChainOfThought makes the LM reason before answering. You swap strategies by changing a type — everything else stays the same.
The idea
APredict<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.
answer field. ChainOfThought adds reasoning on top. Your signature didn’t change. The prompting strategy did.
How augmented output works
ChainOfThought returnsWithReasoning<QAOutput>, not bare QAOutput. But you rarely write that type — inference handles it:
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 areasoning 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’reExample<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.#[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:Swapping strategies
Modules are interchangeable when they share the same input type. Change a type annotation, the compiler tells you what else to update: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
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.
| Module | What it does | Output type | Internal Predicts |
|---|---|---|---|
Predict<S> | Direct LM call | S::Output | 1 (itself) |
ChainOfThought<S> | Reason then answer | WithReasoning<S::Output> | 1 |
| Custom | Your logic | Your choice | Your Predicts |
