Course Content
LangChain Mastery
7 sections · 109 lessons
Write a function to implement a custom memory type in LangChain.
What you need to know
Pick the right extension point
| You want to change | Extend (LangChain 1.x) |
|---|---|
| What the model sees each call | wrap_model_call or before_model middleware |
| What happens after each turn (save facts, export) | after_agent middleware |
| Structured facts remembered in a thread | Custom state_schema fields |
| Facts shared across threads | LangGraph store (put, get, search) |
| Where checkpoints are stored | BaseCheckpointSaver subclass (rare) |
Legacy: storage for RunnableWithMessageHistory | BaseChatMessageHistory subclass |
Keep storage (where state lives) separate from policy (what is kept and sent). Most "custom memory" requests are policy, and middleware is the place for it.
A custom memory type as middleware
This one keeps a per-thread "case file" of key facts in state, updates it after each turn, and shows it to the model on every call:
1from typing_extensions import NotRequired2from langchain.agents import AgentState, create_agent3from langchain.agents.middleware import AgentMiddleware4from langchain.messages import SystemMessage56class CaseState(AgentState):7 case_file: NotRequired[dict[str, str]] # checkpointed with the messages89class CaseFileMemory(AgentMiddleware[CaseState]):10 state_schema = CaseState1112 def after_agent(self, state, runtime):13 facts = extract_facts(state["messages"][-4:]) # small model, structured output14 return {"case_file": {**state.get("case_file", {}), **facts}}1516 def wrap_model_call(self, request, handler):17 facts = request.state.get("case_file", {})18 note = "\n".join(f"- {k}: {v}" for k, v in facts.items()) or "- none yet"19 base = request.system_message.content if request.system_message else ""20 return handler(request.override(21 system_message=SystemMessage(f"{base}\n\nKnown facts:\n{note}")))2223agent = create_agent(model, tools=TOOLS, middleware=[CaseFileMemory()],24 checkpointer=saver)state_schemaon the middleware addscase_fileto the agent state, so the checkpointer saves it per thread, like the messages.after_agentruns once per turn, after the final answer, and returns a state update.wrap_model_calladds the facts to the system message for this call only; nothing extra is stored in the message history.
Because the facts live in their own field, trimming or summarising the messages can never lose them.
Legacy: a custom chat message history
In 2024–2025 code, "custom memory" usually meant subclassing BaseChatMessageHistory and implementing three members: the messages property (read), add_messages (append) and clear (delete). It then plugged into RunnableWithMessageHistory. messages_to_dict and messages_from_dict were used to serialise, so tool calls survived the round trip. Know it for reading old code; the wrapper it served is deprecated since langchain-core 1.3.3.
A real-life example
A law firm's contract assistant must keep a record of every client conversation in the firm's own document-management system (DMS), for confidentiality and audit. The 2024 version did this with a BaseChatMessageHistory subclass that wrote each message to the DMS through its API.
When the firm rebuilt the assistant on create_agent, it kept a Postgres checkpointer on its own servers for working state, and added an after_agent middleware that writes each finished turn — question, answer and clause citations — to the matter's DMS record. A matter_id field in the state schema ties every turn and tool call to the right matter. The storage became standard, the firm-specific rule became about 25 lines of middleware, and the old history class was deleted.
Follow-up questions to expect
- "When would you write a custom checkpointer?" — Only when state must live in a database with no existing saver; you must implement get, list, put, put-writes and delete-thread correctly, sync and async.
- "What three methods did a custom chat history need?" —
messages,add_messagesandclear. - "Why not subclass
BaseMemory?" — It is the deprecated interface of the oldChainclasses, which now live inlangchain-classic; it fits neither LCEL nor LangGraph.