mastra-ai/mastra · error

Code Mode tool id collision: "${prior}" and "${toolId}" both

Error message

Code Mode tool id collision: "${prior}" and "${toolId}" both map to external_${externalName}

What it means

The stub generator emits TypeScript declarations `declare function external_<name>(...)` for each tool exposed to Code Mode. If two tool ids sanitize to the same external name, the generated stubs would collide (duplicate declarations pointing at different tools), so generateStubs throws with both offending ids.

Source

Thrown at packages/core/src/tools/code-mode/stub-generator.ts:178

  declaration: string;
}

/** Generate stubs for every tool in the config. */
export function generateStubs(tools: ToolsInput): CodeModeStub[] {
  // Two distinct tool ids can sanitize to the same `external_*` name (e.g.
  // `a-b` and `a_b`). Without this check the later binding would silently
  // overwrite the earlier one in the runner, so fail fast instead.
  const seen = new Map<string, string>();
  return Object.entries(tools).map(([key, tool]) => {
    const toolId = (tool as { id?: string }).id ?? key;
    const description = (tool as { description?: string }).description;
    const inputType = schemaToTs((tool as { inputSchema?: unknown }).inputSchema, 'input');
    const outputType = schemaToTs((tool as { outputSchema?: unknown }).outputSchema, 'output');
    const externalName = sanitizeToolId(toolId);

    const prior = seen.get(externalName);
    if (prior !== undefined && prior !== toolId) {
      throw new Error(`Code Mode tool id collision: "${prior}" and "${toolId}" both map to external_${externalName}`);
    }
    seen.set(externalName, toolId);

    const doc = description ? `/** ${description.replace(/\*\//g, '* /')} */\n` : '';
    const declaration = `${doc}declare function external_${externalName}(input: ${inputType}): Promise<${outputType}>;`;

    return { toolId, externalName, declaration };
  });
}

const createUsageContract = (toolId: string) => `# Code Mode

You have access to the \`${toolId}\` tool. Instead of calling tools one at a time,
write a single TypeScript program that orchestrates them and returns one result.

Rules:
- Call the available tools via the \`external_*\` functions declared below. Each
  returns a Promise — \`await\` it.

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Rename one of the colliding tool ids.
  2. Deduplicate sanitized names in your tool-registry code before constructing the agent's tool map.
  3. Standardize on camelCase ids for all Code Mode tools.

Example fix

// before
{ 'report-pdf': t1, 'report_pdf': t2 }
// after
{ 'reportPdf': t1, 'reportPdfV2': t2 }
Defensive patterns

Strategy: validation

Validate before calling

const seen = new Set<string>();
for (const id of Object.keys(tools)) {
  const ext = sanitizeToolId(id);
  if (seen.has(ext)) throw new Error(`Duplicate stub name external_${ext} from "${id}"`);
  seen.add(ext);
}

Try / catch

try {
  stubs = generateStubs(tools);
} catch (e) {
  if (String(e.message).includes('tool id collision')) {
    // rename colliding ids listed in the message
  } else throw e;
}

Prevention

When it happens

Trigger: Generating stubs for a tool set where two ids map to the same sanitized external name, e.g. 'report-pdf' and 'report_pdf' both -> 'report_pdf'.

Common situations: Mixed kebab/snake-case tool naming; merging tool maps from several sources without deduplication; refactors that renamed ids only in punctuation.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/3a959dc6bfa65591. Report an issue: GitHub.