How to Fix 'duplicate tool calls in production' in AutoGen (TypeScript)

By Cyprian AaronsUpdated 2026-04-21
duplicate-tool-calls-in-productionautogentypescript

When AutoGen says duplicate tool calls in production, it usually means the model emitted the same tool invocation more than once and your runtime tried to execute both. In TypeScript, this most often shows up when you wire an agent loop incorrectly, retry a turn without deduping tool calls, or let two handlers process the same assistant message.

The important part: this is usually not a model bug. It’s almost always a state management or orchestration bug in your app.

The Most Common Cause

The #1 cause is re-processing the same assistant message and executing tool calls twice.

This happens when you:

  • persist messages,
  • reload them on restart,
  • then call run() again without tracking which tool calls were already handled.

Broken vs fixed pattern

BrokenFixed
Replays the same assistant message and executes tools againDedupes by toolCallId before execution
No idempotency guardStores processed tool call IDs
// ❌ Broken: same assistant message can be processed twice
import { AssistantAgent, UserProxyAgent } from "@autogen/agent";

const assistant = new AssistantAgent({
  name: "assistant",
  modelClient,
  tools: [lookupCustomer],
});

const user = new UserProxyAgent({ name: "user" });

const result = await assistant.run({
  task: "Find customer balance and update CRM",
});

// Later, after restart or retry:
for (const msg of result.messages) {
  if (msg.role === "assistant" && msg.toolCalls) {
    for (const toolCall of msg.toolCalls) {
      // ❌ This runs again if the message is replayed
      await executeTool(toolCall);
    }
  }
}
// ✅ Fixed: dedupe by toolCallId before execution
import { AssistantAgent } from "@autogen/agent";

const processedToolCalls = new Set<string>();

const assistant = new AssistantAgent({
  name: "assistant",
  modelClient,
  tools: [lookupCustomer],
});

const result = await assistant.run({
  task: "Find customer balance and update CRM",
});

for (const msg of result.messages) {
  if (msg.role === "assistant" && msg.toolCalls) {
    for (const toolCall of msg.toolCalls) {
      if (processedToolCalls.has(toolCall.id)) continue;

      processedToolCalls.add(toolCall.id);
      await executeTool(toolCall);
    }
  }
}

If you are using AutoGen’s built-in tool execution path, the same rule applies at a higher level: don’t re-run the same turn unless you have a fresh conversation state.

Other Possible Causes

1. You attached the same tool twice

This is common when composing agents from shared config objects.

// ❌ Broken
const tools = [lookupCustomer, updateCrm];

const agent = new AssistantAgent({
  name: "assistant",
  modelClient,
  tools: [...tools, lookupCustomer], // duplicate entry
});
// ✅ Fixed
const agent = new AssistantAgent({
  name: "assistant",
  modelClient,
  tools: [lookupCustomer, updateCrm],
});

2. Your retry logic replays the whole turn

If your API call fails after the model already emitted tool calls, a naive retry can duplicate execution.

// ❌ Broken
try {
  await assistant.run({ task });
} catch {
  await assistant.run({ task }); // replays same turn
}
// ✅ Fixed
try {
  await assistant.run({ task });
} catch (err) {
  // Retry only if you can guarantee no side effects were executed
  throw err;
}

If you must retry, persist a conversation checkpoint and resume from the last confirmed state instead of restarting from scratch.

3. Two workers are consuming the same conversation

In production, this shows up with queue consumers, cron jobs, or horizontally scaled workers.

// ❌ Broken config: no lock / lease on conversation ID
await processConversation(conversationId);
await processConversation(conversationId); // another worker does this too

Fix it with a distributed lock keyed by conversation ID or run ID.

// ✅ Fixed idea
await lock(`conv:${conversationId}`, async () => {
  await processConversation(conversationId);
});

4. You’re mixing manual tool execution with AutoGen’s executor

If AutoGen is already executing tools and you also execute them yourself from messages, you’ll double-run them.

// ❌ Broken: AutoGen executes tools, then your code does too
const result = await agent.run({ task });

for (const msg of result.messages) {
  if (msg.toolCalls) {
    for (const tc of msg.toolCalls) await executeTool(tc);
  }
}

Use one execution path only:

  • either let AutoGen handle tools,
  • or handle them manually,
  • not both.

How to Debug It

  1. Log every toolCall.id

    • Print the assistant message ID and each toolCall.id.
    • If the same ID appears twice, you have replay/deduplication issues.
  2. Check whether two code paths execute tools

    • Search for executeTool, runTools, toolCalls, and any custom middleware.
    • Make sure only one layer owns execution.
  3. Inspect retries and restarts

    • Look for HTTP retries, job retries, process restarts, or queue redelivery.
    • A duplicated call after failure usually means your retry boundary is too wide.
  4. Verify worker concurrency

    • If multiple instances can process the same conversation ID, add locking.
    • This is especially important with Redis queues, BullMQ, SQS, or cron-triggered jobs.

A useful log shape looks like this:

console.log({
  conversationId,
  messageId: msg.id,
  role: msg.role,
  toolCallIds: msg.toolCalls?.map((t) => t.id),
});

If you see something like:

  • messageId=abc123 with toolCallIds=[call_1]
  • then later again messageId=abc123 with toolCallIds=[call_1]

you are replaying state. If you see different message IDs but identical side effects, your downstream executor is not idempotent.

Prevention

  • Make tool execution idempotent

    • Use database uniqueness constraints or a processed-event table keyed by toolCallId.
  • Treat conversation state as durable state

    • Persist messages and last processed turn.
    • Resume from checkpoints instead of rerunning full tasks after failures.
  • Own one execution layer

    • Either AutoGen runs tools or your app runs them.
    • Don’t split responsibility across middleware, handlers, and post-processing code.

If you want a quick rule of thumb: when you see duplicate tool calls in production, assume your app ran the same agent turn twice until proven otherwise.


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