6 Retrieval
The 2026 evidence says retrieval is where the accuracy points live: a 20-point spread across retrieval methods against 3 to 8 across write strategies. claimgraph’s answer has four layers, each visible below: hybrid search with rank fusion, graph traversal (fixed-depth and evidence-guided), sufficiency escalation across tiers, and honest abstention when the graph does not know.
(ns retrieval
(:require [babashka.fs :as fs]
[claimgraph.core :as core]
[claimgraph.evidence :as evidence]
[claimgraph.store.memory :as mem]))(def store (doto (mem/create) (core/seed!)))(defn brief [f]
{:subject (get-in f [:subject :name])
:predicate (:predicate f)
:object (or (some-> (:object-ref f) :name) (:object-lit f))})A small graph to retrieve from: a payments slice of a project.
(core/assert-fact store {:subject "billing" :predicate :core/depends-on
:object "stripe-client" :source-type :code
:confidence 0.95}){:status :created,
:fact
{:object-ref
{:id "e-62991c08-7dae-4b1b-acb0-7c0c696461a1",
:name "stripe-client",
:type nil,
:scope "project",
:aliases []},
:source-type :code,
:object-lit nil,
:scope "project",
:epistemic :observation,
:recorded-at #inst "2026-08-08T16:41:18.716-00:00",
:id "f-726edc4a-7718-4882-8281-43c08f5f976b",
:t-valid #inst "2026-08-08T16:41:18.716-00:00",
:object-kind :entity,
:t-invalid nil,
:episode nil,
:confidence 0.95,
:predicate :core/depends-on,
:subject
{:id "e-9c9df990-5527-44e5-8651-3f8a9aa1e5eb",
:name "billing",
:type nil,
:scope "project",
:aliases []},
:last-reinforced-at #inst "2026-08-08T16:41:18.716-00:00"}}(core/assert-fact store {:subject "stripe-client" :predicate :core/depends-on
:object "http-kit" :source-type :code
:confidence 0.95}){:status :created,
:fact
{:object-ref
{:id "e-7997cca1-56f4-40f0-9569-22dfb6ed6ec9",
:name "http-kit",
:type nil,
:scope "project",
:aliases []},
:source-type :code,
:object-lit nil,
:scope "project",
:epistemic :observation,
:recorded-at #inst "2026-08-08T16:41:18.717-00:00",
:id "f-2ae32659-ce75-44d4-ac76-a602838190b6",
:t-valid #inst "2026-08-08T16:41:18.717-00:00",
:object-kind :entity,
:t-invalid nil,
:episode nil,
:confidence 0.95,
:predicate :core/depends-on,
:subject
{:id "e-62991c08-7dae-4b1b-acb0-7c0c696461a1",
:name "stripe-client",
:type nil,
:scope "project",
:aliases []},
:last-reinforced-at #inst "2026-08-08T16:41:18.717-00:00"}}(core/assert-fact store {:subject "billing" :predicate :core/prefers
:object "idempotency keys on retries"
:object-kind :literal
:source-type :user-assertion}){:status :created,
:fact
{:object-ref nil,
:source-type :user-assertion,
:object-lit "idempotency keys on retries",
:scope "project",
:epistemic :preference,
:recorded-at #inst "2026-08-08T16:41:18.718-00:00",
:id "f-1267fa2f-1ec8-41a7-8d23-7b08672fa718",
:t-valid #inst "2026-08-08T16:41:18.718-00:00",
:object-kind :literal,
:t-invalid nil,
:episode nil,
:confidence 0.8,
:predicate :core/prefers,
:subject
{:id "e-9c9df990-5527-44e5-8651-3f8a9aa1e5eb",
:name "billing",
:type nil,
:scope "project",
:aliases []},
:last-reinforced-at #inst "2026-08-08T16:41:18.718-00:00"}}(core/assert-fact store {:subject "billing" :predicate :core/decided-against
:object "polling for webhook delivery"
:object-kind :literal
:epistemic :commitment
:source-type :decision-record}){:status :created,
:fact
{:object-ref nil,
:source-type :decision-record,
:object-lit "polling for webhook delivery",
:scope "project",
:epistemic :commitment,
:recorded-at #inst "2026-08-08T16:41:18.719-00:00",
:id "f-5b0077b0-b248-4f24-9119-0ef12f103aa8",
:t-valid #inst "2026-08-08T16:41:18.719-00:00",
:object-kind :literal,
:t-invalid nil,
:episode nil,
:confidence 0.8,
:predicate :core/decided-against,
:subject
{:id "e-9c9df990-5527-44e5-8651-3f8a9aa1e5eb",
:name "billing",
:type nil,
:scope "project",
:aliases []},
:last-reinforced-at #inst "2026-08-08T16:41:18.719-00:00"}}(core/assert-fact store {:subject "checkout" :predicate :core/depends-on
:object "billing" :source-type :code
:confidence 0.95}){:status :created,
:fact
{:object-ref
{:id "e-9c9df990-5527-44e5-8651-3f8a9aa1e5eb",
:name "billing",
:type nil,
:scope "project",
:aliases []},
:source-type :code,
:object-lit nil,
:scope "project",
:epistemic :observation,
:recorded-at #inst "2026-08-08T16:41:18.719-00:00",
:id "f-bcdd92ea-b89b-4c3e-a92e-558a44adb771",
:t-valid #inst "2026-08-08T16:41:18.719-00:00",
:object-kind :entity,
:t-invalid nil,
:episode nil,
:confidence 0.95,
:predicate :core/depends-on,
:subject
{:id "e-e2d9b1a4-87aa-4b0d-a53e-b5fb2ccda2ad",
:name "checkout",
:type nil,
:scope "project",
:aliases []},
:last-reinforced-at #inst "2026-08-08T16:41:18.719-00:00"}}6.1 Hybrid search
search fuses three routes with reciprocal rank fusion, each hit weighted by effective confidence: full-text over literals, names, and episode summaries; entity resolution per query token (so a query naming an entity pulls its facts even when no literal matches); and the one-hop neighborhood of resolved entities at low weight. Rank fusion by reciprocal rank is the classic recipe from Cormack, Clarke, and Buettcher (SIGIR 2009); the confidence weighting is claimgraph’s addition.
(->> (core/search store "billing retries" {})
:facts
(mapv brief))[{:subject "billing",
:predicate :core/prefers,
:object "idempotency keys on retries"}
{:subject "checkout", :predicate :core/depends-on, :object "billing"}
{:subject "billing",
:predicate :core/depends-on,
:object "stripe-client"}
{:subject "billing",
:predicate :core/decided-against,
:object "polling for webhook delivery"}
{:subject "stripe-client",
:predicate :core/depends-on,
:object "http-kit"}]A query that names an entity but matches no stored literal still lands, through the entity-resolution route:
(->> (core/search store "stripe-client" {})
:facts
(mapv brief))[{:subject "billing",
:predicate :core/depends-on,
:object "stripe-client"}
{:subject "stripe-client",
:predicate :core/depends-on,
:object "http-kit"}
{:subject "billing",
:predicate :core/decided-against,
:object "polling for webhook delivery"}
{:subject "billing",
:predicate :core/prefers,
:object "idempotency keys on retries"}]6.2 Traversal
Fixed-depth BFS expands the neighborhood in both directions, computing inverse edges at query time:
(let [n (core/get-neighborhood store {:entity "billing" :depth 2})]
{:entities (sort (map :name (:entities n)))
:edges (count (:facts n))}){:entities ("billing" "checkout" "http-kit" "stripe-client"), :edges 5}The guided walk replaces “everything to depth k” with “follow the edges that look like the query”: each round scores the frontier’s unseen facts by token overlap times effective confidence and takes a beam of the best. Asking about webhooks from checkout walks through billing to the standing decision, without dragging in the whole neighborhood:
(->> (core/guided-walk store {:entity "checkout"
:query "webhook delivery"
:budget 5})
:facts
(mapv (fn [f] (assoc (brief f) :walk-score (:walk-score f)))))[{:subject "billing",
:predicate :core/decided-against,
:object "polling for webhook delivery",
:walk-score 2.4000000000000004}
{:subject "checkout",
:predicate :core/depends-on,
:object "billing",
:walk-score 0.9499999995765883}
{:subject "stripe-client",
:predicate :core/depends-on,
:object "http-kit",
:walk-score 0.9499999994072236}
{:subject "billing",
:predicate :core/depends-on,
:object "stripe-client",
:walk-score 0.9499999993225413}
{:subject "billing",
:predicate :core/prefers,
:object "idempotency keys on retries",
:walk-score 0.7999999995721314}]6.3 Sufficiency escalation
recall answers from the cheapest tier that can support the query: graph facts, then episode summaries, then raw evidence. The sufficiency rule is deterministic and deliberately dumb: a tier suffices when it returns at least :min-hits results. The caller always learns which tier answered.
Facts answer when facts exist:
(:tier (core/recall store "idempotency retries" {})):factsWhen the graph has no matching fact but a closed episode’s summary mentions the topic, the episode tier answers. Close an episode with a summary the way consolidate does:
(def ep (core/open-episode store {:source-type :session-log
:ref "session-42"}))(core/close-episode store {:episode (:id ep)
:summary "Debugged the flaky payout cron; root cause was
a timezone mismatch between the scheduler and the bank API."}){:status :closed, :episode "ep-1ee03805-d09d-402d-9509-1f9fda461be3"}(let [r (core/recall store "payout cron timezone" {})]
{:tier (:tier r)
:episodes (mapv :summary (:episodes r))}){:tier :episodes,
:episodes
["Debugged the flaky payout cron; root cause was\na timezone mismatch between the scheduler and the bank API."]}And when even the summaries are silent, the raw-evidence tier greps the content-addressed artifacts that extraction kept. Store a transcript fragment as evidence, attach it to an episode, and ask about a detail no fact or summary carries:
(def evidence-dir (str (fs/create-temp-dir {:prefix "claimgraph-book"}) "/evidence"))(def transcript
"user: the payout cron failed again
agent: the bank sandbox rejects amounts over 10000 cents in test mode
user: right, cap the fixture amounts")(def ehash (evidence/write! evidence-dir transcript))(core/open-episode store {:source-type :session-log
:ref "session-43"
:evidence ehash}){:id "ep-6de8dbe7-b94c-452f-a589-f4ca94fbae64",
:source-type :session-log,
:ref "session-43",
:opened-at #inst "2026-08-08T16:41:18.732-00:00",
:evidence
"sha256-2878c9b556e20420ae02e6e53069a666335ceee1fc835d096be68a130c3d05b5"}(let [r (core/recall store "sandbox amount limit" {:evidence-dir evidence-dir})]
{:tier (:tier r)
:evidence (mapv :lines (:evidence r))}){:tier :evidence,
:evidence
[["agent: the bank sandbox rejects amounts over 10000 cents in test mode"
"user: right, cap the fixture amounts"]]}6.4 Abstention
The correct answer to a question the graph cannot support is nothing, not a near-miss. Retrieval returns empty rather than garbage, and recall says so explicitly:
(let [r (core/recall store "kafka partitioning strategy" {})]
{:tier (:tier r) :facts (count (:facts r)) :episodes (count (:episodes r))}){:tier :nothing, :facts 0, :episodes 0}The benchmark holds a whole question tier for this: refusal versus confabulation, at the retrieval layer and again at the agent layer.
bin/claim search "billing retries"
bin/claim neighbor --entity billing --depth 2
bin/claim neighbor --entity checkout --query "webhook delivery"
bin/claim recall "sandbox amount limit"source: book/chapters/retrieval.clj