{"record":{"id":"b7671b9712bf7396","repo":"JuliusBrussee/caveman","slug":"cave-output-schema-invalid-json-the-sdk-was-not-valid","errorCode":"cave_output_schema_invalid_json","errorMessage":"the SDK output was not valid JSON","messagePattern":"the SDK output was not valid JSON","errorType":"error_code","errorClass":"CavemanRunError","httpStatus":null,"severity":"error","filePath":"packages/agent/src/claude-runtime.ts","lineNumber":341,"sourceCode":"    if (result === undefined || result.subtype !== \"success\" || result.is_error) {\n      throw carry(\n        `cave_claude_terminal_${result?.subtype ?? \"missing\"}`,\n        `the Claude Agent SDK ended with ${result?.subtype ?? \"no result\"}`,\n      );\n    }\n    if (assistantModel !== undefined && normalizeClaudeModel(assistantModel) !== selected) {\n      throw carry(\n        \"cave_provider_model_identity_mismatch\",\n        `expected ${selected}, the SDK answered as ${assistantModel}`,\n      );\n    }\n    const text = result.result;\n    if (definition.output?.schema !== undefined) {\n      let parsed: unknown;\n      try {\n        parsed = result.structured_output ?? JSON.parse(text);\n      } catch {\n        throw carry(\"cave_output_schema_invalid_json\", \"the SDK output was not valid JSON\");\n      }\n      if (!Value.Check(definition.output.schema, parsed)) {\n        throw carry(\"cave_output_schema_mismatch\", \"the SDK output did not match the declared schema\");\n      }\n    }\n    // Past the success gate, usage must be accountable. A success whose usage\n    // validateProviderUsage rejected is a real evidence failure — surface it\n    // carrying the receipt, never mask it.\n    if (usage === undefined) {\n      throw carry(\n        usageError?.message ?? \"cave_claude_terminal_missing\",\n        \"the SDK reported no accountable usage on a successful result\",\n      );\n    }\n    if (definition.output !== undefined && usage.outputTokens > definition.output.maxTokens) {\n      // The SDK already ran and spent by the time its aggregate usage is known,\n      // so this post-hoc budget breach carries the receipt of what was spent\n      // rather than throwing it away.","sourceCodeStart":323,"sourceCodeEnd":359,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/claude-runtime.ts#L323-L359","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Add an explicit instruction in the agent prompt to reply with a single raw JSON value matching the schema (no prose, no code fences).","Upgrade/verify the Claude Agent SDK version so `result.structured_output` is populated, which bypasses text parsing entirely.","Inspect `result.result` text on failure — if it's fenced, either strip fences in a pre-step or rely on structured_output.","Raise `output.maxTokens` if truncation is cutting the JSON short."],"exampleFix":"// before\nprompt: \"Summarize the findings.\"\n// after\nprompt: \"Summarize the findings. Reply with ONLY a JSON object: {\\\"summary\\\": string, \\\"items\\\": string[]} — no markdown, no commentary.\"","handlingStrategy":"try-catch","validationCode":"// Check the prompt demands JSON before declaring a schema-driven output.\nif (definition.output?.schema && !/JSON/i.test(prompt)) {\n  throw new Error(\"schema declared but prompt never instructs JSON-only output\");\n}","typeGuard":"function looksLikeJson(s: string): boolean {\n  const t = s.trim().replace(/^```(?:json)?|```$/g, \"\").trim();\n  return t.startsWith(\"{\") || t.startsWith(\"[\");\n}","tryCatchPattern":"try {\n  const r = await runClaudeAgent(opts);\n} catch (e) {\n  if (e instanceof CavemanRunError && e.code === \"cave_output_schema_invalid_json\") {\n    console.warn(\"raw SDK text:\", e.receipt); // inspect text, then retry with stricter JSON instruction\n  }\n  throw e;\n}","preventionTips":["Always include an explicit 'reply with only raw JSON, no markdown' instruction when declaring output.schema.","Use an SDK version that populates `structured_output` so text parsing is bypassed.","Keep output.maxTokens comfortably above the largest expected JSON so truncation can't cut it.","Test schema agents with a small fixture run before production."],"tags":["json","structured-output","schema","claude-agent-sdk"],"backgroundTag":"json-parse-error","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}