{"record":{"id":"07e4cc59599ca876","repo":"Budibase/budibase","slug":"ai-message-content-must-be-a-string-07e4cc","errorCode":null,"errorMessage":"AI message content must be a string","messagePattern":"AI message content must be a string","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/server/src/sdk/workspace/ai/llm/messages.ts","lineNumber":12,"sourceCode":"import type { Message } from \"@budibase/types\"\nimport type { ModelMessage } from \"ai\"\n\nexport function toPrompt(messages: Message[]): {\n  instructions?: string\n  messages: ModelMessage[]\n} {\n  const instructions: string[] = []\n  const modelMessages: ModelMessage[] = []\n  for (const message of messages) {\n    if (typeof message.content !== \"string\") {\n      throw new Error(\"AI message content must be a string\")\n    }\n    if (message.role === \"tool\") {\n      throw new Error(\"AI tool messages are not supported\")\n    }\n    if (message.role === \"system\") {\n      instructions.push(message.content)\n    } else {\n      modelMessages.push({\n        role: message.role,\n        content: message.content,\n      } as ModelMessage)\n    }\n  }\n  return {\n    instructions: instructions.join(\"\\n\\n\") || undefined,\n    messages: modelMessages,\n  }\n}","sourceCodeStart":1,"sourceCodeEnd":30,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/sdk/workspace/ai/llm/messages.ts#L1-L30","documentation":"toPrompt converts Budibase AI chat messages into provider model messages, building a system instruction list and a modelMessages array. It enforces that every message's content is a plain string and rejects tool-role messages because the prompt pipeline only supports text conversation, not tool-call transcripts. Passing structured (array/parts) content or tool results therefore throws this Error.","triggerScenarios":"Calling toPrompt with a messages array where any element has non-string content (e.g. AI SDK ModelMessage with array-of-parts content), or where any element has role \"tool\" (tool result messages from a prior tool-calling conversation).","commonSituations":"Feeding messages captured from a Vercel AI SDK / provider response back into toPrompt — those often carry array content blocks or tool results; persisting and replaying chat history that includes tool-call turns; a schema change in the message type from string content to parts.","solutions":["Flatten any non-string content into a plain string before calling toPrompt (e.g. join text parts).","Filter or convert role \"tool\" messages out of the array before calling toPrompt.","Only pass messages you constructed with string content; keep tool transcripts out of the prompt path.","If tool support is needed, use the lower-level LLM API instead of toPrompt."],"exampleFix":"// before\ntoPrompt(history.messages as ModelMessage[])\n// after\nconst msgs = history.messages\n  .filter(m => m.role !== \"tool\")\n  .map(m => ({ ...m, content: typeof m.content === \"string\" ? m.content : JSON.stringify(m.content) }))\ntoPrompt(msgs)","handlingStrategy":"validation","validationCode":"const isPromptSafe = (msgs) => msgs.every(m => typeof m.content === \"string\" && m.role !== \"tool\")\nif (!isPromptSafe(messages)) throw new Error(\"Messages must have string content and no tool role before toPrompt\")","typeGuard":"const isTextMessage = (m: ModelMessage): m is ModelMessage & { content: string } =>\n  typeof m.content === \"string\"","tryCatchPattern":"try {\n  const prompt = toPrompt(messages)\n} catch (e) {\n  if (e.message.includes(\"content must be a string\") || e.message.includes(\"tool messages\")) {\n    messages = messages.filter(m => m.role !== \"tool\").map(m => ({ ...m, content: typeof m.content === \"string\" ? m.content : JSON.stringify(m.content) }))\n    return toPrompt(messages)\n  }\n  throw e\n}","preventionTips":["Always normalize provider responses (array content parts) to string content before building prompts","Strip tool-role messages from any history you replay into toPrompt","Type your history as messages with string content so TypeScript flags incompatible shapes","Add a unit test that runs toPrompt over your persisted history format"],"tags":["ai","validation","messages"],"backgroundTag":"ai-message-content-not-string","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}