Agent Memory & State
Implement short-term and long-term memory, build persistent vector stores for agent recall, manage context windows intelligently, and create agents that learn from past sessions.
The Memory Problem
Why Agents Need Memory
LLMs are stateless — every API call starts from nothing. Without memory, your agent can't remember what it did 5 minutes ago, what the user said yesterday, or what it learned from past mistakes. Memory transforms a forgetful chatbot into a persistent, learning assistant.
The four levels of agent memory, from volatile to permanent:
Memory Types in Detail
| Memory Type | Storage | Capacity | Access Pattern | Use Case |
|---|---|---|---|---|
| In-Context | Current prompt | Context window (128K tokens) | Always present | Current conversation turn |
| Buffer (Short-term) | Message list in memory | Last N messages | FIFO, sliding window | Multi-turn conversation |
| Vector (Long-term) | Vector database | Unlimited | Semantic similarity search | Past conversations, facts, experiences |
| Parametric | Fine-tuned model weights | Unlimited | Implicit (in generation) | Learned behaviors, domain knowledge |
Short-term Memory: Conversation Buffers
"""Short-term memory implementations."""
from collections import deque
from typing import Optional
import tiktoken
class ConversationBuffer:
"""Simple sliding window memory."""
def __init__(self, max_messages: int = 20):
self.messages: deque = deque(maxlen=max_messages)
self.system_prompt: str = ""
def add(self, role: str, content: str):
self.messages.append({"role": role, "content": content})
def get_messages(self) -> list[dict]:
"""Get messages for API call."""
msgs = [{"role": "system", "content": self.system_prompt}]
msgs.extend(list(self.messages))
return msgs
class TokenBudgetBuffer:
"""Memory that manages token budget intelligently."""
def __init__(self, max_tokens: int = 8000, model: str = "gpt-4o"):
self.max_tokens = max_tokens
self.encoder = tiktoken.encoding_for_model(model)
self.messages: list[dict] = []
self.system_prompt: str = ""
def _count_tokens(self, messages: list[dict]) -> int:
return sum(len(self.encoder.encode(m["content"])) for m in messages)
def add(self, role: str, content: str):
self.messages.append({"role": role, "content": content})
self._trim()
def _trim(self):
"""Remove oldest messages until under budget."""
while (self._count_tokens(self.messages) > self.max_tokens
and len(self.messages) > 2):
# Always keep the most recent exchange
self.messages.pop(0)
def get_messages(self) -> list[dict]:
msgs = [{"role": "system", "content": self.system_prompt}]
msgs.extend(self.messages)
return msgs
class SummarizingBuffer:
"""Summarizes old messages to compress history."""
def __init__(self, max_recent: int = 10, summarize_fn=None):
self.recent: deque = deque(maxlen=max_recent)
self.summary: str = ""
self.summarize_fn = summarize_fn # LLM-based summarization
self._overflow_buffer: list = []
def add(self, role: str, content: str):
# When recent buffer is full, overflow triggers summarization
if len(self.recent) == self.recent.maxlen:
self._overflow_buffer.append(self.recent[0])
if len(self._overflow_buffer) >= 5:
self._summarize_overflow()
self.recent.append({"role": role, "content": content})
def _summarize_overflow(self):
"""Summarize old messages into a compressed summary."""
old_text = "\n".join(f"{m['role']}: {m['content']}" for m in self._overflow_buffer)
if self.summarize_fn:
new_summary = self.summarize_fn(self.summary, old_text)
self.summary = new_summary
self._overflow_buffer = []
def get_messages(self) -> list[dict]:
msgs = [{"role": "system", "content": self.system_prompt}]
if self.summary:
msgs.append({"role": "system", "content": f"Conversation history summary:\n{self.summary}"})
msgs.extend(list(self.recent))
return msgs
@property
def system_prompt(self):
return self._system_prompt
@system_prompt.setter
def system_prompt(self, value):
self._system_prompt = value
Long-term Memory: Vector Store
How Vector Memory Works
Every interaction gets embedded and stored in a vector database. When the agent needs past context, it searches semantically — "what do I know about the user's preferences?" retrieves relevant past conversations even if the exact words differ. This gives agents unlimited, searchable memory.
"""Long-term vector memory for persistent agents."""
import json
import hashlib
from datetime import datetime
from typing import Optional
import openai
import chromadb
client = openai.OpenAI()
class VectorMemory:
"""Persistent long-term memory backed by ChromaDB."""
def __init__(self, agent_id: str, collection_name: str = None):
self.agent_id = agent_id
self.chroma = chromadb.PersistentClient(path=f"./memory/{agent_id}")
self.collection = self.chroma.get_or_create_collection(
name=collection_name or f"{agent_id}_memory",
metadata={"hnsw:space": "cosine"}
)
def store(self, content: str, metadata: dict = None) -> str:
"""Store a memory with embedding."""
memory_id = hashlib.md5(content.encode()).hexdigest()[:12]
meta = {
"timestamp": datetime.now().isoformat(),
"agent_id": self.agent_id,
**(metadata or {}),
}
# ChromaDB handles embedding automatically, or we can provide our own
self.collection.upsert(
ids=[memory_id],
documents=[content],
metadatas=[meta],
)
return memory_id
def retrieve(self, query: str, n_results: int = 5,
filter_metadata: dict = None) -> list[dict]:
"""Retrieve relevant memories by semantic similarity."""
kwargs = {
"query_texts": [query],
"n_results": n_results,
}
if filter_metadata:
kwargs["where"] = filter_metadata
results = self.collection.query(**kwargs)
memories = []
for i in range(len(results["ids"][0])):
memories.append({
"id": results["ids"][0][i],
"content": results["documents"][0][i],
"metadata": results["metadatas"][0][i],
"distance": results["distances"][0][i] if results.get("distances") else None,
})
return memories
def store_conversation(self, messages: list[dict], summary: str = None):
"""Store an entire conversation as a memory."""
# Store the summary for quick retrieval
if summary:
self.store(summary, metadata={"type": "conversation_summary"})
# Store individual important exchanges
for msg in messages:
if msg["role"] == "assistant" and len(msg["content"]) > 100:
self.store(msg["content"], metadata={
"type": "assistant_response",
"role": msg["role"],
})
def get_relevant_context(self, current_input: str, max_tokens: int = 2000) -> str:
"""Get relevant past context for the current conversation."""
memories = self.retrieve(current_input, n_results=5)
context_parts = []
token_count = 0
for memory in memories:
content = memory["content"]
# Rough token estimate
est_tokens = len(content.split()) * 1.3
if token_count + est_tokens > max_tokens:
break
context_parts.append(f"[{memory['metadata'].get('timestamp', 'unknown')}] {content}")
token_count += est_tokens
if context_parts:
return "Relevant memories from past interactions:\n" + "\n---\n".join(context_parts)
return ""
Complete Agent with Persistent Memory
"""Agent that remembers across sessions using vector memory."""
class MemoryAgent:
"""Agent with short-term buffer + long-term vector memory."""
def __init__(self, agent_id: str, system_prompt: str):
self.agent_id = agent_id
self.system_prompt = system_prompt
self.short_term = SummarizingBuffer(max_recent=10, summarize_fn=self._summarize)
self.long_term = VectorMemory(agent_id)
self.short_term.system_prompt = system_prompt
self.session_messages: list[dict] = []
def _summarize(self, existing_summary: str, new_text: str) -> str:
"""Use LLM to create a running summary."""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"Summarize this conversation history into key facts and context:\n\n"
f"Previous summary: {existing_summary or 'None'}\n\n"
f"New messages:\n{new_text}\n\n"
f"Output a concise summary (max 200 words) of important facts, "
f"user preferences, and key decisions."
}],
max_tokens=300,
)
return response.choices[0].message.content
def chat(self, user_input: str) -> str:
"""Process user input with memory-enhanced context."""
# 1. Retrieve relevant long-term memories
memory_context = self.long_term.get_relevant_context(user_input)
# 2. Build messages with memory context
self.short_term.add("user", user_input)
messages = self.short_term.get_messages()
# Inject memory context after system prompt
if memory_context:
messages.insert(1, {
"role": "system",
"content": memory_context
})
# 3. Generate response
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
temperature=0.7,
)
assistant_reply = response.choices[0].message.content
self.short_term.add("assistant", assistant_reply)
# 4. Store important interactions in long-term memory
self.session_messages.append({"role": "user", "content": user_input})
self.session_messages.append({"role": "assistant", "content": assistant_reply})
# Store if the exchange seems important (heuristic: length or keywords)
if len(user_input) > 50 or any(kw in user_input.lower()
for kw in ["remember", "preference", "always", "never", "important"]):
self.long_term.store(
f"User said: {user_input}\nAssistant replied: {assistant_reply[:200]}",
metadata={"type": "important_exchange"}
)
return assistant_reply
def end_session(self):
"""Summarize and store the session for future retrieval."""
if len(self.session_messages) > 2:
summary = self._summarize("",
"\n".join(f"{m['role']}: {m['content']}" for m in self.session_messages[-10:]))
self.long_term.store_conversation(self.session_messages, summary=summary)
self.session_messages = []
# ─── Usage ────────────────────────────────────────────────────
agent = MemoryAgent(
agent_id="assistant_001",
system_prompt="You are a helpful personal assistant. You remember past conversations "
"and user preferences. Reference relevant memories when appropriate."
)
# Session 1
print(agent.chat("My name is Alex and I'm a Python developer"))
print(agent.chat("I prefer concise, code-heavy explanations"))
agent.end_session()
# Session 2 (days later — agent still remembers!)
print(agent.chat("Can you help me with a coding problem?"))
# Agent will recall: user is Alex, prefers concise/code-heavy style
Context Window Management
The Context Budget Problem
Even with 128K token windows, agents eat through context fast: system prompt + tools + memory + conversation + tool results. You need a budget strategy that allocates tokens across these competing needs.
"""Context window budget management."""
class ContextBudget:
"""Allocate context window across competing needs."""
def __init__(self, total_tokens: int = 128000, model: str = "gpt-4o"):
self.total = total_tokens
self.reserved_for_output = 4000 # Leave room for response
self.available = total_tokens - self.reserved_for_output
# Budget allocation (percentages)
self.allocations = {
"system_prompt": 0.05, # 5% — ~6K tokens
"tools": 0.10, # 10% — tool schemas
"long_term_memory": 0.15, # 15% — retrieved memories
"short_term_memory": 0.20, # 20% — conversation summary
"recent_messages": 0.40, # 40% — recent exchanges
"current_input": 0.10, # 10% — user's current message
}
def get_budget(self, category: str) -> int:
"""Get token budget for a category."""
return int(self.available * self.allocations.get(category, 0))
def build_context(self, system_prompt: str, tools: list,
long_term: str, short_term: list[dict],
recent: list[dict], current: str) -> list[dict]:
"""Build the final message list within budget."""
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4o")
messages = []
# 1. System prompt (always included)
messages.append({"role": "system", "content": system_prompt})
# 2. Long-term memory (trimmed to budget)
if long_term:
budget = self.get_budget("long_term_memory")
tokens = enc.encode(long_term)
if len(tokens) > budget:
long_term = enc.decode(tokens[:budget])
messages.append({"role": "system", "content": f"Memories:\n{long_term}"})
# 3. Short-term summary
if short_term:
messages.append({"role": "system", "content": f"Session context:\n{short_term}"})
# 4. Recent messages (most recent first, trim from oldest)
budget = self.get_budget("recent_messages")
recent_tokens = 0
kept_recent = []
for msg in reversed(recent):
msg_tokens = len(enc.encode(msg["content"]))
if recent_tokens + msg_tokens > budget:
break
kept_recent.insert(0, msg)
recent_tokens += msg_tokens
messages.extend(kept_recent)
# 5. Current input
messages.append({"role": "user", "content": current})
return messages
| Strategy | Approach | Pro | Con |
|---|---|---|---|
| Sliding Window | Keep last N messages | Simple, fast | Loses early context entirely |
| Summarization | LLM summarizes old messages | Preserves key info | Extra LLM call, lossy |
| RAG on History | Embed + retrieve relevant past | Only includes what's relevant | May miss important context |
| Hybrid | Summary + RAG + recent window | Best coverage | Complex, multiple systems |
State Machines for Agent Control
"""State machine for complex agent workflows."""
from enum import Enum
class AgentState(Enum):
IDLE = "idle"
GATHERING_INFO = "gathering_info"
PLANNING = "planning"
EXECUTING = "executing"
REVIEWING = "reviewing"
COMPLETE = "complete"
ERROR = "error"
class StatefulAgent:
"""Agent with explicit state tracking."""
def __init__(self):
self.state = AgentState.IDLE
self.context: dict = {}
self.transitions = {
AgentState.IDLE: [AgentState.GATHERING_INFO, AgentState.PLANNING],
AgentState.GATHERING_INFO: [AgentState.PLANNING, AgentState.GATHERING_INFO],
AgentState.PLANNING: [AgentState.EXECUTING],
AgentState.EXECUTING: [AgentState.REVIEWING, AgentState.ERROR],
AgentState.REVIEWING: [AgentState.COMPLETE, AgentState.EXECUTING],
AgentState.ERROR: [AgentState.GATHERING_INFO, AgentState.IDLE],
}
def transition(self, new_state: AgentState):
"""Validate and perform state transition."""
if new_state not in self.transitions.get(self.state, []):
raise ValueError(
f"Invalid transition: {self.state.value} → {new_state.value}. "
f"Allowed: {[s.value for s in self.transitions[self.state]]}"
)
print(f" State: {self.state.value} → {new_state.value}")
self.state = new_state
def run(self, goal: str) -> str:
"""Run agent with state machine control."""
self.context["goal"] = goal
self.transition(AgentState.GATHERING_INFO)
# Gather phase
info = self._gather(goal)
self.context["info"] = info
self.transition(AgentState.PLANNING)
plan = self._plan(goal, info)
self.context["plan"] = plan
self.transition(AgentState.EXECUTING)
try:
result = self._execute(plan)
self.context["result"] = result
except Exception as e:
self.transition(AgentState.ERROR)
return f"Error during execution: {e}"
self.transition(AgentState.REVIEWING)
if self._quality_check(result):
self.transition(AgentState.COMPLETE)
return result
else:
# Re-execute with feedback
self.transition(AgentState.EXECUTING)
result = self._execute(plan, feedback="Improve quality")
self.transition(AgentState.REVIEWING)
self.transition(AgentState.COMPLETE)
return result
Mini-Project: Agent with Persistent Memory
🛠️ Build a Personal Assistant That Remembers Everything
Create an agent that maintains persistent memory across sessions, recalls relevant past interactions, and adapts to user preferences over time.
- Implement
VectorMemoryclass with ChromaDB for persistent storage - Build
SummarizingBufferfor short-term context management - Create
MemoryAgentthat combines both memory types - Add automatic memory storage triggers:
- User states a preference → store as "preference" type
- User provides personal info → store as "personal" type
- Agent makes a commitment → store as "commitment" type
- Implement session end: summarize and store the full conversation
- Build
ContextBudgetto manage token allocation across memory types - Test across multiple sessions:
- Session 1: User shares name, role, preferences
- Session 2: Ask "what do you remember about me?" — verify recall
- Session 3: Reference earlier preferences in new responses
Stretch Goals
- Add memory decay: older memories get lower relevance scores over time
- Implement "memory consolidation": periodically merge similar memories into summaries
- Add user controls: "forget this", "remember that", "what do you know about X?"
- Build a memory dashboard showing stored memories, categories, and access patterns
Key Takeaways
- Agent memory has 4 layers: in-context, buffer (short-term), vector (long-term), and parametric (fine-tuning)
- Short-term memory uses sliding windows or summarization to compress conversation history
- Long-term memory uses vector stores for semantic retrieval across sessions
- Context budget management is essential — allocate tokens across system prompt, memory, history, and current input
- Hybrid approaches (summary + RAG + recent window) give the best coverage
- State machines provide explicit control over agent behavior and valid transitions