Memory
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page’s examples target Python 3.12 with LangChain 1.4.2 / langchain-core 1.6.5 / LangGraph 1.2.12
(langgraph-checkpoint 4.2.0) and langchain-ollama 1.1.0, Java 21 with LangChain4j 1.20.2, and Spring Boot 4.1
with Spring AI 2.0.1 (plus the community spring-ai-agent-utils 0.12.0), using DocsAssistant's Ollama
llama3.1:8b chat model and nomic-embed-text embeddings (768 dimensions), as checked on 2026-09-28 (see the
AI Agents baseline).
A model has no memory of its own between calls: everything it "remembers" is something the application put back into the context window. Agent memory is therefore a storage-and-retrieval design problem: what to keep, where, for how long, and how to get the right part of it back into the next call. This page covers the taxonomy and the framework primitives. The vector-store side — storing memories as embeddings, semantic caching, and running it on Redis or PostgreSQL — is on Agent Memory & Semantic Cache, which in turn links the Redis-specific patterns; graph-structured memory (GraphRAG) is on Neo4j Vector Search & GenAI. The planned RAG Systems in Production sub-section will cover conversational memory in production RAG applications.
Short-term and long-term memory
-
Short-term memory is the state of one conversation or task — the message history, tool results and scratchpad of a single thread. It lives as long as the thread and is what the context-window techniques on Context Engineering for Agents manage.
-
Long-term memory survives across threads: facts about a user, what worked last time, how a task is done. It is written deliberately (by the agent or by a background process), stored outside the conversation, and retrieved selectively into future threads.
The distinction maps cleanly onto the frameworks: LangGraph checkpointers persist thread state and stores hold cross-thread memory; LangChain4j and Spring AI provide chat memory for the short-term side and leave long-term memory to a store or a vector store you choose.
A taxonomy of memory
Borrowing from cognitive science, agent memory is usually split into four kinds, plus the sensory buffer of raw input that exists only for the current turn:
| Kind | What it holds for DocsAssistant | Where it usually lives |
|---|---|---|
Working |
This conversation’s messages, retrieved chunks, notes and todo list |
Graph state / chat memory, persisted per thread |
Semantic |
"Alice uses Spring AI and wants Java-only answers"; facts extracted from conversations |
A key-value or vector store, namespaced per user |
Episodic |
"Last time Alice asked about pgvector, the HNSW page answered it"; past trajectories as examples |
A vector store searched by similarity to the current task |
Procedural |
The system prompt, tool descriptions, and rules the agent has learned ("always cite the page path") |
Prompt templates, versioned; sometimes rewritten by the agent itself |
The generative-agents work (Park et al., 2023) is the canonical demonstration of the long-term side: agents that store observations, periodically reflect on them into higher-level memories, and retrieve by a mix of recency, importance and relevance.
Extraction, compression and experience memory
Long-term memory does not fill itself. Three processes turn working memory into something worth keeping:
-
Extraction — after (or during) a conversation, a model call pulls out durable facts ("prefers Java", "is migrating from Spring AI 1.x") and writes them to semantic memory. Doing this in the background, off the hot path, keeps latency down; doing it in the loop via a
remembertool (below) lets the agent decide what matters. -
Compression — memories grow without bound, so they are periodically consolidated: near-duplicates merged, related memories clustered and summarised into one, stale ones dropped. The same summarisation that compacts a thread (see Context Engineering for Agents) applies across threads.
-
Experience memory — storing whole trajectories, or lessons distilled from them, so that the next similar task starts from what worked. This is episodic memory used for improvement, and it is the basis of Reflexion-style learning (see Learning & Improvement).
LangGraph: checkpointers and stores
A checkpointer saves the full graph state after every step, keyed by the thread_id in the run’s config. That
gives an agent short-term memory (resume the same thread and the history is there), and it is also what makes
human-in-the-loop interrupts and time-travel debugging possible. A store is a separate, cross-thread key-value
store with optional semantic search, organised by namespaces (tuples such as ("docsassistant", user_id,
"preference")).
from dataclasses import dataclass
from datetime import datetime, timezone
from langchain.agents import create_agent
from langchain.tools import ToolRuntime, tool
from langchain_ollama import ChatOllama, OllamaEmbeddings
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore
@dataclass
class UserContext:
user_id: str # set by the application, never by the model
role: str = "anonymous" # likewise; the guardrails policy layer reads it
store = InMemoryStore(index={ # semantic search over stored memories
"embed": OllamaEmbeddings(model="nomic-embed-text"),
"dims": 768,
"fields": ["text"],
})
@tool
def remember(fact: str, kind: str, runtime: ToolRuntime[UserContext]) -> str:
"""Save a durable fact about this user: kind is "preference" or "episode"."""
namespace = ("docsassistant", runtime.context.user_id, kind)
key = datetime.now(timezone.utc).isoformat()
runtime.store.put(namespace, key, {"text": fact})
return f"saved as {kind}"
@tool
def recall(query: str, runtime: ToolRuntime[UserContext]) -> list[str]:
"""Search what you remember about this user (preferences and past episodes)."""
hits = runtime.store.search(("docsassistant", runtime.context.user_id), query=query, limit=5)
return [h.value["text"] for h in hits]
agent = create_agent(
ChatOllama(model="llama3.1:8b", temperature=0),
tools=[docs_search, remember, recall],
system_prompt="You are DocsAssistant. Call recall before answering; call remember when the "
"user states a lasting preference.",
context_schema=UserContext,
checkpointer=InMemorySaver(), # short-term: one thread's messages
store=store, # long-term: shared across threads
)
config = {"configurable": {"thread_id": "alice-2026-09-28"}}
agent.invoke({"messages": [("user", "I only use Spring AI, keep answers Java-only.")]},
config, context=UserContext(user_id="alice", role="reader"))
A second thread (thread_id "alice-2026-10-02") starts with an empty message history, but recall finds the
preference because the store is shared across threads and the namespace is keyed by user. The user ID (and the
caller’s role, which Agent Guardrails & Safety checks before
each tool call) comes from the run’s context, supplied by the application, so the model cannot read or write another user’s namespace by
choosing a different ID. agent.get_state(config) returns the saved thread state. docs_search is the tool from
Tool Design.
InMemorySaver and InMemoryStore are for development. In production, use a durable checkpointer (the
langgraph-checkpoint-postgres and langgraph-checkpoint-sqlite packages provide PostgresSaver and
SqliteSaver) and a durable store (PostgresStore), which fits DocsAssistant since its vectors already live in
PostgreSQL. See LangGraph — Persistence,
LangChain — Short-term memory and
LangChain — Long-term memory.
LangChain4j: ChatMemory and ChatMemoryStore
LangChain4j’s ChatMemory is the short-term window (MessageWindowChatMemory or TokenWindowChatMemory); a
ChatMemoryStore is where it persists. Annotating an AI-service parameter with @MemoryId gives every user or
conversation its own memory, created on demand by a ChatMemoryProvider.
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.ChatMessageDeserializer;
import dev.langchain4j.data.message.ChatMessageSerializer;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
public class JdbcChatMemoryStore implements ChatMemoryStore {
private final JdbcTemplate jdbc;
public JdbcChatMemoryStore(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
@Override
public List<ChatMessage> getMessages(Object memoryId) {
List<String> rows = jdbc.queryForList(
"SELECT messages FROM chat_memory WHERE memory_id = ?", String.class, memoryId.toString());
return rows.isEmpty() ? List.of() : ChatMessageDeserializer.messagesFromJson(rows.get(0));
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
jdbc.update("""
INSERT INTO chat_memory (memory_id, messages, updated_at) VALUES (?, ?, now())
ON CONFLICT (memory_id) DO UPDATE SET messages = EXCLUDED.messages, updated_at = now()""",
memoryId.toString(), ChatMessageSerializer.messagesToJson(messages));
}
@Override
public void deleteMessages(Object memoryId) {
jdbc.update("DELETE FROM chat_memory WHERE memory_id = ?", memoryId.toString());
}
}
interface DocsAssistant {
String chat(@MemoryId String userId, @UserMessage String message);
}
DocsAssistant assistant = AiServices.builder(DocsAssistant.class)
.chatModel(chatModel)
.tools(new DocsTools())
.chatMemoryProvider(userId -> MessageWindowChatMemory.builder()
.id(userId)
.maxMessages(20)
.chatMemoryStore(new JdbcChatMemoryStore(jdbcTemplate))
.build())
.build();
ChatMemoryStore has exactly three methods — getMessages, updateMessages, deleteMessages — and the memory
calls updateMessages with the whole (already windowed) list, so an upsert is all a store needs. See
LangChain4j — Chat memory. LangChain4j has no dedicated
long-term memory API; semantic and episodic memory are an EmbeddingStore (for example pgvector, as on
Agent Memory & Semantic Cache) exposed through a
remember/recall pair of @Tool methods, exactly as in the LangGraph example.
Spring AI: ChatMemory, MessageWindowChatMemory, ChatMemoryRepository and memory tools
Spring AI separates the policy (ChatMemory, whose built-in implementation MessageWindowChatMemory keeps a
window of messages, 20 by default) from the storage (ChatMemoryRepository, with an in-memory default and JDBC,
Cassandra, Neo4j, MongoDB, Cosmos DB and Redis implementations). An advisor connects memory to ChatClient:
MessageChatMemoryAdvisor replays the window as messages, and VectorStoreChatMemoryAdvisor retrieves relevant
past messages from a VectorStore instead.
Short-term memory
JdbcChatMemoryRepositoryimport org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository;
@Configuration
class MemoryConfig {
@Bean
ChatMemory chatMemory(JdbcChatMemoryRepository repository) { // auto-configured from the DataSource
return MessageWindowChatMemory.builder()
.chatMemoryRepository(repository)
.maxMessages(20)
.build();
}
@Bean
ChatClient docsAssistant(ChatClient.Builder builder, ChatMemory chatMemory, DocsTools docsTools) {
return builder
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.defaultTools(docsTools)
.build();
}
}
// per request: the conversation ID is derived on the server, never taken from the prompt
String answer = docsAssistant.prompt()
.user(question)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, user.getId() + ":" + sessionId))
.call()
.content();
ChatMemory.CONVERSATION_ID is required on every call that uses a memory advisor. Deriving it from the
authenticated user and session on the server is what keeps one user’s history out of another’s. Spring AI’s
reference notes two current limitations worth designing around: intermediate tool-call messages are not stored in
chat memory, and JdbcChatMemoryRepository does not store tool-call messages. See
Spring AI — Chat Memory.
Long-term memory tools
For memory the agent manages, the community spring-ai-agent-utils library provides AutoMemoryTools: a set of
file-backed memory tools (memoryView, memoryCreate, memoryStrReplace, memoryInsert, memoryDelete,
memoryRename) rooted in one directory, so the model can keep and edit its own notes across sessions, plus an
AutoMemoryToolsAdvisor that wires them and their instructions into a ChatClient.
import org.springaicommunity.agent.tools.AutoMemoryTools;
ChatClient docsAssistant = chatClientBuilder
.defaultTools(docsTools,
AutoMemoryTools.builder()
.memoriesDir("/var/lib/docsassistant/memories/" + userId) // one directory per user
.build())
.build();
Scoping the directory per user is the same isolation rule as the LangGraph namespace above. See spring-ai-agent-utils.
Privacy and retention
Memory turns a stateless assistant into a system that stores personal data, and the obligations follow from that.
Minimise what is kept: extract the preference ("prefers Java examples"), not the transcript it came from, and never
extract secrets, credentials or special-category data. Isolate by user at the storage key (namespace, conversation
ID, directory), derived from authentication rather than from anything the model or the user types. Give every kind
of memory a retention period and enforce it in the storage layer — LangGraph’s store accepts a per-item ttl, a
JDBC table can be pruned on updated_at, and a vector store can filter on a timestamp field. Make deletion real:
a "forget me" request must reach the checkpointer, the store, the chat-memory repository and any vector index,
which is easiest when all of them share one user key. Tell users what is remembered and let them see and edit it.
Finally, treat retrieved memories as untrusted input: a memory written from a prompt-injected conversation can
poison every later one, which is why memory writes belong in the audit trail described on
Agent Guardrails & Safety.
In other frameworks
| Framework | Memory primitives |
|---|---|
OpenAI Agents SDK |
Sessions ( |
Claude Agent SDK |
Sessions can be resumed or forked ( |
Microsoft Agent Framework |
|
CrewAI |
|