retrieval
The router is a paragraph of prose
There is no classifier, no embedding of the query against tool descriptions, no orchestration layer. The routing policy is eight lines of text pasted into five tool descriptions, and the model is expected to read it.
This is the most consequential design decision in the repository, and it is also the least
code. A constant named TOOL_ROUTER_NOTE is defined once and interpolated into the
description of files_search, wiki_index, files_neighbors,
wiki_read_all and files_read. Whichever of those five tools the model
is looking at, it sees the whole map — including the tools it should have called instead.
Router (for any agent, cloud or local): • Don't know what's in the wiki yet? → wiki_index (cheap) • Specific question, want best snippet? → files_search (mode=vector default; "keyword" for exact) • Need a page + everything connected to it? → files_neighbors (Zoom-Out) • Know the path? → files_read • Cloud agent, big context, audit-style "everything about X"? → wiki_read_all • Personal memory / "what did we decide / when"? → memory_recall • "What connects A to B?" (graph) → graph_query / graph_path
Note what each rule keys on. Not the topic of the question — the shape of it, and the state of the agent's own knowledge. “Don't know what's in the wiki yet” is a fact about the conversation, not about the corpus.
src/tools/files.ts:89 and :99.
Search is a subprocess, not a service
files_search does not speak HTTP to a search server. It maps the requested mode to
a subcommand and executes a command-line binary, once per call. The comment in the source is
unusually direct about why:
Backed by QMD CLI (shell-out per call — bypasses upstream MCP transport bug).
src/tools/files.ts:89 — files_search description
A downstream MCP server for the same index exists in the federation configuration and is disabled, with a recorded reason: its HTTP transport returns an error page rather than a valid handshake. Rather than wait for a fix, the tool drops one layer down to the CLI that server wraps. The mode names in the MCP schema and the subcommand names on disk are therefore different vocabularies, mapped explicitly:
MCP mode |
CLI subcommand | Method | Timeout | Stated latency |
|---|---|---|---|---|
keyword | search | BM25, exact match | 15 s | ~50 ms |
vector | vsearch | Semantic embedding search | 30 s | ~200 ms |
deep | query | Query expansion, then LLM rerank | 120 s | 1–2 s |
The output cap is 50 MB per invocation and results default to ten, capped at fifty. Both the query and the mode are recorded in the audit ledger — the query truncated to its first hundred characters.
Degrading in three steps
The failure design is the most carefully written part of the module. A failed search does not
raise; it falls back to a strictly cheaper method, twice, before giving up. The chain is
asymmetric on purpose: deep degrades to vector, anything else degrades to keyword,
and keyword is the floor.
try { text = await callQmd(cliMode, timeouts[cliMode] || 30_000); } catch (err: any) { const fallbackMode = (cliMode === 'query') ? 'vsearch' : 'search'; console.warn('[files_search] ' + cliMode + ' failed: ' + err.message + ', falling back to ' + fallbackMode); try { text = await callQmd(fallbackMode, timeouts[fallbackMode] || 30_000); } catch { console.warn('[files_search] ' + fallbackMode + ' also failed, last resort keyword'); text = await callQmd('search', 15_000); } }
The degradation is invisible to the caller. The model receives results and no indication that it got BM25 when it asked for a reranked semantic search — only the server's stderr records the downgrade. Worth knowing when reasoning about why an answer was thin.
One more piece of defensive plumbing sits immediately after: the CLI's stdout is not assumed to
be clean JSON. The handler slices from the first [ to the last ],
discarding any banner or progress text the binary may have emitted around the payload.
# mode="keyword" → 15s timeout $ qmd search "transport handshake failure" -n 10 --json [{"path": "…", "score": 11.4, "snippet": "…"}, …] # mode="vector" (default) → 30s timeout $ qmd vsearch "why did we choose that transport" -n 10 --json [{"path": "…", "score": 0.83, "snippet": "…"}, …] # mode="deep" → 120s timeout; if it throws, the ladder engages $ qmd query "everything about the transport decision" -n 10 --json error: rerank backend unreachable [files_search] query failed: rerank backend unreachable, falling back to vsearch [{"path": "…", "score": 0.79, "snippet": "…"}, …] # and the graph tools, driven the same way — one process per call $ python -m graphify query "what connects A to B" --graph <wiki>/graphify-out/graph.json --budget 4000 $ python -m graphify path "<node A>" "<node B>" --graph <wiki>/graphify-out/graph.json
Argument vectors are exact, from src/tools/files.ts:111 and
src/tools/graph.ts:45-47. Binary paths, the wiki location and result payloads are
replaced with placeholders.
The lifecycle of one question
Put the pieces in order and a single retrieval looks like this. Note where the boundaries are: authentication happens once per HTTP request, the MCP server object is built and destroyed around it, and the audit row is written before the response is serialised.
src/tools/files.ts:178-180, src/server.ts:100-102).
The client's context window is part of the API
Most tool descriptions describe what a tool does. Two of these describe who should call it. The distinction being drawn is not capability but economics — a hosted frontier model paying per token behaves differently from a quantised local model whose KV cache is a fixed budget.
Cloud agent, large context
wiki_read_allis permitted for audit-style questions — “give me everything about X”.- Cost is roughly 60–100K tokens at the wiki's stated size.
- The default 1 MB cap is a safety valve, not a target.
Local agent, 128K context
wiki_read_allis explicitly discouraged: it “fills KV cache, slows decode ~30–45%”.- Use
files_searchthenfiles_neighborsinstead. files_neighborsis documented as staying “well within KV cache budget”.
A claim, not a measurement
The 30–45% decode slowdown and the 60–100K token estimate appear as assertions in the tool
descriptions at src/tools/files.ts:302-306. There is no benchmark in the
repository that produces them. They are reproduced here because they are what the model is
told — they shape behaviour whether or not they are precise.
Escalation, not selection
Read the router as a ladder rather than a menu and the design intent becomes clear. The agent is expected to start at the bottom and climb only when the cheap rung fails to answer. Every rung returns something the next rung can use: an index yields paths, a search yields a slug, a slug yields a neighbourhood, a neighbourhood yields the names that make a graph query answerable.
Default loop for any wiki question: files_search to find the page → files_neighbors to expand context → answer. Only escalate to wiki_read_all on cloud agents for true audit questions.
src/server.ts:100-102 — client instructions, condensed
Queries shown in the terminal are illustrative; no real query, result or wiki path appears on this site. Binary locations and the wiki root are replaced with placeholders.