Sessions and Chat History
Purpose and Scope
Sessions and chat history are the persistence layer for conversational LangChain applications. A chat model consumes a sequence of messages and returns a message, so any application that wants continuity across turns needs a way to load prior messages, append the newest user and AI messages, and clear or rotate that state when a conversation ends. In LangChain core, the durable storage concern is intentionally separated from the model call. This lets the same chain, agent, or runnable work with in-memory storage during experiments and with Redis, files, databases, or service-backed thread state in production.
The repository exposes this concern at three levels. The lowest level is a history interface that defines how message lists are fetched, appended, and cleared. A lightweight session shape groups messages and function-call metadata when a loader or integration needs to represent a whole conversation. At the runnable layer, a wrapper binds history management around another runnable so application code can pass a session identifier through configuration rather than manually prepending messages on every call. These pieces make stateful chat a reusable concern instead of a model-provider-specific feature.
Sources: libs/core/langchain_core/chat_history.py, libs/core/langchain_core/chat_sessions.py, libs/core/langchain_core/runnables/history.py
Relevant Source Files
- libs/core/langchain_core/chat_history.py - Defines the abstract chat history contract, including synchronous and asynchronous message retrieval, bulk append, and clearing behavior.
- libs/core/langchain_core/chat_sessions.py - Defines the typed shape for a chat session as a group of messages plus optional function calling specifications.
- libs/core/langchain_core/runnables/history.py - Implements the runnable wrapper that reads and updates a chat history around another runnable using configurable session parameters.
Core Primitives
A chat history is a sequence of LangChain message objects representing prior turns in one conversation. The base interface imports and works with the shared message types, including human and AI messages, and documents that reading history may involve I/O. The implementation guidance favors bulk addition because a remote persistence layer may make one network or database round trip per write. That small design detail is important for production agents: appending a user message and an AI response together is cheaper and less error-prone than writing each one through separate helper calls.
A chat session is a broader grouping concept. The typed dictionary describes a single conversation, channel, or other group of messages, and it can also carry function calling specifications. That means session data can represent more than raw transcript text. It can preserve the structured context needed to replay or inspect tool-calling conversations, bridge imported chat logs, or pass grouped state between components. The session type is deliberately small, which keeps it compatible with many storage backends and service APIs while still naming the two pieces LangChain needs: messages and callable function metadata.
The runnable wrapper turns those storage primitives into a developer-facing execution pattern. It wraps another runnable and takes responsibility for reading the right message history before invocation and updating it afterward. By default, the wrapper expects a configurable session identifier, commonly passed as a configuration value, and uses that identifier to create or look up the corresponding history instance. The source documentation explicitly notes that production applications should use a persistent implementation, while in-memory history is mainly useful for experimentation and tests.
Sources: libs/core/langchain_core/chat_history.py, libs/core/langchain_core/chat_sessions.py, libs/core/langchain_core/runnables/history.py
System-to-Code Mapping
| Concept | Source-backed implementation | Developer meaning |
|---|---|---|
| Chat history store | BaseChatMessageHistory | Implement this when messages live in a file, database, cache, or external service. |
| Message list | messages property and aget_messages method | Read prior turns before composing the next model input. |
| Bulk append | add_messages and aadd_messages | Persist multiple new messages with fewer storage round trips. |
| Reset state | clear and aclear | Remove the conversation transcript for a session. |
| Session object | ChatSession | Represent a conversation, channel, or grouped message import. |
| Runnable wrapper | RunnableWithMessageHistory | Add state management to an existing runnable without rewriting the runnable itself. |
| Factory configuration | history_factory_config and configurable fields | Customize which runtime values select or construct the history. |
Execution Flow
A typical stateful chat flow begins before the model is called. The application receives a user input and invokes the history-aware runnable with configuration that identifies the conversation. The wrapper calls the history factory, obtains the matching history store, and loads the existing messages. It then adapts the incoming input into the message format expected by the wrapped runnable. After the wrapped runnable returns, the wrapper extracts the new input and output messages and appends them to the history. The caller receives the runnable result while persistence happens as part of the same logical turn.
This flow is especially useful when the runnable itself is reusable and stateless. For example, a prompt and chat model can be composed once, then invoked for many users or threads by changing only the session identifier in the runtime configuration. Official LangSmith-facing APIs describe adjacent concepts such as authorization sessions and thread history, where remote services expose session identifiers, checkpoints, and historical states. In the core Python package, the corresponding responsibility is narrower: define local interfaces and wrappers that application runtimes and integrations can connect to their chosen persistence systems.
The async design follows the same principle. The base history interface provides asynchronous variants for reading, adding, and clearing messages, but the default implementations can delegate to synchronous methods through an executor. This preserves compatibility with simple synchronous stores while allowing real asynchronous backends to override the methods for more efficient network I/O. When implementing a new history backend, prefer native asynchronous methods if the underlying client is asynchronous, and make sure synchronous fallbacks remain correct for tests, scripts, and environments that do not run an event loop.
Sources: libs/core/langchain_core/chat_history.py, libs/core/langchain_core/runnables/history.py
API Components
BaseChatMessageHistory is the contract to implement for a storage backend. It exposes a message list and methods for retrieving, adding, and clearing messages in both synchronous and asynchronous styles. The class documentation recommends overriding bulk operations where possible and treating older single-message helper methods as compatibility conveniences rather than the preferred path. A file-backed example in the source shows the expected pattern: read existing serialized messages, deserialize them into LangChain message objects, extend the list, serialize the combined transcript, and write the result back to storage.
RunnableWithMessageHistory is the main application integration point. It is a runnable binding, so it participates in the same invocation model as other runnable components. The wrapper supports inputs and outputs that may be message sequences or dictionaries containing messages, and it uses configurable field specifications when a history factory needs more than the default session identifier. This matters for multi-tenant applications where history selection might depend on a user identifier, organization, channel, region, or composite key rather than a single flat session string.
ChatSession is a small typed dictionary rather than an active storage class. Use it when code needs to pass around a loaded session snapshot, not when code needs to manage ongoing persistence. Its optional fields make it suitable for adapters that only have messages, as well as integrations that also load function calling specifications. Because it is defined in core and uses the shared base message type, it gives loaders and migration utilities a common shape without forcing them to depend on a particular database or runnable wrapper.
Sources: libs/core/langchain_core/chat_history.py, libs/core/langchain_core/chat_sessions.py, libs/core/langchain_core/runnables/history.py
Implementation Notes and Edge Cases
When implementing persistent history, treat message retrieval as potentially slow and failure-prone. The base documentation calls out that fetching messages may involve I/O, which means a production backend should handle missing sessions, empty transcripts, serialization failures, and concurrent writes deliberately. A new session should normally return an empty message list rather than raising an error. Clearing should leave the storage location in a valid empty state. If the backend supports transactions or optimistic locking, use those features to avoid losing turns when multiple requests update the same session.
Also decide where trimming, summarization, and retention policies belong. The history interface stores messages; it does not prescribe how many messages should be sent to a model or how old data should be retained. Applications can combine the wrapper with prompt placeholders, retrievers, or middleware that selects a bounded context window. Keeping this boundary clear prevents the persistence backend from silently changing model inputs, while still allowing production systems to enforce storage limits, privacy rules, or tenant-specific retention outside the runnable contract.
Example Pattern
from pydantic import BaseModel, Field
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.messages import BaseMessage
from langchain_core.runnables.history import RunnableWithMessageHistory
class InMemoryHistory(BaseChatMessageHistory, BaseModel):
messages: list[BaseMessage] = Field(default_factory=list)
def add_messages(self, messages: list[BaseMessage]) -> None:
self.messages.extend(messages)
def clear(self) -> None:
self.messages = []
store = {}
def get_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = InMemoryHistory()
return store[session_id]
# with_history = RunnableWithMessageHistory(chain, get_history)
# with_history.invoke(input_value, config={"configurable": {"session_id": "thread-1"}})This pattern mirrors the source examples while keeping the runnable independent from the persistence mechanism. In production, replace the dictionary with a backend that survives process restarts and supports the access pattern your application needs. The important invariant is that the factory returns the same logical history for the same configuration values, and that appended LangChain messages can later be returned in order. Once that is true, the rest of the application can reason in terms of sessions or threads without embedding storage calls in every chain step.
Next Steps
Use this page with the Messages page when you need to understand what is stored in history, and with the Runnables and LCEL page when you need to wrap a composed chain. For deployed agents, also compare this core abstraction with platform-level thread history, checkpoints, streaming, and observability pages. The practical next step is to start with an in-memory implementation for tests, then move the same interface to a persistent backend before exposing long-running sessions to users.