Agent Retrieval Bench¶
Agent Retrieval Bench
(ARB) asks a retriever to rank the files a coding agent needs next for four
workflow signals. codenib-arb runs five CodeNib retrieval methods on all four
positive tasks and scores their rankings with ARB's own evaluator. It can also
score file rankings produced by any other tool.
| Release | Task | Samples |
|---|---|---|
v2_code2test |
Find tests for a code change | 106 |
v2_comment2context |
Find context for a review comment | 80 |
v2_trace2code |
Find code implicated by a failure trace | 101 |
v2_edit2ripple |
Find files affected by an anchor edit | 58 |
| Total | 345 |
Selective (abstention) evaluation, line and block scores, and interactive agent trajectories are not covered. No method calls a model API.
| Benchmark card | |
|---|---|
| Layer | L1 Localization: the method's final ranked files, symbols, code blocks, or line regions |
| Origin | External |
| Task | Four static tasks; code→test, comment→context, trace→code, edit→ripple |
| Ground truth | Released gold files |
| Granularity | File |
| Languages | Mixed; multi-language repositories |
| Size | 345 positive samples |
| Pinned revisions | Evaluator 07014c98, dataset 5901e1ee |
| Scorer | Official functions called directly (Recall@k, MRR, BCY) |
| Setup | ARB checkout and verified release download |
| CodeNib entry point | codenib-arb |
| Evidence | Measured; five methods on all 345 samples |
| Upstream | https://github.com/eyuansu62/agent-retrieval-bench |
Results¶
On all 345 positive samples, dense retrieval puts gold files earliest (the best Recall@5, MRR, and BCY), and fusing it with BM25 raises Recall@20 but lowers MRR. CodeNib's BM25 trails ARB's own BM25 baseline; the cause is a tokenizer gap (#856), not the protocol.
| Method | Source | Recall@5 | Recall@20 | MRR | BCY@8k |
|---|---|---|---|---|---|
dense |
Released corpus | 0.2940 | 0.5736 | 0.2425 | 0.3355 |
hybrid |
Released corpus | 0.2804 | 0.5972 | 0.2144 | 0.3101 |
zoekt |
Released corpus | 0.2498 | 0.4400 | 0.1605 | 0.2569 |
bm25 |
Released corpus | 0.1543 | 0.3889 | 0.1211 | 0.1659 |
graph_expansion |
Full source | 0.1543 | 0.3498 | 0.1197 | 0.1659 |
| ARB BM25 (control) | Released corpus | 0.1899 | 0.4452 | 0.1520 | 0.2051 |
Each row is one run of every sample with no failures; scores are means over all
345 samples. Graph expansion ran as two concurrent shards of the same commit on
disjoint snapshots, scored together with codenib-arb evaluate --source-access
full-source. The control row reruns ARB's eval-baseline --ranker bm25 on the
same machine and reproduces ARB's published BM25 row. Graph expansion reads more
than the released corpus, so it is a separate condition rather than a peer of
the other rows.
Recall@20 by task:
| Method | code2test (106) | comment2context (80) | trace2code (101) | edit2ripple (58) |
|---|---|---|---|---|
dense |
0.690 | 0.550 | 0.480 | 0.556 |
hybrid |
0.607 | 0.627 | 0.561 | 0.601 |
zoekt |
0.304 | 0.325 | 0.607 | 0.556 |
bm25 |
0.299 | 0.529 | 0.391 | 0.356 |
graph_expansion |
0.209 | 0.506 | 0.416 | 0.276 |
No method was tuned on ARB. Recall@20 / MRR on the dev and held-out halves (164 and 181 samples, defined below):
| Method | Dev | Held-out |
|---|---|---|
dense |
0.5526 / 0.2340 | 0.5925 / 0.2502 |
hybrid |
0.6105 / 0.2342 | 0.5852 / 0.1964 |
zoekt |
0.4542 / 0.1589 | 0.4273 / 0.1619 |
bm25 |
0.4233 / 0.1359 | 0.3577 / 0.1077 |
graph_expansion |
0.3704 / 0.1329 | 0.3310 / 0.1077 |
Graph expansion built a code graph for 190 of the 218 snapshots, covering 290 samples. Seventeen transformers snapshots and one diffusers snapshot hit CodeNib's 600-second scip-python limit; the Java indexer produced no graph for five spring-boot snapshots and one mockito snapshot; two scrapy, one gin, and one numpy snapshot also produced none. Those samples rank by BM25. On the 290 samples with a graph, it lowered Recall@20 by 0.047 against BM25 (95% bootstrap interval −0.085 to −0.007). MRR fell by 0.002 (−0.005 to +0.002), and BCY@8k showed no net change (−0.016 to +0.014). It helped trace2code (0.416 against 0.391) and hurt code2test, partly because the Go graphs in this run omitted test files. Adding 15 reference neighbors after five BM25 seeds displaces better BM25 candidates; deciding when to follow references is left to an interactive agent.
Costs. Cold builds are the total over all 218 corpus snapshots; the largest index is the biggest single snapshot; query times are per sample:
| Method | Cold builds, total | Largest index | Query median / p95 | Tool calls per sample |
|---|---|---|---|---|
bm25 |
437 s | 160k chunks, 13.4M tokens, in memory | 0.40 s / 11.97 s | 1 |
dense |
4,035 s | 160k vectors, 665 MB, in memory | 0.15 s / 1.76 s | 1 |
hybrid |
4,365 s | both of the above | 0.55 s / 11.96 s | 2 |
zoekt |
1,315 s | 276 MB on disk (9.2 GB for all snapshots) | 0.67 s / 2.37 s | 14.4 |
graph_expansion |
22,541 s, plus 419 s fetching source | graph of 160k vertices, 343k reference edges | 0.33 s / 9.16 s | 1.8 |
Dense builds embed each distinct chunk text once per run: 0.90M of the 6.2M released chunks were embedded and the rest reused, so a snapshot's cost depends on which snapshots of its repository came before it. No method made a model API call; dense and hybrid ran Qwen3-Embedding-0.6B locally.
The runs used tag arb/2026-10-22/methods
on one host (Intel Xeon Gold 5416S, 64 cores, one H100 shared with other jobs),
with CPU and GPU methods running at the same time, so treat timings as
indicative. The results receipt holds every
method's configuration, per-sample metrics, ranking depths, and costs; the
full rankings are published at sysevol-ai/codenib-arb-rankings with checksums for offline
rescoring.
ARB lists these results, with the rankings and rescoring instructions, under its community results; its maintainers rescored all five methods independently. An earlier eight-sample compatibility check is kept in the smoke receipt.
Pins and preparation¶
| Input | Pinned revision |
|---|---|
| Upstream evaluator | 07014c986f3deadb1548c62b32c0ffbe6a81465d |
| Released dataset | 5901e1ee3aff048290db72edf9c63bc498b79ea3 |
Install CodeNib and use a separate checkout for ARB. The upstream download
command requires the Hugging Face hf CLI, zstd, and tar; it verifies each
release archive's published checksum before extraction.
python -m pip install -e '.[datasets]'
git clone https://github.com/eyuansu62/agent-retrieval-bench /tmp/arb
git -C /tmp/arb checkout 07014c986f3deadb1548c62b32c0ffbe6a81465d
python -m venv /tmp/arb-env
/tmp/arb-env/bin/pip install /tmp/arb huggingface_hub
for release in v2_code2test v2_comment2context v2_trace2code v2_edit2ripple; do
/tmp/arb-env/bin/arb download-benchmark \
--version "$release" \
--revision 5901e1ee3aff048290db72edf9c63bc498b79ea3 \
--local-dir /tmp/arb-data \
--hf-bin /tmp/arb-env/bin/hf
done
codenib-arb prepare \
--data-root /tmp/arb-data \
--output /tmp/arb-prepared.json
Preparation loads all four releases by default. Repeat --release or
--sample-id to select a subset; --limit-per-task 2 selects the first two
sample IDs in each task for a deterministic smoke test. Selection happens
before retrieval. The output freezes sample records, selection, source hashes,
corpus hashes, and snapshot identities. Manifest paths are anchored at
--data-root, regardless of the current working directory.
Local fingerprints detect subsequent changes; they do not authenticate a modified local copy as the published release. Use the verified download above and retain its receipts. Keep the prepared file alongside the data root it references. Invalid sample schemas stop preparation; corpus preparation errors remain attached to their selected samples and produce a nonzero exit status.
Retrieve and rescore¶
codenib-arb run \
--prepared /tmp/arb-prepared.json \
--upstream /tmp/arb \
--output /tmp/arb-bm25
codenib-arb evaluate \
--prepared /tmp/arb-prepared.json \
--pred /tmp/arb-bm25/predictions.json \
--upstream /tmp/arb \
--output /tmp/arb-rescored
Each output directory contains config.json, predictions.json, and
results.json. Native retrieval also writes checkpoint.json after every
case, atomically binding its predictions to input and integrity hashes. Use the
same command with --resume after interruption; the input and implementation receipts must
match. Resume verifies the native checkpoint and rebuilds the public prediction
export from it; edited exports cannot replace native results. Completed successes
and failures are retained. Use a new output directory to retry failures or change inputs. A directory lock excludes
concurrent writers; an interrupted rescore removes the previous report before
starting so an old success cannot be mistaken for a new result.
Preparation checks each selected release against its complete published sample count before applying sample IDs or limits, so truncated downloads cannot reduce the denominator silently.
The evaluator runs in an isolated subprocess against a clean ARB checkout,
rejecting hidden Git index flags as well as ordinary local changes. The
checkout can be the pinned revision or a later one that contains it, provided
every file the scorer imports is unchanged from the pinned revision; changes to
ARB's other evaluators do not matter. Results record both the checkout's
revision and the pinned scoring revision. The evaluator needs no installation
in CodeNib's environment. Optionally set
--evaluator-python /tmp/arb-env/bin/python. Offline rescoring never reruns
retrieval and does not infer the cost of externally produced predictions.
External systems can supply a JSON array or .jsonl records:
{"sample_id":"<released id>","repo":"owner/repo","base_commit":"<40-character SHA>","status":"ok","ranked_files":["src/first.py","tests/test_first.py"]}
Use status: "error" with an error field for a retrieval failure. An explicit
empty ranked_files list is a valid zero-score result; an absent prediction is
a failure. Duplicate file paths collapse at their first occurrence. Unknown or
duplicate sample IDs reject the input. Snapshot mismatches and unknown file
paths fail the corresponding case.
Retrieval methods¶
codenib-arb run --method chooses how files are ranked. Every method reads
only the released query, indexes each corpus snapshot once, and is scored by
the same pinned evaluator. All methods except graph_expansion read only the
released corpus; graph_expansion also reads the full source, a separate
condition described below.
| Method | Index per snapshot | Ranking |
|---|---|---|
bm25 (default) |
CodeNib BM25 over every released file and symbol chunk | Every chunk ranked; each file takes the rank of its first chunk |
dense |
Exact inner-product search over Qwen3-Embedding-0.6B vectors of every chunk | Same first-hit file ranking |
hybrid |
Both indexes above | CodeNib's file-level weighted RRF (k = 60) of the two file rankings |
zoekt |
Zoekt trigram index of every file and symbol chunk | One Zoekt query per code-like term in the query; files ordered by matched terms, then first match |
graph_expansion |
BM25 as above, plus a CodeNib code graph of the full source | The top five BM25 files, then up to 15 files connected to them by code references, then the rest in BM25 order |
rerank |
None; it reads a completed run | The base run's top 50 files rescored by Qwen3-Reranker-4B and fused with the base order by file-level RRF (k = 60); the other files keep the base order |
The dense model uses no query or document instruction, as ARB's own embedding
baseline does, and applies a 512-token limit to queries as well as chunks.
With the pinned tokenizer, 124 of the 345 queries are longer and are cut: 15 of
106 code2test, 2 of 80 comment2context, 52 of 101 trace2code, and 55 of 58
edit2ripple. BM25 and graph expansion read the full query, and hybrid cuts
it only in its dense branch. On a GPU the model runs in bfloat16. --embedding-model with
--embedding-revision (a 40-character commit) selects another
SentenceTransformers model, and --max-seq-length and --embedding-batch-size
change the token limit and batch size; each run records them. --query-prompt registry prepends the query instruction that CodeNib registers for the model, given as text or as the name of one of the model's own prompts, and the run records it. Models registered with a document instruction as well are refused, because chunks are embedded without one. Each distinct chunk text is embedded once per run, because
snapshots of the same repository share most files, and index_size records
how many chunks a build encoded and how many it reused. Dense methods need the
semantic extra; a missing extra stops the run before any case.
rerank is a second stage over a completed run. --rerank-base names that
run's output directory; it must have written its final report and ranked the
same prepared cases with released-corpus access, and a run still in progress is
refused. The model scores each of the top files as its path and released text,
truncated with the query to 8,192 tokens, under CodeNib's default reranker
instruction; another cross-encoder keeps its own length limit and takes no
instruction. It never sees gold labels. A file it cannot score within GPU
memory fails the case rather than scoring zero. --rerank-model with
--rerank-revision, --rerank-top-k, --rerank-max-length, and
--rerank-batch-size change the model and its limits. The receipt records them
with digests of the base run's receipt and predictions. The output holds
exactly the base ranking's files, duplicates removed. tool_calls counts the
stage's one scoring call, and base_tool_calls the base run's, recorded even
when the base failed that case and left empty when the base did not record it.
It needs the semantic extra.
The Zoekt method takes its terms from the query's string values: file names,
backticked spans, and identifiers written with _, camelCase, or capitals, up
to 32 per sample. Each term is one tool call. Files that match no term are not
ranked, so its rankings can be shorter than the corpus. It needs make
zoekt-tool or --zoekt-bin-dir.
Full-source graph expansion¶
A code graph records which symbols reference which, as resolved by the
language's compiler-grade indexer (SCIP, or a language server). The indexer
needs the real repository, so graph_expansion fetches each sample's
base_commit into --workspace and builds the graph from that checkout:
python -m pip install -e '.[datasets,graph]'
make scip # SCIP indexers for Python, TypeScript, Go, Rust, Java, and more
codenib-arb run --method graph_expansion --workspace /tmp/arb-src \
--prepared /tmp/arb-prepared.json --upstream /tmp/arb --output /tmp/arb-graph
The rules for this condition follow the ARB maintainers' reply:
- The checkout is a depth-one fetch of
base_commit, so neither the resolving change nor any later commit is in its object store. Every use resets it to that commit and removes untracked and ignored files; hidden Git index flags are rejected. - Only the released query reaches retrieval; gold and annotations never do.
- Graph neighbors outside the released candidate set are dropped and counted
in
removed_non_candidate_paths. Scores and canonical BCY use released text.
The graph covers each language holding at least 10% of the snapshot's source
files and is built in a child process with a 30-minute limit. It never installs
dependencies or runs project setup, but the Rust and Java indexers run the
project's own build tooling. If no graph can be built, the sample is ranked by
BM25 and recorded with graph_used: false. Fetching source is reported as
source_fetch_seconds, apart from the build. The results above were
measured when CodeNib's Go graph omitted _test.go files, so expansion there
could not propose Go tests; Go graphs now include them.
Rankings produced elsewhere from the full source can be scored under the same
rule with codenib-arb evaluate --source-access full-source.
Metric contract¶
CodeNib calls upstream target_gold_files, query_has_leakage, recall_at,
reciprocal_rank, and bcy_curve.evaluate_sample rather than maintaining a
second metric implementation.
| Metric | Ranking and content |
|---|---|
| File Recall@5/10/20 | First 5, 10, or 20 distinct files |
| MRR | Full supplied file ranking; each row records ranking_depth |
| BCY@4000/8000/16000/32000 | First 20 distinct files, packed with the official regex code tokenizer and canonical renderer |
BCY reads the released kind=file text, which can be truncated by the corpus
builder. It counts a gold file only when content tokens fit; header tokens do
not establish coverage. The boundary file contributes a token prefix. The
report retains upstream packing details and coverage-threshold sensitivities.
The old upstream gold_coverage@8k character-packing score is not reported as
canonical BCY.
The native baseline indexes every released file and symbol chunk, including
tests, documentation, configuration, and extensionless files. It uses CodeNib's
existing tokenizer and BM25 implementation, not upstream's BM25 ranker. The
indexed identity is path:symbol:ordinal; all chunk hits are ranked before
first-hit file deduplication. No graph or embedding index is constructed.
Queries contain only the released query object serialized with sorted JSON
keys. Gold, fix commits, and hard-negative annotations never enter retrieval.
Primary aggregates are per-sample means over all selected cases, with failures contributing zero. The report also includes valid-only means, per-task and per-split summaries, failure IDs, and per-case errors. Commands exit nonzero on failure. Always report coverage and the selected case set with a score; truncated external rankings can lower MRR relative to a complete ranking.
The split assigns each sample to dev when the first byte of
sha256(sample_id) is even and to heldout otherwise: 164 and 181 of the 345
samples. ARB community results use the same rule, so a method tuned on dev
can report heldout separately.
Cost accounting¶
A native run separates the cost of building an index from the cost of using it. Each corpus snapshot is indexed once, and samples that share the snapshot reuse that index. The 345 samples use 218 distinct corpora.
| Quantity | Where it is recorded |
|---|---|
| Cold index build | index_build_seconds and index_size on the sample that triggered the build; corpus_load_seconds and, for graph_expansion, source_fetch_seconds separately |
| Query latency | query_seconds on every sample |
| Tool calls | tool_calls per sample: one search for bm25 and dense, two for hybrid and for graph_expansion with a graph, one per term for zoekt |
| Model usage | No API calls (model_calls is 0). dense and hybrid run a local embedding model; retrieval_usage.embedding records its revision, loads, and encoded and reused chunks |
| Context budget | Not applicable: every method returns a file ranking, and BCY packs released text at its fixed budgets |
index_size depends on the method: BM25 documents, tokens, vocabulary, and
indexed text bytes; dense documents, dimension, and vector bytes; the Zoekt
index's documents and bytes on disk; and the code graph's vertices, edges,
reference edges, and build time.
results.json summarizes these under retrieval_usage (count, total, median,
p95, and maximum) and records the CPU, core count, and peak memory under
environment. A resumed run keeps the timings recorded before the
interruption, and its peak memory covers only the final process. Offline
rescoring reports no cost, because it cannot observe how external rankings
were produced.
Extending retrieval experiments¶
The imported-ranking boundary can score additional methods on the same cases. These routes are candidates for later adapters:
| Existing CodeNib capability | Proposed comparison |
|---|---|
| Zoekt and graph tools inside an agent loop | Multi-round exact symbol, path, trace, and relationship search |
| Retrieve → rerank | Candidate ordering with a model at fixed budgets |
Before comparing agent strategies, fix the model, tool permissions, query budget, and final context budget. Report index build time and size separately from query latency and model cost.
Full-source results are a separate condition from released-corpus results; report them in separate rows, as the graph expansion rules describe.
Indexed tools in ARB's closed-tool loop¶
ARB's indexed_interactive mode lets a policy call index lookups next to
list_dir, grep, read_file, and submit, under the same budgets. CodeNib
supplies the lookups as an ARB tool provider. The mode is not in the ARB
revision pinned above for static scores; it is proposed in
agent-retrieval-bench#12,
so run it from a checkout that includes that change:
arb eval-closed-tool-llm ... --mode indexed_interactive \
--indexed-tools symbol_search,references \
--tool-provider codenib.eval.benchmarks.agent_retrieval_bench_tools:CodeNibToolProvider \
--tool-provider-option workspace=/path/to/checkouts \
--tool-provider-option cache=/path/to/tool-graphs
symbol_search(symbol)returns definitions with that name. A dotted or::name matches the end of the qualified name.references(path, symbol)returns symbols in other files that reference, or are referenced by, that symbol or the symbols it contains, ordered by the number of connecting references.
Both return paths with 1-based line ranges, never file content; a reference
made at file scope reports the line where it occurs. Imports and other
reference-only names are not returned as definitions, and parameter lists such
as run(String) are ignored when matching names. The graph is built as for
full-source graph expansion, so these runs are
full-source runs. Hits are limited to released files.
Graphs are cached per snapshot under cache/graphs, apart from the checkouts
under workspace; workspace must be neither cache nor cache/graphs. A
checkout never replaces a directory it did not publish, such as a graph or
another Git checkout, nor a graph a checkout, so roots that overlap across
providers fail instead of deleting each other's data, and neither root may lie
inside a published graph or checkout. A later run reuses a build, and reports
its original build and checkout-fetch times with cached: true, only when the
build matches: the CodeNib sources, Python and package versions, each graph
backend's command and executable, and the graph settings. A backend that its
launcher loads indirectly, such as python -m, npx, or rustup run, is
identified only by its command, so use a new cache after upgrading one. A
failed or partial build is retried on the next run, and a retry that covers
fewer languages keeps the earlier graph, with the retry's cost and, under
retry, its outcome in the status; set --tool-provider-option
retry_failed=false to keep failures, so that arms sharing a prebuilt cache see
the same index availability.
Each snapshot's status reports released_source_files, the released files in
the graph's languages, and covered_source_files, how many of them the graph
contains. Coverage is incomplete in known cases:
- Java snapshots build no graph, because project preparation is disabled;
- a Go repository builds no graph without a root
go.mod, and a nested Go module that fails to index is left out (see the Go provider notes).
A snapshot without a graph offers the policy only the minimal tools. ARB's indexed tools guide describes the provider interface and what the evaluator enforces.