Layered Anytime Retrieval

A retrieval engine that delivers signals, not answers. Many independent layers, one shared contract, one agent that decides.

1. Purpose

This thesis defines a multi-layer, incremental retrieval engine for agent-driven knowledge work. It serves heterogeneous corpora (source code, scientific papers, prose, tool outputs, external APIs) through a single, agnostic interface.

The engine does one thing: it returns well-tagged evidence about where something is. Interpretation, aggregation, ranking policy, and the final conclusion belong to the calling agent, which holds the most context at the latest possible moment.

2. Thesis

  1. Retrieval answers one question: where does something about X stand? Everything else (answer form, relations, aggregation, negation, conditions) is a property of the query and the reader, not of the index.
  2. The meaningful design axis is where understanding happens: at indexing time, at query time, or in the agent. The engine should not anticipate what the agent can do better with full query context.
  3. Retrieval methods are not competing categories. They are layers: independent signal producers with different cost, granularity, and characteristics, composed additively under a time budget set by the caller.
  4. A retriever is a weaker model than the agent that uses it. Its role is to widen and order the field of view, never to replace judgment.
  5. Cheap, deterministic, always-current layers come first. Expensive, learned layers refine, correct, and extend, and they are built lazily from actual use.

3. Guiding Principles

| Principle | Meaning | |---|---| | Signals, not conclusions | The engine tags evidence. The agent weighs it. | | Additive layers | A layer can only add information. A missing, partial, or stale layer contributes less, never something wrong. | | Understanding as late as possible | Interpretation is shifted toward query time and toward the agent (late interaction, probing, iterative search). | | Agnostic composition | Layers share only storage, orchestration, and change detection. Chunking, granularity, model, and index format are private to each layer. | | Usage-driven materialization | Layers are built lazily, prioritized by what the agent actually touches. | | Caller-defined quality | The caller sets timebox, target set size, and recall/precision emphasis. | | Incremental updates over retrieval speed | Cheap, granular, abortable updates outrank index compression and lookup latency. | | Lossless by reference | Full outputs stay cached. Reductions are views, never deletions. |

4. The Argument Chain

4.1 A continuum, not categories

Static embeddings, dense bi-encoders, multi-vector late interaction, and cross-encoders are points on shared knobs:

These knobs trade pre-computation, static share, index size, and query-time interaction against each other. A generic pipeline exposes the knobs instead of committing to one method. Switching along the continuum by fit and threshold (margin, disagreement between signals) is a natural extension.

4.2 Where understanding happens

One axis organizes the whole space:

`` raw text + terms → embeddings, doc2query, type annotations → extracted facts, graphs, summaries → agent as index (no understanding (some interpretation (relations precomputed, (all understanding in the index) precomputed) queries become lookups) at query time) ``

Pre-computed conclusions amortize across many queries but freeze assumptions. Query-time understanding adapts to each question and sees the full context. Raw text is the only lossless end; the agent is the only component that can answer questions nobody foresaw at indexing time.

4.3 Two levels that must not be mixed

Level 2 is why the project works with multi-vector signals. MaxSim decomposes a score into contributions per query token, each with a concrete counterpart in the document. This yields, without extra machinery:

4.4 The retriever is the weaker model

Dense embeddings fail on private jargon unless trained for it. The agent does not: it learns the vocabulary during the search (a first hit shows the internal term in context, the next query uses it), which is pseudo-relevance feedback with a model as evaluator.

Plain text search with fuzzy matching already imitates multi-vector behavior. Per aspect, the agent runs one search, receives fragments, and combines the findings. That is late interaction with a learned, context-sensitive aggregator instead of a fixed sum. The agent filters with more intent and context than any retrieval model could anticipate, which is the same reason agentic search with grep has displaced vector RAG in coding agents.

4.5 The two-stage pattern

Tool outputs (list, grep, bash, context7, OpenAlex, Exa, and others) are cached. A deterministic filter reduces the signal and returns an ID-associated view. Only a second call retrieves details.

5. Architecture

5.1 Overview

`` ┌──────────────────────────────┐ │ Agent │ │ sets timebox, targets, scope │ └──────────────┬───────────────┘ │ status / search / fetch ┌──────────────▼───────────────┐ │ Orchestrator │ │ scope · timebox · tagging · │ │ layer protocol │ └─┬────┬────┬────┬────┬────┬───┘ fast / always current │ │ │ │ │ │ slow / lazily built ▼ ▼ ▼ ▼ ▼ ▼ glob text tri- BM25 AST embeddings … late interaction search gram ┌──────────────────────────────┐ │ Shared: file index · cache · │ │ change detection │ └──────────────────────────────┘ ``

5.2 What is shared, what is private

Shared

Private to each layer

5.3 Coordinate system

The only common coordinate is the file: 0. Every layer reports hits in these spans.

5.4 Layer contract

``python class Layer(Protocol): name: str # stable identity; version is separate roles: set[Literal["generate", "refine"]] def applies(self, scope) -> bool: ... def status(self, scope) -> LayerStatus: ... # origin, granularity, coverage, query format, cost async def search(self, query, scope, budget, candidates=None) -> list[Hit]: ... async def build(self, units, cancel: asyncio.Event) -> None: ... ``

5.5 Signals as tags, meaning in the status

``` hit: {"id": "a1b2…", "span": [120, 480], "excerpt": "…", "signals": ["bm25", "summary"]}

status: summary → derived, granularity: file, coverage: 12 % bm25 → in text, granularity: chunk, coverage: 100 % ```

Meaning arises from combinations:

| Signals on a span | Reading for the agent | |---|---| | text layers only | The passage states it literally. | | derived layer only (summary, generated question) | Abstraction, inference, or paraphrase not directly stated in the text. A pointer for where reading pays off. | | both | Strongest evidence, directly quotable. |

A derived signal carries optional pointers to the source spans it was produced from, so the agent can verify in the second stage.

5.6 Metadata and derived views

6. Layer Catalogue

Ordered by overall cost, build plus query. G = generates candidates, R = refines given candidates.

| # | Layer | Tools | Build | Role | |---|---|---|---|---| | 1 | File names | fd, glob | none | G | | 2 | Exact / regex text | grep → ripgrep → ugrep (fuzzy, boolean) | none, always current | G | | 3 | Text across formats | ripgrep-all, pdfgrep | cached extraction | G | | 4 | Fuzzy rescoring | rapidfuzz | none | R | | 5 | Structural search | ast-grep | none (parses at run time) | G | | 6 | Trigram index | csearch/cindex, Zoekt, fcs, FTS5 trigram, ugrep-indexer | cheap | G | | 7 | Word-level BM25 | FTS5, bm25s, Tantivy, Recoll, qmd search, with custom tokenization (camelCase, LaTeX) | cheap | G | | 8 | Symbol / AST index | tree-sitter (definitions, callers, spans) | medium, per language | G | | 9 | Formula structure | Tangent-CFT, SSEmb, Approach0 | medium, specialized | G | | 10 | Learned sparse | SPLADE, BGE-M3 sparse | encoder run | G | | 11 | Static embeddings | Model2Vec + matrix | lookup, no transformer | G | | 12 | Dense bi-encoder | BGE-M3, Jina, Qwen3-Embedding, SPECTER2 (abstracts) | encoder run per chunk | G | | 13 | Derived views | summaries, generated questions, formula descriptions, context lines | LLM call per unit; cheap to query | G | | 14 | Late interaction (exact MaxSim) | PyLate / fastembed encoders, sidecar token vectors | encoder run | G + R | | 15 | Multiple late-interaction models in parallel | e.g. BGE-M3 multi + domain-specific ColBERT | doubled cost, decorrelated by choosing different specializations | G |

Beyond the ranking: cross-encoder and LLM rerankers evaluate query and text jointly. The agent itself is the final, richest layer.

Reading the table

7. Late Interaction Without a Heavy Index

Because update cost outranks lookup speed, the late-interaction layer is exact MaxSim over stored token vectors, with no centroid index.

``python sim = q @ block.T # (nq, N_block), block as float32 best = np.maximum.reduceat(sim, starts, axis=1) # max per chunk score = best.sum(axis=0) # sum over query tokens ``

8. Scheduling, Timebox, and Build Behavior

9. Change Detection and Upserts

A shared concern with a single mechanism:

  1. Central file index: path, hash, size, mtime, sequence number, deleted flag, plus the extraction cache. A monotonic sequence number is the cursor (not the timestamp). Deletions are tombstone rows that collapse repeated changes per path.
  2. Lazy detection: the always-current text layer is the detector. When it resolves a match, a stat comparison against the index detects the change; a hash over the read bytes confirms it. The changed file is queued for all following layers with high priority. A cheap stat pass over the directory listing the text layer produces anyway also catches files without a lexical hit.
  3. Per-layer cursor: each layer asks the central index for changes since its last update and refreshes its own chunk view. The cursor is stored with the layer version; a new version restarts lazily. Application is idempotent over (layer, hash).
  4. Hierarchical option: an index file balanced by size, with a parent index mapping time and hash of the child index files (Merkle-style, content-defined boundaries). Snapshot diffs yield added, changed, and removed entries without tombstones; the structure doubles as a versionable and synchronizable file format.
  5. Per-layer upsert contract: apply(add, remove), idempotent and abortable. FTS5 deletes and inserts per hash; vector sidecars are written atomically; AST results are sidecars per hash.
  6. Hash from the bytes actually read, so spans and hash always agree. Debounce editor save patterns.

10. Storage Layout

11. Multi-Index, Multi-Space Generalization

The same philosophy covers multimodal and heterogeneous data.

12. Domain Hints

13. Cost and Latency Reasoning

14. Implementation Hints

15. Why This Is Generic

16. Lineage and Related Work

The building blocks have established precedents that this project combines:

What this manifest adds is the generalization: an open set of independent layers with their own granularity, one minimal contract of spans and signal tags, caller-defined timebox and quality targets, lazy change detection through the always-current text layer, and the agent as the only aggregator.

17. Glossary