Skip to content

Commit 03a1774

Browse files
authored
Merge pull request #153 from alexzhang13/ipython_docs
docs: add IPythonREPL environment page
2 parents a5e8e28 + af99a65 commit 03a1774

4 files changed

Lines changed: 156 additions & 5 deletions

File tree

‎README.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,18 +77,21 @@ make quickstart
7777
</details>
7878

7979
## REPL Environments
80-
We support two types of REPL environments -- isolated, and non-isolated. Non-isolated environments (default) run code execution on the same machine as the RLM (e.g. through `exec`), which is pretty reasonable for some local low-risk tasks, like simple benchmarking, but can be problematic if the prompts or tool calls can interact with malicious users. Fully isolated environments use cloud-based sandboxes (e.g. Prime Sandboxes, [Modal Sandboxes](https://modal.com/docs/guide/sandboxes)) to run code generated by the RLM, ensuring complete isolation from the host process. Environments can be added, but we natively support the following: `local` (default), `docker`, `modal`, `prime`, `daytona`, `e2b`.
80+
We support two types of REPL environments -- isolated, and non-isolated. Non-isolated environments (default) run code execution on the same machine as the RLM (e.g. through `exec`), which is pretty reasonable for some local low-risk tasks, like simple benchmarking, but can be problematic if the prompts or tool calls can interact with malicious users. Fully isolated environments use cloud-based sandboxes (e.g. Prime Sandboxes, [Modal Sandboxes](https://modal.com/docs/guide/sandboxes)) to run code generated by the RLM, ensuring complete isolation from the host process. Environments can be added, but we natively support the following: `local` (default), `ipython`, `docker`, `modal`, `prime`, `daytona`, `e2b`.
8181

8282
```python
8383
rlm = RLM(
84-
environment="...", # "local", "docker", "modal", "prime", "daytona", "e2b"
84+
environment="...", # "local", "ipython", "docker", "modal", "prime", "daytona", "e2b"
8585
environment_kwargs={...},
8686
)
8787
```
8888

8989
### Local Environments
9090
The default `local` environment `LocalREPL` runs in the same process as the RLM itself, with specified global and local namespaces for minimal security. Using this REPL is generally safe, but should not be used for production settings. It also shares the same virtual environment (e.g. Conda or uv) as the host process.
9191

92+
#### IPython (*requires `pip install 'rlms[ipython]'`*)
93+
`IPythonREPL` runs cells inside a real IPython session — either in-process (default) or in a separate `ipykernel` subprocess. Subprocess mode adds hard `cell_timeout` enforcement and full namespace isolation from the RLM host. See the [IPythonREPL docs](https://alexzhang13.github.io/rlm/environments/ipython) for details.
94+
9295
#### Docker <img src="https://github.com/docker.png" alt="Docker" height="20" style="vertical-align: middle;"/> (*requires [Docker installed](https://docs.docker.com/desktop/setup/install/)*)
9396
We also support a Docker-based environment called `DockerREPL` that launches the REPL environment as a Docker image. By default, we use the `python:3.11-slim` image, but the user can specify custom images as well.
9497

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { CodeBlock } from "@/components/CodeBlock";
2+
import { Table } from "@/components/Table";
3+
4+
export default function IPythonPage() {
5+
return (
6+
<div>
7+
<h1 className="text-3xl font-bold mb-4">IPythonREPL</h1>
8+
9+
<p className="text-xl text-muted-foreground mb-6 leading-relaxed">
10+
<strong className="text-foreground">IPythonREPL</strong> executes code inside a real{" "}
11+
<a href="https://ipython.org/" className="text-primary hover:underline font-medium">IPython</a>{" "}
12+
session instead of plain <code className="px-1.5 py-0.5 rounded bg-muted text-foreground text-sm font-semibold">exec()</code>.
13+
It supports two kernel modes — an <strong className="text-foreground">in-process</strong> shell that
14+
runs in the same Python process as the RLM (the default, fastest), and a{" "}
15+
<strong className="text-foreground">subprocess</strong> kernel that runs a real{" "}
16+
<code className="px-1.5 py-0.5 rounded bg-muted text-foreground text-sm font-semibold">ipykernel</code> in a separate
17+
Python process for hard cell timeouts and full namespace isolation from the RLM host.
18+
Both modes give the LM access to IPython&apos;s full surface (cell magics, rich
19+
repr, line tracebacks).
20+
</p>
21+
22+
<p className="text-muted-foreground mb-4">
23+
<strong>Prerequisite:</strong> install the optional extra:
24+
</p>
25+
<CodeBlock language="bash" code={`pip install 'rlms[ipython]'
26+
# or with uv:
27+
# uv pip install -e ".[ipython]"`} />
28+
29+
<CodeBlock code={`from rlm import RLM
30+
31+
# In-process (default kernel_mode): same process, fast.
32+
rlm = RLM(
33+
backend="openai",
34+
backend_kwargs={"model_name": "gpt-5-mini"},
35+
environment="ipython",
36+
environment_kwargs={
37+
"kernel_mode": "in_process",
38+
"cell_timeout": 30, # SIGALRM-based; Unix main thread only
39+
},
40+
)
41+
42+
# Subprocess: separate Python process, hard timeouts, full isolation.
43+
rlm = RLM(
44+
backend="openai",
45+
backend_kwargs={"model_name": "gpt-5-mini"},
46+
environment="ipython",
47+
environment_kwargs={
48+
"kernel_mode": "subprocess",
49+
"cell_timeout": 30, # Hard guarantee via interrupt_kernel
50+
"startup_timeout": 60,
51+
"max_concurrent_subcalls": 4,
52+
},
53+
)`} />
54+
55+
<hr className="my-8 border-border" />
56+
57+
<h2 className="text-2xl font-semibold mb-4">Arguments</h2>
58+
<Table
59+
headers={["Argument", "Type", "Default", "Description"]}
60+
rows={[
61+
[<code key="1">kernel_mode</code>, <code key="2">&quot;in_process&quot; | &quot;subprocess&quot;</code>, <code key="3">&quot;in_process&quot;</code>, "Where the IPython session runs"],
62+
[<code key="4">cell_timeout</code>, <code key="5">float | None</code>, <code key="6">None</code>, "Per-cell timeout in seconds; None disables"],
63+
[<code key="7">startup_timeout</code>, <code key="8">float</code>, <code key="9">60.0</code>, "Subprocess kernel boot timeout"],
64+
[<code key="10">subcall_timeout</code>, <code key="11">float | None</code>, <code key="12">None</code>, "Per-request kernel→broker socket timeout (subprocess)"],
65+
[<code key="13">max_concurrent_subcalls</code>, <code key="14">int</code>, <code key="15">4</code>, "Global cap on concurrent subcall_fn invocations"],
66+
[<code key="16">setup_code</code>, <code key="17">str</code>, <code key="18">None</code>, "Code to run at initialization"],
67+
[<code key="19">custom_tools</code>, <code key="20">dict</code>, <code key="21">None</code>, "Functions / values injected into the namespace"],
68+
]}
69+
/>
70+
71+
<hr className="my-8 border-border" />
72+
73+
<h2 className="text-2xl font-semibold mb-4">In-process vs. subprocess</h2>
74+
<Table
75+
headers={["", "in_process", "subprocess"]}
76+
rows={[
77+
["Process", "Same as host", "Separate Python via ipykernel"],
78+
["Subcall path", "Direct Python call", "TCP broker (4-byte length-prefixed JSON)"],
79+
[<><code key="t1">cell_timeout</code></>, "Best-effort SIGALRM (Unix, main thread)", <>Hard, via <code key="t2">interrupt_kernel</code></>],
80+
["Cell magics (%%timeit, …)", "Yes", "Yes"],
81+
[<><code key="t3">input()</code></>, "Disabled (raises)", <>Disabled (<code key="t4">allow_stdin=False</code>)</>],
82+
["Isolation from host", "Shares stdout/stderr/cwd/SIGALRM", "Full process isolation"],
83+
["Custom tool injection", "Direct namespace inject", <>Pickled with <code key="t5">dill</code> over ZMQ</>],
84+
]}
85+
/>
86+
87+
<hr className="my-8 border-border" />
88+
89+
<h2 className="text-2xl font-semibold mb-4">How It Works</h2>
90+
<h3 className="text-lg font-medium mt-4 mb-2">In-process</h3>
91+
<ol className="list-decimal list-inside text-muted-foreground space-y-1 mb-4">
92+
<li>Creates a fresh <code>InteractiveShell</code> with a per-instance user module (so multiple in-process REPLs don&apos;t share <code>sys.modules[&apos;__main__&apos;]</code>) and IPython&apos;s history database disabled.</li>
93+
<li>Injects scaffold helpers (<code>llm_query</code>, <code>rlm_query</code>, <code>FINAL_VAR</code>, <code>SHOW_VARS</code>) and a stubbed <code>input()</code> into <code>user_ns</code>.</li>
94+
<li><code>execute_code</code> runs each cell via <code>shell.run_cell</code> under an <code>RLock</code>; <code>cell_timeout</code> is enforced with <code>SIGALRM</code> + <code>setitimer</code>.</li>
95+
<li><code>rlm_query</code> calls <code>subcall_fn</code> directly, gated by a per-instance semaphore so kernel-side fan-out can&apos;t exceed <code>max_concurrent_subcalls</code>.</li>
96+
</ol>
97+
98+
<h3 className="text-lg font-medium mt-4 mb-2">Subprocess</h3>
99+
<ol className="list-decimal list-inside text-muted-foreground space-y-1 mb-4">
100+
<li>Starts a TCP broker on <code>127.0.0.1:0</code> (ephemeral port).</li>
101+
<li>Launches an <code>ipykernel</code> subprocess pinned to the host&apos;s <code>sys.executable</code> (so it inherits the same site-packages — important for <code>dill</code>, custom imports, etc.).</li>
102+
<li>Bootstraps kernel-side scaffold helpers that route <code>llm_query</code> to the LM Handler and <code>rlm_query</code> / <code>FINAL_VAR</code> to the broker over the 4-byte-prefixed JSON protocol.</li>
103+
<li>Each user cell first sets a unique <code>_RLM_CURRENT_CELL</code> cell-id in the kernel via a separate <code>execute_interactive</code> call (so cell magics still work). Every broker request carries this id.</li>
104+
<li><code>cell_timeout</code> is enforced by <code>kc.execute_interactive(timeout=…)</code> + <code>km.interrupt_kernel()</code>. Subcall completions whose <code>subcall_fn</code> finishes <em>after</em> the originating cell timed out are stored under that cell&apos;s id and discarded as stale on the next drain — they aren&apos;t misattributed to a later cell.</li>
105+
</ol>
106+
107+
<pre className="text-sm">{`┌──────────────────────────────────────────┐
108+
│ Host (RLM process) │
109+
│ ┌─────────────┐ Socket ┌──────────────┐ │
110+
│ │ Subcall │◄──────►│ LM Handler │ │
111+
│ │ broker │ └──────────────┘ │
112+
│ │ (TCP + JSON)│ │
113+
│ └─────┬───────┘ │
114+
└────────┼─────────────────────────────────┘
115+
│ ZMQ (jupyter_client)
116+
┌────────┼─────────────────────────────────┐
117+
│ ipykernel subprocess │
118+
│ ┌─────▼────────┐ │
119+
│ │ IPython │ rlm_query() / FINAL_VAR│
120+
│ │ user_ns │ → broker over TCP │
121+
│ │ (cell_id │ llm_query() → LM Handler│
122+
│ │ tagged) │ │
123+
│ └──────────────┘ │
124+
└──────────────────────────────────────────┘`}</pre>
125+
126+
<hr className="my-8 border-border" />
127+
128+
<h2 className="text-2xl font-semibold mb-4">Notable behavior</h2>
129+
<ul className="list-disc list-inside text-muted-foreground space-y-2">
130+
<li><strong className="text-foreground">Per-instance serialization.</strong> <code>execute_code</code> takes an <code>RLock</code>, so concurrent calls from different threads are serialized within an instance.</li>
131+
<li><strong className="text-foreground">Global subcall cap.</strong> <code>max_concurrent_subcalls</code> bounds <em>total</em> in-flight <code>subcall_fn</code> invocations on the instance — even if user code spawns kernel-side threads that each fan out a batch.</li>
132+
<li><strong className="text-foreground">Reentry guard.</strong> If <code>subcall_fn</code> calls <code>execute_code</code> back on the parent REPL (or a cell traverses <code>rlm_query.__self__.execute_code(…)</code> in in-process mode), the call raises <code>RuntimeError</code> instead of deadlocking the cell lock or corrupting the in-flight cell&apos;s tracking. <code>subcall_fn</code> should spawn a child REPL.</li>
133+
<li><strong className="text-foreground">Cell-id attribution.</strong> Subcall completions are tagged with the originating cell&apos;s id so a slow <code>subcall_fn</code> that finishes after its cell timed out is never counted under a later cell. Long-lived kernel threads that call <code>rlm_query</code> after their spawning cell ends will, however, be tagged with whatever cell is active at call time.</li>
134+
<li><strong className="text-foreground">In-process is not isolated.</strong> Two in-process instances each get a unique <code>__main__</code> substitute, but they still share the host&apos;s stdout/stderr/cwd/SIGALRM. Use <code>subprocess</code> if you need true isolation.</li>
135+
</ul>
136+
137+
<hr className="my-8 border-border" />
138+
139+
<h2 className="text-2xl font-semibold mb-4">When to use which mode</h2>
140+
<ul className="list-disc list-inside text-muted-foreground space-y-1.5">
141+
<li><strong className="text-foreground">in_process</strong> — fastest path, no IPC, fine for trusted code, development, short-lived cells. <code>cell_timeout</code> is best-effort (Unix main thread only).</li>
142+
<li><strong className="text-foreground">subprocess</strong> — when you need a hard <code>cell_timeout</code> guarantee, or want full namespace / signal / cwd isolation between the LM&apos;s code and the RLM host.</li>
143+
</ul>
144+
</div>
145+
);
146+
}

‎docs/src/app/environments/page.tsx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,13 @@ export default function EnvironmentsPage() {
7272

7373
<div className="my-12">
7474
<h2 className="text-2xl font-bold mb-4">Available Environments</h2>
75-
<Table
75+
<Table
7676
headers={["Environment", "Isolation", "Best For"]}
7777
rows={[
7878
[<Link key="1" href="/environments/local" className="text-primary hover:underline"><code>local</code></Link>, "Non-isolated", "Development"],
79-
[<Link key="2" href="/environments/docker" className="text-primary hover:underline"><code>docker</code></Link>, "Non-isolated", "CI/CD, reproducibility"],
80-
[<Link key="3" href="/environments/modal" className="text-primary hover:underline"><code>modal</code></Link>, "Isolated", "Production"],
79+
[<Link key="2" href="/environments/ipython" className="text-primary hover:underline"><code>ipython</code></Link>, "Non-isolated (in-process) / Isolated host process (subprocess)", "Cells with magics, hard cell timeouts"],
80+
[<Link key="3" href="/environments/docker" className="text-primary hover:underline"><code>docker</code></Link>, "Non-isolated", "CI/CD, reproducibility"],
81+
[<Link key="4" href="/environments/modal" className="text-primary hover:underline"><code>modal</code></Link>, "Isolated", "Production"],
8182
]}
8283
/>
8384
</div>

‎docs/src/components/Sidebar.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const navigation = [
1515
children: [
1616
{ name: "Overview", href: "/environments" },
1717
{ name: "LocalREPL", href: "/environments/local" },
18+
{ name: "IPythonREPL", href: "/environments/ipython" },
1819
{ name: "DockerREPL", href: "/environments/docker" },
1920
{ name: "ModalREPL", href: "/environments/modal" },
2021
],

0 commit comments

Comments
 (0)