# RLM Programming Model

Prime Agent is built around a recursive language model (RLM) runtime: the model works inside a persistent Python control environment and composes capabilities as code. Provider calls, session persistence, child lifecycles, scheduling, and safety policy remain in the TypeScript host; the Python REPL is the model-facing programming surface.

## RLM Loop

```mermaid
flowchart LR
    task["Task + working context"]
    parent["Parent model"]
    kernel["Persistent Python kernel"]
    data["Files · data · shell commands"]
    skills["Python-backed skills"]
    children["rlm(...) child agents"]
    answer["Answer or next turn"]

    task --> parent
    parent -->|"Python call"| kernel
    kernel <-->|"inspect · search · transform"| data
    kernel <-->|"call functions"| skills
    kernel -->|"spawn focused work"| children
    children -->|"agent messages · structured results · files"| parent
    kernel -->|"admission handle"| parent
    parent --> answer
```

The parent keeps its own context focused while Python holds working state and child agents receive only the context needed for their subtasks.

## Core Invariants

### 1. Execution is programmatic

The default RLM runtime exposes one built-in model tool: `ipython`. Reading and editing files, running project commands, transforming results, invoking skills, and delegating work all begin from that persistent kernel instead of separate built-in tool calls.

Python state survives across tool calls and compaction. Variables, imports, functions, parsed results, and task handles remain available on later turns:

```python
from pathlib import Path

config_files = list(Path(".").rglob("*.toml"))
large_files = [path for path in config_files if path.stat().st_size > 10_000]
```

Run a project's normal commands through its own environment with `bash()`:

```python
result = await bash("npm run check")
print(result.output)
```

Each `bash()` call is its own process, while Python state, `os.chdir(...)`, and `os.environ[...]` changes persist in the kernel and apply to later `bash()` calls. Prime Agent extensions may intentionally add custom tools, but the built-in RLM design does not require a separate model tool for every capability.

### 2. Subagents are native RLM calls

The callable `rlm` object is preloaded in the kernel. Every spawn declares how the child reports back with a `result` contract; a schema-less spawn is an error unless the caller passes the explicit opt-out `unguarded=True`. Spawn a child with a direct call:

```python
handle = await rlm(
    "Review the authentication flow for security issues",
    name="auth-reviewer",
    model="provider/model",
    result={"schema": {"type": "object", "properties": {"status": {"type": "string"}}}, "required": False},
)
print(handle.rlm_child_id, handle.name, handle.session_dir, handle.model)
```

The call returns immediately after task admission with a child handle; it never waits for or returns the child's answer. The TypeScript host creates a normal child `AgentSession` with an independent context and session directory. Every spawn must select an exact `provider/model`; the parent model is never inherited implicitly. Provider configuration, skills, tools, retry policy, and the resource loader still follow the parent session.

Spawn independent children in separate calls and end the turn instead of awaiting completion:

```python
api_review = await rlm("Review the public API", name="api-reviewer", model="provider/model")
test_review = await rlm("Review the test coverage", name="test-reviewer", model="provider/model")
integration_audit = await rlm(
    "Run the slow integration audit",
    name="integration-audit",
    model="provider/model",
)
```

With `unguarded=True`, the legacy freeform path applies: results arrive through explicit replies or files, never as an `rlm()` return value, and children can reply when an answer is needed. Guarded children instead submit their only voice through `rlm.submit_result`:

```python
await rlm.submit_result({"status": "approved"})   # from inside a guarded child
await api_review.send("Check the newly added regression test.")
```

#### Composable result and context contracts

A child can receive independent result and context policies without selecting a named mode or preset:

```python
schema = {
    "type": "object",
    "properties": {
        "status": {"type": "string", "maxLength": 32},
        "finding_ids": {"type": "array", "items": {"type": "string"}},
        "private_notes": {"type": "string", "maxLength": 4000},
    },
    "required": ["status", "finding_ids", "private_notes"],
    "additionalProperties": False,
}
result_fields = ["status", "finding_ids"]

review = await rlm(
    "Review the change and submit the contracted result.",
    model="provider/model",
    result={"schema": schema, "sink": {"kind": "file"}, "required": True},
    context={
        "terminal": {"payload": False, "notify": True},
        "direct_messages": {"payload": False, "notify": True},
        "observe": False,
        "result": {"fields": result_fields},
    },
)
```

The host binds the contract to the admitted child identity. With `required=True`, the child submits exactly one value per task turn:

```python
await rlm.submit_result({
    "status": "approved",
    "finding_ids": [],
    "private_notes": "not admitted to parent context",
})
```

The parent reads the schema-validated projection admitted at spawn. Omitting `fields` returns the admitted set; pass `fields` only to request a narrower projection:

```python
result = await rlm.get_result(review)
print(result.value)
```

A projected read must stay under 32 KiB. For larger evidence, make the child return a compact summary or have a bounded fan-in child read the artifact and submit a smaller schema.

Set `required=False` when a task turn may finish without a structured result. The child prompt marks submission as optional. An omitted result creates no ready generation and leaves the latest settled result unchanged. Any submitted value must still match the schema.

Each context field acts independently. `payload=False` prevents child-authored terminal or direct-message text from reaching the recipient; `notify=True` retains a fixed host-authored notice. `observe=False` removes transcript previews and recent messages when a parent or sibling observes the contracted child. `context.result.fields` is the maximum result projection the parent can request.

**Guarded is the default.** When `result` is present and `context` is omitted, the host applies a guarded context: terminal and direct-message payloads are withheld behind fixed notices and observation previews are stripped. The result file is still validated, but no result fields enter parent model context unless `context.result.fields` admits them. Open a channel explicitly with `context={"terminal": {"payload": True}}` or `context={"direct_messages": {"payload": True}}`. The only schema-less escape is `unguarded=True`, which restores legacy freeform children. A result contract is required for every spawn; omitting it without `unguarded=True` fails closed at the spawn boundary.

The host still keeps an internal per-child follow-up journal so daemon restart cannot lose an unadmitted completion notice; this journal is not model-visible and does not add fields to the public handle.

Schemas use a bounded JSON Schema subset. External references, unsupported keywords, oversized schemas and results, invalid values, integers outside JavaScript's safe range, duplicate submissions, and field requests outside the admission list fail closed. Contracts contain model context; they do not restrict same-UID filesystem access or make admitted string fields semantically trustworthy.

#### Child handles and lifecycle

`rlm(...)` and `rlm.list_subagents()` return the same mutable `RLMSubagent` handle. It contains `rlm_child_id`, `name`, `session_dir`, `model`, and `status`; contracted results also add `result_path`. Child usage is attributed to the parent session while remaining distinguishable in context-tree reporting.

The handle owns the whole lifecycle:

```python
await child.refresh()                # merge the latest registry state in place
await child.send("Follow-up note.")  # direct message to the child
await child.observe()                # bounded recent-message previews
await child.result(fields=["status"])  # admitted structured result, when contracted
await child.delete()                 # remove the child when no longer needed
```

Module-level equivalents exist on `rlm` itself: `rlm.send_parent(message)`, `rlm.send_sibling(name, message)`, `rlm.send_child(name, message)`, `rlm.family()`, `rlm.observe(name)`, `rlm.info(name)`, and `rlm.delete_subagent(child_or_id)`.

The parent-scoped child registry survives compaction, kernel restart, and parent restoration:

```python
children = await rlm.list_subagents()
for child in children:
    print(child.name, child.status, child.active_session_id)
```

Successfully completed daemon-backed children remain addressable while their parent session is open. Delete a child only when its context is no longer needed:

```python
await children[0].delete()
```

The default recursion depth allows a root agent to create children. Raising the configured depth allows descendants to recurse further.

### Root session controls

Top-level sessions can inspect and control other top-level daemon sessions through a separate namespace. This is intentionally not part of `rlm`: RLM child handles carry result-contract state and are not root-session handles.

```python
from agent import sessions

roots = await sessions.list()
target = await sessions.get(active_session_id="active-session-id")
recent = await target.recent_turns(count=3, max_chars=6000)
receipt = await target.send("continue the task", delivery="follow_up")
```

`agent.sessions` resolves a target by active session ID, durable session ID, display name, or an unambiguous ID suffix. `recent_turns()` returns bounded user/assistant/tool-call summaries and never exposes an unbounded transcript. `send()` uses the native daemon `steer` or `follow_up` queue. `stop()` terminates and archives a target; `restart()` asks the daemon supervisor to rehydrate its worker. A root cannot stop or restart itself, and RLM child kernels cannot access this namespace. Use `await target.refresh()` after a worker restart to update a retained handle.

Root session handles are separate from `RLMSubagent`: they have no result contract, `result()`, or child deletion semantics. The host remains authoritative for session identity, transcript access, worker routing, and lifecycle transitions.

### 3. Skills add programmatic capability

Prime Agent supports the Agent Skills markdown format and extends it with Python-backed skills. Both use `SKILL.md` for discovery, routing, and instructions. A Python-backed skill also contains a Python package that Prime Agent installs into the kernel environment and exposes by import name.

For a skill named `release-audit`, the model can call:

```python
report = await release_audit(repository=".", target_version="0.4.0")
```

This makes Python-backed skills a superset of instruction-only skills: they can provide guidance, scripts, references, dependencies, typed callables, and optional shell commands. They may also call `rlm(...)` themselves when a capability needs recursive delegation.

Only skill metadata is placed in the startup prompt. The agent loads the full `SKILL.md` when the task matches, then inspects and calls the documented Python API. See [Skills](skills.md) for discovery, packaging, and the built-in skill-creation workflow.

### 4. State is designed to outlive one turn

The RLM programming model assumes useful work may take many turns or continue after the terminal UI closes:

- automatic compaction summarizes older context while preserving recent messages and kernel state;
- daemon-backed workers keep active sessions running after clients detach;
- child registries and session artifacts make subagents recoverable;
- heartbeats and scheduled prompts re-enter a session later;
- persistent goals continue until the objective is complete or the user changes their state; and
- autonomous mode adds bounded continuations and optional quality gates.

See [Long-Running and Background Agents](long-running-agents.md) for these lifecycle features.

## Host Bridge

Python code uses typed host requests for capabilities whose authoritative state belongs outside the kernel. The `rlm` callable itself sends messaging and observation requests this way, and the `goal`, `rlm_heartbeat`, and `compact` skills call `rlm.host_request(...)`; the TypeScript host validates the request and owns the state transition.

This keeps credentials, provider execution, transcript writes, worker routing, and scheduling out of Python while retaining a programmatic model interface.

## Trust Model

The Python kernel runs model-generated Python and project commands with the worker's operating-system permissions. It is a durable control environment, not a security sandbox. Review third-party Python skills and use an external sandbox or restricted environment for untrusted repositories and instructions.

For implementation details, see [RLM Runtime Architecture](rlm-runtime.md).
