# pi-rlm

[Recursive Language Models](https://alexzhang13.github.io/blog/2025/rlm/) for [pi](https://github.com/earendil-works/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](https://github.com/PrimeIntellect-ai/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 string
```

## Install

```bash
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 install
```

Requires **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:

```bash
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:

```python
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 returned
```

`user_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-session
```

From 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 `python3` spawn, lazily on the first cell.
- **Cells are serialized**; `stream` messages mirror stdout/stderr live to the tool row. Esc sends SIGINT (a `KeyboardInterrupt` traceback, 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 `ipython` tool result, so footer and `/session` totals 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

```bash
npm install          # peers (pi packages) resolve for the typechecker
npm test             # LLM-free kernel protocol smoke test (python3)
npm run typecheck    # tsc --noEmit
```

End-to-end probes (uses your configured model):

```bash
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):

```bash
npm version patch        # bumps package.json, commits, tags vX.Y.Z
git push --follow-tags   # the tag triggers the publish workflow
```

## Acknowledgments

- [Recursive Language Models](https://alexzhang13.github.io/blog/2025/rlm/) — the RLM concept (Zhang et al.).
- [prime-agent](https://github.com/PrimeIntellect-ai/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](https://github.com/earendil-works/pi) (MIT, © Mario Zechner) — the platform; the child-process spawn pattern comes from pi's subagent example extension.

## License

MIT
