~/satyajit

Recursive Language Models: context as a variable, recursion as a function call

mdjsonmcp

2026-08-06 · 19 min · agents · llm · recursion · context-management · prime-intellect · explainer

The standard agent loop has one data structure at its center: the transcript. The model reads a growing conversation, emits a tool call that matches a JSON schema, a harness parses it, runs it, and appends the result back as another message. Everything the model can act on has to live in that transcript. Everything it does has to fit the schema. Agent harnesses and the harness effect are both, in different ways, about how much that loop shape costs — in design effort and in tokens. A Recursive Language Model (RLM) doesn't optimize the loop. It replaces the data structure.

An RLM gives the model a persistent Python REPL instead of a transcript. Two things follow from that, and they are the spine of this piece:

  1. Prompt-as-a-variable. The input — a document, a codebase, a 500 MB corpus — becomes a value bound to a name in the REPL, not text sitting in the context window. The model writes code to slice, filter, and search it, and only what it chooses to print ever enters its own context.
  2. Programmatic recursion. Calling another language model is a function call — rlm(...) — that returns a value like any other call. It composes with for loops, if statements, map, and error handling, because it is one of those, not a special harness verb bolted on next to them.

Who actually built this

Prime Intellect's own post is straightforward about credit, and I'll be too: "the Recursive Language Model (RLM), introduced by Alex Zhang in October 2025 as a blog post, and now available as a full paper," with an acknowledgment thanking him "for his original work on recursive language models." Zhang's post frames the mechanism plainly — an RLM is "a thin wrapper around a LM that can spawn (recursive) LM calls for intermediate computation," with an API meant to be a drop-in replacement for an ordinary completion call: rlm.completion(messages) where you'd otherwise write gpt5.completion(messages). The motivating problem is what he calls context rot: model recall degrades as context grows, independent of whether the context still technically fits the window.

The idea was formalized two months later in Recursive Language Models (arXiv 2512.24601, submitted 2025-12-31), authored by Alex L. Zhang, Tim Kraska, and Omar Khattab, all MIT CSAIL. Their own framing in the abstract: RLMs are "a general inference paradigm that treats long prompts as part of an external environment and allows the LLM to programmatically examine, decompose, and recursively call itself over snippets of the prompt." The paper is explicit about what it's reacting against, and credits the right ancestors rather than claiming recursion or code-as-tool-use as new:

So the general idea — treat a long input as an external, programmatically addressable environment, and let recursive delegation happen through control flow instead of prose — has a real, credited origin, and it is not Prime Intellect. What Prime Intellect has done is build two different things on top of it. Their research post says plainly: "we at Prime Intellect have implemented our version of the RLM in verifiers so that it is ready to be used in any environment," landing as the experimental RLMEnv — a reasonably faithful reproduction of Zhang's design, built for running controlled evaluations. Separately, prime-agent-runtime's rlm package — the one this site's Prime Agent piece covers — takes the same two mechanisms and applies them to an entire general-purpose coding agent: files, shell commands, skills, and subagents all go through the same persistent kernel, not just one oversized input prompt. Zhang's design restricts the root model to metadata about the prompt (its length, a prefix) until it explicitly decides to look closer; Prime Agent's root model just has an ordinary working context plus a kernel, because it's built to be a general agent, not a single-prompt inference technique. Related, useful, and worth keeping straight — not the same artifact.

The prompt becomes a variable

Diagram of a Recursive Language Model: a root LM writes code into a Python REPL environment where the input prompt is loaded as a variable; it prints slices of the prompt, calls llm_query() to spawn depth-1 sub-RLMs on chapter fragments, and combines their sub-responses into a final answer.
A Recursive Language Model treats the prompt as part of the environment: loaded as a variable in a REPL, sliced and inspected with code, decomposed into recursive sub-calls whose responses feed the final answer (Zhang, Kraska, Khattab, arXiv 2512.24601, Figure 2 — flattened onto white; layout otherwise unmodified).

In the paper's own algorithm, the root model never receives the prompt as tokens in its context. It receives metadata — length, a prefix, how to access it — and a REPL where that prompt already sits as a variable. The loop is: the model writes code, the REPL executes it, truncated stdout comes back, and this repeats until the model calls FINAL(answer) to return a string directly or FINAL_VAR(name) to return whatever a REPL variable currently holds. Nothing about the prompt's actual content is ever force-fed into the root model's window; the model decides what to look at, a slice at a time.

Prime Agent's version of this same bet is less specialized but the mechanism is identical in spirit: a persistent IPython kernel that survives across turns, with rlm preloaded in the namespace. Here is the actual shim that puts it there, from prime-agent-runtime/src/rlm/__init__.py:

class _RLMCallable:
    async def run(self, prompt: str, **kwargs: Any) -> RLMSpawnHandle:
        return await run(prompt, **kwargs)
 
    async def __call__(self, prompt: str, **kwargs: Any) -> RLMSpawnHandle:
        return await run(prompt, **kwargs)
 
rlm = _RLMCallable()

rlm is not a tool the model selects from a menu. It is a plain Python object with a __call__ method, sitting in the kernel's global namespace the same way any import would. Calling it is calling a function, full stop — which is the whole point: nothing about await rlm(...) needs a harness to specially recognize the string "rlm" and route it through a different code path than any other line of Python.

Here's the same underlying claim made concrete with a task. Say the question is "which of these log files mentions an out-of-memory kill, and which one is worst." A schema-based harness does this as a sequence of round trips — each one a full model turn, a parsed tool call, and an appended result:

# illustrative — the general shape of a schema-based harness, not quoted from a specific product
assistant: tool_call grep(pattern="Out of memory", path="logs/")
tool_result: {"matches": ["logs/worker-014.log:88231", "logs/worker-014.log:88245", ...340 more]}
assistant: tool_call read_file(path="logs/worker-014.log", offset=88200, limit=100)
tool_result: "<100 lines of log text>"
# …and one more round trip per file the model wants to actually look inside

The RLM version, written in the same idiom as the docs' own config_files example (packages/coding-agent/docs/rlm.md):

from pathlib import Path
 
hits = [p for p in Path("logs").rglob("*.log") if "Out of memory" in p.read_text(errors="ignore")]
worst = max(hits, key=lambda p: p.stat().st_size)
print(f"{len(hits)} files mention OOM; worst by size: {worst}")

hits is a real Python list, still there next turn if the model wants to map something else over it. The grep, the read, and the size comparison happen inside one cell. The model's context grows by one printed line, not by one message per file.

context in the window vs. context in a variablewindow size is real, rest is illustrative
read into the windowheld as a REPL variable272.0K tokens+124.7M don't fit~1.2K tokens printed272K-token window (GPT-5, per the RLM paper)corpus: 500 MB (125.0M tokens at ~4 bytes/token)
reading the whole corpus through the window needs about 460 read-and-compact rounds

Drag to 500 MB. The top bar is what a transcript-based agent must do: read chunks into the window until it is full, compact, keep reading — the window never sees more than 272K tokens at once no matter how the corpus is sliced. The bottom bar is the REPL: the corpus sits in a variable sized by machine memory, not context budget, and only what the model chooses to print() — here, a found slice — ever enters its context. This is the same shape as the paper's S-NIAH and OOLONG runs: scale the input and watch one line flatten while the other degrades. The honest caveat is the same one that applies to any REPL-as-context design: if the model prints the whole variable instead of a slice, this bound disappears.

That's the mechanism at data-scale, not code-scale: the top bar is what has to happen when the only way to look at something is to read it into the window — the window caps out at 272K tokens for GPT-5 regardless of how the corpus is chunked, so a 500 MB corpus needs on the order of hundreds of read-and-compact rounds just to scan once, and any single round can only ever see a 272K-token slice. The bottom bar is the REPL: the corpus is bounded by machine memory, not context budget, and only a found, printed slice ever reaches the model. This is the same shape as the paper's own S-NIAH and OOLONG scaling runs — hold the task fixed and grow the input, and one line stays flat while the other falls off past the window boundary.

Recursion is just a call

The second inversion is about delegation. In a schema-based harness, spawning a subagent is a distinct, specially-recognized action — usually literally called Task or subagent in the tool list, with its own parsing path in the harness. In an RLM, rlm(...) is not a different kind of call from anything else in the REPL. It's an async function that happens to start another agent instead of, say, reading a file. Here's the actual implementation, trimmed from the same file:

async def run(prompt: str, **kwargs: Any) -> RLMSpawnHandle:
    """Spawn a recursive Prime Agent child and return once its task is admitted."""
    if not isinstance(prompt, str):
        raise TypeError(f"prompt must be str, got {type(prompt).__name__}")
    payload = await host_request("rlm.run", {"prompt": prompt, "kwargs": kwargs})
    return _spawn_handle_from_payload(payload)

host_request opens a Jupyter comm to the TypeScript host, which creates a real child AgentSession and returns as soon as the task is admitted — not when it's done. That admission- not-completion design is a deliberate choice recorded in Prime Agent's own changelog (covered in more depth in Prime Agent), and it's what makes fan-out compose with ordinary control flow instead of blocking on it:

# real — packages/coding-agent/docs/rlm.md
api_review = await rlm("Review the public API", name="api-reviewer")
test_review = await rlm("Review the test coverage", name="test-reviewer")
integration_audit = await rlm("Run the slow integration audit", name="integration-audit")

Three lines, three independent children, one turn. In a schema-based harness the equivalent is three separate structured messages, each requiring the model to emit a full tool call and the harness to parse and dispatch it — and, if the harness's subagent tool is synchronous, a wait on each before the next line can even be written. Here it's for child in reviewers: await rlm(child) if you want a loop, or three independent statements if you don't. Recursion composes with the rest of the language because it's written in the rest of the language.

recursion tree · rlm(...) as an ordinary function callfrom prime-agent-runtime
root sessionRLM_DEPTH=0child 1RLM_DEPTH=1child 2RLM_DEPTH=1child 3RLM_DEPTH=19 grandchildren requested at RLM_DEPTH=2 — blocked
RLM recursion depth limit reached (RLM_DEPTH=1, RLM_MAX_DEPTH=1)
RLM_MAX_DEPTH
sessions
4
rlm() calls
3 (3 rejected)
own context, per node
1 task
job coverage
3 leaves

Each node's own context holds one task — the model inside it never sees the other branches. The program holds the whole job: the root session's Python namespace, not any single model's context window, is what tracks all 4 sessions. Drag fanout up and the node count grows as fanout^depth, not depth — which is why Prime Agent enforces a real depth budget in code (RLM_DEPTH >= RLM_MAX_DEPTH raises before a comm even opens) instead of trusting the model to stop recursing on its own. Toggle raised to see what an uncapped depth of 2 would actually spawn.

The tree above is the concrete version of "a task over 200 files is a for loop with 200 model calls made by the program, not 200 round trips through the model's own context." Each spawned session's own context holds exactly one task — the child at RLM_DEPTH=1 never sees its siblings, never sees the root's other work. What holds the shape of the whole job is the root session's Python namespace: the list of handles, the loop that produced them, the code that will eventually read their replies. That's a different place for "the state of the whole task" to live than any single model's context window, and it's why the unit of work stops being "how much fits in 200K tokens" and starts being "how many child sessions can the host actually run."

What's actually measured, and by whom

Two different evaluations exist, and they're worth keeping apart because they measure different things with different rigor.

The paper's own numbers (arXiv 2512.24601) are the more citable evidence, because they're in a reviewable artifact with a stated protocol: GPT-5 and Qwen3-Coder-480B-A35B-Instruct, compared against RLM wrappers of themselves, across S-NIAH, OOLONG, OOLONG-Pairs, BrowseComp-Plus, and CodeQA.

The cleanest figures to quote are the ones in the abstract, because they are stated exactly rather than read off a chart, and because they are relative to the right baselines. Wrapping GPT-5 in an RLM beats — by a median across the evaluated benchmarks — 26% against compaction, 130% against CodeAct with sub-calls, and 13% against Claude Code, "while having comparable cost." That middle number is the one that matters most for the argument here: CodeAct also gives the model code execution and sub-calls. The gap between it and an RLM isn't code-versus-schema, it's whether the input lives outside the model's context or inside it. The paper also claims inputs "up to two orders of magnitude beyond model context windows."

And one contribution the write-ups mostly skip: they didn't only wrap existing models, they post-trained one for the paradigm. RLM-Qwen3-8B beats plain Qwen3-8B by 28.3% on average and, per the abstract, "approaches the quality of vanilla GPT-5 on three long-context tasks." An 8B model approaching a frontier model on long-context work by being trained to drive a REPL rather than to read further is the most interesting claim in the paper, and the one I'd most want replicated. The headline figure scales input length from roughly 8K to over 1M tokens on S-NIAH, OOLONG, and OOLONG-Pairs: GPT-5 degrades sharply as input grows, especially past its own 272K-token window where it structurally cannot see the rest of the input at all, while RLM(GPT-5, depth=1) stays roughly flat across the same range. In the paper's tables — read from its figures, so treat the exact decimal as approximate rather than a number I recomputed myself — GPT-5 alone scores around 44% on OOLONG versus roughly 56–58% for the RLM wrapper at recursion depths 1 and 3; on BrowseComp-Plus (1,000 documents, 6–11M tokens total) GPT-5 alone scores 0% because the input doesn't fit at all, versus roughly 91–92% for RLM(GPT-5); on CodeQA, GPT-5 scores around 24% versus roughly 62–66% for the RLM wrapper. These are real, single-paper, not-yet-independently-replicated numbers — but they come with a stated model, a stated task, and a stated context length, which is more than most of what gets cited as evidence for an agent architecture.

Prime Intellect's own post runs a separate, smaller evaluation: GPT-5-mini through their RLMEnv implementation, across four verifiers environments — DeepDive (web research), Math-python, Oolong, and Verbatim-copy — at 50 rollouts each. This is where the honesty has to cut both ways. RLM helps on DeepDive (with explicit strategy tips pushing sub-LLM calls further), helps on Oolong (the plain LLM gets close to zero reward on the longest real-data contexts; RLM keeps working out to roughly 1.5M characters), and helps on Verbatim-copy across most content types. On Math-python it does not: the post reports RLM performing worse than the plain LLM, and ablating the REPL's timeout up to 600 seconds doesn't close the gap. That's a genuine negative result from the people building the thing, reported plainly rather than left out — a useful data point on where "run more code" isn't automatically the right move for a task that's mostly reasoning, not search. Charts, not tables: the post shows relative comparisons rather than a numeric results table, and says so itself — "this is not a measurement of any model's absolute performance on any benchmark."

What this costs

None of the above is free, and the paper and the Prime Agent docs are both honest about the price.

Arbitrary code execution is the primary interface, not a fallback. A fixed tool schema gives you an enumerable, individually-auditable set of actions. A REPL's action space is "anything Python (and a shell cell) can do." Prime Agent's own trust-model documentation says this plainly: the kernel "is a durable control environment, not a security sandbox." Zhang's design narrows the blast radius somewhat by keeping the root model restricted to prompt metadata until it asks for more — but the sub-LM calls and any tool access still execute inside the same interpreter.

A stateful interpreter can wedge. Persistent state across turns is the entire point of the design, but it means a hung cell doesn't just time out cleanly the way a stuck tool call does — Prime Agent's own busy-kernel handling frames the choice as "wait" (preserve state, keep waiting indefinitely) or "kill" (lose every in-memory variable and restart clean). There's no option that gets you both a responsive kernel and the state back.

Recursion needs an enforced budget, not a polite one. The recursion tree above has a real cap behind it: RLM_MAX_DEPTH defaults to 1, and the check —

// packages/coding-agent/src/core/agent-session.ts
if (this._rlmDepth >= this._rlmMaxDepth) {
  throw new Error(
    `RLM recursion depth limit reached (RLM_DEPTH=${this._rlmDepth}, RLM_MAX_DEPTH=${this._rlmMaxDepth})`,
  );
}

— runs in the host before a comm channel even opens, not as a prompt instruction the model could argue its way past. That matters because the cost of recursion is exponential in depth if fan-out is uncapped: fanout^depth sessions, each billed and each capable of spawning more, versus a for loop's cost growing linearly in the number of iterations. A depth cap enforced in code is the difference between "a program that does 200 things" and "a program that can, in principle, spawn without bound."

Harder to sandbox and audit than a fixed schema. Every action a schema-based agent can take is one of N defined functions — individually reviewable, individually denyable. "Any code the model writes" is a much larger surface for a malicious skill, a compromised MCP integration, or a bad instruction to abuse, and it's a correspondingly harder surface to review after the fact.

Debugging shifts from reading a transcript to debugging a program. A schema-based agent's failure mode is usually legible from the transcript alone: read the tool calls and results in order. An RLM's failure mode can be a bug in generated code, a REPL state that's subtly wrong three cells after the mistake that caused it, or a child session that never replies because nothing in the parent's code checks for it. That's a different, and for most engineers a more familiar, debugging discipline — but it is a different one, and treating it like transcript-reading will miss real bugs.

Where this sits

RLM is an inversion of the model Lilian Weng's harness framing and the harness effect both describe: a fixed loop around a fixed tool schema, where the transcript is the only place state can live. RLM doesn't optimize that loop's token economics — it removes the transcript as the place state lives at all, replacing it with an interpreter.

It's also a different axis from two other pieces published alongside it. Recursive Harness Self-Improvement treats the harness as a single text prompt and improves it by comparing it against its own immediately-previous version — harness as a string being optimized. RLM treats the harness as a program the model writes fresh each turn — harness (or at least the working state) as code being executed. And the Continual Harness paperContinual Harness: Online Adaptation for Self-Improving Foundation Agents, by Seth Karten, Joel Zhang, Tersoo Upaa Jr, Ruirong Feng, Wenzhe Li, Chengshuai Shi, Chi Jin and Kiran Vodrahalli — is a third axis again: an online loop that alternates acting with refining the agent's own prompts, skills, memory, and subagent specs during a run, without resetting. (Its Zhang is Joel Zhang, not RLM's Alex L. Zhang — same surname, different author.) Prime Agent's own Continual Harness feature, covered in Prime Agent, draws on this and composes with its RLM runtime. The connection is more than citational: the paper's lead author, Seth Karten, is an active committer to the prime-agent repository, with 39 commits in its history as of 2026-08-06. So the two ideas arrive in the same product from the same people — but they answer different questions. RLM is about how one task executes; Continual Harness is about how the harness's own configuration evolves across tasks.

MemHarness is a useful contrast in the opposite direction from RLM's whole bet. MemHarness's argument is that a retrieved memory should be reconstructed — critiqued and rewritten — against the current state every time it's used, because stale verbatim replay can hurt more than no memory at all. RLM doesn't reconstruct anything: the corpus sits in a variable exactly as it was written, and the model's job is to write code that finds the right slice of it, not to have that slice handed to it pre-digested. Reconstruction spends compute making memory trustworthy before use; RLM spends compute letting the model decide what's worth looking at, each time, from an unmodified source. Different failure modes follow from each: a MemHarness-style system can misreconstruct; an RLM-style system can simply fail to look at the part that mattered.

The idea itself is Zhang, Kraska, and Khattab's, credited honestly by the company building on it. What Prime Intellect has actually built is two separate systems that take the same two mechanisms — context as a variable, recursion as a call — and apply them at different scopes: one faithful to the original single-prompt design, one generalized into an entire agent. Both are real, checkable architecture decisions. The paper's numbers are the most rigorous evidence either has; Prime Intellect's own eval is smaller, honestly mixed, and shown as charts rather than a table; and the product-level ARC-AGI-3 number belongs to a different piece than this one.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Recursive Language Models: context as a variable, recursion as a function call", ai.thesatyajit.com, August 2026.

bibtex
@misc{ghana2026recursivelanguagemodels,
  author = {Satyajit Ghana},
  title  = {Recursive Language Models: context as a variable, recursion as a function call},
  url    = {https://ai.thesatyajit.com/articles/recursive-language-models},
  year   = {2026}
}
share