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

By Cyprian AaronsUpdated 2026-04-21
duplicate-tool-calls-when-scalingautogentypescript

If you’re seeing duplicate tool calls when scaling, AutoGen is usually telling you the same tool invocation is being executed more than once across concurrent runs, retries, or multiple agent turns. In TypeScript, this shows up most often when you scale from a single local test to parallel requests, shared agent instances, or streaming handlers that re-process the same event.

The failure mode is usually not the tool itself. It’s the orchestration around AssistantAgent, ToolCallEvent, and your own idempotency boundaries.

The Most Common Cause

The #1 cause is reusing mutable agent state across concurrent conversations.

In AutoGen TypeScript, if you keep one AssistantAgent instance in memory and run multiple requests through it at the same time, tool call state can bleed across runs. That’s when you start seeing duplicate executions of the same tool call ID or repeated function_call handling.

Broken vs fixed pattern

Broken patternFixed pattern
One shared agent for all requestsOne agent per request/session
Tool side effects are not idempotentTool execution keyed by toolCallId
Concurrent .run() calls on same instanceIsolated runtime per conversation
// ❌ Broken: shared agent instance across concurrent requests
import { AssistantAgent } from "@autogen/core";

const agent = new AssistantAgent({
  name: "support-agent",
  modelClient,
  tools: [lookupPolicyTool],
});

export async function handleRequest(req: Request) {
  const input = await req.text();

  // Multiple HTTP requests can hit this at once
  const result = await agent.run([{ role: "user", content: input }]);

  return Response.json(result);
}
// ✅ Fixed: create an isolated agent per request
import { AssistantAgent } from "@autogen/core";

export async function handleRequest(req: Request) {
  const input = await req.text();

  const agent = new AssistantAgent({
    name: "support-agent",
    modelClient,
    tools: [lookupPolicyTool],
  });

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

  return Response.json(result);
}

If your tool has side effects, make it idempotent too:

const processedCalls = new Set<string>();

const lookupPolicyTool = {
  name: "lookup_policy",
  description: "Fetch policy details",
  parameters: {
    type: "object",
    properties: { policyId: { type: "string" } },
    required: ["policyId"],
  },
  async execute(args: { policyId: string }, ctx?: { toolCallId?: string }) {
    if (ctx?.toolCallId && processedCalls.has(ctx.toolCallId)) {
      return { status: "duplicate_ignored" };
    }

    if (ctx?.toolCallId) processedCalls.add(ctx.toolCallId);

    return fetchPolicy(args.policyId);
  },
};

Other Possible Causes

1. Retry logic replays the same message

If you wrap AutoGen calls in your own retry middleware, you may be replaying the exact same assistant turn after a timeout.

// ❌ Retries can replay the same tool call
await retry(async () => {
  return await agent.run(messages);
});

Fix it by retrying only transport failures, not completed model turns:

// ✅ Retry only before the assistant turn is accepted
const response = await withTimeout(() => modelClient.create(messages));

2. Streaming handler processes deltas twice

When using streaming, it’s easy to attach two listeners or re-emit chunks after reconnect.

// ❌ Duplicate event subscription
stream.on("data", handleChunk);
stream.on("data", handleChunk);

Make sure each stream is consumed once:

// ✅ Single consumer path
for await (const chunk of stream) {
  handleChunk(chunk);
}

3. Tool execution is not deduplicated by toolCallId

AutoGen will surface tool calls with stable IDs in many flows. If your backend ignores them and executes based only on arguments, duplicates become visible under load.

// ❌ No dedupe key
await executeTool(call.name, call.arguments);
// ✅ Use the tool call ID as your idempotency key
await executeToolOnce(call.toolCallId, call.name, call.arguments);

4. Shared memory or stateful termination conditions

If you use shared message history or a global termination condition, one conversation can trigger another conversation’s pending tool action.

// ❌ Shared mutable state across sessions
const messages: any[] = [];
const termination = new MaxMessageTermination(20);

Scope both per session:

// ✅ Per-session state
function createSessionState() {
  return {
    messages: [],
    termination: new MaxMessageTermination(20),
  };
}

How to Debug It

  1. Log every tool call with its ID

    • Print toolCallId, tool name, args hash, and session ID.
    • If the same toolCallId appears twice, the duplicate is upstream of your tool code.
  2. Check whether one agent instance is shared

    • Search for module-level singletons.
    • If AssistantAgent or UserProxyAgent lives outside request scope, that’s suspicious.
  3. Disable retries and streaming temporarily

    • Run a single synchronous request.
    • If duplicates disappear, your issue is in retry orchestration or stream consumption.
  4. Add an idempotency guard at the tool boundary

    • Store processed IDs in Redis or a DB table.
    • If duplicates stop but logs still show repeated calls, AutoGen is re-emitting; if they continue without duplicates being blocked, your backend is executing twice.

Example debug log:

console.log({
  sessionId,
  toolName: call.name,
  toolCallId: call.toolCallId,
  argsHash: hash(JSON.stringify(call.arguments)),
});

Prevention

  • Create agents per request or per conversation. Do not share mutable AutoGen agent instances across concurrent sessions.
  • Make every side-effecting tool idempotent with a durable dedupe key like toolCallId.
  • Treat retries and stream reconnects as replay sources. Only retry safe boundaries and consume each stream once.

If you’re scaling AutoGen TypeScript into production, this error is usually a state isolation problem, not an LLM problem. Fix the orchestration first, then harden the tools with idempotency so duplicates become harmless instead of expensive.


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