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
- Fix the tool body to the expected shape: { name, description, input } with name matching the registered definition.
- Ensure the serializer that produces segment bodies emits exactly these fields.
- Compare the parsed body against the definition to find the mismatching field.
- 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
- Use one shared serializer for tool bodies so the shape never drifts.
- Keep definition.name and the serialized body name in sync (single source of truth).
- Add a registry-level schema check when tools are registered.
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
- tool_schema_transform_invalid
- agent conformance fixtures must cover claude and pi
- assembly slot content is not JSON-serializable
- assembly slot content is not JSON-serializable
- cave_context_ir_unknown_schema
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)