# RLM control plane

> Recursive Language Model model: persistent IPython as the control tool, prompt-as-variable context, and rlm(...) subagent calls.

- Repository: PrimeIntellect-ai/prime-agent
- GitHub: https://github.com/PrimeIntellect-ai/prime-agent
- Human docs: https://grok-wiki.com/public/docs/primeintellect-ai-prime-agent-3367c32760b1
- Complete Markdown: https://grok-wiki.com/public/docs/primeintellect-ai-prime-agent-3367c32760b1/llms-full.txt

## Source Files

- `prime-agent-runtime/src/rlm/__init__.py`
- `packages/agent/src/agent-loop.ts`
- `packages/agent/src/agent.ts`
- `packages/coding-agent/test/acp-rlm-subagents.test.ts`
- `packages/coding-agent/README.md`

---

---
title: "RLM control plane"
description: "Recursive Language Model model: persistent IPython as the control tool, prompt-as-variable context, and rlm(...) subagent calls."
---

Prime Agent’s RLM control plane is the model-facing surface where work runs as Python in a persistent IPython kernel, not as a fixed set of built-in tools. The TypeScript host owns provider streams, session transcripts, child `AgentSession` lifecycles, and credentials; the only default model tool is `ipython`. Context that the model needs again is held as named Python variables and kernel state; recursive work is admitted with `await rlm(...)` over a Jupyter comm bridge (`host.request`), which returns an admission handle immediately and never the child’s answer.

## Control plane shape

| Layer | Component | Owns |
| --- | --- | --- |
| Model loop | `Agent` / agent loop (`packages/agent`) | Streaming turns, tool calls, steering/follow-up queues |
| Session host | `AgentSession` | Policy, host request handlers, child registry, usage attribution |
| Single tool | `ipython` | Lazy kernel provision, bootstrap, cell execute, output shaping |
| Transport | `KernelManager` | Jupyter ZeroMQ channels, comm dispatch, interrupt/shutdown |
| Python surface | `prime-agent-runtime` (`rlm`) | Callable `rlm`, `host_request`, harness proxy, handle types |

```mermaid
flowchart TD
  subgraph host["TypeScript host"]
    loop["Agent loop · stream + tool calls"]
    session["AgentSession"]
    km["KernelManager"]
    child["Child AgentSession"]
    providers["Model providers · BYOK"]
  end

  subgraph kernel["IPython kernel process"]
    ipy["ipython tool cells"]
    rlm["rlm · host_request"]
    skills["Python skills · variables"]
  end

  loop -->|"tool: ipython"| session
  session --> km
  km <-->|"shell · iopub · control"| ipy
  ipy --> rlm
  ipy --> skills
  rlm -->|"comm host.request"| km
  km -->|"typed dispatch"| session
  session --> child
  loop --> providers
  child --> providers
```

Provider choice stays BYOK: spawn can request an exact `provider/model` selector from credentials already configured for the session; the kernel never holds the full auth store.

## Single built-in tool: `ipython`

Default active tools are `["ipython"]`. The tool schema is one field:

<ParamField body="code" type="string" required>
Python scratchpad or a `%%bash` shell cell executed in the agent kernel. Prefer the project’s own environment for project imports, tests, CLIs, and dependency checks.
</ParamField>

On first use the session provisions a kernel (optionally prewarmed). Bootstrap injects:

- callable `rlm` in the user namespace (or a stub that raises if `prime-agent-runtime` is missing);
- pre-imported Python skill modules when installed;
- `nest_asyncio` when available so `await` works inside cells.

Kernel Python resolution order:

1. `PRIME_AGENT_KERNEL_PYTHON` (must import `ipykernel` and a current `prime-agent-runtime` when recursion is used);
2. `~/.prime/agent/kernel-venv` (uv-managed Python 3.11 bootstrap);
3. XDG data path when `~/.prime` is not writable.

Managed bootstrap also installs `prime-agent-runtime`, `dill` (namespace snapshots), and default data packages (`requests`, `httpx`, `pandas`, `numpy`, …). Additional packages in the kernel use `uv pip install <pkg>` (no system `pip` module in that venv).

<Warning>
The kernel runs with the worker’s OS permissions. It is a durable control environment, not a security sandbox. Untrusted repos or third-party skills need an external sandbox or restricted environment.
</Warning>

## Prompt-as-variable context

RLM doctrine in the system prompt treats the kernel as long-lived working memory:

- Assign reads/searches/results to **named variables** so later cells can slice, filter, and act without re-reading.
- **Python state persists** across cells and turns: variables, imports, helpers, parsed outputs, and `rlm` handles.
- **`%%bash` cells** are throw-away subshells: shell `cd` / `export` do not carry over. Persist cwd with `%cd` and env with `os.environ[...]` or `%env`.
- When `%%bash` is used, it must be the **first line** of the cell (no comments or Python before it).
- Do not install project deps into the kernel just to import a repo; run project commands through the project’s own environment.

Ordinary model context remains the host transcript. Compaction can summarize older messages while the kernel (and optional dill snapshot under the session artifact dir) retains intermediate state. Compaction may also restart the kernel to release memory; restore notices then tell the model to recreate lost variables.

```python
from pathlib import Path

configs = list(Path(".").rglob("*.toml"))
large = [p for p in configs if p.stat().st_size > 10_000]
```

```bash
%%bash
npm run check
```

## Recursive calls: `rlm(...)`

### API surface

`prime-agent-runtime` exposes a module-level callable and mirror methods:

| Symbol | Behavior |
| --- | --- |
| `await rlm(prompt, **kwargs)` | Same as `rlm.run` |
| `await rlm.run(prompt, **kwargs)` | Admit a child; return `RLMSpawnHandle` |
| `await rlm.find_models(query="", limit=8)` | Bounded catalog of models with active credentials |
| `await rlm.list_subagents()` | Direct children of this parent session |
| `await rlm.delete_subagent(target)` | Cancel/close and tombstone one direct child |
| `await rlm.host_request(type, payload?)` | Generic typed host bridge |
| `rlm.harness` | Continual harness ledger (session-local by default) |

`RLMSpawnHandle` fields: `rlm_child_id`, `name`, `session_dir`, `model`.

`rlm.run` kwargs (unknown keys fail):

| Kwarg | Type | Rules |
| --- | --- | --- |
| `name` | `str` | Unique among siblings; max 64 chars; optional (host generates `subagent-…` default) |
| `model` | `str` | Exact `provider/id` from `find_models`; unavailable or failed auth preflight aborts spawn (no silent fallback) |

Default recursion: root `RLM_DEPTH` is 0 (or env); `RLM_MAX_DEPTH` defaults to **1** (root may create children; children may not recurse further unless raised via env, settings `rlmMaxDepth`, session inheritance, or the rlm-max-depth APIs).

### Admission contract

1. Model executes `await rlm("…")` inside an `ipython` cell.
2. Kernel opens a `host.request` comm with type `rlm.run`.
3. Host checks depth, name uniqueness, model selection, then creates `sub-xxxxxxxx` under the parent artifact directory.
4. Reply returns the handle **immediately after admission** (status moves through queued/running independently).
5. Child runs as a full `AgentSession` with its own context, incremented `RLM_DEPTH`, and inherited tools/skills/providers unless model was overridden.
6. Child answers only via `agent_message` (or files), never as the `rlm()` return value.

```python
handle = await rlm("Review the authentication flow", name="auth-reviewer")
print(handle.rlm_child_id, handle.name, handle.session_dir, handle.model)

# Parallel independent work — end the turn; do not await completion
api = await rlm("Review the public API", name="api-reviewer")
tests = await rlm("Review test coverage", name="test-reviewer")
```

```python
# Child side (when agent_message is installed)
await agent_message.send(message, receiver_role="parent")

# Parent follow-up
await agent_message.send(
    "Check the new regression test.",
    receiver_role="child",
    receiver_name=api.name,
)

children = await rlm.list_subagents()
await rlm.delete_subagent(children[0])  # when context is no longer needed
```

`list_subagents` status values: `running` | `completed` | `error`. Registry is parent-scoped and survives compaction, kernel restart, and parent restore for daemon-backed completed children; unrelated new parent sessions do not inherit it.

### Host request types (kernel → session)

| Type | Role |
| --- | --- |
| `rlm.run` | Spawn child |
| `rlm.find_models` | Model search (`limit` 1–20; default 8) |
| `rlm.list_subagents` | Registry read |
| `rlm.delete_subagent` | Delete by id / active-session id / session id / unique name |
| `model.info` | Current parent model metadata |
| `goal.*` | Goals (when enabled) |
| `compact.*` | Compaction skill bridge |
| `refine.*` | Harness refine bridge |
| `rlm_heartbeat.*` | Heartbeat scheduling |
| `agent_message.*` | Cross-agent messaging |

Request type is applied last on the comm payload so a payload `"type"` key cannot reroute the call.

## Control channel and deadlock avoidance

IPython processes shell messages serially. If `rlm.run` replied on the **shell** channel while a cell was blocked on that reply, the kernel would deadlock.

The Python shim therefore registers comm handlers on the **control** channel; the host sends admission replies there. Futures complete with `loop.call_soon_threadsafe` because control handlers may run on another thread. Ordinary cell I/O stays on shell/IOPub.

```text
shell    execute_request / execute_reply / kernel_info
iopub    stdout, stderr, results, errors, status, comm_open
control  interrupt, shutdown, host-request replies during execute
```

`KernelManager.execute()` serializes ordinary cells on one shared namespace. Concurrent RLM children still run as separate sessions (and optional kernels), not as concurrent cells in the parent kernel.

## Usage attribution

The admission handle has no usage or completion fields. Child assistant usage/cost is folded asynchronously into the parent assistant turn that launched the child. Transcripts may record `child_usage_attributed` entries for reload. Context-tree reporting separates a node’s own usage from attributed child totals so aggregate cost remains reconcilable.

ACP clients can observe subagent lifecycle as namespaced session-update metadata (`rlm_child_update` → ACP meta `subagents`) for the session lifetime, including fire-and-forget spawns outside a prompt turn.

## Failure modes

| Condition | Result |
| --- | --- |
| Missing `prime-agent-runtime` in kernel | Clear `RuntimeError` on `rlm.*` |
| `RLM_DEPTH >= RLM_MAX_DEPTH` | Host raises recursion limit error (Python may check first) |
| Unsupported `rlm.run` kwargs | Host rejects with listed keys |
| Unknown / unauthenticated `model` | Spawn fails; no substitution |
| Duplicate child `name` | Name unavailable error |
| Parent disposed during spawn | Spawn rejected |
| Busy kernel after interrupt | Wait (preserve state) or kill (lose variables) |
| Parent teardown | Active descendants cancelled; runtimes closed |

## Verification signals

<Check>
After first `ipython` use: kernel starts without import errors; `rlm` is callable in a cell (`await rlm.find_models()` returns a list, possibly empty).
</Check>

<Check>
`handle = await rlm("ping", name="probe")` returns four fields immediately; parent UI/ACP shows a running child update; completion arrives only via message or files.
</Check>

<Check>
At max depth 1, a child call to `rlm(...)` fails with the depth limit message.
</Check>

## Related pages

<CardGroup>
  <Card title="Continual Harness" href="/continual-harness">
    Durable prompts, memories, skill specs, subagent specs, and refine boundaries layered on the RLM runtime.
  </Card>
  <Card title="Subagents and messaging" href="/subagents-messaging">
    Child agents, agent-message skill surface, and multi-agent constraints.
  </Card>
  <Card title="Skills model" href="/skills-model">
    Python-backed skills imported into the same kernel as first-class callables.
  </Card>
  <Card title="Long-running tasks" href="/long-running-tasks">
    Goals, compaction, heartbeats, autonomous mode, and retained children across disconnects.
  </Card>
  <Card title="Sessions and runtime" href="/sessions-runtime">
    Session lifecycle, events, queueing, and session-scoped vs durable state.
  </Card>
  <Card title="ACP mode" href="/acp-mode">
    How RLM subagent updates surface to editor clients.
  </Card>
</CardGroup>
