Query-adaptive context assembly for code retrieval: the score distribution decides how much context a query gets, within a token budget.
Project description
tobler
Query-adaptive context assembly for code retrieval. Tobler's first law for your context window: near things are more related than distant things, so let the score distribution decide how much context a query deserves, instead of a fixed top-5.
If you drive an AI coding agent, tobler feeds it the right code in fewer tokens, and on large repos it finds the target chunk where a fixed top-5 misses it two-thirds of the time. It sits on top of semble retrieval and ships as a library and a FastMCP server.
Requirements: Python ≥ 3.12 and an MCP client (e.g. Claude Code). semble is pulled in automatically (pinned). Published on PyPI as tobler-search (the import name stays tobler).
What it does in practice
- Sharp query ("what does BACKOFF_BASE_MS do") → N_eff = 3, a few full bodies + a map, ε honest.
- Diffuse query ("how does real-time collaboration work") → N_eff = 550, wide map of signatures and stubs.
- Same token budget both times. Fixed top-k is wrong in both directions on the same repo.
Benchmark: hit rate (ground-truth symbol queries, ~40 per repo)
τ was calibrated once on two repos and never touched again; the first three rows are repos it never saw:
| repo | semble top-5 | tobler B=1000 | tobler B=2000 |
|---|---|---|---|
| numpy (16,465 chunks) | 32% | 88% | 92% |
| ripgrep (Rust) | 48% | 92% | 95% |
| zod (TypeScript) | 80% | 95% | 98% |
| 8 smaller repos (247 queries) | 76% | 99% | 100% |
| calibration repos (80 q, not held out) | 94% | 98% | 99% |
Fixed top-5 degrades hard as repos grow: on numpy it finds the right chunk a third of the time. Tobler holds ≥88% everywhere with zero per-repo tuning.
Benchmark: tokens (mean per query)
| repo | semble top-5 | tobler B=1000 | tobler B=2000 |
|---|---|---|---|
| numpy | 1463 | 995 | 1940 |
| ripgrep | 1544 | 993 | 1944 |
| zod | 1453 | 981 | 1940 |
| 8 smaller repos | 1395 | 992 | 1966 |
Semble's spend is uncontrolled (whatever five chunks happen to cost, observed range ~1,000–2,800/query); tobler's budget is a hard ceiling that was never exceeded on any of ~370 benchmark queries. Tokens per correct answer (mean tokens ÷ hit rate): on numpy semble costs ~4,570 tokens per hit vs tobler's ~1,130 at B=1000, so it is 4× more token-efficient; ~3× on ripgrep, ~1.8× on small repos. When the budget genuinely can't cover the relevant code, ε reports it instead of silently truncating.
The math (seven equations, one calibrated knob)
- Center/standardize raw cosine scores over all N chunks:
s̃ = (s − s̄)/σ, which removes the "everything resembles everything" anisotropy baseline. - Softmax at temperature τ:
ŵᵢ = softmax(s̃/τ). - Effective count
N_eff = 1/Σŵᵢ²: how many chunks genuinely matter for this query. - Nucleus cut: smallest set S with mass ≥ p (default 0.9), so k adapts per query.
- Residual tail
ε = 1 − Σ_S ŵᵢ, returned to the caller so the agent knows when it's reasoning inside a truncation. - Budget allocation
bᵢ = B·ŵᵢ/Σ_S ŵⱼ, mapped to level-of-detail tiers (full body → signature+docstring → path+symbol), with greedy repair rolling unspent budget down by weight. - Session smoothing:
m_t = λ_t·m_{t−1} + (1−λ_t)·s̃_twithλ_t = λ_max·max(0, corr(s̃_t, m_{t−1})). An anaphoric follow-up ("what are the arguments to that") gets pulled toward the session's focus; a topic pivot has corr ≈ 0, so memory vanishes with no reset heuristic. The gate experiment calibrated λ_max* = 1.0. The knob removed itself: the correlation is the blend. Follow-up hit rate 9% → 46%, pivots unharmed at 96% (seeexperiments/RESULT.mdAddendum 3).
τ is the one calibrated free parameter (found by maximizing var(log N_eff) across real queries); p, k_min, and the budget B are fixed defaults you can override. On two real repos, τ* = 1.28 on the hybrid channel and N_eff spans two orders of magnitude (p90/p10 = 215×); see experiments/RESULT.md.
Why the scores are hybrid
Scores come from a hybrid channel (z-scored model2vec cosine blended with z-scored raw BM25) because static embedders can't see rare identifiers (mulberry32 ranks ~200th semantically, 3rd lexically). A k_min=5 floor on the nucleus cut guarantees coverage never drops below the old fixed top-5. Warm-index latency: numpy 54 ms/query, ripgrep 15 ms, zod 14 ms.
Quickstart
Install:
pip install tobler-search # or: uv pip install tobler-search
Register the MCP server with Claude Code (uvx needs no separate install):
claude mcp add --scope user tobler -- uvx --from tobler-search tobler-mcp
Tool: search(query, repo, budget=2000, content="code", fresh=False) → markdown with an honesty header (N_eff=… k=… ε=… tokens=…/…, plus mem=… when session memory shaped the result) followed by tiered context blocks. Related follow-up queries are smoothed toward the session's focus; pass fresh=true to drop the memory for a query.
Run the server directly (stdio) with tobler-mcp.
CLI
pip install tobler-search puts the tobler command on your PATH. For shells, scripts, or agents without MCP access, the same search runs from the command line:
tobler index ./repo # warm the semble index first (optional)
tobler search "authentication flow" ./repo # print the adaptive context
tobler search "save model to disk" ./repo --budget 3000
tobler search "deployment guide" ./repo --content docs
Output is the same honesty header + tiered blocks the MCP tool returns. (Session smoothing is MCP-only; the CLI is stateless per call.)
Agent skill
skills/tobler-search/ ships an agent skill that teaches a coding agent when and how to reach for tobler (MCP first, CLI fallback) and how to read the N_eff / k / ε / mem header. Point your agent's skill loader at it, or copy it into your skills directory.
Reproduce the benchmarks
From a source checkout (git clone https://github.com/datacompose/tobler):
uv run pytest # 322 tests
uv run experiments/neff_sweep.py <repo1> <repo2> # the τ calibration
uv run experiments/benchmark.py <repo1> [...] # hit rate + tokens vs semble
uv run experiments/session_sweep.py <repo1> [...] # the session-smoothing gate
Layout
src/tobler/core.py: the pipeline math, pure numpysrc/tobler/bridge.py: the only module touching semble private attrs (pinnedsemble==0.3.4)src/tobler/render.py: tier rendering + token cost estimatessrc/tobler/assemble.py: steps 1–7 composed;Sessionholds the smoothing memorysrc/tobler/mcp.py: FastMCP server, one session stream per (repo, content)src/tobler/cli.py: thetoblercommand (search+index)skills/tobler-search/: shippable agent skill wrapping the MCP tool + CLIexperiments/: the validation gates (Phase 0, hybrid benchmark, session smoothing: all passed)
License
MIT. See LICENSE.
Built in a night, but the numbers aren't hand-wavy: τ calibrated once on held-out repos, every claim gated by an experiment in experiments/. Kick the tires and file issues.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tobler_search-0.1.0.tar.gz.
File metadata
- Download URL: tobler_search-0.1.0.tar.gz
- Upload date:
- Size: 150.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
973b512da1bccdc4f518d13cef99ee69ccb13f780dd7638c0d79e33e08053ba6
|
|
| MD5 |
4e5dfeccd79823fd1a5c3a0e13204e0f
|
|
| BLAKE2b-256 |
01dca4eb0a2303e4ce61c91006a382559890bfad0cc821b811289edfb54c1bed
|
File details
Details for the file tobler_search-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tobler_search-0.1.0-py3-none-any.whl
- Upload date:
- Size: 15.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e78d9a97911e191266d404b78fc1d021eb11c80ec66f9451b21b94f978b5c4e2
|
|
| MD5 |
a3cba3ef7d00d238bd48aed56bd7f5b4
|
|
| BLAKE2b-256 |
8ef6ddbc144e5982f0186d9188f94b228141f586dabb9607932810d328b7ae9f
|