[PR #1768] [MERGED] fix(@langchain/langgraph-checkpoint-redis): deserialize checkpointed state #1713

Closed
opened 2026-02-15 20:16:36 -05:00 by yindo · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/langchain-ai/langgraphjs/pull/1768
Author: @christian-bromann
Created: 11/3/2025
Status: Merged
Merged: 12/1/2025
Merged by: @dqbd

Base: mainHead: cb/unserialize-messages


📝 Commits (2)

  • 0bc3bf0 fix(@langchain/langgraph-checkpoint-redis): deserialize checkpointed state
  • 5c89bc6 Create stale-islands-pull.md

📊 Changes

3 files changed (+72 additions, -30 deletions)

View changed files

.changeset/stale-islands-pull.md (+5 -0)
📝 libs/checkpoint-redis/src/index.ts (+36 -10)
📝 libs/checkpoint-redis/src/shallow.ts (+31 -20)

📄 Description

I found a bug where the redis checkpointer wouldn't deserialize the checkpointed state when the graph is interrupted. The state would be something like this:

{
  messages: [
    { lc: 1, type: 'constructor', id: [Array], kwargs: [Object] },
    { lc: 1, type: 'constructor', id: [Array], kwargs: [Object] },
    { lc: 1, type: 'constructor', id: [Array], kwargs: [Object] },
    { lc: 1, type: 'constructor', id: [Array], kwargs: [Object] },
    { lc: 1, type: 'constructor', id: [Array], kwargs: [Object] },
    { lc: 1, type: 'constructor', id: [Array], kwargs: [Object] },
    { lc: 1, type: 'constructor', id: [Array], kwargs: [Object] },
    { lc: 1, type: 'constructor', id: [Array], kwargs: [Object] }
  ]
}

You can reproduce the bug with the following script:

import { z } from "zod";
import { createAgent, tool, createMiddleware, HumanMessage, ToolMessage } from "langchain";
import { Command, MemorySaver } from "@langchain/langgraph";
import { RedisSaver } from "@langchain/langgraph-checkpoint-redis";

const config = { configurable: { thread_id: "thread-123" } };

const getWeather = tool(
  async (input: { city: string }) => {
    return {
      weather: `${input.city} is ${Math.random() > 0.5 ? "sunny" : "rainy"}`,
    };
  },
  {
    name: "get_weather",
    description: "Get the weather for a city",
    schema: z.object({
      city: z.string(),
    }),
  }
);

// const memorySaver = new MemorySaver();
const redisSaver = await RedisSaver.fromUrl(process.env.REDIS_URL!, {
  defaultTTL: 60, // TTL in minutes
  refreshOnRead: true,
});

const interruptMiddleware = createMiddleware({
  name: "memory",
  async beforeModel(state, runtime) {
    console.log("state1 is", state)
    if (ToolMessage.isInstance(state.messages.at(-1))) {
      const result = await runtime.interrupt?.({
        actionRequests: [
          {
            name: "get_weather",
            args: { city: "Tokyo" },
          },
        ],
      })
      console.log("result is", result)
    }
  },
})

const agent = createAgent({
  model: "anthropic:claude-3-7-sonnet-latest",
  tools: [getWeather],
  checkpointer: redisSaver,
  middleware: [interruptMiddleware],
});

const initialState = {
  messages: [new HumanMessage("What is the weather in Tokyo?")],
};

const stream1 = await agent.stream(initialState, config);
for await (const chunk of stream1) {
  // console.log("chunk1 is", chunk)
}

const agent2 = createAgent({
  model: "anthropic:claude-3-7-sonnet-latest",
  tools: [getWeather],
  checkpointer: redisSaver,
  middleware: [interruptMiddleware],
});

const result2 = await agent2.stream(new Command({
  resume: {
    decisions: [
      {
        type: "approve",
      },
    ],
  },
}), config);
for await (const chunk of result2) {
  // console.log("chunk2 is", chunk)
}

console.log(result2);

Solution

This patch ensures that state is deserialized properly.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/langchain-ai/langgraphjs/pull/1768 **Author:** [@christian-bromann](https://github.com/christian-bromann) **Created:** 11/3/2025 **Status:** ✅ Merged **Merged:** 12/1/2025 **Merged by:** [@dqbd](https://github.com/dqbd) **Base:** `main` ← **Head:** `cb/unserialize-messages` --- ### 📝 Commits (2) - [`0bc3bf0`](https://github.com/langchain-ai/langgraphjs/commit/0bc3bf0615ab26c89f10ab6698f82119f1fa6f8b) fix(@langchain/langgraph-checkpoint-redis): deserialize checkpointed state - [`5c89bc6`](https://github.com/langchain-ai/langgraphjs/commit/5c89bc6c0800f657b2a5b0be516b811eed46fb1b) Create stale-islands-pull.md ### 📊 Changes **3 files changed** (+72 additions, -30 deletions) <details> <summary>View changed files</summary> ➕ `.changeset/stale-islands-pull.md` (+5 -0) 📝 `libs/checkpoint-redis/src/index.ts` (+36 -10) 📝 `libs/checkpoint-redis/src/shallow.ts` (+31 -20) </details> ### 📄 Description I found a bug where the redis checkpointer wouldn't deserialize the checkpointed state when the graph is interrupted. The state would be something like this: ```ts { messages: [ { lc: 1, type: 'constructor', id: [Array], kwargs: [Object] }, { lc: 1, type: 'constructor', id: [Array], kwargs: [Object] }, { lc: 1, type: 'constructor', id: [Array], kwargs: [Object] }, { lc: 1, type: 'constructor', id: [Array], kwargs: [Object] }, { lc: 1, type: 'constructor', id: [Array], kwargs: [Object] }, { lc: 1, type: 'constructor', id: [Array], kwargs: [Object] }, { lc: 1, type: 'constructor', id: [Array], kwargs: [Object] }, { lc: 1, type: 'constructor', id: [Array], kwargs: [Object] } ] } ``` You can reproduce the bug with the following script: ```ts import { z } from "zod"; import { createAgent, tool, createMiddleware, HumanMessage, ToolMessage } from "langchain"; import { Command, MemorySaver } from "@langchain/langgraph"; import { RedisSaver } from "@langchain/langgraph-checkpoint-redis"; const config = { configurable: { thread_id: "thread-123" } }; const getWeather = tool( async (input: { city: string }) => { return { weather: `${input.city} is ${Math.random() > 0.5 ? "sunny" : "rainy"}`, }; }, { name: "get_weather", description: "Get the weather for a city", schema: z.object({ city: z.string(), }), } ); // const memorySaver = new MemorySaver(); const redisSaver = await RedisSaver.fromUrl(process.env.REDIS_URL!, { defaultTTL: 60, // TTL in minutes refreshOnRead: true, }); const interruptMiddleware = createMiddleware({ name: "memory", async beforeModel(state, runtime) { console.log("state1 is", state) if (ToolMessage.isInstance(state.messages.at(-1))) { const result = await runtime.interrupt?.({ actionRequests: [ { name: "get_weather", args: { city: "Tokyo" }, }, ], }) console.log("result is", result) } }, }) const agent = createAgent({ model: "anthropic:claude-3-7-sonnet-latest", tools: [getWeather], checkpointer: redisSaver, middleware: [interruptMiddleware], }); const initialState = { messages: [new HumanMessage("What is the weather in Tokyo?")], }; const stream1 = await agent.stream(initialState, config); for await (const chunk of stream1) { // console.log("chunk1 is", chunk) } const agent2 = createAgent({ model: "anthropic:claude-3-7-sonnet-latest", tools: [getWeather], checkpointer: redisSaver, middleware: [interruptMiddleware], }); const result2 = await agent2.stream(new Command({ resume: { decisions: [ { type: "approve", }, ], }, }), config); for await (const chunk of result2) { // console.log("chunk2 is", chunk) } console.log(result2); ``` ## Solution This patch ensures that state is deserialized properly. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
yindo added the pull-request label 2026-02-15 20:16:36 -05:00
yindo closed this issue 2026-02-15 20:16:36 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraphjs#1713