Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# AGENTS.md

## Documentation

- Architecture, concepts, user flows, and configuration reference live in [`docs/`](docs/README.md), with Mermaid diagrams grounded in `src/rlm/`.
- Keep the diagrams in sync when changing the engine loop, tool registry, supervisor, or config schema.

## Writing code

- **Minimal try/except**: let errors propagate — silent failures hide bugs. Only catch for intentional fault tolerance (retries, robustness).
Expand Down
142 changes: 142 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Architecture

System shape of nano-rlm, grounded in `src/rlm/`. Diagrams use Mermaid and render natively on GitHub.

## System overview

`RLMEngine` (`src/rlm/engine.py`) is the central orchestrator. It bridges natural-language reasoning with a concrete execution space: a persistent IPython kernel, a small set of built-in tools, and a session record on disk.

```mermaid
flowchart TB
subgraph Entry["Entry points"]
CLI["rlm CLI<br/>cli.py"]
ACP["ACP server<br/>acp.py"]
API["Python API<br/>rlm.run — api.py"]
end

subgraph Core["Agent core"]
ENGINE["RLMEngine<br/>engine.py"]
CLIENT["LLM client<br/>client.py"]
PROMPT["System prompt builder<br/>prompt.py"]
end

subgraph Exec["Execution environment"]
REG["Tool registry<br/>tools/registry.py"]
BASH["bash tool"]
EDIT["edit tool"]
IPTOOL["ipython tool"]
REPL["IPythonREPL<br/>tools/ipython.py"]
KERNEL[("Persistent<br/>IPython kernel")]
GUARD["GitHistoryGuard<br/>tools/git_block.py"]
end

subgraph Rec["Recursion"]
SUP["SessionTreeSupervisor<br/>supervisor.py"]
BRK["Broker<br/>broker.py<br/>(unix socket, framed JSON RPC)"]
end

subgraph State["Persistence"]
SES["Session<br/>session.py"]
FS[("$RLM_HOME/sessions/&lt;id&gt;<br/>meta.json · messages.jsonl")]
end

CLI --> ENGINE
ACP --> ENGINE
API --> ENGINE
ENGINE <--> CLIENT
PROMPT --> ENGINE
ENGINE --> REG
REG --> BASH & EDIT & IPTOOL
BASH & IPTOOL --> GUARD
IPTOOL --> REPL
REPL <--> KERNEL
KERNEL <--> BRK
BRK <--> SUP
SUP -->|spawns| ENGINE
ENGINE --> SES
SES --> FS
```

## Key components

| Component | Responsibility | Source |
| ----------- | --------------- | -------- |
| `RLMEngine` | Agent loop: conversation history, tool dispatch, auto-compaction, recursion depth | `src/rlm/engine.py` |
| `IPythonREPL` | Lifecycle of the persistent Jupyter kernel (ZMQ/IPC transport) | `src/rlm/tools/ipython.py` |
| Tool registry | Active built-in tools: `bash`, `edit`, `ipython` | `src/rlm/tools/registry.py` |
| `Session` | On-disk persistence: `meta.json`, `messages.jsonl`, metrics aggregation | `src/rlm/session.py` |
| Skills system | Discovers and injects callable skills (built-in, installed, MCP) into the kernel | `src/rlm/skills/`, `src/rlm/mcp.py` |
| `GitHistoryGuard` | Blocks broad git-history access (`git log --all`, `--reflog`, …) in shell and Python | `src/rlm/tools/git_block.py` |
| `SessionTreeSupervisor` | Owns the recursion tree, depth semaphores, brokered skill calls | `src/rlm/supervisor.py` |
| Broker | Framed JSON RPC over a unix socket between kernels and the supervisor | `src/rlm/broker.py` |

## The turn loop

`_run_loop()` (`engine.py:368`) is an act–observe cycle. Exactly one built-in tool call is allowed per turn.

```mermaid
sequenceDiagram
participant E as RLMEngine
participant L as LLM (client.py)
participant T as Tool (bash/edit/ipython)
participant S as Session

loop for each turn
E->>L: chat.completions.create(messages, tools)<br/>Idempotency-Key: call_id
L-->>E: assistant message (+ usage)
E->>S: log_assistant
alt no tool calls
E->>E: stop_reason = "done"
else more than one tool call
E->>E: error feedback to all calls
else one tool call
E->>T: execute(args, ToolContext)<br/>(asyncio.shield + to_thread)
T-->>E: ToolOutcome
E->>S: log_tool_result
end
opt prompt_tokens ≥ RLM_SUMMARIZE_AT_TOKENS
E->>L: compaction call (tool_choice="none")
L-->>E: handoff summary
E->>E: messages := [system, summary]<br/>kernel NOT restarted
E->>S: log compaction event
end
end
```

Tool cancellation is cooperative: on interruption the engine interrupts the kernel (`repl.interrupt()`), and recovers by restarting the kernel only if it does not return to idle within 2 seconds.

## Recursion tree

Sub-agents never spawn engines themselves — recursion always goes through the supervisor.

```mermaid
flowchart LR
subgraph K0["Root kernel (depth 0)"]
R0["await rlm('sub-task')"]
end
R0 -->|rlm.run RPC| BROKER["Broker<br/>(unix socket)"]
BROKER --> SUP["SessionTreeSupervisor"]
SUP -->|"capability + scope check<br/>depth & call limits"| CHK{allowed?}
CHK -->|no| LIM["RLMResult<br/>[depth limit reached]"]
CHK -->|yes| SEM["depth semaphore<br/>depth_capacities()"]
SEM --> E1["Child RLMEngine<br/>depth + 1"]
E1 --> K1[("Child kernel<br/>sub-&lt;uuid8&gt;/")]
K1 -->|"RLMResult(answer, usage, …)"| R0
```

Guard rails: `RLM_MAX_DEPTH` (default 0 = no sub-agents), `RLM_MAX_SUBAGENT_CALLS` (default 64 per tree), `RLM_MAX_CONCURRENT_SUBAGENTS` (per-depth semaphores so nested calls cannot deadlock).

## Session layout on disk

```mermaid
flowchart TB
HOME["$RLM_HOME (default ~/.rlm)"] --> S["sessions/&lt;uuid12&gt;/"]
S --> M["meta.json<br/>id · model · depth · status<br/>usage · metrics · answer_preview"]
S --> J["messages.jsonl<br/>assistant · tool_result · sub_spawn<br/>compaction · prompt_rollback · done"]
S --> P["programmatic_tool_calls.jsonl<br/>skill calls from kernel/CLI wrappers"]
S --> SK["skill modules<br/>(built-in re-exports, MCP proxies)"]
S --> C1["sub-&lt;uuid8&gt;/<br/>(child session, same layout)"]
C1 --> C2["sub-&lt;uuid8&gt;/ …"]
```

`meta.json` is written atomically (`.tmp` + rename). `aggregate_child_metrics()` recursively merges child stats into the parent session.
121 changes: 121 additions & 0 deletions docs/CONCEPTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Concepts

The design pillars of nano-rlm, per the [official documentation](https://deepwiki.com/PrimeIntellect-ai/nano-rlm) and the source.

## 1. Minimal tool interface

Instead of a large suite of specialized tools (`read_file`, `write_file`, `execute_shell`, …), the model gets a small set of high-capability built-ins — with the `ipython` tool as the centerpiece. The active set is chosen by tooling presets (`RLM_TOOLING` / `RLM_BUILTIN_TOOLS`):

```mermaid
flowchart LR
MODEL["LLM"] -->|"one tool call per turn"| REG{"Tool registry"}
REG --> BASH["bash<br/>fresh bash -c subshell"]
REG --> EDIT["edit<br/>single-occurrence replace"]
REG --> IPY["ipython<br/>persistent kernel"]
IPY --> PY["Python"]
IPY --> SH["!cmd / %%bash"]
IPY --> SK["skills + await rlm()"]
```

The `dual` preset (default) exposes all three tools plus the `bash`/`edit` skills. Because `ipython` runs a real kernel, the agent has the full power of Python — file manipulation, data processing, HTTP, shell escapes — without bespoke tools.

## 2. Persistent state

The IPython kernel stays alive for the entire session lifecycle. Variables, imports, and in-memory data survive across turns — and across context compactions.

```mermaid
flowchart TB
subgraph S["Session lifecycle"]
T1["turn 1<br/>df = pandas.read_csv(...)"] --> T2["turn 2<br/>df.describe()"]
T2 --> C{"context too large?"}
C -->|yes| COMP["compaction:<br/>messages := [system, summary]"]
COMP --> T3["turn 3<br/>df still in memory ✓"]
C -->|no| T3
end
K[("single persistent kernel<br/>never restarted by compaction")] -.-> T1 & T2 & T3
```

The kernel is also credential-isolated: `build_kernel_env()` passes a whitelist of environment variables (PATH, HOME, locale, cert vars, explicit `RLM_KERNEL_ENV` entries) — credentials are de-ambiented by omission, not filtered by name.

## 3. Context compaction

When a turn's `prompt_tokens` reaches `RLM_SUMMARIZE_AT_TOKENS`, the engine triggers a compaction turn:

```mermaid
sequenceDiagram
participant E as RLMEngine
participant L as LLM
participant K as IPython kernel

E->>E: prompt_tokens ≥ summarize_at_tokens<br/>(and max_compactions not exhausted)
E->>E: append CHECKPOINT_COMPACTION_PROMPT
E->>L: summary call (tool_choice="none")
L-->>E: handoff summary
E->>E: messages := [system, framing + summary]<br/>log compaction event + CompactionApplied metric
Note over K: kernel untouched —<br/>all variables intact
E->>L: loop resumes from summary
```

If the agent stalls on the summary, a `REPL_RESTART_NOTE` reminds it that the kernel state is still available.

## 4. Native recursion

Agents spawn sub-agents by calling `await rlm("...")` inside their own kernel — hierarchical task decomposition without external orchestration.

```mermaid
sequenceDiagram
participant K as Parent kernel
participant B as Broker (unix socket)
participant S as SessionTreeSupervisor
participant E2 as Child RLMEngine

K->>B: rlm.run(prompt)<br/>rlm.broker over framed JSON RPC
B->>S: BrokerRunRequest
S->>S: validate capability + scope<br/>check depth / call limits
S->>E2: spawn (depth + 1, child session dir)
E2-->>S: RLMResult(answer, usage, metrics)
S-->>B: response
B-->>K: RLMResult
```

- Depth and concurrency are bounded (`RLM_MAX_DEPTH`, `RLM_MAX_SUBAGENT_CALLS`, per-depth semaphores).
- Each child gets its own session directory (`sub-<uuid8>/`) and kernel.
- `asyncio.gather` over multiple `rlm()` calls gives parallel sub-agents.

## 5. Skills system

Three skill sources are unified into pre-imported, callable modules in the kernel namespace:

```mermaid
flowchart TB
subgraph Sources
BI["Built-in<br/>rlm/skills/<br/>bash · edit · search"]
INST["Installed<br/>rlm-skill-* packages<br/>(uv tool install --with-editable)"]
MCP["MCP servers<br/>RLM_MCP_CONFIG"]
end
BI -->|"re-export module"| DIR["session dir<br/>*.py modules"]
MCP -->|"proxy module<br/>__rlm_brokered__ = True"| DIR
INST --> KER
DIR --> KER["Kernel startup injection<br/>_CallableModule wrapper"]
KER -->|"await skill(...) == await skill.run(...)"| AGENT["Agent code"]
AGENT -->|"brokered: skill.call RPC"| SUP["Supervisor<br/>(credentials stay out of kernel)"]
```

Name collisions across sources raise an error at discovery. Every programmatic skill call is logged to `programmatic_tool_calls.jsonl` for telemetry.

## 6. Git history guard

Active unless `RLM_ALLOW_GIT=1`. It blocks broad-history `git log` (e.g. `--all`, `--reflog`, `--branches`) — not git in general — so evaluation agents can't mine task solutions from history.

```mermaid
flowchart LR
CODE["agent-generated code"] --> CHK{"find_blocked_*"}
CHK -->|"bash tool / !cmd / %%bash"| SH["shell parser:<br/>split && || ; |<br/>shlex per segment"]
CHK -->|"python code"| AST["AST visitor:<br/>tracks subprocess/os aliases<br/>literal argv only"]
SH --> DEC{blocked?}
AST --> DEC
DEC -->|yes| REFUSE["REFUSAL_TEMPLATE error"]
DEC -->|no| EXEC["execute normally"]
```

Documented bypasses (`getattr`, multi-hop reassignment, `__import__`) are accepted as out of scope — the guard is a tripwire, not a sandbox.
Loading