How to Fix 'duplicate tool calls' in LlamaIndex (TypeScript)

By Cyprian AaronsUpdated 2026-04-21
duplicate-tool-callsllamaindextypescript

If you’re seeing Error: duplicate tool calls in LlamaIndex TypeScript, it usually means the agent tried to register or execute the same tool twice in a single workflow. In practice, this shows up when you wire tools into both the agent and the underlying runner, or when your code reuses a tool list across multiple invocations.

This is not a model problem. It’s almost always a wiring issue in your TypeScript setup.

The Most Common Cause

The #1 cause is registering the same tool in two places: once in the agent constructor and again in the chat/run call, or building the tools array in a way that duplicates entries.

Here’s the broken pattern:

import { OpenAI } from "llamaindex";
import { FunctionTool, ReActAgent } from "llamaindex";

const getBalanceTool = FunctionTool.from(async () => {
  return "Balance: $1,250";
}, {
  name: "get_balance",
  description: "Get account balance",
});

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

const agent = new ReActAgent({
  llm,
  tools: [getBalanceTool],
});

// ❌ Broken: passing the same tool again during execution
const response = await agent.chat({
  message: "What's my balance?",
  tools: [getBalanceTool],
});

console.log(response.response);

And here’s the fixed pattern:

import { OpenAI } from "llamaindex";
import { FunctionTool, ReActAgent } from "llamaindex";

const getBalanceTool = FunctionTool.from(async () => {
  return "Balance: $1,250";
}, {
  name: "get_balance",
  description: "Get account balance",
});

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

const agent = new ReActAgent({
  llm,
  tools: [getBalanceTool],
});

// ✅ Fixed: tools are defined once at construction time
const response = await agent.chat({
  message: "What's my balance?",
});

console.log(response.response);

The key rule is simple:

  • Define tools once
  • Pass them once
  • Don’t rebuild or append the same tool list per request unless you dedupe it first

If you’re using FunctionTool, QueryEngineTool, or custom BaseTool implementations, this mistake is easy to make because the code looks harmless.

Other Possible Causes

Here are the other failure modes I see most often.

1. Duplicate tool names

LlamaIndex uses tool names as identifiers. If two tools share the same name, you can get collisions that surface as duplicate tool execution errors.

// ❌ Broken
const t1 = FunctionTool.from(fnA, { name: "lookup_customer", description: "A" });
const t2 = FunctionTool.from(fnB, { name: "lookup_customer", description: "B" });

// ✅ Fixed
const t1 = FunctionTool.from(fnA, { name: "lookup_customer_profile", description: "A" });
const t2 = FunctionTool.from(fnB, { name: "lookup_policy", description: "B" });

2. Reusing and mutating a shared tools array

If you keep a module-level array and push into it on every request, you’ll accumulate duplicates over time.

// ❌ Broken
const tools = [];

export function buildTools(extraTool) {
  tools.push(extraTool);
  return tools;
}

// ✅ Fixed
export function buildTools(extraTool) {
  return [extraTool];
}

If you need composition, dedupe by name before returning:

function uniqueTools(tools) {
  const seen = new Set<string>();
  return tools.filter((tool) => {
    if (seen.has(tool.metadata.name)) return false;
    seen.add(tool.metadata.name);
    return true;
  });
}

3. Creating a new agent inside a loop with shared state

This often happens in server handlers or batch jobs where each iteration reuses cached objects incorrectly.

// ❌ Broken
for (const request of requests) {
  const agent = new ReActAgent({ llm, tools });
  await agent.chat({ message: request.prompt, tools });
}

// ✅ Fixed
for (const request of requests) {
  const agent = new ReActAgent({ llm, tools });
  await agent.chat({ message: request.prompt });
}

4. Tool wrappers that register themselves twice

Some teams wrap FunctionTool inside their own factory and accidentally call registration logic twice.

// ❌ Broken
function makeCustomerTools() {
  const t = FunctionTool.from(getCustomer, {
    name: "get_customer",
    description: "Fetch customer",
  });

  registerGlobalTool(t); // first registration
  return [t];
}

registerGlobalTool(t); // second registration elsewhere

// ✅ Fixed
function makeCustomerTools() {
  return [
    FunctionTool.from(getCustomer, {
      name: "get_customer",
      description: "Fetch customer",
    }),
  ];
}

How to Debug It

  1. Log every tool name before constructing the agent

    You want to see exactly what LlamaIndex receives.

    console.log(tools.map((t) => t.metadata.name));
    

    If you see repeated names like ["get_customer", "get_customer"], that’s your bug.

  2. Search for all tools: assignments

    Check whether you pass tools into:

    • new ReActAgent({ tools })
    • agent.chat({ tools })
    • workflow nodes or runners

    The error often comes from supplying them in more than one layer.

  3. Check for shared mutable arrays

    Look for:

    • module-level arrays
    • .push() on cached lists
    • singleton registries

    If your tool list grows between requests, you’ve found it.

  4. Print stack traces around tool creation

    Wrap your factory code so you can see where each tool instance is created.

    const tool = FunctionTool.from(fn, { name: "lookup_policy", description: "" });
    console.trace("Created tool:", tool.metadata.name);
    

    If the trace appears twice per request, your factory is being called twice.

Prevention

  • Keep tool definitions pure and immutable.
  • Use a single source of truth for each tool list per agent.
  • Enforce unique names with a small helper before building agents:
function assertUniqueToolNames(tools) {
  const names = tools.map((t) => t.metadata.name);
  const dupes = names.filter((name, i) => names.indexOf(name) !== i);
  if (dupes.length > 0) {
    throw new Error(`Duplicate tool names found: ${[...new Set(dupes)].join(", ")}`);
  }
}

If you’re using LlamaIndex TypeScript and hit duplicate tool calls, assume duplication in your wiring first. In most cases, fixing how ReActAgent, FunctionTool, and your per-request state are assembled will clear it immediately.


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