Guides

Integrating with your failure handling

Coalent composes with the retries, fallbacks, evaluators, and tool escalation you already have — it never replaces them. Four recipes by existing failure structure, the two adoption modes, and the DON'Ts.

New in v0.7 Every production pipeline already has a failure structure — retries, fallback ladders, evaluator/critic nodes, tool escalation. Coalent never owns your failure policy — it composes with it. The cache contributes two things to the structure you already run:

  • Signals ($0, advisory, on every read): confidence, coverage, needs_retrieval, gaps, read_idinputs to your existing failure logic, never actions. Coalent never string-matches answers and never decides "failed" itself; your evaluator (a structured answer flag, a judge node, or these signals) makes the call.
  • Verbs (explicit, called by your app when its evaluator says failed): repair(read_id) → a second get()reprobe(read_id)serve_unserved(read_id), plus v0.6's report_refusal(read_id). The recommended ladder on a persistent refusal — but each verb is independent and safe alone.

Behind both sits the trust section: every non-disturbance property is a pinned test, not a promise — see the guarantees table.

The two adoption modes — the risk decision

Make this choice explicitly; it is the only real risk decision in adopting v0.7:

ModeWhat you passFirst-pass behaviorThe claim
Zero-risknothing newthe plain pool default — first-pass serving identical to the measured v0.6 pool configuration; the chain engages only on failurequalitative by construction: no new first-pass behavior to risk
Accuracy-maxyour planner's subs=first-pass ranking changes toothe measured 0.826 vs 0.774 composition (benchmark)

Both compositions ship; subs= is caller-owned, never guessed. Start in zero-risk mode — it costs nothing and cannot regress a correct read — and graduate to subs= when your planner already produces decompositions.

Recipe A — plain RAG chain, no failure handling yet

One if-statement. This becomes your first failure-handling rung ever — and unlike a retry, it permanently heals the store:

r = cache.get(question)
answer = my_llm(r.context["pool"])

if refused(answer):                      # your own refusal check
    cache.repair(r.read_id)              # re-extract what the build missed — permanent
    r = cache.get(question)              # repaired claims now compete
    answer = my_llm(r.context["pool"])

The same failure never costs you twice: the repaired claims serve first-pass on every future read, for every user.

Recipe B — agent with an evaluator/critic node

Don't add a new failure taxonomy — map the outcome classes your evaluator already emits:

Your evaluator's existing classWhat Coalent adds
refusal / incompletethe repair ladder: repair(read_id)get()reprobeserve_unserved
judged wrong, with evidence servednothing — retrieval cannot fix a judgment miss, and pretending otherwise wastes a repair. Route it to your answerer-side handling (prompt, model, adjudication)
missing source (gaps with kind == "corpus_hole")your fetch tool → ingest → retry. The cache tells you what's missing; it never fetches on its own authority
verdict = evaluator(answer, r)                    # YOUR node, YOUR classes
match verdict:
    case "refusal" | "incomplete":
        cache.repair(r.read_id)
        r = cache.get(question); answer = my_llm(r.context["pool"])
    case "missing_source":
        for gap in r.gaps:
            if gap["kind"] == "corpus_hole":
                ingest(my_fetch_tool(gap["probe"]))   # your tools, your index
        r = cache.get(question); answer = my_llm(r.context["pool"])
    case "wrong_with_evidence":
        pass                                      # not a retrieval problem — honestly

Coalent slots under classes you already have; nothing about your graph topology changes.

Recipe C — pipeline with an existing fallback ladder

You already run rerank → bigger-k → web search. Insert the chain before the expensive rungs — repair and reprobe cost embeds plus one small extraction, cheaper than a web search — and if the chain fails, your ladder continues unchanged:

answer = try_answer(r)
if failed(answer):
    # NEW, cheap rungs first: embeds + one small BYO extraction
    cache.repair(r.read_id)
    r2 = cache.get(question); answer = try_answer(r2)
    if failed(answer) and (r3 := cache.reprobe(r2.read_id)):
        answer = try_answer(r3)
    if failed(answer) and (r4 := cache.serve_unserved(r2.read_id)):
        answer = try_answer(r4)
if failed(answer):
    answer = your_existing_ladder(question)   # rerank → bigger-k → web search, unchanged

An additive rung, never a replacement — the chain's worst case hands your ladder exactly the read it would have received anyway, plus a permanently healthier store for next time.

Recipe D — batch / async QA

No serve-path latency budget? Collect flagged read_ids and run repair as a background pass — the store is healed for the next batch, at zero serve-path latency cost:

flagged: list[str] = []
for question in batch:
    r = cache.get(question)
    answer = my_llm(r.context["pool"])
    if failed(answer):
        flagged.append(r.read_id)
    emit(question, answer)

# later / overnight — the healing pass
for read_id in flagged:
    cache.repair(read_id)        # permanent: the NEXT batch reads the repaired store

Every recipe ends the same way: your failure structure stays in charge; Coalent adds rungs and signals to it.

The knob-adoption ladder

One step at a time; each step names what it costs and what it cannot break — the full ladder with guarantees:

  1. OFF — none of the chain runs; serving is the plain pool default (every new v0.7 knob byte-inert, pinned).
  2. gap_detector=True — the free gaps signal; changes nothing (serving byte-identical, pinned).
  3. Wire repair to your failure event — the pump. Failure-gated, it matches always-on accuracy at 14% of the extraction calls.
  4. reprobe / serve_unserved on persistent refusal — $0, structurally safe.
  5. subs= / constraints= from your planner — the opt-in serving changes; the risk decision above.

The DON'Ts

  • Don't string-match answers to detect refusal inside library glue. Define failure in your own contract — a structured answer flag, a judge node, or the read's signals. The library will never do it for you, by design.
  • Don't call repair on successful reads. It's measured waste: always-on repair spends most of its extraction on already-correct reads for zero gain — failure-gating matches its accuracy at 14% of the extraction calls.
  • Don't treat gaps as errors. They're advisory — the read's own doubt, typed so your evaluator can weigh them. A non-empty gaps on a correct answer needs no action.
  • Don't expect the library to fetch. A corpus_hole names what's missing; fetching is your tool's job, on your authority, into your index.

Next