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

By Cyprian AaronsUpdated 2026-04-21
duplicate-tool-calls-during-developmentautogentypescript

If you’re seeing duplicate tool calls during development in AutoGen TypeScript, it usually means the agent is triggering the same tool more than once for a single user turn. In practice, this shows up when your message loop, event handler, or retry logic re-runs the same assistant step and AutoGen emits duplicate tool_calls or repeated function invocations.

This is almost always a wiring issue, not an LLM issue. The bug tends to appear during local development when you have hot reload, React state updates, server re-entry, or a loop that replays the last assistant message.

The Most Common Cause

The #1 cause is re-processing the same assistant response more than once.

With AutoGen TypeScript, that usually means you call run() or continue the conversation inside a callback that is itself triggered by every streamed update or every render. The assistant message gets handled twice, so the tool gets called twice.

Here’s the broken pattern:

BrokenFixed
Re-runs on every state updateProcesses each turn once
No idempotency guardTracks processed message IDs
Often happens in UI codeUses a stable conversation boundary
// BROKEN
import { AssistantAgent } from "@autogen/agent";
import { OpenAIChatCompletionClient } from "@autogen/openai";

const agent = new AssistantAgent({
  name: "support-agent",
  modelClient: new OpenAIChatCompletionClient({ model: "gpt-4o-mini" }),
  tools: [lookupPolicyTool],
});

let lastAssistantMessage: any = null;

async function handleUserMessage(input: string) {
  const result = await agent.run([{ role: "user", content: input }]);

  // Bad: replaying the same result can happen if this function is triggered twice
  for (const msg of result.messages) {
    if (msg.role === "assistant") {
      lastAssistantMessage = msg;
      await agent.run([msg]); // causes duplicate tool calls
    }
  }
}
// FIXED
import { AssistantAgent } from "@autogen/agent";
import { OpenAIChatCompletionClient } from "@autogen/openai";

const agent = new AssistantAgent({
  name: "support-agent",
  modelClient: new OpenAIChatCompletionClient({ model: "gpt-4o-mini" }),
  tools: [lookupPolicyTool],
});

const processedMessageIds = new Set<string>();

async function handleUserMessage(input: string) {
  const result = await agent.run([{ role: "user", content: input }]);

  for (const msg of result.messages) {
    if (msg.role === "assistant" && msg.id && !processedMessageIds.has(msg.id)) {
      processedMessageIds.add(msg.id);
      // Do not feed the assistant message back into run() unless you're intentionally continuing a conversation.
    }
  }
}

The important part: don’t treat an assistant response as fresh user input. If you need multi-step orchestration, continue with the framework’s conversation state, not by replaying messages manually.

Other Possible Causes

1. Hot reload re-registers tools

In dev mode, your module can be evaluated twice. That creates two tool registrations with the same function name, and AutoGen may emit repeated calls.

// Risky in dev if module reloads
export const tools = [lookupPolicyTool];

Fix by keeping tool registration in a singleton or app bootstrap path that runs once.

let cachedTools: any[] | null = null;

export function getTools() {
  if (!cachedTools) {
    cachedTools = [lookupPolicyTool];
  }
  return cachedTools;
}

2. You are using both streaming and non-streaming handlers

If you subscribe to stream chunks and also call run() completion handling on the same turn, you can process the same tool call twice.

// Bad idea: both paths act on the same turn
const stream = await agent.runStream(messages);
stream.on("message", handleMessage);
const final = await stream.result; // handleMessage already processed it

Pick one path per request:

  • streaming-only handling, or
  • final-result-only handling

3. Retry middleware repeats non-idempotent turns

If your HTTP client retries on timeout and your agent endpoint is not idempotent, one user action can create two identical runs.

const client = new OpenAIChatCompletionClient({
  model: "gpt-4o-mini",
  fetchOptions: {
    // aggressive retries can replay requests during dev
    timeoutMs: 3000,
  },
});

If you must retry, attach a request/conversation ID and dedupe at your app layer.

4. Tool implementation triggers its own rerun

Sometimes the tool itself updates state that causes the agent loop to start again.

async function lookupPolicyTool(args: { policyId: string }) {
  await saveToDb(args.policyId); // triggers UI/state refresh
  return { status: "ok" };
}

If that refresh feeds back into the same conversation trigger, you get duplicate calls. Separate side effects from conversation triggers.

How to Debug It

  1. Log every assistant message ID and tool call ID

    • Print message.id, toolCall.id, and toolCall.name.
    • If you see the same IDs twice, you’re replaying state.
    • If IDs differ but arguments are identical, your trigger is firing twice.
  2. Disable hot reload temporarily

    • Run the app in plain Node without watcher tooling.
    • If the error disappears, your dev server is re-importing modules and duplicating agents or tools.
  3. Turn off streaming

    • Use final-result handling only.
    • If duplicates stop, your stream event handler is processing tool calls before completion handling does.
  4. Add a per-turn guard

    • Store a conversationTurnId.
    • Ignore any repeated processing for that turn.
    • This tells you whether duplication comes from UI rerenders or backend retries.

Prevention

  • Keep one source of truth for conversation state.
  • Register tools once at process startup, not inside render functions or request handlers that can be re-entered.
  • Make tool execution idempotent where possible:
    • use unique request IDs
    • dedupe by toolCall.id
    • avoid side effects before validation passes

The core fix is simple: make sure one user turn maps to one agent execution path. In AutoGen TypeScript, duplicate tool calls almost always mean your app is replaying context somewhere between input capture, streaming events, and final response handling.


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