Skip to main content
A module is a prompting strategy over a signature. Everything callable in dsrs implements 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:
The 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:
expands to
Each field’s identifier becomes its leaf name. The names are the canonical identity of each leaf — the trace-name contract:
  1. They become the leaf’s trace-span component name (the optimizer stamps them via PredictorInfo::set_trace_name once per run).
  2. Optimizer candidates address leaves by these names (ambient fx::Params entries bind per leaf at call time).
  3. ModuleState persists per-leaf state under them.
Names must be unique within a module and stable across 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 no ReAct 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:
The derive generates a 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