Skip to content

Conversation

@christian-bromann
Copy link
Member

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.

@changeset-bot
Copy link

changeset-bot bot commented Nov 3, 2025

⚠️ No Changeset found

Latest commit: 0bc3bf0

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants