Almost a year ago we wrote about building a Knowledge Bank so that the tenth agent on a codebase could benefit from the nine before it. The headline then was a 57% reduction in context and the end of agents re-reading the same four config files. The architecture was deliberately plain: PostgreSQL with pgvector, one embedding per search dimension, and two MCP tools, one to query and one to report what you learned. It worked, and it kept working as the bank grew from a few hundred entries to several thousand. Then it started failing in a way that no amount of better embeddings could fix. The entry an agent needed was increasingly one hop away from the entry its query matched, and a ranked list of eight has no vocabulary for “one hop away.” This post is about the graph layer we added to fix that, the retrieval change we call folding, and an honest look at whether any of it is new.
Where We Were, and the Gap Since
The original design treated every entry as an island. An entry had content, a useful for sentence, the systems and tasks it applied to, and free-form metadata; each of those became its own embedding so an agent could search by purpose rather than by wording. Retrieval was a weighted cosine similarity across the dimensions, top-k, done. Agents stored what they learned through report_learning, with a guard that rejected session logs and transient state.
A lot changed between that post and this one, and most of it mattered for what follows:
- Scale. Our own engineering bank passed 7,900 entries this month. The platform runs 208 banks in production, one per repository or client, including per-client business banks that hold how a company actually operates rather than how its code works.
- Dedup that rewrites in place. A nightly pipeline proposes merges and splits, an LLM judge applies the ones that pass a rubric, and the surviving entry is rewritten. Roughly a third of our entries carry an updated time well after their creation for this reason.
- Playbooks and a lexical arm. Multi-step procedures became first-class objects returned whole, and an opt-in keyword arm sits beside the embeddings for rare tokens like error strings and flag names. We wrote about that arc in the agent mesh post and its follow-ups.
- Agents that cite. Entries started referring to other entries by their eight-character id, in prose, because the agents writing them had read the other entry and wanted the reader to go there.
That last point is where the problem announced itself. When we measured it in early October, 312 entries in the engineering bank cited another entry by id in their text, 374 distinct citations in all, and the metadata carried about 900 more typed links left behind by dedup: split from, replaces, recovered from, supersedes. The bank was already a sparse citation graph. Retrieval ignored it completely.
The Problem: Eight Slots and No Hop
A flat top-k over a graph-shaped corpus fails in two ways at once, and they compound.
First, redundancy spends slots. When a gotcha, the correction to that gotcha, and the entry that clarifies the correction all match a query, they each take one of the eight places in the answer. The agent reads three paragraphs about one topic and loses two topics it never sees. Mutually referencing entries are the ones most likely to co-occur in a result set, so the better the bank got at linking, the worse this got.
Second, the needed entry is often not a lexical or semantic match for the task. The recipe for logging in to production during an end-to-end test is a good match for “how do I log in.” The entry that says the session-create call needs HTTP/1.1 from curl because of a duplicated CSRF meta tag is not a match for anything an agent would type before it has hit the error. It is, however, one clarifies edge away from the recipe. We measured this directly in a lab we will describe below: when the base entry an agent needed was not in the flat top-8 for its query, agents reached it in only 27 to 32 percent of episodes, and only by guessing a second, rephrased query.
There was also a product ask behind the engineering one. The owner's original request, from a document review in September, was for “a graph on top of semantic embeddings in the knowledge bank” so that the flow through a business could be shown, holes in workflows nobody had answered yet could be found, and working agents could understand the context of what they were trying to achieve. That is a bigger ambition than retrieval, and it is still in progress. But every version of it needs the same substrate: typed edges between entries that someone trustworthy wrote, and a retrieval path that uses them.
What We Built
1. Edges as first-class, audited data
Every bank got an entry_edges table beside its entries: from, to, a type, a source, a free-text note, an evidence blob, who wrote it and when, with a uniqueness constraint on the (from, to, type) triple and a check that an entry cannot point at itself. Thirteen edge types cover what agents actually say about two pieces of knowledge: cites, see also, part of, supersedes, split from, derived from, corrects, clarifies, conflicts with, precedes, requires, produces, owned by. A separate append-only events table records every creation, deletion and bridge with an actor and a reason.
Three rules govern the table, and we copied them from the task-lineage graph we had already built for our kanban boards because they had already been argued over there:
- Binary trust. An edge exists or it does not. There is no confidence score, and embedding similarity never writes an edge. Similarity may propose a candidate for an agent or a judge to confirm. That is all.
- Every write is audited with its source: author, judge, citation backfill, metadata backfill, bridged, import.
- Deletion bridges. When dedup merges entry B away, every X→B→Y path becomes X→Y before B is removed, keeping a shared transitive type where one exists and falling back to see also, with evidence naming B and both hops. Knowledge of the connection outlives the node.
2. Agents write the edges as they go
Nobody curates this graph. The report_learning tool took a new optional connections field: a list of entry ids, each with a relation and an optional note. An agent that just read entry A, worked for an hour, and is now recording a learning that corrects A says so in the same call, and the edge lands with the entry. A link_kb_entries tool covers the case where an agent notices two existing entries belong together, and an audited unlink covers retracting or retyping one with a mandatory reason.
The note has one rule that turned out to matter more than anything else in the design: it says how the other entry pertains, never what it says. “Concrete working recipe for the loopback cookie-jar pattern described there.” “The session-create POST in that recipe needs --http1.1 from curl.” A note that restated the target's fact would let the reader skip the hop and quietly stop the fact from ever being verified against its source. We learned that by getting it wrong in the first version of our lab, which we will come back to.
The existing citations and dedup metadata were backfilled as edges with the citing sentence stored as the note. One week after launch the numbers looked like this:
| Measured 2026-10-20, one week after launch | Count |
|---|---|
| Production banks with the edge tables | 208 of 208 |
| Banks holding at least one edge | 25 |
| Edges in total | 2,076 |
Declared by agents through report_learning or link_kb_entries | 1,225 |
| Backfilled from citations and dedup metadata (engineering bank only) | 851 |
| New learnings on the busy banks that carry at least one connection | about 30% |
| Edges written by the judge, by bridging, or by import | 0 so far |
That last row is worth being honest about. The graph today is almost entirely agent-authored and backfilled. The judge that merges and splits entries does not yet write supersedes or split from edges of its own, and that is the next source to turn on.
3. Folding at retrieval time
This is the part we think earns a blog post. The query path changed from “fetch eight, return eight” to the following, which runs inside the bank service on every query_knowledge_bank call unless the caller turns it off:
- Fetch a pool of twenty candidates instead of eight, ranked as before, without bumping usage counters.
- Read the edges among the pool in one statement.
- Find connected components with union-find over those edges. Each component is a group of results that are about the same thing, by the agents' own declaration.
- Pick one representative per group. The top two ranked results are protected and always represent themselves. Otherwise the best-ranked member wins unless it is dominated: an entry that is superseded, corrected or clarified by another member of the group yields to that member; an entry that is part of, split from, or derived from another yields to its parent. The representative takes the group's best rank position.
- Fold the rest into pointers. Each folded entry becomes a
relateditem on its representative: id, relation, direction, the edge's note, a one-sentence summary of what it says, and aviahop when the path through the component is longer than one edge. - Refill the freed slots from the pool so the agent still gets eight results, now on more distinct topics.
- Attach neighbors. For each returned result, up to three entries that share an edge with it but were not in the pool at all, note-carrying edges first and corrects and conflicts with ahead of see also. These are marked
kind: neighbor. This is the hop, handed over. - Bump usage only for the ids actually returned.
The whole thing costs 40 to 100 milliseconds on top of a query that already takes about a second end to end through the hosted MCP tool. The result is the same eight slots, more topics, and for every result a short typed list of where to go next and why.
A Fold, Concretely
Here is a real query from an agent on our platform this week, lightly shortened. It was working out how to handle an owner ruling that said “table this card for N days,” and asked the bank whether the right tool was a dated dispatch label or a hold-until label. With folding on, the first result came back like this (summaries and notes are the real text, trimmed):
Read what happened to the slots. Without folding, those five entries about dated dispatch were five of the agent's eight results, and at least one of them was partly stale: the corrects edge says so in as many words. With folding, they occupy one slot. The representative is the entry that nothing in the group dominates, the four others are visible as typed pointers with a sentence each, and the corrects note tells the agent exactly which caveat not to believe. The four freed slots went to four other topics, including the second result above, which arrived with a neighbor of its own that the flat search had never fetched.
A second example shows the other failure mode. An agent preparing a post-deploy end-to-end test asked how to log in to production with Playwright. The top result was the login recipe, as it should be. Attached to it was a cites neighbor the flat search had not returned, whose note began “PROGRAMMATIC API FLOW (no browser): start and stop a session via the admin API.” That was the path the agent actually needed, and it was not a semantic match for “log in with Playwright” because it is the entry about not using Playwright. The sixth result, a verification recipe, carried a clarifies neighbor saying the session-create call needs HTTP/1.1 from curl. The agent could read that sentence before hitting the error instead of after.
What It Measured
We ran two kinds of measurement, because the obvious one is easy to fool.
The hop lab. We built synthetic tasks where the answer required a fact from a base entry B, and placed B so that it was not in the flat top-8 for the task's query but was one edge away from an entry A that was. Two current models ran the task through the bank's real tool schemas, 270 episodes in all. The first run of this lab was wrong, and the way it was wrong is the design lesson of this post: our edge notes restated B's fact, so the models answered correctly without ever fetching B, and the lab reported a triumph that was really a leak. We rewrote every note to say how B pertains and never what it says, added a check that the proof token appears in no note and nowhere in A, and re-ran.
| Hop lab, 270 episodes, leak-free notes | Agent fetched B | B's fact reached the answer |
|---|---|---|
| No edge (flat top-8 only) | 27 to 32% | same |
| Bare A→B edge shown as a pointer | 93 to 100% | same |
| Pointer with a note saying how B pertains | 100% | 100% |
| Tangential neighbor whose note said “historical context only” | 0% | n/a |
| Fact in the answer without fetching B (leak) | 0% | |
Two things in that table surprised us. The pointer's existence did almost all the work; what text we put on it (a summary, the useful for line, a content snippet, or nothing but an id and a relation) did not change the hop rate. And the note worked in both directions: it pushed the needed hop to certain, and it kept the models away from the tangential one every single time. A note is not decoration. It is the agent's reason to go or not go.
Production. We replayed real agent queries from the week after launch against the live bank with folding on and off, and had a blind judge score every returned entry for relevance to the query. On 40 queries from the most recent days: a pointer appeared on 80% of them, 30% of queries gained at least one judged-relevant entry they would not otherwise have had, the average gain was 0.55 relevant entries per query (0.95 on queries with connected candidates, 0.15 on sparse ones), and no relevant entry from the flat search was ever lost. Pointer precision was 24%, up from 20% on launch day, with the noise almost entirely note-less split from siblings from the backfill. Notes were on 53% of pointers, up from 24%.
Then we mined 400 archived agent transcripts to see what agents did with the pointers. Of 1,611 queries, 44% carried a pointer. Agents followed one with an explicit fetch only about 9% of the time. That looks like a disappointment until you notice that the pointer carries the target's one-sentence summary inline, and the transcripts cannot distinguish “read the summary and moved on” from “ignored.” Our best reading, from the judge study and the lab together, is that the graph adds relevant material to about a third of queries at roughly ten percent latency, and most of it is consumed as pointer text rather than as a hop. That is fine. The point was never to make agents fetch more. It was to make the one-hop-away fact reachable, and it is.
Is This New?
No, and yes, and it is worth being precise about which parts are which. Graph-structured retrieval for language models is a crowded field with a clear lineage, and most of our pieces have a name in it.
| Prior work | What it does | Where we differ |
|---|---|---|
| GraphRAG (Edge et al., Microsoft, 2024) | An LLM extracts an entity graph from the corpus, Leiden community detection groups it, and pre-written community summaries answer global “what are the themes” questions. | No extraction and no communities. Our nodes are the entries agents wrote and our edges are their declarations. We serve an agent working one task, not a reader asking about a whole corpus. |
| HippoRAG (Gutiérrez et al., NeurIPS 2024) | OpenIE triples form a schemaless graph; query concepts seed Personalized PageRank, which pulls in multi-hop context in one step. | The closest relative on the hop. We do not run PageRank. We fold the components of what the query already matched and show exactly one hop beyond it, with a reason. The agent decides whether to take it. |
| LightRAG (Guo et al., 2024; EMNLP Findings 2025) | Entity and relation extraction into a graph with dual-level (local and global) retrieval and incremental updates. | Shares the incremental property; the graph grows one learning at a time. Again, our edges are authored, not extracted. |
| A-Mem (Xu et al., NeurIPS 2025) | Zettelkasten-style agent memory: each note gets keywords and tags, the system links it to related notes on insert, and new notes can evolve old ones. | The nearest relative on authorship. A-Mem's memory system links notes to each other by similarity; ours are declared by the working agent with a typed relation and a note, and similarity is forbidden from writing an edge at all. |
| Zep / Graphiti (Rasmussen et al., 2025) | A bi-temporal knowledge graph for agent memory; contradicted facts are invalidated with a timestamp rather than deleted. | We share the stance that provenance must survive change (supersedes and corrects edges, bridging on delete, an audit table) but not the temporal model. Our dedup rewrites in place. |
| Mem0 / Mem0g (Chhikara et al., 2025) | Extracts facts from conversations into a memory store, with a graph variant of entities and labeled relations. | Conversation-derived entity graph versus engineer-written knowledge with typed citations between whole entries. |
| MMR (Carbonell and Goldstein, SIGIR 1998) and field collapsing (Elasticsearch) | Diversify a result list by penalizing similarity to what is already shown; or return one hit per value of a grouping field. | Folding is field collapsing where the “field” is a connected component of a typed graph, and the collapsed members come back as typed pointers instead of vanishing. |
So the honest one-line description is: an implementation of result collapsing over a sparse, agent-authored, typed citation graph, with the next hop handed to the agent rather than taken for it. Every word in that sentence has prior art. Three choices in the combination are ones we have not seen made together, and we would defend them as the contribution:
- The consumers of the memory are its only graph authors. Edges are a side effect of agents doing real work, written in the same call as the learning, typed, and with binary trust. No extraction pass, no similarity edges, no curator. The graph is sparse because nobody writes an edge they are not sure of, and that sparseness is what makes a one-hop neighbor worth showing.
- Notes that say how, never what. A pointer carries the relation and a reason to follow it, and a leak check in the evaluation keeps it that way. This is what moved the tangential hop to zero and the needed hop to certain.
- Collapse that keeps the folded entries visible. Diversification methods remove redundancy. Folding keeps it, as typed pointers with summaries, so the agent sees that three entries exist on the topic, which one corrects which, and whether to read them.
We would also note that the measurement is unusual for this kind of system. Most graph-retrieval papers report answer accuracy on multi-hop QA benchmarks. We care about a different number: whether a working agent, given the choice, reaches the fact it needs. The hop lab measures that directly, with the leak check that keeps it honest, and the production replay with a blind judge measures what the change did to real queries. Both are scripts in the repository, so every number here can be re-run.
Looking at It
A graph you cannot see is a graph you cannot argue about, so the Knowledge Bank Explorer grew a Graph tab. With no query it draws the whole bank as constellations. With a query it shows the answer exactly as the agent reads it, pointers and notes and all, and lights up the hits and their connections inside the whole picture. We prototyped it five ways on the real engineering bank, 883 connected entries and 674 agent-declared or citation edges once the note-less backfill siblings are hidden: Sigma.js on WebGL, Cytoscape with fCoSE, D3 force on canvas, vis-network, and a three.js force graph in 3D.
The 3D one won on sight. Depth separates clusters that overlap in two dimensions, and particles travelling along the lit edges make “the agent was told about this connection” vivid in a way a static highlight does not. It also showed us something the summary statistics had not: highlighting the hits is not enough on a bank this size, because the unlit nodes still fill the frame. The view in the screenshots above shrinks everything that is not part of the answer to a dot. That change came from the owner's first reaction to the five mock-ups, and it is now the spec.
A fair caveat: in the week after launch, agents used the graph on every query and no human opened the graph UI at all. That is the correct order of priorities for a memory system that exists to serve agents, but it also means the picture is for us. It is how we found that split from siblings were most of the pointer noise, and it is how we will watch the judge start writing edges.
What Is Next
- Note coverage is the limiting factor on precision. The backfill wrote citing sentences as notes; the remaining note-less edges are dedup siblings, and the judge that creates them is the obvious author.
- The judge as an edge source. Merges and splits should write supersedes and split from edges with the rubric verdict as the note, which also lets a representative yield to its corrected successor automatically.
- Process flows on the business banks. Precedes, produces, requires and owned by exist in the vocabulary and have two edges between them. The original ask, finding the holes in a workflow nobody has answered, is a walk over exactly those edges looking for a step with no successor and a decision with one branch.
- Structure the graph can compute. Community detection and centrality are cheap once the graph library is in the Explorer, and “which entries are hubs” is a question we already want answered.
The Knowledge Bank started as a way to stop agents re-reading files. It is turning into the place where the organization's reasoning lives, one declared connection at a time. The graph is sparse, honest, and written by the people who use it. We think that is the right order to build it in.
Building a memory your agents will actually use?
We run shared Knowledge Banks for engineering teams and for the businesses they serve: embeddings, typed connections, retrieval that folds, dedup with a judge, and the evaluation harnesses that keep all of it honest. The retrieval is the easy part. The discipline around it is the product.
Get in TouchSources
- Edge, D. et al. From Local to Global: A GraphRAG Approach to Query-Focused Summarization (2024).
- Gutiérrez, B. J. et al. HippoRAG: Neurobiologically Inspired Long-Term Memory for Large Language Models (NeurIPS 2024).
- Guo, Z. et al. LightRAG: Simple and Fast Retrieval-Augmented Generation (2024; Findings of EMNLP 2025).
- Xu, W. et al. A-Mem: Agentic Memory for LLM Agents (NeurIPS 2025).
- Rasmussen, P. et al. Zep: A Temporal Knowledge Graph Architecture for Agent Memory (2025).
- Chhikara, P. et al. Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory (2025).
- Carbonell, J. and Goldstein, J. The Use of MMR, Diversity-Based Reranking for Reordering Documents and Producing Summaries (SIGIR 1998).
- Peng, B. et al. Graph Retrieval-Augmented Generation: A Survey (2024).
- Elastic. Efficient pagination with collapse and cardinality in Elasticsearch.