Skip to content

Architecture

giulio d'erme edited this page Aug 10, 2026 · 2 revisions

Architecture

Two installable packages:

  • recall/ — the library. No server, no framework, no global state.
  • recall_mcp/ — an MCP server wrapping it, so an agent can call the library as a tool.

recall_mcp depends on recall. Nothing in recall knows the MCP server exists.

The request path

flowchart TB
    Q([query]) --> TS["trust.trusted_search()"]
    TS --> R["retriever.HybridRetriever"]
    R --> E["embeddings.Embedder"]
    R --> SD["store.query_dense · pgvector"]
    R --> SS["store.query_sparse · full-text"]
    R -. optional .-> LS["learned sparse · SPLADE"]
    SD --> RRF["Reciprocal Rank Fusion"]
    SS --> RRF
    LS -. optional .-> RRF
    RRF --> RK["rerank.Reranker (optional)"]
    RK --> G["guards: gap_warning · staleness"]
    G --> EV["trust.evaluate()"]
    SUP["store.supersession()"] --> EV
    CAL["calibration.Calibration or generation binding"] --> EV
    EV --> EN["entailment.apply_entailment (opt-in)"]
    EN --> EB["evidence bundle (optional)"]
    EN --> O([TrustedResult])
Loading

The shape worth noticing: trust.evaluate() is pure. It reads no clock, touches no database, and performs no I/O — now, the supersession map and the calibration are all passed in. That is why the trust semantics can be tested exhaustively without a database, and why the ordering rules are verifiable rather than merely observed.

Module ownership

recall/ — core

Module Owns Depends on
types.py The data contract: Chunk, ScoredChunk, TrustedHit, TrustedResult, Verdict, Validity, Provenance, StalenessReport. All frozen dataclasses. nothing
store.py PostgreSQL + pgvector. RLS, dense/sparse queries, upsert, prune, the supersession scan and its cache, connection mode, retry, timeouts, DSN security checks. The largest legacy store module, and the main one that talks to chunk tables. types
generation_store.py Generation-aware serving over immutable chunk tables, with schema checks and active generation routing for production search. store, control_plane
migration.py / schema.py Ordered SQL migration runner, checksum ledger, grants generator, schema status and plan/apply operations. store
control_plane.py Enterprise generation registry, tenant routes, shadow cutover, migration event replay and readiness support. schema, generation_store
embeddings.py The Embedder protocol and its implementations (hashing, FastEmbed, sentence-transformers, Voyage), plus batching, retry-with-backoff and dimension checking. nothing
retriever.py HybridRetriever: dense, sparse and optional learned sparse legs, RRF fusion, rerank hand-off, and annotation with gap/staleness. store, embeddings, rerank, guards, types
sparse.py Learned sparse SPLADE encoding and sparse-vector indexing support. embeddings
rerank.py The Reranker protocol, a no-op, and a cross-encoder implementation. types
guards.py The two cheap honesty signals: gap_warning (are all candidates below threshold) and staleness (is the index too old). Holds the default gap threshold. types
calibration.py Per-embedder abstention threshold and the cosine→confidence mapping; fitting from labelled samples; safe load/save. observability
frontmatter.py The dependency-free frontmatter parser, validity-date interpretation, and supersedes: reference normalisation. nothing
trust.py Verdict assignment, successor resolution, successor promotion, valid-first ordering, the abstention decision and its reason. store, embeddings, retriever, rerank, calibration, frontmatter, guards, types
entailment.py Opt-in near-miss guard: demotes verdict-ok hits that do not entail an answer. Pure post-processing over a TrustedResult, mirroring trust. trust, types
evidence.py Builds citable evidence bundles from trusted hits, renders generator-neutral prompts, and validates that generated citations resolve to supplied chunk IDs. types, trust
index.py Indexer — file discovery, chunking (prose and code), NUL sanitisation, index-root confinement, content-hash skip, batched writes, pruning of vanished sources and the prune guard. store, embeddings, frontmatter, cache, lint, observability
cache.py Content-addressed embedding cache (SQLite), keyed by embedder name + dim + text. embeddings
lint.py Static supersession-graph lint: dangling edges, self-supersession, cycles, ambiguous targets, invalid dates. No database. frontmatter
fix.py Extracts prose closure markers and proposes supersedes: edges — under rules that refuse far more often than they act. frontmatter, lint
check.py Write-time gate for a single file being authored: surfaces candidate edges while the author is still present. frontmatter, lint
semantic_lint.py The missing-edge lint: queries the index with a new memo's text to surface closed decisions it should probably reference. trust, store, embeddings
observability.py get_logger (namespaced, never configures handlers), opt-in configure_logging with JSON output, and the METRICS counter/histogram registry. nothing
timing.py Decorators that wrap an Embedder or Reranker to record per-call latency. Used by the eval harness. embeddings, rerank, types
cli.py Argument parsing and the command implementations. most of the above
_env.py Minimal .env loader for local development. Never overrides an already-set variable. nothing
eval/ The evaluation harness: ablation matrix, trust eval, near-miss eval, calibration fitting, labelled-question runner, synthetic corpus generation, scale/latency runner, metrics. most of the above

recall_mcp/ — the MCP server

Module Owns
server.py Builds the FastMCP server, the five tools, the lifespan, generation-mode startup checks, auth wiring, scope enforcement per tool, and transport selection.
service.py The tool bodies, independent of MCP: search, evidence, index, forget, stats. Pydantic response models. This is where the logic lives, so it is testable without a server.
auth.py Bearer-token registry, principal→tenant mapping, scope definitions, token-file parsing (plaintext or SHA-256 digests), expiry, and the fail-closed configuration check.
limits.py Per-tenant token buckets: call rates per tool class and an aggregate indexing byte budget.
stores.py Per-tenant store construction and lifetime.

Three structural decisions worth knowing

The trust layer is a pure function over a retrieval result. It could have been folded into the retriever's SQL — one query returning already-judged rows would be faster. It is separate because the judgment is the part most likely to be wrong, and a pure function is the only shape in which "a superseded hit with a top cosine loses to its lower-scoring successor" is a test rather than an observation.

Entailment mirrors trust rather than extending it. apply_entailment takes a TrustedResult and returns a TrustedResult. It judges only verdict-ok hits — judging an already-demoted hit would waste a model call and, worse, could resurrect a superseded memory by ranking it back up. The two stages stack; neither replaces the other, and the measurements say so explicitly.

A store is bound to one tenant for its lifetime. Tenancy is not an argument you pass to a query — it is a property of which store object you hold, enforced in the database by a row-level security policy comparing against a per-connection setting. That is what makes a forgotten WHERE clause return nothing rather than another tenant's memories, and it is why the MCP server's authentication has to resolve a tenant before any tool body runs. → Tenancy-and-Auth

Production serving is migration and generation driven. Normal data operations and MCP startup do not run DDL. A migration job applies ordered SQL through RECALL_MIGRATION_DSN; runtime traffic uses RECALL_SERVING_DSN. In production, searches read the tenant's active generation and strict trust refuses when the generation, lineage or certified calibration is not ready. → Installation-and-Setup

Where the pieces are documented

Clone this wiki locally