Home / Articles

Four Kinds of Memory Your Coding Agent Needs (and Only One Is Built)

Code graphs give agents structure, yet engineers also need reasons, history and procedure. A look at four memory types, confidence tiers and how to join them.

Publisher: REACTAPP.TOP11 min read2627 words

This article is published in English.

Sources: Your Coding Agent Is Always the New Guy on Monday.

Lately a family of tools has shown up whose goal is to stop AI coding agents from grepping around repositories. They parse the code with tree-sitter, keep symbols and edges in SQLite and serve them over MCP, or they run language servers and give the agent genuine IDE semantics. The reported gains are large and the idea is sound.

All of them answer the same kind of question: who calls this, what type is that, what would a change break.

Now watch an experienced engineer approach a change in unfamiliar territory. Finding the callers takes about thirty seconds. Then comes an hour on questions that nothing in this category touches. Why was it built this way? What happened the last time someone tried this? What's the usual sequence here, do I regenerate before I run the tests?

Four questions, four different kinds of memory. Nearly every agent setup today builds one of them and labels it context.

The test that separates them

Before the taxonomy comes the test that generates it. For any relationship you want the agent to know, ask whether a formal artifact already records it that a deterministic tool can read.

If so, parse it. The artifact might be a compiler, a type checker, a bytecode reader or a distributed trace. Using a language model to extract relationships that a parser already knows is slower, more expensive and less accurate, and it throws away ground truth you already hold.

If not, the relationship exists only in prose and human judgment, and model extraction is the only way to get it.

This isn't a split between code and documents. It's about whether a grammar exists. The split is clean enough that different kinds of knowledge call for different extraction, different refresh cadence and different confidence semantics. Running them all through a single pipeline means at least one gets handled the wrong way.

1. Structural memory: impact of a change

This is the agent's knowledge of the code itself. It's the memory everybody builds, and the usual mistake is feeding it from just one source.

  • Tree-sitter gives breadth: an inventory of nodes, spans, over 40 languages, tolerance of errors and milliseconds per file. It doesn't resolve types. It can say line 3 contains a call to something named execute but not which execute. Name matching isn't compiler-grade analysis, and the gap shows exactly in polymorphic, layered, dependency-injected code.
  • LSP gives correctness through textDocument/definition, references and implementation, backed by a real type checker. It starts slowly, needs a working build and must stay off the query path, because a cold language server wrecks any latency budget. Run it as a background enricher.
  • Bytecode exposes code that only exists once the build has run: Spring Data derived queries, CGLIB proxies, MapStruct mappers, anything an annotation processor wrote. Source shows none of it. target/classes shows all of it.
  • AOT output or /actuator/beans shows how dependency injection is wired, in other words which concrete class fills which interface under which profile. Reading the source can't tell you, because the answer isn't there.
  • Distributed traces supply edges across services and async boundaries: controller to HTTP to another service, producer to topic to consumer. Every language server stops at a module boundary, and distributed systems consist of little else. If you run OpenTelemetry, you're already collecting a service-to-service call graph and discarding it.

The design decision that carries the most weight is that every edge gets a tier:

  • resolved: a type checker confirmed it, so it's safe to act on.
  • observed: a real run produced it, which is ground truth for the paths covered.
  • inferred: matched on a name alone. Likely correct but unchecked.
  • ambiguous: several candidates exist. Return them all and choose none.
  • dynamic: computed at runtime and undecidable, so record it as undecidable and don't drop it.

Give an agent a graph with no tiers and it treats every edge as fact, then refactors on a guess. Tell it an edge is inferred and it verifies before acting. That one column is the gap between a system that misleads quietly and one that gets worse gracefully.

One consequence that few implementations state outright: in a layered Spring or Django codebase, impact analysis yields a lower bound, never a complete closure. Saying "17 callers, 3 unresolved interface dispatches, 1 generated method" is safe. Saying just "17 callers" is misleading.

Churn is hourly and a rebuild takes seconds.

2. Knowledge memory: the reasons behind the code

This memory holds the reasoning, and it sits in documents no parser can read: decision records, incident postmortems, pull request threads, tickets, RFCs, design docs and commit messages with real content.

Consider a retry count of 3 in a config file. Its explanation is split between a 2023 ADR, a postmortem on duplicate settlement instructions and a thread in which someone described a counterparty's SLA. The link between them, a policy that exists because of an incident and restricts an integration, is stated across three documents, each phrased differently.

GraphRAG is a good tool here, and it helps to know what it does. There are six steps:

  1. Chunking splits documents into text units, deterministically.
  2. Extraction is LLM work: a model reads each chunk and outputs entities and relationships.
  3. Description summarisation is also LLM work, merging the same entity described fifty ways into one description.
  4. Leiden clustering needs no model. It splits the graph into nested communities by modularity, groups whose internal edges greatly outnumber the ones leaving.
  5. Community reports are LLM summaries per community, written bottom-up with higher levels built from the reports below.
  6. Embedding produces vectors that serve as entry points for queries.

Two terms deserve a closer look. An edge in this setting isn't a typed pointer. It holds a prose description from the model along with a weight and source references, so each edge records a model's reading of a sentence. A community is likewise more than an arbitrary subgraph, since a random set of nodes may share no edges at all and leave isolated points with nothing to summarise. Leiden selects clusters whose induced edges are dense, which is what allows a coherent report.

The benefit is global search. Instead of fetching the chunks closest to the query, the system poses the question to every community report on its own and combines the partial answers. Properties of the whole corpus become retrievable: a question about the recurring failure modes in the settlement path has an answer that lives in no single postmortem. Ordinary vector RAG can't deliver that, however finely the chunks are tuned.

Churn is monthly, and rebuilds are expensive: three of the six steps are LLM calls, so indexing cost scales with corpus size, number of gleaning passes and model price. That's acceptable since the corpus barely moves.

3. Episodic memory: what happened on earlier attempts

This one records the agent's own history in this particular repository. Touching SettlementService last time broke three allocation tests. Changing that interface without regenerating the mappers breaks the build. The two most recent PRs that skipped the validation layer were turned down.

It also has a property that neither semantic index can claim: its ground truth is deterministic. A postmortem contains a human assertion that a model pulled out. "The agent changed X and CI went red" is a judged outcome, decided by the build system. For the questions it covers, that makes episodic memory the most trustworthy source in the setup.

Three design points are easy to get wrong:

  • Skip GraphRAG here. Episodes follow a timeline and carry outcomes. Grouping by edge density is the wrong structure, and what's called for is weighting by recency, frequency and success or failure.
  • Eviction is required. The semantic indexes can go without it, this one can't. A lesson from a refactor two architectures back isn't just out of date, it points the agent the wrong way.
  • Mistakes accumulate. Suppose the agent reasoned wrongly and recorded the result. It turns into remembered fact and every later session inherits it. Keep the outcome (CI failed, PR rejected) with high confidence and the agent's own conclusion with far lower confidence, then let the conclusion lapse while the outcome remains.

Churn is per session.

4. Procedural memory: not an index at all

Procedural memory covers how things get done: the build command, the review checklist, the migration order, the house style. People's first impulse is to make it searchable, and that impulse should be resisted.

Anything always relevant is badly served by retrieval. It belongs in the system prompt or an AGENTS.md, loaded each session with no lookup. Placing it behind retrieval creates a failure where the agent doesn't know it should look, and hoping the agent retrieves something isn't a mechanism.

Anything that matters only in certain situations should be a tool with a trigger. Running the integration suite, say, ought to be something the agent can call and not a document it must think to look up.

The case worth watching is the learned workflow. Suppose the agent finds that in this repo mappers must be regenerated before the tests run, and that order keeps working. That's a procedure derived from episodes. Save it as a candidate rule with a success count, promote it to the always-loaded instructions after it proves itself, and keep the originating episodes so the promotion can be audited.

So procedural memory isn't a fourth index. It's what's left over after the third, taken out of retrieval and written into instructions.

The join: one symbol namespace

The three retrieved memories meet at the identity of a symbol, and nobody has solved this part. Structural nodes use fully qualified names. Episodic entries point at the symbol the agent actually handled, so they join precisely. Entities in the knowledge index are strings that a model picked out of prose, such as "SettlementService", "the settlement service", "the settlement reconciliation path" or "the counterparty SLA constraint", and mapping them to real symbols is a fuzzy matching job.

Pretending otherwise is exactly the failure the tiers exist to prevent, so the join gets its own ladder:

  • exact: the name occurs verbatim and points to exactly one symbol.
  • strong: a normalised phrase points clearly to one class.
  • weak: a concept covering several classes whose boundary is vague. A set of symbols comes back and the agent checks.
  • unresolved: a real constraint with no matching symbol anywhere. "The counterparty SLA" is a true architectural constraint that code doesn't represent.

For that last tier, keep it, return it and don't force a match. A readable unresolved constraint beats a wrong symbol binding, and forcing matches is how an agent confidently edits the wrong class.

What the agent actually asks

A query surface might look like this:

what_breaks(symbol)        → structural: resolved callers, tiered
why_is_this(symbol)        → knowledge: decisions, incidents, constraints
what_happened_before(sym)  → episodic: past attempts and their outcomes
related_by_intent(concept) → resolve a phrase to a symbol set, tiered

Then comes a question none of the three can answer by itself: "I'm about to change the retry policy on settlement. What breaks, why is it three and has anyone tried this before?"

Knowledge maps "retry policy" to SettlementService.retry at tier strong and returns the ADR and postmortem. Structural takes that symbol and returns its callers, including two services visible only in traces. Episodic adds that a comparable change last quarter broke the allocation tests.

That's one answer built from three kinds of evidence, each produced by the mechanism able to produce it and each labelled with how far to trust it.

What it costs

  • Most document collections are too thin. Nine ADRs and commits saying "fix bug" give the knowledge index nothing to work with. Look at what you have before building.
  • Entity resolution never gets perfect. The ladder helps you live with it and doesn't cure it. weak will probably be the most crowded tier, so plan for checking and not for trust.
  • Start narrow. Begin with tree-sitter plus LSP. Add bytecode when generated code begins producing wrong answers, and traces when questions cross services. Add the knowledge index only when the corpus is worth indexing.
  • Nobody has built all of it, which is either an opening or a warning. The pieces sit in different communities: code-intelligence people don't read the GraphRAG papers, GraphRAG people don't think about type checkers and memory-architecture people mostly work on chat assistants instead of codebases.

Think about onboarding a person

Say a senior engineer joins on Monday and gets repository access, nothing else. They can read every line, trace each call and run the tests. Come Friday they're still slower than a colleague hired a year earlier, and it isn't down to ability.

The colleague from last year has three things that were never inside the repository, and the newcomer doesn't.

One is the reasons behind odd choices. The retry is three because of an incident in March, and the reconciliation workaround exists because a counterparty's API misbehaves on Fridays. No comment says so. It's in an ADR, a postmortem and a conversation.

Another is the history. Somebody tried simplifying that interface in Q2 and the allocation tests broke for a week, so people leave it alone.

The third is the local way of working. Regenerate mappers before running tests. PRs that skip validation get rejected. Nobody consults a document for that, it's just how the team works.

An agent with only a code graph is that Monday hire: complete recall of the repository, no sense of why anything is as it is, no memory of past attempts and no feel for local custom. Each session starts a new Monday.

That's the gap, and no tool is to blame. A code graph answers its own question well, and today's implementations are solid. What's missing is that three of the four things a capable engineer depends on aren't in the stack at all, so the fourth is left carrying everything.

The one-line version

The short version: the compiler owns structure, since structure is deterministic. A model owns intent, since intent is unstructured prose. The build system rules on experience. Procedure is distilled from experience and loaded up front, never retrieved.

No tool in the category gets any single memory wrong. Each builds one and stops, and nothing ties them together. A better graph isn't the answer. The answer is four memories, each candid about how reliable it is, mapped onto one shared namespace of symbols.

GraphRAG terms and pipeline stages follow Microsoft Research's implementation. Costs swing widely with corpus size, gleaning settings and model choice, so benchmark on your own data before committing.

A fifteen-minute check

Choose an interface in your own codebase that has five or more known implementors, like a PaymentProcessor or NotificationChannel, one wired through @Autowired and not built directly. Ask your code graph who calls it and count the results.

Published benchmarks for this category measure tokens, tool calls and latency. None, as far as is known, measures whether the returned call graph is correct. On a codebase with declared interfaces and known implementors, that's a five-minute check against ground truth you already have.