LlamaIndex Tutorial (TypeScript): debugging agent loops for beginners
This tutorial shows you how to spot and debug agent loops in a LlamaIndex TypeScript agent before they burn tokens or hang your app. You need this when an agent keeps calling tools with no progress, repeats the same reasoning path, or never returns a final answer.
What You'll Need
- •Node.js 18+ and npm
- •A TypeScript project with
"type": "module"or ESM-compatible setup - •
llamaindexinstalled - •An OpenAI API key
- •Basic familiarity with
createAgent, tools, and chat messages in LlamaIndex TypeScript
Install the package if you haven’t already:
npm install llamaindex
Set your API key:
export OPENAI_API_KEY="your-key-here"
Step-by-Step
- •Start with a tiny agent that can loop.
The easiest way to debug loops is to reproduce them with a minimal tool that always returns something the agent can keep chasing. Here we create a fake “search” tool that returns the same kind of response every time, which makes looping behavior obvious.
import { OpenAI } from "llamaindex";
import { FunctionTool, createAgent } from "llamaindex";
const llm = new OpenAI({
model: "gpt-4o-mini",
temperature: 0,
});
const searchTool = FunctionTool.from(
async ({ query }: { query: string }) => {
return `Search results for "${query}": try refining your query.`;
},
{
name: "search",
description: "Search internal docs for a query.",
parameters: {
type: "object",
properties: {
query: { type: "string" },
},
required: ["query"],
},
}
);
const agent = createAgent({
llm,
tools: [searchTool],
});
- •Add explicit turn logging so you can see repetition.
When an agent loops, the model often repeats the same tool call arguments or alternates between two states. Logging each turn gives you a cheap signal before you dig into traces or breakpoints.
import { ChatMessage, MessageRole } from "llamaindex";
async function run() {
const messages: ChatMessage[] = [
{
role: MessageRole.USER,
content:
"Find the policy for remote work and summarize it in one sentence.",
},
];
const response = await agent.chat({
message: messages[0].content,
chatHistory: [],
});
console.log("Final response:");
console.log(response.message.content);
}
run().catch(console.error);
- •Put a hard stop on runaway loops.
In production, you do not want an agent to keep burning tool calls forever. The simplest guardrail is a max-step wrapper around your own execution loop, which lets you fail fast and inspect what happened.
import { ChatMessage, MessageRole } from "llamaindex";
async function debugLoop() {
const history: ChatMessage[] = [];
const userMessage =
"Find the policy for remote work and summarize it in one sentence.";
history.push({ role: MessageRole.USER, content: userMessage });
const response = await agent.chat({
message: userMessage,
chatHistory: history.slice(0, -1),
stream: false,
});
console.log("Assistant:", response.message.content);
if (response.message.content.toLowerCase().includes("try refining")) {
console.log("Warning: tool output may be causing repeated follow-up calls.");
}
}
debugLoop().catch(console.error);
- •Make the tool output more decisive.
Loops often happen because the tool response is vague enough that the model thinks it needs another call. If your tool can return structured results with a clear terminal state, the model has less room to wander.
import { FunctionTool } from "llamaindex";
const betterSearchTool = FunctionTool.from(
async ({ query }: { query: string }) => {
if (query.toLowerCase().includes("remote work")) {
return JSON.stringify({
found: true,
answer:
"Remote work is allowed up to three days per week with manager approval.",
});
}
return JSON.stringify({
found: false,
answer: null,
nextStep: "Ask for department or region.",
});
},
{
name: "search_policy",
description:
"Search policy docs and return either an answer or a clear next step.",
parameters: {
type: "object",
properties: {
query: { type: "string" },
},
required: ["query"],
},
}
);
- •Force the model to stop when it has enough information.
A lot of beginner loops come from weak instructions. Tell the agent exactly when to answer directly instead of calling tools again.
const strictAgent = createAgent({
llm,
tools: [betterSearchTool],
});
async function askStrict() {
const result = await strictAgent.chat({
message:
"Use the policy search tool once. If it returns an answer, summarize it immediately and do not call any other tools.",
chatHistory: [],
});
console.log(result.message.content);
}
askStrict().catch(console.error);
Testing It
Run the script with a question that previously caused repeated searches, like “Find the remote work policy.” If your logs show repeated identical queries or your assistant keeps returning “try refining your query,” you’ve reproduced the loop.
Now swap in betterSearchTool and rerun the same prompt. You should see one tool call followed by a direct answer instead of repeated follow-ups.
If it still loops, inspect two things first:
- •Tool output is too vague or non-terminal
- •The system/user instruction does not clearly say when to stop
A good debugging habit is to compare three runs:
- •Original vague tool output
- •Structured tool output
- •Structured output plus explicit stop instruction
Next Steps
- •Add trace logging with your observability stack so you can inspect every tool call and model turn.
- •Learn how to use structured outputs and schemas for tools that return deterministic data.
- •Build a max-step executor wrapper around agents used in production so loops fail fast instead of hanging requests.
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
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