Metadata-Version: 2.5
Name: fast-rlm
Version: 0.5.0
Summary: Light-dependency, Feature-Rich Implementation of Recursive Language Models
Project-URL: Documentation, https://avbiswas.github.io/fast-rlm/
Project-URL: Repository, https://github.com/avbiswas/fast-rlm
Project-URL: Original Paper, https://arxiv.org/abs/2512.24601
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: pyyaml>=6.0
Provides-Extra: benchmarks
Requires-Dist: datasets==3.6.0; extra == 'benchmarks'
Description-Content-Type: text/markdown

# fast-rlm

[![PyPI](https://img.shields.io/pypi/v/fast-rlm)](https://pypi.org/project/fast-rlm/)
[![GitHub](https://img.shields.io/github/stars/avbiswas/fast-rlm)](https://github.com/avbiswas/fast-rlm)
[![Docs](https://img.shields.io/badge/docs-avbiswas.github.io%2Ffast--rlm-blue)](https://avbiswas.github.io/fast-rlm/)

A minimal implementation of Recursive Language Models (RLMs) using Deno and Pyodide.

[GitHub](https://github.com/avbiswas/fast-rlm) | [Documentation](https://avbiswas.github.io/fast-rlm/) | [PyPI](https://pypi.org/project/fast-rlm/)

> **Watch the full video on YouTube**
> **[RLM Tutorial](https://youtu.be/nxaVvvrezbY)**

## What are RLMs

RLMs are an inference technique where an LLM interacts with arbitrarily long prompts through an external REPL. The LLM can write code to explore, decompose, and transform the prompt. It can recursively invoke sub-agents to complete smaller subtasks. Crucially, sub-agent responses are not automatically loaded into the parent agent's context — they are returned as symbols or variables inside the parent's REPL.

## Support

If you find this helpful, consider supporting on Patreon — it hosts all code, projects, slides, and write-ups from the YouTube channel.

[<img src="https://c5.patreon.com/external/logo/become_a_patron_button.png" alt="Become a Patron!" width="200">](https://www.patreon.com/NeuralBreakdownwithAVB)

---

## Demo

<video src="https://github.com/user-attachments/assets/fcaeab69-e384-4b26-8d6a-b71c1464e7f2" controls width="100%"></video>

---

## Install

```bash
pip install fast-rlm
```

### Requirements

- Python 3.10+
- [Deno](https://deno.land/) 2+
  - macOS/Linux: `curl -fsSL https://deno.land/install.sh | sh`
  - Windows (npm): `npm install -g deno`
- (Optional) [Bun](https://bun.sh/) — only needed for the TUI log viewer
- (Optional) Node/npx — only needed for ACP agents, which are opt-in
  (`fast-rlm acp install`)

### Environment Variables

Set your LLM API key before running:

```bash
export RLM_MODEL_API_KEY=sk-or-...
```

| Variable | Description | Default |
|----------|-------------|---------|
| `RLM_MODEL_API_KEY` | API key for the OpenAI-compatible backend (falls back to `OPENAI_API_KEY`, then `OPENROUTER_API_KEY`) | — |
| `RLM_MODEL_BASE_URL` | OpenAI-compatible base URL | `https://openrouter.ai/api/v1` |

That's all you need to get started. By default, fast-rlm uses [OpenRouter](https://openrouter.ai); you can point it at any OpenAI-compatible API by setting `RLM_MODEL_BASE_URL`. fast-rlm also runs on Vertex AI, the native Anthropic API, and local coding agents (Claude Code, Codex, opencode) — see **Backend setup** at the end of this README.


## Quick Start

![Quickstart](docs/images/quickstart.jpeg)

```python
import fast_rlm
from fast_rlm import RLMConfig

# primary_agent is REQUIRED — there is no default model.
config = RLMConfig(primary_agent="z-ai/glm-5")

result = fast_rlm.run("Generate 50 fruits and count number of r", config=config)
print(result["results"])
print(result["usage"])
```

> **`primary_agent` is required.** Every `run()` needs a config that sets it (e.g. `RLMConfig(primary_agent="...")`); `sub_agent` is optional and defaults to `primary_agent`. The shorter examples below omit `config=` for brevity — pass the `config` above to run them.

### From the command line

The same engine is available as a `fast-rlm` CLI — handy for one-off runs and shell pipelines:

```bash
# A plain prompt
fast-rlm "Generate 50 fruits and count number of r" --primary-agent z-ai/glm-5

# Feed a file as the context. Parsed by extension:
#   .json/.yaml/.yml -> dict/list   .jsonl/.ndjson -> list[dict]
#   anything else (.csv, .tsv, .xml, .toml, .txt, ...) -> raw text the model parses
#       itself (its extension is noted so it knows the format).
# The prompt becomes the instruction; for a dict input with no "instruction" key,
# it's also injected into the dict.
fast-rlm "Aggregate the reviews into a verdict" --input-file reviews.json --primary-agent z-ai/glm-5

# -q prints only the result (clean for piping); other knobs mirror RLMConfig:
fast-rlm "..." --primary-agent acp:opencode --max-depth 2 --max-global-calls 50 -q
```

Run `fast-rlm --help` for all flags (`--sub-agent`, `--max-calls`, `--acp-agents`, `--vertex`, …).

There are also two subcommands for the optional ACP backend:

```bash
fast-rlm acp install [-u]   # install ACP support (-u upgrades the pinned versions)
fast-rlm acp status         # installed versions + available upgrades
```

The same file loading is available from Python — `run()` accepts an `input_file` (in place of `query`):

```python
fast_rlm.run(input_file="reviews.json", instruction="Aggregate into a verdict", config=config)
```

## Model backends

The `primary_agent` / `sub_agent` string selects one of four backends:

| Mode | Example `primary_agent` | What it is |
|---|---|---|
| **Any OpenAI-compatible API** (default) | `"gpt-5-mini"`, `"deepseek-chat"`, `"minimax/minimax-m3"` | OpenAI, DeepSeek, OpenRouter (default), or any compatible endpoint |
| **Vertex AI** | `"vertex/claude-sonnet-4-6"` | Google Cloud (ADC auth) |
| **Anthropic API** | `"claude-haiku-4-5"`, `"anthropic/claude-sonnet-4-6"` | Native Anthropic; falls back to the OpenAI-compatible endpoint if no key |
| **CLI coding agent** | `"cli:claude-code?model=claude-sonnet-5"`, `"cli:codex?model=…"`, `"cli:opencode?model=…"` | Drives a local coding agent through its own non-interactive mode. No install step; a model is required |
| **ACP coding agent** | `"acp:codex"`, `"acp:claude-code"`, `"acp:opencode"` | Same, over the ACP protocol (opt-in: `fast-rlm acp install`) |

Set the credential only for the backend(s) you use — see **Backend setup** at the end of this README. An ACP-only run needs no API key at all.

## Arbitrarily Long Context

The key idea behind RLMs is that the prompt can be arbitrarily long — far beyond any model's context window. The agent explores it programmatically through the REPL rather than trying to fit it all into a single call.

```python
import fast_rlm

transcripts = open("lex_fridman_all_transcripts.txt").read()  # millions of tokens

result = fast_rlm.run(
    "Here are the transcripts of all Lex Fridman podcasts. "
    "Summarize what the first 5 Machine Learning guests had to say about AGI.\n\n"
    + transcripts
)
print(result["results"])
```

The agent will write code to search, filter, and chunk the transcripts on its own — no manual splitting required.

## Structured Input & Output

Instead of squeezing your data into a string, you can pass a `dict` as the query and ask for a typed result back via `output_schema`. The agent receives the dict as a real Python `dict` (no parsing on its first turn), and its `FINAL` value is validated against the schema before being returned.

```python
import fast_rlm
from pydantic import BaseModel

class Verdict(BaseModel):
    movie: str
    average_score: float
    consensus: str

result = fast_rlm.run(
    {
        "task": "Aggregate the reviews into a single verdict.",
        "movie": "The Trail of Pixels",
        "reviews": [
            {"name": "Asha", "score": 8, "text": "Tight pacing..."},
            {"name": "Bo",   "score": 6, "text": "Beautiful but thin..."},
            {"name": "Cy",   "score": 9, "text": "Instant favorite..."},
        ],
    },
    output_schema=Verdict,
)

verdict = Verdict.model_validate(result["results"])
```

**Structured input.** When `query` is a `dict`, the agent's initial probe prints a flat top-level schema (keys + type + length + truncated preview) so it can index `context["reviews"]` directly instead of stringifying.

**Structured output.** `output_schema` accepts:

| Form | Example |
|---|---|
| Pydantic model class | `output_schema=MyModel` |
| Pydantic generic | `output_schema=list[MyModel]` |
| Python primitive | `output_schema=int` (also `str`, `float`, `bool`, `list`, `dict`) |
| Raw JSON Schema dict | `output_schema={"type": "array", "items": {"type": "string"}}` |

The schema is shown to the agent at step 0 (`Required output schema for FINAL (JSON Schema):`). After every `FINAL(...)` call the value is validated; on failure the agent receives the schema and the specific validation errors (path + message) and may retry within its remaining call budget. Pydantic is an *optional* dependency — only required if you pass a Pydantic class or generic.

**Schemas for subagents.** Inside the REPL the agent can require a subagent's output shape by passing a JSON Schema dict as the second argument to `llm_query`:

```repl
schema = {"type": "array", "items": {"type": "string"}}
fruits = await llm_query("Generate 25 fruit names.", schema)
```

The child subagent enforces the schema the same way. See [`examples/structured_io.py`](examples/structured_io.py) and [`examples/parallel_r_count.py`](examples/parallel_r_count.py) for end-to-end demos.

## Tools

Inside the REPL the agent has two built-in tools and may also receive user-defined tools as ordinary Python functions. There is no separate tool-calling API — tools are just callables in the REPL namespace.

Pass Python functions to `fast_rlm.run(..., tools=[my_fn])` and they will be pre-loaded into the root agent's REPL. The RLM is shown the function name, input names, and docstring as description. They are not shown the full internal code of the tool (although they can choose to inspect it if the task requires them to). The agent calls them like any normal function inside the REPL.

```python
def filter_short(items: list[str], max_len: int = 20) -> list[str]:
    """Return only items shorter than max_len."""
    return [x for x in items if len(x) < max_len]

result = fast_rlm.run("Pick the short titles from the list." + str(list_of_titles), tools=[filter_short])
```

Two rules apply to any tool that may be handed to a sub-agent:

- **Sub-agents do NOT inherit tools automatically.** To give a child a tool, the main agent must pass it explicitly in the REPL: `await llm_query("...", tools=[filter_short])`. Set `RLMConfig(inherit_tools=True)` to flip that default so every sub-agent starts with its parent's tools; an explicit `tools=[...]` still overrides per call, and `tools=[]` grants none.
- **Tools must be self-contained.** Do imports *inside* the function body and don't close over REPL-level variables - the child runs in a fresh REPL where outer state does not exist.

The agent can also `def` new functions inside the REPL at any time and pass them down the same way.

Currently all tools are expected to be Python functions. These functions are available inside the REPL. They are NOT available when the LLM produces code or generates reasoning steps.

## Passing environment variables inside the REPL

Tools often need credentials or configuration (API keys, base URLs, account IDs). Pass them through the `env_variables` kwarg on `fast_rlm.run(...)`:


```python
import os
import fast_rlm

def search_web(query: str, top_k: int = 5) -> list[dict]:
    """Search the web via Tavily and return the top results."""
    import os, urllib.request, json
    req = urllib.request.Request(
        "https://api.tavily.com/search",
        data=json.dumps({"query": query, "max_results": top_k}).encode(),
        headers={
            "Authorization": f"Bearer {os.environ['TAVILY_API_KEY']}",
            "Content-Type": "application/json",
        },
    )
    return json.loads(urllib.request.urlopen(req).read())["results"]

result = fast_rlm.run(
    "Find three recent papers on recursive language models.",
    tools=[search_web],
    env_variables={"TAVILY_API_KEY": os.environ["TAVILY_API_KEY"]},
)
```

Behavior:

- `env_variables` must be a `dict[str, str]`.
- Each entry is injected into `os.environ` inside **every** Pyodide REPL spawned by the run — the root agent and all sub-agents.
- They are **not** set on the host Deno process and never appear in prompts, logs, or model context. The model only ever sees a tool's signature + docstring, so the key stays hidden as long as your tool doesn't print or return it.
- Tools read them with the normal `os.environ["..."]` (do the `import os` inside the tool body — see the self-containment rule above).


## Resumable sessions

A `Session` lets follow-up queries reuse the work of earlier ones instead of re-exploring from scratch. After every step, the root agent's picklable REPL variables are auto-saved, REPL-defined functions/classes are saved as source, and comments the agent wrote next to assignments are attached to the variables they describe. The next `query()` restores everything into a fresh REPL and shows the agent the prior queries + answers and the code it ran — so it continues where it left off.

```python
import fast_rlm

session = fast_rlm.Session(session_dir="sessions", session_id="podcasts",
                           config={"primary_agent": "z-ai/glm-5"})

r1 = session.query("Here are all transcripts... Build a guest index and "
                   "summarize what the ML guests said about AGI.\n" + transcripts)
r2 = session.query("Using the index you already built: which guests were most "
                   "optimistic about AGI timelines?")   # no re-exploration

session.variables()   # {name: {type, preview, comment, note, committed}}
session.queries()     # [{"query": ..., "final": ...}, ...]
```

Where state lives (both args optional):

- `Session()` — **ephemeral**: state is kept in a private temp dir and deleted when the object is closed/collected. Every `Session()` starts from a clean slate — the safe default for experiments.
- `Session(session_dir="sessions")` — persistent at `sessions/state.json`; re-open the same dir to resume.
- `Session(session_dir="sessions", session_id="podcasts")` — persistent at `sessions/podcasts/state.json`; `session_id` namespaces several sessions under one dir. (`session_id` without `session_dir` raises.)

Also available as `run(..., session_dir=..., session_id=...)` and `fast-rlm --session-dir ... --session-id ...`.

Behavior and limits:

- **Crash-safe.** State is written after every step (atomically), not at the end — a killed run resumes from its last completed step.
- **Not a 1:1 process clone.** Open handles, generators, and JS proxies don't survive; they're reported to the agent as dropped on resume. Variables over 5 MB pickled are skipped.
- **The conversation is not carried.** A resumed query starts a fresh conversation seeded with the query/answer ledger, the code dump (comments included), and a live inventory of restored variables — so per-query context stays bounded no matter how old the session is.
- **Showing earlier code makes follow-ups faster.** By default the resumed agent sees the code it ran before (`add_session_code_to_context=True`, CLI `--no-session-code` to disable). In our experiments this made multi-query sessions markedly cheaper — the agent reuses *how* it built things instead of re-exploring the restored state each time — while staying just as accurate. Turn it off only when minimizing the resume prompt matters more than speed.
- **`context` is not saved** (it holds each query anew). The agent has a `commit(name, note="...")` function to annotate a variable or force-save skipped names like `context`.
- **Sub-agents are unaffected** — they stay fresh and isolated; only the root agent's state persists.
- **Inspect a whole session** with `fast-rlm-log <session-dir>` — the query→FINAL timeline across every run, with per-query tokens/cost. Add `--tui` for an interactive session timeline: select a query and drill into its full step-by-step run transcript (`[`/`]` to move between queries, `Esc` back to the overview).

## Custom instructions

Pass a directive through the `instruction` kwarg on `fast_rlm.run(...)`. When provided, it is appended to the **end** of the agent's system prompt:

```
Here is the user's instructions - you must follow it closely:
{instruction}
```

```python
result = fast_rlm.run(
    "Summarize the attached incident report.",
    instruction="Write all output in formal British English and never use bullet points.",
)
```

**Instructions apply to one agent only — they are never inherited.** `run(instruction=...)` configures the **root agent** and nothing else. Sub-agents start with no instruction; to give a sub-agent one, the parent must pass it explicitly when it delegates:

```python
# inside an agent's REPL — instruct the child you spawn
result = await llm_query(
    chunk,
    instruction="Extract only dollar amounts; return them as a JSON list.",
)
```

This is a recursive, no-carry-on design: each agent sees only the instruction its spawner handed it. A child does not inherit its parent's instruction, and the child's own `llm_query(...)` calls start fresh unless it passes `instruction=` again. There is intentionally no global, run-wide instruction.

Behavior:

- `instruction` must be a `str`. When omitted (`None`), nothing is appended and the prompt is unchanged.
- Because it is appended *after* the built-in prompt, a forceful instruction can override default behavior (e.g. output format or even the task itself). Keep it focused on *how* to answer rather than restating the task.
- `run(instruction=...)` is a per-call argument, not part of `RLMConfig`; pass it directly to `run(...)`. The in-REPL form is `llm_query(..., instruction=...)`.


## MCP servers

fast-rlm can connect to [Model Context Protocol](https://modelcontextprotocol.io) servers and expose their tools and resources inside the REPL. The agent calls them with `await mcp_call(server, tool, **kwargs)` and reads resources with `await mcp_read_resource(uri)` — just like any other REPL function.

**Nothing extra to install for fast-rlm.** MCP support is optional and lazy: the MCP client lives in the Deno engine, and Deno auto-downloads it on first use. There is *no* `pip install fast-rlm[mcp]` — runs that don't use MCP never load it. You only install the **MCP servers** you actually want to connect to (each per its own docs).

Pass servers to `run(..., mcp_servers={...})`, keyed by name. Transport is chosen by the config shape:

```python
import fast_rlm

result = fast_rlm.run(
    "Read /data/report.md and summarize it in three bullets.",
    mcp_servers={
        # stdio: fast-rlm SPAWNS the server (and kills it on exit) — you don't run it.
        "fs":   {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"]},
        # http: the server must already be running; you point at its URL.
        "web":  {"url": "http://localhost:3333/mcp", "headers": {"Authorization": "Bearer ..."}},
    },
)
```

Install a server the usual way before pointing fast-rlm at it, e.g.:

```bash
# stdio servers are launched on demand via their command (npx/uvx/node/...)
npx -y @modelcontextprotocol/server-filesystem /data    # Node-based
uvx mcp-server-fetch                                     # Python-based
```

| Config key | Transport | Who runs the server? | Notes |
|---|---|---|---|
| `command` (+ `args`, `cwd`, `env`) | stdio | fast-rlm spawns it | grants Deno `--allow-run`; **a shell/filesystem server is full host access, not sandboxed** |
| `url` (+ `headers`) | HTTP | you (must be listening) | |

Inside the REPL the agent gets a small, lazy discovery API (the step-0 probe only shows counts, never full schemas):

- `mcp_list_tools(server=None)` / `mcp_tool_schema("server.tool")` / `await mcp_call(server, tool, **kwargs)`
- `mcp_list_resources()` / `mcp_list_resource_templates()` / `await mcp_read_resource(uri, server=None)`


## Configuration

```python
from fast_rlm import run, RLMConfig

config = RLMConfig.default()
config.primary_agent = "minimax/minimax-m2.5"
config.sub_agent = "minimax/minimax-m2.5"
config.max_depth = 5
config.max_money_spent = 2.0

result = run(
    "Count the r's in 50 fruit names",
    prefix="r_count",
    config=config,
)
```

All config fields:

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `primary_agent` | `str` | **(required)** | Model for the root agent. No default — must be set or `run()` raises. |
| `sub_agent` | `str` | `primary_agent` | Model for child subagents. Defaults to `primary_agent` when unset. |
| `max_depth` | `int` | `3` | Max recursive subagent depth |
| `max_calls_per_subagent` | `int` | `20` | Max LLM calls per subagent |
| `truncate_len` | `int` | `10000` | Output chars shown to the LLM per step. **Quality-critical** for document/navigation tasks — too low and the model sees only a truncated slice of gathered results and guesses the rest. Raise further for large-context extraction. |
| `max_money_spent` | `float` | `0.2` | Hard budget cap in USD. On OpenRouter, real spend is read from `usage.cost_details` (the top-level `cost` is `0` for BYOK keys), so the cap fires correctly. Where no cost is reported, use `max_global_calls` to bound the run. |
| `max_completion_tokens` | `int` | `50000` | Max total completion tokens across all subagents (cumulative) |
| `max_prompt_tokens` | `int` | `200000` | **Per-call** ceiling on a single LLM call's input + output tokens (not a run-wide sum). Bounds how large any one agent's context may grow; the run stops when a single call exceeds it. |
| `max_global_calls` | `int` | `∞` (50 for ACP) | Max total LLM calls across the whole run (root + all subagents) |

## Progress & verbosity

`run()` has two knobs for observing a run — one for the terminal, one for your code.

**`verbosity`** controls how much the engine prints:

| Value | Prints |
|---|---|
| `0` / `"silent"` | nothing |
| `1` / `"summary"` | final result + global usage only |
| `2` / `"full"` (default) | per-step boxes, spinners, and progress chatter |

```python
fast_rlm.run("...", config=config, verbosity="summary")
```

The legacy `verbose: bool` still works (`True` → `"full"`, `False` → `"silent"`) but is superseded by `verbosity`; when both are given, `verbosity` wins.

**`on_step`** gives live, programmatic progress — a callback fired once per step (root and every sub-agent) as the run proceeds, so you don't have to tail the log file:

```python
def on_step(event: dict):
    if event["event_type"] == "execution_result":
        print(f"depth {event['depth']} step {event['step']}: "
              f"{event['totalUsage']['completion_tokens']} completion tokens so far")

result = fast_rlm.run("...", config=config, on_step=on_step)
```

Each `event` is a dict with `event_type` (`"code_generated"`, `"execution_result"`, or `"final_result"`), `run_id`, `parent_run_id`, and `depth`. Step events also carry `step`, `code`, `output`, `hasError`, `reasoning`, `usage`, `totalUsage`, and `timestamps` — the same data written to the JSONL log, delivered as it happens. `on_step` runs in a background thread and fires at any verbosity (including `"silent"`); an exception it raises is caught and warned about rather than aborting the run.

## Best Practices & Troubleshooting

- **Place your task at the top or bottom of the prompt** — the REPL restricts how much context the LLM sees, so don't bury the task in the middle.
- **Mark structured data with backtick blocks** — wrap JSON, CSV, etc. in fenced code blocks and name the format in the prompt.
- **Use strong coding models** — agents write and execute Python, so coding benchmarks matter. See [recommended models](https://avbiswas.github.io/fast-rlm/guide/configuration/#model-names).
- **Inject domain docs when needed** — for obscure domains, add reference material and tell the agent how it's organized (e.g. with `##` headers).
- **Check logs and start with strict limits** — review what the agent is doing before scaling up. Prompt changes usually help more than bigger budgets.

For the full guide, see the [Best Practices & Troubleshooting](https://avbiswas.github.io/fast-rlm/guide/tips/) docs page.



### Vertex AI (Google Gemini) — optional

> Skip this section unless you specifically want to run Gemini models on Google Cloud. It is **not** required for the default OpenRouter (or any OpenAI-compatible) setup above.

Use Gemini models via Vertex AI with IAM-based auth (no API key needed):

```python
import fast_rlm

config = fast_rlm.RLMConfig()
config.primary_agent = "vertex/google/gemini-2.5-flash"
config.sub_agent = "vertex/google/gemini-2.5-flash"

result = fast_rlm.run("Count the r's in 50 fruits", config=config, vertex=True)
```

This path uses these extra environment variables instead of `RLM_MODEL_API_KEY`:

| Variable | Description | Default |
|----------|-------------|---------|
| `GOOGLE_CLOUD_PROJECT` | GCP project ID | — |
| `GOOGLE_CLOUD_LOCATION` | GCP region | `us-central1` |

Auth uses Application Default Credentials. Either run `gcloud auth application-default login` or set `GOOGLE_APPLICATION_CREDENTIALS` to a service account key path.

---

## Log Viewer

![TUI Log Viewer](docs/images/tui.jpeg)

Every run saves a `.jsonl` log file to `logs/` (override the directory with
`run(..., log_dir=...)` or `fast-rlm --log-dir ...`). The exact path is returned
as `log_file` in the result dict, so you can locate or tail the live transcript.

```bash
# Print stats (no extra dependencies)
fast-rlm-log logs/run_xxx.jsonl

# Interactive TUI viewer (requires bun)
fast-rlm-log logs/run_xxx.jsonl --tui
```

---

## Development (from source)

### 1. Install Deno

Windows (npm):

```powershell
npm install -g deno
```

macOS / Linux:

```bash
curl -fsSL https://deno.land/install.sh | sh
```

Then add Deno to your `PATH`:

```bash
export DENO_INSTALL="$HOME/.deno"
export PATH="$DENO_INSTALL/bin:$PATH"
```

### 2. Install Bun (for the log viewer)

```bash
curl -fsSL https://bun.sh/install | bash
cd tui_log_viewer && bun install
```

### 3. API Key Setup

Set your key in `.env` or `.envrc`:

```bash
export RLM_MODEL_API_KEY=sk-or-...
```

### 4. Configuration

Edit `rlm_config.yaml` at the project root:

```yaml
max_calls_per_subagent: 20
max_depth: 3
truncate_len: 10000
primary_agent: "z-ai/glm-5"   # REQUIRED — no default
# sub_agent is optional; omit it to reuse primary_agent for subagents
sub_agent: "minimax/minimax-m2.5"
max_money_spent: 0.2
max_completion_tokens: 50000
max_prompt_tokens: 200000
```

### 5. Running

```bash
# Run the example
deno task test_counting_r

# Run the subagent directly
echo "What is 2+2?" | deno task subagent

# View logs
./viewlog logs/<logfile>.jsonl
```

### 6. Benchmarks

```bash
uv sync --extra benchmarks
uv run benchmarks/oolong_synth_benchmark.py
uv run benchmarks/longbench_benchmark.py
```

---

## Backend setup

fast-rlm picks a backend from the `primary_agent`/`sub_agent` string (see [Model backends](#model-backends)). **Set the credential only for the backend(s) you use** — each is validated at point of use, so an ACP-only run needs no API key at all.

### 1. OpenAI-compatible API (default)

Any OpenAI-compatible endpoint — OpenAI, DeepSeek, OpenRouter (default), or anything else.

```bash
export RLM_MODEL_API_KEY=sk-...                       # or OPENAI_API_KEY, or OPENROUTER_API_KEY
export RLM_MODEL_BASE_URL=https://api.deepseek.com    # optional; defaults to OpenRouter
```
```yaml
primary_agent: "deepseek-chat"     # or "gpt-5-mini", "minimax/minimax-m3", ...
```

### 2. Vertex AI

Google Cloud, via Application Default Credentials (no static key). Prefix the model with `vertex/`, or set `RLM_VERTEX_AI=1` (Python: `run(..., vertex=True)`) to route every model through Vertex.

```bash
gcloud auth application-default login
export GOOGLE_CLOUD_PROJECT=your-project
```
```yaml
primary_agent: "vertex/claude-sonnet-4-6"
```

### 3. Anthropic API (native)

Claude models (`claude-*` or `anthropic/claude-*`) use the native Anthropic API when `ANTHROPIC_API_KEY` is set. If the native call is unavailable, fast-rlm transparently falls back to the OpenAI-compatible endpoint — so `anthropic/...` strings keep working through OpenRouter even without an Anthropic key.

```bash
export ANTHROPIC_API_KEY=sk-ant-...
export ANTHROPIC_BASE_URL=https://my-proxy.example.com   # optional; defaults to https://api.anthropic.com
```
```yaml
primary_agent: "claude-haiku-4-5"        # or "anthropic/claude-sonnet-4-6"
```
Token usage is reported (so budgets apply); cost shows `Unknown` (the SDK returns no cost).

**Prompt caching is automatic.** Unlike OpenAI/Gemini (which cache prefixes on their own), Claude only caches when asked, so fast-rlm attaches `cache_control` breakpoints to the system prompt and the latest message on every native-Anthropic call. This caches the large static system prompt *and* the growing conversation prefix incrementally — matching the automatic caching other providers give you. Cache hits show up as `cached_tokens` in usage. (This applies to the native path only; Claude routed through OpenRouter without an `ANTHROPIC_API_KEY` does not yet get caching.)

### 4. CLI coding agent (recommended for Claude Code / Codex / opencode)

Drives a local coding agent through its **own** non-interactive mode (`claude -p`,
`codex exec`, `opencode run`) — no API key needed (the agent uses its own CLI
login), no install step, and no Node/npx. Unlike ACP there is no bridge package
between fast-rlm and the agent, so it cannot lag behind a CLI release; and
because these CLIs report token usage, the token/cost budgets work normally.

```yaml
primary_agent: "cli:claude-code?model=claude-sonnet-5"   # model is required
```

See the **CLI agents** guide for presets, registering your own agent, and the
`cli_minimal` cost option.

### 5. ACP coding agent

Drives a local coding agent (Claude Code, Codex, opencode) read-only — no API key needed (the agent uses its own CLI login). Because token/cost budgets don't apply to ACP, `max_global_calls` defaults to `50` for ACP runs. See the **ACP agents** section below for presets and the backdoor.

**ACP is opt-in — install it once before first use:**

```bash
fast-rlm acp install
```

```yaml
primary_agent: "acp:opencode"      # or "acp:claude-code", "acp:codex"
```

### Credential resolution

| Backend | Selector | Credential |
|---|---|---|
| OpenAI-compatible | unprefixed (e.g. `gpt-5-mini`) | `RLM_MODEL_API_KEY` → `OPENAI_API_KEY` → `OPENROUTER_API_KEY` (+ optional `RLM_MODEL_BASE_URL`) |
| Vertex AI | `vertex/…` or `RLM_VERTEX_AI=1` | ADC + `GOOGLE_CLOUD_PROJECT` |
| Anthropic | `claude-…` / `anthropic/…` | `ANTHROPIC_API_KEY` (or `RLM_ANTHROPIC_API_KEY`) (+ optional `ANTHROPIC_BASE_URL`) |
| CLI agent | `cli:…` | none (agent's own CLI login) |
| ACP | `acp:…` | none (agent's own CLI login); needs `fast-rlm acp install` |

---

## CLI agents (Claude Code, Codex, opencode)

Drives a coding agent through its **own** non-interactive mode as the "brain".
The agent gets fast-rlm's system prompt + history and replies with a
```` ```repl ```` block, which fast-rlm executes in its own Pyodide sandbox —
exactly like any other model. **The agent never runs the code or writes files.**

Nothing to install: you need only the agent's CLI, logged in as usual.

```yaml
primary_agent: "cli:claude-code?model=claude-sonnet-5"
sub_agent:     "cli:codex?model=gpt-5.5-codex"
```

**A model is required.** Omit it and the run fails immediately: left unset the
model is whatever the vendor's CLI defaults to — it drifts between releases, it
is invisible in your config, and for Claude Code it is Opus (~$0.17/step).

| `cli:` name       | Launches                                | Isolation |
|---|---|---|
| `cli:claude-code` | `claude -p --output-format json`        | `--disallowedTools` + temp cwd |
| `cli:codex`       | `codex exec --json --sandbox read-only` | read-only sandbox + temp cwd |
| `cli:opencode`    | `opencode run --format json --pure`     | temp cwd |

**Why prefer this over `acp:`** for these three agents: no bridge package
between fast-rlm and the agent (so nothing to lag behind a CLI release), no
Node/npx, no install step, subprocess permission scoped to the named binaries
instead of a blanket grant — and **token/cost budgets work**, because these CLIs
report usage where ACP reports none.

> **Billing safety.** `claude` silently prefers `ANTHROPIC_API_KEY` over your
> Pro/Max login, so a run you think uses your plan gets metered instead — with
> no warning. `cli:claude-code` therefore **refuses to start** while that key is
> set; unset it, or opt in explicitly with `cli_allow_api_key`. The preset also
> pins **Sonnet** (Claude Code's own default is Opus, ~$0.17/step); override
> Aliases and exact ids both pass through (`?model=sonnet`,
> `?model=claude-sonnet-5`); pin an exact id for anything you need to reproduce
> later.

**`cli_minimal`** enables each preset's extra cost-saving flags (Claude's
`--bare`): measured at 8,465 prompt tokens/step versus 25,680, and $0.055 versus
$0.161. It is off by default because `--bare` never reads the interactive login
and so requires `ANTHROPIC_API_KEY`.

**Register any other agent** with a JSON non-interactive mode under `cli_agents`
— declarative `command`/`args`/`extract`/`usage`, no code changes. A registered
name overrides a built-in preset, which is how you repair one locally after an
upstream flag change without waiting for a fast-rlm release.

**Keeping work in the REPL.** A coding agent with its own shell can compute
internally and return a hardcoded answer — indistinguishable from REPL work
unless you look. Each preset was tested with a planted canary file:
`cli:claude-code` (`--disallowedTools`) and `cli:opencode` (injected
`opencode.json`, including `task` — its subagents do not inherit denials) can
neither read the file nor run a shell command. **Codex cannot be locked down at
all** — `--sandbox read-only` still permits reads and command execution — so its
bypasses are *detected* from its event stream and fail the run by default
(`cli_native_tools`).

Verify any of it yourself:

```bash
python scripts/verify_agents.py --all      # un-guessable data + transcript audit
```

See the [CLI agents guide](docs/guide/cli-agents.md) for the full spec.

---

## ACP agents (Claude Code, Codex, opencode, …)

fast-rlm can also use any agent that speaks the
[Agent Client Protocol (ACP)](https://agentclientprotocol.com/) as the "brain".
**For Claude Code, Codex and opencode, prefer the `cli:` backend above** — ACP is
the route for other ACP-speaking agents, and remains fully supported. The agent is prompted with fast-rlm's system prompt + history and replies
with a ```` ```repl ```` block, which fast-rlm executes in its own Pyodide sandbox —
exactly like any other model. **The agent itself runs read-only and never writes
files or runs the code; fast-rlm does.**

Select one with an `acp:` prefix on `primary_agent`/`sub_agent` (mirrors the
`vertex/` convention):

```yaml
primary_agent: "acp:claude-code"
sub_agent:     "acp:codex?model=gpt-5.5-codex"   # ?model= is optional
```

```python
run(query, config=RLMConfig(primary_agent="acp:opencode"))
```

### Installing ACP support

ACP support ships **disabled**. It needs two npm dependency trees — the ACP
provider and the Vercel AI SDK — plus Node/npx to spawn each agent's bridge
package, and none of that is useful to the majority of runs that use a plain
API model. So it is neither bundled nor resolved unless you ask for it: the
engine imports it dynamically, and a plain run never sees it in its module
graph.

```bash
fast-rlm acp install       # one time, before your first acp: run
fast-rlm acp status        # what's installed, and what's newer
fast-rlm acp install -u    # move to the latest versions
```

Using an `acp:` agent without installing fails immediately, with the fix:

```
ACP support is not installed.

  'acp:claude-code' needs the ACP bridge packages ...

      fast-rlm acp install
```

**Versions are yours, not the repo's.** `acp install` resolves the current
packages and records them in `~/.fast_rlm/acp.json`; runs then use exactly those
versions, so a run is reproducible instead of silently picking up whatever `npx`
last fetched. When the ACP ecosystem moves — and it moves faster than fast-rlm
releases — `fast-rlm acp install -u` is the single place you update. **You never
have to wait for a new fast-rlm release to use a newer bridge.**

The AI SDK version is derived from what the ACP provider declares, never
resolved independently (the provider currently pins `ai@^6` while `ai@7` is
current, so "latest for both" would install an incompatible pair).

If a bridge package is renamed upstream, edit `bridge_packages` in
`~/.fast_rlm/acp.json` — the package name is marker data too, so a rename also
needs no release.

**Built-in presets** (verified): `acp:claude-code`, `acp:codex`, `acp:opencode`.
Claude Code and Codex are launched via their `npx` adapters, so **Node/npx must be
on PATH** and the agent itself must already be logged in (e.g. `claude /login`,
`codex login`, `opencode auth login`). `acp:opencode` spawns the `opencode`
binary directly and needs no bridge package.

**Backdoor — any other ACP agent.** Register it by command under `acp_agents`, then
select it by name. Built-in presets need no entry; a registered name overrides a
preset of the same name.

```python
run(query, config=RLMConfig(
    primary_agent="acp:hermes",
    acp_agents={
        "hermes": {"command": "hermes", "args": ["acp"]},
        "cursor": {"command": "npx", "args": ["-y", "cursor-agent-acp"]},
    },
))
```

Each entry accepts `command`, `args?`, `readonly_mode?` (the agent's read-only mode
id, if it has one), `model?`, `auth_method?` (ACP auth method id — pinning it
silences the provider's "authMethodId is not configured" warning), `env?`, and
`config_files?` (relative path → JSON content written into the temp cwd before
launch — use this to inject permission configs for custom agents).

**Tool stripping (built-in presets only):**

fast-rlm's goal is that all computation happens in the Pyodide REPL — not inside
the agent's own tool harness. Without restrictions, agents like Claude Code will use
their native bash/file-read tools to pre-compute answers internally and return
hardcoded results, bypassing the observable REPL loop. The three built-in presets
block this by injecting agent-specific permission configs into the throwaway cwd
before each launch:

| Agent | Mechanism |
| ----- | --------- |
| `acp:claude-code` | `.claude/settings.json` written to temp cwd — denies `Bash(*)`, `Read(*)`, `Write(*)`, `Edit(*)`, `WebFetch(*)`, `WebSearch(*)` |
| `acp:opencode`    | `opencode.json` written to temp cwd — sets `bash`, `read`, `edit`, `glob`, `grep` to `"deny"` |
| `acp:codex`       | `-c sandbox_permissions=[]` flag in launch args — empty permissions array |

> **Backdoor agents do not get this automatically.** Agents registered via
> `acp_agents` are launched as-is. To restrict a custom agent, add a `config_files`
> entry to its spec (see the [ACP agents guide](docs/guide/acp-agents.md)) or set
> its `readonly_mode` field.

**Safety & caveats:**

- Every ACP agent runs in a throwaway temp `cwd`, so a stray write is contained.
- When the agent has a read-only session mode (`readonly_mode`), fast-rlm switches
  into it. The presets do this automatically: opencode/claude-code use `plan` (a
  hard block); codex uses `read-only` (approval-gated — it may still write if it
  asks, so the temp `cwd` is its real guardrail).
- Agents with **no session modes** (e.g. cursor, hermes) are contained by the temp
  `cwd` alone.
- **Budgets:** ACP agents report no token usage, so `max_money_spent`,
  `max_completion_tokens`, and `max_prompt_tokens` are **inert** for them (always
  zero, never trip). The only budget that works is **`max_global_calls`**, which
  **defaults to `50`** for ACP runs (override it on the config/CLI as needed).

---

## Contributing

- **Small PRs only** — keep changes focused and minimal. Large PRs will not be accepted.
- **No LLM-generated slop** — AI-assisted code is fine, but bulk-generated boilerplate with no thought behind it will be rejected.
- **Minor features welcome** — small, well-scoped PRs that add useful functionality will be considered.
- **Large feature requests** — open an issue first to discuss the design before writing any code.

## License

MIT License. See [LICENSE](LICENSE).
