/rag

A retrieval layer that never phones home

Embeddings, keyword search and rank fusion, all on your CPU. This is what happens to every prompt before it reaches the model — and what happens when any part of it is missing.

0.927

MRR — skills

0.836

MRR — MCP tools

120 MB

model, downloaded once

3

corpora, one engine

── the pipeline

Two scorers, one fusion

Neither half is trusted alone: embeddings catch the paraphrase, keywords catch the identifier embeddings smooth away, and reciprocal rank fusion settles the argument.

01

Dense — e5-small

Quantized ONNX through fastembed, multilingual, 384 dimensions. It runs on a blocking thread, so the event loop never waits on an embedding.

02

Lexical — TF-IDF

Snowball stemming for English and Russian, so release, releasing and «релиз» collapse onto the same stem before scoring.

03

Fusion — RRF

Reciprocal rank fusion over both rankings, then a cosine floor (min_dense_score = 0.80) for candidates with no keyword overlap. Top 3 per corpus, per turn.

semantic/index.rs — the shape of a hit

// dense + lexical, fused by reciprocal rank
skill      releasing            dense 0.93  kw 0.71  → rrf 1
mcp        playwright.click     dense 0.88  kw 0.55  → rrf 2
history    2026-08-29 checkpoints  dense 0.81  kw 0.64  → rrf 3

── three corpora

One engine, three things worth remembering

Skills, tools and your own history are indexed the same way and searched by the same code — the difference is only what a hit is allowed to do next.

/skills

Skills

Markdown instruction sets discovered across a dozen agent directories. A match rides along as a hint — «this skill may apply, load it with the skill tool» — so capability discovery isn’t your job.

/mcp · tool_search

MCP tools

Every connected server’s tools are indexed by name and description. Matches get their full schema inlined; everything else stays a one-line summary until it is asked for.

/search · history_search

Conversation history

Sessions are chunked, embedded and persisted to data_dir/semantic/history.json, incrementally, with a per-session watermark so a restart re-reads a marker and not the corpus.

── deferred schemas

The tools you aren’t using cost nothing

One Playwright server is about 25 tool definitions. Sending all of them on every request is how a context window disappears before the work starts.

every request, the usual way

tools: [
  {"name":"playwright__browser_click",
   "description":"Perform click on a web page…",
   "inputSchema":{"type":"object","properties":{…}}},
  {"name":"playwright__browser_type", …},
  {"name":"playwright__browser_navigate", …},
  … 22 more, every request, whether or not
    this turn has anything to do with a browser
]

every request, deferred

mcp servers:
  playwright (25 tools)
  github (18 tools)

// the turn mentions a browser →
  + playwright__browser_click (full schema, inlined)
// or the model asks for more:
  tool_search("scrape a page")

mcp_schemas = "auto" defers above twelve tools, "full" never defers, "deferred" always does. Turn RAG off entirely and full schemas come back on their own — the model is never left guessing what exists.

── your own history

The answer you already found

The agent has solved things with you before. That transcript is an index, not an archive — both you and the model can query it.

/search compaction

/search how did we fix the PoW stall

  sort: relevance   role: any   unique: on

  1. 2026-08-29 · checkpoints   0.91
     "…the solver moved to a blocking thread so the
      event loop never waits on SHA-3…"
  2. 2026-08-14 · deepseek auth  0.78

  ↵ opens that session   s sort   r role   u unique
/search
A dedicated screen: sort by relevance, newest or oldest, filter by role, collapse to one hit per session. Enter opens that session.
history_search
The same index as a tool, so the model recalls its own past solution without you remembering that it happened.
incremental
Sessions are embedded once and tracked by a watermark; nothing is re-embedded because you restarted.
on disk
data_dir/semantic/history.json — local, plain, and deletable. Nothing about it is a service.

── controls

Every knob, including the off switch

A layer you can’t turn off is a liability. This one is four commands and four config keys.

/rag
Live status: which corpora are indexed, how large they are, whether the model is ready.
/rag on|off
The whole layer — hints, deferred schemas, history indexing. Off means off.
/rag reload
Re-verify (or re-download) the model and re-embed skills, MCP tools and history.
/rag-limit
The embedder’s batch cap: auto, off, or a fixed number when RAM is tight.

config.toml

[semantic]
enabled = true          # /rag on|off flips this
top_k = 3               # hints per corpus per turn
min_dense_score = 0.80  # cosine floor without keyword overlap
mcp_schemas = "auto"    # auto | full | deferred

── failure modes

It degrades, it doesn’t brick

Every dependency here is optional at runtime, and each one has a defined answer for being missing.

no model on disk
Retrieval falls back to lexical search alone. Hints get cruder; nothing stops working.
RAG switched off
Full MCP schemas return to the system prompt automatically, so no tool becomes unreachable.
first launch
The model downloads in the background and the status bar reports progress. The agent is usable the whole time.
tight RAM
/rag-limit caps the embedding batch — the index takes longer to build, and that is the entire consequence.

See how the loop around it is built

deepseek · deepseek-chatctx:0% · | [streaming]