sRLM wraps a small open-source LLM in a persistent Python REPL loop where it can search a document, extract entities, and recursively spawn sub-agents to answer focused sub-questions — all without task-specific fine-tuning. It is evaluated on the RULER long-context benchmark.
sRLM has four layers:
| Layer | Module | Responsibility |
|---|---|---|
| Entry & Config | arg_utils, data_utils, logging_utils | CLI args, task data loading, structured logging |
| Agent / Model | src/models/srlm.py | Model loading, deterministic generation |
| REPL Environment | env.py | Persistent Python scope, toolset, vector index |
| Session Orchestration | src/utils/model_utils.py | Turn loop, recursion hook, guardrails |
RLMAgent (src/models/srlm.py) wraps any AutoModelForCausalLM:
agent = RLMAgent("Qwen/Qwen2.5-Coder-7B-Instruct")
response = agent.generate(prompt, max_tokens=2000, stop_sequences=["[TURN]"])
do_sample=False) for reproducibility.torch.float16 and device_map="auto".Models available via --agent:
| Key | Model ID |
|---|---|
qwen7 | Qwen/Qwen2.5-Coder-7B-Instruct |
qwen4 | Qwen/Qwen3-4B-Instruct-2507 |
mistral7 | mistralai/Mistral-7B-Instruct-v0.1 |
llama8 | meta-llama/Llama-3.1-8B-Instruct |
gemma12 | google/gemma-3-12b-it |
RLMEnvironment (env.py) is a persistent, sandboxed Python execution scope:
locals_dict — mutable state visible to the agent: P (full document text), result (answer slot), last_search, _history, _sub_history.globals_dict — Python builtins plus all pre-loaded tools.result to any non-failure value terminates the session immediately.re, json, math, collections, statistics, …) are pre-loaded so the model never needs to import them.All tools are injected into globals_dict and called directly from model-generated code — no imports needed.
Core tools (active in all modes):
| Tool | Signature | Description |
|---|---|---|
search | search(keyword) | Regex-based case-insensitive scan over P; results stored in last_search |
semantic_search | semantic_search(query) | all-MiniLM-L6-v2 top-k cosine similarity over pre-chunked P; results stored in last_search |
extract | extract(text, pattern) | Entity extraction after a pattern string: extract(t, "winner is") → "Max" |
llm_query | llm_query(question) | Spawn a recursive sub-agent session to answer a focused sub-question |
read | read(start, length=1000) | Raw character slice of P by index |
Embeddings for semantic_search are built once by the main session (SentenceTransformer('all-MiniLM-L6-v2')) and shared with sub-agents — no re-encoding on recursion.
Extended tools (available in --concise mode):
| Tool | Signature | Description |
|---|---|---|
store_finding | store_finding(content, key=None) | Persist an intermediate discovery to MemoryDB with an optional lookup key |
recall | recall(query=, category=, key=) | Query MemoryDB by content, category, or key |
In concise mode each turn receives filtered [FINDINGS], [RELEVANT_HISTORY], and [RECENT_ERRORS] blocks drawn from MemoryDB instead of the full conversation history.
run_rlm_session() (src/utils/model_utils.py) is the core control loop:
for turn in range(max_turns):
context = build_context(history | memory | summary)
response = agent.generate(context)
response = strip_hallucinated_output(response)
code = extract_code(response)
if duplicate(code): inject_error_message; continue
output = env.execute(code)
if env.locals_dict["result"] is valid: return result
if error in output: inject REFLEXION_PROMPT
return "Failure: Max turns reached"
Recursive sub-agents. When model-generated code calls llm_query(question), the REPL invokes a registered recursive_hook that launches a nested run_rlm_session with:
RLMAgent instance.max_turns=5, max_tokens=1500).max_depth).Every model response must follow this strict 3-part structure:
[REASONING]
<reflect on current state; identify next action>
[CODE]
```python
# ONE tool call or operation per turn
```
[NEXT]
<expected outcome and plan for next turn>
The loop extracts and executes only the Python inside [CODE]. One operation per turn is enforced by the system prompt.
| Mode | Flag | Context injected each turn |
|---|---|---|
| Full History | (default) | Entire accumulated conversation string |
| Concise | --concise | [MEMORY_INDEX] + [FINDINGS] + [RELEVANT_HISTORY] + [RECENT_ERRORS] |
| Concise2 | --concise2 | LLM-generated rolling [HISTORY_SUMMARY] + [LAST TURN] |
All reported results use Full History (default).
| Mechanism | Description |
|---|---|
| Hallucination stripping | strip_hallucinated_output() truncates at the end of the first [NEXT] section, removing any fake [OUTPUT]/[STATE] the model predicts |
| Duplicate code detection | Last 3 executed blocks are stored; an identical repeat triggers a forced strategy-change message |
| Failure marker filtering | result values matching patterns like "none", "not found", "unable to" are cleared; session continues |
| Recursion depth cap | llm_query() returns a system message instead of spawning when current_depth >= max_depth |
| Error reflexion | Python execution errors inject REFLEXION_PROMPT to guide the model toward a fix in the next turn |
Evaluated on RULER — 13 synthetic long-context tasks, 500 validation samples each, 4K token sequences. Metric: string-match accuracy.
Systems: sRLM with Qwen2.5-Coder-7B-Instruct vs. direct-prompting baselines with the same model and Llama-3.1-8B-Instruct.
| Task | sRLM (Qwen-7B) |
|---|---|
| niah_single_1 | 94.2 |
| niah_single_2 | 94.4 |
| niah_single_3 | 82.4 |
| niah_multikey_1 | 93.8 |
| niah_multikey_2 | 95.6 |
| niah_multikey_3 | 76.0 |
| niah_multivalue | 43.3 |
| niah_multiquery | 89.75 |
| vt | 7.08 |
| cwe | 0.78 |
| fwe | 47.6 |
| qa_1 | 16.09 |
| qa_2 | 22.2 |
pip install torch transformers sentence-transformers
# Run sRLM on a built-in task
python main.py --agent qwen7 --task RNIAH --max_turns 10 --max_tokens 2000
# Run against RULER (SLURM array job, one task per job)
sbatch scripts/run_ruler_srlm_array.sh
# Run baseline
sbatch scripts/run_ruler_baseline_array.sh
# Evaluate predictions
python eval/evaluate.py --data_dir eval_results/sRLM/preds_agent_qwen7 --benchmark synthetic
CLI arguments:
| Argument | Default | Description |
|---|---|---|
--agent | qwen7 | Model key (see table above) |
--task | RNIAH | SNIAH |
--max_turns | 10 | Turn limit per session |
--max_tokens | 2000 | Max generated tokens per turn |
--max_depth | 2 | Recursion depth cap for llm_query() |
--concise | off | Enable memory-filtered history mode |
--concise2 | off | Enable rolling LLM-summary history mode |
@misc{srlm2026,
title = {Small Recursive Language Models: A Framework for Tool-Augmented
Recursive Reasoning in Long-Context {NLP} Tasks},
year = {2026},
url = {[https://github.com/anishdash4/Small-Recursive-Language-Models](https://github.com/anishdash4/Small-Recursive-Language-Models)}
}
14 commits
Python
100.0%
sRLM wraps a small open-source LLM in a persistent Python REPL loop where it can search a document, extract entities, and recursively spawn sub-agents to answer focused sub-questions — all without task-specific fine-tuning. It is evaluated on the RULER long-context benchmark.
sRLM has four layers:
| Layer | Module | Responsibility |
|---|---|---|
| Entry & Config | arg_utils, data_utils, logging_utils | CLI args, task data loading, structured logging |
| Agent / Model | src/models/srlm.py | Model loading, deterministic generation |
| REPL Environment | env.py | Persistent Python scope, toolset, vector index |
| Session Orchestration | src/utils/model_utils.py | Turn loop, recursion hook, guardrails |
RLMAgent (src/models/srlm.py) wraps any AutoModelForCausalLM:
agent = RLMAgent("Qwen/Qwen2.5-Coder-7B-Instruct")
response = agent.generate(prompt, max_tokens=2000, stop_sequences=["[TURN]"])
do_sample=False) for reproducibility.torch.float16 and device_map="auto".Models available via --agent:
| Key | Model ID |
|---|---|
qwen7 | Qwen/Qwen2.5-Coder-7B-Instruct |
qwen4 | Qwen/Qwen3-4B-Instruct-2507 |
mistral7 | mistralai/Mistral-7B-Instruct-v0.1 |
llama8 | meta-llama/Llama-3.1-8B-Instruct |
gemma12 | google/gemma-3-12b-it |
RLMEnvironment (env.py) is a persistent, sandboxed Python execution scope:
locals_dict — mutable state visible to the agent: P (full document text), result (answer slot), last_search, _history, _sub_history.globals_dict — Python builtins plus all pre-loaded tools.result to any non-failure value terminates the session immediately.re, json, math, collections, statistics, …) are pre-loaded so the model never needs to import them.All tools are injected into globals_dict and called directly from model-generated code — no imports needed.
Core tools (active in all modes):
| Tool | Signature | Description |
|---|---|---|
search | search(keyword) | Regex-based case-insensitive scan over P; results stored in last_search |
semantic_search | semantic_search(query) | all-MiniLM-L6-v2 top-k cosine similarity over pre-chunked P; results stored in last_search |
extract | extract(text, pattern) | Entity extraction after a pattern string: extract(t, "winner is") → "Max" |
llm_query | llm_query(question) | Spawn a recursive sub-agent session to answer a focused sub-question |
read | read(start, length=1000) | Raw character slice of P by index |
Embeddings for semantic_search are built once by the main session (SentenceTransformer('all-MiniLM-L6-v2')) and shared with sub-agents — no re-encoding on recursion.
Extended tools (available in --concise mode):
| Tool | Signature | Description |
|---|---|---|
store_finding | store_finding(content, key=None) | Persist an intermediate discovery to MemoryDB with an optional lookup key |
recall | recall(query=, category=, key=) | Query MemoryDB by content, category, or key |
In concise mode each turn receives filtered [FINDINGS], [RELEVANT_HISTORY], and [RECENT_ERRORS] blocks drawn from MemoryDB instead of the full conversation history.
run_rlm_session() (src/utils/model_utils.py) is the core control loop:
for turn in range(max_turns):
context = build_context(history | memory | summary)
response = agent.generate(context)
response = strip_hallucinated_output(response)
code = extract_code(response)
if duplicate(code): inject_error_message; continue
output = env.execute(code)
if env.locals_dict["result"] is valid: return result
if error in output: inject REFLEXION_PROMPT
return "Failure: Max turns reached"
Recursive sub-agents. When model-generated code calls llm_query(question), the REPL invokes a registered recursive_hook that launches a nested run_rlm_session with:
RLMAgent instance.max_turns=5, max_tokens=1500).max_depth).Every model response must follow this strict 3-part structure:
[REASONING]
<reflect on current state; identify next action>
[CODE]
```python
# ONE tool call or operation per turn
```
[NEXT]
<expected outcome and plan for next turn>
The loop extracts and executes only the Python inside [CODE]. One operation per turn is enforced by the system prompt.
| Mode | Flag | Context injected each turn |
|---|---|---|
| Full History | (default) | Entire accumulated conversation string |
| Concise | --concise | [MEMORY_INDEX] + [FINDINGS] + [RELEVANT_HISTORY] + [RECENT_ERRORS] |
| Concise2 | --concise2 | LLM-generated rolling [HISTORY_SUMMARY] + [LAST TURN] |
All reported results use Full History (default).
| Mechanism | Description |
|---|---|
| Hallucination stripping | strip_hallucinated_output() truncates at the end of the first [NEXT] section, removing any fake [OUTPUT]/[STATE] the model predicts |
| Duplicate code detection | Last 3 executed blocks are stored; an identical repeat triggers a forced strategy-change message |
| Failure marker filtering | result values matching patterns like "none", "not found", "unable to" are cleared; session continues |
| Recursion depth cap | llm_query() returns a system message instead of spawning when current_depth >= max_depth |
| Error reflexion | Python execution errors inject REFLEXION_PROMPT to guide the model toward a fix in the next turn |
Evaluated on RULER — 13 synthetic long-context tasks, 500 validation samples each, 4K token sequences. Metric: string-match accuracy.
Systems: sRLM with Qwen2.5-Coder-7B-Instruct vs. direct-prompting baselines with the same model and Llama-3.1-8B-Instruct.
| Task | sRLM (Qwen-7B) |
|---|---|
| niah_single_1 | 94.2 |
| niah_single_2 | 94.4 |
| niah_single_3 | 82.4 |
| niah_multikey_1 | 93.8 |
| niah_multikey_2 | 95.6 |
| niah_multikey_3 | 76.0 |
| niah_multivalue | 43.3 |
| niah_multiquery | 89.75 |
| vt | 7.08 |
| cwe | 0.78 |
| fwe | 47.6 |
| qa_1 | 16.09 |
| qa_2 | 22.2 |
pip install torch transformers sentence-transformers
# Run sRLM on a built-in task
python main.py --agent qwen7 --task RNIAH --max_turns 10 --max_tokens 2000
# Run against RULER (SLURM array job, one task per job)
sbatch scripts/run_ruler_srlm_array.sh
# Run baseline
sbatch scripts/run_ruler_baseline_array.sh
# Evaluate predictions
python eval/evaluate.py --data_dir eval_results/sRLM/preds_agent_qwen7 --benchmark synthetic
CLI arguments:
| Argument | Default | Description |
|---|---|---|
--agent | qwen7 | Model key (see table above) |
--task | RNIAH | SNIAH |
--max_turns | 10 | Turn limit per session |
--max_tokens | 2000 | Max generated tokens per turn |
--max_depth | 2 | Recursion depth cap for llm_query() |
--concise | off | Enable memory-filtered history mode |
--concise2 | off | Enable rolling LLM-summary history mode |
@misc{srlm2026,
title = {Small Recursive Language Models: A Framework for Tool-Augmented
Recursive Reasoning in Long-Context {NLP} Tasks},
year = {2026},
url = {[https://github.com/anishdash4/Small-Recursive-Language-Models](https://github.com/anishdash4/Small-Recursive-Language-Models)}
}
14 commits
Python
100.0%