Skip to content

Commit 32e21c6

Browse files
committed
add docker changes to support persistence + parallel sub-calling
1 parent 7b730e4 commit 32e21c6

8 files changed

Lines changed: 1310 additions & 75 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ The default `local` environment `LocalREPL` runs in the same process as the RLM
9898
`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.
9999

100100
#### 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/)*)
101-
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.
101+
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. The container runs fully isolated from the host; a lightweight host-side proxy bridges LM access back into the container.
102+
103+
`DockerREPL` supports the full feature set of the local environment: single LM calls (`llm_query` / `llm_query_batched`), recursive sub-RLM calls (`rlm_query` / `rlm_query_batched`, including parallel batched sub-calls bounded by `max_concurrent_subcalls`), `custom_tools` / `custom_sub_tools`, `persistent=True` multi-turn sessions (versioned `context_N` / `history_N` reused across `completion()` calls), and `compaction=True` auto-summarization of the running `history`. For isolated environments, custom tools should be passed as Python code strings or JSON-serializable values (host callables cannot cross the process boundary).
102104

103105
### Isolated Environments
104106
We support several different REPL environments that run on separate, cloud-based machines. Whenever a recursive sub-call is made in these instances, it is requested from the host process.

‎docs/getting-started.md‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,9 @@ rlm = RLM(
214214

215215
### Docker
216216

217-
Code runs in a Docker container with full isolation.
217+
Code runs in a Docker container with full isolation. A lightweight host-side
218+
proxy bridges LM access back into the container, so the container never needs
219+
API keys or direct network access to the model provider.
218220

219221
```python
220222
rlm = RLM(
@@ -227,7 +229,19 @@ rlm = RLM(
227229
)
228230
```
229231

230-
**Pros:** Containerized isolation, reproducible
232+
`DockerREPL` supports the same capabilities as the local environment:
233+
234+
- `llm_query` / `llm_query_batched` — single LM completions
235+
- `rlm_query` / `rlm_query_batched` — recursive sub-RLM calls (with `max_depth > 1`),
236+
including parallel batched sub-calls bounded by `max_concurrent_subcalls`
237+
- `custom_tools` / `custom_sub_tools` — injected functions/data (pass as Python
238+
code strings or JSON-serializable values, since host callables cannot cross
239+
the container boundary)
240+
- `persistent=True` — multi-turn sessions; the container and its namespace are
241+
kept alive across `completion()` calls, with versioned `context_N` / `history_N`
242+
- `compaction=True` — auto-summarizes the running `history` when context fills up
243+
244+
**Pros:** Containerized isolation, reproducible, full feature parity with local
231245
**Cons:** Requires Docker, slower startup
232246

233247
### Modal

‎examples/docker_repl_example.py‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
"""
2-
Docker REPL example with code execution and LLM queries.
2+
Docker REPL example with code execution, LLM queries, recursive sub-RLM calls,
3+
and custom tools.
34
45
Setup:
56
1. Ensure Docker is running
67
2. Run: python -m examples.docker_repl_example
78
89
The default image (python:3.11-slim) will be pulled automatically.
10+
11+
Persistence and compaction:
12+
DockerREPL also supports multi-turn persistence and compaction. Drive them
13+
through the RLM rather than the env directly, e.g.:
14+
15+
rlm = RLM(backend=..., environment="docker", persistent=True) # multi-turn
16+
rlm = RLM(backend=..., environment="docker", compaction=True) # auto-summarize
917
"""
1018

1119
from rlm.clients.base_lm import BaseLM
1220
from rlm.core.lm_handler import LMHandler
13-
from rlm.core.types import ModelUsageSummary, UsageSummary
21+
from rlm.core.types import ModelUsageSummary, RLMChatCompletion, UsageSummary
1422
from rlm.environments.docker_repl import DockerREPL
1523

1624

@@ -61,6 +69,32 @@ def main():
6169
result = repl.execute_code("print(len(rs))")
6270
print(f" Batched count: {result.stdout.strip()}")
6371

72+
# Recursive sub-RLM calls + custom tools
73+
print("\n[3] Recursive sub-RLM (rlm_query) + custom tools")
74+
75+
def mock_subcall(prompt, model=None):
76+
# In real use this is RLM._subcall, which spawns a child RLM.
77+
return RLMChatCompletion(
78+
root_model=model or "child",
79+
prompt=prompt,
80+
response=f"child-answer({prompt})",
81+
usage_summary=UsageSummary(model_usage_summaries={}),
82+
execution_time=0.0,
83+
)
84+
85+
with DockerREPL(
86+
subcall_fn=mock_subcall,
87+
custom_tools={"triple": "def triple(x):\n return x * 3", "CONST": 7},
88+
) as repl:
89+
result = repl.execute_code("print(triple(CONST))")
90+
print(f" custom tools triple(CONST) → {result.stdout.strip()}")
91+
92+
result = repl.execute_code('print(rlm_query("solve subtask"))')
93+
print(f" rlm_query → {result.stdout.strip()}")
94+
95+
result = repl.execute_code('print(rlm_query_batched(["a", "b", "c"]))')
96+
print(f" rlm_query_batched → {result.stdout.strip()}")
97+
6498
print("\n" + "=" * 50)
6599
print("Done!")
66100

‎rlm/core/rlm.py‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -272,15 +272,17 @@ def _spawn_completion_context(self, prompt: str | dict[str, Any]):
272272
env_kwargs["lm_handler_address"] = (lm_handler.host, lm_handler.port)
273273
env_kwargs["context_payload"] = prompt
274274
env_kwargs["depth"] = self.depth + 1 # Environment depth is RLM depth + 1
275-
# For local/ipython environments with max_depth > 1, pass subcall callback for recursive RLM calls
276-
if self.environment_type in ("local", "ipython") and self.max_depth > 1:
275+
# For environments that support recursive RLM calls, pass the subcall
276+
# callback when max_depth > 1. local/ipython invoke it in-process;
277+
# docker invokes it via its host-side proxy (/rlm_query endpoints).
278+
if self.environment_type in ("local", "ipython", "docker") and self.max_depth > 1:
277279
env_kwargs["subcall_fn"] = self._subcall
278280
# Pass custom tools to the environment
279281
if self.custom_tools is not None:
280282
env_kwargs["custom_tools"] = self.custom_tools
281283
if self.custom_sub_tools is not None:
282284
env_kwargs["custom_sub_tools"] = self.custom_sub_tools
283-
if self.compaction and self.environment_type == "local":
285+
if self.compaction and self.environment_type in ("local", "docker"):
284286
env_kwargs["compaction"] = True
285287
env_kwargs["max_concurrent_subcalls"] = self.max_concurrent_subcalls
286288
environment: BaseEnv = get_environment(self.environment_type, env_kwargs)
@@ -876,13 +878,13 @@ def _validate_persistent_environment_support(self) -> None:
876878
- add_context(payload, index): Add new context for multi-turn conversations
877879
- get_context_count(): Return the number of loaded contexts
878880
879-
Currently only 'local' (LocalREPL) supports these methods.
881+
Currently 'local', 'ipython', and 'docker' support these methods.
880882
881883
Raises:
882884
ValueError: If the environment type does not support persistent mode.
883885
"""
884886
# Known environments that support persistence
885-
persistent_supported_environments = {"local", "ipython"}
887+
persistent_supported_environments = {"local", "ipython", "docker"}
886888

887889
if self.environment_type not in persistent_supported_environments:
888890
raise ValueError(

0 commit comments

Comments
 (0)