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.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" {}))
:facts

When 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