How to Fix 'duplicate tool calls in production' in AutoGen (TypeScript)
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
| Broken | Fixed |
|---|---|
| Replays the same assistant message and executes tools again | Dedupes by toolCallId before execution |
| No idempotency guard | Stores 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
- •
Log every
toolCall.id- •Print the assistant message ID and each
toolCall.id. - •If the same ID appears twice, you have replay/deduplication issues.
- •Print the assistant message ID and each
- •
Check whether two code paths execute tools
- •Search for
executeTool,runTools,toolCalls, and any custom middleware. - •Make sure only one layer owns execution.
- •Search for
- •
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.
- •
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=abc123withtoolCallIds=[call_1] - •then later again
messageId=abc123withtoolCallIds=[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.
- •Use database uniqueness constraints or a processed-event table keyed by
- •
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
- •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