Hello folks - this repo shows a practical, from-scratch implementation of a Recursive Language Model inspired by:
- RLM paper: https://arxiv.org/abs/2512.24601
- Python package: https://pypi.org/project/simple-agents-py/
- Docs: https://docs.simpleagents.craftsmanlabs.net/
The goal is simple: keep long context outside the one-shot prompt, reason with executable steps, and still keep workflow orchestration and traceability.
rlm_runner.py- core recursive loop (parsereplcode, execute, finalize)handlers.py- tool handlers (execute_repl,llm_query) used by workflowllm_callnodesrlm_orchestrator.yaml- llm_call + switch orchestrator with tool-callingtraditional_orchestrator.yaml- traditional one-shot llm_call baseline workflowrun.py- CLI runner (directorworkflowmode)benchmark_rlm.py- benchmark harness (baseline vs RLM)tests/- smoke + utility tests
flowchart TD
A[Query and Long Context] --> B["RLM Root Loop<br/>model to repl code to execute"]
B --> C[llm_query sub-call optional]
B --> D[FINAL or FINAL_VAR]
C --> B
D --> E[Final answer + trace]
F[run.py] --> G[run_workflow_yaml]
G --> H[rlm_orchestrator.yaml]
H --> I[llm_call with tools]
I --> J[execute_repl and llm_query handlers]
As per the paper, this implementation keeps the prompt data as external state (context variable), and lets the model work over it through symbolic computation (repl code blocks), instead of forcing all reasoning into one token stream.
uv initIn pyproject.toml, add:
simple-agents-pypython-dotenvpyyamlpytest(dev)
Then install:
uv sync
uv sync --extra devcp .env.example .envSet these vars:
WORKFLOW_PROVIDER=openaiWORKFLOW_API_BASE=<openai-compatible-base-url>WORKFLOW_API_KEY=<api-key>WORKFLOW_MODEL=gemini-3-flash
Core flow:
- Build a root prompt with query + context metadata.
- Ask model for output containing
```repl ... ```blocks. - Execute those blocks in controlled REPL scope.
- Detect finalization via
FINAL(...)orFINAL_VAR(...). - Save per-turn traces (
code,stdout,error, termination reason).
In rlm_orchestrator.yaml:
- Use
llm_callnodes withoutput_schema - Use
switchrouting for deterministic branching - Add tool declarations for
execute_replandllm_query
In handlers.py:
- Implement tool handlers with deterministic signatures:
execute_repl(...)for sandboxed code execution over statellm_query(...)for concise semantic sub-calls
--mode direct: run runner directly--mode workflow: run through workflow engine- support
--query,--context,--context-file, and optional trace path
- benchmark compares one-shot truncated baseline vs RLM methods
- tests validate parser/utilities and smoke-path behavior
uv sync
uv sync --extra devuv run python run.py \
--mode workflow \
--query "Summarize this context in one sentence." \
--context "RLM keeps context external and reasons with code."uv run python run.py \
--mode direct \
--query "Summarize in 2 bullets" \
--context "Your long context here"uv run pytest -quv run python benchmark_rlm.py \
--records 400 \
--baseline-context-chars 2000 \
--output benchmark_report.jsonCheck these proof signals:
accuracyby methodavg_turns,avg_subcalls- per-task
trace,termination_reason,correct
Compared methods include:
baseline_direct_truncated(direct one-shot with truncated context)traditional_workflow_truncated(YAML one-shot workflow baseline)rlm_direct(RLM runner)rlm_workflow(RLM via YAML llm_call + tool calls)
Here are my real takeaways:
-
External state is the key difference
Bigger context windows help, but external memory + symbolic steps give better control. -
Finalization contract is very important
Models may compute correctly in code but phrase final text loosely. Trace-based checks keep outputs reliable. -
Workflow + RLM is a strong combo
Workflow provides orchestration and guardrails; RLM provides flexible decomposition. -
Limits are non-negotiable
max_turns,max_subcalls, and safe REPL scope are essential for production safety. -
Measure first, then scale
Get benchmark harness in place early, then improve with async subcalls, deeper recursion, and tougher tasks.
