Skip to content

Tool interrupt clears state history in browser #616

Description

@dpbayer

I am encountering an issue where, an interrupt is causing my message history to be forgotten, as well as other odd state behaviour.

The following is a simplified setup to show the issue. The issue is occurring in a browser based application using "deepagents/browser"

Setup

  1. A state back end, and a checkpointer using MemorySaver
  2. A parent agent, with two subagents, “hello” and “goodbye”
  3. The “hello” agent uses an interrupt to check it is allowed to say “hello”,
  4. The “goodbye” agent simply says goodbye.

Steps

  1. I ask the root agent to say hello, it performs the request, and routes to the hello agent. The hello agent initiates an approval (using interrupt) and then says hello..
  2. I ask the root agent what I have asked it to do. It has forgotten its message history and is unable to say.
  3. Restart the app
  4. If I ask the root agent to say goodbye, it performs the request.
  5. I then ask the root agent what I have asked it to do, and it reports back my previous requests.

The checkpoint state / message history has become corrupted or lost when I have used a subagent tool interrupt. This then affects other workflows and follow up questions.

Sample code

Note: For the sample code to work in the browser, it is necessary to register an AsyncLocalStorage. This file provides an AsyncLocalStorage polyfill
https://github.com/langchain-ai/langgraphjs/blob/main/libs/langgraph-core/src/tests/browser.ts

Agent setup
import { z } from "zod";
import { tool } from "langchain";
import { Command, interrupt, MemorySaver } from "@langchain/langgraph";
import { createDeepAgent, StateBackend, type AnySubAgent, type CreateDeepAgentParams } from "deepagents/browser";
import { createChatModel } from "./modelProvider.ts";


function createThreadId(prefix: string): string {
  const timestamp = Date.now().toString(36);
  const randomPart = Math.random().toString(36).slice(2, 10);

  return `${prefix}-${timestamp}-${randomPart}`;
}

const sayHello = tool(
  async ({ name }: { name: string }) => {
    const approval = interrupt({
      type: "approval_request",
      action: `say hello to ${name}`,
      message: `Please approve or reject: say hello to ${name}`,
    }) as { approved?: boolean; reason?: string };

    if (!approval.approved) {
      return `Greeting for ${name} was REJECTED. Reason: ${approval.reason || "No reason provided"}`;
    }

    return `Greeting for ${name} was APPROVED. Hello, ${name}!`;
  },
  {
    name: "sayHello",
    description: "Ask for approval and then return a greeting.",
    schema: z.object({
      name: z.string().default("friend").describe("Who should be greeted"),
    }),
  },
);

const greeterSubagent: AnySubAgent = {
  name: "greeter",
  description: "Handles greeting requests using the sayHello tool.",
  systemPrompt: "If asked to say hello, call the sayHello tool.",
  tools: [sayHello],
};

const sayGoodbyeTool = tool(
  async ({ name }: { name: string }) => {
    return `Goodbye, ${name}!`;
  },
  {
    name: "sayGoodbye",
    description: "Return a goodbye message without requiring approval.",
    schema: z.object({
      name: z.string().default("friend").describe("Who should receive the goodbye"),
    }),
  },
);

const sayGoodbyeSubagent: AnySubAgent = {
  name: "sayGoodbye",
  description: "Handles goodbye requests using the sayGoodbye tool.",
  systemPrompt: "If asked to say goodbye, call the sayGoodbye tool.",
  tools: [sayGoodbyeTool],
};

async function createRootAgent() {
  const model = (await createChatModel({ modelTier: "fast" })) as DeepAgentModel;
  const checkpointer = new MemorySaver();

  return createDeepAgent({
    model,
    checkpointer,
    backend: (runtime) => new StateBackend(runtime),
    subagents: [greeterSubagent, sayGoodbyeSubagent],
    systemPrompt:
      "You are a root assistant. Delegate hello requests to greeter and goodbye requests to sayGoodbye. Keep answers concise.",
  });
}

Hello conversation
thread_id: hello-thread-mqqrutpv-nae4v4r3
Step 1 input: say hello
Interrupt count: 1
Interrupt type: approval_request
Interrupt action: say hello to friend
Interrupt message: Please approve or reject: say hello to friend
Interrupt decision: approved
Step 1 output: Hello, friend!
Step 2 input: tell me the last few requests that I have asked you to do. I am trying to see if you remember my requests or have lost your memory
Step 2 output: I don’t have access to your request history. If you paste the recent conversation/log here (or tell me which app/session), I can summarize the last few requests from that text.
 async function runHelloFlow(): Promise<string> {
  const threadId = createThreadId("hello-thread");
  const agent = await createRootAgent();

  const config: InvokeConfig = { configurable: { thread_id: threadId } };
  const lines: string[] = [];
  lines.push(`thread_id: ${threadId}`);

  const firstResult = await agent.invoke(
    {
      messages: [{ role: "user", content: "say hello" }],
    },
    config,
  );

  lines.push("Step 1 input: say hello");

  const interrupts = (firstResult as { __interrupt__?: Array<{ value?: unknown }> }).__interrupt__;
  lines.push(`Interrupt count: ${interrupts?.length ?? 0}`);

  let helloResponse: unknown = firstResult;

  if (interrupts && interrupts.length > 0) {
    const interruptValue = (interrupts[0].value ?? {}) as {
      type?: string;
      action?: string;
      message?: string;
    };

    lines.push(`Interrupt type: ${interruptValue.type ?? "(unknown)"}`);
    lines.push(`Interrupt action: ${interruptValue.action ?? "(unknown)"}`);
    lines.push(`Interrupt message: ${interruptValue.message ?? "(no message)"}`);

    const approved = window.confirm(interruptValue.message ?? "Please approve this action.");
    lines.push(`Interrupt decision: ${approved ? "approved" : "rejected"}`);

    helloResponse = await agent.invoke(
      new Command({
        resume: approved
          ? { approved: true }
          : {
              approved: false,
              reason: "Rejected by user in browser confirmation dialog.",
            },
      }),
      config,
    );
  }

  lines.push(`Step 1 output: ${getLastAssistantMessage(helloResponse) ?? "(no assistant output found)"}`);

  const secondResult = await agent.invoke(
    {
      messages: [{ role: "user", content: "tell me the last few requests that I have made" }],
    },
    config,
  );

  lines.push("Step 2 input: tell me the last few requests that I have asked you to do. I am trying to see if you remember my requests or have lost your memory");
  lines.push(`Step 2 output: ${getLastAssistantMessage(secondResult) ?? "(no assistant output found)"}`);

  return lines.join("\n");
}
Goodbye conversation
thread_id: goodbye-thread-mqqrx7ib-72m3notm
Step 1 input: say goodbye
Step 1 output: Goodbye, friend!
Step 2 input: tell me the last few requests that I have asked you to do. I am trying to see if you remember my requests or have lost your memory
Step 2 output: I can only see the requests in this chat so far:

1) “say goodbye”  
2) “tell me the last few requests that I have made”
async function runGoodbyeFlow(): Promise<string> {
  const threadId = createThreadId("goodbye-thread");
  const agent = await createRootAgent();
  const config: InvokeConfig = { configurable: { thread_id: threadId } };
  const lines: string[] = [];

  lines.push(`thread_id: ${threadId}`);

  const firstResult = await agent.invoke(
    {
      messages: [{ role: "user", content: "say goodbye" }],
    },
    config,
  );

  lines.push("Step 1 input: say goodbye");
  lines.push(`Step 1 output: ${getLastAssistantMessage(firstResult) ?? "(no assistant output found)"}`);

  const secondResult = await agent.invoke(
    {
      messages: [{ role: "user", content: "tell me the last few requests that I have made" }],
    },
    config,
  );

  lines.push("Step 2 input: tell me the last few requests that I have asked you to do. I am trying to see if you remember my requests or have lost your memory");
  lines.push(`Step 2 output: ${getLastAssistantMessage(secondResult) ?? "(no assistant output found)"}`);

  return lines.join("\n");
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions