Design Patterns in RLM: A Deep Dive into the Recursive Language Model Architecture
The RLM (Recursive Language Model) library implements nine core design patterns—including Abstract Base Classes, Strategy, Adapter, and Builder—to create a modular, extensible framework for executing LLM-generated code across diverse environments.
The alexzhang13/rlm repository provides a Python framework for safely sandboxing and executing code generated by large language models. Understanding the design patterns in RLM reveals how the codebase maintains strict separation between execution environments, LLM provider implementations, and core orchestration logic while remaining highly extensible.
Environment Architecture Patterns
Template Method Pattern in BaseEnv
The BaseEnv abstract class in rlm/environments/base_env.py (lines 97-104) defines the template for all REPL-like environments. It declares three abstract methods—setup(), load_context(), and execute_code()—that concrete subclasses must implement. This guarantees a consistent public API while allowing vastly different runtimes, from local subprocesses to cloud sandboxes. The hierarchy extends through intermediate classes like IsolatedEnv and NonIsolatedEnv, but all concrete implementations follow the template established by BaseEnv.
Strategy Pattern for Execution Backends
Concrete environment classes such as LocalREPL, ModalREPL, PrimeREPL, and DockerREPL in rlm/environments/ (see local_repl.py lines 59-79) act as interchangeable strategies. Each implements the BaseEnv interface but provides distinct execution logic—local Python subprocess versus isolated Docker container versus remote Modal sandbox. The RLM core selects any environment at runtime without modifying code, adhering to the open/closed principle.
This pattern allows seamless switching between execution strategies:
from rlm import RLM
from rlm.environments import LocalREPL
# Local subprocess strategy
rlm = RLM("gpt-4", env=LocalREPL())
result = rlm.run("Calculate fibonacci(10)")
from rlm import RLM
from rlm.environments import ModalREPL
# Cloud-isolated strategy
rlm = RLM("gpt-4", env=ModalREPL(persistent=True))
answer = rlm.run("Write a Python script that scrapes a webpage.")
print(answer.final_answer)
Protocol Interfaces for Optional Capabilities
Python's Protocol objects define lightweight interfaces for optional features. In rlm/environments/base_env.py (lines 30-45), SupportsCustomTools and SupportsPersistence protocols allow the core to query capabilities via isinstance(env, SupportsCustomTools). This decouples feature detection from inheritance hierarchies, enabling duck typing for environments that support custom tool injection or state persistence without forcing all environments to inherit from a heavy base class.
Object Creation and Configuration
Factory Method for Environment Registration
The package's __init__.py registers concrete environment implementations, exposing them through clean imports like from rlm.environments import LocalREPL. This hides concrete class instantiation details from users, allowing the library to evolve by adding new environment types without breaking existing code that relies on the factory interface.
Builder Pattern for Fluent Configuration
The RLM class implements a fluent API that chains configuration calls. As shown in examples/quickstart.py (lines 1-15), users can construct complex instances through method chaining. This builder-style approach makes it easy to assemble fully-specified RLM instances with model, environment, tools, and recursion depth in a readable, step-by-step fashion.
from rlm import RLM
from rlm.environments import LocalREPL
def fetch_data(url: str) -> str:
return f"data from {url}"
custom_tools = {
"fetch_data": {"tool": fetch_data, "description": "Downloads JSON from a URL"},
"API_KEY": "sk-PLACEHOLDER"
}
rlm = (
RLM(model_name="gpt-4")
.with_env(LocalREPL(custom_tools=custom_tools))
.with_depth(2)
)
result = rlm.run(prompt="Use fetch_data to get the weather report.")
print(result.final_answer)
Communication and Coordination Patterns
Command Pattern for Request Serialization
The LMRequest and LMResponse data classes in rlm/core/comms_utils.py (lines 1-30) encapsulate LM calls as command objects. These serializable objects decouple the REPL code from the networking layer, allowing requests to be sent over sockets and handled by LMHandler. This pattern makes it straightforward to mock transports or replace communication mechanisms without affecting business logic.
Observer Pattern for Cloud Sandboxes
Isolated environments like Modal and Prime implement a publish/subscribe model through an HTTP broker. As implemented in rlm/environments/modal_repl.py (lines 150-180), the sandbox code POSTs to /enqueue, the host polls /pending, and the host POSTs results to /respond. This observer pattern keeps the sandbox (publisher) and host LM handler (subscriber) loosely coupled while guaranteeing ordered request/response flow across network boundaries.
Integration and Extension Patterns
Adapter Pattern for LLM Client Abstraction
The BaseLM abstract class in rlm/clients/base_lm.py (lines 1-25) defines a uniform interface with methods like completion(), acompletion(), and get_usage_summary(). Concrete clients in openai.py, anthropic.py, and gemini.py adapt provider-specific SDKs to this interface. This adapter pattern isolates the RLM core from vendor API quirks, enabling seamless provider swapping.
from rlm.clients.base_lm import BaseLM
import my_new_vendor_sdk as sdk
class MyVendorClient(BaseLM):
def __init__(self, api_key: str, model_name: str):
super().__init__(model_name=model_name)
self.client = sdk.Client(api_key=api_key)
def completion(self, prompt, model=None):
return self.client.generate(prompt, model=model or self.model_name)
# Register the client
from rlm.clients import register_client
register_client("myvendor", MyVendorClient)
Decorator Pattern for Runtime Tool Injection
Custom tool handling in rlm/environments/base_env.py (lines 13-27) uses a decorator-like approach to augment the REPL's global namespace. The format_tools_for_prompt and parse_custom_tools methods wrap the execution environment, injecting user-provided callables and values while protecting reserved names. This effectively decorates the runtime context without modifying the underlying interpreter.
Summary
- Template Method Pattern:
BaseEnvdefines the skeleton for environment setup, context loading, and code execution (lines 97-104), deferring implementation details to subclasses. - Strategy Pattern:
LocalREPL,ModalREPL, and other environments provide interchangeable execution strategies behind a common interface. - Protocol Interfaces:
SupportsCustomToolsandSupportsPersistence(lines 30-45) enable duck-typed capability detection without rigid inheritance. - Factory Method: Environment registration through
__init__.pyabstracts concrete class instantiation. - Builder Pattern: The
RLMclass fluent API enables readable, chained configuration of complex instances. - Command Pattern:
LMRequestandLMResponse(lines 1-30) encapsulate communication as serializable objects. - Observer Pattern: HTTP broker endpoints in
modal_repl.py(lines 150-180) implement publish/subscribe for distributed communication. - Adapter Pattern:
BaseLMsubclasses (lines 1-25) adapt vendor-specific LLM SDKs to a uniform interface. - Decorator Pattern: Runtime tool injection (lines 13-27) augments the REPL namespace without modifying core execution logic.
Frequently Asked Questions
What design pattern handles environment selection in RLM?
The Strategy Pattern enables environment selection. By implementing a common interface defined in rlm/environments/base_env.py, concrete classes like LocalREPL and ModalREPL become interchangeable. The RLM core treats all environments uniformly, allowing users to switch from local execution to cloud sandboxes by changing a single constructor call without modifying orchestration code.
How does RLM support multiple LLM providers without vendor lock-in?
The Adapter Pattern in rlm/clients/base_lm.py abstracts provider-specific implementations. Each vendor client (OpenAI, Anthropic, Gemini) inherits from BaseLM and adapts its native SDK to the uniform completion() and acompletion() interface. This allows the rest of the library to remain agnostic to API differences, making it trivial to swap providers or add new ones by implementing the adapter methods.
What enables the step-by-step configuration of RLM instances?
The Builder Pattern powers the fluent API seen in examples/quickstart.py. The RLM class returns self from methods like with_env() and with_custom_tools(), enabling method chaining. This pattern separates the construction of complex objects from their representation, making configuration readable and preventing partially-initialized instances.
How do isolated cloud environments communicate with the RLM host?
The Observer Pattern facilitates communication between isolated sandboxes (like Modal) and the host. The HTTP broker in rlm/environments/modal_repl.py implements a publish/subscribe model where the sandbox POSTs requests to /enqueue and the host polls /pending for work. This decouples the execution environment from the orchestration layer while maintaining reliable request/response ordering across network boundaries.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →