All posts
Engineering28 min read

Introducing Vorpal

Vorpal parses your code, connects definitions across files, and keeps the results ready for your next question.

Ask an agent to change a function and watch what happens before it makes the edit. It searches for the name, reads the matches, follows an import, opens another file, and works out which callers might break. Several tool calls later, it's reconstructed relationships that were already there in the code.

Then you ask the next question. Much of that work starts again.

That's why I built Vorpal. It parses a repository, resolves relationships between definitions, and keeps them in an index that people and agents can query. Ask who calls a function, or search for code by describing what it does. The same tool supports text search, structural matching, rewriting, and lint rules.

Tree-sitter gives Vorpal the structure of each file; a resolver connects definitions across files; graph and search indexes make that work reusable. A trigram index narrows text searches, while a separate vector index uses exact search for smaller sets and Vamana for larger ones. All 49 Tree-sitter grammars are compiled into one local Rust binary, alongside ast-grep's matching and substitution capabilities. You can use it from a shell or give an agent access through MCP. The source is open.

Answering a question

Download the binary for your platform. There is no archive to unpack and no Node.js or Rust toolchain to install.

On macOS with Apple Silicon:

mkdir -p "$HOME/.local/bin"
curl -fL https://github.com/hyper-light/vorpal/releases/latest/download/vorpal-macos-arm64 \
  -o "$HOME/.local/bin/vorpal"
chmod +x "$HOME/.local/bin/vorpal"
export PATH="$HOME/.local/bin:$PATH"
vorpal --help

The export makes Vorpal available in this shell. Add ~/.local/bin to your shell's PATH if you want it available in new terminals too. Intel macOS, Linux, and Windows binaries are on the same releases page.

Then try it on Vorpal itself:

git clone https://github.com/hyper-light/vorpal.git
cd vorpal

vorpal index .

vorpal graph callers resolve_import_path \
  --path crates/resolve/src/resolver.rs \
  --format json

vorpal graph implementors FileExtractor
vorpal search "stat manifest change detection" -k 5 --format json

Suppose you're changing how Vorpal resolves imports. The first query finds callers of the resolve_import_path definition in resolver.rs; the path rules out other definitions with the same name. The next two find implementations of the extraction interface and code that detects changed files.

Vorpal returns resolve as a caller of resolve_import_path, OutlineExtractor as an implementation of FileExtractor, and FileStat for the change-detection search. CLI examples.

vorpal index . writes to ./.vorpal/index. Queries read that location relative to your current directory. If you index a different directory, pass its index path with --index.

Why Tree-sitter matters

Consider these two pieces of C:

void *buffer = kmalloc(
    requested_size,
    GFP_KERNEL
);

const char *example = "kmalloc(requested_size, GFP_KERNEL)";

If you're changing allocations, the first example matters. The second is just a string. A text search still leaves you sorting real calls from examples and checking for calls spread over several lines. That's tedious when you need to change every two-argument call in a large C repository.

Tree-sitter parses the file into a concrete syntax tree. Calls, arguments, strings, comments, and definitions have distinct nodes and source locations, regardless of how they're formatted. It also tolerates incomplete syntax, so you can query code while you're editing it.

Vorpal uses ast-grep to match and rewrite that structure. Here is how you ask for two-argument calls to kmalloc from a C repository:

vorpal run -l c \
  -p 'void example(void) { kmalloc($A, $B); }' \
  --selector call_expression .

$A and $B match the two argument expressions. The surrounding function tells the C parser that this is a call; --selector call_expression selects just the call as the pattern. Keep it single-quoted so your shell doesn't expand $A and $B.

That finds a call and its arguments, but which function does it call? Two files can use the same name for different things; a method call can depend on the receiver's type. Answering that takes a resolver.

How it works

The indexer extracts definitions and references from each file, then resolves them across files. It stores the connections in a graph, so a callers query can read the incoming edges instead of searching the source again.

VORPAL / SYSTEM MAP

How Vorpal works.

  1. Files. Read source files and the repository's change history.
  2. Parse. Tree-sitter finds definitions, references, and call sites in each file.
  3. Resolve. Match references to definitions across files, recording evidence and confidence for each match.
  4. Index. Store the graph, cache each file's results, and build search indexes.
  5. CLI / MCP. Find definitions, follow links, and read source through the command line or an MCP server.
Parsing finds definitions and references; resolution connects them across files. CLI and MCP queries read those connections from the stored graph.Vorpal reparses edited files and reuses cached results for files you haven’t changed.

Extract facts once

Most edits touch a small part of a repository. The indexer checks what's changed and reuses cached extraction results for everything else. Changed files go through Tree-sitter, language-specific definition rules, and a tree walk that collects calls, references, imports, and type information.

Files are processed in parallel, with limits on queued work and temporary parsing memory. The completed graph still grows with the repository; the kernel has millions of nodes. Indexer implementation.

Resolve across files

Two functions named save aren't interchangeable. To work out which one a call refers to, the resolver checks its scope, the file's imports, visible definitions, and the receiver's type where known.

The source doesn't always give us a definite answer. Vorpal marks results as exact, constrained, heuristic, or unresolved, so an agent can distinguish a known binding from a candidate it needs to check. Resolver implementation.

This work is language-specific. Compiling a grammar into the binary gives us a parser, not complete resolution for that language. Language support.

Store the graph

After resolving A calling B, Vorpal indexes both directions. You can ask “what does A call?” or “who calls B?” without scanning every edge in the graph.

Nodes have compact integer IDs and their fields are stored in columns. The index is memory-mapped, so queries don't need to copy it all into application memory. External IDs are derived from the file path and entity path using BLAKE3. They give callers stable identifiers; the smaller internal IDs handle lookups within a generation.

Edges use compressed sparse row storage: a node's offsets identify its slice in a flat array of targets.

neighbors(v) = targets[offsets[v] : offsets[v + 1]]

If a function has three recorded callers, the lookup reads those three edges, not the rest of the codebase. After locating the node, the work scales with the relationships you follow. A traversal of the whole graph still takes O(V + E), and returning source evidence adds reads and serialization. Graph storage.

Update without mixing old and new data

Queries shouldn't have to wait while a saved file is indexed.

Vorpal writes a new generation while queries continue reading the previous one. Each generation has a content-derived ID. Extraction results and graph data are grouped into buckets; unchanged buckets are hard-linked into the new generation instead of copied. Once it's written, CURRENT atomically switches to it. Each query reads one consistent version, identified in its MCP answer.

Vorpal also checks that the search indexes match the source generation. If they don't, it takes a slower query path or reports the feature as unavailable rather than mixing old and new data. Index format.

Why run a server?

After finding a function's callers, you'll probably want to read one, follow its dependencies, or find another implementation. A cached syntax tree saves the next parse, but each question can still repeat the tree walk, name resolution, and collection of callers.

Vorpal's index already contains those relationships, so even a one-shot CLI query avoids rebuilding them. Keeping the process running also avoids reopening the index and warming search data for every request. The server watches for edits and refreshes the index as you work. With MCP, your client starts it locally and communicates over standard input and output. There's no service to deploy.

For Q questions, the costs look roughly like this:

Repeated analysis ≈ Q × (P + A)
Parse cache       ≈ P + Q × A + U_cache
Persistent index  ≈ I + W + Q × (L + q) + U_index

Q is the number of questions. P is parsing, and A is the remaining analysis needed for each answer. For the stored index, I is the initial build, W is warm-up, L is request overhead, and q is the query itself. U_cache and U_index cover updates as files change.

The parse cache removes repeated parsing, but leaves the Q × A term. The persistent index does reusable analysis during the build and updates, leaving a much smaller lookup per question. A cache that also stores resolved relationships gets the same kind of benefit. Literal grep is a different workload and doesn't need a parser.

A fast query isn't much use if it's answering from stale code. Here's what building, querying, and updating the kernel index takes:

OperationPublished time
Cold index, 75,954 parsed files8.1 s
Re-index with nothing changed0.13 s
Re-index after a function-body edit0.4 s
Warm MCP graph query, median round trip0.13 ms
Warm MCP default search, median / p950.8 ms / 2.1 ms
First search, including ranking warm-up0.19 s
Save → daemon answer reflects a body edit2.0 s

The initial build takes 8.1 seconds, but checking an unchanged index takes 0.13 seconds and re-indexing a function-body edit takes 0.4 seconds. The delay you'll notice after saving is longer: change detection and refresh bring save-to-answer time to 2.0 seconds in this test, with individual saves ranging from 1.9 to 4.9 seconds. Indexing and serving benchmarks.

Reuse work inside large files

A one-line edit in a huge generated file can still be expensive: the indexer has to reparse that file. For files over 1 MiB, the running server can retain Tree-sitter's tree and reuse unchanged parts. For C, it also reuses extraction results outside the edited definition:

Per-save workloadFresh parse + extractionIncremental parse + walk splice
54 MB generated C, edit between definitions4.2 s0.7 s
Same file, edit inside its 43 MB definition4.2 s1.7 s
1.4 MB CPython Parser/parser.c107 ms17 ms

An edit between definitions in the 54 MB file takes 0.7 seconds instead of 4.2. Editing inside its 43 MB definition still requires walking that definition again, bringing the time to 1.7 seconds. The smaller Parser/parser.c case drops from 107 ms to 17 ms.

Cached source and extraction snapshots have a default 256 MiB budget; retained syntax trees use additional memory. Extraction reuse currently ships for C and falls back to a full walk if its checks fail. Each benchmark result was checked against full re-extraction. Incremental parsing benchmarks.

A search for kmalloc can skip files that don't contain that text; the trigram index rules them out before parsing. Recorded statement boundaries narrow the remaining work, and repeated patterns reuse unchanged results. Some simple call patterns can be answered from stored calls and argument counts alone. Vorpal falls back to a full parse when those shortcuts can't preserve the result. Structural search.

How search ranks results

The import-resolution example started with a function name. Often you don't know the name. You're looking for code that detects changed files, but it might be called a watcher, a manifest, or a stat cache. Search gives you a starting point; graph queries let you follow its relationships.

Vorpal combines several result lists. Name matching finds definitions by name or name tokens. Lexical vector search compares words from names, signatures, and file basenames in 256 dimensions. A third list ranks name-matched candidates by how often other code references them. If name matching and BM25 find nothing, body-text search can find candidates inside definitions. Search implementation and vector generation.

The default doesn't need a neural model. The optional tiers can learn from the repository's vocabulary or use a pretrained encoder.

VORPAL / REPRESENTATION

From words to a vector.

256 dimensions · no training
  1. 01Split identifiers
  2. 02Two signed hashes
  3. 03Normalize the vector

Words become signed features.

resolve_import_path and resolveImportPath split into the same three words. Each word contributes to two signed buckets in a 256-dimensional vector. The highlighted cells show this query’s buckets.

v[b] += sign(token) → v̂ = v / ‖v‖₂

Definition vectors use the name twice, then the signature and file basename. This matches shared words; it doesn't infer the meaning of an unfamiliar phrase.

resolve [163] −1resolve [144] −1import [244] +1import [82] +1path [118] −1path [1] −1
The GPU illustrates the computation; it isn’t required for lexical hashing. Lexical buckets are calculated from the example query. Learned and neural signals are illustrative. Activity density is not measured GPU utilization or model output.

The lexical embedder turns resolve_import_path into resolve, import, and path. It splits punctuation and camel-case boundaries, lowercases the tokens, and sends each into two signed hash buckets. It then L2-normalizes the vector to unit length, so repeating every word doesn't increase its magnitude. Lexical embedder.

Hashing is cheap, but it doesn't learn which terms your project uses together. The learned tier trains on words and three-to-six-character fragments from repository definitions. Positive pointwise mutual information, or PPMI, measures their co-occurrence; a truncated singular value decomposition, or SVD, compresses that matrix into fewer dimensions.

To limit the influence of common tokens, the model removes dominant directions and uses weighted pooling. The corpus determines the dimension, up to 256, and confidence-graded code-graph relationships can further refine stored document vectors. This repository-specific training is separate from the neural encoder. Learned embedding implementation.

The neural encoder, CodeRankEmbed, produces 768-dimensional vectors by taking the contextual output for the CLS token and L2-normalizing it. Queries receive the prefix Represent this query for searching relevant code: . The background embedding index can include a leading comment and the first paragraph of a definition's source, so it can find matches beyond the name. The reranker uses a shorter name, signature, and basename description. Encoder and document input construction.

The code graph and Vamana serve different purposes. The code graph records relationships such as calls; Vamana connects nearby vectors to speed up search. Being neighbors in Vamana doesn't mean two functions call each other. The neural document index uses a separate quantized scan and rescoring path.

A vector distance and a reference count don't share a unit, so adding their raw scores wouldn't make sense. Vorpal combines the result lists by position instead, using reciprocal rank fusion:

RRF(d) = Σ [1 / (60 + rank_c(d))]
         c where channel c returned d

Vorpal uses zero-based ranks, so first place contributes 1/60. A definition that comes first in one list and sixth in another receives 1/60 + 1/65 ≈ 0.03205; one that appears only first in a single list receives about 0.01667. Absence contributes nothing. Support from several lists can beat a single strong match. Each result retains its ranks so you can check the calculation. Optional dense-search settings can change a list's weight. Fusion implementation.

VORPAL / RECIPROCAL RANK FUSION

From ranked lists to one result order.

Read the ranks. Add their contributions. Sort the totals.

Three search methods nominate results. Keep each result’s original rank, starting at zero.
Inspect the ranks and calculations
Illustrative ranks, starting at zero. Sorted by the computed RRF total.
SymbolNameVectorGraphTotal
loadConfig (C)1200.04919
parseConfig (A)0510.04844
readFile (B)00.01667

loadConfig1/61 + 1/62 + 1/60 = 0.04919

parseConfig1/60 + 1/65 + 1/61 = 0.04844

readFile0 + 1/60 + 0 = 0.01667

Values are rounded for display; totals use the unrounded contributions.

loadConfig ranks first overall. readFile leads the vector list, but loadConfig receives strong contributions from all three. This example uses equal list weights and k = 60.The graph list ranks name-matched candidates by references from other code. Other enabled lists can also contribute; an optional neural reranker runs after fusion. See the fusion implementation.

The neural reranker keeps the fused top result and reorders the remaining candidates. It can't find a definition that's missing from the candidate set. The separate background embedding index can retrieve additional candidates.

The encoder adds a download, memory use, and query time. In our tests, it improved results on CPython and Vorpal but scored lower on the kernel. Retrieval results.

To test which tier works best on your code, put one query => expected-name-or-path pair per line in a file, then run:

vorpal tune --queries my-queries.txt

With expected answers, tune checks whether a tier improves retrieval before enabling it. Without them, it shows the comparisons and leaves your settings alone. Ranking tiers and evaluation.

The benchmarks

On the Linux kernel, Vorpal builds an 8.89-million-node graph in 8.1 seconds. codebase-memory-mcp takes 296 seconds on the same checkout. That's nearly five minutes down to eight seconds, using about a fifth of the peak memory and less than a third of the disk space. A warm graph query takes 0.13 milliseconds.

We also measured updates, queries, and the time and cost of answering questions through an agent.

We ran these benchmarks on an Apple M5 Max with 18 cores and 128 GB RAM, macOS 26.4.1, and rustc 1.98.0. The main indexing and competitor comparisons used v0.9.0 on September 7, 2026. Broader language and optional-tier results include September 5–6 runs; the agent experiments use v0.8.3. The kernel checkout is 1590cf032971, and CPython is b86a41cbf63. Test setup and repository revisions.

Here, “cold” means building a new index, not flushing the operating system's file cache. Vorpal's cold indexing times are the best of three runs on a quiet machine; tgrep's are medians of three. CLI times include process startup. Daemon times measure the round trip from the client to an already-running process.

VORPAL / COLD INDEX

Time to index a codebase.

Vorpal and codebase-memory-mcp. Less time is better.

Linux kernel

75,954 files parsed by Vorpal
36.5× faster
Vorpal
8.1 s
cbm
296 s

CPython

3,841 files parsed by Vorpal
42.8× faster
Vorpal
0.9 s
cbm
38.5 s

Vorpal

1,917 files parsed by Vorpal
6.2× faster
Vorpal
6.9 s
cbm
43.1 s
The wait before your first code-graph query. Each pair has its own zero-based linear scale; compare lengths within a pair.Published September 7, 2026 · M5 Max · 18 cores · 128 GB RAM. Vorpal v0.9.0; cbm 997d087, full mode. Measurements and methods.

Against another code graph

codebase-memory-mcp is the closest comparison: another local binary combining Tree-sitter, a typed code graph, search, and MCP. We built commit 997d087 from source and used its full mode, including semantic edges, on the same machine and checkouts.

RepositoryOperationVorpalcodebase-memory-mcp
Linux kernelCold index8.1 s296 s
Linux kernelNothing changed0.13 s14.2 s
CPythonCold index0.9 s38.5 s
CPythonNothing changed0.02 s5.2 s
Vorpal, including vendored grammarsCold index6.9 s43.1 s
Vorpal, including vendored grammarsNothing changed0.02 s5.4 s

Cold builds are roughly 37× faster on the kernel, 43× on CPython, and 6× on Vorpal's own repository. When nothing's changed, checking the index takes 0.13 seconds on the kernel and 0.02 seconds on the other two—about 109×, 260×, and 270× faster, respectively. An editor or agent can check for changes without waiting several seconds each time.

The build uses less memory, too:

RepositoryMeasurementVorpalcodebase-memory-mcp
Linux kernelNodes8.89 M8.53 M
Linux kernelPeak indexing RSS6.1 GB30.8 GB
Linux kernelIndex on disk4.8 GB15.8 GB
CPythonNodes162,945136,118
CPythonPeak indexing RSS0.7 GB6.5 GB
CPythonIndex on disk160 MB632 MB
VorpalNodes80,61167,797
VorpalPeak indexing RSS11.6 GB31.8 GB
VorpalIndex on disk860 MB297 MB

On the kernel, Vorpal cuts peak indexing memory by about 80% and disk use by 70%, leaving more room for your editor, compiler, and other checkouts. Node counts show what each tool built, but don't establish identical coverage: the extractors and representations differ. These disk figures precede Vorpal warming its additional search tiers; cbm's RSS includes its process tree, sampled every 50 ms.

Each CLI query still has to start a process and open the index. The server avoids that cost:

Kernel queryVorpal CLIcbm CLIVorpal daemon
Search0.15 s5.6 s0.7 ms
Callers of a symbol0.01 s3.6–4.1 s0.1 ms

Vorpal's CLI search is about 37× faster, including process startup. The daemon cuts that 0.15-second request to 0.7 ms. The CLI works well for one-off questions; a running server saves more on repeated requests. A separate serving run measured a 0.8 ms median across multiple default kernel searches.

cbm supports 162 grammars to Vorpal's 49 and produces a smaller on-disk index for Vorpal's own repository. Check language support first: a faster build doesn't help if it can't parse your code. Full comparison.

Across languages and repository sizes

These full graph builds cover more languages and repository sizes. “Files parsed” counts only files handled by a grammar:

RepositoryMain languageFiles parsedNodesCold buildUnchanged
Linux kernelC75,9548,891,7718.1 s0.13 s
LLVMC++86,1241,444,0287.3 s0.34 s
ZigZig17,0251,085,5675.6 s0.04 s
KotlinKotlin75,448795,7192.5 s0.43 s
KubernetesGo26,641692,8281.9 s0.09 s
RoslynC#19,522490,2841.9 s0.08 s
RustRust41,607464,0642.5 s0.09 s
WordPressPHP4,195286,8241.7 s0.02 s
SparkScala11,512253,7531.5 s0.06 s
KafkaJava7,246209,1310.7 s0.04 s
Next.jsTS / JS27,216204,7540.9 s0.25 s
GHCHaskell15,837178,2590.6 s0.05 s
CPythonPython / C3,841162,9450.9 s0.02 s
RailsRuby3,95249,6350.3 s0.03 s
NeovimC / Lua1,47640,5070.2 s0.01 s
VueVue / TS62611,1910.1 s0.01 s
Vorpal, with vendored grammarsRust / generated C1,91780,6116.9 s0.02 s

Vorpal's own checkout has only 1,917 parsed files, yet takes 6.9 seconds. One is a 33 MB generated parser.c. Other files finish in parallel, but the build still has to wait for that parser. LLVM spreads its work across more than 86,000 parsed files and finishes in 7.3 seconds. File count alone won't predict build time; one huge file can dominate it. Repository revisions and test dates.

You don't need a code graph to find text. tgrep 1.0.4 provides trigram-indexed grep through a server. Vorpal also parses syntax and resolves relationships; here's what that extra work costs.

VORPAL / INDEXED TEXT SEARCH

Vorpal and tgrep.

Same 0–10 s scale for every repositoryLower is better

Wall time for a new index, including process startup.

Linux kernel

Vorpal
Lowest8.1 s
tgrep
8.2 s

CPython

Vorpal
0.9 s
tgrep
Lowest0.54 s

Vorpal

Vorpal
6.9 s
tgrep
Lowest0.69 s
All measured results 18 values
Vorpal and tgrep — all indexing measurements
RepositoryMeasurementVorpaltgrep
Linux kernelCold build8.1 s8.2 s
Linux kernelPeak RSS6.1 GB0.31 GB
Linux kernelDisk4.8 GB1.0 GB
CPythonCold build0.9 s0.54 s
CPythonPeak RSS0.7 GB0.14 GB
CPythonDisk160 MB74 MB
VorpalCold build6.9 s0.69 s
VorpalPeak RSS11.6 GB0.26 GB
VorpalDisk860 MB28 MB
Compare build time, peak RAM, and disk usage. Each metric uses the same zero-based scale across repositories.tgrep indexes text. Vorpal also parses syntax and connects definitions. On the kernel, tgrep indexed 94,719 files; Vorpal parsed 75,954.September 7, 2026 · Vorpal v0.9.0 and tgrep 1.0.4, built from source on the same machine and checkouts. Vorpal build times are the best of three; tgrep times are medians of three. Benchmark comparison.

tgrep's text index uses much less memory and disk, and covers more of the kernel checkout: 94,719 files versus Vorpal's 75,954 parsed files. Vorpal's extra indexing work pays off when you ask for a call expression or a function's callers, rather than matching lines:

Kernel workloadVorpaltgrep
102-query text-search suite, median per query12.8 ms, lines with their containing symbol21 ms, text lines
kmalloc search35 ms, 2,715 two-argument call expressions with their functions18 ms, 3,387 lines matching kmalloc\(
Callers of vfs_read0.10 ms, 3 call edges with call sites7.7 ms, 13 matching lines
Save a file, then query the updated kernel clone4.3 s4.4 s

For ordinary text search, Vorpal is about 1.6× faster across the 102-query suite and includes the symbol containing each matched line. Every query was checked against an exhaustive text scan, so this is a direct comparison of text results.

Thirteen lines mentioning vfs_read still need to be inspected for calls; Vorpal returns three resolved call edges with their call sites. The kmalloc query returns two-argument call expressions rather than regex-matching lines. Those aren't equivalent result sets, so their timings aren't a like-for-like comparison. Vorpal's ranked definition search takes 0.65 ms here, but finds definitions rather than replacing exhaustive grep.

The save-to-answer test includes change detection under a busy filesystem event service. With that service quiet, Vorpal took 2.0–2.2 seconds. That's the wait until a query reflects your edit, not the query's execution time. tgrep comparison.

The trigram index also speeds up structural search by skipping files that can't contain a pattern's literal text:

Structural queryBefore the text indexWith the text index
code_search kmalloc($A, $B)4.3 s35 ms, 2,715 calls
structural_search kmalloc($A, $B)4.1 s, stopped at 100 results51 ms, all 2,715 calls
code_search $R = schedule_timeout($A)4.4 s45 ms
code_search kfree($A)Not reported90 ms, 40,499 calls
code_search if ($C) return $X;Not reported3.7 s first call; 0.4 s repeated

The kmalloc code search drops from 4.3 seconds to 35 ms, about 123× faster. Structural search returns all 2,715 matches in 51 ms; previously it took 4.1 seconds and stopped at 100. You get the full result set instead of a small slice of the repository.

Patterns with little literal text still require more work. The broad if pattern has 381,811 matches and takes 3.7 seconds on first use, then 0.4 seconds with cached parse results. These results are medians of three calls to one daemon. Structural search results.

A scan without the warm daemon shows the cost of parsing syntax:

ToolTimeResult
vorpal scan1.5 s warm; 2.5 s first run6,819 matching call-expression nodes
ripgrep0.8 s3,387 matching text lines

Across 63,775 C files, the structural rule finds call expressions containing kmalloc, including kmalloc_array, devm_kmalloc, and outer calls wrapping them. The grep finds lines containing kmalloc(. If you only need lines, grep is quicker. To inspect or rewrite expressions, you'll need the syntax the structural scan returns. Scan comparison.

Query latency, warm-up, and memory

The learned and neural tiers increase search latency and resident memory. We measured 30 client-side stdio round trips per tool, sampling resident memory after each call:

Index / tierSearch medianSearch p95First searchGraph queryPeak RSS
Kernel / default0.8 ms2.1 ms0.19 s0.13 ms2.1 GB
Kernel / learned2.1 ms2.7 ms0.22 s0.18 ms2.4 GB
Kernel / learned + f1636 ms319 ms0.69 s0.13 ms3.0 GB
Kernel / learned + f3236 ms324 ms0.59 s0.13 ms2.9 GB
CPython / default0.3 ms0.7 ms5 ms0.13 ms110 MB
CPython / learned1.4 ms1.7 ms11 ms0.14 ms154 MB
CPython / learned + f1636 ms253 ms0.37 s0.15 ms748 MB
CPython / learned + f3235 ms256 ms0.29 s0.15 ms658 MB
Vorpal / default0.3 ms0.5 ms3 ms0.10 ms65 MB
Vorpal / learned1.3 ms1.4 ms6 ms0.12 ms79 MB
Vorpal / learned + f1635 ms218 ms0.42 s0.13 ms652 MB
Vorpal / learned + f3235 ms226 ms0.33 s0.13 ms561 MB

Default search medians are below a millisecond on all three codebases. The learned tier adds a little time and memory; the encoder adds considerably more. Its roughly 35–36 ms median benefits from cached query embeddings. A new query costs more, reflected in the p95 of roughly 0.2–0.3 seconds. Graph queries don't use the encoder, so following callers stays fast in every tier.

f16 doesn't mean half the RAM: the download is smaller, but the model decodes to f32. These runs include background embedding work, complete on CPython and Vorpal and capped at ten minutes on the kernel. First-search timings have the weights and index in the OS page cache; they aren't measurements of the first launch after a reboot.

Warmed search tiers add disk space beyond the cold-build index.

VORPAL / RESOURCE USAGE

Memory and disk usage.

Repository 1 / 3

Linux kernel

Peak resident memory4 GB chart scale
Default
2.1 GB

25 MB per cell. Partial cells show the remaining MB.

All measured results 18 rows
All published RAM and disk measurements
RepositoryResourceTierUsage
Linux kernelRAMDefault2.1 GB
Linux kernelRAMLearned2.4 GB
Linux kernelRAMLearned + f163.0 GB
Linux kernelRAMLearned + f322.9 GB
Linux kernelDiskDefault8.1 GB
Linux kernelDiskLearned8.5 GB
CPythonRAMDefault110 MB
CPythonRAMLearned154 MB
CPythonRAMLearned + f16748 MB
CPythonRAMLearned + f32658 MB
CPythonDiskDefault210 MB
CPythonDiskLearned280 MB
VorpalRAMDefault65 MB
VorpalRAMLearned79 MB
VorpalRAMLearned + f16652 MB
VorpalRAMLearned + f32561 MB
VorpalDiskDefault880 MB
VorpalDiskLearned910 MB
Filled cells show the memory and disk space used to keep this repository searchable. The 4 GB RAM and 10 GB disk limits are fixed chart scales, not the machine’s capacity.Peak RAM is sampled after each of 30 stdio MCP round trips per tool. Encoder runs include background embedding. Disk covers one committed index generation after warming search.Encoder files add 274 MB (f16) or 547 MB (f32) on disk, shared across repositories. Memory and storage results.

Adding another checkout needs another index, not another copy of the model. Serving and storage results.

Finding the right result

You shouldn't need to know a function's name to find it. The search benchmark tests whether Vorpal returns the relevant code when you describe what you're looking for, and how high it appears in the results.

The evaluation uses 54 kernel queries, 54 CPython queries, and 55 Vorpal queries across six classes, from exact names to paraphrases. Relevance labels cite source lines, so you can check why a result counts as a match.

Normalized discounted cumulative gain, or NDCG, measures how well those matches are ranked:

DCG@10  = Σ (2^g_i − 1) / log2(i + 1), for i = 1 … 10
NDCG@10 = DCG@10 / ideal_DCG@10

g_i is the relevance grade at position i, starting at one. More relevant answers earn more points; the logarithmic denominator reduces the points for results further down the list. Dividing by the best possible ordering gives a score between zero and one. It's a ranking score, not the percentage of questions answered correctly. Reported scores are averaged across queries. Evaluator.

MRR measures how high the first relevant result appears. Recall@5 measures how much of the labeled relevant set appears in the first five results.

VORPAL / RETRIEVAL QUALITY

Finding the right result.

54 labeled queries · 1 / 3

Linux kernel

NDCG@10Higher is better
Default
Highest0.329
Learned
0.315
Learned + encoder, f32
0.295

Rewards relevant results near the top, relative to the best possible ordering.

All measured results 9 rows
Retrieval quality — all corpora and tiers
CorpusTierNDCG@10MRRRecall@5
Linux kernelDefault0.3290.3270.358
Linux kernelLearned0.3150.3040.361
Linux kernelLearned + encoder, f320.2950.2900.302
CPythonDefault0.3060.2910.333
CPythonLearned0.3410.3220.389
CPythonLearned + encoder, f320.3510.3310.426
VorpalDefault0.4020.3950.445
VorpalLearned0.4300.4270.455
VorpalLearned + encoder, f320.4550.4480.500
Which tier finds the code you need? The highlighted tier leads for the selected repository and metric. All scores share a zero-to-one scale.54 kernel, 54 CPython, and 55 Vorpal queries. Default: September 7; learned and encoder: September 6. Measured before background embedding fill. Benchmark results.

On CPython, the encoder raises NDCG@10 by about 15% and recall@5 by 28% over the default: more of the relevant code reaches the first five results. Vorpal's own repository gains about 13% on NDCG@10. On the kernel, the default leads on NDCG and MRR; the learned tier gains a little recall. The encoder doesn't justify its extra cost for those kernel queries.

These measurements precede the encoder's background embedding fill. In those runs, descriptive kernel queries and paraphrases remained weak; the reranker can't fix a missing candidate. The default column is from September 7, after body-text candidates were added; learned and encoder columns are from September 6. Use vorpal tune to test your own questions. Retrieval results and labels.

What an agent saves

An agent that gets callers and call sites in one answer doesn't need more rounds of searching and reading to reconstruct them. Fewer rounds also mean less context sent through the model.

First, we compared a warm daemon request with the ripgrep-and-read operations behind the agent's built-in tools. These September 5 results use v0.8.3, with Vorpal times taken as medians of five requests after an initial call:

QuestionVorpalGrep + ReadWhat comes back
Vorpal: callers of tool_result0.10 ms16 ms2 call-site records vs 3 lines
Vorpal: callees of tool_result0.14 msNo direct equivalent7 call-site records
Vorpal: what run_install reaches0.05 ms, one call6 ms4 records vs 76 lines
Vorpal: source of render_toml0.05 ms39 ms, two commandsVerified body vs 58 lines
Kernel: callers of schedule_timeout_interruptible2.7 ms748 msFirst 100 of 140 resolved records vs 164 lines
Kernel: callers of vfs_read0.10 ms692 ms3 call-site records vs 13 lines
Kernel: callees of vfs_read0.11 ms763 ms, then read the body4 call-site records vs 41 lines
Kernel: find schedule_timeout0.05 ms677 ms1 definition vs 1 line
Kernel: source of vfs_read0.08 ms713 ms, then a readVerified body vs 42 lines
Kernel: what vfs_read reaches, depth two0.06 msNo direct equivalent3 records

For run_install, one graph query returns four records instead of 76 lines to interpret. Source lookup returns a verified function body, so the agent doesn't have to find its boundaries in a larger read. Fewer records don't guarantee completeness, though: the graph can miss relationships, and some graph results are larger than the grep output.

Then we timed the agent answering the questions. On September 6, Claude Code 2.1.261, running Opus 5 at high effort, answered four questions using Grep + Read, Vorpal MCP, and Vorpal CLI. Each question ran four times per setup; the results are medians of the last three runs with a warm prompt cache.

VORPAL / AGENT TASKS

Less work for the agent.

Vorpal repository · 1 / 4

Callers of tool_result

TimeLower is better
Grep + Read
9.4 s
Vorpal MCP
5.9 s
Vorpal CLI
Lowest5.3 s

Wall-clock time for the agent to finish the question.

All measured results 12 rows
Agent questions — all access paths
QuestionAccessModel turnsTokens processedBilled costWall time
Vorpal repository: Callers of tool_resultGrep + Read573 K$0.0819.4 s
Vorpal repository: Callers of tool_resultVorpal MCP363 K$0.0545.9 s
Vorpal repository: Callers of tool_resultVorpal CLI243 K$0.0285.3 s
Vorpal repository: What run_install reachesGrep + Read477 K$0.13611.8 s
Vorpal repository: What run_install reachesVorpal MCP364 K$0.0457.0 s
Vorpal repository: What run_install reachesVorpal CLI244 K$0.0407.9 s
Linux kernel: Callers of vfs_readGrep + Read5104 K$0.20016.6 s
Linux kernel: Callers of vfs_readVorpal MCP351 K$0.0426.9 s
Linux kernel: Callers of vfs_readVorpal CLI236 K$0.0296.1 s
Linux kernel: Callees of vfs_readGrep + Read359 K$0.0538.2 s
Linux kernel: Callees of vfs_readVorpal MCP351 K$0.0465.7 s
Linux kernel: Callees of vfs_readVorpal CLI236 K$0.0265.8 s
Same question, three ways to reach the code. Compare the agent's time, tokens, cost, and turns. Each metric keeps the same zero-based scale across all four questions.September 6, 2026 · Vorpal v0.8.3 · Claude Code 2.1.261, Opus 5, high effort. Medians of the last three of four runs; prompt cache warm. Benchmark results.

For the kernel callers question, MCP reduced processed tokens by 51%, billed cost by 79%, and wall time by 58%. The CLI reduced them by 65%, 86%, and 63%, taking two model turns instead of five with Grep + Read.

In this client, first use of an MCP tool takes a separate turn to load its schema. The shell tool is already available, so the CLI avoids that turn. Token counts include input and cache reads and writes, but exclude output; cached input still contributes to the bill.

After an idle hour, the first question incurred extra cache-write costs of $0.09–$0.14 for grep, $0.10–$0.13 for MCP, and $0.17–$0.22 for the shell setup. The prompt prefixes differed in size and how much was cached. Those costs aren't included in the warm-cache savings above.

The transcripts show mistakes on both sides. Grep found two inline helpers in vfs_read missing from the graph, plus a transitive call inside a struct literal. But on the Vorpal callers question, grep-based runs named the enclosing function correctly in only one of six recorded runs. The model also correctly rejected some constrained graph candidates. That makes the source evidence and confidence grades worth checking, whichever route finds the code. Agent experiments and transcripts.

Vorpal reduced measured wall time on all four questions. Most of the remaining time was in the model, with fewer turns spent finding and interpreting the code.

Using MCP

To give your agent the same queries, add Vorpal to its MCP configuration. For clients using JSON, replace the path below with your repository's absolute path:

{
  "mcpServers": {
    "vorpal": {
      "command": "vorpal",
      "args": [
        "mcp",
        "--index",
        "/absolute/path/to/project/.vorpal/index",
        "--profile",
        "analysis"
      ]
    }
  }
}

The client starts the process. If it can't find vorpal on PATH, use the binary's absolute path for command. The analysis profile exposes search and graph queries, leaving out the explicit index tool and full structural tools. It still updates the index as files change.

With the <project>/.vorpal/index layout, Vorpal can build a missing index and watch the repository automatically. A custom location outside that layout needs explicit refreshes. For automatic client setup, vorpal mcp install --dry-run previews the changes without applying them. MCP setup.

To find callers of resolve_import_path in the Vorpal checkout, the agent calls graph with these arguments:

{
  "relation": "callers",
  "name": "resolve_import_path",
  "path": "crates/resolve/src/resolver.rs",
  "format": "lean"
}

Change relation to callees to follow calls in the other direction, or to references to find uses of the definition.

The agent can follow a call site or read its source and evidence. The generation identifies the index version used. For paginated results, total, truncated, and nextCursor show what remains and how to fetch it. MCP tools.

Try it on your code

Start with a function you know well. Use the CLI to find its callers, follow a dependency, and check the source behind an edge. Compare the answers with code you know before connecting an MCP client. Dynamic calls, macros, and gaps in a language's extractor can leave missing relationships; the graph can only follow edges Vorpal found.

Updating after an edit should produce the same index as a fresh build. Release checks compare incremental results with fresh builds and check independent builds byte-for-byte. Build checks.

Vorpal keeps the parsing, resolution, and indexing work ready for your next question. You shouldn't have to start over every time.