Intermediate Messages Not Output Until All Interrupts Are Resolved in a Single Graph #267

Closed
opened 2026-02-15 18:15:17 -05:00 by yindo · 1 comment
Owner

Originally created by @sveyek on GitHub (May 27, 2025).

In LangGraph, when a single graph contains multiple interrupts within a ReAct agent node, intermediate messages added to the state are not emitted via graph.stream (using stream_mode="updates") until all interrupts are resolved.

Context

  • Setup: Multiple ReAct agents, each with different tools, managed by a LangGraph supervisor.
  • Issue: Some tools bound to a ReAct agent trigger interrupts for user accept/decline actions. When multiple interrupted tools are called within a single agent, intermediate messages are not streamed until the last interrupt is resolved.
  • Example: When streaming a query with agent.stream, the supervisor delegates to Agent 1, which calls three interrupted tools. Messages up to the first interrupt are streamed, but no further messages appear until the third interrupt is resolved, at which point all intermediate messages are output.

Steps to Reproduce

  1. Set up a LangGraph with a supervisor and a ReAct agent node.
  2. Bind multiple tools to the agent, with some triggering interrupts.
  3. Add intermediate messages to the state before each interrupt.
  4. Stream the graph with graph.stream, handling interrupts.
  5. Observe that intermediate messages are only streamed after resolving all interrupts.
Originally created by @sveyek on GitHub (May 27, 2025). In LangGraph, when a single graph contains multiple interrupts within a ReAct agent node, intermediate messages added to the state are not emitted via graph.stream (using stream_mode="updates") until all interrupts are resolved. **Context** - _Setup_: Multiple ReAct agents, each with different tools, managed by a LangGraph supervisor. - _Issue_: Some tools bound to a ReAct agent trigger interrupts for user accept/decline actions. When multiple interrupted tools are called within a single agent, intermediate messages are not streamed until the last interrupt is resolved. - _Example_: When streaming a query with agent.stream, the supervisor delegates to Agent 1, which calls three interrupted tools. Messages up to the first interrupt are streamed, but no further messages appear until the third interrupt is resolved, at which point all intermediate messages are output. **Steps to Reproduce** 1. Set up a LangGraph with a supervisor and a ReAct agent node. 2. Bind multiple tools to the agent, with some triggering interrupts. 3. Add intermediate messages to the state before each interrupt. 4. Stream the graph with graph.stream, handling interrupts. 5. Observe that intermediate messages are only streamed after resolving all interrupts.
yindo closed this issue 2026-02-15 18:15:17 -05:00
Author
Owner

@dqbd commented on GitHub (Sep 11, 2025):

Hello! This behaviour is by design, since interrupt will cancel the execution of the node and prevent persisting any state. If you do need to persist state in a separate node and then call interrupt in a different node, eg something like this:

import {
  Command,
  interrupt,
  MemorySaver,
  Send,
  StateGraph,
} from "@langchain/langgraph";
import { registry } from "@langchain/langgraph/zod";
import { z } from "zod/v4";

const checkpointer = new MemorySaver();

const graph = new StateGraph(
  z.object({
    tasks: z.array(z.string()),
    taskResult: z.array(z.string()).register(registry, {
      reducer: { schema: z.string(), fn: (a, b) => [...a, b] },
      default: () => [] as string[],
    }),
  }),
)
  .addNode(
    "map",
    async (state) =>
      new Command({
        goto: state.tasks.map((task) => new Send("task", task)),
      }),
    { ends: ["task"] },
  )
  .addNode(
    "task",
    async (task: string) => {
      if (task.includes("interrupt")) {
        return new Command({
          goto: new Send("task_interrupt", task),
          update: { taskResult: [`task result for "${task}"`] },
        });
      }

      return { taskResult: [`task result for "${task}"`] };
    },
    { ends: ["task_interrupt", "__end__"] },
  )
  .addNode("task_interrupt", async (task: string) => {
    const value = interrupt({ question: `Interruption for "${task}"` });
    return { taskResult: [value] };
  })
  .addEdge("__start__", "map")
  .compile({ checkpointer })
  .withConfig({ configurable: { thread_id: "1" } });

console.log(
  await graph.invoke({ tasks: ["task1", "interrupt task2", "interrupt task3"] }),
);

Which will result in something like this

{
  tasks: [ 'task1', 'interrupt task2', 'interrupt task3' ],
  taskResult: [
    [ 'task result for "task1"' ],
    [ 'task result for "interrupt task2"' ],
    [ 'task result for "interrupt task3"' ]
  ],
  __interrupt__: [
    {
      value: [Object],
      when: 'during',
      resumable: true,
      ns: [Array],
      interrupt_id: [Getter]
    },
    {
      value: [Object],
      when: 'during',
      resumable: true,
      ns: [Array],
      interrupt_id: [Getter]
    }
  ]
}

Note that we're both persisting the taskResult from task as well as interrupting in parallel

@dqbd commented on GitHub (Sep 11, 2025): Hello! This behaviour is by design, since `interrupt` will cancel the execution of the node and prevent persisting any state. If you do need to persist state in a separate node and then call interrupt in a different node, eg something like this: ```typescript import { Command, interrupt, MemorySaver, Send, StateGraph, } from "@langchain/langgraph"; import { registry } from "@langchain/langgraph/zod"; import { z } from "zod/v4"; const checkpointer = new MemorySaver(); const graph = new StateGraph( z.object({ tasks: z.array(z.string()), taskResult: z.array(z.string()).register(registry, { reducer: { schema: z.string(), fn: (a, b) => [...a, b] }, default: () => [] as string[], }), }), ) .addNode( "map", async (state) => new Command({ goto: state.tasks.map((task) => new Send("task", task)), }), { ends: ["task"] }, ) .addNode( "task", async (task: string) => { if (task.includes("interrupt")) { return new Command({ goto: new Send("task_interrupt", task), update: { taskResult: [`task result for "${task}"`] }, }); } return { taskResult: [`task result for "${task}"`] }; }, { ends: ["task_interrupt", "__end__"] }, ) .addNode("task_interrupt", async (task: string) => { const value = interrupt({ question: `Interruption for "${task}"` }); return { taskResult: [value] }; }) .addEdge("__start__", "map") .compile({ checkpointer }) .withConfig({ configurable: { thread_id: "1" } }); console.log( await graph.invoke({ tasks: ["task1", "interrupt task2", "interrupt task3"] }), ); ``` Which will result in something like this ```typescript { tasks: [ 'task1', 'interrupt task2', 'interrupt task3' ], taskResult: [ [ 'task result for "task1"' ], [ 'task result for "interrupt task2"' ], [ 'task result for "interrupt task3"' ] ], __interrupt__: [ { value: [Object], when: 'during', resumable: true, ns: [Array], interrupt_id: [Getter] }, { value: [Object], when: 'during', resumable: true, ns: [Array], interrupt_id: [Getter] } ] } ``` Note that we're both persisting the `taskResult` from `task` as well as interrupting in parallel
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraphjs#267