ContextPull · proposal · document retrieval for LLM agents

ContextPull. Pull, don't push.

Classic RAG pushes the top-k chunks at the model before it has thought about the question. Claude Code works the other way round: a small index is always in context, and the model pulls exact content on demand with tools. ContextPull packages that pattern for any document corpus as an MCP server, and measures it honestly against the pipeline it replaces.

The model never receives content it did not ask for. It reads verbatim sections by ID, then generates.
one question, two mechanisms

Watch the same request go through both.

A comparison question, the shape naive RAG fails hardest on. Press play, or step through. Amber marks a decision the model makes itself.

USER What changed in the refund window between the 2024 and 2025 policy?

Push · classic RAG

retrieve → stuff → generate
  1. S
    Embed the question
    One vector for a question about two documents. [0.12, -0.44, 0.08, …]
  2. S
    Nearest neighbours, k = 5
    Both years score alike, so the store returns a blend, plus a stale year that happens to be close.
    policy-2025.md#3 policy-2024.md#3 policy-2023.md#3 faq.md#2 policy-2024.md#7
  3. S
    Stuff the prompt
    Five chunks concatenated ahead of the question. The model has not chosen any of them.
  4. M
    Generate
    “Refunds are accepted within 30 days of purchase. Store credit may be offered after that period.”
Fluent, and wrong.
30 days is the 2023 and 2024 ruleno diff was producednothing errored
  1. $ press play to run the request

Pull · ContextPull

index → decide → read → generate
  1. M
    Read the always-in-context index
    A table of contents with one line per document, about 2k tokens. Like MEMORY.md in Claude Code.
    policy-2023.md · refunds, superseded policy-2024.md · refunds, shipping policy-2025.md · refunds, credits faq.md
  2. M
    Decide what to fetch
    The question names two years. The model plans two reads, not one search.
  3. M
    Call search
    search("refund window", in: ["policy-2024.md", "policy-2025.md"]) → two section IDs, with headings.
    policy-2024.md#3 · Refund windowpolicy-2025.md#3 · Refund window
  4. M
    Call read twice
    Verbatim text of each section, nothing else. 2024: “full refund within 30 days.” 2025: “full refund within 14 days; store credit to day 30.”
  5. M
    Generate, with citations
    “The full-refund window shrank from 30 days to 14. Days 15–30 now give store credit instead. [policy-2024.md#3] [policy-2025.md#3]
Correct, and checkable.
two cited sectionsthree tool callsthe stale 2023 doc was never read
  1. $ press play to run the request

The refund policy corpus is an illustrative example. Measured numbers, including two pull rows, are further down.

what we would build

ContextPull is an MCP server that makes a document folder feel like a codebase.

Ingest once into a store with stable section IDs. Emit a compact index that lives in context. Expose four tools. The client can be Claude Code or any MCP host.

Document corpus policy-2024.md policy-2025.md faq.md · *.pdf ingest once Section store stable IDs policy-2025.md#3 heading path, neighbours BM25 index embeddings, optional ContextPull server index() search(q, in?) read(id) grep(pattern) neighbours(id) table of contents, ≈2k tokens IDs + headings, never bodies verbatim text of one section exact identifiers, error codes the row's header, the next clause always in context calls, on demand verbatim + id Model Claude Code, any MCP host reads index decides what to fetch reads sections repeats if needed then generates with section IDs as citations the push edge: top-k chunks straight into the prompt. Removed.
Two arrows carry the whole argument. The index arrow is always on and small. The read arrow only fires when the model asks. The dashed edge at the bottom is what classic RAG does and what this design deletes.
what actually changes

Same store. Different controller.

Push · classic RAGPull · ContextPull
Who decides what is retrievedThe pipeline, from one embedding of the raw questionThe model, after reading the index and thinking about the question
What the model seesk chunks it did not choose, relevant or notOnly sections it asked for, verbatim, with IDs
Comparison and aggregation questionsOne blended query returns a mush of both documents, or misses a source entirelySeveral targeted reads, then reasoning across them
Exact lookupsEmbeddings blur identifiers; needs a separate BM25 pathgrep is a first-class tool
TablesChunking cuts the row from its headerneighbours returns the header with the row
CitationsPost-hoc, often approximateThe section ID the model read
CostPredictable: one embed, one promptVariable: more calls, more latency, more tokens on hard questions
Failure modeSilent. The answer is fluent from the wrong chunksVisible. The trace shows what was read, and the model can say it found nothing
Where it can still failRetrievalRetrieval. A bad index or a weak search still misses. The loop fixes ranking, not recall
how we will know

The agentic row goes in the same table as everything else.

We already have the measuring instrument. ragbisect builds an eval set from the corpus, scores each retrieval stage on its own, and prints what a run spent. An agentic retriever fits its one-method adapter: run the loop, return the section IDs the model read.

configrecall@5mrr@5ndcg@5 given hittokens / query
bm250.9230.8050.9050
dense, text-embedding-3-small0.8510.7380.901≈ 15
hybrid dense + bm25, RRF0.9640.8210.889≈ 15
ContextPull pull, Claude Code (10-question sample)1.0000.8830.913≈ 86,700
ContextPull pull, gpt-5.4-mini reasoning off0.6150.5850.964≈ 10,900

Measured on the uv documentation, 603 ContextPull sections, 221 self-generated questions in two shapes, recall@5. Two pull rows: a strong agent reads the right section on every sampled question; a small model with reasoning off reads the right section when it reads but misses it on 38% of questions. Push costs milliseconds and no model tokens; pull costs tens of thousands of tokens a question. Full table and the mistakes made while measuring are in the ContextPull docs.

build plan

Four weeks to a number.

  1. Ingest and store

    Markdown, text, then PDF. Heading-aware sectioning with stable IDs, heading paths, neighbours, a BM25 index. Reuse the ragbisect chunker and corpus code.

  2. ContextPull server

    Five tools: index, search, read, grep, neighbours. Stdio transport first. Ships as contextpull on PyPI and npm. Works in Claude Code with one config line.

  3. Agentic adapter

    A ragbisect adapter that runs the tool loop against ContextPull and returns the section IDs read. Fill the blank row. Include tokens and latency per query.

  4. Publish, including the losses

    All five question shapes, several corpora, both pipelines. Where pull does not help, say so. That is the whole positioning.

what we are betting on

Why now

  • Context windows are large enough that a 2k-token index for a whole corpus is cheap to keep resident.
  • Models are good at multi-step tool use. A wrong first search gets corrected instead of being stuffed into the prompt.
  • MCP gives a standard way to attach the store to Claude Code, Claude Desktop and other hosts without a rewrite.
  • Nobody publishes the comparison. The measurement is the moat.
still open

What could sink it

  • Large corpora. A 2k-token index does not cover 50,000 documents. Hierarchical indexes or a first-hop search are needed past some size.
  • Latency. Three tool calls is three round trips. Fine for a coding agent, maybe not for a chat widget.
  • Prior art overlaps: MCP filesystem servers, agentic RAG in LlamaIndex and LangGraph. The differentiator has to be the index design and the numbers.
  • A weak search tool makes the whole loop weak. The store still has to be good.