A module that transforms context from "input" to "environment", enabling LLMs to programmatically explore large contexts through a REPL interface rather than embedding them in prompts.
Details
Instead of llm(prompt, context=huge_document), RLM stores context as R
variables that the LLM can peek, slice, search, and recursively query.
The execution flow is:
Context is made available as variables in an R execution environment
LLM generates R code to explore and analyze the context
Code is executed by the configured code runner
Results are fed back to the LLM for the next iteration
Process continues until SUBMIT() is called or max_iterations reached
If max_iterations reached without SUBMIT(), fallback extraction is used
Available REPL tools:
SUBMIT(...): Terminate and return final output valuespeek(var, start, end): View a slice of a variable (default: first 1000 chars)search(var, pattern): Regex search in variablellm_query(query, context_slice): Recursive LLM call (requires sub_lm)llm_query_batched(queries, slices): Batched recursive calls
Security: Code execution requires explicit opt-in via runner or
interpreter_factory.
The built-in runner uses a separate process but is NOT a security sandbox.
Inspect runner$policy() before execution. For untrusted inputs, provide a
runner backed by OS-level sandboxing, such as mcp_repl_runner().
Authenticated RLM control frames sent through mcp-repl are limited to 3,000
encoded bytes. If aggregate output is compacted into a file preview or pager,
the iteration fails closed because mcp-repl does not expose structured
compaction metadata; dsprrr does not read a sandbox-disclosed path from the
host process.
Runner lifecycle: supply exactly one runtime source. runner is
caller-owned, reused across calls, and never closed by dsprrr. The backend determines
whether execution state persists and whether reset() is available;
serialize access to stateful backends. interpreter_factory is a
zero-argument function that returns a fresh runner implementing execute(),
policy(), optional start(), and terminal shutdown() or close(). The
module owns that runner for one invocation and shuts it down exactly once on
success, error, or interrupt.
run_async() supports factory-backed RLM in an isolated mirai process and
rejects caller-owned runners. stream_async() and a module's $stream()
method remain unavailable because streaming would bypass execution. The
run_stream() one-shot forward() fallback remains available.
Examples
if (FALSE) { # \dontrun{
# Create a runner (required for code execution)
runner <- r_code_runner(timeout = 30)
# Create an RLM module for exploring large documents
rlm <- rlm_module(
signature = "document, question -> answer",
runner = runner
)
# Use it for context exploration
long_doc <- paste(readLines("large_file.txt"), collapse = "\n")
result <- run(rlm, document = long_doc, question = "What are the main themes?", .llm = llm)
# Enable recursive LLM calls for complex reasoning
rlm_recursive <- rlm_module(
signature = "document -> summary",
runner = runner,
sub_lm = ellmer::chat_openai(model = "gpt-4o-mini"),
max_llm_calls = 10
)
} # }