{"record":{"id":"9bbbc89f8748c2a4","repo":"JuliusBrussee/caveman","slug":"field-is-not-an-array-got-typeof-value","errorCode":null,"errorMessage":"${field} is not an array (got ${typeof value})","messagePattern":"(.+?) is not an array \\(got (.+?)\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/subagent-tax/lib/analyze.mjs","lineNumber":20,"sourceCode":"// prints. All sizes are JSON-serialized character counts of what the harness\n// actually sent — nothing is inferred here except the chars themselves.\n\nconst LLM_KINDS = new Set([\n  \"anthropic-messages\",\n  \"openai-chat\",\n  \"openai-responses\",\n  \"gemini-generatecontent\",\n]);\n\nconst jsonChars = (value) => (value === undefined || value === null ? 0 : JSON.stringify(value).length);\n\nconst isSystemRole = (m) => m && (m.role === \"system\" || m.role === \"developer\");\n\n// A field present in an unexpected shape is reported as unparsed, never\n// silently counted as zero — a false \"0 tools\" reads as a real measurement.\nfunction expectArray(value, field) {\n  if (value === undefined || value === null) return [];\n  if (!Array.isArray(value)) throw new Error(`${field} is not an array (got ${typeof value})`);\n  return value;\n}\n\nfunction anthropicParts(body) {\n  const tools = expectArray(body.tools, \"tools\").map((t) => ({ name: t?.name ?? t?.type ?? \"?\", chars: jsonChars(t) }));\n  // The fixed instruction payload arrives as `system` AND, in some harnesses,\n  // as system-role entries inside `messages` — count both or the flagship row\n  // under-reports its own prefix.\n  const messages = expectArray(body.messages, \"messages\");\n  const sysMessages = messages.filter(isSystemRole);\n  const rest = messages.filter((m) => !isSystemRole(m));\n  return {\n    system_chars: jsonChars(body.system) + (sysMessages.length ? jsonChars(sysMessages) : 0),\n    tools,\n    messages_chars: jsonChars(rest.length ? rest : undefined),\n  };\n}\n","sourceCodeStart":2,"sourceCodeEnd":38,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/subagent-tax/lib/analyze.mjs#L2-L38","documentation":"Thrown by expectArray() in the subagent request analyzer when a field it expects to be an array (e.g. tools, messages) is present but has a non-array type. The library deliberately fails loudly instead of coercing: an unexpectedly-shaped field must be reported as unparsed, because silently counting it as zero would publish a false measurement (e.g. \"0 tools\" that looks like a real result).","triggerScenarios":"Calling the analyzer (anthropicParts or the equivalent openai/gemini body parsers) with a captured request body where body.tools is a string, an object, or a number — for example a harness that sends tools as a single object instead of a one-element array, or messages as an object map keyed by role.","commonSituations":"Analyzing logs from a new or custom agent harness whose request shape drifted from the provider API; a partial/corrupted JSON line where the field parsed to a scalar; a schema change by the provider (e.g. tools becoming an object with a \"definitions\" wrapper).","solutions":["Inspect the offending request body (the error names the field and its typeof) and fix the producer so the field is a real array.","If the input is a corrupted log line, re-capture or repair the fixture before re-running the analyzer.","If you are adding support for a new harness shape, normalize the body to the provider's documented array shape before passing it to the analyzer — do not change expectArray to coerce."],"exampleFix":"// before\nconst body = { tools: { name: \"bash\" }, messages: [] };\nconst parts = anthropicParts(body); // throws: tools is not an array (got object)\n\n// after\nconst body = { tools: [{ name: \"bash\" }], messages: [] };\nconst parts = anthropicParts(body);","handlingStrategy":"validation","validationCode":"function hasArrayFields(body, fields) {\n  return fields.every((f) => body[f] === undefined || body[f] === null || Array.isArray(body[f]));\n}\nif (!hasArrayFields(body, [\"tools\", \"messages\", \"system\"])) {\n  // route to an 'unparsed' bucket instead of calling the analyzer\n}","typeGuard":"const isMessageArray = (v) => Array.isArray(v) && v.every((m) => m && typeof m.role === \"string\");","tryCatchPattern":"try {\n  const parts = anthropicParts(body);\n} catch (err) {\n  if (err instanceof Error && / is not an array \\(got /.test(err.message)) {\n    recordUnparsed(body); // never treat as zero — mark unparsed\n  } else throw err;\n}","preventionTips":["Validate captured request bodies against the provider's documented schema before analysis.","Keep a corpus of known-good fixture bodies in tests so shape regressions surface there first."],"tags":["validation","runtime","analytics","api-contract"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}