Predict<S> is the leaf module. One Predict is one prompt template and one LM call: it formats a signature’s fields, instruction, and demos into a prompt, sends it to the configured LM, and parses the response into S::Output. Every other module ultimately delegates to one or more Predict leaves, and optimizers tune a program by rewriting Predict state (instruction override and demos). See How DSRs thinks for the mental model.
Usage
QAInput and QAOutput from the field markers. .call() returns Result<Predicted<QAOutput>, PredictError>, and Predicted<O> implements Deref<Target = O>, so output fields read directly off the result. Everything beyond the defaults, such as demos, an instruction override, tools, or a per-instance LM, goes through the builder.
Construction
PredictBuilder<S>
Demo<S> is the typed input/output pair for few-shot prompting: Demo::new(input, output) with public fields input: S::Input and output: S::Output. Demos render as user/assistant exchanges in the prompt, and the types guarantee a demo matches the signature — a Demo<QA> cannot be attached to a Predict<SummarizeSig>. To seed a demo from a labeled trainset row, project the row through its ToInput/ToOutput impls: Demo::new(row.to_input()?, row.to_output()?), see Data. Tools are settable only at build time. Demos and the instruction override are also writable after construction through the state-install seam (PredictorInfo::load_state, used by ModuleState::apply and the optimizer’s final install of the winning candidate), see State. Every state mutation invalidates the cached instance overlay.
Calling
Predict<S> executes as a 1-node IR Program: a predict leaf named after the component, over SignatureDef::of::<S>() — or an agent leaf when tools are attached (the IR says Predict carries no tools; a tooled predictor is an agent loop). Each call executes this pipeline:
- Build (once, then cache) the 1-node program. The leaf name is the component name, so span identity and capture/replay keying are unchanged.
- Resolve the LM — the per-instance
.lm(...)if set, otherwise the globalconfigure()LM — and load the Interpreter against it (cached; reloaded when the resolved LM changes). - Compose the effective
ir::Overlay: instance state (instruction override + demos, minted once as an overlay against the cached program) plus any ambient optimizer candidate (fx::with_params/fx::with_overlay), ambient values winning per slot. - Run the interpreter (
run_collecting), which renders the prompt, consults any active replay scope, calls the LM (for anagentleaf: the tool loop, with the defaultStopSpec—until_parse,max_turns = 8), and parses the response through the[[ ## field ## ]]protocol, evaluating#[check]and#[assert]constraints. - Reassemble the run’s per-leaf metadata (
LeafOutcome) intoCallMetadata, and record a trace span when inside acapture()scope.
build_chat/call_and_parse are the conversation surface: the caller owns the Chat between turns. Both are thin wrappers over the interpreter’s conversation entry (Runtime): build_chat renders the opening turn through Interpreter::conversation_opening, and call_and_parse sends the chat through Interpreter::run_conversation — the same overlay-resolved rendering, span recording, and replay interception as the typed call path, so a conversation turn and a typed call over the same state produce byte-identical prompts. A turn is not a run: each call_and_parse records one trace span, and seq increments per turn. When tools are attached, the turn runs the same AgentLoop as the typed path, dispatching tool calls through the attached executors; for the “return me the tool calls, I’ll execute them” pattern, use Interpreter::run_conversation_caller_managed directly.
Replay interception
Before any provider call, the interpreter consults the active replay scope — typedcalls and conversation turns alike. A Serve directive returns the recorded span with zero provider calls and zero tool re-executions; a Refuse directive returns PredictError::Replay; Live (or no scope) proceeds normally. Served predictions carry no per-field parse metadata. See Traces.
Predicted<O>
Every call returns Predicted<O>: the typed output plus runtime bookkeeping. It implements Deref<Target = O>, so output fields read directly (result.answer).
CallMetadata
Each
FieldMeta records raw_text (the text the LM produced for that field), flags (Vec<Flag>, non-fatal coercion observations such as a stripped code fence), and checks (Vec<ConstraintResult> with label, expression, passed). Accessors: field_meta(), field_raw(field), field_flags(field), field_checks(field), field_names(), and has_failed_checks() for a quick scan across all fields. Failed #[check] constraints land here; failed #[assert] constraints become a Parse error instead.
PredictError
PredictError::class() buckets into ErrorClass (BadRequest, Temporary, BadResponse, Internal); is_retryable() drives retry logic. Parse errors include the raw response and the token usage: failed parses still consume tokens.
ToolSet
ToolSet is pre-fetched tool definitions plus name-indexed executors, built once and reused across calls.
Predict builds and caches one ToolSet per instance from its builder tools on first use. To put Code Mode on a predictor, add the CodeModeTool (re-exported by dspy_rs under the code-mode feature) as the single tool:
ToolSet::code_mode(...) set drops into LM::call_with_toolset directly. See Code Mode.
