The problem in one line: every long-context model advertises near-perfect needle-in-a-haystack recall, and it tells you nothing about where your agent breaks — because needle recall measures match-and-copy, the one operation whose accuracy doesn't degrade with length. If cheap 1M-token context has you tempted to stop managing your agent's window, run this first. It's 20 minutes and about a hundred calls, and it gives you the only number that matters: where your model, on your prompt shape, on your domain, falls off the cliff.

The four moves#

The whole eval is a grid. You insert a fact you know into filler of varying length, at varying depth, then ask a question that can't be answered by keyword-matching, and score how accuracy changes across the grid.

1. Filler from your own domain — never lorem ipsum

Context rot is content-dependent. A model padded with plausible, on-topic distractors degrades far more than one padded with obvious nonsense — and your production context is nothing but plausible distractors. Use real (redacted) material from what you ship: support tickets for a support agent, repo files for a code agent.

# haystack.py — build padding from YOUR domain, not lorem ipsum
from pathlib import Path

def load_filler(target_tokens: int, est_tok_per_char=0.25) -> str:
    # concatenate real domain text until we hit ~target_tokens
    chunks, total = [], 0
    for f in Path("sample_domain_text/").glob("*.txt"):
        t = f.read_text()
        chunks.append(t); total += len(t) * est_tok_per_char
        if total >= target_tokens:
            break
    return "\n\n".join(chunks)

2. Insert the needle at several depths and lengths

Bracket your real usage. A practical grid is four lengths — 4k, 16k, 64k, and your true max (say 200k) — crossed with three depths — 10%, 50%, 90% through the filler. Twelve cells.

# place.py — drop a known fact at a fractional depth in the filler
def plant(filler: str, needle: str, depth: float) -> str:
    cut = int(len(filler) * depth)
    return filler[:cut] + f"\n\n{needle}\n\n" + filler[cut:]

# The needle is a fact you invent, so there's zero chance it's in the filler:
NEEDLE = "Marisol always leaves the spare key under the third planter on the back porch."

3. Ask a non-lexical question (this is the whole trick)

A verbatim question lets the model string-match its way to the answer without understanding anything — which is why NIAH flatters every model to ~99%. Ask a NoLiMa-style question that shares no keywords with the needle, so the model has to associate by meaning. Then add one multi-hop question that forces it to combine the needle with a second planted fact — the hardest case, where degradation is largest.

# Lexical (DON'T rely on this — it cheats via shared words):
#   "Where does Marisol leave the spare key?"
#
# Non-lexical (DO use this — no shared keywords with the needle):
QUESTION = "If someone was locked out of the house, where should they look to get in?"
EXPECTED = "third planter"   # substring match is enough to score

4. Score across the grid and read the cliff

Run each cell several times with different needle facts to average out noise (~8 runs × 12 cells ≈ 100 calls), and record exact-substring accuracy per cell.

# run.py — sketch of the grid runner
import itertools, statistics

LENGTHS = [4_000, 16_000, 64_000, 200_000]
DEPTHS  = [0.10, 0.50, 0.90]

def score_cell(client, model, length, depth, trials=8):
    hits = 0
    for _ in range(trials):
        filler = load_filler(length)
        ctx = plant(filler, NEEDLE, depth)
        ans = ask(client, model, ctx, QUESTION)   # your call wrapper
        hits += (EXPECTED.lower() in ans.lower())
    return hits / trials

grid = {(L, d): score_cell(client, MODEL, L, d)
        for L, d in itertools.product(LENGTHS, DEPTHS)}

for L in LENGTHS:
    row = "  ".join(f"{grid[(L,d)]:.0%}" for d in DEPTHS)
    print(f"{L:>7}:  {row}")

The output is a heat map of accuracy by length (rows) and depth (columns). You're reading for two things:

Reading your result#

What to do with the number#

Set your context editing or compaction trigger just below the cliff — keep the live window under the length where accuracy holds, and offload the rest to retrieval or a memory tool. Then re-run this whenever you change models or materially reshape your prompt; the cliff moves with both. If you want the benchmark theory behind why the non-lexical question matters, RULER vs needle-in-a-haystack is the deeper cut, and how to evaluate a RAG pipeline shares most of this scaffolding for the retrieval side.

Publish the result to your team in one sentence: "Our effective window is 40k, not 200k." That single number kills a whole class of bugs — because the model will never tell you it's guessing, and the vendor's spec sheet certainly won't.