JuliusBrussee/caveman · error · CavemanRunError

cave_output_schema_invalid_json

cave_output_schema_invalid_json

Error message

the SDK output was not valid JSON

What it means

When the agent definition declares `output.schema`, the executor must parse the SDK's final text as JSON (`result.structured_output` is preferred, otherwise `JSON.parse(text)`). If both are unavailable or the text is not valid JSON, the declared structured-output contract cannot be checked, so the run fails as a CavemanRunError carrying the usage receipt. The library throws rather than returning raw text because the caller explicitly asked for schema-validated structured output.

Solutions

  1. Add an explicit instruction in the agent prompt to reply with a single raw JSON value matching the schema (no prose, no code fences).
  2. Upgrade/verify the Claude Agent SDK version so `result.structured_output` is populated, which bypasses text parsing entirely.
  3. Inspect `result.result` text on failure — if it's fenced, either strip fences in a pre-step or rely on structured_output.
  4. Raise `output.maxTokens` if truncation is cutting the JSON short.

Example fix

// before
prompt: "Summarize the findings."
// after
prompt: "Summarize the findings. Reply with ONLY a JSON object: {\"summary\": string, \"items\": string[]} — no markdown, no commentary."
Defensive patterns

Strategy: try-catch

Validate before calling

// Check the prompt demands JSON before declaring a schema-driven output.
if (definition.output?.schema && !/JSON/i.test(prompt)) {
  throw new Error("schema declared but prompt never instructs JSON-only output");
}

Type guard

function looksLikeJson(s: string): boolean {
  const t = s.trim().replace(/^```(?:json)?|```$/g, "").trim();
  return t.startsWith("{") || t.startsWith("[");
}

Try / catch

try {
  const r = await runClaudeAgent(opts);
} catch (e) {
  if (e instanceof CavemanRunError && e.code === "cave_output_schema_invalid_json") {
    console.warn("raw SDK text:", e.receipt); // inspect text, then retry with stricter JSON instruction
  }
  throw e;
}

Prevention

When it happens

Trigger: Declaring `definition.output.schema` while the Claude SDK run ends with free-form text instead of JSON — e.g. the model wrapped JSON in prose/markdown fences with no `structured_output`, the prompt never instructed JSON output, or the final result text was truncated.

Common situations: Porting an agent that previously returned plain text and adding an output schema without adjusting the prompt; SDK versions that don't populate `structured_output`; long outputs truncated by maxTokens cutting the JSON mid-string; the model answering a clarification instead of JSON.

Understand the failure class

Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.

Related errors


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

Appendix: source

Thrown at packages/agent/src/claude-runtime.ts:341

    if (result === undefined || result.subtype !== "success" || result.is_error) {
      throw carry(
        `cave_claude_terminal_${result?.subtype ?? "missing"}`,
        `the Claude Agent SDK ended with ${result?.subtype ?? "no result"}`,
      );
    }
    if (assistantModel !== undefined && normalizeClaudeModel(assistantModel) !== selected) {
      throw carry(
        "cave_provider_model_identity_mismatch",
        `expected ${selected}, the SDK answered as ${assistantModel}`,
      );
    }
    const text = result.result;
    if (definition.output?.schema !== undefined) {
      let parsed: unknown;
      try {
        parsed = result.structured_output ?? JSON.parse(text);
      } catch {
        throw carry("cave_output_schema_invalid_json", "the SDK output was not valid JSON");
      }
      if (!Value.Check(definition.output.schema, parsed)) {
        throw carry("cave_output_schema_mismatch", "the SDK output did not match the declared schema");
      }
    }
    // Past the success gate, usage must be accountable. A success whose usage
    // validateProviderUsage rejected is a real evidence failure — surface it
    // carrying the receipt, never mask it.
    if (usage === undefined) {
      throw carry(
        usageError?.message ?? "cave_claude_terminal_missing",
        "the SDK reported no accountable usage on a successful result",
      );
    }
    if (definition.output !== undefined && usage.outputTokens > definition.output.maxTokens) {
      // The SDK already ran and spent by the time its aggregate usage is known,
      // so this post-hoc budget breach carries the receipt of what was spent
      // rather than throwing it away.

View on GitHub (pinned to 3ee70a1026)