{"owner":"alexzhang13","repo":"rlm","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md"],"skills":{"AGENTS.md":"# AGENTS.md\n\nThis guide covers best practices for contributing to the core Recursive Language Models `rlm` library and developing new environments (in `rlm/environments/`) and LM clients (in `rlm/clients/`).\n\n## Setup\n\nWe use `uv` for developing `rlm`.\n```bash\n# Install uv (first time)\ncurl -LsSf https://astral.sh/uv/install.sh | sh\n\n# Setup blank project if needed\nuv init && uv venv --python 3.12\nsource .venv/bin/activate\n\n# Install in editable mode\nuv pip install -e .\n\n# For Modal sandbox support\nuv pip install -e \".[modal]\"\n\n# For Prime sandbox support\nuv pip install -e \".[prime]\"\n```\n\n## General Guidelines\n\n### Code Style & Typing\n- **Formatting**: Strict `ruff` enforcement. All PRs must pass `ruff check --fix .`\n- **Typing**: Explicit types preferred\n  - **OK**: `cast(...)`, `assert ...` for type narrowing\n  - **SOMETIMES OK**: Untyped args for simple cases (e.g., prompt handlers)\n  - **NOT OK**: `# type: ignore` without strong justification\n\n### Naming Conventions\n- **Methods**: snake_case\n- **Classes**: PascalCase (e.g., `LocalREPL`, `PortkeyClient`)\n- **Variables**: snake_case\n- **Constants**: UPPER_CASE (e.g., `_SAFE_BUILTINS`, `RLM_SYSTEM_PROMPT`)\n\nDo NOT use `_` prefix for private methods unless explicitly requested.\n\n### Error Handling Philosophy\n- **Fail fast, fail loud** - No defensive programming or silent fallbacks\n- **Minimize branching** - Prefer single code paths; every `if`/`try` needs justification\n- **Example**: Missing API key → immediate `ValueError`, not graceful fallback\n\n## Core Repository Development\n\nFor PRs to `rlm` core:\n```bash\ngit clone https://github.com/alexzhang13/rlm.git\ncd rlm\n\n# Standard development:\nuv sync\n\n# Install dev + test dependencies:\nuv sync --group dev --group test\n\n# Install pre-commit hooks:\nuv run pre-commit install\n```\n\n### Dependencies\n- Avoid new core dependencies\n- Use optional extras for non-essential features (e.g., `modal` extra)\n- Exception: tiny deps that simplify widely-used code\n\n### Testing\n- `uv run pytest` with discovery under `tests/`\n- Write simple, deterministic unit tests\n- Update tests when changing functionality\n- For isolated environments, mock external services\n\n### Documentation\n- Keep concise and actionable\n- Update README when behavior changes\n- Avoid content duplication\n\n### Scope\n- Small, focused diffs\n- One change per PR\n- Backward compatibility is only desirable if it can be done without introducing excessive maintenance burden\n- Delete dead code (don't guard it)\n\n### Checklist\n\nBefore a PR:\n\n```bash\n# Run style + lint checks:\nuv run ruff check --fix .\nuv run ruff format .\nuv run pre-commit run --all-files\n\n# Run tests:\nuv run pytest\n```\n\nEnsure docs and tests are updated if necessary, and dead code is deleted. Strive for minimal, surgical diffs.\n\n## Developing LM Clients\n\nLM client implementations live in `rlm/clients/`. All clients must inherit from `BaseLM`.\n\n### Client Pattern\n\n| Base Class | When to Use | Key Methods |\n|------------|-------------|-------------|\n| `BaseLM` | All LM integrations | `completion`, `acompletion`, `get_usage_summary`, `get_last_usage` |\n\n### Requirements\n- Inherit from `BaseLM` in `rlm/clients/base_lm.py`\n- Implement all abstract methods: `completion`, `acompletion`, `get_usage_summary`, `get_last_usage`\n- Track per-model usage (calls, input/output tokens)\n- Handle both string and message list prompts\n- Register client in `rlm/clients/__init__.py`\n\n### Example Structure\n```python\nfrom rlm.clients.base_lm import BaseLM\nfrom rlm.core.types import ModelUsageSummary, UsageSummary\n\nclass MyClient(BaseLM):\n    def __init__(self, api_key: str, model_name: str, **kwargs):\n        super().__init__(model_name=model_name, **kwargs)\n        # Initialize your client\n        \n    def completion(self, prompt: str | list[dict[str, Any]], model: str | None = None) -> str:\n        # Handle both str and message list formats\n        # Track usage with _track_cost()\n        # Return response string\n        \n    def get_usage_summary(self) -> UsageSummary:\n        # Return aggregated usage across all calls\n```\n\n### Configuration Guidelines\n- **Environment variables**: ONLY for API keys (document in README)\n- **Hardcode**: Default base URLs, reasonable defaults\n- **Arguments**: Essential customization via `__init__()`\n\n## Developing Environments\n\nEnvironment implementations live in `rlm/environments/`. Choose the appropriate base class.\n\n### Environment Pattern\n\n| Pattern | Base Class | When to Use | Key Methods |\n|---------|------------|-------------|-------------|\n| **Non-isolated** | `NonIsolatedEnv` | Local execution, same machine | `setup`, `load_context`, `execute_code` |\n| **Isolated** | `IsolatedEnv` | Cloud sandboxes (Modal, Prime) | `setup`, `load_context`, `execute_code` |\n\n### Requirements\n- Inherit from `NonIsolatedEnv` or `IsolatedEnv` in `rlm/environments/base_env.py`\n- Implement all abstract methods: `setup`, `load_context`, `execute_code`\n- Return `REPLResult` from `execute_code`\n- Handle `lm_handler_address` for LM calls via `llm_query()` and `rlm_query()`\n- Implement `cleanup()` for resource management\n- Register environment in `rlm/environments/__init__.py`\n\n### Key Implementation Details\n- `setup()`: Initialize globals, locals, and helper functions\n- `load_context()`: Make context available as `context` variable\n- `execute_code()`: Execute code, capture stdout/stderr, return `REPLResult`\n- Always provide `llm_query`, `llm_query_batched`, `rlm_query`, and `rlm_query_batched` functions in environment globals\n\n### State Management\nEnvironments must provide these globals to executed code:\n- `context`: The loaded context payload\n- `llm_query(prompt, model=None)`: Plain single LM completion (no REPL, no iteration)\n- `llm_query_batched(prompts, model=None)`: Batched plain LM completions\n- `rlm_query(prompt, model=None)`: Recursive child RLM call (own REPL + iteration). Falls back to `llm_query` at max depth.\n- `rlm_query_batched(prompts, model=None)`: Batched recursive child RLM calls\n- `answer`: A dict (`{\"content\": \"\", \"ready\": False}`); the model sets `answer[\"content\"]` and `answer[\"ready\"] = True` to return a final answer. The env surfaces the content on `REPLResult.final_answer` once `ready` flips truthy.\n- `SHOW_VARS()`: For listing available variables\n\n### Example Structure\n```python\nfrom rlm.environments.base_env import NonIsolatedEnv\nfrom rlm.core.types import REPLResult\n\nclass MyEnvironment(NonIsolatedEnv):\n    def __init__(self, lm_handler_address: tuple[str, int] | None = None, \n                 context_payload: dict | list | str | None = None, **kwargs):\n        super().__init__(**kwargs)\n        self.lm_handler_address = lm_handler_address\n        self.setup()\n        if context_payload:\n            self.load_context(context_payload)\n            \n    def setup(self):\n        # Initialize execution namespace\n        \n    def load_context(self, context_payload: dict | list | str):\n        # Make context available to executed code\n        \n    def execute_code(self, code: str) -> REPLResult:\n        # Execute code and return REPLResult\n        \n    def cleanup(self):\n        # Clean up resources\n```\n\n### Checklist\n- Guidelines here are followed\n- Environment works with basic RLM completion calls\n- `cleanup()` properly releases all resources\n- Sub-LM calls work via `llm_query()` and `rlm_query()`\n- Reserved names (`llm_query`, `rlm_query`, `context`, `history`, `answer`, `SHOW_VARS`) are restored after each execution\n\n## Architecture: Environment ↔ LM Handler Communication\n\nUnderstanding how environments communicate with the LM Handler is essential for developing new environments.\n\n### Overview\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│  Host Machine                                                       │\n│  ┌─────────────┐       Socket (TCP)        ┌──────────────────────┐ │\n│  │   RLM       │◄──────────────────────────►  LMHandler           │ │\n│  │  (main)     │                           │  (ThreadingTCPServer)│ │\n│  └─────────────┘                           └──────────────────────┘ │\n│        │                                            ▲               │\n│        ▼                                            │               │\n│  ┌─────────────┐       Socket (TCP)                 │               │\n│  │ LocalREPL   │────────────────────────────────────┘               │\n│  │ (exec code) │  llm_query() / rlm_query() → LM calls               │\n│  └─────────────┘                                                    │\n└─────────────────────────────────────────────────────────────────────┘\n```\n\n### Socket Protocol (Non-Isolated Environments)\n\nNon-isolated environments like `LocalREPL` communicate directly with the `LMHandler` via TCP sockets using a length-prefixed JSON protocol:\n\n**Protocol Format**: `4-byte big-endian length prefix + UTF-8 JSON payload`\n\n```python\n# Sending a message (from rlm/core/comms_utils.py)\ndef socket_send(sock: socket.socket, data: dict) -> None:\n    payload = json.dumps(data).encode(\"utf-8\")\n    sock.sendall(struct.pack(\">I\", len(payload)) + payload)\n```\n\n**Request Flow**:\n1. Environment's `llm_query(prompt)` or `rlm_query(prompt)` is called during code execution\n2. For `llm_query`: creates `LMRequest` and calls `send_lm_request(address, request)`. For `rlm_query`: invokes `subcall_fn` to spawn a child RLM (or falls back to `llm_query` at max depth).\n3. Opens TCP connection to `LMHandler` at `(host, port)`\n4. Sends length-prefixed JSON request\n5. `LMHandler` processes via `LMRequestHandler.handle()`\n6. Returns `LMResponse` with `RLMChatCompletion` or error\n\n**Key Components**:\n- `LMHandler` (`rlm/core/lm_handler.py`): Multi-threaded TCP server wrapping LM clients\n- `LMRequest` / `LMResponse` (`rlm/core/comms_utils.py`): Typed request/response dataclasses\n- `send_lm_request()` / `send_lm_request_batched()`: Helper functions for socket communication\n\n### HTTP Broker Pattern (Isolated Environments)\n\nIsolated environments (Modal, Prime) cannot directly connect to the host's socket server. They use an HTTP broker pattern:\n\n```\n┌─────────────────────────────────────────────────────────────────────────────┐\n│  Host Machine                                                               │\n│  ┌─────────┐    Socket    ┌────────────┐    HTTP Poll    ┌────────────────┐ │\n│  │   RLM   │◄────────────►│  LMHandler │◄────────────────│   ModalREPL    │ │\n│  └─────────┘              └────────────┘                 │  (poller)      │ │\n│                                                          └────────────────┘ │\n│                                                                  │          │\n│                                                          HTTP (tunnel)      │\n│                                                                  │          │\n└──────────────────────────────────────────────────────────────────┼──────────┘\n                                                                   │\n┌──────────────────────────────────────────────────────────────────┼──────────┐\n│  Cloud Sandbox (Modal/Prime)                                     ▼          │\n│  ┌─────────────┐     HTTP (localhost)     ┌─────────────────────────────┐   │\n│  │ Exec Script │◄────────────────────────►│   Broker Server (Flask)     │   │\n│  │ (exec code) │     /enqueue, etc.       │   - /enqueue (submit req)   │   │\n│  └─────────────┘                          │   - /pending (poll reqs)    │   │\n│                                           │   - /respond (return resp)  │   │\n│                                           └─────────────────────────────┘   │\n└─────────────────────────────────────────────────────────────────────────────┘\n```\n\n**How It Works**:\n\n1. **Sandbox Setup**: Environment creates a cloud sandbox with an HTTP broker server running inside\n2. **Tunnel Exposure**: Broker server is exposed via encrypted tunnel (e.g., Modal's `encrypted_ports`)\n3. **Code Execution**: When `llm_query()` is called inside sandbox, it POSTs to `http://localhost:8080/enqueue`\n4. **Request Queuing**: Broker queues the request and blocks waiting for response\n5. **Host Polling**: `ModalREPL` on host polls `{tunnel_url}/pending` for new requests\n6. **LM Forwarding**: Host forwards requests to `LMHandler` via socket, gets response\n7. **Response Delivery**: Host POSTs response to `{tunnel_url}/respond`\n8. **Unblocking**: Broker unblocks the original `/enqueue` call with the response\n\n**Broker Endpoints**:\n| Endpoint | Method | Purpose |\n|----------|--------|---------|\n| `/enqueue` | POST | Submit LLM request from sandbox code (blocks until response) |\n| `/pending` | GET | Get list of pending requests (called by host poller) |\n| `/respond` | POST | Submit response for a request ID (called by host poller) |\n| `/health` | GET | Health check |\n\n**Key Implementation Details**:\n- Broker runs as a Flask server inside the sandbox\n- Uses `threading.Event` for request/response synchronization\n- Poller thread on host runs in background with 100ms polling interval\n- State persistence via `dill` serialization to `/tmp/rlm_state.dill`\n\n### Implementing a New Isolated Environment\n\nWhen building a new isolated environment (e.g., for a new cloud provider):\n\n1. **Create broker server** - Flask/HTTP server with `/enqueue`, `/pending`, `/respond` endpoints\n2. **Expose tunnel** - Use provider's tunnel/port forwarding to expose broker to host\n3. **Implement poller** - Background thread on host to poll and forward requests\n4. **Build exec script** - Script that runs inside sandbox with `llm_query()` calling broker\n5. **Handle state** - Serialize/deserialize execution state between code blocks\n\nSee `rlm/environments/modal_repl.py` as the canonical reference implementation.\n\n"},"files":{"AGENTS.md":"# AGENTS.md\n\nThis guide covers best practices for contributing to the core Recursive Language Models `rlm` library and developing new environments (in `rlm/environments/`) and LM clients (in `rlm/clients/`).\n\n## Setup\n\nWe use `uv` for developing `rlm`.\n```bash\n# Install uv (first time)\ncurl -LsSf https://astral.sh/uv/install.sh | sh\n\n# Setup blank project if needed\nuv init && uv venv --python 3.12\nsource .venv/bin/activate\n\n# Install in editable mode\nuv pip install -e .\n\n# For Modal sandbox support\nuv pip install -e \".[modal]\"\n\n# For Prime sandbox support\nuv pip install -e \".[prime]\"\n```\n\n## General Guidelines\n\n### Code Style & Typing\n- **Formatting**: Strict `ruff` enforcement. All PRs must pass `ruff check --fix .`\n- **Typing**: Explicit types preferred\n  - **OK**: `cast(...)`, `assert ...` for type narrowing\n  - **SOMETIMES OK**: Untyped args for simple cases (e.g., prompt handlers)\n  - **NOT OK**: `# type: ignore` without strong justification\n\n### Naming Conventions\n- **Methods**: snake_case\n- **Classes**: PascalCase (e.g., `LocalREPL`, `PortkeyClient`)\n- **Variables**: snake_case\n- **Constants**: UPPER_CASE (e.g., `_SAFE_BUILTINS`, `RLM_SYSTEM_PROMPT`)\n\nDo NOT use `_` prefix for private methods unless explicitly requested.\n\n### Error Handling Philosophy\n- **Fail fast, fail loud** - No defensive programming or silent fallbacks\n- **Minimize branching** - Prefer single code paths; every `if`/`try` needs justification\n- **Example**: Missing API key → immediate `ValueError`, not graceful fallback\n\n## Core Repository Development\n\nFor PRs to `rlm` core:\n```bash\ngit clone https://github.com/alexzhang13/rlm.git\ncd rlm\n\n# Standard development:\nuv sync\n\n# Install dev + test dependencies:\nuv sync --group dev --group test\n\n# Install pre-commit hooks:\nuv run pre-commit install\n```\n\n### Dependencies\n- Avoid new core dependencies\n- Use optional extras for non-essential features (e.g., `modal` extra)\n- Exception: tiny deps that simplify widely-used code\n\n### Testing\n- `uv run pytest` with discovery under `tests/`\n- Write simple, deterministic unit tests\n- Update tests when changing functionality\n- For isolated environments, mock external services\n\n### Documentation\n- Keep concise and actionable\n- Update README when behavior changes\n- Avoid content duplication\n\n### Scope\n- Small, focused diffs\n- One change per PR\n- Backward compatibility is only desirable if it can be done without introducing excessive maintenance burden\n- Delete dead code (don't guard it)\n\n### Checklist\n\nBefore a PR:\n\n```bash\n# Run style + lint checks:\nuv run ruff check --fix .\nuv run ruff format .\nuv run pre-commit run --all-files\n\n# Run tests:\nuv run pytest\n```\n\nEnsure docs and tests are updated if necessary, and dead code is deleted. Strive for minimal, surgical diffs.\n\n## Developing LM Clients\n\nLM client implementations live in `rlm/clients/`. All clients must inherit from `BaseLM`.\n\n### Client Pattern\n\n| Base Class | When to Use | Key Methods |\n|------------|-------------|-------------|\n| `BaseLM` | All LM integrations | `completion`, `acompletion`, `get_usage_summary`, `get_last_usage` |\n\n### Requirements\n- Inherit from `BaseLM` in `rlm/clients/base_lm.py`\n- Implement all abstract methods: `completion`, `acompletion`, `get_usage_summary`, `get_last_usage`\n- Track per-model usage (calls, input/output tokens)\n- Handle both string and message list prompts\n- Register client in `rlm/clients/__init__.py`\n\n### Example Structure\n```python\nfrom rlm.clients.base_lm import BaseLM\nfrom rlm.core.types import ModelUsageSummary, UsageSummary\n\nclass MyClient(BaseLM):\n    def __init__(self, api_key: str, model_name: str, **kwargs):\n        super().__init__(model_name=model_name, **kwargs)\n        # Initialize your client\n        \n    def completion(self, prompt: str | list[dict[str, Any]], model: str | None = None) -> str:\n        # Handle both str and message list formats\n        # Track usage with _track_cost()\n        # Return response string\n        \n    def get_usage_summary(self) -> UsageSummary:\n        # Return aggregated usage across all calls\n```\n\n### Configuration Guidelines\n- **Environment variables**: ONLY for API keys (document in README)\n- **Hardcode**: Default base URLs, reasonable defaults\n- **Arguments**: Essential customization via `__init__()`\n\n## Developing Environments\n\nEnvironment implementations live in `rlm/environments/`. Choose the appropriate base class.\n\n### Environment Pattern\n\n| Pattern | Base Class | When to Use | Key Methods |\n|---------|------------|-------------|-------------|\n| **Non-isolated** | `NonIsolatedEnv` | Local execution, same machine | `setup`, `load_context`, `execute_code` |\n| **Isolated** | `IsolatedEnv` | Cloud sandboxes (Modal, Prime) | `setup`, `load_context`, `execute_code` |\n\n### Requirements\n- Inherit from `NonIsolatedEnv` or `IsolatedEnv` in `rlm/environments/base_env.py`\n- Implement all abstract methods: `setup`, `load_context`, `execute_code`\n- Return `REPLResult` from `execute_code`\n- Handle `lm_handler_address` for LM calls via `llm_query()` and `rlm_query()`\n- Implement `cleanup()` for resource management\n- Register environment in `rlm/environments/__init__.py`\n\n### Key Implementation Details\n- `setup()`: Initialize globals, locals, and helper functions\n- `load_context()`: Make context available as `context` variable\n- `execute_code()`: Execute code, capture stdout/stderr, return `REPLResult`\n- Always provide `llm_query`, `llm_query_batched`, `rlm_query`, and `rlm_query_batched` functions in environment globals\n\n### State Management\nEnvironments must provide these globals to executed code:\n- `context`: The loaded context payload\n- `llm_query(prompt, model=None)`: Plain single LM completion (no REPL, no iteration)\n- `llm_query_batched(prompts, model=None)`: Batched plain LM completions\n- `rlm_query(prompt, model=None)`: Recursive child RLM call (own REPL + iteration). Falls back to `llm_query` at max depth.\n- `rlm_query_batched(prompts, model=None)`: Batched recursive child RLM calls\n- `answer`: A dict (`{\"content\": \"\", \"ready\": False}`); the model sets `answer[\"content\"]` and `answer[\"ready\"] = True` to return a final answer. The env surfaces the content on `REPLResult.final_answer` once `ready` flips truthy.\n- `SHOW_VARS()`: For listing available variables\n\n### Example Structure\n```python\nfrom rlm.environments.base_env import NonIsolatedEnv\nfrom rlm.core.types import REPLResult\n\nclass MyEnvironment(NonIsolatedEnv):\n    def __init__(self, lm_handler_address: tuple[str, int] | None = None, \n                 context_payload: dict | list | str | None = None, **kwargs):\n        super().__init__(**kwargs)\n        self.lm_handler_address = lm_handler_address\n        self.setup()\n        if context_payload:\n            self.load_context(context_payload)\n            \n    def setup(self):\n        # Initialize execution namespace\n        \n    def load_context(self, context_payload: dict | list | str):\n        # Make context available to executed code\n        \n    def execute_code(self, code: str) -> REPLResult:\n        # Execute code and return REPLResult\n        \n    def cleanup(self):\n        # Clean up resources\n```\n\n### Checklist\n- Guidelines here are followed\n- Environment works with basic RLM completion calls\n- `cleanup()` properly releases all resources\n- Sub-LM calls work via `llm_query()` and `rlm_query()`\n- Reserved names (`llm_query`, `rlm_query`, `context`, `history`, `answer`, `SHOW_VARS`) are restored after each execution\n\n## Architecture: Environment ↔ LM Handler Communication\n\nUnderstanding how environments communicate with the LM Handler is essential for developing new environments.\n\n### Overview\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│  Host Machine                                                       │\n│  ┌─────────────┐       Socket (TCP)        ┌──────────────────────┐ │\n│  │   RLM       │◄──────────────────────────►  LMHandler           │ │\n│  │  (main)     │                           │  (ThreadingTCPServer)│ │\n│  └─────────────┘                           └──────────────────────┘ │\n│        │                                            ▲               │\n│        ▼                                            │               │\n│  ┌─────────────┐       Socket (TCP)                 │               │\n│  │ LocalREPL   │────────────────────────────────────┘               │\n│  │ (exec code) │  llm_query() / rlm_query() → LM calls               │\n│  └─────────────┘                                                    │\n└─────────────────────────────────────────────────────────────────────┘\n```\n\n### Socket Protocol (Non-Isolated Environments)\n\nNon-isolated environments like `LocalREPL` communicate directly with the `LMHandler` via TCP sockets using a length-prefixed JSON protocol:\n\n**Protocol Format**: `4-byte big-endian length prefix + UTF-8 JSON payload`\n\n```python\n# Sending a message (from rlm/core/comms_utils.py)\ndef socket_send(sock: socket.socket, data: dict) -> None:\n    payload = json.dumps(data).encode(\"utf-8\")\n    sock.sendall(struct.pack(\">I\", len(payload)) + payload)\n```\n\n**Request Flow**:\n1. Environment's `llm_query(prompt)` or `rlm_query(prompt)` is called during code execution\n2. For `llm_query`: creates `LMRequest` and calls `send_lm_request(address, request)`. For `rlm_query`: invokes `subcall_fn` to spawn a child RLM (or falls back to `llm_query` at max depth).\n3. Opens TCP connection to `LMHandler` at `(host, port)`\n4. Sends length-prefixed JSON request\n5. `LMHandler` processes via `LMRequestHandler.handle()`\n6. Returns `LMResponse` with `RLMChatCompletion` or error\n\n**Key Components**:\n- `LMHandler` (`rlm/core/lm_handler.py`): Multi-threaded TCP server wrapping LM clients\n- `LMRequest` / `LMResponse` (`rlm/core/comms_utils.py`): Typed request/response dataclasses\n- `send_lm_request()` / `send_lm_request_batched()`: Helper functions for socket communication\n\n### HTTP Broker Pattern (Isolated Environments)\n\nIsolated environments (Modal, Prime) cannot directly connect to the host's socket server. They use an HTTP broker pattern:\n\n```\n┌─────────────────────────────────────────────────────────────────────────────┐\n│  Host Machine                                                               │\n│  ┌─────────┐    Socket    ┌────────────┐    HTTP Poll    ┌────────────────┐ │\n│  │   RLM   │◄────────────►│  LMHandler │◄────────────────│   ModalREPL    │ │\n│  └─────────┘              └────────────┘                 │  (poller)      │ │\n│                                                          └────────────────┘ │\n│                                                                  │          │\n│                                                          HTTP (tunnel)      │\n│                                                                  │          │\n└──────────────────────────────────────────────────────────────────┼──────────┘\n                                                                   │\n┌──────────────────────────────────────────────────────────────────┼──────────┐\n│  Cloud Sandbox (Modal/Prime)                                     ▼          │\n│  ┌─────────────┐     HTTP (localhost)     ┌─────────────────────────────┐   │\n│  │ Exec Script │◄────────────────────────►│   Broker Server (Flask)     │   │\n│  │ (exec code) │     /enqueue, etc.       │   - /enqueue (submit req)   │   │\n│  └─────────────┘                          │   - /pending (poll reqs)    │   │\n│                                           │   - /respond (return resp)  │   │\n│                                           └─────────────────────────────┘   │\n└─────────────────────────────────────────────────────────────────────────────┘\n```\n\n**How It Works**:\n\n1. **Sandbox Setup**: Environment creates a cloud sandbox with an HTTP broker server running inside\n2. **Tunnel Exposure**: Broker server is exposed via encrypted tunnel (e.g., Modal's `encrypted_ports`)\n3. **Code Execution**: When `llm_query()` is called inside sandbox, it POSTs to `http://localhost:8080/enqueue`\n4. **Request Queuing**: Broker queues the request and blocks waiting for response\n5. **Host Polling**: `ModalREPL` on host polls `{tunnel_url}/pending` for new requests\n6. **LM Forwarding**: Host forwards requests to `LMHandler` via socket, gets response\n7. **Response Delivery**: Host POSTs response to `{tunnel_url}/respond`\n8. **Unblocking**: Broker unblocks the original `/enqueue` call with the response\n\n**Broker Endpoints**:\n| Endpoint | Method | Purpose |\n|----------|--------|---------|\n| `/enqueue` | POST | Submit LLM request from sandbox code (blocks until response) |\n| `/pending` | GET | Get list of pending requests (called by host poller) |\n| `/respond` | POST | Submit response for a request ID (called by host poller) |\n| `/health` | GET | Health check |\n\n**Key Implementation Details**:\n- Broker runs as a Flask server inside the sandbox\n- Uses `threading.Event` for request/response synchronization\n- Poller thread on host runs in background with 100ms polling interval\n- State persistence via `dill` serialization to `/tmp/rlm_state.dill`\n\n### Implementing a New Isolated Environment\n\nWhen building a new isolated environment (e.g., for a new cloud provider):\n\n1. **Create broker server** - Flask/HTTP server with `/enqueue`, `/pending`, `/respond` endpoints\n2. **Expose tunnel** - Use provider's tunnel/port forwarding to expose broker to host\n3. **Implement poller** - Background thread on host to poll and forward requests\n4. **Build exec script** - Script that runs inside sandbox with `llm_query()` calling broker\n5. **Handle state** - Serialize/deserialize execution state between code blocks\n\nSee `rlm/environments/modal_repl.py` as the canonical reference implementation.\n\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# AGENTS.md\n\nThis guide covers best practices for contributing to the core Recursive Language Models `rlm` library and developing new environments (in `rlm/environments/`) and LM clients (in `rlm/clients/`).\n\n## Setup\n\nWe use `uv` for developing `rlm`.\n```bash\n# Install uv (first time)\ncurl -LsSf https://astral.sh/uv/install.sh | sh\n\n# Setup blank project if needed\nuv init && uv venv --python 3.12\nsource .venv/bin/activate\n\n# Install in editable mode\nuv pip install -e .\n\n# For Modal sandbox support\nuv pip install -e \".[modal]\"\n\n# For Prime sandbox support\nuv pip install -e \".[prime]\"\n```\n\n## General Guidelines\n\n### Code Style & Typing\n- **Formatting**: Strict `ruff` enforcement. All PRs must pass `ruff check --fix .`\n- **Typing**: Explicit types preferred\n  - **OK**: `cast(...)`, `assert ...` for type narrowing\n  - **SOMETIMES OK**: Untyped args for simple cases (e.g., prompt handlers)\n  - **NOT OK**: `# type: ignore` without strong justification\n\n### Naming Conventions\n- **Methods**: snake_case\n- **Classes**: PascalCase (e.g., `LocalREPL`, `PortkeyClient`)\n- **Variables**: snake_case\n- **Constants**: UPPER_CASE (e.g., `_SAFE_BUILTINS`, `RLM_SYSTEM_PROMPT`)\n\nDo NOT use `_` prefix for private methods unless explicitly requested.\n\n### Error Handling Philosophy\n- **Fail fast, fail loud** - No defensive programming or silent fallbacks\n- **Minimize branching** - Prefer single code paths; every `if`/`try` needs justification\n- **Example**: Missing API key → immediate `ValueError`, not graceful fallback\n\n## Core Repository Development\n\nFor PRs to `rlm` core:\n```bash\ngit clone https://github.com/alexzhang13/rlm.git\ncd rlm\n\n# Standard development:\nuv sync\n\n# Install dev + test dependencies:\nuv sync --group dev --group test\n\n# Install pre-commit hooks:\nuv run pre-commit install\n```\n\n### Dependencies\n- Avoid new core dependencies\n- Use optional extras for non-essential features (e.g., `modal` extra)\n- Exception: tiny deps that simplify widely-used code\n\n### Testing\n- `uv run pytest` with discovery under `tests/`\n- Write simple, deterministic unit tests\n- Update tests when changing functionality\n- For isolated environments, mock external services\n\n### Documentation\n- Keep concise and actionable\n- Update README when behavior changes\n- Avoid content duplication\n\n### Scope\n- Small, focused diffs\n- One change per PR\n- Backward compatibility is only desirable if it can be done without introducing excessive maintenance burden\n- Delete dead code (don't guard it)\n\n### Checklist\n\nBefore a PR:\n\n```bash\n# Run style + lint checks:\nuv run ruff check --fix .\nuv run ruff format .\nuv run pre-commit run --all-files\n\n# Run tests:\nuv run pytest\n```\n\nEnsure docs and tests are updated if necessary, and dead code is deleted. Strive for minimal, surgical diffs.\n\n## Developing LM Clients\n\nLM client implementations live in `rlm/clients/`. All clients must inherit from `BaseLM`.\n\n### Client Pattern\n\n| Base Class | When to Use | Key Methods |\n|------------|-------------|-------------|\n| `BaseLM` | All LM integrations | `completion`, `acompletion`, `get_usage_summary`, `get_last_usage` |\n\n### Requirements\n- Inherit from `BaseLM` in `rlm/clients/base_lm.py`\n- Implement all abstract methods: `completion`, `acompletion`, `get_usage_summary`, `get_last_usage`\n- Track per-model usage (calls, input/output tokens)\n- Handle both string and message list prompts\n- Register client in `rlm/clients/__init__.py`\n\n### Example Structure\n```python\nfrom rlm.clients.base_lm import BaseLM\nfrom rlm.core.types import ModelUsageSummary, UsageSummary\n\nclass MyClient(BaseLM):\n    def __init__(self, api_key: str, model_name: str, **kwargs):\n        super().__init__(model_name=model_name, **kwargs)\n        # Initialize your client\n        \n    def completion(self, prompt: str | list[dict[str, Any]], model: str | None = None) -> str:\n        # Handle both str and message list formats\n        # Track usage with _track_cost()\n        # Return response string\n        \n    def get_usage_summary(self) -> UsageSummary:\n        # Return aggregated usage across all calls\n```\n\n### Configuration Guidelines\n- **Environment variables**: ONLY for API keys (document in README)\n- **Hardcode**: Default base URLs, reasonable defaults\n- **Arguments**: Essential customization via `__init__()`\n\n## Developing Environments\n\nEnvironment implementations live in `rlm/environments/`. Choose the appropriate base class.\n\n### Environment Pattern\n\n| Pattern | Base Class | When to Use | Key Methods |\n|---------|------------|-------------|-------------|\n| **Non-isolated** | `NonIsolatedEnv` | Local execution, same machine | `setup`, `load_context`, `execute_code` |\n| **Isolated** | `IsolatedEnv` | Cloud sandboxes (Modal, Prime) | `setup`, `load_context`, `execute_code` |\n\n### Requirements\n- Inherit from `NonIsolatedEnv` or `IsolatedEnv` in `rlm/environments/base_env.py`\n- Implement all abstract methods: `setup`, `load_context`, `execute_code`\n- Return `REPLResult` from `execute_code`\n- Handle `lm_handler_address` for LM calls via `llm_query()` and `rlm_query()`\n- Implement `cleanup()` for resource management\n- Register environment in `rlm/environments/__init__.py`\n\n### Key Implementation Details\n- `setup()`: Initialize globals, locals, and helper functions\n- `load_context()`: Make context available as `context` variable\n- `execute_code()`: Execute code, capture stdout/stderr, return `REPLResult`\n- Always provide `llm_query`, `llm_query_batched`, `rlm_query`, and `rlm_query_batched` functions in environment globals\n\n### State Management\nEnvironments must provide these globals to executed code:\n- `context`: The loaded context payload\n- `llm_query(prompt, model=None)`: Plain single LM completion (no REPL, no iteration)\n- `llm_query_batched(prompts, model=None)`: Batched plain LM completions\n- `rlm_query(prompt, model=None)`: Recursive child RLM call (own REPL + iteration). Falls back to `llm_query` at max depth.\n- `rlm_query_batched(prompts, model=None)`: Batched recursive child RLM calls\n- `answer`: A dict (`{\"content\": \"\", \"ready\": False}`); the model sets `answer[\"content\"]` and `answer[\"ready\"] = True` to return a final answer. The env surfaces the content on `REPLResult.final_answer` once `ready` flips truthy.\n- `SHOW_VARS()`: For listing available variables\n\n### Example Structure\n```python\nfrom rlm.environments.base_env import NonIsolatedEnv\nfrom rlm.core.types import REPLResult\n\nclass MyEnvironment(NonIsolatedEnv):\n    def __init__(self, lm_handler_address: tuple[str, int] | None = None, \n                 context_payload: dict | list | str | None = None, **kwargs):\n        super().__init__(**kwargs)\n        self.lm_handler_address = lm_handler_address\n        self.setup()\n        if context_payload:\n            self.load_context(context_payload)\n            \n    def setup(self):\n        # Initialize execution namespace\n        \n    def load_context(self, context_payload: dict | list | str):\n        # Make context available to executed code\n        \n    def execute_code(self, code: str) -> REPLResult:\n        # Execute code and return REPLResult\n        \n    def cleanup(self):\n        # Clean up resources\n```\n\n### Checklist\n- Guidelines here are followed\n- Environment works with basic RLM completion calls\n- `cleanup()` properly releases all resources\n- Sub-LM calls work via `llm_query()` and `rlm_query()`\n- Reserved names (`llm_query`, `rlm_query`, `context`, `history`, `answer`, `SHOW_VARS`) are restored after each execution\n\n## Architecture: Environment ↔ LM Handler Communication\n\nUnderstanding how environments communicate with the LM Handler is essential for developing new environments.\n\n### Overview\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│  Host Machine                                                       │\n│  ┌─────────────┐       Socket (TCP)        ┌──────────────────────┐ │\n│  │   RLM       │◄──────────────────────────►  LMHandler           │ │\n│  │  (main)     │                           │  (ThreadingTCPServer)│ │\n│  └─────────────┘                           └──────────────────────┘ │\n│        │                                            ▲               │\n│        ▼                                            │               │\n│  ┌─────────────┐       Socket (TCP)                 │               │\n│  │ LocalREPL   │────────────────────────────────────┘               │\n│  │ (exec code) │  llm_query() / rlm_query() → LM calls               │\n│  └─────────────┘                                                    │\n└─────────────────────────────────────────────────────────────────────┘\n```\n\n### Socket Protocol (Non-Isolated Environments)\n\nNon-isolated environments like `LocalREPL` communicate directly with the `LMHandler` via TCP sockets using a length-prefixed JSON protocol:\n\n**Protocol Format**: `4-byte big-endian length prefix + UTF-8 JSON payload`\n\n```python\n# Sending a message (from rlm/core/comms_utils.py)\ndef socket_send(sock: socket.socket, data: dict) -> None:\n    payload = json.dumps(data).encode(\"utf-8\")\n    sock.sendall(struct.pack(\">I\", len(payload)) + payload)\n```\n\n**Request Flow**:\n1. Environment's `llm_query(prompt)` or `rlm_query(prompt)` is called during code execution\n2. For `llm_query`: creates `LMRequest` and calls `send_lm_request(address, request)`. For `rlm_query`: invokes `subcall_fn` to spawn a child RLM (or falls back to `llm_query` at max depth).\n3. Opens TCP connection to `LMHandler` at `(host, port)`\n4. Sends length-prefixed JSON request\n5. `LMHandler` processes via `LMRequestHandler.handle()`\n6. Returns `LMResponse` with `RLMChatCompletion` or error\n\n**Key Components**:\n- `LMHandler` (`rlm/core/lm_handler.py`): Multi-threaded TCP server wrapping LM clients\n- `LMRequest` / `LMResponse` (`rlm/core/comms_utils.py`): Typed request/response dataclasses\n- `send_lm_request()` / `send_lm_request_batched()`: Helper functions for socket communication\n\n### HTTP Broker Pattern (Isolated Environments)\n\nIsolated environments (Modal, Prime) cannot directly connect to the host's socket server. They use an HTTP broker pattern:\n\n```\n┌─────────────────────────────────────────────────────────────────────────────┐\n│  Host Machine                                                               │\n│  ┌─────────┐    Socket    ┌────────────┐    HTTP Poll    ┌────────────────┐ │\n│  │   RLM   │◄────────────►│  LMHandler │◄────────────────│   ModalREPL    │ │\n│  └─────────┘              └────────────┘                 │  (poller)      │ │\n│                                                          └────────────────┘ │\n│                                                                  │          │\n│                                                          HTTP (tunnel)      │\n│                                                                  │          │\n└──────────────────────────────────────────────────────────────────┼──────────┘\n                                                                   │\n┌──────────────────────────────────────────────────────────────────┼──────────┐\n│  Cloud Sandbox (Modal/Prime)                                     ▼          │\n│  ┌─────────────┐     HTTP (localhost)     ┌─────────────────────────────┐   │\n│  │ Exec Script │◄────────────────────────►│   Broker Server (Flask)     │   │\n│  │ (exec code) │     /enqueue, etc.       │   - /enqueue (submit req)   │   │\n│  └─────────────┘                          │   - /pending (poll reqs)    │   │\n│                                           │   - /respond (return resp)  │   │\n│                                           └─────────────────────────────┘   │\n└─────────────────────────────────────────────────────────────────────────────┘\n```\n\n**How It Works**:\n\n1. **Sandbox Setup**: Environment creates a cloud sandbox with an HTTP broker server running inside\n2. **Tunnel Exposure**: Broker server is exposed via encrypted tunnel (e.g., Modal's `encrypted_ports`)\n3. **Code Execution**: When `llm_query()` is called inside sandbox, it POSTs to `http://localhost:8080/enqueue`\n4. **Request Queuing**: Broker queues the request and blocks waiting for response\n5. **Host Polling**: `ModalREPL` on host polls `{tunnel_url}/pending` for new requests\n6. **LM Forwarding**: Host forwards requests to `LMHandler` via socket, gets response\n7. **Response Delivery**: Host POSTs response to `{tunnel_url}/respond`\n8. **Unblocking**: Broker unblocks the original `/enqueue` call with the response\n\n**Broker Endpoints**:\n| Endpoint | Method | Purpose |\n|----------|--------|---------|\n| `/enqueue` | POST | Submit LLM request from sandbox code (blocks until response) |\n| `/pending` | GET | Get list of pending requests (called by host poller) |\n| `/respond` | POST | Submit response for a request ID (called by host poller) |\n| `/health` | GET | Health check |\n\n**Key Implementation Details**:\n- Broker runs as a Flask server inside the sandbox\n- Uses `threading.Event` for request/response synchronization\n- Poller thread on host runs in background with 100ms polling interval\n- State persistence via `dill` serialization to `/tmp/rlm_state.dill`\n\n### Implementing a New Isolated Environment\n\nWhen building a new isolated environment (e.g., for a new cloud provider):\n\n1. **Create broker server** - Flask/HTTP server with `/enqueue`, `/pending`, `/respond` endpoints\n2. **Expose tunnel** - Use provider's tunnel/port forwarding to expose broker to host\n3. **Implement poller** - Background thread on host to poll and forward requests\n4. **Build exec script** - Script that runs inside sandbox with `llm_query()` calling broker\n5. **Handle state** - Serialize/deserialize execution state between code blocks\n\nSee `rlm/environments/modal_repl.py` as the canonical reference implementation.\n\n","category":"root","tokens":3429}]}