Skip to main content
Droste — nested frames converging on the answer

Droste

The recursive harness for your data.

Droste is a Recursive Language Model (RLM) engine: instead of stuffing your data into a context window, the model gets it as a variable in a sandboxed Python REPL. It inspects the data, writes code over it, and fans out llm_query / llm_query_batched subcalls over the pieces that need semantic judgment — map-reduce, where the model writes both the map and the reduce.

Coding harnesses bolt a model onto a transcript. Droste harnesses it to data.

uvx droste "which customer had a failed charge, and why?" server.log
uvx droste "which plan has the highest refund rate vs its MRR?" shop.db

droste answering a two-part question over a 435 kB server log, streaming its code as it works

The first example, against a 231 KB log with gemini-3.5-flash:

$ droste "Which customer had a failed charge, for what amount, and why?
  How many timeout errors are there, and which upstream do they blame?" server.log

1. Failed charge: customer cus_9982, amount 1499 ($14.99), reason card_declined
2. Timeout errors: 66 total; all blame the upstream service payments-v2

The counts are exact because the model counted them in Python — it never read 4,000 log lines through its attention. In --db mode the model introspects your schema, writes read-only SQL, and computes over the rows; in the demo above it noticed the free plan makes refund-rate-vs-MRR undefined and answered for the paid plans instead.

Why

Two things, at once: better answers and bounded cost.

Better answers. Long-context models read everything and still miss things. Retrieval finds the right chunk but can't compute across all of them. An RLM does what you would do: look at the shape of the data, narrow mechanically (regex and SQL find where; subcalls understand what), delegate semantic judgment in bounded batches, and aggregate in code.

Bounded cost. The naive version of this loop is ruinous — left to itself, one subcall spent 62,911 thinking tokens producing a 4-token answer, and tasks cost $4–6 each. Subcall work is extraction, not reasoning: with subcall reasoning off and the default 2,048-token output cap, the same tasks measured ~25× cheaper at the same accuracy. The cap is a client default; reasoning-off is enforced server-side on ModelRelay and passed through on BYOK (--reasoning-effort none, for endpoints that honor it). And because the root writes the map and the reduce, you can put a strong model at the root and a cheap one in the fan-out (--model gemini-3.5-pro --subcall-model gemini-3.5-flash) — a few expensive tokens where judgment happens, thousands of cheap ones where reading happens.

Measured on OOLONG (131k-token contexts, 50 tasks, gemini-3.5-flash everywhere):

approach score cost/task wall/task
Droste (server defaults) 0.84 ~$0.37 27s
same model, full context inline 0.52 ~$0.26 44s
dspy.RLM (matched models & budgets) 0.74 ~$0.26 73s

+32 points over stuffing the window, at comparable cost. On TAG-Bench (agentic analysis over SQL), Droste scores 50% strict-match where published text-to-SQL baselines sit under 20% — no pipeline, just droste pointed at the .db file.

Caveats: one dataset per benchmark, one context length, one model family, n=50. Per-task artifacts and cost derivations ship with the benchmark harness.

Use it

Ask questions over files, folders, and SQLite from the terminal. The contract: args that exist are data, the one that doesn't is the question, no args means the current directory, pipes are data too — and it always prints one line saying what it read.

uvx droste "…" ./docs        # zero-install, npx-style
uv tool install droste       # or keep the binary around
pipx install droste          # the older equivalent
droste login                 # one-time setup: free credits, or your own key
droste "what changed between these?" report.txt logs.txt
droste "which customers churned last month?" app.db
droste "how does auth work here?" ./docs
cd ~/notes && droste "what did I decide about pricing?"
tail -5000 app.log | droste "why did it crash?"

SQLite files are recognized by their magic bytes — no flag needed (--db remains as an explicit override). Directory walks skip binaries, dotfiles, and the usual junk (.git, node_modules, …) and cap sizes (--max-file-bytes, --max-bytes); every skip is counted in the report line. droste ask … still works as an alias.

Files are materialized as the sandbox's context variable — the model is told each file's name and size (not its contents) and pulls data in via code, so multi-MB files are fine. What the model reads is whatever its code chooses to print. --db uses the engine's local-mode SQL data source (read-only policy as a guardrail, not a boundary; OS permissions are the boundary).

Engine knobs mirror RLMConfig: --subcall-model, --subcall-max-output-tokens (default 2048), --reasoning-effort, --max-iterations, --max-subcalls. --json prints a result object for scripting; --verbose streams one-line progress to stderr (watch it think); --trace dumps the full loop (generated code, outputs, responses). Exit code 0 means a confirmed (or extracted-with-note) answer.

Three worked starting points live in docs/recipes.md (logs, chat archives, SQLite).

Pointing --base-url at ModelRelay lights up the platform features (validated SQL policies, server-enforced subcall cost controls, audit) — documented, not required. droste is the engine CLI; mrl remains the ModelRelay platform CLI.

Embed it

The same wheel is the engine as a library — zero runtime dependencies, urllib-only. Add it to your app and point the loop at your own data sources:

uv add droste        # or: pip install droste

Using is asking over your data; embedding is building RLM answers into a product for your users.

BYOK: OpenAI-compatible endpoints, and Anthropic natively

The engine ships built-in clients for any endpoint that speaks the OpenAI chat-completions shape (OpenAI, OpenRouter, Google's OpenAI-compat endpoint, vLLM, Ollama, ...) — plus a native client for Anthropic's Messages API (their compat layer is a testing shim, so Claude gets its own client). Bring your own key — no ModelRelay account required. The CLI detects the provider from facts: an sk-ant-… key (or ANTHROPIC_API_KEY) routes to Anthropic; an explicit --base-url/OPENAI_BASE_URL always wins.

export ANTHROPIC_API_KEY=sk-ant-...
droste "why did it crash?" ./logs --model claude-opus-4-8
from droste import (
    OpenAICompatClient,
    OpenAICompatSubcallClient,
    create_execution_context,
    run_rlm,
)

context = create_execution_context(max_calls=50, max_depth=1)
root = OpenAICompatClient(model="gpt-5.2-mini")  # OPENAI_API_KEY / OPENAI_BASE_URL from env
subcalls = OpenAICompatSubcallClient(
    model="gpt-5.2-mini",
    context=context,               # shared call/token accounting
    max_output_tokens=2048,        # per-subcall output bound (cost control)
)

env = ...  # your RLMEnvironment implementation (see Core Concepts below)
result = run_rlm(question, environment=env, root_llm=root, subcalls=subcalls, context=context)

Explicit base_url= / api_key= constructor args win over the environment variables. Subcall batches run with bounded concurrency (5 workers) and every subcall's usage block is added to result.tokens_used.

reasoning_effort and extra_body pass through to the endpoint as-is. Disabling thinking per-subcall is a gateway capability: ModelRelay enforces it server-side; raw endpoints may ignore a client-side disable.

Runner architecture (droste_runner)

The droste_runner package is a thin orchestration layer that wires droste to HTTP-backed root LLM calls and subcalls. It is shared across hosts (ModelRelay's hosted runner, in-process embedders) so the loop logic stays in one place. For custom environments, set adapter_module in the runner request to delegate to an adapter module's run(request) function.

flowchart LR
    Host[Host App] --> Runner[droste_runner]
    Runner --> Core[droste run_rlm]
    Runner --> Env[RunnerEnvironment]
    Env --> Sandbox[Python REPL execute]

    Core --> RootLLM[LLMClient responses_create]
    RootLLM --> Responses[Host /responses]

    Core --> Subcalls[SubcallClient llm_query llm_batch]
    Subcalls --> SubcallAPI[Host /rlm/subcall]

Runner Inputs

  • root_endpoint + subcall_endpoint + token: required for HTTP-backed runs.
  • adapter_module: optional Python module path to override the runner entirely.

Core concepts

Protocols

Implement these to integrate with your infrastructure:

  • RLMEnvironment - Sandboxed Python REPL with data access
  • LLMClient - Chat completion interface for the root LLM
  • SubcallClient - Provides llm_query() and llm_batch() for sub-LLM calls
  • DataSource - Optional data source integration

Configuration

RLMConfig(
    max_iterations=20,      # Max refinement loops (default)
    max_depth=1,            # Max nested subcall depth (default)
    max_calls=50,           # Max total subcalls (default)
    max_output_chars=25000, # Output budget per iteration (default)
)

Result

RLMResult(
    answer="...",           # Final answer from answer["content"]
    ready=True,             # Whether answer["ready"] was set
    iterations=3,           # Iterations used
    tokens_used=1500,       # Total tokens consumed
    sub_calls_made=12,      # Total llm_query/llm_batch calls
    trajectory=[...],       # Full execution history
)

Development

uv sync          # Install dependencies
uv run pytest    # Run tests
uv build         # Build wheel

The name

The Droste effect is the picture that contains itself. M.C. Escher's Print Gallery pushed it to its limit — a man in a gallery viewing a print that contains the gallery he is standing in — and Escher left the center of the spiral famously blank, signed but uncompleted, where the recursion outran his hand. Fifty years later, mathematicians completed it; their project was titled "The Mathematics Behind the Droste Effect."

The answer at the center of the spiral — the part the picture couldn't hold — is what recursion computes.

License

Apache-2.0. See LICENSE. Contributions welcome — CONTRIBUTING.md. Versioning is semver; the runner protocol and source-registry contract carry an explicit compatibility window (see docs/architecture.md).

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

droste-0.9.0.tar.gz (2.4 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

droste-0.9.0-py3-none-any.whl (108.7 kB view details)

Uploaded Python 3

File details

Details for the file droste-0.9.0.tar.gz.

File metadata

  • Download URL: droste-0.9.0.tar.gz
  • Upload date:
  • Size: 2.4 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for droste-0.9.0.tar.gz
Algorithm Hash digest
SHA256 f582841e29ad63f76cc3c1177515636ab270fe542783fedba552be8f546569c8
MD5 449cdb63fd29aa2a89445c4545b25c1c
BLAKE2b-256 ea5ed7c03039ea3465e3a587d32652bd00a1bb447021e2d192a1c2f3679f4464

See more details on using hashes here.

File details

Details for the file droste-0.9.0-py3-none-any.whl.

File metadata

  • Download URL: droste-0.9.0-py3-none-any.whl
  • Upload date:
  • Size: 108.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for droste-0.9.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ead2bcd66b2c6dc734081da3ed7ae87d91f4121e8aa8f1291ca65d25e86df2b3
MD5 70b8d8bd0bdf7cbba796581e0b3c5955
BLAKE2b-256 af7dfb6afefae4ea54fd82d237d6158b5cf8b11726d15ec4c47a50ac356d0ad7

See more details on using hashes here.

Release history Release notifications | RSS feed

0.26.0

2 files

0.25.0

2 files

0.24.0

2 files

0.23.0

2 files

0.22.0

2 files

0.21.1

2 files

0.21.0

2 files

0.20.1

2 files

0.20.0

2 files

0.19.2

2 files

0.19.1

2 files

0.19.0

2 files

0.18.0

2 files

0.17.0

2 files

0.16.0

2 files

0.15.5

2 files

0.15.4

2 files

0.15.3

2 files

0.15.2

2 files

0.15.1

2 files

0.15.0

2 files

0.14.1

2 files

0.13.1

2 files

0.13.0

2 files

0.12.1

2 files

0.12.0

2 files

0.11.0

2 files

0.10.6

2 files

0.10.5

2 files

0.10.4

2 files

0.10.3

2 files

0.10.2

2 files

0.10.1

2 files

0.10.0

2 files

This release

0.9.0 This release

2 files

0.0.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page