--- title: Build deep Recursive Language Models description: Implement recursive language model agents where each agent runs in its own isolated Daytona sandbox. --- This guide demonstrates how to build a recursive language model (RLM) agent system that uses Daytona sandboxes, based on the approach pioneered in [Recursive Language Models](https://arxiv.org/abs/2512.24601) (Zhang, Kraska, Khattab) and further explored by [Prime Intellect](https://www.primeintellect.ai/blog/rlm). While the original paper and Prime Intellect's implementation focus on single-level recursion (depth=1), this guide extends the concept to **unlimited recursion depth** — agents can spawn sub-agents, which can spawn their own sub-agents, and so on. Each agent runs in its own isolated Daytona sandbox with a fresh clone of the target repository. --- ### 1. Workflow Overview The system implements a recursive agent architecture where agents can delegate subtasks to child agents: 1. **Initialize**: Root agent receives a task and gets a Daytona sandbox with a fresh repository clone 2. **Iterate**: Agent runs a loop: LLM call → extract Python code → execute in REPL 3. **Delegate**: Code can call `rlm_query()` to spawn sub-agents, each with their own sandbox 4. **Aggregate**: Sub-agents return results; parent synthesizes findings and optionally runs more code 5. **Complete**: Root agent receives all sub-agent results, produces a git patch; all sandboxes are cleaned up ``` Root Agent (depth=0) ├── Sub-Agent A (depth=1) │ ├── Sub-Agent A1 (depth=2) │ └── Sub-Agent A2 (depth=2) └── Sub-Agent B (depth=1) ├── Sub-Agent B1 (depth=2) └── Sub-Agent B2 (depth=2) ``` Each agent runs in its own isolated Daytona sandbox with a fresh repository clone, enabling parallel exploration. ### 2. Setup #### Clone the Repository Clone the [Daytona repository](https://github.com/daytona/guides.git) and navigate to the example directory: ```bash git clone https://github.com/daytona/guides.git cd guides/python/recursive-language-models ``` #### Create Virtual Environment ```bash python3.10 -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate ``` #### Install Dependencies ```bash pip install -e . ``` This installs: - `daytona` - Daytona SDK for sandbox management - `litellm` - Unified LLM interface for any provider - `typer` - CLI framework - `pyyaml` - Configuration parsing #### Configure Environment Get your Daytona API key from the [Daytona Dashboard](https://app.daytona.io/dashboard/keys) and create a `.env` file: ```bash DAYTONA_API_KEY=your_daytona_api_key LLM_API_KEY=your_llm_api_key ``` The `LLM_API_KEY` is used via [LiteLLM](https://docs.litellm.ai/), supporting OpenRouter, OpenAI, Anthropic, and other providers. ### 3. Running an Agent With setup complete, let's run an agent. Here's an example that investigates TODO comments in scikit-learn: ```bash python run.py https://github.com/scikit-learn/scikit-learn \ -p "Investigate TODO comments across this repository. Spawn sub-agents to explore different modules. Find the easiest TODO and fix it." ``` This spawns a root agent that explores the codebase, delegates to sub-agents for parallel investigation, and produces a git patch fixing the easiest TODO it finds. We'll walk through the results and trace the execution in detail later, but first, let's look at how the code works. #### CLI Options | Option | Description | |--------|-------------| | `repo` | GitHub repository URL (required) | | `-p, --prompt` | Task prompt for the agent (required) | | `-b, --branch` | Branch name (optional) | | `--commit` | Specific commit SHA (optional) | | `-c, --config` | Path to config file (default: `config.yaml`) | | `-o, --output` | Output file for patch (default: stdout) | ### 4. Understanding the Code Let's walk through the key components of the agent system. #### Agent Execution Loop Each agent runs an iteration loop that calls the LLM, extracts code blocks, and executes them. The core loop in `agent.py`: ```python def _run_loop(self) -> None: """Run the main iteration loop.""" system_prompt = build_system_prompt(depth=self.depth) messages = [{"role": "system", "content": system_prompt}] execution_result = None for iteration in range(self.config.rlm.max_iterations): # Check global timeout if self._is_timeout(): break # Build user prompt with previous execution result user_prompt = build_user_prompt(iteration, execution_result) messages.append({"role": "user", "content": user_prompt}) # Get model completion response = self.client.completion(messages) messages.append({"role": "assistant", "content": response}) # Execute code blocks in REPL repl_result = self.repl.execute_response(response) # Check for final answer if repl_result.final_answer is not None: self._result = repl_result.final_answer break # Format result for next iteration execution_result = format_execution_result(...) ``` Each iteration: 1. Builds a prompt with context from previous execution 2. Gets an LLM completion 3. Extracts and executes Python code blocks 4. Checks if the agent called `FINAL()` to submit results 5. Formats the output for the next iteration #### Sub-Agent Spawning When agent code calls `rlm_query()`, a new sub-agent is created with its own sandbox: ```python def _handle_rlm_query(self, task: str) -> str: """Spawn a sub-agent for a specific task.""" # Check sandbox budget if not self.sandbox_manager.budget.can_acquire(): return "Error: sandbox budget exhausted" # Create sub-agent at depth + 1 sub_agent = RLMAgent( client=self.client, sandbox_manager=self.sandbox_manager, config=self.config, depth=self.depth + 1, task=task, # ... other params ) # Run sub-agent (blocking) result = sub_agent.run() # Return result, truncated if necessary return result.result or "No result" ``` For parallel spawning, `rlm_query_batched()` uses a thread pool: ```python def _handle_rlm_query_batched(self, tasks: list[str]) -> list[str]: """Spawn multiple sub-agents in parallel.""" results = [""] * len(tasks) with ThreadPoolExecutor(max_workers=10) as executor: future_to_idx = { executor.submit(self._handle_rlm_query, task): i for i, task in enumerate(tasks) } for future in as_completed(future_to_idx): idx = future_to_idx[future] results[idx] = future.result() return results ``` #### Agent Code Interface Inside the REPL, agents have access to these functions: | Function | Description | |----------|-------------| | `rlm_query(task)` | Spawn a single sub-agent, returns result string | | `rlm_query_batched(tasks)` | Spawn multiple sub-agents in parallel | | `FINAL(answer)` | Submit final result (root: triggers patch extraction) | | `FINAL_VAR(var_name)` | Submit the value of a variable as result | | `edit_file(path, old, new)` | Edit a file with syntax validation | Example spawning pattern used by agents: ```python # Spawn multiple sub-agents to explore different modules results = rlm_query_batched([ "Search for TODO comments in sklearn/linear_model/ and assess difficulty", "Search for TODO comments in sklearn/ensemble/ and assess difficulty", "Search for TODO comments in sklearn/tree/ and assess difficulty", ]) for i, result in enumerate(results): print(f"=== Sub-agent {i+1} findings ===") print(result) ``` ### 5. Example Walkthrough Let's trace what happens when we run an agent on a popular machine learning library, scikit-learn: ```bash python run.py https://github.com/scikit-learn/scikit-learn \ -p "Investigate TODO comments across this repository. Spawn sub-agents to explore different modules under sklearn/ in parallel. For each TODO found, assess how difficult it would be to fix (easy/medium/hard). After gathering results, pick the easiest TODO and fix it." ``` Note that there are about 400 lines in scikit-learn that contain the substring "# TODO". **Step 1: Root agent explores and spawns depth-1 sub-agents** The root agent (depth=0) examines the repository structure, identifies all sklearn modules, and spawns 25 sub-agents in parallel: ```python # Define the subdirectories to investigate subdirs = [ "cluster", "compose", "covariance", "cross_decomposition", "datasets", "decomposition", "ensemble", "feature_extraction", "feature_selection", "gaussian_process", "impute", "inspection", "linear_model", "manifold", "metrics", "mixture", "model_selection", "neighbors", "neural_network", "preprocessing", "semi_supervised", "svm", "tree", "utils" ] # Create queries for sub-agents queries = [ f"Search for 'TODO' comments in 'sklearn/{subdir}/'. For each TODO found, provide: " f"1. The file path and line number. 2. The content of the TODO. 3. An assessment " f"of how difficult it would be to fix (easy/medium/hard) with a brief justification." for subdir in subdirs ] results = rlm_query_batched(queries) ``` Each of these 25 sub-agents gets its own Daytona sandbox with a fresh clone of scikit-learn. **Step 2: Depth-1 agents spawn depth-2 agents** Some depth-1 agents decide their module is too large and spawn their own sub-agents. For example, the `sklearn/metrics/` agent spawned 3 depth-2 agents: ```python # Inside the sklearn/metrics/ agent (depth=1) # To efficiently handle the large number of TODOs, spawn sub-agents for sub-directories tasks = [ "Identify and assess TODOs in 'sklearn/metrics/cluster/'. Provide file, line, content, and difficulty.", "Identify and assess TODOs in 'sklearn/metrics/tests/'. Provide file, line, content, and difficulty.", "Identify and assess TODOs in 'sklearn/metrics/_plot/' and its 'tests' sub-directory." ] results = rlm_query_batched(tasks) ``` **Step 3: Results propagate back** Each sub-agent returns findings via `FINAL()`. Results flow back up: - Depth-2 → Depth-1: Detailed analysis of specific subdirectories - Depth-1 → Root: Module-level summaries with difficulty ratings **Step 4: Root agent synthesizes and acts** The root agent reviews all findings, identifies the easiest TODO, and makes the fix. **Step 5: Git patch produced** ```python import subprocess subprocess.run(['git', 'add', '-A'], cwd='/workspace') result = subprocess.run(['git', 'diff', '--cached', 'HEAD'], capture_output=True, text=True, cwd='/workspace') FINAL(result.stdout) ``` #### Results - Execution time: **316 seconds** (~5.3 minutes) - Agents spawned: **40** (25 at depth 1, 15 at depth 2) **Generated patch:** ```diff diff --git a/sklearn/utils/_array_api.py b/sklearn/utils/_array_api.py --- a/sklearn/utils/_array_api.py +++ b/sklearn/utils/_array_api.py @@ -19,8 +19,7 @@ from sklearn.externals.array_api_compat import numpy as np_compat from sklearn.utils._dataframe import is_df_or_series from sklearn.utils.fixes import parse_version -# TODO: complete __all__ -__all__ = ["xpx"] # we import xpx here just to re-export it, need this to appease ruff +__all__ = ['device', 'get_namespace', 'get_namespace_and_device', 'indexing_dtype', 'move_to', 'size', 'supported_float_dtypes', 'xpx', 'yield_namespace_device_dtype_combinations', 'yield_namespaces'] ``` The agent found the easiest TODO (`# TODO: complete __all__` in `sklearn/utils/_array_api.py`) and completed the `__all__` list with all public symbols from the module. ### 6. Configuration Configure the agent in `config.yaml`: ```yaml # Model configuration - using LiteLLM format model: name: "openrouter/google/gemini-3-flash-preview" # RLM configuration rlm: max_sandboxes: 50 max_iterations: 50 global_timeout: 3600 result_truncation_limit: 10000 ``` | Parameter | Default | Description | |-----------|---------|-------------| | `model.name` | `openrouter/google/gemini-3-flash-preview` | LLM model in LiteLLM format | | `rlm.max_sandboxes` | 50 | Maximum total sandboxes across entire rollout | | `rlm.max_iterations` | 50 | Maximum iterations per agent | | `rlm.global_timeout` | 3600 | Total timeout in seconds | | `rlm.result_truncation_limit` | 10000 | Max chars in sub-agent results | - Increase `max_sandboxes` for tasks requiring more parallel exploration - The sandbox budget tracks total sandboxes created over the lifetime of the rollout - Sub-agent sandboxes are deleted immediately after completion ### 7. Viewing Results Results are saved to the `results/` directory as JSON files. Use the built-in viewer: ```bash python -m http.server 8000 # Open http://localhost:8000/viewer/ ``` The viewer provides: - Interactive tree visualization of the agent hierarchy - Iteration details with code and output for each agent - Statistics: agent count, max depth, total iterations ### 8. Conclusion Current language models aren't specifically trained to leverage recursive delegation, so RLMs don't necessarily outperform single-agent approaches on benchmarks yet. However, the architecture demonstrates compelling properties for complex tasks. In our scikit-learn example, 40 agents ran in parallel across the agent tree, each with its own isolated sandbox, completing the entire run in just over 5 minutes. This level of parallelism, where each agent can freely modify files, run tests, and explore without affecting others, would be difficult to achieve without per-agent sandboxes. **Key advantages of this approach:** - **Recursive decomposition**: Complex tasks naturally break into sub-tasks handled by specialized agents - **Isolated execution**: Each agent gets a fresh sandbox, preventing interference - **Parallel exploration**: `rlm_query_batched()` enables concurrent investigation