Hirestack
Hirestack
Chapter 14

AI Agents · Memory architectures

📖 3 min read · 7 sections · May 2026

Why this matters

LLMs are stateless. Every call starts from zero. Production agents need to remember — across turns, across sessions, across users. There are five distinct memory types; mixing them up is a common cause of confused-feeling assistants.

Five memory types

TypeStoresLifetimeWhere it lives
Working / scratchpadCurrent task's intermediate stateSingle requestIn-process (the agent state dict)
Short-term / conversationalThe current chat historyOne sessionDatabase keyed by session ID
Long-term episodicSpecific user events ("on 5 Mar, user said X")PermanentTime-stamped store; vector DB for retrieval
Long-term semanticDistilled facts ("user prefers vegetarian")PermanentStructured store (JSON, columns)
Long-term proceduralLearnt how-to ("when user asks for X, do Y")PermanentSkill registry; fine-tuned prompts

Short-term: keeping the conversation window alive

# Simple sliding window + summary pattern
def get_context(session_id, current_query):
    msgs = db.messages.find(session_id).order_by(time).limit(20)
    if len(msgs) >= 20:
        older = db.messages.find(session_id).older_than(msgs[0].time)
        summary = summarise_with_llm(older)   # cached; only recompute when stale
        return [{"role":"system","content":summary}, *msgs]
    return msgs

Long-term: vector-store-backed semantic memory

Distil important facts from conversations into discrete "memories", embed them, retrieve relevant ones at the start of each new turn. This is what Mem0, Zep, and the "memory" features in ChatGPT do under the hood.

sequenceDiagram
    participant U as User
    participant A as Agent
    participant M as Memory store
    participant LLM
    U->>A: New message
    A->>M: retrieve(top_k=5, query=msg)
    M-->>A: 5 relevant memories
    A->>LLM: [memories + current_msg]
    LLM-->>A: response
    A->>LLM: extract_facts(conversation)
    LLM-->>A: new facts to store
    A->>M: upsert(new_facts)
    A->>U: response

Memory extraction is the hard part

Storing every message is noise. Storing nothing is amnesia. The trick is a dedicated memory extractor — a small LLM call after each turn that decides what's worth keeping:

EXTRACT_PROMPT = """
Given this conversation turn, extract durable facts about the user worth remembering.
Return JSON: { "facts": [{"text": "...", "category": "...", "confidence": 0-1}] }
If nothing is worth keeping, return { "facts": [] }.

Examples of facts worth keeping:
- "User is vegetarian"
- "User is hiring for a backend role"
- "User's company is Acme"

Examples NOT to keep:
- Greetings, pleasantries, small talk
- Information about other people, only about THIS user
- Anything the user might want to change or undo

Conversation:
{conversation}
"""

Privacy and the forget button

Build "forget me" from day one. Memory that grows forever is a liability — GDPR, user trust, simple ops cost. Every memory record needs:
  • A clear owner (user_id)
  • A creation timestamp
  • A reason it was kept (the extractor's justification)
  • A way to be deleted via one API call

The token budget for memory

Every memory you stuff into the prompt costs tokens. A 4k context with 2k of memory leaves 2k for actual conversation. Production budgets:

Q: How do you avoid the agent contradicting itself across sessions?
Three things: (1) make the memory extractor strict about extracting only durable facts, not opinions; (2) version memories — when the user says something that contradicts a stored fact, mark the old one superseded rather than deleting (audit trail); (3) at retrieval time, prefer recent over old when memories conflict.