DEV Community

Vitalii Cherepanov
Vitalii Cherepanov

Posted on

Seven principles of real memory for AI agents

Part 2 of 3 — "Memory for AI agents"
Architecture. Concrete. With formulas and lifecycle.


Article

In the previous post I broke the "RAG = memory" pitch into three uncomfortable problems: a chunk doesn't know it's a chunk; retrieval has no structure, only cosine; time doesn't exist as a first-class concept. In short — RAG is search wearing the marketing word "memory."

Today — what should be there instead.

A disclaimer up front. I don't claim to have invented any single item on this list. Atomic facts go back to Wittgenstein. Temporal validity is basic logic. Knowledge graphs are a whole field with textbooks. Lifecycle for data is standard in any normal information system.

I claim something different. I claim that all seven properties have to work in one system at the same time, and that any system in which only five of seven actually work continues to lie to the user with a confident face. There's only one way to see this — try assembling all seven into one codebase and watch what happens.

I tried. It worked. Called it braincore. Open source, Apache-2.0, single Go binary, MCP-stdio. I won't turn the article into a pitch — but in each section below I'll add one line about how it's done in braincore, so it's clear we're not talking theory.

Let's go.


Principle 1. Atomic Knowledge Units with lifecycle, not "chunks in Qdrant"

The pain. In RAG, any incoming text — dialogue, design doc, git commit, meeting transcript — gets sliced into chunks and shipped into the vector DB without questions. From there, no matter what happens — all chunks are equivalent, all equally "fresh," all equally "true." Six months later, one collection holds a soup of stale, current, hypothetical, and refuted facts. And every one of them has exactly one chance of making it into retrieval — by cosine.

What should be in the schema. Any incoming information does not flow into memory directly. It runs through a pipeline:

input
  → initial trust (by source: user=0.9, llm=0.3, web=0.4..0.7)
  → parse (entity / fact / relation / event / rule / hypothesis)
  → atomic knowledge units
  → validate (source / graph / dedup / contradiction / temporal / rule)
  → link (at least 1 edge into graph OR review item)
  → working memory (TTL + activation)
  → iterative verification loop
  → consolidation
  → long-term memory (only confirmed + linked)
  → edge strengthening (usage + success + co-occurrence − decay)
Enter fullscreen mode Exit fullscreen mode

The core rule: nothing enters long-term memory immediately. Every atomic knowledge unit has at minimum:

  • truth_status: hypothesis | candidate | confirmed | contradicted | deprecated
  • lifecycle: staging | working | consolidated | archived
  • source_ref — where it came from
  • confidence — numerical certainty estimate
  • valid_from / valid_until — when it's true

Compare that to a RAG chunk that has only text and embedding. It's the difference between a junk drawer and a warehouse with inventory.

What this enables. When yesterday you said "we use Postgres" and today "we migrated to ClickHouse, Postgres is OLTP only" — the old fact automatically gets valid_until = today and superseded_by = new_fact_id. On retrieve, it either doesn't appear at all, or it comes flagged "historical, not current." Not because of a smart model. Because of the schema.

How braincore does it. The pipeline staging → working → consolidated is implemented literally — three separate SQLite tables plus an intermediate verification loop. A record reaches consolidated only if truth_status = confirmed, has at least one graph edge, no unresolved contradictions, and confidence ≥ threshold. Otherwise it stays in working with a TTL, or moves to a review queue.


Principle 2. Strict Mode and the right to abstain

This is, possibly, the most important point in the entire series. And the most absent from commercial memory frameworks.

The pain. The standard metric AI systems are measured by — "how often they give the right answer." This is a rotten metric. 95% correct answers and 5% confident hallucinations is a system you cannot trust in production. Because you don't know in advance which 5% you're in right now.

The right metric reads:

0% confidently-wrong actions at an acceptable abstain rate.

Not "always answer." But "never take a wrong action without verification." And if verification is missing — say "I don't know" and assign yourself a task to fix it.

What should be in the schema. Before a fact lands in prompt context, it passes through a gate:

  • is there a source_ref?
  • confidence ≥ threshold?
  • trust_score ≥ threshold (for the source)?
  • temporal_valid == true (valid at query time)?
  • no unresolved contradiction in the graph?
  • no unresolved ambiguity?

If even one requirement fails — the fact does not reach context. If no fact made it through for a query — the system returns abstain = true with reason = no_accepted_facts (or contradiction_unresolved, or temporal_invalid — always explicit).

And — pay attention, here's where the magic happens — abstain is not delivered to the user as a dead end. It becomes a brain task in the backlog: "I need evidence for X to answer with confidence. The source is here, the specific conflict is here." The system knows what it doesn't know, and assigns itself the task to fix it.

What this enables. An AI agent you can trust. Not because it's always right — but because when it's not sure, it stays silent or asks for clarification. And when it does take action — the action is grounded in facts that passed the gate, not "well, ChatGPT thought this was better."

Show me one RAG stack that does this. I'll wait.

How braincore does it. The internal/strictmode package is a separate module with explicit gate rules. By default, every query passes through strict mode; for UX scenarios where abstain is unacceptable (brainstorming, for example), you can drop it via an explicit --allow-uncertainty flag. All abstain events are logged as brain tasks with their source and reason.


Principle 3. Causal Decision Chains, not flat facts

The pain. In RAG, any decision is stored as "text about a decision." On retrieve, you get a chunk of text that describes the decision — but doesn't answer "why?", "what alternatives did we consider?", "what came of it?"

Six months later, you ask "why did we pick JWT over sessions?" — RAG returns three fragments of the declaration, and the model fills in the reasoning itself. Sometimes correctly. Sometimes inventing it from popular patterns in its training data. You don't know which one this time.

What should be in the schema. The entity is not a "document" and not a "memory entry." The entity is called decision and has a schema:

problem      → what we were solving
alternatives → what we considered and rejected (with reasons)
decision     → what we chose
reasoning    → why this specifically
outcome      → what came of it (filled in later, post-hoc)
superseded_by → link to a new decision if this one was revised
Enter fullscreen mode Exit fullscreen mode

This isn't "let's stuff text into an embedding." This is a causal chain that answers WHY, not just WHAT.

What this enables. Six months later, you ask "why JWT?" — the system returns a structured answer:

  • Problem: session scaling + audit requirements.
  • Alternatives (rejected): stateful sessions with Redis (violates audit), opaque tokens with centralized lookup (latency).
  • Decision: JWT with short TTL.
  • Reasoning: stateless, audit-neutral, latency acceptable.
  • Outcome (recorded 4 months later): invalidation complexity higher than expected; added refresh tokens.
  • Superseded by: none.

RAG returns three fragments. A decision chain returns reasoning. These are different products.

How braincore does it. Decisions are a separate entity type in the graph with required problem, alternatives[], decision, reasoning fields, and optional outcome/superseded_by. They're stored not as chunks but as structured records with explicit edges into the code graph and into other decisions.


Principle 4. Stable code identity through AST, not strings

The pain. This one is specific to AI agents working with code — but it hits all of them. You renamed GetUser → FetchUser, moved it from pkg/auth to pkg/user, changed the signature from a pointer receiver to a value receiver. All the references in RAG memory pointing to "GetUser in pkg/auth" are now dead. Because RAG is bound to strings.

And nobody tells you. The chunk keeps living in Qdrant, its cosine to auth-related queries stays high. The agent pulls dead information and works against it. Congratulations, you have memory rot disguised as memory.

What should be in the schema. Code parsing through go/ast (for Go) and tree-sitter (for PHP, JS, TS, Python, Rust, Java, and beyond). Node identity is built not from a string and not from a file path, but from a structural hash:

node_id = sha256(qualified_name + kind + signature_hash)
Enter fullscreen mode Exit fullscreen mode

Which means:

  • Renaming a function does not break references to it (qualified_name changed, but the link is updated automatically on next parse, with a back-reference to the old node_id as renamed_from).
  • Moving between packages — same thing.
  • Changing the signature (pointer → value receiver) — signature_hash changes, and old references automatically get marked stale — the brain knows they now require review.

What this enables. When the AI agent is about to edit FetchUser, the system pulls three past decisions about that function, two regressions in this module, and active project rules — before the agent starts writing code. Not because cosine happened to align. Because it's a code graph, and FetchUser has edges to decisions, regressions, and rules by identity, not by text similarity.

I call this pre-edit warning. And it's a qualitatively different kind of error prevention than "let's run a linter after generation."

How braincore does it. The code graph is a separate layer over AST/tree-sitter, with background reindex on filesystem watch events. Identity hashes live in SQLite, edges live there too. On a pre-edit hook, the agent gets the context of related decisions/rules/regressions automatically.


Principle 5. Internal Git as memory versioning

The pain. RAG has no concept of time beyond created_at. That's metadata about a record, not about a state of knowledge. You can't ask "show me what I knew about this code a month ago." You can't roll back the state of memory to before the agent dragged in garbage. You can't switch to a feature branch and have a parallel mental state for it.

What should be in the schema. Every change in memory is a commit. Not metaphorically. Literally, through go-git, into a hidden .internal-git/ repository that lives parallel to the project's main repo.

This gives you:

  • git log over the project's memory — what was added, what changed, when.
  • git checkout to roll back the brain state by N days — for audit, for regression investigation, for tests.
  • When you switch to a feature branch in the main repo, the brain mirrors that, and each branch has its own mental state. An experiment in a feature branch doesn't pollute master's memory.

What this enables. Time-travel queries: "which decision did I consider current 30 days ago?" Audit: "when exactly did the agent start believing we use ClickHouse?" Branch isolation: "in feature/oauth we have a different approach to auth, but that knowledge shouldn't leak into main."

RAG can't do this. RAG has no concept of "state of knowledge" — only a set of vectors that grows.

How braincore does it. The .internal-git/ is created on braincore init. Commits are made automatically on every change to knowledge units and graph edges. Branch tracking is synchronized with the main git through a post-checkout hook.


Principle 6. Memory Scoring — because not all knowledge is equal

The pain. In RAG, all chunks are equal. Top-k by cosine doesn't distinguish "this is confirmed by ten past uses" from "this was written yesterday and never used again." It doesn't distinguish "this is critical for the architecture" from "this is a random note in a corner." It doesn't distinguish "this is in active use" from "this has been gathering dust since last year."

What should be in the schema. Every knowledge unit has a composite MemoryScore, computed as a weighted sum:

MemoryScore =
  + 0.22 * ImportanceScore     (explicit importance, or derived from connectivity)
  + 0.22 * TrustScore          (source reliability + history of confirmations)
  + 0.20 * TaskRelevanceScore  (relevance to current work context)
  + 0.12 * UsageScore          (how often it's used)
  + 0.10 * RecencyScore        (freshness)
  + 0.10 * StabilityScore      (how often it changes — stable is more reliable)
  + 0.08 * NoveltyScore        (novelty as a soft boost)
  − 0.18 * RiskScore           (potential harm from use)
  − 0.18 * NoiseScore          (noise, duplicates, low coherence)
Enter fullscreen mode Exit fullscreen mode

And on retrieve, what runs is no longer cosine similarity, but:

RetrievalScore =
  + 0.35 * semantic_similarity
  + 0.20 * memory_score
  + 0.15 * graph_relevance
  + 0.15 * temporal_validity
  + 0.10 * trust_score
  − 0.15 * ambiguity_penalty
Enter fullscreen mode Exit fullscreen mode

These weights aren't ultimate truth — they're empirically tuned and shift with usage profile. The point isn't the numbers, it's the architectural shift: retrieval stops being "text similarity" and becomes "similarity × importance × trust × freshness."

Lifecycle transitions automatically:

  • memory_score ≥ 0.80 and trust ≥ 0.75consolidated (knowledge becomes "firmware")
  • memory_score ≥ 0.55 → stays in working
  • memory_score ≥ 0.30staging
  • memory_score < 0.30archive candidate

What this enables. Active memory. Not storage. An active environment in which what's important strengthens through use, and noise decays on its own — like in a biological brain, where rarely-used synapses weaken and frequently-used ones strengthen.

RAG = a hard drive that never gets defragmented.
Brain = a brain in which junk settles on its own and gets archived automatically.

How braincore does it. Scoring is recomputed by a background job every N hours. Lifecycle transitions are atomic and logged (see Principle 5). All weights are exposed in config — tune them per project.


Principle 7. Negative Memory and Rule Engine

The pain. Here's what every LLM agent does today: repeats mistakes. Yesterday it broke a migration — today it'll break a similar one. RAG won't help, because the broken migration doesn't go into RAG. What goes into RAG is "how to write migrations" from the official docs. The fact that you personally already stepped on this rake — recorded nowhere.

What should be in the schema. A separate class — negative memory: what broke, why it broke, how it was fixed, which commit/test confirms it. First-class entity, not a marginal field.

And during planning, every patch passes through a Rule Engine before code is generated:

patch
  → architectural rules
  → code rules
  → security rules
  → performance rules
  → anti-patterns (including "this exact one I broke before")
  → repair plan OR abstain
Enter fullscreen mode Exit fullscreen mode

If a rule with severity critical or high is violated — the code does not get written. A repair plan is created. If repair is impossible — abstain (see Principle 2). No "let's hope this passes" generation.

And, critically, the safe execution pipeline closes the loop:

checkpoint
  → apply patch
  → rules validate
  → build
  → tests
  → success → commit
  → fail → rollback → record into negative memory
Enter fullscreen mode Exit fullscreen mode

Every executed action is either confirmed by tests, rolled back, or recorded as negative evidence for future decisions.

What this enables. An agent that cannot repeat your last year's mistake. Not because it has a great model — but because the rule engine physically refuses to let through any patch that violates a rule derived from that mistake.

RAG helps the agent find something. Good memory prevents the agent from breaking something.

These are different products. And I feel sorry for those who keep mixing them up.

How braincore does it. Negative memory is a separate entity type with a required link to a failing test or git commit. The rule engine is a pre-execution gate, severity-aware, with override possible only via explicit user confirmation.


Bonus principle. Entity Disambiguation

Formally a special case of Principle 1 (atomic units), but it breaks separately often enough to deserve its own callout.

In RAG, there's no concept of an entity. There's only text. If your project has two User classes — one in pkg/auth, one in pkg/billing — for RAG these are two pieces of text with similar embeddings. On retrieve, they mix together, and the model confidently explains auth logic in the context of billing.

This isn't theory. This is happening right now in every code RAG agent.

The fix — EntityFingerprint:

fingerprint(symbol) = hash(
  project_id +
  file_path +
  symbol_name +
  symbol_type +
  signature +
  language
)
Enter fullscreen mode Exit fullscreen mode

Two User entities in different files = two fingerprints = two distinct entities that never auto-merge. When a new candidate arrives, a SameEntityScore is computed:

SameEntityScore =
  + 0.30 * name_similarity
  + 0.20 * alias_match
  + 0.20 * context_similarity
  + 0.15 * graph_neighborhood_similarity
  + 0.10 * temporal_consistency
  + 0.05 * source_consistency
Enter fullscreen mode Exit fullscreen mode

And:

  • ≥ 0.92auto_merge
  • ≥ 0.82same_as link (soft link, not merge)
  • ≥ 0.65ambiguous — an ambiguity record is created, requiring human review
  • otherwise — new entity

The core rule: never merge entities at low confidence. Better to create an ambiguity record and ask a human than to silently glue them together and lie forever after.


Why all of this together

I'm deliberately not framing this as "this is nowhere done, I'm first." Each of the seven principles already exists. Atomic facts with lifecycle — in knowledge management systems. Strict mode + abstain — in last century's expert systems. Causal chains — in decision support tools. AST identity — in IDEs. Internal git — in tools like Pijul and in Datalog database experiments. Memory scoring — in research papers on episodic memory. Negative memory — in RL and reliability engineering.

Uniqueness isn't in the ideas. It's in the assembly.

If you have atomic units but no strict mode — you have a structured database of hallucinations. If you have strict mode but no causal chains — you abstain without understanding why. If you have causal chains but no AST identity — your decisions point into the void after two refactorings. If you have all of the above but no memory scoring — you have a perfectly structured dump in which the important drowns in noise.

Each property in isolation is an improvement. All seven together is a different category of product.

This, by the way, is the answer to the question I get most often: "why write something new if I already have Mem0/Letta/Zep?" The answer — look at their schemas and check how many of the seven principles are implemented not as a marketing claim, but as an enforced gate in code. For most, the honest count is two or three. For some — four. They aren't bad products. They're partial solutions, more honestly called "structured retrieval" than "memory."


In Part 3

Seven principles is engineering. What should be in the architecture. But behind engineering sits a deeper question: why should an AI agent know what it doesn't know? Why abstain at all, if it can just answer?

Part 3 is about the right of an AI agent to stay silent. About self-tasking. About why cognitive runtime matters more than model size. And about why the right metric for production AI isn't accuracy, but zero confidently-wrong actions at an acceptable abstain rate.

It's the shortest and most philosophical piece in the series. Drops next week.


Part 2 of 3. If you missed Part 1 — here (on why RAG is search and not memory). If this resonated — a repost would help.

Top comments (0)