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>, ReAct<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 facet::Facet derive is what lets optimizer discovery find the Predict leaves inside the struct. 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, core/module_ext.rs, modules/chain_of_thought.rs, modules/react.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, derive facet::Facet so the optimizer’s walker can discover the Predict leaves, and implement forward, as in the usage example above.

Batch execution: forward_all

forward_all is a free function, not a trait method.

Combinators: ModuleExt

ModuleExt is blanket-implemented for every Module. It post-processes output without a full impl Module. Both wrappers keep Input = M::Input, set Output = T, and pass CallMetadata through unchanged. The wrapper structs derive Facet with the inner module as a real field (the closure is #[facet(opaque, skip)]), so the inner Predict leaves remain visible to optimizer discovery through the wrapper.

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.

ReAct<S>

ReAct<S> runs a thought, action, observation loop over a set of tools, then extracts a typed answer. Bounds: S: Signature, S::Input: Schema + Clone, S::Output: Schema. As a Module: Input = S::Input, Output = S::Output (no wrapper type). Internally it holds two Predict leaves: an action step (inputs input, trajectory; outputs thought, action, action_input, all strings) and an extract step (inputs input, trajectory; output S::Output). Both are visible to optimizer discovery.

Loop semantics

  1. The input struct is serialized to JSON and becomes the input field of both internal signatures.
  2. The trajectory is seeded with a tool manifest (Available tools: with each tool’s name and description, or (none)).
  3. Each step, up to max_steps (default 4): the action predictor produces thought, action, action_input. The action name is trimmed of whitespace and surrounding quotes.
  4. If the action is finish, final, or done (case insensitive), the loop stops.
  5. Otherwise the named tool runs with action_input as its argument string. Matching is case insensitive: exact name, or substring containment in either direction. An unknown name yields the observation tool_not_found: {name}; a tool error yields tool_error: {err}. The step (thought, action, input, observation) is appended to the trajectory.
  6. After the loop ends (terminal action or steps exhausted), the extract predictor reads the full trajectory and returns S::Output.
Metadata: each executed tool is recorded in CallMetadata::tool_calls with id react-step-{n}, and the manifest plus formatted per-step traces appear in CallMetadata::tool_executions, merged with the extract call’s metadata.

Builder

ReAct::<S>::new() equals ReAct::<S>::builder().build(). The builder type is ReActBuilder<S>; it is not re-exported at the crate root (full path dspy_rs::modules::react::ReActBuilder), so obtain it through ReAct::<S>::builder(). Tools implement rig’s ToolDyn. 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