Graph engineering for agent memory
Half of graph engineering has real prior art and works. The other half only exists once the graph is something you write to instead of something you index, and it is the half that decides whether an agent gets helped or quietly misled.

Two different jobs, one word
Graph engineering currently names two disciplines that share a shape and nothing else.
One is about how work runs. Nodes are units of work, edges are data dependencies, and the payoff is parallelism: find the dependencies that are not real, run the independent work at once, and the longest surviving chain bounds how fast the whole thing can finish. This is fifty-year-old machinery. Critical-path scheduling dates to 1957, make parallelised builds in the seventies, and every data pipeline since has been a directed acyclic graph. If that is your problem, the existing literature is excellent and this post is not about it.
The other is about what the agent knows. Nodes are facts, edges are relationships, and the payoff is retrieving the thing you would never have thought to search for.
That second one splits again, and this is the split that matters. Retrieving over a graph you built from a corpus has real research behind it and largely works. Maintaining a graph that accumulates from work does not, because a corpus is indexed once and a record keeps changing underneath you.
Part one: the retrieval graph
Start with the case that justifies the structure, because it is concrete and it is not about graph theory.
You ask an agent to change how checkout retries failed payments. A semantic search over your project notes finds the checkout decision, because your question and that note share obvious words. It does not find the note about the payment provider's idempotency keys, written eight months ago by somebody who never used the word checkout. That note is the one that matters: retry without idempotency keys and you double-charge people.
A graph finds it, because the checkout decision has an edge to it. You retrieved it by relationship rather than by similarity, and that is the whole argument for the extra structure.
The fact you most need is often the one that shares no words with your question.
The mechanics are not exotic and we would not pretend otherwise. Search produces seeds: about ten starting points, from keyword and vector matching run as separate channels, because they fail in different places. Then a walk goes outward from those seeds along typed edges, one hop, collecting what it reaches. Everything reached is scored, ranked, and capped.
One hop sounds timid. It is not: at depth two on a well-connected graph you reach most of it and the results turn to soup. Spend extra reach on better seeds rather than more depth.
The scoring, and the two weights that are scar tissue
by category by relation followed distance
decision 3 fulfills 3 −0.5 per hop
architecture 3 blocks 3
goal 2 supersedes 3 container nodes
risk 2 caused-by 2 epic / objective
lesson 2 contradicts 2 /milestone: −3
gap 1 relates-to 1
fyi 1 mentions 1Container nodes score negative three. Anything that groups other things carries almost no information about the work in front of you, but children point at parents through strong edges, so containers arrive with high scores and flood the results with entries like Q3 Platform Work. A strong negative base cancels even a top-tier edge.
Structural nodes score zero. Ask about authentication and you match the component literally named Auth, which then takes a slot to tell you that a subsystem exists.
Both fixes are the same move, and it is the most repeated decision in this system: downrank, never exclude. An excluded node cannot come back on the day it genuinely is the answer. A downranked one can, and costs a slot only when nothing beats it.
Cap what you return. Ours is twenty. Injected context competes with the user's actual question for the model's attention, so forty entries is not twice as useful as twenty, it is twenty useful entries diluted by twenty distractions.
The inversion that eats your best results
There is a trap in this design that is easy to miss and expensive to leave in, and it is worth spelling out because it follows directly from the scoring above.
A seed arrives with no relation and zero distance, so its score is category weight plus recency and nothing else. A walked node arrives with an edge bonus. The cheapest possible edge adds one point and the distance penalty takes back half, so a neighbour outranks an identical seed by half a point, and by two and a half points through a strong edge.
Read that as a claim and it is obviously wrong. The ranking is asserting that being adjacent to a match is better evidence of relevance than being the match. A seed carries direct query evidence: something in it actually matched what the user asked. A walked node was never compared against the query at all.
Adjacency to a match is not evidence. It is a hypothesis, and it should not outrank the evidence that produced it.
Sort one merged pool of seeds and walked nodes, slice it at the cap, and query-blind neighbours systematically evict the results that actually matched. The graph feature you added to improve retrieval quietly starts costing you the thing it was meant to improve.
The fix is the cap, not the walk. Tier it: seeds fill the cap first on their own ranking, and walked nodes compete for whatever is left. The walk keeps its real job, which is reaching the fact that shares no keywords with your question, and loses the ability to displace the evidence.
One more pin is needed once you have supersession, and it is the kind of interaction you only find by hitting it. If a retired fact surfaces and its replacement is a walked node, the replacement gets demoted into the lower tier and trimmed at the cap, and you have shown an agent a dead decision with nothing next to it. So the replacement half of a superseded pair is pinned alongside the seeds, never in the walked tier.
Part two: the record that ages
Everything above assumes the graph exists and the job is reading it well. The second half begins when the graph is something your agents write to, and almost none of it has prior art, because indexing a corpus never raised these questions.
Facts that disagree
In March somebody records that sessions live in a signed cookie. In August it is reversed, because revocation turned out to matter more than the saved round trip. Both facts are in your graph, both match the query, both look equally authoritative. A retriever hands over whichever is the better semantic match, and has no way to prefer the true one.
Deleting the old one answers what is true now and destroys the answer to why did we change our minds, which is what stops somebody re-proposing signed cookies next spring.
So retirement becomes its own operation, requiring a reason and verbatim evidence that the old state actually ended rather than merely being contradicted. We also added a read replica supersedes nothing.
Our first version then archived the retired fact and hid it, which is correct on paper and has one fatal property: a mistaken retirement is invisible and unrecoverable. The next agent does not get a wrong answer, it gets no answer.
That mattered because agents get this wrong constantly. On conversational source material we measured false retirements eleven times out of eleven, and rewriting the guidance moved the rate by exactly zero. The fix was not better instructions. It was making the mistake survivable.
retired knowledge × 0.35 needs a far stronger match than a live fact
done work, fresh × 0.60 finished this week: this is the changelog
done work, decayed → ~0.2 finished months ago: this is exhaust
cancelled work × 0.15 things we deliberately decided not to doCancelled work takes the harshest discount for a specific reason: it is imperative text describing something the team decided not to do. An agent that retrieves Add SSO for enterprise accounts without the cancellation attached will go and implement it.
Facts that expire without being replaced
Supersession handles replacement. Most stale facts are not replaced by anything, they just quietly stop being true.
Nobody audits a knowledge graph quarterly. That plan always fails. What works is that everybody can answer when does this stop being true at the moment they write it down, because that is the only moment they know. So the choice is mandatory at write time: bind it to the work that resolves it, give it an expiry date, or deliberately neither for something durable.
Facts that cannot be found by searching
Vector search fails exactly where vocabulary diverges. Somebody wrote a constraint using the word throttle; the person about to break it is searching for middleware ordering. A better embedding model does not fix that.
What fixes it is letting a fact declare the files and symbols it is about, so the trigger stops being a keyword and becomes an edit.
lesson: "Rate limiter must stay in front of the auth check"
anchor_files: [src/middleware/index.ts, src/middleware/rate_limit.ts]
anchor_symbols: [buildMiddlewareChain, RATE_LIMIT_WINDOW]
body: |
Moving the limiter behind auth in Nov 2025 let unauthenticated
traffic reach the token-metered path. 4h outage.Now touching buildMiddlewareChain surfaces the lesson whatever words the person had in mind. One honest limit: this fires on edits, not on reads, so an agent that only ever reads a file never triggers it and semantic recall carries that case alone.
Two agents, one piece of work
Shared memory lets two agents read the same facts. It does nothing to stop them doing the same work, because a note cannot be held. What stops it is that starting work means claiming it atomically, and the loser is told who holds it and since when rather than just being refused. An agent told only no retries; an agent told another session has been on this for forty minutes goes and does something else.
Knowing which facts are still true
The last one decides whether any of the others matter in a year. A fact needs two independent clocks. Freshness is when the content last changed: cheap, automatic, nearly worthless alone. Verification is when somebody last confirmed the claim still holds against reality.
The rule that makes verification mean anything is that an ordinary edit must never set it. If fixing a typo counts as confirming a fact, the signal is gone. Keep them apart and you can answer a question a folder of notes cannot: which of our facts has nobody confirmed in ninety days?
Then something has to act on the answer.
Ours runs when an agent runs it. A fully autonomous version that tends the graph with nobody prompting it is something we are building, not something that ships today.
What this adds up to
Part one is a weekend if you have built retrieval before, plus a few rounds of being wrong about weights. Part two is a product.
It is also not optional, which is the uncomfortable bit. Skip supersession and your agent confidently cites a decision you reversed. Skip lifecycle and the graph fills with facts nobody can vouch for. Skip anchors and the constraint stays invisible to the person breaking it. Skip verification and you cannot tell a graph that works from one that quietly stopped being true. Every one is load-bearing, and you find that out in production.
If your corpus does not change, none of part two applies. Retrieval over a static reference set is a retrieval problem, and everything after the walk solves something you do not have. Build part one and stop.
Or connect one that already does it
This is what we build. Stele ships as an MCP server, so if your agent speaks MCP, which is now most of them, connecting it is the same as connecting anything else.
A stdio server any MCP client can spawn, on every plan. Point a Python agent at it, or a framework that did not exist when this was written, and it gets the same sixteen tools: recall and search for reading, knowledge and task for writing, node for edges and edits, plus the lifecycle operations. For the ten harnesses Stele installs into directly, stele install also wires the hooks that inject relevant context automatically, so retrieval happens on every turn without the agent choosing to ask.
What stays yours: the live conversation buffer, which belongs to your framework and should. The loop, the retries, the orchestration, which is the other kind of graph engineering entirely. And anything multimodal, since we ingest text rather than images or audio.
The retrieval graph was never the hard part. It is the part with tutorials, and the reason they feel like they end early is that they do.