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() |
Recursive language models over an R REPL; accepts the 3.3
max_iters spelling and either runner lifecycle. Tool names,
inputs, sub-LM responses, and authenticated control frames are validated
strictly; compacted mcp-repl control responses fail closed |
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.
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 |
BootstrapFewShot |
BootstrapFewShot |
Equivalent; compiles pipelines jointly (demos for every step harvested from end-to-end traces) |
BootstrapFewShotWithRandomSearch |
BootstrapFewShotWithRandomSearch |
Equivalent |
MIPROv2 |
MIPROv2 |
Discrete Bayesian optimization with UCB over instruction + demo candidates |
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 |
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_formatBootstrapFewShot 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()andragnar_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)
In rough priority order, based on the stable DSPy 3.3 runtime:
-
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. - One package-wide invocation/result contract carrying native turns, usage, cost, cache state, timing, and normalized errors across every module.
- Package-level OpenTelemetry spans for module, optimizer, evaluation, cache, and tool activity, composed with ellmer’s provider telemetry.
-
Native reasoning-trace capture as a typed output
(analogous to
dspy.Reasoning). - Joint multi-step optimization for other instruction optimizers (including MIPROv2); demo bootstrapping is already joint and GEPA now selects complete-program components explicitly.
-
Adapter-style fallbacks for models with weak
structured-output support (analogous to
TwoStepAdapter). - 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.