Module: the bare LM call (Predict<S>), ChainOfThought<S>, and any struct you compose from them. Swapping Predict<QA> for ChainOfThought<QA> changes the output type, and the compiler surfaces every downstream site that must change.
Usage
Given the QA signature from Signatures, the strategy is the only thing that changes between these two calls:ChainOfThought<QA> returns WithReasoning<QAOutput> instead of QAOutput, so code that consumes the extra field is type-checked.
A custom module is a struct holding predictor fields plus a Module impl whose forward body is ordinary Rust:
predictors! line is what makes the module optimizable and persistable: it names the Predict leaves for optimizer discovery (see Predictor discovery below). forward is plain async Rust, so branching, loops, and early returns between the LM calls need no framework support.
Source: crates/dspy-rs/src/core/module.rs, modules/chain_of_thought.rs, augmentation.rs. All items below are re-exported at the crate root unless noted.
The Module trait
Every call returns
Predicted<Output>: the output struct (accessible directly via Deref) plus CallMetadata (token counts, raw response, tool traces) via .metadata(). Errors are always PredictError.
forward takes input by value. This is deliberate: pipeline authors move fields into sub-module inputs with zero clones. The cost is one input clone per example in evaluation loops that reuse a trainset.
To author a module: define a struct holding Predict/ChainOfThought fields, declare those fields with predictors! so optimizers and ModuleState can address them by name, and implement forward, as in the usage example above.
Predictor discovery: Predictors
Optimizable leaves are declared explicitly — there is no reflection walker and no derive magic. A module that wants to be optimizable (or persistable via ModuleState) implements the Predictors trait, almost always through the predictors! macro:
- They become the leaf’s trace-span component name (the optimizer stamps them via
PredictorInfo::set_trace_nameonce per run). - Optimizer candidates address leaves by these names (ambient
fx::Paramsentries bind per leaf at call time). ModuleStatepersists per-leaf state under them.
predictors()/predictors_mut(). PredictorInfo is the typed, object-safe per-leaf view: read methods (schema(), instruction(), default_instruction(), demos_as_json(), dump_state()) plus two boundary mutations — set_trace_name (the naming pass) and load_state (the install seam, used by ModuleState::apply and the optimizer’s one-shot install of the winning candidate; candidate evaluation never calls it). See Optimizers.
Batch execution: forward_all
forward_all is a free function, not a trait method.
ChainOfThought<S>
ChainOfThought is pure sugar, a type alias rather than a distinct struct:
Reasoning is an augmentation that prepends reasoning: String as the first output field. The LM generates the reasoning text before the answer fields, so the chain of thought is in context when subsequent fields are produced.
For reasoning models (o1, o3, DeepSeek-R1) prefer bare
Predict. An explicit reasoning field on top of internal thinking is redundant and can hurt quality.Agent loops
There is noReAct module. The tool-loop strategy lives in the IR instead: attach tools to a Predict (which executes as a 1-node agent program, see Predict), or declare the loop as a first-class AgentLoop node with the #[agent] macro inside a #[module]. See Tools and agents.
Augmentation
Signature augmentation adds output fields that the LM actually generates. It is a prompt schema modification, not metadata and not data synthesis: the added field appears in the rendered output format, and the model fills it in.
Augmentations are usually derived:
With{Name}<O> wrapper struct: the augmentation fields plus a flattened inner: O. With prepend the added fields come before inner in the output schema (how Reasoning guarantees reasoning is generated first); without it they follow. Every field must be #[output] (#[input] is rejected), descriptions come from doc comments or #[output(desc = "...")], and #[alias("...")] renames the serialized field. WithReasoning<O> is exactly this expansion for Reasoning.
See also
- Predict and PredictBuilder for the leaf module and its builder methods
- Signatures for
Signature,Schema, and generated input/output structs - The module macro for declaring modules as bodyless functions
- Optimizers for how
Predictleaves are discovered through modules - How DSRs thinks for the call path from module to LM
- ChainOfThought smoke example
- Module authoring smoke example
- Module iteration example
