Hmbown/CodeWhale · error · Error

Missing Engine .

Error message

Missing Engine ${field}.

What it means

The `id()` helper inside observe() reads a required string field (`tool_call_id`, `tool_name`, or `id` depending on the event branch) and throws `Missing Engine <field>.` when the field is absent, not a string, or an empty string. The event passed the general validation but is missing the specific identifier the event's switch branch requires.

Solutions

  1. Populate the required id field for the event kind being emitted (check the switch branch in pet-engine.ts to see which field it needs).
  2. Replace empty strings with a real identifier before calling observe(), or skip the event.
  3. If your upstream uses a different key name, map it to the engine's expected field (e.g. callId → tool_call_id).
  4. Log the full event object on this error to see which `field` name appears in the message — it names exactly what is missing.

Example fix

// before
engine.observe({ event: 'tool_started', tool_name: '' }, now);

// after
engine.observe({ event: 'tool_started', tool_name: 'web_search', tool_call_id: callId }, now);
Defensive patterns

Strategy: validation

Validate before calling

function hasRequiredIds(e: Record<string, unknown>): boolean {
  const need = e.event === 'tool' || e.event === 'tool_finished' ? ['tool_call_id','tool_name'] : ['id'];
  return need.every(k => typeof e[k] === 'string' && e[k].length > 0);
}
if (!hasRequiredIds(event)) throw new TypeError('event missing required id field');

Type guard

const hasId = (e: Record<string, unknown>, field: string): e is Record<string, unknown> & { [k: string]: string } =>
  typeof e[field] === 'string' && (e[field] as string).length > 0;

Try / catch

try {
  engine.observe(event, at);
} catch (err) {
  const m = /^Missing Engine (.+)\.$/.exec(err instanceof Error ? err.message : '');
  if (m) {
    console.error(`event missing field '${m[1]}'`, event);
    return; // drop or enqueue for repair
  }
  throw err;
}

Prevention

When it happens

Trigger: Emitting a `message_finished`/`tool` event whose `id` or `tool_call_id` is undefined, null, or ''; passing `tool_name: ''` for a tool span start; omitting the id because the upstream pairing (tool_call → result) lost the identifier.

Common situations: Streaming providers that omit ids on partial chunks; renaming `tool_call_id` to `callId` in your own wrapper; empty-string ids when a template substituted a missing variable; forwarding events from a queue schema that made ids optional.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/43277e752675a8db. Report an issue: GitHub.

Appendix: source

Thrown at pet/src/core/pet-engine.ts:86

    next.sequence = this.sequence; next.lastTime = this.lastTime;
    return next;
  }

  observe(value: unknown, at: number): void {
    if (!Number.isFinite(at) || at < this.lastTime || at > PET_MAX_SECONDS * 1000) throw new Error('Invalid Engine pet clock.');
    if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid Engine pet metadata.');
    const e = value as Record<string, unknown>;
    const allowed = ['event', 'index', 'channel', 'tool_call_id', 'tool_name', 'id', 'worker_status', 'failed'];
    if (Object.keys(e).some(k => !allowed.includes(k)) || typeof e.event !== 'string'
      || Object.values(e).some(v => typeof v === 'string' && v.length > 4096)
      || e.channel !== undefined && !['text', 'reasoning'].includes(e.channel as string)
      || ['tool_call_id', 'tool_name', 'id', 'worker_status'].some(k => e[k] !== undefined && typeof e[k] !== 'string')
      || e.failed !== undefined && typeof e.failed !== 'boolean'
      || e.index !== undefined && (!Number.isSafeInteger(e.index) || (e.index as number) < 0))
      throw new Error('Invalid Engine pet metadata fields.');
    this.lastTime = at;
    this.events = this.events.filter(span => span.endTime >= at - 12_800);
    const id = (field: string) => { const s = e[field]; if (typeof s !== 'string' || !s) throw new Error(`Missing Engine ${field}.`); return s; };
    const index = () => { if (!Number.isSafeInteger(e.index)) throw new Error('Missing Engine index.'); return String(e.index); };
    const start = (key: string, name: string, category: Category, agentId?: string) => {
      if (this.active.size >= 256 && !this.active.has(key)) throw new Error('Too many active Engine pet spans.');
      this.active.set(key, this.add(name, category, at, agentId));
    };
    const finish = (key: string) => { this.pulse(key, at); this.active.delete(key); };
    switch (e.event) {
      case 'turn_started': this.active.clear(); this.waiting = undefined; break;
      case 'message_started': start(`message:${index()}`, 'assistant_message', 'communication'); this.waiting = undefined; break;
      case 'thinking_started': start(`thinking:${index()}`, 'thinking', 'reasoning'); this.waiting = undefined; break;
      case 'response_delta': {
        const reasoning = e.channel === 'reasoning';
        const key = `${reasoning ? 'thinking' : 'message'}:${index()}`;
        if (!this.active.has(key)) start(key, reasoning ? 'thinking' : 'assistant_message', reasoning ? 'reasoning' : 'communication');
        else this.pulse(key, at);
        this.waiting = undefined; break;
      }
      case 'message_complete': finish(`message:${index()}`); break;

View on GitHub (pinned to 433685b202)