rlm-harness
A clean, reusable harness for building any task on top of
DSPy's Recursive Language Model module (dspy.RLM).
RLMs (Zhang & Khattab, MIT, arXiv:2512.24601)
let a model explore unbounded context by treating it as a variable in a sandboxed
Python REPL and recursively calling sub-LLMs over it. DSPy's dspy.RLM is the
first-party implementation (Khattab co-authored both DSPy and the RLM paper) — it
works with existing Signatures and is optimizer-compatible (GEPA/MIPRO). This kit
distills the boilerplate around it into one small, opinionated layer.
rlm-harness is domain-agnostic — anything dspy.RLM can do fits: multi-hop "deep
research", an RSS-digest agent that posts to a webhook, structured extraction,
detection authoring, you name it. Security happens to be the author's own first
use of it, but it isn't the kit's scope.
Why this exists
Using dspy.RLM directly leaves you re-writing the same plumbing for every task:
model/sub-model config, a retry+validation loop, a sandbox choice, observability.
rlm-harness makes a task a declaration:
from rlm_harness import RLMConfig, RLMTask, configure
from rlm_harness.tools import make_schema_validator
from pydantic import BaseModel
class Article(BaseModel):
title: str
summary: str
class Summarize(RLMTask):
signature = "document: str -> article: Article"
output_field = "article"
output_model = Article
instructions = "Read the document and produce a title and a one-paragraph summary."
tools = [make_schema_validator(Article)]
configure(RLMConfig.from_env())
article = Summarize().run(document=long_text) # validated Article
The retry loop, pydantic validation, sandbox selection, and budget caps are all inherited.
Installation
pip install rlm-harness
# or with uv:
uv add rlm-harness
rlm-harness needs Python ≥ 3.11
and pulls in dspy + pydantic; extras are opt-in — observability (pip install "rlm-harness[observe]")
and running on a Claude Pro/Max subscription instead of an API key
(pip install "rlm-harness[subscription]" → rlm_harness.ClaudeAgentLM, injected via configure(main_lm=…)). A
live dspy.RLM run additionally needs model credentials (see the guide's
Configuration) and a
Deno sandbox — the logic and tests run without either. dspy requires Deno >=2.0.0,<3.0.0:
brew install deno, or let dspy manage it with pip install "dspy[deno]".
What's in the box
- Tasks as declarations. Subclass
RLMTask— the retry+validation loop, sandbox selection, budget caps, and observability are inherited. - The whole trajectory, recorded.
TraceRecorderwrites main steps, every sub-LM call, and every tool call into one append-only JSONL stream — replayable and exportable as SFT/RL datasets (reward-free: scoring belongs to your trainer). - The recursion seat, interceptable. Every sub-LM escalation is traced as a
sub_callautomatically — no wrapper needed.intercept_sub_lmadds a deterministic validate/post-process pipeline on top;model_as_toollets the main LM choose to consult another named model, in the trajectory. - Tools, the base/wrap way. Pydantic/JSON-Schema validators, an SSRF-guarded
fetch_url, provider-agnostic web search, the generic model-as-tool core, arun_commandseam over your isolated runner, an MCP client bridge, and skills-as-tools progressive disclosure. - Sandboxed by default. The pyodide/deno interpreter; the
localinterpreter is refused unless explicitly opted into; an opt-in Dockercontainerinterpreter for when the REPL itself needs real subprocesses. - Offline-testable.
rlm_harness.testingdrives the realdspy.RLMforward loop with no model, no Deno, no network.
Documentation — the guide
The deep documentation lives in
rlm_harness/README.md:
- Layout — what each module owns.
- RLM as harness engineering — the sub-LM hook + trajectory tracing.
- Sub-LM vs. tool — which model goes where; the choice decides what your RL data records.
- Skills, MCP tools, running local commands, and the container interpreter — the tool & environment surfaces.
- Grounded completeness and judgement-only SUBMIT — the rollout conventions.
- Building a consumer — the five-step extension contract.
- Configuration — every env var, adapter selection, model naming.
- Testing the forward path offline — the scripted offline harness.
Built with rlm-harness
Real projects using rlm-harness as their RLM scaffold:
- ctx-distillery: distils an AI coding agent's session transcripts and memory store into a judgement-only distillation plan — what to prune, cross-reference, or promote into durable memory or a reusable Skill. It proposes; it writes nothing.
- cve-reverser: reverses publicly disclosed CVEs from their patches into local-lab PoCs and Nuclei detection templates. A traced, trainable RLM harness.
- diff-sentry: classifies GitHub changes (PRs, issues, pushes) for malicious intent — the diff is read as untrusted data in the sandboxed REPL, emitting evidence-backed benign / suspicious / malicious verdicts into a SIEM.
- toolscout: an ATLAS-style rollout harness — a small planner progressively discovers a large MCP toolspace and computes over tool results as code, emitting reward-free trajectories + per-criterion facts for a downstream trainer.
Built something on rlm-harness? Open a PR to add it here.
Security note — the sandbox is the boundary
RLM executes model-written code. When that code processes untrusted scraped
content, the interpreter choice is your attack surface. The default
(pyodide/deno) is the sandboxed DSPy interpreter. The local interpreter runs
code on the host and is refused unless you set
allow_insecure_sandbox=True / RLM_ALLOW_INSECURE_SANDBOX=1. Don't.
The default sandbox is built by the kit (not handed straight to dspy) so it can
pre-bind the JSON literals true/false/null to True/False/None in the
REPL namespace — a JSON-trained instruct model otherwise writes SUBMIT({"ok": true}) and the REPL raises NameError: name 'true' is not defined, which the model
tends to retry verbatim. Isolation is unchanged; RLMTask owns the teardown.
Develop
uv sync --group dev
uv run pytest # logic tests (no live LLM needed)
Tests cover config parsing, the retry/validation engine, the sandbox guard, the
tools, the sub-LM-hook/trace/replay/dataset layer, and a real-dspy.RLM
construction check (dspy-bearing tests use DummyLM or skip if dspy is absent).
A live run additionally needs real credentials and a Deno sandbox
(brew install deno, or pip install "dspy[deno]" for dspy's managed binary; it requires Deno
>=2.0.0,<3.0.0); examples/mini_run.py shows it. To drive the real forward
loop offline (no model, no Deno), see the guide's
Testing the forward path offline.
See CLAUDE.md for invariants when modifying the kit.
Status
Released versions, with what changed in each, are on the
Releases page and in
CHANGELOG.md. This section
used to restate the current one and fell five versions behind, so it no longer tries.
What is worth saying here is the part that does not change with a version number.
1.0.0 means the public surface is a contract: __init__.__all__, the rlm-harness/trace/v1 wire format,
and RLMTask's declaration fields are frozen under
SemVer and pinned by tests/test_contract.py. Additions ship in a minor
release; a rename or removal ships with an alias and a DeprecationWarning first, and the removal
itself waits for the next major. The trace format carries its own version and evolves
additive-only within v1. A _-prefixed name or module internal is not part of that promise.
Next: enable optimize.compile_task against a labelled trainset to actually
GEPA-compile tasks (currently a documented stub).
License
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rlm_harness-1.8.2.tar.gz.
File metadata
- Download URL: rlm_harness-1.8.2.tar.gz
- Upload date:
- Size: 632.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ea155d6aef4b6dee31c61f2715545472739ae2b41c1e2bc9cee7886e3229d154
|
|
| MD5 |
da8356d84112affccd9e3e5c6cc3caa6
|
|
| BLAKE2b-256 |
12af71ed13250add62b673367e99078e26da4c0a00094b778d1111a3432c155d
|
Provenance
The following attestation bundles were made for rlm_harness-1.8.2.tar.gz:
Publisher:
release.yml on qazbnm456/rlm-harness
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rlm_harness-1.8.2.tar.gz -
Subject digest:
ea155d6aef4b6dee31c61f2715545472739ae2b41c1e2bc9cee7886e3229d154 - Sigstore transparency entry: 2672177857
- Sigstore integration time:
-
Permalink:
qazbnm456/rlm-harness@9c6f485045a08ff03fb5ff800465fba8847c4961 -
Branch / Tag:
refs/tags/v1.8.2 - Owner: https://github.com/qazbnm456
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9c6f485045a08ff03fb5ff800465fba8847c4961 -
Trigger Event:
release
-
Statement type:
File details
Details for the file rlm_harness-1.8.2-py3-none-any.whl.
File metadata
- Download URL: rlm_harness-1.8.2-py3-none-any.whl
- Upload date:
- Size: 255.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e539602c1b10f0516dd5bf7c28abe46b0c276266844159701b836048c84822cb
|
|
| MD5 |
4b7c974c25681c9be64c60224d46c1fb
|
|
| BLAKE2b-256 |
18bedada0e2fbda0e250083e9ed2e5ba48797c9c529f653d3e9eb84e75a2294c
|
Provenance
The following attestation bundles were made for rlm_harness-1.8.2-py3-none-any.whl:
Publisher:
release.yml on qazbnm456/rlm-harness
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rlm_harness-1.8.2-py3-none-any.whl -
Subject digest:
e539602c1b10f0516dd5bf7c28abe46b0c276266844159701b836048c84822cb - Sigstore transparency entry: 2672177918
- Sigstore integration time:
-
Permalink:
qazbnm456/rlm-harness@9c6f485045a08ff03fb5ff800465fba8847c4961 -
Branch / Tag:
refs/tags/v1.8.2 - Owner: https://github.com/qazbnm456
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9c6f485045a08ff03fb5ff800465fba8847c4961 -
Trigger Event:
release
-
Statement type: