How to Fix 'agent infinite loop during development' in LangGraph (TypeScript)

By Cyprian AaronsUpdated 2026-04-21
agent-infinite-loop-during-developmentlanggraphtypescript

When LangGraph throws an agent infinite loop during development error, it usually means your graph keeps routing back into the agent node without ever reaching a stop condition. In practice, this happens when the model keeps producing tool calls, your conditional edge never exits, or your state never changes in a way that breaks the cycle.

This is common in TypeScript projects where the graph compiles fine, runs once, then gets stuck during iterative development because a node returns the wrong shape or your routing logic is too permissive.

The Most Common Cause

The #1 cause is a bad conditional edge that always routes back to the agent, even after the assistant has already produced a final answer.

In LangGraph terms, you usually see the loop around nodes like agent, tools, and __end__. The graph keeps hitting something like:

  • agent -> tools
  • tools -> agent
  • agent -> tools

until LangGraph detects repeated execution and throws an infinite loop error.

Here’s the broken pattern:

BrokenFixed
Always routes back to agentRoutes to __end__ when there are no tool calls
import { StateGraph, START, END } from "@langchain/langgraph";
import { AIMessage } from "@langchain/core/messages";

type State = {
  messages: any[];
};

const shouldContinue = (state: State) => {
  const last = state.messages[state.messages.length - 1];
  // Broken: if you always return "tools" or "agent", you never terminate
  return "tools";
};

const graph = new StateGraph<State>({
  channels: {
    messages: {
      value: (x: any[], y: any[]) => x.concat(y),
      default: () => [],
    },
  },
})
  .addNode("agent", async (state) => {
    return {
      messages: [new AIMessage("Final answer here")],
    };
  })
  .addNode("tools", async () => ({ messages: [] }))
  .addEdge(START, "agent")
  .addConditionalEdges("agent", shouldContinue, {
    tools: "tools",
    agent: "agent",
    end: END,
  })
  .addEdge("tools", "agent");

The fixed version checks whether the last assistant message actually contains tool calls. If not, it ends the graph.

import { StateGraph, START, END } from "@langchain/langgraph";
import { AIMessage } from "@langchain/core/messages";

type State = {
  messages: any[];
};

const shouldContinue = (state: State) => {
  const last = state.messages[state.messages.length - 1];

  if (last?.tool_calls?.length) return "tools";
  return END;
};

const graph = new StateGraph<State>({
  channels: {
    messages: {
      value: (x: any[], y: any[]) => x.concat(y),
      default: () => [],
    },
  },
})
  .addNode("agent", async (state) => ({
    messages: [new AIMessage("Final answer here")],
  }))
  .addNode("tools", async () => ({ messages: [] }))
  .addEdge(START, "agent")
  .addConditionalEdges("agent", shouldContinue, {
    tools: "tools",
    [END]: END,
  })
  .addEdge("tools", "agent");

The key point is simple: only route to tools when there are actual tool calls. If your assistant already answered, stop.

Other Possible Causes

1. Your tool node does not append a tool result

If your tool node returns nothing useful, the model sees no progress and asks for the same tool again.

// Broken
.addNode("tools", async () => {
  return { messages: [] };
});

// Fixed
.addNode("tools", async (state) => {
  return {
    messages: [
      {
        type: "tool",
        content: "Fetched account balance",
        tool_call_id: state.messages.at(-1)?.tool_calls?.[0]?.id,
      },
    ],
  };
});

2. Your state reducer overwrites instead of appending

If you replace messages on every step, LangGraph loses context and may re-enter the same branch.

// Broken
messages: {
  value: (_prev, next) => next,
}

// Fixed
messages: {
  value: (prev, next) => prev.concat(next),
}

3. The model is configured to always call tools

If you force tool calling on every response, the agent never produces a final answer.

// Broken
const model = llm.bindTools(tools, { tool_choice: "any" });

// Better
const model = llm.bindTools(tools);

If you need strict control, only force tools in specific nodes or prompts. Do not leave it on globally unless you really want every turn to be a tool turn.

4. Your recursion limit is too high while debugging

This does not cause the loop by itself, but it hides the real problem until later. In development, keep it low so failures show up quickly.

const result = await graph.invoke(
  { messages },
  { recursionLimit: 10 }
);

LangGraph will eventually throw an error like:

  • Error: Recursion limit of X reached without hitting a stop condition
  • GraphRecursionError
  • or a similar runtime guard depending on version

How to Debug It

  1. Log every node transition

    • Print node name and last message type.
    • You want to see whether execution is bouncing between agent and tools.
  2. Inspect the last assistant message

    • Check whether it contains tool_calls.
    • If it does not, your conditional edge should go to END, not back into the loop.
  3. Verify your reducer behavior

    • Confirm that messages grows over time.
    • If each step resets state, your agent will repeat itself.
  4. Temporarily disable tools

    • Run the agent with tools removed.
    • If the loop disappears, the issue is in tool routing or tool output formatting.

A practical debug helper looks like this:

const debugNode = (name: string) => async (state: any) => {
  const last = state.messages?.at(-1);
  console.log(`[${name}] last=`, JSON.stringify(last, null, 2));
};

Use that on both agent and tools. If you see repeated identical payloads, you’ve found your cycle.

Prevention

  • Keep your conditional routing strict:
    • route to tools only when last.tool_calls?.length > 0
  • Make message reducers append-only unless you have a very specific reason to overwrite state.
  • Add a low recursionLimit in development so loops fail fast instead of burning time and tokens.
  • Test one complete happy path before adding more tools or branches.

If you want one rule to remember: LangGraph agents need an explicit exit path. No exit path means no finish line; just another turn through the loop.


Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides