Core concepts

Context intelligence

Retrieval ≠ context. Coalent serves the minimum decision-relevant slice, auto-escalates when it's thin, and folds in related units.

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. get() returns ctx.context — the decision-relevant slice for this query — while ctx.evidence / ctx.raw_text keep the full source reachable. Minimal payload, nothing lost.

The coverage gate (auto-escalation)

Every read scores how well the cached unit covers the query. A hit that under-covers automatically pulls fresh raw for that query — no LLM call, no manual signal:

ctx = cache.get("how many leave days for a 5-year employee?")
ctx.coverage     # 0.0–1.0: how well the unit covers the query
ctx.escalated    # True if it had to fetch fresh raw to cover the question

So a cached unit that's broadly right but missing a specific number doesn't serve a thin answer — it escalates to fetch that detail. The specifics are always there when the model needs them.

New in v0.5 Builds can also widen instead of escalating later: with widen_chunks=N, a miss-triggered build reads up to N chunks of the dominant source (via a duck-typed retriever.widen(artifact_id, limit=) or a BYO source_fetcher) rather than just the retrieval keyhole — so the unit covers later questions instead of running thin on them. It never fires at ingest; widen_on_admission extends it to thin-coverage admission rebuilds. In the cold-start pilot, widened units read a median 23 chunks of their source vs 2 for keyhole builds, and rebuild churn fell 460 → 31.

Tuning coverage — and entailment-grade precision

New in v0.3 The default coverage check is cosine over the unit's per-claim embeddings — cheap, no extra model call. It's tunable, and for hard cases it's pluggable:

SemanticCache(
    retriever, synth, embedder=OpenAIEmbedder(),
    coverage_floor=0.3,                # below this -> escalate (auto-derived per embedder by default)
    enable_coverage_escalation=True,   # the RAG-floor safety net (on by default)
    coverage_scorer=my_entailment,     # OPTIONAL: a containment check cosine can't do
    coverage_ceiling=0.65,             # two-tier: consult the scorer only on borderline queries
)

Cosine measures similarity, not containment — so a query that's only topically adjacent to a unit (e.g. "sick days" vs an annual-leave unit) can score "covered" yet lack the specific fact. For containment-grade accuracy, plug a coverage_scorer — a (query, understanding) -> float callable backed by a cross-encoder, an NLI model, or a one-token LLM yes/no. coverage_ceiling keeps it cheap by consulting the scorer only in the ambiguous band. The default stays pure cosine; the scorer is entirely opt-in.

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).

Minimum-context projection

ctx.context is a compact, query-shaped payload:

ctx.context["understanding"]   # summary + only the query-relevant claims/facts
ctx.context["raw"]             # raw included only when needed (see strategies)

Irrelevant claims and facts are trimmed for this query — less noise to the LLM, which is both cheaper and higher quality.

Strategies

Choose how much raw rides along (the full raw is always reachable regardless):

from coalent import ContextStrategy

SemanticCache(retriever, synth, strategy=ContextStrategy.CONTEXT_FIRST)  # default
Strategyctx.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).

The pool read path (v0.6)

New in v0.6 read_path="pool" (default "unit") is the shipped, opt-in alternative to unit-anchored serving: 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.

cache = SemanticCache(retriever, synth, embedder=OpenAIEmbedder(), read_path="pool")

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 (v0.5 preserved).
  • 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.
  • Freshness holds at claim granularity: a stale unit's claims are masked the moment a source changes.
  • Constructor guards fail loud: read_path="pool" under the lexical HashingEmbedder raises ValueError (claim cosine would collapse to keyword overlap) — use a semantic 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 still works verbatim in v0.6 but is deprecated (removal in v0.7) and is never auto-mapped to read_path="pool". 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 default path.

Result contract on the pool path

The unit path is untouched; on the pool path check these deltas:

FieldUnit path (default)Pool path
understanding{"summary": ..., "claims": ...}{"claims": [...]}no summary key
unit_idthe matched unitowner of the top-ranked served claim
confidenceblended unit scorepre-decision pool coverage
evidencepopulated on hits[] on non-escalated serves — cite via drill(unit_id)
cache_hitmatch above thresholdzero synthesis calls ran this read
pool[]served claims in served order

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 default ## {unit.query[:60]} (what ships)0.678136
your metadata callable [title | source | date] (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, nowhere on the unit — so wire them in (an ingest-time metadata field is a v0.7 item). The recipe the 0.731 rig used:

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, read_path="pool", 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.

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(),
    read_path="pool",
    residual_spans=True,     # span capture + refusal fallback
    query_keys=True,         # durable keys from confirmed rescues (requires the pool path)
)

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. query_keys=True without read_path="pool" raises at construction — on the unit path keys would attach but structurally never fire.

get() folds in related cognition units — ones sharing an entity or a source with the match, ranked by relevance to your query:

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

This is light, lazy multi-hop — enough for cross-document reuse, not a graph engine.

Agent affordances

For agent loops, escalation is also explicit:

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

Next