RemoteGraph recursion loop #314

Closed
opened 2026-02-15 18:15:49 -05:00 by yindo · 3 comments
Owner

Originally created by @martijn-ixor on GitHub (Jul 24, 2025).

Checked other resources

  • I added a very descriptive title to this issue.
  • I searched the LangGraph.js documentation with the integrated search.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangGraph.js rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangGraph (or the specific integration package).

Example Code

This normally works:

    const remoteGraph = new RemoteGraph({
      url: "http://localhost:2023",
      graphId: "agent",
    });
    const output = await remoteGraph.invoke(state);

I think it might be the combination of this being a subgraph and usage of LangGraph server cli.

Error Message and Stack Trace (if applicable)

obj: Timeout {
  _idleTimeout: 250,
  _idlePrev: [TimersList],
  _idleNext: [TimersList],
  _idleStart: 15930,
  _onTimeout: [Function (anonymous)],
  _timerArgs: undefined,
  _repeat: null,
  _destroyed: false,
  [Symbol(refed)]: true,
  [Symbol(kHasPrimitive)]: false,
  [Symbol(asyncId)]: 23565,
  [Symbol(triggerId)]: 23536,
  [Symbol(kResourceStore)]: undefined
}
obj: TimersList {
  _idleNext: [Timeout],
  _idlePrev: [Timeout],
  expiry: 16180,
  id: -9007199254740161,
  msecs: 250,
  priorityQueuePosition: 4
}
error:   ┏ Background run failed:
RangeError: Maximum call stack size exceeded

Description

I have created a supervisor agent that calls other RemoteGraphs.

However, when the RemoteGraph gets executed, I always seem to get a RangeError for a maximum call stack size.

The issue seems to be that the config is getting recursively sanitized in the remote.ts file.

I have added logs and have seen that a Timer object causes the recursion.

I use LangGraph Studio to test and the code that I run is adapted from langgraph-supervisor to support RemoteGraph.

I tried to manually limit the config in .invoke, but that does not seem to affect the config in the RemoteGraph.

When I manually edit the remote.ts file to skip the sanitization, the issue seems to be resolved.

Does anyone have an idea on how to fix this properly? I'm open to make a PR on the remote.ts file.

System Info

├── @langchain/community@0.3.49
├── @langchain/core@0.3.66
├── @langchain/langgraph-cli@0.0.51
├── @langchain/langgraph-sdk@0.0.101
├── @langchain/langgraph-supervisor@0.0.15
├── @langchain/langgraph@0.3.11
├── @langchain/mcp-adapters@0.5.4
├── @langchain/openai@0.5.18
├── concurrently@9.2.0
├── langchain@0.3.30
├── langsmith@0.3.48
├── llamaindex@0.11.15
├── tsx@4.20.3
├── turbo@2.5.4
├── typescript@5.8.3
└── zod@3.25.76

Platform: Mac M1
Node version: 20
Package manager: npm (10.8.2)

Originally created by @martijn-ixor on GitHub (Jul 24, 2025). ### Checked other resources - [x] I added a very descriptive title to this issue. - [x] I searched the LangGraph.js documentation with the integrated search. - [x] I used the GitHub search to find a similar question and didn't find it. - [x] I am sure that this is a bug in LangGraph.js rather than my code. - [x] The bug is not resolved by updating to the latest stable version of LangGraph (or the specific integration package). ### Example Code This normally works: ```typescript const remoteGraph = new RemoteGraph({ url: "http://localhost:2023", graphId: "agent", }); const output = await remoteGraph.invoke(state); ``` I think it might be the combination of this being a subgraph and usage of LangGraph server cli. ### Error Message and Stack Trace (if applicable) ```shell obj: Timeout { _idleTimeout: 250, _idlePrev: [TimersList], _idleNext: [TimersList], _idleStart: 15930, _onTimeout: [Function (anonymous)], _timerArgs: undefined, _repeat: null, _destroyed: false, [Symbol(refed)]: true, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 23565, [Symbol(triggerId)]: 23536, [Symbol(kResourceStore)]: undefined } obj: TimersList { _idleNext: [Timeout], _idlePrev: [Timeout], expiry: 16180, id: -9007199254740161, msecs: 250, priorityQueuePosition: 4 } error: ┏ Background run failed: RangeError: Maximum call stack size exceeded ``` ### Description I have created a supervisor agent that calls other RemoteGraphs. However, when the RemoteGraph gets executed, I always seem to get a `RangeError` for a maximum call stack size. The issue seems to be that the config is getting recursively sanitized in the [`remote.ts`](libs/langgraph/src/pregel/remote.ts) file. I have added logs and have seen that a Timer object causes the recursion. I use LangGraph Studio to test and the code that I run is adapted from [langgraph-supervisor](https://github.com/langchain-ai/langgraphjs/blob/main/libs/langgraph-supervisor/src/supervisor.ts) to support `RemoteGraph`. I tried to manually limit the config in `.invoke`, but that does not seem to affect the config in the RemoteGraph. When I manually edit the `remote.ts` file to skip the sanitization, the issue seems to be resolved. Does anyone have an idea on how to fix this properly? I'm open to make a PR on the `remote.ts` file. ### System Info ```shell ├── @langchain/community@0.3.49 ├── @langchain/core@0.3.66 ├── @langchain/langgraph-cli@0.0.51 ├── @langchain/langgraph-sdk@0.0.101 ├── @langchain/langgraph-supervisor@0.0.15 ├── @langchain/langgraph@0.3.11 ├── @langchain/mcp-adapters@0.5.4 ├── @langchain/openai@0.5.18 ├── concurrently@9.2.0 ├── langchain@0.3.30 ├── langsmith@0.3.48 ├── llamaindex@0.11.15 ├── tsx@4.20.3 ├── turbo@2.5.4 ├── typescript@5.8.3 └── zod@3.25.76 ``` Platform: Mac M1 Node version: 20 Package manager: npm (10.8.2)
yindo closed this issue 2026-02-15 18:15:49 -05:00
Author
Owner

@MartijnLeplae commented on GitHub (Jul 30, 2025):

I was able to create a code sample to reproduce the issue:

Remote graph (graph.ts):

import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { AzureChatOpenAI } from "@langchain/openai";

const model = new AzureChatOpenAI({
    azureOpenAIApiKey: process.env.AZURE_OPENAI_API_KEY,
    azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION || "2024-02-01",
    azureOpenAIEndpoint: process.env.AZURE_OPENAI_API_URL,
    azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_DEPLOYMENT_NAME || 'gpt-4o-mini',
    modelName: 'gpt-4o-mini'
  });

export const graph = createReactAgent({
  llm: model,
  tools: [],
  name: "remote_agent",
  prompt: "You are a remote agent!"
});

Use of remote graph (reproduce-remote.ts):

import { RunnableConfig } from "@langchain/core/runnables";
import { MessagesAnnotation, StateGraph, Annotation, Command } from "@langchain/langgraph";
import { RemoteGraph } from "@langchain/langgraph/remote";
import { v4 as uuidv4 } from 'uuid';


async function callRemote(state: Record<string, unknown>, config?: any) {
  const remoteGraph: RemoteGraph = new RemoteGraph(
    {
        graphId: "agent",
        name: "remote-agent",
        url: "http://localhost:2024"
    }
  )
  const config2 = {
    configurable: {
      thread_id: uuidv4()
    }
  }
  const output = await remoteGraph.invoke({messages: state.messages}, config2);
  let { messages } = output;

  return { ...output, messages };
};

function callModel(
  state: Record<string, unknown>,
  config: RunnableConfig,
) {
  console.log("config", config);
  return new Command({
    goto: "remote",
  })
}


const workflow = new StateGraph(MessagesAnnotation)
  .addNode("remote", callRemote)
  .addNode("local", callModel, {ends: ["remote"]})
  .addEdge("__start__", "local")
  .addEdge("remote", "local")
  .addEdge("local", "__end__");

export async function graph(config: RunnableConfig) {
  return workflow.compile({
    interruptBefore: [],
    interruptAfter: [],
  });
}

The result in LangGraph Studio:

Image

Interestingly, this behaviour changes when the graph is simplified to:

const workflow = new StateGraph(MessagesAnnotation)
  .addNode("remote", callRemote)
  .addEdge("__start__", "remote")
  .addEdge("remote", "__end__");
Image

My langgraph.json looks like:

{
  "node_version": "20",
  "dependencies": [
    "."
  ],
  "graphs": {
    "agent": "./graph.ts:graph",
    "reproduce-remote": "./reproduce-remote.ts:graph"
  },
  "env": ".env"
}
@MartijnLeplae commented on GitHub (Jul 30, 2025): I was able to create a code sample to reproduce the issue: Remote graph (`graph.ts`): ```typescript import { createReactAgent } from "@langchain/langgraph/prebuilt"; import { AzureChatOpenAI } from "@langchain/openai"; const model = new AzureChatOpenAI({ azureOpenAIApiKey: process.env.AZURE_OPENAI_API_KEY, azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION || "2024-02-01", azureOpenAIEndpoint: process.env.AZURE_OPENAI_API_URL, azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_DEPLOYMENT_NAME || 'gpt-4o-mini', modelName: 'gpt-4o-mini' }); export const graph = createReactAgent({ llm: model, tools: [], name: "remote_agent", prompt: "You are a remote agent!" }); ``` Use of remote graph (`reproduce-remote.ts`): ```typescript import { RunnableConfig } from "@langchain/core/runnables"; import { MessagesAnnotation, StateGraph, Annotation, Command } from "@langchain/langgraph"; import { RemoteGraph } from "@langchain/langgraph/remote"; import { v4 as uuidv4 } from 'uuid'; async function callRemote(state: Record<string, unknown>, config?: any) { const remoteGraph: RemoteGraph = new RemoteGraph( { graphId: "agent", name: "remote-agent", url: "http://localhost:2024" } ) const config2 = { configurable: { thread_id: uuidv4() } } const output = await remoteGraph.invoke({messages: state.messages}, config2); let { messages } = output; return { ...output, messages }; }; function callModel( state: Record<string, unknown>, config: RunnableConfig, ) { console.log("config", config); return new Command({ goto: "remote", }) } const workflow = new StateGraph(MessagesAnnotation) .addNode("remote", callRemote) .addNode("local", callModel, {ends: ["remote"]}) .addEdge("__start__", "local") .addEdge("remote", "local") .addEdge("local", "__end__"); export async function graph(config: RunnableConfig) { return workflow.compile({ interruptBefore: [], interruptAfter: [], }); } ``` The result in LangGraph Studio: <img width="1026" height="262" alt="Image" src="https://github.com/user-attachments/assets/1d80e5fb-7911-4d2c-805a-16273427a479" /> Interestingly, this behaviour changes when the graph is simplified to: ```typescript const workflow = new StateGraph(MessagesAnnotation) .addNode("remote", callRemote) .addEdge("__start__", "remote") .addEdge("remote", "__end__"); ``` <img width="977" height="350" alt="Image" src="https://github.com/user-attachments/assets/a855adb6-fd50-460d-9efd-55f7f8cc9a78" /> My `langgraph.json` looks like: ```json { "node_version": "20", "dependencies": [ "." ], "graphs": { "agent": "./graph.ts:graph", "reproduce-remote": "./reproduce-remote.ts:graph" }, "env": ".env" } ```
Author
Owner

@MartijnLeplae commented on GitHub (Jul 30, 2025):

This could be fixed by adapting the behaviour of sanitizeObj in remote.ts.

From:

const sanitizeObj = (obj: any): any => {
      // Remove non-JSON serializable fields from the given object
      if (obj && typeof obj === "object") {
        if (Array.isArray(obj)) {
          return obj.map((v) => sanitizeObj(v));
        } else {
          return Object.fromEntries(
            Object.entries(obj).map(([k, v]) => [k, sanitizeObj(v)])
          );
        }
      }

      try {
        JSON.stringify(obj);
        return obj;
      } catch {
        return null;
      }
    };

To:

const sanitizeObj = (obj: any, seen = new WeakSet()): any => {
  if (obj && typeof obj === "object") {
    if (!seen.has(obj)) {
      seen.add(obj);

      if (Array.isArray(obj)) {
        return obj.map((v) => sanitizeObj(v, seen));
      } else {
        return Object.fromEntries(
          Object.entries(obj).map(([k, v]) => [k, sanitizeObj(v, seen)])
        );
      }
    }
  }

  try {
    JSON.stringify(obj);
    return obj;
  } catch {
    return null;
  }
};

I'm not sure whether this fix is sufficient, or if there is a deeper root cause. I will work on a PR with this fix already.

@MartijnLeplae commented on GitHub (Jul 30, 2025): This could be fixed by adapting the behaviour of `sanitizeObj` in `remote.ts`. From: ```typescript const sanitizeObj = (obj: any): any => { // Remove non-JSON serializable fields from the given object if (obj && typeof obj === "object") { if (Array.isArray(obj)) { return obj.map((v) => sanitizeObj(v)); } else { return Object.fromEntries( Object.entries(obj).map(([k, v]) => [k, sanitizeObj(v)]) ); } } try { JSON.stringify(obj); return obj; } catch { return null; } }; ``` To: ```typescript const sanitizeObj = (obj: any, seen = new WeakSet()): any => { if (obj && typeof obj === "object") { if (!seen.has(obj)) { seen.add(obj); if (Array.isArray(obj)) { return obj.map((v) => sanitizeObj(v, seen)); } else { return Object.fromEntries( Object.entries(obj).map(([k, v]) => [k, sanitizeObj(v, seen)]) ); } } } try { JSON.stringify(obj); return obj; } catch { return null; } }; ``` I'm not sure whether this fix is sufficient, or if there is a deeper root cause. I will work on a PR with this fix already.
Author
Owner

@dqbd commented on GitHub (Jul 30, 2025):

Hello! Should be fixed in 0.4.1!

@dqbd commented on GitHub (Jul 30, 2025): Hello! Should be fixed in 0.4.1!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraphjs#314