diff --git a/README.md b/README.md deleted file mode 100644 index ca4d96b..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# langgraph_docs diff --git a/add-memory.mdx b/add-memory.mdx index 25bf670..a925bbf 100644 --- a/add-memory.mdx +++ b/add-memory.mdx @@ -3,7 +3,6 @@ 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. @@ -54,7 +53,7 @@ In production, use a checkpointer backed by a database: ```python from langgraph.checkpoint.postgres import PostgresSaver -DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" +DB_URI = "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable" with PostgresSaver.from_conn_string(DB_URI) as checkpointer: # [!code highlight] builder = StateGraph(...) graph = builder.compile(checkpointer=checkpointer) # [!code highlight] @@ -62,15 +61,31 @@ with PostgresSaver.from_conn_string(DB_URI) as checkpointer: # [!code highlight ::: :::js -```typescript -import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres"; + + + ```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 DB_URI = "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable"; + const checkpointer = PostgresSaver.fromConnString(DB_URI); -const builder = new StateGraph(...); -const graph = builder.compile({ checkpointer }); -``` + const builder = new StateGraph(...); + const graph = builder.compile({ checkpointer }); + ``` + + + ```typescript + import { MongoClient } from "mongodb"; + import { MongoDBSaver } from "@langchain/langgraph-checkpoint-mongodb"; + + const client = new MongoClient("mongodb://user:password@localhost:27017"); + const checkpointer = new MongoDBSaver({ client }); + + const builder = new StateGraph(...); + const graph = builder.compile({ checkpointer }); + ``` + + ::: @@ -92,7 +107,7 @@ const graph = builder.compile({ checkpointer }); model = init_chat_model(model="claude-haiku-4-5-20251001") - DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" + DB_URI = "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable" with PostgresSaver.from_conn_string(DB_URI) as checkpointer: # [!code highlight] # checkpointer.setup() @@ -112,19 +127,21 @@ const graph = builder.compile({ checkpointer }); } } - for chunk in graph.stream( + stream = graph.stream_events( {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, config, # [!code highlight] - stream_mode="values" - ): - chunk["messages"][-1].pretty_print() + version="v3", + ) + for snapshot in stream.values: + print(snapshot) - for chunk in graph.stream( + stream = graph.stream_events( {"messages": [{"role": "user", "content": "what's my name?"}]}, config, # [!code highlight] - stream_mode="values" - ): - chunk["messages"][-1].pretty_print() + version="v3", + ) + for snapshot in stream.values: + print(snapshot) ``` @@ -135,7 +152,7 @@ const graph = builder.compile({ checkpointer }); model = init_chat_model(model="claude-haiku-4-5-20251001") - DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" + DB_URI = "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable" async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer: # [!code highlight] # await checkpointer.setup() @@ -155,19 +172,23 @@ const graph = builder.compile({ checkpointer }); } } - async for chunk in graph.astream( + stream = await graph.astream_events( {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, config, # [!code highlight] - stream_mode="values" - ): - chunk["messages"][-1].pretty_print() + version="v3", + ) + async for message in stream.messages: + async for token in message.text: + print(token, end="", flush=True) - async for chunk in graph.astream( + stream = await graph.astream_events( {"messages": [{"role": "user", "content": "what's my name?"}]}, config, # [!code highlight] - stream_mode="values" - ): - chunk["messages"][-1].pretty_print() + version="v3", + ) + async for message in stream.messages: + async for token in message.text: + print(token, end="", flush=True) ``` @@ -193,7 +214,7 @@ const graph = builder.compile({ checkpointer }); const model = new ChatAnthropic({ model: "claude-haiku-4-5-20251001" }); - const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"; + const DB_URI = "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable"; const checkpointer = PostgresSaver.fromConnString(DB_URI); // await checkpointer.setup(); @@ -214,33 +235,35 @@ const graph = builder.compile({ checkpointer }); } }; - for await (const chunk of await graph.stream( + const stream1 = await graph.streamEvents( { messages: [{ role: "user", content: "hi! I'm bob" }] }, - { ...config, streamMode: "values" } - )) { - console.log(chunk.messages.at(-1)?.content); + { ...config, version: "v3" } + ); + for await (const snapshot of stream1.values) { + console.log(snapshot); } - for await (const chunk of await graph.stream( + const stream2 = await graph.streamEvents( { messages: [{ role: "user", content: "what's my name?" }] }, - { ...config, streamMode: "values" } - )) { - console.log(chunk.messages.at(-1)?.content); + { ...config, version: "v3" } + ); + for await (const snapshot of stream2.values) { + console.log(snapshot); } ``` ::: -:::python + :::python ``` pip install -U pymongo langgraph langgraph-checkpoint-mongodb ``` - + **Setup** To use the [MongoDB checkpointer](https://pypi.org/project/langgraph-checkpoint-mongodb/), 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. - + @@ -251,8 +274,8 @@ const graph = builder.compile({ checkpointer }); 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] + MONGODB_URI = "localhost:27017" + with MongoDBSaver.from_conn_string(MONGODB_URI) as checkpointer: # [!code highlight] def call_model(state: MessagesState): response = model.invoke(state["messages"]) @@ -270,19 +293,21 @@ const graph = builder.compile({ checkpointer }); } } - for chunk in graph.stream( + stream = graph.stream_events( {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, config, # [!code highlight] - stream_mode="values" - ): - chunk["messages"][-1].pretty_print() + version="v3", + ) + for snapshot in stream.values: + print(snapshot) - for chunk in graph.stream( + stream = graph.stream_events( {"messages": [{"role": "user", "content": "what's my name?"}]}, config, # [!code highlight] - stream_mode="values" - ): - chunk["messages"][-1].pretty_print() + version="v3", + ) + for snapshot in stream.values: + print(snapshot) ``` @@ -293,8 +318,8 @@ const graph = builder.compile({ checkpointer }); 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] + MONGODB_URI = "localhost:27017" + async with AsyncMongoDBSaver.from_conn_string(MONGODB_URI) as checkpointer: # [!code highlight] async def call_model(state: MessagesState): response = await model.ainvoke(state["messages"]) @@ -312,24 +337,86 @@ const graph = builder.compile({ checkpointer }); } } - async for chunk in graph.astream( + stream = await graph.astream_events( {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, config, # [!code highlight] - stream_mode="values" - ): - chunk["messages"][-1].pretty_print() + version="v3", + ) + async for message in stream.messages: + async for token in message.text: + print(token, end="", flush=True) - async for chunk in graph.astream( + stream = await graph.astream_events( {"messages": [{"role": "user", "content": "what's my name?"}]}, config, # [!code highlight] - stream_mode="values" - ): - chunk["messages"][-1].pretty_print() + version="v3", + ) + async for message in stream.messages: + async for token in message.text: + print(token, end="", flush=True) ``` + ::: + + :::js + ``` + npm install @langchain/langgraph-checkpoint-mongodb + ``` + + + **Setup** + To use `MongoDBSaver`, 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. + + + ```typescript + import { ChatAnthropic } from "@langchain/anthropic"; + import { StateGraph, StateSchema, MessagesValue, GraphNode, START } from "@langchain/langgraph"; + import { MongoDBSaver } from "@langchain/langgraph-checkpoint-mongodb"; + import { MongoClient } from "mongodb"; + + const State = new StateSchema({ + messages: MessagesValue, + }); + + const model = new ChatAnthropic({ model: "claude-haiku-4-5-20251001" }); + + const client = new MongoClient("mongodb://user:password@localhost:27017"); + const checkpointer = new MongoDBSaver({ client, dbName: "langgraph" }); + + const callModel: GraphNode = async (state) => { + const response = await model.invoke(state.messages); + return { messages: [response] }; + }; + + const builder = new StateGraph(State) + .addNode("call_model", callModel) + .addEdge(START, "call_model"); + + const graph = builder.compile({ checkpointer }); + + const config = { configurable: { thread_id: "1" } }; + + const stream1 = await graph.streamEvents( + { messages: [{ role: "user", content: "hi! I'm bob" }] }, + { ...config, version: "v3" } + ); + for await (const snapshot of stream1.values) { + console.log(snapshot); + } + + const stream2 = await graph.streamEvents( + { messages: [{ role: "user", content: "what's my name?" }] }, + { ...config, version: "v3" } + ); + for await (const snapshot of stream2.values) { + console.log(snapshot); + } + ``` + ::: +:::python ``` pip install -U langgraph langgraph-checkpoint-redis @@ -368,20 +455,22 @@ const graph = builder.compile({ checkpointer }); } } - for chunk in graph.stream( + stream = graph.stream_events( {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, config, # [!code highlight] - stream_mode="values" - ): - chunk["messages"][-1].pretty_print() + version="v3", + ) + for snapshot in stream.values: + print(snapshot) - for chunk in graph.stream( + stream = graph.stream_events( {"messages": [{"role": "user", "content": "what's my name?"}]}, config, # [!code highlight] - stream_mode="values" - ): - chunk["messages"][-1].pretty_print() -``` + version="v3", + ) + for snapshot in stream.values: + print(snapshot) + ``` ```python @@ -411,19 +500,133 @@ const graph = builder.compile({ checkpointer }); } } - async for chunk in graph.astream( + stream = await graph.astream_events( {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, config, # [!code highlight] - stream_mode="values" - ): - chunk["messages"][-1].pretty_print() + version="v3", + ) + async for message in stream.messages: + async for token in message.text: + print(token, end="", flush=True) - async for chunk in graph.astream( + stream = await graph.astream_events( {"messages": [{"role": "user", "content": "what's my name?"}]}, config, # [!code highlight] - stream_mode="values" - ): - chunk["messages"][-1].pretty_print() + version="v3", + ) + async for message in stream.messages: + async for token in message.text: + print(token, end="", flush=True) +``` + + + + + + ``` + pip install -U langgraph langgraph-oracledb + ``` + + + **Setup** + To use the [Oracle checkpointer](https://pypi.org/project/langgraph-oracledb/), you will need an Oracle AI Database instance. A local container (for example `gvenzl/oracle-free:23-slim`) or an Oracle Autonomous Database in OCI both work. + + + + You need to call `checkpointer.setup()` the first time you're using the Oracle checkpointer. + + + + + ```python + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph_oracledb.checkpoint.oracle import OracleSaver # [!code highlight] + + model = init_chat_model(model="claude-haiku-4-5-20251001") + + DB_URI = "user/password@localhost:1521/FREEPDB1" + with OracleSaver.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] + } + } + + stream = graph.stream_events( + {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, + config, # [!code highlight] + version="v3", + ) + for snapshot in stream.values: + print(snapshot) + + stream = graph.stream_events( + {"messages": [{"role": "user", "content": "what's my name?"}]}, + config, # [!code highlight] + version="v3", + ) + for snapshot in stream.values: + print(snapshot) +``` + + + ```python + from langchain.chat_models import init_chat_model + from langgraph.graph import StateGraph, MessagesState, START + from langgraph_oracledb.checkpoint.oracle import AsyncOracleSaver # [!code highlight] + + model = init_chat_model(model="claude-haiku-4-5-20251001") + + DB_URI = "user/password@localhost:1521/FREEPDB1" + async with AsyncOracleSaver.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] + } + } + + stream = await graph.astream_events( + {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, + config, # [!code highlight] + version="v3", + ) + async for message in stream.messages: + async for token in message.text: + print(token, end="", flush=True) + + stream = await graph.astream_events( + {"messages": [{"role": "user", "content": "what's my name?"}]}, + config, # [!code highlight] + version="v3", + ) + async for message in stream.messages: + async for token in message.text: + print(token, end="", flush=True) ``` @@ -579,7 +782,6 @@ graph.invoke( :::js ```typescript import { StateGraph, StateSchema, MessagesValue, GraphNode, START } from "@langchain/langgraph"; -import { v4 as uuidv4 } from "uuid"; const State = new StateSchema({ messages: MessagesValue, @@ -599,7 +801,7 @@ const callModel: GraphNode = async (state, runtime) => { // ... Use memories in model call // Store a new memory - await runtime.store?.put(namespace, uuidv4(), { data: "User prefers dark mode" }); + await runtime.store?.put(namespace, crypto.randomUUID(), { data: "User prefers dark mode" }); }; const builder = new StateGraph(State) @@ -624,7 +826,7 @@ In production, use a store backed by a database: ```python from langgraph.store.postgres import PostgresStore -DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" +DB_URI = "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable" with PostgresStore.from_conn_string(DB_URI) as store: # [!code highlight] builder = StateGraph(...) graph = builder.compile(store=store) # [!code highlight] @@ -632,15 +834,33 @@ with PostgresStore.from_conn_string(DB_URI) as store: # [!code highlight] ::: :::js -```typescript -import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres/store"; + + + ```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 DB_URI = "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable"; + const store = PostgresStore.fromConnString(DB_URI); -const builder = new StateGraph(...); -const graph = builder.compile({ store }); -``` + const builder = new StateGraph(...); + const graph = builder.compile({ store }); + ``` + + + ```typescript + import { MongoDBStore } from "@langchain/langgraph-checkpoint-mongodb"; + + const MONGODB_URI = "mongodb://user:password@localhost:27017"; + const store = await MongoDBStore.fromConnString(MONGODB_URI, { + dbName: "langgraph", + collectionName: "store", + }); + + const builder = new StateGraph(...); + const graph = builder.compile({ store }); + ``` + + ::: @@ -691,7 +911,7 @@ const graph = builder.compile({ store }); ) return {"messages": response} - DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" + DB_URI = "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable" async with ( AsyncPostgresStore.from_conn_string(DB_URI) as store, # [!code highlight] @@ -710,22 +930,26 @@ const graph = builder.compile({ store }); ) config = {"configurable": {"thread_id": "1"}} - async for chunk in graph.astream( + stream = await graph.astream_events( {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, config, - stream_mode="values", + version="v3", context=Context(user_id="1"), # [!code highlight] - ): - chunk["messages"][-1].pretty_print() + ) + async for message in stream.messages: + async for token in message.text: + print(token, end="", flush=True) config = {"configurable": {"thread_id": "2"}} - async for chunk in graph.astream( + stream = await graph.astream_events( {"messages": [{"role": "user", "content": "what is my name?"}]}, config, - stream_mode="values", + version="v3", context=Context(user_id="1"), # [!code highlight] - ): - chunk["messages"][-1].pretty_print() + ) + async for message in stream.messages: + async for token in message.text: + print(token, end="", flush=True) ``` @@ -765,7 +989,7 @@ const graph = builder.compile({ store }); ) return {"messages": response} - DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable" + DB_URI = "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable" with ( PostgresStore.from_conn_string(DB_URI) as store, # [!code highlight] @@ -784,22 +1008,24 @@ const graph = builder.compile({ store }); ) config = {"configurable": {"thread_id": "1"}} - for chunk in graph.stream( + stream = graph.stream_events( {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, config, - stream_mode="values", + version="v3", context=Context(user_id="1"), # [!code highlight] - ): - chunk["messages"][-1].pretty_print() + ) + for snapshot in stream.values: + print(snapshot) config = {"configurable": {"thread_id": "2"}} - for chunk in graph.stream( + stream = graph.stream_events( {"messages": [{"role": "user", "content": "what is my name?"}]}, config, - stream_mode="values", + version="v3", context=Context(user_id="1"), # [!code highlight] - ): - chunk["messages"][-1].pretty_print() + ) + for snapshot in stream.values: + print(snapshot) ``` @@ -819,7 +1045,6 @@ const graph = builder.compile({ store }); import { StateGraph, StateSchema, MessagesValue, GraphNode, START } from "@langchain/langgraph"; import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres"; import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres/store"; - import { v4 as uuidv4 } from "uuid"; const State = new StateSchema({ messages: MessagesValue, @@ -838,7 +1063,7 @@ const graph = builder.compile({ store }); const lastMessage = state.messages.at(-1); if (lastMessage?.content?.toLowerCase().includes("remember")) { const memory = "User name is Bob"; - await runtime.store?.put(namespace, uuidv4(), { data: memory }); + await runtime.store?.put(namespace, crypto.randomUUID(), { data: memory }); } const response = await model.invoke([ @@ -848,7 +1073,7 @@ const graph = builder.compile({ store }); return { messages: [response] }; }; - const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable"; + const DB_URI = "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable"; const store = PostgresStore.fromConnString(DB_URI); const checkpointer = PostgresSaver.fromConnString(DB_URI); @@ -864,18 +1089,92 @@ const graph = builder.compile({ store }); store, }); - for await (const chunk of await graph.stream( + const stream1 = await graph.streamEvents( { messages: [{ role: "user", content: "Hi! Remember: my name is Bob" }] }, - { configurable: { thread_id: "1" }, context: { userId: "1" }, streamMode: "values" } - )) { - console.log(chunk.messages.at(-1)?.content); + { configurable: { thread_id: "1" }, context: { userId: "1" }, version: "v3" } + ); + for await (const snapshot of stream1.values) { + console.log(snapshot); } - for await (const chunk of await graph.stream( + const stream2 = await graph.streamEvents( { messages: [{ role: "user", content: "what is my name?" }] }, - { configurable: { thread_id: "2" }, context: { userId: "1" }, streamMode: "values" } - )) { - console.log(chunk.messages.at(-1)?.content); + { configurable: { thread_id: "2" }, context: { userId: "1" }, version: "v3" } + ); + for await (const snapshot of stream2.values) { + console.log(snapshot); + } + ``` + ::: + + + + :::js + ``` + npm install @langchain/langgraph-checkpoint-mongodb + ``` + + ```typescript + import { ChatAnthropic } from "@langchain/anthropic"; + import { MemorySaver, StateGraph, StateSchema, MessagesValue, GraphNode, START } from "@langchain/langgraph"; + import { MongoDBStore } from "@langchain/langgraph-checkpoint-mongodb"; + + const State = new StateSchema({ + messages: MessagesValue, + }); + + const model = new ChatAnthropic({ model: "claude-sonnet-4-6" }); + + const callModel: GraphNode = async (state, runtime) => { + const userId = runtime.context?.userId; + const namespace = ["memories", userId]; + const memories = await runtime.store?.search(namespace); + const info = memories?.map(d => d.value.data).join("\n") || "n/a"; + 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 runtime.store?.put(namespace, crypto.randomUUID(), { data: memory }); + } + + const response = await model.invoke([ + { role: "system", content: systemMsg }, + ...state.messages + ]); + return { messages: [response] }; + }; + + const MONGODB_URI = "mongodb://user:password@localhost:27017"; + + const store = await MongoDBStore.fromConnString(MONGODB_URI, { + dbName: "langgraph", + collectionName: "store", + }); + + const checkpointer = new MemorySaver(); + + const builder = new StateGraph(State) + .addNode("call_model", callModel) + .addEdge(START, "call_model"); + + const graph = builder.compile({ checkpointer, store }); + + const stream1 = await graph.streamEvents( + { messages: [{ role: "user", content: "Hi! Remember: my name is Bob" }] }, + { configurable: { thread_id: "1" }, context: { userId: "1" }, version: "v3" } + ); + for await (const snapshot of stream1.values) { + console.log(snapshot); + } + + const stream2 = await graph.streamEvents( + { messages: [{ role: "user", content: "what is my name?" }] }, + { configurable: { thread_id: "2" }, context: { userId: "1" }, version: "v3" } + ); + for await (const snapshot of stream2.values) { + console.log(snapshot); } ``` ::: @@ -949,22 +1248,24 @@ const graph = builder.compile({ store }); ) config = {"configurable": {"thread_id": "1"}} - async for chunk in graph.astream( + stream = await graph.astream_events( {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, config, - stream_mode="values", + version="v3", context=Context(user_id="1"), # [!code highlight] - ): - chunk["messages"][-1].pretty_print() + ) + async for snapshot in stream.values: + snapshot["messages"][-1].pretty_print() config = {"configurable": {"thread_id": "2"}} - async for chunk in graph.astream( + stream = await graph.astream_events( {"messages": [{"role": "user", "content": "what is my name?"}]}, config, - stream_mode="values", + version="v3", context=Context(user_id="1"), # [!code highlight] - ): - chunk["messages"][-1].pretty_print() + ) + async for snapshot in stream.values: + snapshot["messages"][-1].pretty_print() ``` @@ -1023,22 +1324,221 @@ const graph = builder.compile({ store }); ) config = {"configurable": {"thread_id": "1"}} - for chunk in graph.stream( + stream = graph.stream_events( {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, config, - stream_mode="values", + version="v3", context=Context(user_id="1"), # [!code highlight] - ): - chunk["messages"][-1].pretty_print() + ) + for snapshot in stream.values: + snapshot["messages"][-1].pretty_print() config = {"configurable": {"thread_id": "2"}} - for chunk in graph.stream( + stream = graph.stream_events( {"messages": [{"role": "user", "content": "what is my name?"}]}, config, - stream_mode="values", + version="v3", context=Context(user_id="1"), # [!code highlight] + ) + for snapshot in stream.values: + snapshot["messages"][-1].pretty_print() +``` + + + + + + + ``` + pip install -U langgraph langgraph-oracledb langchain-openai + ``` + + + **Setup** + To use the [Oracle store](https://pypi.org/project/langgraph-oracledb/), you will need an Oracle AI Database instance — the vector index used for semantic `search` requires [Oracle AI Vector Search](https://docs.oracle.com/en/database/oracle/oracle-database/23/vecse/). + + + + You need to call `store.setup()` and `checkpointer.setup()` the first time you're using the Oracle store and checkpointer. + + + + + ```python + import uuid + + from langchain.chat_models import init_chat_model + from langchain.embeddings import init_embeddings + from langchain_core.runnables import RunnableConfig + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.store.base import BaseStore + from langgraph_oracledb.checkpoint.oracle import OracleSaver + from langgraph_oracledb.store.oracle import OracleStore # [!code highlight] + + model = init_chat_model(model="claude-haiku-4-5-20251001") + embeddings = init_embeddings("openai:text-embedding-3-small") + + DB_URI = "user/password@localhost:1521/FREEPDB1" + + with ( + OracleStore.from_conn_string( # [!code highlight] + DB_URI, + index={"embed": embeddings, "dims": 1536}, # [!code highlight] + ) as store, + OracleSaver.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] + } + } + stream = graph.stream_events( + {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, + config, # [!code highlight] + version="v3", + ) + for snapshot in stream.values: + snapshot["messages"][-1].pretty_print() + + config = { + "configurable": { + "thread_id": "2", # [!code highlight] + "user_id": "1", + } + } + + stream = graph.stream_events( + {"messages": [{"role": "user", "content": "what is my name?"}]}, + config, # [!code highlight] + version="v3", + ) + for snapshot in stream.values: + snapshot["messages"][-1].pretty_print() +``` + + + ```python + import uuid + + from langchain.chat_models import init_chat_model + from langchain.embeddings import init_embeddings + from langchain_core.runnables import RunnableConfig + from langgraph.graph import StateGraph, MessagesState, START + from langgraph.store.base import BaseStore + from langgraph_oracledb.checkpoint.oracle import AsyncOracleSaver + from langgraph_oracledb.store.oracle import AsyncOracleStore # [!code highlight] + + model = init_chat_model(model="claude-haiku-4-5-20251001") + embeddings = init_embeddings("openai:text-embedding-3-small") + + DB_URI = "user/password@localhost:1521/FREEPDB1" + + async with ( + AsyncOracleStore.from_conn_string( # [!code highlight] + DB_URI, + index={"embed": embeddings, "dims": 1536}, # [!code highlight] + ) as store, + AsyncOracleSaver.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] ): - chunk["messages"][-1].pretty_print() + 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] + } + } + stream = await graph.astream_events( + {"messages": [{"role": "user", "content": "Hi! Remember: my name is Bob"}]}, + config, # [!code highlight] + version="v3", + ) + async for snapshot in stream.values: + snapshot["messages"][-1].pretty_print() + + config = { + "configurable": { + "thread_id": "2", # [!code highlight] + "user_id": "1", + } + } + + stream = await graph.astream_events( + {"messages": [{"role": "user", "content": "what is my name?"}]}, + config, # [!code highlight] + version="v3", + ) + async for snapshot in stream.values: + snapshot["messages"][-1].pretty_print() ``` @@ -1096,6 +1596,12 @@ const items = await store.search(["user_123", "memories"], { ``` ::: +:::js + +`InMemoryStore` is suitable for development. For production, use a persistent store like `PostgresStore`, `MongoDBStore`, or `RedisStore`. + +::: + :::python @@ -1107,7 +1613,7 @@ const items = await store.search(["user_123", "memories"], { from langgraph.graph import START, MessagesState, StateGraph from langgraph.runtime import Runtime # [!code highlight] - model = init_chat_model("gpt-4.1-mini") + model = init_chat_model("gpt-5.4-mini") # Create store with semantic search enabled embeddings = init_embeddings("openai:text-embedding-3-small") @@ -1142,69 +1648,202 @@ const items = await store.search(["user_123", "memories"], { builder.add_edge(START, "chat") graph = builder.compile(store=store) - async for message, metadata in graph.astream( - input={"messages": [{"role": "user", "content": "I'm hungry"}]}, - stream_mode="messages", - ): - print(message.content, end="") + stream = await graph.astream_events( + {"messages": [{"role": "user", "content": "I'm hungry"}]}, + version="v3", + ) + async for message in stream.messages: + async for token in message.text: + print(token, end="", flush=True) ``` ::: :::js - - ```typescript - import { OpenAIEmbeddings, ChatOpenAI } from "@langchain/openai"; - import { StateGraph, StateSchema, MessagesValue, GraphNode, START, InMemoryStore } from "@langchain/langgraph"; - - const State = new StateSchema({ - messages: MessagesValue, - }); - - const model = new ChatOpenAI({ model: "gpt-4.1-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: GraphNode = async (state, runtime) => { - // Search based on user's last message - const items = await runtime.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(State) - .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); - } - } - ``` + + + ```typescript + import { OpenAIEmbeddings, ChatOpenAI } from "@langchain/openai"; + import { StateGraph, StateSchema, MessagesValue, GraphNode, START, InMemoryStore } from "@langchain/langgraph"; + + const State = new StateSchema({ + messages: MessagesValue, + }); + + const model = new ChatOpenAI({ model: "gpt-5.4-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: GraphNode = async (state, runtime) => { + // Search based on user's last message + const items = await runtime.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(State) + .addNode("chat", chat) + .addEdge(START, "chat"); + const graph = builder.compile({ store }); + + const stream = await graph.streamEvents( + { messages: [{ role: "user", content: "I'm hungry" }] }, + { version: "v3" } + ); + for await (const message of stream.messages) { + for await (const token of message.text) { + process.stdout.write(token); + } + } + ``` + + + ```typescript + import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai"; + import { MongoDBStore } from "@langchain/langgraph-checkpoint-mongodb"; + import { StateGraph, StateSchema, MessagesValue, GraphNode, START } from "@langchain/langgraph"; + + const State = new StateSchema({ + messages: MessagesValue, + }); + + const model = new ChatOpenAI({ model: "gpt-5.4-mini" }); + + // Create store with semantic search enabled + const MONGODB_URI = "mongodb://user:password@localhost:27017"; + const store = await MongoDBStore.fromConnString(MONGODB_URI, { + dbName: "langgraph", + collectionName: "store", + embeddings: new OpenAIEmbeddings({ model: "text-embedding-3-small" }), + indexConfig: { + name: "store_vector_index", + dims: 1536, + embeddingKey: "text", + }, + }); + + 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: GraphNode = async (state, runtime) => { + // Search based on user's last message + const items = await runtime.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(State) + .addNode("chat", chat) + .addEdge(START, "chat"); + const graph = builder.compile({ store }); + + const stream = await graph.streamEvents( + { messages: [{ role: "user", content: "I'm hungry" }] }, + { version: "v3" } + ); + for await (const message of stream.messages) { + for await (const token of message.text) { + process.stdout.write(token); + } + } + ``` + + + + Auto embedding requires MongoDB Atlas. MongoDB generates embeddings server-side via Voyage AI. See the [Automated Embedding documentation](https://www.mongodb.com/docs/atlas/atlas-vector-search/automated-embedding/) for more information. + + + ```typescript + import { StateGraph, StateSchema, MessagesValue, GraphNode, START } from "@langchain/langgraph"; + import { MongoDBStore } from "@langchain/langgraph-checkpoint-mongodb"; + import { ChatOpenAI } from "@langchain/openai"; + + const State = new StateSchema({ + messages: MessagesValue, + }); + + const model = new ChatOpenAI({ model: "gpt-5.4-mini" }); + + // Auto embedding: no embeddings instance needed. + // Configure the Voyage AI model and the field path MongoDB will read server-side. + const MONGODB_URI = "mongodb://user:password@localhost:27017"; + const store = await MongoDBStore.fromConnString(MONGODB_URI, { + dbName: "langgraph", + collectionName: "store", + indexConfig: { + name: "store_vector_index", + path: "value.content", // MongoDB reads this field and embeds it server-side + model: "voyage-4", // Voyage AI model used by MongoDB Atlas + }, + }); + + // Values must have the content field matching the configured path (value.content) + await store.put(["user_123", "memories"], "1", { content: "I love pizza" }); + await store.put(["user_123", "memories"], "2", { content: "I am a plumber" }); + + const chat: GraphNode = async (state, runtime) => { + // MongoDB generates the query embedding server-side + const items = await runtime.store.search( + ["user_123", "memories"], + { query: state.messages.at(-1)?.content, limit: 2 } + ); + const memories = items.map(item => item.value.content).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(State) + .addNode("chat", chat) + .addEdge(START, "chat"); + const graph = builder.compile({ store }); + + const stream = await graph.streamEvents( + { messages: [{ role: "user", content: "I'm hungry" }] }, + { version: "v3" } + ); + for await (const message of stream.messages) { + for await (const token of message.text) { + process.stdout.write(token); + } + } + ``` + + ::: @@ -1456,19 +2095,21 @@ When deleting messages, **make sure** that the resulting message history is vali checkpointer = InMemorySaver() app = builder.compile(checkpointer=checkpointer) - for event in app.stream( + stream = app.stream_events( {"messages": [{"role": "user", "content": "hi! I'm bob"}]}, config, - stream_mode="values" - ): - print([(message.type, message.content) for message in event["messages"]]) + version="v3" + ) + for snapshot in stream.values: + print([(message.type, message.content) for message in snapshot["messages"]]) - for event in app.stream( + stream = app.stream_events( {"messages": [{"role": "user", "content": "what's my name?"}]}, config, - stream_mode="values" - ): - print([(message.type, message.content) for message in event["messages"]]) + version="v3" + ) + for snapshot in stream.values: + print([(message.type, message.content) for message in snapshot["messages"]]) ``` ``` @@ -1517,18 +2158,20 @@ When deleting messages, **make sure** that the resulting message history is vali const config = { configurable: { thread_id: "1" } }; - for await (const event of await app.stream( + const stream1 = await app.streamEvents( { messages: [{ role: "user", content: "hi! I'm bob" }] }, - { ...config, streamMode: "values" } - )) { - console.log(event.messages.map(message => [message.getType(), message.content])); + { ...config, version: "v3" } + ); + for await (const snapshot of stream1.values) { + console.log(snapshot.messages.map(message => [message.getType(), message.content])); } - for await (const event of await app.stream( + const stream2 = await app.streamEvents( { messages: [{ role: "user", content: "what's my name?" }] }, - { ...config, streamMode: "values" } - )) { - console.log(event.messages.map(message => [message.getType(), message.content])); + { ...config, version: "v3" } + ); + for await (const snapshot of stream2.values) { + console.log(snapshot.messages.map(message => [message.getType(), message.content])); } ``` @@ -1732,7 +2375,6 @@ const summarizeConversation: GraphNode = async (state) => { MemorySaver, } from "@langchain/langgraph"; import * as z from "zod"; - import { v4 as uuidv4 } from "uuid"; const memory = new MemorySaver(); @@ -1752,7 +2394,7 @@ const summarizeConversation: GraphNode = async (state) => { let { messages } = state; if (summary) { const systemMessage = new SystemMessage({ - id: uuidv4(), + id: crypto.randomUUID(), content: `Summary of conversation earlier: ${summary}`, }); messages = [systemMessage, ...messages]; @@ -1789,7 +2431,7 @@ const summarizeConversation: GraphNode = async (state) => { const allMessages = [ ...messages, - new HumanMessage({ id: uuidv4(), content: summaryMessage }), + new HumanMessage({ id: crypto.randomUUID(), content: summaryMessage }), ]; const response = await model.invoke(allMessages); @@ -2160,7 +2802,7 @@ await checkpointer.deleteThread(threadId); ## Database management -If you are using any database-backed persistence implementation (such as Postgres or Redis) to store short and/or long-term memory, you will need to run migrations to set up the required schema before you can use it with your database. +If you are using any database-backed persistence implementation (such as Postgres, Redis, or Oracle) to store short and/or long-term memory, you will need to run migrations to set up the required schema before you can use it with your database. By convention, most database-specific libraries define a `setup()` method on the checkpointer or store instance that runs the required migrations. However, you should check with your specific implementation of @[`BaseCheckpointSaver`] or @[`BaseStore`] to confirm the exact method name and usage. diff --git a/agentic-rag.mdx b/agentic-rag.mdx index e3f55a6..451586d 100644 --- a/agentic-rag.mdx +++ b/agentic-rag.mdx @@ -3,6 +3,28 @@ title: Build a custom RAG agent with LangGraph sidebarTitle: Custom RAG agent --- +import AgenticRagAssembleGraphJs from '/snippets/code-samples/agentic-rag-assemble-graph-js.mdx'; +import AgenticRagAssembleGraphPy from '/snippets/code-samples/agentic-rag-assemble-graph-py.mdx'; +import AgenticRagCreateRetrieverPy from '/snippets/code-samples/agentic-rag-create-retriever-py.mdx'; +import AgenticRagCreateRetrieverToolJs from '/snippets/code-samples/agentic-rag-create-retriever-tool-js.mdx'; +import AgenticRagCreateRetrieverToolPy from '/snippets/code-samples/agentic-rag-create-retriever-tool-py.mdx'; +import AgenticRagGenerateAnswerJs from '/snippets/code-samples/agentic-rag-generate-answer-js.mdx'; +import AgenticRagGenerateAnswerPy from '/snippets/code-samples/agentic-rag-generate-answer-py.mdx'; +import AgenticRagGenerateQueryOrRespondJs from '/snippets/code-samples/agentic-rag-generate-query-or-respond-js.mdx'; +import AgenticRagGenerateQueryOrRespondPy from '/snippets/code-samples/agentic-rag-generate-query-or-respond-py.mdx'; +import AgenticRagGradeDocumentsJs from '/snippets/code-samples/agentic-rag-grade-documents-js.mdx'; +import AgenticRagGradeDocumentsPy from '/snippets/code-samples/agentic-rag-grade-documents-py.mdx'; +import AgenticRagPreprocessJs from '/snippets/code-samples/agentic-rag-preprocess-js.mdx'; +import AgenticRagPreprocessPy from '/snippets/code-samples/agentic-rag-preprocess-py.mdx'; +import AgenticRagRewriteQuestionJs from '/snippets/code-samples/agentic-rag-rewrite-question-js.mdx'; +import AgenticRagRewriteQuestionPy from '/snippets/code-samples/agentic-rag-rewrite-question-py.mdx'; +import AgenticRagRunAgentJs from '/snippets/code-samples/agentic-rag-run-agent-js.mdx'; +import AgenticRagRunAgentPy from '/snippets/code-samples/agentic-rag-run-agent-py.mdx'; +import AgenticRagSetupEnvPy from '/snippets/code-samples/agentic-rag-setup-env-py.mdx'; +import AgenticRagSplitDocumentsJs from '/snippets/code-samples/agentic-rag-split-documents-js.mdx'; +import AgenticRagSplitDocumentsPy from '/snippets/code-samples/agentic-rag-split-documents-py.mdx'; +import AgenticRagVisualizeGraphPy from '/snippets/code-samples/agentic-rag-visualize-graph-py.mdx'; + ## Overview In this tutorial we will build a [retrieval](/oss/langchain/retrieval) agent using LangGraph. @@ -30,39 +52,28 @@ Let's download the required packages and set our API keys: :::python ```python -pip install -U langgraph "langchain[openai]" langchain-community langchain-text-splitters bs4 +pip install -U langgraph langchain-anthropic langchain-text-splitters bs4 requests ``` -```python -import getpass -import os - - -def _set_env(key: str): - if key not in os.environ: - os.environ[key] = getpass.getpass(f"{key}:") - - -_set_env("OPENAI_API_KEY") -``` + ::: :::js ```bash npm -npm install @langchain/langgraph @langchain/openai @langchain/community @langchain/textsplitters +npm install @langchain/langgraph @langchain/openai @langchain/textsplitters cheerio ``` ```bash pnpm -pnpm install @langchain/langgraph @langchain/openai @langchain/community @langchain/textsplitters +pnpm install @langchain/langgraph @langchain/openai @langchain/textsplitters cheerio ``` ```bash yarn -yarn add @langchain/langgraph @langchain/openai @langchain/community @langchain/textsplitters +yarn add @langchain/langgraph @langchain/openai @langchain/textsplitters cheerio ``` ```bash bun -bun add @langchain/langgraph @langchain/openai @langchain/community @langchain/textsplitters +bun add @langchain/langgraph @langchain/openai @langchain/textsplitters cheerio ``` @@ -75,64 +86,17 @@ bun add @langchain/langgraph @langchain/openai @langchain/community @langchain/t ## 1. Preprocess documents :::python -1. Fetch documents to use in our RAG system. We will use three of the most recent pages from [Lilian Weng's excellent blog](https://lilianweng.github.io/). We'll start by fetching the content of the pages using `WebBaseLoader` utility: - ```python - from langchain_community.document_loaders import WebBaseLoader - - urls = [ - "https://lilianweng.github.io/posts/2024-11-28-reward-hacking/", - "https://lilianweng.github.io/posts/2024-07-07-hallucination/", - "https://lilianweng.github.io/posts/2024-04-12-diffusion-video/", - ] - - docs = [WebBaseLoader(url).load() for url in urls] - ``` - ```python - docs[0][0].page_content.strip()[:1000] - ``` +1. Fetch documents to use in our RAG system. We will use three of the most recent pages from [Lilian Weng's excellent blog](https://lilianweng.github.io/). We'll start by fetching the content of the pages with a minimal helper built on `requests` and `BeautifulSoup`. + 2. Split the fetched documents into smaller chunks for indexing into our vectorstore: - ```python - from langchain_text_splitters import RecursiveCharacterTextSplitter - - docs_list = [item for sublist in docs for item in sublist] - - text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder( - chunk_size=100, chunk_overlap=50 - ) - doc_splits = text_splitter.split_documents(docs_list) - ``` - ```python - doc_splits[0].page_content.strip() - ``` + ::: :::js -1. Fetch documents to use in our RAG system. We will use three of the most recent pages from [Lilian Weng's excellent blog](https://lilianweng.github.io/). We'll start by fetching the content of the pages using `CheerioWebBaseLoader`: - ```typescript - import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio"; - - const urls = [ - "https://lilianweng.github.io/posts/2023-06-23-agent/", - "https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/", - "https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/", - ]; - - const docs = await Promise.all( - urls.map((url) => new CheerioWebBaseLoader(url).load()), - ); - ``` +1. Fetch documents to use in our RAG system. We will use three of the most recent pages from [Lilian Weng's excellent blog](https://lilianweng.github.io/). We'll start by fetching the content of the pages with a minimal helper built on `fetch` and `cheerio`: + 2. Split the fetched documents into smaller chunks for indexing into our vectorstore: - ```typescript - import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters"; - - const docsList = docs.flat(); - - const textSplitter = new RecursiveCharacterTextSplitter({ - chunkSize: 500, - chunkOverlap: 50, - }); - const docSplits = await textSplitter.splitDocuments(docsList); - ``` + ::: ## 2. Create a retriever tool @@ -141,27 +105,9 @@ Now that we have our split documents, we can index them into a vector store that :::python 1. Use an in-memory vector store and OpenAI embeddings: - ```python - from langchain_core.vectorstores import InMemoryVectorStore - from langchain_openai import OpenAIEmbeddings - - vectorstore = InMemoryVectorStore.from_documents( - documents=doc_splits, embedding=OpenAIEmbeddings() - ) - retriever = vectorstore.as_retriever() - ``` + 2. Create a retriever tool using the `@tool` decorator: - ```python - from langchain.tools import tool - - @tool - def retrieve_blog_posts(query: str) -> str: - """Search and return information about Lilian Weng blog posts.""" - docs = retriever.invoke(query) - return "\n\n".join([doc.page_content for doc in docs]) - - retriever_tool = retrieve_blog_posts - ``` + 3. Test the tool: ```python retriever_tool.invoke({"query": "types of reward hacking"}) @@ -170,17 +116,7 @@ Now that we have our split documents, we can index them into a vector store that :::js 1. Use an in-memory vector store and OpenAI embeddings: - ```typescript - import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory"; - import { OpenAIEmbeddings } from "@langchain/openai"; - - const vectorStore = await MemoryVectorStore.fromDocuments( - docSplits, - new OpenAIEmbeddings(), - ); - - const retriever = vectorStore.asRetriever(); - ``` + 2. Create a retriever tool using LangChain's prebuilt `createRetrieverTool`: ```typescript import { createRetrieverTool } from "@langchain/classic/tools/retriever"; @@ -205,23 +141,7 @@ Now we will start building components ([nodes](/oss/langgraph/graph-api#nodes) a Note that the components will operate on the [`MessagesState`](/oss/langgraph/graph-api#messagesstate)—graph state that contains a `messages` key with a list of [chat messages](https://python.langchain.com/docs/concepts/messages/). 1. Build a `generate_query_or_respond` node. It will call an LLM to generate a response based on the current graph state (list of messages). Given the input messages, it will decide to retrieve using the retriever tool, or respond directly to the user. Note that we're giving the chat model access to the `retriever_tool` we created earlier via `.bind_tools`: - ```python - from langgraph.graph import MessagesState - from langchain.chat_models import init_chat_model - - response_model = init_chat_model("gpt-4.1", temperature=0) - - - def generate_query_or_respond(state: MessagesState): - """Call the model to generate a response based on the current state. Given - the question, it will decide to retrieve using the retriever tool, or simply respond to the user. - """ - response = ( - response_model - .bind_tools([retriever_tool]).invoke(state["messages"]) # [!code highlight] - ) - return {"messages": [response]} - ``` + 2. Try it on a random input: ```python input = {"messages": [{"role": "user", "content": "hello!"}]} @@ -258,22 +178,7 @@ Note that the components will operate on the [`MessagesState`](/oss/langgraph/gr :::js 1. Build a `generateQueryOrRespond` node. It will call an LLM to generate a response based on the current graph state (list of messages). Given the input messages, it will decide to retrieve using the retriever tool, or respond directly to the user. Note that we're giving the chat model access to the `tools` we created earlier via `.bindTools`: - ```typescript - import { ChatOpenAI } from "@langchain/openai"; - import { GraphNode } from "@langchain/langgraph"; - - const generateQueryOrRespond: GraphNode = async (state) => { - const model = new ChatOpenAI({ - model: "gpt-4.1", - temperature: 0, - }).bindTools(tools); // [!code highlight] - - const response = await model.invoke(state.messages); - return { - messages: [response], - }; - } - ``` + 2. Try it on a random input: ```typescript import { HumanMessage } from "@langchain/core/messages"; @@ -319,51 +224,7 @@ Note that the components will operate on the [`MessagesState`](/oss/langgraph/gr :::python 1. Add a [conditional edge](/oss/langgraph/graph-api#conditional-edges)—`grade_documents`—to determine whether the retrieved documents are relevant to the question. We will use a model with a structured output schema `GradeDocuments` for document grading. The `grade_documents` function will return the name of the node to go to based on the grading decision (`generate_answer` or `rewrite_question`): - ```python - from pydantic import BaseModel, Field - from typing import Literal - - GRADE_PROMPT = ( - "You are a grader assessing relevance of a retrieved document to a user question. \n " - "Here is the retrieved document: \n\n {context} \n\n" - "Here is the user question: {question} \n" - "If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \n" - "Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question." - ) - - - class GradeDocuments(BaseModel): # [!code highlight] - """Grade documents using a binary score for relevance check.""" - - binary_score: str = Field( - description="Relevance score: 'yes' if relevant, or 'no' if not relevant" - ) - - - grader_model = init_chat_model("gpt-4.1", temperature=0) - - - def grade_documents( - state: MessagesState, - ) -> Literal["generate_answer", "rewrite_question"]: - """Determine whether the retrieved documents are relevant to the question.""" - question = state["messages"][0].content - context = state["messages"][-1].content - - prompt = GRADE_PROMPT.format(question=question, context=context) - response = ( - grader_model - .with_structured_output(GradeDocuments).invoke( # [!code highlight] - [{"role": "user", "content": prompt}] - ) - ) - score = response.binary_score - - if score == "yes": - return "generate_answer" - else: - return "rewrite_question" - ``` + 2. Run this with irrelevant documents in the tool response: ```python from langchain_core.messages import convert_to_messages @@ -425,48 +286,8 @@ Note that the components will operate on the [`MessagesState`](/oss/langgraph/gr ::: :::js -1. Add a node—`gradeDocuments`—to determine whether the retrieved documents are relevant to the question. We will use a model with structured output using Zod for document grading. We'll also add a [conditional edge](/oss/langgraph/graph-api#conditional-edges)—`checkRelevance`—that checks the grading result and returns the name of the node to go to (`generate` or `rewrite`): - ```typescript - import * as z from "zod"; - import { ChatPromptTemplate } from "@langchain/core/prompts"; - import { ChatOpenAI } from "@langchain/openai"; - import { GraphNode } from "@langchain/langgraph"; - import { AIMessage } from "@langchain/core/messages"; - - const prompt = ChatPromptTemplate.fromTemplate( - `You are a grader assessing relevance of retrieved docs to a user question. - Here are the retrieved docs: - \n ------- \n - {context} - \n ------- \n - Here is the user question: {question} - If the content of the docs are relevant to the users question, score them as relevant. - Give a binary score 'yes' or 'no' score to indicate whether the docs are relevant to the question. - Yes: The docs are relevant to the question. - No: The docs are not relevant to the question.`, - ); - - const gradeDocumentsSchema = z.object({ - binaryScore: z.string().describe("Relevance score 'yes' or 'no'"), // [!code highlight] - }) - - const gradeDocuments: GraphNode = async (state) => { - const model = new ChatOpenAI({ - model: "gpt-4.1", - temperature: 0, - }).withStructuredOutput(gradeDocumentsSchema); - - const score = await prompt.pipe(model).invoke({ - question: state.messages.at(0)?.content, - context: state.messages.at(-1)?.content, - }); - - if (score.binaryScore === "yes") { - return "generate"; - } - return "rewrite"; - } - ``` +1. Add a node—`gradeDocuments`—to determine whether the retrieved documents are relevant to the question. This node first uses a model with structured output using Zod for document grading, and falls back to a plain yes or no response if structured parsing fails. We then add a [conditional edge](/oss/langgraph/graph-api#conditional-edges) that routes according to the `gradeDocuments` result (`generate` or `rewrite`): + 2. Run this with irrelevant documents in the tool response: ```typescript import { ToolMessage } from "@langchain/core/messages"; @@ -521,27 +342,7 @@ Note that the components will operate on the [`MessagesState`](/oss/langgraph/gr :::python 1. Build the `rewrite_question` node. The retriever tool can return potentially irrelevant documents, which indicates a need to improve the original user question. To do so, we will call the `rewrite_question` node: - ```python - from langchain.messages import HumanMessage - - REWRITE_PROMPT = ( - "Look at the input and try to reason about the underlying semantic intent / meaning.\n" - "Here is the initial question:" - "\n ------- \n" - "{question}" - "\n ------- \n" - "Formulate an improved question:" - ) - - - def rewrite_question(state: MessagesState): - """Rewrite the original user question.""" - messages = state["messages"] - question = messages[0].content - prompt = REWRITE_PROMPT.format(question=question) - response = response_model.invoke([{"role": "user", "content": prompt}]) - return {"messages": [HumanMessage(content=response.content)]} - ``` + 2. Try it out: ```python input = { @@ -578,34 +379,7 @@ Note that the components will operate on the [`MessagesState`](/oss/langgraph/gr :::js 1. Build the `rewrite` node. The retriever tool can return potentially irrelevant documents, which indicates a need to improve the original user question. To do so, we will call the `rewrite` node: - ```typescript - import { ChatPromptTemplate } from "@langchain/core/prompts"; - import { ChatOpenAI } from "@langchain/openai"; - import { GraphNode } from "@langchain/langgraph"; - - const rewritePrompt = ChatPromptTemplate.fromTemplate( - `Look at the input and try to reason about the underlying semantic intent / meaning. \n - Here is the initial question: - \n ------- \n - {question} - \n ------- \n - Formulate an improved question:`, - ); - - const rewrite: GraphNode = async (state) => { - const question = state.messages.at(0)?.content; - - const model = new ChatOpenAI({ - model: "gpt-4.1", - temperature: 0, - }); - - const response = await rewritePrompt.pipe(model).invoke({ question }); - return { - messages: [response], - }; - } - ``` + 2. Try it out: ```typescript import { HumanMessage, AIMessage, ToolMessage } from "@langchain/core/messages"; @@ -641,25 +415,7 @@ Note that the components will operate on the [`MessagesState`](/oss/langgraph/gr :::python 1. Build `generate_answer` node: if we pass the grader checks, we can generate the final answer based on the original question and the retrieved context: - ```python - GENERATE_PROMPT = ( - "You are an assistant for question-answering tasks. " - "Use the following pieces of retrieved context to answer the question. " - "If you don't know the answer, just say that you don't know. " - "Use three sentences maximum and keep the answer concise.\n" - "Question: {question} \n" - "Context: {context}" - ) - - - def generate_answer(state: MessagesState): - """Generate an answer.""" - question = state["messages"][0].content - context = state["messages"][-1].content - prompt = GENERATE_PROMPT.format(question=question, context=context) - response = response_model.invoke([{"role": "user", "content": prompt}]) - return {"messages": [response]} - ``` + 2. Try it: ```python input = { @@ -702,41 +458,7 @@ Note that the components will operate on the [`MessagesState`](/oss/langgraph/gr :::js 1. Build `generate` node: if we pass the grader checks, we can generate the final answer based on the original question and the retrieved context: - ```typescript - import { ChatPromptTemplate } from "@langchain/core/prompts"; - import { ChatOpenAI } from "@langchain/openai"; - import { GraphNode } from "@langchain/langgraph"; - - const generate: GraphNode = async (state) => { - const question = state.messages.at(0)?.content; - const context = state.messages.at(-1)?.content; - - const prompt = ChatPromptTemplate.fromTemplate( - `You are an assistant for question-answering tasks. - Use the following pieces of retrieved context to answer the question. - If you don't know the answer, just say that you don't know. - Use three sentences maximum and keep the answer concise. - Question: {question} - Context: {context}` - ); - - const llm = new ChatOpenAI({ - model: "gpt-4.1", - temperature: 0, - }); - - const ragChain = prompt.pipe(llm); - - const response = await ragChain.invoke({ - context, - question, - }); - - return { - messages: [response], - }; - } - ``` + 2. Try it: ```typescript import { HumanMessage, AIMessage, ToolMessage } from "@langchain/core/messages"; @@ -777,59 +499,18 @@ Now we'll assemble all the nodes and edges into a complete graph: :::python * Start with a `generate_query_or_respond` and determine if we need to call `retriever_tool` -* Route to next step using `tools_condition`: +* Route to next step based on whether the model made tool calls: * If `generate_query_or_respond` returned `tool_calls`, call `retriever_tool` to retrieve context * Otherwise, respond directly to the user * Grade retrieved document content for relevance to the question (`grade_documents`) and route to next step: * If not relevant, rewrite the question using `rewrite_question` and then call `generate_query_or_respond` again * If relevant, proceed to `generate_answer` and generate final response using the @[`ToolMessage`] with the retrieved document context -```python -from langgraph.graph import StateGraph, START, END -from langgraph.prebuilt import ToolNode, tools_condition - -workflow = StateGraph(MessagesState) - -# Define the nodes we will cycle between -workflow.add_node(generate_query_or_respond) -workflow.add_node("retrieve", ToolNode([retriever_tool])) -workflow.add_node(rewrite_question) -workflow.add_node(generate_answer) - -workflow.add_edge(START, "generate_query_or_respond") - -# Decide whether to retrieve -workflow.add_conditional_edges( - "generate_query_or_respond", - # Assess LLM decision (call `retriever_tool` tool or respond to the user) - tools_condition, - { - # Translate the condition outputs to nodes in our graph - "tools": "retrieve", - END: END, - }, -) - -# Edges taken after the `action` node is called. -workflow.add_conditional_edges( - "retrieve", - # Assess agent decision - grade_documents, -) -workflow.add_edge("generate_answer", END) -workflow.add_edge("rewrite_question", "generate_query_or_respond") - -# Compile -graph = workflow.compile() -``` + Visualize the graph: -```python -from IPython.display import Image, display - -display(Image(graph.get_graph().draw_mermaid_png())) -``` + = (state) => { - const lastMessage = state.messages.at(-1); - if (AIMessage.isInstance(lastMessage) && lastMessage.tool_calls.length) { - return "retrieve"; - } - return END; -} - -// Define the graph -const builder = new StateGraph(State) - .addNode("generateQueryOrRespond", generateQueryOrRespond) - .addNode("retrieve", toolNode) - .addNode("gradeDocuments", gradeDocuments) - .addNode("rewrite", rewrite) - .addNode("generate", generate) - // Add edges - .addEdge(START, "generateQueryOrRespond") - // Decide whether to retrieve - .addConditionalEdges("generateQueryOrRespond", shouldRetrieve) - .addEdge("retrieve", "gradeDocuments") - // Edges taken after grading documents - .addConditionalEdges( - "gradeDocuments", - // Route based on grading decision - (state) => { - // The gradeDocuments function returns either "generate" or "rewrite" - const lastMessage = state.messages.at(-1); - return lastMessage.content === "generate" ? "generate" : "rewrite"; - } - ) - .addEdge("generate", END) - .addEdge("rewrite", "generateQueryOrRespond"); - -// Compile -const graph = builder.compile(); -``` + ::: ## 8. Run the agentic RAG @@ -899,117 +536,9 @@ const graph = builder.compile(); Now let's test the complete graph by running it with a question: :::python -```python -for chunk in graph.stream( - { - "messages": [ - { - "role": "user", - "content": "What does Lilian Weng say about types of reward hacking?", - } - ] - } -): - for node, update in chunk.items(): - print("Update from node", node) - update["messages"][-1].pretty_print() - print("\n\n") -``` - -**Output:** - -``` -Update from node generate_query_or_respond -================================== Ai Message ================================== -Tool Calls: - retrieve_blog_posts (call_NYu2vq4km9nNNEFqJwefWKu1) - Call ID: call_NYu2vq4km9nNNEFqJwefWKu1 - Args: - query: types of reward hacking - - - -Update from node retrieve -================================= Tool Message ================================== -Name: retrieve_blog_posts - -(Note: Some work defines reward tampering as a distinct category of misalignment behavior from reward hacking. But I consider reward hacking as a broader concept here.) -At a high level, reward hacking can be categorized into two types: environment or goal misspecification, and reward tampering. - -Why does Reward Hacking Exist?# - -Pan et al. (2022) investigated reward hacking as a function of agent capabilities, including (1) model size, (2) action space resolution, (3) observation space noise, and (4) training time. They also proposed a taxonomy of three types of misspecified proxy rewards: - -Let's Define Reward Hacking# -Reward shaping in RL is challenging. Reward hacking occurs when an RL agent exploits flaws or ambiguities in the reward function to obtain high rewards without genuinely learning the intended behaviors or completing the task as designed. In recent years, several related concepts have been proposed, all referring to some form of reward hacking: - - - -Update from node generate_answer -================================== Ai Message ================================== - -Lilian Weng categorizes reward hacking into two types: environment or goal misspecification, and reward tampering. She considers reward hacking as a broad concept that includes both of these categories. Reward hacking occurs when an agent exploits flaws or ambiguities in the reward function to achieve high rewards without performing the intended behaviors. -``` + ::: :::js -```typescript -import { HumanMessage } from "@langchain/core/messages"; - -const inputs = { - messages: [ - new HumanMessage("What does Lilian Weng say about types of reward hacking?") - ] -}; - -for await (const output of await graph.stream(inputs)) { - for (const [key, value] of Object.entries(output)) { - const lastMsg = output[key].messages[output[key].messages.length - 1]; - console.log(`Output from node: '${key}'`); - console.log({ - type: lastMsg._getType(), - content: lastMsg.content, - tool_calls: lastMsg.tool_calls, - }); - console.log("---\n"); - } -} -``` - -**Output:** - -``` -Output from node: 'generateQueryOrRespond' -{ - type: 'ai', - content: '', - tool_calls: [ - { - name: 'retrieve_blog_posts', - args: { query: 'types of reward hacking' }, - id: 'call_...', - type: 'tool_call' - } - ] -} ---- - -Output from node: 'retrieve' -{ - type: 'tool', - content: '(Note: Some work defines reward tampering as a distinct category...\n' + - 'At a high level, reward hacking can be categorized into two types: environment or goal misspecification, and reward tampering.\n' + - '...', - tool_calls: undefined -} ---- - -Output from node: 'generate' -{ - type: 'ai', - content: 'Lilian Weng categorizes reward hacking into two types: environment or goal misspecification, and reward tampering. She considers reward hacking as a broad concept that includes both of these categories. Reward hacking occurs when an agent exploits flaws or ambiguities in the reward function to achieve high rewards without performing the intended behaviors.', - tool_calls: [] -} ---- -``` + ::: diff --git a/backward-compatibility.mdx b/backward-compatibility.mdx new file mode 100644 index 0000000..7e4b755 --- /dev/null +++ b/backward-compatibility.mdx @@ -0,0 +1,167 @@ +--- +title: Backward compatibility +description: Update LangGraph graph code in production without breaking in-flight runs. +--- + +Software needs to change in production. New requirements, bug fixes, and refactors all eventually land in your graph code. Because LangGraph runs the latest deployed graph against state that has been [persisted](/oss/langgraph/persistence) for existing threads, every change you ship is effectively a backward-compatible API change with respect to your existing checkpoints. + +Unlike workflow engines that pin a run to the version of code it started with, LangGraph applies the latest graph immediately to *every* thread, both new threads and threads that resume from a checkpoint. This is convenient: bug fixes propagate to in-flight conversations and agents without ceremony. It also means you must reason about how each change interacts with runs that started under the previous version of the code. + +There are three categories of compatibility issues to watch for, in roughly the order you will encounter them: + +1. [Technical compatibility](#technical-compatibility): The most common; the new code must still load and execute against existing State. +2. [Business compatibility](#business-compatibility): Less common; existing runs should keep following the old business logic even though the code has changed. +3. [Non-determinism](#non-determinism): Only applies to the [Functional API](/oss/langgraph/functional-api). + + +For a short summary of which graph topology and state changes the runtime supports by default, see [Graph migrations](/oss/langgraph/graph-api#graph-migrations). The rest of this page covers the patterns you can apply when a change falls outside that supported set. + + +## Technical compatibility + +Technical compatibility is the equivalent of an API breaking change in a microservice. The "API" here is the contract between your graph code and the data already persisted by the [checkpointer](/oss/langgraph/checkpointers#checkpointer-libraries) for existing threads. When a thread resumes, LangGraph deserializes the saved state, dispatches it to a node by name, and expects the node to return values that fit the state schema. + +Common technical breakages: + +- **Renaming or removing a node** while threads are paused at or about to enter that node, for example at an @[`interrupt`] or via a checkpointed conditional edge that still routes to the old name. On resume, LangGraph cannot find the node by its saved name and the run fails. The starting point for resuming a run is the beginning of the node where execution stopped, so a missing node has nowhere to resume from. +- **Renaming or removing a State key** that older checkpoints still contain or that downstream nodes still read. +- **Tightening a State field**, such as making an `Optional` field required, narrowing a type, or adding a new required field with no default. Existing checkpoints will not satisfy the new schema. + +Edge topology itself is *not* persisted in the checkpoint. Adding, removing, or rerouting edges between nodes that still exist is safe for in-flight threads. Per the [Graph migrations](/oss/langgraph/graph-api#graph-migrations) summary, the only topology change that can break an interrupted thread is renaming or removing a node. + +### Recommended patterns + +:::python + +- Add new state fields as `NotRequired` (or `Optional[...] = None`) so old checkpoints still validate: + + ```python + from typing import NotRequired + from typing_extensions import TypedDict + + class State(TypedDict): + messages: list + summary: NotRequired[str] # [!code ++] + ``` + +- Treat removals as deprecations. Keep the field defined on the state for at least one drain cycle, even if no node reads it, so existing checkpoints continue to load. +- Rename through *add-then-remove*. Add the new field or node alongside the old one, dual-write or route to both for a deprecation window, then remove the old one once you have confirmed no in-flight thread depends on it. +- Keep node functions tolerant of unknown keys. `TypedDict` ignores extra keys at runtime, so leftover state from an older code version will not raise unless a node explicitly reads a missing key. +- Use [time travel](/oss/langgraph/use-time-travel) and @[`graph.get_state`][get_state] to spot-check existing threads against the new code in a staging deployment before rolling out. + +::: + +:::js + +- Mark new state fields as optional (`z.string().optional()` or `.nullish()`) so old checkpoints still validate. +- Treat removals as deprecations: keep the field on the schema for at least one drain cycle so existing checkpoints continue to load. +- Rename via add-then-remove: add the new field or node alongside the old one, dual-write or route to both for a deprecation window, then remove the old one once no in-flight thread depends on it. +- Use [time travel](/oss/langgraph/use-time-travel) and @[`graph.getState`][get_state] to spot-check existing threads against the new code in a staging deployment before rolling out. + +::: + +### Detecting in-flight threads + +Before you remove a node, rename a State key, or otherwise make a change that older threads cannot tolerate, you want to know whether any threads are currently parked on the version of the code you are about to drop. LangGraph itself does not maintain a search index over thread state, so the answer depends on where your graph runs. + +**If you deploy to [LangSmith](/langsmith/deployment).** Use the Agent Server's thread search to filter by status. The `status` field accepts `idle`, `busy`, `interrupted`, and `error`, so you can bulk-query for `interrupted` or `busy` threads, optionally narrowed with metadata filters. See [Filter by thread status](/langsmith/use-threads#filter-by-thread-status) and [List threads](/langsmith/use-threads#list-threads). + +**Anywhere LangGraph runs.** Use [LangSmith tracing](/oss/langgraph/observability) to monitor which nodes are being entered and exited in production. This is the most reliable signal that a node or state field is no longer reachable in any active code path. + +**When you already have a `thread_id`.** Inspect that single thread directly: + +:::python + +- @[`graph.get_state(config)`][get_state] returns the latest checkpoint, including which node the thread is paused at and any pending interrupts. +- @[`graph.get_state_history(config)`][get_state_history] returns the full chronological list of checkpoints for the thread. + +::: + +:::js + +- @[`graph.getState(config)`][get_state] returns the latest checkpoint, including which node the thread is paused at and any pending interrupts. +- @[`graph.getStateHistory(config)`][get_state_history] returns the full chronological list of checkpoints for the thread. + +::: + +When in doubt, keep the deprecated node or field in place until both the Agent Server thread list and tracing show no further activity on it. + +## Business compatibility + +Sometimes a change is technically valid (every existing checkpoint still loads and every node still resolves), but the *meaning* of the new graph differs from the old one. The new behavior is correct for new threads, and you do not want to retroactively apply it to threads that started under the old logic. + +For example, suppose your graph runs `intake → triage → respond`, and you decide to insert a new `policy_check` step between `triage` and `respond`: + +- Threads that have already passed `triage` should continue straight to `respond` (the old flow). +- New threads should run the full new flow. + +The recommended pattern is to record the relevant *behavioral version* on the state at thread start, then branch on it with a [conditional edge](/oss/langgraph/graph-api#conditional-edges): + +:::python + +```python +from typing import NotRequired +from typing_extensions import TypedDict + +from langgraph.graph import END, START, StateGraph + + +class State(TypedDict): + request: str + flow_version: NotRequired[int] + response: NotRequired[str] + + +def intake(state: State) -> dict: + # Stamp new threads with the current flow version. Existing threads + # that resume past `intake` keep whatever value was already saved. + return {"flow_version": state.get("flow_version", 2)} + + +def triage(state: State) -> dict: ... +def policy_check(state: State) -> dict: ... +def respond(state: State) -> dict: ... + + +def after_triage(state: State) -> str: + if state.get("flow_version", 1) >= 2: + return "policy_check" + return "respond" + + +builder = StateGraph(State) +builder.add_node("intake", intake) +builder.add_node("triage", triage) +builder.add_node("policy_check", policy_check) +builder.add_node("respond", respond) +builder.add_edge(START, "intake") +builder.add_edge("intake", "triage") +builder.add_conditional_edges("triage", after_triage, ["policy_check", "respond"]) +builder.add_edge("policy_check", "respond") +builder.add_edge("respond", END) + +graph = builder.compile() +``` + +::: + +Old threads that resume after `triage` read `flow_version` from their saved state (or fall through to the v1 default) and skip `policy_check`. New threads start at `intake`, are stamped with `flow_version=2`, and run the new path. Once all v1 threads have completed, you can remove the version flag and the conditional edge. + +This pattern only works if you set the version *at thread start*, before any branch that needs to be versioned. Setting it later means existing threads will not have it set when they need it. + +## Non-determinism + +This category only applies to the [Functional API](/oss/langgraph/functional-api) and to [**tasks**](/oss/langgraph/functional-api#task) or @[`interrupt`] calls inside a [Graph API](/oss/langgraph/graph-api) **node**. Plain Graph API **nodes** [re-run from the start of the node function](/oss/langgraph/graph-api#re-execution-and-idempotency) on resume; design side effects to be idempotent, but you do not need to preserve task call order unless you use **tasks** or @[`interrupt`] in that **node**. + +A Functional API **entrypoint** compiles to a single **node** that replays the entrypoint body from the beginning when a run resumes, using cached @[`@task`][task] results to skip work that has already been done. Two kinds of changes break this model: + +- **Adding, removing, or reordering `@task` calls or @[`interrupt`] calls** that come *before* the resume point. LangGraph matches cached results and resume values to calls by their position in the replay, so shifting that position can cause the wrong cached value to be replayed against a different call. +- **Introducing non-deterministic operations outside of a `@task`**, such as `time.time()`, `random.random()`, or a network call inlined in the entrypoint body. On replay these produce different values than they did on the first run, which can change the control flow. + +For a deeper treatment with examples, see [Determinism](/oss/langgraph/functional-api#determinism) and [Common pitfalls](/oss/langgraph/functional-api#common-pitfalls) in the Functional API guide. + +If you need to make non-trivial code changes to an `@entrypoint` that has in-flight runs, the safest options are: + +- Let in-flight runs drain before deploying the change. +- Wrap any new logic in a new `@task` so its results are checkpointed independently. +- Register a new entrypoint under a new graph name in `langgraph.json` for the new behavior, and route new threads to it. diff --git a/case-studies.mdx b/case-studies.mdx index 9c191a1..a0db9f3 100644 --- a/case-studies.mdx +++ b/case-studies.mdx @@ -3,7 +3,6 @@ title: Case studies --- - This list of companies using LangGraph and their success stories is compiled from public sources. If your company uses LangGraph, we'd love for you to share your story and add it to the list. You’re also welcome to contribute updates based on publicly available information from other companies, such as blog posts or press releases. | Company | Industry | Use case | Reference | diff --git a/checkpointers.mdx b/checkpointers.mdx new file mode 100644 index 0000000..357d42e --- /dev/null +++ b/checkpointers.mdx @@ -0,0 +1,1159 @@ +--- +title: Checkpointers +description: LangGraph checkpointers save graph state as checkpoints at each step, enabling persistence, human-in-the-loop, and fault-tolerant execution. +--- + +A checkpointer saves a snapshot of graph state at each super-step, organized into **threads**. Compile a graph with a checkpointer to enable human-in-the-loop workflows, time travel debugging, fault-tolerant execution, and conversational memory. + +![Checkpoints](/oss/images/checkpoints.jpg) + + +**Agent Server handles checkpointing automatically** +When using the [Agent Server](/langsmith/agent-server), you do not need to implement or configure checkpointers manually. The server handles all persistence infrastructure for you behind the scenes. + + + +Trace checkpointed state and debug how your agent resumes across sessions with [LangSmith](https://smith.langchain.com). Follow the [tracing quickstart](/langsmith/trace-with-langgraph) to get set up. + + +## Why use checkpointers + +Checkpointers are required for the following features: + +- **Human-in-the-loop**: 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 person has to be able to view the state of a graph at any point in time, and the graph has to be able to resume execution after the person has made any updates to the state. See [Interrupts](/oss/langgraph/interrupts) for examples. +- **Memory**: 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. +- **Time travel**: 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. +- **Fault-tolerance**: Checkpointing 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. + +- **Pending writes**: When a graph node fails mid-execution at a given [super-step](#super-steps), LangGraph stores pending checkpoint writes from any other nodes that completed successfully at that super-step. When you resume graph execution from that super-step you don't re-run the successful nodes. + +## Core concepts + +### 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/runs). 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://reference.langchain.com/python/langsmith/) for more details. + +The checkpointer uses `thread_id` as the primary key for storing and retrieving checkpoints. Without it, the checkpointer cannot save state or resume execution after an [interrupt](/oss/langgraph/interrupts), since the checkpointer uses `thread_id` to load the saved state. + +### Checkpoints + +The state of a thread at a particular point in time is called a checkpoint. A checkpoint is a snapshot of the graph state saved at each [super-step](#super-steps) and is represented by a `StateSnapshot` object (see [StateSnapshot fields](#statesnapshot-fields) for the full field reference). + +#### Super-steps + +LangGraph creates a checkpoint at each **super-step** boundary. A super-step is a single "tick" of the graph where all nodes scheduled for that step execute (potentially in parallel). For a sequential graph like `START -> A -> B -> END`, there are separate super-steps for the input, node A, and node B — producing a checkpoint after each one. Understanding super-step boundaries is important for [time travel](/oss/langgraph/use-time-travel), because you can only resume execution from a checkpoint (i.e., a super-step boundary). + +In addition to super-step checkpoints, LangGraph also persists writes at the **node (task) level**. As each node within a super-step finishes, its outputs are written to the checkpointer's `checkpoint_writes` table as task entries linked to the in-progress checkpoint. These per-task writes are what enable [pending writes](#pending-writes) recovery: if another node in the same super-step fails, the successful nodes' writes are already durable and don't need to be re-run on resume. The full state snapshot is then committed once the super-step completes. + +LangGraph also persists writes from individual node executions within a super-step. These writes are stored as tasks and used for fault tolerance: if another node in the same super-step fails, successful node writes do not need to be recomputed when you resume. These task writes are not full `StateSnapshot` checkpoints, so time travel resumes from full checkpoints at super-step boundaries. + +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": "", "bar":[]}, config) +``` +::: + +:::js +```typescript +import { StateGraph, StateSchema, ReducedValue, START, END, MemorySaver } from "@langchain/langgraph"; +import { z } from "zod/v4"; + +const State = new StateSchema({ + foo: z.string(), + bar: new ReducedValue( + z.array(z.string()).default(() => []), + { + inputSchema: z.array(z.string()), + reducer: (x, y) => x.concat(y), + } + ), +}); + +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: "", bar: [] }, config); +``` +::: + +:::python +After you run the graph, there will be 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 the `bar` channel values contain outputs from both nodes because this example has a reducer for the `bar` channel. +::: + +:::js +After you run the graph, there will be 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 because this example has a reducer for the `bar` channel. +::: + +#### Checkpoint namespace + +Each checkpoint has a `checkpoint_ns` (checkpoint namespace) field that identifies which graph or subgraph it belongs to: + +- **`""`** (empty string): The checkpoint belongs to the parent (root) graph. +- **`"node_name:uuid"`**: The checkpoint belongs to a subgraph invoked as the given node. For nested subgraphs, namespaces are joined with `|` separators (e.g., `"outer_node:uuid|inner_node:uuid"`). + +You can access the checkpoint namespace from within a node via the config: + +:::python +```python +from langchain_core.runnables import RunnableConfig + +def my_node(state: State, config: RunnableConfig): + checkpoint_ns = config["configurable"]["checkpoint_ns"] + # "" for the parent graph, "node_name:uuid" for a subgraph +``` +::: + +:::js +```typescript +import { RunnableConfig } from "@langchain/core/runnables"; + +function myNode(state: typeof State.Type, config: RunnableConfig) { + const checkpointNs = config.configurable?.checkpoint_ns; + // "" for the parent graph, "node_name:uuid" for a subgraph +} +``` +::: + +See [Subgraphs](/oss/langgraph/use-subgraphs) for more details on working with subgraph state and checkpoints. + +## Get and update state + +### 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. + +```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 this example, the output of `get_state` 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=() +) +``` +::: + +:::js +In this example, the output of `getState` 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 fields + +:::python + +| Field | Type | Description | +|-------|------|-------------| +| `values` | `dict` | State channel values at this checkpoint. | +| `next` | `tuple[str, ...]` | Node names to execute next. Empty `()` means the graph is complete. | +| `config` | `dict` | Contains `thread_id`, `checkpoint_ns`, and `checkpoint_id`. | +| `metadata` | `dict` | Execution metadata. Contains `source` (`"input"`, `"loop"`, or `"update"`), `writes` (node outputs), and `step` (super-step counter). | +| `created_at` | `str` | ISO 8601 timestamp of when this checkpoint was created. | +| `parent_config` | `dict \| None` | Config of the previous checkpoint. `None` for the first checkpoint. | +| `tasks` | `tuple[PregelTask, ...]` | Tasks to execute at this step. Each task has `id`, `name`, `error`, `interrupts`, and optionally `state` (subgraph snapshot, when using `subgraphs=True`). | + +::: + +:::js + +| Field | Type | Description | +|-------|------|-------------| +| `values` | `object` | State channel values at this checkpoint. | +| `next` | `string[]` | Node names to execute next. Empty `[]` means the graph is complete. | +| `config` | `object` | Contains `thread_id`, `checkpoint_ns`, and `checkpoint_id`. | +| `metadata` | `object` | Execution metadata. Contains `source` (`"input"`, `"loop"`, or `"update"`), `writes` (node outputs), and `step` (super-step counter). | +| `createdAt` | `string` | ISO 8601 timestamp of when this checkpoint was created. | +| `parentConfig` | `object \| null` | Config of the previous checkpoint. `null` for the first checkpoint. | +| `tasks` | `PregelTask[]` | Tasks to execute at this step. Each task has `id`, `name`, `error`, `interrupts`, and optionally `state` (subgraph snapshot, when using `subgraphs: true`). | + +::: + +### Get state history + +:::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)) +``` +::: + +:::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 this 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 this 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) + +#### Find a specific checkpoint + +You can filter the state history to find checkpoints matching specific criteria: + +:::python +```python +history = list(graph.get_state_history(config)) + +# Find the checkpoint before a specific node executed +before_node_b = next(s for s in history if s.next == ("node_b",)) + +# Find a checkpoint by step number +step_2 = next(s for s in history if s.metadata["step"] == 2) + +# Find checkpoints created by update_state +forks = [s for s in history if s.metadata["source"] == "update"] + +# Find the checkpoint where an interrupt occurred +interrupted = next( + s for s in history + if s.tasks and any(t.interrupts for t in s.tasks) +) +``` +::: + +:::js +```typescript +const history: StateSnapshot[] = []; +for await (const state of graph.getStateHistory(config)) { + history.push(state); +} + +// Find the checkpoint before a specific node executed +const beforeNodeB = history.find((s) => s.next.includes("nodeB")); + +// Find a checkpoint by step number +const step2 = history.find((s) => s.metadata.step === 2); + +// Find checkpoints created by updateState +const forks = history.filter((s) => s.metadata.source === "update"); + +// Find the checkpoint where an interrupt occurred +const interrupted = history.find( + (s) => s.tasks.length > 0 && s.tasks.some((t) => t.interrupts.length > 0) +); +``` +::: + +### Replay + +Replay re-executes steps from a prior checkpoint. Invoke the graph with a prior `checkpoint_id` to re-run nodes after that checkpoint. Nodes before the checkpoint are skipped (their results are already saved). Nodes after the checkpoint re-execute, including any LLM calls, API requests, or [interrupts](/oss/langgraph/interrupts) — which are always re-triggered during replay. + +See [Time travel](/oss/langgraph/use-time-travel) for full details and code examples on replaying past executions. + +![Replay](/oss/images/re_play.png) + +### Update state + +:::python +You can edit the graph state using @[`update_state`]. This creates a new checkpoint with the updated values — it does not modify the original checkpoint. The update is treated the same as a node update: values are passed through [reducer](/oss/langgraph/graph-api#reducers) functions when defined, so channels with reducers _accumulate_ values rather than overwrite them. + +You can optionally specify `as_node` to control which node the update is treated as coming from, which affects which node executes next. See [Time travel: `as_node`](/oss/langgraph/use-time-travel#from-a-specific-node) for details. +::: + +:::js +You can edit the graph state using `graph.updateState()`. This creates a new checkpoint with the updated values — it does not modify the original checkpoint. The update is treated the same as a node update: values are passed through [reducer](/oss/langgraph/graph-api#reducers) functions when defined, so channels with reducers _accumulate_ values rather than overwrite them. + +You can optionally specify `asNode` to control which node the update is treated as coming from, which affects which node executes next. See [Time travel: `asNode`](/oss/langgraph/use-time-travel#from-a-specific-node) for details. +::: + +![Update](/oss/images/checkpoints_full_story.jpg) + +## Durability modes + +LangGraph supports three durability modes that let you balance performance and data consistency. You can specify the durability mode when calling any graph execution method: + +:::python +```python +graph.stream( + {"input": "test"}, + durability="sync" +) +``` +::: + +:::js +```typescript +await graph.stream( + { input: "test" }, + { durability: "sync" } +) +``` +::: + +The durability modes, from least to most durable, are as follows: + +* `"exit"`: LangGraph persists changes only when graph execution exits — successfully, with an error, or due to a human-in-the-loop interrupt. This provides the best performance for long-running graphs but means intermediate state is not saved, so you cannot recover from system failures (like process crashes) mid-execution. +* `"async"`: LangGraph persists changes asynchronously while the next step executes. This provides good performance and durability, but there is a small risk that LangGraph does not write checkpoints if the process crashes during execution. +* `"sync"`: LangGraph persists changes synchronously before the next step starts. This ensures that LangGraph writes every checkpoint before continuing execution, providing high durability at the cost of some performance overhead. + +## Optimize checkpoint storage + +:::python +By default, LangGraph checkpoints write the full value of every state channel at each super-step. For long-running threads with large accumulations—such as multi-turn conversations—this can produce significant storage growth over time. + +@[`DeltaChannel`] stores only incremental deltas instead of the full accumulated value, substantially reducing checkpoint size for append-heavy channels. See [DeltaChannel](/oss/langgraph/pregel#deltachannel) for usage and the storage-vs-latency tradeoff. + + +`DeltaChannel` requires `langgraph>=1.2` and is currently in beta. The API may change in future releases. + +::: + +## 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 + +See [checkpointer integrations](/oss/integrations/checkpointers/index) for available providers. + + +* `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. +* `langchain-azure-cosmosdb`: An implementation of LangGraph checkpointer that uses Azure Cosmos DB for NoSQL (@[`CosmosDBSaverSync`] / @[`CosmosDBSaver`]). Ideal for using in production with Azure. Supports both sync and async operations, with Microsoft Entra ID authentication. 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. +* `@langchain/langgraph-checkpoint-mongodb`: An advanced checkpointer (`MongoDBSaver`) and long-term memory store (`MongoDBStore`) backed by MongoDB. The store supports cross-thread persistence with optional integrated vector search. Ideal for production use. Needs to be installed separately. +* `@langchain/langgraph-checkpoint-redis`: An advanced checkpointer that uses Redis database (`RedisSaver`). 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`). + + +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()` +::: + +:::python + +### Serializer + +When checkpointers save the graph state, they need to serialize the channel values in the state. This is done using serializer objects. + +`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. + +If you want to fallback to pickle for objects not currently supported by the msgpack encoder (such as Pandas dataframes), +you can use the `pickle_fallback` argument of the @[`JsonPlusSerializer`]: + +```python +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer + +# ... Define the graph ... +graph.compile( + checkpointer=InMemorySaver(serde=JsonPlusSerializer(pickle_fallback=True)) +) +``` + +#### Encryption + +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): + +```python +import sqlite3 + +from langgraph.checkpoint.serde.encrypted import EncryptedSerializer +from langgraph.checkpoint.sqlite import SqliteSaver + +serde = EncryptedSerializer.from_pycryptodome_aes() # reads LANGGRAPH_AES_KEY +checkpointer = SqliteSaver(sqlite3.connect("checkpoint.db"), serde=serde) +``` + +```python +from langgraph.checkpoint.serde.encrypted import EncryptedSerializer +from langgraph.checkpoint.postgres import PostgresSaver + +serde = EncryptedSerializer.from_pycryptodome_aes() +checkpointer = PostgresSaver.from_conn_string("postgresql://...", serde=serde) +checkpointer.setup() +``` + +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`]. + +::: + +## Build a custom checkpointer + +:::python + + +Validate your implementation as you build using the [conformance test suite](#testing-with-the-conformance-suite). It covers all five base methods and extended capabilities including delta channels. Run it in CI before shipping. + + +This section covers implementing `BaseCheckpointSaver` from scratch for a custom storage backend. If you already have a working checkpointer and only need to add delta channel support, jump to [Delta channel support](#delta-channel-support). + +### Overview + +LangGraph's persistence layer is built on two storage abstractions: + +- **Checkpoints table** — one row per superstep; stores the serialized graph state (`channel_values`, `channel_versions`, `versions_seen`) and links to its parent checkpoint. +- **Writes table** — one row per node output within a superstep; stores `(task_id, channel, value)` tuples linked to a checkpoint. + +Your checkpointer manages both tables. `put` writes a checkpoint row; `put_writes` writes node-output rows; `get_tuple` reads both back into a `CheckpointTuple`. + +### Base contract + +Subclass `BaseCheckpointSaver` and implement these five methods. All are required — a missing base method raises `NotImplementedError` at runtime. + +```python +from collections.abc import AsyncIterator, Iterator, Sequence +from typing import Any +from langchain_core.runnables import RunnableConfig +from langgraph.checkpoint.base import ( + BaseCheckpointSaver, + ChannelVersions, + Checkpoint, + CheckpointMetadata, + CheckpointTuple, +) + +class MyCheckpointer(BaseCheckpointSaver): + async def aput( + self, + config: RunnableConfig, + checkpoint: Checkpoint, + metadata: CheckpointMetadata, + new_versions: ChannelVersions, + ) -> RunnableConfig: + ... + + async def aput_writes( + self, + config: RunnableConfig, + writes: Sequence[tuple[str, Any]], + task_id: str, + task_path: str = "", + ) -> None: + ... + + async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None: + ... + + async def alist( + self, + config: RunnableConfig | None, + *, + filter: dict[str, Any] | None = None, + before: RunnableConfig | None = None, + limit: int | None = None, + ) -> AsyncIterator[CheckpointTuple]: + ... + yield # make this an async generator + + async def adelete_thread(self, thread_id: str) -> None: + ... +``` + +#### put / aput + +Store one checkpoint row. Return an updated config with the stored `checkpoint_id`. + +Key requirements: + +- Serialize the checkpoint using `self.serde.dumps_typed(checkpoint)` — this handles all LangGraph-native types including `_DeltaSnapshot` blobs used by delta channels. +- Store `metadata` in full — do not strip unknown keys. LangGraph adds new metadata fields (such as `counters_since_delta_snapshot` for delta channels) in minor releases; discarding them silently breaks features. +- Store `config["configurable"].get("checkpoint_id")` as the parent checkpoint ID so `get_tuple` can populate `parent_config`. + +```python +async def aput(self, config, checkpoint, metadata, new_versions): + thread_id = config["configurable"]["thread_id"] + checkpoint_ns = config["configurable"]["checkpoint_ns"] + checkpoint_id = checkpoint["id"] + parent_id = config["configurable"].get("checkpoint_id") + + type_, blob = self.serde.dumps_typed(checkpoint) + serialized_metadata = self.serde.dumps_typed(metadata) + + await self.db.execute( + "INSERT INTO checkpoints (...) VALUES (...)", + thread_id, checkpoint_ns, checkpoint_id, parent_id, + type_, blob, *serialized_metadata, + ) + return { + "configurable": { + "thread_id": thread_id, + "checkpoint_ns": checkpoint_ns, + "checkpoint_id": checkpoint_id, + } + } +``` + +#### put_writes / aput_writes + +Store node-output rows for a single task within the current superstep. These rows are linked to the checkpoint by `(thread_id, checkpoint_ns, checkpoint_id)`. + +```python +async def aput_writes(self, config, writes, task_id, task_path=""): + thread_id = config["configurable"]["thread_id"] + checkpoint_ns = config["configurable"]["checkpoint_ns"] + checkpoint_id = config["configurable"]["checkpoint_id"] + + rows = [] + for idx, (channel, value) in enumerate(writes): + type_, blob = self.serde.dumps_typed(value) + final_idx = WRITES_IDX_MAP.get(channel, idx) + rows.append((thread_id, checkpoint_ns, checkpoint_id, + task_id, task_path, final_idx, channel, type_, blob)) + + await self.db.executemany("INSERT INTO writes (...) VALUES (...)", rows) +``` + +Import `WRITES_IDX_MAP` from `langgraph.checkpoint.base`. It maps special channels (`__error__`, `__interrupt__`, etc.) to reserved negative indices so they do not collide with regular write indices. + +#### get_tuple / aget_tuple + +Retrieve a checkpoint. The config may contain: + +- **No `checkpoint_id`** — return the latest checkpoint for the thread + namespace. +- **A specific `checkpoint_id`** — return that exact checkpoint. + +**Both paths must work correctly.** The specific-id path is used for time travel and — critically — for delta channel state reconstruction on every graph invocation (see [Delta channel support](#delta-channel-support)). A broken specific-id lookup silently corrupts delta channel state. + +```python +async def aget_tuple(self, config): + thread_id = config["configurable"]["thread_id"] + checkpoint_ns = config["configurable"].get("checkpoint_ns", "") + checkpoint_id = config["configurable"].get("checkpoint_id") + + if checkpoint_id: + row = await self.db.fetchone( + "SELECT * FROM checkpoints " + "WHERE thread_id=? AND checkpoint_ns=? AND checkpoint_id=?", + thread_id, checkpoint_ns, checkpoint_id, + ) + else: + row = await self.db.fetchone( + "SELECT * FROM checkpoints " + "WHERE thread_id=? AND checkpoint_ns=? " + "ORDER BY checkpoint_id DESC LIMIT 1", + thread_id, checkpoint_ns, + ) + + if row is None: + return None + + writes = await self.db.fetchall( + "SELECT task_id, channel, type, value FROM writes " + "WHERE thread_id=? AND checkpoint_ns=? AND checkpoint_id=? " + "ORDER BY task_id, idx", + thread_id, checkpoint_ns, row["checkpoint_id"], + ) + pending_writes = [ + (w["task_id"], w["channel"], self.serde.loads_typed((w["type"], w["value"]))) + for w in writes + ] + + checkpoint = self.serde.loads_typed((row["type"], row["blob"])) + metadata = self.serde.loads_typed((row["metadata_type"], row["metadata"])) + + parent_config = None + if row["parent_checkpoint_id"]: + parent_config = { + "configurable": { + "thread_id": thread_id, + "checkpoint_ns": checkpoint_ns, + "checkpoint_id": row["parent_checkpoint_id"], + } + } + + return CheckpointTuple( + config={ + "configurable": { + "thread_id": thread_id, + "checkpoint_ns": checkpoint_ns, + "checkpoint_id": row["checkpoint_id"], + } + }, + checkpoint=checkpoint, + metadata=metadata, + parent_config=parent_config, + pending_writes=pending_writes, + ) +``` + + +**Row key / index design matters for the specific-id lookup.** If your storage uses a time-ordered key (e.g., a reversed timestamp) that does not embed `checkpoint_id`, you cannot do a direct row read by id. You must either encode `checkpoint_id` in the row key, or build a secondary index. A scan with a value filter on every lookup works but does not scale. + + +#### list / alist + +Return checkpoints for a thread, newest first. Respect `before` (return only checkpoints older than that config's `checkpoint_id`) and `limit`. + +#### delete_thread / adelete_thread + +Delete all checkpoints and writes for a thread. Both checkpoint rows and write rows must be deleted. + +### Row key / index design + +How you store and index checkpoints directly affects correctness and performance. + +**Recommended schema (SQL):** + +```sql +CREATE TABLE checkpoints ( + thread_id TEXT NOT NULL, + checkpoint_ns TEXT NOT NULL DEFAULT '', + checkpoint_id TEXT NOT NULL, -- ULID, lexicographically sortable newest-last + parent_checkpoint_id TEXT, + type TEXT, + checkpoint BYTEA, + metadata JSONB, + PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id) +); + +CREATE TABLE writes ( + thread_id TEXT NOT NULL, + checkpoint_ns TEXT NOT NULL DEFAULT '', + checkpoint_id TEXT NOT NULL, + task_id TEXT NOT NULL, + task_path TEXT NOT NULL DEFAULT '', + idx INTEGER NOT NULL, + channel TEXT NOT NULL, + type TEXT, + value BYTEA, + PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, task_path, idx) +); +``` + +Because `checkpoint_id` is a ULID, it sorts lexicographically — larger values are newer. "Get latest" is `ORDER BY checkpoint_id DESC LIMIT 1`; "get by id" is an equality lookup on the primary key. + +**For non-SQL stores:** the same principle applies. Whatever key scheme you use, direct lookup by `(thread_id, checkpoint_ns, checkpoint_id)` must be O(1) or close to it. Avoid designs where the only way to find a checkpoint by id is to scan all rows for a thread. + +### Serialization + +Always use `self.serde` (inherited from `BaseCheckpointSaver`, defaults to `JsonPlusSerializer`) for checkpoints, writes, and metadata. Do not use `pickle` directly for metadata — it works, but `JsonPlusSerializer` produces human-readable output and handles versioning better. + +`JsonPlusSerializer` handles all LangGraph-native types automatically: +- `_DeltaSnapshot` — the sentinel blob used by delta channels (msgpack ext code 7) +- Pydantic v2 models, dataclasses, numpy arrays, datetimes, enums, and more + +If you write a custom serializer, make sure it can round-trip `_DeltaSnapshot` from `langgraph.checkpoint.serde.types`. + +### Extended capabilities + +These methods are optional but unlock additional Agent Server features. Implement them if your storage backend can support them efficiently. + +| Method | What it enables | +|---|---| +| `adelete_for_runs` | Rollback multitask strategy | +| `acopy_thread` | Efficient thread forking | +| `aprune` | Thread history pruning | +| `aget_delta_channel_history` | Efficient delta channel state reconstruction (see below) | + +Agent Server auto-detects which capabilities your checkpointer implements at startup and activates the corresponding features. + +### Delta channel support + + +**DeltaChannel is in beta.** The API and on-disk representation may change while the design stabilizes. + + +`DeltaChannel` is a reducer channel that stores only a sentinel (`MISSING`) in checkpoint blobs instead of the full channel value. State is reconstructed by replaying ancestor writes through the reducer. This makes checkpoint blobs O(1) per step instead of O(N) for channels like `messages` that accumulate over time. + +#### What the runtime needs + +When loading a checkpoint whose delta channels are absent from `channel_values`, LangGraph calls `saver.get_delta_channel_history(config=config, channels=[...])`. This returns, for each channel: + +- **`writes`** — all writes to that channel in the ancestor chain, oldest first, up to the nearest snapshot. +- **`seed`** (optional) — the stored `_DeltaSnapshot` blob at the nearest ancestor that has one; absent if the walk reaches the root without finding a snapshot. + +The runtime then calls `channel.from_checkpoint(seed)` and `channel.replay_writes(writes)` to reconstruct the live value. + +#### Default implementation + +`BaseCheckpointSaver` provides a default `get_delta_channel_history` that works with any correct `get_tuple` implementation: + +```python +# Simplified from BaseCheckpointSaver +def get_delta_channel_history(self, *, config, channels): + target = self.get_tuple(config) # load the head checkpoint + cursor = target.parent_config # walk from its parent + collected = {ch: [] for ch in channels} + seed = {} + remaining = set(channels) + + while cursor and remaining: + tup = self.get_tuple(cursor) # ← requires correct by-id lookup + if tup is None: + break + for write in reversed(tup.pending_writes or []): + if write[1] in remaining: + collected[write[1]].append(write) + for ch in list(remaining): + if ch in tup.checkpoint["channel_values"]: + seed[ch] = tup.checkpoint["channel_values"][ch] + remaining.discard(ch) + cursor = tup.parent_config + + return { + ch: {"writes": list(reversed(collected[ch])), **({"seed": seed[ch]} if ch in seed else {})} + for ch in channels + } +``` + +**The critical dependency:** `get_tuple(cursor)` is always called with a specific `checkpoint_id` (the parent's id). If that lookup returns `None`, the walk stops immediately and every delta channel reconstructs as empty — silently, with no error. This is why the specific-id path in `get_tuple` must be correct. + +#### Performance override + +The default walk issues one `get_tuple` call per ancestor checkpoint. For backends with good query support, override `get_delta_channel_history` (and its async twin) to retrieve the ancestor chain and writes in two queries: + +```python +async def aget_delta_channel_history(self, *, config, channels): + if not channels: + return {} + + thread_id = config["configurable"]["thread_id"] + checkpoint_ns = config["configurable"].get("checkpoint_ns", "") + checkpoint_id = config["configurable"]["checkpoint_id"] + + # Stage 1: stream ancestors newest-first until every channel has a seed + ancestors = await self.db.fetchall( + "SELECT checkpoint_id, parent_checkpoint_id, type, checkpoint " + "FROM checkpoints " + "WHERE thread_id=? AND checkpoint_ns=? AND checkpoint_id < ? " + "ORDER BY checkpoint_id DESC", + thread_id, checkpoint_ns, checkpoint_id, + ) + + chain_by_ch: dict[str, list[str]] = {ch: [] for ch in channels} + seed_by_ch: dict[str, Any] = {} + remaining = set(channels) + cur_id = config["configurable"]["checkpoint_id"] + + for row in ancestors: + if not remaining: + break + parent_id = row["parent_checkpoint_id"] + ckpt = self.serde.loads_typed((row["type"], row["checkpoint"])) + cv = ckpt.get("channel_values") or {} + for ch in list(remaining): + chain_by_ch[ch].append(row["checkpoint_id"]) + if ch in cv: + seed_by_ch[ch] = cv[ch] + remaining.discard(ch) + cur_id = parent_id + + # Stage 2: fetch writes for each channel's ancestor chain in one query + result: dict[str, DeltaChannelHistory] = {} + for ch in channels: + chain = chain_by_ch[ch] + if not chain: + entry: DeltaChannelHistory = {"writes": []} + if ch in seed_by_ch: + entry["seed"] = seed_by_ch[ch] + result[ch] = entry + continue + + write_rows = await self.db.fetchall( + f"SELECT checkpoint_id, task_id, idx, type, value FROM writes " + f"WHERE thread_id=? AND checkpoint_ns=? AND channel=? " + f"AND checkpoint_id IN ({','.join('?' * len(chain))})" + f"ORDER BY checkpoint_id, task_id, idx", + thread_id, checkpoint_ns, ch, *chain, + ) + writes_by_cid: dict[str, list[PendingWrite]] = {} + for row in write_rows: + cid = row["checkpoint_id"] + value = self.serde.loads_typed((row["type"], row["value"])) + writes_by_cid.setdefault(cid, []).append((row["task_id"], ch, value)) + + # chain is newest-first; iterate oldest-first to get correct replay order + collected: list[PendingWrite] = [] + for cid in reversed(chain): + collected.extend(writes_by_cid.get(cid, [])) + + entry = {"writes": collected} + if ch in seed_by_ch: + entry["seed"] = seed_by_ch[ch] + result[ch] = entry + + return result +``` + +#### Pruning with delta channels + +`DeltaChannel` state is not self-contained in a single checkpoint — it depends on the ancestor write chain back to the nearest `_DeltaSnapshot`. If you implement `prune` or `delete_for_runs`, you must not delete write rows that a surviving checkpoint's delta channels depend on. + +Safe options: + +1. **Walk before pruning** — for each checkpoint you intend to keep, walk its ancestor chain and mark all write rows up to the nearest `_DeltaSnapshot` as non-deletable. +2. **Force a snapshot before pruning** — rewrite `channel_values[ch] = _DeltaSnapshot(reconstructed_value)` on the checkpoint you are keeping, then delete ancestors freely. +3. **Skip pruning for delta-channel threads** — the safest short-term option if you do not yet need pruning. + +#### Copy thread with delta channels + +When implementing `copy_thread`, copy the complete ancestor chain — not just the head checkpoint. The target thread must have write rows going back to at least one `_DeltaSnapshot` for every delta channel, or those channels will reconstruct as empty after the copy. + +### Testing with the conformance suite + +`langgraph-checkpoint-conformance` validates your implementation against the full contract, including delta channel history: + +```python +pip install langgraph-checkpoint-conformance +``` + +```python +import asyncio +from langgraph.checkpoint.conformance import checkpointer_test, validate + +@checkpointer_test(name="MyCheckpointer") +async def my_checkpointer(): + async with MyCheckpointer.create() as saver: + yield saver + +async def main(): + report = await validate(my_checkpointer) + report.print_report() + # Fails the process if any base capability is missing or broken + if not report.passed_all_base(): + raise RuntimeError("Checkpointer failed conformance suite") + +asyncio.run(main()) +``` + +The suite auto-detects which extended capabilities your checkpointer implements (including `aget_delta_channel_history`) and runs the relevant tests for each. Run it as part of your CI before shipping. + +::: diff --git a/deploy.mdx b/deploy.mdx index a6c1846..7675003 100644 --- a/deploy.mdx +++ b/deploy.mdx @@ -1,34 +1,46 @@ --- -title: LangSmith Deployment +title: Deployment +description: Deploy LangGraph agents to production with LangSmith Cloud or JavaScript frameworks and hosting platforms. +sidebarTitle: Deployment --- -This guide shows you how to deploy your agent to **[LangSmith Cloud](/langsmith/deploy-to-cloud)**, a fully managed hosting platform designed for agent workloads. With Cloud deployment, you can deploy directly from your GitHub repository—LangSmith handles the infrastructure, scaling, and operational concerns. +import DeployFrameworksPlatformsReference from '/snippets/langsmith/deploy-frameworks-platforms-reference.mdx'; -Traditional hosting platforms are built for stateless, short-lived web applications. LangSmith Cloud is **purpose-built for stateful, long-running agents** that require persistent state and background execution. +When you are ready to deploy your LangGraph agent to production, choose a hosting model that fits your stack. **[LangSmith Cloud](/langsmith/deploy-to-cloud)** provides fully managed infrastructure for stateful, long-running agents with persistent state and background execution. + +:::js +You can also deploy on **JavaScript frameworks and platforms** such as Next.js, SvelteKit, Nuxt, Cloudflare Workers, and Deno Deploy using the same [Agent Streaming Protocol](https://github.com/langchain-ai/agent-protocol/tree/main/streaming). + + +::: -LangSmith offers multiple deployment options beyond Cloud, including deploying with a [control plane (hybrid/self-hosted)](/langsmith/deploy-with-control-plane) or as [standalone servers](/langsmith/deploy-standalone-server). For more information, refer to the [Deployment overview](/langsmith/deployment). +LangSmith offers multiple deployment options beyond Cloud, including [hybrid](/langsmith/hybrid), [standalone servers](/langsmith/deploy-standalone-server), and [self-hosted with control plane](/langsmith/deploy-with-control-plane). For more information, see the [LangSmith Deployment overview](/langsmith/deployment). -## Prerequisites +## LangSmith Cloud + +This section walks through deploying your agent to LangSmith Cloud from a GitHub repository. LangSmith handles infrastructure, scaling, and operational concerns. + +### Prerequisites Before you begin, ensure you have the following: - A [GitHub account](https://github.com/) -- A [LangSmith account](https://smith.langchain.com/) (free to sign up) +- A [LangSmith account](https://smith.langchain.com) (free to sign up) -## Deploy your agent +### Deploy your agent -### 1. Create a repository on GitHub +#### 1. Create a repository on GitHub Your application's code must reside in a GitHub repository to be deployed on LangSmith. Both public and private repositories are supported. For this quickstart, first make sure your app is LangGraph-compatible by following the [local server setup guide](/oss/langgraph/studio#set-up-local-agent-server). Then, push your code to the repository. -### 2. Deploy to LangSmith +#### 2. Deploy to LangSmith - Log in to [LangSmith](https://smith.langchain.com/). In the left sidebar, select **Deployments**. + Log in to [LangSmith](https://smith.langchain.com). In the left sidebar, select **Deployments**. Click the **+ New Deployment** button. A pane will open where you can fill in the required fields. @@ -41,19 +53,19 @@ Your application's code must reside in a GitHub repository to be deployed on Lan -### 3. Test your application in Studio +#### 3. Test your application in Studio Once your application is deployed: 1. Select the deployment you just created to view more details. 2. Click the **Studio** button in the top right corner. Studio will open to display your graph. -### 4. Get the API URL for your deployment +#### 4. Get the API URL for your deployment 1. In the **Deployment details** view in LangGraph, click the **API URL** to copy it to your clipboard. 2. Click the `URL` to copy it to the clipboard. -### 5. Test the API +#### 5. Test the API You can now test the API: @@ -125,9 +137,9 @@ for await (const chunk of streamResponse) { curl -s --request POST \ --url /runs/stream \ --header 'Content-Type: application/json' \ - --header "X-Api-Key: " \ + --header "X-Api-Key: \ --data "{ - \"assistant_id\": \"agent\", + \"assistant_id\": \"agent\", `# Name of agent. Defined in langgraph.json.` \"input\": { \"messages\": [ { @@ -141,4 +153,3 @@ curl -s --request POST \ ``` - diff --git a/durable-execution.mdx b/durable-execution.mdx deleted file mode 100644 index 388a477..0000000 --- a/durable-execution.mdx +++ /dev/null @@ -1,284 +0,0 @@ ---- -title: Durable execution ---- - - - -**Durable execution** is a technique in which a process or workflow saves its progress at key points, allowing it to pause and later resume exactly where it left off. This is particularly useful in scenarios that require [human-in-the-loop](/oss/langgraph/interrupts), where users can inspect, validate, or modify the process before continuing, and in long-running tasks that might encounter interruptions or errors (e.g., calls to an LLM timing out). By preserving completed work, durable execution enables a process to resume without reprocessing previous steps -- even after a significant delay (e.g., a week later). - -LangGraph's built-in [persistence](/oss/langgraph/persistence) layer provides durable execution for workflows, ensuring that the state of each execution step is saved to a durable store. This capability guarantees that if a workflow is interrupted -- whether by a system failure or for [human-in-the-loop](/oss/langgraph/interrupts) interactions -- it can be resumed from its last recorded state. - - -If you are using LangGraph with a checkpointer, you already have durable execution enabled. You can pause and resume workflows at any point, even after interruptions or failures. -To make the most of durable execution, ensure that your workflow is designed to be [deterministic](#determinism-and-consistent-replay) and [idempotent](#determinism-and-consistent-replay) and wrap any side effects or non-deterministic operations inside [tasks](/oss/langgraph/functional-api#task). You can use [tasks](/oss/langgraph/functional-api#task) from both the [StateGraph (Graph API)](/oss/langgraph/graph-api) and the [Functional API](/oss/langgraph/functional-api). - - -## Requirements - -To leverage durable execution in LangGraph, you need to: - -1. Enable [persistence](/oss/langgraph/persistence) in your workflow by specifying a [checkpointer](/oss/langgraph/persistence#checkpointer-libraries) that will save workflow progress. -2. Specify a [thread identifier](/oss/langgraph/persistence#threads) when executing a workflow. This will track the execution history for a particular instance of the workflow. - -3. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside @[tasks][task] to ensure that when a workflow is resumed, these operations are not repeated for the particular run, and instead their results are retrieved from the persistence layer. For more information, see [Determinism and Consistent Replay](#determinism-and-consistent-replay). - -## Determinism and consistent replay - -When you resume a workflow run, the code does **NOT** resume from the **same line of code** where execution stopped; instead, it will identify an appropriate [starting point](#starting-points-for-resuming-workflows) from which to pick up where it left off. This means that the workflow will replay all steps from the [starting point](#starting-points-for-resuming-workflows) until it reaches the point where it was stopped. - -As a result, when you are writing a workflow for durable execution, you must wrap any non-deterministic operations (e.g., random number generation) and any operations with side effects (e.g., file writes, API calls) inside [tasks](/oss/langgraph/functional-api#task) or [nodes](/oss/langgraph/graph-api#nodes). - -To ensure that your workflow is deterministic and can be consistently replayed, follow these guidelines: - -* **Avoid Repeating Work**: If a [node](/oss/langgraph/graph-api#nodes) contains multiple operations with side effects (e.g., logging, file writes, or network calls), wrap each operation in a separate **task**. This ensures that when the workflow is resumed, the operations are not repeated, and their results are retrieved from the persistence layer. -* **Encapsulate Non-Deterministic Operations:** Wrap any code that might yield non-deterministic results (e.g., random number generation) inside **tasks** or **nodes**. This ensures that, upon resumption, the workflow follows the exact recorded sequence of steps with the same outcomes. -* **Use Idempotent Operations**: When possible ensure that side effects (e.g., API calls, file writes) are idempotent. This means that if an operation is retried after a failure in the workflow, it will have the same effect as the first time it was executed. This is particularly important for operations that result in data writes. In the event that a **task** starts but fails to complete successfully, the workflow's resumption will re-run the **task**, relying on recorded outcomes to maintain consistency. Use idempotency keys or verify existing results to avoid unintended duplication, ensuring a smooth and predictable workflow execution. - -For some examples of pitfalls to avoid, see the [Common Pitfalls](/oss/langgraph/functional-api#common-pitfalls) section in the functional API, which shows -how to structure your code using **tasks** to avoid these issues. The same principles apply to the @[StateGraph (Graph API)][StateGraph]. - -## Durability modes - -LangGraph supports three durability modes that allow you to balance performance and data consistency based on your application's requirements. A higher durability mode adds more overhead to the workflow execution. You can specify the durability mode when calling any graph execution method: - -:::python -```python -graph.stream( - {"input": "test"}, - durability="sync" -) -``` -::: - -:::js -```typescript -await graph.stream( - { input: "test" }, - { durability: "sync" } -) -``` -::: - -The durability modes, from least to most durable, are as follows: - -* `"exit"`: LangGraph persists changes only when graph execution exits either successfully, with an error, or due to a human in the loop interrupt. This provides the best performance for long-running graphs but means intermediate state is not saved, so you cannot recover from system failures (like process crashes) that occur mid-execution. -* `"async"`: LangGraph persists changes asynchronously while the next step executes. This provides good performance and durability, but there's a small risk that LangGraph does not write checkpoints if the process crashes during execution. -* `"sync"`: LangGraph persists changes synchronously before the next step starts. This ensures that LangGraph writes every checkpoint before continuing execution, providing high durability at the cost of some performance overhead. - -## Using tasks in nodes - -If a [node](/oss/langgraph/graph-api#nodes) contains multiple operations, you may find it easier to convert each operation into a **task** rather than refactor the operations into individual nodes. - -:::python - - - ```python - from typing import NotRequired - from typing_extensions import TypedDict - from langchain_core.utils.uuid import uuid7 - - from langgraph.checkpoint.memory import InMemorySaver - from langgraph.graph import StateGraph, START, END - import requests - - # Define a TypedDict to represent the state - class State(TypedDict): - url: str - result: NotRequired[str] - - def call_api(state: State): - """Example node that makes an API request.""" - result = requests.get(state['url']).text[:100] # Side-effect # [!code highlight] - return { - "result": result - } - - # Create a StateGraph builder and add a node for the call_api function - builder = StateGraph(State) - builder.add_node("call_api", call_api) - - # Connect the start and end nodes to the call_api node - builder.add_edge(START, "call_api") - builder.add_edge("call_api", END) - - # Specify a checkpointer - checkpointer = InMemorySaver() - - # Compile the graph with the checkpointer - graph = builder.compile(checkpointer=checkpointer) - - # Define a config with a thread ID. - thread_id = str(uuid7()) - config = {"configurable": {"thread_id": thread_id}} - - # Invoke the graph - graph.invoke({"url": "https://www.example.com"}, config) -``` - - - ```python - from typing import NotRequired - from typing_extensions import TypedDict - from langchain_core.utils.uuid import uuid7 - - from langgraph.checkpoint.memory import InMemorySaver - from langgraph.func import task - from langgraph.graph import StateGraph, START, END - import requests - - # Define a TypedDict to represent the state - class State(TypedDict): - urls: list[str] - result: NotRequired[list[str]] - - - @task - def _make_request(url: str): - """Make a request.""" - return requests.get(url).text[:100] # [!code highlight] - - def call_api(state: State): - """Example node that makes an API request.""" - requests = [_make_request(url) for url in state['urls']] # [!code highlight] - results = [request.result() for request in requests] - return { - "results": results - } - - # Create a StateGraph builder and add a node for the call_api function - builder = StateGraph(State) - builder.add_node("call_api", call_api) - - # Connect the start and end nodes to the call_api node - builder.add_edge(START, "call_api") - builder.add_edge("call_api", END) - - # Specify a checkpointer - checkpointer = InMemorySaver() - - # Compile the graph with the checkpointer - graph = builder.compile(checkpointer=checkpointer) - - # Define a config with a thread ID. - thread_id = str(uuid7()) - config = {"configurable": {"thread_id": thread_id}} - - # Invoke the graph - graph.invoke({"urls": ["https://www.example.com"]}, config) -``` - - -::: - -:::js - - - ```typescript - import { StateGraph, StateSchema, GraphNode, START, END, MemorySaver } from "@langchain/langgraph"; - import { v7 as uuid7 } from "uuid"; - import * as z from "zod"; - - // Define a StateSchema to represent the state - const State = new StateSchema({ - url: z.string(), - result: z.string().optional(), - }); - - const callApi: GraphNode = async (state) => { - const response = await fetch(state.url); // [!code highlight] - const text = await response.text(); - const result = text.slice(0, 100); // Side-effect - return { - result, - }; - }; - - // Create a StateGraph builder and add a node for the callApi function - const builder = new StateGraph(State) - .addNode("callApi", callApi) - .addEdge(START, "callApi") - .addEdge("callApi", END); - - // Specify a checkpointer - const checkpointer = new MemorySaver(); - - // Compile the graph with the checkpointer - const graph = builder.compile({ checkpointer }); - - // Define a config with a thread ID. - const threadId = uuid7(); - const config = { configurable: { thread_id: threadId } }; - - // Invoke the graph - await graph.invoke({ url: "https://www.example.com" }, config); -``` - - - ```typescript - import { StateGraph, StateSchema, GraphNode, START, END, MemorySaver, task } from "@langchain/langgraph"; - import { v7 as uuid7 } from "uuid"; - import * as z from "zod"; - - // Define a StateSchema to represent the state - const State = new StateSchema({ - urls: z.array(z.string()), - results: z.array(z.string()).optional(), - }); - - const makeRequest = task("makeRequest", async (url: string) => { - const response = await fetch(url); // [!code highlight] - const text = await response.text(); - return text.slice(0, 100); - }); - - const callApi: GraphNode = async (state) => { - const requests = state.urls.map((url) => makeRequest(url)); // [!code highlight] - const results = await Promise.all(requests); - return { - results, - }; - }; - - // Create a StateGraph builder and add a node for the callApi function - const builder = new StateGraph(State) - .addNode("callApi", callApi) - .addEdge(START, "callApi") - .addEdge("callApi", END); - - // Specify a checkpointer - const checkpointer = new MemorySaver(); - - // Compile the graph with the checkpointer - const graph = builder.compile({ checkpointer }); - - // Define a config with a thread ID. - const threadId = uuid7(); - const config = { configurable: { thread_id: threadId } }; - - // Invoke the graph - await graph.invoke({ urls: ["https://www.example.com"] }, config); -``` - - -::: - -## Resuming workflows - -Once you have enabled durable execution in your workflow, you can resume execution for the following scenarios: - -:::python -* **Pausing and Resuming Workflows:** Use the @[interrupt][interrupt] function to pause a workflow at specific points and the @[`Command`] primitive to resume it with updated state. See [**Interrupts**](/oss/langgraph/interrupts) for more details. -* **Recovering from Failures:** Automatically resume workflows from the last successful checkpoint after an exception (e.g., LLM provider outage). This involves executing the workflow with the same thread identifier by providing it with a `None` as the input value (see this [example](/oss/langgraph/use-functional-api#resuming-after-an-error) with the functional API). -::: - -:::js -* **Pausing and Resuming Workflows:** Use the @[interrupt][interrupt] function to pause a workflow at specific points and the @[`Command`] primitive to resume it with updated state. See [**Interrupts**](/oss/langgraph/interrupts) for more details. -* **Recovering from Failures:** Automatically resume workflows from the last successful checkpoint after an exception (e.g., LLM provider outage). This involves executing the workflow with the same thread identifier by providing it with a `null` as the input value (see this [example](/oss/langgraph/use-functional-api#resuming-after-an-error) with the functional API). -::: - -## Starting points for resuming workflows - -* If you're using a [StateGraph (Graph API)](/oss/langgraph/graph-api), the starting point is the beginning of the [**node**](/oss/langgraph/graph-api#nodes) where execution stopped. -* If you're making a subgraph call inside a node, the starting point will be the **parent** node that called the subgraph that was halted. - Inside the subgraph, the starting point will be the specific [**node**](/oss/langgraph/graph-api#nodes) where execution stopped. -* If you're using the Functional API, the starting point is the beginning of the [**entrypoint**](/oss/langgraph/functional-api#entrypoint) where execution stopped. diff --git a/errors/GRAPH_RECURSION_LIMIT.mdx b/errors/GRAPH_RECURSION_LIMIT.mdx index d91c73f..fbd64be 100644 --- a/errors/GRAPH_RECURSION_LIMIT.mdx +++ b/errors/GRAPH_RECURSION_LIMIT.mdx @@ -51,7 +51,7 @@ However, complex graphs may hit the default limit naturally. * If you have a complex graph, you can pass in a higher `recursion_limit` value into your `config` object when invoking your graph like this: ```python -graph.invoke({...}, {"recursion_limit": 100}) +graph.invoke({...}, {"recursion_limit": 1000}) ``` ::: @@ -59,6 +59,6 @@ graph.invoke({...}, {"recursion_limit": 100}) * If you have a complex graph, you can pass in a higher `recursionLimit` value into your `config` object when invoking your graph like this: ```typescript -await graph.invoke({...}, { recursionLimit: 100 }); +await graph.invoke({...}, { recursionLimit: 1000 }); ``` ::: diff --git a/event-streaming.mdx b/event-streaming.mdx new file mode 100644 index 0000000..84164ea --- /dev/null +++ b/event-streaming.mdx @@ -0,0 +1,827 @@ +--- +title: Event streaming +description: Stream LangGraph runs with typed projections for messages, state, subgraphs, output, and extensions. +--- + +Event streaming is the recommended in-process streaming model for most LangGraph application code. It returns a run stream object that can be consumed in multiple ways at the same time. + +## Quickstart + +:::python +```py +stream = graph.stream_events({ + "messages": [{"role": "user", "content": "What is 42 * 17?"}], +}, version="v3") + +for message in stream.messages: + for token in message.text: + print(token, end="", flush=True) + +final_state = stream.output +``` +::: + +:::js +```ts +const stream = await graph.streamEvents( + { messages: [{ role: "user", content: "What is 42 * 17?" }] }, + { version: "v3" } +); + +for await (const message of stream.messages) { + for await (const token of message.text) { + process.stdout.write(token); + } +} + +const finalState = await stream.output; +``` +::: + +To stream against a graph deployed behind an Agent Server, see the [LangSmith Streaming API](/langsmith/streaming). + +## How the pieces fit together + +The streaming stack has two main layers: + +1. **Streaming** emits raw graph execution events from the Pregel engine. +2. **Event streaming** normalizes those events, runs them through stream transformers, and exposes typed projections. + +
+
+
+
Pregel engine
+
Runs graph steps
+
+
emits
+
+
Raw Pregel events
+
updates, values, messages, custom, checkpoints, tasks, debug
+
+
sent to
+
+
Event router
+
Routes each event through the transformer pipeline
+
+
cascades through
+
+
Stream transformers
+
+
ValuesTransformer
+
MessagesTransformer
+
...
+
Custom transformers
+
+
+
produces
+
+
Event Stream
+
Projected events for application code
+
+
+
+ +The event router is the bridge between the two layers. It receives normalized Pregel events and passes each event through the registered stream transformers. Built-in transformers create standard projections such as `stream.messages`, `stream.values`, `stream.subgraphs`, and `stream.output`. Custom transformers can add application-specific projections under `stream.extensions`. + +## What event streaming provides + +The run stream exposes typed projections over one underlying event flow: + +| Projection | Use | +| ---------- | --- | +| `stream` | Iterate every protocol event. | +| `stream.messages` | Stream chat model messages and token deltas. | +| `stream.values` | Iterate state snapshots and await the final value. | +| `stream.output` | Await the final output. | +| `stream.subgraphs` | Discover and observe nested graph executions. | +| `stream.interrupts` | Inspect human-in-the-loop interrupt payloads. | +| `stream.interrupted` | Check whether the run paused for human input. | +| `stream.extensions` | Consume custom stream transformer projections. | + +Multiple consumers can read these projections concurrently. Reading `stream.messages` does not consume events needed by `stream.values`, `stream.subgraphs`, or `stream.output`. + +Event streaming sits one level above [streaming](/oss/langgraph/streaming), which exposes raw graph execution events through `stream_mode` modes such as `updates`, `values`, `messages`, `custom`, `checkpoints`, `tasks`, and `debug`. Use streaming when you need low-level access to those modes; use event streaming when application code benefits from typed projections. + +## Stream messages + +Use `stream.messages` for chat model output: + +:::python +```py +stream = graph.stream_events(input, version="v3") + +for message in stream.messages: + text = str(message.text) + usage = message.output.usage_metadata + + print(text) + print(usage) +``` +::: + +:::js +```ts +const stream = await graph.streamEvents(input, { version: "v3" }); + +for await (const message of stream.messages) { + const text = await message.text; + const usage = await message.usage; + + console.log(text); + console.log(usage); +} +``` +::: + +:::python +`message.text` is iterable in synchronous code. Iterate it for token-by-token output, or call `str(message.text)` for the complete text. + +`message.reasoning` exposes reasoning deltas, and `message.tool_calls` exposes tool-call argument chunks. If you need text, reasoning, and tool-call chunks in exact arrival order, iterate the message stream's raw events instead of each projection separately. +::: + +:::js +`message.text` is both an async iterable and a promise-like value. Iterate it for token-by-token output, or await it for the complete text. +::: + +## Stream subgraphs + +Use `stream.subgraphs` to observe nested graph work without parsing namespace strings: + +:::python +```py +stream = graph.stream_events(input, version="v3") + +for subgraph in stream.subgraphs: + print(subgraph.graph_name, subgraph.path) + + for message in subgraph.messages: + print(message.text) +``` +::: + +:::js +```ts +const stream = await graph.streamEvents(input, { version: "v3" }); + +for await (const subgraph of stream.subgraphs) { + console.log(subgraph.name, subgraph.path); + + for await (const message of subgraph.messages) { + console.log(await message.text); + } +} +``` +::: + +`subgraph.graph_name` is the `name` of the compiled graph or agent. A named agent dispatched from a tool (for example, a `create_agent(name=...)` invoked through the Deep Agents `task` tool) surfaces here under that name, and the `lifecycle` event that opens the scope carries a `cause` linking back to the dispatching tool call. See [Lifecycle](#lifecycle) for more information. + +For product-specific streams, see [Deep Agents streaming](/oss/deepagents/event-streaming) for subagent streams and [LangChain agent streaming](/oss/langchain/streaming) for tool calls and middleware events. + +## Stream state + +Use `stream.values` to stream full state snapshots after each step: + +:::python +```py +stream = graph.stream_events(input, version="v3") + +for snapshot in stream.values: + print(snapshot) + +final_state = stream.output +``` +::: + +:::js +```ts +const stream = await graph.streamEvents(input, { version: "v3" }); + +for await (const snapshot of stream.values) { + console.log(snapshot); +} + +const finalState = await stream.output; +``` +::: + +## Stream multiple projections + +:::python +For concurrent consumption in async code, use `astream_events` with `asyncio.gather`: + +```py +import asyncio + +stream = await graph.astream_events(input, version="v3") + +async def consume_messages(): + async for message in stream.messages: + print(f"[llm] node={message.node}") + +async def consume_subgraphs(): + async for subgraph in stream.subgraphs: + print(f"[subgraph] path={subgraph.path}") + +await asyncio.gather(consume_messages(), consume_subgraphs()) +``` + +For synchronous code, use `stream.interleave(...)` to consume multiple projections in strict arrival order: + +```py +stream = graph.stream_events(input, version="v3") + +for name, item in stream.interleave("values", "messages", "subgraphs"): + if name == "values": + print(f"[state] keys={list(item)}") + elif name == "messages": + print(f"[llm] node={item.node}") + elif name == "subgraphs": + print(f"[subgraph] path={item.path}") +``` +::: + +:::js +Use concurrent consumers when you need multiple projections in JavaScript: + +```ts +await Promise.all([ + (async () => { + for await (const message of stream.messages) { + console.log(await message.text); + } + })(), + (async () => { + for await (const subgraph of stream.subgraphs) { + console.log(subgraph.path); + } + })(), +]); +``` +::: + +## Resume after an interrupt + +When a graph pauses for human input, inspect `stream.interrupted` and `stream.interrupts`, then resume by calling `stream_events(..., version="v3")` again with `Command`. + +Resume requires a graph compiled with a checkpointer and a config carrying a thread ID — see [persistence](/oss/langgraph/persistence). + +:::python +```py +from langgraph.types import Command + +stream = graph.stream_events(input, version="v3") + +for message in stream.messages: + print(message.text) + +if stream.interrupted: + print(stream.interrupts) + +stream = graph.stream_events( + Command(resume={"decisions": [{"type": "approve"}]}), + version="v3", +) +final_state = stream.output +``` +::: + +:::js +```ts +import { Command } from "@langchain/langgraph"; + +let stream = await graph.streamEvents(input, { version: "v3" }); + +for await (const message of stream.messages) { + console.log(await message.text); +} + +if (stream.interrupted) { + console.log(stream.interrupts); +} + +stream = await graph.streamEvents( + new Command({ resume: { decisions: [{ type: "approve" }] } }), + { version: "v3" } +); +const finalState = await stream.output; +``` +::: + +## Stream all protocol events + +Use the run object itself when you want the raw protocol event stream: + +:::python +```py +stream = graph.stream_events({ + "messages": [{"role": "user", "content": "What is 42 * 17?"}], +}, version="v3") + +for event in stream: + namespace = event["params"]["namespace"] + print(namespace, event["method"], event["params"]["data"]) +``` +::: + +:::js +```ts +const stream = await graph.streamEvents( + { messages: [{ role: "user", content: "What is 42 * 17?" }] }, + { version: "v3" } +); + +for await (const event of stream) { + const namespace = event.params.namespace; + console.log(namespace, event.method, event.params.data); +} +``` +::: + +Each event is a `ProtocolEvent` envelope wrapping a channel-specific payload. The same shape is what a transformer's `process(event)` receives. + +:::python +```py +class ProtocolEvent(TypedDict): + seq: int # strictly increasing within a run; use for ordering + method: str # channel name: "messages", "values", "updates", "custom", "tools", "lifecycle", ... + params: ProtocolEventParams + + +class ProtocolEventParams(TypedDict): + namespace: list[str] # path of ":" segments from the root graph; [] is the root + timestamp: int # wall-clock milliseconds; can drift, don't rely on for ordering + data: Any # channel-specific payload; shape depends on `method` +``` +::: + +:::js +```ts +interface ProtocolEvent { + readonly seq: number; // strictly increasing within a run; use for ordering + readonly method: string; // channel name: "messages", "values", "updates", "custom", "tools", "lifecycle", ... + readonly params: { + readonly namespace: string[]; // path of ":" segments from the root graph; [] is the root + readonly timestamp: number; // wall-clock milliseconds; can drift, don't rely on for ordering + readonly node?: string; // graph node that emitted this event, when applicable + readonly data: unknown; // channel-specific payload; shape depends on `method` + }; +} +``` +::: + +The `namespace` is a path from the root graph to the scope that emitted the event. The root is the empty array `[]`. Each child execution adds one `"name:runtime_id"` segment, so a nested tool call inside a subgraph looks like `["researcher:6f4d", "tools:91ac"]`. The name before `:` is the stable graph or node name; the suffix is a per-invocation runtime ID. Filter raw events by namespace yourself when you only care about a specific subtree — `stream.subgraphs` already does this for nested graph executions. + +## Channels and event lifecycle + +Raw events flow on channels. The channel name appears as the event's `method`; each channel emits a specific event shape. + +| Channel | Purpose | +| ------- | ------- | +| `values` | Full graph state snapshots. | +| `updates` | Per-node state deltas. | +| `messages` | Content-block-centric chat model output. | +| `tools` | Tool call start, streamed output, finish, and error events. | +| `lifecycle` | Run, subgraph, and subagent status changes. | +| `checkpoints` | Lightweight checkpoint envelopes for branching and time travel. | +| `input` | Human-in-the-loop input requests and responses. | +| `tasks` | Pregel task creation and result events. | +| `custom` | User-defined payloads from graph code. | +| `custom:` | Application-defined stream transformer output. | + +The typed projections (`stream.messages`, `stream.values`, etc.) are built from these channels. The channel name appears as the `method` field on raw events when you iterate the run object directly. + +### Messages + +The `messages` channel models output as content blocks. The data's `event` field is one of: + +- `message-start` +- `content-block-start` +- `content-block-delta` +- `content-block-finish` +- `message-finish` + +Content blocks have explicit boundaries: a block starts, emits zero or more deltas, and finishes before the next block in the same message starts. This makes token streaming, reasoning blocks, tool-call blocks, and multimodal content explicit without requiring provider-specific formats. `message-finish` may include token usage; unrecoverable model-call failures arrive as message error events. + +To consume raw content-block events directly instead of using the `stream.messages` projection: + +:::python +```py +for event in stream: + if event["method"] != "messages": + continue + + data = event["params"]["data"][0] + if not isinstance(data, dict): + continue + if data.get("event") != "content-block-delta": + continue + + block = data.get("delta") or {} + if block.get("type") == "text-delta": + print(block.get("text", ""), end="", flush=True) + elif block.get("type") == "reasoning-delta": + print(f"[thinking]{block.get('reasoning', '')}", end="", flush=True) +``` +::: + +:::js +```ts +for await (const event of stream) { + if (event.method !== "messages") continue; + + const data = event.params.data; + if (data.event !== "content-block-delta") continue; + + const block = data.delta ?? {}; + if (block.type === "text-delta") { + process.stdout.write(block.text ?? ""); + } else if (block.type === "reasoning-delta") { + process.stdout.write(`[thinking]${block.reasoning ?? ""}`); + } +} +``` +::: + +### Tools + +The `tools` channel exposes tool execution. The data's `event` field is one of: + +- `tool-started` +- `tool-output-delta` +- `tool-finished` +- `tool-error` + +Tool events are correlated by tool call ID, so a tool execution can be joined back to its originating tool-call content block on the `messages` channel. + +### Lifecycle + +The `lifecycle` channel tracks root run, subgraph, and subagent status. The data's `event` field is one of: + +- `started` +- `running` +- `completed` +- `failed` +- `interrupted` + +Beyond `event`, lifecycle data may include an optional `graph_name`, `error`, and `cause` describing why a child scope started (parent tool call, fan-out send, edge transition). + +## Build your own projection + +Stream transformers are the projection layer in event streaming. They observe protocol events, keep their own state, and expose derived views of a run — things like tool activity, token totals, progress events, artifacts, or messages for another protocol. `StreamChannel` is the projection primitive transformers use to publish those views. + +Built-in projections (`stream.messages`, `stream.values`, `stream.subgraphs`, `stream.output`) and product-specific projections (LangChain's `stream.tool_calls`, Deep Agents' `stream.subagents`) are themselves transformers using this same contract. User transformers stack on top via compile-time or call-time registration, and their projections appear under `stream.extensions`. + +Write one when the existing projections don't match the shape an application needs. + +### How transformers work + +Event streaming starts with streaming output from the LangGraph Pregel engine. The runtime normalizes those chunks into protocol events, then a stream handler routes each event through a stack of stream transformers. + +```mermaid +flowchart TD + A[Pregel modes] --> B[Events] + B --> C[Built-in projections] + C --> D[User transformers] + D --> E[Run projections] +``` + +The stream handler is the central dispatcher for one stream. For every protocol event, it: + +1. Calls each registered transformer's `process(event)` hook in order. +2. Wires named `StreamChannel` pushes back onto the protocol event stream. +3. Stores the event in the run stream unless a transformer suppresses it. +4. Calls `finalize()` or `fail()` on every transformer when the run ends. + +Transformers are observational. They do not call back into the graph runtime. Instead, they consume events and push derived values into `StreamChannel`, promises, or other projection objects. + +### Transformer shape + +A transformer implements the `StreamTransformer` interface: + +:::python +```py +from langgraph.stream import ProtocolEvent, StreamTransformer + + +class MyTransformer(StreamTransformer): + def init(self) -> dict: + ... + + def process(self, event: ProtocolEvent) -> bool: + ... + + def finalize(self) -> None: + ... + + def fail(self, err: BaseException) -> None: + ... +``` +::: + +:::js +```ts +interface StreamTransformer { + init(): TProjection; + process(event: ProtocolEvent): boolean; + finalize?(): void | PromiseLike; + fail?(err: unknown): void; +} +``` +::: + +- `init()` creates the projection object. User transformer projections appear under `stream.extensions`. +- `process()` observes each protocol event. See [Stream all protocol events](#stream-all-protocol-events) for the `ProtocolEvent` shape. Return `false` only when you intentionally want to suppress the original event. +- `finalize()` closes or resolves non-channel projections after a successful stream. +- `fail()` propagates errors to non-channel projections. + +### Declaring required stream modes + +`required_stream_modes` controls which Pregel stream modes the underlying graph emits during the stream. The runtime takes the union of every registered transformer's `required_stream_modes` and passes that union as the `stream_mode` argument to the graph's `.stream()` call. **Modes that no transformer requests are never emitted** — declaring `("custom",)` is what causes `custom` events to flow through the run at all. + +:::python +```py +class CustomTransformer(StreamTransformer): + required_stream_modes = ("custom",) # [!code highlight] + + def process(self, event: ProtocolEvent) -> bool: + if event["method"] == "custom": + ... + return True +``` +::: + +`process()` receives every event the graph emits and is responsible for filtering by `event["method"]`. The declaration turns on upstream emission; it does not narrow what `process()` sees. Valid values are the Pregel stream modes: `"messages"`, `"tools"`, `"custom"`, `"values"`, `"updates"`, `"checkpoints"`, `"tasks"`, `"debug"`. Each transformer must declare every mode it acts on — an omitted mode is not emitted by the graph and never reaches `process()`. + +### StreamChannel + +`StreamChannel` is the projection primitive a transformer uses for streaming values. It always exposes an iterable stream on `stream.extensions.`. The constructor argument decides whether each `push()` also flows into the run's main event stream as a `custom:` event—that is, whether the projection's values show up when iterating raw protocol events. + +:::js +| Need | Use | +| ---- | --- | +| Side-channel projection only | `new StreamChannel()` | +| Also flow each push into the main event stream | `new StreamChannel(name)` | +::: + +:::python +| Need | Use | +| ---- | --- | +| Side-channel projection only | `StreamChannel()` | +| Also flow each push into the main event stream | `StreamChannel(name)` | +::: + +Named channel payloads must be serializable, because each pushed value also becomes a `custom:` protocol event in the main stream. Keep promises, async iterables, class instances, and other in-process handles in unnamed channels. + +The stream handler owns channel lifecycle. Once `init()` returns a channel, the handler closes or fails it for you when the run ends. Transformers only push values. + +### Example: named channel + +Pass a string name to `StreamChannel` to expose a streaming projection through `stream.extensions` *and* forward each pushed value into the run's main event stream as a `custom:` protocol event: + +:::python +```py +from typing import TypedDict + +from langgraph.stream import ProtocolEvent, StreamChannel, StreamTransformer + + +class ToolActivity(TypedDict): + name: str + status: str + + +class ToolActivityTransformer(StreamTransformer): + required_stream_modes = ("tools",) + + def __init__(self, scope: tuple[str, ...] = ()) -> None: + super().__init__(scope) + self.activity = StreamChannel[ToolActivity]("tool_activity") + + def init(self) -> dict: + return {"tool_activity": self.activity} + + def process(self, event: ProtocolEvent) -> bool: + if event["method"] != "tools": + return True + + data = event["params"]["data"] + if isinstance(data, dict) and data.get("tool_name") and data.get("event"): + status = "error" if data["event"] == "tool-error" else "started" + self.activity.push({"name": data["tool_name"], "status": status}) + return True +``` +::: + +:::js +```ts +import { StreamChannel } from "@langchain/langgraph"; + +const toolActivityTransformer = () => { + const activity = new StreamChannel<{ + name: string; + status: "started" | "finished" | "error"; + }>("toolActivity"); + + return { + init: () => ({ toolActivity: activity }), + process(event) { + if (event.method === "tools") { + const data = event.params.data as { tool_name?: string; event?: string }; + if (data.tool_name && data.event) { + activity.push({ + name: data.tool_name, + status: data.event === "tool-error" ? "error" : "started", + }); + } + } + return true; + }, + }; +}; +``` +::: + +### Example: unnamed channel + +Without a name, the channel is a side-channel projection only — accessible on `stream.extensions` but not visible to consumers iterating raw events. This is the right choice for projections that hold in-process handles (promises, async iterables, class instances) that can't be serialized onto the main event stream. + +The example below pairs an unnamed channel with `get_stream_writer`, which lets graph nodes emit `custom`-channel events that the transformer then drains into the projection: + +:::python +```py +from langgraph.config import get_stream_writer +from langgraph.stream import ProtocolEvent, StreamChannel, StreamTransformer + + +def node(state): + writer = get_stream_writer() + writer({"kind": "progress", "message": "retrieving context"}) + return state + + +class CustomTransformer(StreamTransformer): + required_stream_modes = ("custom",) + + def __init__(self, scope: tuple[str, ...] = ()) -> None: + super().__init__(scope) + self.log = StreamChannel() + + def init(self) -> dict: + return {"custom": self.log} + + def process(self, event: ProtocolEvent) -> bool: + if event["method"] == "custom": + self.log.push(event["params"]["data"]) + return True + + +stream = graph.stream_events(input, version="v3", transformers=[CustomTransformer]) + +for item in stream.extensions["custom"]: + print(item) +``` +::: + +:::js +```ts +import { StreamChannel } from "@langchain/langgraph"; + +const customTransformer = () => { + const custom = new StreamChannel(); + + return { + init: () => ({ custom }), + process(event) { + if (event.method === "custom") { + custom.push(event.params.data); + } + return true; + }, + }; +}; +``` +::: + +### Example: final-value projection + +Use unnamed streams, promises, or other in-process objects when the projection should not flow into the main event stream: + +:::python +```py +from langgraph.stream import ProtocolEvent, StreamChannel, StreamTransformer + + +class StatsTransformer(StreamTransformer): + required_stream_modes = ("messages",) + + def __init__(self, scope: tuple[str, ...] = ()) -> None: + super().__init__(scope) + self.total_tokens = 0 + self.total_tokens_log = StreamChannel[int]() + + def init(self) -> dict: + return {"total_tokens": self.total_tokens_log} + + def process(self, event: ProtocolEvent) -> bool: + data = event["params"]["data"] + if isinstance(data, dict): + usage = data.get("usage") or {} + self.total_tokens += usage.get("output_tokens") or 0 + return True + + def finalize(self) -> None: + self.total_tokens_log.push(self.total_tokens) + self.total_tokens_log.close() +``` +::: + +:::js +```ts +const statsTransformer = () => { + let totalTokens = 0; + let resolveTotal!: (value: number) => void; + const totalTokensPromise = new Promise((resolve) => { + resolveTotal = resolve; + }); + + return { + init: () => ({ totalTokens: totalTokensPromise }), + process(event) { + if (event.method === "messages") { + const data = event.params.data as { usage?: { output_tokens?: number } }; + totalTokens += data.usage?.output_tokens ?? 0; + } + return true; + }, + finalize: () => resolveTotal(totalTokens), + }; +}; +``` +::: + +### Register at call time or compile time + +Pass transformers at call time for local experimentation: + +:::python +```py +stream = graph.stream_events( + input, + version="v3", + transformers=[StatsTransformer, ToolActivityTransformer], +) +``` +::: + +:::js +```ts +const stream = await graph.streamEvents(input, { + version: "v3", + transformers: [statsTransformer, toolActivityTransformer], +}); +``` +::: + +Compile transformers into the graph when every run of that graph should produce the projection: + +:::python +```py +graph = builder.compile( + transformers=[StatsTransformer, ToolActivityTransformer], +) +``` +::: + +:::js +```ts +const graph = builder.compile({ + transformers: [statsTransformer, toolActivityTransformer], +}); +``` +::: + +### Built-in: `ToolCallTransformer` + +:::python +LangGraph ships `ToolCallTransformer` as a built-in. Register it to expose `stream.tool_calls` on a plain `StateGraph`: + +```py +from langgraph.prebuilt import ToolCallTransformer + +stream = graph.stream_events(input, version="v3", transformers=[ToolCallTransformer]) + +for tool_call in stream.tool_calls: + print(tool_call.tool_name, tool_call.input) +``` +::: + +## Related + +LangGraph defines the streaming primitives. For using streaming with LangChain or Deep Agents, review the relevant product docs: + +- [LangChain agent streaming](/oss/langchain/event-streaming) covers ReAct-style agent messages, tool calls, and middleware updates. +- [Deep Agents streaming](/oss/deepagents/event-streaming) covers subagents, nested messages, and subagent tool calls. +- [LangChain frontend patterns](/oss/langchain/frontend/overview) and [LangGraph frontend patterns](/oss/langgraph/frontend/overview) show UI use cases built on top of streamed state. +- [LangSmith Streaming API](/langsmith/streaming) covers streaming against a graph deployed behind an Agent Server. + +The wire-level event and command formats are defined in the [Agent Protocol](https://github.com/langchain-ai/agent-protocol) repository and consumable as [`langchain-protocol`](https://pypi.org/project/langchain-protocol/) on PyPI and [`@langchain/protocol`](https://www.npmjs.com/package/@langchain/protocol) on npm. diff --git a/fault-tolerance.mdx b/fault-tolerance.mdx new file mode 100644 index 0000000..efd0548 --- /dev/null +++ b/fault-tolerance.mdx @@ -0,0 +1,1289 @@ +--- +title: Fault tolerance +description: Configure per-node timeouts, retries, and error handlers in LangGraph. +--- + +When a node fails—from a slow external API, a transient network error, or an unhandled exception—LangGraph gives you three composable mechanisms to respond: + +- [**Retries**](#retries) — automatically re-run failed attempts based on exception type and backoff settings +- [**Timeouts**](#timeouts) — cap how long a single attempt may run +- [**Error handling**](#error-handling) — run a recovery function after all retries are exhausted + +:::python +Use [**`set_node_defaults`**](#graph-defaults) to configure these mechanisms once for all nodes instead of repeating them on every `add_node` call. +::: + +:::js +Use [**`setNodeDefaults`**](#graph-defaults) to configure these mechanisms once for all nodes instead of repeating them on every `addNode` call. +::: + +These compose in a fixed order: when a node attempt raises any exception (including @[`NodeTimeoutError`] from a timeout), the retry policy decides whether to retry. Only after retries are exhausted does the error handler run. + +For stopping a run cleanly at a superstep boundary and resuming later, see [Graceful shutdown](#graceful-shutdown). + +:::python + +Per-node timeouts and node-level error handlers require `langgraph>=1.2`. + +::: + +:::js + +Per-node timeouts and node-level error handlers require `@langchain/langgraph>=1.4.0`. + +::: + +```mermaid +%%{init:{'theme':'base','themeVariables':{'lineColor':'#40668D','primaryColor':'#E5F4FF','primaryTextColor':'#030710','primaryBorderColor':'#006DDD'}}}%% +flowchart LR + start([Attempt starts]) --> exec[Run node] + exec -->|"success"| done([Continue graph]) + exec -->|"any exception
including NodeTimeoutError"| retry{retry_policy
matches?} + retry -->|"yes, attempts left"| exec + retry -->|"exhausted or absent"| handler{error_handler?} + handler -->|"yes"| run_handler["Invoke handler
with NodeError"] + run_handler --> route([Update state +
Command goto]) + handler -->|"no"| bubble([Exception
bubbles up]) + + classDef process fill:#E5F4FF,stroke:#006DDD,stroke-width:2px,color:#030710 + classDef decision fill:#FDF3FF,stroke:#7E65AE,stroke-width:2px,color:#504B5F + classDef alert fill:#F8E8E6,stroke:#B27D75,stroke-width:2px,color:#634643 + classDef output fill:#EBD0F0,stroke:#885270,stroke-width:2px,color:#441E33 + + class exec,run_handler process + class retry,handler decision + class bubble alert + class done,route,start output +``` + +## Retries + +A retry policy automatically re-runs a failed node attempt based on exception type and backoff settings. + +:::python +Pass `retry_policy=` to @[`add_node`]: + +```python +from langgraph.types import RetryPolicy + +builder.add_node( + "call_api", + call_api, + retry_policy=RetryPolicy(max_attempts=3), +) +``` +::: + +:::js +Pass `retryPolicy` to @[`addNode`]: + +```typescript +import { StateGraph } from "@langchain/langgraph"; + +const graph = new StateGraph(State) + .addNode("callApi", callApi, { retryPolicy: { maxAttempts: 3 } }) + .compile(); +``` +::: + +### Default behavior + +:::python +By default, `retry_on` uses `default_retry_on`, which retries on **any** exception except the following (and their subclasses): + +- `ValueError` +- `TypeError` +- `ArithmeticError` +- `ImportError` +- `LookupError` +- `NameError` +- `SyntaxError` +- `RuntimeError` +- `ReferenceError` +- `StopIteration` +- `StopAsyncIteration` +- `OSError` + +For exceptions from popular HTTP libraries such as `requests` and `httpx`, it only retries on 5xx status codes. @[`NodeTimeoutError`] is retryable by default. +::: + +:::js +Retries are opt-in. A node retries only when it has a `retryPolicy` configured, either directly or through graph defaults with [`setNodeDefaults`](#graph-defaults). An empty policy (`{}`) is enough. Without a policy, the first failure ends the attempt and LangGraph does not call `retryOn`. + +If the policy omits `retryOn`, LangGraph uses a built-in handler that retries thrown errors except: + +- Abort and cancellation errors: `error.name === "AbortError"`, or `error.message` starts with `"Cancel"` or `"AbortError"` +- `GraphValueError`, matched by `error.name` +- Aborted connections: `error.code === "ECONNABORTED"` +- HTTP client errors with status 400, 401, 402, 403, 404, 405, 406, 407, or 409, read from `error.response?.status` or `error.status` for clients such as `fetch`, Axios, and similar clients +- OpenAI-style quota errors: `error.error?.code === "insufficient_quota"` + +Other HTTP statuses, including 408 and 5xx responses, are retryable unless you override `retryOn`. @[`NodeTimeoutError`] is not on this blocklist, so it is retryable when a retry policy is configured. + +Some failures bypass `retryOn`. Graph control-flow errors, such as `GraphInterrupt` and `Command` routing, bubble up without retrying. An aborted run signal also stops the retry loop, even if `retryOn` would return `true`. +::: + +### Parameters + +:::python +| Parameter | Type | Default | Description | +| --------- | ---- | ------- | ----------- | +| `max_attempts` | `int` | `3` | Maximum number of attempts, including the first. | +| `initial_interval` | `float` | `0.5` | Seconds before the first retry. | +| `backoff_factor` | `float` | `2.0` | Multiplier applied to the interval after each retry. | +| `max_interval` | `float` | `128.0` | Maximum seconds between retries. | +| `jitter` | `bool` | `True` | Add random jitter to the interval. | +| `retry_on` | `type[Exception] \| Sequence[type[Exception]] \| Callable[[Exception], bool]` | `default_retry_on` | Exceptions to retry on, or a callable returning `True` for retryable exceptions. | +::: + +:::js +| Parameter | Type | Default | Description | +| --------- | ---- | ------- | ----------- | +| `maxAttempts` | `number` | `3` | Maximum number of attempts, including the first. | +| `initialInterval` | `number` | `500` | Milliseconds before the first retry. | +| `backoffFactor` | `number` | `2.0` | Multiplier applied to the interval after each retry. | +| `maxInterval` | `number` | `128000` | Maximum milliseconds between retries. | +| `jitter` | `boolean` | `true` | Add random jitter to the interval. | +| `retryOn` | `(error: unknown) => boolean` | built-in handler (when policy is set) | Callable returning `true` for retryable exceptions. Only used when `retryPolicy` is configured. | +| `logWarning` | `boolean` | `true` | Whether to log a warning when a retry is attempted. | +::: + +### Custom retry logic + +:::python +Pass a callable or exception type to `retry_on`. Import `default_retry_on` to extend the default behavior: + +```python +from langgraph.types import RetryPolicy, default_retry_on + +def custom_retry_on(exc: BaseException) -> bool: + if isinstance(exc, MyCustomError): + return False + return default_retry_on(exc) + +builder.add_node( + "call_api", + call_api, + retry_policy=RetryPolicy(max_attempts=3, retry_on=custom_retry_on), +) +``` +::: + +:::js +Pass a callable to `retryOn`. Unlike Python, there is no exported `defaultRetryOn` helper—implement your own predicate: + +```typescript +import { StateGraph } from "@langchain/langgraph"; + +class MyCustomError extends Error {} + +const graph = new StateGraph(State) + .addNode("callApi", callApi, { + retryPolicy: { + maxAttempts: 3, + retryOn: (error: unknown) => { + if (error instanceof MyCustomError) return false; + // Retry on other errors + return true; + }, + }, + }) + .compile(); +``` +::: + +### Inspect retry state + +Use execution info inside a node to inspect the current attempt number. This is useful for switching to a fallback when the primary call keeps failing: + +:::python +```python +from langgraph.graph import StateGraph, START, END +from langgraph.runtime import Runtime +from langgraph.types import RetryPolicy +from typing_extensions import TypedDict + +class State(TypedDict): + result: str + +def my_node(state: State, runtime: Runtime) -> State: + if runtime.execution_info.node_attempt > 1: # [!code highlight] + return {"result": call_fallback_api()} + return {"result": call_primary_api()} + +builder = StateGraph(State) +builder.add_node("my_node", my_node, retry_policy=RetryPolicy(max_attempts=3)) +builder.add_edge(START, "my_node") +builder.add_edge("my_node", END) +``` + +`execution_info` exposes the following fields: + +| Attribute | Type | Description | +| --------- | ---- | ----------- | +| `node_attempt` | `int` | Current attempt number (1-indexed). `1` on the first try, `2` on the first retry, etc. | +| `node_first_attempt_time` | `float \| None` | Unix timestamp of when the first attempt started. Constant across retries. | +| `thread_id` | `str \| None` | Thread ID for the current execution. `None` without a checkpointer. | +| `run_id` | `str \| None` | Run ID for the current execution. `None` when not provided in config. | +| `checkpoint_id` | `str` | Checkpoint ID for the current execution. | +| `task_id` | `str` | Task ID for the current execution. | + +`execution_info` is available even without a retry policy—`node_attempt` defaults to `1`. +::: + +:::js +```typescript +import { StateGraph, StateSchema, START, END, type Runtime } from "@langchain/langgraph"; +import * as z from "zod"; + +const State = new StateSchema({ + result: z.string(), +}); + +const myNode = async (state: typeof State.State, runtime: Runtime) => { + if ((runtime.executionInfo?.nodeAttempt ?? 1) > 1) { // [!code highlight] + return { result: await callFallbackApi() }; + } + return { result: await callPrimaryApi() }; +}; + +const graph = new StateGraph(State) + .addNode("myNode", myNode, { retryPolicy: { maxAttempts: 3 } }) + .addEdge(START, "myNode") + .addEdge("myNode", END) + .compile(); +``` + +`executionInfo` exposes the following fields: + +| Attribute | Type | Description | +| --------- | ---- | ----------- | +| `nodeAttempt` | `number` | Current attempt number (1-indexed). `1` on the first try, `2` on the first retry, etc. | +| `nodeFirstAttemptTime` | `number \| undefined` | Unix timestamp (ms) of when the first attempt started. Constant across retries. | +| `threadId` | `string \| undefined` | Thread ID for the current execution. `undefined` without a checkpointer. | +| `runId` | `string \| undefined` | Run ID for the current execution. `undefined` when not provided in config. | +| `checkpointId` | `string` | Checkpoint ID for the current execution. | +| `checkpointNs` | `string` | Checkpoint namespace for the current execution. | +| `taskId` | `string` | Task ID for the current execution. | + +`executionInfo` is available even without a retry policy—`nodeAttempt` defaults to `1`. +::: + +## Timeouts + +:::python + +Requires `langgraph>=1.2`. + + +The `timeout=` parameter on @[`add_node`] caps how long a single node attempt may run. Pass a number (seconds), a `timedelta`, or a @[`TimeoutPolicy`] for separate run and idle limits: + +```python +from datetime import timedelta +from langgraph.types import TimeoutPolicy + +# Simple wall-clock cap +builder.add_node("call_model", call_model, timeout=60) +builder.add_node("call_model", call_model, timeout=timedelta(minutes=2)) + +# Separate run and idle limits +builder.add_node( + "call_model", + call_model, + timeout=TimeoutPolicy(run_timeout=120, idle_timeout=30), +) +``` + + +Node timeouts only apply to **async** nodes. Sync nodes with a `timeout` are rejected at compile time. To wrap blocking I/O, use `asyncio.to_thread` inside an async node. + +::: + +:::js + +Requires `@langchain/langgraph>=1.4.0`. + + +The `timeout` parameter on @[`addNode`] caps how long a single node attempt may run. Pass a number (milliseconds) or a @[`TimeoutPolicy`] for separate run and idle limits: + +```typescript +import { StateGraph, type TimeoutPolicy } from "@langchain/langgraph"; + +// Simple wall-clock cap (60 seconds) +new StateGraph(State).addNode("callModel", callModel, { timeout: 60_000 }); + +// Separate run and idle limits +new StateGraph(State).addNode("callModel", callModel, { + timeout: { runTimeout: 120_000, idleTimeout: 30_000 }, +}); +``` +::: + +### Run timeout + +:::python +`run_timeout` is a hard wall-clock cap on a single attempt. It is never refreshed, regardless of node activity: + +```python +from langgraph.types import TimeoutPolicy + +builder.add_node( + "call_model", + call_model, + timeout=TimeoutPolicy(run_timeout=120), +) +``` +::: + +:::js +`runTimeout` is a hard wall-clock cap on a single attempt. It is never refreshed, regardless of node activity: + +```typescript +const graph = new StateGraph(State) + .addNode("callModel", callModel, { + timeout: { runTimeout: 120_000 }, + }) + .compile(); +``` +::: + +When the limit is exceeded, LangGraph raises @[`NodeTimeoutError`], clears any writes from the failed attempt, and lets the retry policy decide whether to retry. + +### Idle timeout + +:::python +`idle_timeout` is a progress-resetting cap. It fires only when the node stops making observable progress for the specified duration—unlike `run_timeout`, the clock resets whenever the node produces a progress signal: + +```python +builder.add_node( + "call_model", + call_model, + timeout=TimeoutPolicy(idle_timeout=30), +) +``` +::: + +:::js +`idleTimeout` is a progress-resetting cap. It fires only when the node stops making observable progress for the specified duration—unlike `runTimeout`, the clock resets whenever the node produces a progress signal: + +```typescript +const graph = new StateGraph(State) + .addNode("callModel", callModel, { + timeout: { idleTimeout: 30_000 }, + }) + .compile(); +``` +::: + +:::python +You can set `run_timeout` and `idle_timeout` together. Whichever fires first cancels the attempt. +::: + +:::js +You can set `runTimeout` and `idleTimeout` together. Whichever fires first cancels the attempt. +::: + +#### Progress signals + +:::python +Under the default `refresh_on="auto"`, the idle clock resets on any of the following: + +- State writes via `CONFIG_KEY_SEND` +- Stream output (yielded async stream chunks) +- Child-task scheduling +- Runtime stream-writer calls +- Any LangChain callback event from the node or its descendants (LLM tokens, tool calls, chain start/end, etc.) +::: + +:::js +Under the default `refreshOn: "auto"`, the idle clock resets on any of the following: + +- State writes through the graph write path +- Custom stream output via `runtime.writer` +- Child-task scheduling +- Any LangChain callback event from the node or its descendants (LLM tokens, tool calls, chain start/end, etc.) +::: + +#### Heartbeat mode + +:::python +Set `refresh_on="heartbeat"` to narrow the refresh source to explicit `runtime.heartbeat()` calls only. This is useful when you want a strict idle definition that isn't reset by chatty subordinates: + +```python +builder.add_node( + "call_model", + call_model, + timeout=TimeoutPolicy(idle_timeout=30, refresh_on="heartbeat"), +) +``` +::: + +:::js +Set `refreshOn: "heartbeat"` to narrow the refresh source to explicit `runtime.heartbeat()` calls only. This is useful when you want a strict idle definition that isn't reset by chatty subordinates: + +```typescript +const graph = new StateGraph(State) + .addNode("callModel", callModel, { + timeout: { idleTimeout: 30_000, refreshOn: "heartbeat" }, + }) + .compile(); +``` +::: + +#### Manual heartbeats + +For long-running work that doesn't naturally emit progress signals, call `runtime.heartbeat()` to manually reset the idle clock: + +:::python +```python +from langgraph.graph import StateGraph, START, END +from langgraph.runtime import Runtime +from langgraph.types import TimeoutPolicy +from typing_extensions import TypedDict + +class State(TypedDict): + result: str + +async def long_running_node(state: State, runtime: Runtime) -> State: + for batch in fetch_batches(): + process(batch) + runtime.heartbeat() # [!code highlight] + return {"result": "done"} + +builder = StateGraph(State) +builder.add_node( + "long_running_node", + long_running_node, + timeout=TimeoutPolicy(idle_timeout=30, refresh_on="heartbeat"), +) +builder.add_edge(START, "long_running_node") +builder.add_edge("long_running_node", END) +``` +::: + +:::js +```typescript +import { + StateGraph, + StateSchema, + START, + END, + type Runtime, +} from "@langchain/langgraph"; +import * as z from "zod"; + +const State = new StateSchema({ + result: z.string(), +}); + +const longRunningNode = async ( + state: typeof State.State, + runtime: Runtime +) => { + for (const batch of fetchBatches()) { + process(batch); + runtime.heartbeat?.(); // [!code highlight] + } + return { result: "done" }; +}; + +const graph = new StateGraph(State) + .addNode("longRunningNode", longRunningNode, { + timeout: { idleTimeout: 30_000, refreshOn: "heartbeat" }, + }) + .addEdge(START, "longRunningNode") + .addEdge("longRunningNode", END) + .compile(); +``` +::: + +`runtime.heartbeat()` is a no-op outside an idle-timed attempt, so you can call it unconditionally. + +### NodeTimeoutError + +When a timeout fires, LangGraph raises @[`NodeTimeoutError`] with structured context about which limit was hit: + +:::python +| Attribute | Type | Description | +| --------- | ---- | ----------- | +| `node` | `str` | Name of the node whose execution timed out. | +| `elapsed` | `float` | Seconds elapsed before the timeout fired. | +| `kind` | `Literal["idle", "run"]` | Which timeout fired. | +| `idle_timeout` | `float \| None` | The configured idle timeout (seconds), if any. | +| `run_timeout` | `float \| None` | The configured run timeout (seconds), if any. | +::: + +:::js +| Attribute | Type | Description | +| --------- | ---- | ----------- | +| `node` | `string` | Name of the node whose execution timed out. | +| `elapsed` | `number` | Milliseconds elapsed before the timeout fired. | +| `kind` | `"idle" \| "run"` | Which timeout fired. | +| `timeout` | `number` | The value (ms) of the timeout that fired. | +| `idleTimeout` | `number \| undefined` | The configured idle timeout (milliseconds), if any. | +| `runTimeout` | `number \| undefined` | The configured run timeout (milliseconds), if any. | + +Use `isNodeTimeoutError(error)` to narrow caught errors in TypeScript. +::: + +`NodeTimeoutError` is retryable by default. Combining `timeout` with a retry policy works out of the box—the timeout clock resets on each new attempt, and writes from a timed-out attempt are cleared before the next retry: + +:::python +```python +from langgraph.types import RetryPolicy, TimeoutPolicy + +builder.add_node( + "call_model", + call_model, + timeout=TimeoutPolicy(idle_timeout=30), + retry_policy=RetryPolicy(max_attempts=3), +) +``` +::: + +:::js +```typescript +const graph = new StateGraph(State) + .addNode("callModel", callModel, { + timeout: { idleTimeout: 30_000 }, + retryPolicy: { maxAttempts: 3 }, + }) + .compile(); +``` +::: + +### Dynamic timeouts with Send + +When using @[`Send`] to dispatch nodes dynamically (for example, in map-reduce patterns), you can pass a timeout directly on the `Send` to override the target node's static timeout for that specific push: + +:::python +```python +from langgraph.types import Send, TimeoutPolicy + +def fan_out(state: OverallState): + return [ + Send("process_item", {"item": item}, timeout=TimeoutPolicy(idle_timeout=15)) + for item in state["items"] + ] +``` +::: + +:::js +```typescript +import { Send } from "@langchain/langgraph"; + +const fanOut = (state: typeof State.State) => + state.items.map( + (item) => + new Send("processItem", { item }, { timeout: { idleTimeout: 15_000 } }) + ); +``` +::: + +:::python +If the timeout is omitted on the `Send`, the target node's timeout (set at @[`add_node`] time) applies. This lets you set a default timeout on the node and tighten it for individual calls. +::: + +:::js +If the timeout is omitted on the `Send`, the target node's timeout (set at @[`addNode`] time) applies. This lets you set a default timeout on the node and tighten it for individual calls. +::: + +## Error handling + +:::python + +Requires `langgraph>=1.2`. + +::: + +:::js + +Requires `@langchain/langgraph>=1.4.0`. + +::: + +An error handler runs after a node fails and all retries are exhausted. It receives the current state and can update it or route to a different node using @[`Command`]. This is useful for compensation flows (Saga patterns) where you want to recover gracefully rather than abort the entire graph. + +:::python +Pass `error_handler=` to @[`add_node`]: + +```python +from langgraph.errors import NodeError +from langgraph.types import Command, RetryPolicy +from langgraph.graph import StateGraph, START +from typing_extensions import TypedDict + +class State(TypedDict): + status: str + +def charge_payment(state: State) -> State: + raise RuntimeError("payment gateway timeout") + +def payment_error_handler(state: State, error: NodeError) -> Command: + return Command( + update={"status": f"compensated: {error.error}"}, + goto="finalize", + ) + +def finalize(state: State) -> State: + return state + +graph = ( + StateGraph(State) + .add_node( + "charge_payment", + charge_payment, + retry_policy=RetryPolicy(max_attempts=3, retry_on=ConnectionError), + error_handler=payment_error_handler, + ) + .add_node("finalize", finalize) + .add_edge(START, "charge_payment") + .compile() +) +``` +::: + +:::js +Pass `errorHandler` to @[`addNode`] on @[`StateGraph`] only (not the base `Graph` class): + +```typescript +import { + StateGraph, + StateSchema, + START, + Command, + NodeError, +} from "@langchain/langgraph"; +import * as z from "zod"; + +class ConnectionError extends Error {} + +const State = new StateSchema({ + status: z.string(), +}); + +const chargePayment = () => { + throw new Error("payment gateway timeout"); +}; + +const paymentErrorHandler = ( + state: typeof State.State, + error: NodeError +) => + new Command({ + update: { status: `compensated: ${error.error.message}` }, + goto: "finalize", + }); + +const finalize = (state: typeof State.State) => state; + +const graph = new StateGraph(State) + .addNode("chargePayment", chargePayment, { + retryPolicy: { + maxAttempts: 3, + retryOn: (err) => err instanceof ConnectionError, + }, + errorHandler: paymentErrorHandler, + }) + .addNode("finalize", finalize) + .addEdge(START, "chargePayment") + .compile(); +``` +::: + +The handler fires only after the retry policy is exhausted, or immediately if no retry policy is configured. The retry policy and the error handler stay decoupled: configure when to retry and when to compensate independently. + +### NodeError + +:::python +Error handlers receive failure context through a typed `error: NodeError` parameter, injected by type annotation (the same pattern as `runtime: Runtime`): + +```python +from langgraph.errors import NodeError + +def my_handler(state: State, error: NodeError) -> Command: + print(f"Node {error.node} failed with: {error.error}") + return Command(update={"status": "recovered"}, goto="next_step") +``` + +@[`NodeError`] is a frozen dataclass with two fields: + +| Attribute | Type | Description | +| --------- | ---- | ----------- | +| `node` | `str` | Name of the node whose execution failed. | +| `error` | `BaseException` | The exception raised by the failed node. | + +The `error: NodeError` parameter is opt-in. Handlers that don't need failure context can use simpler signatures like `(state)` or `(state, runtime)`. +::: + +:::js +Error handlers receive failure context through a typed `error: NodeError` parameter: + +```typescript +import { Command, NodeError } from "@langchain/langgraph"; + +const myHandler = (state: typeof State.State, error: NodeError) => { + console.log(`Node ${error.node} failed with: ${error.error.message}`); + return new Command({ + update: { status: "recovered" }, + goto: "nextStep", + }); +}; +``` + +@[`NodeError`] is a class with two fields: + +| Attribute | Type | Description | +| --------- | ---- | ----------- | +| `node` | `string` | Name of the node whose execution failed. | +| `error` | `Error` | The exception thrown by the failed node. | + +The `error: NodeError` parameter is opt-in. Handlers that don't need failure context can omit the second argument and accept only `state`. +::: + +### Route with Command + +Error handlers can return a @[`Command`] to update state and route to a specific node, enabling Saga / compensation patterns: + +:::python +```python +from langgraph.errors import NodeError +from langgraph.types import Command, RetryPolicy +from langgraph.graph import StateGraph, START +from typing_extensions import TypedDict + +class State(TypedDict): + status: str + +def reserve_inventory(state: State) -> State: + return {"status": "reserved"} + +def charge_payment(state: State) -> State: + raise RuntimeError("payment timeout") + +def payment_error_handler(state: State, error: NodeError) -> Command: + return Command( + update={"status": f"compensated_after_{error.node}: {error.error}"}, + goto="finalize", + ) + +def finalize(state: State) -> State: + return state + +graph = ( + StateGraph(State) + .add_node("reserve_inventory", reserve_inventory) + .add_node( + "charge_payment", + charge_payment, + retry_policy=RetryPolicy(max_attempts=3, retry_on=ConnectionError), + error_handler=payment_error_handler, + ) + .add_node("finalize", finalize) + .add_edge(START, "reserve_inventory") + .add_edge("reserve_inventory", "charge_payment") + .compile() +) +``` + +`charge_payment` retries on `ConnectionError` up to 3 times. If retries are exhausted (or the error isn't a `ConnectionError`), the handler compensates by updating state and routing to `finalize` instead of aborting the graph. +::: + +:::js +```typescript +import { + StateGraph, + StateSchema, + START, + Command, + NodeError, +} from "@langchain/langgraph"; +import * as z from "zod"; + +class ConnectionError extends Error {} + +const State = new StateSchema({ + status: z.string(), +}); + +const reserveInventory = () => ({ status: "reserved" }); + +const chargePayment = () => { + throw new Error("payment timeout"); +}; + +const paymentErrorHandler = ( + state: typeof State.State, + error: NodeError +) => + new Command({ + update: { + status: `compensated_after_${error.node}: ${error.error.message}`, + }, + goto: "finalize", + }); + +const finalize = (state: typeof State.State) => state; + +const graph = new StateGraph(State) + .addNode("reserveInventory", reserveInventory) + .addNode("chargePayment", chargePayment, { + retryPolicy: { + maxAttempts: 3, + retryOn: (err) => err instanceof ConnectionError, + }, + errorHandler: paymentErrorHandler, + }) + .addNode("finalize", finalize) + .addEdge(START, "reserveInventory") + .addEdge("reserveInventory", "chargePayment") + .compile(); +``` + +`chargePayment` retries on `ConnectionError` up to 3 times. If retries are exhausted (or the error isn't a `ConnectionError`), the handler compensates by updating state and routing to `finalize` instead of aborting the graph. +::: + +### Resume-safe failures + + +Failure provenance is checkpointed. If the graph is interrupted or the process crashes after a node fails but before the handler completes, the handler sees the same `NodeError` context when the graph resumes from its checkpoint. + + +### Behavior with `interrupt()` + + +`interrupt()` raised inside a node is **not** routed to the error handler. Interrupts use the `GraphBubbleUp` mechanism to pause graph execution for human-in-the-loop workflows, bypassing both retry policies and error handlers. The graph pauses as usual. + + +### Subgraph failures + +If a node wraps a subgraph and the subgraph raises an unhandled exception, that exception surfaces to the parent node. If the parent node has an error handler, the handler fires with the subgraph's exception in `error.error`. + +:::python + +## Graph defaults + + +Requires `langgraph>=1.2`. + + +Instead of repeating the same `retry_policy=`, `error_handler=`, `timeout=`, or `cache_policy=` on every `add_node` call, use `set_node_defaults()` to configure graph-wide defaults in one place: + +```python +from langgraph.errors import NodeError +from langgraph.types import RetryPolicy, TimeoutPolicy +from langgraph.graph import StateGraph, START +from typing_extensions import TypedDict + +class State(TypedDict): + status: str + +def default_error_handler(state: State, error: NodeError) -> State: + return {"status": f"handled: {error.error}"} + +graph = ( + StateGraph(State) + .set_node_defaults( + retry_policy=RetryPolicy(max_attempts=3), + error_handler=default_error_handler, + timeout=TimeoutPolicy(run_timeout=30), + ) + .add_node("step_a", step_a) + .add_node("step_b", step_b) + .add_edge(START, "step_a") + .compile() +) +``` + +Both `step_a` and `step_b` now share the same retry policy, error handler, and timeout without any duplication. + +### Precedence + +Per-node values passed directly to `add_node()` always override the defaults set by `set_node_defaults()`. Defaults are resolved at `compile()` time, so you can call `set_node_defaults()` before or after `add_node()` in any order: + +```python +graph = ( + StateGraph(State) + .set_node_defaults(error_handler=default_error_handler) + .add_node("step_a", step_a) # uses default_error_handler + .add_node("step_b", step_b, error_handler=custom_error_handler) # uses custom_error_handler + .add_edge(START, "step_a") + .compile() +) +``` + +### Default error handler + +The `error_handler` default is particularly valuable when you want a single catch-all recovery function for any node that fails without its own handler. The handler accepts the same `(state, error: NodeError)` signature described in [Error handling](#error-handling): + +```python +from langgraph.errors import NodeError +from langgraph.graph import StateGraph, START +from langgraph.types import RetryPolicy +from typing_extensions import TypedDict + +class State(TypedDict): + status: str + +def always_failing(state: State) -> State: + raise ValueError("something went wrong") + +def default_handler(state: State, error: NodeError) -> State: + return {"status": f"recovered from {error.node}: {error.error}"} + +graph = ( + StateGraph(State) + .set_node_defaults( + retry_policy=RetryPolicy(max_attempts=2), + error_handler=default_handler, + ) + .add_node("always_failing", always_failing) + .add_edge(START, "always_failing") + .compile() +) +``` + +The node is retried twice, then `default_handler` runs. The default handler also accepts `RunnableConfig` as an optional third argument if you need access to config values such as `thread_id`: + +```python +from langchain_core.runnables import RunnableConfig + +def default_handler(state: State, error: NodeError, config: RunnableConfig) -> State: + thread_id = config["configurable"].get("thread_id") + return {"status": f"handled on thread {thread_id}"} +``` + +### Applicability matrix + +Not all defaults apply to all node types. Error-handler nodes (those registered via `add_node(error_handler=...)`) are excluded from certain defaults to prevent unsafe behavior: + +| `set_node_defaults` parameter | Applies to regular nodes | Applies to error-handler nodes | Reason | +| ----------------------------- | ------------------------ | ------------------------------ | ------ | +| `retry_policy` | ✅ | ✅ | Handlers should be retried on transient failures | +| `timeout` | ✅ | ✅ | Stuck handlers should be cancelled like stuck regular nodes | +| `error_handler` | ✅ | ❌ | Handlers must never catch themselves | +| `cache_policy` | ✅ | ❌ | Caching handler results is unsafe | + +### Scope + +Defaults set on a parent graph are **not** inherited by subgraphs. Each graph maintains its own defaults. + +::: + +:::js + +## Graph defaults + + +Requires `@langchain/langgraph>=1.4.0`. + + +Instead of repeating the same `retryPolicy`, `errorHandler`, `timeout`, or `cachePolicy` on every `addNode` call, use [`setNodeDefaults`](https://reference.langchain.com/javascript/langchain-langgraph/index/StateGraph#member-setNodeDefaults) to configure graph-wide defaults in one place: + +```typescript +import { StateGraph, START, NodeError } from "@langchain/langgraph"; + +const defaultErrorHandler = ( + state: typeof State.State, + error: NodeError +) => ({ status: `handled: ${error.error.message}` }); + +const graph = new StateGraph(State) + .setNodeDefaults({ + retryPolicy: { maxAttempts: 3 }, + errorHandler: defaultErrorHandler, + timeout: { runTimeout: 30_000 }, + cachePolicy: { ttl: 60 }, + }) + .addNode("stepA", stepA) + .addNode("stepB", stepB) + .addEdge(START, "stepA") + .compile(); +``` + +Both `stepA` and `stepB` now share the same retry policy, error handler, timeout, and cache policy without any duplication. + +### Precedence + +Per-node values passed directly to `addNode()` always override defaults set by `setNodeDefaults()`. Defaults are resolved at `compile()` time, so you can call `setNodeDefaults()` before or after `addNode()` in any order: + +```typescript +import { StateGraph, START } from "@langchain/langgraph"; + +const graph = new StateGraph(State) + .setNodeDefaults({ errorHandler: defaultErrorHandler }) + .addNode("stepA", stepA) // uses defaultErrorHandler + .addNode("stepB", stepB, { errorHandler: customErrorHandler }) // overrides default + .addEdge(START, "stepA") + .compile(); +``` + +### Applicability matrix + +Not all defaults apply to all node types. Error-handler nodes (those registered via `addNode(..., { errorHandler })`) are excluded from certain defaults to prevent unsafe behavior: + +| `setNodeDefaults` parameter | Applies to regular nodes | Applies to error-handler nodes | Reason | +| ----------------------------- | ------------------------ | ------------------------------ | ------ | +| `retryPolicy` | ✅ | ✅ | Handlers should be retried on transient failures | +| `timeout` | ✅ | ✅ | Stuck handlers should be cancelled like stuck regular nodes | +| `errorHandler` | ✅ | ❌ | Handlers must never catch themselves | +| `cachePolicy` | ✅ | ❌ | Caching handler results is unsafe | + +### Scope + +Defaults set on a parent graph are **not** inherited by subgraphs. Each graph maintains its own defaults. + +::: + +## Functional API + +:::python +The same `timeout=` and `retry_policy=` parameters are available on `@task` and `@entrypoint` in the functional API: + +```python +from langgraph.func import entrypoint, task +from langgraph.types import RetryPolicy, TimeoutPolicy + +@task( + timeout=TimeoutPolicy(idle_timeout=30), + retry_policy=RetryPolicy(max_attempts=3), +) +async def call_api(url: str) -> str: + response = await fetch(url) + return response.text + +@entrypoint(timeout=60) +async def my_workflow(inputs: dict) -> str: + result = await call_api("https://api.example.com/data") + return result +``` + +The behavior is identical to `add_node`: `NodeTimeoutError` is raised on timeout, buffered writes are cleared, and the retry policy decides whether to retry. +::: + +:::js +The `timeout` option is available on `task` and `entrypoint`; `task` also accepts a `retry` option (not `retryPolicy`): + +```typescript +import { entrypoint, task } from "@langchain/langgraph"; + +const callApi = task( + { + name: "callApi", + timeout: { idleTimeout: 30_000 }, + retry: { maxAttempts: 3 }, + }, + async (url: string) => { + const response = await fetch(url); + return response.text(); + } +); + +const myWorkflow = entrypoint( + { name: "myWorkflow", timeout: 60_000 }, + async (inputs: { url: string }) => { + return await callApi(inputs.url); + } +); +``` + +The behavior matches `addNode`: `NodeTimeoutError` is raised on timeout, buffered writes are cleared, and the retry policy decides whether to retry. Error handlers are not available on `task` / `entrypoint` in the JavaScript/TypeScript SDK—use `StateGraph.addNode(..., { errorHandler })` instead. +::: + +## Graceful shutdown + +Cooperative shutdown lets you stop an in-flight graph run after the current superstep completes and save a resumable checkpoint. This is useful for handling SIGTERM signals or any external supervisor that needs to reclaim resources without losing work. + +:::python + +Requires `langgraph>=1.2`. + + +Create a @[`RunControl`] and pass it as `control=` to `invoke` or `stream`. Call `request_drain()` from any thread to signal that the run should stop: + +```python +from langgraph.runtime import RunControl +from langgraph.errors import GraphDrained + +control = RunControl() + +# In a signal handler or supervisor: +# control.request_drain("sigterm") + +try: + result = graph.invoke(inputs, config, control=control) +except GraphDrained as e: + # The graph stopped early and saved a checkpoint. + # Resume later with the same config. + print(f"Drained: {e.reason}") +``` +::: + +:::js + +Requires `@langchain/langgraph>=1.4.0`. + + +Create a @[`RunControl`] and pass it as `control` to `invoke` or `stream`. Call `requestDrain()` from any context to signal that the run should stop: + +```typescript +import { RunControl, GraphDrained } from "@langchain/langgraph"; + +const control = new RunControl(); + +// In a signal handler or supervisor: +// control.requestDrain("sigterm"); + +try { + const result = await graph.invoke(inputs, { ...config, control }); +} catch (e) { + if (e instanceof GraphDrained) { + // The graph stopped early and saved a checkpoint. + // Resume later with the same config. + console.log(`Drained: ${e.reason}`); + } else { + throw e; + } +} +``` +::: + +### Semantics + +Drain is cooperative and operates between supersteps, never preempting work that is already running: + +:::python +| Scenario | Behavior | +| -------- | -------- | +| Node mid-execution | Runs to completion. Drain takes effect on the next superstep. | +| Node with a retry policy currently retrying | Retry loop runs to exhaustion or success. Drain takes effect after. | +| Graph finishes naturally on the same tick as drain | Returns normally. Inspect `control.drain_requested` to distinguish from a normal run. | +| More supersteps remain | Raises `GraphDrained(reason)`. Checkpoint is saved and resumable. | +| Subgraph requests drain | `GraphDrained` bubbles up through the parent and stops it at its own next superstep boundary. | +::: + +:::js +| Scenario | Behavior | +| -------- | -------- | +| Node mid-execution | Runs to completion. Drain takes effect on the next superstep. | +| Node with a retry policy currently retrying | Retry loop runs to exhaustion or success. Drain takes effect after. | +| Graph finishes naturally on the same tick as drain | Returns normally. Inspect `control.drainRequested` to distinguish from a normal run. | +| More supersteps remain | Raises `GraphDrained(reason)`. Checkpoint is saved and resumable. | +| Subgraph requests drain | `GraphDrained` bubbles up through the parent and stops it at its own next superstep boundary. | +::: + +### Resume after drain + +:::python +Resume a drained run with `invoke(None, config)` using the same `thread_id`: + +```python +result = graph.invoke(None, config) +``` +::: + +:::js +Resume a drained run with `invoke(null, config)` using the same `thread_id`: + +```typescript +const result = await graph.invoke(null, config); +``` +::: + +### Read drain state inside a node + +Access drain state through the `runtime` parameter to adjust node behavior before the superstep boundary is reached: + +:::python +```python +from langgraph.runtime import Runtime + +async def my_node(state: State, runtime: Runtime) -> State: + if runtime.drain_requested: + # Skip expensive work and return a minimal result + return {"status": "skipped", "reason": runtime.drain_reason} + return {"status": await do_work()} +``` +::: + +:::js +```typescript +import { type Runtime } from "@langchain/langgraph"; + +const myNode = async (state: typeof State.State, runtime: Runtime) => { + if (runtime.control?.drainRequested) { + // Skip expensive work and return a minimal result + return { status: "skipped", reason: runtime.control.drainReason }; + } + return { status: await doWork() }; +}; +``` +::: + +### SIGTERM hook pattern + +The recommended pattern for handling process shutdown: + +:::python +```python +import signal +from langgraph.runtime import RunControl +from langgraph.errors import GraphDrained + +control = RunControl() +signal.signal(signal.SIGTERM, lambda *_: control.request_drain("sigterm")) + +try: + result = graph.invoke(inputs, config, control=control) +except GraphDrained as e: + log.info("graph drained: %s", e.reason) + # Resume on next startup with the same config +``` + + +`request_drain()` does not cancel running asyncio tasks or kill threads. For a hard upper bound, pair drain with a graceful timeout and task cancellation. + +::: + +:::js +```typescript +import process from "node:process"; +import { RunControl, GraphDrained } from "@langchain/langgraph"; + +const control = new RunControl(); +process.on("SIGTERM", () => control.requestDrain("sigterm")); + +try { + const result = await graph.invoke(inputs, { ...config, control }); +} catch (e) { + if (e instanceof GraphDrained) { + console.log(`graph drained: ${e.reason}`); + // Resume on next startup with the same config + } else { + throw e; + } +} +``` +::: + +:::js + +`requestDrain()` does not cancel in-flight async work. For a hard upper bound, pair drain with a graceful timeout and an `AbortSignal`. + +::: + +## Limitations + +:::python +- **Timeouts are async-only**: sync nodes with a `timeout` are rejected at compile time. +- **One handler per node**: each node can have at most one `error_handler`. +- **Handler failures bubble up**: if the error handler itself raises, that exception propagates as if the node had no handler. +- **`set_node_defaults` is not inherited by subgraphs**: each graph manages its own defaults independently. +::: + +:::js +- **`setNodeDefaults` is not inherited by subgraphs**: each graph manages its own defaults independently. +- **Error handlers are `StateGraph`-only**: pass `errorHandler` to `StateGraph.addNode`, not the base `Graph` class. Error handlers are not available on `task` / `entrypoint`. +- **One handler per node**: each node can have at most one `errorHandler`. +- **Handler failures bubble up**: if the error handler itself throws, that exception propagates as if the node had no handler. +::: diff --git a/frontend/custom-stream-channels.mdx b/frontend/custom-stream-channels.mdx new file mode 100644 index 0000000..652fc9f --- /dev/null +++ b/frontend/custom-stream-channels.mdx @@ -0,0 +1,404 @@ +--- +title: Custom stream channels +description: Stream custom server-side data to the frontend and read it with useExtension and useChannel +--- + +LangGraph agents stream more than messages and tool calls. A server-side +**stream transformer** can inspect or rewrite the protocol as it flows to the +client and publish its own structured data on a named **custom channel**. The +frontend reads that channel with two selectors: @[`useExtension`] for the latest +payload, and @[`useChannel`] as a raw-events escape hatch. + +The example below is a customer-support agent whose transformer redacts PII +(emails, phone numbers, SSNs, card numbers, IPs) from every event before it +reaches the browser, and publishes running redaction counts on a +`redaction-stats` channel. The side panel renders those counts live. + +import { PatternEmbed } from "/snippets/pattern-embed.jsx" +import UseStreamTypeInference from '/snippets/oss/use-stream-type-inference.mdx'; + + + +## How custom channels work + +A custom channel has two ends. On the server, a @[`StreamTransformer`] opens a +named @[`StreamChannel`] and pushes payloads onto it. On the client, a selector +subscribes to the matching `custom:` channel and exposes the payloads as +reactive state. + +The transformer's `process` method runs for every protocol event. It can mutate +the event in place (here, scrubbing PII from `messages`, `tools`, and `values` +data) and push side-channel updates whenever it has something to report. + +The client-side selectors (`useExtension`, `useChannel`) ship with the v1 +frontend SDK packages (`@langchain/react`, `@langchain/vue`, +`@langchain/svelte`, `@langchain/angular`). + +:::python + + +Stream transformers and `StreamChannel` require `langgraph>=1.2`. + + +::: + +:::js + + +Stream transformers and `StreamChannel` require `@langchain/langgraph>=1.3.1`. + + +::: + +:::python + +```python +import time + +from langgraph.stream import ProtocolEvent, StreamChannel, StreamTransformer + + +class RedactionStatsTransformer(StreamTransformer): + def __init__(self, scope: tuple[str, ...] = ()) -> None: + super().__init__(scope) + # Open a channel named "redaction-stats". + self.redaction_stats = StreamChannel("redaction-stats") + self.counts = empty_counts() + + def init(self) -> dict[str, StreamChannel]: + return {"redactionStats": self.redaction_stats} + + def process(self, event: ProtocolEvent) -> bool: + # Redact event["params"]["data"] in place and tally what was found. + delta = redact_in_place(event, self.counts) + if delta: + # Publish a payload on the channel. + self.redaction_stats.push( + { + "kind": "update", + "at": int(time.time() * 1000), + "delta": delta, + "counts": dict(self.counts), + "total": sum(self.counts.values()), + } + ) + return True # Keep the (now-redacted) event in the stream. + + +def create_redaction_stats_transformer() -> RedactionStatsTransformer: + return RedactionStatsTransformer() +``` + +Attach the transformer when you build the agent: + +```python +from langchain.agents import create_agent + +agent = create_agent( + model="anthropic:claude-haiku-4-5", + tools=[...], + transformers=[create_redaction_stats_transformer], +) +``` + +::: + +:::js + +```ts +import { StreamChannel } from "@langchain/langgraph"; +import type { ProtocolEvent, StreamTransformer } from "@langchain/langgraph"; + +export const createRedactionStatsTransformer = (): StreamTransformer<{ + redactionStats: StreamChannel; +}> => { + // Open a remote channel named "redaction-stats". + const redactionStats = StreamChannel.remote("redaction-stats"); + const counts = emptyCounts(); + + return { + init: () => ({ redactionStats }), + + process(event: ProtocolEvent): boolean { + // Redact event.params.data in place and tally what was found. + const delta = redactInPlace(event, counts); + if (Object.keys(delta).length > 0) { + // Publish a payload on the channel. + redactionStats.push({ + kind: "update", + at: Date.now(), + delta, + counts: { ...counts }, + total: totalRedactions(counts), + }); + } + return true; // Keep the (now-redacted) event in the stream. + }, + }; +}; +``` + +Attach the transformer when you build the agent: + +```ts +import { createAgent } from "langchain"; + +const agent = createAgent({ + model: "anthropic:claude-haiku-4-5", + tools: [...], + streamTransformers: [createRedactionStatsTransformer], +}); +``` + +::: + +The payload type is whatever the transformer pushes. The client examples below +read this shape: + +```ts +type PiiType = "email" | "phone" | "ssn" | "credit_card" | "ip_address"; + +type RedactionStatsEvent = { + kind: "update"; + at: number; + delta: Partial>; + counts: Record; + total: number; +}; +``` + +## Setting up `useStream` + +Wire up @[`useStream`] as usual. The custom-channel selectors take the same +`stream` handle returned here. + + + + +```tsx React +import { useStream } from "@langchain/react"; + +const AGENT_URL = "http://localhost:2024"; + +export function RedactionChat() { + const stream = useStream({ + apiUrl: AGENT_URL, + assistantId: "custom_stream_channel", + }); + + return ; +} +``` + +```vue Vue + + + +``` + +```svelte Svelte + + + +``` + +```ts Angular +import { Component } from "@angular/core"; +import { injectStream } from "@langchain/angular"; + +const AGENT_URL = "http://localhost:2024"; + +@Component({ + selector: "app-redaction-chat", + template: ``, +}) +export class RedactionChatComponent { + stream = injectStream({ + apiUrl: AGENT_URL, + assistantId: "custom_stream_channel", + }); +} +``` + + +## Read the latest payload with `useExtension` + +`useExtension` subscribes to a `custom:` channel and returns the most +recent payload the transformer pushed, already unwrapped and typed. It is the +ergonomic choice when the UI only needs the current value, such as a live +counter, progress percentage, or status badge. + +Pass the bare channel name (`"redaction-stats"`), not the `custom:` prefix: + + +```tsx React +import { useExtension } from "@langchain/react"; + +const latest = useExtension(stream, "redaction-stats"); +// latest?.total, latest?.counts.email, latest?.delta +``` + +```vue Vue +import { useExtension } from "@langchain/vue"; + +const latest = useExtension(stream, "redaction-stats"); +// latest.value?.total +``` + +```svelte Svelte +import { useExtension } from "@langchain/svelte"; + +const latest = useExtension(stream, "redaction-stats"); +// latest?.total +``` + +```ts Angular +import { injectExtension } from "@langchain/angular"; + +const latest = injectExtension(stream, "redaction-stats"); +// latest()?.total +``` + + +The return value follows each framework's reactivity model: a plain value in +React and Svelte, a `Ref` in Vue (`latest.value`), and a signal in Angular +(`latest()`). The value is `undefined` until the first payload arrives. + +An optional third `target` argument scopes the subscription to a namespace, the +same way `useMessages(stream, node)` scopes messages to a discovered graph node. +See [Graph execution](/oss/langgraph/frontend/graph-execution) for namespace +targeting. + +## Buffer raw events with `useChannel` + +`useChannel` is the raw-events escape hatch. It subscribes to one or more +channels and returns a bounded buffer of the underlying protocol events rather +than a single unwrapped value. Reach for it when you need history instead of the +latest value, such as an event log or audit trail, or when you need a channel +that no higher-level selector covers. + +Pass the full channel id (`"custom:redaction-stats"`): + + +```tsx React +import { useChannel } from "@langchain/react"; + +const rawEvents = useChannel(stream, ["custom:redaction-stats"]); +``` + +```vue Vue +import { useChannel } from "@langchain/vue"; + +const rawEvents = useChannel(stream, ["custom:redaction-stats"]); +// rawEvents.value +``` + +```svelte Svelte +import { useChannel } from "@langchain/svelte"; + +const rawEvents = useChannel(stream, ["custom:redaction-stats"]); +``` + +```ts Angular +import { injectChannel } from "@langchain/angular"; + +const rawEvents = injectChannel(stream, ["custom:redaction-stats"]); +// rawEvents() +``` + + +Each entry is a raw protocol event, so the payload sits under +`event.params.data`. Unwrap it yourself: + +```ts +function parseRedactionStatsEvents(rawEvents: Event[]): RedactionStatsEvent[] { + const out: RedactionStatsEvent[] = []; + for (const event of rawEvents) { + const data = event.params?.data; + const payload = data?.payload ?? data; + if (payload?.kind === "update") out.push(payload); + } + return out; +} +``` + +Control the buffer with the options argument: + +```ts +const rawEvents = useChannel( + stream, + ["custom:redaction-stats"], + undefined, // target namespace + { bufferSize: 200, replay: true }, +); +``` + +| Option | Default | Effect | +| --- | --- | --- | +| `bufferSize` | `"default"` | Maximum number of buffered events. Older events drop once the cap is reached. | +| `replay` | `true` | Replay events already seen on the channel when the selector mounts, instead of only live events. | + + +Prefer the higher-level selectors (`useExtension`, `useMessages`, +`useToolCalls`, `useValues`) for common cases. They return typed, unwrapped +values and track only what you render. Use `useChannel` when you specifically +need the raw event stream. + + +## Choosing between `useExtension` and `useChannel` + +Both read the same custom channel but differ in what they return: + +| | `useExtension` | `useChannel` | +| --- | --- | --- | +| **Returns** | Latest payload (`T \| undefined`) | Bounded buffer of raw events (`Event[]`) | +| **Shape** | Unwrapped, typed payload | Raw protocol events; unwrap `event.params.data` yourself | +| **Subscribe by** | Channel name (`"redaction-stats"`) | Full channel id (`["custom:redaction-stats"]`) | +| **Use when** | You need the current value | You need history, a log, or multiple channels | +| **Options** | — | `bufferSize`, `replay` | + +A common pattern is to use both on the same channel: `useExtension` drives a +live summary (current totals), while `useChannel` backs a scrolling event log of +every update across the thread. + +## Use cases + +Custom channels fit any server-side signal that does not map cleanly to +messages, tool calls, or graph state: + +- **Compliance and redaction stats**: counts of scrubbed PII, blocked content, + or policy hits, as in the example above. +- **Progress reporting**: percentage complete or step labels emitted by a + long-running tool. +- **Live metrics**: token usage, latency, or cost accumulating during a run. +- **Sources and citations**: retrieved documents pushed to a side panel as the + agent grounds its answer. +- **Domain events**: any structured update your backend wants to surface + without changing the message transcript. + +## Related + +- [Overview](/oss/langgraph/frontend/overview) — the LangGraph frontend stream + API and architecture. +- [Graph execution](/oss/langgraph/frontend/graph-execution) — namespace-scoped + selectors for multi-node pipelines. diff --git a/frontend/graph-execution.mdx b/frontend/graph-execution.mdx index 8395954..b8ce6d8 100644 --- a/frontend/graph-execution.mdx +++ b/frontend/graph-execution.mdx @@ -10,7 +10,13 @@ for each node, showing its status, streaming its content in real time, and tracking completion across the entire workflow. Users see exactly what the agent is doing, which step it's on, and what each step produced. +This pattern is especially useful for production agents because it turns graph +structure into product UX. Instead of treating the run as a single assistant +response, you can expose the same checkpoints, node names, state keys, and +stream metadata that LangGraph uses internally. + import { PatternEmbed } from "/snippets/pattern-embed.jsx" +import UseStreamTypeInference from '/snippets/oss/use-stream-type-inference.mdx'; @@ -24,54 +30,38 @@ task. For example, a research pipeline might have: 3. **Analyze**: draw conclusions from the research 4. **Synthesize**: produce a final, polished response -Each node writes its output to a specific key in the graph's state. By mapping -these node names and state keys to UI components, you can create a visual -representation of the entire pipeline. +Each node writes its output to a specific key in the graph's state. On the +frontend, you don't need to hardcode that mapping as @[`useStream`] discovers +each node as it runs via `stream.subgraphs` and exposes a +@[`SubgraphDiscoverySnapshot`] for every observed step: ```ts -const PIPELINE_NODES = [ - { name: "classify", stateKey: "classification", label: "Classify" }, - { name: "do_research", stateKey: "research", label: "Research" }, - { name: "analyze", stateKey: "analysis", label: "Analyze" }, - { name: "synthesize", stateKey: "synthesis", label: "Synthesize" }, -]; - -const PIPELINE_NODE_NAMES = new Set(PIPELINE_NODES.map((n) => n.name)); -``` - -## Setting up useStream - -Wire up `useStream` as usual. The key properties you'll use are `messages` -(for streaming content routing), `values` (for completed node outputs), and -`getMessagesMetadata` (for identifying which node produced each token). - -:::js +// Nodes are discovered automatically — no hardcoded list needed +const graphNodes = [...stream.subgraphs.values()]; -Define a TypeScript interface matching your agent's state schema and pass it as a type parameter to `useStream` for type-safe access to state values, including custom state keys for each pipeline node. In the examples below, replace `typeof myAgent` with your interface name: - -```ts -import type { BaseMessage } from "@langchain/core/messages"; - -interface AgentState { - messages: BaseMessage[]; - classification: string; - research: string; - analysis: string; - synthesis: string; -} +// Each snapshot carries the node name and current status +graphNodes.forEach((node) => { + console.log(node.nodeName, node.status); // "classify", "running" +}); ``` -::: +Use `node.nodeName` for labels in the progress bar and card headers. Pass each +snapshot to `useMessages(stream, node)` to render node-scoped streaming content +without coupling the UI to graph state key names. -:::js +This mapping becomes the contract between your graph and your UI. Backend +authors can add, rename, or reorder nodes intentionally, while frontend authors +decide how each state key should be visualized: a status badge, markdown panel, +table, chart, trace view, or approval card. -Import your agent and pass `typeof myAgent` as a type parameter to `useStream` for type-safe access to state values: +## Setting up `useStream` -```ts -import type { myAgent } from "./agent"; -``` +Wire up @[`useStream`] as usual. The key properties you'll use are `messages` +(for the conversation) and `subgraphs` (for the graph nodes discovered in the +current run). Pass each discovered subgraph snapshot to a selector to read the +messages scoped to that node. -::: + ```tsx React @@ -84,16 +74,12 @@ export function PipelineChat() { apiUrl: AGENT_URL, assistantId: "graph_execution_cards", }); + const graphNodes = [...stream.subgraphs.values()]; return (
- - + +
); } @@ -113,12 +99,14 @@ const stream = useStream({ @@ -130,26 +118,25 @@ const stream = useStream({ const AGENT_URL = "http://localhost:2024"; - const { messages, values, getMessagesMetadata, submit } = useStream({ + const stream = useStream({ apiUrl: AGENT_URL, assistantId: "graph_execution_cards", });
- +
``` ```ts Angular -import { Component } from "@angular/core"; -import { useStream } from "@langchain/angular"; +import { Component, computed } from "@angular/core"; +import { injectStream } from "@langchain/angular"; const AGENT_URL = "http://localhost:2024"; @@ -158,86 +145,66 @@ const AGENT_URL = "http://localhost:2024"; template: `
`, }) export class PipelineChatComponent { - PIPELINE_NODES = PIPELINE_NODES; - - stream = useStream({ + stream = injectStream({ apiUrl: AGENT_URL, assistantId: "graph_execution_cards", }); + + graphNodes = computed(() => [...this.stream.subgraphs().values()]); } ```
## Routing streaming tokens to nodes -As the agent streams, each message is annotated with metadata that identifies -which graph node produced it. Use `getMessagesMetadata` to extract the -`langgraph_node` value and route tokens to the correct card: +As the graph streams, each discovered subgraph snapshot identifies the node it +belongs to. Pass that snapshot to a selector hook or composable to read the +messages scoped to that node: -```ts -function getStreamingContent( - messages: BaseMessage[], - getMetadata: (msg: BaseMessage) => MessageMetadata | undefined -): Record { - const content: Record = {}; - - for (const message of messages) { - if (message.type !== "ai") continue; - - const metadata = getMetadata(message); - const node = metadata?.streamMetadata?.langgraph_node; - - if (node && PIPELINE_NODE_NAMES.has(node)) { - content[node] = typeof message.content === "string" - ? message.content - : ""; - } - } - - return content; +```tsx +import { AIMessage } from "langchain"; +import { useMessages, type AnyStream, type SubgraphDiscoverySnapshot } from "@langchain/react"; + +function NodeCard({ + node, + stream, +}: { + node: SubgraphDiscoverySnapshot; + stream: AnyStream; +}) { + const messages = useMessages(stream, node); + const lastAIMessage = messages.find(AIMessage.isInstance); + const streamingContent = lastAIMessage?.text ?? ""; + + return ; } ``` -This gives you a map from node name to its current streaming content. As tokens -arrive, the corresponding card updates in real time. - - -The `streamMetadata.langgraph_node` field is set automatically by LangGraph. -You don't need any special configuration on the backend. Just stream messages -as usual, and the metadata is included. - +The first mounted selector opens a scoped subscription for that node namespace. +When the node card unmounts, the subscription is released automatically. ## Determining node status -Each node can be in one of four states: not started, streaming, complete, or -idle. You derive the status from two sources: the streaming content map (for -active nodes) and `stream.values` (for completed nodes): +Each discovered node carries its current status. Use `node.status` directly; +the discovery snapshot reports `"pending"`, `"running"`, `"complete"`, or +`"error"`: ```ts -type NodeStatus = "idle" | "streaming" | "complete"; - -function getNodeStatus( - node: { name: string; stateKey: string }, - streamingContent: Record, - values: Record -): NodeStatus { - if (values?.[node.stateKey]) return "complete"; - if (streamingContent[node.name]) return "streaming"; - return "idle"; -} +type NodeStatus = SubgraphDiscoverySnapshot["status"]; + +const status: NodeStatus = node.status; ``` ## Building the pipeline progress bar @@ -248,29 +215,32 @@ entire pipeline. Each step is a labeled segment that fills in as nodes complete: ```tsx function PipelineProgress({ nodes, - values, - streamingContent, + isLoading, }: { - nodes: typeof PIPELINE_NODES; - values: Record; - streamingContent: Record; + nodes: SubgraphDiscoverySnapshot[]; + isLoading: boolean; }) { + const firstIncompleteIdx = nodes.findIndex((node) => node.status !== "complete"); + return (
{nodes.map((node, i) => { - const status = getNodeStatus(node, streamingContent, values); + const isRunning = + isLoading && node.status !== "complete" && firstIncompleteIdx === i; const colors = { - idle: "bg-gray-200 text-gray-500", - streaming: "bg-blue-400 text-white animate-pulse", + pending: "bg-gray-200 text-gray-500", + running: "bg-blue-400 text-white animate-pulse", complete: "bg-green-500 text-white", + error: "bg-red-500 text-white", }; + const status = isRunning ? "running" : node.status; return ( -
+
- {node.label} + {node.nodeName}
{i < nodes.length - 1 && (
{ + if (node.status === "running") setOpen(true); + if (node.status === "complete") setOpen(false); + }, [node.status]); return (
- {!collapsed && displayContent && ( + {open && (
- {displayContent} - {status === "streaming" && ( - - )} + {lastAIMessage?.text?.trim() + ? {lastAIMessage.text} + :

Processing...

}
)}
); } - -function formatContent(value: unknown): string { - if (typeof value === "string") return value; - if (value == null) return ""; - return JSON.stringify(value, null, 2); -} ``` ## Streaming vs. completed content -There are two sources of content for each node, and picking the right one -matters for a smooth UX: +The node card reads scoped messages for both streaming and final content. This +avoids assuming that a graph node name matches the state key it writes to (for +example, `do_research` writes to `research` in the playground graph): | Source | When to use | | --- | --- | -| `streamingContent[node.name]` | While the node is actively streaming, this contains tokens as they arrive | -| `stream.values[node.stateKey]` | After the node completes, this contains the final, committed output | +| `useMessages(stream, node)` | Render node-scoped streaming and final messages | +| `stream.values` | Read whole-graph state such as the final `synthesis` field, using the actual state key | + +The pattern is: show the most recent scoped AI message in the node card, and +use `stream.values` only when you intentionally need a graph state field. -The pattern is: show streaming content for live updates, fall back to the -committed state value once the node is done. +Because scoped messages are tied to the producing node, the UI can support +parallel graph paths without guessing from message order. Each card updates from +the stream events that belong to its node, and completed values remain available +through `stream.values`. ```ts -for (const node of PIPELINE_NODES) { - const status = getNodeStatus(node, streamingContent, stream.values); +function NodeContent({ stream, node }: { stream: AnyStream; node: SubgraphDiscoverySnapshot }) { + const messages = useMessages(stream, node); + const content = messages.find(AIMessage.isInstance)?.text ?? ""; - const content = - status === "streaming" - ? streamingContent[node.name] - : stream.values?.[node.stateKey]; + return {content}; } ``` @@ -398,30 +348,23 @@ rendering: ```tsx function NodeCardList({ nodes, - messages, - values, - getMetadata, + stream, + isLoading, }: { - nodes: typeof PIPELINE_NODES; - messages: BaseMessage[]; - values: Record; - getMetadata: (msg: BaseMessage) => MessageMetadata | undefined; + nodes: SubgraphDiscoverySnapshot[]; + stream: AnyStream; + isLoading: boolean; }) { - const streamingContent = getStreamingContent(messages, getMetadata); + const firstIncompleteIdx = nodes.findIndex((node) => node.status !== "complete"); return (
- {nodes.map((node) => { - const status = getNodeStatus(node, streamingContent, values); - return ( - - ); + {nodes.map((node, i) => { + const isComplete = node.status === "complete"; + const isRunning = isLoading && !isComplete && firstIncompleteIdx === i; + if (!isComplete && !isRunning) return null; + + return ; })}
); @@ -445,16 +388,11 @@ matters: ## Handling dynamic pipelines Not all graphs have a fixed set of nodes. Some pipelines add or skip nodes -based on the input. Handle this by checking which state keys actually have -values: +based on the input. The discovery map contains only nodes observed for the +current thread: ```ts -const activeNodes = PIPELINE_NODES.filter( - (node) => - streamingContent[node.name] || - values?.[node.stateKey] || - node.name === currentNode -); +const activeNodes = [...stream.subgraphs.values()]; ``` This ensures your UI only shows cards for nodes that are relevant to the @@ -462,17 +400,21 @@ current execution, avoiding empty placeholder cards. If your graph has conditional branching (e.g., skip "Research" for simple -factual queries), the skipped nodes will never appear in the streaming content -or state values. Your pipeline progress bar should reflect this by dimming or -hiding skipped steps. +factual queries), skipped nodes will not appear in `stream.subgraphs`. Your +pipeline progress bar can render discovered nodes only or dim expected nodes +that have no matching snapshot. ## Best practices -- **Define nodes declaratively**. Keep your `PIPELINE_NODES` array as a single - source of truth that maps node names, state keys, and display labels. -- **Prefer streaming content for active nodes**. It gives users immediate - feedback. Only fall back to committed state values after the node completes. +- **Discover nodes from the stream**. Render cards from `stream.subgraphs` + rather than hardcoding expected nodes; conditional or skipped steps won't + appear until they run. +- **Treat state keys as UI contracts**. Decide which graph outputs should be + stable enough for the frontend to render, and keep those keys documented next + to the graph definition. +- **Use scoped messages for node cards**. They work while a node is streaming + and after it completes, without coupling UI cards to state key names. - **Auto-collapse completed nodes**. In long pipelines, auto-collapse finished cards so users can focus on the currently active step. - **Show estimated timing**. If you have historical data on how long each node diff --git a/frontend/overview.md b/frontend/overview.md index 144013c..52825ab 100644 --- a/frontend/overview.md +++ b/frontend/overview.md @@ -3,11 +3,23 @@ title: Overview description: Render LangGraph agents to the frontend --- -Build frontends that visualize LangGraph pipelines in real time. These patterns show how to render multi-step graph execution with per-node status and streaming content from custom `StateGraph` workflows. +Build frontends that visualize LangGraph pipelines in real time. These patterns +show how to render multi-step graph execution with per-node status and streaming +content from custom `StateGraph` workflows. + +LangGraph's frontend advantage is that the UI can follow the same structure as +the graph. Nodes, state keys, checkpoints, interrupts, subgraphs, and streamed +messages are all visible runtime concepts, so you can build interfaces that +explain what the system is doing instead of hiding execution behind one +assistant message. + + +These patterns use the v1 frontend SDK packages. If you are using an earlier version, see the migration guides for [React](https://github.com/langchain-ai/langgraphjs/blob/main/libs/sdk-react/docs/v1-migration.md), [Vue](https://github.com/langchain-ai/langgraphjs/blob/main/libs/sdk-vue/docs/v1-migration.md), [Svelte](https://github.com/langchain-ai/langgraphjs/blob/main/libs/sdk-svelte/docs/v1-migration.md), and [Angular](https://github.com/langchain-ai/langgraphjs/blob/main/libs/sdk-angular/docs/v1-migration.md). + ## Architecture -LangGraph graphs are composed of named nodes connected by edges. Each node executes a step (classify, research, analyze, synthesize) and writes output to a specific state key. On the frontend, `useStream` provides reactive access to node outputs, streaming tokens, and graph metadata so you can map each node to a UI card. +LangGraph graphs are composed of named nodes connected by edges. Each node executes a step (classify, research, analyze, synthesize) and writes output to a specific state key. On the frontend, the SDK stream handle provides reactive access to node outputs, streaming tokens, and discovered subgraphs so you can map each node to a UI card. ```mermaid %%{ @@ -48,15 +60,18 @@ class State(MessagesState): classification: str research: str analysis: str + synthesis: str graph = StateGraph(State) graph.add_node("classify", classify_node) -graph.add_node("research", research_node) +graph.add_node("do_research", research_node) graph.add_node("analyze", analyze_node) +graph.add_node("synthesize", synthesize_node) graph.add_edge(START, "classify") -graph.add_edge("classify", "research") -graph.add_edge("research", "analyze") -graph.add_edge("analyze", END) +graph.add_edge("classify", "do_research") +graph.add_edge("do_research", "analyze") +graph.add_edge("analyze", "synthesize") +graph.add_edge("synthesize", END) app = graph.compile() ``` @@ -66,30 +81,36 @@ app = graph.compile() :::js ```ts -import { StateGraph, StateSchema, MessagesValue, START, END } from "@langchain/langgraph"; -import * as z from "zod"; - -const State = new StateSchema({ - messages: MessagesValue, - classification: z.string(), - research: z.string(), - analysis: z.string(), +import { Annotation, MessagesAnnotation, StateGraph, START, END } from "@langchain/langgraph"; + +const State = Annotation.Root({ + ...MessagesAnnotation.spec, + classification: Annotation(), + research: Annotation(), + analysis: Annotation(), + synthesis: Annotation(), }); const graph = new StateGraph(State) .addNode("classify", classifyNode) - .addNode("research", researchNode) + .addNode("do_research", researchNode) .addNode("analyze", analyzeNode) + .addNode("synthesize", synthesizeNode) .addEdge(START, "classify") - .addEdge("classify", "research") - .addEdge("research", "analyze") - .addEdge("analyze", END) + .addEdge("classify", "do_research") + .addEdge("do_research", "analyze") + .addEdge("analyze", "synthesize") + .addEdge("synthesize", END) .compile(); ``` ::: -On the frontend, `useStream` exposes `stream.values` for completed node outputs and `getMessagesMetadata` for identifying which node produced each streaming token. +On the frontend, @[`useStream`] exposes `stream.subgraphs` for graph-node discovery +and selector helpers such as `useMessages(stream, node)` for node-scoped +streaming content. `stream.values` still holds the full graph state when you +need fields such as the final `synthesis`. Angular uses the same stream API +shape through @[`injectStream`]. ```ts import { useStream } from "@langchain/react"; @@ -103,17 +124,39 @@ function Pipeline() { const classification = stream.values?.classification; const research = stream.values?.research; const analysis = stream.values?.analysis; + const graphNodes = [...stream.subgraphs.values()]; } ``` +## What makes this different from a chat stream + +Custom graphs often power product workflows: research pipelines, approval flows, +data pipelines, data enrichment, code review, planning, and multi-step analysis. The +frontend SDK lets you render these workflows using graph-native signals: + +| Runtime concept | Frontend UX | +| --- | --- | +| **Named nodes** | One card, timeline step, or status badge per graph node. | +| **State keys** | Dedicated UI regions for typed outputs such as classification, sources, analysis, and final synthesis. | +| **Streaming metadata** | Route partial messages to the node that produced them. | +| **Checkpoints** | Inspect or resume from prior graph states for debugging and auditability. | +| **Interrupts** | Pause a node for human input, approval, or correction, then continue. | +| **Subgraphs** | Reveal nested execution only when the user needs more detail. | + +Because the SDK exposes these concepts directly, you can scale from a simple +chat panel to a full workflow debugger without changing the backend protocol. + ## Patterns Visualize multi-step graph pipelines with per-node status and streaming content. + + Stream custom server-side data to the frontend and read it with `useExtension` and `useChannel`. + ## Related patterns -The [LangChain frontend patterns](/oss/langchain/frontend/overview)—markdown messages, tool calling, optimistic updates, and more—work with any LangGraph graph. The `useStream` hook provides the same core API whether you use `createAgent`, `createDeepAgent`, or a custom `StateGraph`. +The [LangChain frontend patterns](/oss/langchain/frontend/overview)—markdown messages, tool calling, human-in-the-loop, resumable streams, and time travel—work with any LangGraph graph. The stream API provides the same core data model whether you use `createAgent`, `createDeepAgent`, or a custom `StateGraph`. diff --git a/functional-api.mdx b/functional-api.mdx index db0006a..9cb2faa 100644 --- a/functional-api.mdx +++ b/functional-api.mdx @@ -3,7 +3,10 @@ title: Functional API overview sidebarTitle: Functional API --- - +import LanggraphFunctionalApiInterruptStreamPy from '/snippets/code-samples/langgraph-functional-api-interrupt-stream-py.mdx'; +import LanggraphFunctionalApiInterruptResumePy from '/snippets/code-samples/langgraph-functional-api-interrupt-resume-py.mdx'; +import LanggraphFunctionalApiInterruptStreamJs from '/snippets/code-samples/langgraph-functional-api-interrupt-stream-js.mdx'; +import LanggraphFunctionalApiInterruptResumeJs from '/snippets/code-samples/langgraph-functional-api-interrupt-resume-js.mdx'; The **Functional API** allows you to add LangGraph's key features ([persistence](/oss/langgraph/persistence), [memory](/oss/langgraph/add-memory), [human-in-the-loop](/oss/langgraph/interrupts), and [streaming](/oss/langgraph/streaming)) to your applications with minimal changes to your existing code. @@ -114,157 +117,21 @@ const workflow = entrypoint( When the workflow is resumed, it executes from the very start, but because the result of the `writeEssay` task was already saved, the task result will be loaded from the checkpoint instead of being recomputed. :::python - ```python - import time - from langchain_core.utils.uuid import uuid7 - from langgraph.func import entrypoint, task - from langgraph.types import interrupt - from langgraph.checkpoint.memory import InMemorySaver - - - @task - def write_essay(topic: str) -> str: - """Write an essay about the given topic.""" - time.sleep(1) # This is a placeholder for a long-running task. - return f"An essay about topic: {topic}" - - @entrypoint(checkpointer=InMemorySaver()) - def workflow(topic: str) -> dict: - """A simple workflow that writes an essay and asks for a review.""" - essay = write_essay("cat").result() - is_approved = interrupt( - { - # Any json-serializable payload provided to interrupt as argument. - # It will be surfaced on the client side as an Interrupt when streaming data - # from the workflow. - "essay": essay, # The essay we want reviewed. - # We can add any additional information that we need. - # For example, introduce a key called "action" with some instructions. - "action": "Please approve/reject the essay", - } - ) - return { - "essay": essay, # The essay that was generated - "is_approved": is_approved, # Response from HIL - } - - - thread_id = str(uuid7()) - config = {"configurable": {"thread_id": thread_id}} - for item in workflow.stream("cat", config): - print(item) - # > {'write_essay': 'An essay about topic: cat'} - # > { - # > '__interrupt__': ( - # > Interrupt( - # > value={ - # > 'essay': 'An essay about topic: cat', - # > 'action': 'Please approve/reject the essay' - # > }, - # > id='b9b2b9d788f482663ced6dc755c9e981' - # > ), - # > ) - # > } - ``` + An essay has been written and is ready for review. Once the review is provided, we can resume the workflow: - ```python - from langgraph.types import Command - - # Get review from a user (e.g., via a UI) - # In this case, we're using a bool, but this can be any json-serializable value. - human_review = True - - for item in workflow.stream(Command(resume=human_review), config): - print(item) - ``` - - ```pycon - {'workflow': {'essay': 'An essay about topic: cat', 'is_approved': False}} - ``` + The workflow has been completed and the review has been added to the essay. ::: :::js - ```typescript - import { v7 as uuid7 } from "uuid"; - import { MemorySaver, entrypoint, task, interrupt } from "@langchain/langgraph"; - - const writeEssay = task("writeEssay", async (topic: string) => { - // This is a placeholder for a long-running task. - await new Promise(resolve => setTimeout(resolve, 1000)); - return `An essay about topic: ${topic}`; - }); - - const workflow = entrypoint( - { checkpointer: new MemorySaver(), name: "workflow" }, - async (topic: string) => { - const essay = await writeEssay(topic); - const isApproved = interrupt({ - // Any json-serializable payload provided to interrupt as argument. - // It will be surfaced on the client side as an Interrupt when streaming data - // from the workflow. - essay, // The essay we want reviewed. - // We can add any additional information that we need. - // For example, introduce a key called "action" with some instructions. - action: "Please approve/reject the essay", - }); - - return { - essay, // The essay that was generated - isApproved, // Response from HIL - }; - } - ); - - const threadId = uuid7(); - - const config = { - configurable: { - thread_id: threadId - } - }; - - for await (const item of workflow.stream("cat", config)) { - console.log(item); - } - ``` - - ```console - { writeEssay: 'An essay about topic: cat' } - { - __interrupt__: [{ - value: { essay: 'An essay about topic: cat', action: 'Please approve/reject the essay' }, - resumable: true, - ns: ['workflow:f7b8508b-21c0-8b4c-5958-4e8de74d2684'], - when: 'during' - }] - } - ``` + An essay has been written and is ready for review. Once the review is provided, we can resume the workflow: - ```typescript - import { Command } from "@langchain/langgraph"; - - // Get review from a user (e.g., via a UI) - // In this case, we're using a bool, but this can be any json-serializable value. - const humanReview = true; - - const stream = await workflow.stream( - new Command({ resume: humanReview }), - config - ); - for await (const item of stream) { - console.log(item); - } - ``` - - ```console - { workflow: { essay: 'An essay about topic: cat', isApproved: true } } - ``` + The workflow has been completed and the review has been added to the essay. ::: @@ -425,8 +292,10 @@ Using the [`@entrypoint`](#entrypoint) yields a @[`Pregel`][Pregel.stream] objec } } - for chunk in my_workflow.stream(some_input, config): - print(chunk) + stream = my_workflow.stream_events(some_input, config, version="v3") + for message in stream.messages: + for token in message.text: + print(token, end="", flush=True) ``` @@ -437,8 +306,10 @@ Using the [`@entrypoint`](#entrypoint) yields a @[`Pregel`][Pregel.stream] objec } } - async for chunk in my_workflow.astream(some_input, config): - print(chunk) + stream = await my_workflow.astream_events(some_input, config, version="v3") + async for message in stream.messages: + async for token in message.text: + print(token, end="", flush=True) ``` @@ -466,8 +337,11 @@ Using the [`entrypoint`](#entrypoint) function will return an object that can be } }; - for await (const chunk of myWorkflow.stream(someInput, config)) { - console.log(chunk); + const stream = await myWorkflow.streamEvents(someInput, config, { version: "v3" }); + for await (const message of stream.messages) { + for await (const token of message.text) { + process.stdout.write(token); + } } ``` @@ -516,8 +390,10 @@ Resuming an execution after an @[interrupt][interrupt] can be done by passing a } } - for chunk in my_workflow.stream(Command(resume=some_resume_value), config): - print(chunk) + stream = my_workflow.stream_events(Command(resume=some_resume_value), config, version="v3") + for message in stream.messages: + for token in message.text: + print(token, end="", flush=True) ``` @@ -530,8 +406,10 @@ Resuming an execution after an @[interrupt][interrupt] can be done by passing a } } - async for chunk in my_workflow.astream(Command(resume=some_resume_value), config): - print(chunk) + stream = await my_workflow.astream_events(Command(resume=some_resume_value), config, version="v3") + async for message in stream.messages: + async for token in message.text: + print(token, end="", flush=True) ``` @@ -564,13 +442,16 @@ Resuming an execution after an @[interrupt][interrupt] can be done by passing a } }; - const stream = await myWorkflow.stream( + const stream = await myWorkflow.streamEvents( new Command({ resume: someResumableValue }), config, - ) + { version: "v3" }, + ); - for await (const chunk of stream) { - console.log(chunk); + for await (const message of stream.messages) { + for await (const token of message.text) { + process.stdout.write(token); + } } ``` @@ -618,8 +499,10 @@ This assumes that the underlying **error** has been resolved and execution can p } } - for chunk in my_workflow.stream(None, config): - print(chunk) + stream = my_workflow.stream_events(None, config, version="v3") + for message in stream.messages: + for token in message.text: + print(token, end="", flush=True) ``` @@ -631,8 +514,10 @@ This assumes that the underlying **error** has been resolved and execution can p } } - async for chunk in my_workflow.astream(None, config): - print(chunk) + stream = await my_workflow.astream_events(None, config, version="v3") + async for message in stream.messages: + async for token in message.text: + print(token, end="", flush=True) ``` @@ -665,8 +550,11 @@ This assumes that the underlying **error** has been resolved and execution can p } }; - for await (const chunk of myWorkflow.stream(null, config)) { - console.log(chunk); + const stream = await myWorkflow.streamEvents(null, config, { version: "v3" }); + for await (const message of stream.messages) { + for await (const token of message.text) { + process.stdout.write(token); + } } ``` @@ -675,7 +563,7 @@ This assumes that the underlying **error** has been resolved and execution can p ### Short-term memory -When an `entrypoint` is defined with a `checkpointer`, it stores information between successive invocations on the same **thread id** in [checkpoints](/oss/langgraph/persistence#checkpoints). +When an `entrypoint` is defined with a `checkpointer`, it stores information between successive invocations on the same **thread id** in [checkpoints](/oss/langgraph/checkpointers#checkpoints). :::python This allows accessing the state from the previous invocation using the `previous` parameter. @@ -877,7 +765,7 @@ const myWorkflow = entrypoint( * **Checkpointing**: When you need to save the result of a long-running operation to a checkpoint, so you don't need to recompute it when resuming the workflow. * **Human-in-the-loop**: If you're building a workflow that requires human intervention, you MUST use **tasks** to encapsulate any randomness (e.g., API calls) to ensure that the workflow can be resumed correctly. See the [determinism](#determinism) section for more details. * **Parallel Execution**: For I/O-bound tasks, **tasks** enable parallel execution, allowing multiple operations to run concurrently without blocking (e.g., calling multiple APIs). -* **Observability**: Wrapping operations in **tasks** provides a way to track the progress of the workflow and monitor the execution of individual operations using [LangSmith](/langsmith/home). +* **Observability**: Wrapping operations in **tasks** provides a way to track the progress of the workflow and monitor the execution of individual operations using [LangSmith](/langsmith/observability). * **Retryable Work**: When work needs to be retried to handle failures or inconsistencies, **tasks** provide a way to encapsulate and manage the retry logic. ## Serialization @@ -901,15 +789,25 @@ Providing non-serializable inputs or outputs will result in a runtime error when ## Determinism -To utilize features like **human-in-the-loop**, any randomness should be encapsulated inside of **tasks**. This guarantees that when execution is halted (e.g., for human in the loop) and then resumed, it will follow the same _sequence of steps_, even if **task** results are non-deterministic. +When you resume a workflow run, the code does **NOT** resume from the **same line of code** where execution stopped. Execution returns to a checkpoint boundary, and the workflow **replays** forward until it reaches the pause again. + +For the Functional API, replay starts at the beginning of the **entrypoint** while LangGraph restores completed [**task**](/oss/langgraph/functional-api#task) and [**subgraph**](/oss/langgraph/use-subgraphs) results from the checkpointer instead of recomputing them. That preserves the recorded order of steps across pauses, including for long-running or non-deterministic **task** outputs. + +To use features like **human-in-the-loop**, you must place non-deterministic work (for example, random values) and side effects (for example, file writes or API calls) in [**tasks**](/oss/langgraph/functional-api#task). + +Different runs of a workflow can produce different results, but resuming a **specific** thread should replay the same persisted **task** and **subgraph** results. -LangGraph achieves this behavior by persisting **task** and [**subgraph**](/oss/langgraph/use-subgraphs) results as they execute. A well-designed workflow ensures that resuming execution follows the _same sequence of steps_, allowing previously computed results to be retrieved correctly without having to re-execute them. This is particularly useful for long-running **tasks** or **tasks** with non-deterministic results, as it avoids repeating previously done work and allows resuming from essentially the same. +To ensure that your workflow is deterministic and can be consistently replayed, follow these guidelines: -While different runs of a workflow can produce different results, resuming a **specific** run should always follow the same sequence of recorded steps. This allows LangGraph to efficiently look up **task** and **subgraph** results that were executed prior to the graph being interrupted and avoid recomputing them. +* **Avoid repeating work**: In an **entrypoint**, if you chain several side effects (for example, logging, file writes, or network calls), give each its own **task** so resume restores their outputs from the checkpointer instead of running them again. +* **Encapsulate non-deterministic operations**: Keep values that can change between attempts (for example, random numbers or wall-clock reads) inside **tasks**, so replay lines up with what was checkpointed. +* **Use idempotent operations**: For partial task failures and retries, see [Idempotency](#idempotency). ## Idempotency -Idempotency ensures that running the same operation multiple times produces the same result. This helps prevent duplicate API calls and redundant processing if a step is rerun due to a failure. Always place API calls inside **tasks** functions for checkpointing, and design them to be idempotent in case of re-execution. Re-execution can occur if a **task** starts, but does not complete successfully. Then, if the workflow is resumed, the **task** will run again. Use idempotency keys or verify existing results to avoid duplication. +Idempotency ensures that running the same operation multiple times produces the same result. This helps prevent duplicate API calls and redundant processing if a step is rerun due to a failure. Always place API calls inside **tasks** functions for checkpointing, and design them to be idempotent in case of re-execution. +This is particularly important for operations that result in data writes. +When a workflow resumes, LangGraph replays completed **task** results from the checkpoint. A **task** that started but did not finish may run again on that resume, so design side effects to be idempotent. Use idempotency keys or verify existing results to avoid unintended duplication. ## Common pitfalls diff --git a/graph-api.mdx b/graph-api.mdx index b3a6d2b..3646daa 100644 --- a/graph-api.mdx +++ b/graph-api.mdx @@ -3,7 +3,15 @@ title: Graph API overview sidebarTitle: Graph API --- - +import GraphApiUsingTasksOriginalJs from '/snippets/code-samples/graph-api-using-tasks-original-js.mdx'; +import GraphApiUsingTasksOriginalPy from '/snippets/code-samples/graph-api-using-tasks-original-py.mdx'; +import GraphApiUsingTasksTaskJs from '/snippets/code-samples/graph-api-using-tasks-task-js.mdx'; +import GraphApiUsingTasksTaskPy from '/snippets/code-samples/graph-api-using-tasks-task-py.mdx'; +import LanggraphGraphApiMultipleSchemasJs from '/snippets/code-samples/langgraph-graph-api-multiple-schemas-js.mdx'; +import LanggraphGraphApiMultipleSchemasPy from '/snippets/code-samples/langgraph-graph-api-multiple-schemas-py.mdx'; +import LanggraphGraphApiResumeV2Py from '/snippets/code-samples/langgraph-graph-api-resume-v2-py.mdx'; +import LanggraphGraphApiStreamPrivateChannelJs from '/snippets/code-samples/langgraph-graph-api-stream-private-channel-js.mdx'; +import LanggraphGraphApiStreamPrivateChannelPy from '/snippets/code-samples/langgraph-graph-api-stream-private-channel-py.mdx'; ## Graphs @@ -149,103 +157,13 @@ Let's look at an example: :::python -```python -class InputState(TypedDict): - user_input: str - -class OutputState(TypedDict): - graph_output: str - -class OverallState(TypedDict): - foo: str - user_input: str - graph_output: str - -class PrivateState(TypedDict): - bar: str - -def node_1(state: InputState) -> OverallState: - # Write to OverallState - return {"foo": state["user_input"] + " name"} - -def node_2(state: OverallState) -> PrivateState: - # Read from OverallState, write to PrivateState - return {"bar": state["foo"] + " is"} - -def node_3(state: PrivateState) -> OutputState: - # Read from PrivateState, write to OutputState - return {"graph_output": state["bar"] + " Lance"} - -builder = StateGraph(OverallState,input_schema=InputState,output_schema=OutputState) -builder.add_node("node_1", node_1) -builder.add_node("node_2", node_2) -builder.add_node("node_3", node_3) -builder.add_edge(START, "node_1") -builder.add_edge("node_1", "node_2") -builder.add_edge("node_2", "node_3") -builder.add_edge("node_3", END) - -graph = builder.compile() -graph.invoke({"user_input":"My"}) -# {'graph_output': 'My name is Lance'} -``` + ::: :::js -```typescript -import { StateSchema, GraphNode } from "@langchain/langgraph"; -import * as z from "zod"; - -const InputState = new StateSchema({ - userInput: z.string(), -}); - -const OutputState = new StateSchema({ - graphOutput: z.string(), -}); - -const OverallState = new StateSchema({ - foo: z.string(), - userInput: z.string(), - graphOutput: z.string(), -}); - -const PrivateState = new StateSchema({ - bar: z.string(), -}); - -const graph = new StateGraph({ - state: OverallState, - input: InputState, - output: OutputState, -}) - .addNode("node1", (state) => { - // Write to OverallState - return { foo: state.userInput + " name" }; - }) - .addNode("node2", (state) => { - // Read from OverallState, write to PrivateState - return { bar: state.foo + " is" }; - }) - .addNode( - "node3", - (state) => { - // Read from PrivateState, write to OutputState - return { graphOutput: state.bar + " Lance" }; - }, - { input: PrivateState } - ) - .addEdge(START, "node1") - .addEdge("node1", "node2") - .addEdge("node2", "node3") - .addEdge("node3", END) - .compile(); - -await graph.invoke({ userInput: "My" }); -// { graphOutput: 'My name is Lance' } -``` + ::: @@ -277,6 +195,60 @@ There are two subtle and important points to note here: 2. We initialize the graph with `StateGraph({ state: OverallState, input: InputState, output: OutputState })`. How can we write to `PrivateState` in `node2`? How does the graph gain access to this schema if it was not passed in the `StateGraph` initialization? We can do this because _nodes can also declare additional state channels_ as long as the state schema definition exists. In this case, the `PrivateState` schema is defined, so we can add `bar` as a new state channel in the graph and write to it. ::: +:::python + +**Private channels are not redacted when streaming.** + +Input, output, and private schemas constrain what each node _reads_ (its input schema) and what `invoke` _returns_ (the output schema). They do **not** hide channels from `stream`. + +When you stream with `stream_mode="values"`, the graph emits **all** of its state channels by default, including private ones, because values streaming defaults to the full set of state channels rather than the output schema. This is why a private channel like `bar` is hidden by `invoke` but visible while streaming: + + + +To restrict the streamed values to a specific set of channels (e.g. only the output schema), pass `output_keys`: + +```python +stream = graph.stream_events( + {"user_input": "My"}, + version="v3", + output_keys=["graph_output"], # [!code highlight] +) +for snapshot in stream.values: + print(snapshot) +# {'graph_output': 'My name is Lance'} +``` + +If you only need the channels a node actually produced each step (rather than the full accumulated state), use `stream_mode="updates"` instead. + +::: + +:::js + +**Private channels are not redacted when streaming.** + +Input, output, and private schemas constrain what each node _reads_ (its input schema) and what `invoke` _returns_ (the output schema). They do **not** hide channels from `stream`. + +When you stream with `streamMode: "values"`, the graph emits **all** of its state channels by default — including private ones — because values streaming defaults to the full set of state channels rather than the output schema. This is why a private channel like `bar` is hidden by `invoke` but visible while streaming: + + + +To restrict the streamed values to a specific set of channels (e.g. only the output schema), pass `outputKeys`: + +```typescript +const stream = await graph.streamEvents( + { userInput: "My" }, + { version: "v3", outputKeys: ["graphOutput"] } // [!code highlight] +); +for await (const snapshot of stream.values) { + console.log(snapshot); +} +// { graphOutput: 'My name is Lance' } +``` + +If you only need the channels a node actually produced each step (rather than the full accumulated state), use `streamMode: "updates"` instead. + +::: + ### Reducers Reducers are key to understanding how updates from nodes are applied to the `State`. Each key in the `State` has its own independent reducer function. If no reducer function is explicitly specified then it is assumed that all updates to that key should override it. There are a few different types of reducers, starting with the default type of reducer: @@ -627,7 +599,7 @@ In LangGraph, nodes are Python functions (either synchronous or asynchronous) th 1. `state`—The [state](#state) of the graph 2. `config`—A @[`RunnableConfig`] object that contains configuration information like `thread_id` and tracing information like `tags` -3. `runtime`—A `Runtime` object that contains [runtime `context`](#runtime-context) and other information like `store`, `stream_writer`, `execution_info`, and `server_info` +3. `runtime`—A `Runtime` object that contains [runtime `context`](#runtime-context) and other information like `store`, `stream_writer`, `execution_info`, `server_info`, `heartbeat` (for idle timeout refresh), and `control` (for [graceful shutdown](/oss/langgraph/fault-tolerance#graceful-shutdown)) Similar to `NetworkX`, you add these nodes to a graph using the @[`add_node`] method: @@ -705,7 +677,7 @@ const builder = new StateGraph(State) ::: -Behind the scenes, functions are converted to @[`RunnableLambda`], which add batch and async support to your function, along with [native tracing and debugging](/langsmith/home). +Behind the scenes, functions are converted to @[`RunnableLambda`], which add batch and async support to your function, along with [native tracing and debugging](/langsmith/observability). If you add a node to a graph without specifying a name, it will be given a default name equivalent to the function name. @@ -727,6 +699,58 @@ builder.addNode(myNode); ::: +### Re-execution and idempotency + +:::python + +When you compile with a [checkpointer](/oss/langgraph/persistence), LangGraph saves checkpoints at [super-step](#graphs) boundaries, not mid-function inside a node. If execution stops and later resumes (for example after an [interrupt](/oss/langgraph/interrupts) or a [retry](/oss/langgraph/fault-tolerance#retries)), the affected **node** runs again from the start of its function. Code and side effects before the pause run again. + +**Idempotency.** Design **node** logic so re-execution does not corrupt state. If a node inserts a database row, running it twice should not create duplicate rows unless that is intentional. Use idempotency keys, upserts, or read-before-write checks. For effects around `interrupt()`, see [Side effects called before `interrupt` must be idempotent](/oss/langgraph/interrupts#side-effects-called-before-interrupt-must-be-idempotent). + +**Graph changes.** [Determinism](/oss/langgraph/functional-api#determinism) rules about code changes do not apply to graph structure. You can add or remove **nodes** and edges without breaking resume for existing threads. Resumed runs use saved state and execute whatever graph you compile now. + +**Tasks and interrupts inside a node.** If a **node** calls [**tasks**](/oss/langgraph/functional-api#task) or @[`interrupt`], stricter determinism rules apply on resume. LangGraph restores completed **task** results from the checkpointer, but changing **task** or @[`interrupt`] order in code before the resume point can mismatch cached values. A [Functional API](/oss/langgraph/functional-api) **entrypoint** compiles to a single **node** that runs the whole entrypoint method this way. See [Determinism](/oss/langgraph/functional-api#determinism), [Idempotency](/oss/langgraph/functional-api#idempotency), and [Using tasks in nodes](#using-tasks-in-nodes). + +::: + +:::js + +When you compile with a [checkpointer](/oss/langgraph/persistence), LangGraph saves checkpoints at [super-step](#graphs) boundaries, not mid-function inside a node. If execution stops and later resumes (for example after an [interrupt](/oss/langgraph/interrupts) or a retry), the affected **node** runs again from the start of its function. Code and side effects before the pause run again. + +**Idempotency.** Design **node** logic so re-execution does not corrupt state. If a node inserts a database row, running it twice should not create duplicate rows unless that is intentional. Use idempotency keys, upserts, or read-before-write checks. For effects around `interrupt()`, see [Side effects called before `interrupt` must be idempotent](/oss/langgraph/interrupts#side-effects-called-before-interrupt-must-be-idempotent). + +**Graph changes.** [Determinism](/oss/langgraph/functional-api#determinism) rules about code changes do not apply to graph structure. You can add or remove **nodes** and edges without breaking resume for existing threads. Resumed runs use saved state and execute whatever graph you compile now. + +**Tasks and interrupts inside a node.** If a **node** calls [**tasks**](/oss/langgraph/functional-api#task) or @[`interrupt`], stricter determinism rules apply on resume. LangGraph restores completed **task** results from the checkpointer, but changing **task** or @[`interrupt`] order in code before the resume point can mismatch cached values. A [Functional API](/oss/langgraph/functional-api) **entrypoint** compiles to a single **node** that runs the whole entrypoint method this way. See [Determinism](/oss/langgraph/functional-api#determinism), [Idempotency](/oss/langgraph/functional-api#idempotency), and [Using tasks in nodes](#using-tasks-in-nodes). + +::: + +### Using tasks in nodes + +If a [node](#nodes) contains multiple operations, you may find it easier to implement each operation as a [**task**](/oss/langgraph/functional-api#task) instead of splitting the logic across multiple nodes. Task results are checkpointed when the graph uses a checkpointer, so resuming a thread can skip completed **task** work inside the node. + +:::python + + + + + + + + +::: + +:::js + + + + + + + + +::: + ### `START` node The @[`START`] Node is a special node that represents the node that sends user input to the graph. The main purpose for referencing this node is to determine which nodes should be called first. @@ -821,6 +845,17 @@ print(graph.invoke({"x": 5}, stream_mode='updates')) # [!code highlight] # [{'expensive_node': {'result': 10}, '__metadata__': {'cached': True}}] ``` + +`set_entry_point(node)` defines the first node the graph will execute. +It is equivalent to `builder.add_edge(START, node)`. + +`set_finish_point(node)` defines the last node in the graph. +It is equivalent to `builder.add_edge(node, END)`. + +Both methods are valid but `add_edge(START, ...)` and `add_edge(..., END)` +are the recommended modern syntax. + + 1. First run takes two seconds to run (due to mocked expensive computation). 2. Second run utilizes cache and returns quickly. ::: @@ -873,6 +908,10 @@ Edges define how the logic is routed and how the graph decides to stop. This is A node can have multiple outgoing edges. If a node has multiple outgoing edges, **all** of those destination nodes will be executed in parallel as a part of the next superstep. + +For each node, choose one routing mechanism: use normal edges for static routing, or use conditional edges / @[`Command`] for dynamic routing. Do not mix normal edges and dynamic routing from the same node, because both paths can execute and make graph behavior harder to reason about. + + ### Normal edges :::python @@ -1013,6 +1052,8 @@ By default, `Nodes` and `Edges` are defined ahead of time and operate on the sam To support this design pattern, LangGraph supports returning @[`Send`] objects from conditional edges. `Send` takes two arguments: first is the name of the node, and second is the state to pass to that node. ```python +from langgraph.types import Send + def continue_to_jokes(state: OverallState): return [Send("generate_joke", {"subject": s}) for s in state['subjects']] @@ -1131,7 +1172,7 @@ builder.addNode("myNode", myNode, { -@[`Command`] only adds dynamic edges—static edges defined with `add_edge` / `addEdge` still execute. For example, if `node_a` returns `Command(goto="my_other_node")` and you also have `graph.add_edge("node_a", "node_b")`, both `node_b` and `my_other_node` will run. +@[`Command`] only adds dynamic edges—static edges defined with `add_edge` / `addEdge` still execute. For example, if `node_a` returns `Command(goto="my_other_node")` and you also have `graph.add_edge("node_a", "node_b")`, both `node_b` and `my_other_node` will run. For each node, use either @[`Command`] or static edges to route to the next nodes, not both. @@ -1194,7 +1235,7 @@ This is particularly useful when implementing [multi-agent handoffs](/oss/langch -`Command(resume=...)` is the **only** `Command` pattern intended as input to `invoke()`/`stream()`. Do not use `Command(update=...)` as input to continue multi-turn conversations—because passing any `Command` as input resumes from the latest checkpoint (i.e. the last step that ran, not `__start__`), the graph will appear stuck if it already finished. To continue a conversation on an existing thread, pass a plain input dict: +`Command(resume=...)` is the **only** `Command` pattern intended as input to `invoke()`/`stream()` (optionally combined with `update=...` to also apply a state change while resuming). Do not use `Command(update=...)` alone as input to continue multi-turn conversations—because passing any `Command` as input resumes from the latest checkpoint (i.e. the last step that ran, not `__start__`), the graph will appear stuck if it already finished. To continue a conversation on an existing thread, pass a plain input dict: ```python # WRONG - graph resumes from the latest checkpoint @@ -1217,7 +1258,7 @@ graph.invoke( { # [!code ++] -`new Command({ resume: ... })` is the **only** `Command` pattern intended as input to `invoke()`/`stream()`. Do not use `new Command({ update: ... })` as input to continue multi-turn conversations—because passing any `Command` as input resumes from the latest checkpoint (i.e. the last step that ran, not `__start__`), the graph will appear stuck if it already finished. To continue a conversation on an existing thread, pass a plain input object: +`new Command({ resume: ... })` is the **only** `Command` pattern intended as input to `invoke()`/`stream()` (optionally combined with `update` to also apply a state change while resuming). Do not use `new Command({ update: ... })` alone as input to continue multi-turn conversations—because passing any `Command` as input resumes from the latest checkpoint (i.e. the last step that ran, not `__start__`), the graph will appear stuck if it already finished. To continue a conversation on an existing thread, pass a plain input object: ```typescript // WRONG - graph resumes from the latest checkpoint @@ -1238,20 +1279,7 @@ await graph.invoke({ messages: [{ role: "user", content: "follow up" }] }, confi Use `Command(resume=...)` to provide a value and resume graph execution after an [interrupt](/oss/langgraph/interrupts). The value passed to `resume` becomes the return value of the `interrupt()` call inside the paused node: -```python -from langgraph.types import Command, interrupt - -def human_review(state: State): - # Pauses the graph and waits for a value - answer = interrupt("Do you approve?") - return {"messages": [{"role": "user", "content": answer}]} - -# First invocation - hits the interrupt and pauses -result = graph.invoke({"messages": [...]}, config) - -# Resume with a value - the interrupt() call returns "yes" -result = graph.invoke(Command(resume="yes"), config) -``` + Check out the [interrupts conceptual guide](/oss/langgraph/interrupts) for full details on interrupt patterns, including multiple interrupts and validation loops. @@ -1287,7 +1315,7 @@ You can return @[`Command`] from tools to update graph state and control flow. U -When used inside tools, `goto` adds a dynamic edge—any static edges already defined on the node that called the tool will still execute. +When used inside tools, `goto` adds a dynamic edge—any static edges already defined on the node that called the tool will still execute. For each node, use either tool-driven dynamic routing or static edges to route to the next nodes, not both. @@ -1436,13 +1464,13 @@ The current step counter is accessible in `config.metadata.langgraph_step` withi :::python -The step counter is stored in `config["metadata"]["langgraph_step"]`. The recursion limit check follows the logic: `step > stop` where `stop = step + recursion_limit + 1`. When the limit is exceeded, LangGraph raises a `GraphRecursionError`. +The step counter is stored in `config["metadata"]["langgraph_step"]`. LangGraph increments this counter as the graph executes and raises a `GraphRecursionError` once the configured `recursion_limit` is exceeded. ::: :::js -The step counter is stored in `config.metadata.langgraph_step`. The recursion limit check follows the logic: `step > stop` where `stop = step + recursionLimit + 1`. When the limit is exceeded, LangGraph raises a `GraphRecursionError`. +The step counter is stored in `config.metadata.langgraph_step`. LangGraph increments this counter as the graph executes and raises a `GraphRecursionError` once the configured `recursionLimit` is exceeded. ::: @@ -1785,7 +1813,7 @@ It's often nice to be able to visualize graphs, especially as they get more comp ## Observability and Tracing -To trace, debug and evaluate your agents, use [LangSmith](/langsmith/home). +To trace, debug and evaluate your agents, use [LangSmith](/langsmith/observability). ## Learn more diff --git a/install.mdx b/install.mdx index aa321a6..dbb0879 100644 --- a/install.mdx +++ b/install.mdx @@ -4,7 +4,6 @@ sidebarTitle: Install --- - To install the base LangGraph package: :::python diff --git a/interrupts.mdx b/interrupts.mdx index a0b49eb..9bf442a 100644 --- a/interrupts.mdx +++ b/interrupts.mdx @@ -2,6 +2,18 @@ title: Interrupts --- +import LanggraphInterruptsResumeV2Py from '/snippets/code-samples/langgraph-interrupts-resume-v2-py.mdx'; +import LanggraphInterruptsMultiplePy from '/snippets/code-samples/langgraph-interrupts-multiple-py.mdx'; +import LanggraphInterruptsHitlStreamPy from '/snippets/code-samples/langgraph-interrupts-hitl-stream-py.mdx'; +import LanggraphInterruptsHitlStreamJs from '/snippets/code-samples/langgraph-interrupts-hitl-stream-js.mdx'; +import LanggraphInterruptsApprovalPy from '/snippets/code-samples/langgraph-interrupts-approval-py.mdx'; +import LanggraphInterruptsReviewPy from '/snippets/code-samples/langgraph-interrupts-review-py.mdx'; +import LanggraphInterruptsValidatePy from '/snippets/code-samples/langgraph-interrupts-validate-py.mdx'; +import LanggraphInterruptsValidateConditionalEdgePatternPy from '/snippets/code-samples/langgraph-interrupts-validate-conditional-edge-pattern-py.mdx'; +import LanggraphInterruptsValidateConditionalEdgePatternJs from '/snippets/code-samples/langgraph-interrupts-validate-conditional-edge-pattern-js.mdx'; +import LanggraphInterruptsValidateConditionalEdgeJs from '/snippets/code-samples/langgraph-interrupts-validate-conditional-edge-js.mdx'; +import LanggraphInterruptsValidateConditionalEdgePy from '/snippets/code-samples/langgraph-interrupts-validate-conditional-edge-py.mdx'; + Interrupts allow you to pause graph execution at specific points and wait for external input before continuing. This enables human-in-the-loop patterns where you need external input to proceed. When an interrupt is triggered, LangGraph saves the graph state using its [persistence](/oss/langgraph/persistence) layer and waits indefinitely until you resume execution. Interrupts work by calling the `interrupt()` function at any point in your graph nodes. The function accepts any JSON-serializable value which is surfaced to the caller. When you're ready to continue, you resume execution by re-invoking the graph using `Command`, which then becomes the return value of the `interrupt()` call from inside the node. @@ -11,7 +23,7 @@ Unlike static breakpoints (which pause before or after specific nodes), interrup :::python - **Checkpointing keeps your place:** the checkpointer writes the exact graph state so you can resume later, even when in an error state. - **`thread_id` is your pointer:** set `config={"configurable": {"thread_id": ...}}` to tell the checkpointer which state to load. -- **Interrupt payloads surface via `chunk["interrupts"]`:** when streaming with `version="v2"`, the values you pass to `interrupt()` appear in the `interrupts` field of `values` stream parts so you know what the graph is waiting on. +- **Interrupt payloads surface via `stream.interrupts`:** when using [event streaming](/oss/langgraph/event-streaming) (`graph.stream_events(..., version="v3")`), the values you pass to `interrupt()` appear on `stream.interrupts`, and `stream.interrupted` is `True` when the run pauses for input. ::: :::js - **Checkpointing keeps your place:** the checkpointer writes the exact graph state so you can resume later, even when in an error state. @@ -61,7 +73,12 @@ When you call @[`interrupt`], here's what happens: 1. **Graph execution gets suspended** at the exact point where @[`interrupt`] is called 2. **State is saved** using the checkpointer so execution can be resumed later, In production, this should be a persistent checkpointer (e.g. backed by a database) +:::python +3. **Value is returned** to the caller on `stream.interrupts` when using [event streaming](/oss/langgraph/event-streaming) (`graph.stream_events(..., version="v3")`), or under `__interrupt__` with the default `invoke()` API; it can be any JSON-serializable value (string, object, array, etc.) +::: +:::js 3. **Value is returned** to the caller under `__interrupt__`; it can be any JSON-serializable value (string, object, array, etc.) +::: 4. **Graph waits indefinitely** until you resume execution with a response 5. **Response is passed back** into the node when you resume, becoming the return value of the `interrupt()` call @@ -70,42 +87,13 @@ When you call @[`interrupt`], here's what happens: After an interrupt pauses execution, you resume the graph by invoking it again with a `Command` that contains the resume value. The resume value is passed back to the `interrupt` call, allowing the node to continue execution with the external input. :::python - - - ```python - from langgraph.types import Command - - # Initial run - hits the interrupt and pauses - # thread_id is the persistent pointer (stores a stable ID in production) - config = {"configurable": {"thread_id": "thread-1"}} - result = graph.invoke({"input": "data"}, config=config, version="v2") +The recommended way to drive a graph that may interrupt is [event streaming](/oss/langgraph/event-streaming) — it surfaces interrupts via `stream.interrupts` and `stream.interrupted`, and exposes the final state through `stream.output`. - # result is a GraphOutput with .value and .interrupts - # .interrupts contains the payloads passed to interrupt() - print(result.interrupts) - # > (Interrupt(value='Do you approve this action?'),) + - # Resume with the human's response - # The resume payload becomes the return value of interrupt() inside the node - graph.invoke(Command(resume=True), config=config, version="v2") - ``` - - - ```python - from langgraph.types import Command - - config = {"configurable": {"thread_id": "thread-1"}} - result = graph.invoke({"input": "data"}, config=config) - - # __interrupt__ contains the payload that was passed to interrupt() - print(result["__interrupt__"]) - # > [Interrupt(value='Do you approve this action?')] - - # Resume with the human's response - graph.invoke(Command(resume=True), config=config) - ``` - - + +The default `graph.invoke(...)` API still works and surfaces interrupts under `result["__interrupt__"]`. Use it when you don't need streamed projections; otherwise prefer `graph.stream_events(..., version="v3")`. + **Key points about resuming:** @@ -116,7 +104,7 @@ After an interrupt pauses execution, you resume the graph by invoking it again w -`Command(resume=...)` is the **only** `Command` pattern intended as input to `invoke()`/`stream()`. The other `Command` parameters (`update`, `goto`, `graph`) are designed for [returning from node functions](/oss/langgraph/graph-api#command). Do not pass `Command(update=...)` as input to continue multi-turn conversations—pass a plain input dict instead. +`Command(resume=...)` is the **only** `Command` pattern intended as input to `invoke()`/`stream()`/`stream_events()`. The other `Command` parameters (`update`, `goto`, `graph`) are designed for [returning from node functions](/oss/langgraph/graph-api#command). Do not pass `Command(update=...)` as input to continue multi-turn conversations—pass a plain input dict instead. @@ -150,7 +138,7 @@ await graph.invoke(new Command({ resume: true }), config); -`new Command({ resume: ... })` is the **only** `Command` pattern intended as input to `invoke()`/`stream()`. The other `Command` parameters (`update`, `goto`, `graph`) are designed for [returning from node functions](/oss/langgraph/graph-api#command). Do not pass `new Command({ update: ... })` as input to continue multi-turn conversations—pass a plain input object instead. +`new Command({ resume: ... })` is the **only** `Command` pattern intended as input to `invoke()`/`stream()`/`stream_events()`. The other `Command` parameters (`update`, `goto`, `graph`) are designed for [returning from node functions](/oss/langgraph/graph-api#command). Do not pass `new Command({ update: ... })` as input to continue multi-turn conversations—pass a plain input object instead. @@ -168,46 +156,31 @@ The key thing that interrupts unlock is the ability to pause execution and wait ### Stream with human-in-the-loop (HITL) interrupts -When building interactive agents with human-in-the-loop workflows, you can stream both message chunks and node updates simultaneously to provide real-time feedback while handling interrupts. +When building interactive agents with human-in-the-loop workflows, you can use [event streaming](/oss/langgraph/event-streaming) to consume message chunks and state snapshots concurrently while handling interrupts. -Use multiple stream modes (`"messages"` and `"updates"`) with `subgraphs=True` (if subgraphs are present) to: -- Stream AI responses in real-time as they're generated -- Detect when the graph encounters an interrupt -- Handle user input and resume execution seamlessly +Use the typed projections returned by `graph.stream_events(..., version="v3")` in a loop until the run finishes: +- Stream AI responses token-by-token via `stream.messages` +- Observe per-step state snapshots via `stream.values` +- Detect interrupts via `stream.interrupted` and read their payloads from `stream.interrupts` +- Resume execution by calling `stream_events` again with `Command(resume=...)` and repeat until `stream.interrupted` is false :::python -```python -async for chunk in graph.astream( - initial_input, - stream_mode=["messages", "updates"], - subgraphs=True, - config=config, - version="v2", -): - if chunk["type"] == "messages": - # Handle streaming message content - msg, _ = chunk["data"] - if isinstance(msg, AIMessageChunk) and msg.content: - display_streaming_content(msg.content) - - elif chunk["type"] == "updates": - # Check for interrupts in the updates data - if "__interrupt__" in chunk["data"]: - interrupt_info = chunk["data"]["__interrupt__"][0].value - user_response = get_user_input(interrupt_info) - initial_input = Command(resume=user_response) - break - else: - current_node = list(chunk["data"].keys())[0] -``` + -- **`version="v2"`**: All chunks are `StreamPart` dicts with `type`, `ns`, and `data` keys -- **`chunk["type"]`**: Narrow on the stream mode (`"messages"`, `"updates"`, etc.) for type inference -- **`chunk["ns"]`**: Identifies the source graph (empty tuple for root, populated for subgraphs) -- **`subgraphs=True`**: Required for interrupt detection in nested graphs +- **`stream.messages`**: Chat-model output as content blocks; iterate each `message.text` for token deltas. For nested subgraphs, read message chunks from `stream.subgraphs[*].messages`. +- **`stream.values`**: Full state snapshots after each step +- **`stream.interrupted` / `stream.interrupts`**: After each run, check whether the graph paused; read payloads from `stream.interrupts` +- **`Command(resume=...)`**: Pass as the next `stream_events` input to resume; loop until the run completes without interrupting ::: -- **`Command(resume=...)`**: Resumes graph execution with user-provided data +:::js + + +- **`stream.messages`**: Chat-model output as content blocks; iterate `message.text` for token deltas. For nested subgraphs, read message chunks from `stream.subgraphs[*].messages`. +- **`stream.values`**: Full state snapshots after each step +- **`stream.interrupted` / `stream.interrupts`**: After each run, check whether the graph paused; read payloads from `stream.interrupts` +- **`Command(resume=...)`**: Pass as the next `streamEvents` input to resume; loop until the run completes without interrupting +::: ### Handling multiple interrupts @@ -217,65 +190,7 @@ This ensures each response is paired with the correct interrupt at runtime. :::python -```python -from typing import Annotated, TypedDict -import operator - -from langgraph.checkpoint.memory import InMemorySaver -from langgraph.graph import START, END, StateGraph -from langgraph.types import Command, interrupt - - -class State(TypedDict): - vals: Annotated[list[str], operator.add] - - -def node_a(state): - answer = interrupt("question_a") - return {"vals": [f"a:{answer}"]} - - -def node_b(state): - answer = interrupt("question_b") - return {"vals": [f"b:{answer}"]} - - -graph = ( - StateGraph(State) - .add_node("a", node_a) - .add_node("b", node_b) - .add_edge(START, "a") - .add_edge(START, "b") - .add_edge("a", END) - .add_edge("b", END) - .compile(checkpointer=InMemorySaver()) -) - -config = {"configurable": {"thread_id": "1"}} - -# Step 1: invoke - both parallel nodes hit interrupt() and pause -interrupted_result = graph.invoke({"vals": []}, config) -print(interrupted_result) -""" -{ - 'vals': [], - '__interrupt__': [ - Interrupt(value='question_a', id='bd4f3183600f2c41dddafbf8f0f7be7b'), - Interrupt(value='question_b', id='29963e3d3585f0cef025dd0f14323f55') - ] -} -""" - -# Step 2: resume all pending interrupts at once -resume_map = { - i.id: f"answer for {i.value}" - for i in interrupted_result["__interrupt__"] -} -result = graph.invoke(Command(resume=resume_map), config) - -print("Final state:", result) -#> Final state: {'vals': ['a:answer for question_a', 'b:answer for question_b']} -``` + ::: @@ -365,7 +280,7 @@ from typing import Literal from langgraph.types import interrupt, Command def approval_node(state: State) -> Command[Literal["proceed", "cancel"]]: - # Pause execution; payload shows up under result["__interrupt__"] + # Pause execution; payload shows up on stream.interrupts (with stream_events) or result["__interrupt__"] (with invoke) is_approved = interrupt({ "question": "Do you want to proceed with this action?", "details": state["action_details"] @@ -404,10 +319,10 @@ const approvalNode: typeof State.Node = (state) => { When you resume the graph, pass `True` to approve or `False` to reject: ```python # To approve -graph.invoke(Command(resume=True), config=config) +graph.stream_events(Command(resume=True), config=config, version="v3").output # To reject -graph.invoke(Command(resume=False), config=config) +graph.stream_events(Command(resume=False), config=config, version="v3").output ``` ::: @@ -425,61 +340,8 @@ await graph.invoke(new Command({ resume: false }), config); :::python - ```python - from typing import Literal, Optional, TypedDict - - from langgraph.checkpoint.memory import MemorySaver - from langgraph.graph import StateGraph, START, END - from langgraph.types import Command, interrupt - - - class ApprovalState(TypedDict): - action_details: str - status: Optional[Literal["pending", "approved", "rejected"]] - - - def approval_node(state: ApprovalState) -> Command[Literal["proceed", "cancel"]]: - # Expose details so the caller can render them in a UI - decision = interrupt({ - "question": "Approve this action?", - "details": state["action_details"], - }) - - # Route to the appropriate node after resume - return Command(goto="proceed" if decision else "cancel") - - - def proceed_node(state: ApprovalState): - return {"status": "approved"} - - - def cancel_node(state: ApprovalState): - return {"status": "rejected"} - - - builder = StateGraph(ApprovalState) - builder.add_node("approval", approval_node) - builder.add_node("proceed", proceed_node) - builder.add_node("cancel", cancel_node) - builder.add_edge(START, "approval") - builder.add_edge("proceed", END) - builder.add_edge("cancel", END) - - # Use a more durable checkpointer in production - checkpointer = MemorySaver() - graph = builder.compile(checkpointer=checkpointer) - - config = {"configurable": {"thread_id": "approval-123"}} - initial = graph.invoke( - {"action_details": "Transfer $500", "status": "pending"}, - config=config, - ) - print(initial["__interrupt__"]) # -> [Interrupt(value={'question': ..., 'details': ...})] + - # Resume with the decision; True routes to proceed, False to cancel - resumed = graph.invoke(Command(resume=True), config=config) - print(resumed["status"]) # -> "approved" - ``` ::: :::js @@ -545,7 +407,7 @@ Sometimes you want to let a human review and edit part of the graph state before from langgraph.types import interrupt def review_node(state: State): - # Pause and show the current content for review (surfaces in result["__interrupt__"]) + # Pause and show the current content for review (payload surfaces on stream.interrupts) edited_content = interrupt({ "instruction": "Review and edit this content", "content": state["generated_text"] @@ -577,10 +439,11 @@ When resuming, provide the edited content: :::python ```python -graph.invoke( +graph.stream_events( Command(resume="The edited and improved text"), # Value becomes the return from interrupt() - config=config -) + config=config, + version="v3", +).output ``` ::: @@ -596,47 +459,8 @@ await graph.invoke( :::python - ```python - import sqlite3 - from typing import TypedDict - - from langgraph.checkpoint.memory import MemorySaver - from langgraph.graph import StateGraph, START, END - from langgraph.types import Command, interrupt - - - class ReviewState(TypedDict): - generated_text: str - - - def review_node(state: ReviewState): - # Ask a reviewer to edit the generated content - updated = interrupt({ - "instruction": "Review and edit this content", - "content": state["generated_text"], - }) - return {"generated_text": updated} - - - builder = StateGraph(ReviewState) - builder.add_node("review", review_node) - builder.add_edge(START, "review") - builder.add_edge("review", END) - - checkpointer = MemorySaver() - graph = builder.compile(checkpointer=checkpointer) - - config = {"configurable": {"thread_id": "review-42"}} - initial = graph.invoke({"generated_text": "Initial draft"}, config=config) - print(initial["__interrupt__"]) # -> [Interrupt(value={'instruction': ..., 'content': ...})] + - # Resume with the edited text from the reviewer - final_state = graph.invoke( - Command(resume="Improved draft after review"), - config=config, - ) - print(final_state["generated_text"]) # -> "Improved draft after review" - ``` ::: :::js @@ -703,7 +527,7 @@ from langgraph.types import interrupt def send_email(to: str, subject: str, body: str): """Send an email to a recipient.""" - # Pause before sending; payload surfaces in result["__interrupt__"] + # Pause before sending; payload surfaces on stream.interrupts when using event streaming response = interrupt({ "action": "send_email", "to": to, @@ -768,24 +592,25 @@ This approach is useful when you want the approval logic to live with the tool i ```python import sqlite3 - from typing import TypedDict - + import operator + from typing import TypedDict, Annotated, Literal from langchain.tools import tool from langchain_anthropic import ChatAnthropic from langgraph.checkpoint.sqlite import SqliteSaver from langgraph.graph import StateGraph, START, END from langgraph.types import Command, interrupt + from langchain.messages import AnyMessage, SystemMessage, ToolMessage class AgentState(TypedDict): - messages: list[dict] + messages: Annotated[list[AnyMessage], operator.add] @tool def send_email(to: str, subject: str, body: str): """Send an email to a recipient.""" - # Pause before sending; payload surfaces in result["__interrupt__"] + # Pause before sending; payload surfaces on stream.interrupts when using event streaming response = interrupt({ "action": "send_email", "to": to, @@ -807,39 +632,65 @@ This approach is useful when you want the approval logic to live with the tool i model = ChatAnthropic(model="claude-sonnet-4-6").bind_tools([send_email]) + tools_by_name = {"send_email": send_email} def agent_node(state: AgentState): # LLM may decide to call the tool; interrupt pauses before sending result = model.invoke(state["messages"]) - return {"messages": state["messages"] + [result]} - + return {"messages": [result]} + + def tool_node(state: AgentState): + """Performs the tool call""" + result = [] + for tool_call in state["messages"][-1].tool_calls: + tool = tools_by_name[tool_call["name"]] + observation = tool.invoke(tool_call["args"]) + result.append(ToolMessage(content=observation, tool_call_id=tool_call["id"])) + return {"messages": result} + + def should_continue(state: AgentState) -> Literal["tool_node", END]: + """Decide if we should continue the loop or stop based upon whether the LLM made a tool call""" + messages = state["messages"] + last_message = messages[-1] + + if last_message.tool_calls: + return "tool_node" + return END builder = StateGraph(AgentState) builder.add_node("agent", agent_node) + builder.add_node("tool_node", tool_node) + builder.add_edge(START, "agent") - builder.add_edge("agent", END) + builder.add_conditional_edges("agent", should_continue, ["tool_node", END]) # Routes to "tools" or END + builder.add_edge("tool_node", "agent") # Loop back after tools - checkpointer = SqliteSaver(sqlite3.connect("tool-approval.db")) + checkpointer = SqliteSaver( + sqlite3.connect("tool-approval.db", check_same_thread=False) + ) graph = builder.compile(checkpointer=checkpointer) config = {"configurable": {"thread_id": "email-workflow"}} - initial = graph.invoke( + initial = graph.stream_events( { "messages": [ {"role": "user", "content": "Send an email to alice@example.com about the meeting"} ] }, config=config, + version="v3", ) - print(initial["__interrupt__"]) # -> [Interrupt(value={'action': 'send_email', ...})] + initial.output # drive the stream to completion + print(initial.interrupts) # -> (Interrupt(value={'action': 'send_email', ...}),) # Resume with approval and optionally edited arguments - resumed = graph.invoke( + resumed = graph.stream_events( Command(resume={"action": "approve", "subject": "Updated subject"}), config=config, + version="v3", ) - print(resumed["messages"][-1]) # -> Tool result returned by send_email + print(resumed.output["messages"][-1]) # -> Tool result returned by send_email ``` ::: @@ -938,103 +789,47 @@ This approach is useful when you want the approval logic to live with the tool i ### Validating human input -Sometimes you need to validate input from humans and ask again if it's invalid. You can do this using multiple @[`interrupt`] calls in a loop. +Sometimes you need to validate input from humans and re-prompt if the value is invalid. The recommended approach is to call `interrupt()` **once per node invocation**, return from the node with the error message stored in state, and use a **conditional edge** to loop back to the node until a valid value is provided. + + + +**Avoid `while True` + `interrupt()` loops inside a single node.** Because the node re-runs from the beginning on every resume (see [Rules of interrupts](#rules-of-interrupts)), a loop that calls `interrupt()` multiple times causes each resume to replay all previous iterations: the first resume replays 1 iteration, the second replays 2, and so on. The result is exponential re-execution of any code inside the loop body. + + :::python -```python -from langgraph.types import interrupt +The correct pattern: -def get_age_node(state: State): - prompt = "What is your age?" +1. Store the re-prompt question in state (e.g. `pending_question`). +2. In the node, call `interrupt()` **exactly once**, passing the current question from state. +3. If the answer is invalid, return the updated `pending_question` so the next invocation re-prompts. +4. Use `add_conditional_edges` to route back to the node until a valid value is collected. - while True: - answer = interrupt(prompt) # payload surfaces in result["__interrupt__"] + - # Validate the input - if isinstance(answer, int) and answer > 0: - # Valid input - continue - break - else: - # Invalid input - ask again with a more specific prompt - prompt = f"'{answer}' is not a valid age. Please enter a positive number." +Each resume invokes `get_age_node` exactly once, runs the `interrupt()` call once, and exits. When the answer is invalid, the conditional edge loops back and the next interrupt re-prompts with the updated question. No code runs more than once per resume. - return {"age": answer} -``` ::: :::js -```typescript -import { interrupt } from "@langchain/langgraph"; +The correct pattern: -const getAgeNode: typeof State.Node = (state) => { - let prompt = "What is your age?"; +1. Store the re-prompt question in state (e.g. `pendingQuestion`). +2. In the node, call `interrupt()` **exactly once**, passing the current question from state. +3. If the answer is invalid, return the updated `pendingQuestion` so the next invocation re-prompts. +4. Use `addConditionalEdges` to route back to the node until a valid value is collected. - while (true) { - const answer = interrupt(prompt); // payload surfaces in result.__interrupt__ + - // Validate the input - if (typeof answer === "number" && answer > 0) { - // Valid input - continue - return { age: answer }; - } else { - // Invalid input - ask again with a more specific prompt - prompt = `'${answer}' is not a valid age. Please enter a positive number.`; - } - } -} -``` -::: +Each resume invokes `getAgeNode` exactly once, runs the `interrupt()` call once, and exits. When the answer is invalid, the conditional edge loops back and the next interrupt re-prompts with the updated question. -Each time you resume the graph with invalid input, it will ask again with a clearer message. Once valid input is provided, the node completes and the graph continues. +::: :::python - ```python - import sqlite3 - from typing import TypedDict - - from langgraph.checkpoint.sqlite import SqliteSaver - from langgraph.graph import StateGraph, START, END - from langgraph.types import Command, interrupt - + - class FormState(TypedDict): - age: int | None - - - def get_age_node(state: FormState): - prompt = "What is your age?" - - while True: - answer = interrupt(prompt) # payload surfaces in result["__interrupt__"] - - if isinstance(answer, int) and answer > 0: - return {"age": answer} - - prompt = f"'{answer}' is not a valid age. Please enter a positive number." - - - builder = StateGraph(FormState) - builder.add_node("collect_age", get_age_node) - builder.add_edge(START, "collect_age") - builder.add_edge("collect_age", END) - - checkpointer = SqliteSaver(sqlite3.connect("forms.db")) - graph = builder.compile(checkpointer=checkpointer) - - config = {"configurable": {"thread_id": "form-1"}} - first = graph.invoke({"age": None}, config=config) - print(first["__interrupt__"]) # -> [Interrupt(value='What is your age?', ...)] - - # Provide invalid data; the node re-prompts - retry = graph.invoke(Command(resume="thirty"), config=config) - print(retry["__interrupt__"]) # -> [Interrupt(value="'thirty' is not a valid age...", ...)] - - # Provide valid data; loop exits and state updates - final = graph.invoke(Command(resume=30), config=config) - print(final["age"]) # -> 30 - ``` ::: :::js @@ -1053,37 +848,36 @@ Each time you resume the graph with invalid input, it will ask again with a clea const State = new StateSchema({ age: z.number().nullable(), + pendingQuestion: z.string().nullable(), }); const builder = new StateGraph(State) .addNode("collectAge", (state) => { - let prompt = "What is your age?"; - - while (true) { - const answer = interrupt(prompt); // payload surfaces in result.__interrupt__ - - if (typeof answer === "number" && answer > 0) { - return { age: answer }; - } + const question = state.pendingQuestion ?? "What is your age?"; + const answer = interrupt(question); // called exactly once per invocation - prompt = `'${answer}' is not a valid age. Please enter a positive number.`; + if (typeof answer === "number" && answer > 0) { + return { age: answer, pendingQuestion: null }; } + return { pendingQuestion: `'${answer}' is not a valid age. Please enter a positive number.` }; }) .addEdge(START, "collectAge") - .addEdge("collectAge", END); + .addConditionalEdges("collectAge", (state) => + state.age !== null ? END : "collectAge" + ); const checkpointer = new MemorySaver(); const graph = builder.compile({ checkpointer }); const config = { configurable: { thread_id: "form-1" } }; - const first = await graph.invoke({ age: null }, config); + const first = await graph.invoke({ age: null, pendingQuestion: null }, config); console.log(first.__interrupt__); // -> [{ value: "What is your age?", ... }] - // Provide invalid data; the node re-prompts + // Provide invalid data; the node re-prompts via the conditional edge const retry = await graph.invoke(new Command({ resume: "thirty" }), config); console.log(retry.__interrupt__); // -> [{ value: "'thirty' is not a valid age...", ... }] - // Provide valid data; loop exits and state updates + // Provide valid data; route returns END and the graph finishes const final = await graph.invoke(new Command({ resume: 30 }), config); console.log(final.age); // -> 30 ``` @@ -1240,7 +1034,7 @@ async function nodeA(state: State) { ::: * 🔴 Do not conditionally skip @[`interrupt`] calls within a node -* 🔴 Do not loop @[`interrupt`] calls using logic that isn't deterministic across executions +* 🔴 Do not loop @[`interrupt`] calls using logic that isn't deterministic across executions, including `while True` validation loops. Use a conditional edge instead (see [Validating human input](#validating-human-input)) :::python @@ -1778,7 +1572,7 @@ To debug and test a graph, you can use static interrupts as breakpoints to step -To debug your interrupts, use [LangSmith](/langsmith/home). +To debug your interrupts, use [LangSmith](/langsmith/observability). ### Using LangSmith Studio diff --git a/local-server.mdx b/local-server.mdx index 4d9d3b8..7a6e7c2 100644 --- a/local-server.mdx +++ b/local-server.mdx @@ -4,7 +4,6 @@ sidebarTitle: Local server --- - This guide shows you how to run a LangGraph application locally. ## Prerequisites @@ -321,7 +320,7 @@ https://smith.langchain.com/studio/?baseUrl=http://myhost:3000 Now that you have a LangGraph app running locally, take your journey further by exploring deployment and advanced features: * [Deployment quickstart](/langsmith/deployment-quickstart): Deploy your LangGraph app using LangSmith. -* [LangSmith](/langsmith/home): Learn about foundational LangSmith concepts. +* [LangSmith](/langsmith/observability): Learn about foundational LangSmith concepts. :::python * [SDK Reference](https://reference.langchain.com/python/langsmith/deployment/sdk/): Explore the SDK API Reference. diff --git a/memory.mdx b/memory.mdx deleted file mode 100644 index ac44a23..0000000 --- a/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. - -![Short vs long](/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. - -![Filter](/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. - -![Update profile](/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](/langsmith/evaluate-chatbot-tutorial)) 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. - -![Update list](/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](/langsmith/manage-datasets) to store your data and implement your own retrieval logic to select the most relevant examples based on user input. - -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 examples to align an LLM 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 and implement your own retrieval logic to select the most relevant examples based on user input. - -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 examples to align an LLM 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: GraphNode = async (state, config) => { - const namespace = ["agent_instructions"]; - const instructions = await config.store?.get(namespace, "agent_a"); - // Application logic - const prompt = promptTemplate.format({ - instructions: instructions[0].value.instructions - }); - // ... -}; - -// Node that updates instructions -const updateInstructions: GraphNode = async (state, config) => { - const namespace = ["instructions"]; - const currentInstructions = await config.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 - }); - // ... -}; -``` -::: - -![Update instructions](/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). - -![Hot path vs 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/observability.mdx b/observability.mdx index 58f7c6c..1a7dd92 100644 --- a/observability.mdx +++ b/observability.mdx @@ -2,7 +2,7 @@ title: LangSmith Observability --- -Traces are a series of steps that your application takes to go from input to output. Each of these individual steps is represented by a run. You can use [LangSmith](https://smith.langchain.com/) to visualize these execution steps. To use it, [enable tracing for your application](/langsmith/trace-with-langgraph). This enables you to do the following: +Traces are a series of steps that your application takes to go from input to output. Each of these individual steps is represented by a run. You can use [LangSmith](https://smith.langchain.com) to visualize these execution steps. To use it, [enable tracing for your application](/langsmith/trace-with-langgraph). This enables you to do the following: * [Debug a locally running application](/langsmith/observability-studio#debug-langsmith-traces). * [Evaluate the application performance](/oss/langchain/test/evals). @@ -13,13 +13,13 @@ Traces are a series of steps that your application takes to go from input to out Before you begin, ensure you have the following: - **A LangSmith account**: Sign up (for free) or log in at [smith.langchain.com](https://smith.langchain.com). -- **A LangSmith API key**: Follow the [Create an API key](/langsmith/create-account-api-key#create-an-api-key) guide. +- **A LangSmith API key**: Follow the [Create an API key](/langsmith/create-account-api-key) guide. ## Enable tracing To enable tracing for your application, set the following environment variables: -```bash +```python export LANGSMITH_TRACING=true export LANGSMITH_API_KEY= ``` @@ -150,7 +150,7 @@ await agent.invoke( { messages: [{role: "user", content: "Send a test email to alice@example.com"}] }, - { + config: { tags: ["production", "email-assistant", "v1.0"], metadata: { userId: "user123", @@ -167,7 +167,7 @@ This custom metadata and tags will be attached to the trace in LangSmith. -To learn more about how to use traces to debug, evaluate, and monitor your agents, see the [LangSmith documentation](/langsmith/home). +To learn more about how to use traces to debug, evaluate, and monitor your agents, see the [LangSmith documentation](/langsmith/observability). diff --git a/overview.mdx b/overview.mdx index eae819a..98b33f8 100644 --- a/overview.mdx +++ b/overview.mdx @@ -12,6 +12,18 @@ We will commonly use [LangChain](/oss/langchain/overview) components throughout LangGraph is focused on the underlying capabilities important for agent orchestration: durable execution, streaming, human-in-the-loop, and more. + + +- [Deep Agents](/oss/deepagents/overview) is an [agent harness](/oss/concepts/products#agent-harnesses-like-the-deep-agents-sdk): planning, subagents, filesystem tools, and context management on top of LangGraph. +- [LangChain](/oss/langchain/overview) is the agent framework: abstractions and integrations for models, tools, and agent loops. +- [LangGraph](/oss/langgraph/overview) is the orchestration runtime: durable execution, streaming, human-in-the-loop, and persistence. +- [LangSmith](/langsmith/observability) is the platform for tracing, evaluation, prompts, and deployment across frameworks. +- [LangSmith Engine](/langsmith/engine) detects issues in your LangGraph agent traces and proposes fixes. You can open a pull request with the proposed fix directly from the Engine tab. +- [LangSmith Fleet](/langsmith/fleet/index) is the no-code agent builder for templates, integrations, and routine automation. + +Read [Frameworks, runtimes, and harnesses](/oss/concepts/products) for a comparison of the open source stack. + + ## Install :::python @@ -67,7 +79,7 @@ graph.invoke({"messages": [{"role": "user", "content": "hi!"}]}) :::js ```typescript -import { StateSchema, MessagesValue, GraphNode, StateGraph, START, END } from "@langchain/langgraph"; +import { StateSchema, MessagesValue, type GraphNode, StateGraph, START, END } from "@langchain/langgraph"; const State = new StateSchema({ messages: MessagesValue, @@ -88,17 +100,17 @@ await graph.invoke({ messages: [{ role: "user", content: "hi!" }] }); ::: -Use [LangSmith](/langsmith/home) to trace requests, debug agent behavior, and evaluate outputs. Set `LANGSMITH_TRACING=true` and your API key to get started. +Use [LangSmith](/langsmith/observability) to trace requests, debug agent behavior, and evaluate outputs. Set `LANGSMITH_TRACING=true` and your API key to get started. Follow the [tracing quickstart](/langsmith/trace-with-langchain) to get set up. We recommend you also set up [LangSmith Engine](/langsmith/engine) which monitors your traces, detects issues, and proposes fixes. ## Core benefits LangGraph provides low-level supporting infrastructure for *any* long-running, stateful workflow or agent. LangGraph does not abstract prompts or architecture, and provides the following central benefits: -* [Durable execution](/oss/langgraph/durable-execution): Build agents that persist through failures and can run for extended periods, resuming from where they left off. +* [Persistence](/oss/langgraph/persistence): Build agents that persist through failures and can run for extended periods, resuming from where they left off. * [Human-in-the-loop](/oss/langgraph/interrupts): Incorporate human oversight by inspecting and modifying agent state at any point. * [Comprehensive memory](/oss/concepts/memory): Create stateful agents with both short-term working memory for ongoing reasoning and long-term memory across sessions. -* [Debugging with LangSmith](/langsmith/home): Gain deep visibility into complex agent behavior with visualization tools that trace execution paths, capture state transitions, and provide detailed runtime metrics. +* [Debugging with LangSmith](/langsmith/observability): Gain deep visibility into complex agent behavior with visualization tools that trace execution paths, capture state transitions, and provide detailed runtime metrics. * [Production-ready deployment](/langsmith/deployment): Deploy sophisticated agent systems confidently with scalable infrastructure designed to handle the unique challenges of stateful, long-running workflows. ## LangGraph ecosystem diff --git a/persistence.mdx b/persistence.mdx index 8fd752d..577e315 100644 --- a/persistence.mdx +++ b/persistence.mdx @@ -1,1172 +1,79 @@ --- title: Persistence +description: LangGraph's persistence layer gives agents short-term memory through checkpointers and long-term memory through stores. --- +{/* Anchor stubs for backwards-compatible deep links */} + + + + + + +Persistence lets LangGraph applications keep useful information beyond a single graph run. It matters when an agent needs to continue a conversation, resume after an interruption, recover from a failure, or remember information across interactions. -LangGraph has a built-in persistence layer that saves graph state as checkpoints. When you compile a graph with a checkpointer, a snapshot of the graph state is saved at every step of execution, organized into threads. This enables human-in-the-loop workflows, conversational memory, time travel debugging, and fault-tolerant execution. +LangGraph provides two complementary persistence systems: -![Checkpoints](/oss/images/checkpoints.jpg) +- **[Checkpointers](/oss/langgraph/checkpointers)** persist a thread's graph state as checkpoints. Use them for short-term, thread-scoped memory, including conversation continuity, human-in-the-loop workflows, time travel, and fault tolerance. +- **[Stores](/oss/langgraph/stores)** persist application-defined data outside the graph state. Use them for long-term, cross-thread memory, including user preferences, facts, and shared knowledge. - -**Agent Server handles checkpointing automatically** -When using the [Agent Server](/langsmith/agent-server), you don't need to implement or configure checkpointers manually. The server handles all persistence infrastructure for you behind the scenes. - - -## Why use persistence - -Persistence is required for the following features: - -- **Human-in-the-loop**: 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 person has to be able to view the state of a graph at any point in time, and the graph has to be able to resume execution after the person has made any updates to the state. See [Interrupts](/oss/langgraph/interrupts) for examples. -- **Memory**: 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. -- **Time travel**: 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. -- **Fault-tolerance**: Checkpointing 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. - -- **Pending writes**: When a graph node fails mid-execution at a given [super-step](#super-steps), LangGraph stores pending checkpoint writes from any other nodes that completed successfully at that super-step. When you resume graph execution from that super-step you don't re-run the successful nodes. - -## Core concepts - -### Threads +Most applications can use both: a [checkpointer](/oss/langgraph/checkpointers) tracks the current thread, and a [store](/oss/langgraph/stores) tracks durable information across 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/runs). 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. +## Quickstart -When invoking a graph with a checkpointer, you **must** specify a `thread_id` as part of the `configurable` portion of the config: +Compile your graph with a checkpointer, a store, or both: :::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://reference.langchain.com/python/langsmith/) for more details. - -The checkpointer uses `thread_id` as the primary key for storing and retrieving checkpoints. Without it, the checkpointer cannot save state or resume execution after an [interrupt](/oss/langgraph/interrupts), since the checkpointer uses `thread_id` to load the saved state. - -### Checkpoints - -The state of a thread at a particular point in time is called a checkpoint. A checkpoint is a snapshot of the graph state saved at each [super-step](#super-steps) and is represented by a `StateSnapshot` object (see [StateSnapshot fields](#statesnapshot-fields) for the full field reference). - -#### Super-steps - -LangGraph created a checkpoint at each **super-step** boundary. A super-step is a single "tick" of the graph where all nodes scheduled for that step execute (potentially in parallel). For a sequential graph like `START -> A -> B -> END`, there are separate super-steps for the input, node A, and node B — producing a checkpoint after each one. Understanding super-step boundaries is important for [time travel](/oss/langgraph/use-time-travel), because you can only resume execution from a checkpoint (i.e., a super-step boundary). - -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) +from langgraph.store.memory import InMemoryStore checkpointer = InMemorySaver() -graph = workflow.compile(checkpointer=checkpointer) - -config: RunnableConfig = {"configurable": {"thread_id": "1"}} -graph.invoke({"foo": "", "bar":[]}, config) -``` -::: - -:::js -```typescript -import { StateGraph, StateSchema, ReducedValue, START, END, MemorySaver } from "@langchain/langgraph"; -import { z } from "zod/v4"; - -const State = new StateSchema({ - foo: z.string(), - bar: new ReducedValue( - z.array(z.string()).default(() => []), - { - inputSchema: z.array(z.string()), - reducer: (x, y) => x.concat(y), - } - ), -}); - -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: "", bar: [] }, 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. -::: - -#### Checkpoint namespace - -Each checkpoint has a `checkpoint_ns` (checkpoint namespace) field that identifies which graph or subgraph it belongs to: - -- **`""`** (empty string): The checkpoint belongs to the parent (root) graph. -- **`"node_name:uuid"`**: The checkpoint belongs to a subgraph invoked as the given node. For nested subgraphs, namespaces are joined with `|` separators (e.g., `"outer_node:uuid|inner_node:uuid"`). - -You can access the checkpoint namespace from within a node via the config: - -:::python -```python -from langchain_core.runnables import RunnableConfig - -def my_node(state: State, config: RunnableConfig): - checkpoint_ns = config["configurable"]["checkpoint_ns"] - # "" for the parent graph, "node_name:uuid" for a subgraph -``` -::: - -:::js -```typescript -import { RunnableConfig } from "@langchain/core/runnables"; - -function myNode(state: typeof State.Type, config: RunnableConfig) { - const checkpointNs = config.configurable?.checkpoint_ns; - // "" for the parent graph, "node_name:uuid" for a subgraph -} -``` -::: - -See [Subgraphs](/oss/langgraph/use-subgraphs) for more details on working with subgraph state and checkpoints. - -## Get and update state - -### 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. - -```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: - -``` -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=() -) -``` -::: - -:::js -In our example, the output of `getState` 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 fields - -:::python - -| Field | Type | Description | -|-------|------|-------------| -| `values` | `dict` | State channel values at this checkpoint. | -| `next` | `tuple[str, ...]` | Node names to execute next. Empty `()` means the graph is complete. | -| `config` | `dict` | Contains `thread_id`, `checkpoint_ns`, and `checkpoint_id`. | -| `metadata` | `dict` | Execution metadata. Contains `source` (`"input"`, `"loop"`, or `"update"`), `writes` (node outputs), and `step` (super-step counter). | -| `created_at` | `str` | ISO 8601 timestamp of when this checkpoint was created. | -| `parent_config` | `dict \| None` | Config of the previous checkpoint. `None` for the first checkpoint. | -| `tasks` | `tuple[PregelTask, ...]` | Tasks to execute at this step. Each task has `id`, `name`, `error`, `interrupts`, and optionally `state` (subgraph snapshot, when using `subgraphs=True`). | - -::: - -:::js - -| Field | Type | Description | -|-------|------|-------------| -| `values` | `object` | State channel values at this checkpoint. | -| `next` | `string[]` | Node names to execute next. Empty `[]` means the graph is complete. | -| `config` | `object` | Contains `thread_id`, `checkpoint_ns`, and `checkpoint_id`. | -| `metadata` | `object` | Execution metadata. Contains `source` (`"input"`, `"loop"`, or `"update"`), `writes` (node outputs), and `step` (super-step counter). | -| `createdAt` | `string` | ISO 8601 timestamp of when this checkpoint was created. | -| `parentConfig` | `object \| null` | Config of the previous checkpoint. `null` for the first checkpoint. | -| `tasks` | `PregelTask[]` | Tasks to execute at this step. Each task has `id`, `name`, `error`, `interrupts`, and optionally `state` (subgraph snapshot, when using `subgraphs: true`). | - -::: - -### Get state history - -:::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)) -``` -::: - -:::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) - -#### Find a specific checkpoint - -You can filter the state history to find checkpoints matching specific criteria: - -:::python -```python -history = list(graph.get_state_history(config)) - -# Find the checkpoint before a specific node executed -before_node_b = next(s for s in history if s.next == ("node_b",)) - -# Find a checkpoint by step number -step_2 = next(s for s in history if s.metadata["step"] == 2) +store = InMemoryStore() -# Find checkpoints created by update_state -forks = [s for s in history if s.metadata["source"] == "update"] +graph = builder.compile(checkpointer=checkpointer, store=store) -# Find the checkpoint where an interrupt occurred -interrupted = next( - s for s in history - if s.tasks and any(t.interrupts for t in s.tasks) +result = graph.invoke( + {"messages": [{"role": "user", "content": "Hi, my name is Bob."}]}, + {"configurable": {"thread_id": "thread-1"}}, ) ``` ::: :::js ```typescript -const history: StateSnapshot[] = []; -for await (const state of graph.getStateHistory(config)) { - history.push(state); -} +import { MemorySaver, MemoryStore } from "@langchain/langgraph"; -// Find the checkpoint before a specific node executed -const beforeNodeB = history.find((s) => s.next.includes("nodeB")); - -// Find a checkpoint by step number -const step2 = history.find((s) => s.metadata.step === 2); +const checkpointer = new MemorySaver(); +const store = new MemoryStore(); -// Find checkpoints created by updateState -const forks = history.filter((s) => s.metadata.source === "update"); +const graph = builder.compile({ checkpointer, store }); -// Find the checkpoint where an interrupt occurred -const interrupted = history.find( - (s) => s.tasks.length > 0 && s.tasks.some((t) => t.interrupts.length > 0) +const result = await graph.invoke( + { messages: [{ role: "user", content: "Hi, my name is Bob." }] }, + { configurable: { thread_id: "thread-1" } } ); ``` ::: -### Replay - -Replay re-executes steps from a prior checkpoint. Invoke the graph with a prior `checkpoint_id` to re-run nodes after that checkpoint. Nodes before the checkpoint are skipped (their results are already saved). Nodes after the checkpoint re-execute, including any LLM calls, API requests, or [interrupts](/oss/langgraph/interrupts) — which are always re-triggered during replay. - -See [Time travel](/oss/langgraph/use-time-travel) for full details and code examples on replaying past executions. - -![Replay](/oss/images/re_play.png) - -### Update state - -:::python -You can edit the graph state using @[`update_state`]. This creates a new checkpoint with the updated values — it does not modify the original checkpoint. The update is treated the same as a node update: values are passed through [reducer](/oss/langgraph/graph-api#reducers) functions when defined, so channels with reducers _accumulate_ values rather than overwrite them. - -You can optionally specify `as_node` to control which node the update is treated as coming from, which affects which node executes next. See [Time travel: `as_node`](/oss/langgraph/use-time-travel#from-a-specific-node) for details. -::: - -:::js -You can edit the graph state using `graph.updateState()`. This creates a new checkpoint with the updated values — it does not modify the original checkpoint. The update is treated the same as a node update: values are passed through [reducer](/oss/langgraph/graph-api#reducers) functions when defined, so channels with reducers _accumulate_ values rather than overwrite them. - -You can optionally specify `asNode` to control which node the update is treated as coming from, which affects which node executes next. See [Time travel: `asNode`](/oss/langgraph/use-time-travel#from-a-specific-node) for details. -::: - -![Update](/oss/images/checkpoints_full_story.jpg) - -## Memory store - -![Model of shared state](/oss/images/shared_state.png) - -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. - -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! - -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 pass the store. - -**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. +**Agent Server handles persistence automatically** +When using the [Agent Server](/langsmith/agent-server), you do not need to implement or configure checkpointers or stores manually. The server handles persistence infrastructure behind the scenes. - -@[InMemoryStore] is suitable for development and testing. For production, use a persistent store like `PostgresStore` or `RedisStore`. All implementations extend @[BaseStore], which is the type annotation to use in node function signatures. - - -### Basic usage - -First, let's showcase this in isolation without using LangGraph. +## Checkpointer vs. store -:::python -```python -from langgraph.store.memory import InMemoryStore -store = InMemoryStore() -``` -::: +| | Checkpointer | Store | +| ---- | ---- | ---- | +| Persists | Graph state snapshots | Application-defined key-value data | +| Scope | A single thread | Across threads | +| Memory type | Short-term, thread-scoped memory | Long-term, cross-thread memory | +| Use for | Conversation continuity, human-in-the-loop, time travel, and fault tolerance | User preferences, facts, and shared knowledge | +| Access pattern | Pass a `thread_id` in graph config | Read and write items from nodes or application code | +| Full guide | [Checkpointers](/oss/langgraph/checkpointers) | [Stores](/oss/langgraph/stores) | -:::js -```typescript -import { MemoryStore } from "@langchain/langgraph"; - -const memoryStore = new MemoryStore(); -``` -::: - -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. - -:::python -```python -user_id = "1" -namespace_for_memory = (user_id, "memories") -``` -::: - -:::js -```typescript -const userId = "1"; -const namespaceForMemory = [userId, "memories"]; -``` -::: - -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. - -:::python -```python -memory_id = str(uuid.uuid4()) -memory = {"food_preference" : "I like pizza"} -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); -``` -::: - -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 = 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'} -``` - -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 tuple of strings, the namespace of this memory type - - - While the type is `tuple[str, ...]`, it may be serialized as a list when converted to JSON (for example, `['1', 'memories']`). - - -* `created_at`: Timestamp for when this memory was created -* `updated_at`: Timestamp for when this memory was updated - -::: +## Next steps -:::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 tuple of strings, the namespace of this memory type - - - While the type is `tuple`, it may be serialized as a list when converted to JSON (for example, `['1', 'memories']`). - - -* `createdAt`: Timestamp for when this memory was created -* `updatedAt`: Timestamp for when this memory was updated - -::: - -### Semantic search - -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 - } -) -``` -::: - -:::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 -) -``` -::: - -:::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 -}); -``` -::: - -You can control which parts of your memories get embedded by configuring the `fields` parameter or by specifying the `index` parameter when storing memories: - -:::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 } -); -``` -::: - -### Using in LangGraph - -:::python -With this all in place, we use the store in LangGraph. The store works hand-in-hand with the checkpointer: the checkpointer saves state to threads, as discussed above, and the store allows us to store arbitrary information for access _across_ threads. We compile the graph with both the checkpointer and the store as follows. - -```python -from dataclasses import dataclass -from langgraph.checkpoint.memory import InMemorySaver - -@dataclass -class Context: - user_id: str - -# We need this because we want to enable threads (conversations) -checkpointer = InMemorySaver() - -# ... Define the graph ... - -# Compile the graph with the checkpointer and store -builder = StateGraph(MessagesState, context_schema=Context) -# ... add nodes and edges ... -graph = builder.compile(checkpointer=checkpointer, store=store) -``` -::: - -:::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 -config = {"configurable": {"thread_id": "1"}} - -# First let's just say hi to the AI -for update in graph.stream( - {"messages": [{"role": "user", "content": "hi"}]}, - config, - stream_mode="updates", - context=Context(user_id="1"), -): - print(update) -``` -::: - -:::js -```typescript -// Invoke the graph -const userId = "1"; -const config = { configurable: { thread_id: "1" }, context: { 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); -} -``` -::: - -:::python -You can access the store and the `user_id` in _any node_ by using the `Runtime` object. The `Runtime` is automatically injected by LangGraph when you add it as a parameter to your node function. Here's how you might use it to save memories: - -```python -from langgraph.runtime import Runtime -from dataclasses import dataclass - -@dataclass -class Context: - user_id: str - -async def update_memory(state: MessagesState, runtime: Runtime[Context]): - - # Get the user id from the runtime context - user_id = runtime.context.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 - await runtime.store.aput(namespace, memory_id, {"memory": memory}) - -``` - - -::: - -:::js -You can access the store and the `userId` in _any node_ with the `runtime` argument. Here's how you might use it to save memories: - -```typescript -import { StateSchema, MessagesValue, Runtime } from "@langchain/langgraph"; -import { v4 as uuidv4 } from "uuid"; - -const MessagesState = new StateSchema({ - messages: MessagesValue, -}); - -const updateMemory: GraphNode = async (state, runtime) => { - // 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 - const memory = "Some memory content"; - - // Create a new memory ID - const memoryId = uuidv4(); - - // We create a new memory - await runtime.store?.put(namespace, memoryId, { memory }); -}; -``` -::: - -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. - -:::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'} -``` -::: - -:::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 -from dataclasses import dataclass -from langgraph.runtime import Runtime - -@dataclass -class Context: - user_id: str - -async def call_model(state: MessagesState, runtime: Runtime[Context]): - # Get the user id from the runtime context - user_id = runtime.context.user_id - - # Namespace the memory - namespace = (user_id, "memories") - - # Search based on the most recent message - memories = await runtime.store.asearch( - namespace, - query=state["messages"][-1].content, - limit=3 - ) - info = "\n".join([d.value["memory"] for d in memories]) - - # ... Use memories in the model call -``` -::: - -:::js -```typescript -const callModel: GraphNode = async (state, runtime) => { - // Get the user id from the config - const userId = runtime.context?.user_id; - - // Namespace the memory - const namespace = [userId, "memories"]; - - // Search based on the most recent message - const memories = await runtime.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 -}; -``` -::: - -If we create a new thread, we can still access the same memories so long as the `user_id` is the same. - -:::python -```python -# Invoke the graph on a new thread -config = {"configurable": {"thread_id": "2"}} - -# Let's say hi again -for update in graph.stream( - {"messages": [{"role": "user", "content": "hi, tell me about my memories"}]}, - config, - stream_mode="updates", - context=Context(user_id="1"), -): - print(update) -``` -::: - -:::js -```typescript -// Invoke the graph -const config = { configurable: { thread_id: "2" }, context: { userId: "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); -} -``` -::: - -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: - -```json -{ - ... - "store": { - "index": { - "embed": "openai:text-embeddings-3-small", - "dims": 1536, - "fields": ["$"] - } - } -} -``` - -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 - -See [checkpointer integrations](/oss/integrations/checkpointers/index) for available providers. - - -* `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. -* `langgraph-checkpoint-cosmosdb`: An implementation of LangGraph checkpointer that uses Azure Cosmos DB (`CosmosDBSaver` / `AsyncCosmosDBSaver`). Ideal for using in production with Azure. Supports both sync and async operations. 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. -* `@langchain/langgraph-checkpoint-mongodb`: An advanced checkpointer that uses MongoDB database (`MongoDBSaver`). Ideal for using in production. Needs to be installed separately. -* `@langchain/langgraph-checkpoint-redis`: An advanced checkpointer that uses Redis database (`RedisSaver`). 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`). - - -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()` -::: - -:::python - -### Serializer - -When checkpointers save the graph state, they need to serialize the channel values in the state. This is done using serializer objects. - -`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. - -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`]: - -```python -from langgraph.checkpoint.memory import InMemorySaver -from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer - -# ... Define the graph ... -graph.compile( - checkpointer=InMemorySaver(serde=JsonPlusSerializer(pickle_fallback=True)) -) -``` - -#### Encryption - -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): - -```python -import sqlite3 - -from langgraph.checkpoint.serde.encrypted import EncryptedSerializer -from langgraph.checkpoint.sqlite import SqliteSaver - -serde = EncryptedSerializer.from_pycryptodome_aes() # reads LANGGRAPH_AES_KEY -checkpointer = SqliteSaver(sqlite3.connect("checkpoint.db"), serde=serde) -``` - -```python -from langgraph.checkpoint.serde.encrypted import EncryptedSerializer -from langgraph.checkpoint.postgres import PostgresSaver - -serde = EncryptedSerializer.from_pycryptodome_aes() -checkpointer = PostgresSaver.from_conn_string("postgresql://...", serde=serde) -checkpointer.setup() -``` - -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`]. - -::: +- [Use checkpointers](/oss/langgraph/checkpointers) to persist and inspect thread state. +- [Use stores](/oss/langgraph/stores) to persist durable data across threads. diff --git a/pregel.mdx b/pregel.mdx index 3923876..0722584 100644 --- a/pregel.mdx +++ b/pregel.mdx @@ -4,7 +4,6 @@ sidebarTitle: Runtime --- - :::python @[`Pregel`] implements LangGraph's runtime, managing the execution of LangGraph applications. @@ -45,18 +44,174 @@ An **actor** is a `PregelNode`. It subscribes to channels, reads data from them, ## Channels -Channels are used to communicate between actors (PregelNodes). Each channel has a value type, an update type, and an update function—which takes a sequence of updates and modifies the stored value. Channels can be used to send data from one chain to another, or to send data from a chain to itself in a future step. LangGraph provides a number of built-in channels: +Channels are used to communicate between actors (PregelNodes). Each channel has a value type, an update type, and an update function—which takes a sequence of updates and modifies the stored value. Channels can be used to send data from one chain to another, or to send data from a chain to itself in a future step. + +### LastValue + +@[`LastValue`] is the default channel type. It stores the last value written to it, overwriting any previous value. Use it for input and output values, or for passing data from one step to the next. + +:::python +```python +from langgraph.channels import LastValue + +channel: LastValue[int] = LastValue(int) +``` +::: + +:::js +```typescript +import { LastValue } from "@langchain/langgraph/channels"; + +const channel = new LastValue(); +``` +::: + +### Topic + +@[`Topic`] is a configurable PubSub channel useful for sending multiple values between actors or accumulating output across steps. It can be configured to deduplicate values or to accumulate all values written during a run. :::python -* @[`LastValue`]: The default channel, stores the last value sent to the channel, useful for input and output values, or for sending data from one step to the next. -* @[`Topic`]: A configurable PubSub Topic, useful for sending multiple values between **actors**, or for accumulating output. Can be configured to deduplicate values or to accumulate values over the course of multiple steps. -* @[`BinaryOperatorAggregate`]: stores a persistent value, updated by applying a binary operator to the current value and each update sent to the channel, useful for computing aggregates over multiple steps; e.g.,`total = BinaryOperatorAggregate(int, operator.add)` +```python +from langgraph.channels import Topic + +# Accumulate all values written across steps +channel: Topic[str] = Topic(str, accumulate=True) +``` ::: :::js -* @[`LastValue`]: The default channel, stores the last value sent to the channel, useful for input and output values, or for sending data from one step to the next. -* @[`Topic`]: A configurable PubSub Topic, useful for sending multiple values between **actors**, or for accumulating output. Can be configured to deduplicate values or to accumulate values over the course of multiple steps. -* @[`BinaryOperatorAggregate`]: stores a persistent value, updated by applying a binary operator to the current value and each update sent to the channel, useful for computing aggregates over multiple steps; e.g.,`total = BinaryOperatorAggregate(int, operator.add)` +```typescript +import { Topic } from "@langchain/langgraph/channels"; + +// Accumulate all values written across steps +const channel = new Topic({ accumulate: true }); +``` +::: + +### BinaryOperatorAggregate + +@[`BinaryOperatorAggregate`] stores a persistent value that is updated by applying a binary operator to the current value and each new update. Use it to compute running aggregates across steps. + +:::python +```python +import operator +from langgraph.channels import BinaryOperatorAggregate + +# Running total: each write adds to the current value +total = BinaryOperatorAggregate(int, operator.add) +``` +::: + +:::js +```typescript +import { BinaryOperatorAggregate } from "@langchain/langgraph/channels"; + +// Running total: each write adds to the current value +const total = new BinaryOperatorAggregate({ operator: (a, b) => a + b }); +``` +::: + +:::python +### DeltaChannel + + +`DeltaChannel` requires `langgraph>=1.2` and is currently in **beta**. The API may change in future releases. + + +@[`DeltaChannel`] stores only the incremental delta at each step rather than the full accumulated value. This is most useful for channels that are written frequently and accumulate large values over time—for example, a conversation message list in a long-running thread. Without delta storage, the full list is re-serialized into every checkpoint; with `DeltaChannel`, only the new messages written at each step are stored. + + +Consider `DeltaChannel` when a channel is both written to frequently and grows large over time. A good signal: if you notice checkpoint sizes growing linearly with thread length for a particular channel, `DeltaChannel` is likely a good fit. + + +Use `DeltaChannel` in an `Annotated` type annotation the same way you would use a plain reducer: + +```python +from typing import Annotated, Sequence +from typing_extensions import TypedDict +from langgraph.channels import DeltaChannel + + +def my_reducer(state: list[str], writes: Sequence[list[str]]) -> list[str]: + result = list(state) + for write in writes: + result.extend(write) + return result + + +class State(TypedDict): + messages: Annotated[list[str], DeltaChannel(my_reducer)] +``` + +#### Bulk reducer requirement + +The `reducer` passed to `DeltaChannel` is a **bulk reducer**: it receives the current state and a *sequence* of all writes from the current step in a single call—not pairwise like a standard reducer. This differs from the per-key reducers used with `Annotated` in a `StateGraph`, where the reducer is called once per update. + + +The bulk reducer **must be associative** (batching-invariant): + +``` +reducer(reducer(state, [xs]), [ys]) == reducer(state, [xs, ys]) +``` + +If your reducer is not associative, the reconstructed state may differ depending on how LangGraph batches writes across steps, producing inconsistent behavior. + + + +**The reducer runs on reconstruction, not on write.** Unlike a @[`BinaryOperatorAggregate`], whose reducer is invoked at write time so the combined value is what gets serialized into the checkpoint, a `DeltaChannel` reducer is invoked when the channel value is *rebuilt* from its persisted writes. The raw per-step writes are what get serialized; the reducer is only called when the value is materialized—on the next read, on the next step's actors, or when replaying history. + +Practical consequences when designing a reducer: + +- **Make it a pure function of `(state, writes)`.** Any side effects, randomness, or wall-clock reads (e.g., `uuid.uuid4()`, `datetime.now()`) execute every time the value is reconstructed and produce different results on each replay. They are *not* baked into the persisted writes. +- **Do not rely on mutations to incoming writes being persisted.** If your reducer mutates a write object (for example, assigning a stable ID to an item that arrived without one), that mutation lives only in the reconstructed value. The stored write still has the original shape, so the next reconstruction will see the un-mutated input again. +- **Attach identity and other stable metadata upstream.** If downstream code needs to reference an item by ID across turns (e.g., to update or remove it later), assign that ID before the value is written to the channel—not inside the reducer. + + +Here are bulk reducers for the two most common cases: + +```python +from typing import Any, Sequence + + +# List: append all writes in order +def list_reducer(state: list[Any], writes: Sequence[list[Any]]) -> list[Any]: + result = list(state) + for write in writes: + result.extend(write) + return result + + +# Dict: merge all writes, last write wins on key conflicts +def dict_reducer( + state: dict[str, Any], writes: Sequence[dict[str, Any]] +) -> dict[str, Any]: + result = dict(state) + for write in writes: + result.update(write) + return result +``` + +Both are associative: applying batches one at a time produces the same result as applying them together. + +#### Use snapshot_frequency for bounded read latency + +Without snapshots, reading a `DeltaChannel` value requires replaying the full write history—O(N) for a thread with N steps. Setting `snapshot_frequency=K` writes a full snapshot every K pregel steps, bounding read depth to at most K steps: + +```python +class State(TypedDict): + messages: Annotated[ + list[str], + DeltaChannel(my_reducer, snapshot_frequency=5), + ] +``` + +Higher values of `snapshot_frequency` reduce storage overhead but increase read latency. Lower values bound latency more tightly at the cost of larger checkpoints. `None` (the default) skips snapshots entirely—appropriate when reads are rare or threads are short. + +#### Version compatibility and rollbacks + + +**Rolling back to a version without `DeltaChannel` support is not supported.** `langgraph>=1.2` writes delta channel checkpoints in a new format that earlier versions cannot read. Once a thread has used `DeltaChannel`, downgrading LangGraph leaves those checkpoints unreadable as older runtimes do not understand the delta format and cannot reconstruct channel state. If you need to roll back, use the [delta-channel-dump recovery script](https://github.com/langchain-ai/langgraph/tree/main/examples/delta-channel-dump) to migrate affected threads, or discard them, before downgrading. + ::: ## Examples @@ -313,6 +468,10 @@ Below are a few different examples to give you a sense of the Pregel API. app.invoke({"a": "foo"}) ``` + + ```console + { 'c': 'foofoo | foofoofoofoo' } + ``` ::: :::js diff --git a/quickstart.mdx b/quickstart.mdx index 90da63e..5e21abc 100644 --- a/quickstart.mdx +++ b/quickstart.mdx @@ -3,7 +3,6 @@ title: Quickstart --- - This quickstart demonstrates how to build a calculator agent using the LangGraph Graph API or the Functional API. @@ -27,7 +26,7 @@ For this example, you will need to set up a [Claude (Anthropic)](https://www.ant ## 1. Define tools and model -In this example, we'll use the Claude Sonnet 4.6 model and define tools for addition, multiplication, and division. +In this example, we'll use the Claude Sonnet 4.5 model and define tools for addition, multiplication, and division. :::python ```python @@ -400,7 +399,9 @@ for (const message of result.messages) { ::: - To learn how to trace your agent with LangSmith, see the [LangSmith documentation](/langsmith/trace-with-langgraph). +Trace and debug your agent with [LangSmith](https://smith.langchain.com). Follow the [tracing quickstart](/langsmith/trace-with-langgraph) to get set up. When ready for production, see [Deploy](/langsmith/deployment) for hosting options. + +We recommend you also set up [LangSmith Engine](/langsmith/engine) which monitors your traces, detects issues, and proposes fixes. Congratulations! You've built your first agent using the LangGraph Graph API. @@ -475,7 +476,7 @@ class MessagesState(TypedDict): from langchain.messages import SystemMessage -def llm_call(state: dict): +def llm_call(state: MessagesState): """LLM decides whether to call a tool or not""" return { @@ -498,7 +499,7 @@ def llm_call(state: dict): from langchain.messages import ToolMessage -def tool_node(state: dict): +def tool_node(state: MessagesState): """Performs the tool call""" result = [] @@ -724,7 +725,7 @@ for (const message of result.messages) { ## 1. Define tools and model -In this example, we'll use the Claude Sonnet 4.6 model and define tools for addition, multiplication, and division. +In this example, we'll use the Claude Sonnet 4.5 model and define tools for addition, multiplication, and division. :::python @@ -940,8 +941,9 @@ def agent(messages: list[BaseMessage]): # Invoke messages = [HumanMessage(content="Add 3 and 4.")] -for chunk in agent.stream(messages, stream_mode="updates"): - print(chunk) +stream = agent.stream_events(messages, version="v3") +for snapshot in stream.values: + print(snapshot) print("\n") ``` ::: @@ -982,7 +984,9 @@ for (const message of result) { ::: - To learn how to trace your agent with LangSmith, see the [LangSmith documentation](/langsmith/trace-with-langgraph). +Trace and debug your agent with [LangSmith](https://smith.langchain.com). Follow the [tracing quickstart](/langsmith/trace-with-langgraph) to get set up. When ready for production, see [Deploy](/langsmith/deployment) for hosting options. + +We recommend you also set up [LangSmith Engine](/langsmith/engine) which monitors your traces, detects issues, and proposes fixes. Congratulations! You've built your first agent using the LangGraph Functional API. @@ -1099,8 +1103,9 @@ def agent(messages: list[BaseMessage]): # Invoke messages = [HumanMessage(content="Add 3 and 4.")] -for chunk in agent.stream(messages, stream_mode="updates"): - print(chunk) +stream = agent.stream_events(messages, version="v3") +for snapshot in stream.values: + print(snapshot) print("\n") ``` ::: diff --git a/sql-agent.mdx b/sql-agent.mdx index 177f60e..1cc05d8 100644 --- a/sql-agent.mdx +++ b/sql-agent.mdx @@ -5,7 +5,28 @@ sidebarTitle: Custom SQL agent import ChatModelTabsPy from '/snippets/chat-model-tabs.mdx'; import ChatModelTabsJS from '/snippets/chat-model-tabs-js.mdx'; - +import SqlAgentDownloadChinookPy from '/snippets/code-samples/sql-agent-download-chinook-py.mdx'; +import SqlAgentExploreDatabasePy from '/snippets/code-samples/sql-agent-explore-database-py.mdx'; +import LanggraphSqlAgentToolsPy from '/snippets/code-samples/langgraph-sql-agent-tools-py.mdx'; +import LanggraphSqlAgentDefineStepsPy from '/snippets/code-samples/langgraph-sql-agent-define-steps-py.mdx'; +import LanggraphSqlAgentAssembleAgentPy from '/snippets/code-samples/langgraph-sql-agent-assemble-agent-py.mdx'; +import LanggraphSqlAgentVisualizeGraphPy from '/snippets/code-samples/langgraph-sql-agent-visualize-graph-py.mdx'; +import LanggraphSqlAgentStreamAgentPy from '/snippets/code-samples/langgraph-sql-agent-stream-agent-py.mdx'; +import LanggraphSqlAgentHitlInterruptPy from '/snippets/code-samples/langgraph-sql-agent-hitl-interrupt-py.mdx'; +import LanggraphSqlAgentHitlAssemblePy from '/snippets/code-samples/langgraph-sql-agent-hitl-assemble-py.mdx'; +import LanggraphSqlAgentHitlStreamPy from '/snippets/code-samples/langgraph-sql-agent-hitl-stream-py.mdx'; +import LanggraphSqlAgentHitlResumePy from '/snippets/code-samples/langgraph-sql-agent-hitl-resume-py.mdx'; +import LanggraphSqlAgentDownloadChinookJs from '/snippets/code-samples/langgraph-sql-agent-download-chinook-js.mdx'; +import LanggraphSqlAgentExploreDatabaseJs from '/snippets/code-samples/langgraph-sql-agent-explore-database-js.mdx'; +import LanggraphSqlAgentToolsJs from '/snippets/code-samples/langgraph-sql-agent-tools-js.mdx'; +import LanggraphSqlAgentDefineStepsJs from '/snippets/code-samples/langgraph-sql-agent-define-steps-js.mdx'; +import LanggraphSqlAgentAssembleAgentJs from '/snippets/code-samples/langgraph-sql-agent-assemble-agent-js.mdx'; +import LanggraphSqlAgentVisualizeGraphJs from '/snippets/code-samples/langgraph-sql-agent-visualize-graph-js.mdx'; +import LanggraphSqlAgentStreamAgentJs from '/snippets/code-samples/langgraph-sql-agent-stream-agent-js.mdx'; +import LanggraphSqlAgentHitlInterruptJs from '/snippets/code-samples/langgraph-sql-agent-hitl-interrupt-js.mdx'; +import LanggraphSqlAgentHitlAssembleJs from '/snippets/code-samples/langgraph-sql-agent-hitl-assemble-js.mdx'; +import LanggraphSqlAgentHitlStreamJs from '/snippets/code-samples/langgraph-sql-agent-hitl-stream-js.mdx'; +import LanggraphSqlAgentHitlResumeJs from '/snippets/code-samples/langgraph-sql-agent-hitl-resume-js.mdx'; In this tutorial we will build a custom agent that can answer questions about a SQL database using LangGraph. @@ -34,20 +55,20 @@ We will cover the following concepts: :::python ```bash pip - pip install langchain langgraph langchain-community + pip install langchain langgraph ``` ::: :::js ```bash npm - npm i langchain @langchain/core @langchain/classic @langchain/langgraph @langchain/openai typeorm sqlite3 zod + npm i langchain @langchain/core @langchain/langgraph @langchain/openai sqlite3 zod ``` ```bash yarn - yarn add langchain @langchain/core @langchain/classic @langchain/langgraph @langchain/openai typeorm sqlite3 zod + yarn add langchain @langchain/core @langchain/langgraph @langchain/openai sqlite3 zod ``` ```bash pnpm - pnpm add langchain @langchain/core @langchain/classic @langchain/langgraph @langchain/openai typeorm sqlite3 zod + pnpm add langchain @langchain/core @langchain/langgraph @langchain/openai sqlite3 zod ``` ::: @@ -80,34 +101,12 @@ You will be creating a [SQLite database](https://www.sqlitetutorial.net/sqlite-s For convenience, we have hosted the database (`Chinook.db`) on a public GCS bucket. :::python -```python -import requests, pathlib - -url = "https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db" -local_path = pathlib.Path("Chinook.db") - -if local_path.exists(): - print(f"{local_path} already exists, skipping download.") -else: - response = requests.get(url) - if response.status_code == 200: - local_path.write_bytes(response.content) - print(f"File downloaded and saved as {local_path}") - else: - print(f"Failed to download the file. Status code: {response.status_code}") -``` - -We will use a handy SQL database wrapper available in the `langchain_community` package to interact with the database. The wrapper provides a simple interface to execute SQL queries and fetch results: -```python -from langchain_community.utilities import SQLDatabase + -db = SQLDatabase.from_uri("sqlite:///Chinook.db") +We will use Python's built-in `sqlite3` module to interact with the database: -print(f"Dialect: {db.dialect}") -print(f"Available tables: {db.get_usable_table_names()}") -print(f'Sample output: {db.run("SELECT * FROM Artist LIMIT 5;")}') -``` + ``` Dialect: sqlite Available tables: ['Album', 'Artist', 'Customer', 'Employee', 'Genre', 'Invoice', 'InvoiceLine', 'MediaType', 'Playlist', 'PlaylistTrack', 'Track'] @@ -115,45 +114,12 @@ Sample output: [(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Moris ``` ::: :::js -```typescript -import fs from "node:fs/promises"; -import path from "node:path"; - -const url = "https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db"; -const localPath = path.resolve("Chinook.db"); - -async function resolveDbPath() { - const exists = await fs.access(localPath).then(() => true).catch(() => false); - if (exists) { - console.log(`${localPath} already exists, skipping download.`); - return localPath; - } - const resp = await fetch(url); - if (!resp.ok) throw new Error(`Failed to download DB. Status code: ${resp.status}`); - const buf = Buffer.from(await resp.arrayBuffer()); - await fs.writeFile(localPath, buf); - console.log(`File downloaded and saved as ${localPath}`); - return localPath; -} -``` -We will use a handy SQL database wrapper available in the `@langchain/classic/sql_db` module to interact with the database. The wrapper provides a simple interface to execute SQL queries and fetch results: + -```typescript -import { SqlDatabase } from "@langchain/classic/sql_db"; -import { DataSource } from "typeorm"; +We will use the `sqlite3` library to interact with the database: -const dbPath = await resolveDbPath(); -const datasource = new DataSource({ type: "sqlite", database: dbPath }); -const db = await SqlDatabase.fromDataSourceParams({ appDataSource: datasource }); -const dialect = db.appDataSourceOptions.type; - -console.log(`Dialect: ${dialect}`); -const tableNames = db.allTables.map(t => t.tableName); -console.log(`Available tables: ${tableNames.join(", ")}`); -const sampleResults = await db.run("SELECT * FROM Artist LIMIT 5;"); -console.log(`Sample output: ${sampleResults}`); -``` + ``` Dialect: sqlite Available tables: Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track @@ -163,89 +129,31 @@ Sample output: [{"ArtistId":1,"Name":"AC/DC"},{"ArtistId":2,"Name":"Accept"},{"A ## 3. Add tools for database interactions -:::python -Use the `SQLDatabase` wrapper available in the `langchain_community` package to interact with the database. The wrapper provides a simple interface to execute SQL queries and fetch results: - -```python -from langchain_community.agent_toolkits import SQLDatabaseToolkit - -toolkit = SQLDatabaseToolkit(db=db, llm=model) + +The following database tools are minimal wrappers for demonstration purposes only. They are not intended to be secure or used in production. Use narrowly scoped database permissions and add application-specific validation before executing model-generated SQL. + -tools = toolkit.get_tools() +:::python +We can implement database [tools](/oss/langchain/tools) as thin wrappers using the `@tool` decorator from `langchain.tools`: -for tool in tools: - print(f"{tool.name}: {tool.description}\n") -``` + ``` -sql_db_query: Input to this tool is a detailed and correct SQL query, output is a result from the database. If the query is not correct, an error message will be returned. If an error is returned, rewrite the query, check the query, and try again. If you encounter an issue with Unknown column 'xxxx' in 'field list', use sql_db_schema to query the correct table fields. - -sql_db_schema: Input to this tool is a comma-separated list of tables, output is the schema and sample rows for those tables. Be sure that the tables actually exist by calling sql_db_list_tables first! Example Input: table1, table2, table3 - sql_db_list_tables: Input is an empty string, output is a comma-separated list of tables in the database. -sql_db_query_checker: Use this tool to double check if your query is correct before executing it. Always use this tool before executing a query with sql_db_query! +sql_db_schema: Input to this tool is a comma-separated list of tables, output is the schema and sample rows for those tables. + Be sure that the tables actually exist by calling sql_db_list_tables first! + Example Input: table1, table2, table3 + +sql_db_query: Input to this tool is a detailed and correct SQL query, output is a result from the database. + If the query is not correct, an error message will be returned. + If an error is returned, rewrite the query, check the query, and try again. + If you encounter an issue with Unknown column 'xxxx' in 'field list', use sql_db_schema to query the correct table fields. ``` ::: :::js We'll create custom tools to interact with the database: -```typescript -import { tool } from "langchain"; -import * as z from "zod"; - -// Tool to list all tables -const listTablesTool = tool( - async () => { - const tableNames = db.allTables.map(t => t.tableName); - return tableNames.join(", "); - }, - { - name: "sql_db_list_tables", - description: "Input is an empty string, output is a comma-separated list of tables in the database.", - schema: z.object({}), - } -); - -// Tool to get schema for specific tables -const getSchemaTool = tool( - async ({ table_names }) => { - const tables = table_names.split(",").map(t => t.trim()); - return await db.getTableInfo(tables); - }, - { - name: "sql_db_schema", - description: "Input to this tool is a comma-separated list of tables, output is the schema and sample rows for those tables. Be sure that the tables actually exist by calling sql_db_list_tables first! Example Input: table1, table2, table3", - schema: z.object({ - table_names: z.string().describe("Comma-separated list of table names"), - }), - } -); - -// Tool to execute SQL query -const queryTool = tool( - async ({ query }) => { - try { - const result = await db.run(query); - return typeof result === "string" ? result : JSON.stringify(result); - } catch (error) { - return `Error: ${error.message}`; - } - }, - { - name: "sql_db_query", - description: "Input to this tool is a detailed and correct SQL query, output is a result from the database. If the query is not correct, an error message will be returned. If an error is returned, rewrite the query, check the query, and try again.", - schema: z.object({ - query: z.string().describe("SQL query to execute"), - }), - } -); - -const tools = [listTablesTool, getSchemaTool, queryTool]; - -for (const tool of tools) { - console.log(`${tool.name}: ${tool.description}\n`); -} -``` + ``` sql_db_list_tables: Input is an empty string, output is a comma-separated list of tables in the database. @@ -267,224 +175,14 @@ We construct dedicated nodes for the following steps: Putting these steps in dedicated nodes lets us (1) force tool-calls when needed, and (2) customize the prompts associated with each step. :::python -```python -from typing import Literal - -from langchain.messages import AIMessage -from langchain_core.runnables import RunnableConfig -from langgraph.graph import END, START, MessagesState, StateGraph -from langgraph.prebuilt import ToolNode - - -get_schema_tool = next(tool for tool in tools if tool.name == "sql_db_schema") -get_schema_node = ToolNode([get_schema_tool], name="get_schema") - -run_query_tool = next(tool for tool in tools if tool.name == "sql_db_query") -run_query_node = ToolNode([run_query_tool], name="run_query") - - -# Example: create a predetermined tool call -def list_tables(state: MessagesState): - tool_call = { - "name": "sql_db_list_tables", - "args": {}, - "id": "abc123", - "type": "tool_call", - } - tool_call_message = AIMessage(content="", tool_calls=[tool_call]) - - list_tables_tool = next(tool for tool in tools if tool.name == "sql_db_list_tables") - tool_message = list_tables_tool.invoke(tool_call) - response = AIMessage(f"Available tables: {tool_message.content}") - - return {"messages": [tool_call_message, tool_message, response]} - -# Example: force a model to create a tool call -def call_get_schema(state: MessagesState): - # Note that LangChain enforces that all models accept `tool_choice="any"` - # as well as `tool_choice=`. - llm_with_tools = model.bind_tools([get_schema_tool], tool_choice="any") - response = llm_with_tools.invoke(state["messages"]) + - return {"messages": [response]} - - -generate_query_system_prompt = """ -You are an agent designed to interact with a SQL database. -Given an input question, create a syntactically correct {dialect} query to run, -then look at the results of the query and return the answer. Unless the user -specifies a specific number of examples they wish to obtain, always limit your -query to at most {top_k} results. - -You can order the results by a relevant column to return the most interesting -examples in the database. Never query for all the columns from a specific table, -only ask for the relevant columns given the question. - -DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database. -""".format( - dialect=db.dialect, - top_k=5, -) - - -def generate_query(state: MessagesState): - system_message = { - "role": "system", - "content": generate_query_system_prompt, - } - # We do not force a tool call here, to allow the model to - # respond naturally when it obtains the solution. - llm_with_tools = model.bind_tools([run_query_tool]) - response = llm_with_tools.invoke([system_message] + state["messages"]) - - return {"messages": [response]} - - -check_query_system_prompt = """ -You are a SQL expert with a strong attention to detail. -Double check the {dialect} query for common mistakes, including: -- Using NOT IN with NULL values -- Using UNION when UNION ALL should have been used -- Using BETWEEN for exclusive ranges -- Data type mismatch in predicates -- Properly quoting identifiers -- Using the correct number of arguments for functions -- Casting to the correct data type -- Using the proper columns for joins - -If there are any of the above mistakes, rewrite the query. If there are no mistakes, -just reproduce the original query. - -You will call the appropriate tool to execute the query after running this check. -""".format(dialect=db.dialect) - - -def check_query(state: MessagesState): - system_message = { - "role": "system", - "content": check_query_system_prompt, - } - - # Generate an artificial user message to check - tool_call = state["messages"][-1].tool_calls[0] - user_message = {"role": "user", "content": tool_call["args"]["query"]} - llm_with_tools = model.bind_tools([run_query_tool], tool_choice="any") - response = llm_with_tools.invoke([system_message, user_message]) - response.id = state["messages"][-1].id - - return {"messages": [response]} -``` ::: :::js -```typescript -import { AIMessage, ToolMessage, SystemMessage, HumanMessage } from "@langchain/core/messages"; -import { ToolNode } from "@langchain/langgraph/prebuilt"; -import { StateSchema, MessagesValue, GraphNode, StateGraph, START, END } from "@langchain/langgraph"; -import { z } from "zod/v4"; - -// Create tool nodes for schema and query execution -const getSchemaNode = new ToolNode([getSchemaTool]); -const runQueryNode = new ToolNode([queryTool]); - -// Define state schema -const MessagesState = new StateSchema({ - messages: MessagesValue, -}); - -// Example: create a predetermined tool call -const listTables: GraphNode = async (state) => { - const toolCall = { - name: "sql_db_list_tables", - args: {}, - id: "abc123", - type: "tool_call" as const, - }; - const toolCallMessage = new AIMessage({ - content: "", - tool_calls: [toolCall], - }); - - const toolMessage = await listTablesTool.invoke({}); - const response = new AIMessage(`Available tables: ${toolMessage}`); - - return { messages: [toolCallMessage, new ToolMessage({ content: toolMessage, tool_call_id: "abc123" }), response] }; -}; - -// Example: force a model to create a tool call -const callGetSchema: GraphNode = async (state) => { - const llmWithTools = model.bindTools([getSchemaTool], { - tool_choice: "any", - }); - const response = await llmWithTools.invoke(state.messages); - - return { messages: [response] }; -}; - -const topK = 5; - -const generateQuerySystemPrompt = ` -You are an agent designed to interact with a SQL database. -Given an input question, create a syntactically correct ${dialect} -query to run, then look at the results of the query and return the answer. Unless -the user specifies a specific number of examples they wish to obtain, always limit -your query to at most ${topK} results. - -You can order the results by a relevant column to return the most interesting -examples in the database. Never query for all the columns from a specific table, -only ask for the relevant columns given the question. - -DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database. -`; - -const generateQuery: GraphNode = async (state) => { - const systemMessage = new SystemMessage(generateQuerySystemPrompt); - // We do not force a tool call here, to allow the model to - // respond naturally when it obtains the solution. - const llmWithTools = model.bindTools([queryTool]); - const response = await llmWithTools.invoke([systemMessage, ...state.messages]); - - return { messages: [response] }; -}; - -const checkQuerySystemPrompt = ` -You are a SQL expert with a strong attention to detail. -Double check the ${dialect} query for common mistakes, including: -- Using NOT IN with NULL values -- Using UNION when UNION ALL should have been used -- Using BETWEEN for exclusive ranges -- Data type mismatch in predicates -- Properly quoting identifiers -- Using the correct number of arguments for functions -- Casting to the correct data type -- Using the proper columns for joins - -If there are any of the above mistakes, rewrite the query. If there are no mistakes, -just reproduce the original query. - -You will call the appropriate tool to execute the query after running this check. -`; - -const checkQuery: GraphNode = async (state) => { - const systemMessage = new SystemMessage(checkQuerySystemPrompt); - - // Generate an artificial user message to check - const lastMessage = state.messages[state.messages.length - 1]; - if (!lastMessage.tool_calls || lastMessage.tool_calls.length === 0) { - throw new Error("No tool calls found in the last message"); - } - const toolCall = lastMessage.tool_calls[0]; - const userMessage = new HumanMessage(toolCall.args.query); - const llmWithTools = model.bindTools([queryTool], { - tool_choice: "any", - }); - const response = await llmWithTools.invoke([systemMessage, userMessage]); - // Preserve the original message ID - response.id = lastMessage.id; - - return { messages: [response] }; -}; -``` + + + ::: ## 5. Implement the agent @@ -492,86 +190,22 @@ const checkQuery: GraphNode = async (state) => { We can now assemble these steps into a workflow using the [Graph API](/oss/langgraph/graph-api). We define a [conditional edge](/oss/langgraph/graph-api#conditional-edges) at the query generation step that will route to the query checker if a query is generated, or end if there are no tool calls present, such that the LLM has delivered a response to the query. :::python -```python -def should_continue(state: MessagesState) -> Literal[END, "check_query"]: - messages = state["messages"] - last_message = messages[-1] - if not last_message.tool_calls: - return END - else: - return "check_query" - - -builder = StateGraph(MessagesState) -builder.add_node(list_tables) -builder.add_node(call_get_schema) -builder.add_node(get_schema_node, "get_schema") -builder.add_node(generate_query) -builder.add_node(check_query) -builder.add_node(run_query_node, "run_query") - -builder.add_edge(START, "list_tables") -builder.add_edge("list_tables", "call_get_schema") -builder.add_edge("call_get_schema", "get_schema") -builder.add_edge("get_schema", "generate_query") -builder.add_conditional_edges( - "generate_query", - should_continue, -) -builder.add_edge("check_query", "run_query") -builder.add_edge("run_query", "generate_query") -agent = builder.compile() -``` + + We visualize the application below: -```python -from IPython.display import Image, display -from langchain_core.runnables.graph import CurveStyle, MermaidDrawMethod, NodeStyles -display(Image(agent.get_graph().draw_mermaid_png())) -``` + + ::: :::js -```typescript -import { StateGraph, ConditionalEdgeRouter } from "@langchain/langgraph"; - -const shouldContinue: ConditionalEdgeRouter = (state) => { - const messages = state.messages; - const lastMessage = messages[messages.length - 1]; - if (!lastMessage.tool_calls || lastMessage.tool_calls.length === 0) { - return END; - } else { - return "check_query"; - } -}; - -const builder = new StateGraph(MessagesState) - .addNode("list_tables", listTables) - .addNode("call_get_schema", callGetSchema) - .addNode("get_schema", getSchemaNode) - .addNode("generate_query", generateQuery) - .addNode("check_query", checkQuery) - .addNode("run_query", runQueryNode) - .addEdge(START, "list_tables") - .addEdge("list_tables", "call_get_schema") - .addEdge("call_get_schema", "get_schema") - .addEdge("get_schema", "generate_query") - .addConditionalEdges("generate_query", shouldContinue) - .addEdge("check_query", "run_query") - .addEdge("run_query", "generate_query"); - -const agent = builder.compile(); -``` + + + We visualize the application below: -```typescript -import * as fs from "node:fs/promises"; -const drawableGraph = await agent.getGraphAsync(); -const image = await drawableGraph.drawMermaidPng(); -const imageBuffer = new Uint8Array(await image.arrayBuffer()); + -await fs.writeFile("graph.png", imageBuffer); -``` ::: + ::: :::js -```typescript -const question = "Which genre on average has the longest tracks?"; - -const stream = await agent.stream( - { messages: [{ role: "user", content: question }] }, - { streamMode: "values" } -); - -for await (const step of stream) { - if (step.messages && step.messages.length > 0) { - const lastMessage = step.messages[step.messages.length - 1]; - console.log(lastMessage.toFormattedString()); - } -} -``` + + + ::: ``` ================================ Human Message ================================= @@ -703,84 +319,14 @@ Here we leverage LangGraph's [human-in-the-loop](/oss/langgraph/interrupts) feat Let's wrap the `sql_db_query` tool in a node that receives human input. We can implement this using the [interrupt](/oss/langgraph/interrupts) function. Below, we allow for input to approve the tool call, edit its arguments, or provide user feedback. :::python -```python -from langchain_core.runnables import RunnableConfig -from langchain.tools import tool -from langgraph.types import interrupt - -@tool( - run_query_tool.name, - description=run_query_tool.description, - args_schema=run_query_tool.args_schema -) -def run_query_tool_with_interrupt(config: RunnableConfig, **tool_input): - request = { - "action": run_query_tool.name, - "args": tool_input, - "description": "Please review the tool call" - } - response = interrupt([request]) # [!code highlight] - # approve the tool call - if response["type"] == "accept": - tool_response = run_query_tool.invoke(tool_input, config) - # update tool call args - elif response["type"] == "edit": - tool_input = response["args"]["args"] - tool_response = run_query_tool.invoke(tool_input, config) - # respond to the LLM with user feedback - elif response["type"] == "response": - user_feedback = response["args"] - tool_response = user_feedback - else: - raise ValueError(f"Unsupported interrupt response type: {response['type']}") - - return tool_response - -# Redefine the tool node to use the interrupt version -run_query_node = ToolNode([run_query_tool_with_interrupt], name="run_query") # [!code highlight] -``` + + + ::: :::js -```typescript -import { RunnableConfig } from "@langchain/core/runnables"; -import { tool } from "langchain"; -import { interrupt } from "@langchain/langgraph"; -import * as z from "zod"; - -const queryToolWithInterrupt = tool( - async (input, config: RunnableConfig) => { - const request = { - action: queryTool.name, - args: input, - description: "Please review the tool call", - }; - const response = interrupt([request]); // [!code highlight] - // approve the tool call - if (response.type === "accept") { - const toolResponse = await queryTool.invoke(input, config); - return toolResponse; - } - // update tool call args - else if (response.type === "edit") { - const editedInput = response.args.args; - const toolResponse = await queryTool.invoke(editedInput, config); - return toolResponse; - } - // respond to the LLM with user feedback - else if (response.type === "response") { - const userFeedback = response.args; - return userFeedback; - } else { - throw new Error(`Unsupported interrupt response type: ${response.type}`); - } - }, - { - name: queryTool.name, - description: queryTool.description, - schema: queryTool.schema, - } -); -``` + + + ::: @@ -790,123 +336,27 @@ The above implementation follows the [tool interrupt example](/oss/langgraph/int Let's now re-assemble our graph. We will replace the programmatic check with human review. Note that we now include a [checkpointer](/oss/langgraph/persistence); this is required to pause and resume the run. :::python -```python -from langgraph.checkpoint.memory import InMemorySaver - -def should_continue(state: MessagesState) -> Literal[END, "run_query"]: - messages = state["messages"] - last_message = messages[-1] - if not last_message.tool_calls: - return END - else: - return "run_query" - -builder = StateGraph(MessagesState) -builder.add_node(list_tables) -builder.add_node(call_get_schema) -builder.add_node(get_schema_node, "get_schema") -builder.add_node(generate_query) -builder.add_node(run_query_node, "run_query") - -builder.add_edge(START, "list_tables") -builder.add_edge("list_tables", "call_get_schema") -builder.add_edge("call_get_schema", "get_schema") -builder.add_edge("get_schema", "generate_query") -builder.add_conditional_edges( - "generate_query", - should_continue, -) -builder.add_edge("run_query", "generate_query") -checkpointer = InMemorySaver() # [!code highlight] -agent = builder.compile(checkpointer=checkpointer) # [!code highlight] -``` + + ::: :::js -```typescript -import { MemorySaver, ConditionalEdgeRouter } from "@langchain/langgraph"; - -const shouldContinueWithHuman: ConditionalEdgeRouter = (state) => { - const messages = state.messages; - const lastMessage = messages[messages.length - 1]; - if (!lastMessage.tool_calls || lastMessage.tool_calls.length === 0) { - return END; - } else { - return "run_query"; - } -}; - -const runQueryNodeWithInterrupt = new ToolNode([queryToolWithInterrupt]); - -const builderWithHuman = new StateGraph(MessagesState) - .addNode("list_tables", listTables) - .addNode("call_get_schema", callGetSchema) - .addNode("get_schema", getSchemaNode) - .addNode("generate_query", generateQuery) - .addNode("run_query", runQueryNodeWithInterrupt) - .addEdge(START, "list_tables") - .addEdge("list_tables", "call_get_schema") - .addEdge("call_get_schema", "get_schema") - .addEdge("get_schema", "generate_query") - .addConditionalEdges("generate_query", shouldContinueWithHuman) - .addEdge("run_query", "generate_query"); - -const checkpointer = new MemorySaver(); // [!code highlight] -const agentWithHuman = builderWithHuman.compile({ checkpointer }); // [!code highlight] -``` + + + ::: We can invoke the graph as before. This time, execution is interrupted: :::python -```python -import json - -config = {"configurable": {"thread_id": "1"}} - -question = "Which genre on average has the longest tracks?" - -for step in agent.stream( - {"messages": [{"role": "user", "content": question}]}, - config, - stream_mode="values", -): - if "messages" in step: - step["messages"][-1].pretty_print() - elif "__interrupt__" in step: - action = step["__interrupt__"][0] - print("INTERRUPTED:") - for request in action.value: - print(json.dumps(request, indent=2)) - else: - pass -``` -::: -:::js -```typescript -const config = { configurable: { thread_id: "1" } }; -const question = "Which genre on average has the longest tracks?"; + -const stream = await agentWithHuman.stream( - { messages: [{ role: "user", content: question }] }, - { ...config, streamMode: "values" } -); +::: +:::js -for await (const step of stream) { - if (step.messages && step.messages.length > 0) { - const lastMessage = step.messages[step.messages.length - 1]; - console.log(lastMessage.toFormattedString()); - } -} + -// Check for interrupts -const state = await agentWithHuman.getState(config); -if (state.next.length > 0) { - console.log("\nINTERRUPTED:"); - console.log(JSON.stringify(state.tasks[0].interrupts[0], null, 2)); -} -``` ::: ``` @@ -924,44 +374,14 @@ INTERRUPTED: We can accept or edit the tool call using [Command](/oss/langgraph/use-graph-api#combine-control-flow-and-state-updates-with-command): :::python -```python -from langgraph.types import Command - - -for step in agent.stream( - Command(resume={"type": "accept"}), - # Command(resume={"type": "edit", "args": {"query": "..."}}), - config, - stream_mode="values", -): - if "messages" in step: - step["messages"][-1].pretty_print() - elif "__interrupt__" in step: - action = step["__interrupt__"][0] - print("INTERRUPTED:") - for request in action.value: - print(json.dumps(request, indent=2)) - else: - pass -``` + + + ::: :::js -```typescript -import { Command } from "@langchain/langgraph"; - -const resumeStream = await agentWithHuman.stream( - new Command({ resume: { type: "accept" } }), - // new Command({ resume: { type: "edit", args: { query: "..." } } }), - { ...config, streamMode: "values" } -); - -for await (const step of resumeStream) { - if (step.messages && step.messages.length > 0) { - const lastMessage = step.messages[step.messages.length - 1]; - console.log(lastMessage.toFormattedString()); - } -} -``` + + + ::: ``` diff --git a/stores.mdx b/stores.mdx new file mode 100644 index 0000000..9c930f3 --- /dev/null +++ b/stores.mdx @@ -0,0 +1,669 @@ +--- +title: Stores +description: LangGraph stores provide cross-thread long-term memory, complementing per-thread checkpointer persistence. +--- + +import StoreListNamespaceSearchPy from '/snippets/code-samples/store-list-namespace-search-py.mdx'; +import StoreListNamespaceSearchJs from '/snippets/code-samples/store-list-namespace-search-js.mdx'; +import StoreListNamespacePaginatePy from '/snippets/code-samples/store-list-namespace-paginate-py.mdx'; +import StoreListNamespacePaginateJs from '/snippets/code-samples/store-list-namespace-paginate-js.mdx'; +import StoreListNamespaceListPy from '/snippets/code-samples/store-list-namespace-list-py.mdx'; +import StoreListNamespaceListJs from '/snippets/code-samples/store-list-namespace-list-js.mdx'; + +Stores let agents persist information across threads, including user preferences, accumulated knowledge, and facts that should survive beyond a single conversation. Unlike [checkpointers](/oss/langgraph/checkpointers), which save the full graph state scoped to one thread, stores hold arbitrary key-value data accessible from any thread. + +![Model of shared state](/oss/images/shared_state.png) + + +**Agent Server handles stores automatically** +When using the [Agent Server](/langsmith/agent-server), you do not need to implement or configure stores manually. The API handles all storage infrastructure for you behind the scenes. + + + +@[InMemoryStore] is suitable for development and testing. For production, use a persistent store like `PostgresStore`, `MongoDBStore`, or `RedisStore`. All implementations extend @[BaseStore], which is the type annotation to use in node function signatures. + + +## Basic usage + +The following code snippet shows the @[InMemoryStore] in isolation without using LangGraph: + +:::python +```python +from langgraph.store.memory import InMemoryStore +store = InMemoryStore() +``` +::: + +:::js +```typescript +import { MemoryStore } from "@langchain/langgraph"; + +const memoryStore = new MemoryStore(); +``` +::: + +Memories are namespaced by a `tuple`, which is `(, "memories")` in the following example. The namespace can be any length and represent anything, does not have to be user specific. + +:::python +```python +user_id = "1" +namespace_for_memory = (user_id, "memories") +``` +::: + +:::js +```typescript +const userId = "1"; +const namespaceForMemory = [userId, "memories"]; +``` +::: + +Use the `store.put` method to save memories to the namespace in the store. 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. + +:::python +```python +memory_id = str(uuid.uuid4()) +memory = {"food_preference" : "I like pizza"} +store.put(namespace_for_memory, memory_id, memory) +``` +::: + +:::js +```typescript +const memoryId = crypto.randomUUID(); +const memory = { food_preference: "I like pizza" }; +await memoryStore.put(namespaceForMemory, memoryId, memory); +``` +::: + +Read out memories from your namespace using the `store.search` method, which returns memories for a given user as a list, up to the `limit` argument (default `10`). With `InMemoryStore`, items are returned in insertion order, so the most recent memory is last in the list; other backends may order memories differently (see [Listing items in a namespace](#listing-items-in-a-namespace)). + +:::python +```python +memories = 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'} +``` + +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 with `.dict`. + +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 tuple of strings, the namespace of this memory type + + + While the type is `tuple[str, ...]`, it may be serialized as a list when converted to JSON (for example, `['1', 'memories']`). + + +* `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 tuple of strings, the namespace of this memory type + + + While the type is `tuple`, it may be serialized as a list when converted to JSON (for example, `['1', 'memories']`). + + +* `createdAt`: Timestamp for when this memory was created +* `updatedAt`: Timestamp for when this memory was updated + +::: + +## Listing items in a namespace + +:::python +Calling [`store.search`](https://reference.langchain.com/python/langgraph/store/#langgraph.store.base.BaseStore.search) (or the async [`store.asearch`](https://reference.langchain.com/python/langgraph/store/#langgraph.store.base.BaseStore.asearch)) with no `query` and no `filter` returns the items stored under `namespace_prefix`, up to `limit`. Use this to enumerate everything in a namespace when you don't need semantic ranking. +::: + +:::js +Calling `store.search` with no `query` and no `filter` returns the items stored under the namespace prefix, up to `limit`. Use this to enumerate everything in a namespace when you don't need semantic ranking. +::: + +:::python + + + +::: + +:::js + + + +::: + +Three behaviors to keep in mind: + +- **`namespace_prefix` matches by prefix, not exactly.** `("alice",)` also returns items under `("alice", "memories")`, `("alice", "preferences")`, and so on. To restrict to a single level, pass the full namespace or filter the returned items client-side on `item.namespace`. +- **Results past `limit` are silently truncated.** There is no overflow signal—set `limit` above your expected maximum, or paginate with `offset`. +- **Default ordering depends on the store backend.** `PostgresStore` and `AsyncPostgresStore` return results ordered by `updated_at` descending (most recently updated first). `InMemoryStore` returns results in insertion order (most recently inserted last). Do not rely on a specific order across implementations; sort client-side on `item.updated_at` if order matters. + +:::python +To page through a large namespace: + + + +::: + +:::js +To page through a large namespace: + + + +::: + +:::python +To discover which namespaces exist (for example, to iterate over every user before listing their memories), use [`store.list_namespaces`](https://reference.langchain.com/python/langgraph/store/#langgraph.store.base.BaseStore.list_namespaces) or [`store.alist_namespaces`](https://reference.langchain.com/python/langgraph/store/#langgraph.store.base.BaseStore.alist_namespaces): + + + +::: + +:::js +To discover which namespaces exist (for example, to iterate over every user before listing their memories), use `store.listNamespaces`: + + + +::: + +## Semantic search + +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 + } +) +``` +::: + +:::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 +) +``` +::: + +:::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 +}); +``` +::: + +You can control which parts of your memories get embedded by configuring the `fields` parameter or by specifying the `index` parameter when storing memories: + +:::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, + crypto.randomUUID(), + { + 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, + crypto.randomUUID(), + { system_info: "Last updated: 2024-01-01" }, + { index: false } +); +``` +::: + +## Using in LangGraph + +:::python +The store works hand-in-hand with the checkpointer: the checkpointer saves state to threads, as discussed above, and the store allows you to store arbitrary information for access _across_ threads. Compile the graph with both the checkpointer and the store as follows. + +```python +from dataclasses import dataclass +from langgraph.checkpoint.memory import InMemorySaver + +@dataclass +class Context: + user_id: str + +# We need this because we want to enable threads (conversations) +checkpointer = InMemorySaver() + +# ... Define the graph ... + +# Compile the graph with the checkpointer and store +builder = StateGraph(MessagesState, context_schema=Context) +# ... add nodes and edges ... +graph = builder.compile(checkpointer=checkpointer, store=store) +``` +::: + +:::js +The `memoryStore` works hand-in-hand with the checkpointer: the checkpointer saves state to threads, as discussed above, and the `memoryStore` allows you to store arbitrary information for access _across_ threads. 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 }); +``` +::: + +Then invoke the graph with a `thread_id`, as before, and also with a `user_id`, which serves as the namespace for memories for this particular user as before. + +:::python +```python +# Invoke the graph +config = {"configurable": {"thread_id": "1"}} + +# First let's just say hi to the AI +for update in graph.stream( + {"messages": [{"role": "user", "content": "hi"}]}, + config, + stream_mode="updates", + context=Context(user_id="1"), +): + print(update) +``` +::: + +:::js +```typescript +// Invoke the graph +const userId = "1"; +const config = { configurable: { thread_id: "1" }, context: { 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); +} +``` +::: + +:::python +You can access the store and the `user_id` from _any node_ by using the `Runtime` object. The `Runtime` is automatically injected by LangGraph when you add it as a parameter to your node function. You can use it to save memories: + +```python +from langgraph.runtime import Runtime +from dataclasses import dataclass + +@dataclass +class Context: + user_id: str + +async def update_memory(state: MessagesState, runtime: Runtime[Context]): + + # Get the user id from the runtime context + user_id = runtime.context.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 + await runtime.store.aput(namespace, memory_id, {"memory": memory}) + +``` + + +::: + +:::js +You can access the store and the `userId` from _any node_ with the `runtime` argument. You can use it to save memories: + +```typescript +import { StateSchema, MessagesValue, Runtime } from "@langchain/langgraph"; + +const MessagesState = new StateSchema({ + messages: MessagesValue, +}); + +const updateMemory: GraphNode = async (state, runtime) => { + // 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 + const memory = "Some memory content"; + + // Create a new memory ID + const memoryId = crypto.randomUUID(); + + // We create a new memory + await runtime.store?.put(namespace, memoryId, { memory }); +}; +``` +::: + +You can also access the store from any node and use the `store.search` method to get memories. Memories are returned as a list of objects that can be converted to a dictionary. + +:::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'} +``` +::: + +:::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' +// } +``` +::: + +You access the memories and use them in model calls. + +:::python +```python +from dataclasses import dataclass +from langgraph.runtime import Runtime + +@dataclass +class Context: + user_id: str + +async def call_model(state: MessagesState, runtime: Runtime[Context]): + # Get the user id from the runtime context + user_id = runtime.context.user_id + + # Namespace the memory + namespace = (user_id, "memories") + + # Search based on the most recent message + memories = await runtime.store.asearch( + namespace, + query=state["messages"][-1].content, + limit=3 + ) + info = "\n".join([d.value["memory"] for d in memories]) + + # ... Use memories in the model call +``` +::: + +:::js +```typescript +const callModel: GraphNode = async (state, runtime) => { + // Get the user id from the config + const userId = runtime.context?.user_id; + + // Namespace the memory + const namespace = [userId, "memories"]; + + // Search based on the most recent message + const memories = await runtime.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 +}; +``` +::: + +If you create a new thread, you can still access the same memories so long as the `user_id` is the same. + +:::python +```python +# Invoke the graph on a new thread +config = {"configurable": {"thread_id": "2"}} + +# Let's say hi again +for update in graph.stream( + {"messages": [{"role": "user", "content": "hi, tell me about my memories"}]}, + config, + stream_mode="updates", + context=Context(user_id="1"), +): + print(update) +``` +::: + +:::js +```typescript +// Invoke the graph +const config = { configurable: { thread_id: "2" }, context: { userId: "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); +} +``` +::: + +When you use LangSmith locally (e.g., in [Studio](/langsmith/studio)) or [hosted](/langsmith/platform-setup), the base store is available to use by default and you do not need to specify it during graph compilation. To enable semantic search, however, you **do** need to configure the indexing settings in your `langgraph.json` file. For example: + +```json +{ + ... + "store": { + "index": { + "embed": "openai:text-embeddings-3-small", + "dims": 1536, + "fields": ["$"] + } + } +} +``` + +See the [deployment guide](/langsmith/semantic-search) for more details and configuration options. + +:::python +## Build a custom store + + +To use a storage backend other than the built-in implementations, subclass @[BaseStore] and implement its required methods. The built-in @[InMemoryStore] is the simplest reference implementation. + +### Base contract + +All five async methods are required. Sync counterparts (`put`, `get`, `delete`, `search`, `list_namespaces`) are optional but recommended for compatibility with sync graph execution. + +| Method | Description | +|--------|-------------| +| `aput(namespace, key, value, index=None)` | Store or overwrite a single item | +| `aget(namespace, key)` | Retrieve a single item by key; return `None` if missing | +| `adelete(namespace, key)` | Delete a single item | +| `asearch(namespace_prefix, *, query=None, filter=None, limit=10, offset=0)` | Search items under a namespace prefix; optionally by semantic query | +| `alist_namespaces(*, prefix=None, suffix=None, max_depth=None, limit=100, offset=0)` | List namespaces matching a prefix/suffix pattern | + +Look up exact signatures before implementing: + +```python +import inspect +from langgraph.store.base import BaseStore +print(inspect.getsource(BaseStore)) +``` + +### Namespace design + +Namespaces are tuples of strings, e.g. `("user_id", "memories")`. Store implementations must support: + +- **Prefix matching**: `asearch(("alice",))` returns items under `("alice",)`, `("alice", "memories")`, and any other sub-namespace. +- **Exact key lookup**: `aget(("alice", "memories"), "some-key")` must be O(1) or close to it. + +For SQL backends, a common schema: + +```sql +CREATE TABLE store_items ( + namespace TEXT[] NOT NULL, + key TEXT NOT NULL, + value JSONB NOT NULL, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + PRIMARY KEY (namespace, key) +); + +CREATE INDEX ON store_items USING gin(namespace); +``` + +### Serialization + +Store values are plain Python dicts — no special serializer is required. Serialize with `json.dumps` / `json.loads` or a JSONB column directly. Do not store raw Python objects that are not JSON-serializable. + +### Semantic search support + +If your backend supports vector search, implement the `query` parameter on `asearch`: + +- Accept a `query: str | None` argument. +- When `query` is not `None`, embed it and rank results by cosine similarity. +- Results should include a `score` field on each `Item` when `query` is provided. + +If your backend does not support vector search, raise `NotImplementedError` when `query` is passed. + +### Testing + +There is currently no conformance suite for custom stores. Test against @[InMemoryStore] as the reference: + +```python +import pytest +from langgraph.store.memory import InMemoryStore +from your_module import YourStore + +@pytest.fixture +async def store(): + async with YourStore.create() as s: + yield s + +@pytest.fixture +def reference(): + return InMemoryStore() + +async def test_put_and_get(store, reference): + ns = ("test", "ns") + for s in [store, reference]: + await s.aput(ns, "k1", {"val": 1}) + item = await s.aget(ns, "k1") + assert item is not None + assert item.value == {"val": 1} + +async def test_delete(store, reference): + ns = ("test", "ns") + for s in [store, reference]: + await s.aput(ns, "k1", {"val": 1}) + await s.adelete(ns, "k1") + assert await s.aget(ns, "k1") is None + +async def test_search_prefix(store, reference): + for s in [store, reference]: + await s.aput(("user", "memories"), "m1", {"text": "likes pizza"}) + results = await s.asearch(("user",)) + assert any(r.key == "m1" for r in results) +``` +::: + +### Next steps + +- [Add a custom store to Agent Server](/langsmith/custom-store) — deploying your implementation +- [Checkpointers](/oss/langgraph/checkpointers) — thread-scoped state persistence diff --git a/streaming.mdx b/streaming.mdx index 8e33c37..814a305 100644 --- a/streaming.mdx +++ b/streaming.mdx @@ -5,7 +5,11 @@ title: Streaming import NostreamTagPy from '/snippets/code-samples/nostream-tag-py.mdx'; import NostreamTagJs from '/snippets/code-samples/nostream-tag-js.mdx'; -LangGraph implements a streaming system to surface real-time updates. Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs. + +For new applications, we recommend [event streaming](/oss/langgraph/event-streaming)—the typed-projection API introduced in LangGraph v1.2. Event streaming gives you separate iterators per projection (messages, values, subgraphs, output) so you can consume them independently instead of branching on `stream_mode` chunks. + + +This page covers LangGraph's stream-mode API. It exposes graph execution through stream modes such as `updates`, `values`, `messages`, `custom`, `checkpoints`, `tasks`, and `debug`. Use it when you need direct access to graph-runtime events or specific stream-mode output. ## Get started @@ -88,6 +92,10 @@ for await (const chunk of await graph.stream(inputs, { ``` ::: + +Debug streaming events, inspect token-by-token LLM output, and monitor latency with [LangSmith](https://smith.langchain.com). Follow the [tracing quickstart](/langsmith/trace-with-langgraph) to get set up. + + :::python ### Stream output format (v2) @@ -338,7 +346,7 @@ class MyState: joke: str = "" -model = init_chat_model(model="gpt-4.1-mini") +model = init_chat_model(model="gpt-5.4-mini") def call_model(state: MyState): """Call the LLM to generate a joke about a topic""" @@ -389,7 +397,7 @@ const MyState = new StateSchema({ joke: z.string().default(""), }); -const model = new ChatOpenAI({ model: "gpt-4.1-mini" }); +const model = new ChatOpenAI({ model: "gpt-5.4-mini" }); const callModel: GraphNode = async (state) => { // Call the LLM to generate a joke about a topic @@ -428,9 +436,9 @@ You can associate `tags` with LLM invocations to filter the streamed tokens by L from langchain.chat_models import init_chat_model # model_1 is tagged with "joke" -model_1 = init_chat_model(model="gpt-4.1-mini", tags=['joke']) +model_1 = init_chat_model(model="gpt-5.4-mini", tags=['joke']) # model_2 is tagged with "poem" -model_2 = init_chat_model(model="gpt-4.1-mini", tags=['poem']) +model_2 = init_chat_model(model="gpt-5.4-mini", tags=['poem']) graph = ... # define a graph that uses these LLMs @@ -456,12 +464,12 @@ import { ChatOpenAI } from "@langchain/openai"; // model1 is tagged with "joke" const model1 = new ChatOpenAI({ - model: "gpt-4.1-mini", + model: "gpt-5.4-mini", tags: ['joke'] }); // model2 is tagged with "poem" const model2 = new ChatOpenAI({ - model: "gpt-4.1-mini", + model: "gpt-5.4-mini", tags: ['poem'] }); @@ -491,9 +499,9 @@ for await (const [msg, metadata] of await graph.stream( from langgraph.graph import START, StateGraph # The joke_model is tagged with "joke" - joke_model = init_chat_model(model="gpt-4.1-mini", tags=["joke"]) + joke_model = init_chat_model(model="gpt-5.4-mini", tags=["joke"]) # The poem_model is tagged with "poem" - poem_model = init_chat_model(model="gpt-4.1-mini", tags=["poem"]) + poem_model = init_chat_model(model="gpt-5.4-mini", tags=["poem"]) class State(TypedDict): @@ -550,12 +558,12 @@ for await (const [msg, metadata] of await graph.stream( // The jokeModel is tagged with "joke" const jokeModel = new ChatOpenAI({ - model: "gpt-4.1-mini", + model: "gpt-5.4-mini", tags: ["joke"] }); // The poemModel is tagged with "poem" const poemModel = new ChatOpenAI({ - model: "gpt-4.1-mini", + model: "gpt-5.4-mini", tags: ["poem"] }); @@ -673,7 +681,7 @@ for await (const [msg, metadata] of await graph.stream( from langgraph.graph import START, StateGraph from langchain_openai import ChatOpenAI - model = ChatOpenAI(model="gpt-4.1-mini") + model = ChatOpenAI(model="gpt-5.4-mini") class State(TypedDict): @@ -730,7 +738,7 @@ for await (const [msg, metadata] of await graph.stream( import { StateGraph, StateSchema, GraphNode, START } from "@langchain/langgraph"; import * as z from "zod"; - const model = new ChatOpenAI({ model: "gpt-4.1-mini" }); + const model = new ChatOpenAI({ model: "gpt-5.4-mini" }); const State = new StateSchema({ topic: z.string(), @@ -1020,7 +1028,7 @@ for await (const [mode, chunk] of await graph.stream( #### Use tool progress in React with `useStream` -The `useStream` hook from `@langchain/langgraph-sdk/react` exposes a `toolProgress` array when you include `"tools"` in your stream modes. Each entry is a `ToolProgress` object that tracks the current state of a running tool: +The @[`useStream`] hook from `@langchain/langgraph-sdk/react` exposes a `toolProgress` array when you include `"tools"` in your stream modes. Each entry is a `ToolProgress` object that tracks the current state of a running tool: | Field | Description | |-------|-------------| @@ -1618,7 +1626,7 @@ for await (const chunk of await graph.stream( from openai import AsyncOpenAI openai_client = AsyncOpenAI() - model_name = "gpt-4.1-mini" + model_name = "gpt-5.4-mini" async def stream_tokens(model_name: str, messages: list[dict]): @@ -1735,7 +1743,7 @@ for await (const chunk of await graph.stream( import OpenAI from "openai"; const openaiClient = new OpenAI(); - const modelName = "gpt-4.1-mini"; + const modelName = "gpt-5.4-mini"; async function* streamTokens(modelName: string, messages: any[]) { const response = await openaiClient.chat.completions.create({ @@ -2000,7 +2008,7 @@ This limits LangGraph ability to automatically propagate context, and affects La from langgraph.graph import START, StateGraph from langchain.chat_models import init_chat_model - model = init_chat_model(model="gpt-4.1-mini") + model = init_chat_model(model="gpt-5.4-mini") class State(TypedDict): topic: str diff --git a/studio.mdx b/studio.mdx index 435dbc8..a9a7600 100644 --- a/studio.mdx +++ b/studio.mdx @@ -6,14 +6,14 @@ When building agents with LangChain locally, it's helpful to visualize what's ha Studio connects to your locally running agent to show you each step your agent takes: the prompts sent to the model, tool calls and their results, and the final output. You can test different inputs, inspect intermediate states, and iterate on your agent's behavior without additional code or deployment. -This page describes how to set up Studio with your local LangChain agent. +This pages describes how to set up Studio with your local LangChain agent. ## Prerequisites Before you begin, ensure you have the following: - **A LangSmith account**: Sign up (for free) or log in at [smith.langchain.com](https://smith.langchain.com). -- **A LangSmith API key**: Follow the [Create an API key](/langsmith/create-account-api-key#create-an-api-key) guide. +- **A LangSmith API key**: Follow the [Create an API key](/langsmith/create-account-api-key) guide. - If you don't want data [traced](/langsmith/observability-concepts#traces) to LangSmith, set `LANGSMITH_TRACING=false` in your application's `.env` file. With tracing disabled, no data leaves your local server. ## Set up local Agent server @@ -55,7 +55,7 @@ def send_email(to: str, subject: str, body: str): return f"Email sent to {to}" agent = create_agent( - "gpt-4.1", + "gpt-5.5", tools=[send_email], system_prompt="You are an email assistant. Always use the send_email tool.", ) @@ -79,7 +79,7 @@ function sendEmail(to: string, subject: string, body: string): string { } const agent = createAgent({ - model: "gpt-4.1", + model: "gpt-5.5", tools: [sendEmail], systemPrompt: "You are an email assistant. Always use the sendEmail tool.", }); @@ -214,7 +214,7 @@ With Studio connected to your local agent, you can iterate quickly on your agent The development server supports hot-reloading—make changes to prompts or tool signatures in your code, and Studio reflects them immediately. Re-run conversation threads from any step to test your changes without starting over. This workflow scales from simple single-tool agents to complex multi-node graphs. -For more information on how to run Studio, refer to the following guides in the [LangSmith docs](/langsmith/home): +For more information on how to run Studio, refer to the following guides in the [LangSmith docs](/langsmith/observability): - [Run application](/langsmith/use-studio#run-application) - [Manage assistants](/langsmith/use-studio#manage-assistants) diff --git a/test.mdx b/test.mdx index 5fc5a75..891835e 100644 --- a/test.mdx +++ b/test.mdx @@ -3,7 +3,6 @@ title: Test --- - After you've prototyped your LangGraph agent, a natural next step is to add tests. This guide covers some useful patterns you can use when writing unit tests. :::python diff --git a/thinking-in-langgraph.mdx b/thinking-in-langgraph.mdx index b763243..07298b2 100644 --- a/thinking-in-langgraph.mdx +++ b/thinking-in-langgraph.mdx @@ -3,6 +3,8 @@ title: Thinking in LangGraph description: Learn how to think about building agents with LangGraph --- +import LanggraphThinkingHitlV2Py from '/snippets/code-samples/langgraph-thinking-hitl-v2-py.mdx'; + When you build an agent with LangGraph, you will first break it apart into discrete steps called **nodes**. Then, you will describe the different decisions and transitions from each of your nodes. Finally, you connect nodes together through a shared **state** that each node can read from and write to. In this walkthrough, we'll guide you through the thought process of building a customer support email agent with LangGraph. @@ -55,7 +57,7 @@ flowchart TD H --> J[END] I --> J[END] - classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E3A8A + classDef process fill:#E5F4FF,stroke:#006DDD,stroke-width:2px,color:#030710 class A,B,C,D,E,F,G,H,I,J process ``` @@ -295,14 +297,17 @@ Different errors need different handling strategies: | Transient errors (network issues, rate limits) | System (automatic) | Retry policy | Temporary failures that usually resolve on retry | | LLM-recoverable errors (tool failures, parsing issues) | LLM | Store error in state and loop back | LLM can see the error and adjust its approach | | User-fixable errors (missing information, unclear instructions) | Human | Pause with `interrupt()` | Need user input to proceed | +| Recoverable failure after retries | Developer (declarative) | `error_handler` | Run a compensation/recovery branch after retry exhaustion | | Unexpected errors | Developer | Let them bubble up | Unknown issues that need debugging | - Add a retry policy to automatically retry network issues and rate limits: + Add a retry policy to automatically retry network issues and rate limits. :::python + Combine with `timeout=` to cap each attempt. See [Fault tolerance](/oss/langgraph/fault-tolerance) for the full lifecycle. + ```python from langgraph.types import RetryPolicy @@ -469,6 +474,39 @@ Different errors need different handling strategies: ::: + + + After retries are exhausted, run a recovery function that updates state and routes to a compensation branch. + + :::python + + See [Fault tolerance](/oss/langgraph/fault-tolerance#error-handling) for the full pattern. + + + `error_handler` requires `langgraph>=1.2`. + + + ```python + from langgraph.errors import NodeError + from langgraph.types import Command, RetryPolicy + + def payment_error_handler(state: State, error: NodeError) -> Command: + return Command( + update={"status": f"compensated: {error.error}"}, + goto="finalize", + ) + + workflow.add_node( + "charge_payment", + charge_payment, + retry_policy=RetryPolicy(max_attempts=3, retry_on=ConnectionError), + error_handler=payment_error_handler, + ) + ``` + + ::: + + @@ -967,35 +1005,7 @@ Let's run our agent with an urgent billing issue that needs human review: :::python -```python -# Test with an urgent billing issue -initial_state = { - "email_content": "I was charged twice for my subscription! This is urgent!", - "sender_email": "customer@example.com", - "email_id": "email_123", - "messages": [] -} - -# Run with a thread_id for persistence -config = {"configurable": {"thread_id": "customer_123"}} -result = app.invoke(initial_state, config) -# The graph will pause at human_review -print(f"human review interrupt:{result['__interrupt__']}") - -# When ready, provide human input to resume -from langgraph.types import Command - -human_response = Command( - resume={ - "approved": True, - "edited_response": "We sincerely apologize for the double charge. I've initiated an immediate refund..." - } -) - -# Resume execution -final_result = app.invoke(human_response, config) -print(f"Email sent successfully!") -``` + ::: @@ -1082,7 +1092,7 @@ Or why separate Doc Search from Draft Reply? The answer involves trade-offs between resilience and observability. -**The resilience consideration:** LangGraph's [durable execution](/oss/langgraph/durable-execution) creates checkpoints at node boundaries. When a workflow resumes after an interruption or failure, it starts from the beginning of the node where execution stopped. Smaller nodes mean more frequent checkpoints, which means less work to repeat if something goes wrong. If you combine multiple operations into one large node, a failure near the end means re-executing everything from the start of that node. +**The resilience consideration:** LangGraph's [persistence layer](/oss/langgraph/persistence) creates checkpoints at node boundaries. When a workflow resumes after an interruption or failure, it starts from the beginning of the node where execution stopped. Smaller nodes mean more frequent checkpoints, which means less work to repeat if something goes wrong. If you combine multiple operations into one large node, a failure near the end means re-executing everything from the start of that node. Why we chose this breakdown for the email agent: @@ -1098,7 +1108,7 @@ A different valid approach: You could combine `Read Email` and `Classify Intent` Application-level concerns: The caching discussion in Step 2 (whether to cache search results) is an application-level decision, not a LangGraph framework feature. You implement caching within your node functions based on your specific requirements—LangGraph doesn't prescribe this. -Performance considerations: More nodes doesn't mean slower execution. LangGraph writes checkpoints in the background by default ([async durability mode](/oss/langgraph/durable-execution#durability-modes)), so your graph continues running without waiting for checkpoints to complete. This means you get frequent checkpoints with minimal performance impact. You can adjust this behavior if needed—use `"exit"` mode to checkpoint only at completion, or `"sync"` mode to block execution until each checkpoint is written. +Performance considerations: More nodes doesn't mean slower execution. LangGraph writes checkpoints in the background by default ([async durability mode](/oss/langgraph/checkpointers#durability-modes)), so your graph continues running without waiting for checkpoints to complete. This means you get frequent checkpoints with minimal performance impact. You can adjust this behavior if needed—use `"exit"` mode to checkpoint only at completion, or `"sync"` mode to block execution until each checkpoint is written. ### Where to go from here diff --git a/ui.mdx b/ui.mdx index b7c0ee2..b4d631b 100644 --- a/ui.mdx +++ b/ui.mdx @@ -2,9 +2,9 @@ title: Agent Chat UI --- -import AgentChatUi from '/snippets/oss/agent-chat-ui.mdx'; +import agent_chat_ui from '/snippets/oss/agent-chat-ui.mdx'; - + ### Connect to your agent diff --git a/use-functional-api.mdx b/use-functional-api.mdx index 4e6a7ea..5b8c497 100644 --- a/use-functional-api.mdx +++ b/use-functional-api.mdx @@ -3,7 +3,8 @@ title: Use the functional API sidebarTitle: Use the Functional API --- - +import LanggraphFunctionalApiStreamCustomDataPy from '/snippets/code-samples/langgraph-functional-api-stream-custom-data-py.mdx'; +import LanggraphFunctionalApiStreamCustomDataJs from '/snippets/code-samples/langgraph-functional-api-stream-custom-data-js.mdx'; The [**Functional API**](/oss/langgraph/functional-api) allows you to add LangGraph's key features ([persistence](/oss/langgraph/persistence), [memory](/oss/langgraph/add-memory), [human-in-the-loop](/oss/langgraph/interrupts), and [streaming](/oss/langgraph/streaming)) to your applications with minimal changes to your existing code. @@ -524,48 +525,18 @@ const myWorkflow = entrypoint( The **Functional API** uses the same streaming mechanism as the **Graph API**. Please read the [**streaming guide**](/oss/langgraph/streaming) section for more details. -Example of using the streaming API to stream both updates and custom data. +Example of using the streaming API to stream value chunks from a workflow run. :::python -```python -from langgraph.func import entrypoint -from langgraph.checkpoint.memory import InMemorySaver -from langgraph.config import get_stream_writer # [!code highlight] -checkpointer = InMemorySaver() - -@entrypoint(checkpointer=checkpointer) -def main(inputs: dict) -> int: - writer = get_stream_writer() # [!code highlight] - writer("Started processing") # [!code highlight] - result = inputs["x"] * 2 - writer(f"Result is {result}") # [!code highlight] - return result - -config = {"configurable": {"thread_id": "abc"}} - -for mode, chunk in main.stream( # [!code highlight] - {"x": 5}, - stream_mode=["custom", "updates"], # [!code highlight] - config=config -): - print(f"{mode}: {chunk}") -``` + 1. Import @[`get_stream_writer`] from `langgraph.config`. 2. Obtain a stream writer instance within the entrypoint. 3. Emit custom data before computation begins. 4. Emit another custom message after computing the result. -5. Use `.stream()` to process streamed output. -6. Specify which streaming modes to use. - -```pycon -('updates', {'add_one': 2}) -('updates', {'add_two': 3}) -('custom', 'hello') -('custom', 'world') -('updates', {'main': 5}) -``` +5. Use `stream_events()` to process streamed output. +6. Iterate over `(mode, chunk)` pairs from `interleave("values")`. **Async with Python < 3.11** @@ -583,51 +554,12 @@ async def main(inputs: dict, writer: StreamWriter) -> int: # [!code highlight] ::: :::js -```typescript -import { - entrypoint, - MemorySaver, - LangGraphRunnableConfig, -} from "@langchain/langgraph"; - -const checkpointer = new MemorySaver(); - -const main = entrypoint( - { checkpointer, name: "main" }, - async ( - inputs: { x: number }, - config: LangGraphRunnableConfig - ): Promise => { - config.writer?.("Started processing"); // [!code highlight] - const result = inputs.x * 2; - config.writer?.(`Result is ${result}`); // [!code highlight] - return result; - } -); - -const config = { configurable: { thread_id: "abc" } }; - - // [!code highlight] -for await (const [mode, chunk] of await main.stream( - { x: 5 }, - { streamMode: ["custom", "updates"], ...config } // [!code highlight] -)) { - console.log(`${mode}: ${JSON.stringify(chunk)}`); -} -``` + 1. Emit custom data before computation begins. 2. Emit another custom message after computing the result. -3. Use `.stream()` to process streamed output. -4. Specify which streaming modes to use. - -``` -updates: {"addOne": 2} -updates: {"addTwo": 3} -custom: "hello" -custom: "world" -updates: {"main": 5} -``` +3. Use `streamEvents()` to process streamed output. +4. Iterate `stream.values` to receive value snapshots. ::: ## Retry policy @@ -730,6 +662,46 @@ await main.invoke({ any_input: "foobar" }, config); ``` ::: +:::python + +## Set task and entrypoint timeouts + +Use the `timeout` parameter with `@task` or `@entrypoint` to limit how long a single async attempt can run. Provide the timeout in seconds or as a `datetime.timedelta`. + +```python +import asyncio + +from langgraph.errors import NodeTimeoutError +from langgraph.func import entrypoint, task +from langgraph.types import RetryPolicy + + +@task( + timeout=1.0, + retry_policy=RetryPolicy(retry_on=NodeTimeoutError), +) +async def call_api(url: str) -> str: + await asyncio.sleep(2) + return f"result from {url}" + + +@entrypoint(timeout=5.0) +async def workflow(inputs: dict) -> str: + return await call_api(inputs["url"]) + + +try: + await workflow.ainvoke({"url": "https://example.com"}) +except NodeTimeoutError: + print("Task timed out") +``` + +Timeouts are supported only for async tasks and entrypoints. If you set `timeout` on a sync function, LangGraph raises an error when the task or entrypoint is declared. + +When a task or entrypoint exceeds its timeout, LangGraph raises `NodeTimeoutError`, which subclasses Python's built-in `TimeoutError`. If a retry policy retries `TimeoutError` or `NodeTimeoutError`, the timed-out attempt is retried. The timeout applies to each attempt independently, so the timer resets for every retry. + +::: + ## Caching tasks :::python @@ -753,12 +725,9 @@ def main(inputs: dict) -> dict[str, int]: return {"result1": result1, "result2": result2} -for chunk in main.stream({"x": 5}, stream_mode="updates"): - print(chunk) - -#> {'slow_add': 10} -#> {'slow_add': 10, '__metadata__': {'cached': True}} -#> {'main': {'result1': 10, 'result2': 10}} +stream = main.stream_events({"x": 5}, version="v3") +for snapshot in stream.values: + print(snapshot) ``` 1. `ttl` is specified in seconds. The cache will be invalidated after this time. @@ -793,16 +762,10 @@ const main = entrypoint( } ); -for await (const chunk of await main.stream( - { x: 5 }, - { streamMode: "updates" } -)) { - console.log(chunk); +const stream = await main.streamEvents({ x: 5 }, { version: "v3" }); +for await (const snapshot of stream.values) { + console.log(snapshot); } - -//> { slowAdd: 10 } -//> { slowAdd: 10, '__metadata__': { cached: true } } -//> { main: { result1: 10, result2: 10 } } ``` 1. `ttl` is specified in seconds. The cache will be invalidated after this time. @@ -1070,9 +1033,10 @@ Let's send in a query string: ```python config = {"configurable": {"thread_id": "1"}} -for event in graph.stream("foo", config): - print(event) - print("\n") +stream = graph.stream_events("foo", config, version="v3") +for message in stream.messages: + for token in message.text: + print(token, end="", flush=True) ``` ::: @@ -1080,9 +1044,11 @@ for event in graph.stream("foo", config): ```typescript const config = { configurable: { thread_id: "1" } }; -for await (const event of await graph.stream("foo", config)) { - console.log(event); - console.log("\n"); +const stream = await graph.streamEvents("foo", config, { version: "v3" }); +for await (const message of stream.messages) { + for await (const token of message.text) { + process.stdout.write(token); + } } ``` ::: @@ -1092,21 +1058,25 @@ Note that we've paused with an @[`interrupt`] after `step_1`. The interrupt prov :::python ```python # Continue execution -for event in graph.stream(Command(resume="baz"), config): - print(event) - print("\n") +stream = graph.stream_events(Command(resume="baz"), config, version="v3") +for message in stream.messages: + for token in message.text: + print(token, end="", flush=True) ``` ::: :::js ```typescript // Continue execution -for await (const event of await graph.stream( +const stream = await graph.streamEvents( new Command({ resume: "baz" }), - config -)) { - console.log(event); - console.log("\n"); + config, + { version: "v3" } +); +for await (const message of stream.messages) { + for await (const token of message.text) { + process.stdout.write(token); + } } ``` ::: @@ -1591,12 +1561,14 @@ def workflow(inputs: list[BaseMessage], *, previous: list[BaseMessage]): config = {"configurable": {"thread_id": "1"}} input_message = {"role": "user", "content": "hi! I'm bob"} -for chunk in workflow.stream([input_message], config, stream_mode="values"): - chunk.pretty_print() +stream = workflow.stream_events([input_message], config, version="v3") +for snapshot in stream.values: + print(snapshot) input_message = {"role": "user", "content": "what's my name?"} -for chunk in workflow.stream([input_message], config, stream_mode="values"): - chunk.pretty_print() +stream = workflow.stream_events([input_message], config, version="v3") +for snapshot in stream.values: + print(snapshot) ``` ::: @@ -1645,19 +1617,15 @@ const workflow = entrypoint( const config = { configurable: { thread_id: "1" } }; const inputMessage = { role: "user", content: "hi! I'm bob" }; -for await (const chunk of await workflow.stream([inputMessage], { - ...config, - streamMode: "values", -})) { - console.log(chunk.content); +const stream1 = await workflow.streamEvents([inputMessage], { ...config, version: "v3" }); +for await (const snapshot of stream1.values) { + console.log(snapshot); } const inputMessage2 = { role: "user", content: "what's my name?" }; -for await (const chunk of await workflow.stream([inputMessage2], { - ...config, - streamMode: "values", -})) { - console.log(chunk.content); +const stream2 = await workflow.streamEvents([inputMessage2], { ...config, version: "v3" }); +for await (const snapshot of stream2.values) { + console.log(snapshot); } ``` ::: diff --git a/use-graph-api.mdx b/use-graph-api.mdx index 8b5cdad..3ef0b03 100644 --- a/use-graph-api.mdx +++ b/use-graph-api.mdx @@ -3,8 +3,6 @@ title: Use the graph API sidebarTitle: Use the graph API --- - - import ChatModelTabs from '/snippets/chat-model-tabs.mdx'; This guide demonstrates the basics of LangGraph's Graph API. It walks through [state](#define-and-update-state), as well as composing common graph structures such as [sequences](#create-a-sequence-of-steps), [branches](#create-branches), and [loops](#create-and-control-loops). It also covers LangGraph's control features, including the [Send API](#map-reduce-and-the-send-api) for map-reduce workflows and the [Command API](#combine-control-flow-and-state-updates-with-command) for combining state updates with "hops" across nodes. @@ -1209,7 +1207,7 @@ console.log(await graph.invoke({}, { context: { myRuntimeValue: "b" } })); // [ MODELS = { "anthropic": init_chat_model("claude-haiku-4-5-20251001"), - "openai": init_chat_model("gpt-4.1-mini"), + "openai": init_chat_model("gpt-5.4-mini"), } def call_model(state: MessagesState, runtime: Runtime[ContextSchema]): @@ -1237,7 +1235,7 @@ console.log(await graph.invoke({}, { context: { myRuntimeValue: "b" } })); // [ ``` claude-haiku-4-5-20251001 - gpt-4.1-mini-2025-04-14 + gpt-5.4-mini ``` ::: @@ -1260,7 +1258,7 @@ console.log(await graph.invoke({}, { context: { myRuntimeValue: "b" } })); // [ const MODELS = { anthropic: new ChatAnthropic({ model: "claude-haiku-4-5-20251001" }), - openai: new ChatOpenAI({ model: "gpt-4.1-mini" }), + openai: new ChatOpenAI({ model: "gpt-5.4-mini" }), }; const callModel: GraphNode = async (state, config) => { @@ -1292,7 +1290,7 @@ console.log(await graph.invoke({}, { context: { myRuntimeValue: "b" } })); // [ ``` claude-haiku-4-5-20251001 - gpt-4.1-mini-2025-04-14 + gpt-5.4-mini ``` ::: @@ -1316,7 +1314,7 @@ console.log(await graph.invoke({}, { context: { myRuntimeValue: "b" } })); // [ MODELS = { "anthropic": init_chat_model("claude-haiku-4-5-20251001"), - "openai": init_chat_model("gpt-4.1-mini"), + "openai": init_chat_model("gpt-5.4-mini"), } def call_model(state: MessagesState, runtime: Runtime[ContextSchema]): @@ -1372,7 +1370,7 @@ console.log(await graph.invoke({}, { context: { myRuntimeValue: "b" } })); // [ const MODELS = { anthropic: new ChatAnthropic({ model: "claude-haiku-4-5-20251001" }), - openai: new ChatOpenAI({ model: "gpt-4.1-mini" }), + openai: new ChatOpenAI({ model: "gpt-5.4-mini" }), }; const callModel: GraphNode = async (state, config) => { @@ -1483,14 +1481,15 @@ By default, the retry policy retries on any exception except for the following: from langchain.chat_models import init_chat_model from langgraph.graph import END, MessagesState, StateGraph, START from langgraph.types import RetryPolicy - from langchain_community.utilities import SQLDatabase from langchain.messages import AIMessage - db = SQLDatabase.from_uri("sqlite:///:memory:") + con = sqlite3.connect(":memory:") model = init_chat_model("claude-haiku-4-5-20251001") def query_database(state: MessagesState): - query_result = db.run("SELECT * FROM Artist LIMIT 10;") + cursor = con.cursor() + cursor.execute("SELECT * FROM Artist LIMIT 10;") + query_result = str(cursor.fetchall()) return {"messages": [AIMessage(content=query_result)]} def call_model(state: MessagesState): @@ -1568,6 +1567,97 @@ By default, the retry policy retries on any exception except for the following: :::python +## Set node timeouts + +Use the `timeout` parameter with @[`add_node`] to limit how long a single async node invocation can run. Provide the timeout in seconds or as a `datetime.timedelta`. + +```python +import asyncio +from typing_extensions import TypedDict + +from langgraph.errors import NodeTimeoutError +from langgraph.graph import END, START, StateGraph + + +class State(TypedDict): + value: str + + +async def call_model(state: State) -> State: + await asyncio.sleep(2) + return {"value": "done"} + + +builder = StateGraph(State) +builder.add_node("model", call_model, timeout=1.0) +builder.add_edge(START, "model") +builder.add_edge("model", END) +graph = builder.compile() + +try: + await graph.ainvoke({"value": "start"}) +except NodeTimeoutError: + print("Node timed out") +``` + +Node timeouts are supported only for async nodes. If you set `timeout` on a sync node, LangGraph raises an error when the graph is compiled because sync Python execution cannot be safely canceled in-process. + +When a node exceeds its timeout, LangGraph raises `NodeTimeoutError`, which subclasses Python's built-in `TimeoutError`. If the node has a `retry_policy` that retries `TimeoutError` or `NodeTimeoutError`, the timed-out attempt is retried. The timeout applies to each attempt independently, so the timer resets for every retry. + +Timed-out attempts do not commit their buffered writes. This prevents state updates or child-task scheduling from leaking out after the timeout boundary. + +## Configure node timeouts + +The `timeout=` parameter on @[`add_node`] caps how long a single async node attempt may run. Pass a number (seconds), a `timedelta`, or a @[`TimeoutPolicy`] for finer control over run and idle timeouts. When the limit is exceeded, LangGraph raises @[`NodeTimeoutError`] and lets the retry policy decide whether to retry. + + +Per-node timeouts require `langgraph>=1.2`. + + +```python +from langgraph.types import TimeoutPolicy + +builder.add_node( + "call_model", + call_model, + timeout=TimeoutPolicy(run_timeout=120, idle_timeout=30), +) +``` + +See [Fault tolerance](/oss/langgraph/fault-tolerance#timeouts) for the full timeout lifecycle, idle-timeout refresh sources, and `runtime.heartbeat()`. + +## Handle node errors + +The `error_handler=` parameter on @[`add_node`] registers a function that runs after a node fails and all retries are exhausted. The handler receives the current state and a typed @[`NodeError`] with failure context, and can route to a recovery branch via @[`Command`]: + + +Node-level error handlers require `langgraph>=1.2`. + + +```python +from langgraph.errors import NodeError +from langgraph.types import Command, RetryPolicy + +def payment_error_handler(state: State, error: NodeError) -> Command: + return Command( + update={"status": f"compensated: {error.error}"}, + goto="finalize", + ) + +builder.add_node( + "charge_payment", + charge_payment, + retry_policy=RetryPolicy(max_attempts=3, retry_on=ConnectionError), + error_handler=payment_error_handler, +) +``` + +See [Fault tolerance](/oss/langgraph/fault-tolerance#error-handling) for compensation patterns and `Command` routing. + +::: + +:::python + ### Access execution info inside a node You can access execution identity and retry information via `runtime.execution_info`. This surfaces thread, run, and checkpoint identifiers as well as retry state, without needing to read from `config` directly. @@ -1674,6 +1764,28 @@ graph = builder.compile() Requires `deepagents>=0.5.0` (or `langgraph>=1.1.5`) for `runtime.execution_info` and `runtime.server_info`. +### Access drain state inside a node + +When a [graceful shutdown](/oss/langgraph/fault-tolerance#graceful-shutdown) has been requested, `runtime.drain_requested` is `True`. Read this inside a node to skip expensive work before the next superstep boundary: + +```python +from langgraph.runtime import Runtime + +def my_node(state: State, runtime: Runtime) -> State: + if runtime.drain_requested: # [!code highlight] + return {"status": "skipped", "reason": runtime.drain_reason} + return {"status": do_work()} +``` + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `drain_requested` | `bool` | `True` if `RunControl.request_drain()` has been called for this run. | +| `drain_reason` | `str \| None` | The reason string passed to `request_drain()`, or `None` if drain was not requested. | + + +Requires `langgraph>=1.2`. See [Graceful shutdown](/oss/langgraph/fault-tolerance#graceful-shutdown) for the full `RunControl` API. + + ::: :::js @@ -2586,8 +2698,10 @@ display(Image(graph.get_graph().draw_mermaid_png())) ```python # Call the graph: here we call it to generate a list of jokes -for step in graph.stream({"topic": "animals"}): - print(step) +stream = graph.stream_events({"topic": "animals"}, version="v3") +for message in stream.messages: + for token in message.text: + print(token, end="", flush=True) ``` ``` @@ -2658,8 +2772,11 @@ await fs.writeFile("graph.png", imageBuffer); ```typescript // Call the graph: here we call it to generate a list of jokes -for await (const step of await graph.stream({ topic: "animals" })) { - console.log(step); +const stream = await graph.streamEvents({ topic: "animals" }, { version: "v3" }); +for await (const message of stream.messages) { + for await (const token of message.text) { + process.stdout.write(token); + } } ``` @@ -2725,9 +2842,8 @@ const graph = new StateGraph(State) ``` ::: -To control the recursion limit, specify `"recursionLimit"` in the config. This will raise a `GraphRecursionError`, which you can catch and handle: - :::python +To control the recursion limit, specify `"recursion_limit"` in the config. This will raise a `GraphRecursionError`, which you can catch and handle: ```python from langgraph.errors import GraphRecursionError @@ -2739,6 +2855,7 @@ except GraphRecursionError: ::: :::js +To control the recursion limit, specify `"recursionLimit"` in the config. This will raise a `GraphRecursionError`, which you can catch and handle: ```typescript import { GraphRecursionError } from "@langchain/langgraph"; diff --git a/use-subgraphs.mdx b/use-subgraphs.mdx index ff625c1..57b1bcf 100644 --- a/use-subgraphs.mdx +++ b/use-subgraphs.mdx @@ -3,6 +3,8 @@ title: Subgraphs sidebarTitle: Subgraphs --- +import LanggraphSubgraphsInterruptV2Py from '/snippets/code-samples/langgraph-subgraphs-interrupt-v2-py.mdx'; + This guide explains the mechanics of using subgraphs. A subgraph is a [graph](/oss/langgraph/graph-api#graphs) that is used as a [node](/oss/langgraph/graph-api#nodes) in another graph. Subgraphs are useful for: @@ -169,16 +171,17 @@ const graph = builder.compile(); builder.add_edge("node_1", "node_2") graph = builder.compile() - for chunk in graph.stream({"foo": "foo"}, subgraphs=True, version="v2"): - if chunk["type"] == "updates": - print(chunk["ns"], chunk["data"]) + stream = graph.stream_events({"foo": "foo"}, version="v3") + for event in stream: + if event["method"] == "updates": + print(event["params"]["namespace"], event["params"]["data"]) ``` ``` - () {'node_1': {'foo': 'hi! foo'}} - ('node_2:577b710b-64ae-31fb-9455-6a4d4cc2b0b9',) {'subgraph_node_1': {'baz': 'baz'}} - ('node_2:577b710b-64ae-31fb-9455-6a4d4cc2b0b9',) {'subgraph_node_2': {'bar': 'hi! foobaz'}} - () {'node_2': {'foo': 'hi! foobaz'}} + [] {'node_1': {'foo': 'hi! foo'}} + ['node_2:577b710b-64ae-31fb-9455-6a4d4cc2b0b9'] {'subgraph_node_1': {'baz': 'baz'}} + ['node_2:577b710b-64ae-31fb-9455-6a4d4cc2b0b9'] {'subgraph_node_2': {'bar': 'hi! foobaz'}} + [] {'node_2': {'foo': 'hi! foobaz'}} ``` ::: @@ -224,11 +227,14 @@ const graph = builder.compile(); const graph = builder.compile(); - for await (const chunk of await graph.stream( + const stream = await graph.streamEvents( { foo: "foo" }, - { subgraphs: true } - )) { - console.log(chunk); + { subgraphs: true, version: "v3" } + ); + for await (const message of stream.messages) { + for await (const token of message.text) { + process.stdout.write(token); + } } ``` @@ -315,17 +321,18 @@ const graph = builder.compile(); parent_graph = parent.compile() - for chunk in parent_graph.stream({"my_key": "Bob"}, subgraphs=True, version="v2"): - if chunk["type"] == "updates": - print(chunk["ns"], chunk["data"]) + stream = parent_graph.stream_events({"my_key": "Bob"}, version="v3") + for event in stream: + if event["method"] == "updates": + print(event["params"]["namespace"], event["params"]["data"]) ``` ``` - () {'parent_1': {'my_key': 'hi Bob'}} - ('child:2e26e9ce-602f-862c-aa66-1ea5a4655e3b', 'child_1:781bb3b1-3971-84ce-810b-acf819a03f9c') {'grandchild_1': {'my_grandchild_key': 'hi Bob, how are you'}} - ('child:2e26e9ce-602f-862c-aa66-1ea5a4655e3b',) {'child_1': {'my_child_key': 'hi Bob, how are you today?'}} - () {'child': {'my_key': 'hi Bob, how are you today?'}} - () {'parent_2': {'my_key': 'hi Bob, how are you today? bye!'}} + [] {'parent_1': {'my_key': 'hi Bob'}} + ['child:2e26e9ce-602f-862c-aa66-1ea5a4655e3b', 'child_1:781bb3b1-3971-84ce-810b-acf819a03f9c'] {'grandchild_1': {'my_grandchild_key': 'hi Bob, how are you'}} + ['child:2e26e9ce-602f-862c-aa66-1ea5a4655e3b'] {'child_1': {'my_child_key': 'hi Bob, how are you today?'}} + [] {'child': {'my_key': 'hi Bob, how are you today?'}} + [] {'parent_2': {'my_key': 'hi Bob, how are you today? bye!'}} ``` ::: @@ -391,11 +398,14 @@ const graph = builder.compile(); const parentGraph = parent.compile(); - for await (const chunk of await parentGraph.stream( + const stream = await parentGraph.streamEvents( { myKey: "Bob" }, - { subgraphs: true } - )) { - console.log(chunk); + { subgraphs: true, version: "v3" } + ); + for await (const message of stream.messages) { + for await (const token of message.text) { + process.stdout.write(token); + } } ``` @@ -529,9 +539,10 @@ const graph = builder.compile(); builder.add_edge("node_1", "node_2") graph = builder.compile() - for chunk in graph.stream({"foo": "foo"}, version="v2"): - if chunk["type"] == "updates": - print(chunk["data"]) + stream = graph.stream_events({"foo": "foo"}, version="v3") + for event in stream: + if event["method"] == "updates" and not event["params"]["namespace"]: + print(event["params"]["data"]) ``` ``` @@ -580,8 +591,11 @@ const graph = builder.compile(); const graph = builder.compile(); - for await (const chunk of await graph.stream({ foo: "foo" })) { - console.log(chunk); + const stream = await graph.streamEvents({ foo: "foo" }, { version: "v3" }); + for await (const message of stream.messages) { + for await (const token of message.text) { + process.stdout.write(token); + } } ``` @@ -603,7 +617,7 @@ The `checkpointer` parameter on `.compile()` controls subgraph persistence: | Mode | `checkpointer=` | Behavior | |------|-----------------|----------| -| [Per-invocation](#per-invocation-default) | `None` (default) | Each call starts fresh and inherits the parent's checkpointer to support [interrupts](/oss/langgraph/interrupts) and [durable execution](/oss/langgraph/durable-execution) within a single call. | +| [Per-invocation](#per-invocation-default) | `None` (default) | Each call starts fresh and inherits the parent's checkpointer to support [interrupts](/oss/langgraph/interrupts) and [durable execution](/oss/langgraph/persistence) within a single call. | | [Per-thread](#per-thread) | `True` | State accumulates across calls on the same thread. Each call picks up where the last one left off. | | [Stateless](#stateless) | `False` | No checkpointing at all—runs like a plain function call. No interrupts or durable execution. | @@ -619,12 +633,12 @@ The examples below use LangChain's @[`create_agent`], which is a common way to b ### Stateful -Stateful subgraphs inherit the parent graph's checkpointer, which enables [interrupts](/oss/langgraph/interrupts), [durable execution](/oss/langgraph/durable-execution), and state inspection. The two stateful modes differ in how long state is retained. +Stateful subgraphs inherit the parent graph's checkpointer, which enables [interrupts](/oss/langgraph/interrupts), [persistence](/oss/langgraph/persistence), and state inspection. The two stateful modes differ in how long state is retained. #### Per-invocation (default) -This is the recommended mode for most applications, including [multi-agent](/oss/langchain/multi-agent) systems where subagents are invoked as tools. It supports interrupts, [durable execution](/oss/langgraph/durable-execution), and parallel calls while keeping each invocation isolated. +This is the recommended mode for most applications, including [multi-agent](/oss/langchain/multi-agent) systems where subagents are invoked as tools. It supports [interrupts](/oss/langgraph/interrupts), [persistence](/oss/langgraph/persistence), and parallel calls while keeping each invocation isolated. Use per-invocation persistence when each call to the subgraph is independent and the subagent doesn't need to remember anything from previous calls. This is the most common pattern, especially for [multi-agent](/oss/langchain/multi-agent) systems where subagents handle one-off requests like "look up this customer's order" or "summarize this document." @@ -653,13 +667,13 @@ def veggie_info(veggie_name: str) -> str: # Subagents - no checkpointer setting (inherits parent) fruit_agent = create_agent( - model="gpt-4.1-mini", + model="gpt-5.4-mini", tools=[fruit_info], prompt="You are a fruit expert. Use the fruit_info tool. Respond in one sentence.", ) veggie_agent = create_agent( - model="gpt-4.1-mini", + model="gpt-5.4-mini", tools=[veggie_info], prompt="You are a veggie expert. Use the veggie_info tool. Respond in one sentence.", ) @@ -683,7 +697,7 @@ def ask_veggie_expert(question: str) -> str: # Outer agent with checkpointer agent = create_agent( - model="gpt-4.1-mini", + model="gpt-5.4-mini", tools=[ask_fruit_expert, ask_veggie_expert], prompt=( "You have two experts: ask_fruit_expert and ask_veggie_expert. " @@ -705,20 +719,7 @@ agent = create_agent( return f"Info about {fruit_name}" ``` - ```python - config = {"configurable": {"thread_id": "1"}} - - # Invoke - the subagent's tool calls interrupt() - response = agent.invoke( - {"messages": [{"role": "user", "content": "Tell me about apples"}]}, - config=config, - ) - # response contains __interrupt__ - - # Resume - approve the interrupt - response = agent.invoke(Command(resume=True), config=config) # [!code highlight] - # Subagent message count: 4 - ``` + Each invocation starts with a fresh subagent state. The subagent does not remember previous calls: @@ -786,13 +787,13 @@ const veggieInfo = tool( // Subagents - no checkpointer setting (inherits parent) const fruitAgent = createAgent({ - model: "gpt-4.1-mini", + model: "gpt-5.4-mini", tools: [fruitInfo], prompt: "You are a fruit expert. Use the fruit_info tool. Respond in one sentence.", }); const veggieAgent = createAgent({ - model: "gpt-4.1-mini", + model: "gpt-5.4-mini", tools: [veggieInfo], prompt: "You are a veggie expert. Use the veggie_info tool. Respond in one sentence.", }); @@ -828,7 +829,7 @@ const askVeggieExpert = tool( // Outer agent with checkpointer const agent = createAgent({ - model: "gpt-4.1-mini", + model: "gpt-5.4-mini", tools: [askFruitExpert, askVeggieExpert], prompt: "You have two experts: ask_fruit_expert and ask_veggie_expert. " + @@ -939,7 +940,7 @@ def fruit_info(fruit_name: str) -> str: # Subagent with checkpointer=True for persistent state fruit_agent = create_agent( - model="gpt-4.1-mini", + model="gpt-5.4-mini", tools=[fruit_info], prompt="You are a fruit expert. Use the fruit_info tool. Respond in one sentence.", checkpointer=True, # [!code highlight] @@ -958,7 +959,7 @@ def ask_fruit_expert(question: str) -> str: # Use ToolCallLimitMiddleware to prevent parallel calls to per-thread subagents, # which would cause checkpoint conflicts. agent = create_agent( - model="gpt-4.1-mini", + model="gpt-5.4-mini", tools=[ask_fruit_expert], prompt="You have a fruit expert. ALWAYS delegate fruit questions to ask_fruit_expert.", middleware=[ # [!code highlight] @@ -980,20 +981,7 @@ agent = create_agent( return f"Info about {fruit_name}" ``` - ```python - config = {"configurable": {"thread_id": "1"}} - - # Invoke - the subagent's tool calls interrupt() - response = agent.invoke( - {"messages": [{"role": "user", "content": "Tell me about apples"}]}, - config=config, - ) - # response contains __interrupt__ - - # Resume - approve the interrupt - response = agent.invoke(Command(resume=True), config=config) # [!code highlight] - # Subagent message count: 4 - ``` + State accumulates across invocations—the subagent remembers past conversations: @@ -1036,11 +1024,11 @@ agent = create_agent( ) fruit_agent = create_sub_agent( - "gpt-4.1-mini", name="fruit_agent", + "gpt-5.4-mini", name="fruit_agent", tools=[fruit_info], prompt="...", checkpointer=True, ) veggie_agent = create_sub_agent( - "gpt-4.1-mini", name="veggie_agent", + "gpt-5.4-mini", name="veggie_agent", tools=[veggie_info], prompt="...", checkpointer=True, ) @@ -1086,7 +1074,7 @@ const fruitInfo = tool( // Subagent with checkpointer=true for persistent state const fruitAgent = createAgent({ - model: "gpt-4.1-mini", + model: "gpt-5.4-mini", tools: [fruitInfo], prompt: "You are a fruit expert. Use the fruit_info tool. Respond in one sentence.", checkpointer: true, // [!code highlight] @@ -1111,7 +1099,7 @@ const askFruitExpert = tool( // Use toolCallLimitMiddleware to prevent parallel calls to per-thread subagents, // which would cause checkpoint conflicts. const agent = createAgent({ - model: "gpt-4.1-mini", + model: "gpt-5.4-mini", tools: [askFruitExpert], prompt: "You have a fruit expert. ALWAYS delegate fruit questions to ask_fruit_expert.", middleware: [ // [!code highlight] @@ -1192,10 +1180,10 @@ const agent = createAgent({ .compile(); } - const fruitAgent = createSubAgent("gpt-4.1-mini", { + const fruitAgent = createSubAgent("gpt-5.4-mini", { name: "fruit_agent", tools: [fruitInfo], prompt: "...", checkpointer: true, }); - const veggieAgent = createSubAgent("gpt-4.1-mini", { + const veggieAgent = createSubAgent("gpt-5.4-mini", { name: "veggie_agent", tools: [veggieInfo], prompt: "...", checkpointer: true, }); const config = { configurable: { thread_id: "1" } }; @@ -1270,7 +1258,7 @@ const subgraph = builder.compile({ checkpointer: false }); // or true, or null | State inspection | ⚠️ | ✅ | ❌ | - **Interrupts (HITL)**: The subgraph can use [interrupt()](/oss/langgraph/interrupts) to pause execution and wait for user input, then resume where it left off. -- **Multi-turn memory**: The subgraph retains its state across multiple invocations within the same [thread](/oss/langgraph/persistence#threads). Each call picks up where the last one left off rather than starting fresh. +- **Multi-turn memory**: The subgraph retains its state across multiple invocations within the same [thread](/oss/langgraph/checkpointers#threads). Each call picks up where the last one left off rather than starting fresh. - **Multiple calls (different subgraphs)**: Multiple different subgraph instances can be invoked within a single node without checkpoint namespace conflicts. - **Multiple calls (same subgraph)**: The same subgraph instance can be invoked multiple times within a single node. With stateful persistence, these calls write to the same checkpoint namespace and conflict—use per-invocation persistence instead. - **State inspection**: The subgraph's state is available via `get_state(config, subgraphs=True)` for debugging and monitoring. @@ -1437,48 +1425,40 @@ Viewing subgraph state requires that LangGraph can **statically discover** the s ## Stream subgraph outputs -To include outputs from subgraphs in the streamed outputs, you can set the subgraphs option in the stream method of the parent graph. This will stream outputs from both the parent graph and any subgraphs. +To observe nested graph executions, we recommend [event streaming](/oss/langgraph/event-streaming): the `stream.subgraphs` projection discovers each nested run and exposes its `path`, `messages`, and `values` without parsing namespace strings. :::python - - - With `version="v2"`, subgraph events use the same `StreamPart` format. The `ns` field identifies the source graph: - - ```python - for chunk in graph.stream( - {"foo": "foo"}, - subgraphs=True, # [!code highlight] - stream_mode="updates", - version="v2", # [!code highlight] - ): - print(chunk["type"]) # "updates" - print(chunk["ns"]) # () for root, ("node_2:",) for subgraph - print(chunk["data"]) # {"node_name": {"key": "value"}} - ``` - - - ```python - for chunk in graph.stream( - {"foo": "foo"}, - subgraphs=True, # [!code highlight] - stream_mode="updates", - ): - print(chunk) - ``` - - +```python +stream = graph.stream_events({"foo": "foo"}, version="v3") # [!code highlight] + +for subgraph in stream.subgraphs: + print(subgraph.graph_name, subgraph.path) + + for snapshot in subgraph.values: + print(subgraph.path, snapshot) +``` + +If you need the raw protocol events, iterate the stream directly and filter on `event["method"]` and `event["params"]["namespace"]`: + +```python +stream = graph.stream_events({"foo": "foo"}, version="v3") +for event in stream: + if event["method"] == "updates": + print(event["params"]["namespace"], event["params"]["data"]) +``` ::: :::js ```typescript -for await (const chunk of await graph.stream( +const stream = await graph.streamEvents( { foo: "foo" }, { subgraphs: true, // [!code highlight] - streamMode: "updates", + version: "v3", } -)) { - console.log(chunk); +); +for await (const snapshot of stream.values) { + console.log(snapshot); } ``` @@ -1525,21 +1505,17 @@ for await (const chunk of await graph.stream( builder.add_edge("node_1", "node_2") graph = builder.compile() - for chunk in graph.stream( - {"foo": "foo"}, - stream_mode="updates", - subgraphs=True, # [!code highlight] - version="v2", # [!code highlight] - ): - if chunk["type"] == "updates": - print(chunk["ns"], chunk["data"]) + stream = graph.stream_events({"foo": "foo"}, version="v3") # [!code highlight] + for event in stream: + if event["method"] == "updates": + print(event["params"]["namespace"], event["params"]["data"]) ``` ``` - () {'node_1': {'foo': 'hi! foo'}} - ('node_2:e58e5673-a661-ebb0-70d4-e298a7fc28b7',) {'subgraph_node_1': {'bar': 'bar'}} - ('node_2:e58e5673-a661-ebb0-70d4-e298a7fc28b7',) {'subgraph_node_2': {'foo': 'hi! foobar'}} - () {'node_2': {'foo': 'hi! foobar'}} + [] {'node_1': {'foo': 'hi! foo'}} + ['node_2:e58e5673-a661-ebb0-70d4-e298a7fc28b7'] {'subgraph_node_1': {'bar': 'bar'}} + ['node_2:e58e5673-a661-ebb0-70d4-e298a7fc28b7'] {'subgraph_node_2': {'foo': 'hi! foobar'}} + [] {'node_2': {'foo': 'hi! foobar'}} ``` ::: @@ -1583,14 +1559,15 @@ for await (const chunk of await graph.stream( const graph = builder.compile(); - for await (const chunk of await graph.stream( + const stream = await graph.streamEvents( { foo: "foo" }, { - streamMode: "updates", subgraphs: true, // [!code highlight] + version: "v3", } - )) { - console.log(chunk); + ); + for await (const snapshot of stream.values) { + console.log(snapshot); } ``` diff --git a/use-time-travel.mdx b/use-time-travel.mdx index 69cfd38..c544374 100644 --- a/use-time-travel.mdx +++ b/use-time-travel.mdx @@ -6,7 +6,7 @@ description: Replay past executions and fork to explore alternative paths in Lan ## Overview -LangGraph supports time travel through [checkpoints](/oss/langgraph/persistence#checkpoints): +LangGraph supports time travel through [checkpoints](/oss/langgraph/checkpointers#checkpoints): - **[Replay](#replay)**: Retry from a prior checkpoint. - **[Fork](#fork)**: Branch from a prior checkpoint with modified state to explore an alternative path. diff --git a/workflows-agents.mdx b/workflows-agents.mdx index 4def587..a29a467 100644 --- a/workflows-agents.mdx +++ b/workflows-agents.mdx @@ -3,7 +3,8 @@ title: Workflows and agents sidebarTitle: Workflows + agents --- - +import WorkflowsAgentsToolRuntimeStateContextPy from "/snippets/code-samples/workflows-agents-tool-runtime-state-context-py.mdx"; +import WorkflowsAgentsToolRuntimeStateContextJs from "/snippets/code-samples/workflows-agents-tool-runtime-state-context-js.mdx"; This guide reviews common workflow and agent patterns. @@ -14,6 +15,10 @@ This guide reviews common workflow and agent patterns. LangGraph offers several benefits when building agents and workflows, including [persistence](/oss/langgraph/persistence), [streaming](/oss/langgraph/streaming), and support for debugging as well as [deployment](/oss/langgraph/deploy). + +Trace and compare these workflow patterns with [LangSmith](https://smith.langchain.com). Follow the [tracing quickstart](/langsmith/trace-with-langgraph) to see how data flows through each step. We recommend you also set up [LangSmith Engine](/langsmith/engine) which monitors your traces, detects issues, and proposes fixes. + + ## Setup To build a workflow or agent, you can use [any chat model](/oss/integrations/chat) that supports structured outputs and tool calling. The following example uses Anthropic: @@ -300,8 +305,9 @@ def prompt_chaining_workflow(topic: str): return polish_joke(improved_joke).result() # Invoke -for step in prompt_chaining_workflow.stream("cats", stream_mode="updates"): - print(step) +stream = prompt_chaining_workflow.stream_events("cats", version="v3") +for snapshot in stream.values: + print(snapshot) print("\n") ``` @@ -433,12 +439,9 @@ const workflow = entrypoint( } ); -const stream = await workflow.stream("cats", { - streamMode: "updates", -}); - -for await (const step of stream) { - console.log(step); +const stream = await workflow.streamEvents("cats", { version: "v3" }); +for await (const snapshot of stream.values) { + console.log(snapshot); } ``` @@ -572,8 +575,9 @@ def parallel_workflow(topic: str): ).result() # Invoke -for step in parallel_workflow.stream("cats", stream_mode="updates"): - print(step) +stream = parallel_workflow.stream_events("cats", version="v3") +for snapshot in stream.values: + print(snapshot) print("\n") ``` @@ -693,12 +697,9 @@ const workflow = entrypoint( ); // Invoke -const stream = await workflow.stream("cats", { - streamMode: "updates", -}); - -for await (const step of stream) { - console.log(step); +const stream = await workflow.streamEvents("cats", { version: "v3" }); +for await (const snapshot of stream.values) { + console.log(snapshot); } ``` @@ -884,8 +885,9 @@ def router_workflow(input_: str): return llm_call(input_).result() # Invoke -for step in router_workflow.stream("Write me a joke about cats", stream_mode="updates"): - print(step) +stream = router_workflow.stream_events("Write me a joke about cats", version="v3") +for snapshot in stream.values: + print(snapshot) print("\n") ``` @@ -1089,12 +1091,9 @@ const workflow = entrypoint( ); // Invoke -const stream = await workflow.stream("Write me a joke about cats", { - streamMode: "updates", -}); - -for await (const step of stream) { - console.log(step); +const stream = await workflow.streamEvents("Write me a joke about cats", { version: "v3" }); +for await (const snapshot of stream.values) { + console.log(snapshot); } ``` @@ -1297,12 +1296,9 @@ const workflow = entrypoint( ); // Invoke -const stream = await workflow.stream("Create a report on LLM scaling laws", { - streamMode: "updates", -}); - -for await (const step of stream) { - console.log(step); +const stream = await workflow.streamEvents("Create a report on LLM scaling laws", { version: "v3" }); +for await (const snapshot of stream.values) { + console.log(snapshot); } ``` @@ -1650,8 +1646,9 @@ def optimizer_workflow(topic: str): return joke # Invoke -for step in optimizer_workflow.stream("Cats", stream_mode="updates"): - print(step) +stream = optimizer_workflow.stream_events("Cats", version="v3") +for snapshot in stream.values: + print(snapshot) print("\n") ``` @@ -1792,12 +1789,9 @@ const workflow = entrypoint( ); // Invoke -const stream = await workflow.stream("Cats", { - streamMode: "updates", -}); - -for await (const step of stream) { - console.log(step); +const stream = await workflow.streamEvents("Cats", { version: "v3" }); +for await (const snapshot of stream.values) { + console.log(snapshot); console.log("\n"); } ``` @@ -1883,7 +1877,7 @@ def llm_call(state: MessagesState): } -def tool_node(state: dict): +def tool_node(state: MessagesState): """Performs the tool call""" result = [] @@ -1988,8 +1982,9 @@ def agent(messages: list[BaseMessage]): # Invoke messages = [HumanMessage(content="Add 3 and 4.")] -for chunk in agent.stream(messages, stream_mode="updates"): - print(chunk) +stream = agent.stream_events(messages, version="v3") +for snapshot in stream.values: + print(snapshot) print("\n") ``` @@ -2167,13 +2162,104 @@ const messages = [{ content: "Add 3 and 4." }]; -const stream = await agent.stream([messages], { - streamMode: "updates", -}); - -for await (const step of stream) { - console.log(step); +const stream = await agent.streamEvents([messages], { version: "v3" }); +for await (const snapshot of stream.values) { + console.log(snapshot); } ``` ::: + +### ToolNode + +@[`ToolNode`] is a prebuilt node that executes tools in LangGraph workflows. It handles parallel tool execution, error handling, and state injection automatically. + +Use @[`ToolNode`] when you need fine-grained control over how your graph executes tools. This is the building block that powers tool execution in many LangGraph agent patterns. + +:::python + +```python +from langchain.tools import tool +from langgraph.prebuilt import ToolNode +from langgraph.graph import MessagesState, StateGraph + +@tool +def search(query: str) -> str: + """Search for information.""" + return f"Results for: {query}" + +@tool +def calculator(expression: str) -> str: + """Evaluate a math expression.""" + return str(eval(expression)) + +builder = StateGraph(MessagesState) +builder.add_node("tools", ToolNode([search, calculator])) +# ... add other nodes and edges +graph = builder.compile() +``` + +::: + +:::js + +```typescript +import { ToolNode } from "@langchain/langgraph/prebuilt"; +import { tool } from "@langchain/core/tools"; +import * as z from "zod"; + +const search = tool( + ({ query }) => `Results for: ${query}`, + { + name: "search", + description: "Search for information.", + schema: z.object({ query: z.string() }), + } +); + +const calculator = tool( + ({ expression }) => String(eval(expression)), + { + name: "calculator", + description: "Evaluate a math expression.", + schema: z.object({ expression: z.string() }), + } +); + +const toolNode = new ToolNode([search, calculator]); +``` + +::: + +#### Access graph state and context from tools + +Tools executed by `ToolNode` receive the arguments generated by the model as +their first argument. To read graph-side data that was not generated by the +model, use one of these options: + +- In Python, read state and run-scoped context from the injected + @[`ToolRuntime`] argument. +- In JavaScript, read state and run-scoped context from the tool's second + argument, typed as @[`ToolRuntime`]. + + +Tools can only access the state values passed to the `ToolNode`. When +`ToolNode` is added directly as a `StateGraph` node, that input is the current +graph state. If you invoke a `ToolNode` manually from another node, pass the +full state when tools need custom state fields. For example, `tool_node.invoke(state)` +or `toolNode.invoke(state, config)` exposes the full state, while passing only +`{"messages": state["messages"]}` or `{ messages: state.messages }` only exposes +`messages`. + + +:::python + + + +::: + +:::js + + + +:::