LlamaIndex Tutorial (TypeScript): debugging agent loops for advanced developers

By Cyprian AaronsUpdated 2026-04-21
llamaindexdebugging-agent-loops-for-advanced-developerstypescript

This tutorial shows you how to diagnose and stop runaway agent loops in LlamaIndex TypeScript by instrumenting the agent, constraining tool calls, and adding explicit loop guards. You need this when an agent keeps calling the same tool, repeats reasoning without making progress, or burns tokens until your app times out.

What You'll Need

  • Node.js 18+ and npm
  • A TypeScript project with ts-node or a build step
  • llamaindex installed
  • An OpenAI API key set as OPENAI_API_KEY
  • Basic familiarity with LlamaIndex agents, tools, and chat models

Install the package:

npm install llamaindex

Set your environment variable:

export OPENAI_API_KEY="your-key"

Step-by-Step

  1. Start with a minimal agent that can loop. The point here is not to build a perfect agent yet, but to create a controlled reproduction of the failure mode so you can inspect it.
import { FunctionTool, OpenAI, ReActAgent } from "llamaindex";

const llm = new OpenAI({ model: "gpt-4o-mini" });

const echoTool = FunctionTool.from(
  async ({ input }: { input: string }) => {
    return `echo:${input}`;
  },
  {
    name: "echo_tool",
    description: "Echoes the input back.",
    parameters: {
      type: "object",
      properties: {
        input: { type: "string" },
      },
      required: ["input"],
    },
  }
);

async function main() {
  const agent = ReActAgent.fromTools([echoTool], llm);
  const response = await agent.chat({
    message: "Use the tool repeatedly until you are sure.",
  });

  console.log(response.message.content);
}

main();
  1. Add a tool-call trace so you can see whether the model is repeating the same action. In practice, most “agent loop” bugs are either repeated identical tool calls or a tool that returns ambiguous output and invites another call.
import { FunctionTool } from "llamaindex";

export function tracedTool() {
  let count = 0;

  return FunctionTool.from(
    async ({ input }: { input: string }) => {
      count += 1;
      console.log(`[tool-call ${count}] input=${input}`);
      return JSON.stringify({
        ok: true,
        count,
        input,
      });
    },
    {
      name: "trace_tool",
      description: "Returns structured output for debugging.",
      parameters: {
        type: "object",
        properties: {
          input: { type: "string" },
        },
        required: ["input"],
      },
    }
  );
}
  1. Put a hard ceiling on iterations at the application layer. Do not rely only on prompt instructions; if the model ignores them, your runtime still needs to stop the loop.
import { OpenAI, ReActAgent } from "llamaindex";
import { tracedTool } from "./tracedTool";

const llm = new OpenAI({ model: "gpt-4o-mini" });
const tool = tracedTool();

async function main() {
  const agent = ReActAgent.fromTools([tool], llm);

  const response = await agent.chat({
    message:
      "Answer in one pass. If you need the tool more than twice, stop and explain why.",
  });

  console.log("FINAL:", response.message.content);
}

main();
  1. Make your tool output unambiguous and stateful. Agents loop when they cannot tell whether they have enough information, so return explicit status fields instead of free-form text.
import { FunctionTool } from "llamaindex";

export const accountLookupTool = FunctionTool.from(
  async ({ accountId }: { accountId: string }) => {
    if (accountId === "missing") {
      return JSON.stringify({
        found: false,
        reason: "account_not_found",
        nextAction: "ask_user_for_valid_account_id",
      });
    }

    return JSON.stringify({
      found: true,
      accountId,
      balanceCents: 125000,
      currency: "USD",
      nextAction: "summarize_result",
    });
  },
  {
    name: "account_lookup",
    description:
      "Looks up an account by ID and returns structured status information.",
    parameters: {
      type: "object",
      properties: {
        accountId: { type: "string" },
      },
      required: ["accountId"],
    },
  }
);
  1. Wrap the agent call with a watchdog that detects repeated identical outputs or too many tool invocations. This is the production fix when prompt tuning is not enough.
type LoopState = {
  lastOutput?: string;
  repeatCount: number;
};

export function updateLoopState(state: LoopState, output?: string) {
  if (!output) return state;

  if (state.lastOutput === output) {
    return { lastOutput: output, repeatCount: state.repeatCount + 1 };
  }

  return { lastOutput: output, repeatCount: 0 };
}

export function shouldStopLoop(state: LoopState) {
  return state.repeatCount >= 2;
}
  1. Combine everything into one debug runner so you can reproduce, inspect, and stop bad behavior quickly. This is the version you keep around for incidents and regression tests.
import { OpenAI, ReActAgent } from "llamaindex";
import { accountLookupTool } from "./accountLookupTool";
import { updateLoopState, shouldStopLoop } from "./loopGuard";

const llm = new OpenAI({ model: "gpt-4o-mini" });

async function main() {
  const agent = ReActAgent.fromTools([accountLookupTool], llm);
  let state = { repeatCount: 0 as number };

  const response = await agent.chat({
    message:
      "Look up account missing and explain what happened without retrying forever.",
  });

  state = updateLoopState(state, response.message.content ?? "");
  
  if (shouldStopLoop(state)) {
    throw new Error("Detected repeated agent output loop.");
  }

  console.log(response.message.content);
}

main();

Testing It

Run the script with a prompt that forces an error path, like a missing account ID or an underspecified request. If the model keeps asking for the same thing or re-calling the same tool with no new information, your trace logs should make that obvious.

Then test a clean path where the tool returns structured success data and verify the agent stops after one tool call plus one final answer. If you still see repeated calls, tighten the tool schema and make sure your descriptions include when not to call again.

For regression coverage, save one failing prompt and one passing prompt in your test suite. The goal is not just to detect loops manually; it is to lock in behavior so future prompt or model changes do not reintroduce them.

Next Steps

  • Add OpenTelemetry spans around each tool call and agent turn so loop diagnostics show up in your tracing backend.
  • Build a per-agent max-turn policy keyed by workflow risk level.
  • Move from free-form responses to strict JSON outputs for any workflow that feeds downstream automation.

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