affaan-m/ECC · error · ReplayError

tool.unknown

tool.unknown

Error message

tool ${name} is not declared

What it means

replayer.call(name, args) throws ReplayError('tool.unknown') when the requested tool name is not a key of the tools map passed to createReplayer. The replayer only permits calls to explicitly declared tools so every call's effect class and determinism are known. There is no dynamic registration after construction.

Solutions

  1. Add the tool to the tools map passed to createReplayer with full declaration (effect_class, determinism, impl), or
  2. Fix the name in the call() site to exactly match a declared key — print Object.keys(tools) to compare.
  3. Synchronize tool names across config, fixtures, and call sites; consider exporting a shared TOOLS constant used by both declaration and call sites.
  4. If the name comes from data (test plan/fixture), validate it against the registry before running the suite.

Example fix

// before
r.call('fetchWether', { city: 'Paris' }); // typo

// after
r.call('fetchWeather', { city: 'Paris' });
Defensive patterns

Strategy: type-guard

Validate before calling

if (!(toolName in tools)) {
  throw new Error(`unknown tool ${toolName}; available: ${Object.keys(tools).join(', ')}`);
}

Type guard

function isDeclaredTool(tools, name) {
  return Object.prototype.hasOwnProperty.call(tools, name);
}

Try / catch

try {
  return replayer.call(name, args);
} catch (e) {
  if (e instanceof ReplayError && e.code === 'tool.unknown') {
    throw new Error(`${name} not declared; declared tools: ${Object.keys(tools).join(', ')}`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling r.call('fetchWether', ...) with a typo; calling a tool that was declared in another suite's tools map; a name mismatch between where tools are declared and where call() happens (e.g. kebab-case vs camelCase); calling a tool that was removed in a refactor.

Common situations: Renaming a tool in the registry without updating call sites; loading tool names from config/test data where the fixture references an old name; sharing fixture sets between suites with different registries; case-sensitive lookup ('FetchWeather' ≠ 'fetchWeather').

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/b8af2532dc31669d. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/eval-harness/replay.js:123

      throw new ReplayError('replay.bad_declaration', `tool ${name} must declare determinism`);
    }
  }

  const calls = [];
  const emit = (entry) => {
    calls.push(entry);
    if (typeof options.onCall === 'function') {
      options.onCall(entry);
    }
  };

  return {
    mode,
    calls,
    call(name, args = {}) {
      const tool = tools[name];
      if (!tool) {
        throw new ReplayError('tool.unknown', `tool ${name} is not declared`);
      }
      const rank = envelope.effectRank(tool.effect_class);
      if (rank > maxRank) {
        emit({ tool: name, mode, status: 'refused', code: 'tool.effect_forbidden' });
        throw new ReplayError('tool.effect_forbidden', `tool ${name} is ${tool.effect_class}, above the allowed ${options.maxEffectClass || 'SE2'}`, { tool: name });
      }
      if (mode === 'replay') {
        if (rank >= envelope.effectRank('SE3')) {
          emit({ tool: name, mode, status: 'refused', code: 'tool.effect_forbidden' });
          throw new ReplayError('tool.effect_forbidden', `tool ${name} (${tool.effect_class}) can never be replayed`, { tool: name });
        }
        const record = store.get(name, args);
        emit({ tool: name, mode, status: 'replayed', fixture_key: record.key, args_hash: record.args_hash, response_hash: record.response_hash });
        return record.response;
      }
      const response = tool.impl(args);
      const record = store.put(name, args, response);
      emit({ tool: name, mode, status: 'recorded', fixture_key: record.key, args_hash: record.args_hash, response_hash: record.response_hash });

View on GitHub (pinned to 8321021c54)