{"record":{"id":"ec02b6cc7cdd54b0","repo":"JuliusBrussee/caveman","slug":"cave-output-schema-mismatch-the-sdk-did-not-match-the","errorCode":"cave_output_schema_mismatch","errorMessage":"the SDK output did not match the declared schema","messagePattern":"the SDK output did not match the declared schema","errorType":"error_code","errorClass":"CavemanRunError","httpStatus":null,"severity":"error","filePath":"packages/agent/src/claude-runtime.ts","lineNumber":344,"sourceCode":"        `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.\n      throw carry(\n        \"cave_output_budget_exceeded\",\n        `output ${usage.outputTokens} exceeded budget ${definition.output.maxTokens}`,","sourceCodeStart":326,"sourceCodeEnd":362,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/claude-runtime.ts#L326-L362","documentation":"After the SDK output parses as JSON, it is validated with TypeBox's `Value.Check` against the agent's declared `output.schema`. A parse that produces JSON of the wrong shape fails this gate, throwing a CavemanRunError that carries the spend receipt. The library throws because the caller's declared output contract is part of the run's evidence; silently returning malformed data would break downstream consumers.","triggerScenarios":"Declaring `definition.output.schema` and receiving JSON that parses but violates it: missing required fields, wrong types (string vs number), extra/null fields under strict schemas, or the model returning an array where an object was declared.","commonSituations":"Schemas made stricter after agents were already prompted; models omitting optional-but-required-in-schema fields; numeric IDs returned as strings; LLM hallucinating extra wrapper keys like `{\"result\": {...}}` around the expected object.","solutions":["Log/inspect the parsed value and relax or correct the TypeBox schema to match what the model actually returns (e.g. allow `nullable`, fix field types).","Strengthen the prompt with the exact JSON shape, field names, and types, ideally embedding the schema itself.","Add a retry pass: re-invoke the agent asking it to fix its JSON to the schema when validation fails.","Use TypeBox options deliberately (`additionalProperties`, `minimum`, etc.) so borderline outputs aren't rejected."],"exampleFix":"// before\nschema: Type.Object({ count: Type.Number() }) // model returned {\"count\": \"12\"}\n// after\nprompt: '... Return {\"count\": <number>} — count must be a JSON number, not a string.'","handlingStrategy":"validation","validationCode":"import { Value } from \"typebox/value\";\n// Validate the schema itself is what you intend before runs.\nconst schema = Type.Object({ count: Type.Number() });\nif (!Value.Check(schema, { count: 0 })) throw new Error(\"declared schema rejects its own example\");","typeGuard":"function matchesOutput(v: unknown, schema: TSchema): boolean {\n  return Value.Check(schema, v);\n}","tryCatchPattern":"try {\n  const r = await runClaudeAgent({ ...opts });\n} catch (e) {\n  if (e instanceof CavemanRunError && e.code === \"cave_output_schema_mismatch\") {\n    // retry once with a corrective message embedding the schema\n  }\n  throw e;\n}","preventionTips":["Embed the exact JSON shape (field names/types) in the prompt, not just prose.","Keep the schema as loose as the task allows; tighten only with prompt reinforcement.","Add a one-shot self-correction retry that re-asks the model to fix its JSON against the schema.","Snapshot-test prompts against the schema with representative model outputs."],"tags":["schema","validation","structured-output","typebox"],"backgroundTag":"schema-validation-failed","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"}