mastra-ai/mastra · error

Code Mode external identifier collision: tools

Error message

Code Mode external identifier collision: tools "${existing}" and "${toolId}" both map to external_${externalName}

What it means

When building the runner, two distinct tool ids can sanitize to the same external name (e.g. 'a-b' and 'a_b' both become 'a_b'), which would make the second global overwrite the first and leave one tool unreachable. buildRunner fails fast with this collision error instead.

Solutions

  1. Rename one of the colliding tool ids so their sanitized external names differ.
  2. Adopt a single naming convention (e.g. camelCase) for tool ids used with Code Mode.
  3. Pre-check sanitized names for duplicates before passing the tool map to createCodeMode.

Example fix

// before
createCodeMode({ tools: { 'get-user': a, 'get_user': b } });
// after
createCodeMode({ tools: { 'getUser': a, 'get_user_v2': b } });
Defensive patterns

Strategy: validation

Validate before calling

const seen = new Map<string, string>();
for (const id of Object.keys(tools)) {
  const ext = sanitizeToolId(id);
  if (seen.has(ext)) throw new Error(`Collision: "${seen.get(ext)}" and "${id}" -> external_${ext}`);
  seen.set(ext, id);
}

Try / catch

try {
  codeMode = createCodeMode({ tools });
} catch (e) {
  if (String(e.message).includes('external identifier collision')) {
    // rename one of the tools named in the message and retry
  } else throw e;
}

Prevention

When it happens

Trigger: Registering two tools in createCodeMode whose ids differ only by characters sanitized to underscores, e.g. 'get-data' and 'get_data'.

Common situations: Teams mixing kebab-case and snake_case tool names; importing tools from multiple packages with overlapping names; programmatically generated ids that differ only in punctuation.

Related errors


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

Appendix: source

Thrown at packages/core/src/tools/code-mode/runner.ts:60

/**
 * Produce the full runner source to write into the sandbox and run with node.
 */
export function buildRunner({ programModule, externals }: BuildRunnerOptions): string {
  // `buildRunner` is exported, so a caller could pass a non-sanitized name.
  // External names become global property suffixes, so reject anything that
  // isn't a legal identifier instead of producing an unusable global.
  const SAFE_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
  const seen = new Map<string, string>();
  for (const { externalName, toolId } of externals) {
    if (!SAFE_IDENT.test(externalName)) {
      throw new Error(`Invalid Code Mode external identifier: ${externalName}`);
    }
    // Two tool ids can sanitize to the same external name (e.g. `a-b` and
    // `a_b` both become `a_b`). The install loop below would silently overwrite
    // the earlier global, leaving one tool unreachable. Fail fast instead.
    const existing = seen.get(externalName);
    if (existing) {
      throw new Error(
        `Code Mode external identifier collision: tools "${existing}" and "${toolId}" both map to external_${externalName}`,
      );
    }
    seen.set(externalName, toolId);
  }

  // Externals are emitted as JSON data, not interpolated identifiers. The
  // runner installs each `external_<name>` global in a loop using bracket
  // assignment, so no caller-derived string is ever spliced into the generated
  // source as code. This keeps tool ids strictly data, even if `sanitize`
  // changes.
  const externalsJson = JSON.stringify(externals.map(({ externalName, toolId }) => ({ externalName, toolId })));

  return `'use strict';
const FRAME_PREFIX = ${JSON.stringify(FRAME_PREFIX)};

function __emit(frame) {
  process.stdout.write(FRAME_PREFIX + JSON.stringify(frame) + '\\n');

View on GitHub (pinned to 75dd419e61)