recursive-llm 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,733 @@
1
+ Metadata-Version: 2.4
2
+ Name: recursive-llm
3
+ Version: 0.5.0
4
+ Summary: Recursive Language Models for efficient long-context processing
5
+ Author: Grigori Gvadzabia
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/grishahq/recursive-llm
8
+ Project-URL: Documentation, https://github.com/grishahq/recursive-llm
9
+ Project-URL: Repository, https://github.com/grishahq/recursive-llm
10
+ Project-URL: Issues, https://github.com/grishahq/recursive-llm/issues
11
+ Project-URL: Changelog, https://github.com/grishahq/recursive-llm/blob/main/CHANGELOG.md
12
+ Keywords: llm,ai,nlp,recursive,language-models
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: litellm<1.98.0,>=1.0.0
24
+ Requires-Dist: RestrictedPython>=6.0
25
+ Requires-Dist: python-dotenv>=1.0.0
26
+ Provides-Extra: visualizer
27
+ Requires-Dist: pypdf<7,>=6.18.0; extra == "visualizer"
28
+ Provides-Extra: dev
29
+ Requires-Dist: build>=1.2.0; extra == "dev"
30
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
31
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
32
+ Requires-Dist: pytest-cov>=7.0.0; extra == "dev"
33
+ Requires-Dist: coverage[toml]>=7.10.0; extra == "dev"
34
+ Requires-Dist: black==25.11.0; extra == "dev"
35
+ Requires-Dist: ruff==0.16.0; extra == "dev"
36
+ Requires-Dist: mypy<2.0.0,>=1.0.0; extra == "dev"
37
+ Dynamic: license-file
38
+
39
+ # Recursive Language Models (RLM)
40
+
41
+ ![Recursive Language Models — large-context analysis through recursive selection](https://github.com/user-attachments/assets/c1c629fc-90ce-4c95-907e-0fd161628920)
42
+
43
+ [![CI](https://github.com/grishahq/recursive-llm/actions/workflows/ci.yml/badge.svg)](https://github.com/grishahq/recursive-llm/actions/workflows/ci.yml)
44
+ [![Python 3.9-3.12](https://img.shields.io/badge/python-3.9--3.12-3776AB.svg)](https://www.python.org/)
45
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/grishahq/recursive-llm/blob/main/LICENSE)
46
+ [![Latest release](https://img.shields.io/github/v/release/grishahq/recursive-llm)](https://github.com/grishahq/recursive-llm/releases/latest)
47
+
48
+ Analyze large contexts with bounded cost and inspectable execution. Instead of placing the full
49
+ source in model prompts, RLM keeps it in a restricted Python REPL, where the model can search,
50
+ compute, partition, and send only selected sections to language-model calls.
51
+
52
+ This is an independent Python implementation of the
53
+ [Recursive Language Models paper](https://arxiv.org/abs/2512.24601), focused on practical library
54
+ use: provider portability, tree-wide budgets, structured failures, reproducible benchmarks, and
55
+ complete run trajectories.
56
+
57
+ [Quick start](#quick-start) | [Visual comparison](#visual-comparison) | [Measured results](#measured-results) |
58
+ [Document-format evaluation](https://github.com/grishahq/recursive-llm/blob/main/DOCUMENT_EVALUATION.md) | [Configuration](#configuration) |
59
+ [Security](https://github.com/grishahq/recursive-llm/blob/main/SECURITY.md) |
60
+ [Original implementation](https://github.com/alexzhang13/rlm)
61
+
62
+ ## Why use this implementation?
63
+
64
+ | Capability | What it provides |
65
+ | --- | --- |
66
+ | Externalized context | Large source text stays in the REPL instead of being repeated in model prompts |
67
+ | Bounded execution | Tree-wide limits for calls, tokens, estimated cost, elapsed time, and local execution |
68
+ | Observable runs | Per-run statistics, typed failures, versioned JSONL records, and complete trajectories |
69
+ | Provider portability | OpenAI, Anthropic, DeepSeek, local models, and other LiteLLM providers |
70
+ | Reproducible evaluation | Exact graders, generated corpora, pinned public documents, and repeated live runs |
71
+ | Restricted execution | Spawned RestrictedPython worker with hard timeouts and optional POSIX resource limits |
72
+
73
+ ## Visual comparison
74
+
75
+ To celebrate 600 stars, the project includes a [side-by-side visualizer](https://github.com/grishahq/recursive-llm/blob/main/visualizer/README.md) of
76
+ real RLM and direct-model runs: synchronized replay, Python steps, call trees, exact grading, and
77
+ exportable recordings. Both sides use GPT-5.6 Luna through a ChatGPT-authenticated Codex CLI.
78
+
79
+ [![RLM and direct LLM comparison](https://raw.githubusercontent.com/grishahq/recursive-llm/main/.github/assets/rlm-comparison.gif)](https://github.com/grishahq/recursive-llm/blob/main/visualizer/public/rlm-comparison.mp4)
80
+
81
+ On one 100k-character paired experiment, both answers were exact. RLM used 11,656 total CLI model
82
+ tokens versus 37,521 directly, with elapsed times of 30.7 and 29.0 seconds respectively. The 10k
83
+ experiment favored direct completion. See [CODEX_COMPARISON.md](https://github.com/grishahq/recursive-llm/blob/main/CODEX_COMPARISON.md) for both
84
+ outcomes, raw recordings, methodology, and reproduction commands. These are individual experiments,
85
+ not a general performance claim or a measurement of subscription credits saved.
86
+
87
+ ```bash
88
+ # Uses the existing ChatGPT login and consumes Codex subscription allowance
89
+ python examples/capture_codex_comparison.py --chars 100000 --output comparison.json
90
+ ```
91
+
92
+ To test another model through the same transport, pass its settings explicitly:
93
+
94
+ ```bash
95
+ python examples/capture_codex_comparison.py --model gpt-6.1-sol \
96
+ --reasoning-effort low --max-depth 1 --chars 100000 --output comparison-sol.json
97
+ ```
98
+
99
+ The web companion keeps the historical Luna/medium/depth-1 defaults. Imported captures record
100
+ their own model, reasoning effort, and depth; changing depth enables subcalls but does not force
101
+ the model to use them.
102
+
103
+ ## Measured results
104
+
105
+ The exact 100k-character aggregation benchmark used one generated corpus seed and three live runs
106
+ per configuration. On this task, RLM improved exact correctness while reducing model-token usage.
107
+
108
+ ![Direct and RLM results on the generated 100k-character benchmark](https://raw.githubusercontent.com/grishahq/recursive-llm/main/.github/assets/benchmark-100k.svg)
109
+
110
+ | Model and mode | Exact passes | Mean model tokens | Mean estimated cost |
111
+ | --- | ---: | ---: | ---: |
112
+ | GPT-5 mini, direct | 0/3 | 37,928 | $0.0088788 |
113
+ | GPT-5 mini, RLM | 3/3 | 8,224 | $0.0048132 |
114
+ | DeepSeek V4 Flash, direct | 0/3 | 39,364 | $0.0012240 |
115
+ | DeepSeek V4 Flash, RLM | 2/3 | 15,209 | $0.0010156 |
116
+
117
+ These measurements are an engineering check, not a paper reproduction or a universal quality
118
+ claim. Direct completion was faster and cheaper on short tasks. A separate 1M-character RLM scale
119
+ check passed 6/6 exact-graded runs, but those runs used local REPL computation without child RLMs.
120
+ See [BENCHMARK_RESULTS.md](https://github.com/grishahq/recursive-llm/blob/main/BENCHMARK_RESULTS.md) for methodology, raw summaries, public-document
121
+ experiments, limitations, and reproduction commands.
122
+ The separate [document-format evaluation](https://github.com/grishahq/recursive-llm/blob/main/DOCUMENT_EVALUATION.md) compares RLM with direct
123
+ completion on a SHA-pinned TXT book, PDF, CSV, and HTML documentation page and records two
124
+ reliability/performance changes evaluated one at a time.
125
+
126
+ Historical transaction and literary scores used legacy graders. Current graders reject ambiguous
127
+ or contradictory fields and record their version; see the grading-version note in
128
+ [BENCHMARK_RESULTS.md](https://github.com/grishahq/recursive-llm/blob/main/BENCHMARK_RESULTS.md) before comparing old and new runs.
129
+
130
+ ## When to use RLM
131
+
132
+ RLM is a good fit when:
133
+
134
+ - the source is too large or too expensive to place in a normal model prompt;
135
+ - the task benefits from search, filtering, exact counting, or local Python computation;
136
+ - selected sections can be delegated to model calls instead of repeatedly sending the full source;
137
+ - call, token, cost, and time limits must apply to the whole recursive run.
138
+
139
+ Prefer direct completion when the context fits comfortably in the model window and the task is
140
+ small. Semantic synthesis across long narrative text remains difficult and should be evaluated on
141
+ your own data before production use.
142
+
143
+ ## Execution at a glance
144
+
145
+ ```text
146
+ query -> root model -> Python REPL over full context -> selected chunks or child calls -> answer
147
+ | |
148
+ +-- local search/compute +-- shared budgets and trajectory
149
+ ```
150
+
151
+ The root model receives the query and instructions, while the source is exposed as the `context`
152
+ variable. REPL state persists across iterations. The model can inspect small regions, perform local
153
+ computation, call a plain LM, or create a child RLM when the configured depth permits it.
154
+
155
+ ## Installation
156
+
157
+ Use a fresh virtual environment (recommended), then install the latest release from PyPI:
158
+
159
+ ```bash
160
+ pip install recursive-llm
161
+ ```
162
+
163
+ `recursive-llm` and `rlms` both provide the `rlm` import namespace. Install only one of them in a
164
+ given environment.
165
+
166
+ For development, install from source:
167
+
168
+ ```bash
169
+ git clone https://github.com/grishahq/recursive-llm.git
170
+ cd recursive-llm
171
+ pip install -e .
172
+ ```
173
+
174
+ Or install the current GitHub version directly:
175
+
176
+ ```bash
177
+ pip install "recursive-llm @ git+https://github.com/grishahq/recursive-llm.git"
178
+ ```
179
+
180
+ ## Requirements
181
+
182
+ - Python 3.9 or higher
183
+ - An API key for your chosen LLM provider (OpenAI, Anthropic, etc.)
184
+ - Or a local model setup (Ollama, llama.cpp, etc.)
185
+
186
+ ## Quick Start
187
+
188
+ Set the provider key, create `quickstart.py`, and point it at a UTF-8 text file:
189
+
190
+ ```bash
191
+ export OPENAI_API_KEY="sk-..."
192
+ ```
193
+
194
+ ```python
195
+ from pathlib import Path
196
+
197
+ from rlm import RLM
198
+
199
+
200
+ def main():
201
+ rlm = RLM(model="gpt-5-mini")
202
+
203
+ result = rlm.complete_result(
204
+ query="What are the main themes in this document?",
205
+ context=Path("document.txt").read_text(encoding="utf-8"),
206
+ )
207
+ print(result.answer)
208
+ print(result.stats)
209
+
210
+
211
+ if __name__ == "__main__":
212
+ main()
213
+ ```
214
+
215
+ ```bash
216
+ python quickstart.py
217
+ ```
218
+
219
+ RLM uses a spawned worker process for isolated REPL execution. Executable Python scripts must use
220
+ the standard `if __name__ == "__main__":` entry-point guard, as shown above. This is required by
221
+ Python multiprocessing on spawn-based platforms; all repository examples follow this pattern.
222
+
223
+ ### Usage and Cost Statistics
224
+
225
+ `RLM.stats` aggregates model calls across the complete recursion tree. Token usage comes from
226
+ provider responses, while cost is calculated on a best-effort basis using LiteLLM's model pricing
227
+ metadata.
228
+
229
+ ```python
230
+ rlm = RLM(
231
+ model="gpt-5-mini",
232
+ recursive_model="deepseek/deepseek-v4-flash",
233
+ )
234
+ result = rlm.complete(query="Summarize this", context=document)
235
+
236
+ print(rlm.stats)
237
+ # {
238
+ # "llm_calls": 11,
239
+ # "root_calls": 3,
240
+ # "recursive_calls": 8,
241
+ # "leaf_calls": 4,
242
+ # "prompt_tokens": 12500,
243
+ # "completion_tokens": 3200,
244
+ # "cached_tokens": 6000,
245
+ # "estimated_cost_usd": 0.0047,
246
+ # "by_model": {
247
+ # "gpt-5-mini": {"calls": 3, ...},
248
+ # "deepseek/deepseek-v4-flash": {"calls": 8, ...},
249
+ # },
250
+ # }
251
+ ```
252
+
253
+ Each root completion receives fresh statistics, so they describe one recursion tree rather than
254
+ lifetime usage.
255
+ `estimated_cost_usd` is `None` when LiteLLM has no pricing metadata for any completed call. Compare
256
+ `priced_calls` with `llm_calls` before treating the estimate as the full run cost.
257
+
258
+ When the same `RLM` instance runs concurrent completions, `RLM.stats` describes whichever root run
259
+ completed most recently. Use the structured result API for exact per-run statistics and trajectory:
260
+
261
+ ```python
262
+ result = rlm.complete_result(query="Summarize this", context=document)
263
+ print(result.answer)
264
+ print(result.stats)
265
+ for event in result.trajectory:
266
+ print(event.kind, event.depth, event.node_id, event.parent_id)
267
+
268
+ # Append one complete, versioned run record for later comparison
269
+ result.write_jsonl("runs.jsonl")
270
+ ```
271
+
272
+ `acomplete_result` is the asynchronous equivalent. Trajectories include the complete root, child
273
+ RLM, and leaf-call tree. Query, context, model response, code, and output content are represented by
274
+ character counts by default. Set `capture_trajectory_content=True` only when the resulting logs are
275
+ allowed to contain that data. An optional `event_handler` receives events as they occur; handler
276
+ failures do not interrupt model completion.
277
+
278
+ Use the non-raising result API when failed runs must be persisted or compared alongside successful
279
+ runs:
280
+
281
+ ```python
282
+ run = rlm.try_complete_result(query="Summarize this", context=document)
283
+ if run.succeeded:
284
+ print(run.answer)
285
+ else:
286
+ print(run.error_type, run.error)
287
+
288
+ # Success and failure records share the same versioned JSONL schema.
289
+ run.write_jsonl("runs.jsonl")
290
+ ```
291
+
292
+ `atry_complete_result` is the asynchronous equivalent. Ordinary run failures return a
293
+ `FailedCompletionResult` with exact per-run statistics, secret-free configuration, and the complete
294
+ partial trajectory, including its terminal `run_error` event. Cancellation and other process-control
295
+ exceptions are not converted. The existing `complete`, `acomplete`, `complete_result`, and
296
+ `acomplete_result` APIs keep their exception behavior.
297
+
298
+ Completed JSONL records contain the final answer; failed records contain `answer: null` and a typed
299
+ error. Query, context, intermediate model responses, code, and REPL output follow the same redaction
300
+ setting as the in-memory trajectory.
301
+
302
+ ### Live Model Comparison
303
+
304
+ The comparison script uses the same model for both root and recursive calls. It runs one small
305
+ recursive smoke test by default and reports latency, calls, tokens, and estimated cost:
306
+
307
+ ```bash
308
+ python benchmarks/compare_same_model.py gpt-5-mini
309
+ python benchmarks/compare_same_model.py deepseek/deepseek-v4-flash
310
+ ```
311
+
312
+ Use repeated runs and save raw records before comparing configurations:
313
+
314
+ ```bash
315
+ python benchmarks/compare_same_model.py gpt-5-mini --full --runs 3 --jsonl results.jsonl
316
+ python benchmarks/compare_same_model.py gpt-5-mini --full --runs 3 --mode direct
317
+ python benchmarks/compare_same_model.py gpt-5-mini --generated-chars 100000 --seed 2026
318
+ ```
319
+
320
+ `--max-depth` compares recursion capabilities; `--mode direct` sends task and context in one normal
321
+ long-context model request. The script reports pass rate, p50/p95 latency, calls, tokens, and
322
+ best-effort cost. Task-specific graders require exact IDs, numeric boundaries, and explicit labeled
323
+ counts rather than accepting arbitrary substrings. Live benchmarks make paid API calls and require
324
+ the corresponding provider keys. `--trace` includes sensitive content-bearing trajectories in the
325
+ JSON output and should be used deliberately.
326
+
327
+ The repository also includes a SHA-pinned real-document benchmark over the public-domain English
328
+ translation of *War and Peace*. The book itself is downloaded separately and is not committed:
329
+
330
+ ```bash
331
+ curl -L https://www.gutenberg.org/files/2600/2600-0.txt \
332
+ -o /tmp/war-and-peace-2600-0.txt
333
+ python benchmarks/war_and_peace.py gpt-5-mini /tmp/war-and-peace-2600-0.txt
334
+ python benchmarks/war_and_peace.py deepseek/deepseek-v4-flash \
335
+ /tmp/war-and-peace-2600-0.txt
336
+ ```
337
+
338
+ `benchmarks/multi_document.py` extends this to three independently sourced large corpora: *War and
339
+ Peace*, the official 9/11 Commission Report, and the official Python 3.14 text documentation. It
340
+ verifies every prepared artifact by SHA-256 and uses exact labeled-field graders. Download and PDF
341
+ extraction commands, hashes, task definitions, and measured candidate results are in
342
+ [`BENCHMARK_RESULTS.md`](https://github.com/grishahq/recursive-llm/blob/main/BENCHMARK_RESULTS.md).
343
+
344
+ ## API Keys Setup
345
+
346
+ Copy the example environment file and add keys only for the providers you use:
347
+
348
+ ```bash
349
+ cp .env.example .env
350
+ ```
351
+
352
+ ```dotenv
353
+ OPENAI_API_KEY=sk-...
354
+ DEEPSEEK_API_KEY=...
355
+ MOONSHOT_API_KEY=...
356
+ ```
357
+
358
+ Use the LiteLLM provider prefix for non-OpenAI models, for example
359
+ `deepseek/deepseek-v4-flash` or `moonshot/kimi-k2.6`. This lets a hybrid RLM select the correct API
360
+ key for each model automatically.
361
+
362
+ Or pass directly in code:
363
+ ```python
364
+ rlm = RLM(model="gpt-5-mini", api_key="sk-...")
365
+ ```
366
+
367
+ ## Supported Models
368
+
369
+ Works with 100+ LLM providers via LiteLLM:
370
+
371
+ ```python
372
+ # OpenAI
373
+ rlm = RLM(model="gpt-5")
374
+ rlm = RLM(model="gpt-5-mini")
375
+
376
+ # Anthropic
377
+ rlm = RLM(model="claude-sonnet-4")
378
+ rlm = RLM(model="claude-sonnet-4-20250514")
379
+
380
+ # Ollama (local)
381
+ rlm = RLM(model="ollama/llama3.2")
382
+ rlm = RLM(model="ollama/mistral")
383
+
384
+ # llama.cpp (local)
385
+ rlm = RLM(
386
+ model="openai/local",
387
+ api_base="http://localhost:8000/v1"
388
+ )
389
+
390
+ # Azure OpenAI
391
+ rlm = RLM(model="azure/gpt-4-deployment")
392
+
393
+ # And many more via LiteLLM...
394
+ ```
395
+
396
+ ## Advanced Usage
397
+
398
+ ### Two Models (Optimize Cost)
399
+
400
+ Use a cheaper model for recursive calls:
401
+
402
+ ```python
403
+ rlm = RLM(
404
+ model="gpt-5", # Root LM (main decisions)
405
+ recursive_model="gpt-5-mini" # Recursive calls (cheaper)
406
+ )
407
+ ```
408
+
409
+ ### Async API
410
+
411
+ For better performance with parallel recursive calls:
412
+
413
+ ```python
414
+ import asyncio
415
+
416
+ async def main():
417
+ rlm = RLM(model="gpt-5-mini")
418
+ result = await rlm.acomplete(query, context)
419
+ print(result)
420
+
421
+ if __name__ == "__main__":
422
+ asyncio.run(main())
423
+ ```
424
+
425
+ ### Configuration
426
+
427
+ ```python
428
+ rlm = RLM(
429
+ model="gpt-5-mini",
430
+ max_depth=2, # One child RLM level, then a plain-LM fallback
431
+ max_iterations=20, # Maximum REPL iterations per RLM
432
+ repl_timeout=5, # Hard timeout for each local Python step
433
+ max_output_chars=2000, # Observation truncation limit
434
+ repl_max_snapshot_bytes=1_000_000, # Parent-copy cap; full state stays in the worker
435
+ max_concurrent_subcalls=4, # Bound batch concurrency
436
+ max_total_calls=24, # Exact provider-call cap for the full recursion tree
437
+ max_total_tokens=100_000, # Stop after reported usage crosses this value
438
+ max_total_cost_usd=0.10, # Stop after reported cost crosses this value
439
+ max_elapsed_seconds=300, # Deadline shared by root and child calls
440
+ max_retries=2, # Retry transient provider failures; default is 0
441
+ retry_backoff_seconds=1.0, # Exponential retry delay; Retry-After is respected
442
+ # Optional LiteLLM params: temperature, timeout, etc.
443
+ )
444
+ ```
445
+
446
+ Call limits are reserved atomically before provider requests, including batched and recursive
447
+ subcalls. Token and cost limits are evaluated after each response because providers only report
448
+ those values after generation; the crossing response is included in partial statistics attached to
449
+ `BudgetExceededError`. A deadline also bounds in-flight provider requests and REPL exchanges, and
450
+ prevents a validator result completed after the deadline from being accepted. A synchronous
451
+ validator cannot be forcibly stopped safely, so slow validator code can delay delivery of the
452
+ resulting `BudgetExceededError`; validators should remain fast and independently bounded. All
453
+ limits are optional and are reset for each root completion.
454
+
455
+ Retries are opt-in and share the same call and elapsed-time budgets as the rest of the recursion
456
+ tree. Every retry is counted as a provider call and recorded in statistics and trajectories.
457
+ RLM disables hidden LiteLLM retries to preserve exact accounting; configure `max_retries` on `RLM`
458
+ instead of passing `num_retries` or `max_retries` as a LiteLLM option. Null provider content is
459
+ normalized to an empty response so the existing repair iteration can recover, while malformed
460
+ response structures fail with `ProviderResponseError` or use the bounded retry path when enabled.
461
+
462
+ ### Final Answer Validation
463
+
464
+ Applications can reject a syntactically valid final answer and give deterministic feedback to the
465
+ model without leaving the bounded run:
466
+
467
+ ```python
468
+ def validate_answer(answer: str):
469
+ if not answer.startswith("count="):
470
+ return "Answer must start with 'count='."
471
+ return None
472
+
473
+ rlm = RLM(
474
+ model="gpt-5-mini",
475
+ final_answer_validator=validate_answer,
476
+ )
477
+ ```
478
+
479
+ The validator returns `None` to accept an answer or a non-empty error string to reject it. Rejected
480
+ answers and feedback are represented by lengths in redacted trajectories and by content only when
481
+ `capture_trajectory_content=True`. Validator exceptions abort the run as application errors.
482
+
483
+ `max_depth` is an explicit constructor option so that runs remain reproducible. It is not read from
484
+ an environment variable by the library. Applications may map their own configuration or environment
485
+ variables to this argument.
486
+
487
+ | `max_depth` | Behavior |
488
+ | ---: | --- |
489
+ | `0` | Root RLM and REPL only; no LM subcalls |
490
+ | `1` (default) | Root RLM may call a plain LM |
491
+ | `2` | Root RLM may create one child RLM; the child falls back to a plain LM |
492
+ | `n` | Adds one child RLM level for every increment above `1` |
493
+
494
+ This follows the paper's capability-based depth convention. The root RLM itself is depth `0` and is
495
+ still valid when `max_depth=0`.
496
+
497
+ ### REPL Subcall API
498
+
499
+ The model can use these functions from its persistent REPL:
500
+
501
+ ```python
502
+ # One plain LM call, without another REPL loop
503
+ llm_query("Extract the date", context[1000:2000])
504
+
505
+ # Child RLM when depth permits; otherwise one plain-LM boundary call
506
+ rlm_query("Analyze this section", context[2000:8000])
507
+
508
+ # Ordered parallel calls, limited by max_concurrent_subcalls
509
+ results = llm_query_batched(queries, chunks)
510
+ ```
511
+
512
+ `recursive_llm` remains as a backward-compatible alias for `rlm_query`. A step can finish directly
513
+ through `FINAL(...)`, `FINAL_VAR(...)`, or the mutable `answer` object:
514
+
515
+ ```python
516
+ answer["content"] = result
517
+ answer["ready"] = True
518
+ ```
519
+
520
+ ## How It Works
521
+
522
+ 1. **Context is stored as a variable** in a Python REPL environment
523
+ 2. **Root LM gets only the query** plus instructions
524
+ 3. **LM can explore context** using Python code:
525
+ ```python
526
+ # Peek at context
527
+ context[:1000]
528
+
529
+ # Search with regex
530
+ re.findall(r'pattern', context)
531
+
532
+ # Recursive processing with a plain-LM boundary fallback
533
+ rlm_query("extract dates", context[1000:2000])
534
+ ```
535
+ 4. **Returns the final answer** via a standalone `FINAL("answer")`, `FINAL_VAR(name)`, or `answer`
536
+ publication
537
+
538
+ REPL variables persist between iterations. Each local step executes in an isolated subprocess, its
539
+ final expression is evaluated exactly once, print output is isolated per step, and non-terminating
540
+ local code is terminated by `repl_timeout`. Time spent waiting for model subcalls is not charged to
541
+ the local Python timeout. Imports are limited to the already exposed `re`, `json`, `math`,
542
+ `datetime`, and `collections` helpers; arbitrary modules remain blocked.
543
+
544
+ Ordinary user variables are copied back to the parent after each step only up to
545
+ `repl_max_snapshot_bytes` (1 MB by default), preferring smaller values. Larger values remain intact
546
+ in the persistent worker and are still available to later code and `FINAL_VAR`; the cap avoids
547
+ repeatedly serializing large intermediate lists or strings. Increase the limit only when an
548
+ application directly depends on the parent-side compatibility snapshot.
549
+
550
+ POSIX deployments may also opt in to worker-process limits:
551
+
552
+ ```python
553
+ rlm = RLM(
554
+ model="gpt-5-mini",
555
+ repl_memory_limit_mb=512,
556
+ repl_cpu_time_limit_seconds=10,
557
+ repl_max_open_files=64,
558
+ )
559
+ ```
560
+
561
+ These values depend on the runtime and workload, so the library does not guess defaults. A
562
+ configured limit that the platform cannot enforce fails worker startup explicitly. RestrictedPython
563
+ and a subprocess are defense-in-depth controls, not a security boundary for hostile code; read
564
+ [SECURITY.md](https://github.com/grishahq/recursive-llm/blob/main/SECURITY.md) before processing untrusted prompts or contexts.
565
+
566
+ ## Examples
567
+
568
+ See the `examples/` directory for complete working examples:
569
+ - `basic_usage.py` - Simple complete with OpenAI
570
+ - `ollama_local.py` - Using Ollama locally
571
+ - `two_models.py` - Cost optimization with two models
572
+ - `long_document.py` - Processing 50k+ token documents
573
+ - `data_extraction.py` - Extract structured data from text
574
+ - `multi_file.py` - Process multiple documents
575
+ - `custom_config.py` - Advanced configuration
576
+
577
+ Run an example:
578
+ ```bash
579
+ # Set your API key first
580
+ export OPENAI_API_KEY="sk-..."
581
+
582
+ # Run example
583
+ python examples/basic_usage.py
584
+ ```
585
+
586
+ ## Performance
587
+
588
+ ### Paper Results
589
+
590
+ On OOLONG benchmark (132k tokens):
591
+ - GPT-5: baseline
592
+ - RLM(GPT-5-Mini): **33% better than GPT-5** at similar cost
593
+
594
+ ### Reproducible Project Benchmark
595
+
596
+ `benchmarks/compare_same_model.py` contains deterministic structured contexts with exact expected
597
+ answers. `benchmarks/generated_long_context.py` creates byte-reproducible transaction corpora with
598
+ seed, SHA-256 identity, and a computed answer key. Model outputs remain stochastic, so use `--runs`
599
+ and compare pass rate plus p50/p95 latency before drawing quality conclusions. See
600
+ [BENCHMARK_RESULTS.md](https://github.com/grishahq/recursive-llm/blob/main/BENCHMARK_RESULTS.md) for the latest checked-in live comparison.
601
+
602
+ `benchmarks/war_and_peace.py` adds exact chapter counting, distant fact retrieval, and narrative
603
+ evidence synthesis over a verified 3.2-million-character real document.
604
+
605
+ ## Development
606
+
607
+ ```bash
608
+ # Clone repository
609
+ git clone https://github.com/grishahq/recursive-llm.git
610
+ cd recursive-llm
611
+
612
+ # Install with dev dependencies
613
+ pip install -e ".[dev]"
614
+
615
+ # Run tests, branch coverage, and the enforced coverage gate
616
+ pytest
617
+
618
+ # Type checking
619
+ mypy src/rlm
620
+
621
+ # Linting
622
+ ruff check src tests benchmarks examples
623
+
624
+ # Format code
625
+ black src tests examples benchmarks
626
+
627
+ # Build the source distribution and wheel
628
+ python -m build
629
+ ```
630
+
631
+ GitHub Actions runs these gates across Python 3.9-3.12 on Linux, plus Python 3.12 on macOS and
632
+ Windows. It also installs the built wheel and runs the offline demo.
633
+
634
+ ## Architecture
635
+
636
+ ```
637
+ RLM
638
+ ├── Core (async completion logic)
639
+ ├── Run State (per-invocation budget, usage, and trajectory)
640
+ ├── REPL Executor (restricted subprocess, persistent state, hard step timeout)
641
+ ├── Prompt Builder (system prompts)
642
+ └── Parser (extract FINAL() answers)
643
+ ```
644
+
645
+ Built on top of LiteLLM for universal LLM support.
646
+
647
+ ## Limitations
648
+
649
+ - Python REPL steps are sequential; explicit batched LM/RLM subcalls can run concurrently
650
+ - No prefix caching (future enhancement)
651
+ - Recursion depth is limited (configurable via `max_depth`)
652
+ - No streaming support yet
653
+
654
+ ## Troubleshooting
655
+
656
+ ### "Max iterations exceeded"
657
+ - Increase `max_iterations` parameter
658
+ - Simplify your query
659
+ - Check if the model is getting stuck in a loop
660
+
661
+ ### "API key not found"
662
+ - Copy `.env.example` to `.env` and set the appropriate provider variable:
663
+ - `OPENAI_API_KEY` for OpenAI
664
+ - `DEEPSEEK_API_KEY` for DeepSeek
665
+ - `MOONSHOT_API_KEY` for Kimi
666
+ - Or pass `api_key` parameter to RLM constructor
667
+
668
+ ### "Model not found"
669
+ - Check model name format for your provider
670
+ - See LiteLLM docs: https://docs.litellm.ai/docs/providers
671
+
672
+ ### Using Ollama
673
+ - Make sure Ollama is running: `ollama serve`
674
+ - Pull a model first: `ollama pull llama3.2`
675
+ - Use model format: `ollama/model-name`
676
+
677
+ ## Contributing
678
+
679
+ Contributions welcome! Please:
680
+ 1. Fork the repository
681
+ 2. Create a feature branch
682
+ 3. Add tests for new features
683
+ 4. Ensure all tests pass (`pytest tests/`)
684
+ 5. Follow code style (use `black` and `ruff`)
685
+ 6. Submit a pull request
686
+
687
+ ## Citation
688
+
689
+ This implementation is based on the RLM paper by Alex L. Zhang, Tim Kraska, and Omar Khattab.
690
+
691
+ **To cite this implementation:**
692
+ ```bibtex
693
+ @software{rlm_python,
694
+ title = {recursive-llm: Python Implementation of Recursive Language Models},
695
+ author = {Gvadzabia, Grisha},
696
+ year = {2025},
697
+ url = {https://github.com/grishahq/recursive-llm}
698
+ }
699
+ ```
700
+
701
+ **To cite the original paper:**
702
+ ```bibtex
703
+ @misc{zhang2025rlm,
704
+ title = {Recursive Language Models},
705
+ author = {Zhang, Alex L. and Kraska, Tim and Khattab, Omar},
706
+ year = {2025},
707
+ url = {https://arxiv.org/abs/2512.24601},
708
+ eprint = {2512.24601},
709
+ archivePrefix = {arXiv}
710
+ }
711
+ ```
712
+
713
+ ## License
714
+
715
+ MIT License - see LICENSE file for details
716
+
717
+ ## Acknowledgments
718
+
719
+ Based on the Recursive Language Models paper by Alex L. Zhang, Tim Kraska, and Omar Khattab.
720
+
721
+ Built using:
722
+ - LiteLLM for universal LLM API support
723
+ - RestrictedPython for restricted code execution
724
+
725
+ ## Links
726
+
727
+ - **Paper**: https://alexzhang13.github.io/blog/2025/rlm/
728
+ - **arXiv**: https://arxiv.org/abs/2512.24601
729
+ - **Official implementation**: https://github.com/alexzhang13/rlm
730
+ - **LiteLLM Docs**: https://docs.litellm.ai/
731
+ - **Changelog**: https://github.com/grishahq/recursive-llm/blob/main/CHANGELOG.md
732
+ - **Releases**: https://github.com/grishahq/recursive-llm/releases
733
+ - **Issues**: https://github.com/grishahq/recursive-llm/issues