Most developers who add memory to an AI agent start by storing everything as the same type. One blob of text, one vector, one table. It works — until it doesn't.
The problem surfaces during recall. When a user asks "what did we discuss last time?", you want episodic context — the narrative of past interactions. When they ask "what's my subscription plan?", you want semantic facts. When your agent is about to generate code, you want procedural rules — conventions, constraints, patterns.
Mix them all together and your similarity search returns a soup of everything. Kronvex supports three memory types, each optimized for different retrieval patterns. Here's how to use them.
The three types
This mirrors how human long-term memory is structured, as described by Endel Tulving's model of memory systems. Your agent should work the same way.
Episodic memory — the event log
Episodic memories are timestamped interactions. Use them to store what happened in a session — what the user said, what problem they had, what was resolved.
# After resolving a support ticket kx.remember( "User reported login issue on mobile app. Root cause: cached " "OAuth token from old device. Resolved by clearing app data. " "User is satisfied.", memory_type="episodic", session_id="ticket_4821", metadata={"resolved": True, "category": "auth"} )
Episodic memories are naturally TTL candidates. A support ticket from 6 months ago is less useful than one from last week. Use ttl_days=180 to auto-expire stale events while pinning critical ones.
# Expires in 90 days, but keeps the fact it was a VIP issue kx.remember( "VIP client (ACME Corp) escalated billing dispute to CEO.", memory_type="episodic", ttl_days=90, pinned=True, # pinned = survives even past ttl metadata={"vip": True, "escalation_level": "ceo"} )
Semantic memory — the knowledge base
Semantic memories are facts about the world or the user that don't expire with time. They're not about events — they're about states: what plan someone is on, what language they prefer, what their role is.
# User profile facts — no TTL, these are permanent kx.remember( "User is a senior backend engineer, 8 years experience. " "Prefers Python (FastAPI) and Go. Hostile to unnecessary abstractions.", memory_type="semantic", pinned=True ) kx.remember( "User's company: Dataflow Inc, Series A startup, 25 employees, " "building an AI-powered data pipeline tool.", memory_type="semantic", pinned=True )
Semantic memory is what makes an agent feel like it knows you. When a user asks "what stack should I use?", the agent doesn't ask for context — it already has it.
Updating semantic facts
A key challenge: semantic facts change. Users switch plans, companies pivot. The simplest pattern is to store the update as a new memory with higher recency weight — the confidence scoring system (similarity×0.6 + recency×0.2 + frequency×0.2) will naturally surface the most recent version first.
For critical updates, delete the outdated memory explicitly before storing the new one.
Procedural memory — the rulebook
Procedural memories are instructions the agent should follow. They're not about events or facts — they're about behaviour. Think of them as injected system prompt fragments that persist across sessions.
# Codebase conventions — shared across team kx.remember( "We use CQRS for all write operations. Commands go through " "the CommandBus, queries use direct repository calls. Never mix.", memory_type="procedural", pinned=True ) kx.remember( "All API responses must follow RFC 9457 (Problem Details). " "Error format: {type, title, status, detail, instance}.", memory_type="procedural", pinned=True )
Procedural memories are almost always pinned and have no TTL — architectural decisions don't expire. They change only when intentionally updated.
Choosing the right type: a decision table
| SCENARIO | TYPE | TTL? | PINNED? |
|---|---|---|---|
| User resolved a support ticket | episodic | Yes (90–180d) | No |
| User mentioned their tech stack | semantic | No | Yes |
| Sales call notes, objections raised | episodic | Yes (30–60d) | No |
| User is on Pro plan, 3 agents | semantic | No | Yes |
| Architecture decision: use event sourcing | procedural | No | Yes |
| Tutoring session: student struggled with recursion | episodic | Yes (60d) | No |
| Student's mastery level: recursion = intermediate | semantic | No | No (update regularly) |
Coding convention: no any in TypeScript |
procedural | No | Yes |
| Renewal intel: contract up in 30 days | episodic | Yes (30d) | No |
Filtering by type during recall
Memory type filtering is one of the most underused features. When you know the kind of memory you need, filtering by type reduces noise and improves the confidence scores of your results.
# Before generating code: only want rules conventions = kx.recall( "error handling in API responses", memory_type="procedural", top_k=3 ) # Before responding to a support message: want event history history = kx.recall( "past issues with this user", memory_type="episodic", session_id=user_id, # scoped to this user's thread top_k=5 ) # For personalisation: want profile facts profile = kx.recall( "user preferences and background", memory_type="semantic", top_k=5 )
Combining types in inject-context
For most use cases, /inject-context handles the merge automatically — it returns the top-K most relevant memories across all types. But for power users, you can build a richer context block manually:
def build_system_prompt(user_message: str, agent_id: str) -> str: kx = Kronvex(agent_id=agent_id) # Fetch each type in parallel (async version available) rules = kx.recall(user_message, memory_type="procedural", top_k=3) facts = kx.recall(user_message, memory_type="semantic", top_k=4) history = kx.recall(user_message, memory_type="episodic", top_k=3) # Build context sections sections = [] if rules.results: sections.append("## Rules & Conventions\n" + "\n".join(f"- {r.memory.content}" for r in rules.results)) if facts.results: sections.append("## User Profile\n" + "\n".join(f"- {r.memory.content}" for r in facts.results)) if history.results: sections.append("## Recent History\n" + "\n".join(f"- {r.memory.content}" for r in history.results)) context = "\n\n".join(sections) return f"[CONTEXT]\n{context}\n[/CONTEXT]\n\nYou are a helpful assistant."
What happens if you mix types?
Storing everything as episodic is the most common mistake. It works at small scale but creates two problems at scale:
- Precision drops. Fact-like memories compete with event memories during recall. A user's subscription plan has the same embedding neighbourhood as a billing event from 6 months ago.
- No meaningful TTL strategy. You can't expire episodic events without also expiring semantic facts you need to keep forever.
The fix is straightforward: classify at store time, filter at recall time. It's one extra parameter that pays off compoundingly as your memory store grows.
TL;DR
- Episodic → what happened (interactions, events) → use TTL freely
- Semantic → what is true (facts, profiles) → pin the important ones
- Procedural → how to behave (rules, conventions) → always pin, no TTL
- Filter by type during recall for better precision, especially at scale
- Use
/inject-contextfor simple cases, manual multi-type recall for complex agents
Start building with 100 free memories
Three memory types. One API. No infrastructure to manage.
Get your free API key →