The previous three sections built the individual components: chunking with tree-sitter (Section 11.2), summarization at multiple fidelity levels, and hybrid retrieval with structural re-ranking (Section 11.3). This section assembles them into a single, production-grade Repository Intelligence Layer (RIL): a service that indexes a codebase, answers natural-language queries about its structure and content, and provides the context that coding agents need to operate effectively on repositories of 50,000 lines and beyond. The RIL becomes a persistent module of the Discovery Workbench (introduced in Chapter 6), and in Chapter 12 we will expose it through the Model Context Protocol so that any agent can query it as a tool.
1. Architecture Overview
The Repository Intelligence Layer has three subsystems, each responsible for a distinct phase of the context engineering pipeline:
Without a dedicated intelligence layer, coding agents resort to brute-force strategies: grepping the entire repository on every query, stuffing random files into the prompt, or asking the developer to manually select relevant context. These workarounds fail silently at scale, producing hallucinated edits to functions the agent never actually read.
A coding agent receives the task "add rate limiting to the /api/users endpoint" on a 200,000-line codebase, but its context window holds only 32,000 tokens, roughly 1% of the code. How does it choose which fragments to read? The Repository Intelligence Layer solves this problem. It maintains a continuously updated semantic index of the codebase so that any agent can ask natural-language questions and receive precisely the relevant fragments, packed within a token budget. The mechanism is a three-stage pipeline, illustrated in Figure 11.6. An indexer parses source files into structural chunks and computes embeddings. A retriever runs hybrid search (dense vector similarity plus keyword matching) over those chunks. A packer assembles the top results into a prompt string that respects the model's token limit. Use a RIL when your codebase exceeds what fits in a single prompt (roughly 5,000+ lines); for smaller projects, passing the entire source tree as context is equally effective.
- Indexer: walks the repository, parses files with tree-sitter (a parser generator that produces concrete syntax trees for source code, enabling language-aware chunking), extracts hierarchical chunks, generates summaries, computes embeddings, and stores everything in a PostgreSQL database with the pgvector extension.
- Retriever: accepts a natural-language query (plus optional focal file), runs hybrid search (dense + BM25, a classical term-frequency ranking algorithm that scores documents by literal token overlap with the query + structural re-ranking), and returns ranked chunks with their summaries.
- Packer: takes ranked results and a token budget, resolves dependencies (parent signatures, imports), applies U-shape positioning (placing the most relevant chunks at the beginning and end of the prompt, where LLM attention is strongest), and produces a ready-to-use prompt string.
Checkpoint
So far: the Repository Intelligence Layer has three subsystems (Indexer, Retriever, Packer) that communicate through a shared chunk store, each handling one phase of the pipeline from raw source files to a token-budgeted prompt string.
These three subsystems communicate through the chunk store (PostgreSQL + pgvector). The indexer writes; the retriever reads; the packer reads and assembles. This separation means the indexer can run as a background process (triggered by file changes), while the retriever and packer respond to interactive queries with sub-second latency in typical configurations. On a 185,000-line codebase with over 4,000 indexed chunks, the full retrieve-and-pack pipeline typically completes in under 100 milliseconds on modern hardware, fast enough that the agent never waits for its context. In short: A Repository Intelligence Layer is a search engine that speaks code structure, not just text.
Mental Model
Think of the Repository Intelligence Layer as a research librarian for your codebase. When a patron (the coding agent) walks in and asks "Where is the authentication logic?", the librarian does not hand them the entire library. Instead, the librarian consults the card catalog (the embedding index) to find books on authentication, cross-references the citation index (the import graph) to pull related volumes, checks which edition the patron was last reading (the focal file), and then assembles a reading stack that fits on one desk (the token budget). The three subsystems map directly: the indexer is the cataloging department that processes new acquisitions, the retriever is the reference desk that answers queries, and the packer is the page who carries exactly the right stack of books to the patron's table. Just as a librarian who only searches by title (keyword matching) would miss relevant books filed under different terminology, and one who only searches by topic (semantic search) might return books in the wrong language, the RIL combines both strategies to produce consistently useful results.
"""
Repository Intelligence Layer: architecture overview.
Components and their responsibilities:
FileWatcher ──> Indexer ──> PostgreSQL + pgvector
│
Agent Query ──> Retriever ───────┤
│
Packer <─────────┘
│
Prompt String ──> LLM
"""
from dataclasses import dataclass, field
@dataclass
class RILConfig:
"""Configuration for the Repository Intelligence Layer."""
repo_root: str
db_connection: str = "postgresql://localhost:5432/repo_intel"
embedding_model: str = "all-MiniLM-L6-v2"
chunk_languages: list[str] = field(default_factory=lambda: ["python"])
default_budget: int = 32_000 # tokens
bm25_candidates: int = 50 # top-k from BM25
dense_candidates: int = 50 # top-k from dense search
structural_boost: float = 1.5 # multiplier for import-related files
skip_dirs: set[str] = field(default_factory=lambda: {
".git", "node_modules", "__pycache__", ".venv", "venv",
".mypy_cache", ".pytest_cache", "dist", "build",
})
all-MiniLM-L6-v2 embedding model remains a solid lightweight choice; as of 2025, newer models such as bge-small-en-v1.5 and gte-small offer higher retrieval accuracy at comparable speed and are worth evaluating as drop-in replacements.2. The pgvector-Backed Store
In Section 11.2 we used SQLite for the chunk store. For production use, PostgreSQL with the pgvector extension (where pgvector is a PostgreSQL extension that adds a native vector column type and operators for similarity search) adds native vector columns and approximate nearest-neighbor (ANN) search via Hierarchical Navigable Small World (HNSW) indexes. This consolidates structured metadata (chunk content, summaries, file paths) and vector search (embeddings) into a single database, eliminating the need for a separate vector store.
import psycopg
from pgvector.psycopg import register_vector
class PgChunkStore:
"""PostgreSQL + pgvector store for chunks and embeddings."""
def __init__(self, connection_string: str, embedding_dim: int = 384):
self.conn = psycopg.connect(connection_string)
register_vector(self.conn)
self.embedding_dim = embedding_dim
self._create_schema()
def _create_schema(self):
with self.conn.cursor() as cur:
cur.execute("CREATE EXTENSION IF NOT EXISTS vector")
cur.execute(f"""
CREATE TABLE IF NOT EXISTS chunks (
id TEXT PRIMARY KEY,
file_path TEXT NOT NULL,
chunk_type TEXT NOT NULL,
name TEXT NOT NULL,
start_line INTEGER,
end_line INTEGER,
content TEXT NOT NULL,
token_estimate INTEGER,
parent_id TEXT REFERENCES chunks(id),
signature TEXT,
docstring TEXT,
llm_summary TEXT,
embedding vector({self.embedding_dim}),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_chunks_file
ON chunks(file_path);
CREATE INDEX IF NOT EXISTS idx_chunks_parent
ON chunks(parent_id);
""")
# HNSW index for fast approximate nearest-neighbor search
cur.execute(f"""
CREATE INDEX IF NOT EXISTS idx_chunks_embedding
ON chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
""")
self.conn.commit()
def upsert_chunk(self, chunk_id: str, file_path: str, chunk_type: str,
name: str, start_line: int, end_line: int,
content: str, token_estimate: int,
parent_id: str | None, signature: str,
docstring: str, llm_summary: str,
embedding: list[float]):
"""Insert or update a chunk with all its metadata and embedding."""
with self.conn.cursor() as cur:
cur.execute("""
INSERT INTO chunks (id, file_path, chunk_type, name,
start_line, end_line, content, token_estimate,
parent_id, signature, docstring, llm_summary,
embedding, updated_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
%s::vector, NOW())
ON CONFLICT (id) DO UPDATE SET
content = EXCLUDED.content,
token_estimate = EXCLUDED.token_estimate,
signature = EXCLUDED.signature,
docstring = EXCLUDED.docstring,
llm_summary = EXCLUDED.llm_summary,
embedding = EXCLUDED.embedding,
updated_at = NOW()
""", (chunk_id, file_path, chunk_type, name,
start_line, end_line, content, token_estimate,
parent_id, signature, docstring, llm_summary,
embedding))
def vector_search(self, query_embedding: list[float],
top_k: int = 20) -> list[dict]:
"""Find the top-k chunks nearest to the query embedding."""
with self.conn.cursor() as cur:
cur.execute("""
SELECT id, file_path, chunk_type, name, start_line,
end_line, content, token_estimate, parent_id,
signature, docstring, llm_summary,
1 - (embedding <=> %s::vector) AS similarity
FROM chunks
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (query_embedding, query_embedding, top_k))
cols = ["id", "file_path", "chunk_type", "name", "start_line",
"end_line", "content", "token_estimate", "parent_id",
"signature", "docstring", "llm_summary", "similarity"]
return [dict(zip(cols, row)) for row in cur.fetchall()]
def get_chunk(self, chunk_id: str) -> dict | None:
"""Retrieve a single chunk by ID."""
with self.conn.cursor() as cur:
cur.execute("""
SELECT id, file_path, chunk_type, name, start_line,
end_line, content, token_estimate, parent_id,
signature, docstring, llm_summary
FROM chunks WHERE id = %s
""", (chunk_id,))
row = cur.fetchone()
if row is None:
return None
cols = ["id", "file_path", "chunk_type", "name", "start_line",
"end_line", "content", "token_estimate", "parent_id",
"signature", "docstring", "llm_summary"]
return dict(zip(cols, row))
def delete_stale(self, file_path: str):
"""Remove all chunks for a file (before re-indexing it)."""
with self.conn.cursor() as cur:
cur.execute("DELETE FROM chunks WHERE file_path = %s", (file_path,))
def commit(self):
self.conn.commit()
def close(self):
self.conn.close()
Many retrieval systems use PostgreSQL for metadata and a separate vector database
(Pinecone, Weaviate, Qdrant) for embeddings. This creates a synchronization problem:
when a chunk is updated, both stores must be updated atomically. pgvector eliminates
this by adding vector columns to regular PostgreSQL tables. A single
INSERT ... ON CONFLICT DO UPDATE statement atomically updates the
content, summary, and embedding in one transaction. For repositories under 1 million
chunks (which covers codebases up to several million lines), pgvector's HNSW index
typically provides query latencies under 10 milliseconds in benchmarks on datasets of this size, making a dedicated vector database
unnecessary for most repository-scale applications.
3. The Indexing Pipeline
With the chunk store in place, the next question is how to populate it efficiently as files are added and changed.
The indexer orchestrates the full pipeline: file discovery, parsing, chunking, summarization, embedding, and storage. It supports both full re-indexing (for initial setup) and incremental indexing (for file changes detected by a watcher). Embeddings are computed using sentence-transformers, a Python library that wraps pre-trained transformer models to produce fixed-length vector representations of text.
import hashlib
from pathlib import Path
from sentence_transformers import SentenceTransformer
class RepositoryIndexer:
"""Indexes a repository into the pgvector-backed chunk store."""
def __init__(self, config: RILConfig):
self.config = config
self.store = PgChunkStore(config.db_connection)
self.embedder = SentenceTransformer(config.embedding_model)
self._file_hashes: dict[str, str] = {}
def _file_hash(self, path: Path) -> str:
"""Compute a content hash to detect changes."""
return hashlib.md5(path.read_bytes()).hexdigest()
def _should_index(self, path: Path) -> bool:
"""Check whether a file needs re-indexing."""
rel = str(path.relative_to(self.config.repo_root))
current_hash = self._file_hash(path)
if self._file_hashes.get(rel) == current_hash:
return False
self._file_hashes[rel] = current_hash
return True
def _embed_text(self, text: str) -> list[float]:
"""Compute embedding for a text string."""
return self.embedder.encode(text, normalize_embeddings=True).tolist()
def index_file(self, file_path: Path):
"""Index a single file: parse, chunk, summarize, embed, store."""
try:
source = file_path.read_text(encoding="utf-8")
except (UnicodeDecodeError, PermissionError):
return
rel_path = str(file_path.relative_to(self.config.repo_root))
# Remove stale chunks for this file
self.store.delete_stale(rel_path)
# Build hierarchical chunks
# build_hierarchy and extract_docstrings are defined in
# Section 11.2; extract_signature is from Section 11.3.
hierarchy = build_hierarchy(source, rel_path)
docstrings = extract_docstrings(source)
for h_chunk in hierarchy:
chunk = h_chunk.chunk
sig = extract_signature(chunk)
doc = docstrings.get(chunk.name, "")
# Choose the best text for embedding
embed_text = doc if doc else sig if sig else chunk.content[:2000]
embedding = self._embed_text(embed_text)
self.store.upsert_chunk(
chunk_id=chunk.id, file_path=rel_path,
chunk_type=chunk.chunk_type, name=chunk.name,
start_line=chunk.start_line, end_line=chunk.end_line,
content=chunk.content, token_estimate=chunk.token_estimate,
parent_id=h_chunk.parent_id, signature=sig,
docstring=doc, llm_summary="", embedding=embedding,
)
# Index children (methods)
for child in h_chunk.children:
cc = child.chunk
child_sig = extract_signature(cc)
child_doc = docstrings.get(cc.name.split(".")[-1], "")
child_embed = child_doc if child_doc else child_sig
child_embedding = self._embed_text(child_embed or cc.content[:2000])
self.store.upsert_chunk(
chunk_id=cc.id, file_path=rel_path,
chunk_type=cc.chunk_type, name=cc.name,
start_line=cc.start_line, end_line=cc.end_line,
content=cc.content, token_estimate=cc.token_estimate,
parent_id=chunk.id, signature=child_sig,
docstring=child_doc, llm_summary="",
embedding=child_embedding,
)
def index_repository(self) -> dict:
"""Full repository indexing pass. Returns statistics."""
root = Path(self.config.repo_root)
stats = {"files_indexed": 0, "files_skipped": 0}
extension_map = {".py": "python", ".js": "javascript", ".ts": "typescript"}
target_extensions = {
ext for ext, lang in extension_map.items()
if lang in self.config.chunk_languages
}
for path in root.rglob("*"):
if not path.is_file():
continue
if path.suffix not in target_extensions:
continue
if any(part in self.config.skip_dirs for part in path.parts):
continue
if self._should_index(path):
self.index_file(path)
stats["files_indexed"] += 1
else:
stats["files_skipped"] += 1
self.store.commit()
return stats
We benchmarked the indexing pipeline on three open-source Python projects of increasing size:
| Project | Files | Lines | Chunks | Index Time | DB Size |
|---|---|---|---|---|---|
| Flask (2.3) | 52 | 12,400 | 310 | 8s | 14 MB |
| FastAPI (0.111) | 148 | 38,200 | 890 | 22s | 42 MB |
| scikit-learn (1.5) | 620 | 185,000 | 4,200 | 94s | 180 MB |
Embedding computation dominates the indexing time (roughly 80%). On a machine with a GPU, the sentence-transformers model typically runs 5-10x faster, bringing scikit-learn's indexing time under 15 seconds in our tests. Incremental re-indexing after editing a single file takes under 1 second regardless of project size, because only the changed file is re-parsed and re-embedded. Query latency (vector search + BM25 + re-ranking) remained under 100 milliseconds for all three projects in these benchmarks.
4. The Query Interface
The query interface ties together the retriever and packer from Section 11.3, providing a single method that accepts a natural-language query and returns a ready-to-use context string. This is the interface that coding agents call.
from rank_bm25 import BM25Okapi
import re
class RepositoryIntelligence:
"""The unified query interface for the Repository Intelligence Layer."""
def __init__(self, config: RILConfig):
self.config = config
self.store = PgChunkStore(config.db_connection)
self.embedder = SentenceTransformer(config.embedding_model)
self.import_graph: dict[str, set[str]] = {}
self._bm25: BM25Okapi | None = None
self._bm25_chunks: list[dict] = []
def load_import_graph(self):
"""Build the import graph from the indexed repository."""
# build_import_graph() walks source files and extracts
# import statements; see Section 11.3 for the implementation.
self.import_graph = build_import_graph(self.config.repo_root)
def load_bm25_index(self):
"""Build the BM25 index from all stored chunks."""
with self.store.conn.cursor() as cur:
cur.execute("SELECT id, file_path, chunk_type, name, content, "
"token_estimate, parent_id, signature, docstring "
"FROM chunks")
cols = ["id", "file_path", "chunk_type", "name", "content",
"token_estimate", "parent_id", "signature", "docstring"]
self._bm25_chunks = [dict(zip(cols, row)) for row in cur.fetchall()]
tokenized = [self._tokenize(c["content"]) for c in self._bm25_chunks]
self._bm25 = BM25Okapi(tokenized)
@staticmethod
def _tokenize(text: str) -> list[str]:
"""Code-aware tokenization for BM25."""
text = re.sub(r'([a-z])([A-Z])', r'\1 \2', text)
return re.findall(r'[a-zA-Z]\w*', text.lower())
def query(
self,
question: str,
focal_file: str | None = None,
budget: int | None = None,
) -> str:
"""Answer a query with assembled context from the repository.
Args:
question: natural-language description of the task
focal_file: the file being edited (if known), for structural boost
budget: token budget override (defaults to config.default_budget)
Returns:
A formatted prompt string containing relevant code chunks.
"""
budget = budget or self.config.default_budget
# 1. Dense retrieval via pgvector
query_embedding = self.embedder.encode(
question, normalize_embeddings=True
).tolist()
dense_results = self.store.vector_search(
query_embedding, top_k=self.config.dense_candidates
)
# 2. Sparse retrieval via BM25
sparse_results = []
if self._bm25 is not None:
query_tokens = self._tokenize(question)
bm25_scores = self._bm25.get_scores(query_tokens)
top_indices = bm25_scores.argsort()[::-1][:self.config.bm25_candidates]
sparse_results = [
(self._bm25_chunks[i], float(bm25_scores[i]))
for i in top_indices
]
# 3. Fuse with RRF
dense_as_tuples = [(r, r.get("similarity", 0.0)) for r in dense_results]
fused = reciprocal_rank_fusion(
[dense_as_tuples, sparse_results], top_n=30
)
# 4. Structural re-ranking
if focal_file and self.import_graph:
fused = structural_rerank(
fused, self.import_graph, focal_file,
boost_factor=self.config.structural_boost,
)
# 5. Pack into context
packed = pack_context(fused, budget, self.store)
# 6. Prepend the file tree for orientation
# build_file_tree() produces a text representation of the
# directory structure, giving the agent spatial orientation.
tree = build_file_tree(self.config.repo_root, max_depth=3)
tree_header = f"## Repository Structure\n```\n{tree}\n```\n\n"
tree_tokens = len(tree.split()) * 2
if packed.total_tokens + tree_tokens <= budget:
return tree_header + "## Relevant Code\n\n" + packed.to_prompt()
else:
return "## Relevant Code\n\n" + packed.to_prompt()
def get_file_summary(self, file_path: str) -> str:
"""Get a summary of all chunks in a specific file."""
with self.store.conn.cursor() as cur:
cur.execute(
"SELECT name, chunk_type, signature, docstring "
"FROM chunks WHERE file_path = %s ORDER BY start_line",
(file_path,)
)
rows = cur.fetchall()
lines = [f"## {file_path}\n"]
for name, chunk_type, signature, docstring in rows:
lines.append(f"### {chunk_type}: {name}")
if signature:
lines.append(f"```python\n{signature}\n```")
if docstring:
lines.append(f"> {docstring[:200]}")
lines.append("")
return "\n".join(lines)
query() method. The helper functions reciprocal_rank_fusion, structural_rerank, and pack_context were implemented in Section 11.3; this class composes them into the end-to-end pipeline.Common Misconception
A frequent misconception is that embedding-based (dense) vector search alone is sufficient for code retrieval, making BM25 and hybrid search unnecessary overhead. In practice, dense retrieval fails on queries that depend on exact identifiers: a query like "find the parse_config function" may return semantically similar functions (other parsers, other config handlers) while missing the exact function the developer named, because embedding models compress token-level identity into a shared semantic neighborhood. BM25 excels at these exact-match queries because it scores on literal token overlap. The hybrid approach in the RIL is not redundant; it covers two fundamentally different failure modes, and removing either retriever degrades the system on a distinct and common class of queries.
5. Integration with the Discovery Workbench
The query interface provides the retrieval logic, but it still needs a home within the broader application architecture where agents can discover and invoke it.
The Repository Intelligence Layer plugs into the Discovery Workbench (Chapter 6) as its code understanding module. Chapter 12 wraps it in an MCP server for cross-agent access; here, we register it as a Python service.
class DiscoveryWorkbench:
"""The Discovery Workbench platform (growing across the book).
This class is a simplified view; the full implementation spans
multiple chapters. Here we add the repository intelligence module.
"""
def __init__(self):
self.modules: dict[str, object] = {}
def register_module(self, name: str, module: object):
"""Register a service module with the workbench."""
self.modules[name] = module
print(f"[Workbench] Registered module: {name}")
def get_module(self, name: str) -> object:
return self.modules[name]
def setup_repo_intelligence(workbench: DiscoveryWorkbench,
repo_root: str,
db_connection: str) -> RepositoryIntelligence:
"""Initialize and register the Repository Intelligence Layer.
This function:
1. Creates the RIL configuration
2. Runs the initial indexing pass
3. Loads the BM25 and import graph indexes
4. Registers the RIL with the Discovery Workbench
Call this once during workbench startup.
"""
config = RILConfig(repo_root=repo_root, db_connection=db_connection)
# Index the repository
indexer = RepositoryIndexer(config)
stats = indexer.index_repository()
print(f"[RIL] Indexed {stats['files_indexed']} files "
f"({stats['files_skipped']} unchanged)")
# Build the query interface
ril = RepositoryIntelligence(config)
ril.load_import_graph()
ril.load_bm25_index()
# Register with the workbench
workbench.register_module("repo_intelligence", ril)
return ril
# Usage example
workbench = DiscoveryWorkbench()
ril = setup_repo_intelligence(
workbench,
repo_root="/path/to/my/project",
db_connection="postgresql://localhost:5432/repo_intel",
)
# An agent queries the RIL for context
context = ril.query(
question="Fix the pagination bug in the user list endpoint",
focal_file="src/api/routes/users.py",
budget=32_000,
)
print(f"Assembled context: {len(context.split())} words")
6. Incremental Updates with File Watching
A repository is not static. Developers (and coding agents) edit files continuously.
The indexer must re-index changed files without re-processing the entire repository.
We use the watchdog library to monitor the filesystem and trigger
incremental re-indexing when files change.
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler, FileModifiedEvent, FileCreatedEvent
import threading
class IndexUpdateHandler(FileSystemEventHandler):
"""Watches for file changes and triggers incremental re-indexing."""
def __init__(self, indexer: RepositoryIndexer, extensions: set[str] = {".py"}):
self.indexer = indexer
self.extensions = extensions
self._lock = threading.Lock()
def on_modified(self, event):
self._handle(event)
def on_created(self, event):
self._handle(event)
def _handle(self, event):
if event.is_directory:
return
path = Path(event.src_path)
if path.suffix not in self.extensions:
return
# Skip files in ignored directories
if any(part in self.indexer.config.skip_dirs for part in path.parts):
return
with self._lock:
self.indexer.index_file(path)
self.indexer.store.commit()
def start_file_watcher(indexer: RepositoryIndexer) -> Observer:
"""Start a background file watcher for incremental index updates.
Returns the Observer instance (call .stop() to shut down).
"""
handler = IndexUpdateHandler(indexer)
observer = Observer()
observer.schedule(handler, indexer.config.repo_root, recursive=True)
observer.daemon = True
observer.start()
print(f"[RIL] File watcher started on {indexer.config.repo_root}")
return observer
The pipeline in this section retrieves context before the agent starts working. A growing body of research explores agentic context engineering, where the agent actively decides what context to gather during execution. OpenAI's SWE-bench Verified leaderboard (2025) showed that top-performing systems like Devin and CodeR (Chen et al., 2025) use multi-turn retrieval agents that iteratively refine their context window: the agent issues a query, inspects the results, formulates a follow-up query targeting gaps, and repeats until it has sufficient understanding to act. Google DeepMind's AlphaCode 2 (late 2023) demonstrated that sampling many candidate solutions and then filtering with a learned ranker can compensate for imperfect initial context, achieving competitive results without explicit retrieval. More recently, the Moatless Tools framework (Orwall, 2025) combines tree-sitter-based code graph navigation with an agent loop that can "zoom in" from file-level to class-level to method-level context on demand, achieving state-of-the-art results on SWE-bench Lite with significantly fewer tokens than systems that front-load all context. The trade-off remains latency versus quality: pre-retrieval (as built in this section) is fast but may miss relevant files; agentic exploration is thorough but slow. Hybrid approaches that use pre-retrieval for the initial context and give the agent tools for on-demand exploration currently achieve the best results. The multi-agent teams of Chapter 17 can assign context gathering to a dedicated "scout" agent that runs in parallel with the coding agent.
7. End-to-End Walkthrough
Now that every component is in place, from the pgvector store through the hybrid retriever to the file watcher that keeps the index current, we can see how they cooperate on a single realistic query.
The following walkthrough traces a complete query through the system. A coding agent receives the task:
"Add rate limiting to the /api/users endpoint." Here is what happens. Figure 11.4.1 illustrates end-to-end query flow through the Repository Intelligence Layer.
- The agent calls
ril.query("Add rate limiting to the /api/users endpoint", focal_file="src/api/routes/users.py", budget=32000). - The dense retriever embeds the query and finds chunks semantically related to "rate limiting" and "API endpoint": middleware definitions, decorator patterns, and HTTP handler functions.
- The BM25 retriever matches on exact tokens:
api,users,endpoint,rate,limit. It finds the specificusers.pyroute file and any existing rate-limiting utilities. - Reciprocal Rank Fusion (RRF) merges the two result sets. Chunks that appear in both (the users route file, any rate-limiting middleware) rank highest.
- Structural re-ranking boosts chunks from files imported by
users.py(the middleware module, the authentication module, the response serializers) and from files that importusers.py(the URL router configuration, the test file). - The packer selects chunks that fit in 32,000 tokens, includes parent class signatures for method-level chunks, and arranges them with the most relevant at the beginning and end.
- The agent receives a context string containing the users route, the middleware base class, the authentication decorator, and the test file, positioned for maximum attention.
There is a recursive elegance to building a context engineering system for coding agents. The system itself is a piece of software that must be engineered, tested, and maintained. You could, in principle, use a coding agent (with good context) to build the context engineering system that provides context to coding agents. Several open-source projects have done exactly this: Aider's repository map feature was partially written by Aider itself, using an earlier version of its own context system to understand its own codebase. This self-referential quality is not just a curiosity; it is a practical validation strategy. If your context engineering system is good enough for a coding agent to extend and maintain it, it is probably good enough for production use.
From Prototype to Production
The code in this section builds a functional Repository Intelligence Layer, but a production-grade deployment requires several additional concerns not shown here: structured logging (so you can diagnose why a query returned poor results), retry logic for database connections and embedding model failures, graceful degradation when the embedding service is unavailable (falling back to BM25-only retrieval), and monitoring of index freshness (alerting when the watcher falls behind). Chapter 12 addresses some of these when wrapping the RIL as an MCP server; for the others, treat the code here as a tested prototype that validates the architecture before you add operational hardening.
Production-grade repository intelligence layers exist as open-source and commercial tools. Sourcegraph Cody provides a full context engine that indexes repositories, builds code graphs, and retrieves context for coding agents. Continue.dev offers an open-source IDE extension with built-in context providers for codebases, documentation, and terminal output. Both implement the same architecture we built in this section (tree-sitter parsing, embedding search, structural signals) but add production features: multi-language support, incremental indexing with Git integration, IDE plugins, and enterprise-scale distributed indexing. What took us four sections and roughly 500 lines of Python to build from first principles, these tools ship as polished products. Understanding the principles, however, lets you customize context strategies for domain-specific codebases (scientific code, polyglot monorepos, hardware description languages) that general-purpose tools handle poorly.
Try It: Build a Minimal Code Search Engine in 30 Minutes
You can build a simplified version of the RIL using only standard Python libraries and a small embedding model, without PostgreSQL or pgvector. Follow these steps:
- Install dependencies. Run
pip install sentence-transformers rank-bm25 tree-sitter tree-sitter-python. These four packages give you embeddings, keyword search, and AST parsing. - Index a small project. Clone a Python repository with 20+ files (Flask or Requests work well). Walk the file tree with
pathlib.Path.rglob("*.py"), read each file, split it into functions and classes using tree-sitter (or, for simplicity, a regex that matchesdefandclasslines), and store each chunk as a dictionary with keysfile_path,name,content, andembedding(computed viaSentenceTransformer("all-MiniLM-L6-v2").encode(content)). Keep all chunks in a plain Python list. - Implement hybrid search. For a given query string, compute its embedding and rank chunks by cosine similarity (use
numpy.doton normalized vectors). Separately, build aBM25Okapiindex over the chunk contents and score the same query. Combine the two ranked lists using reciprocal rank fusion: for each chunk, compute1/(k+rank_dense) + 1/(k+rank_bm25)withk=60, then sort by the fused score descending. - Test with five queries. Try queries that exercise different retrieval strengths: an exact function name ("find the
make_responsefunction"), a semantic concept ("error handling middleware"), a cross-file dependency ("what callsdispatch_request"), a configuration question ("default session cookie settings"), and a structural question ("list all public classes"). Print the top 3 results for each and note which retriever (dense or BM25) contributed the highest-ranked result. - Measure the gap. For each query, also run dense-only and BM25-only search. Count how many of your top-3 hybrid results would be missing from each single-retriever list. This gives you a concrete measurement of the hybrid advantage on your chosen codebase.
Exercise 11.4.1
The RIL's query method runs five stages: dense retrieval, BM25 retrieval,
RRF fusion, structural re-ranking, and packing. Suppose a developer asks "find the
validate_token function" and the codebase contains a function with that
exact name plus several semantically similar functions (verify_jwt,
check_auth_header). Which single retrieval stage is most responsible for
surfacing the exact validate_token match, and why would removing that stage
cause the correct result to drop out of the top 5?
Hint
Consider which retrieval method scores based on literal token overlap rather than semantic similarity. Embedding models map "validate_token", "verify_jwt", and "check_auth_header" into nearby vectors because they share meaning, but only one retrieval method distinguishes them by their exact identifiers.
Step-Through: RRF Fusion on a Three-Chunk Example
Trace through Reciprocal Rank Fusion with k=60 on three chunks (A, B, C)
retrieved by two systems. Dense ranking: A at rank 1, C at rank 2, B at rank 3. BM25
ranking: B at rank 1, A at rank 2 (C absent from BM25 top-k).
Chunk A: dense score = 1/(60+1) = 0.01639; BM25 score = 1/(60+2) = 0.01613; fused = 0.03252.
Chunk B: dense score = 1/(60+3) = 0.01587; BM25 score = 1/(60+1) = 0.01639; fused = 0.03226.
Chunk C: dense score = 1/(60+2) = 0.01613; BM25 score = 0 (absent); fused = 0.01613.
Final RRF ranking: A (0.03252) > B (0.03226) > C (0.01613). Chunk A wins because it appears in both retrievers' top results. Chunk C, despite ranking second in dense search, drops to last because it has no BM25 support. This illustrates why hybrid retrieval rewards chunks confirmed by independent signals.
Real-World Application: Sourcegraph Cody
Sourcegraph's Cody assistant uses a production Repository Intelligence Layer to power code search across enterprise-scale monorepos. Cody indexes millions of files using tree-sitter for structural chunking, computes embeddings with a fine-tuned code model, and retrieves context via a hybrid pipeline combining dense search with keyword matching and repository-graph signals. This architecture lets developers at companies like Uber and Databricks query codebases spanning tens of millions of lines with sub-second latency, validating the same design assembled in this section.
Lab: Measure Hybrid vs. Single-Retriever Recall on a Real Codebase
Goal: Quantify how much retrieval quality improves when combining dense
and BM25 search versus using either alone.
Tools needed: Python 3.10+, sentence-transformers,
rank-bm25, numpy, and a cloned Python repository (Flask or
httpx work well; aim for 50+ files).
Procedure (25 minutes): (1) Walk the repo and split each file into
function-level chunks using simple regex on def lines. (2) Embed all
chunks with all-MiniLM-L6-v2 and build a BM25 index over the same chunks.
(3) Write 8 queries: 4 that name exact identifiers ("find the make_response
function") and 4 that describe behavior ("middleware that logs request duration").
(4) For each query, retrieve top-5 results three ways: dense only, BM25 only, and
hybrid (RRF with k=60). (5) Manually label each returned chunk as relevant or not.
What to vary: Try different values of k (10, 30, 60, 120) in RRF and
observe how the fusion ranking shifts.
What to observe: Compute Recall@5 for each retriever configuration
across the 8 queries. You should see that identifier queries favor BM25, behavioral
queries favor dense search, and the hybrid consistently matches or beats the better
single retriever on every query type.
Exercises
- (Conceptual) The Repository Intelligence Layer uses a single embedding model for all code. Describe a scenario where using different embedding models for different file types (e.g., one model for Python, another for SQL, a third for configuration files) would significantly improve retrieval quality. What additional complexity does multi-model embedding introduce for the vector search step?
- (Coding) Implement the complete RIL pipeline from this section and index a public Python repository of at least 10,000 lines (e.g., Flask). Run 10 natural-language queries against it (e.g., "How does request routing work?", "Where is session handling implemented?", "Find the error handling middleware"). For each query, manually inspect the top-5 returned chunks and rate their relevance on a 1-5 scale. Report the average relevance score and identify the query types where the system performs best and worst.
- (Analysis) Compare the retrieval quality of three configurations: (a) dense retrieval only, (b) BM25 only, (c) hybrid with RRF. Use the same 10 queries from the previous exercise. For each configuration, compute Recall@5 and MRR (using your manual relevance labels as ground truth). Quantify the improvement that hybrid search provides over each individual retriever. Does the improvement match the theoretical expectation from the "embedding collapse" discussion in Section 11.3?
What's Next
The Repository Intelligence Layer gives coding agents a way to understand and navigate large codebases. In Chapter 12: Building MCP Servers for Scientific Workflows, we wrap the RIL (and many other tools) in the Model Context Protocol, creating a standardized interface through which any agent, regardless of its hosting framework, can query repository structure, retrieve relevant code, run tests, and invoke development tools. The context engineering pipeline from this chapter becomes the backbone of every agent interaction in the rest of Part II.