@florianguyonnet/pi-rlm
v0.2.0
Published
Recursive Language Models for pi: a persistent Python REPL with rlm() sub-agent recursion, as a single extension
Maintainers
Readme
pi-rlm
Recursive Language Models for pi — as a single extension, no fork required.
The agent gets one tool, ipython: a persistent Python REPL whose namespace doubles as long-term memory. Inside that namespace lives rlm(prompt, context=None), which spawns a recursive sub-agent (a fresh pi process with its own clean context window) and returns its answer as a plain Python string. Because sub-results land in REPL variables instead of the conversation, the agent can fan out over far more text than fits in its own context window.
This is a port of the RLM capabilities of prime-agent (which vendors a whole pi fork to get them) onto pi's public extension API — including the namespace snapshot and the continual harness (/refine).
pi ── ipython tool ──► KernelProcess (src/kernel.ts)
│ unix socket, JSONL
▼
rlm_kernel.py ← persistent namespace (stdlib only)
│ rlm("summarize", context=chunk)
▼
child pi -e pi-rlm --rlm (src/child.ts)
│ PI_RLM_DEPTH + 1, may recurse further
▼
answer returns as a Python stringInstall
pi install npm:@florianguyonnet/pi-rlm # from npm (scoped: the bare `pi-rlm` name is taken)
pi install git:github.com/florianguyonnet/pi-rlm # from git
pi install /path/to/pi-rlm # local checkout
pi -e /path/to/pi-rlm/src/index.ts # one-shot, no installRequires Python 3 on PATH (stdlib only — no venv, no pip install). Override with PI_RLM_PYTHON.
Usage
RLM mode — the pure REPL agent. Active tools collapse to ipython only and the system prompt is replaced with the RLM prompt:
pi --rlm "Summarize each chapter of @huge.log and reconcile the totals"or toggle it mid-session with /rlm. Toggle off to restore the full toolset (ipython stays available).
Light mode — with the extension installed but RLM mode off, ipython + rlm() sit alongside pi's normal tools, and a short addendum documents them.
Inside the kernel:
text = open("huge.log").read() # big data stays in the REPL
chunks = [text[i:i+50_000] for i in range(0, len(text), 50_000)]
notes = [rlm("Summarize this chunk, keep all numbers", context=c) for c in chunks]
len(notes), sum(len(n) for n in notes) # repr of trailing expr is returneduser_prompt (the current user request) is injected into the namespace before each turn.
Namespace snapshots
When the session ends, picklable data variables are pickled to ~/.pi/agent/pi-rlm/snapshots/<session-id>.pickle; resuming the session restores them at kernel boot (functions, classes and modules are skipped — re-define those). /new starts fresh, /rlm restart wipes on purpose, ephemeral sessions (--no-session) skip persistence. Snapshots older than 30 days are garbage-collected.
Continual harness (/refine)
pi-rlm ports prime-agent's refinement loop: persistent memories (reusable facts/tactics) and policies (behavior notes) that are injected into the system prompt on every turn.
/refine list # show current entries
/refine prefer small diffs over rewrites # refine now, session-scoped
/refine global always verify with tests # refine now, cross-sessionFrom inside the kernel: refine("create a memory about X", global_=True) — returns immediately, the refinement runs when the turn settles (never mid-cell).
A refinement spawns a tool-less child LLM that analyzes the recent trajectory and applies structured edits (create/update/delete). Local entries live in the pi session (survive /resume, branch with the session tree); global entries live in ~/.pi/agent/pi-rlm/harness.jsonl.
Recursion depth
Root agent = depth 0; each rlm() child is parent depth + 1. Requests beyond the budget are refused in-kernel (rlm() raises, the model adapts). Default max depth: 2.
| Where | How |
|---|---|
| per session | /rlm depth 3 |
| per launch | pi --rlm --rlm-max-depth 3 |
| environment | PI_RLM_MAX_DEPTH=3 (0 disables recursion) |
Commands & flags
| Command | Effect |
|---|---|
| /rlm | toggle RLM mode (persisted in the session) |
| /rlm status | mode, depth, model for children, kernel stats |
| /rlm restart | kill the kernel (next cell reboots with an empty namespace) |
| /rlm depth <n> | session-scoped recursion budget |
| /refine [global] [instructions] | run a harness refinement now |
| /refine list | show harness entries |
| --rlm | start in RLM mode |
| --rlm-max-depth <n> | launch-scoped recursion budget |
Design notes
- No ipykernel, no ZeroMQ. The extension owns both ends of a unix socket; the shim is ~250 lines of stdlib Python. Kernel boot is one
python3spawn, lazily on the first cell. - Cells are serialized;
streammessages mirror stdout/stderr live to the tool row. Esc sends SIGINT (aKeyboardInterrupttraceback, kernel survives); a grace timer escalates to SIGKILL and the next cell reboots with a fresh namespace. - Sub-agents are child pi processes (
--mode json -p --no-session --no-extensions -e <this extension> --rlm), the pattern from pi's subagent example. They inherit the session's model and may recurse until the depth budget runs out. - Nested usage accounting: child token/cost totals roll up onto the
ipythontool result, so footer and/sessiontotals include recursion. - Compaction: the kernel survives it; pi-rlm injects a namespace snapshot (variable names/types) after compaction so the model doesn't lose track of REPL state.
- Snapshots: data survives restarts too — the namespace is pickled per session at shutdown and restored on resume.
rlm()results never enter the parent's context — only what cells print. That's the context-engineering point of RLM, and it falls out of the socket design for free.- Need packages? The kernel is stdlib-only by design, but nothing stops a cell from
import subprocess; subprocess.run([sys.executable, "-m", "pip", "install", "--user", "pandas"]). A managed venv is deliberately out of scope.
What a fork still gets you (and this doesn't)
Honest list, for the prime-agent comparison: child sessions don't appear in pi's /tree or parent accounting UI (we roll usage up manually instead); no fork-server, so each child pays Python + pi cold start; child registry doesn't survive a daemon restart; per-cell cancellation is SIGINT-based and can't interrupt a blocked C call; harness entries cover memories/policies only (no skill/subagent kinds, no rollback history).
Development
npm install # peers (pi packages) resolve for the typechecker
npm test # LLM-free kernel protocol smoke test (python3)
npm run typecheck # tsc --noEmitEnd-to-end probes (uses your configured model):
pi -e ./src/index.ts -p --no-session "Use ipython twice: x = sum of squares 0..9, then x*2. Reply with the number."
PI_RLM_MAX_DEPTH=0 pi -e ./src/index.ts --rlm -p --no-session "Try rlm('hi'), tell me what happens"Releasing
GitHub Actions publishes to npm on version tags (requires the NPM_TOKEN repository secret):
npm version patch # bumps package.json, commits, tags vX.Y.Z
git push --follow-tags # the tag triggers the publish workflowAcknowledgments
- Recursive Language Models — the RLM concept (Zhang et al.).
- prime-agent (MIT, © Prime Intellect) — the product this ports to pi's extension API. pi-rlm reimplements the mechanism from scratch (different kernel architecture, rewritten prompts, own refinement store); no prime-agent code is copied.
- pi (MIT, © Mario Zechner) — the platform; the child-process spawn pattern comes from pi's subagent example extension.
License
MIT
