Skip to contents

dsprrr is an R implementation of DSPy’s programming model, built on ellmer and tidyverse conventions. If you know DSPy, this page tells you what carries over, what is different, and what is not (yet) available.

Version baseline

This comparison was checked against DSPy 3.3.0, released on 2026-08-03. That release introduces experimental Flex and ReActV2 modules, advances an experimental typed provider-neutral LMRequest -> LMResponse migration path, and hardens errors, execution limits, and serialization. See the DSPy 3.3.0 release notes.

dsprrr adopts the durable contracts that fit R and ellmer. It does not present an existing prompt optimizer as equivalent to a new DSPy feature when the execution or safety model differs.

dsprrr is not a line-by-line port. It follows DSPy’s concepts — signatures, modules, metrics, and optimizers (“teleprompters”) — while embracing R idioms: tibbles in and out, S7/R6 objects, and ellmer for all provider communication.

Modules

DSPy dsprrr Notes
dspy.Predict module(sig, type = "predict") Core predictor
dspy.ChainOfThought chain_of_thought(), with_reasoning() Implemented as signature transforms
dspy.ReAct / experimental ReActV2 module(sig, type = "react") Native ellmer turn history, tool-call IDs, parallel calls per assistant turn, enforced iteration limit, then structured finalization; behaviorally aligned, not a port of the Python class
dspy.ProgramOfThought program_of_thought() Generates and executes R code (not Python); accepts either a caller-owned reused runner or a fresh per-invocation interpreter factory
dspy.CodeAct code_act() Hybrid tools + R code execution with an enforced inner tool-call limit; accepts either a caller-owned reused runner or a fresh per-invocation interpreter factory. The built-in runner is trusted-input-only, and sandboxed backends can implement the runner protocol
dspy.BestOfN best_of_n() Reward-function-guided retries
dspy.Refine refine() Retries with LLM-generated feedback
dspy.MultiChainComparison multi_chain_comparison()
dspy.RLM rlm_module() (experimental) Inference-time adaptive exploration over an R REPL, with optimizable action and extraction predictors, inherited or separate sub-LMs, replayed recursive values, typed submission repair, and trajectory metadata
Experimental dspy.Flex flex() GEPA can optimize a bounded predictor graph or an R forward() program. dsprrr requires an explicit fresh interpreter for executable source rather than choosing a default sandbox
dspy.Parallel / Module.batch run_dataset(), run(..., .parallel = TRUE) Batch over a data frame; heterogeneous (module, example) fan-out is not yet a dedicated module
dspy.majority ensemble() with reduce_majority() Plus reduce_weighted_vote(), reduce_best_by_metric()
dspy.KNN KNNFewShot teleprompter / KNN module Bring-your-own vectorizer (e.g., ragnar::embed_openai())
Retrieval (custom functions) rag_module() + ragnar First-class ragnar retriever integration

For ProgramOfThought, CodeAct, and RLM, choose either a caller-owned runner or a zero-argument interpreter_factory that creates a fresh runner per invocation. Executable Flex accepts only the factory form and requires an enforced sandbox by default; JSON Flex needs no interpreter. See Flex: Optimize the Whole Program for the practical choice between those modes.

The one-call rlm() helper is the exception to the explicit-binding rule: it creates a fresh managed mcp-repl sandbox factory by default. That path is intended for compact JSON-compatible context. For trusted rich R objects, opt into r_code_runner(persistent = TRUE) so one callr process stages the context once and keeps derived values for the invocation.

RLM parity and intentional differences

dsprrr now matches the DSPy 3.3 RLM execution contracts that determine how an investigation behaves:

  • one factory-owned interpreter belongs to each invocation, and persistent backends retain state across its iterations;
  • generate_action and extract are child predictors visible to graph optimizers;
  • sub_lm = NULL inherits the outer LM, while an explicit sub-LM can handle focused queries;
  • llm_query() and llm_query_batched() return host-produced values to the same generated R evaluation through validated replay;
  • invalid typed SUBMIT() calls become repairable observations rather than silently becoming final output;
  • execution observations use a 10,000-character head-and-tail module bound after any stricter runner transport limit; and
  • structured results include the output source, bounded REPL trajectory, recursive-call counts, usage, and runner policy.

The remaining RLM differences are intentional and user-visible:

Boundary DSPy dsprrr
Generated language Python R, with signature inputs under .context
Returned value Prediction with trajectory and top-level final_reasoning run() output plus structured metadata; per-step reasoning is in the bounded trajectory and output source is explicit
Interpreter binding Optional interpreter may be supplied to each forward() call Runner or factory is bound when constructing rlm_module(); the one-call rlm() helper creates that binding for one invocation
Default local execution DSPy interpreter configuration rlm() creates a fresh managed mcp-repl OS sandbox with network disabled and workspace writes allowed
Large or rich input SandboxSerializable can define interpreter-specific one-time staging Persistent trusted callr stages serializable native R objects once; no general public custom-staging protocol yet. Managed MCP requires the final raw-or-compressed JSON-RPC request to fit 7 KB and each encoded control frame to fit 3,000 bytes
Typed outputs Signature adapters validate the configured Python/Pydantic type RLM validates explicit ellmer string, number, integer, boolean, enum, array, and object types; opaque TypeJsonSchema nodes are rejected rather than accepted without validation
Stored trajectory Full interpreter trajectory Bounded, formatted execution evidence; large raw runner output is not retained
Recursive replay Interpreter-dependent One nonce-bound, schema-checked ordered ledger returns query/tool values in code, but cannot roll back direct external side effects before a request
Batch failures Per-provider failures become ordered error values Per-request provider failures become ordered [ERROR] ... values; unexpected or batch-infrastructure failures terminate
Host tools Interpreter-defined Declared RLM tools execute in the host process, outside the guest sandbox; the bridge caps one generated step at 1,000 tool calls

RLM remains experimental in dsprrr. The execution and result contracts are tested; the convenience API may still evolve. See the deterministic release regression and the RLM execution contract.

One notable difference: DSPy 3.0 removed dspy.Assert/dspy.Suggest in favor of BestOfN/Refine. dsprrr keeps both styles: declarative assertions with retry/backtracking (with_assertions(), assert_output(), suggest_output()) and the best_of_n()/refine() wrappers. If you prefer the modern DSPy style, use the wrappers; use assertions when you want declarative output contracts with automatic feedback injection.

Optimizers (teleprompters)

DSPy dsprrr Fidelity notes
LabeledFewShot LabeledFewShot Equivalent for a root Predict module; nested-predictor graphs, including RLM and Flex wrappers, are rejected rather than receiving mismatched root-task demos
BootstrapFewShot BootstrapFewShot Compiles ordinary pipelines jointly; programs containing Flex or RLM are rejected because their runtime predictors cannot receive root-task demos
BootstrapFewShotWithRandomSearch BootstrapFewShotWithRandomSearch Random search over ordinary labeled/bootstrap candidates; rejects programs containing Flex or RLM rather than returning a baseline-only no-op
MIPROv2 MIPROv2 Root Predict modules search instruction + demo candidates; nested graphs search child instructions with max_bootstrapped_demos = 0L
SIMBA SIMBA Adapted: hard-example mining + LLM-generated rules; simplified vs. the full introspective algorithm
GEPA GEPA Adapted reflective optimization for instructions and complete Flex sources, with separate train/validation roles, multi-objective selection, lineage, and optional retained outputs. Cached subsample merge acceptance, fine-grained resume, and built-in experiment trackers remain different
COPRO COPRO Equivalent (coordinate ascent over instructions)
KNNFewShot KNNFewShot Equivalent
Ensemble Ensemble Equivalent
BetterTogether BetterTogether Chains prompt optimizers via strategy strings; does not alternate prompt/weight optimization (no finetuning backend)
BootstrapFinetune Not implemented (planned); dsprrr currently optimizes prompts, not weights
GRPO (RL via Arbor) Not implemented
BootstrapFewShotWithOptuna, AvatarOptimizer, InferRules Niche/legacy in DSPy; not planned
GridSearchTeleprompter, optimize_grid() dsprrr addition: tidymodels-style grid search over module parameters
Omni dsprrr addition: independent best-of exploration plus a fresh continuation optimizer, with common validation scoring and optional mirai concurrency
AutoResearch dsprrr addition: persistent research-agent loop over validated, jointly editable module snapshots with sandboxed R analysis
MetaHarness dsprrr addition: fresh batch proposers plus host-owned frontier selection, lineage, budgets, and checkpoint resume

For an RLM graph, GEPA can tune both child predictors. MIPROv2 currently tunes their instructions only and requires max_bootstrapped_demos = 0L; nested demo bootstrapping fails explicitly until predictor-local child evidence is available. BootstrapFewShot(), its random-search variant, and LabeledFewShot() reject programs containing an RLM because task examples do not match the children’s state -> ... signatures.

GEPA feedback metrics

DSPy’s GEPA expects metrics that return a score and textual feedback. dsprrr supports the same protocol:

metric <- metric_with_feedback(
  function(prediction, expected) {
    if (identical(prediction$answer, expected)) {
      list(score = 1, feedback = "Correct.")
    } else {
      list(
        score = 0,
        feedback = paste("Wrong: expected", expected, "- check the arithmetic.")
      )
    }
  },
  field = "answer"
)

tp <- GEPA(metric = metric, generations = 5L)
compiled <- compile(tp, mod, trainset, .llm = llm)

The feedback for failed examples is injected into GEPA’s reflection prompt, so the reflection LLM learns why outputs failed, not just that they did.

Trace-aware metrics

DSPy 3.3 makes execution traces available to metrics used by reflective optimization. dsprrr provides the same durable capability through metric_with_trace(). The wrapped function receives the prediction, expected row, and a stable trace envelope:

metric <- metric_with_trace(
  function(prediction, expected, program_trace) {
    correct <- identical(prediction$answer, expected$answer)
    list(
      score = as.numeric(correct),
      feedback = paste(
        program_trace$status,
        "with",
        length(program_trace$events),
        "trace events"
      )
    )
  },
  field = "answer"
)

result <- evaluate(mod, testset, metric, .llm = llm)
result$traces[[1]][c("row_id", "epoch", "status")]

Each trace contains row_id, epoch, status, ordered events, and module metadata. With repeated evaluation, result$epoch_traces preserves the row-aligned traces for every epoch. Trace events may contain prompts, inputs, and model responses, so handle them as potentially sensitive data.

Signatures and types

DSPy dsprrr
"question -> answer: int" string signatures signature("question -> answer: integer")
Class-based signatures with InputField/OutputField signature(inputs = list(input(...)), output_type = ...)
Signature.with_instructions() / Signature.append_instructions() with_instructions() / append_instructions(); both return a new signature without mutating the original
Pydantic-typed outputs ellmer type objects (type_string(), type_enum(), type_object(), type_array())
dspy.Image, dspy.Audio, dspy.File ellmer Content objects (images, PDFs) passed as inputs
dspy.History Native ellmer turns preserved in ReAct metadata and traces; not a signature type
dspy.Tool, dspy.ToolCalls, ToolCallResults ellmer ToolDef, ContentToolRequest, and ContentToolResult; IDs remain attached to native turns
dspy.Reasoning (native reasoning traces) Not yet first-class; with_reasoning() adds a prompted reasoning field

Programs and composition

DSPy composes programs as Python classes with multiple predictors. dsprrr composes pipelines:

program <- mod_retrieve %>>%
  map_inputs(mod_answer, documents = "context") %>>%
  mod_format

BootstrapFewShot compiles pipelines jointly, like DSPy: the teacher pipeline runs end-to-end, final outputs are scored, and each step harvests demonstrations from passing traces. Other teleprompters currently optimize a pipeline’s steps individually (instruction-level optimizers operate on single modules).

Infrastructure

Capability DSPy dsprrr
LM client dspy.LM; experimental typed LMRequest -> LMResponse migration boundary in 3.3 Provider-neutral ellmer Chat; build_module_request() normalizes prompt/content input, but a complete package-wide invocation record is still planned
Configuration dspy.configure() / dspy.context() dsp_configure(), with_lm(), local_lm()
Caching Two-tier memory + disk Two-tier memory + disk (configure_cache())
Async acall/aforward, asyncify run_async() with promises for ordinary Predict modules and isolated background workflows for factory-backed ProgramOfThought, CodeAct, and RLM. Caller-owned interpreters and specialized streaming remain rejected rather than shared or bypassed
Streaming streamify() + StreamListener run_stream() + stream_listener(); one-shot fallback preserves specialized forward() semantics, while direct token streaming is limited to ordinary Predict steps and emits status events per pipeline step
Usage tracking track_usage get_tokens(), get_cost(), session_cost()
Parallel evaluation Evaluate(num_threads = ...) evaluate(.parallel = TRUE) via mirai or ellmer’s native parallelism. Declarative zero/one-step Flex is supported; executable and multi-step Flex currently require sequential rows
Saving programs save/load; sanitized LM state and explicit unsafe-class opt-in in 3.3 Versioned whole-program artifacts via save_program() / load_program() or pins, with registry-backed runtime IDs and explicit trusted opt-in
Observability MLflow autolog, OpenTelemetry callbacks Traces tibble, inspect_history(), export_traces(); package-level OpenTelemetry spans are planned on top of ellmer
Adapters (Chat/JSON/XML/TwoStep/BAML) Yes No adapter layer; ellmer’s chat_structured() handles structured output
Evaluation framework dspy.Evaluate, including trace-aware metrics evaluate(), eval_program(), metric_with_trace(), plus vitals integration

What dsprrr has that DSPy doesn’t

  • tidymodels integration: use modules as parsnip engines, tune with dials parameters (temperature, top_p, reasoning_effort).
  • vitals integration: bridge modules and metrics to the vitals evaluation framework (as_vitals_solver(), as_dsprrr_metric()).
  • ragnar integration: production RAG with rag_module() and ragnar_tool().
  • Assertions with backtracking: kept and maintained (removed in DSPy 3.0).
  • Grid search compilation: optimize_grid() for explicit, tidymodels-style parameter sweeps.

Known gaps (roadmap)

The RLM differences above are disclosed boundaries. Some are intentional API choices; others, such as a general public custom-staging protocol, remain parity work. Remaining package-level gaps are:

In rough priority order, based on the stable DSPy 3.3 runtime:

  1. General custom input staging for interpreter-backed modules, analogous to DSPy’s SandboxSerializable, beyond the current persistent-callr and runner-specific paths.
  2. Remaining Flex gaps: concurrent executable evaluation, fine-grained checkpoint resume, and GEPA’s cached subsample merge-acceptance gate remain unimplemented. The top-level R forward() function, explicit interpreter factory, and non-portable R/Python source are intentional API differences, not missing parity.
  3. One package-wide invocation/result contract carrying native turns, usage, cost, cache state, timing, and normalized errors across every module.
  4. Package-level OpenTelemetry spans for module, optimizer, evaluation, cache, and tool activity, composed with ellmer’s provider telemetry.
  5. Native reasoning-trace capture as a typed output (analogous to dspy.Reasoning).
  6. Predictor-local RLM demo evidence so MIPROv2 can bootstrap child demos; its graph mode currently tunes child instructions only, while GEPA selects the RLM child components explicitly.
  7. Adapter-style fallbacks for models with weak structured-output support (analogous to TwoStepAdapter).
  8. Weight and RL optimization, after provider-neutral training data, reproducibility, cost accounting, and artifact contracts are stable.

If one of these blocks your use case, please open an issue.