{"record":{"id":"d19b7f52fecfb3c9","repo":"JuliusBrussee/caveman","slug":"cave-output-schema-invalid-json","errorCode":"cave_output_schema_invalid_json","errorMessage":"cave_output_schema_invalid_json","messagePattern":"cave_output_schema_invalid_json","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":2119,"sourceCode":"      throw new Error(\"cave_subagent_spend_evidence_incomplete\");\n    }\n    // A run stopped before its first call has no assistant message, and that is\n    // the honest outcome rather than missing evidence: the caller gets an empty\n    // answer, zero usage, and the reason the runtime declined to spend.\n    if (!finalMessage && stopReason === undefined) {\n      throw new Error(\"cave_incomplete_evidence: Pi emitted no final assistant message\");\n    }\n    if (finalMessage &&\n        (finalMessage.stopReason === \"error\" || finalMessage.stopReason === \"aborted\")) {\n      throw new Error(`cave_provider_terminal_${finalMessage.stopReason}`);\n    }\n    const text = finalMessage === undefined ? \"\" : assistantText(finalMessage);\n    if (definition.output?.schema && finalMessage !== undefined) {\n      let parsed: unknown;\n      try {\n        parsed = JSON.parse(text);\n      } catch {\n        throw new Error(\"cave_output_schema_invalid_json\");\n      }\n      if (!Value.Check(definition.output.schema, parsed)) {\n        throw new Error(\"cave_output_schema_mismatch\");\n      }\n    }\n    if (appliedPlan.appliedTransformIDs.length > 0 && !cacheBoundaryKnown) {\n      cacheBust = true;\n      markRequestPassThrough(headers, appliedPlan, \"cache_boundary_unobserved\");\n    }\n    for (const child of nestedReceipts) receipt.recordSubagent(child);\n    // Built once so the result and its receipt cannot disagree about a breach.\n    const runReceipt = receipt.build({\n      runId,\n      agentId: definition.id,\n      stopReason: stopReason ?? \"complete\",\n      meter: budgetMeter,\n      ...(breakers === undefined ? {} : { breakers: breakers.recorded }),\n    });","sourceCodeStart":2101,"sourceCodeEnd":2137,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/runtime.ts#L2101-L2137","documentation":"Thrown when the agent definition declares an output schema (definition.output.schema) and the final assistant message's text is not parseable as JSON. The runtime validates structured output before returning it, so callers never receive invalid JSON typed as valid.","triggerScenarios":"definition.output.schema is set, a finalMessage exists, and assistantText(finalMessage) fails JSON.parse — the model answered in prose or emitted truncated JSON.","commonSituations":"Output max_tokens too small so JSON is cut off; instructions not demanding JSON-only output; models weak at instruction-following; transforms (e.g. text/toon compression) applied to the output segment distorting it.","solutions":["Strengthen the output instruction — the injected <cave-output> tag says 'Return output matching declared schema'; reinforce in agent instructions that the final message must be JSON only","Raise definition.output.maxTokens so the JSON is not truncated","Use a model with reliable structured-output behavior","Check appliedPlan for transforms routing the output segment and exclude the final answer from compression"],"exampleFix":"// before\nconst agent = defineAgent({\n  id: \"extractor\",\n  output: { schema: ExtractSchema, maxTokens: 128 }, // too small, JSON truncates\n});\n\n// after\nconst agent = defineAgent({\n  id: \"extractor\",\n  instructions: \"Reply with a single JSON object and nothing else.\",\n  output: { schema: ExtractSchema, maxTokens: 2048 },\n});","handlingStrategy":"validation","validationCode":"// Pre-check the model's answer shape in development by running without a schema\n// and validating yourself before enabling definition.output.schema:\nfunction isJson(text: string): boolean {\n  try { JSON.parse(text); return true; } catch { return false; }\n}","typeGuard":"function isInvalidOutputJson(e: unknown): e is Error {\n  return e instanceof Error && e.message === \"cave_output_schema_invalid_json\";\n}","tryCatchPattern":"try {\n  return await agent.run(input, opts);\n} catch (e) {\n  if (isInvalidOutputJson(e)) {\n    // retry with reinforced JSON-only instructions or higher output maxTokens\n  } else throw e;\n}","preventionTips":["State 'JSON only, no prose' explicitly in agent instructions","Size output maxTokens generously for the schema's serialized size","Exclude the final answer segment from compression transforms"],"tags":["output-schema","json","structured-output"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}