From 0cfa62633672f9fab19ea0ad11f2de30bd6a8ed4 Mon Sep 17 00:00:00 2001 From: Caspar Broekhuizen Date: Sun, 16 Nov 2025 20:59:36 -0800 Subject: [PATCH] docs: improve persistence docs first pass --- src/docs.json | 4 +- src/oss/langgraph/add-memory-old.mdx | 2142 ++++++++++++++++ src/oss/langgraph/add-memory.mdx | 3396 +++++++++++++++----------- src/oss/langgraph/memory.mdx | 274 --- src/oss/langgraph/persistence.mdx | 1257 ++-------- src/oss/langgraph/use-subgraphs.mdx | 181 +- 6 files changed, 4360 insertions(+), 2894 deletions(-) create mode 100644 src/oss/langgraph/add-memory-old.mdx delete mode 100644 src/oss/langgraph/memory.mdx diff --git a/src/docs.json b/src/docs.json index 944cd19c1b..e0d04492ea 100644 --- a/src/docs.json +++ b/src/docs.json @@ -222,11 +222,11 @@ "group": "Capabilities", "pages": [ "oss/python/langgraph/persistence", + "oss/python/langgraph/add-memory", "oss/python/langgraph/durable-execution", "oss/python/langgraph/streaming", "oss/python/langgraph/interrupts", "oss/python/langgraph/use-time-travel", - "oss/python/langgraph/add-memory", "oss/python/langgraph/use-subgraphs" ] }, @@ -557,11 +557,11 @@ "group": "Capabilities", "pages": [ "oss/javascript/langgraph/persistence", + "oss/javascript/langgraph/add-memory", "oss/javascript/langgraph/durable-execution", "oss/javascript/langgraph/streaming", "oss/javascript/langgraph/interrupts", "oss/javascript/langgraph/use-time-travel", - "oss/javascript/langgraph/add-memory", "oss/javascript/langgraph/use-subgraphs" ] }, diff --git a/src/oss/langgraph/add-memory-old.mdx b/src/oss/langgraph/add-memory-old.mdx new file mode 100644 index 0000000000..46ca3234bf --- /dev/null +++ b/src/oss/langgraph/add-memory-old.mdx @@ -0,0 +1,2142 @@ +--- +title: Memory +--- + + + +AI applications need [memory](/oss/concepts/memory) to share context across multiple interactions. In LangGraph, you can add two types of memory: + +* [Add short-term memory](#add-short-term-memory) as a part of your agent's [state](/oss/langgraph/graph-api#state) to enable multi-turn conversations. +* [Add long-term memory](#add-long-term-memory) to store user-specific or application-level data across sessions. + +## Add short-term memory + +**Short-term** memory (thread-level [persistence](/oss/langgraph/persistence)) enables agents to track multi-turn conversations. To add short-term memory: + +:::python +```python +from langgraph.checkpoint.memory import InMemorySaver # [!code highlight] +from langgraph.graph import StateGraph + +checkpointer = InMemorySaver() # [!code highlight] + +builder = StateGraph(...) +graph = builder.compile(checkpointer=checkpointer) # [!code highlight] + +graph.invoke( + {"messages": [{"role": "user", "content": "hi! i am Bob"}]}, + {"configurable": {"thread_id": "1"}}, # [!code highlight] +) +``` +::: + +:::js +```typescript +import { MemorySaver, StateGraph } from "@langchain/langgraph"; + +const checkpointer = new MemorySaver(); + +const builder = new StateGraph(...); +const graph = builder.compile({ checkpointer }); + +await graph.invoke( + { messages: [{ role: "user", content: "hi! i am Bob" }] }, + { configurable: { thread_id: "1" } } +); +``` +::: + +### Use in production + +In production, use a checkpointer backed by a database: + +:::python +```python +from langgraph.checkpoint.postgres import PostgresSaver + +DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" +with PostgresSaver.from_conn_string(DB_URI) as checkpointer: # [!code highlight] + builder = StateGraph(...) + graph = builder.compile(checkpointer=checkpointer) # [!code highlight] +``` +::: + +:::js +```typescript +import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres"; + +const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"; +const checkpointer = PostgresSaver.fromConnString(DB_URI); + +const builder = new StateGraph(...); +const graph = builder.compile({ checkpointer }); +``` +::: + + + :::python + ``` + pip install -U "psycopg[binary,pool]" langgraph langgraph-checkpoint-postgres + ``` + + + You need to call `checkpointer.setup()` the first time you're using Postgres checkpointer + + + + + ```python + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.checkpoint.postgres import PostgresSaver # [!code highlight] + + model = init_chat_model(model="claude-haiku-4-5-20251001") + + DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" + with PostgresSaver.from_conn_string(DB_URI) as checkpointer: # [!code highlight] + # checkpointer.setup() + + def call_model(state: MessagesState): + response = model.invoke(state["messages"]) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + graph = builder.compile(checkpointer=checkpointer) # [!code highlight] + + config = { + "configurable": { + "thread_id": "1" # [!code highlight] + } + } + + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, + config, # [!code highlight] + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() + + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "what's my name?"}]}, + config, # [!code highlight] + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() +``` + + + ```python + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver # [!code highlight] + + model = init_chat_model(model="claude-haiku-4-5-20251001") + + DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" + async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer: # [!code highlight] + # await checkpointer.setup() + + async def call_model(state: MessagesState): + response = await model.ainvoke(state["messages"]) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + graph = builder.compile(checkpointer=checkpointer) # [!code highlight] + + config = { + "configurable": { + "thread_id": "1" # [!code highlight] + } + } + + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, + config, # [!code highlight] + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() + + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "what's my name?"}]}, + config, # [!code highlight] + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() +``` + + + ::: + + :::js + ``` + npm install @langchain/langgraph-checkpoint-postgres + ``` + + + You need to call `checkpointer.setup()` the first time you're using Postgres checkpointer + + + ```typescript + import { ChatAnthropic } from "@langchain/anthropic"; + import { StateGraph, MessagesZodMeta, START } from "@langchain/langgraph"; + import { BaseMessage } from "@langchain/core/messages"; + import { registry } from "@langchain/langgraph/zod"; + import * as z from "zod"; + import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres"; + + const MessagesZodState = z.object({ + messages: z + .array(z.custom()) + .register(registry, MessagesZodMeta), + }); + + const model = new ChatAnthropic({ model: "claude-haiku-4-5-20251001" }); + + const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"; + const checkpointer = PostgresSaver.fromConnString(DB_URI); + // await checkpointer.setup(); + + const builder = new StateGraph(MessagesZodState) + .addNode("call_model", async (state) => { + const response = await model.invoke(state.messages); + return { messages: [response] }; + }) + .addEdge(START, "call_model"); + + const graph = builder.compile({ checkpointer }); + + const config = { + configurable: { + thread_id: "1" + } + }; + + for await (const chunk of await graph.stream( + { messages: [{ role: "user", content: "hi! I'm bob" }] }, + { ...config, streamMode: "values" } + )) { + console.log(chunk.messages.at(-1)?.content); + } + + for await (const chunk of await graph.stream( + { messages: [{ role: "user", content: "what's my name?" }] }, + { ...config, streamMode: "values" } + )) { + console.log(chunk.messages.at(-1)?.content); + } + ``` + ::: + + +:::python + + ``` + pip install -U pymongo langgraph langgraph-checkpoint-mongodb + ``` + + + **Setup** + To use the MongoDB checkpointer, you will need a MongoDB cluster. Follow [this guide](https://www.mongodb.com/docs/guides/atlas/cluster/) to create a cluster if you don't already have one. + + + + + ```python + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.checkpoint.mongodb import MongoDBSaver # [!code highlight] + + model = init_chat_model(model="claude-haiku-4-5-20251001") + + DB_URI = "localhost:27017" + with MongoDBSaver.from_conn_string(DB_URI) as checkpointer: # [!code highlight] + + def call_model(state: MessagesState): + response = model.invoke(state["messages"]) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + graph = builder.compile(checkpointer=checkpointer) # [!code highlight] + + config = { + "configurable": { + "thread_id": "1" # [!code highlight] + } + } + + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, + config, # [!code highlight] + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() + + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "what's my name?"}]}, + config, # [!code highlight] + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() +``` + + + ```python + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.checkpoint.mongodb.aio import AsyncMongoDBSaver # [!code highlight] + + model = init_chat_model(model="claude-haiku-4-5-20251001") + + DB_URI = "localhost:27017" + async with AsyncMongoDBSaver.from_conn_string(DB_URI) as checkpointer: # [!code highlight] + + async def call_model(state: MessagesState): + response = await model.ainvoke(state["messages"]) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + graph = builder.compile(checkpointer=checkpointer) # [!code highlight] + + config = { + "configurable": { + "thread_id": "1" # [!code highlight] + } + } + + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, + config, # [!code highlight] + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() + + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "what's my name?"}]}, + config, # [!code highlight] + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() +``` + + + + + + ``` + pip install -U langgraph langgraph-checkpoint-redis + ``` + + + You need to call `checkpointer.setup()` the first time you're using Redis checkpointer + + + + + ```python + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.checkpoint.redis import RedisSaver # [!code highlight] + + model = init_chat_model(model="claude-haiku-4-5-20251001") + + DB_URI = "redis://localhost:6379" + with RedisSaver.from_conn_string(DB_URI) as checkpointer: # [!code highlight] + # checkpointer.setup() + + def call_model(state: MessagesState): + response = model.invoke(state["messages"]) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + graph = builder.compile(checkpointer=checkpointer) # [!code highlight] + + config = { + "configurable": { + "thread_id": "1" # [!code highlight] + } + } + + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, + config, # [!code highlight] + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() + + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "what's my name?"}]}, + config, # [!code highlight] + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() +``` + + + ```python + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.checkpoint.redis.aio import AsyncRedisSaver # [!code highlight] + + model = init_chat_model(model="claude-haiku-4-5-20251001") + + DB_URI = "redis://localhost:6379" + async with AsyncRedisSaver.from_conn_string(DB_URI) as checkpointer: # [!code highlight] + # await checkpointer.asetup() + + async def call_model(state: MessagesState): + response = await model.ainvoke(state["messages"]) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + graph = builder.compile(checkpointer=checkpointer) # [!code highlight] + + config = { + "configurable": { + "thread_id": "1" # [!code highlight] + } + } + + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, + config, # [!code highlight] + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() + + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "what's my name?"}]}, + config, # [!code highlight] + stream_mode="values" + ): + chunk["messages"][-1].pretty_print() +``` + + + +::: + +### Use in subgraphs + +If your graph contains [subgraphs](/oss/langgraph/use-subgraphs), you only need to provide the checkpointer when compiling the parent graph. LangGraph will automatically propagate the checkpointer to the child subgraphs. + +:::python +```python +from langgraph.graph import START, StateGraph +from langgraph.checkpoint.memory import InMemorySaver +from typing import TypedDict + +class State(TypedDict): + foo: str + +# Subgraph + +def subgraph_node_1(state: State): + return {"foo": state["foo"] + "bar"} + +subgraph_builder = StateGraph(State) +subgraph_builder.add_node(subgraph_node_1) +subgraph_builder.add_edge(START, "subgraph_node_1") +subgraph = subgraph_builder.compile() # [!code highlight] + +# Parent graph + +builder = StateGraph(State) +builder.add_node("node_1", subgraph) # [!code highlight] +builder.add_edge(START, "node_1") + +checkpointer = InMemorySaver() +graph = builder.compile(checkpointer=checkpointer) # [!code highlight] +``` +::: + +:::js +```typescript +import { StateGraph, START, MemorySaver } from "@langchain/langgraph"; +import * as z from "zod"; + +const State = z.object({ foo: z.string() }); + +const subgraphBuilder = new StateGraph(State) + .addNode("subgraph_node_1", (state) => { + return { foo: state.foo + "bar" }; + }) + .addEdge(START, "subgraph_node_1"); +const subgraph = subgraphBuilder.compile(); + +const builder = new StateGraph(State) + .addNode("node_1", subgraph) + .addEdge(START, "node_1"); + +const checkpointer = new MemorySaver(); +const graph = builder.compile({ checkpointer }); +``` +::: + +If you want the subgraph to have its own memory, you can compile it with the appropriate checkpointer option. This is useful in [multi-agent](/oss/langchain/multi-agent) systems, if you want agents to keep track of their internal message histories. + +:::python +```python +subgraph_builder = StateGraph(...) +subgraph = subgraph_builder.compile(checkpointer=True) # [!code highlight] +``` +::: + +:::js +```typescript +const subgraphBuilder = new StateGraph(...); +const subgraph = subgraphBuilder.compile({ checkpointer: true }); // [!code highlight] +``` +::: + +## Add long-term memory + +Use long-term memory to store user-specific or application-specific data across conversations. + +:::python +```python +from langgraph.store.memory import InMemoryStore # [!code highlight] +from langgraph.graph import StateGraph + +store = InMemoryStore() # [!code highlight] + +builder = StateGraph(...) +graph = builder.compile(store=store) # [!code highlight] +``` +::: + +:::js +```typescript +import { InMemoryStore, StateGraph } from "@langchain/langgraph"; + +const store = new InMemoryStore(); + +const builder = new StateGraph(...); +const graph = builder.compile({ store }); +``` +::: + +### Use in production + +In production, use a store backed by a database: + +:::python +```python +from langgraph.store.postgres import PostgresStore + +DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" +with PostgresStore.from_conn_string(DB_URI) as store: # [!code highlight] + builder = StateGraph(...) + graph = builder.compile(store=store) # [!code highlight] +``` +::: + +:::js +```typescript +import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres/store"; + +const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"; +const store = PostgresStore.fromConnString(DB_URI); + +const builder = new StateGraph(...); +const graph = builder.compile({ store }); +``` +::: + + + :::python + ``` + pip install -U "psycopg[binary,pool]" langgraph langgraph-checkpoint-postgres + ``` + + + You need to call `store.setup()` the first time you're using Postgres store + + + + + ```python + from langchain_core.runnables import RunnableConfig + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.checkpoint.postgres import PostgresSaver + from langgraph.store.postgres import PostgresStore # [!code highlight] + from langgraph.store.base import BaseStore + + model = init_chat_model(model="claude-haiku-4-5-20251001") + + DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" + + with ( + PostgresStore.from_conn_string(DB_URI) as store, # [!code highlight] + PostgresSaver.from_conn_string(DB_URI) as checkpointer, + ): + # store.setup() + # checkpointer.setup() + + def call_model( + state: MessagesState, + config: RunnableConfig, + *, + store: BaseStore, # [!code highlight] + ): + user_id = config["configurable"]["user_id"] + namespace = ("memories", user_id) + memories = store.search(namespace, query=str(state["messages"][-1].content)) # [!code highlight] + info = "\n".join([d.value["data"] for d in memories]) + system_msg = f"You are a helpful assistant talking to the user. User info: {info}" + + # Store new memories if the user asks the model to remember + last_message = state["messages"][-1] + if "remember" in last_message.content.lower(): + memory = "User name is Bob" + store.put(namespace, str(uuid.uuid4()), {"data": memory}) # [!code highlight] + + response = model.invoke( + [{"role": "system", "content": system_msg}] + state["messages"] + ) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + graph = builder.compile( + checkpointer=checkpointer, + store=store, # [!code highlight] + ) + + config = { + "configurable": { + "thread_id": "1", # [!code highlight] + "user_id": "1", # [!code highlight] + } + } + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, + config, # [!code highlight] + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() + + config = { + "configurable": { + "thread_id": "2", # [!code highlight] + "user_id": "1", + } + } + + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "what is my name?"}]}, + config, # [!code highlight] + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() +``` + + + ```python + from langchain_core.runnables import RunnableConfig + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver + from langgraph.store.postgres.aio import AsyncPostgresStore # [!code highlight] + from langgraph.store.base import BaseStore + + model = init_chat_model(model="claude-haiku-4-5-20251001") + + DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" + + async with ( + AsyncPostgresStore.from_conn_string(DB_URI) as store, # [!code highlight] + AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer, + ): + # await store.setup() + # await checkpointer.setup() + + async def call_model( + state: MessagesState, + config: RunnableConfig, + *, + store: BaseStore, # [!code highlight] + ): + user_id = config["configurable"]["user_id"] + namespace = ("memories", user_id) + memories = await store.asearch(namespace, query=str(state["messages"][-1].content)) # [!code highlight] + info = "\n".join([d.value["data"] for d in memories]) + system_msg = f"You are a helpful assistant talking to the user. User info: {info}" + + # Store new memories if the user asks the model to remember + last_message = state["messages"][-1] + if "remember" in last_message.content.lower(): + memory = "User name is Bob" + await store.aput(namespace, str(uuid.uuid4()), {"data": memory}) # [!code highlight] + + response = await model.ainvoke( + [{"role": "system", "content": system_msg}] + state["messages"] + ) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + graph = builder.compile( + checkpointer=checkpointer, + store=store, # [!code highlight] + ) + + config = { + "configurable": { + "thread_id": "1", # [!code highlight] + "user_id": "1", # [!code highlight] + } + } + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, + config, # [!code highlight] + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() + + config = { + "configurable": { + "thread_id": "2", # [!code highlight] + "user_id": "1", + } + } + + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "what is my name?"}]}, + config, # [!code highlight] + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() +``` + + + ::: + + :::js + ``` + npm install @langchain/langgraph-checkpoint-postgres + ``` + + + You need to call `store.setup()` the first time you're using Postgres store + + + ```typescript + import { ChatAnthropic } from "@langchain/anthropic"; + import { StateGraph, MessagesZodMeta, START, LangGraphRunnableConfig } from "@langchain/langgraph"; + import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres"; + import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres/store"; + import { BaseMessage } from "@langchain/core/messages"; + import { registry } from "@langchain/langgraph/zod"; + import * as z from "zod"; + import { v4 as uuidv4 } from "uuid"; + + const MessagesZodState = z.object({ + messages: z + .array(z.custom()) + .register(registry, MessagesZodMeta), + }); + + const model = new ChatAnthropic({ model: "claude-haiku-4-5-20251001" }); + + const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"; + + const store = PostgresStore.fromConnString(DB_URI); + const checkpointer = PostgresSaver.fromConnString(DB_URI); + // await store.setup(); + // await checkpointer.setup(); + + const callModel = async ( + state: z.infer, + config: LangGraphRunnableConfig, + ) => { + const userId = config.configurable?.userId; + const namespace = ["memories", userId]; + const memories = await config.store?.search(namespace, { query: state.messages.at(-1)?.content }); + const info = memories?.map(d => d.value.data).join("\n") || ""; + const systemMsg = `You are a helpful assistant talking to the user. User info: ${info}`; + + // Store new memories if the user asks the model to remember + const lastMessage = state.messages.at(-1); + if (lastMessage?.content?.toLowerCase().includes("remember")) { + const memory = "User name is Bob"; + await config.store?.put(namespace, uuidv4(), { data: memory }); + } + + const response = await model.invoke([ + { role: "system", content: systemMsg }, + ...state.messages + ]); + return { messages: [response] }; + }; + + const builder = new StateGraph(MessagesZodState) + .addNode("call_model", callModel) + .addEdge(START, "call_model"); + + const graph = builder.compile({ + checkpointer, + store, + }); + + const config = { + configurable: { + thread_id: "1", + userId: "1", + } + }; + + for await (const chunk of await graph.stream( + { messages: [{ role: "user", content: "Hi! Remember: my name is Bob" }] }, + { ...config, streamMode: "values" } + )) { + console.log(chunk.messages.at(-1)?.content); + } + + const config2 = { + configurable: { + thread_id: "2", + userId: "1", + } + }; + + for await (const chunk of await graph.stream( + { messages: [{ role: "user", content: "what is my name?" }] }, + { ...config2, streamMode: "values" } + )) { + console.log(chunk.messages.at(-1)?.content); + } + ``` + ::: + + +:::python + + ``` + pip install -U langgraph langgraph-checkpoint-redis + ``` + + + You need to call `store.setup()` the first time you're using Redis store + + + + + ```python + from langchain_core.runnables import RunnableConfig + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.checkpoint.redis import RedisSaver + from langgraph.store.redis import RedisStore # [!code highlight] + from langgraph.store.base import BaseStore + + model = init_chat_model(model="claude-haiku-4-5-20251001") + + DB_URI = "redis://localhost:6379" + + with ( + RedisStore.from_conn_string(DB_URI) as store, # [!code highlight] + RedisSaver.from_conn_string(DB_URI) as checkpointer, + ): + store.setup() + checkpointer.setup() + + def call_model( + state: MessagesState, + config: RunnableConfig, + *, + store: BaseStore, # [!code highlight] + ): + user_id = config["configurable"]["user_id"] + namespace = ("memories", user_id) + memories = store.search(namespace, query=str(state["messages"][-1].content)) # [!code highlight] + info = "\n".join([d.value["data"] for d in memories]) + system_msg = f"You are a helpful assistant talking to the user. User info: {info}" + + # Store new memories if the user asks the model to remember + last_message = state["messages"][-1] + if "remember" in last_message.content.lower(): + memory = "User name is Bob" + store.put(namespace, str(uuid.uuid4()), {"data": memory}) # [!code highlight] + + response = model.invoke( + [{"role": "system", "content": system_msg}] + state["messages"] + ) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + graph = builder.compile( + checkpointer=checkpointer, + store=store, # [!code highlight] + ) + + config = { + "configurable": { + "thread_id": "1", # [!code highlight] + "user_id": "1", # [!code highlight] + } + } + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, + config, # [!code highlight] + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() + + config = { + "configurable": { + "thread_id": "2", # [!code highlight] + "user_id": "1", + } + } + + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "what is my name?"}]}, + config, # [!code highlight] + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() +``` + + + ```python + from langchain_core.runnables import RunnableConfig + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.checkpoint.redis.aio import AsyncRedisSaver + from langgraph.store.redis.aio import AsyncRedisStore # [!code highlight] + from langgraph.store.base import BaseStore + + model = init_chat_model(model="claude-haiku-4-5-20251001") + + DB_URI = "redis://localhost:6379" + + async with ( + AsyncRedisStore.from_conn_string(DB_URI) as store, # [!code highlight] + AsyncRedisSaver.from_conn_string(DB_URI) as checkpointer, + ): + # await store.setup() + # await checkpointer.asetup() + + async def call_model( + state: MessagesState, + config: RunnableConfig, + *, + store: BaseStore, # [!code highlight] + ): + user_id = config["configurable"]["user_id"] + namespace = ("memories", user_id) + memories = await store.asearch(namespace, query=str(state["messages"][-1].content)) # [!code highlight] + info = "\n".join([d.value["data"] for d in memories]) + system_msg = f"You are a helpful assistant talking to the user. User info: {info}" + + # Store new memories if the user asks the model to remember + last_message = state["messages"][-1] + if "remember" in last_message.content.lower(): + memory = "User name is Bob" + await store.aput(namespace, str(uuid.uuid4()), {"data": memory}) # [!code highlight] + + response = await model.ainvoke( + [{"role": "system", "content": system_msg}] + state["messages"] + ) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + graph = builder.compile( + checkpointer=checkpointer, + store=store, # [!code highlight] + ) + + config = { + "configurable": { + "thread_id": "1", # [!code highlight] + "user_id": "1", # [!code highlight] + } + } + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, + config, # [!code highlight] + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() + + config = { + "configurable": { + "thread_id": "2", # [!code highlight] + "user_id": "1", + } + } + + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "what is my name?"}]}, + config, # [!code highlight] + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() +``` + + + +::: + +### Use semantic search + +Enable semantic search in your graph's memory store to let graph agents search for items in the store by semantic similarity. + +:::python +```python +from langchain.embeddings import init_embeddings +from langgraph.store.memory import InMemoryStore + +# Create store with semantic search enabled +embeddings = init_embeddings("openai:text-embedding-3-small") +store = InMemoryStore( + index={ + "embed": embeddings, + "dims": 1536, + } +) + +store.put(("user_123", "memories"), "1", {"text": "I love pizza"}) +store.put(("user_123", "memories"), "2", {"text": "I am a plumber"}) + +items = store.search( + ("user_123", "memories"), query="I'm hungry", limit=1 +) +``` +::: + +:::js +```typescript +import { OpenAIEmbeddings } from "@langchain/openai"; +import { InMemoryStore } from "@langchain/langgraph"; + +// Create store with semantic search enabled +const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small" }); +const store = new InMemoryStore({ + index: { + embeddings, + dims: 1536, + }, +}); + +await store.put(["user_123", "memories"], "1", { text: "I love pizza" }); +await store.put(["user_123", "memories"], "2", { text: "I am a plumber" }); + +const items = await store.search(["user_123", "memories"], { + query: "I'm hungry", + limit: 1, +}); +``` +::: + + + :::python + + ```python + + from langchain.embeddings import init_embeddings + from langchain.chat_models import init_chat_model + from langgraph.store.base import BaseStore + from langgraph.store.memory import InMemoryStore + from langgraph.graph import START, MessagesState, StateGraph + + model = init_chat_model("gpt-4o-mini") + + # Create store with semantic search enabled + embeddings = init_embeddings("openai:text-embedding-3-small") + store = InMemoryStore( + index={ + "embed": embeddings, + "dims": 1536, + } + ) + + store.put(("user_123", "memories"), "1", {"text": "I love pizza"}) + store.put(("user_123", "memories"), "2", {"text": "I am a plumber"}) + + def chat(state, *, store: BaseStore): + # Search based on user's last message + items = store.search( + ("user_123", "memories"), query=state["messages"][-1].content, limit=2 + ) + memories = "\n".join(item.value["text"] for item in items) + memories = f"## Memories of user\n{memories}" if memories else "" + response = model.invoke( + [ + {"role": "system", "content": f"You are a helpful assistant.\n{memories}"}, + *state["messages"], + ] + ) + return {"messages": [response]} + + + builder = StateGraph(MessagesState) + builder.add_node(chat) + builder.add_edge(START, "chat") + graph = builder.compile(store=store) + + for message, metadata in graph.stream( + input={"messages": [{"role": "user", "content": "I'm hungry"}]}, + stream_mode="messages", + ): + print(message.content, end="") + ``` + ::: + + :::js + + ```typescript + import { OpenAIEmbeddings, ChatOpenAI } from "@langchain/openai"; + import { StateGraph, START, MessagesZodMeta, InMemoryStore } from "@langchain/langgraph"; + import { BaseMessage } from "@langchain/core/messages"; + import { registry } from "@langchain/langgraph/zod"; + import * as z from "zod"; + + const MessagesZodState = z.object({ + messages: z + .array(z.custom()) + .register(registry, MessagesZodMeta), + }); + + const model = new ChatOpenAI({ model: "gpt-4o-mini" }); + + // Create store with semantic search enabled + const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small" }); + const store = new InMemoryStore({ + index: { + embeddings, + dims: 1536, + } + }); + + await store.put(["user_123", "memories"], "1", { text: "I love pizza" }); + await store.put(["user_123", "memories"], "2", { text: "I am a plumber" }); + + const chat = async (state: z.infer, config) => { + // Search based on user's last message + const items = await config.store.search( + ["user_123", "memories"], + { query: state.messages.at(-1)?.content, limit: 2 } + ); + const memories = items.map(item => item.value.text).join("\n"); + const memoriesText = memories ? `## Memories of user\n${memories}` : ""; + + const response = await model.invoke([ + { role: "system", content: `You are a helpful assistant.\n${memoriesText}` }, + ...state.messages, + ]); + + return { messages: [response] }; + }; + + const builder = new StateGraph(MessagesZodState) + .addNode("chat", chat) + .addEdge(START, "chat"); + const graph = builder.compile({ store }); + + for await (const [message, metadata] of await graph.stream( + { messages: [{ role: "user", content: "I'm hungry" }] }, + { streamMode: "messages" } + )) { + if (message.content) { + console.log(message.content); + } + } + ``` + ::: + + +## Manage short-term memory + +With [short-term memory](#add-short-term-memory) enabled, long conversations can exceed the LLM's context window. Common solutions are: + +* [Trim messages](#trim-messages): Remove first or last N messages (before calling LLM) +* [Delete messages](#delete-messages) from LangGraph state permanently +* [Summarize messages](#summarize-messages): Summarize earlier messages in the history and replace them with a summary +* [Manage checkpoints](#manage-checkpoints) to store and retrieve message history +* Custom strategies (e.g., message filtering, etc.) + +This allows the agent to keep track of the conversation without exceeding the LLM's context window. + +### Trim messages + +:::python +Most LLMs have a maximum supported context window (denominated in tokens). One way to decide when to truncate messages is to count the tokens in the message history and truncate whenever it approaches that limit. If you're using LangChain, you can use the trim messages utility and specify the number of tokens to keep from the list, as well as the `strategy` (e.g., keep the last `max_tokens`) to use for handling the boundary. +::: +:::js +Most LLMs have a maximum supported context window (denominated in tokens). One way to decide when to truncate messages is to count the tokens in the message history and truncate whenever it approaches that limit. If you're using LangChain, you can use the trim messages utility and specify the number of tokens to keep from the list, as well as the `strategy` (e.g., keep the last `maxTokens`) to use for handling the boundary. +::: + +:::python +To trim message history, use the [`trim_messages`](https://python.langchain.com/api_reference/core/messages/langchain_core.messages.utils.trim_messages.html) function: + +```python +from langchain_core.messages.utils import ( # [!code highlight] + trim_messages, # [!code highlight] + count_tokens_approximately # [!code highlight] +) # [!code highlight] + +def call_model(state: MessagesState): + messages = trim_messages( # [!code highlight] + state["messages"], + strategy="last", + token_counter=count_tokens_approximately, + max_tokens=128, + start_on="human", + end_on=("human", "tool"), + ) + response = model.invoke(messages) + return {"messages": [response]} + +builder = StateGraph(MessagesState) +builder.add_node(call_model) +... +``` +::: + +:::js +To trim message history, use the [`trimMessages`](https://js.langchain.com/docs/how_to/trim_messages/) function: + +```typescript +import { trimMessages } from "@langchain/core/messages"; + +const callModel = async (state: z.infer) => { + const messages = trimMessages(state.messages, { + strategy: "last", + maxTokens: 128, + startOn: "human", + endOn: ["human", "tool"], + }); + const response = await model.invoke(messages); + return { messages: [response] }; +}; + +const builder = new StateGraph(MessagesZodState) + .addNode("call_model", callModel); +// ... +``` +::: + + + :::python + ```python + from langchain_core.messages.utils import ( + trim_messages, # [!code highlight] + count_tokens_approximately # [!code highlight] + ) + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, START, MessagesState + + model = init_chat_model("claude-sonnet-4-5-20250929") + summarization_model = model.bind(max_tokens=128) + + def call_model(state: MessagesState): + messages = trim_messages( # [!code highlight] + state["messages"], + strategy="last", + token_counter=count_tokens_approximately, + max_tokens=128, + start_on="human", + end_on=("human", "tool"), + ) + response = model.invoke(messages) + return {"messages": [response]} + + checkpointer = InMemorySaver() + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + graph = builder.compile(checkpointer=checkpointer) + + config = {"configurable": {"thread_id": "1"}} + graph.invoke({"messages": "hi, my name is bob"}, config) + graph.invoke({"messages": "write a short poem about cats"}, config) + graph.invoke({"messages": "now do the same but for dogs"}, config) + final_response = graph.invoke({"messages": "what's my name?"}, config) + + final_response["messages"][-1].pretty_print() +``` + + ``` + ================================== Ai Message ================================== + + Your name is Bob, as you mentioned when you first introduced yourself. + ``` + ::: + + :::js + ```typescript + import { trimMessages, BaseMessage } from "@langchain/core/messages"; + import { ChatAnthropic } from "@langchain/anthropic"; + import { StateGraph, START, MessagesZodMeta, MemorySaver } from "@langchain/langgraph"; + import { registry } from "@langchain/langgraph/zod"; + import * as z from "zod"; + + const MessagesZodState = z.object({ + messages: z + .array(z.custom()) + .register(registry, MessagesZodMeta), + }); + + const model = new ChatAnthropic({ model: "claude-3-5-sonnet-20241022" }); + + const callModel = async (state: z.infer) => { + const messages = trimMessages(state.messages, { + strategy: "last", + maxTokens: 128, + startOn: "human", + endOn: ["human", "tool"], + tokenCounter: model, + }); + const response = await model.invoke(messages); + return { messages: [response] }; + }; + + const checkpointer = new MemorySaver(); + const builder = new StateGraph(MessagesZodState) + .addNode("call_model", callModel) + .addEdge(START, "call_model"); + const graph = builder.compile({ checkpointer }); + + const config = { configurable: { thread_id: "1" } }; + await graph.invoke({ messages: [{ role: "user", content: "hi, my name is bob" }] }, config); + await graph.invoke({ messages: [{ role: "user", content: "write a short poem about cats" }] }, config); + await graph.invoke({ messages: [{ role: "user", content: "now do the same but for dogs" }] }, config); + const finalResponse = await graph.invoke({ messages: [{ role: "user", content: "what's my name?" }] }, config); + + console.log(finalResponse.messages.at(-1)?.content); + ``` + + ``` + Your name is Bob, as you mentioned when you first introduced yourself. + ``` + ::: + + +### Delete messages + +You can delete messages from the graph state to manage the message history. This is useful when you want to remove specific messages or clear the entire message history. + +:::python +To delete messages from the graph state, you can use the `RemoveMessage`. For `RemoveMessage` to work, you need to use a state key with @[`add_messages`] [reducer](/oss/langgraph/graph-api#reducers), like [`MessagesState`](/oss/langgraph/graph-api#messagesstate). + +To remove specific messages: + +```python +from langchain.messages import RemoveMessage # [!code highlight] + +def delete_messages(state): + messages = state["messages"] + if len(messages) > 2: + # remove the earliest two messages + return {"messages": [RemoveMessage(id=m.id) for m in messages[:2]]} # [!code highlight] +``` + +To remove **all** messages: + +```python +from langgraph.graph.message import REMOVE_ALL_MESSAGES # [!code highlight] + +def delete_messages(state): + return {"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES)]} # [!code highlight] +``` +::: + +:::js +To delete messages from the graph state, you can use the `RemoveMessage`. For `RemoveMessage` to work, you need to use a state key with @[`messagesStateReducer`][messagesStateReducer] [reducer](/oss/langgraph/graph-api#reducers), like `MessagesZodState`. + +To remove specific messages: + +```typescript +import { RemoveMessage } from "@langchain/core/messages"; + +const deleteMessages = (state) => { + const messages = state.messages; + if (messages.length > 2) { + // remove the earliest two messages + return { + messages: messages + .slice(0, 2) + .map((m) => new RemoveMessage({ id: m.id })), + }; + } +}; +``` +::: + + +When deleting messages, **make sure** that the resulting message history is valid. Check the limitations of the LLM provider you're using. For example: + +* Some providers expect message history to start with a `user` message +* Most providers require `assistant` messages with tool calls to be followed by corresponding `tool` result messages. + + + + :::python + ```python + from langchain.messages import RemoveMessage # [!code highlight] + + def delete_messages(state): + messages = state["messages"] + if len(messages) > 2: + # remove the earliest two messages + return {"messages": [RemoveMessage(id=m.id) for m in messages[:2]]} # [!code highlight] + + def call_model(state: MessagesState): + response = model.invoke(state["messages"]) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_sequence([call_model, delete_messages]) + builder.add_edge(START, "call_model") + + checkpointer = InMemorySaver() + app = builder.compile(checkpointer=checkpointer) + + for event in app.stream( + {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, + config, + stream_mode="values" + ): + print([(message.type, message.content) for message in event["messages"]]) + + for event in app.stream( + {"messages": [{"role": "user", "content": "what's my name?"}]}, + config, + stream_mode="values" + ): + print([(message.type, message.content) for message in event["messages"]]) +``` + + ``` + [('human', "hi! I'm bob")] + [('human', "hi! I'm bob"), ('ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?')] + [('human', "hi! I'm bob"), ('ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?'), ('human', "what's my name?")] + [('human', "hi! I'm bob"), ('ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?'), ('human', "what's my name?"), ('ai', 'Your name is Bob.')] + [('human', "what's my name?"), ('ai', 'Your name is Bob.')] + ``` + ::: + + :::js + ```typescript + import { RemoveMessage, BaseMessage } from "@langchain/core/messages"; + import { ChatAnthropic } from "@langchain/anthropic"; + import { StateGraph, START, MemorySaver, MessagesZodMeta } from "@langchain/langgraph"; + import * as z from "zod"; + import { registry } from "@langchain/langgraph/zod"; + + const MessagesZodState = z.object({ + messages: z + .array(z.custom()) + .register(registry, MessagesZodMeta), + }); + + const model = new ChatAnthropic({ model: "claude-3-5-sonnet-20241022" }); + + const deleteMessages = (state: z.infer) => { + const messages = state.messages; + if (messages.length > 2) { + // remove the earliest two messages + return { messages: messages.slice(0, 2).map(m => new RemoveMessage({ id: m.id })) }; + } + return {}; + }; + + const callModel = async (state: z.infer) => { + const response = await model.invoke(state.messages); + return { messages: [response] }; + }; + + const builder = new StateGraph(MessagesZodState) + .addNode("call_model", callModel) + .addNode("delete_messages", deleteMessages) + .addEdge(START, "call_model") + .addEdge("call_model", "delete_messages"); + + const checkpointer = new MemorySaver(); + const app = builder.compile({ checkpointer }); + + const config = { configurable: { thread_id: "1" } }; + + for await (const event of await app.stream( + { messages: [{ role: "user", content: "hi! I'm bob" }] }, + { ...config, streamMode: "values" } + )) { + console.log(event.messages.map(message => [message.getType(), message.content])); + } + + for await (const event of await app.stream( + { messages: [{ role: "user", content: "what's my name?" }] }, + { ...config, streamMode: "values" } + )) { + console.log(event.messages.map(message => [message.getType(), message.content])); + } + ``` + + ``` + [['human', "hi! I'm bob"]] + [['human', "hi! I'm bob"], ['ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?']] + [['human', "hi! I'm bob"], ['ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?'], ['human', "what's my name?"]] + [['human', "hi! I'm bob"], ['ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?'], ['human', "what's my name?"], ['ai', 'Your name is Bob.']] + [['human', "what's my name?"], ['ai', 'Your name is Bob.']] + ``` + ::: + + +### Summarize messages + +The problem with trimming or removing messages, as shown above, is that you may lose information from culling of the message queue. Because of this, some applications benefit from a more sophisticated approach of summarizing the message history using a chat model. + +![](/oss/images/summary.png) + + +:::python +Prompting and orchestration logic can be used to summarize the message history. For example, in LangGraph you can extend the [`MessagesState`](/oss/langgraph/graph-api#working-with-messages-in-graph-state) to include a `summary` key: + +```python +from langgraph.graph import MessagesState +class State(MessagesState): + summary: str +``` + +Then, you can generate a summary of the chat history, using any existing summary as context for the next summary. This `summarize_conversation` node can be called after some number of messages have accumulated in the `messages` state key. + +```python +def summarize_conversation(state: State): + + # First, we get any existing summary + summary = state.get("summary", "") + + # Create our summarization prompt + if summary: + + # A summary already exists + summary_message = ( + f"This is a summary of the conversation to date: {summary}\n\n" + "Extend the summary by taking into account the new messages above:" + ) + + else: + summary_message = "Create a summary of the conversation above:" + + # Add prompt to our history + messages = state["messages"] + [HumanMessage(content=summary_message)] + response = model.invoke(messages) + + # Delete all but the 2 most recent messages + delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]] + return {"summary": response.content, "messages": delete_messages} +``` +::: + +:::js +Prompting and orchestration logic can be used to summarize the message history. For example, in LangGraph you can include a `summary` key in the state alongside the `messages` key: + +```typescript +import { BaseMessage } from "@langchain/core/messages"; +import { MessagesZodMeta } from "@langchain/langgraph"; +import { registry } from "@langchain/langgraph/zod"; +import * as z from "zod"; + +const State = z.object({ + messages: z + .array(z.custom()) + .register(registry, MessagesZodMeta), + summary: z.string().optional(), +}); +``` + +Then, you can generate a summary of the chat history, using any existing summary as context for the next summary. This `summarizeConversation` node can be called after some number of messages have accumulated in the `messages` state key. + +```typescript +import { RemoveMessage, HumanMessage } from "@langchain/core/messages"; + +const summarizeConversation = async (state: z.infer) => { + // First, we get any existing summary + const summary = state.summary || ""; + + // Create our summarization prompt + let summaryMessage: string; + if (summary) { + // A summary already exists + summaryMessage = + `This is a summary of the conversation to date: ${summary}\n\n` + + "Extend the summary by taking into account the new messages above:"; + } else { + summaryMessage = "Create a summary of the conversation above:"; + } + + // Add prompt to our history + const messages = [ + ...state.messages, + new HumanMessage({ content: summaryMessage }) + ]; + const response = await model.invoke(messages); + + // Delete all but the 2 most recent messages + const deleteMessages = state.messages + .slice(0, -2) + .map(m => new RemoveMessage({ id: m.id })); + + return { + summary: response.content, + messages: deleteMessages + }; +}; +``` +::: + + + :::python + ```python + from typing import Any, TypedDict + + from langchain.chat_models import init_chat_model + from langchain.messages import AnyMessage + from langchain_core.messages.utils import count_tokens_approximately + from langgraph.graph import StateGraph, START, MessagesState + from langgraph.checkpoint.memory import InMemorySaver + from langmem.short_term import SummarizationNode, RunningSummary # [!code highlight] + + model = init_chat_model("claude-sonnet-4-5-20250929") + summarization_model = model.bind(max_tokens=128) + + class State(MessagesState): + context: dict[str, RunningSummary] # [!code highlight] + + class LLMInputState(TypedDict): # [!code highlight] + summarized_messages: list[AnyMessage] + context: dict[str, RunningSummary] + + summarization_node = SummarizationNode( # [!code highlight] + token_counter=count_tokens_approximately, + model=summarization_model, + max_tokens=256, + max_tokens_before_summary=256, + max_summary_tokens=128, + ) + + def call_model(state: LLMInputState): # [!code highlight] + response = model.invoke(state["summarized_messages"]) + return {"messages": [response]} + + checkpointer = InMemorySaver() + builder = StateGraph(State) + builder.add_node(call_model) + builder.add_node("summarize", summarization_node) # [!code highlight] + builder.add_edge(START, "summarize") + builder.add_edge("summarize", "call_model") + graph = builder.compile(checkpointer=checkpointer) + + # Invoke the graph + config = {"configurable": {"thread_id": "1"}} + graph.invoke({"messages": "hi, my name is bob"}, config) + graph.invoke({"messages": "write a short poem about cats"}, config) + graph.invoke({"messages": "now do the same but for dogs"}, config) + final_response = graph.invoke({"messages": "what's my name?"}, config) + + final_response["messages"][-1].pretty_print() + print("\nSummary:", final_response["context"]["running_summary"].summary) +``` + + 1. We will keep track of our running summary in the `context` field + + (expected by the `SummarizationNode`). + + 1. Define private state that will be used only for filtering + + the inputs to `call_model` node. + + 1. We're passing a private input state here to isolate the messages returned by the summarization node + + ``` + ================================== Ai Message ================================== + + From our conversation, I can see that you introduced yourself as Bob. That's the name you shared with me when we began talking. + + Summary: In this conversation, I was introduced to Bob, who then asked me to write a poem about cats. I composed a poem titled "The Mystery of Cats" that captured cats' graceful movements, independent nature, and their special relationship with humans. Bob then requested a similar poem about dogs, so I wrote "The Joy of Dogs," which highlighted dogs' loyalty, enthusiasm, and loving companionship. Both poems were written in a similar style but emphasized the distinct characteristics that make each pet special. + ``` + ::: + + :::js + ```typescript + import { ChatAnthropic } from "@langchain/anthropic"; + import { + SystemMessage, + HumanMessage, + RemoveMessage, + type BaseMessage + } from "@langchain/core/messages"; + import { + MessagesZodMeta, + StateGraph, + START, + END, + MemorySaver, + } from "@langchain/langgraph"; + import { BaseMessage } from "@langchain/core/messages"; + import { registry } from "@langchain/langgraph/zod"; + import * as z from "zod"; + import { v4 as uuidv4 } from "uuid"; + + const memory = new MemorySaver(); + + // We will add a `summary` attribute (in addition to `messages` key, + // which MessagesZodState already has) + const GraphState = z.object({ + messages: z + .array(z.custom()) + .register(registry, MessagesZodMeta), + summary: z.string().default(""), + }); + + // We will use this model for both the conversation and the summarization + const model = new ChatAnthropic({ model: "claude-haiku-4-5-20251001" }); + + // Define the logic to call the model + const callModel = async (state: z.infer) => { + // If a summary exists, we add this in as a system message + const { summary } = state; + let { messages } = state; + if (summary) { + const systemMessage = new SystemMessage({ + id: uuidv4(), + content: `Summary of conversation earlier: ${summary}`, + }); + messages = [systemMessage, ...messages]; + } + const response = await model.invoke(messages); + // We return an object, because this will get added to the existing state + return { messages: [response] }; + }; + + // We now define the logic for determining whether to end or summarize the conversation + const shouldContinue = (state: z.infer) => { + const messages = state.messages; + // If there are more than six messages, then we summarize the conversation + if (messages.length > 6) { + return "summarize_conversation"; + } + // Otherwise we can just end + return END; + }; + + const summarizeConversation = async (state: z.infer) => { + // First, we summarize the conversation + const { summary, messages } = state; + let summaryMessage: string; + if (summary) { + // If a summary already exists, we use a different system prompt + // to summarize it than if one didn't + summaryMessage = + `This is summary of the conversation to date: ${summary}\n\n` + + "Extend the summary by taking into account the new messages above:"; + } else { + summaryMessage = "Create a summary of the conversation above:"; + } + + const allMessages = [ + ...messages, + new HumanMessage({ id: uuidv4(), content: summaryMessage }), + ]; + + const response = await model.invoke(allMessages); + + // We now need to delete messages that we no longer want to show up + // I will delete all but the last two messages, but you can change this + const deleteMessages = messages + .slice(0, -2) + .map((m) => new RemoveMessage({ id: m.id! })); + + if (typeof response.content !== "string") { + throw new Error("Expected a string response from the model"); + } + + return { summary: response.content, messages: deleteMessages }; + }; + + // Define a new graph + const workflow = new StateGraph(GraphState) + // Define the conversation node and the summarize node + .addNode("conversation", callModel) + .addNode("summarize_conversation", summarizeConversation) + // Set the entrypoint as conversation + .addEdge(START, "conversation") + // We now add a conditional edge + .addConditionalEdges( + // First, we define the start node. We use `conversation`. + // This means these are the edges taken after the `conversation` node is called. + "conversation", + // Next, we pass in the function that will determine which node is called next. + shouldContinue, + ) + // We now add a normal edge from `summarize_conversation` to END. + // This means that after `summarize_conversation` is called, we end. + .addEdge("summarize_conversation", END); + + // Finally, we compile it! + const app = workflow.compile({ checkpointer: memory }); + ``` + ::: + + +### Manage checkpoints + +You can view and delete the information stored by the checkpointer. + + +#### View thread state + +:::python + + + ```python + config = { + "configurable": { + "thread_id": "1", # [!code highlight] + # optionally provide an ID for a specific checkpoint, + # otherwise the latest checkpoint is shown + # "checkpoint_id": "1f029ca3-1f5b-6704-8004-820c16b69a5a" # [!code highlight] + + } + } + graph.get_state(config) # [!code highlight] +``` + + ``` + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today?), HumanMessage(content="what's my name?"), AIMessage(content='Your name is Bob.')]}, next=(), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1f5b-6704-8004-820c16b69a5a'}}, + metadata={ + 'source': 'loop', + 'writes': {'call_model': {'messages': AIMessage(content='Your name is Bob.')}}, + 'step': 4, + 'parents': {}, + 'thread_id': '1' + }, + created_at='2025-05-05T16:01:24.680462+00:00', + parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, + tasks=(), + interrupts=() + ) + ``` + + + ```python + config = { + "configurable": { + "thread_id": "1", # [!code highlight] + # optionally provide an ID for a specific checkpoint, + # otherwise the latest checkpoint is shown + # "checkpoint_id": "1f029ca3-1f5b-6704-8004-820c16b69a5a" # [!code highlight] + + } + } + checkpointer.get_tuple(config) # [!code highlight] +``` + + ``` + CheckpointTuple( + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1f5b-6704-8004-820c16b69a5a'}}, + checkpoint={ + 'v': 3, + 'ts': '2025-05-05T16:01:24.680462+00:00', + 'id': '1f029ca3-1f5b-6704-8004-820c16b69a5a', + 'channel_versions': {'__start__': '00000000000000000000000000000005.0.5290678567601859', 'messages': '00000000000000000000000000000006.0.3205149138784782', 'branch:to:call_model': '00000000000000000000000000000006.0.14611156755133758'}, 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000004.0.5736472536395331'}, 'call_model': {'branch:to:call_model': '00000000000000000000000000000005.0.1410174088651449'}}, + 'channel_values': {'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today?), HumanMessage(content="what's my name?"), AIMessage(content='Your name is Bob.')]}, + }, + metadata={ + 'source': 'loop', + 'writes': {'call_model': {'messages': AIMessage(content='Your name is Bob.')}}, + 'step': 4, + 'parents': {}, + 'thread_id': '1' + }, + parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, + pending_writes=[] + ) + ``` + + +::: + +:::js +```typescript +const config = { + configurable: { + thread_id: "1", + // optionally provide an ID for a specific checkpoint, + // otherwise the latest checkpoint is shown + // checkpoint_id: "1f029ca3-1f5b-6704-8004-820c16b69a5a" + }, +}; +await graph.getState(config); +``` + +``` +{ + values: { messages: [HumanMessage(...), AIMessage(...), HumanMessage(...), AIMessage(...)] }, + next: [], + config: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1f029ca3-1f5b-6704-8004-820c16b69a5a' } }, + metadata: { + source: 'loop', + writes: { call_model: { messages: AIMessage(...) } }, + step: 4, + parents: {}, + thread_id: '1' + }, + createdAt: '2025-05-05T16:01:24.680462+00:00', + parentConfig: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1f029ca3-1790-6b0a-8003-baf965b6a38f' } }, + tasks: [], + interrupts: [] +} +``` +::: + + +#### View the history of the thread + +:::python + + + ```python + config = { + "configurable": { + "thread_id": "1" # [!code highlight] + } + } + list(graph.get_state_history(config)) # [!code highlight] +``` + + ``` + [ + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?'), HumanMessage(content="what's my name?"), AIMessage(content='Your name is Bob.')]}, + next=(), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1f5b-6704-8004-820c16b69a5a'}}, + metadata={'source': 'loop', 'writes': {'call_model': {'messages': AIMessage(content='Your name is Bob.')}}, 'step': 4, 'parents': {}, 'thread_id': '1'}, + created_at='2025-05-05T16:01:24.680462+00:00', + parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, + tasks=(), + interrupts=() + ), + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?'), HumanMessage(content="what's my name?")]}, + next=('call_model',), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, + metadata={'source': 'loop', 'writes': None, 'step': 3, 'parents': {}, 'thread_id': '1'}, + created_at='2025-05-05T16:01:23.863421+00:00', + parent_config={...} + tasks=(PregelTask(id='8ab4155e-6b15-b885-9ce5-bed69a2c305c', name='call_model', path=('__pregel_pull', 'call_model'), error=None, interrupts=(), state=None, result={'messages': AIMessage(content='Your name is Bob.')}),), + interrupts=() + ), + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')]}, + next=('__start__',), + config={...}, + metadata={'source': 'input', 'writes': {'__start__': {'messages': [{'role': 'user', 'content': "what's my name?"}]}}, 'step': 2, 'parents': {}, 'thread_id': '1'}, + created_at='2025-05-05T16:01:23.863173+00:00', + parent_config={...} + tasks=(PregelTask(id='24ba39d6-6db1-4c9b-f4c5-682aeaf38dcd', name='__start__', path=('__pregel_pull', '__start__'), error=None, interrupts=(), state=None, result={'messages': [{'role': 'user', 'content': "what's my name?"}]}),), + interrupts=() + ), + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')]}, + next=(), + config={...}, + metadata={'source': 'loop', 'writes': {'call_model': {'messages': AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')}}, 'step': 1, 'parents': {}, 'thread_id': '1'}, + created_at='2025-05-05T16:01:23.862295+00:00', + parent_config={...} + tasks=(), + interrupts=() + ), + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob")]}, + next=('call_model',), + config={...}, + metadata={'source': 'loop', 'writes': None, 'step': 0, 'parents': {}, 'thread_id': '1'}, + created_at='2025-05-05T16:01:22.278960+00:00', + parent_config={...} + tasks=(PregelTask(id='8cbd75e0-3720-b056-04f7-71ac805140a0', name='call_model', path=('__pregel_pull', 'call_model'), error=None, interrupts=(), state=None, result={'messages': AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')}),), + interrupts=() + ), + StateSnapshot( + values={'messages': []}, + next=('__start__',), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-0870-6ce2-bfff-1f3f14c3e565'}}, + metadata={'source': 'input', 'writes': {'__start__': {'messages': [{'role': 'user', 'content': "hi! I'm bob"}]}}, 'step': -1, 'parents': {}, 'thread_id': '1'}, + created_at='2025-05-05T16:01:22.277497+00:00', + parent_config=None, + tasks=(PregelTask(id='d458367b-8265-812c-18e2-33001d199ce6', name='__start__', path=('__pregel_pull', '__start__'), error=None, interrupts=(), state=None, result={'messages': [{'role': 'user', 'content': "hi! I'm bob"}]}),), + interrupts=() + ) + ] + ``` + + + ```python + config = { + "configurable": { + "thread_id": "1" # [!code highlight] + } + } + list(checkpointer.list(config)) # [!code highlight] +``` + + ``` + [ + CheckpointTuple( + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1f5b-6704-8004-820c16b69a5a'}}, + checkpoint={ + 'v': 3, + 'ts': '2025-05-05T16:01:24.680462+00:00', + 'id': '1f029ca3-1f5b-6704-8004-820c16b69a5a', + 'channel_versions': {'__start__': '00000000000000000000000000000005.0.5290678567601859', 'messages': '00000000000000000000000000000006.0.3205149138784782', 'branch:to:call_model': '00000000000000000000000000000006.0.14611156755133758'}, + 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000004.0.5736472536395331'}, 'call_model': {'branch:to:call_model': '00000000000000000000000000000005.0.1410174088651449'}}, + 'channel_values': {'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?'), HumanMessage(content="what's my name?"), AIMessage(content='Your name is Bob.')]}, + }, + metadata={'source': 'loop', 'writes': {'call_model': {'messages': AIMessage(content='Your name is Bob.')}}, 'step': 4, 'parents': {}, 'thread_id': '1'}, + parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, + pending_writes=[] + ), + CheckpointTuple( + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, + checkpoint={ + 'v': 3, + 'ts': '2025-05-05T16:01:23.863421+00:00', + 'id': '1f029ca3-1790-6b0a-8003-baf965b6a38f', + 'channel_versions': {'__start__': '00000000000000000000000000000005.0.5290678567601859', 'messages': '00000000000000000000000000000006.0.3205149138784782', 'branch:to:call_model': '00000000000000000000000000000006.0.14611156755133758'}, + 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000004.0.5736472536395331'}, 'call_model': {'branch:to:call_model': '00000000000000000000000000000005.0.1410174088651449'}}, + 'channel_values': {'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?'), HumanMessage(content="what's my name?")], 'branch:to:call_model': None} + }, + metadata={'source': 'loop', 'writes': None, 'step': 3, 'parents': {}, 'thread_id': '1'}, + parent_config={...}, + pending_writes=[('8ab4155e-6b15-b885-9ce5-bed69a2c305c', 'messages', AIMessage(content='Your name is Bob.'))] + ), + CheckpointTuple( + config={...}, + checkpoint={ + 'v': 3, + 'ts': '2025-05-05T16:01:23.863173+00:00', + 'id': '1f029ca3-1790-616e-8002-9e021694a0cd', + 'channel_versions': {'__start__': '00000000000000000000000000000004.0.5736472536395331', 'messages': '00000000000000000000000000000003.0.7056767754077798', 'branch:to:call_model': '00000000000000000000000000000003.0.22059023329132854'}, + 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0.7040775356287469'}, 'call_model': {'branch:to:call_model': '00000000000000000000000000000002.0.9300422176788571'}}, + 'channel_values': {'__start__': {'messages': [{'role': 'user', 'content': "what's my name?"}]}, 'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')]} + }, + metadata={'source': 'input', 'writes': {'__start__': {'messages': [{'role': 'user', 'content': "what's my name?"}]}}, 'step': 2, 'parents': {}, 'thread_id': '1'}, + parent_config={...}, + pending_writes=[('24ba39d6-6db1-4c9b-f4c5-682aeaf38dcd', 'messages', [{'role': 'user', 'content': "what's my name?"}]), ('24ba39d6-6db1-4c9b-f4c5-682aeaf38dcd', 'branch:to:call_model', None)] + ), + CheckpointTuple( + config={...}, + checkpoint={ + 'v': 3, + 'ts': '2025-05-05T16:01:23.862295+00:00', + 'id': '1f029ca3-178d-6f54-8001-d7b180db0c89', + 'channel_versions': {'__start__': '00000000000000000000000000000002.0.18673090920108737', 'messages': '00000000000000000000000000000003.0.7056767754077798', 'branch:to:call_model': '00000000000000000000000000000003.0.22059023329132854'}, + 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0.7040775356287469'}, 'call_model': {'branch:to:call_model': '00000000000000000000000000000002.0.9300422176788571'}}, + 'channel_values': {'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')]} + }, + metadata={'source': 'loop', 'writes': {'call_model': {'messages': AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')}}, 'step': 1, 'parents': {}, 'thread_id': '1'}, + parent_config={...}, + pending_writes=[] + ), + CheckpointTuple( + config={...}, + checkpoint={ + 'v': 3, + 'ts': '2025-05-05T16:01:22.278960+00:00', + 'id': '1f029ca3-0874-6612-8000-339f2abc83b1', + 'channel_versions': {'__start__': '00000000000000000000000000000002.0.18673090920108737', 'messages': '00000000000000000000000000000002.0.30296526818059655', 'branch:to:call_model': '00000000000000000000000000000002.0.9300422176788571'}, + 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0.7040775356287469'}}, + 'channel_values': {'messages': [HumanMessage(content="hi! I'm bob")], 'branch:to:call_model': None} + }, + metadata={'source': 'loop', 'writes': None, 'step': 0, 'parents': {}, 'thread_id': '1'}, + parent_config={...}, + pending_writes=[('8cbd75e0-3720-b056-04f7-71ac805140a0', 'messages', AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?'))] + ), + CheckpointTuple( + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-0870-6ce2-bfff-1f3f14c3e565'}}, + checkpoint={ + 'v': 3, + 'ts': '2025-05-05T16:01:22.277497+00:00', + 'id': '1f029ca3-0870-6ce2-bfff-1f3f14c3e565', + 'channel_versions': {'__start__': '00000000000000000000000000000001.0.7040775356287469'}, + 'versions_seen': {'__input__': {}}, + 'channel_values': {'__start__': {'messages': [{'role': 'user', 'content': "hi! I'm bob"}]}} + }, + metadata={'source': 'input', 'writes': {'__start__': {'messages': [{'role': 'user', 'content': "hi! I'm bob"}]}}, 'step': -1, 'parents': {}, 'thread_id': '1'}, + parent_config=None, + pending_writes=[('d458367b-8265-812c-18e2-33001d199ce6', 'messages', [{'role': 'user', 'content': "hi! I'm bob"}]), ('d458367b-8265-812c-18e2-33001d199ce6', 'branch:to:call_model', None)] + ) + ] + ``` + + +::: + +:::js +```typescript +const config = { + configurable: { + thread_id: "1", + }, +}; + +const history = []; +for await (const state of graph.getStateHistory(config)) { + history.push(state); +} +``` +::: + +#### Delete all checkpoints for a thread + +:::python +```python +thread_id = "1" +checkpointer.delete_thread(thread_id) +``` +::: + +:::js +```typescript +const threadId = "1"; +await checkpointer.deleteThread(threadId); +``` +::: + +:::python +## Prebuilt memory tools + +**LangMem** is a LangChain-maintained library that offers tools for managing long-term memories in your agent. See the [LangMem documentation](https://langchain-ai.github.io/langmem/) for usage examples. +::: diff --git a/src/oss/langgraph/add-memory.mdx b/src/oss/langgraph/add-memory.mdx index 46ca3234bf..df86c967a8 100644 --- a/src/oss/langgraph/add-memory.mdx +++ b/src/oss/langgraph/add-memory.mdx @@ -2,30 +2,38 @@ title: Memory --- +AI applications need [memory](/oss/concepts/memory) to share context across multiple interactions. In LangGraph, you can add two types of memory: +* [**Checkpointers**](#checkpointers) - Thread-scoped persistence for multi-turn conversations and workflow state. +* [**Stores**](#stores) - Cross-thread persistence for user-specific or application-level data. -AI applications need [memory](/oss/concepts/memory) to share context across multiple interactions. In LangGraph, you can add two types of memory: +--- + +## Checkpointers -* [Add short-term memory](#add-short-term-memory) as a part of your agent's [state](/oss/langgraph/graph-api#state) to enable multi-turn conversations. -* [Add long-term memory](#add-long-term-memory) to store user-specific or application-level data across sessions. +Checkpointers provide thread-level [persistence](/oss/langgraph/persistence), enabling your graph to: +- Track multi-turn conversations +- Resume after interruptions or failures ([durable execution](/oss/langgraph/durable-execution)) +- Access historical states for debugging and [time travel](/oss/langgraph/use-time-travel) +- Enable [human-in-the-loop](/oss/langgraph/interrupts) workflows -## Add short-term memory +### Add checkpointers -**Short-term** memory (thread-level [persistence](/oss/langgraph/persistence)) enables agents to track multi-turn conversations. To add short-term memory: +To add checkpointers to your graph: :::python ```python -from langgraph.checkpoint.memory import InMemorySaver # [!code highlight] +from langgraph.checkpoint.memory import InMemorySaver from langgraph.graph import StateGraph -checkpointer = InMemorySaver() # [!code highlight] +checkpointer = InMemorySaver() builder = StateGraph(...) -graph = builder.compile(checkpointer=checkpointer) # [!code highlight] +graph = builder.compile(checkpointer=checkpointer) graph.invoke( {"messages": [{"role": "user", "content": "hi! i am Bob"}]}, - {"configurable": {"thread_id": "1"}}, # [!code highlight] + {"configurable": {"thread_id": "1"}}, ) ``` ::: @@ -46,7 +54,13 @@ await graph.invoke( ``` ::: -### Use in production + +**What is a thread?** + +A thread is a unique conversation or workflow session identified by a `thread_id`. When you invoke your graph with a specific `thread_id`, LangGraph saves checkpoints (snapshots of the graph state) to that thread. All subsequent invocations with the same `thread_id` can access and continue from the saved state. + + +#### Use in production In production, use a checkpointer backed by a database: @@ -55,9 +69,9 @@ In production, use a checkpointer backed by a database: from langgraph.checkpoint.postgres import PostgresSaver DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" -with PostgresSaver.from_conn_string(DB_URI) as checkpointer: # [!code highlight] +with PostgresSaver.from_conn_string(DB_URI) as checkpointer: builder = StateGraph(...) - graph = builder.compile(checkpointer=checkpointer) # [!code highlight] + graph = builder.compile(checkpointer=checkpointer) ``` ::: @@ -88,12 +102,12 @@ const graph = builder.compile({ checkpointer }); ```python from langchain.chat_models import init_chat_model from langgraph.graph import StateGraph, MessagesState, START - from langgraph.checkpoint.postgres import PostgresSaver # [!code highlight] + from langgraph.checkpoint.postgres import PostgresSaver model = init_chat_model(model="claude-haiku-4-5-20251001") DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" - with PostgresSaver.from_conn_string(DB_URI) as checkpointer: # [!code highlight] + with PostgresSaver.from_conn_string(DB_URI) as checkpointer: # checkpointer.setup() def call_model(state: MessagesState): @@ -104,24 +118,24 @@ const graph = builder.compile({ checkpointer }); builder.add_node(call_model) builder.add_edge(START, "call_model") - graph = builder.compile(checkpointer=checkpointer) # [!code highlight] + graph = builder.compile(checkpointer=checkpointer) config = { "configurable": { - "thread_id": "1" # [!code highlight] + "thread_id": "1" } } for chunk in graph.stream( {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, - config, # [!code highlight] + config, stream_mode="values" ): chunk["messages"][-1].pretty_print() for chunk in graph.stream( {"messages": [{"role": "user", "content": "what's my name?"}]}, - config, # [!code highlight] + config, stream_mode="values" ): chunk["messages"][-1].pretty_print() @@ -131,12 +145,12 @@ const graph = builder.compile({ checkpointer }); ```python from langchain.chat_models import init_chat_model from langgraph.graph import StateGraph, MessagesState, START - from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver # [!code highlight] + from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver model = init_chat_model(model="claude-haiku-4-5-20251001") DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" - async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer: # [!code highlight] + async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer: # await checkpointer.setup() async def call_model(state: MessagesState): @@ -147,24 +161,24 @@ const graph = builder.compile({ checkpointer }); builder.add_node(call_model) builder.add_edge(START, "call_model") - graph = builder.compile(checkpointer=checkpointer) # [!code highlight] + graph = builder.compile(checkpointer=checkpointer) config = { "configurable": { - "thread_id": "1" # [!code highlight] + "thread_id": "1" } } async for chunk in graph.astream( {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, - config, # [!code highlight] + config, stream_mode="values" ): chunk["messages"][-1].pretty_print() async for chunk in graph.astream( {"messages": [{"role": "user", "content": "what's my name?"}]}, - config, # [!code highlight] + config, stream_mode="values" ): chunk["messages"][-1].pretty_print() @@ -235,7 +249,7 @@ const graph = builder.compile({ checkpointer }); :::python - + ``` pip install -U pymongo langgraph langgraph-checkpoint-mongodb ``` @@ -250,12 +264,12 @@ const graph = builder.compile({ checkpointer }); ```python from langchain.chat_models import init_chat_model from langgraph.graph import StateGraph, MessagesState, START - from langgraph.checkpoint.mongodb import MongoDBSaver # [!code highlight] + from langgraph.checkpoint.mongodb import MongoDBSaver model = init_chat_model(model="claude-haiku-4-5-20251001") DB_URI = "localhost:27017" - with MongoDBSaver.from_conn_string(DB_URI) as checkpointer: # [!code highlight] + with MongoDBSaver.from_conn_string(DB_URI) as checkpointer: def call_model(state: MessagesState): response = model.invoke(state["messages"]) @@ -265,24 +279,24 @@ const graph = builder.compile({ checkpointer }); builder.add_node(call_model) builder.add_edge(START, "call_model") - graph = builder.compile(checkpointer=checkpointer) # [!code highlight] + graph = builder.compile(checkpointer=checkpointer) config = { "configurable": { - "thread_id": "1" # [!code highlight] + "thread_id": "1" } } for chunk in graph.stream( {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, - config, # [!code highlight] + config, stream_mode="values" ): chunk["messages"][-1].pretty_print() for chunk in graph.stream( {"messages": [{"role": "user", "content": "what's my name?"}]}, - config, # [!code highlight] + config, stream_mode="values" ): chunk["messages"][-1].pretty_print() @@ -292,12 +306,12 @@ const graph = builder.compile({ checkpointer }); ```python from langchain.chat_models import init_chat_model from langgraph.graph import StateGraph, MessagesState, START - from langgraph.checkpoint.mongodb.aio import AsyncMongoDBSaver # [!code highlight] + from langgraph.checkpoint.mongodb.aio import AsyncMongoDBSaver model = init_chat_model(model="claude-haiku-4-5-20251001") DB_URI = "localhost:27017" - async with AsyncMongoDBSaver.from_conn_string(DB_URI) as checkpointer: # [!code highlight] + async with AsyncMongoDBSaver.from_conn_string(DB_URI) as checkpointer: async def call_model(state: MessagesState): response = await model.ainvoke(state["messages"]) @@ -307,24 +321,24 @@ const graph = builder.compile({ checkpointer }); builder.add_node(call_model) builder.add_edge(START, "call_model") - graph = builder.compile(checkpointer=checkpointer) # [!code highlight] + graph = builder.compile(checkpointer=checkpointer) config = { "configurable": { - "thread_id": "1" # [!code highlight] + "thread_id": "1" } } async for chunk in graph.astream( {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, - config, # [!code highlight] + config, stream_mode="values" ): chunk["messages"][-1].pretty_print() async for chunk in graph.astream( {"messages": [{"role": "user", "content": "what's my name?"}]}, - config, # [!code highlight] + config, stream_mode="values" ): chunk["messages"][-1].pretty_print() @@ -333,7 +347,7 @@ const graph = builder.compile({ checkpointer }); - + ``` pip install -U langgraph langgraph-checkpoint-redis ``` @@ -347,12 +361,12 @@ const graph = builder.compile({ checkpointer }); ```python from langchain.chat_models import init_chat_model from langgraph.graph import StateGraph, MessagesState, START - from langgraph.checkpoint.redis import RedisSaver # [!code highlight] + from langgraph.checkpoint.redis import RedisSaver model = init_chat_model(model="claude-haiku-4-5-20251001") DB_URI = "redis://localhost:6379" - with RedisSaver.from_conn_string(DB_URI) as checkpointer: # [!code highlight] + with RedisSaver.from_conn_string(DB_URI) as checkpointer: # checkpointer.setup() def call_model(state: MessagesState): @@ -363,24 +377,24 @@ const graph = builder.compile({ checkpointer }); builder.add_node(call_model) builder.add_edge(START, "call_model") - graph = builder.compile(checkpointer=checkpointer) # [!code highlight] + graph = builder.compile(checkpointer=checkpointer) config = { "configurable": { - "thread_id": "1" # [!code highlight] + "thread_id": "1" } } for chunk in graph.stream( {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, - config, # [!code highlight] + config, stream_mode="values" ): chunk["messages"][-1].pretty_print() for chunk in graph.stream( {"messages": [{"role": "user", "content": "what's my name?"}]}, - config, # [!code highlight] + config, stream_mode="values" ): chunk["messages"][-1].pretty_print() @@ -390,12 +404,12 @@ const graph = builder.compile({ checkpointer }); ```python from langchain.chat_models import init_chat_model from langgraph.graph import StateGraph, MessagesState, START - from langgraph.checkpoint.redis.aio import AsyncRedisSaver # [!code highlight] + from langgraph.checkpoint.redis.aio import AsyncRedisSaver model = init_chat_model(model="claude-haiku-4-5-20251001") DB_URI = "redis://localhost:6379" - async with AsyncRedisSaver.from_conn_string(DB_URI) as checkpointer: # [!code highlight] + async with AsyncRedisSaver.from_conn_string(DB_URI) as checkpointer: # await checkpointer.asetup() async def call_model(state: MessagesState): @@ -406,24 +420,24 @@ const graph = builder.compile({ checkpointer }); builder.add_node(call_model) builder.add_edge(START, "call_model") - graph = builder.compile(checkpointer=checkpointer) # [!code highlight] + graph = builder.compile(checkpointer=checkpointer) config = { "configurable": { - "thread_id": "1" # [!code highlight] + "thread_id": "1" } } async for chunk in graph.astream( {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, - config, # [!code highlight] + config, stream_mode="values" ): chunk["messages"][-1].pretty_print() async for chunk in graph.astream( {"messages": [{"role": "user", "content": "what's my name?"}]}, - config, # [!code highlight] + config, stream_mode="values" ): chunk["messages"][-1].pretty_print() @@ -433,1026 +447,1750 @@ const graph = builder.compile({ checkpointer }); ::: -### Use in subgraphs +### Checkpointers in subgraphs -If your graph contains [subgraphs](/oss/langgraph/use-subgraphs), you only need to provide the checkpointer when compiling the parent graph. LangGraph will automatically propagate the checkpointer to the child subgraphs. +When your graph contains [subgraphs](/oss/langgraph/use-subgraphs), understanding how checkpointing works is critical for building multi-agent systems and complex workflows. The behavior depends on which checkpointer option you use when compiling the subgraph. -:::python -```python -from langgraph.graph import START, StateGraph -from langgraph.checkpoint.memory import InMemorySaver -from typing import TypedDict + +**LangGraph API handles subgraph checkpointing automatically** -class State(TypedDict): - foo: str +When using the LangGraph API, checkpointing is configured automatically for subgraphs. The information below is relevant when self-hosting or when you need to understand the behavior. + -# Subgraph +#### Checkpointer options -def subgraph_node_1(state: State): - return {"foo": state["foo"] + "bar"} +When compiling a subgraph, you can control how its state is persisted: -subgraph_builder = StateGraph(State) -subgraph_builder.add_node(subgraph_node_1) -subgraph_builder.add_edge(START, "subgraph_node_1") -subgraph = subgraph_builder.compile() # [!code highlight] +:::python +```python +# Option 1: No persistent state across invocations (default) +subgraph = subgraph_builder.compile() -# Parent graph +# Option 2: Persistent state across invocations +subgraph = subgraph_builder.compile(checkpointer=True) -builder = StateGraph(State) -builder.add_node("node_1", subgraph) # [!code highlight] -builder.add_edge(START, "node_1") +# Option 3: No checkpointing at all +subgraph = subgraph_builder.compile(checkpointer=False) -checkpointer = InMemorySaver() -graph = builder.compile(checkpointer=checkpointer) # [!code highlight] +# Alternative: Use a separate checkpointer instance +from langgraph.checkpoint.memory import InMemorySaver +subgraph = subgraph_builder.compile(checkpointer=InMemorySaver()) ``` ::: :::js ```typescript -import { StateGraph, START, MemorySaver } from "@langchain/langgraph"; -import * as z from "zod"; - -const State = z.object({ foo: z.string() }); - -const subgraphBuilder = new StateGraph(State) - .addNode("subgraph_node_1", (state) => { - return { foo: state.foo + "bar" }; - }) - .addEdge(START, "subgraph_node_1"); +// Option 1: No persistent state across invocations (default) const subgraph = subgraphBuilder.compile(); -const builder = new StateGraph(State) - .addNode("node_1", subgraph) - .addEdge(START, "node_1"); +// Option 2: Persistent state across invocations +const subgraph = subgraphBuilder.compile({ checkpointer: true }); -const checkpointer = new MemorySaver(); -const graph = builder.compile({ checkpointer }); +// Option 3: No checkpointing at all +const subgraph = subgraphBuilder.compile({ checkpointer: false }); + +// Alternative: Use a separate checkpointer instance +import { MemorySaver } from "@langchain/langgraph"; +const subgraph = subgraphBuilder.compile({ checkpointer: new MemorySaver() }); ``` ::: -If you want the subgraph to have its own memory, you can compile it with the appropriate checkpointer option. This is useful in [multi-agent](/oss/langchain/multi-agent) systems, if you want agents to keep track of their internal message histories. +The most common options are `checkpointer=None` (default) and `checkpointer=True`, which both use the parent's checkpointer but differ in how they namespace the subgraph's state. You can also pass any `BaseCheckpointSaver` instance to give the subgraph its own separate checkpointer, though the common practice for leveraging functionality like `interrupt()` in subgraphs is to use the parent's checkpointer with either `checkpointer=True` or the default `checkpointer=None`. + + + + With the default option, the subgraph uses a dynamic namespace that changes with each invocation. This means the subgraph state resets between runs - each invocation starts with a clean slate. The state from each run is still stored and accessible by the parent graph, but the subgraph itself doesn't carry over state from previous invocations. + + Use this when your subgraph acts as a reusable tool that may be called multiple times and each invocation should start fresh, such as a search tool or data processing pipeline. + + :::python + ```python expandable + from langgraph.graph import StateGraph + from langgraph.checkpoint.memory import InMemorySaver + from typing_extensions import TypedDict + from typing import Annotated + import operator + + checkpointer = InMemorySaver() + + class State(TypedDict): + parent: Annotated[int, operator.add] + + class SubgraphState(TypedDict): + subgraph: Annotated[int, operator.add] + + def node_a(state: State): + return {"parent": 1} + + def node_b(state: SubgraphState): + return {"subgraph": 1} + + # Subgraph with checkpointer=None (default) + subgraph_builder = StateGraph(SubgraphState) + subgraph_builder.add_node("node_b", node_b) + subgraph_builder.set_entry_point("node_b") + subgraph = subgraph_builder.compile() # checkpointer=None + + # Parent graph + builder = StateGraph(State) + builder.add_node("node_a", node_a) + builder.add_node("subgraph", subgraph) + builder.set_entry_point("node_a") + builder.add_edge("node_a", "subgraph") + graph = builder.compile(checkpointer=checkpointer) + + config = {"configurable": {"thread_id": "1"}} + + # Run 1 + for chunk in graph.stream({}, subgraphs=True, stream_mode="values", config=config): + print(chunk) + # Output: + # ((), {'parent': 1}) + # (('subgraph:ac5cc169-30a2-7d6e-ae2a-00800959aeb8',), {'subgraph': 1}) + + # Run 2 + for chunk in graph.stream({}, subgraphs=True, stream_mode="values", config=config): + print(chunk) + # Output: + # ((), {'parent': 2}) # parent accumulated + # (('subgraph:b84c6daf-b464-1756-8b39-30d38e101fcf',), {'subgraph': 1}) # subgraph reset + ``` + + Notice the subgraph namespace changes each run (`ac5cc169...` vs `b84c6daf...`), and `subgraph` value resets to `1` each time. + ::: + :::js + ```typescript expandable + import { StateGraph, MemorySaver } from "@langchain/langgraph"; + import { registry } from "@langchain/langgraph/zod"; + import * as z from "zod"; + + const checkpointer = new MemorySaver(); + + const State = z.object({ + parent: z.number().register(registry, { + reducer: { fn: (x, y) => x + y }, + default: () => 0, + }), + }); + + const SubgraphState = z.object({ + subgraph: z.number().register(registry, { + reducer: { fn: (x, y) => x + y }, + default: () => 0, + }), + }); + + const nodeA = (state: z.infer) => ({ parent: 1 }); + const nodeB = (state: z.infer) => ({ subgraph: 1 }); + + // Subgraph without persistence + const subgraphBuilder = new StateGraph(SubgraphState) + .addNode("nodeB", nodeB) + .addEntrypoint("nodeB"); + const subgraph = subgraphBuilder.compile(); // No checkpointer + + // Parent graph + const builder = new StateGraph(State) + .addNode("nodeA", nodeA) + .addNode("subgraph", subgraph) + .addEntrypoint("nodeA") + .addEdge("nodeA", "subgraph"); + const graph = builder.compile({ checkpointer }); + + const config = { configurable: { thread_id: "1" } }; + + // Run 1 + for await (const chunk of await graph.stream({}, { ...config, subgraphs: true, streamMode: "values" })) { + console.log(chunk); + } + // Output: + // [[], { parent: 1 }] + // [['subgraph:ac5cc169-30a2-7d6e-ae2a-00800959aeb8'], { subgraph: 1 }] + + // Run 2 + for await (const chunk of await graph.stream({}, { ...config, subgraphs: true, streamMode: "values" })) { + console.log(chunk); + } + // Output: + // [[], { parent: 2 }] // parent accumulated + // [['subgraph:b84c6daf-b464-1756-8b39-30d38e101fcf'], { subgraph: 1 }] // subgraph reset + ``` + + Notice the subgraph namespace changes each run, and `subgraph` value resets to `1` each time. + ::: + + + + When you use `checkpointer=True`, the subgraph uses a static namespace that stays the same across invocations. This means the subgraph state persists across runs within the same thread. The subgraph resumes from its last execution, replaying from the last stored checkpoint. Note that this is not time-linear - it replays from the last checkpoint in the namespace, regardless of when that execution occurred. + + Use this for multi-agent systems where each agent should maintain its own conversation history, or when building workflows where subgraphs need to remember things across multiple invocations. + + :::python + ```python expandable + from langgraph.graph import StateGraph + from langgraph.checkpoint.memory import InMemorySaver + from typing_extensions import TypedDict + from typing import Annotated + import operator + + checkpointer = InMemorySaver() + + class State(TypedDict): + parent: Annotated[int, operator.add] + + class SubgraphState(TypedDict): + subgraph: Annotated[int, operator.add] + + def node_a(state: State): + return {"parent": 1} + + def node_b(state: SubgraphState): + return {"subgraph": 1} + + # Subgraph with checkpointer=True + subgraph_builder = StateGraph(SubgraphState) + subgraph_builder.add_node("node_b", node_b) + subgraph_builder.set_entry_point("node_b") + subgraph = subgraph_builder.compile(checkpointer=True) # State persists! + + # Parent graph + builder = StateGraph(State) + builder.add_node("node_a", node_a) + builder.add_node("subgraph", subgraph) + builder.set_entry_point("node_a") + builder.add_edge("node_a", "subgraph") + graph = builder.compile(checkpointer=checkpointer) + + config = {"configurable": {"thread_id": "1"}} + + # Run 1 + for chunk in graph.stream({}, subgraphs=True, stream_mode="values", config=config): + print(chunk) + # Output: + # ((), {'parent': 1}) + # (('subgraph',), {'subgraph': 1}) + + # Run 2 + for chunk in graph.stream({}, subgraphs=True, stream_mode="values", config=config): + print(chunk) + # Output: + # ((), {'parent': 2}) # parent accumulated + # (('subgraph',), {'subgraph': 2}) # subgraph accumulated! + ``` + + Notice the subgraph namespace stays stable (`'subgraph'`), and the `subgraph` value accumulates across runs (`1` → `2`). + ::: + :::js + ```typescript expandable + import { StateGraph, MemorySaver } from "@langchain/langgraph"; + import { registry } from "@langchain/langgraph/zod"; + import * as z from "zod"; + + const checkpointer = new MemorySaver(); + + const State = z.object({ + parent: z.number().register(registry, { + reducer: { fn: (x, y) => x + y }, + default: () => 0, + }), + }); + + const SubgraphState = z.object({ + subgraph: z.number().register(registry, { + reducer: { fn: (x, y) => x + y }, + default: () => 0, + }), + }); + + const nodeA = (state: z.infer) => ({ parent: 1 }); + const nodeB = (state: z.infer) => ({ subgraph: 1 }); + + // Subgraph with persistent state + const subgraphBuilder = new StateGraph(SubgraphState) + .addNode("nodeB", nodeB) + .addEntrypoint("nodeB"); + const subgraph = subgraphBuilder.compile({ checkpointer: true }); // State persists! + + // Parent graph + const builder = new StateGraph(State) + .addNode("nodeA", nodeA) + .addNode("subgraph", subgraph) + .addEntrypoint("nodeA") + .addEdge("nodeA", "subgraph"); + const graph = builder.compile({ checkpointer }); + + const config = { configurable: { thread_id: "1" } }; + + // Run 1 + for await (const chunk of await graph.stream({}, { ...config, subgraphs: true, streamMode: "values" })) { + console.log(chunk); + } + // Output: + // [[], { parent: 1 }] + // [['subgraph'], { subgraph: 1 }] + + // Run 2 + for await (const chunk of await graph.stream({}, { ...config, subgraphs: true, streamMode: "values" })) { + console.log(chunk); + } + // Output: + // [[], { parent: 2 }] // parent accumulated + // [['subgraph'], { subgraph: 2 }] // subgraph accumulated! + ``` + + Notice the subgraph namespace stays stable, and the `subgraph` value accumulates across runs. + ::: + + + + When you use `checkpointer=False`, the subgraph does not use checkpointing at all. No state is saved anywhere, even if the parent graph has a checkpointer configured. + + Use this when the subgraph is purely stateless and you want to avoid any persistence overhead. + + + + +If your subgraph is used like a function that might be called multiple times (e.g., a tool in a ReAct agent), use `checkpointer=None`. If your subgraph represents an entity with its own memory (e.g., an agent with conversation history), use `checkpointer=True`. + + +### Access state + +With checkpointing enabled, you can access the current state, historical states, and subgraph states. + +#### Get current state + +You can retrieve the current state of your graph at any time, including the state values and the next nodes that will execute. :::python ```python -subgraph_builder = StateGraph(...) -subgraph = subgraph_builder.compile(checkpointer=True) # [!code highlight] +config = {"configurable": {"thread_id": "1"}} +state = graph.get_state(config) + +print(state.values) # Current state values +print(state.next) # Next nodes to execute ``` ::: :::js ```typescript -const subgraphBuilder = new StateGraph(...); -const subgraph = subgraphBuilder.compile({ checkpointer: true }); // [!code highlight] +const config = { configurable: { thread_id: "1" } }; +const state = await graph.getState(config); + +console.log(state.values); // Current state values +console.log(state.next); // Next nodes to execute ``` ::: -## Add long-term memory +#### Get state history -Use long-term memory to store user-specific or application-specific data across conversations. +You can access the full execution history for a thread to see how the state evolved over time. The history is ordered chronologically from most recent to oldest. :::python ```python -from langgraph.store.memory import InMemoryStore # [!code highlight] -from langgraph.graph import StateGraph - -store = InMemoryStore() # [!code highlight] +config = {"configurable": {"thread_id": "1"}} +history = list(graph.get_state_history(config)) -builder = StateGraph(...) -graph = builder.compile(store=store) # [!code highlight] +# History is ordered from most recent to oldest +for state in history: + print(f"Step: {state.metadata['step']}, Values: {state.values}") ``` ::: :::js ```typescript -import { InMemoryStore, StateGraph } from "@langchain/langgraph"; - -const store = new InMemoryStore(); +const config = { configurable: { thread_id: "1" } }; +const history = []; +for await (const state of graph.getStateHistory(config)) { + history.push(state); +} -const builder = new StateGraph(...); -const graph = builder.compile({ store }); +// History is ordered from most recent to oldest +for (const state of history) { + console.log(`Step: ${state.metadata.step}, Values: ${state.values}`); +} ``` ::: -### Use in production + + :::python + ```python + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob!')]}, + next=(), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28fe-6528-8002-5a559208592c'}}, + metadata={'source': 'loop', 'writes': {'call_model': {'messages': AIMessage(content='Hi Bob!')}}, 'step': 2}, + created_at='2024-08-29T19:19:38.821749+00:00', + parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f9-6ec4-8001-31981c2c39f8'}}, + tasks=() + ) + ``` + ::: -In production, use a store backed by a database: + :::js + ```javascript + { + values: { messages: [HumanMessage(...), AIMessage(...)] }, + next: [], + config: { + configurable: { + thread_id: '1', + checkpoint_ns: '', + checkpoint_id: '1ef663ba-28fe-6528-8002-5a559208592c' + } + }, + metadata: { + source: 'loop', + writes: { call_model: { messages: AIMessage(...) } }, + step: 2 + }, + createdAt: '2024-08-29T19:19:38.821749+00:00', + parentConfig: { + configurable: { + thread_id: '1', + checkpoint_ns: '', + checkpoint_id: '1ef663ba-28f9-6ec4-8001-31981c2c39f8' + } + }, + tasks: [] + } + ``` + ::: + + +#### Get subgraph state + +To access subgraph state, use `subgraphs=True`: :::python ```python -from langgraph.store.postgres import PostgresStore - -DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" -with PostgresStore.from_conn_string(DB_URI) as store: # [!code highlight] - builder = StateGraph(...) - graph = builder.compile(store=store) # [!code highlight] +config = {"configurable": {"thread_id": "1"}} +state = graph.get_state(config, subgraphs=True) + +# Access subgraph state from tasks +for task in state.tasks: + if task.state: + print(f"Subgraph namespace: {task.state.config['configurable']['checkpoint_ns']}") + print(f"Subgraph values: {task.state.values}") ``` ::: :::js ```typescript -import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres/store"; +const config = { configurable: { thread_id: "1" } }; +const state = await graph.getState(config, { subgraphs: true }); + +// Access subgraph state from tasks +for (const task of state.tasks) { + if (task.state) { + console.log(`Subgraph namespace: ${task.state.config.configurable.checkpoint_ns}`); + console.log(`Subgraph values: ${task.state.values}`); + } +} +``` +::: -const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"; -const store = PostgresStore.fromConnString(DB_URI); +#### Get subgraph state history -const builder = new StateGraph(...); -const graph = builder.compile({ store }); +:::python +To get a subgraph's state history, first get the subgraph's `checkpoint_ns` from `get_state(subgraphs=True)`, then call `get_state_history` with that namespace: + +```python +# Step 1: Get the subgraph's checkpoint namespace +config = {"configurable": {"thread_id": "1"}} +state = graph.get_state(config, subgraphs=True) +subgraph_config = state.tasks[0].state.config +subgraph_ns = subgraph_config["configurable"]["checkpoint_ns"] + +# Step 2: Get subgraph history using that namespace +subgraph_history = list(graph.get_state_history({ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_ns + } +})) + +for h in subgraph_history: + print(f"Subgraph step: {h.metadata['step']}, Values: {h.values}") ``` + + +**Why doesn't `get_state_history()` have a `subgraphs=True` option?** + +`get_state_history()` returns an Iterator to avoid loading all checkpoints into memory at once. Adding `subgraphs=True` would require loading all checkpoints from all nested subgraphs simultaneously, which could cause huge memory overhead. Instead, you explicitly get each subgraph's namespace and fetch its history separately. + ::: - - :::python - ``` - pip install -U "psycopg[binary,pool]" langgraph langgraph-checkpoint-postgres - ``` +:::js +To get a subgraph's state history, first get the subgraph's `checkpoint_ns` from `getState({ subgraphs: true })`, then call `getStateHistory` with that namespace: - - You need to call `store.setup()` the first time you're using Postgres store - +```typescript +// Step 1: Get the subgraph's checkpoint namespace +const config = { configurable: { thread_id: "1" } }; +const state = await graph.getState(config, { subgraphs: true }); +const subgraphConfig = state.tasks[0].state.config; +const subgraphNs = subgraphConfig.configurable.checkpoint_ns; + +// Step 2: Get subgraph history using that namespace +const subgraphHistory = []; +for await (const h of graph.getStateHistory({ + configurable: { + thread_id: "1", + checkpoint_ns: subgraphNs + } +})) { + subgraphHistory.push(h); +} - - - ```python - from langchain_core.runnables import RunnableConfig - from langchain.chat_models import init_chat_model - from langgraph.graph import StateGraph, MessagesState, START - from langgraph.checkpoint.postgres import PostgresSaver - from langgraph.store.postgres import PostgresStore # [!code highlight] - from langgraph.store.base import BaseStore +for (const h of subgraphHistory) { + console.log(`Subgraph step: ${h.metadata.step}, Values: ${h.values}`); +} +``` - model = init_chat_model(model="claude-haiku-4-5-20251001") + +**Why doesn't `getStateHistory()` have a `subgraphs: true` option?** - DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" +`getStateHistory()` returns an Iterator to avoid loading all checkpoints into memory at once. Adding `subgraphs: true` would require loading all checkpoints from all nested subgraphs simultaneously, which could cause huge memory overhead. Instead, you explicitly get each subgraph's namespace and fetch its history separately. + +::: - with ( - PostgresStore.from_conn_string(DB_URI) as store, # [!code highlight] - PostgresSaver.from_conn_string(DB_URI) as checkpointer, - ): - # store.setup() - # checkpointer.setup() + + :::python + ```python + from langgraph.graph import StateGraph + from langgraph.checkpoint.memory import InMemorySaver + from typing_extensions import TypedDict + from langgraph.types import interrupt - def call_model( - state: MessagesState, - config: RunnableConfig, - *, - store: BaseStore, # [!code highlight] - ): - user_id = config["configurable"]["user_id"] - namespace = ("memories", user_id) - memories = store.search(namespace, query=str(state["messages"][-1].content)) # [!code highlight] - info = "\n".join([d.value["data"] for d in memories]) - system_msg = f"You are a helpful assistant talking to the user. User info: {info}" + checkpointer = InMemorySaver() - # Store new memories if the user asks the model to remember - last_message = state["messages"][-1] - if "remember" in last_message.content.lower(): - memory = "User name is Bob" - store.put(namespace, str(uuid.uuid4()), {"data": memory}) # [!code highlight] + class State(TypedDict): + step: str - response = model.invoke( - [{"role": "system", "content": system_msg}] + state["messages"] - ) - return {"messages": response} + def node_a(state: State): + return {"step": "a"} - builder = StateGraph(MessagesState) - builder.add_node(call_model) - builder.add_edge(START, "call_model") + def node_b(state: State): + return {"step": "b"} - graph = builder.compile( - checkpointer=checkpointer, - store=store, # [!code highlight] - ) + def node_c(state: State): + return {"step": "c"} - config = { - "configurable": { - "thread_id": "1", # [!code highlight] - "user_id": "1", # [!code highlight] - } - } - for chunk in graph.stream( - {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, - config, # [!code highlight] - stream_mode="values", - ): - chunk["messages"][-1].pretty_print() + def node_d(state: State): + interrupt("") + return {"step": "d"} - config = { - "configurable": { - "thread_id": "2", # [!code highlight] - "user_id": "1", - } - } + # Build subgraph + subgraph_builder = StateGraph(State) + subgraph_builder.add_node("node_b", node_b) + subgraph_builder.add_node("node_c", node_c) + subgraph_builder.add_node("node_d", node_d) + subgraph_builder.set_entry_point("node_b") + subgraph_builder.add_edge("node_b", "node_c") + subgraph_builder.add_edge("node_c", "node_d") + subgraph = subgraph_builder.compile() # checkpointer=None - for chunk in graph.stream( - {"messages": [{"role": "user", "content": "what is my name?"}]}, - config, # [!code highlight] - stream_mode="values", - ): - chunk["messages"][-1].pretty_print() -``` - - - ```python - from langchain_core.runnables import RunnableConfig - from langchain.chat_models import init_chat_model - from langgraph.graph import StateGraph, MessagesState, START - from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver - from langgraph.store.postgres.aio import AsyncPostgresStore # [!code highlight] - from langgraph.store.base import BaseStore + # Build parent graph + builder = StateGraph(State) + builder.add_node("node_a", node_a) + builder.add_node("subgraph", subgraph) + builder.set_entry_point("node_a") + builder.add_edge("node_a", "subgraph") + graph = builder.compile(checkpointer=checkpointer) - model = init_chat_model(model="claude-haiku-4-5-20251001") + # Run the graph + config = {"configurable": {"thread_id": "1"}} + graph.invoke({"step": "START"}, config) + + # Get subgraph state and history + state = graph.get_state(config, subgraphs=True) + subgraph_config = state.tasks[0].state.config + subgraph_ns = subgraph_config["configurable"]["checkpoint_ns"] + print(f"Subgraph namespace: {subgraph_ns}\n") + + state_history = list(graph.get_state_history({ + "configurable": { + "checkpoint_ns": subgraph_ns, + "thread_id": "1" + } + })) + + for h in state_history: + print(f"Step: {h.metadata['step']}, Values: {h.values}") + ``` + ::: - DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" + :::js + ```typescript + import { StateGraph, MemorySaver } from "@langchain/langgraph"; + import { interrupt } from "@langchain/langgraph"; + import * as z from "zod"; - async with ( - AsyncPostgresStore.from_conn_string(DB_URI) as store, # [!code highlight] - AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer, - ): - # await store.setup() - # await checkpointer.setup() + const checkpointer = new MemorySaver(); - async def call_model( - state: MessagesState, - config: RunnableConfig, - *, - store: BaseStore, # [!code highlight] - ): - user_id = config["configurable"]["user_id"] - namespace = ("memories", user_id) - memories = await store.asearch(namespace, query=str(state["messages"][-1].content)) # [!code highlight] - info = "\n".join([d.value["data"] for d in memories]) - system_msg = f"You are a helpful assistant talking to the user. User info: {info}" + const State = z.object({ step: z.string() }); - # Store new memories if the user asks the model to remember - last_message = state["messages"][-1] - if "remember" in last_message.content.lower(): - memory = "User name is Bob" - await store.aput(namespace, str(uuid.uuid4()), {"data": memory}) # [!code highlight] + const nodeA = (state: z.infer) => ({ step: "a" }); + const nodeB = (state: z.infer) => ({ step: "b" }); + const nodeC = (state: z.infer) => ({ step: "c" }); + const nodeD = (state: z.infer) => { + interrupt(""); + return { step: "d" }; + }; - response = await model.ainvoke( - [{"role": "system", "content": system_msg}] + state["messages"] - ) - return {"messages": response} + // Build subgraph + const subgraphBuilder = new StateGraph(State) + .addNode("nodeB", nodeB) + .addNode("nodeC", nodeC) + .addNode("nodeD", nodeD) + .addEntrypoint("nodeB") + .addEdge("nodeB", "nodeC") + .addEdge("nodeC", "nodeD"); + const subgraph = subgraphBuilder.compile(); + + // Build parent graph + const builder = new StateGraph(State) + .addNode("nodeA", nodeA) + .addNode("subgraph", subgraph) + .addEntrypoint("nodeA") + .addEdge("nodeA", "subgraph"); + const graph = builder.compile({ checkpointer }); - builder = StateGraph(MessagesState) - builder.add_node(call_model) - builder.add_edge(START, "call_model") - - graph = builder.compile( - checkpointer=checkpointer, - store=store, # [!code highlight] - ) + // Run the graph + const config = { configurable: { thread_id: "1" } }; + await graph.invoke({ step: "START" }, config); - config = { - "configurable": { - "thread_id": "1", # [!code highlight] - "user_id": "1", # [!code highlight] - } - } - async for chunk in graph.astream( - {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, - config, # [!code highlight] - stream_mode="values", - ): - chunk["messages"][-1].pretty_print() + // Get subgraph state and history + const state = await graph.getState(config, { subgraphs: true }); + const subgraphConfig = state.tasks[0].state.config; + const subgraphNs = subgraphConfig.configurable.checkpoint_ns; + console.log(`Subgraph namespace: ${subgraphNs}\n`); - config = { - "configurable": { - "thread_id": "2", # [!code highlight] - "user_id": "1", - } - } + const stateHistory = []; + for await (const h of graph.getStateHistory({ + configurable: { + checkpoint_ns: subgraphNs, + thread_id: "1" + } + })) { + stateHistory.push(h); + } - async for chunk in graph.astream( - {"messages": [{"role": "user", "content": "what is my name?"}]}, - config, # [!code highlight] - stream_mode="values", - ): - chunk["messages"][-1].pretty_print() -``` - - + for (const h of stateHistory) { + console.log(`Step: ${h.metadata.step}, Values: ${h.values}`); + } + ``` ::: + - :::js - ``` - npm install @langchain/langgraph-checkpoint-postgres - ``` +### Manage checkpointers - - You need to call `store.setup()` the first time you're using Postgres store - +With checkpointing enabled, long conversations can exceed the LLM's context window. Common solutions are: - ```typescript - import { ChatAnthropic } from "@langchain/anthropic"; - import { StateGraph, MessagesZodMeta, START, LangGraphRunnableConfig } from "@langchain/langgraph"; - import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres"; - import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres/store"; - import { BaseMessage } from "@langchain/core/messages"; - import { registry } from "@langchain/langgraph/zod"; - import * as z from "zod"; - import { v4 as uuidv4 } from "uuid"; +* [Trim messages](#trim-messages) - Remove first or last N messages (before calling LLM) +* [Delete messages](#delete-messages) - Remove messages from LangGraph state permanently +* [Summarize messages](#summarize-messages) - Summarize earlier messages and replace them with a summary +* [Manage checkpoints](#manage-checkpoints) - Store and retrieve message history +* Custom strategies (e.g., message filtering) - const MessagesZodState = z.object({ - messages: z - .array(z.custom()) - .register(registry, MessagesZodMeta), - }); +#### Trim messages - const model = new ChatAnthropic({ model: "claude-haiku-4-5-20251001" }); +:::python +Most LLMs have a maximum supported context window (denominated in tokens). One way to decide when to truncate messages is to count the tokens in the message history and truncate whenever it approaches that limit. If you're using LangChain, you can use the trim messages utility and specify the number of tokens to keep from the list, as well as the `strategy` (e.g., keep the last `max_tokens`) to use for handling the boundary. +::: +:::js +Most LLMs have a maximum supported context window (denominated in tokens). One way to decide when to truncate messages is to count the tokens in the message history and truncate whenever it approaches that limit. If you're using LangChain, you can use the trim messages utility and specify the number of tokens to keep from the list, as well as the `strategy` (e.g., keep the last `maxTokens`) to use for handling the boundary. +::: - const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"; +:::python +To trim message history, use the [`trim_messages`](https://python.langchain.com/api_reference/core/messages/langchain_core.messages.utils.trim_messages.html) function: - const store = PostgresStore.fromConnString(DB_URI); - const checkpointer = PostgresSaver.fromConnString(DB_URI); - // await store.setup(); - // await checkpointer.setup(); +```python +from langchain_core.messages.utils import trim_messages, count_tokens_approximately - const callModel = async ( - state: z.infer, - config: LangGraphRunnableConfig, - ) => { - const userId = config.configurable?.userId; - const namespace = ["memories", userId]; - const memories = await config.store?.search(namespace, { query: state.messages.at(-1)?.content }); - const info = memories?.map(d => d.value.data).join("\n") || ""; - const systemMsg = `You are a helpful assistant talking to the user. User info: ${info}`; +def call_model(state: MessagesState): + messages = trim_messages( + state["messages"], + strategy="last", + token_counter=count_tokens_approximately, + max_tokens=128, + start_on="human", + end_on=("human", "tool"), + ) + response = model.invoke(messages) + return {"messages": [response]} - // Store new memories if the user asks the model to remember - const lastMessage = state.messages.at(-1); - if (lastMessage?.content?.toLowerCase().includes("remember")) { - const memory = "User name is Bob"; - await config.store?.put(namespace, uuidv4(), { data: memory }); - } +builder = StateGraph(MessagesState) +builder.add_node(call_model) +# ... +``` +::: - const response = await model.invoke([ - { role: "system", content: systemMsg }, - ...state.messages - ]); - return { messages: [response] }; - }; +:::js +To trim message history, use the [`trimMessages`](https://js.langchain.com/docs/how_to/trim_messages/) function: - const builder = new StateGraph(MessagesZodState) - .addNode("call_model", callModel) - .addEdge(START, "call_model"); +```typescript +import { trimMessages } from "@langchain/core/messages"; - const graph = builder.compile({ - checkpointer, - store, +const callModel = async (state: z.infer) => { + const messages = trimMessages(state.messages, { + strategy: "last", + maxTokens: 128, + startOn: "human", + endOn: ["human", "tool"], }); + const response = await model.invoke(messages); + return { messages: [response] }; +}; - const config = { - configurable: { - thread_id: "1", - userId: "1", - } - }; +const builder = new StateGraph(MessagesZodState) + .addNode("call_model", callModel); +// ... +``` +::: - for await (const chunk of await graph.stream( - { messages: [{ role: "user", content: "Hi! Remember: my name is Bob" }] }, - { ...config, streamMode: "values" } - )) { - console.log(chunk.messages.at(-1)?.content); - } +:::python +```python expandable +from langchain_core.messages.utils import trim_messages, count_tokens_approximately +from langchain.chat_models import init_chat_model +from langgraph.graph import StateGraph, START, MessagesState +from langgraph.checkpoint.memory import InMemorySaver - const config2 = { - configurable: { - thread_id: "2", - userId: "1", - } - }; +model = init_chat_model("claude-sonnet-4-5-20250929") - for await (const chunk of await graph.stream( - { messages: [{ role: "user", content: "what is my name?" }] }, - { ...config2, streamMode: "values" } - )) { - console.log(chunk.messages.at(-1)?.content); - } - ``` - ::: - +def call_model(state: MessagesState): + messages = trim_messages( + state["messages"], + strategy="last", + token_counter=count_tokens_approximately, + max_tokens=128, + start_on="human", + end_on=("human", "tool"), + ) + response = model.invoke(messages) + return {"messages": [response]} -:::python - - ``` - pip install -U langgraph langgraph-checkpoint-redis - ``` +checkpointer = InMemorySaver() +builder = StateGraph(MessagesState) +builder.add_node(call_model) +builder.add_edge(START, "call_model") +graph = builder.compile(checkpointer=checkpointer) - - You need to call `store.setup()` the first time you're using Redis store - +config = {"configurable": {"thread_id": "1"}} +graph.invoke({"messages": "hi, my name is bob"}, config) +graph.invoke({"messages": "write a short poem about cats"}, config) +graph.invoke({"messages": "now do the same but for dogs"}, config) +final_response = graph.invoke({"messages": "what's my name?"}, config) - - - ```python - from langchain_core.runnables import RunnableConfig - from langchain.chat_models import init_chat_model - from langgraph.graph import StateGraph, MessagesState, START - from langgraph.checkpoint.redis import RedisSaver - from langgraph.store.redis import RedisStore # [!code highlight] - from langgraph.store.base import BaseStore +final_response["messages"][-1].pretty_print() +``` - model = init_chat_model(model="claude-haiku-4-5-20251001") +``` +================================== Ai Message ================================== - DB_URI = "redis://localhost:6379" +Your name is Bob, as you mentioned when you first introduced yourself. +``` +::: - with ( - RedisStore.from_conn_string(DB_URI) as store, # [!code highlight] - RedisSaver.from_conn_string(DB_URI) as checkpointer, - ): - store.setup() - checkpointer.setup() +:::js +```typescript expandable +import { trimMessages, BaseMessage } from "@langchain/core/messages"; +import { ChatAnthropic } from "@langchain/anthropic"; +import { StateGraph, START, MessagesZodMeta, MemorySaver } from "@langchain/langgraph"; +import { registry } from "@langchain/langgraph/zod"; +import * as z from "zod"; - def call_model( - state: MessagesState, - config: RunnableConfig, - *, - store: BaseStore, # [!code highlight] - ): - user_id = config["configurable"]["user_id"] - namespace = ("memories", user_id) - memories = store.search(namespace, query=str(state["messages"][-1].content)) # [!code highlight] - info = "\n".join([d.value["data"] for d in memories]) - system_msg = f"You are a helpful assistant talking to the user. User info: {info}" +const MessagesZodState = z.object({ + messages: z + .array(z.custom()) + .register(registry, MessagesZodMeta), +}); - # Store new memories if the user asks the model to remember - last_message = state["messages"][-1] - if "remember" in last_message.content.lower(): - memory = "User name is Bob" - store.put(namespace, str(uuid.uuid4()), {"data": memory}) # [!code highlight] +const model = new ChatAnthropic({ model: "claude-3-5-sonnet-20241022" }); - response = model.invoke( - [{"role": "system", "content": system_msg}] + state["messages"] - ) - return {"messages": response} +const callModel = async (state: z.infer) => { + const messages = trimMessages(state.messages, { + strategy: "last", + maxTokens: 128, + startOn: "human", + endOn: ["human", "tool"], + tokenCounter: model, + }); + const response = await model.invoke(messages); + return { messages: [response] }; +}; - builder = StateGraph(MessagesState) - builder.add_node(call_model) - builder.add_edge(START, "call_model") +const checkpointer = new MemorySaver(); +const builder = new StateGraph(MessagesZodState) + .addNode("call_model", callModel) + .addEdge(START, "call_model"); +const graph = builder.compile({ checkpointer }); - graph = builder.compile( - checkpointer=checkpointer, - store=store, # [!code highlight] - ) +const config = { configurable: { thread_id: "1" } }; +await graph.invoke({ messages: [{ role: "user", content: "hi, my name is bob" }] }, config); +await graph.invoke({ messages: [{ role: "user", content: "write a short poem about cats" }] }, config); +await graph.invoke({ messages: [{ role: "user", content: "now do the same but for dogs" }] }, config); +const finalResponse = await graph.invoke({ messages: [{ role: "user", content: "what's my name?" }] }, config); - config = { - "configurable": { - "thread_id": "1", # [!code highlight] - "user_id": "1", # [!code highlight] - } - } - for chunk in graph.stream( - {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, - config, # [!code highlight] - stream_mode="values", - ): - chunk["messages"][-1].pretty_print() +console.log(finalResponse.messages.at(-1)?.content); +``` - config = { - "configurable": { - "thread_id": "2", # [!code highlight] - "user_id": "1", - } - } - - for chunk in graph.stream( - {"messages": [{"role": "user", "content": "what is my name?"}]}, - config, # [!code highlight] - stream_mode="values", - ): - chunk["messages"][-1].pretty_print() ``` - - - ```python - from langchain_core.runnables import RunnableConfig - from langchain.chat_models import init_chat_model - from langgraph.graph import StateGraph, MessagesState, START - from langgraph.checkpoint.redis.aio import AsyncRedisSaver - from langgraph.store.redis.aio import AsyncRedisStore # [!code highlight] - from langgraph.store.base import BaseStore - - model = init_chat_model(model="claude-haiku-4-5-20251001") - - DB_URI = "redis://localhost:6379" - - async with ( - AsyncRedisStore.from_conn_string(DB_URI) as store, # [!code highlight] - AsyncRedisSaver.from_conn_string(DB_URI) as checkpointer, - ): - # await store.setup() - # await checkpointer.asetup() - - async def call_model( - state: MessagesState, - config: RunnableConfig, - *, - store: BaseStore, # [!code highlight] - ): - user_id = config["configurable"]["user_id"] - namespace = ("memories", user_id) - memories = await store.asearch(namespace, query=str(state["messages"][-1].content)) # [!code highlight] - info = "\n".join([d.value["data"] for d in memories]) - system_msg = f"You are a helpful assistant talking to the user. User info: {info}" - - # Store new memories if the user asks the model to remember - last_message = state["messages"][-1] - if "remember" in last_message.content.lower(): - memory = "User name is Bob" - await store.aput(namespace, str(uuid.uuid4()), {"data": memory}) # [!code highlight] +Your name is Bob, as you mentioned when you first introduced yourself. +``` +::: - response = await model.ainvoke( - [{"role": "system", "content": system_msg}] + state["messages"] - ) - return {"messages": response} +#### Delete messages - builder = StateGraph(MessagesState) - builder.add_node(call_model) - builder.add_edge(START, "call_model") +You can delete messages from the graph state to manage the message history. - graph = builder.compile( - checkpointer=checkpointer, - store=store, # [!code highlight] - ) +:::python +There are two ways to delete messages from the graph state: - config = { - "configurable": { - "thread_id": "1", # [!code highlight] - "user_id": "1", # [!code highlight] - } - } - async for chunk in graph.astream( - {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, - config, # [!code highlight] - stream_mode="values", - ): - chunk["messages"][-1].pretty_print() +**Remove specific messages** using `RemoveMessage`: - config = { - "configurable": { - "thread_id": "2", # [!code highlight] - "user_id": "1", - } - } +```python +from langchain.messages import RemoveMessage - async for chunk in graph.astream( - {"messages": [{"role": "user", "content": "what is my name?"}]}, - config, # [!code highlight] - stream_mode="values", - ): - chunk["messages"][-1].pretty_print() +def delete_messages(state): + messages = state["messages"] + if len(messages) > 2: + # Remove the earliest two messages + return {"messages": [RemoveMessage(id=m.id) for m in messages[:2]]} ``` - - - -::: - -### Use semantic search -Enable semantic search in your graph's memory store to let graph agents search for items in the store by semantic similarity. +**Remove all messages** using `Overwrite`: -:::python ```python -from langchain.embeddings import init_embeddings -from langgraph.store.memory import InMemoryStore - -# Create store with semantic search enabled -embeddings = init_embeddings("openai:text-embedding-3-small") -store = InMemoryStore( - index={ - "embed": embeddings, - "dims": 1536, - } -) - -store.put(("user_123", "memories"), "1", {"text": "I love pizza"}) -store.put(("user_123", "memories"), "2", {"text": "I am a plumber"}) +from langgraph.types import Overwrite -items = store.search( - ("user_123", "memories"), query="I'm hungry", limit=1 -) +def delete_all_messages(state): + # Bypass the add_messages reducer and clear all messages + return {"messages": Overwrite([])} ``` + + +`RemoveMessage` works with the `add_messages` [reducer](/oss/langgraph/graph-api#reducers) (like in [`MessagesState`](/oss/langgraph/graph-api#messagesstate)), while `Overwrite` bypasses the reducer entirely. [Learn more about Overwrite](/oss/langgraph/use-graph-api#bypass-reducers-with-overwrite). + ::: :::js -```typescript -import { OpenAIEmbeddings } from "@langchain/openai"; -import { InMemoryStore } from "@langchain/langgraph"; +To delete messages from the graph state, use `RemoveMessage`. For `RemoveMessage` to work, you need to use a state key with `messagesStateReducer` [reducer](/oss/langgraph/graph-api#reducers), like `MessagesZodState`. -// Create store with semantic search enabled -const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small" }); -const store = new InMemoryStore({ - index: { - embeddings, - dims: 1536, - }, -}); +To remove specific messages: -await store.put(["user_123", "memories"], "1", { text: "I love pizza" }); -await store.put(["user_123", "memories"], "2", { text: "I am a plumber" }); +```typescript +import { RemoveMessage } from "@langchain/core/messages"; -const items = await store.search(["user_123", "memories"], { - query: "I'm hungry", - limit: 1, -}); +const deleteMessages = (state) => { + const messages = state.messages; + if (messages.length > 2) { + // remove the earliest two messages + return { + messages: messages + .slice(0, 2) + .map((m) => new RemoveMessage({ id: m.id })), + }; + } +}; ``` ::: - - :::python + +When deleting messages, **make sure** that the resulting message history is valid. Check the limitations of the LLM provider you're using. For example: - ```python +* Some providers expect message history to start with a `user` message +* Most providers require `assistant` messages with tool calls to be followed by corresponding `tool` result messages. + - from langchain.embeddings import init_embeddings - from langchain.chat_models import init_chat_model - from langgraph.store.base import BaseStore - from langgraph.store.memory import InMemoryStore - from langgraph.graph import START, MessagesState, StateGraph + + :::python + ```python + from langchain.messages import RemoveMessage + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, START, MessagesState + from langgraph.checkpoint.memory import InMemorySaver - model = init_chat_model("gpt-4o-mini") + model = init_chat_model("claude-haiku-4-5-20251001") - # Create store with semantic search enabled - embeddings = init_embeddings("openai:text-embedding-3-small") - store = InMemoryStore( - index={ - "embed": embeddings, - "dims": 1536, - } - ) + def delete_messages(state): + messages = state["messages"] + if len(messages) > 2: + return {"messages": [RemoveMessage(id=m.id) for m in messages[:2]]} - store.put(("user_123", "memories"), "1", {"text": "I love pizza"}) - store.put(("user_123", "memories"), "2", {"text": "I am a plumber"}) + def call_model(state: MessagesState): + response = model.invoke(state["messages"]) + return {"messages": response} - def chat(state, *, store: BaseStore): - # Search based on user's last message - items = store.search( - ("user_123", "memories"), query=state["messages"][-1].content, limit=2 - ) - memories = "\n".join(item.value["text"] for item in items) - memories = f"## Memories of user\n{memories}" if memories else "" - response = model.invoke( - [ - {"role": "system", "content": f"You are a helpful assistant.\n{memories}"}, - *state["messages"], - ] - ) - return {"messages": [response]} + builder = StateGraph(MessagesState) + builder.add_sequence([call_model, delete_messages]) + builder.add_edge(START, "call_model") + checkpointer = InMemorySaver() + app = builder.compile(checkpointer=checkpointer) - builder = StateGraph(MessagesState) - builder.add_node(chat) - builder.add_edge(START, "chat") - graph = builder.compile(store=store) + config = {"configurable": {"thread_id": "1"}} - for message, metadata in graph.stream( - input={"messages": [{"role": "user", "content": "I'm hungry"}]}, - stream_mode="messages", - ): - print(message.content, end="") - ``` - ::: + for event in app.stream( + {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, + config, + stream_mode="values" + ): + print([(message.type, message.content) for message in event["messages"]]) - :::js + for event in app.stream( + {"messages": [{"role": "user", "content": "what's my name?"}]}, + config, + stream_mode="values" + ): + print([(message.type, message.content) for message in event["messages"]]) + ``` + ::: - ```typescript - import { OpenAIEmbeddings, ChatOpenAI } from "@langchain/openai"; - import { StateGraph, START, MessagesZodMeta, InMemoryStore } from "@langchain/langgraph"; - import { BaseMessage } from "@langchain/core/messages"; - import { registry } from "@langchain/langgraph/zod"; - import * as z from "zod"; + :::js + ```typescript + import { RemoveMessage, BaseMessage } from "@langchain/core/messages"; + import { ChatAnthropic } from "@langchain/anthropic"; + import { StateGraph, START, MemorySaver, MessagesZodMeta } from "@langchain/langgraph"; + import * as z from "zod"; + import { registry } from "@langchain/langgraph/zod"; - const MessagesZodState = z.object({ - messages: z - .array(z.custom()) - .register(registry, MessagesZodMeta), - }); + const MessagesZodState = z.object({ + messages: z + .array(z.custom()) + .register(registry, MessagesZodMeta), + }); - const model = new ChatOpenAI({ model: "gpt-4o-mini" }); + const model = new ChatAnthropic({ model: "claude-3-5-sonnet-20241022" }); - // Create store with semantic search enabled - const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small" }); - const store = new InMemoryStore({ - index: { - embeddings, - dims: 1536, - } - }); + const deleteMessages = (state: z.infer) => { + const messages = state.messages; + if (messages.length > 2) { + return { messages: messages.slice(0, 2).map(m => new RemoveMessage({ id: m.id })) }; + } + return {}; + }; - await store.put(["user_123", "memories"], "1", { text: "I love pizza" }); - await store.put(["user_123", "memories"], "2", { text: "I am a plumber" }); + const callModel = async (state: z.infer) => { + const response = await model.invoke(state.messages); + return { messages: [response] }; + }; - const chat = async (state: z.infer, config) => { - // Search based on user's last message - const items = await config.store.search( - ["user_123", "memories"], - { query: state.messages.at(-1)?.content, limit: 2 } - ); - const memories = items.map(item => item.value.text).join("\n"); - const memoriesText = memories ? `## Memories of user\n${memories}` : ""; + const builder = new StateGraph(MessagesZodState) + .addNode("call_model", callModel) + .addNode("delete_messages", deleteMessages) + .addEdge(START, "call_model") + .addEdge("call_model", "delete_messages"); - const response = await model.invoke([ - { role: "system", content: `You are a helpful assistant.\n${memoriesText}` }, - ...state.messages, - ]); + const checkpointer = new MemorySaver(); + const app = builder.compile({ checkpointer }); - return { messages: [response] }; - }; + const config = { configurable: { thread_id: "1" } }; - const builder = new StateGraph(MessagesZodState) - .addNode("chat", chat) - .addEdge(START, "chat"); - const graph = builder.compile({ store }); + for await (const event of await app.stream( + { messages: [{ role: "user", content: "hi! I'm bob" }] }, + { ...config, streamMode: "values" } + )) { + console.log(event.messages.map(message => [message.getType(), message.content])); + } - for await (const [message, metadata] of await graph.stream( - { messages: [{ role: "user", content: "I'm hungry" }] }, - { streamMode: "messages" } - )) { - if (message.content) { - console.log(message.content); - } + for await (const event of await app.stream( + { messages: [{ role: "user", content: "what's my name?" }] }, + { ...config, streamMode: "values" } + )) { + console.log(event.messages.map(message => [message.getType(), message.content])); + } + ``` + ::: + + +#### Summarize messages + +The problem with trimming or removing messages is that you may lose information. Because of this, some applications benefit from a more sophisticated approach of summarizing the message history using a chat model. + +![](/oss/images/summary.png) + +:::python +Prompting and orchestration logic can be used to summarize the message history. For example, in LangGraph you can extend the [`MessagesState`](/oss/langgraph/graph-api#working-with-messages-in-graph-state) to include a `summary` key: + +```python +from langgraph.graph import MessagesState +class State(MessagesState): + summary: str +``` + +Then, you can generate a summary of the chat history, using any existing summary as context for the next summary. This `summarize_conversation` node can be called after some number of messages have accumulated in the `messages` state key. + +```python +def summarize_conversation(state: State): + + # First, we get any existing summary + summary = state.get("summary", "") + + # Create our summarization prompt + if summary: + + # A summary already exists + summary_message = ( + f"This is a summary of the conversation to date: {summary}\n\n" + "Extend the summary by taking into account the new messages above:" + ) + + else: + summary_message = "Create a summary of the conversation above:" + + # Add prompt to our history + messages = state["messages"] + [HumanMessage(content=summary_message)] + response = model.invoke(messages) + + # Delete all but the 2 most recent messages + delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]] + return {"summary": response.content, "messages": delete_messages} +``` +::: + +:::js +Prompting and orchestration logic can be used to summarize the message history. For example, in LangGraph you can include a `summary` key in the state alongside the `messages` key: + +```typescript +import { BaseMessage } from "@langchain/core/messages"; +import { MessagesZodMeta } from "@langchain/langgraph"; +import { registry } from "@langchain/langgraph/zod"; +import * as z from "zod"; + +const State = z.object({ + messages: z + .array(z.custom()) + .register(registry, MessagesZodMeta), + summary: z.string().optional(), +}); +``` + +Then, you can generate a summary of the chat history, using any existing summary as context for the next summary. This `summarizeConversation` node can be called after some number of messages have accumulated in the `messages` state key. + +```typescript +import { RemoveMessage, HumanMessage } from "@langchain/core/messages"; + +const summarizeConversation = async (state: z.infer) => { + // First, we get any existing summary + const summary = state.summary || ""; + + // Create our summarization prompt + let summaryMessage: string; + if (summary) { + // A summary already exists + summaryMessage = + `This is a summary of the conversation to date: ${summary}\n\n` + + "Extend the summary by taking into account the new messages above:"; + } else { + summaryMessage = "Create a summary of the conversation above:"; + } + + // Add prompt to our history + const messages = [ + ...state.messages, + new HumanMessage({ content: summaryMessage }) + ]; + const response = await model.invoke(messages); + + // Delete all but the 2 most recent messages + const deleteMessages = state.messages + .slice(0, -2) + .map(m => new RemoveMessage({ id: m.id })); + + return { + summary: response.content, + messages: deleteMessages + }; +}; +``` +::: + + + :::python + ```python + from typing import Any, TypedDict + + from langchain.chat_models import init_chat_model + from langchain.messages import AnyMessage + from langchain_core.messages.utils import count_tokens_approximately + from langgraph.graph import StateGraph, START, MessagesState + from langgraph.checkpoint.memory import InMemorySaver + from langmem.short_term import SummarizationNode, RunningSummary + + model = init_chat_model("claude-sonnet-4-5-20250929") + summarization_model = model.bind(max_tokens=128) + + class State(MessagesState): + context: dict[str, RunningSummary] + + class LLMInputState(TypedDict): + summarized_messages: list[AnyMessage] + context: dict[str, RunningSummary] + + summarization_node = SummarizationNode( + token_counter=count_tokens_approximately, + model=summarization_model, + max_tokens=256, + max_tokens_before_summary=256, + max_summary_tokens=128, + ) + + def call_model(state: LLMInputState): + response = model.invoke(state["summarized_messages"]) + return {"messages": [response]} + + checkpointer = InMemorySaver() + builder = StateGraph(State) + builder.add_node(call_model) + builder.add_node("summarize", summarization_node) + builder.add_edge(START, "summarize") + builder.add_edge("summarize", "call_model") + graph = builder.compile(checkpointer=checkpointer) + + # Invoke the graph + config = {"configurable": {"thread_id": "1"}} + graph.invoke({"messages": "hi, my name is bob"}, config) + graph.invoke({"messages": "write a short poem about cats"}, config) + graph.invoke({"messages": "now do the same but for dogs"}, config) + final_response = graph.invoke({"messages": "what's my name?"}, config) + + final_response["messages"][-1].pretty_print() + print("\nSummary:", final_response["context"]["running_summary"].summary) + ``` + ::: + + :::js + ```typescript + import { ChatAnthropic } from "@langchain/anthropic"; + import { + SystemMessage, + HumanMessage, + RemoveMessage, + type BaseMessage + } from "@langchain/core/messages"; + import { + MessagesZodMeta, + StateGraph, + START, + END, + MemorySaver, + } from "@langchain/langgraph"; + import { registry } from "@langchain/langgraph/zod"; + import * as z from "zod"; + import { v4 as uuidv4 } from "uuid"; + + const memory = new MemorySaver(); + + const GraphState = z.object({ + messages: z + .array(z.custom()) + .register(registry, MessagesZodMeta), + summary: z.string().default(""), + }); + + const model = new ChatAnthropic({ model: "claude-haiku-4-5-20251001" }); + + const callModel = async (state: z.infer) => { + const { summary } = state; + let { messages } = state; + if (summary) { + const systemMessage = new SystemMessage({ + id: uuidv4(), + content: `Summary of conversation earlier: ${summary}`, + }); + messages = [systemMessage, ...messages]; } - ``` - ::: + const response = await model.invoke(messages); + return { messages: [response] }; + }; + + const shouldContinue = (state: z.infer) => { + const messages = state.messages; + if (messages.length > 6) { + return "summarize_conversation"; + } + return END; + }; + + const summarizeConversation = async (state: z.infer) => { + const { summary, messages } = state; + let summaryMessage: string; + if (summary) { + summaryMessage = + `This is summary of the conversation to date: ${summary}\n\n` + + "Extend the summary by taking into account the new messages above:"; + } else { + summaryMessage = "Create a summary of the conversation above:"; + } + + const allMessages = [ + ...messages, + new HumanMessage({ id: uuidv4(), content: summaryMessage }), + ]; + + const response = await model.invoke(allMessages); + + const deleteMessages = messages + .slice(0, -2) + .map((m) => new RemoveMessage({ id: m.id! })); + + if (typeof response.content !== "string") { + throw new Error("Expected a string response from the model"); + } + + return { summary: response.content, messages: deleteMessages }; + }; + + const workflow = new StateGraph(GraphState) + .addNode("conversation", callModel) + .addNode("summarize_conversation", summarizeConversation) + .addEdge(START, "conversation") + .addConditionalEdges("conversation", shouldContinue) + .addEdge("summarize_conversation", END); + + const app = workflow.compile({ checkpointer: memory }); + ``` + ::: + + +#### Manage checkpoints + +You can view and delete the information stored by the checkpointer. + + + :::python + + + ```python + config = { + "configurable": { + "thread_id": "1", + # optionally provide an ID for a specific checkpoint, + # otherwise the latest checkpoint is shown + # "checkpoint_id": "1f029ca3-1f5b-6704-8004-820c16b69a5a" + + } + } + graph.get_state(config) + ``` + + ``` + StateSnapshot( + values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today?), HumanMessage(content="what's my name?"), AIMessage(content='Your name is Bob.')]}, next=(), + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1f5b-6704-8004-820c16b69a5a'}}, + metadata={ + 'source': 'loop', + 'writes': {'call_model': {'messages': AIMessage(content='Your name is Bob.')}}, + 'step': 4, + 'parents': {}, + 'thread_id': '1' + }, + created_at='2025-05-05T16:01:24.680462+00:00', + parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, + tasks=(), + interrupts=() + ) + ``` + + + ```python + config = { + "configurable": { + "thread_id": "1", + # optionally provide an ID for a specific checkpoint, + # otherwise the latest checkpoint is shown + # "checkpoint_id": "1f029ca3-1f5b-6704-8004-820c16b69a5a" + + } + } + checkpointer.get_tuple(config) + ``` + + ``` + CheckpointTuple( + config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1f5b-6704-8004-820c16b69a5a'}}, + checkpoint={ + 'v': 3, + 'ts': '2025-05-05T16:01:24.680462+00:00', + 'id': '1f029ca3-1f5b-6704-8004-820c16b69a5a', + 'channel_versions': {'__start__': '00000000000000000000000000000005.0.5290678567601859', 'messages': '00000000000000000000000000000006.0.3205149138784782', 'branch:to:call_model': '00000000000000000000000000000006.0.14611156755133758'}, 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000004.0.5736472536395331'}, 'call_model': {'branch:to:call_model': '00000000000000000000000000000005.0.1410174088651449'}}, + 'channel_values': {'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today?), HumanMessage(content="what's my name?"), AIMessage(content='Your name is Bob.')]}, + }, + metadata={ + 'source': 'loop', + 'writes': {'call_model': {'messages': AIMessage(content='Your name is Bob.')}}, + 'step': 4, + 'parents': {}, + 'thread_id': '1' + }, + parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, + pending_writes=[] + ) + ``` + + + ::: + + :::js + ```typescript + const config = { + configurable: { + thread_id: "1", + // optionally provide an ID for a specific checkpoint, + // otherwise the latest checkpoint is shown + // checkpoint_id: "1f029ca3-1f5b-6704-8004-820c16b69a5a" + }, + }; + await graph.getState(config); + ``` + + ``` + { + values: { messages: [HumanMessage(...), AIMessage(...), HumanMessage(...), AIMessage(...)] }, + next: [], + config: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1f029ca3-1f5b-6704-8004-820c16b69a5a' } }, + metadata: { + source: 'loop', + writes: { call_model: { messages: AIMessage(...) } }, + step: 4, + parents: {}, + thread_id: '1' + }, + createdAt: '2025-05-05T16:01:24.680462+00:00', + parentConfig: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1f029ca3-1790-6b0a-8003-baf965b6a38f' } }, + tasks: [], + interrupts: [] + } + ``` + ::: + + + + :::python + + + ```python + config = { + "configurable": { + "thread_id": "1" + } + } + list(graph.get_state_history(config)) + ``` + + ``` + [ + StateSnapshot(...), + StateSnapshot(...), + StateSnapshot(...), + # ... more checkpoints + ] + ``` + + + ```python + config = { + "configurable": { + "thread_id": "1" + } + } + list(checkpointer.list(config)) + ``` + + ``` + [ + CheckpointTuple(...), + CheckpointTuple(...), + CheckpointTuple(...), + # ... more checkpoints + ] + ``` + + + ::: + + :::js + ```typescript + const config = { + configurable: { + thread_id: "1", + }, + }; + + const history = []; + for await (const state of graph.getStateHistory(config)) { + history.push(state); + } + ``` + ::: -## Manage short-term memory + + :::python + ```python + thread_id = "1" + checkpointer.delete_thread(thread_id) + ``` + ::: -With [short-term memory](#add-short-term-memory) enabled, long conversations can exceed the LLM's context window. Common solutions are: + :::js + ```typescript + const threadId = "1"; + await checkpointer.deleteThread(threadId); + ``` + ::: + -* [Trim messages](#trim-messages): Remove first or last N messages (before calling LLM) -* [Delete messages](#delete-messages) from LangGraph state permanently -* [Summarize messages](#summarize-messages): Summarize earlier messages in the history and replace them with a summary -* [Manage checkpoints](#manage-checkpoints) to store and retrieve message history -* Custom strategies (e.g., message filtering, etc.) +### Checkpoint data -This allows the agent to keep track of the conversation without exceeding the LLM's context window. +Checkpointers need to serialize state when saving it to storage. LangGraph provides flexible serialization options and supports encryption for sensitive data. -### Trim messages +#### Serialization :::python -Most LLMs have a maximum supported context window (denominated in tokens). One way to decide when to truncate messages is to count the tokens in the message history and truncate whenever it approaches that limit. If you're using LangChain, you can use the trim messages utility and specify the number of tokens to keep from the list, as well as the `strategy` (e.g., keep the last `max_tokens`) to use for handling the boundary. -::: -:::js -Most LLMs have a maximum supported context window (denominated in tokens). One way to decide when to truncate messages is to count the tokens in the message history and truncate whenever it approaches that limit. If you're using LangChain, you can use the trim messages utility and specify the number of tokens to keep from the list, as well as the `strategy` (e.g., keep the last `maxTokens`) to use for handling the boundary. -::: +Checkpointers use `JsonPlusSerializer` by default, which handles: +- LangChain and LangGraph primitives +- Python datetimes +- Enums +- Common Python types -:::python -To trim message history, use the [`trim_messages`](https://python.langchain.com/api_reference/core/messages/langchain_core.messages.utils.trim_messages.html) function: +For objects not supported by the default serializer (e.g., Pandas dataframes), use pickle fallback: ```python -from langchain_core.messages.utils import ( # [!code highlight] - trim_messages, # [!code highlight] - count_tokens_approximately # [!code highlight] -) # [!code highlight] - -def call_model(state: MessagesState): - messages = trim_messages( # [!code highlight] - state["messages"], - strategy="last", - token_counter=count_tokens_approximately, - max_tokens=128, - start_on="human", - end_on=("human", "tool"), - ) - response = model.invoke(messages) - return {"messages": [response]} +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer -builder = StateGraph(MessagesState) -builder.add_node(call_model) -... +checkpointer = InMemorySaver( + serde=JsonPlusSerializer(pickle_fallback=True) +) ``` ::: :::js -To trim message history, use the [`trimMessages`](https://js.langchain.com/docs/how_to/trim_messages/) function: - -```typescript -import { trimMessages } from "@langchain/core/messages"; +Checkpointers include default serialization that handles: +- LangChain and LangGraph primitives +- JavaScript Date objects +- Common JavaScript types +::: -const callModel = async (state: z.infer) => { - const messages = trimMessages(state.messages, { - strategy: "last", - maxTokens: 128, - startOn: "human", - endOn: ["human", "tool"], - }); - const response = await model.invoke(messages); - return { messages: [response] }; -}; +#### Encryption -const builder = new StateGraph(MessagesZodState) - .addNode("call_model", callModel); -// ... -``` -::: +:::python +To encrypt all persisted state, pass an instance of @[`EncryptedSerializer`] to the `serde` argument of any checkpointer. The easiest way is via @[`from_pycryptodome_aes`], which reads the AES key from the `LANGGRAPH_AES_KEY` environment variable: - - :::python + + ```python - from langchain_core.messages.utils import ( - trim_messages, # [!code highlight] - count_tokens_approximately # [!code highlight] - ) - from langchain.chat_models import init_chat_model - from langgraph.graph import StateGraph, START, MessagesState + import sqlite3 + from langgraph.checkpoint.serde.encrypted import EncryptedSerializer + from langgraph.checkpoint.sqlite import SqliteSaver - model = init_chat_model("claude-sonnet-4-5-20250929") - summarization_model = model.bind(max_tokens=128) + serde = EncryptedSerializer.from_pycryptodome_aes() # reads LANGGRAPH_AES_KEY + checkpointer = SqliteSaver(sqlite3.connect("checkpoint.db"), serde=serde) + ``` + - def call_model(state: MessagesState): - messages = trim_messages( # [!code highlight] - state["messages"], - strategy="last", - token_counter=count_tokens_approximately, - max_tokens=128, - start_on="human", - end_on=("human", "tool"), - ) - response = model.invoke(messages) - return {"messages": [response]} + + ```python + from langgraph.checkpoint.serde.encrypted import EncryptedSerializer + from langgraph.checkpoint.postgres import PostgresSaver - checkpointer = InMemorySaver() - builder = StateGraph(MessagesState) - builder.add_node(call_model) - builder.add_edge(START, "call_model") - graph = builder.compile(checkpointer=checkpointer) + serde = EncryptedSerializer.from_pycryptodome_aes() # reads LANGGRAPH_AES_KEY + checkpointer = PostgresSaver.from_conn_string("postgresql://...", serde=serde) + checkpointer.setup() + ``` + + - config = {"configurable": {"thread_id": "1"}} - graph.invoke({"messages": "hi, my name is bob"}, config) - graph.invoke({"messages": "write a short poem about cats"}, config) - graph.invoke({"messages": "now do the same but for dogs"}, config) - final_response = graph.invoke({"messages": "what's my name?"}, config) + +When running on LangSmith, encryption is automatically enabled whenever `LANGGRAPH_AES_KEY` is present. Other encryption schemes can be used by implementing @[`CipherProtocol`] and supplying it to @[`EncryptedSerializer`]. + +::: - final_response["messages"][-1].pretty_print() -``` +--- - ``` - ================================== Ai Message ================================== +## Stores - Your name is Bob, as you mentioned when you first introduced yourself. - ``` - ::: +Stores enable long-term, cross-thread memory for storing user-specific or application-level data that should persist across multiple conversations or workflow sessions. - :::js - ```typescript - import { trimMessages, BaseMessage } from "@langchain/core/messages"; - import { ChatAnthropic } from "@langchain/anthropic"; - import { StateGraph, START, MessagesZodMeta, MemorySaver } from "@langchain/langgraph"; - import { registry } from "@langchain/langgraph/zod"; - import * as z from "zod"; +![Model of shared state](/oss/images/shared_state.png) - const MessagesZodState = z.object({ - messages: z - .array(z.custom()) - .register(registry, MessagesZodMeta), - }); +While [checkpointers](#checkpointers) save state to a specific thread, stores allow you to share information **across threads**. For example, you might want to remember a user's preferences or facts about them across all of their conversations with your agent. - const model = new ChatAnthropic({ model: "claude-3-5-sonnet-20241022" }); +### Add stores - const callModel = async (state: z.infer) => { - const messages = trimMessages(state.messages, { - strategy: "last", - maxTokens: 128, - startOn: "human", - endOn: ["human", "tool"], - tokenCounter: model, - }); - const response = await model.invoke(messages); - return { messages: [response] }; - }; +:::python +```python +from langgraph.store.memory import InMemoryStore +from langgraph.graph import StateGraph - const checkpointer = new MemorySaver(); - const builder = new StateGraph(MessagesZodState) - .addNode("call_model", callModel) - .addEdge(START, "call_model"); - const graph = builder.compile({ checkpointer }); +store = InMemoryStore() - const config = { configurable: { thread_id: "1" } }; - await graph.invoke({ messages: [{ role: "user", content: "hi, my name is bob" }] }, config); - await graph.invoke({ messages: [{ role: "user", content: "write a short poem about cats" }] }, config); - await graph.invoke({ messages: [{ role: "user", content: "now do the same but for dogs" }] }, config); - const finalResponse = await graph.invoke({ messages: [{ role: "user", content: "what's my name?" }] }, config); +builder = StateGraph(...) +graph = builder.compile(store=store) +``` +::: - console.log(finalResponse.messages.at(-1)?.content); - ``` +:::js +```typescript +import { InMemoryStore, StateGraph } from "@langchain/langgraph"; - ``` - Your name is Bob, as you mentioned when you first introduced yourself. - ``` - ::: - +const store = new InMemoryStore(); -### Delete messages +const builder = new StateGraph(...); +const graph = builder.compile({ store }); +``` +::: -You can delete messages from the graph state to manage the message history. This is useful when you want to remove specific messages or clear the entire message history. +Stores organize data using **namespaces** - tuples that help you categorize and retrieve information: :::python -To delete messages from the graph state, you can use the `RemoveMessage`. For `RemoveMessage` to work, you need to use a state key with @[`add_messages`] [reducer](/oss/langgraph/graph-api#reducers), like [`MessagesState`](/oss/langgraph/graph-api#messagesstate). - -To remove specific messages: - ```python -from langchain.messages import RemoveMessage # [!code highlight] +# Store user preferences +user_id = "123" +namespace = (user_id, "preferences") +store.put(namespace, "theme", {"value": "dark"}) + +# Store user facts +namespace = (user_id, "facts") +store.put(namespace, "fact_1", {"text": "Likes pizza"}) +``` +::: -def delete_messages(state): - messages = state["messages"] - if len(messages) > 2: - # remove the earliest two messages - return {"messages": [RemoveMessage(id=m.id) for m in messages[:2]]} # [!code highlight] +:::js +```typescript +// Store user preferences +const userId = "123"; +let namespace = [userId, "preferences"]; +await store.put(namespace, "theme", { value: "dark" }); + +// Store user facts +namespace = [userId, "facts"]; +await store.put(namespace, "fact_1", { text: "Likes pizza" }); ``` +::: + +#### Use in production -To remove **all** messages: +In production, use a store backed by a database: +:::python ```python -from langgraph.graph.message import REMOVE_ALL_MESSAGES # [!code highlight] +from langgraph.store.postgres import PostgresStore -def delete_messages(state): - return {"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES)]} # [!code highlight] +DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" +with PostgresStore.from_conn_string(DB_URI) as store: + builder = StateGraph(...) + graph = builder.compile(store=store) ``` ::: :::js -To delete messages from the graph state, you can use the `RemoveMessage`. For `RemoveMessage` to work, you need to use a state key with @[`messagesStateReducer`][messagesStateReducer] [reducer](/oss/langgraph/graph-api#reducers), like `MessagesZodState`. - -To remove specific messages: - ```typescript -import { RemoveMessage } from "@langchain/core/messages"; +import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres/store"; -const deleteMessages = (state) => { - const messages = state.messages; - if (messages.length > 2) { - // remove the earliest two messages - return { - messages: messages - .slice(0, 2) - .map((m) => new RemoveMessage({ id: m.id })), - }; - } -}; +const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"; +const store = PostgresStore.fromConnString(DB_URI); + +const builder = new StateGraph(...); +const graph = builder.compile({ store }); ``` ::: - -When deleting messages, **make sure** that the resulting message history is valid. Check the limitations of the LLM provider you're using. For example: + + :::python + ``` + pip install -U "psycopg[binary,pool]" langgraph langgraph-checkpoint-postgres + ``` + + + You need to call `store.setup()` the first time you're using Postgres store + + + + + ```python + from langchain_core.runnables import RunnableConfig + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.checkpoint.postgres import PostgresSaver + from langgraph.store.postgres import PostgresStore + from langgraph.store.base import BaseStore + import uuid + + model = init_chat_model(model="claude-haiku-4-5-20251001") + + DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" + + with ( + PostgresStore.from_conn_string(DB_URI) as store, + PostgresSaver.from_conn_string(DB_URI) as checkpointer, + ): + # store.setup() + # checkpointer.setup() + + def call_model( + state: MessagesState, + config: RunnableConfig, + *, + store: BaseStore, + ): + user_id = config["configurable"]["user_id"] + namespace = ("memories", user_id) + memories = store.search(namespace, query=str(state["messages"][-1].content)) + info = "\n".join([d.value["data"] for d in memories]) + system_msg = f"You are a helpful assistant talking to the user. User info: {info}" + + # Store new memories if the user asks the model to remember + last_message = state["messages"][-1] + if "remember" in last_message.content.lower(): + memory = "User name is Bob" + store.put(namespace, str(uuid.uuid4()), {"data": memory}) + + response = model.invoke( + [{"role": "system", "content": system_msg}] + state["messages"] + ) + return {"messages": response} + + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") + + graph = builder.compile( + checkpointer=checkpointer, + store=store, + ) + + config = { + "configurable": { + "thread_id": "1", + "user_id": "1", + } + } + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, + config, + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() + + config = { + "configurable": { + "thread_id": "2", + "user_id": "1", + } + } + + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "what is my name?"}]}, + config, + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() +``` + + + ```python + from langchain_core.runnables import RunnableConfig + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver + from langgraph.store.postgres.aio import AsyncPostgresStore + from langgraph.store.base import BaseStore + import uuid + + model = init_chat_model(model="claude-haiku-4-5-20251001") -* Some providers expect message history to start with a `user` message -* Most providers require `assistant` messages with tool calls to be followed by corresponding `tool` result messages. - + DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" - - :::python - ```python - from langchain.messages import RemoveMessage # [!code highlight] + async with ( + AsyncPostgresStore.from_conn_string(DB_URI) as store, + AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer, + ): + # await store.setup() + # await checkpointer.setup() - def delete_messages(state): - messages = state["messages"] - if len(messages) > 2: - # remove the earliest two messages - return {"messages": [RemoveMessage(id=m.id) for m in messages[:2]]} # [!code highlight] + async def call_model( + state: MessagesState, + config: RunnableConfig, + *, + store: BaseStore, + ): + user_id = config["configurable"]["user_id"] + namespace = ("memories", user_id) + memories = await store.asearch(namespace, query=str(state["messages"][-1].content)) + info = "\n".join([d.value["data"] for d in memories]) + system_msg = f"You are a helpful assistant talking to the user. User info: {info}" - def call_model(state: MessagesState): - response = model.invoke(state["messages"]) - return {"messages": response} + # Store new memories if the user asks the model to remember + last_message = state["messages"][-1] + if "remember" in last_message.content.lower(): + memory = "User name is Bob" + await store.aput(namespace, str(uuid.uuid4()), {"data": memory}) - builder = StateGraph(MessagesState) - builder.add_sequence([call_model, delete_messages]) - builder.add_edge(START, "call_model") + response = await model.ainvoke( + [{"role": "system", "content": system_msg}] + state["messages"] + ) + return {"messages": response} - checkpointer = InMemorySaver() - app = builder.compile(checkpointer=checkpointer) + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") - for event in app.stream( - {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, - config, - stream_mode="values" - ): - print([(message.type, message.content) for message in event["messages"]]) + graph = builder.compile( + checkpointer=checkpointer, + store=store, + ) - for event in app.stream( - {"messages": [{"role": "user", "content": "what's my name?"}]}, - config, - stream_mode="values" - ): - print([(message.type, message.content) for message in event["messages"]]) + config = { + "configurable": { + "thread_id": "1", + "user_id": "1", + } + } + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, + config, + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() + + config = { + "configurable": { + "thread_id": "2", + "user_id": "1", + } + } + + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "what is my name?"}]}, + config, + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() ``` + + + ::: + :::js ``` - [('human', "hi! I'm bob")] - [('human', "hi! I'm bob"), ('ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?')] - [('human', "hi! I'm bob"), ('ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?'), ('human', "what's my name?")] - [('human', "hi! I'm bob"), ('ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?'), ('human', "what's my name?"), ('ai', 'Your name is Bob.')] - [('human', "what's my name?"), ('ai', 'Your name is Bob.')] + npm install @langchain/langgraph-checkpoint-postgres ``` - ::: - :::js + + You need to call `store.setup()` the first time you're using Postgres store + + ```typescript - import { RemoveMessage, BaseMessage } from "@langchain/core/messages"; import { ChatAnthropic } from "@langchain/anthropic"; - import { StateGraph, START, MemorySaver, MessagesZodMeta } from "@langchain/langgraph"; - import * as z from "zod"; + import { StateGraph, MessagesZodMeta, START, LangGraphRunnableConfig } from "@langchain/langgraph"; + import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres"; + import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres/store"; + import { BaseMessage } from "@langchain/core/messages"; import { registry } from "@langchain/langgraph/zod"; + import * as z from "zod"; + import { v4 as uuidv4 } from "uuid"; const MessagesZodState = z.object({ messages: z @@ -1460,680 +2198,522 @@ When deleting messages, **make sure** that the resulting message history is vali .register(registry, MessagesZodMeta), }); - const model = new ChatAnthropic({ model: "claude-3-5-sonnet-20241022" }); + const model = new ChatAnthropic({ model: "claude-haiku-4-5-20251001" }); - const deleteMessages = (state: z.infer) => { - const messages = state.messages; - if (messages.length > 2) { - // remove the earliest two messages - return { messages: messages.slice(0, 2).map(m => new RemoveMessage({ id: m.id })) }; + const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"; + + const store = PostgresStore.fromConnString(DB_URI); + const checkpointer = PostgresSaver.fromConnString(DB_URI); + // await store.setup(); + // await checkpointer.setup(); + + const callModel = async ( + state: z.infer, + config: LangGraphRunnableConfig, + ) => { + const userId = config.configurable?.userId; + const namespace = ["memories", userId]; + const memories = await config.store?.search(namespace, { query: state.messages.at(-1)?.content }); + const info = memories?.map(d => d.value.data).join("\n") || ""; + const systemMsg = `You are a helpful assistant talking to the user. User info: ${info}`; + + // Store new memories if the user asks the model to remember + const lastMessage = state.messages.at(-1); + if (lastMessage?.content?.toLowerCase().includes("remember")) { + const memory = "User name is Bob"; + await config.store?.put(namespace, uuidv4(), { data: memory }); } - return {}; - }; - const callModel = async (state: z.infer) => { - const response = await model.invoke(state.messages); + const response = await model.invoke([ + { role: "system", content: systemMsg }, + ...state.messages + ]); return { messages: [response] }; }; const builder = new StateGraph(MessagesZodState) .addNode("call_model", callModel) - .addNode("delete_messages", deleteMessages) - .addEdge(START, "call_model") - .addEdge("call_model", "delete_messages"); + .addEdge(START, "call_model"); - const checkpointer = new MemorySaver(); - const app = builder.compile({ checkpointer }); + const graph = builder.compile({ + checkpointer, + store, + }); - const config = { configurable: { thread_id: "1" } }; + const config = { + configurable: { + thread_id: "1", + userId: "1", + } + }; - for await (const event of await app.stream( - { messages: [{ role: "user", content: "hi! I'm bob" }] }, + for await (const chunk of await graph.stream( + { messages: [{ role: "user", content: "Hi! Remember: my name is Bob" }] }, { ...config, streamMode: "values" } )) { - console.log(event.messages.map(message => [message.getType(), message.content])); + console.log(chunk.messages.at(-1)?.content); } - for await (const event of await app.stream( - { messages: [{ role: "user", content: "what's my name?" }] }, - { ...config, streamMode: "values" } + const config2 = { + configurable: { + thread_id: "2", + userId: "1", + } + }; + + for await (const chunk of await graph.stream( + { messages: [{ role: "user", content: "what is my name?" }] }, + { ...config2, streamMode: "values" } )) { - console.log(event.messages.map(message => [message.getType(), message.content])); + console.log(chunk.messages.at(-1)?.content); } ``` - - ``` - [['human', "hi! I'm bob"]] - [['human', "hi! I'm bob"], ['ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?']] - [['human', "hi! I'm bob"], ['ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?'], ['human', "what's my name?"]] - [['human', "hi! I'm bob"], ['ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?'], ['human', "what's my name?"], ['ai', 'Your name is Bob.']] - [['human', "what's my name?"], ['ai', 'Your name is Bob.']] - ``` ::: -### Summarize messages - -The problem with trimming or removing messages, as shown above, is that you may lose information from culling of the message queue. Because of this, some applications benefit from a more sophisticated approach of summarizing the message history using a chat model. - -![](/oss/images/summary.png) - - :::python -Prompting and orchestration logic can be used to summarize the message history. For example, in LangGraph you can extend the [`MessagesState`](/oss/langgraph/graph-api#working-with-messages-in-graph-state) to include a `summary` key: - -```python -from langgraph.graph import MessagesState -class State(MessagesState): - summary: str -``` - -Then, you can generate a summary of the chat history, using any existing summary as context for the next summary. This `summarize_conversation` node can be called after some number of messages have accumulated in the `messages` state key. - -```python -def summarize_conversation(state: State): - - # First, we get any existing summary - summary = state.get("summary", "") - - # Create our summarization prompt - if summary: - - # A summary already exists - summary_message = ( - f"This is a summary of the conversation to date: {summary}\n\n" - "Extend the summary by taking into account the new messages above:" - ) - - else: - summary_message = "Create a summary of the conversation above:" - - # Add prompt to our history - messages = state["messages"] + [HumanMessage(content=summary_message)] - response = model.invoke(messages) - - # Delete all but the 2 most recent messages - delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]] - return {"summary": response.content, "messages": delete_messages} -``` -::: - -:::js -Prompting and orchestration logic can be used to summarize the message history. For example, in LangGraph you can include a `summary` key in the state alongside the `messages` key: + + ``` + pip install -U langgraph langgraph-checkpoint-redis + ``` -```typescript -import { BaseMessage } from "@langchain/core/messages"; -import { MessagesZodMeta } from "@langchain/langgraph"; -import { registry } from "@langchain/langgraph/zod"; -import * as z from "zod"; + + You need to call `store.setup()` the first time you're using Redis store + -const State = z.object({ - messages: z - .array(z.custom()) - .register(registry, MessagesZodMeta), - summary: z.string().optional(), -}); -``` + + + ```python + from langchain_core.runnables import RunnableConfig + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.checkpoint.redis import RedisSaver + from langgraph.store.redis import RedisStore + from langgraph.store.base import BaseStore + import uuid -Then, you can generate a summary of the chat history, using any existing summary as context for the next summary. This `summarizeConversation` node can be called after some number of messages have accumulated in the `messages` state key. + model = init_chat_model(model="claude-haiku-4-5-20251001") -```typescript -import { RemoveMessage, HumanMessage } from "@langchain/core/messages"; + DB_URI = "redis://localhost:6379" -const summarizeConversation = async (state: z.infer) => { - // First, we get any existing summary - const summary = state.summary || ""; + with ( + RedisStore.from_conn_string(DB_URI) as store, + RedisSaver.from_conn_string(DB_URI) as checkpointer, + ): + store.setup() + checkpointer.setup() - // Create our summarization prompt - let summaryMessage: string; - if (summary) { - // A summary already exists - summaryMessage = - `This is a summary of the conversation to date: ${summary}\n\n` + - "Extend the summary by taking into account the new messages above:"; - } else { - summaryMessage = "Create a summary of the conversation above:"; - } + def call_model( + state: MessagesState, + config: RunnableConfig, + *, + store: BaseStore, + ): + user_id = config["configurable"]["user_id"] + namespace = ("memories", user_id) + memories = store.search(namespace, query=str(state["messages"][-1].content)) + info = "\n".join([d.value["data"] for d in memories]) + system_msg = f"You are a helpful assistant talking to the user. User info: {info}" - // Add prompt to our history - const messages = [ - ...state.messages, - new HumanMessage({ content: summaryMessage }) - ]; - const response = await model.invoke(messages); + # Store new memories if the user asks the model to remember + last_message = state["messages"][-1] + if "remember" in last_message.content.lower(): + memory = "User name is Bob" + store.put(namespace, str(uuid.uuid4()), {"data": memory}) - // Delete all but the 2 most recent messages - const deleteMessages = state.messages - .slice(0, -2) - .map(m => new RemoveMessage({ id: m.id })); + response = model.invoke( + [{"role": "system", "content": system_msg}] + state["messages"] + ) + return {"messages": response} - return { - summary: response.content, - messages: deleteMessages - }; -}; -``` -::: + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") - - :::python - ```python - from typing import Any, TypedDict + graph = builder.compile( + checkpointer=checkpointer, + store=store, + ) - from langchain.chat_models import init_chat_model - from langchain.messages import AnyMessage - from langchain_core.messages.utils import count_tokens_approximately - from langgraph.graph import StateGraph, START, MessagesState - from langgraph.checkpoint.memory import InMemorySaver - from langmem.short_term import SummarizationNode, RunningSummary # [!code highlight] + config = { + "configurable": { + "thread_id": "1", + "user_id": "1", + } + } + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, + config, + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() - model = init_chat_model("claude-sonnet-4-5-20250929") - summarization_model = model.bind(max_tokens=128) + config = { + "configurable": { + "thread_id": "2", + "user_id": "1", + } + } - class State(MessagesState): - context: dict[str, RunningSummary] # [!code highlight] + for chunk in graph.stream( + {"messages": [{"role": "user", "content": "what is my name?"}]}, + config, + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() +``` + + + ```python + from langchain_core.runnables import RunnableConfig + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.checkpoint.redis.aio import AsyncRedisSaver + from langgraph.store.redis.aio import AsyncRedisStore + from langgraph.store.base import BaseStore + import uuid - class LLMInputState(TypedDict): # [!code highlight] - summarized_messages: list[AnyMessage] - context: dict[str, RunningSummary] + model = init_chat_model(model="claude-haiku-4-5-20251001") - summarization_node = SummarizationNode( # [!code highlight] - token_counter=count_tokens_approximately, - model=summarization_model, - max_tokens=256, - max_tokens_before_summary=256, - max_summary_tokens=128, - ) + DB_URI = "redis://localhost:6379" - def call_model(state: LLMInputState): # [!code highlight] - response = model.invoke(state["summarized_messages"]) - return {"messages": [response]} + async with ( + AsyncRedisStore.from_conn_string(DB_URI) as store, + AsyncRedisSaver.from_conn_string(DB_URI) as checkpointer, + ): + # await store.setup() + # await checkpointer.asetup() - checkpointer = InMemorySaver() - builder = StateGraph(State) - builder.add_node(call_model) - builder.add_node("summarize", summarization_node) # [!code highlight] - builder.add_edge(START, "summarize") - builder.add_edge("summarize", "call_model") - graph = builder.compile(checkpointer=checkpointer) + async def call_model( + state: MessagesState, + config: RunnableConfig, + *, + store: BaseStore, + ): + user_id = config["configurable"]["user_id"] + namespace = ("memories", user_id) + memories = await store.asearch(namespace, query=str(state["messages"][-1].content)) + info = "\n".join([d.value["data"] for d in memories]) + system_msg = f"You are a helpful assistant talking to the user. User info: {info}" - # Invoke the graph - config = {"configurable": {"thread_id": "1"}} - graph.invoke({"messages": "hi, my name is bob"}, config) - graph.invoke({"messages": "write a short poem about cats"}, config) - graph.invoke({"messages": "now do the same but for dogs"}, config) - final_response = graph.invoke({"messages": "what's my name?"}, config) + # Store new memories if the user asks the model to remember + last_message = state["messages"][-1] + if "remember" in last_message.content.lower(): + memory = "User name is Bob" + await store.aput(namespace, str(uuid.uuid4()), {"data": memory}) - final_response["messages"][-1].pretty_print() - print("\nSummary:", final_response["context"]["running_summary"].summary) -``` + response = await model.ainvoke( + [{"role": "system", "content": system_msg}] + state["messages"] + ) + return {"messages": response} - 1. We will keep track of our running summary in the `context` field + builder = StateGraph(MessagesState) + builder.add_node(call_model) + builder.add_edge(START, "call_model") - (expected by the `SummarizationNode`). + graph = builder.compile( + checkpointer=checkpointer, + store=store, + ) - 1. Define private state that will be used only for filtering + config = { + "configurable": { + "thread_id": "1", + "user_id": "1", + } + } + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, + config, + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() - the inputs to `call_model` node. + config = { + "configurable": { + "thread_id": "2", + "user_id": "1", + } + } - 1. We're passing a private input state here to isolate the messages returned by the summarization node + async for chunk in graph.astream( + {"messages": [{"role": "user", "content": "what is my name?"}]}, + config, + stream_mode="values", + ): + chunk["messages"][-1].pretty_print() +``` + + + ::: + :::js ``` - ================================== Ai Message ================================== - - From our conversation, I can see that you introduced yourself as Bob. That's the name you shared with me when we began talking. - - Summary: In this conversation, I was introduced to Bob, who then asked me to write a poem about cats. I composed a poem titled "The Mystery of Cats" that captured cats' graceful movements, independent nature, and their special relationship with humans. Bob then requested a similar poem about dogs, so I wrote "The Joy of Dogs," which highlighted dogs' loyalty, enthusiasm, and loving companionship. Both poems were written in a similar style but emphasized the distinct characteristics that make each pet special. + npm install @langchain/langgraph-checkpoint-postgres ``` - ::: - :::js + + You need to call `store.setup()` the first time you're using Postgres store + + ```typescript import { ChatAnthropic } from "@langchain/anthropic"; - import { - SystemMessage, - HumanMessage, - RemoveMessage, - type BaseMessage - } from "@langchain/core/messages"; - import { - MessagesZodMeta, - StateGraph, - START, - END, - MemorySaver, - } from "@langchain/langgraph"; + import { StateGraph, MessagesZodMeta, START, LangGraphRunnableConfig } from "@langchain/langgraph"; + import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres"; + import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres/store"; import { BaseMessage } from "@langchain/core/messages"; import { registry } from "@langchain/langgraph/zod"; import * as z from "zod"; import { v4 as uuidv4 } from "uuid"; - const memory = new MemorySaver(); - - // We will add a `summary` attribute (in addition to `messages` key, - // which MessagesZodState already has) - const GraphState = z.object({ + const MessagesZodState = z.object({ messages: z .array(z.custom()) .register(registry, MessagesZodMeta), - summary: z.string().default(""), }); - // We will use this model for both the conversation and the summarization const model = new ChatAnthropic({ model: "claude-haiku-4-5-20251001" }); - // Define the logic to call the model - const callModel = async (state: z.infer) => { - // If a summary exists, we add this in as a system message - const { summary } = state; - let { messages } = state; - if (summary) { - const systemMessage = new SystemMessage({ - id: uuidv4(), - content: `Summary of conversation earlier: ${summary}`, - }); - messages = [systemMessage, ...messages]; - } - const response = await model.invoke(messages); - // We return an object, because this will get added to the existing state - return { messages: [response] }; - }; + const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"; - // We now define the logic for determining whether to end or summarize the conversation - const shouldContinue = (state: z.infer) => { - const messages = state.messages; - // If there are more than six messages, then we summarize the conversation - if (messages.length > 6) { - return "summarize_conversation"; - } - // Otherwise we can just end - return END; - }; + const store = PostgresStore.fromConnString(DB_URI); + const checkpointer = PostgresSaver.fromConnString(DB_URI); + // await store.setup(); + // await checkpointer.setup(); - const summarizeConversation = async (state: z.infer) => { - // First, we summarize the conversation - const { summary, messages } = state; - let summaryMessage: string; - if (summary) { - // If a summary already exists, we use a different system prompt - // to summarize it than if one didn't - summaryMessage = - `This is summary of the conversation to date: ${summary}\n\n` + - "Extend the summary by taking into account the new messages above:"; - } else { - summaryMessage = "Create a summary of the conversation above:"; + const callModel = async ( + state: z.infer, + config: LangGraphRunnableConfig, + ) => { + const userId = config.configurable?.userId; + const namespace = ["memories", userId]; + const memories = await config.store?.search(namespace, { query: state.messages.at(-1)?.content }); + const info = memories?.map(d => d.value.data).join("\n") || ""; + const systemMsg = `You are a helpful assistant talking to the user. User info: ${info}`; + + // Store new memories if the user asks the model to remember + const lastMessage = state.messages.at(-1); + if (lastMessage?.content?.toLowerCase().includes("remember")) { + const memory = "User name is Bob"; + await config.store?.put(namespace, uuidv4(), { data: memory }); } - const allMessages = [ - ...messages, - new HumanMessage({ id: uuidv4(), content: summaryMessage }), - ]; + const response = await model.invoke([ + { role: "system", content: systemMsg }, + ...state.messages + ]); + return { messages: [response] }; + }; - const response = await model.invoke(allMessages); + const builder = new StateGraph(MessagesZodState) + .addNode("call_model", callModel) + .addEdge(START, "call_model"); - // We now need to delete messages that we no longer want to show up - // I will delete all but the last two messages, but you can change this - const deleteMessages = messages - .slice(0, -2) - .map((m) => new RemoveMessage({ id: m.id! })); + const graph = builder.compile({ + checkpointer, + store, + }); - if (typeof response.content !== "string") { - throw new Error("Expected a string response from the model"); + const config = { + configurable: { + thread_id: "1", + userId: "1", } - - return { summary: response.content, messages: deleteMessages }; }; - // Define a new graph - const workflow = new StateGraph(GraphState) - // Define the conversation node and the summarize node - .addNode("conversation", callModel) - .addNode("summarize_conversation", summarizeConversation) - // Set the entrypoint as conversation - .addEdge(START, "conversation") - // We now add a conditional edge - .addConditionalEdges( - // First, we define the start node. We use `conversation`. - // This means these are the edges taken after the `conversation` node is called. - "conversation", - // Next, we pass in the function that will determine which node is called next. - shouldContinue, - ) - // We now add a normal edge from `summarize_conversation` to END. - // This means that after `summarize_conversation` is called, we end. - .addEdge("summarize_conversation", END); + for await (const chunk of await graph.stream( + { messages: [{ role: "user", content: "Hi! Remember: my name is Bob" }] }, + { ...config, streamMode: "values" } + )) { + console.log(chunk.messages.at(-1)?.content); + } - // Finally, we compile it! - const app = workflow.compile({ checkpointer: memory }); + const config2 = { + configurable: { + thread_id: "2", + userId: "1", + } + }; + + for await (const chunk of await graph.stream( + { messages: [{ role: "user", content: "what is my name?" }] }, + { ...config2, streamMode: "values" } + )) { + console.log(chunk.messages.at(-1)?.content); + } ``` ::: -### Manage checkpoints - -You can view and delete the information stored by the checkpointer. +### Use semantic search - -#### View thread state +Enable semantic search in your graph's memory store to let graph agents search for items by semantic similarity instead of exact matches. :::python - - - ```python - config = { - "configurable": { - "thread_id": "1", # [!code highlight] - # optionally provide an ID for a specific checkpoint, - # otherwise the latest checkpoint is shown - # "checkpoint_id": "1f029ca3-1f5b-6704-8004-820c16b69a5a" # [!code highlight] +```python +from langchain.embeddings import init_embeddings +from langgraph.store.memory import InMemoryStore - } +# Create store with semantic search enabled +embeddings = init_embeddings("openai:text-embedding-3-small") +store = InMemoryStore( + index={ + "embed": embeddings, + "dims": 1536, } - graph.get_state(config) # [!code highlight] -``` +) - ``` - StateSnapshot( - values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today?), HumanMessage(content="what's my name?"), AIMessage(content='Your name is Bob.')]}, next=(), - config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1f5b-6704-8004-820c16b69a5a'}}, - metadata={ - 'source': 'loop', - 'writes': {'call_model': {'messages': AIMessage(content='Your name is Bob.')}}, - 'step': 4, - 'parents': {}, - 'thread_id': '1' - }, - created_at='2025-05-05T16:01:24.680462+00:00', - parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, - tasks=(), - interrupts=() - ) - ``` - - - ```python - config = { - "configurable": { - "thread_id": "1", # [!code highlight] - # optionally provide an ID for a specific checkpoint, - # otherwise the latest checkpoint is shown - # "checkpoint_id": "1f029ca3-1f5b-6704-8004-820c16b69a5a" # [!code highlight] +store.put(("user_123", "memories"), "1", {"text": "I love pizza"}) +store.put(("user_123", "memories"), "2", {"text": "I am a plumber"}) - } - } - checkpointer.get_tuple(config) # [!code highlight] +items = store.search( + ("user_123", "memories"), query="I'm hungry", limit=1 +) ``` - - ``` - CheckpointTuple( - config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1f5b-6704-8004-820c16b69a5a'}}, - checkpoint={ - 'v': 3, - 'ts': '2025-05-05T16:01:24.680462+00:00', - 'id': '1f029ca3-1f5b-6704-8004-820c16b69a5a', - 'channel_versions': {'__start__': '00000000000000000000000000000005.0.5290678567601859', 'messages': '00000000000000000000000000000006.0.3205149138784782', 'branch:to:call_model': '00000000000000000000000000000006.0.14611156755133758'}, 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000004.0.5736472536395331'}, 'call_model': {'branch:to:call_model': '00000000000000000000000000000005.0.1410174088651449'}}, - 'channel_values': {'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today?), HumanMessage(content="what's my name?"), AIMessage(content='Your name is Bob.')]}, - }, - metadata={ - 'source': 'loop', - 'writes': {'call_model': {'messages': AIMessage(content='Your name is Bob.')}}, - 'step': 4, - 'parents': {}, - 'thread_id': '1' - }, - parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, - pending_writes=[] - ) - ``` - - ::: :::js ```typescript -const config = { - configurable: { - thread_id: "1", - // optionally provide an ID for a specific checkpoint, - // otherwise the latest checkpoint is shown - // checkpoint_id: "1f029ca3-1f5b-6704-8004-820c16b69a5a" - }, -}; -await graph.getState(config); -``` +import { OpenAIEmbeddings } from "@langchain/openai"; +import { InMemoryStore } from "@langchain/langgraph"; -``` -{ - values: { messages: [HumanMessage(...), AIMessage(...), HumanMessage(...), AIMessage(...)] }, - next: [], - config: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1f029ca3-1f5b-6704-8004-820c16b69a5a' } }, - metadata: { - source: 'loop', - writes: { call_model: { messages: AIMessage(...) } }, - step: 4, - parents: {}, - thread_id: '1' +// Create store with semantic search enabled +const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small" }); +const store = new InMemoryStore({ + index: { + embeddings, + dims: 1536, }, - createdAt: '2025-05-05T16:01:24.680462+00:00', - parentConfig: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1f029ca3-1790-6b0a-8003-baf965b6a38f' } }, - tasks: [], - interrupts: [] -} +}); + +await store.put(["user_123", "memories"], "1", { text: "I love pizza" }); +await store.put(["user_123", "memories"], "2", { text: "I am a plumber" }); + +const items = await store.search(["user_123", "memories"], { + query: "I'm hungry", + limit: 1, +}); ``` ::: - -#### View the history of the thread + + :::python -:::python - - ```python - config = { - "configurable": { - "thread_id": "1" # [!code highlight] + + from langchain.embeddings import init_embeddings + from langchain.chat_models import init_chat_model + from langgraph.store.base import BaseStore + from langgraph.store.memory import InMemoryStore + from langgraph.graph import START, MessagesState, StateGraph + + model = init_chat_model("gpt-4o-mini") + + # Create store with semantic search enabled + embeddings = init_embeddings("openai:text-embedding-3-small") + store = InMemoryStore( + index={ + "embed": embeddings, + "dims": 1536, } - } - list(graph.get_state_history(config)) # [!code highlight] -``` + ) - ``` - [ - StateSnapshot( - values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?'), HumanMessage(content="what's my name?"), AIMessage(content='Your name is Bob.')]}, - next=(), - config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1f5b-6704-8004-820c16b69a5a'}}, - metadata={'source': 'loop', 'writes': {'call_model': {'messages': AIMessage(content='Your name is Bob.')}}, 'step': 4, 'parents': {}, 'thread_id': '1'}, - created_at='2025-05-05T16:01:24.680462+00:00', - parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, - tasks=(), - interrupts=() - ), - StateSnapshot( - values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?'), HumanMessage(content="what's my name?")]}, - next=('call_model',), - config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, - metadata={'source': 'loop', 'writes': None, 'step': 3, 'parents': {}, 'thread_id': '1'}, - created_at='2025-05-05T16:01:23.863421+00:00', - parent_config={...} - tasks=(PregelTask(id='8ab4155e-6b15-b885-9ce5-bed69a2c305c', name='call_model', path=('__pregel_pull', 'call_model'), error=None, interrupts=(), state=None, result={'messages': AIMessage(content='Your name is Bob.')}),), - interrupts=() - ), - StateSnapshot( - values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')]}, - next=('__start__',), - config={...}, - metadata={'source': 'input', 'writes': {'__start__': {'messages': [{'role': 'user', 'content': "what's my name?"}]}}, 'step': 2, 'parents': {}, 'thread_id': '1'}, - created_at='2025-05-05T16:01:23.863173+00:00', - parent_config={...} - tasks=(PregelTask(id='24ba39d6-6db1-4c9b-f4c5-682aeaf38dcd', name='__start__', path=('__pregel_pull', '__start__'), error=None, interrupts=(), state=None, result={'messages': [{'role': 'user', 'content': "what's my name?"}]}),), - interrupts=() - ), - StateSnapshot( - values={'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')]}, - next=(), - config={...}, - metadata={'source': 'loop', 'writes': {'call_model': {'messages': AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')}}, 'step': 1, 'parents': {}, 'thread_id': '1'}, - created_at='2025-05-05T16:01:23.862295+00:00', - parent_config={...} - tasks=(), - interrupts=() - ), - StateSnapshot( - values={'messages': [HumanMessage(content="hi! I'm bob")]}, - next=('call_model',), - config={...}, - metadata={'source': 'loop', 'writes': None, 'step': 0, 'parents': {}, 'thread_id': '1'}, - created_at='2025-05-05T16:01:22.278960+00:00', - parent_config={...} - tasks=(PregelTask(id='8cbd75e0-3720-b056-04f7-71ac805140a0', name='call_model', path=('__pregel_pull', 'call_model'), error=None, interrupts=(), state=None, result={'messages': AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')}),), - interrupts=() - ), - StateSnapshot( - values={'messages': []}, - next=('__start__',), - config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-0870-6ce2-bfff-1f3f14c3e565'}}, - metadata={'source': 'input', 'writes': {'__start__': {'messages': [{'role': 'user', 'content': "hi! I'm bob"}]}}, 'step': -1, 'parents': {}, 'thread_id': '1'}, - created_at='2025-05-05T16:01:22.277497+00:00', - parent_config=None, - tasks=(PregelTask(id='d458367b-8265-812c-18e2-33001d199ce6', name='__start__', path=('__pregel_pull', '__start__'), error=None, interrupts=(), state=None, result={'messages': [{'role': 'user', 'content': "hi! I'm bob"}]}),), - interrupts=() + store.put(("user_123", "memories"), "1", {"text": "I love pizza"}) + store.put(("user_123", "memories"), "2", {"text": "I am a plumber"}) + + def chat(state, *, store: BaseStore): + # Search based on user's last message + items = store.search( + ("user_123", "memories"), query=state["messages"][-1].content, limit=2 + ) + memories = "\n".join(item.value["text"] for item in items) + memories = f"## Memories of user\n{memories}" if memories else "" + response = model.invoke( + [ + {"role": "system", "content": f"You are a helpful assistant.\n{memories}"}, + *state["messages"], + ] ) - ] + return {"messages": [response]} + + + builder = StateGraph(MessagesState) + builder.add_node(chat) + builder.add_edge(START, "chat") + graph = builder.compile(store=store) + + for message, metadata in graph.stream( + input={"messages": [{"role": "user", "content": "I'm hungry"}]}, + stream_mode="messages", + ): + print(message.content, end="") ``` - - - ```python - config = { - "configurable": { - "thread_id": "1" # [!code highlight] + ::: + + :::js + + ```typescript + import { OpenAIEmbeddings, ChatOpenAI } from "@langchain/openai"; + import { StateGraph, START, MessagesZodMeta, InMemoryStore } from "@langchain/langgraph"; + import { BaseMessage } from "@langchain/core/messages"; + import { registry } from "@langchain/langgraph/zod"; + import * as z from "zod"; + + const MessagesZodState = z.object({ + messages: z + .array(z.custom()) + .register(registry, MessagesZodMeta), + }); + + const model = new ChatOpenAI({ model: "gpt-4o-mini" }); + + // Create store with semantic search enabled + const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small" }); + const store = new InMemoryStore({ + index: { + embeddings, + dims: 1536, } - } - list(checkpointer.list(config)) # [!code highlight] -``` + }); - ``` - [ - CheckpointTuple( - config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1f5b-6704-8004-820c16b69a5a'}}, - checkpoint={ - 'v': 3, - 'ts': '2025-05-05T16:01:24.680462+00:00', - 'id': '1f029ca3-1f5b-6704-8004-820c16b69a5a', - 'channel_versions': {'__start__': '00000000000000000000000000000005.0.5290678567601859', 'messages': '00000000000000000000000000000006.0.3205149138784782', 'branch:to:call_model': '00000000000000000000000000000006.0.14611156755133758'}, - 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000004.0.5736472536395331'}, 'call_model': {'branch:to:call_model': '00000000000000000000000000000005.0.1410174088651449'}}, - 'channel_values': {'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?'), HumanMessage(content="what's my name?"), AIMessage(content='Your name is Bob.')]}, - }, - metadata={'source': 'loop', 'writes': {'call_model': {'messages': AIMessage(content='Your name is Bob.')}}, 'step': 4, 'parents': {}, 'thread_id': '1'}, - parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, - pending_writes=[] - ), - CheckpointTuple( - config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-1790-6b0a-8003-baf965b6a38f'}}, - checkpoint={ - 'v': 3, - 'ts': '2025-05-05T16:01:23.863421+00:00', - 'id': '1f029ca3-1790-6b0a-8003-baf965b6a38f', - 'channel_versions': {'__start__': '00000000000000000000000000000005.0.5290678567601859', 'messages': '00000000000000000000000000000006.0.3205149138784782', 'branch:to:call_model': '00000000000000000000000000000006.0.14611156755133758'}, - 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000004.0.5736472536395331'}, 'call_model': {'branch:to:call_model': '00000000000000000000000000000005.0.1410174088651449'}}, - 'channel_values': {'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?'), HumanMessage(content="what's my name?")], 'branch:to:call_model': None} - }, - metadata={'source': 'loop', 'writes': None, 'step': 3, 'parents': {}, 'thread_id': '1'}, - parent_config={...}, - pending_writes=[('8ab4155e-6b15-b885-9ce5-bed69a2c305c', 'messages', AIMessage(content='Your name is Bob.'))] - ), - CheckpointTuple( - config={...}, - checkpoint={ - 'v': 3, - 'ts': '2025-05-05T16:01:23.863173+00:00', - 'id': '1f029ca3-1790-616e-8002-9e021694a0cd', - 'channel_versions': {'__start__': '00000000000000000000000000000004.0.5736472536395331', 'messages': '00000000000000000000000000000003.0.7056767754077798', 'branch:to:call_model': '00000000000000000000000000000003.0.22059023329132854'}, - 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0.7040775356287469'}, 'call_model': {'branch:to:call_model': '00000000000000000000000000000002.0.9300422176788571'}}, - 'channel_values': {'__start__': {'messages': [{'role': 'user', 'content': "what's my name?"}]}, 'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')]} - }, - metadata={'source': 'input', 'writes': {'__start__': {'messages': [{'role': 'user', 'content': "what's my name?"}]}}, 'step': 2, 'parents': {}, 'thread_id': '1'}, - parent_config={...}, - pending_writes=[('24ba39d6-6db1-4c9b-f4c5-682aeaf38dcd', 'messages', [{'role': 'user', 'content': "what's my name?"}]), ('24ba39d6-6db1-4c9b-f4c5-682aeaf38dcd', 'branch:to:call_model', None)] - ), - CheckpointTuple( - config={...}, - checkpoint={ - 'v': 3, - 'ts': '2025-05-05T16:01:23.862295+00:00', - 'id': '1f029ca3-178d-6f54-8001-d7b180db0c89', - 'channel_versions': {'__start__': '00000000000000000000000000000002.0.18673090920108737', 'messages': '00000000000000000000000000000003.0.7056767754077798', 'branch:to:call_model': '00000000000000000000000000000003.0.22059023329132854'}, - 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0.7040775356287469'}, 'call_model': {'branch:to:call_model': '00000000000000000000000000000002.0.9300422176788571'}}, - 'channel_values': {'messages': [HumanMessage(content="hi! I'm bob"), AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')]} - }, - metadata={'source': 'loop', 'writes': {'call_model': {'messages': AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?')}}, 'step': 1, 'parents': {}, 'thread_id': '1'}, - parent_config={...}, - pending_writes=[] - ), - CheckpointTuple( - config={...}, - checkpoint={ - 'v': 3, - 'ts': '2025-05-05T16:01:22.278960+00:00', - 'id': '1f029ca3-0874-6612-8000-339f2abc83b1', - 'channel_versions': {'__start__': '00000000000000000000000000000002.0.18673090920108737', 'messages': '00000000000000000000000000000002.0.30296526818059655', 'branch:to:call_model': '00000000000000000000000000000002.0.9300422176788571'}, - 'versions_seen': {'__input__': {}, '__start__': {'__start__': '00000000000000000000000000000001.0.7040775356287469'}}, - 'channel_values': {'messages': [HumanMessage(content="hi! I'm bob")], 'branch:to:call_model': None} - }, - metadata={'source': 'loop', 'writes': None, 'step': 0, 'parents': {}, 'thread_id': '1'}, - parent_config={...}, - pending_writes=[('8cbd75e0-3720-b056-04f7-71ac805140a0', 'messages', AIMessage(content='Hi Bob! How are you doing today? Is there anything I can help you with?'))] - ), - CheckpointTuple( - config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f029ca3-0870-6ce2-bfff-1f3f14c3e565'}}, - checkpoint={ - 'v': 3, - 'ts': '2025-05-05T16:01:22.277497+00:00', - 'id': '1f029ca3-0870-6ce2-bfff-1f3f14c3e565', - 'channel_versions': {'__start__': '00000000000000000000000000000001.0.7040775356287469'}, - 'versions_seen': {'__input__': {}}, - 'channel_values': {'__start__': {'messages': [{'role': 'user', 'content': "hi! I'm bob"}]}} - }, - metadata={'source': 'input', 'writes': {'__start__': {'messages': [{'role': 'user', 'content': "hi! I'm bob"}]}}, 'step': -1, 'parents': {}, 'thread_id': '1'}, - parent_config=None, - pending_writes=[('d458367b-8265-812c-18e2-33001d199ce6', 'messages', [{'role': 'user', 'content': "hi! I'm bob"}]), ('d458367b-8265-812c-18e2-33001d199ce6', 'branch:to:call_model', None)] - ) - ] - ``` - - -::: + await store.put(["user_123", "memories"], "1", { text: "I love pizza" }); + await store.put(["user_123", "memories"], "2", { text: "I am a plumber" }); -:::js -```typescript -const config = { - configurable: { - thread_id: "1", - }, -}; + const chat = async (state: z.infer, config) => { + // Search based on user's last message + const items = await config.store.search( + ["user_123", "memories"], + { query: state.messages.at(-1)?.content, limit: 2 } + ); + const memories = items.map(item => item.value.text).join("\n"); + const memoriesText = memories ? `## Memories of user\n${memories}` : ""; -const history = []; -for await (const state of graph.getStateHistory(config)) { - history.push(state); -} -``` -::: + const response = await model.invoke([ + { role: "system", content: `You are a helpful assistant.\n${memoriesText}` }, + ...state.messages, + ]); -#### Delete all checkpoints for a thread + return { messages: [response] }; + }; -:::python -```python -thread_id = "1" -checkpointer.delete_thread(thread_id) -``` -::: + const builder = new StateGraph(MessagesZodState) + .addNode("chat", chat) + .addEdge(START, "chat"); + const graph = builder.compile({ store }); -:::js -```typescript -const threadId = "1"; -await checkpointer.deleteThread(threadId); -``` -::: + for await (const [message, metadata] of await graph.stream( + { messages: [{ role: "user", content: "I'm hungry" }] }, + { streamMode: "messages" } + )) { + if (message.content) { + console.log(message.content); + } + } + ``` + ::: + + +--- :::python ## Prebuilt memory tools diff --git a/src/oss/langgraph/memory.mdx b/src/oss/langgraph/memory.mdx deleted file mode 100644 index a22530d41c..0000000000 --- a/src/oss/langgraph/memory.mdx +++ /dev/null @@ -1,274 +0,0 @@ ---- -title: Memory overview ---- - - - -[Memory](/oss/langgraph/add-memory) is a system that remembers information about previous interactions. For AI agents, memory is crucial because it lets them remember previous interactions, learn from feedback, and adapt to user preferences. As agents tackle more complex tasks with numerous user interactions, this capability becomes essential for both efficiency and user satisfaction. - -This conceptual guide covers two types of memory, based on their recall scope: - -* [Short-term memory](#short-term-memory), or [thread](/oss/langgraph/persistence#threads)-scoped memory, tracks the ongoing conversation by maintaining message history within a session. LangGraph manages short-term memory as a part of your agent's [state](/oss/langgraph/graph-api#state). State is persisted to a database using a [checkpointer](/oss/langgraph/persistence#checkpoints) so the thread can be resumed at any time. Short-term memory updates when the graph is invoked or a step is completed, and the State is read at the start of each step. -* [Long-term memory](#long-term-memory) stores user-specific or application-level data across sessions and is shared _across_ conversational threads. It can be recalled _at any time_ and _in any thread_. Memories are scoped to any custom namespace, not just within a single thread ID. LangGraph provides [stores](/oss/langgraph/persistence#memory-store) ([reference doc](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore)) to let you save and recall long-term memories. - -![](/oss/images/short-vs-long.png) - -## Short-term memory - -[Short-term memory](/oss/langgraph/add-memory#add-short-term-memory) lets your application remember previous interactions within a single [thread](/oss/langgraph/persistence#threads) or conversation. A [thread](/oss/langgraph/persistence#threads) organizes multiple interactions in a session, similar to the way email groups messages in a single conversation. - -LangGraph manages short-term memory as part of the agent's state, persisted via thread-scoped checkpoints. This state can normally include the conversation history along with other stateful data, such as uploaded files, retrieved documents, or generated artifacts. By storing these in the graph's state, the bot can access the full context for a given conversation while maintaining separation between different threads. - -### Manage short-term memory - -Conversation history is the most common form of short-term memory, and long conversations pose a challenge to today's LLMs. A full history may not fit inside an LLM's context window, resulting in an irrecoverable error. Even if your LLM supports the full context length, most LLMs still perform poorly over long contexts. They get "distracted" by stale or off-topic content, all while suffering from slower response times and higher costs. - -Chat models accept context using messages, which include developer provided instructions (a system message) and user inputs (human messages). In chat applications, messages alternate between human inputs and model responses, resulting in a list of messages that grows longer over time. Because context windows are limited and token-rich message lists can be costly, many applications can benefit from using techniques to manually remove or forget stale information. - -![](/oss/images/filter.png) - -For more information on common techniques for managing messages, see the [Add and manage memory](/oss/langgraph/add-memory#manage-short-term-memory) guide. - -## Long-term memory - -[Long-term memory](/oss/langgraph/add-memory#add-long-term-memory) in LangGraph allows systems to retain information across different conversations or sessions. Unlike short-term memory, which is **thread-scoped**, long-term memory is saved within custom "namespaces." - -Long-term memory is a complex challenge without a one-size-fits-all solution. However, the following questions provide a framework to help you navigate the different techniques: - -* What is the type of memory? Humans use memories to remember facts ([semantic memory](#semantic-memory)), experiences ([episodic memory](#episodic-memory)), and rules ([procedural memory](#procedural-memory)). AI agents can use memory in the same ways. For example, AI agents can use memory to remember specific facts about a user to accomplish a task. -* [When do you want to update memories?](#writing-memories) Memory can be updated as part of an agent's application logic (e.g., "on the hot path"). In this case, the agent typically decides to remember facts before responding to a user. Alternatively, memory can be updated as a background task (logic that runs in the background / asynchronously and generates memories). We explain the tradeoffs between these approaches in the [section below](#writing-memories). - -Different applications require various types of memory. Although the analogy isn't perfect, examining [human memory types](https://www.psychologytoday.com/us/basics/memory/types-of-memory?ref=blog.langchain.dev) can be insightful. Some research (e.g., the [CoALA paper](https://arxiv.org/pdf/2309.02427)) have even mapped these human memory types to those used in AI agents. - -| Memory Type | What is Stored | Human Example | Agent Example | -|-------------|----------------|---------------|---------------| -| [Semantic](#semantic-memory) | Facts | Things I learned in school | Facts about a user | -| [Episodic](#episodic-memory) | Experiences | Things I did | Past agent actions | -| [Procedural](#procedural-memory) | Instructions | Instincts or motor skills | Agent system prompt | - -### Semantic memory - -[Semantic memory](https://en.wikipedia.org/wiki/Semantic_memory), both in humans and AI agents, involves the retention of specific facts and concepts. In humans, it can include information learned in school and the understanding of concepts and their relationships. For AI agents, semantic memory is often used to personalize applications by remembering facts or concepts from past interactions. - - -Semantic memory is different from "semantic search," which is a technique for finding similar content using "meaning" (usually as embeddings). Semantic memory is a term from psychology, referring to storing facts and knowledge, while semantic search is a method for retrieving information based on meaning rather than exact matches. - - -#### Profile - -Semantic memories can be managed in different ways. For example, memories can be a single, continuously updated "profile" of well-scoped and specific information about a user, organization, or other entity (including the agent itself). A profile is generally just a JSON document with various key-value pairs you've selected to represent your domain. - -When remembering a profile, you will want to make sure that you are **updating** the profile each time. As a result, you will want to pass in the previous profile and [ask the model to generate a new profile](https://github.com/langchain-ai/memory-template) (or some [JSON patch](https://github.com/hinthornw/trustcall) to apply to the old profile). This can be become error-prone as the profile gets larger, and may benefit from splitting a profile into multiple documents or **strict** decoding when generating documents to ensure the memory schemas remains valid. - -![](/oss/images/update-profile.png) - -#### Collection - -Alternatively, memories can be a collection of documents that are continuously updated and extended over time. Each individual memory can be more narrowly scoped and easier to generate, which means that you're less likely to **lose** information over time. It's easier for an LLM to generate _new_ objects for new information than reconcile new information with an existing profile. As a result, a document collection tends to lead to [higher recall downstream](https://en.wikipedia.org/wiki/Precision_and_recall). - -However, this shifts some complexity memory updating. The model must now _delete_ or _update_ existing items in the list, which can be tricky. In addition, some models may default to over-inserting and others may default to over-updating. See the [Trustcall](https://github.com/hinthornw/trustcall) package for one way to manage this and consider evaluation (e.g., with a tool like [LangSmith](https://docs.smith.langchain.com/tutorials/Developers/evaluation)) to help you tune the behavior. - -Working with document collections also shifts complexity to memory **search** over the list. The `Store` currently supports both [semantic search](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.SearchOp.query) and [filtering by content](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.SearchOp.filter). - -Finally, using a collection of memories can make it challenging to provide comprehensive context to the model. While individual memories may follow a specific schema, this structure might not capture the full context or relationships between memories. As a result, when using these memories to generate responses, the model may lack important contextual information that would be more readily available in a unified profile approach. - -![](/oss/images/update-list.png) - -Regardless of memory management approach, the central point is that the agent will use the semantic memories to [ground its responses](https://python.langchain.com/docs/concepts/rag/), which often leads to more personalized and relevant interactions. - -### Episodic memory - -[Episodic memory](https://en.wikipedia.org/wiki/Episodic_memory), in both humans and AI agents, involves recalling past events or actions. The [CoALA paper](https://arxiv.org/pdf/2309.02427) frames this well: facts can be written to semantic memory, whereas *experiences* can be written to episodic memory. For AI agents, episodic memory is often used to help an agent remember how to accomplish a task. - -:::python -In practice, episodic memories are often implemented through [few-shot example prompting](/langsmith/create-few-shot-evaluators), where agents learn from past sequences to perform tasks correctly. Sometimes it's easier to "show" than "tell" and LLMs learn well from examples. Few-shot learning lets you ["program"](https://x.com/karpathy/status/1627366413840322562) your LLM by updating the prompt with input-output examples to illustrate the intended behavior. While various [best-practices](https://python.langchain.com/docs/concepts/#1-generating-examples) can be used to generate few-shot examples, often the challenge lies in selecting the most relevant examples based on user input. -::: - -:::js -In practice, episodic memories are often implemented through few-shot example prompting, where agents learn from past sequences to perform tasks correctly. Sometimes it's easier to "show" than "tell" and LLMs learn well from examples. Few-shot learning lets you ["program"](https://x.com/karpathy/status/1627366413840322562) your LLM by updating the prompt with input-output examples to illustrate the intended behavior. While various best-practices can be used to generate few-shot examples, often the challenge lies in selecting the most relevant examples based on user input. -::: - -:::python -Note that the memory [store](/oss/langgraph/persistence#memory-store) is just one way to store data as few-shot examples. If you want to have more developer involvement, or tie few-shots more closely to your evaluation harness, you can also use a [LangSmith Dataset](https://docs.smith.langchain.com/evaluation/how_to_guides/datasets/index_datasets_for_dynamic_few_shot_example_selection) to store your data. Then dynamic few-shot example selectors can be used out-of-the box to achieve this same goal. LangSmith will index the dataset for you and enable retrieval of few shot examples that are most relevant to the user input based upon keyword similarity ([using a BM25-like algorithm](https://docs.smith.langchain.com/how_to_guides/datasets/index_datasets_for_dynamic_few_shot_example_selection) for keyword based similarity). - -See this how-to [video](https://www.youtube.com/watch?v=37VaU7e7t5o) for example usage of dynamic few-shot example selection in LangSmith. Also, see this [blog post](https://blog.langchain.dev/few-shot-prompting-to-improve-tool-calling-performance/) showcasing few-shot prompting to improve tool calling performance and this [blog post](https://blog.langchain.dev/aligning-llm-as-a-judge-with-human-preferences/) using few-shot example to align an LLMs to human preferences. -::: - -:::js -Note that the memory [store](/oss/langgraph/persistence#memory-store) is just one way to store data as few-shot examples. If you want to have more developer involvement, or tie few-shots more closely to your evaluation harness, you can also use a LangSmith Dataset to store your data. Then dynamic few-shot example selectors can be used out-of-the box to achieve this same goal. LangSmith will index the dataset for you and enable retrieval of few shot examples that are most relevant to the user input based upon keyword similarity. - -See this how-to [video](https://www.youtube.com/watch?v=37VaU7e7t5o) for example usage of dynamic few-shot example selection in LangSmith. Also, see this [blog post](https://blog.langchain.dev/few-shot-prompting-to-improve-tool-calling-performance/) showcasing few-shot prompting to improve tool calling performance and this [blog post](https://blog.langchain.dev/aligning-llm-as-a-judge-with-human-preferences/) using few-shot example to align an LLMs to human preferences. -::: - -### Procedural memory - -[Procedural memory](https://en.wikipedia.org/wiki/Procedural_memory), in both humans and AI agents, involves remembering the rules used to perform tasks. In humans, procedural memory is like the internalized knowledge of how to perform tasks, such as riding a bike via basic motor skills and balance. Episodic memory, on the other hand, involves recalling specific experiences, such as the first time you successfully rode a bike without training wheels or a memorable bike ride through a scenic route. For AI agents, procedural memory is a combination of model weights, agent code, and agent's prompt that collectively determine the agent's functionality. - -In practice, it is fairly uncommon for agents to modify their model weights or rewrite their code. However, it is more common for agents to modify their own prompts. - -One effective approach to refining an agent's instructions is through ["Reflection"](https://blog.langchain.dev/reflection-agents/) or meta-prompting. This involves prompting the agent with its current instructions (e.g., the system prompt) along with recent conversations or explicit user feedback. The agent then refines its own instructions based on this input. This method is particularly useful for tasks where instructions are challenging to specify upfront, as it allows the agent to learn and adapt from its interactions. - -For example, we built a [Tweet generator](https://www.youtube.com/watch?v=Vn8A3BxfplE) using external feedback and prompt re-writing to produce high-quality paper summaries for Twitter. In this case, the specific summarization prompt was difficult to specify *a priori*, but it was fairly easy for a user to critique the generated Tweets and provide feedback on how to improve the summarization process. - -The below pseudo-code shows how you might implement this with the LangGraph memory [store](/oss/langgraph/persistence#memory-store), using the store to save a prompt, the `update_instructions` node to get the current prompt (as well as feedback from the conversation with the user captured in `state["messages"]`), update the prompt, and save the new prompt back to the store. Then, the `call_model` get the updated prompt from the store and uses it to generate a response. - -:::python -```python -# Node that *uses* the instructions -def call_model(state: State, store: BaseStore): - namespace = ("agent_instructions", ) - instructions = store.get(namespace, key="agent_a")[0] - # Application logic - prompt = prompt_template.format(instructions=instructions.value["instructions"]) - ... - -# Node that updates instructions -def update_instructions(state: State, store: BaseStore): - namespace = ("instructions",) - instructions = store.search(namespace)[0] - # Memory logic - prompt = prompt_template.format(instructions=instructions.value["instructions"], conversation=state["messages"]) - output = llm.invoke(prompt) - new_instructions = output['new_instructions'] - store.put(("agent_instructions",), "agent_a", {"instructions": new_instructions}) - ... -``` -::: - -:::js -```typescript -// Node that *uses* the instructions -const callModel = async (state: State, store: BaseStore) => { - const namespace = ["agent_instructions"]; - const instructions = await store.get(namespace, "agent_a"); - // Application logic - const prompt = promptTemplate.format({ - instructions: instructions[0].value.instructions - }); - // ... -}; - -// Node that updates instructions -const updateInstructions = async (state: State, store: BaseStore) => { - const namespace = ["instructions"]; - const currentInstructions = await store.search(namespace); - // Memory logic - const prompt = promptTemplate.format({ - instructions: currentInstructions[0].value.instructions, - conversation: state.messages - }); - const output = await llm.invoke(prompt); - const newInstructions = output.new_instructions; - await store.put(["agent_instructions"], "agent_a", { - instructions: newInstructions - }); - // ... -}; -``` -::: - -![](/oss/images/update-instructions.png) - -### Writing memories - -There are two primary methods for agents to write memories: ["in the hot path"](#in-the-hot-path) and ["in the background"](#in-the-background). - -![](/oss/images/hot_path_vs_background.png) - -#### In the hot path - -Creating memories during runtime offers both advantages and challenges. On the positive side, this approach allows for real-time updates, making new memories immediately available for use in subsequent interactions. It also enables transparency, as users can be notified when memories are created and stored. - -However, this method also presents challenges. It may increase complexity if the agent requires a new tool to decide what to commit to memory. In addition, the process of reasoning about what to save to memory can impact agent latency. Finally, the agent must multitask between memory creation and its other responsibilities, potentially affecting the quantity and quality of memories created. - -As an example, ChatGPT uses a [save_memories](https://openai.com/index/memory-and-new-controls-for-chatgpt/) tool to upsert memories as content strings, deciding whether and how to use this tool with each user message. See our [memory-agent](https://github.com/langchain-ai/memory-agent) template as an reference implementation. - -#### In the background - -Creating memories as a separate background task offers several advantages. It eliminates latency in the primary application, separates application logic from memory management, and allows for more focused task completion by the agent. This approach also provides flexibility in timing memory creation to avoid redundant work. - -However, this method has its own challenges. Determining the frequency of memory writing becomes crucial, as infrequent updates may leave other threads without new context. Deciding when to trigger memory formation is also important. Common strategies include scheduling after a set time period (with rescheduling if new events occur), using a cron schedule, or allowing manual triggers by users or the application logic. - -See our [memory-service](https://github.com/langchain-ai/memory-template) template as an reference implementation. - -### Memory storage - -LangGraph stores long-term memories as JSON documents in a [store](/oss/langgraph/persistence#memory-store). Each memory is organized under a custom `namespace` (similar to a folder) and a distinct `key` (like a file name). Namespaces often include user or org IDs or other labels that makes it easier to organize information. This structure enables hierarchical organization of memories. Cross-namespace searching is then supported through content filters. - -:::python -```python -from langgraph.store.memory import InMemoryStore - - -def embed(texts: list[str]) -> list[list[float]]: - # Replace with an actual embedding function or LangChain embeddings object - return [[1.0, 2.0] * len(texts)] - - -# InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production use. -store = InMemoryStore(index={"embed": embed, "dims": 2}) -user_id = "my-user" -application_context = "chitchat" -namespace = (user_id, application_context) -store.put( - namespace, - "a-memory", - { - "rules": [ - "User likes short, direct language", - "User only speaks English & python", - ], - "my-key": "my-value", - }, -) -# get the "memory" by ID -item = store.get(namespace, "a-memory") -# search for "memories" within this namespace, filtering on content equivalence, sorted by vector similarity -items = store.search( - namespace, filter={"my-key": "my-value"}, query="language preferences" -) -``` -::: - -:::js -```typescript -import { InMemoryStore } from "@langchain/langgraph"; - -const embed = (texts: string[]): number[][] => { - // Replace with an actual embedding function or LangChain embeddings object - return texts.map(() => [1.0, 2.0]); -}; - -// InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production use. -const store = new InMemoryStore({ index: { embed, dims: 2 } }); -const userId = "my-user"; -const applicationContext = "chitchat"; -const namespace = [userId, applicationContext]; - -await store.put( - namespace, - "a-memory", - { - rules: [ - "User likes short, direct language", - "User only speaks English & TypeScript", - ], - "my-key": "my-value", - } -); - -// get the "memory" by ID -const item = await store.get(namespace, "a-memory"); - -// search for "memories" within this namespace, filtering on content equivalence, sorted by vector similarity -const items = await store.search( - namespace, - { - filter: { "my-key": "my-value" }, - query: "language preferences" - } -); -``` -::: - -For more information about the memory store, see the [Persistence](/oss/langgraph/persistence#memory-store) guide. diff --git a/src/oss/langgraph/persistence.mdx b/src/oss/langgraph/persistence.mdx index 776c40ccb3..aadf6cedcf 100644 --- a/src/oss/langgraph/persistence.mdx +++ b/src/oss/langgraph/persistence.mdx @@ -2,1179 +2,302 @@ title: Persistence --- +LangGraph has a built-in persistence layer that enables powerful capabilities including conversation memory, human-in-the-loop workflows, time travel, and fault tolerance. Persistence is implemented through two complementary mechanisms: - -LangGraph has a built-in persistence layer, implemented through checkpointers. When you compile a graph with a checkpointer, the checkpointer saves a `checkpoint` of the graph state at every super-step. Those checkpoints are saved to a `thread`, which can be accessed after graph execution. Because `threads` allow access to graph's state after execution, several powerful capabilities including human-in-the-loop, memory, time travel, and fault-tolerance are all possible. Below, we'll discuss each of these concepts in more detail. +* [**Checkpointers**](#checkpointers) - Save workflow state to threads for resumable, multi-turn interactions +* [**Stores**](#stores) - Share data across threads for cross-session memory ![Checkpoints](/oss/images/checkpoints.jpg) -**LangGraph API handles checkpointing automatically** -When using the LangGraph API, you don't need to implement or configure checkpointers manually. The API handles all persistence infrastructure for you behind the scenes. - - -## Threads - -A thread is a unique ID or thread identifier assigned to each checkpoint saved by a checkpointer. It contains the accumulated state of a sequence of [runs](/langsmith/assistants#execution). When a run is executed, the [state](/oss/langgraph/graph-api#state) of the underlying graph of the assistant will be persisted to the thread. - -When invoking a graph with a checkpointer, you **must** specify a `thread_id` as part of the `configurable` portion of the config. - -:::python -```python -{"configurable": {"thread_id": "1"}} -``` -::: - -:::js -```typescript -{ - configurable: { - thread_id: "1"; - } -} -``` -::: - -A thread's current and historical state can be retrieved. To persist state, a thread must be created prior to executing a run. The LangSmith API provides several endpoints for creating and managing threads and thread state. See the [API reference](https://langchain-ai.github.io/langgraph/cloud/reference/api/) for more details. - -## Checkpoints - -The state of a thread at a particular point in time is called a checkpoint. Checkpoint is a snapshot of the graph state saved at each super-step and is represented by `StateSnapshot` object with the following key properties: - -* `config`: Config associated with this checkpoint. -* `metadata`: Metadata associated with this checkpoint. -* `values`: Values of the state channels at this point in time. -* `next` A tuple of the node names to execute next in the graph. -* `tasks`: A tuple of `PregelTask` objects that contain information about next tasks to be executed. If the step was previously attempted, it will include error information. If a graph was interrupted [dynamically](/oss/langgraph/interrupts#pause-using-interrupt) from within a node, tasks will contain additional data associated with interrupts. - -Checkpoints are persisted and can be used to restore the state of a thread at a later time. - -Let's see what checkpoints are saved when a simple graph is invoked as follows: - -:::python -```python -from langgraph.graph import StateGraph, START, END -from langgraph.checkpoint.memory import InMemorySaver -from langchain_core.runnables import RunnableConfig -from typing import Annotated -from typing_extensions import TypedDict -from operator import add - -class State(TypedDict): - foo: str - bar: Annotated[list[str], add] - -def node_a(state: State): - return {"foo": "a", "bar": ["a"]} - -def node_b(state: State): - return {"foo": "b", "bar": ["b"]} - - -workflow = StateGraph(State) -workflow.add_node(node_a) -workflow.add_node(node_b) -workflow.add_edge(START, "node_a") -workflow.add_edge("node_a", "node_b") -workflow.add_edge("node_b", END) - -checkpointer = InMemorySaver() -graph = workflow.compile(checkpointer=checkpointer) - -config: RunnableConfig = {"configurable": {"thread_id": "1"}} -graph.invoke({"foo": ""}, config) -``` -::: +**LangGraph API handles persistence automatically** -:::js -```typescript -import { StateGraph, START, END, MemoryServer } from "@langchain/langgraph"; -import { registry } from "@langchain/langgraph/zod"; -import * as z from "zod"; - -const State = z.object({ - foo: z.string(), - bar: z.array(z.string()).register(registry, { - reducer: { - fn: (x, y) => x.concat(y), - }, - default: () => [] as string[], - }), -}); - -const workflow = new StateGraph(State) - .addNode("nodeA", (state) => { - return { foo: "a", bar: ["a"] }; - }) - .addNode("nodeB", (state) => { - return { foo: "b", bar: ["b"] }; - }) - .addEdge(START, "nodeA") - .addEdge("nodeA", "nodeB") - .addEdge("nodeB", END); - -const checkpointer = new MemorySaver(); -const graph = workflow.compile({ checkpointer }); - -const config = { configurable: { thread_id: "1" } }; -await graph.invoke({ foo: "" }, config); -``` -::: - -:::js -```typescript -import { StateGraph, START, END, MemoryServer } from "@langchain/langgraph"; -import { registry } from "@langchain/langgraph/zod"; -import * as z from "zod"; - -const State = z.object({ - foo: z.string(), - bar: z.array(z.string()).register(registry, { - reducer: { - fn: (x, y) => x.concat(y), - }, - default: () => [] as string[], - }), -}); - -const workflow = new StateGraph(State) - .addNode("nodeA", (state) => { - return { foo: "a", bar: ["a"] }; - }) - .addNode("nodeB", (state) => { - return { foo: "b", bar: ["b"] }; - }) - .addEdge(START, "nodeA") - .addEdge("nodeA", "nodeB") - .addEdge("nodeB", END); - -const checkpointer = new MemorySaver(); -const graph = workflow.compile({ checkpointer }); - -const config = { configurable: { thread_id: "1" } }; -await graph.invoke({ foo: "" }, config); -``` -::: - -:::python -After we run the graph, we expect to see exactly 4 checkpoints: - -* Empty checkpoint with @[`START`] as the next node to be executed -* Checkpoint with the user input `{'foo': '', 'bar': []}` and `node_a` as the next node to be executed -* Checkpoint with the outputs of `node_a` `{'foo': 'a', 'bar': ['a']}` and `node_b` as the next node to be executed -* Checkpoint with the outputs of `node_b` `{'foo': 'b', 'bar': ['a', 'b']}` and no next nodes to be executed - -Note that we `bar` channel values contain outputs from both nodes as we have a reducer for `bar` channel. -::: - -:::js -After we run the graph, we expect to see exactly 4 checkpoints: - -* Empty checkpoint with @[`START`] as the next node to be executed -* Checkpoint with the user input `{'foo': '', 'bar': []}` and `nodeA` as the next node to be executed -* Checkpoint with the outputs of `nodeA` `{'foo': 'a', 'bar': ['a']}` and `nodeB` as the next node to be executed -* Checkpoint with the outputs of `nodeB` `{'foo': 'b', 'bar': ['a', 'b']}` and no next nodes to be executed - -Note that the `bar` channel values contain outputs from both nodes as we have a reducer for the `bar` channel. -::: - -### Get state - -:::python -When interacting with the saved graph state, you **must** specify a [thread identifier](#threads). You can view the _latest_ state of the graph by calling `graph.get_state(config)`. This will return a `StateSnapshot` object that corresponds to the latest checkpoint associated with the thread ID provided in the config or a checkpoint associated with a checkpoint ID for the thread, if provided. - -```python -# get the latest state snapshot -config = {"configurable": {"thread_id": "1"}} -graph.get_state(config) - -# get a state snapshot for a specific checkpoint_id -config = {"configurable": {"thread_id": "1", "checkpoint_id": "1ef663ba-28fe-6528-8002-5a559208592c"}} -graph.get_state(config) -``` -::: - -:::js -When interacting with the saved graph state, you **must** specify a [thread identifier](#threads). You can view the _latest_ state of the graph by calling `graph.getState(config)`. This will return a `StateSnapshot` object that corresponds to the latest checkpoint associated with the thread ID provided in the config or a checkpoint associated with a checkpoint ID for the thread, if provided. +When using the LangGraph API, you don't need to implement or configure persistence manually. The API handles all persistence infrastructure for you behind the scenes. + -```typescript -// get the latest state snapshot -const config = { configurable: { thread_id: "1" } }; -await graph.getState(config); - -// get a state snapshot for a specific checkpoint_id -const config = { - configurable: { - thread_id: "1", - checkpoint_id: "1ef663ba-28fe-6528-8002-5a559208592c", - }, -}; -await graph.getState(config); -``` -::: +--- -:::python -In our example, the output of `get_state` will look like this: +## Checkpointers -``` -StateSnapshot( - values={'foo': 'b', 'bar': ['a', 'b']}, - next=(), - config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28fe-6528-8002-5a559208592c'}}, - metadata={'source': 'loop', 'writes': {'node_b': {'foo': 'b', 'bar': ['b']}}, 'step': 2}, - created_at='2024-08-29T19:19:38.821749+00:00', - parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f9-6ec4-8001-31981c2c39f8'}}, tasks=() -) -``` -::: +Checkpointers save snapshots of your graph's state at each execution step to a **thread** - a unique conversation or workflow session identified by a `thread_id`. -:::js -In our example, the output of `getState` will look like this: +### Key concepts -``` -StateSnapshot { - values: { foo: 'b', bar: ['a', 'b'] }, - next: [], - config: { - configurable: { - thread_id: '1', - checkpoint_ns: '', - checkpoint_id: '1ef663ba-28fe-6528-8002-5a559208592c' - } - }, - metadata: { - source: 'loop', - writes: { nodeB: { foo: 'b', bar: ['b'] } }, - step: 2 - }, - createdAt: '2024-08-29T19:19:38.821749+00:00', - parentConfig: { - configurable: { - thread_id: '1', - checkpoint_ns: '', - checkpoint_id: '1ef663ba-28f9-6ec4-8001-31981c2c39f8' - } - }, - tasks: [] -} -``` -::: +#### Threads -### Get state history +A thread represents a unique workflow session. When you invoke a graph with a `thread_id`, LangGraph saves checkpoints to that thread, enabling the graph to: +- Resume from where it left off +- Access historical states +- Support multi-turn conversations :::python -You can get the full history of the graph execution for a given thread by calling @[`graph.get_state_history(config)`][get_state_history]. This will return a list of `StateSnapshot` objects associated with the thread ID provided in the config. Importantly, the checkpoints will be ordered chronologically with the most recent checkpoint / `StateSnapshot` being the first in the list. - ```python -config = {"configurable": {"thread_id": "1"}} -list(graph.get_state_history(config)) +config = {"configurable": {"thread_id": "some_thread_id"}} +graph.invoke(input, config) ``` ::: :::js -You can get the full history of the graph execution for a given thread by calling `graph.getStateHistory(config)`. This will return a list of `StateSnapshot` objects associated with the thread ID provided in the config. Importantly, the checkpoints will be ordered chronologically with the most recent checkpoint / `StateSnapshot` being the first in the list. - ```typescript const config = { configurable: { thread_id: "1" } }; -for await (const state of graph.getStateHistory(config)) { - console.log(state); -} -``` -::: - -:::python -In our example, the output of @[`get_state_history`] will look like this: - -``` -[ - StateSnapshot( - values={'foo': 'b', 'bar': ['a', 'b']}, - next=(), - config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28fe-6528-8002-5a559208592c'}}, - metadata={'source': 'loop', 'writes': {'node_b': {'foo': 'b', 'bar': ['b']}}, 'step': 2}, - created_at='2024-08-29T19:19:38.821749+00:00', - parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f9-6ec4-8001-31981c2c39f8'}}, - tasks=(), - ), - StateSnapshot( - values={'foo': 'a', 'bar': ['a']}, - next=('node_b',), - config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f9-6ec4-8001-31981c2c39f8'}}, - metadata={'source': 'loop', 'writes': {'node_a': {'foo': 'a', 'bar': ['a']}}, 'step': 1}, - created_at='2024-08-29T19:19:38.819946+00:00', - parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f4-6b4a-8000-ca575a13d36a'}}, - tasks=(PregelTask(id='6fb7314f-f114-5413-a1f3-d37dfe98ff44', name='node_b', error=None, interrupts=()),), - ), - StateSnapshot( - values={'foo': '', 'bar': []}, - next=('node_a',), - config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f4-6b4a-8000-ca575a13d36a'}}, - metadata={'source': 'loop', 'writes': None, 'step': 0}, - created_at='2024-08-29T19:19:38.817813+00:00', - parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f0-6c66-bfff-6723431e8481'}}, - tasks=(PregelTask(id='f1b14528-5ee5-579c-949b-23ef9bfbed58', name='node_a', error=None, interrupts=()),), - ), - StateSnapshot( - values={'bar': []}, - next=('__start__',), - config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef663ba-28f0-6c66-bfff-6723431e8481'}}, - metadata={'source': 'input', 'writes': {'foo': ''}, 'step': -1}, - created_at='2024-08-29T19:19:38.816205+00:00', - parent_config=None, - tasks=(PregelTask(id='6d27aa2e-d72b-5504-a36f-8620e54a76dd', name='__start__', error=None, interrupts=()),), - ) -] -``` -::: - -:::js -In our example, the output of `getStateHistory` will look like this: - -``` -[ - StateSnapshot { - values: { foo: 'b', bar: ['a', 'b'] }, - next: [], - config: { - configurable: { - thread_id: '1', - checkpoint_ns: '', - checkpoint_id: '1ef663ba-28fe-6528-8002-5a559208592c' - } - }, - metadata: { - source: 'loop', - writes: { nodeB: { foo: 'b', bar: ['b'] } }, - step: 2 - }, - createdAt: '2024-08-29T19:19:38.821749+00:00', - parentConfig: { - configurable: { - thread_id: '1', - checkpoint_ns: '', - checkpoint_id: '1ef663ba-28f9-6ec4-8001-31981c2c39f8' - } - }, - tasks: [] - }, - StateSnapshot { - values: { foo: 'a', bar: ['a'] }, - next: ['nodeB'], - config: { - configurable: { - thread_id: '1', - checkpoint_ns: '', - checkpoint_id: '1ef663ba-28f9-6ec4-8001-31981c2c39f8' - } - }, - metadata: { - source: 'loop', - writes: { nodeA: { foo: 'a', bar: ['a'] } }, - step: 1 - }, - createdAt: '2024-08-29T19:19:38.819946+00:00', - parentConfig: { - configurable: { - thread_id: '1', - checkpoint_ns: '', - checkpoint_id: '1ef663ba-28f4-6b4a-8000-ca575a13d36a' - } - }, - tasks: [ - PregelTask { - id: '6fb7314f-f114-5413-a1f3-d37dfe98ff44', - name: 'nodeB', - error: null, - interrupts: [] - } - ] - }, - StateSnapshot { - values: { foo: '', bar: [] }, - next: ['node_a'], - config: { - configurable: { - thread_id: '1', - checkpoint_ns: '', - checkpoint_id: '1ef663ba-28f4-6b4a-8000-ca575a13d36a' - } - }, - metadata: { - source: 'loop', - writes: null, - step: 0 - }, - createdAt: '2024-08-29T19:19:38.817813+00:00', - parentConfig: { - configurable: { - thread_id: '1', - checkpoint_ns: '', - checkpoint_id: '1ef663ba-28f0-6c66-bfff-6723431e8481' - } - }, - tasks: [ - PregelTask { - id: 'f1b14528-5ee5-579c-949b-23ef9bfbed58', - name: 'node_a', - error: null, - interrupts: [] - } - ] - }, - StateSnapshot { - values: { bar: [] }, - next: ['__start__'], - config: { - configurable: { - thread_id: '1', - checkpoint_ns: '', - checkpoint_id: '1ef663ba-28f0-6c66-bfff-6723431e8481' - } - }, - metadata: { - source: 'input', - writes: { foo: '' }, - step: -1 - }, - createdAt: '2024-08-29T19:19:38.816205+00:00', - parentConfig: null, - tasks: [ - PregelTask { - id: '6d27aa2e-d72b-5504-a36f-8620e54a76dd', - name: '__start__', - error: null, - interrupts: [] - } - ] - } -] -``` -::: - -![State](/oss/images/get_state.jpg) - -### Replay - -It's also possible to play-back a prior graph execution. If we `invoke` a graph with a `thread_id` and a `checkpoint_id`, then we will _re-play_ the previously executed steps _before_ a checkpoint that corresponds to the `checkpoint_id`, and only execute the steps _after_ the checkpoint. - -* `thread_id` is the ID of a thread. -* `checkpoint_id` is an identifier that refers to a specific checkpoint within a thread. - -You must pass these when invoking the graph as part of the `configurable` portion of the config: - -:::python -```python -config = {"configurable": {"thread_id": "1", "checkpoint_id": "0c62ca34-ac19-445d-bbb0-5b4984975b2a"}} -graph.invoke(None, config=config) +await graph.invoke(input, config); ``` ::: -:::js -```typescript -const config = { - configurable: { - thread_id: "1", - checkpoint_id: "0c62ca34-ac19-445d-bbb0-5b4984975b2a", - }, -}; -await graph.invoke(null, config); -``` -::: - -Importantly, LangGraph knows whether a particular step has been executed previously. If it has, LangGraph simply _re-plays_ that particular step in the graph and does not re-execute the step, but only for the steps _before_ the provided `checkpoint_id`. All of the steps _after_ `checkpoint_id` will be executed (i.e., a new fork), even if they have been executed previously. See this [how to guide on time-travel to learn more about replaying](/oss/langgraph/use-time-travel). - -![Replay](/oss/images/re_play.png) - -### Update state - -:::python -In addition to re-playing the graph from specific `checkpoints`, we can also _edit_ the graph state. We do this using @[`update_state`]. This method accepts three different arguments: -::: - -:::js -In addition to re-playing the graph from specific `checkpoints`, we can also _edit_ the graph state. We do this using `graph.updateState()`. This method accepts three different arguments: -::: - -#### `config` - -The config should contain `thread_id` specifying which thread to update. When only the `thread_id` is passed, we update (or fork) the current state. Optionally, if we include `checkpoint_id` field, then we fork that selected checkpoint. - -#### `values` - -These are the values that will be used to update the state. Note that this update is treated exactly as any update from a node is treated. This means that these values will be passed to the [reducer](/oss/langgraph/graph-api#reducers) functions, if they are defined for some of the channels in the graph state. This means that @[`update_state`] does NOT automatically overwrite the channel values for every channel, but only for the channels without reducers. Let's walk through an example. - -Let's assume you have defined the state of your graph with the following schema (see full example above): - -:::python -```python -from typing import Annotated -from typing_extensions import TypedDict -from operator import add - -class State(TypedDict): - foo: int - bar: Annotated[list[str], add] -``` -::: - -:::js -```typescript -import { registry } from "@langchain/langgraph/zod"; -import * as z from "zod"; - -const State = z.object({ - foo: z.number(), - bar: z.array(z.string()).register(registry, { - reducer: { - fn: (x, y) => x.concat(y), - }, - default: () => [] as string[], - }), -}); -``` -::: - -Let's now assume the current state of the graph is - -:::python -``` -{"foo": 1, "bar": ["a"]} -``` -::: - -:::js -```typescript -{ foo: 1, bar: ["a"] } -``` -::: - -If you update the state as below: - -:::python -```python -graph.update_state(config, {"foo": 2, "bar": ["b"]}) -``` -::: - -:::js -```typescript -await graph.updateState(config, { foo: 2, bar: ["b"] }); -``` -::: - -Then the new state of the graph will be: - -:::python -``` -{"foo": 2, "bar": ["a", "b"]} -``` - -The `foo` key (channel) is completely changed (because there is no reducer specified for that channel, so @[`update_state`] overwrites it). However, there is a reducer specified for the `bar` key, and so it appends `"b"` to the state of `bar`. -::: - -:::js -```typescript -{ foo: 2, bar: ["a", "b"] } -``` +#### Checkpoints -The `foo` key (channel) is completely changed (because there is no reducer specified for that channel, so `updateState` overwrites it). However, there is a reducer specified for the `bar` key, and so it appends `"b"` to the state of `bar`. -::: +A checkpoint is a snapshot of the graph state at a particular point in time. Each checkpoint contains: -#### `as_node` +* `config`: Configuration associated with this checkpoint (including `thread_id` and `checkpoint_id`) +* `values`: State values at this point in time +* `next`: Tuple of node names to execute next +* `metadata`: Additional information about this checkpoint (step number, source, writes) +* `tasks`: Information about tasks to be executed -:::python -The final thing you can optionally specify when calling @[`update_state`] is `as_node`. If you provided it, the update will be applied as if it came from node `as_node`. If `as_node` is not provided, it will be set to the last node that updated the state, if not ambiguous. The reason this matters is that the next steps to execute depend on the last node to have given an update, so this can be used to control which node executes next. See this [how to guide on time-travel to learn more about forking state](/oss/langgraph/use-time-travel). -::: +Checkpoints are ordered chronologically, with the most recent checkpoint first when accessing history. -:::js -The final thing you can optionally specify when calling `updateState` is `asNode`. If you provide it, the update will be applied as if it came from node `asNode`. If `asNode` is not provided, it will be set to the last node that updated the state, if not ambiguous. The reason this matters is that the next steps to execute depend on the last node to have given an update, so this can be used to control which node executes next. See this [how to guide on time-travel to learn more about forking state](/oss/langgraph/use-time-travel). -::: +### What checkpointers enable -![Update](/oss/images/checkpoints_full_story.jpg) + + + Track context across multiple turns in a conversation. + -## Memory Store + + Pause workflows for human inspection, approval, or modification. + -![Model of shared state](/oss/images/shared_state.png) + + Replay prior executions and fork from historical states. + -A [state schema](/oss/langgraph/graph-api#schema) specifies a set of keys that are populated as a graph is executed. As discussed above, state can be written by a checkpointer to a thread at each graph step, enabling state persistence. + + Resume workflows after failures without losing progress. + + -But, what if we want to retain some information _across threads_? Consider the case of a chatbot where we want to retain specific information about the user across _all_ chat conversations (e.g., threads) with that user! +### Implementation -With checkpointers alone, we cannot share information across threads. This motivates the need for the [`Store`](https://reference.langchain.com/python/langgraph/store/) interface. As an illustration, we can define an `InMemoryStore` to store information about a user across threads. We simply compile our graph with a checkpointer, as before, and with our new `in_memory_store` variable. +For detailed information on implementing checkpointers, including: +- How to add checkpointers to your graph +- Production database setup (Postgres, SQLite, Redis, MongoDB) +- Checkpointing in subgraphs (useful for multi-agent systems) +- Accessing your graph's state and state history +- Managing memory (trimming, deleting, summarizing messages) - -**LangGraph API handles stores automatically** -When using the LangGraph API, you don't need to implement or configure stores manually. The API handles all storage infrastructure for you behind the scenes. - +See the [Memory guide](/oss/langgraph/memory#checkpointers). -### Basic Usage +--- -First, let's showcase this in isolation without using LangGraph. +## Stores -:::python -```python -from langgraph.store.memory import InMemoryStore -in_memory_store = InMemoryStore() -``` -::: +While checkpointers save state to individual threads, **stores** enable you to share data **across threads**. This is useful for maintaining user preferences, facts, or any information that should persist across multiple conversation sessions. -:::js -```typescript -import { MemoryStore } from "@langchain/langgraph"; +### How stores differ from checkpointers -const memoryStore = new MemoryStore(); -``` -::: + + + **Thread-scoped persistence** -Memories are namespaced by a `tuple`, which in this specific example will be `(, "memories")`. The namespace can be any length and represent anything, does not have to be user specific. + - State is tied to a specific `thread_id` + - Each thread maintains its own independent state + - Used for conversation history within a session + - Cannot share data across different threads + -:::python -```python -user_id = "1" -namespace_for_memory = (user_id, "memories") -``` -::: + + **Cross-thread persistence** -:::js -```typescript -const userId = "1"; -const namespaceForMemory = [userId, "memories"]; -``` -::: + - Data is organized by namespaces (tuples) + - Same data accessible from any thread + - Used for user preferences, facts, application-level data + - Supports semantic search for intelligent retrieval + + -We use the `store.put` method to save memories to our namespace in the store. When we do this, we specify the namespace, as defined above, and a key-value pair for the memory: the key is simply a unique identifier for the memory (`memory_id`) and the value (a dictionary) is the memory itself. +### Example: User preferences across conversations :::python ```python -memory_id = str(uuid.uuid4()) -memory = {"food_preference" : "I like pizza"} -in_memory_store.put(namespace_for_memory, memory_id, memory) -``` -::: - -:::js -```typescript -import { v4 as uuidv4 } from "uuid"; - -const memoryId = uuidv4(); -const memory = { food_preference: "I like pizza" }; -await memoryStore.put(namespaceForMemory, memoryId, memory); -``` -::: +# Thread 1: User sets a preference +user_id = "123" +namespace = (user_id, "preferences") +store.put(namespace, "theme", {"value": "dark"}) -We can read out memories in our namespace using the `store.search` method, which will return all memories for a given user as a list. The most recent memory is the last in the list. - -:::python -```python -memories = in_memory_store.search(namespace_for_memory) -memories[-1].dict() -{'value': {'food_preference': 'I like pizza'}, - 'key': '07e0caf4-1631-47b7-b15f-65515d4c1843', - 'namespace': ['1', 'memories'], - 'created_at': '2024-10-02T17:22:31.590602+00:00', - 'updated_at': '2024-10-02T17:22:31.590605+00:00'} +# Thread 2: Access the same preference +# (Different conversation, but same user data) +preferences = store.search(namespace) ``` - -Each memory type is a Python class ([`Item`](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.Item)) with certain attributes. We can access it as a dictionary by converting via `.dict` as above. - -The attributes it has are: - -* `value`: The value (itself a dictionary) of this memory -* `key`: A unique key for this memory in this namespace -* `namespace`: A list of strings, the namespace of this memory type -* `created_at`: Timestamp for when this memory was created -* `updated_at`: Timestamp for when this memory was updated ::: :::js ```typescript -const memories = await memoryStore.search(namespaceForMemory); -memories[memories.length - 1]; - -// { -// value: { food_preference: 'I like pizza' }, -// key: '07e0caf4-1631-47b7-b15f-65515d4c1843', -// namespace: ['1', 'memories'], -// createdAt: '2024-10-02T17:22:31.590602+00:00', -// updatedAt: '2024-10-02T17:22:31.590605+00:00' -// } -``` - -The attributes it has are: - -* `value`: The value of this memory -* `key`: A unique key for this memory in this namespace -* `namespace`: A list of strings, the namespace of this memory type -* `createdAt`: Timestamp for when this memory was created -* `updatedAt`: Timestamp for when this memory was updated -::: - -### Semantic Search +// Thread 1: User sets a preference +const userId = "123"; +const namespace = [userId, "preferences"]; +await store.put(namespace, "theme", { value: "dark" }); -Beyond simple retrieval, the store also supports semantic search, allowing you to find memories based on meaning rather than exact matches. To enable this, configure the store with an embedding model: - -:::python -```python -from langchain.embeddings import init_embeddings - -store = InMemoryStore( - index={ - "embed": init_embeddings("openai:text-embedding-3-small"), # Embedding provider - "dims": 1536, # Embedding dimensions - "fields": ["food_preference", "$"] # Fields to embed - } -) +// Thread 2: Access the same preference +// (Different conversation, but same user data) +const preferences = await store.search(namespace); ``` ::: -:::js -```typescript -import { OpenAIEmbeddings } from "@langchain/openai"; - -const store = new InMemoryStore({ - index: { - embeddings: new OpenAIEmbeddings({ model: "text-embedding-3-small" }), - dims: 1536, - fields: ["food_preference", "$"], // Fields to embed - }, -}); -``` -::: - -Now when searching, you can use natural language queries to find relevant memories: - -:::python -```python -# Find memories about food preferences -# (This can be done after putting memories into the store) -memories = store.search( - namespace_for_memory, - query="What does the user like to eat?", - limit=3 # Return top 3 matches -) -``` -::: +### Implementation -:::js -```typescript -// Find memories about food preferences -// (This can be done after putting memories into the store) -const memories = await store.search(namespaceForMemory, { - query: "What does the user like to eat?", - limit: 3, // Return top 3 matches -}); -``` -::: +For detailed information on implementing stores, including: +- How to add stores to your graph +- Production database setup +- Semantic search configuration +- Usage patterns and best practices -You can control which parts of your memories get embedded by configuring the `fields` parameter or by specifying the `index` parameter when storing memories: +See the [Memory guide](/oss/langgraph/memory#stores). -:::python -```python -# Store with specific fields to embed -store.put( - namespace_for_memory, - str(uuid.uuid4()), - { - "food_preference": "I love Italian cuisine", - "context": "Discussing dinner plans" - }, - index=["food_preference"] # Only embed "food_preferences" field -) - -# Store without embedding (still retrievable, but not searchable) -store.put( - namespace_for_memory, - str(uuid.uuid4()), - {"system_info": "Last updated: 2024-01-01"}, - index=False -) -``` -::: +--- -:::js -```typescript -// Store with specific fields to embed -await store.put( - namespaceForMemory, - uuidv4(), - { - food_preference: "I love Italian cuisine", - context: "Discussing dinner plans", - }, - { index: ["food_preference"] } // Only embed "food_preferences" field -); - -// Store without embedding (still retrievable, but not searchable) -await store.put( - namespaceForMemory, - uuidv4(), - { system_info: "Last updated: 2024-01-01" }, - { index: false } -); -``` -::: +## Checkpointer libraries -### Using in LangGraph +LangGraph provides several checkpointer implementations for different use cases: :::python -With this all in place, we use the `in_memory_store` in LangGraph. The `in_memory_store` works hand-in-hand with the checkpointer: the checkpointer saves state to threads, as discussed above, and the `in_memory_store` allows us to store arbitrary information for access _across_ threads. We compile the graph with both the checkpointer and the `in_memory_store` as follows. - -```python -from langgraph.checkpoint.memory import InMemorySaver - -# We need this because we want to enable threads (conversations) -checkpointer = InMemorySaver() - -# ... Define the graph ... - -# Compile the graph with the checkpointer and store -graph = graph.compile(checkpointer=checkpointer, store=in_memory_store) -``` +* `langgraph-checkpoint` - Base interface (@[`BaseCheckpointSaver`]) and in-memory implementation (@[`InMemorySaver`]) +* `langgraph-checkpoint-sqlite` - SQLite-backed persistence (@[`SqliteSaver`] / @[`AsyncSqliteSaver`]) +* `langgraph-checkpoint-postgres` - Postgres-backed persistence (@[`PostgresSaver`] / @[`AsyncPostgresSaver`]) ::: :::js -With this all in place, we use the `memoryStore` in LangGraph. The `memoryStore` works hand-in-hand with the checkpointer: the checkpointer saves state to threads, as discussed above, and the `memoryStore` allows us to store arbitrary information for access _across_ threads. We compile the graph with both the checkpointer and the `memoryStore` as follows. - -```typescript -import { MemorySaver } from "@langchain/langgraph"; - -// We need this because we want to enable threads (conversations) -const checkpointer = new MemorySaver(); - -// ... Define the graph ... - -// Compile the graph with the checkpointer and store -const graph = workflow.compile({ checkpointer, store: memoryStore }); -``` -::: - -We invoke the graph with a `thread_id`, as before, and also with a `user_id`, which we'll use to namespace our memories to this particular user as we showed above. - -:::python -```python -# Invoke the graph -user_id = "1" -config = {"configurable": {"thread_id": "1", "user_id": user_id}} - -# First let's just say hi to the AI -for update in graph.stream( - {"messages": [{"role": "user", "content": "hi"}]}, config, stream_mode="updates" -): - print(update) -``` +* `@langchain/langgraph-checkpoint` - Base interface and in-memory implementation (@[`MemorySaver`]) +* `@langchain/langgraph-checkpoint-sqlite` - SQLite-backed persistence (@[`SqliteSaver`]) +* `@langchain/langgraph-checkpoint-postgres` - Postgres-backed persistence (@[`PostgresSaver`]) ::: -:::js -```typescript -// Invoke the graph -const userId = "1"; -const config = { configurable: { thread_id: "1", user_id: userId } }; - -// First let's just say hi to the AI -for await (const update of await graph.stream( - { messages: [{ role: "user", content: "hi" }] }, - { ...config, streamMode: "updates" } -)) { - console.log(update); -} -``` -::: +### Checkpointer interface :::python -We can access the `in_memory_store` and the `user_id` in _any node_ by passing `store: BaseStore` and `config: RunnableConfig` as node arguments. Here's how we might use semantic search in a node to find relevant memories: - -```python -def update_memory(state: MessagesState, config: RunnableConfig, *, store: BaseStore): - - # Get the user id from the config - user_id = config["configurable"]["user_id"] - - # Namespace the memory - namespace = (user_id, "memories") - - # ... Analyze conversation and create a new memory - - # Create a new memory ID - memory_id = str(uuid.uuid4()) - - # We create a new memory - store.put(namespace, memory_id, {"memory": memory}) - -``` -::: - -:::js -We can access the `memoryStore` and the `user_id` in _any node_ by accessing `config` and `store` as node arguments. Here's how we might use semantic search in a node to find relevant memories: - -```typescript -import { MessagesZodMeta, Runtime } from "@langchain/langgraph"; -import { BaseMessage } from "@langchain/core/messages"; -import { registry } from "@langchain/langgraph/zod"; -import * as z from "zod"; - -const MessagesZodState = z.object({ - messages: z - .array(z.custom()) - .register(registry, MessagesZodMeta), -}); - -const updateMemory = async ( - state: z.infer, - runtime: Runtime<{ user_id: string }>, -) => { - // Get the user id from the config - const userId = runtime.context?.user_id; - if (!userId) throw new Error("User ID is required"); - - // Namespace the memory - const namespace = [userId, "memories"]; - - // ... Analyze conversation and create a new memory - - // Create a new memory ID - const memoryId = uuidv4(); - - // We create a new memory - await runtime.store?.put(namespace, memoryId, { memory }); -}; -``` -::: +Each checkpointer implements @[`BaseCheckpointSaver`] with the following methods: -As we showed above, we can also access the store in any node and use the `store.search` method to get memories. Recall the memories are returned as a list of objects that can be converted to a dictionary. +* `.put` - Store a checkpoint with its configuration and metadata +* `.put_writes` - Store intermediate writes (pending writes) +* `.get_tuple` - Fetch a checkpoint for a given configuration +* `.list` - List checkpoints matching given criteria -:::python -```python -memories[-1].dict() -{'value': {'food_preference': 'I like pizza'}, - 'key': '07e0caf4-1631-47b7-b15f-65515d4c1843', - 'namespace': ['1', 'memories'], - 'created_at': '2024-10-02T17:22:31.590602+00:00', - 'updated_at': '2024-10-02T17:22:31.590605+00:00'} -``` +For async graph execution (`.ainvoke`, `.astream`, `.abatch`), use async methods (`.aput`, `.aput_writes`, `.aget_tuple`, `.alist`) with @[`InMemorySaver`], @[`AsyncSqliteSaver`], or @[`AsyncPostgresSaver`]. ::: :::js -```typescript -memories[memories.length - 1]; -// { -// value: { food_preference: 'I like pizza' }, -// key: '07e0caf4-1631-47b7-b15f-65515d4c1843', -// namespace: ['1', 'memories'], -// createdAt: '2024-10-02T17:22:31.590602+00:00', -// updatedAt: '2024-10-02T17:22:31.590605+00:00' -// } -``` -::: - -We can access the memories and use them in our model call. - -:::python -```python -def call_model(state: MessagesState, config: RunnableConfig, *, store: BaseStore): - # Get the user id from the config - user_id = config["configurable"]["user_id"] - - # Namespace the memory - namespace = (user_id, "memories") - - # Search based on the most recent message - memories = store.search( - namespace, - query=state["messages"][-1].content, - limit=3 - ) - info = "\n".join([d.value["memory"] for d in memories]) - - # ... Use memories in the model call -``` -::: +Each checkpointer implements the @[`BaseCheckpointSaver`] interface with the following methods: -:::js -```typescript -const callModel = async ( - state: z.infer, - config: LangGraphRunnableConfig, - store: BaseStore -) => { - // Get the user id from the config - const userId = config.configurable?.user_id; - - // Namespace the memory - const namespace = [userId, "memories"]; - - // Search based on the most recent message - const memories = await store.search(namespace, { - query: state.messages[state.messages.length - 1].content, - limit: 3, - }); - const info = memories.map((d) => d.value.memory).join("\n"); - - // ... Use memories in the model call -}; -``` +* `.put` - Store a checkpoint with its configuration and metadata +* `.putWrites` - Store intermediate writes (pending writes) +* `.getTuple` - Fetch a checkpoint for a given configuration +* `.list` - List checkpoints matching given criteria ::: -If we create a new thread, we can still access the same memories so long as the `user_id` is the same. +For more details on the checkpointer interface and serialization, see the [Features](#features) section below. -:::python -```python -# Invoke the graph -config = {"configurable": {"thread_id": "2", "user_id": "1"}} - -# Let's say hi again -for update in graph.stream( - {"messages": [{"role": "user", "content": "hi, tell me about my memories"}]}, config, stream_mode="updates" -): - print(update) -``` -::: +--- -:::js -```typescript -// Invoke the graph -const config = { configurable: { thread_id: "2", user_id: "1" } }; - -// Let's say hi again -for await (const update of await graph.stream( - { messages: [{ role: "user", content: "hi, tell me about my memories" }] }, - { ...config, streamMode: "updates" } -)) { - console.log(update); -} -``` -::: +## Features -When we use the LangSmith, either locally (e.g., in [Studio](/langsmith/studio)) or [hosted with LangSmith](/langsmith/platform-setup), the base store is available to use by default and does not need to be specified during graph compilation. To enable semantic search, however, you **do** need to configure the indexing settings in your `langgraph.json` file. For example: + + + You can replay a prior graph execution by invoking with both a `thread_id` and a `checkpoint_id`: -```json -{ - ... - "store": { - "index": { - "embed": "openai:text-embeddings-3-small", - "dims": 1536, - "fields": ["$"] + :::python + ```python + config = { + "configurable": { + "thread_id": "1", + "checkpoint_id": "0c62ca34-ac19-445d-bbb0-5b4984975b2a" } } -} -``` - -See the [deployment guide](/langsmith/semantic-search) for more details and configuration options. - -## Checkpointer libraries - -Under the hood, checkpointing is powered by checkpointer objects that conform to @[`BaseCheckpointSaver`] interface. LangGraph provides several checkpointer implementations, all implemented via standalone, installable libraries: - -:::python -* `langgraph-checkpoint`: The base interface for checkpointer savers (@[`BaseCheckpointSaver`]) and serialization/deserialization interface (@[`SerializerProtocol`]). Includes in-memory checkpointer implementation (@[`InMemorySaver`]) for experimentation. LangGraph comes with `langgraph-checkpoint` included. -* `langgraph-checkpoint-sqlite`: An implementation of LangGraph checkpointer that uses SQLite database (@[`SqliteSaver`] / @[`AsyncSqliteSaver`]). Ideal for experimentation and local workflows. Needs to be installed separately. -* `langgraph-checkpoint-postgres`: An advanced checkpointer that uses Postgres database (@[`PostgresSaver`] / @[`AsyncPostgresSaver`]), used in LangSmith. Ideal for using in production. Needs to be installed separately. -::: - -:::js -* `@langchain/langgraph-checkpoint`: The base interface for checkpointer savers (@[`BaseCheckpointSaver`]) and serialization/deserialization interface (@[`SerializerProtocol`]). Includes in-memory checkpointer implementation (@[`MemorySaver`]) for experimentation. LangGraph comes with `@langchain/langgraph-checkpoint` included. -* `@langchain/langgraph-checkpoint-sqlite`: An implementation of LangGraph checkpointer that uses SQLite database (@[`SqliteSaver`]). Ideal for experimentation and local workflows. Needs to be installed separately. -* `@langchain/langgraph-checkpoint-postgres`: An advanced checkpointer that uses Postgres database (@[`PostgresSaver`]), used in LangSmith. Ideal for using in production. Needs to be installed separately. -::: - -### Checkpointer interface - -:::python -Each checkpointer conforms to @[`BaseCheckpointSaver`] interface and implements the following methods: - -* `.put` - Store a checkpoint with its configuration and metadata. -* `.put_writes` - Store intermediate writes linked to a checkpoint (i.e. [pending writes](#pending-writes)). -* `.get_tuple` - Fetch a checkpoint tuple using for a given configuration (`thread_id` and `checkpoint_id`). This is used to populate `StateSnapshot` in `graph.get_state()`. -* `.list` - List checkpoints that match a given configuration and filter criteria. This is used to populate state history in `graph.get_state_history()` - -If the checkpointer is used with asynchronous graph execution (i.e. executing the graph via `.ainvoke`, `.astream`, `.abatch`), asynchronous versions of the above methods will be used (`.aput`, `.aput_writes`, `.aget_tuple`, `.alist`). + graph.invoke(None, config=config) + ``` + ::: - -For running your graph asynchronously, you can use @[`InMemorySaver`], or async versions of Sqlite/Postgres checkpointers -- @[`AsyncSqliteSaver`] / @[`AsyncPostgresSaver`] checkpointers. - -::: - -:::js -Each checkpointer conforms to the @[`BaseCheckpointSaver`] interface and implements the following methods: - -* `.put` - Store a checkpoint with its configuration and metadata. -* `.putWrites` - Store intermediate writes linked to a checkpoint (i.e. [pending writes](#pending-writes)). -* `.getTuple` - Fetch a checkpoint tuple using for a given configuration (`thread_id` and `checkpoint_id`). This is used to populate `StateSnapshot` in `graph.getState()`. -* `.list` - List checkpoints that match a given configuration and filter criteria. This is used to populate state history in `graph.getStateHistory()` -::: - -### Serializer - -When checkpointers save the graph state, they need to serialize the channel values in the state. This is done using serializer objects. - -:::python -`langgraph_checkpoint` defines @[protocol][SerializerProtocol] for implementing serializers provides a default implementation (@[`JsonPlusSerializer`]) that handles a wide variety of types, including LangChain and LangGraph primitives, datetimes, enums and more. - -#### Serialization with `pickle` - -The default serializer, @[`JsonPlusSerializer`], uses ormsgpack and JSON under the hood, which is not suitable for all types of objects. + :::js + ```typescript + const config = { + configurable: { + thread_id: "1", + checkpoint_id: "0c62ca34-ac19-445d-bbb0-5b4984975b2a", + }, + }; + await graph.invoke(null, config); + ``` + ::: -If you want to fallback to pickle for objects not currently supported by our msgpack encoder (such as Pandas dataframes), -you can use the `pickle_fallback` argument of the @[`JsonPlusSerializer`]: + LangGraph will replay steps **before** the checkpoint and re-execute steps **after** it. See the [time travel guide](/oss/langgraph/use-time-travel) for more details. -```python -from langgraph.checkpoint.memory import InMemorySaver -from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer + ![Replay](/oss/images/re_play.png) + -# ... Define the graph ... -graph.compile( - checkpointer=InMemorySaver(serde=JsonPlusSerializer(pickle_fallback=True)) -) -``` + + :::python + You can manually edit the graph state using @[`update_state`]. This method accepts: -#### Encryption + **`config`**: Thread to update (required `thread_id`, optional `checkpoint_id` to fork from a specific checkpoint) -Checkpointers can optionally encrypt all persisted state. To enable this, pass an instance of @[`EncryptedSerializer`] to the `serde` argument of any @[`BaseCheckpointSaver`] implementation. The easiest way to create an encrypted serializer is via @[`from_pycryptodome_aes`], which reads the AES key from the `LANGGRAPH_AES_KEY` environment variable (or accepts a `key` argument): + **`values`**: State updates to apply. These are passed through [reducers](/oss/langgraph/graph-api#reducers) if defined: -```python -import sqlite3 + ```python + # If state has a reducer for 'bar' channel: + graph.update_state(config, {"foo": 2, "bar": ["b"]}) + # Result: foo is overwritten, bar is appended to + ``` -from langgraph.checkpoint.serde.encrypted import EncryptedSerializer -from langgraph.checkpoint.sqlite import SqliteSaver + **`as_node`**: Apply update as if it came from a specific node (controls which node executes next) -serde = EncryptedSerializer.from_pycryptodome_aes() # reads LANGGRAPH_AES_KEY -checkpointer = SqliteSaver(sqlite3.connect("checkpoint.db"), serde=serde) -``` + ```python + graph.update_state(config, {"foo": 2}, as_node="node_a") + ``` -```python -from langgraph.checkpoint.serde.encrypted import EncryptedSerializer -from langgraph.checkpoint.postgres import PostgresSaver + See the [time travel guide](/oss/langgraph/use-time-travel) for more details on forking state. + ::: -serde = EncryptedSerializer.from_pycryptodome_aes() -checkpointer = PostgresSaver.from_conn_string("postgresql://...", serde=serde) -checkpointer.setup() -``` + :::js + You can manually edit the graph state using `updateState`. This method accepts: -When running on LangSmith, encryption is automatically enabled whenever `LANGGRAPH_AES_KEY` is present, so you only need to provide the environment variable. Other encryption schemes can be used by implementing @[`CipherProtocol`] and supplying it to @[`EncryptedSerializer`]. -::: + **`config`**: Thread to update (required `thread_id`, optional `checkpoint_id` to fork from a specific checkpoint) -:::js -`@langchain/langgraph-checkpoint` defines protocol for implementing serializers and provides a default implementation that handles a wide variety of types, including LangChain and LangGraph primitives, datetimes, enums and more. -::: + **`values`**: State updates to apply. These are passed through [reducers](/oss/langgraph/graph-api#reducers) if defined: -## Capabilities + ```typescript + // If state has a reducer for 'bar' channel: + await graph.updateState(config, { foo: 2, bar: ["b"] }); + // Result: foo is overwritten, bar is appended to + ``` -### Human-in-the-loop + **`asNode`**: Apply update as if it came from a specific node (controls which node executes next) -First, checkpointers facilitate [human-in-the-loop workflows](/oss/langgraph/interrupts) by allowing humans to inspect, interrupt, and approve graph steps. Checkpointers are needed for these workflows as the human has to be able to view the state of a graph at any point in time, and the graph has to be to resume execution after the human has made any updates to the state. See [the how-to guides](/oss/langgraph/interrupts) for examples. + ```typescript + await graph.updateState(config, { foo: 2 }, { asNode: "nodeA" }); + ``` -### Memory + See the [time travel guide](/oss/langgraph/use-time-travel) for more details on forking state. + ::: -Second, checkpointers allow for ["memory"](/oss/concepts/memory) between interactions. In the case of repeated human interactions (like conversations) any follow up messages can be sent to that thread, which will retain its memory of previous ones. See [Add memory](/oss/langgraph/add-memory) for information on how to add and manage conversation memory using checkpointers. + ![Update](/oss/images/checkpoints_full_story.jpg) + -### Time Travel + + Persistence enables durable execution - the ability to pause and resume workflows even after interruptions, failures, or extended delays. -Third, checkpointers allow for ["time travel"](/oss/langgraph/use-time-travel), allowing users to replay prior graph executions to review and / or debug specific graph steps. In addition, checkpointers make it possible to fork the graph state at arbitrary checkpoints to explore alternative trajectories. + When using a checkpointer, your workflow can: + - Pause for human input and resume days later + - Recover from failures at the last successful checkpoint + - Avoid re-running successful operations -### Fault-tolerance + + To ensure workflows are deterministic and can be consistently replayed, wrap side effects and non-deterministic operations inside [tasks](/oss/langgraph/functional-api#task). + -Lastly, checkpointing also provides fault-tolerance and error recovery: if one or more nodes fail at a given superstep, you can restart your graph from the last successful step. Additionally, when a graph node fails mid-execution at a given superstep, LangGraph stores pending checkpoint writes from any other nodes that completed successfully at that superstep, so that whenever we resume graph execution from that superstep we don't re-run the successful nodes. + See the [durable execution guide](/oss/langgraph/durable-execution) for details. + -#### Pending writes + + Checkpointers use serialization to store state and support encryption for sensitive data. -Additionally, when a graph node fails mid-execution at a given superstep, LangGraph stores pending checkpoint writes from any other nodes that completed successfully at that superstep, so that whenever we resume graph execution from that superstep we don't re-run the successful nodes. + For details on serialization options (including pickle fallback) and encryption configuration, see the [Checkpoint data section](/oss/langgraph/add-memory#checkpoint-data) in the Memory guide. + + diff --git a/src/oss/langgraph/use-subgraphs.mdx b/src/oss/langgraph/use-subgraphs.mdx index 4a2759cb65..2cf13b3a08 100644 --- a/src/oss/langgraph/use-subgraphs.mdx +++ b/src/oss/langgraph/use-subgraphs.mdx @@ -596,27 +596,8 @@ You only need to **provide the checkpointer when compiling the parent graph**. L ```python from langgraph.graph import START, StateGraph from langgraph.checkpoint.memory import MemorySaver -from typing_extensions import TypedDict - -class State(TypedDict): - foo: str - -# Subgraph - -def subgraph_node_1(state: State): - return {"foo": state["foo"] + "bar"} - -subgraph_builder = StateGraph(State) -subgraph_builder.add_node(subgraph_node_1) -subgraph_builder.add_edge(START, "subgraph_node_1") -subgraph = subgraph_builder.compile() - -# Parent graph - -builder = StateGraph(State) -builder.add_node("node_1", subgraph) -builder.add_edge(START, "node_1") +# Parent graph gets the checkpointer checkpointer = MemorySaver() graph = builder.compile(checkpointer=checkpointer) ``` @@ -624,151 +605,65 @@ graph = builder.compile(checkpointer=checkpointer) :::js ```typescript -import { StateGraph, START, MemorySaver } from "@langchain/langgraph"; -import * as z from "zod"; - -const State = z.object({ - foo: z.string(), -}); - -// Subgraph -const subgraphBuilder = new StateGraph(State) - .addNode("subgraphNode1", (state) => { - return { foo: state.foo + "bar" }; - }) - .addEdge(START, "subgraphNode1"); - -const subgraph = subgraphBuilder.compile(); - -// Parent graph -const builder = new StateGraph(State) - .addNode("node1", subgraph) - .addEdge(START, "node1"); +import { StateGraph, MemorySaver } from "@langchain/langgraph"; +// Parent graph gets the checkpointer const checkpointer = new MemorySaver(); const graph = builder.compile({ checkpointer }); ``` ::: -If you want the subgraph to **have its own memory**, you can compile it with the appropriate checkpointer option. This is useful in [multi-agent](/oss/langchain/multi-agent) systems, if you want agents to keep track of their internal message histories: +The behavior of subgraph checkpointing depends on how you compile the subgraph: -:::python -```python -subgraph_builder = StateGraph(...) -subgraph = subgraph_builder.compile(checkpointer=True) -``` -::: +- **`checkpointer=None`** (default) - Subgraph state resets on every invocation +- **`checkpointer=True`** - Subgraph state persists across invocations (useful for multi-agent systems) +- **`checkpointer=InMemorySaver()`** - Subgraph uses a separate checkpointer -:::js -```typescript -const subgraphBuilder = new StateGraph(...) -const subgraph = subgraphBuilder.compile({ checkpointer: true }); -``` -::: +This is critical for [multi-agent systems](/oss/langchain/multi-agent) where you want agents to maintain their own conversation histories. + + +Understanding when to use each option is important to avoid unexpected behavior. For detailed explanations, examples, and guidance on when to use each option, see the [Checkpointers in subgraphs](/oss/langgraph/add-memory#checkpointers-in-subgraphs) section. + ## View subgraph state -When you enable [persistence](/oss/langgraph/persistence), you can [inspect the graph state](/oss/langgraph/persistence#checkpoints) (checkpoint) via the appropriate method. To view the subgraph state, you can use the subgraphs option. +When you enable [persistence](/oss/langgraph/persistence), you can access both parent and subgraph state. + +To view the subgraph state, use the `subgraphs` parameter: :::python -You can inspect the graph state via `graph.get_state(config)`. To view the subgraph state, you can use `graph.get_state(config, subgraphs=True)`. +```python +# Get state including subgraphs +state = graph.get_state(config, subgraphs=True) + +# Access subgraph state from tasks +for task in state.tasks: + if task.state: + print(f"Subgraph state: {task.state.values}") +``` ::: :::js -You can inspect the graph state via `graph.getState(config)`. To view the subgraph state, you can use `graph.getState(config, { subgraphs: true })`. +```typescript +// Get state including subgraphs +const state = await graph.getState(config, { subgraphs: true }); + +// Access subgraph state from tasks +for (const task of state.tasks) { + if (task.state) { + console.log(`Subgraph state: ${task.state.values}`); + } +} +``` ::: -**Available **only** when interrupted** Subgraph state can only be viewed **when the subgraph is interrupted**. Once you resume the graph, you won't be able to access the subgraph state. - - :::python - ```python - from langgraph.graph import START, StateGraph - from langgraph.checkpoint.memory import MemorySaver - from langgraph.types import interrupt, Command - from typing_extensions import TypedDict - - class State(TypedDict): - foo: str - - # Subgraph - - def subgraph_node_1(state: State): - value = interrupt("Provide value:") - return {"foo": state["foo"] + value} - - subgraph_builder = StateGraph(State) - subgraph_builder.add_node(subgraph_node_1) - subgraph_builder.add_edge(START, "subgraph_node_1") - - subgraph = subgraph_builder.compile() - - # Parent graph - - builder = StateGraph(State) - builder.add_node("node_1", subgraph) - builder.add_edge(START, "node_1") - - checkpointer = MemorySaver() - graph = builder.compile(checkpointer=checkpointer) - - config = {"configurable": {"thread_id": "1"}} - - graph.invoke({"foo": ""}, config) - parent_state = graph.get_state(config) - - # This will be available only when the subgraph is interrupted. - # Once you resume the graph, you won't be able to access the subgraph state. - subgraph_state = graph.get_state(config, subgraphs=True).tasks[0].state - - # resume the subgraph - graph.invoke(Command(resume="bar"), config) - ``` - - 1. This will be available only when the subgraph is interrupted. Once you resume the graph, you won't be able to access the subgraph state. - ::: - - :::js - ```typescript - import { StateGraph, START, MemorySaver, interrupt, Command } from "@langchain/langgraph"; - import * as z from "zod"; - - const State = z.object({ - foo: z.string(), - }); - - // Subgraph - const subgraphBuilder = new StateGraph(State) - .addNode("subgraphNode1", (state) => { - const value = interrupt("Provide value:"); - return { foo: state.foo + value }; - }) - .addEdge(START, "subgraphNode1"); - - const subgraph = subgraphBuilder.compile(); - - // Parent graph - const builder = new StateGraph(State) - .addNode("node1", subgraph) - .addEdge(START, "node1"); - - const checkpointer = new MemorySaver(); - const graph = builder.compile({ checkpointer }); - - const config = { configurable: { thread_id: "1" } }; - - await graph.invoke({ foo: "" }, config); - const parentState = await graph.getState(config); - const subgraphState = (await graph.getState(config, { subgraphs: true })).tasks[0].state; // [!code highlight] - - // resume the subgraph - await graph.invoke(new Command({ resume: "bar" }), config); - ``` - ::: - + +For detailed information on accessing subgraph state and state history (including how to get historical subgraph checkpoints), see the [Access state and state history](/oss/langgraph/add-memory#access-state-and-state-history) section. + ## Stream subgraph outputs