{ "cells": [ { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "# How to view and update state in subgraphs\n", "\n", "
\n", "

Prerequisites

\n", "

\n", " This guide assumes familiarity with the following:\n", "

\n", "

\n", "
\n", "\n", "Once you add [persistence](../subgraph-persistence), you can view and update the state of the subgraph at any point in time. This enables human-in-the-loop interaction patterns such as:\n", "\n", "- You can surface a state during an interrupt to a user to let them accept an action.\n", "- You can rewind the subgraph to reproduce or avoid issues.\n", "- You can modify the state to let the user better control its actions.\n", "\n", "This guide shows how you can do this.\n", "\n", "## Setup\n", "\n", "First we need to install required packages:\n", "\n", "```bash\n", "npm install @langchain/langgraph @langchain/core @langchain/openai\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, we need to set API keys for OpenAI (the provider we'll use for this guide):" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "// process.env.OPENAI_API_KEY = \"YOUR_API_KEY\";" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "

Set up LangSmith for LangGraph development

\n", "

\n", " Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started here. \n", "

\n", "
" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Define subgraph\n", "\n", "First, let's set up our subgraph. For this, we will create a simple graph that can get the weather for a specific city. We will compile this graph with a [breakpoint](https://langchain-ai.github.io/langgraphjs/how-tos/human_in_the_loop/breakpoints/) before the `weather_node`:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import { z } from \"zod\";\n", "import { tool } from \"@langchain/core/tools\";\n", "import { ChatOpenAI } from \"@langchain/openai\";\n", "import { StateGraph, MessagesAnnotation, Annotation } from \"@langchain/langgraph\";\n", "\n", "const getWeather = tool(async ({ city }) => {\n", " return `It's sunny in ${city}`;\n", "}, {\n", " name: \"get_weather\",\n", " description: \"Get the weather for a specific city\",\n", " schema: z.object({\n", " city: z.string().describe(\"A city name\")\n", " })\n", "});\n", "\n", "const rawModel = new ChatOpenAI({ model: \"gpt-4o-mini\" });\n", "const model = rawModel.withStructuredOutput(getWeather);\n", "\n", "// Extend the base MessagesAnnotation state with another field\n", "const SubGraphAnnotation = Annotation.Root({\n", " ...MessagesAnnotation.spec,\n", " city: Annotation,\n", "});\n", "\n", "const modelNode = async (state: typeof SubGraphAnnotation.State) => {\n", " const result = await model.invoke(state.messages);\n", " return { city: result.city };\n", "};\n", "\n", "const weatherNode = async (state: typeof SubGraphAnnotation.State) => {\n", " const result = await getWeather.invoke({ city: state.city });\n", " return {\n", " messages: [\n", " {\n", " role: \"assistant\",\n", " content: result,\n", " }\n", " ]\n", " };\n", "};\n", "\n", "const subgraph = new StateGraph(SubGraphAnnotation)\n", " .addNode(\"modelNode\", modelNode)\n", " .addNode(\"weatherNode\", weatherNode)\n", " .addEdge(\"__start__\", \"modelNode\")\n", " .addEdge(\"modelNode\", \"weatherNode\")\n", " .addEdge(\"weatherNode\", \"__end__\")\n", " .compile({ interruptBefore: [\"weatherNode\"] });" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Define parent graph\n", "\n", "We can now setup the overall graph. This graph will first route to the subgraph if it needs to get the weather, otherwise it will route to a normal LLM." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "import { MemorySaver } from \"@langchain/langgraph\";\n", "\n", "const memory = new MemorySaver();\n", "\n", "const RouterStateAnnotation = Annotation.Root({\n", " ...MessagesAnnotation.spec,\n", " route: Annotation<\"weather\" | \"other\">,\n", "});\n", "\n", "const routerModel = rawModel.withStructuredOutput(\n", " z.object({\n", " route: z.enum([\"weather\", \"other\"]).describe(\"A step that should execute next to based on the currnet input\")\n", " }),\n", " {\n", " name: \"router\"\n", " }\n", ");\n", "\n", "const routerNode = async (state: typeof RouterStateAnnotation.State) => {\n", " const systemMessage = {\n", " role: \"system\",\n", " content: \"Classify the incoming query as either about weather or not.\",\n", " };\n", " const messages = [systemMessage, ...state.messages]\n", " const { route } = await routerModel.invoke(messages);\n", " return { route };\n", "}\n", "\n", "const normalLLMNode = async (state: typeof RouterStateAnnotation.State) => {\n", " const responseMessage = await rawModel.invoke(state.messages);\n", " return { messages: [responseMessage] };\n", "};\n", "\n", "const routeAfterPrediction = async (state: typeof RouterStateAnnotation.State) => {\n", " if (state.route === \"weather\") {\n", " return \"weatherGraph\";\n", " } else {\n", " return \"normalLLMNode\";\n", " }\n", "};\n", "\n", "const graph = new StateGraph(RouterStateAnnotation)\n", " .addNode(\"routerNode\", routerNode)\n", " .addNode(\"normalLLMNode\", normalLLMNode)\n", " .addNode(\"weatherGraph\", subgraph)\n", " .addEdge(\"__start__\", \"routerNode\")\n", " .addConditionalEdges(\"routerNode\", routeAfterPrediction)\n", " .addEdge(\"normalLLMNode\", \"__end__\")\n", " .addEdge(\"weatherGraph\", \"__end__\")\n", " .compile({ checkpointer: memory });" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here's a diagram of the graph we just created:\n", "\n", "![](../img/single-nested-subgraph.jpeg)\n", "\n", "Let's test this out with a normal query to make sure it works as intended!" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{ routerNode: { route: 'other' } }\n", "{\n", " normalLLMNode: {\n", " messages: [\n", " AIMessage {\n", " \"id\": \"chatcmpl-ABtbbiB5N3Uue85UNrFUjw5KhGaud\",\n", " \"content\": \"Hello! How can I assist you today?\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {\n", " \"tokenUsage\": {\n", " \"completionTokens\": 9,\n", " \"promptTokens\": 9,\n", " \"totalTokens\": 18\n", " },\n", " \"finish_reason\": \"stop\",\n", " \"system_fingerprint\": \"fp_f85bea6784\"\n", " },\n", " \"tool_calls\": [],\n", " \"invalid_tool_calls\": [],\n", " \"usage_metadata\": {\n", " \"input_tokens\": 9,\n", " \"output_tokens\": 9,\n", " \"total_tokens\": 18\n", " }\n", " }\n", " ]\n", " }\n", "}\n" ] } ], "source": [ "const config = { configurable: { thread_id: \"1\" } };\n", "\n", "const inputs = { messages: [{ role: \"user\", content: \"hi!\" }] };\n", "\n", "const stream = await graph.stream(inputs, { ...config, streamMode: \"updates\" });\n", "\n", "for await (const update of stream) {\n", " console.log(update);\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Great! We didn't ask about the weather, so we got a normal response from the LLM.\n", "\n", "## Resuming from breakpoints\n", "\n", "Let's now look at what happens with breakpoints. Let's invoke it with a query that should get routed to the weather subgraph where we have the interrupt node." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{ routerNode: { route: 'weather' } }\n" ] } ], "source": [ "const config2 = { configurable: { thread_id: \"2\" } };\n", "\n", "const streamWithBreakpoint = await graph.stream({\n", " messages: [{\n", " role: \"user\",\n", " content: \"what's the weather in sf\"\n", " }]\n", "}, { ...config2, streamMode: \"updates\" });\n", "\n", "for await (const update of streamWithBreakpoint) {\n", " console.log(update);\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that the graph stream doesn't include subgraph events. If we want to stream subgraph events, we can pass `subgraphs: True` in our config and get back subgraph events like so:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[ [], { routerNode: { route: 'weather' } } ]\n", "[\n", " [ 'weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0' ],\n", " { modelNode: { city: 'San Francisco' } }\n", "]\n" ] } ], "source": [ "const streamWithSubgraphs = await graph.stream({\n", " messages: [{\n", " role: \"user\",\n", " content: \"what's the weather in sf\"\n", " }]\n", "}, { configurable: { thread_id: \"3\" }, streamMode: \"updates\", subgraphs: true });\n", "\n", "for await (const update of streamWithSubgraphs) {\n", " console.log(update);\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This time, we see the format of the streamed updates has changed. It's now a tuple where the first item is a nested array with information about the subgraph and the second is the actual state update. If we get the state now, we can see that it's paused on `weatherGraph` as expected:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[ 'weatherGraph' ]\n" ] } ], "source": [ "const state = await graph.getState({ configurable: { thread_id: \"3\" } })\n", "state.next" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we look at the pending tasks for our current state, we can see that we have one task named `weatherGraph`, which corresponds to the subgraph task." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[\n", " {\n", " \"id\": \"ec67e50f-d29c-5dee-8a80-08723a937de0\",\n", " \"name\": \"weatherGraph\",\n", " \"path\": [\n", " \"__pregel_pull\",\n", " \"weatherGraph\"\n", " ],\n", " \"interrupts\": [],\n", " \"state\": {\n", " \"configurable\": {\n", " \"thread_id\": \"3\",\n", " \"checkpoint_ns\": \"weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0\"\n", " }\n", " }\n", " }\n", "]\n" ] } ], "source": [ "JSON.stringify(state.tasks, null, 2);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "However since we got the state using the config of the parent graph, we don't have access to the subgraph state. If you look at the `state` value of the task above you will note that it is simply the configuration of the parent graph. If we want to actually populate the subgraph state, we can pass in `subgraphs: True` to the second parameter of `getState` like so:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[\n", " {\n", " \"id\": \"ec67e50f-d29c-5dee-8a80-08723a937de0\",\n", " \"name\": \"weatherGraph\",\n", " \"path\": [\n", " \"__pregel_pull\",\n", " \"weatherGraph\"\n", " ],\n", " \"interrupts\": [],\n", " \"state\": {\n", " \"values\": {\n", " \"messages\": [\n", " {\n", " \"lc\": 1,\n", " \"type\": \"constructor\",\n", " \"id\": [\n", " \"langchain_core\",\n", " \"messages\",\n", " \"HumanMessage\"\n", " ],\n", " \"kwargs\": {\n", " \"content\": \"what's the weather in sf\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {},\n", " \"id\": \"094b6752-6bea-4b43-b837-c6b0bb6a4c44\"\n", " }\n", " }\n", " ],\n", " \"city\": \"San Francisco\"\n", " },\n", " \"next\": [\n", " \"weatherNode\"\n", " ],\n", " \"tasks\": [\n", " {\n", " \"id\": \"2f2f8b8f-6a99-5225-8ff2-b6c49c3e9caf\",\n", " \"name\": \"weatherNode\",\n", " \"path\": [\n", " \"__pregel_pull\",\n", " \"weatherNode\"\n", " ],\n", " \"interrupts\": []\n", " }\n", " ],\n", " \"metadata\": {\n", " \"source\": \"loop\",\n", " \"writes\": {\n", " \"modelNode\": {\n", " \"city\": \"San Francisco\"\n", " }\n", " },\n", " \"step\": 1,\n", " \"parents\": {\n", " \"\": \"1ef7c6ba-3d36-65e0-8001-adc1f8841274\"\n", " }\n", " },\n", " \"config\": {\n", " \"configurable\": {\n", " \"thread_id\": \"3\",\n", " \"checkpoint_id\": \"1ef7c6ba-4503-6700-8001-61e828d1c772\",\n", " \"checkpoint_ns\": \"weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0\",\n", " \"checkpoint_map\": {\n", " \"\": \"1ef7c6ba-3d36-65e0-8001-adc1f8841274\",\n", " \"weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0\": \"1ef7c6ba-4503-6700-8001-61e828d1c772\"\n", " }\n", " }\n", " },\n", " \"createdAt\": \"2024-09-27T00:58:43.184Z\",\n", " \"parentConfig\": {\n", " \"configurable\": {\n", " \"thread_id\": \"3\",\n", " \"checkpoint_ns\": \"weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0\",\n", " \"checkpoint_id\": \"1ef7c6ba-3d3b-6400-8000-fe27ae37c785\"\n", " }\n", " }\n", " }\n", " }\n", "]\n" ] } ], "source": [ "const stateWithSubgraphs = await graph.getState({ configurable: { thread_id: \"3\" } }, { subgraphs: true })\n", "JSON.stringify(stateWithSubgraphs.tasks, null, 2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we have access to the subgraph state!\n", "\n", "To resume execution, we can just invoke the outer graph as normal:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[\n", " [],\n", " {\n", " messages: [\n", " HumanMessage {\n", " \"id\": \"094b6752-6bea-4b43-b837-c6b0bb6a4c44\",\n", " \"content\": \"what's the weather in sf\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {}\n", " }\n", " ],\n", " route: 'weather'\n", " }\n", "]\n", "[\n", " [ 'weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0' ],\n", " {\n", " messages: [\n", " HumanMessage {\n", " \"id\": \"094b6752-6bea-4b43-b837-c6b0bb6a4c44\",\n", " \"content\": \"what's the weather in sf\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {}\n", " }\n", " ],\n", " city: 'San Francisco'\n", " }\n", "]\n", "[\n", " [ 'weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0' ],\n", " {\n", " messages: [\n", " HumanMessage {\n", " \"id\": \"094b6752-6bea-4b43-b837-c6b0bb6a4c44\",\n", " \"content\": \"what's the weather in sf\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {}\n", " },\n", " AIMessage {\n", " \"id\": \"55d7a03f-876a-4887-9418-027321e747c7\",\n", " \"content\": \"It's sunny in San Francisco\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {},\n", " \"tool_calls\": [],\n", " \"invalid_tool_calls\": []\n", " }\n", " ],\n", " city: 'San Francisco'\n", " }\n", "]\n", "[\n", " [],\n", " {\n", " messages: [\n", " HumanMessage {\n", " \"id\": \"094b6752-6bea-4b43-b837-c6b0bb6a4c44\",\n", " \"content\": \"what's the weather in sf\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {}\n", " },\n", " AIMessage {\n", " \"id\": \"55d7a03f-876a-4887-9418-027321e747c7\",\n", " \"content\": \"It's sunny in San Francisco\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {},\n", " \"tool_calls\": [],\n", " \"invalid_tool_calls\": []\n", " }\n", " ],\n", " route: 'weather'\n", " }\n", "]\n" ] } ], "source": [ "const resumedStream = await graph.stream(null, {\n", " configurable: { thread_id: \"3\" },\n", " streamMode: \"values\",\n", " subgraphs: true,\n", "});\n", "\n", "for await (const update of resumedStream) {\n", " console.log(update);\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Resuming from specific subgraph node\n", "\n", "In the example above, we were replaying from the outer graph - which automatically replayed the subgraph from whatever state it was in previously (paused before the `weatherNode` in our case), but it is also possible to replay from inside a subgraph. In order to do so, we need to get the configuration from the exact subgraph state that we want to replay from.\n", "\n", "We can do this by exploring the state history of the subgraph, and selecting the state before `modelNode` - which we can do by filtering on the `.next` parameter.\n", "\n", "To get the state history of the subgraph, we need to first pass in the parent graph state before the subgraph:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "let parentGraphStateBeforeSubgraph;\n", "\n", "const histories = await graph.getStateHistory({ configurable: { thread_id: \"3\" } });\n", "\n", "for await (const historyEntry of histories) {\n", " if (historyEntry.next[0] === \"weatherGraph\") {\n", " parentGraphStateBeforeSubgraph = historyEntry;\n", " }\n", "}" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " values: {\n", " messages: [\n", " HumanMessage {\n", " \"id\": \"094b6752-6bea-4b43-b837-c6b0bb6a4c44\",\n", " \"content\": \"what's the weather in sf\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {}\n", " }\n", " ]\n", " },\n", " next: [ 'modelNode' ],\n", " tasks: [\n", " {\n", " id: '6d0d44fd-279b-56b0-8160-8f4929f9bfe6',\n", " name: 'modelNode',\n", " path: [Array],\n", " interrupts: [],\n", " state: undefined\n", " }\n", " ],\n", " metadata: {\n", " source: 'loop',\n", " writes: null,\n", " step: 0,\n", " parents: { '': '1ef7c6ba-3d36-65e0-8001-adc1f8841274' }\n", " },\n", " config: {\n", " configurable: {\n", " thread_id: '3',\n", " checkpoint_ns: 'weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0',\n", " checkpoint_id: '1ef7c6ba-3d3b-6400-8000-fe27ae37c785',\n", " checkpoint_map: [Object]\n", " }\n", " },\n", " createdAt: '2024-09-27T00:58:42.368Z',\n", " parentConfig: {\n", " configurable: {\n", " thread_id: '3',\n", " checkpoint_ns: 'weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0',\n", " checkpoint_id: '1ef7c6ba-3d38-6cf1-ffff-b3912beb00b9'\n", " }\n", " }\n", "}\n" ] } ], "source": [ "let subgraphStateBeforeModelNode;\n", "\n", "const subgraphHistories = await graph.getStateHistory(parentGraphStateBeforeSubgraph.tasks[0].state);\n", "\n", "for await (const subgraphHistoryEntry of subgraphHistories) {\n", " if (subgraphHistoryEntry.next[0] === \"modelNode\") {\n", " subgraphStateBeforeModelNode = subgraphHistoryEntry;\n", " }\n", "}\n", "\n", "console.log(subgraphStateBeforeModelNode);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This pattern can be extended no matter how many levels deep.\n", "\n", "We can confirm that we have gotten the correct state by comparing the `.next` parameter of the `subgraphStateBeforeModelNode`." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[ 'modelNode' ]\n" ] } ], "source": [ "subgraphStateBeforeModelNode.next;" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Perfect! We have gotten the correct state snaphshot, and we can now resume from the `modelNode` inside of our subgraph:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[\n", " [ 'weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0' ],\n", " { modelNode: { city: 'San Francisco' } }\n", "]\n" ] } ], "source": [ "const resumeSubgraphStream = await graph.stream(null, {\n", " ...subgraphStateBeforeModelNode.config,\n", " streamMode: \"updates\",\n", " subgraphs: true\n", "});\n", "\n", "for await (const value of resumeSubgraphStream) {\n", " console.log(value);\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can see that it reruns the `modelNode` and breaks right before the `weatherNode` as expected.\n", "\n", "This subsection has shown how you can replay from any node, no matter how deeply nested it is inside your graph - a powerful tool for testing how deterministic your agent is." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Modifying state\n", "\n", "### Update the state of a subgraph\n", "\n", "What if we want to modify the state of a subgraph? We can do this similarly to how we [update the state of normal graphs](https://langchain-ai.github.io/langgraphjs/how-tos/time-travel/). We just need to ensure we pass the config of the subgraph to `updateState`. Let's run our graph as before:" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{ routerNode: { route: 'weather' } }\n" ] } ], "source": [ "const graphStream = await graph.stream({\n", " messages: [{\n", " role: \"user\",\n", " content: \"what's the weather in sf\"\n", " }],\n", "}, {\n", " configurable: {\n", " thread_id: \"4\",\n", " }\n", "});\n", "\n", "for await (const update of graphStream) {\n", " console.log(update);\n", "}" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " values: {\n", " messages: [\n", " HumanMessage {\n", " \"id\": \"07ed1a38-13a9-4ec2-bc88-c4f6b713ec85\",\n", " \"content\": \"what's the weather in sf\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {}\n", " }\n", " ],\n", " city: 'San Francisco'\n", " },\n", " next: [ 'weatherNode' ],\n", " tasks: [\n", " {\n", " id: 'eabfbb82-6cf4-5ecd-932e-ed994ea44f23',\n", " name: 'weatherNode',\n", " path: [Array],\n", " interrupts: [],\n", " state: undefined\n", " }\n", " ],\n", " metadata: {\n", " source: 'loop',\n", " writes: { modelNode: [Object] },\n", " step: 1,\n", " parents: { '': '1ef7c6ba-563f-60f0-8001-4fce0e78ef56' }\n", " },\n", " config: {\n", " configurable: {\n", " thread_id: '4',\n", " checkpoint_id: '1ef7c6ba-5c71-6f90-8001-04f60f3c8173',\n", " checkpoint_ns: 'weatherGraph:8d8c9278-bd2a-566a-b9e1-e72286634681',\n", " checkpoint_map: [Object]\n", " }\n", " },\n", " createdAt: '2024-09-27T00:58:45.641Z',\n", " parentConfig: {\n", " configurable: {\n", " thread_id: '4',\n", " checkpoint_ns: 'weatherGraph:8d8c9278-bd2a-566a-b9e1-e72286634681',\n", " checkpoint_id: '1ef7c6ba-5641-6800-8000-96bcde048fa6'\n", " }\n", " }\n", "}\n" ] } ], "source": [ "const outerGraphState = await graph.getState({\n", " configurable: {\n", " thread_id: \"4\",\n", " }\n", "}, { subgraphs: true })\n", "\n", "console.log(outerGraphState.tasks[0].state);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In order to update the state of the **inner** graph, we need to pass the config for the **inner** graph, which we can get by accessing calling `state.tasks[0].state.config` - since we interrupted inside the subgraph, the state of the task is just the state of the subgraph." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " configurable: {\n", " thread_id: '4',\n", " checkpoint_ns: 'weatherGraph:8d8c9278-bd2a-566a-b9e1-e72286634681',\n", " checkpoint_id: '1ef7c6ba-5de0-62f0-8002-3618a75d1fce',\n", " checkpoint_map: {\n", " '': '1ef7c6ba-563f-60f0-8001-4fce0e78ef56',\n", " 'weatherGraph:8d8c9278-bd2a-566a-b9e1-e72286634681': '1ef7c6ba-5de0-62f0-8002-3618a75d1fce'\n", " }\n", " }\n", "}\n" ] } ], "source": [ "import type { StateSnapshot } from \"@langchain/langgraph\";\n", "\n", "await graph.updateState((outerGraphState.tasks[0].state as StateSnapshot).config, { city: \"la\" });" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can now resume streaming the outer graph (which will resume the subgraph!) and check that we updated our search to use LA instead of SF." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[\n", " [\n", " \"weatherGraph:8d8c9278-bd2a-566a-b9e1-e72286634681\"\n", " ],\n", " {\n", " \"weatherNode\": {\n", " \"messages\": [\n", " {\n", " \"role\": \"assistant\",\n", " \"content\": \"It's sunny in la\"\n", " }\n", " ]\n", " }\n", " }\n", "]\n", "[\n", " [],\n", " {\n", " \"weatherGraph\": {\n", " \"messages\": [\n", " {\n", " \"lc\": 1,\n", " \"type\": \"constructor\",\n", " \"id\": [\n", " \"langchain_core\",\n", " \"messages\",\n", " \"HumanMessage\"\n", " ],\n", " \"kwargs\": {\n", " \"content\": \"what's the weather in sf\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {},\n", " \"id\": \"07ed1a38-13a9-4ec2-bc88-c4f6b713ec85\"\n", " }\n", " },\n", " {\n", " \"lc\": 1,\n", " \"type\": \"constructor\",\n", " \"id\": [\n", " \"langchain_core\",\n", " \"messages\",\n", " \"AIMessage\"\n", " ],\n", " \"kwargs\": {\n", " \"content\": \"It's sunny in la\",\n", " \"tool_calls\": [],\n", " \"invalid_tool_calls\": [],\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {},\n", " \"id\": \"94c29f6c-38b3-420f-a9fb-bd85548f0c03\"\n", " }\n", " }\n", " ]\n", " }\n", " }\n", "]\n" ] } ], "source": [ "const resumedStreamWithUpdatedState = await graph.stream(null, {\n", " configurable: {\n", " thread_id: \"4\",\n", " },\n", " streamMode: \"updates\",\n", " subgraphs: true,\n", "})\n", "\n", "for await (const update of resumedStreamWithUpdatedState) {\n", " console.log(JSON.stringify(update, null, 2));\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Fantastic! The AI responded with \"It's sunny in LA!\" as we expected.\n", "\n", "### Acting as a subgraph node\n", "\n", "Instead of editing the state before `weatherNode` in the `weatherGraph` subgraph, another way we could update the state is by acting as the `weatherNode` ourselves. We can do this by passing the subgraph config along with a node name passed as a third positional argument, which allows us to update the state as if we are the node we specify.\n", "\n", "We will set an interrupt before the `weatherNode` and then using the update state function as the `weatherNode`, the graph itself never calls `weatherNode` directly but instead we decide what the output of `weatherNode` should be." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{ routerNode: { route: 'weather' } }\n", "interrupted!\n", "{\n", " values: {\n", " messages: [\n", " HumanMessage {\n", " \"id\": \"90e9ff28-5b13-4e10-819a-31999efe303c\",\n", " \"content\": \"What's the weather in sf\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {}\n", " }\n", " ],\n", " route: 'weather'\n", " },\n", " next: [ 'weatherGraph' ],\n", " tasks: [\n", " {\n", " id: 'f421fca8-de9e-5683-87ab-6ea9bb8d6275',\n", " name: 'weatherGraph',\n", " path: [Array],\n", " interrupts: [],\n", " state: [Object]\n", " }\n", " ],\n", " metadata: {\n", " source: 'loop',\n", " writes: { routerNode: [Object] },\n", " step: 1,\n", " parents: {}\n", " },\n", " config: {\n", " configurable: {\n", " thread_id: '14',\n", " checkpoint_id: '1ef7c6ba-63ac-68f1-8001-5f7ada5f98e8',\n", " checkpoint_ns: ''\n", " }\n", " },\n", " createdAt: '2024-09-27T00:58:46.399Z',\n", " parentConfig: {\n", " configurable: {\n", " thread_id: '14',\n", " checkpoint_ns: '',\n", " checkpoint_id: '1ef7c6ba-5f20-6020-8000-1ff649773a32'\n", " }\n", " }\n", "}\n", "[ [], { weatherGraph: { messages: [Array] } } ]\n", "{\n", " values: {\n", " messages: [\n", " HumanMessage {\n", " \"id\": \"90e9ff28-5b13-4e10-819a-31999efe303c\",\n", " \"content\": \"What's the weather in sf\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {}\n", " },\n", " AIMessage {\n", " \"id\": \"af761e6d-9f6a-4467-9a3c-489bed3fbad7\",\n", " \"content\": \"rainy\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {},\n", " \"tool_calls\": [],\n", " \"invalid_tool_calls\": []\n", " }\n", " ],\n", " route: 'weather'\n", " },\n", " next: [],\n", " tasks: [],\n", " metadata: {\n", " source: 'loop',\n", " writes: { weatherGraph: [Object] },\n", " step: 2,\n", " parents: {}\n", " },\n", " config: {\n", " configurable: {\n", " thread_id: '14',\n", " checkpoint_id: '1ef7c6ba-69e6-6cc0-8002-1751fc5bdd8f',\n", " checkpoint_ns: ''\n", " }\n", " },\n", " createdAt: '2024-09-27T00:58:47.052Z',\n", " parentConfig: {\n", " configurable: {\n", " thread_id: '14',\n", " checkpoint_ns: '',\n", " checkpoint_id: '1ef7c6ba-63ac-68f1-8001-5f7ada5f98e8'\n", " }\n", " }\n", "}\n" ] } ], "source": [ "const streamWithAsNode = await graph.stream({\n", " messages: [{\n", " role: \"user\",\n", " content: \"What's the weather in sf\",\n", " }]\n", "}, {\n", " configurable: {\n", " thread_id: \"14\",\n", " }\n", "});\n", "\n", "for await (const update of streamWithAsNode) {\n", " console.log(update);\n", "}\n", "\n", "// Graph execution should stop before the weather node\n", "console.log(\"interrupted!\");\n", "\n", "const interruptedState = await graph.getState({\n", " configurable: {\n", " thread_id: \"14\",\n", " }\n", "}, { subgraphs: true });\n", "\n", "console.log(interruptedState);\n", "\n", "// We update the state by passing in the message we want returned from the weather node\n", "// and make sure to pass `\"weatherNode\"` to signify that we want to act as this node.\n", "await graph.updateState((interruptedState.tasks[0].state as StateSnapshot).config, {\n", " messages: [{\n", " \"role\": \"assistant\",\n", " \"content\": \"rainy\"\n", " }]\n", "}, \"weatherNode\");\n", "\n", "\n", "const resumedStreamWithAsNode = await graph.stream(null, {\n", " configurable: {\n", " thread_id: \"14\",\n", " },\n", " streamMode: \"updates\",\n", " subgraphs: true,\n", "});\n", "\n", "for await (const update of resumedStreamWithAsNode) {\n", " console.log(update);\n", "}\n", "\n", "console.log(await graph.getState({\n", " configurable: {\n", " thread_id: \"14\",\n", " }\n", "}, { subgraphs: true }));" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Perfect! The agent responded with the message we passed in ourselves, and identified the weather in SF as `rainy` instead of `sunny`.\n", "\n", "### Acting as the entire subgraph\n", "\n", "Lastly, we could also update the graph just acting as the **entire** subgraph. This is similar to the case above but instead of acting as just the `weatherNode` we are acting as the entire `weatherGraph` subgraph. This is done by passing in the normal graph config as well as the `asNode` argument, where we specify the we are acting as the entire subgraph node." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[ [], { routerNode: { route: 'weather' } } ]\n", "[\n", " [ 'weatherGraph:db9c3bb2-5d27-5dae-a724-a8d702b33e86' ],\n", " { modelNode: { city: 'San Francisco' } }\n", "]\n", "interrupted!\n", "[\n", " HumanMessage {\n", " \"id\": \"001282b0-ca2e-443f-b6ee-8cb16c81bf59\",\n", " \"content\": \"what's the weather in sf\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {}\n", " },\n", " AIMessage {\n", " \"id\": \"4b5d49cf-0f87-4ee8-b96f-eaa8716b9e9c\",\n", " \"content\": \"rainy\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {},\n", " \"tool_calls\": [],\n", " \"invalid_tool_calls\": []\n", " }\n", "]\n" ] } ], "source": [ "const entireSubgraphExampleStream = await graph.stream({\n", " messages: [\n", " {\n", " role: \"user\",\n", " content: \"what's the weather in sf\"\n", " }\n", " ],\n", "}, {\n", " configurable: {\n", " thread_id: \"8\",\n", " },\n", " streamMode: \"updates\",\n", " subgraphs: true,\n", "});\n", "\n", "for await (const update of entireSubgraphExampleStream) {\n", " console.log(update);\n", "}\n", "\n", "// Graph execution should stop before the weather node\n", "console.log(\"interrupted!\");\n", "\n", "// We update the state by passing in the message we want returned from the weather graph.\n", "// Note that we don't need to pass in the subgraph config, since we aren't updating the state inside the subgraph\n", "await graph.updateState({\n", " configurable: {\n", " thread_id: \"8\",\n", " }\n", "}, {\n", " messages: [{ role: \"assistant\", content: \"rainy\" }]\n", "}, \"weatherGraph\");\n", "\n", "const resumedEntireSubgraphExampleStream = await graph.stream(null, {\n", " configurable: {\n", " thread_id: \"8\",\n", " },\n", " streamMode: \"updates\",\n", "});\n", "\n", "for await (const update of resumedEntireSubgraphExampleStream) {\n", " console.log(update);\n", "}\n", "\n", "const currentStateAfterUpdate = await graph.getState({\n", " configurable: {\n", " thread_id: \"8\",\n", " }\n", "});\n", "\n", "console.log(currentStateAfterUpdate.values.messages);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Again, the agent responded with \"rainy\" as we expected.\n", "\n", "## Double nested subgraphs\n", "\n", "This same functionality continues to work no matter the level of nesting. Here is an example of doing the same things with a double nested subgraph (although any level of nesting will work). We add another router on top of our already defined graphs.\n", "\n", "First, let's recreate the graph we've been using above. This time we will compile it with no checkpointer, since it itself will be a subgraph!" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "const parentGraph = new StateGraph(RouterStateAnnotation)\n", " .addNode(\"routerNode\", routerNode)\n", " .addNode(\"normalLLMNode\", normalLLMNode)\n", " .addNode(\"weatherGraph\", subgraph)\n", " .addEdge(\"__start__\", \"routerNode\")\n", " .addConditionalEdges(\"routerNode\", routeAfterPrediction)\n", " .addEdge(\"normalLLMNode\", \"__end__\")\n", " .addEdge(\"weatherGraph\", \"__end__\")\n", " .compile();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's declare a \"grandparent\" graph that uses this graph as a subgraph:" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "const checkpointer = new MemorySaver();\n", "\n", "const GrandfatherStateAnnotation = Annotation.Root({\n", " ...MessagesAnnotation.spec,\n", " toContinue: Annotation,\n", "});\n", "\n", "const grandparentRouterNode = async (_state: typeof GrandfatherStateAnnotation.State) => {\n", " // Dummy logic that will always continue\n", " return { toContinue: true };\n", "};\n", "\n", "const grandparentConditionalEdge = async (state: typeof GrandfatherStateAnnotation.State) => {\n", " if (state.toContinue) {\n", " return \"parentGraph\";\n", " } else {\n", " return \"__end__\";\n", " }\n", "};\n", "\n", "const grandparentGraph = new StateGraph(GrandfatherStateAnnotation)\n", " .addNode(\"routerNode\", grandparentRouterNode)\n", " .addNode(\"parentGraph\", parentGraph)\n", " .addEdge(\"__start__\", \"routerNode\")\n", " .addConditionalEdges(\"routerNode\", grandparentConditionalEdge)\n", " .addEdge(\"parentGraph\", \"__end__\")\n", " .compile({ checkpointer });" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here's a diagram showing what this looks like:\n", "\n", "![](../img/doubly-nested-subgraph.jpeg)\n", "\n", "If we run until the interrupt, we can now see that there are snapshots of the state of all three graphs" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[ [], { routerNode: { toContinue: true } } ]\n", "[\n", " [ 'parentGraph:095bb8a9-77d3-5a0c-9a23-e1390dcf36bc' ],\n", " { routerNode: { route: 'weather' } }\n", "]\n", "[\n", " [\n", " 'parentGraph:095bb8a9-77d3-5a0c-9a23-e1390dcf36bc',\n", " 'weatherGraph:b1da376c-25a5-5aae-82da-4ff579f05d43'\n", " ],\n", " { modelNode: { city: 'San Francisco' } }\n", "]\n" ] } ], "source": [ "const grandparentConfig = {\n", " configurable: { thread_id: \"123\" },\n", "};\n", "\n", "const grandparentGraphStream = await grandparentGraph.stream({\n", " messages: [{\n", " role: \"user\",\n", " content: \"what's the weather in SF\"\n", " }],\n", "}, {\n", " ...grandparentConfig,\n", " streamMode: \"updates\",\n", " subgraphs: true\n", "});\n", "\n", "for await (const update of grandparentGraphStream) {\n", " console.log(update);\n", "}" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Grandparent State:\n", "{\n", " messages: [\n", " HumanMessage {\n", " \"id\": \"5788e436-a756-4ff5-899a-82117a5c59c7\",\n", " \"content\": \"what's the weather in SF\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {}\n", " }\n", " ],\n", " toContinue: true\n", "}\n", "---------------\n", "Parent Graph State:\n", "{\n", " messages: [\n", " HumanMessage {\n", " \"id\": \"5788e436-a756-4ff5-899a-82117a5c59c7\",\n", " \"content\": \"what's the weather in SF\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {}\n", " }\n", " ],\n", " route: 'weather'\n", "}\n", "---------------\n", "Subgraph State:\n", "{\n", " messages: [\n", " HumanMessage {\n", " \"id\": \"5788e436-a756-4ff5-899a-82117a5c59c7\",\n", " \"content\": \"what's the weather in SF\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {}\n", " }\n", " ],\n", " city: 'San Francisco'\n", "}\n" ] } ], "source": [ "const grandparentGraphState = await grandparentGraph.getState(\n", " grandparentConfig, \n", " { subgraphs: true }\n", ");\n", "\n", "const parentGraphState = grandparentGraphState.tasks[0].state as StateSnapshot;\n", "const subgraphState = parentGraphState.tasks[0].state as StateSnapshot;\n", "\n", "console.log(\"Grandparent State:\");\n", "console.log(grandparentGraphState.values);\n", "console.log(\"---------------\");\n", "console.log(\"Parent Graph State:\");\n", "console.log(parentGraphState.values);\n", "console.log(\"---------------\");\n", "console.log(\"Subgraph State:\");\n", "console.log(subgraphState.values);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can now continue, acting as the node three levels down" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[\n", " [ 'parentGraph:095bb8a9-77d3-5a0c-9a23-e1390dcf36bc' ],\n", " { weatherGraph: { messages: [Array] } }\n", "]\n", "[ [], { parentGraph: { messages: [Array] } } ]\n", "[\n", " HumanMessage {\n", " \"id\": \"5788e436-a756-4ff5-899a-82117a5c59c7\",\n", " \"content\": \"what's the weather in SF\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {}\n", " },\n", " AIMessage {\n", " \"id\": \"1c161973-9a9d-414d-b631-56791d85e2fb\",\n", " \"content\": \"rainy\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {},\n", " \"tool_calls\": [],\n", " \"invalid_tool_calls\": []\n", " }\n", "]\n" ] } ], "source": [ "await grandparentGraph.updateState(subgraphState.config, {\n", " messages: [{\n", " role: \"assistant\",\n", " content: \"rainy\"\n", " }]\n", "}, \"weatherNode\");\n", "\n", "const updatedGrandparentGraphStream = await grandparentGraph.stream(null, {\n", " ...grandparentConfig,\n", " streamMode: \"updates\",\n", " subgraphs: true,\n", "});\n", "\n", "for await (const update of updatedGrandparentGraphStream) {\n", " console.log(update);\n", "}\n", "\n", "console.log((await grandparentGraph.getState(grandparentConfig)).values.messages)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As in the cases above, we can see that the AI responds with \"rainy\" as we expect.\n", "\n", "We can explore the state history to see how the state of the grandparent graph was updated at each step." ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " values: {\n", " messages: [\n", " HumanMessage {\n", " \"id\": \"5788e436-a756-4ff5-899a-82117a5c59c7\",\n", " \"content\": \"what's the weather in SF\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {}\n", " },\n", " AIMessage {\n", " \"id\": \"1c161973-9a9d-414d-b631-56791d85e2fb\",\n", " \"content\": \"rainy\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {},\n", " \"tool_calls\": [],\n", " \"invalid_tool_calls\": []\n", " }\n", " ],\n", " toContinue: true\n", " },\n", " next: [],\n", " tasks: [],\n", " metadata: {\n", " source: 'loop',\n", " writes: { parentGraph: [Object] },\n", " step: 2,\n", " parents: {}\n", " },\n", " config: {\n", " configurable: {\n", " thread_id: '123',\n", " checkpoint_ns: '',\n", " checkpoint_id: '1ef7c6ba-8560-67d0-8002-2e2cedd7de18'\n", " }\n", " },\n", " createdAt: '2024-09-27T00:58:49.933Z',\n", " parentConfig: {\n", " configurable: {\n", " thread_id: '123',\n", " checkpoint_ns: '',\n", " checkpoint_id: '1ef7c6ba-7977-62c0-8001-13e89cb2bbab'\n", " }\n", " }\n", "}\n", "-----\n", "{\n", " values: {\n", " messages: [\n", " HumanMessage {\n", " \"id\": \"5788e436-a756-4ff5-899a-82117a5c59c7\",\n", " \"content\": \"what's the weather in SF\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {}\n", " }\n", " ],\n", " toContinue: true\n", " },\n", " next: [ 'parentGraph' ],\n", " tasks: [\n", " {\n", " id: '095bb8a9-77d3-5a0c-9a23-e1390dcf36bc',\n", " name: 'parentGraph',\n", " path: [Array],\n", " interrupts: [],\n", " state: [Object]\n", " }\n", " ],\n", " metadata: {\n", " source: 'loop',\n", " writes: { routerNode: [Object] },\n", " step: 1,\n", " parents: {}\n", " },\n", " config: {\n", " configurable: {\n", " thread_id: '123',\n", " checkpoint_ns: '',\n", " checkpoint_id: '1ef7c6ba-7977-62c0-8001-13e89cb2bbab'\n", " }\n", " },\n", " createdAt: '2024-09-27T00:58:48.684Z',\n", " parentConfig: {\n", " configurable: {\n", " thread_id: '123',\n", " checkpoint_ns: '',\n", " checkpoint_id: '1ef7c6ba-7972-64a0-8000-243575e3244f'\n", " }\n", " }\n", "}\n", "-----\n", "{\n", " values: {\n", " messages: [\n", " HumanMessage {\n", " \"id\": \"5788e436-a756-4ff5-899a-82117a5c59c7\",\n", " \"content\": \"what's the weather in SF\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {}\n", " }\n", " ]\n", " },\n", " next: [ 'routerNode' ],\n", " tasks: [\n", " {\n", " id: '00ed334c-47b5-5693-92b4-a5b83373e2a0',\n", " name: 'routerNode',\n", " path: [Array],\n", " interrupts: [],\n", " state: undefined\n", " }\n", " ],\n", " metadata: { source: 'loop', writes: null, step: 0, parents: {} },\n", " config: {\n", " configurable: {\n", " thread_id: '123',\n", " checkpoint_ns: '',\n", " checkpoint_id: '1ef7c6ba-7972-64a0-8000-243575e3244f'\n", " }\n", " },\n", " createdAt: '2024-09-27T00:58:48.682Z',\n", " parentConfig: {\n", " configurable: {\n", " thread_id: '123',\n", " checkpoint_ns: '',\n", " checkpoint_id: '1ef7c6ba-796f-6d90-ffff-25ed0eb5bb38'\n", " }\n", " }\n", "}\n", "-----\n", "{\n", " values: { messages: [] },\n", " next: [ '__start__' ],\n", " tasks: [\n", " {\n", " id: 'ea62628e-881d-558d-bafc-e8b6a734e8aa',\n", " name: '__start__',\n", " path: [Array],\n", " interrupts: [],\n", " state: undefined\n", " }\n", " ],\n", " metadata: {\n", " source: 'input',\n", " writes: { __start__: [Object] },\n", " step: -1,\n", " parents: {}\n", " },\n", " config: {\n", " configurable: {\n", " thread_id: '123',\n", " checkpoint_ns: '',\n", " checkpoint_id: '1ef7c6ba-796f-6d90-ffff-25ed0eb5bb38'\n", " }\n", " },\n", " createdAt: '2024-09-27T00:58:48.681Z',\n", " parentConfig: undefined\n", "}\n", "-----\n" ] } ], "source": [ "const grandparentStateHistories = await grandparentGraph.getStateHistory(grandparentConfig);\n", "for await (const state of grandparentStateHistories) {\n", " console.log(state);\n", " console.log(\"-----\");\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "TypeScript", "language": "typescript", "name": "tslab" }, "language_info": { "codemirror_mode": { "mode": "typescript", "name": "javascript", "typescript": true }, "file_extension": ".ts", "mimetype": "text/typescript", "name": "typescript", "version": "3.7.2" } }, "nbformat": 4, "nbformat_minor": 4 }