Cannot use streamMode when streaming from a subgraph #236

Closed
opened 2026-02-15 17:17:43 -05:00 by yindo · 5 comments
Owner

Originally created by @au-re on GitHub (Apr 18, 2025).

I think there is a bug when using streamMode in nested subgraphs. I couldn't find anything about this in the documentation.

Given the following setup with a subgraph / main graph, I would like to create custom events based on state updates in the subgraph. However, only stream events of type "values" are triggered from the subgraph, regardless of the streamMode I specify.

@langchain/langgraph: "0.2.64"

StreamMode not working as expected

import { entrypoint, MemorySaver, task } from "@langchain/langgraph";

let i = 0;

const doSomething = task("do_something", async (text: string) => {
  await new Promise((resolve) => setTimeout(resolve, 1000));
  return `(do ${text}): ${i++}`;
});

const internal = entrypoint({ name: "internal" }, async (params: { query: string }, config) => {
  let internalState: string[] = [];

  internalState.push(await doSomething(params.query));

  internalState.push(await doSomething(params.query));

  internalState.push(await doSomething(params.query));

  return internalState;
});

const main = entrypoint({ checkpointer: new MemorySaver(), name: "main" }, async (params, config) => {
  let mainState: string[] = [];

  const res1 = await doSomething("task 1");

  mainState.push(res1);

  const stream = await internal.stream({ query: "internal task" }, { streamMode: ["updates"] });

  for await (const chunk of stream) {
    console.log("type:", chunk[0]); // streamMode is ignored: yields "values" instead of "updates"

    if (chunk[1]?.do_something) mainState.push(chunk[1].do_something);

    config.writer?.(mainState);
  }

  return mainState;
});

Running the entrypoint

const stream = await main.stream(
  {},
  {
    configurable: { thread_id: "1" },
    streamMode: ["custom"],
  },
);

for await (const chunk of stream) {
  console.log(JSON.stringify(chunk, null, 2));
}

Logs

Only a single event is logged (when the subgraph is done running and we receive the "values" event.

[
  "custom",
  [
    "(do task 1): 0"
  ]
]

I can get the stream to work by passing a new configurables object to the subgraph, but this breaks the checkpointer setup and I am no longer able to resume from the subgraph:

Checkpointer not working as expected

const main = entrypoint({ checkpointer: new MemorySaver(), name: "main" }, async (params, config) => {
  let mainState: string[] = [];

  const res1 = await doSomething("task 1");

  mainState.push(res1);

  const stream = await internal.stream(
    { query: "internal task" },
    {
      ...config,
      configurable: { thread_id: "1" }, // overwriting the configurable object
      streamMode: ["updates"],
    },
  );

  for await (const chunk of stream) {
    console.log("type:", chunk[0]);

    if (chunk[1].do_something) mainState.push(chunk[1].do_something);

    config.writer?.(mainState);
  }

  return mainState;
});

Now we write the events as expected (on update events from the subgraph)

[
  "custom",
  [
    "(do task 1): 0",
    "(do internal task): 1"
  ]
]

[
  "custom",
  [
    "(do task 1): 0",
    "(do internal task): 1",
    "(do internal task): 2"
  ]
]

[
  "custom",
  [
    "(do task 1): 0",
    "(do internal task): 1",
    "(do internal task): 2",
    "(do internal task): 3"
  ]
]

[
  "custom",
  [
    "(do task 1): 0",
    "(do internal task): 1",
    "(do internal task): 2",
    "(do internal task): 3"
  ]
]

But if I overwride the configurables object, this breaks the checkpointer (even if the thread_id is the same). I can no longer resume from the subgraph when adding the following:

const internal = entrypoint({ name: "internal" }, async (params: { query: string }, config) => {
  let internalState: string[] = [];

  internalState.push(await doSomething(params.query));

  internalState.push(await doSomething(params.query));

  internalState.push(await doSomething(params.query));

  const followup: string = interrupt({}); // added an interrupt here

  internalState.push(await doSomething(followup)); 

  return internalState;
});
const resumed = await main.stream(
  new Command({
    resume: "interrupt",
  }),
  {
    configurable: { thread_id: "1" },
    streamMode: ["custom"],
  },
);

for await (const chunk of resumed) {
  console.log(JSON.stringify(chunk, null, 2));
}

Removing the configurables object makes the checkpointer work again but then I am back to not being able to listen to "updates" events.

Originally created by @au-re on GitHub (Apr 18, 2025). I think there is a bug when using streamMode in nested subgraphs. I couldn't find anything about this in the documentation. Given the following setup with a subgraph / main graph, I would like to create custom events based on state updates in the subgraph. However, only stream events of type "values" are triggered from the subgraph, regardless of the streamMode I specify. @langchain/langgraph: "0.2.64" ## StreamMode not working as expected ```js import { entrypoint, MemorySaver, task } from "@langchain/langgraph"; let i = 0; const doSomething = task("do_something", async (text: string) => { await new Promise((resolve) => setTimeout(resolve, 1000)); return `(do ${text}): ${i++}`; }); const internal = entrypoint({ name: "internal" }, async (params: { query: string }, config) => { let internalState: string[] = []; internalState.push(await doSomething(params.query)); internalState.push(await doSomething(params.query)); internalState.push(await doSomething(params.query)); return internalState; }); const main = entrypoint({ checkpointer: new MemorySaver(), name: "main" }, async (params, config) => { let mainState: string[] = []; const res1 = await doSomething("task 1"); mainState.push(res1); const stream = await internal.stream({ query: "internal task" }, { streamMode: ["updates"] }); for await (const chunk of stream) { console.log("type:", chunk[0]); // streamMode is ignored: yields "values" instead of "updates" if (chunk[1]?.do_something) mainState.push(chunk[1].do_something); config.writer?.(mainState); } return mainState; }); ``` ### Running the entrypoint ```js const stream = await main.stream( {}, { configurable: { thread_id: "1" }, streamMode: ["custom"], }, ); for await (const chunk of stream) { console.log(JSON.stringify(chunk, null, 2)); } ``` ### Logs Only a single event is logged (when the subgraph is done running and we receive the "values" event. ``` [ "custom", [ "(do task 1): 0" ] ] ``` **I can get the stream to work by passing a new configurables object to the subgraph**, but this breaks the checkpointer setup and I am no longer able to resume from the subgraph: ## Checkpointer not working as expected ```js const main = entrypoint({ checkpointer: new MemorySaver(), name: "main" }, async (params, config) => { let mainState: string[] = []; const res1 = await doSomething("task 1"); mainState.push(res1); const stream = await internal.stream( { query: "internal task" }, { ...config, configurable: { thread_id: "1" }, // overwriting the configurable object streamMode: ["updates"], }, ); for await (const chunk of stream) { console.log("type:", chunk[0]); if (chunk[1].do_something) mainState.push(chunk[1].do_something); config.writer?.(mainState); } return mainState; }); ``` Now we write the events as expected (on update events from the subgraph) ``` [ "custom", [ "(do task 1): 0", "(do internal task): 1" ] ] [ "custom", [ "(do task 1): 0", "(do internal task): 1", "(do internal task): 2" ] ] [ "custom", [ "(do task 1): 0", "(do internal task): 1", "(do internal task): 2", "(do internal task): 3" ] ] [ "custom", [ "(do task 1): 0", "(do internal task): 1", "(do internal task): 2", "(do internal task): 3" ] ] ``` But if I overwride the configurables object, this breaks the checkpointer (even if the thread_id is the same). I can no longer resume from the subgraph when adding the following: ```js const internal = entrypoint({ name: "internal" }, async (params: { query: string }, config) => { let internalState: string[] = []; internalState.push(await doSomething(params.query)); internalState.push(await doSomething(params.query)); internalState.push(await doSomething(params.query)); const followup: string = interrupt({}); // added an interrupt here internalState.push(await doSomething(followup)); return internalState; }); ``` ```js const resumed = await main.stream( new Command({ resume: "interrupt", }), { configurable: { thread_id: "1" }, streamMode: ["custom"], }, ); for await (const chunk of resumed) { console.log(JSON.stringify(chunk, null, 2)); } ``` Removing the configurables object makes the checkpointer work again but then I am back to not being able to listen to "updates" events.
yindo added the docs-needed label 2026-02-15 17:17:43 -05:00
yindo closed this issue 2026-02-15 17:17:43 -05:00
Author
Owner

@benjamincburns commented on GitHub (Apr 28, 2025):

Hmm, it does indeed seem that we have a gap in our docs around this topic. I would've expected to see details about streaming from subgraphs in either the Streaming or Subgraphs sections of the How-to docs, but neither addresses it.

To answer your question - you'll need to set the subgraphs option to true. It can be found on the config object that you pass as the second argument to the stream method.

Please ping me if this doesn't get you on your way and I'll reopen this issue.

@benjamincburns commented on GitHub (Apr 28, 2025): Hmm, it does indeed seem that we have a gap in our docs around this topic. I would've expected to see details about streaming from subgraphs in either the [Streaming](https://langchain-ai.github.io/langgraphjs/how-tos/#streaming) or [Subgraphs](https://langchain-ai.github.io/langgraphjs/how-tos/#subgraphs) sections of the How-to docs, but neither addresses it. To answer your question - you'll need to set the `subgraphs` option to true. It can be found on [the config object](https://langchain-ai.github.io/langgraphjs/reference/interfaces/langgraph.PregelOptions.html) that you pass as the second argument to the `stream` method. Please ping me if this doesn't get you on your way and I'll reopen this issue.
Author
Owner

@au-re commented on GitHub (Apr 28, 2025):

@benjamincburns the issue with "subgraphs": true is that if I add it to internal.stream, it won't do anything because that has no other subgraphs which means the code above still won't work as expected. I can add it to main.stream but then how can I update the main graph state on stream events from subgraphs?

What I would like to do is update the state of the main graph each time subgraphs are emitting update / message events.

@au-re commented on GitHub (Apr 28, 2025): @benjamincburns the issue with `"subgraphs": true` is that if I add it to `internal.stream`, it won't do anything because that has no other subgraphs which means the code above still won't work as expected. I can add it to `main.stream` but then how can I update the main graph state on stream events from subgraphs? What I would like to do is update the state of the main graph each time subgraphs are emitting update / message events.
Author
Owner

@au-re commented on GitHub (Apr 28, 2025):

@benjamincburns I guess for now the only way to do this is to manage that state at the top level. Which is not ideal when you have nested graphs with different state structures, since you'll have to transform their results in two places: after invoke + at the top level. It's also really tricky to manually reconcile chunks into the main state for every event across all subgraphs at once...

Below is the work around I am currently using, it's ok for this simple example but becomes tricky for more complex graphs:

import { Command, entrypoint, interrupt, MemorySaver, task } from "@langchain/langgraph";

let i = 0;

const doSomething = task("do_something", async (text: string) => {
  await new Promise((resolve) => setTimeout(resolve, 1000));
  return `(do ${text}): ${i++}`;
});

const internal = entrypoint({ name: "internal" }, async (params: { query: string }, config) => {
  let internalState: string[] = [];
  internalState.push(await doSomething(params.query));
  internalState.push(await doSomething(params.query));
  internalState.push(await doSomething(params.query));
  const followup: string = interrupt({});
  internalState.push(await doSomething(followup));
  return internalState;
});

const main = entrypoint({ checkpointer: new MemorySaver(), name: "main" }, async (params, config) => {
  let mainState: string[] = [];
  mainState.push(await doSomething("task 1"));
  const res = await internal.invoke({ query: "internal task" });
  mainState = [...mainState, ...res];
  mainState.push(await doSomething("last task"));
  return mainState;
});

const stream = await main.stream(
  {},
  {
    configurable: { thread_id: "1" },
    streamMode: ["updates"],
    subgraphs: true,
  },
);

const mainState: string[] = [];

for await (const chunk of stream) {
  if (chunk[1] === "updates") {
    if (chunk[2]?.do_something) {
      mainState.push(chunk[2].do_something);
      console.log("STATE:", JSON.stringify(mainState, null, 2));
    }
  }
}

const resumed = await main.stream(
  new Command({
    resume: "interrupt",
  }),
  {
    configurable: { thread_id: "1" },
    streamMode: ["updates"],
    subgraphs: true,
  },
);

const resumedState: string[] = [];

for await (const chunk of resumed) {
  if (chunk[1] === "updates") {
    if (chunk[2]?.do_something) {
      resumedState.push(chunk[2].do_something);
      console.log("STATE:", JSON.stringify(resumedState, null, 2));
    }
  }
}
@au-re commented on GitHub (Apr 28, 2025): @benjamincburns I guess for now the only way to do this is to manage that state at the top level. Which is not ideal when you have nested graphs with different state structures, since you'll have to transform their results in two places: after invoke + at the top level. It's also really tricky to manually reconcile chunks into the main state for every event across all subgraphs at once... Below is the work around I am currently using, it's ok for this simple example but becomes tricky for more complex graphs: ```ts import { Command, entrypoint, interrupt, MemorySaver, task } from "@langchain/langgraph"; let i = 0; const doSomething = task("do_something", async (text: string) => { await new Promise((resolve) => setTimeout(resolve, 1000)); return `(do ${text}): ${i++}`; }); const internal = entrypoint({ name: "internal" }, async (params: { query: string }, config) => { let internalState: string[] = []; internalState.push(await doSomething(params.query)); internalState.push(await doSomething(params.query)); internalState.push(await doSomething(params.query)); const followup: string = interrupt({}); internalState.push(await doSomething(followup)); return internalState; }); const main = entrypoint({ checkpointer: new MemorySaver(), name: "main" }, async (params, config) => { let mainState: string[] = []; mainState.push(await doSomething("task 1")); const res = await internal.invoke({ query: "internal task" }); mainState = [...mainState, ...res]; mainState.push(await doSomething("last task")); return mainState; }); const stream = await main.stream( {}, { configurable: { thread_id: "1" }, streamMode: ["updates"], subgraphs: true, }, ); const mainState: string[] = []; for await (const chunk of stream) { if (chunk[1] === "updates") { if (chunk[2]?.do_something) { mainState.push(chunk[2].do_something); console.log("STATE:", JSON.stringify(mainState, null, 2)); } } } const resumed = await main.stream( new Command({ resume: "interrupt", }), { configurable: { thread_id: "1" }, streamMode: ["updates"], subgraphs: true, }, ); const resumedState: string[] = []; for await (const chunk of resumed) { if (chunk[1] === "updates") { if (chunk[2]?.do_something) { resumedState.push(chunk[2].do_something); console.log("STATE:", JSON.stringify(resumedState, null, 2)); } } } ```
Author
Owner

@benjamincburns commented on GitHub (May 22, 2025):

Sorry to be so slow to reopen this @au-re - @dqbd might be worth considering how to address this.

@benjamincburns commented on GitHub (May 22, 2025): Sorry to be so slow to reopen this @au-re - @dqbd might be worth considering how to address this.
Author
Owner

@dqbd commented on GitHub (Jun 26, 2025):

This will be fixed with https://github.com/langchain-ai/langgraphjs/issues/1116!

@dqbd commented on GitHub (Jun 26, 2025): This will be fixed with https://github.com/langchain-ai/langgraphjs/issues/1116!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraphjs#236