Coalent

Real-time, provenance-invalidated context for AI agents & RAG.
Build understanding once. Reuse it everywhere. Keep it fresh β€” automatically.

pypi python license typed tests discord

πŸ“– Documentation  Β·  coalent.ai  Β·  πŸ’¬ Discord

Quickstart Β· What's new in v0.7 Β· Gate ladder Β· Bring your own stack Β· MCP Β· LangChain Β· Benchmark Β· CLI

--- > **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) `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= 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 ` β€” 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). 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) 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).

Context that's trustworthy, not just cheap.