Core concepts

Context intelligence

Retrieval ≠ context. The default pool path serves a budget-packed, attributed payload of globally-ranked fresh claims; coverage gates escalate to raw when the cache under-covers. Unit-path projection and strategies are documented as the legacy path.

Traditional RAG does retrieval — dump the top-k and hope the answer is in the noise. Coalent does context engineering — the minimum sufficient signal for the decision, with the raw on tap.

TIP

Key idea (the default pool path). Every read serves r.context["pool"] — a token-budgeted payload of globally-ranked fresh claims, grouped under per-source attribution headers. r.pool lists the served claims in served order. Raw evidence stays reachable (drill(unit_id), or appended automatically when the read escalates). Minimal payload, nothing lost.

The pool payload (the default)

New in v0.7 read_path="pool" is the default whenever a semantic embedder is available — which is every keyed or production deployment. It's the measured path behind every published benchmark number, shipped opt-in through the v0.6 transition releases. Every read is answered by budget-packing the globally-ranked pool of fresh claims across all units; units remain the ownership / freshness / build / provenance skeleton. (read_path="unit" is the legacy escape hatch and the keyless fallback — byte-identical pre-0.7 behavior, below.)

cache = SemanticCache(retriever, synth, embedder=OpenAIEmbedder())   # semantic embedder → pool path

r = cache.get("what changed in the Enterprise plan this quarter?")
r.context["pool"]     # the packed, attributed claim payload — hand this to your answerer
r.pool                # served claims in served order (owner + score per claim)
  • serve_budget is the one knob most users touch: None resolves to 1000 on the pool path (the measured operating point: 0.731 strict accuracy @ 981 mean tokens, n=605) and 600 on the unit path.
  • serve_gate (default None) is the serve-vs-build decision: an explicit float is absolute (reproducible benches); None adapts against the pool's own null-shaped noise ceiling — see why it doesn't serve the wrong answer.
  • Freshness holds at claim granularity: a stale unit's claims are masked the moment a source changes.
  • Pool ranking needs a semantic embedder (claim cosine collapses to keyword overlap on the lexical fallback). Keyless zero-config construction therefore falls back to the unit path with a warning, and explicitly pinning read_path="pool" under the HashingEmbedder raises. Set OPENAI_API_KEY or pass embedder=.

Measured on the 605-question news benchmark (frozen held-out queries, strict grading): 0.731 @ 981 mean context tokens — naive k9's accuracy at 25% fewer tokens (0.711 @ 1,311) and naive's best measured point (k12 0.731 @ 1,729) at 43% fewer. Gold-claim rank in final pool order: p50/p75/p90 = 1/6/15. CIs overlap with naive — this is parity at fewer tokens, not an accuracy win. See the benchmark.

CAUTION

The v0.5 serve="pool" preview (a different knob) is deprecated and never auto-mapped to the pool read path; its deprecation clock runs from 0.7.0, the release that flipped the default. On the pool path the unit-path read knobs (hit_threshold, hit_margin, route_by_claim, recall_*, select_floor, ...) are inert; they keep full function on the legacy path.

Result contract, path by path

FieldPool path (default)Unit path (legacy)
understanding{"claims": [...]} — no summary key{"summary": ..., "claims": ...}
unit_idowner of the top-ranked served claimthe matched unit
confidencepre-decision pool coverageblended unit score
evidence[] on non-escalated serves — cite via drill(unit_id)populated on hits
cache_hitzero synthesis calls ran this readmatch above threshold
poolserved claims in served order[]
recalled[] — the scan is already cross-unitcross-unit recall results

The pool_header golden path

The pool payload attributes every source group with a header — and the header choice is a measured ladder (same frozen store, 605 queries, strict grading):

HeaderAccuracyRefusals
opaque id [source: art:N]0.641153
built-in unit-title fallback ## {unit.query[:60]}0.678136
[title | source | date] metadata (recommended)0.731101

The gap between the last two is a unit-metadata limit, not a header-format problem: outlet and date live in your corpus metadata. New in v0.7 closes it the easy way — pass meta= at ingest and the default metadata-first header renders the golden path with no callable at all:

retriever.add("news:az-01", article_text,
              meta={"title": "Azure region refresh", "source": "CloudWire", "date": "2026-05-02"})
# the default header now renders [Azure region refresh | CloudWire | 2026-05-02]

Meta-less ingests keep the unit-title fallback header, and an explicit pool_header callable still wins when you set one — the callable remains the recipe for corpora you can't re-ingest:

meta = {art["artifact_id"]: art for art in corpus}   # your corpus metadata

def header(unit) -> str:
    aid = unit.evidence[0].artifact_id if unit.evidence else ""
    art = meta.get(aid)
    if art is None:
        # an empty return serves the group HEADERLESS (the built-in default does
        # NOT take over once a callable is set) — emit your own fallback instead:
        return f"[source: {aid or unit.id}]"
    return f"[{art['title']} | {art['source']} | {art['published_at'][:10]}]"

cache = SemanticCache(retriever, synth, embedder=OpenAIEmbedder(), pool_header=header)

A raising header callable is swallowed (the group serves headerless — attribution degrades, serving never breaks); header tokens count against serve_budget. If your questions never ask "which source said X", the default header is fine — its extra refusals are honest refusals of per-outlet questions its payload cannot attribute.

Coverage and the RAG floor (both paths)

Every read scores coverage — how well what's about to serve actually answers this query — and when the final coverage falls below coverage_floor (~0.28 OpenAI, auto-derived), the read escalates: attributed raw retrieval is appended to the payload, no LLM call, the cache untouched. A read that under-covers degrades to honest retrieval, never below it — and never answers thin.

r = cache.get("how many leave days for a 5-year employee?")
r.coverage     # 0.0–1.0
r.escalated    # True if attributed raw was appended to cover the question

New in v0.3 The default coverage check is cosine — cheap, no extra model call. For containment-grade precision, plug a coverage_scorer — a (query, understanding) -> float callable backed by a cross-encoder, NLI model, or one-token LLM yes/no — consulted only in the ambiguous band below coverage_ceiling. Applies on both paths.

NOTE

cache.stats() reports hit_rate and escalation_rate — your live signal for whether coverage is too loose (silent gaps) or too strict (escalating to raw too often).

The behavioral stack (opt-in, default-OFF)

New in v0.6 Four pieces, none of which runs unless switched on: residual_spans=True captures fact-bearing sentences the extractor missed as tier-2 spans on the unit; a refusal from your answerer triggers report_refusal(read_id) → an attributed retry payload; report_success(read_id) confirms the rescue, earns a durable query key (query_keys=True), and the unit self-repairs append-only on its next rebuild touch.

cache = SemanticCache(
    retriever, synth, embedder=OpenAIEmbedder(),
    residual_spans=True,     # span capture + refusal fallback
    query_keys=True,         # durable keys from confirmed rescues (pool path only)
)

r = cache.get("what did the report say about Q3 revenue?")
answer = my_llm(r.context["pool"])
if is_refusal(answer):                        # your own refusal detector
    retry = cache.report_refusal(r.read_id)   # None when no span clears the floor
    if retry is not None:
        answer = my_llm(r.context["pool"] + "\n\n" + retry)
        if not is_refusal(answer):
            cache.report_success(r.read_id)   # confirms the rescue -> durable key

Measured on the capstone (n=605, same store): final accuracy +3.1 pts, refusals 91 → 61 (−33%), tokens +2.1%, zero newly-wrong answers; keyed-class first-pass 0% → 61% on mild-paraphrase revisits (n=18, small).

Honest limits, part of the claim: the fallback flips roughly 20% of natural refusals unconditionally (33% when the payload contains the answer verbatim) — the 68% lab figure holds only for span-derived questions. Query keys can collide across sibling articles in dense same-topic corpora (observed 3/605, answers still correct) — raise key_floor above its 0.85 default there. And keys don't move final accuracy on diverse rewordings; their value is converting refusal round-trips into first-pass answers.

Wiring rules: call report_refusal only on genuine refusals, with the same read's Result.read_id (a 64-read ring buffer; stale ids return None, never raise); call report_success only when the retry answered; bound your retry loop to one fallback round-trip.

New in v0.7 Behind this v0.6 retry sits the deeper ladder: when the refusal persists, repair(read_id) re-extracts what the build missed and appends it permanently, and reprobe / serve_unserved rework the unchanged pool at $0. See The failure chain.

Agent affordances

Escalation is also explicit, on both paths:

cache.drill(r.unit_id)              # the full raw evidence behind a unit
cache.widen("our leave policy")     # a fresh retrieval for a query

The unit path (legacy)

Version-scoped: everything below describes read_path="unit" — the pre-0.7 default, kept byte-identical as the escape hatch.

Minimum-context projection. On the unit path, r.context is a compact, query-shaped slice of the one matched unit: r.context["understanding"] carries the summary plus only the query-relevant claims/facts (irrelevant atoms trimmed for this query), and r.context["raw"] rides along per the strategy below. The pool path replaced per-unit projection with global claim ranking — packing is the projection there.

Strategies. How much raw rides along in the unit-path payload (the full raw stays reachable regardless):

Strategyr.context["raw"]
CONTEXT_FIRST (default)raw only when the read escalated
CONTEXT_RAWraw always
CONTEXT_ONLYnever (understanding only)

Override per call: cache.get(query, strategy=ContextStrategy.CONTEXT_RAW).

Coverage on the matched unit. The unit path's coverage is the max per-claim cosine of the matched unit — a broadly-right unit missing a specific number escalates rather than answering thin. New in v0.5 Builds can also widen instead of escalating later: widen_chunks=N lets a miss-triggered build read up to N chunks of the dominant source rather than the retrieval keyhole (cold-start pilot: median 23 chunks read vs 2; rebuild churn 460 → 31). Never fires at ingest.

Related units (cross-unit reuse). get() folds in up to related= units sharing an entity or a source with the match, ranked by relevance — light, lazy multi-hop, not a graph engine:

r = cache.get("our leave policy", related=3)      # unit path
for rel in r.related:
    rel.unit_id, rel.relation, rel.understanding  # "shared_entity" | "shared_source"

Next