JuliusBrussee/caveman · error

cave_tool_schema_invalid

cave_tool_schema_invalid

Error message

cave_tool_schema_invalid:${definition.name}

What it means

The tool's body bytes, once parsed, must be a record whose "name" equals the definition name, with a string "description" and a record "input" schema. Otherwise the runtime declares the stored tool schema invalid and includes the tool name in the message.

Solutions

  1. Fix the tool body to the expected shape: { name, description, input } with name matching the registered definition.
  2. Ensure the serializer that produces segment bodies emits exactly these fields.
  3. Compare the parsed body against the definition to find the mismatching field.
  4. If a transform rewrote the body, disable or correct that transform.

Example fix

// before: mismatched name in stored body
{ "name": "SearchTool", "description": "...", "input": {} }
// after: name matches definition.name
{ "name": "search", "description": "...", "input": { "type": "object" } }
Defensive patterns

Strategy: validation

Validate before calling

const parsed = JSON.parse(new TextDecoder().decode(body));
const ok = parsed && typeof parsed === 'object' &&
  parsed.name === definition.name &&
  typeof parsed.description === 'string' &&
  parsed.input && typeof parsed.input === 'object';
if (!ok) throw new Error(`tool body for ${definition.name} must be { name, description, input }`);

Type guard

function validBody(v: unknown, name: string): v is { name: string; description: string; input: Record<string, unknown> } {
  const r = v as Record<string, unknown> | null;
  return !!r && typeof r === 'object' && r.name === name && typeof r.description === 'string' && !!r.input && typeof r.input === 'object';
}

Try / catch

try {
  return providerToolDefinition(def, lowered, plan);
} catch (e) {
  if (String(e?.message).startsWith('cave_tool_schema_invalid')) {
    console.error(`stored schema for ${def.name} is malformed; regenerate bodies`);
  }
  throw e;
}

Prevention

When it happens

Trigger: JSON.parse of the segment body yields a non-record, a mismatched name, a non-string description, or a non-record input at runtime.ts:3821.

Common situations: A custom tool serializer writing a different JSON shape; a tool renamed in one place but not the other; hand-authored tool bodies with missing fields; a transform encoding the schema under a different key.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/9b393ae623f0ab4a. Report an issue: GitHub.

Appendix: source

Thrown at packages/agent/src/runtime.ts:3821

      typeof parsed.description !== "string" || !isRecord(parsed.input)) {
    throw new Error(`tool_schema_transform_invalid:${segmentID}`);
  }
  return output;
}

function providerToolDefinition(
  definition: ToolDefinition,
  lowered: LoweredContext,
  appliedPlan: AppliedPlan,
): { description: string; input: TSchema } {
  const segment = lowered.ir.segments.find((item) => item.id === `tool.${definition.name}`);
  if (!segment) throw new Error(`cave_context_segment_missing:tool.${definition.name}`);
  const body = appliedPlan.bodies.get(segment.bodyHandle);
  if (!body) throw new Error(`cave_context_body_missing:tool.${definition.name}`);
  const parsed = JSON.parse(new TextDecoder().decode(body)) as unknown;
  if (!isRecord(parsed) || parsed.name !== definition.name ||
      typeof parsed.description !== "string" || !isRecord(parsed.input)) {
    throw new Error(`cave_tool_schema_invalid:${definition.name}`);
  }
  return { description: parsed.description, input: parsed.input as TSchema };
}

function admitSubagent(state: InvocationState): () => void {
  const ledger = state.ledger;
  if (ledger.maxInvocations !== undefined && ledger.admitted >= ledger.maxInvocations) {
    ledger.invocationRejections++;
    throw new Error("cave_subagent_invocation_limit");
  }
  if (ledger.maxConcurrent !== undefined && ledger.active >= ledger.maxConcurrent) {
    ledger.concurrencyRejections++;
    throw new Error("cave_subagent_concurrency_limit");
  }
  ledger.admitted++;
  ledger.active++;
  ledger.peakActive = Math.max(ledger.peakActive, ledger.active);
  let released = false;

View on GitHub (pinned to 3ee70a1026)