coalent-mcp
README.md
<p align="center">
<img src="https://raw.githubusercontent.com/Vectorlink-Labs/coalent/main/brand/wordmark.png" alt="Coalent" width="320">
</p>
<p align="center">
<b>Real-time, provenance-invalidated context for AI agents & RAG.</b><br>
<i>Build understanding once. Reuse it everywhere. Keep it fresh β automatically.</i>
</p>
<p align="center">
<img alt="pypi" src="https://img.shields.io/pypi/v/coalent?color=5145E5">
<img alt="python" src="https://img.shields.io/badge/python-3.10%2B-4F46E5">
<img alt="license" src="https://img.shields.io/badge/license-Apache%202.0-22D3EE">
<img alt="typed" src="https://img.shields.io/badge/mypy-strict-2DD4BF">
<img alt="tests" src="https://img.shields.io/badge/tests-passing-10B981">
<a href="https://discord.gg/v3hvg3nwr"><img alt="discord" src="https://img.shields.io/badge/Discord-join-5865F2?logo=discord&logoColor=white"></a>
</p>
<p align="center">
<b>π <a href="https://coalent.ai/docs">Documentation</a></b> Β· <a href="https://coalent.ai">coalent.ai</a> Β· <a href="https://discord.gg/v3hvg3nwr">π¬ Discord</a>
</p>
<p align="center">
<a href="#quickstart">Quickstart</a> Β·
<a href="#whats-new-in-v07">What's new in v0.7</a> Β·
<a href="#the-read-path--a-ladder-of-gates">Gate ladder</a> Β·
<a href="#bring-your-own-stack">Bring your own stack</a> Β·
<a href="#use-it-from-claude-code--cursor-mcp">MCP</a> Β·
<a href="#langchain">LangChain</a> Β·
<a href="#benchmark">Benchmark</a> Β·
<a href="#cli">CLI</a>
</p>
---
> **Your agent re-reads the same sources on every call β and the moment a source changes, every cached answer is silently wrong.**
>
> Coalent builds the *understanding* once, caches it by what the query **means**, and invalidates it **surgically** the instant an underlying source changes. As correct as re-reading everything, at a fraction of the cost β and never stale.
## Why Coalent
Every context layer is forced to trade off three things. Coalent is built to hold all three at once:
- π§ **Extractive understanding, not chunks.** It caches a *query-independent* set of atomic, source-grounded **claims** your LLM extracted β keeping every number and fact β so one cached unit answers many *different* later questions. The raw evidence is retained with each unit, so a hit that under-covers a query falls back to retrieval instead of answering thin.
- β»οΈ **Reuse across queries, agents β and documents.** A semantic cache keyed by query *meaning*: ask again, or from another agent, and it's a warm hit. **Cross-unit recall** pools claims across units to answer **multi-hop** questions whose evidence spans documents β at **zero extra LLM calls**.
- πΏ **Fresh by provenance.** Every unit remembers the exact sources it used. When one changes, only the units that actually used it go stale β precisely, automatically, and lazily.
Coalent sits **above retrieval** β bring any retriever (vector DB, hybrid search, GraphRAG, tools, APIs). It's the freshness-and-reuse layer, not another retriever β deliberately the *opposite* of GraphRAG's build-the-whole-graph-upfront tax: **lightweight, independent units, built lazily only when a query actually needs one**, and refreshed by dirtying a single unit (no graph surgery).
> **New in v0.7** β the **self-healing release**. The failure chain: when a read fails,
> your agent calls `repair(read_id)` and the cache re-extracts what its build missed β
> **permanently**. Measured on the same frozen 605-question rig: **0.826 vs 0.774** for
> the strongest v0.6 configuration, **+5.3 points at an identical ~983-token serving
> budget**, final refusals **β69%**. And the **default read path flips to pool**: under a
> semantic embedder the read path resolves to `"pool"` automatically (`read_path="unit"`
> stays the byte-identical escape hatch). See [What's new in v0.7](#whats-new-in-v07).
>
> **New in v0.6.1** β the **MCP server**: `coalent-mcp` puts the cache one line away from Claude Code, Cursor, or any MCP client ([Use it from Claude Code / Cursor](#use-it-from-claude-code--cursor-mcp)), and **[`langchain-coalent`](#langchain)** makes your existing LangChain stack the cache's substrate. Both additive-only.
>
> **v0.6** β the **pool read path** (`read_path="pool"`): every read serves the token-budgeted, globally ranked fresh-claim pool. Measured on the same benchmark: **0.731 accuracy @ 981 context tokens** β naive's best measured accuracy (top-12: 0.731 @ 1,729) at **~43% fewer tokens**. Plus the default-OFF **behavioral stack**, measured at **β33% refusals** and **+3.1 pts** on the same store.
## Install
```bash
pip install coalent # the core has zero required dependencies
```
## Quickstart
Runs as-is β `StubSynthesizer` needs no API key, so you can feel the loop in ten seconds:
```python
from coalent import SemanticCache, InMemoryRetriever, StubSynthesizer
# 1. Any retriever β a vector DB, a tool, an API. (In-memory here for the demo.)
retriever = InMemoryRetriever()
retriever.add("confluence:hr", "Leave policy: 21 days of annual leave per year.")
# 2. Build the cache. Swap StubSynthesizer for a real LLM below.
cache = SemanticCache(retriever, StubSynthesizer())
# 3. Ask. The first call builds understanding and caches it; the next is a warm hit.
result = cache.get("what is our leave policy?")
print(result.context["understanding"])
print(result.cache_hit) # False (cold) -> True on the next call
# 4. A source changed? Only the units that used it go stale β surgically.
cache.source_changed("confluence:hr", text="Leave policy: now 25 days.")
# the next matching read rebuilds just that one unit, lazily
```
(The no-key demo above runs the classic **unit** read path β with no semantic embedder
available, the cache says so loudly and falls back. Wire a real embedder, as below, and
the default becomes the measured **pool** read path.)
Wire in a real model β any text-in / text-out LLM works. In v0.4 the synthesizer builds **extractive** understanding by default (query-independent atomic claims that keep every fact), and the cache does **cross-unit recall** β both on automatically:
```python
from coalent import SemanticCache, LLMSynthesizer, OpenAIProvider, OpenAIEmbedder
cache = SemanticCache(
retriever,
LLMSynthesizer(OpenAIProvider(), model="gpt-4o-mini"), # extract=True by default (v0.4)
embedder=OpenAIEmbedder(), # match queries by MEANING (recommended for real use)
)
# ^ with a semantic embedder, v0.7 resolves the default read path to "pool" β the
# measured path. Pass read_path="unit" for the pre-0.7 unit default, byte-identical.
# Multi-hop across documents? recall is already on; raise its trigger to bridge units:
# SemanticCache(retriever, synth, embedder=..., recall_threshold=0.7)
```
**The pool read path β the DEFAULT since v0.7** (whenever a semantic embedder is available): every read serves the budget-packed, globally ranked fresh-claim pool instead of one routed unit. Attribution is the one thing to wire: a 3-line `pool_header` callable mapping each unit to `[title | source | date]` from your own corpus metadata. This is the measured golden path β on a 605-question news benchmark (strict grading), **0.68** accuracy with the bare built-in header vs **0.73** with this callable, same store, same queries:
```python
DOC_META = { # your corpus metadata, keyed by artifact id
"docs:azure-refresh": {"title": "Azure region refresh", "source": "CloudWire", "date": "2026-05-02"},
}
def pool_header(unit) -> str: # the [title | source | date] golden path β 3 lines
meta = DOC_META.get(unit.evidence[0].artifact_id if unit.evidence else "")
return f"[{meta['title']} | {meta['source']} | {meta['date']}]" if meta else f"[source: {unit.id}]"
cache = SemanticCache(retriever, synthesizer, embedder=OpenAIEmbedder(),
pool_header=pool_header) # pool is the resolved default (v0.7)
result = cache.get("which regions got the refresh?")
result.context["pool"] # the packed, attributed claim payload β hand it to your answer model
```
Runnable no-API-key demo, including the refusal loop: [examples/pool_read_path.py](examples/pool_read_path.py).
## Use it from Claude Code / Cursor (MCP)
<!-- mcp-name: io.github.nisarg-pujara-vectorlink/coalent -->
`coalent-mcp` serves fresh, attributed facts from a Coalent cache to any MCP client β
and the facts are invalidated the instant their source changes. One line to wire it into
Claude Code:
```bash
pip install "coalent[mcp,openai]"
claude mcp add coalent -- coalent-mcp --cache-factory my_cache:build
```
(Cursor / Claude Desktop / any MCP client: register the same `coalent-mcp ...` command in
its MCP config.)
**Bring your own cache (`--cache-factory module:function`) β the primary mode.** Your
factory returns a fully constructed `SemanticCache`: your vector DB, your embedder, your
LLM, every knob. The server adds protocol glue only β and the glue is measured to add
**zero quality loss**: factory mode reproduced the library's own benchmark result
byte-identically (0.710 on a 100-question validation run drawn from our n=605 news
benchmark β identical CIs, 100/100 serves, 98/100 answer payloads byte-equal to the
library run).
```python
# my_cache.py β importable from the directory you launch in
from coalent import (SemanticCache, LLMSynthesizer, OpenAIProvider,
OpenAIEmbedder, SQLiteCognitionStore)
def build() -> SemanticCache:
return SemanticCache(
my_vector_retriever, # YOUR vector DB / retriever
LLMSynthesizer(OpenAIProvider()), # YOUR synthesis model
embedder=OpenAIEmbedder(), # YOUR embedder
read_path="pool",
residual_spans=True, query_keys=True, # the behavioral stack, opt-in as ever
pool_header=my_metadata_header, # [title | source | date] β the measured golden path
store=SQLiteCognitionStore("kb.db"), # persistence is yours too
)
```
Freshness here is signal-driven: your ingestion pipeline calls the `source_changed` tool
when a document changes and the affected facts invalidate immediately. (Adding
`--watch DIR` alongside the factory also fires it on file edits β invalidation only; it
never ingests into your index, and it matches only when your artifact ids equal the
watch-relative paths.)
**Zero-config folder mode (`--watch DIR`) β the demo wedge.** Point it at a folder of
docs and you get the recommended v0.6 deployment (pool path, residual spans, query keys,
SQLite persistence, automatic `[path | modified date]` attribution) with no code at all:
```bash
claude mcp add coalent --env OPENAI_API_KEY=$OPENAI_API_KEY -- coalent-mcp --watch ./docs
```
Every read rescans the watched files (mtime + content hash) before serving β you cannot
get a stale answer after saving a file β and an untouched folder restarts fully warm.
**The honest number:** on the same 100-question validation run, folder mode scored
**0.46 vs 0.71** for a factory-built cache (40 vs 18 refusals) β the measured cost of the
generic paragraph chunker and on-demand keyhole builds. Use it to feel the freshness loop
in a minute; bring your own stack for production quality. One regime note: a question
about a *just-added* file can honestly refuse from a warm cache until a read triggers
that file's first build β a refusal, never a stale or wrong answer.
**One shared cache for many agents (`--transport http`).**
```bash
COALENT_MCP_TOKEN=<secret> coalent-mcp --cache-factory my_cache:build --transport http --port 8765
```
One long-lived process, many concurrent MCP clients, ONE shared cache β shared
compounding, no store races (validated: two concurrent clients matched the sequential
reference on all 20 reads, zero duplicate builds). When `COALENT_MCP_TOKEN` is set, every
request must carry `Authorization: Bearer <token>` β bind localhost or trusted networks.
Corollary for stdio: each stdio launch is its own process, so never point two apps at the
same `--store` path β HTTP mode *is* the shared-cache answer.
**The seven tools:** `get_context(query, budget?)` β the attributed, budget-packed
payload + a `read_id` Β· `report_refusal(read_id)` / `report_success(read_id)` β the
behavioral repair loop over MCP Β· `source_changed(artifact_id, text?)` β the BYO
freshness feed (unchanged content is hash-detected and skipped) Β· `list_sources()` Β·
`cache_stats()` Β· `refresh()`.
## LangChain
[`langchain-coalent`](integrations/langchain-coalent) makes Coalent a LangChain-native
freshness/reuse layer β BYO-first: your existing VectorStore (or retriever), embeddings,
and chat model become the cache's substrate, unchanged.
```bash
pip install langchain-coalent
```
```python
from langchain_coalent import create_coalent_cache, CoalentRetriever
cache = create_coalent_cache(my_vectorstore, llm=my_chat_model, embeddings=my_embeddings)
retriever = CoalentRetriever(cache=cache) # drop-in LangChain BaseRetriever
docs = retriever.invoke("what is our leave policy?")
docs[0].page_content # the served, attributed context payload
docs[0].metadata["read_id"] # -> cache.report_refusal() / report_success()
docs[0].metadata["cache_hit"] # True == served with zero LLM spend
cache.source_changed("policy.md", text=new_text) # surgical, provenance-keyed invalidation
```
Every Coalent knob passes through `create_coalent_cache`; the refusalβrepair loop ships
as a runnable LangGraph-shaped example in the package. Depends only on `coalent>=0.6` and
`langchain-core>=0.3`.
## What's new in v0.7
The self-healing release β and the release that **flips the default read path**. The
read now ships its own doubt, and the cache gains an explicit failure chain your agent
drives β each rung fires **only on a failed read**, so a read that succeeds pays nothing
new. One deliberate default changes (the read path, first item below); **every NEW v0.7
knob is default-OFF and byte-inert until armed** (pinned by tests). Full details in the
[CHANGELOG](CHANGELOG.md).
<!-- SANCTIONED 2026-09-15: every number in the next paragraph -->
Measured on the same frozen news rig as every anchor since v0.5 (609 articles, 605
held-out questions, strict grading + locked adjudication rules): the full v0.7
composition scores **0.826 vs 0.774** for the strongest v0.6 configuration β **+5.3
points at an identical ~983-token serving budget**, final refusals **β69%** (61 β 19).
Gating repair on failure matches always-on accuracy at **14% of the extraction calls**.
The agentic loop β your evaluator decides, the cache heals:
```python
cache = SemanticCache(
retriever, synthesizer, embedder=OpenAIEmbedder(),
# read_path resolves to "pool" automatically under a semantic embedder (v0.7 default)
gap_detector=True, # observe-only: the read reports its own holes
repair_extractor=my_extractor, # BYO callable(span, region, existing_claims) -> [claims]
)
r = cache.get(
question,
subs=planner_subquestions, # your planner's decomposition (optional)
constraints={"dates": ["2023-10-05"], "sources": ["TechCrunch"]}, # intent metadata (optional)
)
answer = my_answerer(r.context["pool"]) # your model, your prompt
r.sources # artifact ids behind the payload
r.max_source_age_s # freshness age of this serve
r.gaps # [{probe, span, source, kind}, ...] β the read's own doubt
if failed(answer): # the failure chain β each rung ONLY on failure
cache.repair(r.read_id) # re-extract what the build missed β PERMANENT
r2 = cache.get(question, subs=planner_subquestions) # repaired claims now compete
answer = my_answerer(r2.context["pool"])
if failed(answer):
r3 = cache.reprobe(r2.read_id) # entity-probe re-rank of the same pool
answer = my_answerer(r3.context["pool"]) if r3 else answer
if failed(answer):
r4 = cache.serve_unserved(r2.read_id) # last rung: force-pack admitted-but-unserved claims
answer = my_answerer(r4.context["pool"]) if r4 else answer
```
- **BREAKING β the default read path resolves to pool.** `read_path` now defaults to
`None` and resolves to `"pool"` whenever a semantic embedder is available β every
keyed/real deployment (`OPENAI_API_KEY` set, or any non-lexical `embedder=`). Pool is
the measured path (every published v0.6/v0.7 number; the flip criteria were met by
the 0.7 battery evidence β 0.826 vs 0.774 above). Keyless zero-config falls back to
the unit path with a **loud warning** naming both remedies (set `OPENAI_API_KEY` /
pass `embedder=`). **Escape hatch:** explicit `read_path="unit"` stays fully
supported and byte-identical to the pre-0.7 default; an explicit `read_path` always
wins over the resolution.
- **The read ships its own provenance and doubt.** `Result.sources` (the artifact ids
actually behind the payload), `Result.max_source_age_s` (the serve's freshness age),
and β with `gap_detector=True` β `Result.probes` / `probe_coverage` / `gaps`: per
probe, whether a raw evidence sentence outscores every claim (`extraction_hole` β
repair terrain) or nothing reaches the probe (`corpus_hole` β route to a tool).
Observe-only by construction: serving is byte-identical ON vs OFF (pinned), and the
sentence tier costs $0 at serve (lazy, embedded once, cached).
- **`repair(read_id)` β the pump.** Consumes the read's banked gap and constraint
candidates plus bridge candidates near what served; one span-anchored BYO extractor
call per candidate (the library still never calls an LLM itself); mechanical near-dup
and defect rejection (antecedent-free pronoun subjects, truncated spans); then an
**append-only, provenance-stamped, permanent** store improvement β the cost is paid
once, every future read benefits. Returns a `RepairReport`.
- **`serve_unserved(read_id)` and `reprobe(read_id, hint=None)` β the refusal rungs.**
Force-pack the claims that lost the packing race, or re-rank the unchanged pool with
entity probes harvested from what served. Embeds only β no LLM, no retrieval, no store
mutation β and by contract they run on refusals, so they cannot un-answer a correct
read.
- **`subs=` / `constraints=` on `get()`.** Your planner hands the read its decomposition
(`subs=["...", ...]` β wins over the `decompose=` callable) and its metadata intent
(`constraints={"dates": ..., "sources": ..., "entities": ...}` β AND across keys, OR
within a key, falling back to OR rather than silencing a read). Constraints are a
**feeder only**: they bank repair candidates and never touch pool scoring or serving
(pinned).
- **Ingest metadata closes the v0.6 attribution gap.** `retriever.add(..., meta={"title":
..., "source": ..., "date": ...})` puts the metadata ON the unit (`source_meta`), and
the default pool header renders the measured `[title | source | date]` golden path
without the `pool_header` callable. Meta-less ingests are byte-identical to 0.6.
- **Flip status:** the pool-path default flip pre-registered in 0.6.0 **is taken in
0.7.0** β on the 0.7 battery evidence and an explicit maintainer call (2026-09-15), in
place of the three pre-registered gates that were never run as specified. It is
conditional by design (pool requires a semantic embedder by contract, so the lexical
fallback stays on unit, loudly). Not a deprecation: `read_path="unit"` stays fully
supported with no `DeprecationWarning`, and `serve="pool"` (the v0.5 preview)
survives unchanged in 0.7.
## What's new in v0.6
The pool-first release. Every n=605 number below comes from one frozen rig β a 609-article news corpus, 605 held-out questions, gpt-4.1-mini answerer, strict grading β the same rig the v0.5 numbers were measured on. Full details in the [CHANGELOG](CHANGELOG.md) and [UPGRADE-0.5-to-0.6.md](UPGRADE-0.5-to-0.6.md).
- **`read_path="pool"` β the claim-pool-first read path** (opt-in in 0.6; **the resolved default since v0.7** under a semantic embedder). Reads are answered by budget-packing the global fresh-claim pool; units remain the ownership / freshness / provenance skeleton. Measured: **0.731 strict accuracy @ 981 mean context tokens** β matching naive top-9 (0.711 @ 1,311) at **~25% fewer tokens** and naive's best measured point (top-12: 0.731 @ 1,729) at **~43% fewer**. The claim is **parity at fewer tokens** (CIs overlap) β not an accuracy beat. Gold-claim serving rank: **p50/p75/p90 = 1/6/15** in pool order.
- **Attribution by default, and a measured header ladder.** `pool_header=None` now renders a built-in per-source header. Same store, same queries: opaque id **0.641** β shipping default **0.678** β your `[title | source | date]` metadata callable **0.731**. The gap is a *unit-metadata* limit (outlet/date live in your corpus, not on the unit) β wire the callable (quickstart above).
- **The behavioral stack (all default-OFF): spans β fallback β repair β keys.** `residual_spans=True` captures fact-bearing sentences the extractor missed as tier-2 spans on the unit (never in the pool). When *your* answerer refuses, `report_refusal(read_id)` returns an attributed retry payload; `report_success(read_id)` confirms the rescue and β with `query_keys=True` β earns a durable alternate key; lossy-marked units self-repair **append-only** on their next rebuild. Measured, driven through the full loop: refusals **91 β 61 (β33%)**, **+3.1 pts** final accuracy at **+2.1% tokens** (same-store comparison), **zero newly-wrong answers**; keyed-class first-pass **0% β 61%** on paraphrase revisits.
- **Adaptive serve gate** (`serve_gate=None`) β adapts against the pool's own noise ceiling; an explicit float disables adaptation (reproducible benches). Shipped only after a $0 replay gate: 605/605 identical serve decisions on both arms, zero builds, zero LLM calls.
- **Hardening & plumbing:** every payload surface carries source attribution (pool payload and escalation raw); cross-owner near-duplicate claims are kept as corroboration; 14 new observability events (`pool_served`, `residual_fallback`, `key_confirmed`, ...); `reranker` hook (serving order only β it can never cause a false serve); `claim_index` BYO pool storage; a v0.5 pool-preview stale-serve hole is fixed.
- **Deprecated:** `serve="pool"` (the v0.5 preview) β still works verbatim in 0.6 and 0.7; migrate to `read_path="pool"`. In 0.6 the default read path stayed `"unit"` (exact v0.5 behavior) behind five pre-registered flip gates β **v0.7 took the flip** (see [What's new in v0.7](#whats-new-in-v07)).
### Honest limits (measured, not hypothetical)
- **The refusal fallback flips ~20% of natural refusals** (33% when the payload contains the answer verbatim) β the 68% figure from lab questions holds only where the payload contains the answer by construction. It is a net over the extraction tail, not a second retriever.
- **Query keys can collide across sibling articles** in dense same-topic corpora (observed 3/605 reads; answers still correct). Raise `key_floor` above its 0.85 default there. Keys convert refusal round-trips into first-pass answers; they do **not** raise final accuracy on diverse rewordings.
- **The shipping default header can only attribute what the unit knows** β the 0.678 β 0.731 gap is a unit-metadata limit, closed today by the `pool_header` callable; an optional ingest-time metadata field is a v0.7 item.
## What's new in v0.5
The pool release β everything a month-long, pre-registered benchmark war on real news data
(MultiHopRAG, 609 articles, third-party questions) taught us, shipped as opt-in features:
- **`preset="multi_hop"`** β one argument arms cross-unit recall + the hop-2 bridge with
calibrated thresholds. Explicit kwargs always win.
- **Source widening** (`widen_chunks=24`) β a miss-triggered build reads up to N chunks of the
dominant source instead of only the retrieved keyhole. Effect in E2E: rebuild churn 460 β 31,
warm-pass accuracy flipped from decaying to compounding. Never fires at ingest.
- **Provenance admission** (`provenance_admission=True`) β an exact-text containment probe
prevents duplicate understanding: covered reads serve without building; thin coverage
widen-rebuilds in place.
- **Adaptive hit gate** (`adaptive_hit=True`) β self-calibrates against score inflation as the
cache grows (fixed thresholds provably absorb everything at scale).
- **Pool serving preview** (`serve="pool"`, `serve_budget=600`, `pool_header=...`) β serve the
token-budgeted, globally ranked fresh-claim pool instead of one routed unit (**experimental**;
superseded by `read_path="pool"` in v0.6 β the preview still works verbatim in 0.6 and 0.7).
Held-out n=605: 0.699 accuracy vs 0.579 for unit serving (z=6.66); statistically ties naive's
k9 arm β its best measured at the time β at 0.79Γ its tokens; 95% null honesty.
Stale units' claims are masked from the pool the moment a source changes.
- **`fast="auto"`** β numpy-accelerated read path when numpy is present
(`pip install "coalent[fast]"`); results are equivalence-pinned to the pure-Python core.
- **Observability** (`on_event=...`) β structured freshness events: builds, rebuilds, admission
reuse, stale reads prevented, recall and bridge activity.
- Deprecated: `select_floor` (superseded by pool serving).
Full numbers and method in the [benchmark](#benchmark) section and CHANGELOG.
## What's new in v0.4
Two capabilities that were an opt-in preview are now the **defaults**, because they're strictly better on the structured / reuse-heavy corpora Coalent targets β and free or dormant everywhere else. Both have a one-line escape hatch back to exact v0.3 (`extract=False`, `cross_unit_recall=False`).
- π― **Extractive understanding (`extract=True`, default).** Instead of a question-shaped prose summary, the synthesizer extracts a *query-independent* list of atomic, source-grounded claims. The same unit now answers many *different* later questions, and **no number is dropped** β a prose summary silently lost ~40% of the numbers in a source in our tests.
- π **Cross-unit claim recall (`cross_unit_recall=True`, default).** When one unit under-covers a query, the cache pools per-claim memory across **all** fresh units (MaxSim) and surfaces the bridge facts β answering **multi-hop** questions naive retrieval *structurally can't* (evidence in a document that doesn't resemble the question), at **zero extra LLM calls**. Dormant/free on single-hop; auto-off under a non-semantic embedder. Surfaced as `result.recalled`.
- π‘οΈ **Precision & serving knobs (opt-in, default off):** `hit_margin` (refuse ambiguous ties), `select_floor` (serve atoms by meaning, fewer tokens), `residual_floor` (recover extractor-missed number spans). See the [gate ladder](#the-read-path--a-ladder-of-gates) for when to reach for each.
Upgrading from v0.3? See **[UPGRADE-0.3-to-0.4.md](UPGRADE-0.3-to-0.4.md)** β additive, one behaviour change (understanding is now claims, not prose).
## How it works
```
query βββΊ embed βββΊ semantic cache
β hit & fresh? βββΊ serve cached understanding (no retrieval, no LLM)
β miss / stale? ββ
βΌ βΌ
your Retriever βββΊ your Synthesizer βββΊ Cognition unit
(vector/tool/API) (LLM or passthrough) { understanding
β² + raw evidence
β + provenance }
source changed βββββββββββββ dirties ONLY the units that used that source
```
1. **Embed the query** and look for an existing unit with similar meaning.
2. **Hit + fresh** β return the cached understanding (no retrieval, no LLM call).
3. **Miss or stale** β retrieve, synthesize understanding, **retain the raw evidence**, record **provenance** (the exact sources used), and cache it.
4. **A source changes** β `source_changed(id)` marks only the units whose provenance includes that id; they rebuild lazily on the next read.
Unchanged content is skipped via a content-hash compare, so a no-op change costs nothing.
## The read path β a ladder of gates
This is the **unit read path** (`read_path="unit"` β exact v0.5 behavior; the keyless fallback, and the byte-identical escape hatch from the v0.7 pool default). The v0.6 [pool path](#whats-new-in-v06) β **the resolved default since v0.7** under a semantic embedder β replaces unit routing with global claim-pool packing and makes these unit-routing knobs inert; its own knobs are in [UPGRADE-0.5-to-0.6.md](UPGRADE-0.5-to-0.6.md).
Coalent keys on what a unit **knows** β an embedding of its *understanding*, not the query's words β so *"how many vacation days?"* hits your leave unit, while *"exchange policy"* does **not**. Every `get(query)` then walks a fixed ladder of gates. The defaults are **pure cosine** β no extra model, no heavy dependency β and each gate is a tunable knob. In firing order:
| # | Gate | Default | Fires when β what happens |
|---|---|---|---|
| 1 | **`hit_threshold`** β match | auto (OpenAI ~0.33) | best unit's blended score (`0.7Β·topic + 0.3Β·seed`) below it β **miss** β retrieve + synthesize a new unit |
| 2 | **`hit_margin`** β precision guard | `0.0` (off) | top unit beats runner-up by less than the margin β ambiguous β build the query's own unit instead |
| 3 | **freshness** | provenance / TTL | matched unit dirty or expired β re-materialize it |
| 4 | **coverage** β does it answer? | max per-claim cosine | how well the matched unit covers *this* query (one perfect claim = covered) |
| 5 | **`cross_unit_recall`** | **on (v0.4)** | coverage `< recall_threshold` β pool the best claims across **all** fresh units (MaxSim), can lift coverage. Free when dormant, no LLM call |
| 6 | **`coverage_scorer` (S2)** | `None` (off) | in the ambiguous band `[coverage_floor, coverage_ceiling)` β a cross-encoder / NLI / LLM entailment check overrides cosine |
| 7 | **`coverage_floor`** β the RAG floor | auto (~0.28) | coverage still below it β **escalate**: append fresh raw retrieval (no LLM call), so a thin hit falls back to retrieval rather than answering wrong |
| 8 | **`select_floor`** β serve | `None` (lexical trim) | serve the unit's atoms by *meaning* (per-claim cosine β₯ floor) instead of a keyword trim β the query-relevant facts, fewer tokens |
Plus one **build-time** knob β `residual_floor`: retain number-bearing source spans the extractor dropped (best per-claim cosine < floor) as extra atoms. Embedding-only.
Other hooks: `route_by_claim` (late-interaction routing over a fat unit's claims), `relevance_gate` (BYO reranker before synthesis), `depth` (synthesis completeness vs cost), `calibrate_thresholds()` / `suggest_thresholds()`.
**Which knob for which workload** β the defaults are tuned for structured, single-hop reuse; reach for these when your data differs:
| Reach for⦠| When |
|---|---|
| **`recall_threshold β 0.7`** | **multi-hop / cross-document** questions β makes recall bridge partially-covered reads (the full multi-hop win) |
| **`hit_margin > 0`** | **contradiction- / collision-heavy** corpora where near-ties are ambiguous (costs rebuilds β leave off on clean data) |
| **`select_floor`** | **paraphrase-heavy** queries over large units (a keyword trim misses when query and claim share no words) |
| **`residual_floor`** | **messy real prose** where the extractor might drop a number (cheap insurance) |
| **`coverage_scorer` (S2)** | **high-stakes ambiguity** where a wrong serve is costly (adds one judge call per borderline read) |
`stats()` reports `hit_rate`, `escalation_rate`, and the active thresholds, so you can see β and tune β exactly what the cache is doing.
## Bring your own stack
Coalent owns a tiny contract and passes everything else through to your tools.
**Retrievers** β a ladder from one-liner to full control:
| You have⦠| Use |
|---|---|
| Qdrant / Chroma / pgvector | a shipped adapter (bring-your-own-client) |
| another vector DB | extend `BaseVectorRetriever` |
| an existing search function | `FunctionRetriever` |
| several sources to fuse | `CompositeRetriever` |
| anything else | implement `Retriever` (one method) |
```python
from coalent import QdrantRetriever
retriever = QdrantRetriever(client=my_client, collection="docs", embed=my_embed)
```
**Synthesizers** β turn evidence into understanding:
- `LLMSynthesizer` β structured, citation-grounded understanding via your LLM (OpenAI, Anthropic, or any provider). You own the `instruction` and `fields`; Coalent owns the source / strict-JSON / citation envelope, so provenance is captured no matter what you ask for.
- `JSONPassthroughSynthesizer` β for already-structured tool/API JSON: caches it *as* the understanding, **no LLM call**.
**Embeddings** β how the cache matches queries by *meaning*. With `coalent[openai]` installed and `OPENAI_API_KEY` set, the cache uses OpenAI embeddings **automatically**; otherwise it warns and falls back to a lexical matcher. Override anytime:
```python
from coalent import SemanticCache, OpenAIEmbedder, FunctionEmbedder
cache = SemanticCache(retriever, synthesizer, embedder=OpenAIEmbedder("text-embedding-3-large"))
# or a local model: embedder=FunctionEmbedder(lambda t: my_model.encode(t).tolist())
```
> Use a real embedder for semantic matching β the no-key `HashingEmbedder` fallback matches on keyword overlap, not meaning, so similar-but-differently-worded queries can miss the cache.
**Stores** β durable and restart-safe (the invalidation graph rebuilds on startup):
```python
from coalent import SemanticCache, SQLiteCognitionStore # stdlib, no server
from coalent import RedisCognitionStore # shared across processes / hosts
cache = SemanticCache(retriever, synthesizer, store=SQLiteCognitionStore("coalent.db"))
```
**Any agent framework** β the read API is a single call, so it drops in anywhere. Shipped helpers for graph nodes and MCP tools:
```python
from coalent import make_cognition_node, build_mcp_tools
node = make_cognition_node(cache) # a graph node: state -> { context: fresh understanding }
tools = build_mcp_tools(cache) # expose the cache as an MCP tool
```
For the full standalone MCP server (freshness loop, seven tools, HTTP transport), see the
[next section](#use-it-from-claude-code--cursor-mcp); for LangChain, see
[langchain-coalent](#langchain).
## Benchmark
### Real-world: the v0.7 failure chain (n=605)
<!-- SANCTIONED 2026-09-15: every number in this subsection -->
The same frozen rig as every anchor below (609 real news articles, 605 held-out
questions, gpt-4.1-mini answerer, strict grading + locked adjudication rules), measuring
the full v0.7 composition against **our own strongest v0.6 configuration β not naive**:
| Arm | Accuracy | Context tokens | Final refusals |
|---|:---:|:---:|:---:|
| strongest v0.6 configuration (metadata header + behavioral stack) | 0.774 | ~983 | 61 |
| **v0.7 composition (subs + gap detector + constraints β the failure chain)** | **0.826** | **~983** | **19 (β69%)** |
- **+5.3 points at an identical serving budget** β zero extra serving tokens; the chain
adds cost only on reads that failed (gating repair on failure matches always-on
accuracy at **14% of the extraction calls**).
- **Where the 57 fixes came from:** 37 first-pass (decomposition + metadata serving),
10 `repair()`, 6 `serve_unserved()`, 4 `reprobe()` β every rung earned its place.
- Each rung fires **only on a failed read** β a refusal is never a correct answer, so
the downstream rungs have nothing to break. Repairs are **permanent**: the claims join
the store with per-claim provenance, and every future read of every user benefits.
### Real-world: the pool read path (v0.6, n=605)
Same rig as the v0.5 numbers below β 609 real news articles, 605 frozen held-out questions,
gpt-4.1-mini answerer, strict grading β with naive's own token-scaling curve as the fairness
control, now extended to its best measured point:
| Arm | Accuracy | Context tokens |
|---|:---:|:---:|
| naive top-9 | 0.711 | 1,311 |
| naive top-12 (best measured) | 0.731 | 1,729 |
| **Coalent `read_path="pool"` (defaults + metadata header)** | **0.731** | **981** |
- **The claim is parity at fewer tokens β not an accuracy beat.** CIs overlap on every pair.
0.731 @ 981 matches naive top-9 accuracy at **~25% fewer** context tokens and naive's best
measured point at **~43% fewer** (57% of its budget) β plus what retrieval alone cannot do
(freshness, provenance, behavioral compounding).
- **Serving ranks:** the gold claim sits at **p50/p75/p90 = 1/6/15** in pool order (over
claim-present queries), with the default cosine ranking β no reranker.
- **Headers are measured, not vibes:** opaque id 0.641 β shipping default 0.678 β your
`[title | source | date]` callable **0.731**. The 0.731 row above uses the metadata callable;
wire `pool_header` (see [quickstart](#quickstart)).
- **Behavioral stack** (opt-in): final accuracy **+3.1 pts** at **+2.1% tokens**, refusals
**β33%**, zero newly-wrong answers β a same-store, same-population comparison driven through
the full report_refusal/report_success loop.
- The v0.5 anchor on this rig was 0.699 @ ~1,036: the v0.6 rewrite holds the point (CIs
overlap) with the stale-serve hole fixed and attribution on by default.
### Real-world: MultiHopRAG (v0.5, pre-registered)
609 real news articles, third-party gold questions, answered by gpt-4.1-mini with exact-match
grading β the corpus **maximally friendly to chunk retrieval** (questions are generated from
article sentences), chosen as the adversarial test. We run the fairness control most benchmarks
skip: **naive's own token-scaling curve** on the same stream (k4 0.58 @ 590 tok Β· k6 0.64 @ 882 Β·
k9 0.71 @ 1311, n=605 held-out).
- **Pool serving (`serve="pool"`, warmed cache): 0.699 @ ~1,036 tokens** β beats naive k6
(paired McNemar z=3.22) and **statistically ties naive's k9 arm β its best measured at the
time β at 0.79Γ its tokens** (z=0.60). We do not claim to beat the curve here; the claim is
match-at-fewer-tokens plus what retrieval alone cannot do (freshness, provenance, compounding
reuse).
- **Null honesty** (n=100 unanswerable): pool **95%** refusal vs naive's 85β88%.
- **Build layer** (cold-start, on-the-fly): widened units read a median **23 chunks** of their
source vs 2 for keyhole builds; rebuild churn **460 β 31**; warm-pass accuracy flipped from
decaying (β0.03) to compounding (+0.04).
- Misattribution 2β6%; cross-unit recall fired on ~90% of reads (fully instrumented).
*Older benchmark history β the v0.4 structured-regime study and the benchmark-transparency
note (what we found, fixed, and how) β lives in the [docs](https://coalent.ai/docs/benchmark).*
## CLI
Installing Coalent gives you a `coalent` command β a `redis-cli` for your cognition cache (over a SQLite store):
```console
$ coalent ls
STATUS HITS AGE SRC ID QUERY
fresh 6 2m 2 cog:c95a9d2897e0af what is our leave policy?
dirty 1 12m 1 cog:7f1a0b9c3d2e4f remote work rules
$ coalent show cog:c95a9d2897e0af # understanding + provenance + raw evidence
$ coalent invalidate confluence:98231 # fire a change event
$ coalent stats
```
## Documentation
π **Full docs: [coalent.ai/docs](https://coalent.ai/docs)** β concepts, provenance & freshness, retrievers, synthesizers, persistence, worked examples (vector search, MCP & tools, agents), and the complete `get()` / data-model reference.
## Install options
```bash
pip install coalent # core, zero required deps
pip install "coalent[openai]" # OpenAI provider (also: anthropic)
pip install "coalent[qdrant]" # vector adapters (also: chroma, pgvector)
pip install "coalent[redis]" # distributed store
pip install "coalent[dev]" # tests + lint + types
```
## Contributing
Issues and PRs welcome. Run the gate before pushing:
```bash
pip install -e ".[dev]"
pytest && ruff check src && mypy src
```
One CI reality to know: the project pins mypy's analysis target to 3.10
(`python_version` in `pyproject.toml`), but if numpy >= 2.5 is installed (what the
`[fast]` extra resolves to on Python 3.12+), its stubs use syntax a 3.10 analysis
target cannot parse. In that case run `mypy src --python-version 3.12` β matching
your interpreter β exactly as the CI matrix does.
## Status & license
**Alpha** β the API may change before 1.0. Fully typed (`mypy --strict`), linted, and tested.
Licensed under [Apache-2.0](./LICENSE).
<p align="center"><sub>Context that's trustworthy, not just cheap.</sub></p>
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessUnresponsive