LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnAdvanced Agents & RetrievalRecursive Language Models (RLM)
📈HardReasoning & Scaling

Recursive Language Models (RLM)

Learn Recursive Language Models (RLMs): keep long context in a programmable environment, delegate targeted sub-calls, and release the design only after measured quality, cost, and safety checks.

49 min read
Learning path
Step 133 of 196 in the full curriculum
Agent Failure & RecoveryMulti-Agent Orchestration

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

Agent recovery gave you iteration budgets, circuit breakers, and checkpointed state when a tool loop goes wrong. Now imagine finding one deprecated-endpoint exception across ten thousand pages of API docs, changelogs, and security advisories. A direct call can lose the clause in a huge context; an aggressive summary can delete it before reasoning starts.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601

An Recursive language model (RLM) keeps that corpus outside the root model's context window.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 The model writes code to inspect the data, asks targeted plain-LM or child-RLM calls for semantic work, and carries compact findings back into its next decision.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 That makes RLM an inference-time interface, not a bigger model or a new attention layer.[2]Reference 2Recursive Language Models.https://alexzhang13.github.io/blog/2025/rlm/

Keep that request on the page. First watch a small-window controller cross the context boundary; then compare it with a fixed split-and-merge baseline. After that, add adaptive recursion, budgets, evaluation, and sandbox policy. The result should be practical: choose a route, account for each call, and stop a run when its evidence or operating contract fails.

The small-window dispatcher

An engineer answering questions against a thousand API-policy Markdown files doesn't keep the whole monorepo open. The working set is the few files they can inspect at once, while the repository remains available for the next search.

Paste every policy file into one prompt and the packet overflows. Early endpoint definitions get buried under later changelog noise. Practitioners call that context rot (attention quality and fact recall getting worse as input grows, even inside the advertised window).[3]Reference 3Context Rot: How Increasing Input Tokens Impacts LLM Performancehttps://research.trychroma.com/context-rot The RLM paper uses the same term to motivate the design.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601

Before naming the machinery, predict what should cross the boundary if the root can't hold the corpus: the answer should be a length, a pointer, a few candidate clauses, or a status, not ten thousand raw files. That prediction is the design constraint.

An RLM run can look like a scratchpad script with a controlled open-files limit: grep for endpoint names, open a few related files, summarize a batch, and write compact findings back. A fixed summarize-every-file tree is a useful baseline. The distinguishing move is that the model can choose and revise that program while it works.

The scratchpad is the external execution environment. Requesting file slices and combining their results is one recursive decomposition; an RLM lets the controller choose and revise that decomposition in code. The model doesn't load a thousand files into the prompt at once, but the scratchpad preserves the thread and the raw evidence.

Why long context still breaks

Maximum advertised window size is a capacity limit, not a quality guarantee. The paper argues that view is incomplete.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 Fit is necessary, but it doesn't tell you whether the requested relationships remain manageable.

Two prompts can have the same length and very different reasoning difficulty. A single needle-in-a-haystack lookup is usually easy. Aggregating semantics over most lines is harder. Aggregating over most pairs of lines is much harder.

Use the same 10,000-page manual to make that difference visible. "Which endpoint replaced /v1/search?" is one fact hidden in one page. The model needs to locate a needle.

"Summarize every deprecation policy change in 2024" spreads evidence across thousands of pages. The model must aggregate roughly 10,000 facts. "List every pair of migration guides whose version requirements conflict" asks for almost every page to be compared with almost every other page, or about 50 million pairs.

Predict the route before reading the notation: direct lookup, scan-and-reduce, or pairwise comparison? The first may fit one call, the second needs coverage and merging, and the third needs an explicit way to control combinatorial work.

The paper's approximate information complexity categories grow with context length NNN:

  • Needle lookup has roughly constant answer-relevant information: one piece of evidence answers the question. A system still has to locate that evidence through attention, search, or indexing.
  • OOLONG-style aggregation requires semantic work across roughly one item per input row, so the relevant work grows approximately linearly with NNN.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601
  • OOLONG-Pairs-style aggregation requires relationships across many pairs, so the relevant work grows approximately quadratically with NNN.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 Ten thousand items contain about 50 million unordered pairs.

We write this with Big-Theta notation:

I(N)∈{Θ(1), Θ(N), Θ(N2),… }\mathcal{I}(N) \in \{\Theta(1),\ \Theta(N),\ \Theta(N^2),\dots\}I(N)∈{Θ(1), Θ(N), Θ(N2),…}

where I(N)\mathcal{I}(N)I(N) describes answer-relevant processing work and the Greek letter Θ\ThetaΘ describes its growth rate.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 This isn't a runtime guarantee: retrieval choices, batching, and model errors still determine real latency and quality.

What is the practical difference between Θ(N)\Theta(N)Θ(N) and Θ(N2)\Theta(N^2)Θ(N2) in long-context work?

Answer

Θ(N)\Theta(N)Θ(N) means work grows roughly one-for-one with document size. Θ(N2)\Theta(N^2)Θ(N2) means pairwise work grows much faster, so doubling document size can require about four times as many comparisons.

Turn that scaling intuition into concrete counts before choosing a controller:

count-pairwise-work.py
1def unordered_pairs(items: int) -> int: 2 return items * (items - 1) // 2 3 4small = unordered_pairs(5_000) 5large = unordered_pairs(10_000) 6 7print(f"pairs_at_5000={small:,}") 8print(f"pairs_at_10000={large:,}") 9print(f"doubling_ratio={large / small:.2f}x")
Output
1pairs_at_5000=12,497,500 2pairs_at_10000=49,995,000 3doubling_ratio=4.00x
Illustrative information-complexity plot: needle evidence stays flat, aggregation grows linearly, and pairwise comparison grows quadratically as context size increases.
RLM is most useful when the task grows faster than raw context length helps. Needle lookup stays flat, aggregation grows with document size, and pairwise comparison grows much faster.

As complexity rises, direct model performance degrades faster, even before you hit hard context limits.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 A longer context window doesn't solve a Θ(N2)\Theta(N^2)Θ(N2) relationship-tracking problem by itself. The bottleneck is the rapidly growing web of relationships, not token capacity alone.

That is the opening for RLM. It keeps neural context focused while moving large-scale symbolic manipulation into an external execution environment. The move changes what the controller can inspect at once; it doesn't make pairwise work disappear.

Token count isn't enough: Raw token count isn't enough to choose a long-context strategy. A pairwise migration-guide comparison task can overwhelm a direct call even when all text fits, so compare direct, REPL-only, and recursive paths on held-out tasks.

Common mistake: Treating maximum context length as maximum reasoning capability. "My model supports 1M tokens" doesn't mean "my model can reason well over any 1M-token task."

The RLM interface

The running question is now simple: what can the root model see, and what can it ask the environment to do? The answer is easier to follow in the boundary picture than in an API signature.

Recursive Language Model loop: the raw prompt loads once into a REPL environment, the root model writes code and queries into that environment, a compact preview returns, and an explicit ready answer leaves from the root.
The raw prompt loads once into the REPL. Compact metadata and previews cross back to the root model; the corpus itself never has to sit in the root window.

The figure separates two kinds of state: the root controller's conversation and the environment's raw data.

Start with a base model M\mathcal{M}M with context limit KKK. Given a prompt string PPP where ∣P∣≫K|P| \gg K∣P∣≫K, an RLM scaffold initializes an environment E\mathcal{E}E for the run (typically a Python REPL, which stands for Read-Eval-Print Loop) and stores PPP as data inside that environment.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601

The root model doesn't read all of PPP directly. It receives compact metadata such as length, chunk structure, or a prefix, then writes code to inspect, transform, and recurse. Ask what should happen to a 40,000-token manual: its length and pointers belong in history; its full text stays in E\mathcal{E}E until a query selects a slice.

One subtlety matters. Zhang et al. distinguish depth 0 (REPL access without sub-calls), depth 1 (sub-calling LMs), and depth >1 (sub-calling RLMs).[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 In the current open-source runtime, llm_query() and llm_query_batched() issue plain LM completions, while rlm_query() and rlm_query_batched() spawn child RLM work when recursion is enabled.[4]Reference 4Recursive Language Models (RLM) Repository.https://github.com/alexzhang13/rlm Depth, sub-call count, token budget, cost budget, and timeout are limits to measure, not evidence that more recursion is better. A child is justified by stateful work, not by the mere existence of another record.

Now trace one turn. The root model receives metadata, chooses an action, and emits code; the environment executes it and returns a compact preview or completion state. If that action needs semantic work on selected slices, the environment can call sub-models there:

Diagram showing Store prompt P in REPL env E, Root LM receives compact metadata, Emit code or choose sub-calls, and Execute in E; save variables or answer content.
Store prompt P in REPL env E, Root LM receives compact metadata, Emit code or choose sub-calls, and Execute in E; save variables or answer content.

Read those capabilities as three different pressure valves, not as a promise that every route needs all three:

  1. Input beyond one model window in principle, because prompt text lives in environment memory instead of only in model context.
  2. Output beyond one model turn in principle, because long outputs can be assembled in variables and returned symbolically.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601
  3. Symbolic recursion, where the model can launch many sub-calls programmatically (for loops, chunk maps, reducers), instead of relying on verbal "call tool once" behavior.

These are design capabilities, not automatic correctness guarantees. For a year of deployment logs, the scaffold can keep raw rows outside model context while extracting candidate rollback-trigger events. A release check still has to test recall against labeled cases; external storage preserves availability, not selection quality.

RLM vs. standard long context

The comparison below answers a practical choice: where does state live, and which component pays for each unit of work? Read it after the boundary picture, then use it to explain why RLM adds new failure modes as well as new capacity.

FeatureStandard Context WindowRecursive Language Model (RLM)
Memory constraintBound by configured model window; serving memory and cost rise with attention state, including the KV cache (Key-Value cache)External environment and budgets; no single-call input-window requirement
Root model contextAll tokens must fit within window KKKOnly metadata + compact summaries within KKK; but KKK must be managed via selective history summarization
Compute allocationOne attention pass over provided tokensController may inspect, transform, or delegate selected slices
Information densityMay miss scattered relationshipsCan improve dense aggregation when decomposition works
Failure modeMissing or diluted evidenceMissing evidence plus protocol, budget, or execution failures

Beyond traditional agents

The paper draws a boundary between RLM and traditional tool-use agents. Many existing agents have tools, sub-agents, or summarization steps, but they often still keep user prompt handling tied to the main model context. That choice pulls the same window bottleneck back in as soon as the prompt itself is large.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 If a tool returns the whole 10,000-line incident log, the tool call has moved data around but hasn't moved the boundary.

RLM's design goal is to keep the prompt external and make prompt processing itself programmable. It turns the prompt from a static input into a dynamic, queryable database. The useful question becomes "which slice or aggregate do I need next?" rather than "how do I fit everything into this turn?"

Side-by-side history payloads for the same 10,000-line incident log. The dump path pastes every line into root context. The preview path keeps the log in a REPL variable and returns a 40-character preview plus a pointer.
The dump path recreates the original window bottleneck in one turn. The preview path leaves the log in a REPL variable and sends back only a short preview plus a pointer.

A first baseline: fixed recursive summarization

Before letting a model write its own inspection program, build a fixed baseline. Suppose you have a 40,000-token API compatibility manual, and your model's context limit is 4,096 tokens. You want a one-page summary of every policy that affects deprecated endpoints. Now you have a known call tree to compare against when an adaptive controller starts making different choices.

A static split-and-merge summarizer can keep every leaf call within the window. It's not yet an RLM: no root model is inspecting a REPL, choosing searches, or changing decomposition after seeing intermediate evidence. It does teach two costs an RLM must control: sub-call growth and information lost during merging. Keep both measurements; otherwise a smarter-looking route has no baseline.

Step 1: split until it fits

Start with a runnable toy version that uses character counts and a fake LM so you can test recursion logic without a tokenizer or API keys. A real implementation must split on token and document boundaries, then measure whether key evidence survives the merge.

step-1-split-until-it-fits.py
1from dataclasses import dataclass 2 3@dataclass 4class FakeLLM: 5 calls: int = 0 6 7 def call(self, prompt: str) -> str: 8 self.calls += 1 9 return f"summary-{self.calls}: {prompt[:24]}" 10 11def split_text(text: str) -> tuple[str, str]: 12 midpoint = len(text) // 2 13 return text[:midpoint], text[midpoint:] 14 15def recursive_summarize(text: str, llm: FakeLLM, limit_chars: int = 4096) -> str: 16 if len(text) <= limit_chars: 17 return llm.call(f"Summarize this policy chunk: {text}") 18 19 left, right = split_text(text) 20 left_summary = recursive_summarize(left, llm, limit_chars) 21 right_summary = recursive_summarize(right, llm, limit_chars) 22 23 return llm.call(f"Combine these summaries: {left_summary} | {right_summary}") 24 25manual = "deprecation policy " * 400 26llm = FakeLLM() 27summary = recursive_summarize(manual, llm, limit_chars=512) 28 29print(f"final summary: {summary}") 30print(f"model calls: {llm.calls}") 31print(f"summary_has_prefix={summary.startswith('summary-')}") 32print(f"call_count_expected={llm.calls == 31}")
Output
1final summary: summary-31: Combine these summaries: 2model calls: 31 3summary_has_prefix=True 4call_count_expected=True

For a token-aware version of the same binary strategy, 40,000 tokens split under a 4,096-token limit produce 16 leaves of about 2,500 tokens each. The toy code gets the same tree shape using a smaller character-count fixture. Before looking at the tree, predict its size: sixteen leaves plus one merge node for every internal branch.

Step 2: build the call tree

The recursion creates a binary tree of model calls. At the bottom are 16 leaf calls, each reading about 2,500 tokens. Above them are 8 merge calls, then 4, then 2, then 1 final merge at the root. The figure uses representative leaves rather than drawing all 16 leaf calls.

Fixed recursive summarization baseline that splits a 40,000-token manual into 16 leaf summaries of about 2,500 tokens, then merges 8, 4, 2, and 1 for 31 total model calls.
Each layer halves the active summaries: 16 leaf calls become 8, 4, 2, and 1 root answer. Counting every node gives 31 calls. No leaf reads more than about 2,500 source tokens.

The prediction checks out: 16 leaf calls plus 8, 4, 2, and 1 merge calls gives 31 total. Total source tokens read by leaf calls is 40,000, and no single leaf call sees more than roughly 2,500 source tokens. Coverage is intact, but that says nothing about merge fidelity.

The baseline doesn't skip a chunk, yet a merge can still discard the one clause a downstream answer needs. Measure evidence recall, not whether recursion completes alone. That missing-clause risk is why the next check compares source evidence with the candidate summary.

A small release check can catch a lossy merge even when every chunk was processed:

check-evidence-survives-summary.py
1source_chunks = [ 2 "Deprecated endpoints require a migration note. Sunset date is 2026-09-30.", 3 "Auth exception allows temporary legacy tokens with audit logging.", 4] 5candidate_summary = "Deprecated endpoints require a migration note. Auth exception requires audit logging." 6required_terms = {"sunset date", "auth exception"} 7 8available = { 9 term for term in required_terms 10 if any(term in chunk.lower() for chunk in source_chunks) 11} 12retained = {term for term in available if term in candidate_summary.lower()} 13missing = sorted(available - retained) 14 15print(f"source_evidence={sorted(available)}") 16print(f"missing_from_summary={missing}") 17print(f"release_allowed={not missing}")
Output
1source_evidence=['auth exception', 'sunset date'] 2missing_from_summary=['sunset date'] 3release_allowed=False

Step 3: watch the base case

The base case is what stops the loop. When len(text) <= limit_chars, the function stops splitting and asks the model to summarize directly. Without that check, a recursive summarizer can keep dividing a single sentence into smaller and smaller pieces until it burns through the API budget or hits a recursion depth limit.

That is the first stop condition: the current piece is small enough. The outer controller still needs independent depth, iteration, time, and cost limits, because a valid leaf can sit inside an invalid overall plan.

What are the two guardrails that keep recursive summarization from running forever?

Answer

A base case stops splitting when a chunk fits the model, and an outer iteration or depth budget stops bad controller behavior even if the model emits faulty recursion code.

The control loop in code

The fixed tree always follows its preset split. An RLM needs a controller that can inspect what it found, choose the next operation, and stop when evidence is sufficient. The conceptual loop below takes a root model, a REPL (Read-Eval-Print Loop) environment, and the raw user prompt, then returns the synthesized output.

Before reading the code, predict its state transitions: load the raw prompt once, expose metadata, execute one action, retain only a preview, and either accept an explicit answer or spend another iteration. If a turn prints a plausible sentence but never marks completion, the loop should keep working or fail, not guess.

the-control-loop-in-code.py
1from dataclasses import dataclass 2from typing import Protocol 3 4@dataclass 5class RLMState: 6 history: list[dict[str, str]] 7 8@dataclass 9class ExecResult: 10 stdout: str 11 stderr: str 12 final_answer: str | None = None 13 14class RootLM(Protocol): 15 def generate(self, state: dict[str, object]) -> str: ... 16 17class Repl(Protocol): 18 def load_context(self, prompt: str) -> None: ... 19 def describe_context(self) -> dict[str, object]: ... 20 def execute(self, code: str) -> ExecResult: ... 21 22def run_rlm(root_lm: RootLM, repl: Repl, prompt: str, max_iters: int = 30) -> str: 23 repl.load_context(prompt) 24 state = RLMState(history=[]) 25 26 for step in range(max_iters): 27 metadata = repl.describe_context() # length, prefix, chunk stats 28 code = root_lm.generate( 29 { 30 "iteration": step, 31 "metadata": metadata, 32 "history": state.history, 33 } 34 ) 35 exec_result = repl.execute(code) 36 37 # Keep root context compact by storing previews, not full dumps. 38 state.history.append( 39 { 40 "stdout_preview": exec_result.stdout[:200], 41 "stderr_preview": exec_result.stderr[:200], 42 } 43 ) 44 45 if exec_result.final_answer is not None: 46 return exec_result.final_answer 47 48 raise RuntimeError("RLM hit iteration budget without producing a final answer") 49 50class FakeRootLM: 51 def generate(self, state: dict[str, object]) -> str: 52 return "answer['content'] = 'summarized answer'\nanswer['ready'] = True" 53 54class FakeRepl: 55 def __init__(self) -> None: 56 self.context = "" 57 58 def load_context(self, prompt: str) -> None: 59 self.context = prompt 60 61 def describe_context(self) -> dict[str, object]: 62 return {"length": len(self.context), "prefix": self.context[:20]} 63 64 def execute(self, code: str) -> ExecResult: 65 answer = {"content": "", "ready": False} 66 if "answer['ready'] = True" in code: 67 answer["content"] = "summarized answer" 68 answer["ready"] = True 69 final = str(answer["content"]) if answer["ready"] else None 70 return ExecResult(stdout="", stderr="", final_answer=final) 71 72answer = run_rlm(FakeRootLM(), FakeRepl(), "large prompt") 73print(f"final answer: {answer}") 74print(f"answer_matches_expected={answer == 'summarized answer'}")
Output
1final answer: summarized answer 2answer_matches_expected=True

The output confirms the predicted transition. This simplified loop uses the current runtime's completion shape: executed code fills answer["content"] and sets answer["ready"] = True.[4]Reference 4Recursive Language Models (RLM) Repository.https://github.com/alexzhang13/rlm The paper's algorithm uses a Final environment variable, and its experimental prompt also describes FINAL(...) and FINAL_VAR(...) tags.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 The durable idea is explicit completion state; when implementing a system, follow the interface documented for the version you deploy.

The current library defaults max_iterations to 30. If that limit expires without answer["ready"], its documented completion path asks the model for one more best-effort answer from remaining history.[4]Reference 4Recursive Language Models (RLM) Repository.https://github.com/alexzhang13/rlm Treat that fallback as incomplete in your wrapper unless you have a separate extraction contract. A synthesized last-ditch reply can look like a successful completion.

Logs and resumability are implementation choices

Don't assume an arbitrary live REPL can migrate between workers. It may contain open connections, non-serializable objects, or generated code that you shouldn't replay blindly. The object that makes exploration convenient is not automatically a durable recovery artifact.

The current open-source runtime supports trajectory metadata and JSON Lines (JSONL) logging through RLMLogger, which is useful for replay and evaluation. It also offers persistent=True to reuse one environment across multiple completion() calls.[4]Reference 4Recursive Language Models (RLM) Repository.https://github.com/alexzhang13/rlm That can support a multi-turn session, but it isn't a distributed checkpoint.

For a distributed service, persist the input corpus or its storage pointer, compact controller metadata, budgets consumed, and logged actions. Reconstruct only approved state on retry; don't treat a Python call stack as a durable checkpoint. This is the operational handoff: a new worker should know what was searched, what evidence was retained, and what budget remains without inheriting an opaque interpreter.

RLMState only appends compact previews rather than the full output of every command. If the code block executes a complex search over thousands of documents, the root model should only see a condensed result or status message. This selective memory keeps the root model's context window focused on orchestration.

The loop also uses an explicit max_iters budget. Because the model writes code that runs automatically, it can enter an infinite loop of failed retries. The toy here fails loud. The current library instead asks for a best-effort answer after the last turn, so your wrapper still has to treat a missing answer["ready"] as incomplete. A limit is useful only if its outcome remains visible to the release decision.

Iteration count answers "how many turns?" Cost answers a different question: "can the next call still fit the request's spend limit?" A cost budget must stop a trajectory before a useful-looking plan becomes an expensive run. Predict which calls fit under the toy $0.25 cap before reading the output:

stop-a-run-at-its-budget.py
1def run_under_budget(proposed_costs: list[float], max_budget: float) -> tuple[int, float]: 2 spent = 0.0 3 completed = 0 4 for cost in proposed_costs: 5 if spent + cost > max_budget: 6 break 7 spent += cost 8 completed += 1 9 return completed, spent 10 11completed, spent = run_under_budget([0.05, 0.08, 0.09, 0.12], max_budget=0.25) 12print(f"completed_sub_calls={completed}") 13print(f"spent_usd={spent:.2f}") 14print(f"budget_blocked_next={completed < 4}")
Output
1completed_sub_calls=3 2spent_usd=0.22 3budget_blocked_next=True

The toy accepts three calls for $0.22 and blocks the fourth. That is the right shape of a budget decision: partial progress is recorded, but the next action isn't silently authorized. Finally, final answers should be signaled through explicit state rather than inferred from arbitrary printed text. In the current runtime, that state is the answer dictionary; its environment surfaces answer["content"] after answer["ready"] becomes true.[4]Reference 4Recursive Language Models (RLM) Repository.https://github.com/alexzhang13/rlm

The current runtime also exposes max_iterations, max_depth, max_budget, max_timeout, and max_tokens controls. max_budget requires a backend that reports cost.[4]Reference 4Recursive Language Models (RLM) Repository.https://github.com/alexzhang13/rlm Optional compaction=True keeps root history in a REPL variable and summarizes it when it reaches a fraction of the model window (default 85%). That's a recovery tool for a long controller trace, not a substitute for keeping dumps out of history in the first place. Set these limits in deployment configuration and log which one stopped a run. A prompt reminder isn't a budget.

What the benchmarks show

The fixed baseline taught us to count calls and check retained evidence. Benchmarks ask whether a learned controller earns that extra work. The strongest evidence in the paper isn't "it works on one benchmark." It's cross-task behavior on tasks with very different information complexity and scale.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601

Table 1 covers four tasks: deep research (BrowseComp+, 6M to 11M tokens), code-repository understanding (LongBench-v2 CodeQA, 23K to 4.2M tokens), information aggregation (OOLONG), and a synthetic pairwise variant (OOLONG-Pairs) that stresses quadratic, pairwise-heavy reasoning over scattered facts. A separate scaling experiment adds needle retrieval (S-NIAH) to compare degradation as inputs grow. Read the table as a route comparison, not as a universal leaderboard: task shape, model, depth, and cost all move together.

The paper's own headline is a set of median gains for RLM(GPT-5) against strong scaffolds: about 26% over a compaction agent, 130% over CodeAct with sub-calls, and 13% over Claude Code.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 Treat these as the authors' framing on their four-task mix, not as universal speedups. On individual slices the ranking moves. OpenCode with context offloaded to a file scored 94.0 on BrowseComp+ (1K), above RLM(GPT-5, depth=1) at 91.3, while RLM pulled far ahead on OOLONG-Pairs.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601

The useful prediction is conditional: a route with more recursion should win only when its decomposition matches the task's information structure. The numbers below let you test that prediction.

Model observations

From Table 1 in the paper, using GPT-5 as the closed model and Qwen3-Coder-480B-A35B as the open model:[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601

Benchmark sliceBase modelStrongest comparatorRLM resultTakeaway
BrowseComp+ (GPT-5)0.0*70.5 (compaction agent)91.3 at depth 1On this oversized corpus, RLM outscored compaction; direct GPT-5 hit context limits.
OOLONG-Pairs (GPT-5)0.124.7 (CodeAct + BM25)58.0 at depth 1, 76.0 at depth 3In this pairwise-heavy benchmark, added recursion helped.
OOLONG-Pairs (Qwen3-Coder-480B-A35B)0.117.3 (RLM, depth 0)23.1 at depth 1Plain REPL use wasn't enough for the best measured result.
CodeQA (Qwen3-Coder-480B-A35B)20.0*66.0 (RLM, depth 0)56.0 at depth 1REPL access helps, but recursive fanout can add overhead on simpler code tasks.

The prediction is only partly true. The paper rounds non-zero scores to at least one decimal place, so you shouldn't over-read fake precision from this table.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 On CodeQA, recursive fanout can hurt: depth 0 is the strongest Qwen comparator. On pairwise aggregation, depth 1 helps GPT-5 substantially. The scaffold earns its keep only where its extra actions match the work.

The cost story is also more mixed than "RLM is cheaper." On BrowseComp+, the direct GPT-5 path hits context limits, so there isn't a meaningful successful direct-call price in Table 1. On tasks that do fit, RLM can be cheaper or more expensive depending on the task. For example, GPT-5 + RLM depth 1 is slightly cheaper than the base model on CodeQA ($0.11 vs. $0.13 average), but more expensive on OOLONG-Pairs ($0.33 vs. $0.16 average).[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601

One subtle benchmark detail matters here: for the GPT-5 setup, the root controller is GPT-5, while recursive sub-calls use GPT-5-mini as a capability-cost trade-off.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601

That last point about CodeQA is model-dependent. For Qwen3-Coder-480B-A35B, depth 0 scores 66.0 and beats every recursive variant (depth 1 is 56.0). For GPT-5, CodeQA rises from 58.0 at depth 0 to 66.0 at depth 2, then falls back to 58.0 at depth 3.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 Recursion isn't a knob you turn up until quality improves. Keep depth 0 in the same experiment so you can tell whether recursion added signal or only overhead.

Recursion can also organize reasoning, not only documents

Table 2 in the same paper tests LongCoT-mini, a compositional reasoning benchmark rather than a long-document one. GPT-5.2 scores 38.7 overall. RLM(GPT-5.2, depth=1) scores 50.6, but MATH drops from 26.0 to 5.6 and CS from 40.4 to 11.0. Adding explicit decomposition hints lifts the same RLM to 65.6 overall and recovers those domains.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 This is the counterexample to "more recursion is more reasoning": the REPL can host a graph of subproblems, but an unplanned graph spends budget on the wrong decomposition.

Latency-accuracy trade-off

The paper reports runtime distributions in its appendix, but warns that wall-clock values depend heavily on implementation details such as machine choice, provider latency, and asynchronous execution. Its benchmark implementation used blocking, sequential sub-LM calls.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 The current runtime exposes batched helpers for independent work. The repository also notes that Prime Sandboxes are still in beta and can be slow, which reinforces the latency trade-off in practice.[4]Reference 4Recursive Language Models (RLM) Repository.https://github.com/alexzhang13/rlm

Don't ship RLM as the default route. Keep it for tasks whose measured complexity pays for the slower control loop. A single-needle lookup in a 10,000-page manual usually doesn't. That decision belongs in an eval matrix, not in a slogan about long context.

Keep a depth-0 ablation in your eval matrix. An ablation removes one system component so you can measure what that component contributes. It's often the right baseline for latency-sensitive workloads.

Cost variance is real

Table 1 reports average API cost with standard deviation, and some settings show wide spread across runs.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 That doesn't give a full tail-latency distribution, but it does show that trajectory length and decomposition quality can swing spend materially. Because the model runs in an external execution environment, poor decomposition decisions can lead to unnecessary sub-calls that inflate API usage.

Average cost per successful answer can still hide a long tail. Cap recursion depth, cap wall-clock, and reject routes whose p95 cost or p95 latency misses the release budget, even when the mean looks fine. The benchmark result becomes an operating decision only after that tail is measured.

Teaching a small model to recurse

The benchmark results leave a practical question: if the skill lives partly in choosing searches and stopping, can a smaller controller learn it? The authors don't stop at wrapping frontier models. They also post-train a smaller controller model for the RLM role.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601

What the paper claims

The paper's headline result is RLM-Qwen3-8B: a post-trained Qwen3-8B controller that learns how to operate the scaffold more effectively.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 The change is scaffold- and protocol-level, not model-architecture-level. The weights still implement the same attention. What changes is the controller protocol: inspect state, decide when to recurse, and terminate through the final-output interface.

The data-collection process makes the dependency visible: a strong controller first demonstrates trajectories, then a smaller controller learns the protocol around those trajectories.

Diagram showing Bootstrapping, Post-Training, Strong frontier RLM, and Recursive trajectories.
Bootstrapping, Post-Training, Strong frontier RLM, and Recursive trajectories.

What changed

The paper reports a 28.3% median improvement for RLM-Qwen3-8B over base Qwen3-8B across four evaluation tasks and says it approaches vanilla GPT-5 on three of them.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 Treat that as early evidence of a learnable controller skill, not as proof of a fully mature training recipe. The weights aren't being granted a larger context window; the controller is getting better at operating the same boundary.

That result suggests a meaningful chunk of RLM performance comes from learning how to operate the scaffold itself, rather than from having a stronger base model underneath it alone. The training target is a sequence of useful actions: inspect, decompose, call, preserve evidence, and finalize.

What the recipe does and doesn't prove

The main text summarizes the training set as 1,000 filtered trajectories generated by Qwen3-Coder-480B-A35B acting as an RLM on LongBenchPro tasks.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 The appendix adds useful detail: the authors sampled 2,250 candidate trajectories over 750 English tasks, dropped zero-score and one-turn runs (1,072 left), filtered controller turns that exceeded the smaller model's context limit, corrected common FINAL(...) and FINAL_VAR(...) template mistakes, then fine-tuned for 300 steps with batch size 64 over 48 H100-hours.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601

That's a concrete small-scale recipe, not a settled standard. Notice why filtering matters: zero-score trajectories encode failure, one-turn trajectories carry little supervision about a recursive control loop, and overlong turns can't fit the student controller's own context. A training set that ignores those boundaries would reward the wrong behavior.

A later experiment in the same paper RL-trains Qwen3-4B-Instruct-0527 as an RLM on a short MRCRv2 split and reports length generalization to a 1M-token, 8-needle split.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 The author-maintained runtime now also ships a verifiers / prime-rl training harness under training/.[4]Reference 4Recursive Language Models (RLM) Repository.https://github.com/alexzhang13/rlm Those are existence proofs that recursive scaffold behavior is trainable. They don't prove that the same filtering choices, controller prompt, or training budget are optimal for other models and workloads.

Even after post-training, the controller still depends on the same scaffold constraints as the frontier-model version: recursion budgets, safe execution, and an explicit final-answer protocol. Post-training improves the operator, but it doesn't remove the need for infrastructure guardrails. The handoff to operations is unchanged: deploy the policy and the limits together, then re-run the same evidence, cost, and safety gates.

Production guardrails

The training result doesn't make a production service safe by itself. The open-source repository makes the operational surface concrete: the current README lists local, ipython, docker, modal, prime, daytona, and e2b execution environments, plus trajectory logging for replay and inspection.[4]Reference 4Recursive Language Models (RLM) Repository.https://github.com/alexzhang13/rlm Each choice changes isolation, latency, and recovery, so carry the request's evidence contract into deployment.

1. Keep root context tiny and structured

Don't stream huge REPL dumps back into the root model's history. When a recursive sub-call finishes a large chunk, return a short status string or a pointer to a saved variable, not the chunk itself. If the root model needs more detail, it can query that variable. Before choosing a preview format, ask what the next decision needs: a count, evidence IDs, a few clauses, or an error.

Context-window bottlenecks return when root-model history fills with log lines, intermediate reasoning, or large payloads. The current runtime prompt makes this boundary concrete: REPL outputs over roughly 20,000 characters are truncated, and the controller is told to inspect slices instead of printing whole variables.[4]Reference 4Recursive Language Models (RLM) Repository.https://github.com/alexzhang13/rlm Truncation protects the root from one oversized return, but it can also hide the evidence the answer needs.

Keep the extracted clauses in a REPL variable. Send the root model a status line such as extracted 15 clauses into clauses, then let it read clauses[:3] if it still needs text. Control stays in the prompt. Data stays in the environment. That's how an RLM can work over millions of source tokens without pasting them into root-model history, while still leaving a deliberate path back to raw evidence.

2. Batch sub-calls aggressively

Sub-call explosion is the fastest way to lose latency and cost control in an RLM system. If the model makes a separate recursive call for every stack trace line or API symbol in a 10,000-row incident export, both execution time and API costs can rise quickly without measured quality gains. Predict the first metric to move: request count, then spend and queue time, before answer quality has a chance to improve.

At minimum, your controller prompt and routing policy should discourage one-call-per-sentence behavior. Table 1 gives the intuition: the depth-0 ablation already beats full RLM on Qwen3-Coder CodeQA, so recursive fanout only pays when it adds real task-level signal.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601 A simple cost model makes that risk visible:

Ctotal=Croot+∑i=1mCsub,iC_{\text{total}} = C_{\text{root}} + \sum_{i=1}^{m} C_{\text{sub}, i}Ctotal​=Croot​+i=1∑m​Csub,i​

where mmm is the sub-call count. Every additional sub-call adds cost. Latency also grows when calls are sequential, while batching independent calls can overlap part of that wait. If your quality metric remains flat while mmm keeps rising, your recursion policy is broken and needs tuning.

Why is sub-call count the first RLM cost metric to watch?

Answer

Each sub-call adds model latency and token cost. If quality doesn't improve as mmm rises, recursion is expanding work without buying better answers.

A root model might emit this concrete example in the REPL. Each clause needs one plain classification, so the right primitive is llm_query() rather than a child RLM. The batched form uses llm_query_batched() for independent requests; reserve rlm_query() for subtasks that need their own REPL and multiple turns. Batched child-RLM calls are also concurrency-capped (max_concurrent_subcalls, default 4 in the current runtime).[4]Reference 4Recursive Language Models (RLM) Repository.https://github.com/alexzhang13/rlm The boundary is operational: batch work with shared shape, isolate work with shared state.

The toy below doesn't talk to a model. It only counts round trips: one sequential loop makes one trip per clause, while a batched dispatch is one trip for the whole list. The labels stay the same, so the saved round trip isn't buying a different answer in this fixture.

2-batch-sub-calls-aggressively.py
1def llm_query(prompt: str) -> str: 2 return f"analysis: {prompt[:20]}" 3 4def llm_query_batched(prompts: list[str]) -> list[str]: 5 return [llm_query(prompt) for prompt in prompts] 6 7def analyze_unbatched(clauses: list[str]) -> tuple[list[str], int]: 8 results = [llm_query(f"Analyze clause: {clause}") for clause in clauses] 9 return results, len(clauses) 10 11def analyze_batched(clauses: list[str]) -> tuple[list[str], int]: 12 prompts = [f"Analyze clause: {clause}" for clause in clauses] 13 return llm_query_batched(prompts), 1 14 15clauses = ["deprecated endpoint grace period", "auth exception audit log"] 16unbatched, unbatched_trips = analyze_unbatched(clauses) 17batched, batched_trips = analyze_batched(clauses) 18 19print(f"unbatched_round_trips={unbatched_trips}") 20print(f"batched_round_trips={batched_trips}") 21print(f"same_labels={unbatched == batched}")
Output
1unbatched_round_trips=2 2batched_round_trips=1 3same_labels=True

3. Enforce strict final-output protocol

Batching controls work inside a route. Completion state controls whether that work can leave the route. Because the interface relies on explicit final-answer signaling, output extraction is protocol-sensitive. In the paper, the algorithm terminates when the REPL sets Final; in the current runtime, code sets answer["content"] and flips answer["ready"] to true.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601[4]Reference 4Recursive Language Models (RLM) Repository.https://github.com/alexzhang13/rlm

Treat the final-output protocol as hard infrastructure rather than soft prompt guidance. Validate the documented completion signal, log violations, and treat stdout-only output as a failed or partial trajectory rather than silently promoting it to a release answer. Ask what the downstream caller can prove: only an explicit ready state identifies the answer payload.

This contract test rejects a printed answer unless the explicit state is ready:

accept-only-explicit-final-state.py
1def extract_final(stdout: str, answer: dict[str, object]) -> str: 2 if not answer.get("ready"): 3 raise ValueError("missing explicit final-answer signal") 4 return str(answer["content"]) 5 6try: 7 extract_final("sunset date is 2026-09-30", {"content": "", "ready": False}) 8except ValueError as error: 9 print(f"stdout_only_rejected={error}") 10 11accepted = extract_final("", {"content": "sunset date is 2026-09-30", "ready": True}) 12print(f"explicit_answer={accepted}")
Output
1stdout_only_rejected=missing explicit final-answer signal 2explicit_answer=sunset date is 2026-09-30

4. Isolate execution for untrusted inputs

The official repository documentation notes that local, non-isolated execution is convenient for initial experimentation, but it isn't suitable for production settings.[4]Reference 4Recursive Language Models (RLM) Repository.https://github.com/alexzhang13/rlm Because RLMs operate by dynamically executing code generated by the language model, they are exposed to code injection or unintended side effects if the environment isn't tightly constrained. The context boundary protects model quality; it doesn't protect the host.

When user-controlled input or untrusted documents are processed, the model could be tricked into generating malicious commands. Therefore, production deployments must run the execution environment in a secure sandbox. A properly configured sandbox can block host filesystem access, restrict outbound networking, and cap resource usage. Test the policy as an admission decision, not as a model instruction.

The repository documents Docker execution and cloud sandboxes such as Modal, Prime, Daytona, and E2B.[4]Reference 4Recursive Language Models (RLM) Repository.https://github.com/alexzhang13/rlm Choose an environment whose configured isolation policy meets your workload's threat model. If you build your own sandbox stack, two common design points are:

  • gVisor: User-space syscall interception that can add strong process isolation without full microVM overhead.[5]Reference 5The True Cost of Containing: A gVisor Case Study.https://www.usenix.org/conference/hotcloud19/presentation/young
  • Firecracker: Lightweight microVMs that provide stronger VM-style isolation for arbitrary code execution.[6]Reference 6Firecracker: Lightweight Virtualization for Serverless Applications.https://www.usenix.org/conference/nsdi20/presentation/agache

Shipping RLM with host-process execution on untrusted workloads is a serious security bug. Sandboxing is mandatory when user-controlled content can influence code generation. A fast answer from an unsafe route is not a successful trajectory.

Make that rule executable in deployment configuration. Product name alone isn't enough: many cloud sandbox templates leave outbound network open, which is exactly what document injection needs for exfiltration. Admit only when an isolation policy object meets your minimums. See also the code-generation sandboxing lesson for host mounts, non-root users, and egress allowlists.

reject-host-execution-for-untrusted-input.py
1from typing import TypedDict 2 3class SandboxPolicy(TypedDict): 4 isolation: str # "docker" | "microvm" | "host" 5 network: str # "deny" | "allowlist" 6 secrets: str # "none" | "mounted" 7 filesystem: str # "ephemeral" | "host_mount" 8 cpu_seconds: int 9 memory_mb: int 10 11MINIMUM_UNTRUSTED = { 12 "isolation": {"docker", "microvm"}, 13 "network": {"deny", "allowlist"}, 14 "secrets": {"none"}, 15 "filesystem": {"ephemeral"}, 16} 17 18def policy_meets_minimum(policy: SandboxPolicy) -> bool: 19 if policy["isolation"] not in MINIMUM_UNTRUSTED["isolation"]: 20 return False 21 if policy["network"] not in MINIMUM_UNTRUSTED["network"]: 22 return False 23 if policy["secrets"] not in MINIMUM_UNTRUSTED["secrets"]: 24 return False 25 if policy["filesystem"] not in MINIMUM_UNTRUSTED["filesystem"]: 26 return False 27 return policy["cpu_seconds"] <= 30 and policy["memory_mb"] <= 2048 28 29def environment_allowed( 30 untrusted_input: bool, 31 policy: SandboxPolicy, 32) -> bool: 33 if not untrusted_input: 34 return True 35 return policy_meets_minimum(policy) 36 37# Product name is metadata; admission keys on isolation policy. 38modal_open_network: SandboxPolicy = { 39 "isolation": "docker", 40 "network": "open", # default template left egress open 41 "secrets": "none", 42 "filesystem": "ephemeral", 43 "cpu_seconds": 30, 44 "memory_mb": 1024, 45} 46modal_locked: SandboxPolicy = { 47 "isolation": "docker", 48 "network": "deny", 49 "secrets": "none", 50 "filesystem": "ephemeral", 51 "cpu_seconds": 30, 52 "memory_mb": 1024, 53} 54host_local: SandboxPolicy = { 55 "isolation": "host", 56 "network": "open", 57 "secrets": "mounted", 58 "filesystem": "host_mount", 59 "cpu_seconds": 3600, 60 "memory_mb": 8192, 61} 62 63print(f"host_for_uploaded_docs={environment_allowed(True, host_local)}") 64print(f"modal_open_network={environment_allowed(True, modal_open_network)}") 65print(f"modal_locked={environment_allowed(True, modal_locked)}") 66print(f"host_for_private_fixture={environment_allowed(False, host_local)}")
Output
1host_for_uploaded_docs=False 2modal_open_network=False 3modal_locked=True 4host_for_private_fixture=True

Dynamic routing: when to recurse

The three guardrails above protect one chosen route. Routing decides whether that route should exist for this request. Not every query needs RLM's overhead. Build a lightweight routing layer that estimates task complexity before committing to a recursive pipeline:

  1. Metadata inspection: If the input comfortably fits in the base model's context window and the task is single-hop retrieval, bypass RLM entirely and use a standard call.
  2. Complexity classifier: Train a cheap classifier (or use a fast LLM) to estimate information complexity (Θ(1)\Theta(1)Θ(1) vs. Θ(N)\Theta(N)Θ(N) vs. Θ(N2)\Theta(N^2)Θ(N2)) based on the query structure and document metadata.
  3. Cost circuit breaker: Set a per-query circuit breaker. If the RLM loop has consumed more than a configured dollar cap without workload-defined progress (for example, no new evidence IDs), terminate and fall back to a summary-based approach.

For example, a single "Which file defines TokenStore?" lookup is a direct-path candidate, while "Find every pair of migration-guide clauses that conflict" is a recursive-path candidate. Predict the cheap route first, then ask whether the measured recall gate rejects it. Neither label releases an answer without held-out quality and budget measurements.

Routing tree: a request that fits and is single-hop uses a direct call, a large single-pass job uses REPL-only RLM, and aggregation or branching uses recursive sub-calls. All three routes meet a held-out eval gate before release.
Fit and decomposition pick a candidate route. Direct, REPL-only, and recursive paths all still have to clear the same held-out recall, cost, and latency gates.

Use measured outcomes as an eval gate: select the smallest route that clears quality and operational limits. The route is a hypothesis until the gate accepts it.

release-a-route-from-eval-results.py
1results = [ 2 {"route": "direct", "recall": 0.73, "p95_cost": 0.08, "p95_seconds": 4.1}, 3 {"route": "repl_only", "recall": 0.92, "p95_cost": 0.19, "p95_seconds": 8.7}, 4 {"route": "recursive", "recall": 0.95, "p95_cost": 0.58, "p95_seconds": 24.0}, 5] 6 7eligible = [ 8 row for row in results 9 if row["recall"] >= 0.90 10 and row["p95_cost"] <= 0.30 11 and row["p95_seconds"] <= 12.0 12] 13chosen = min(eligible, key=lambda row: (row["p95_cost"], row["p95_seconds"])) 14 15print(f"eligible_routes={[row['route'] for row in eligible]}") 16print(f"released_route={chosen['route']}") 17print(f"recursive_blocked_by_budget={results[2]['p95_cost'] > 0.30}")
Output
1eligible_routes=['repl_only'] 2released_route=repl_only 3recursive_blocked_by_budget=True

Where an RLM scaffold can fail

A one-shot completion has no internal recursive call graph or REPL history to grow. An RLM does. The failures below appear once the model can write code against a live REPL, so diagnose the execution path as well as the final text.

Infinite recursion and loops

Because an RLM can write code to call itself, it can write a while True: loop or an infinitely recursive function. This isn't only a theoretical risk. If the model fails to extract the needed information, its retry logic might get stuck. The first useful diagnosis is to ask which limit should fire before the next call starts.

Production systems need independent stop lines. Maximum depth caps the call stack for recursive invocations. A turn limit caps the outer root loop. A compute timeout forcefully ends a REPL execution that runs too long and returns an error so the root can change strategy. A cost budget adds a fourth line when the backend reports spend.

These limits answer different failure modes. A shallow loop can still run too many turns; a short turn count can still contain one expensive call; a cheap loop can still hang inside code. Log which line stopped the run, because "stopped" without a reason is hard to debug and easy to misclassify as success.

Output protocol brittleness

The model signals it's finished through explicit REPL state: Final in the paper's algorithm, and the answer dictionary in the current runtime.[1]Reference 1Recursive Language Models.https://arxiv.org/abs/2512.24601[4]Reference 4Recursive Language Models (RLM) Repository.https://github.com/alexzhang13/rlm Models can still print an apparent answer without setting the required state, or forget to finalize at all. That makes stdout useful diagnostic evidence, but not a release decision.

A safer scaffold records stdout for diagnosis but accepts only its declared final-answer contract for normal success. Recovering arbitrary prints as released answers hides controller regressions.

State bloat and context exhaustion

Every root-model step appends its result to history. If the REPL code prints a 10,000-line log file, that entire dump might stream back into the root model's context, immediately exhausting it.

To solve this, the environment must truncate or summarize execution outputs. Instead of returning raw output, the environment might return: Output truncated: 10,000 lines. The first 5 lines are.... This forces the root model to write more targeted code, keeping the neural context compact. The trade-off is visible: a preview protects the controller but can omit the clause it needs, so retain a queryable variable or source pointer.

Test that the history path preserves a preview without forwarding the entire dump:

truncate-repl-output-before-history.py
1def preview_for_history(stdout: str, limit: int = 40) -> str: 2 if len(stdout) <= limit: 3 return stdout 4 return stdout[:limit] + "...[truncated]" 5 6raw = "\n".join(f"incident log line {index}" for index in range(20)) 7preview = preview_for_history(raw) 8 9print(f"raw_chars={len(raw)}") 10print(f"preview_chars={len(preview)}") 11print(f"truncated={'[truncated]' in preview}")
Output
1raw_chars=409 2preview_chars=54 3truncated=True

Delusion reinforcement across sub-calls

Recursion can turn one tentative guess into several apparently agreeing answers. Suppose the root controller guesses that /v1/search was removed, then asks three children to explain why it was removed. Each child receives the same unsupported premise, so their agreement is correlated. Counting three answers as three pieces of evidence lets the root recursively validate its own mistake.

Treat sub-call text as analysis, not evidence. A child should receive a neutral mission such as "find evidence for or against this claim," return source IDs and counterevidence, and preserve uncertainty when it finds neither. Deduplicate by source identity before aggregation because several children may all summarize the same chunk.

This small checker rejects repeated premise echoes until at least one report points to an approved direct source. One authoritative source can be enough for this fixture; the policy should depend on claim risk rather than a universal vote count.

reject-recursive-premise-echoes.py
1reports = [ 2 { 3 "worker": "scan-a", 4 "claim": "/v1/search removed", 5 "origin": "root-guess-7", 6 "source_ids": [], 7 "contradicts": False, 8 }, 9 { 10 "worker": "scan-b", 11 "claim": "/v1/search removed", 12 "origin": "root-guess-7", 13 "source_ids": [], 14 "contradicts": False, 15 }, 16 { 17 "worker": "scan-c", 18 "claim": "/v1/search removed", 19 "origin": "root-guess-7", 20 "source_ids": [], 21 "contradicts": False, 22 }, 23] 24 25approved_sources = {"doc:deprecation-registry"} 26 27def adjudicate(worker_reports: list[dict[str, object]]) -> str: 28 if any(report["contradicts"] for report in worker_reports): 29 return "conflict" 30 direct_evidence = { 31 source_id 32 for report in worker_reports 33 for source_id in report["source_ids"] 34 if source_id in approved_sources 35 } 36 return "supported" if direct_evidence else "needs_evidence" 37 38print("worker_votes=", len(reports)) 39print("unique_direct_sources=", 0) 40print("echo_verdict=", adjudicate(reports)) 41 42verified = reports + [ 43 { 44 "worker": "registry-check", 45 "claim": "/v1/search removed", 46 "origin": "direct-lookup", 47 "source_ids": ["doc:deprecation-registry"], 48 "contradicts": False, 49 } 50] 51print("verified_verdict=", adjudicate(verified)) 52 53assert adjudicate(reports) == "needs_evidence" 54assert adjudicate(verified) == "supported"
Output
1worker_votes= 3 2unique_direct_sources= 0 3echo_verdict= needs_evidence 4verified_verdict= supported

The checker doesn't prove the cited source itself is correct. It prevents a more basic accounting error: repeated descendants of one root premise aren't independent corroboration. Store lineage, source IDs, and contradiction flags in the trajectory so review can reconstruct how a claim became accepted. If no direct evidence survives, the correct stop state is needs_evidence, not a majority vote.

Failure patterns: symptoms, causes, and fixes

SymptomCauseFix
API bill spikes 10x on a single queryThe root model emitted an unbatched loop with one sub-call per sentence or row.Enforce batching, cap delegated calls per turn, and add a cost circuit breaker.
The model "forgets" the original goal halfway throughContext fragmentation: recursive sub-calls don't receive the original query or global constraints.Pass a compact mission string to every sub-call, and store global variables in the REPL environment rather than relying on context memory.
Runs timeout after 30 seconds with no outputInfinite recursion or a while True: retry loop caused by a missing base case.Enforce max_iters, recursion depth limits, and REPL compute timeouts. Log the last emitted code for debugging.
Final answer is missing, garbled, or appears after the iteration budget expiredThe model printed text without setting the documented final-answer state, or the runtime synthesized a best-effort reply after max_iterations.Fail or flag the trajectory unless answer["ready"] (or the versioned equivalent) is set. Don't promote a timeout fallback to a release answer.
RLM underperforms on simple retrievalYou deployed recursive decomposition for a Θ(1)\Theta(1)Θ(1) needle lookup where a single direct call would win on both latency and cost.Add a routing layer that bypasses RLM when the input fits comfortably in the base model's window and the task is low complexity.
Several children confidently repeat one false claimThe root framed an unsupported guess as a premise, and descendants echoed it without direct evidence.Ask children to seek support and contradiction, require source IDs, deduplicate shared sources, and preserve needs_evidence.

Practice

Use the same 40,000-token API compatibility manual as a small release exercise. Start with the fixed split-and-merge script from this chapter and print its call tree. Before you run it, predict the shape: about 16 leaf calls and 31 calls in total, because the 16 leaves are followed by merge layers of 8, 4, 2, and 1. If your count differs, find whether you counted source chunks, merge nodes, or both.

Next, compare that baseline with two candidate routes: one direct call to a frontier model with a 128K context window, and a recursive chain using a cheaper model with a 4K window. Don't decide from per-token price alone. Write down the sub-call count, batching efficiency, retries, latency, evidence recall, and p95 cost you would measure before release.

Finish with a diagnosis. If the RLM's final summary answers a different question, inspect one child request before blaming model size. A child that received only a local text slice may have no way to preserve the original mission. Forward a compact mission string to every recursive call, then re-run the same held-out evidence check. That is the evidence gate: the route is improved only when the failure symptom and its cause both disappear.

Where RLM sits next to training and prompting

The practice exercise leaves one final distinction. RLM doesn't replace a better pretrained checkpoint. It spends extra inference compute after the weights are already frozen.

Three knobs sit next to each other. Train-time scaling changes the weights through more data, parameters, pretraining, or post-training. Test-time scaling spends extra tokens or samples on a hard query after the weights are fixed.[7]Reference 7Scaling LLM Test-Time Compute Optimally Can Be More Effective Than Scaling Model Parameters.https://arxiv.org/abs/2408.03314 RLM organizes one form of that extra compute by keeping the corpus in a REPL and letting the model write the inspection program. They can be combined, but they answer different bottlenecks.

Sutton's point is older than this paper: a general search loop in a live environment often outlasts a pile of special-case heuristics.[8]Reference 8The Bitter Lesson.http://www.incompleteideas.net/IncIdeas/BitterLesson.html An RLM is that kind of loop for million-token documents. Use it when a direct-context pass misses needed clauses or pairwise relationships, not because "more compute" is fashionable. The loop still needs an evidence contract and an exit.

Maturity is still early. The paper is author-reported. The runtime is author-maintained, with runnable sandbox backends, and that is the concrete artifact to inspect, not a wave of independent production case studies.[4]Reference 4Recursive Language Models (RLM) Repository.https://github.com/alexzhang13/rlm Reproduce the numbers on your workload. Cap depth, sandbox the REPL, keep a depth-0 ablation, and route simple lookups around the scaffold. A competent handoff records which route won, why it won, and which stop condition would reject it next time.

Earlier training chapters changed the weights. Prompt Optimization with DSPy compiled a program around a fixed model. RLM is the runtime fork: reorganize inference over an external environment. The official repository now lists a DSPy.RLM integration among the systems using that interface.[4]Reference 4Recursive Language Models (RLM) Repository.https://github.com/alexzhang13/rlm The released answer still depends on the whole loop (controller, sandbox, budgets, final-answer contract), not only on the checkpoint.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A task examines each unordered pair in a 10,000-item set. What planning estimate follows?

Correct answer: The pair count is roughly 50 million, so an O(N²) design needs batching, pruning, or another explicit cost control.

N(N-1)/2 for 10,000 items is about 50 million comparisons. Moving data into a REPL can reduce prompt size, but it does not erase the underlying pairwise work.

2.What should an RLM place in the outer prompt when the full dataset is available through a controlled REPL?

Correct answer: Keep the raw dataset external and provide compact metadata plus the REPL contract needed to inspect it.

The RLM pattern saves context by keeping large data outside the prompt while exposing safe, documented access. Metadata helps the model plan queries without pretending that a lossy summary is the source of truth.

3.An RLM has selected 500 records. Most need one independent classification call; a small subset needs iterative search, code, and private working state. Which execution plan matches the recursion boundary?

Correct answer: Batch the independent classifications, and allocate child RLMs only to subtasks that need their own REPL and state.

Independent calls benefit from batching and do not justify recursive state. Child RLMs are reserved for decompositions that need an isolated interpreter, iterative tools, and their own budget.

4.A 40,000-token manual is processed with a 4,096-token limit using the article's fixed binary split-and-merge baseline. What budget should the planner expect?

Correct answer: About 16 leaf calls and 31 total calls, with 8, 4, 2, and 1 merge calls above the leaves.

The fixed binary baseline rounds the source into 16 leaves of roughly 2,500 tokens. Counting 16 leaf calls plus 8, 4, 2, and 1 merge calls gives 31 total model calls.

5.How should an RLM mark completion when the final answer is produced?

Correct answer: Emit an explicit answer.ready event or field so the controller can distinguish completion from ordinary stdout.

Explicit completion is part of the protocol. Stdout may contain debugging or intermediate data, so a controller that guesses from text can truncate, duplicate, or accept an unfinished result.

6.A task launches many independent llm_query calls over already-selected records. Which execution choice is most appropriate?

Correct answer: Use llm_query_batched for independent calls, reserving child RLMs for subtasks that need their own REPL.

Batching improves throughput when calls are independent and share the same interface. A child RLM is justified by recursive tool/state needs, not by mere multiplicity of ordinary queries.

7.Which sandbox contract is required before exposing an RLM to untrusted prompts?

Correct answer: Deny egress, expose no secrets, use ephemeral storage, and enforce depth, time, and budget limits.

The REPL is a powerful execution boundary. Network, credentials, persistent files, recursion, and runtime budgets must be constrained by the sandbox rather than left to model compliance.

8.A route gate requires quality >= 0.90, p95 cost <= 0.30, and p95 latency <= 12 seconds. Results are direct 0.73/0.08/4.1, REPL 0.92/0.19/8.7, and recursive 0.95/0.58/24. Which route is eligible?

Correct answer: REPL only, because it clears all three gates while direct misses quality and recursive exceeds cost and latency.

Routing applies every binding gate rather than trading one metric against another without authorization. Direct misses quality; recursive exceeds both resource ceilings; REPL alone satisfies the full contract.

9.A premise-verification task receives several model answers that all repeat the same unsupported claim. What aggregation rule reduces correlated-echo errors?

Correct answer: Require neutral support or contradiction checks and record source IDs instead of treating repeated wording as independent evidence.

Correlated samples can echo a shared premise rather than provide independent confirmation. Evidence-aware aggregation tests the claim against neutral support or contradiction and preserves provenance.

9 questions remaining.

Next Step
Continue to Multi-Agent Orchestration

An RLM still has one controller, one environment, and one final-answer contract, even when it launches child calls. Multi-agent orchestration is the next boundary: specialized workers, shared typed state, dependency graphs, review, and escalation when one controller is no longer the right owner for every branch.

PreviousAgent Failure & Recovery
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Recursive Language Models.

Zhang, A. L., Kraska, T., & Khattab, O. · 2025

https://arxiv.org/abs/2512.24601

Recursive Language Models.

Zhang, A. L. · 2025

https://alexzhang13.github.io/blog/2025/rlm/

Context Rot: How Increasing Input Tokens Impacts LLM Performance

Hong, K., Troynikov, A., & Huber, J. · 2025

https://research.trychroma.com/context-rot

Recursive Language Models (RLM) Repository.

Zhang, A. L., et al. · 2026

https://github.com/alexzhang13/rlm

The True Cost of Containing: A gVisor Case Study.

Young, E. W., et al. · 2019 · HotCloud 19

https://www.usenix.org/conference/hotcloud19/presentation/young

Firecracker: Lightweight Virtualization for Serverless Applications.

Agache, A., et al. · 2020 · NSDI 2020

https://www.usenix.org/conference/nsdi20/presentation/agache

Scaling LLM Test-Time Compute Optimally Can Be More Effective Than Scaling Model Parameters.

Snell, C., et al. · 2024 · arXiv preprint

https://arxiv.org/abs/2408.03314

The Bitter Lesson.

Sutton, R. S. · 2019

http://www.incompleteideas.net/IncIdeas/BitterLesson.html

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.