{"record":{"id":"2b878191013d864b","repo":"moeru-ai/airi","slug":"tool-messages-require-a-correlated-tool-result-responses","errorCode":null,"errorMessage":"Tool messages require a correlated tool result","messagePattern":"Tool messages require a correlated tool result","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/core-agent/src/runtime/responses.ts","lineNumber":40,"sourceCode":"    case 'image': return { type: 'input_image', image_url: segment.url, detail: segment.detail }\n    case 'file':\n      if (segment.providerFileId)\n        throw new Error('Responses file references require native input Items')\n      return { type: 'input_file', file_data: segment.data, filename: segment.name, file_url: segment.url }\n    case 'audio': throw new Error('This Responses adapter does not support audio input')\n    default: return { type: 'input_text', text: renderSegmentText(segment) }\n  }\n}\n\nfunction renderMessage(message: ProjectionEntry): ItemParam[] {\n  const items: ItemParam[] = []\n  const role = message.role === 'context' || message.role === 'event' || message.role === 'summary' ? 'user' : message.role\n  let parts: MessageSegment[] = []\n  function flush() {\n    if (!parts.length)\n      return\n    if (role === 'tool')\n      throw new Error('Tool messages require a correlated tool result')\n    if (role === 'assistant') {\n      const content = parts.map(part => part.type === 'refusal'\n        ? { type: 'refusal' as const, refusal: part.text }\n        : { type: 'output_text' as const, text: renderSegmentText(part) })\n      items.push({ type: 'message', role, content })\n    }\n    else if (role === 'user') {\n      const content = parts.map(inputPart)\n      items.push({ type: 'message', role, content: content.every(part => part.type === 'input_text') ? content.map(part => part.text).join('') : content })\n    }\n    else {\n      items.push({ type: 'message', role, content: parts.map(part => ({ type: 'input_text', text: renderSegmentText(part) })) })\n    }\n    parts = []\n  }\n  for (const segment of message.segments) {\n    if (segment.type === 'tool-call') {\n      if (role !== 'assistant')","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/packages/core-agent/src/runtime/responses.ts#L22-L58","documentation":"During Responses-protocol projection, a message with role 'tool' accumulated plain text/image parts (flush was called with pending parts) instead of only containing tool-result segments. The OpenAI Responses API represents tool outputs as `function_call_output` items correlated by `call_id`, not as plain tool-role messages. This library throws because a tool-role text message cannot be represented as a valid Responses item.","triggerScenarios":"Passing a Conversation/ProjectionEntry with role 'tool' whose segments include text/image segments rather than a single tool-result segment (renderMessage -> flush with role 'tool' and non-empty parts). Typically happens when hand-constructed messages use role 'tool' without the tool-result segment type, or after a version change in the internal message model where tool outputs moved into segments of type 'tool-result'.","commonSituations":"Migrating from a chat-completions-style history (role:'tool' with string content) into the core-agent Conversation model; importing persisted transcripts produced by an older runtime; manually appending tool messages to a Conversation instead of using the tool-call/tool-result segment pairs created by readOutput.","solutions":["Represent the tool output as a message with a segment of type 'tool-result' (with callId matching the preceding assistant 'tool-call' segment) instead of plain text segments on a 'tool' role message.","If the message is really contextual text, change its role to 'user' (or 'context'/'event'/'summary', which project to 'user') so flush renders it as an input_text message.","Drop or transform the offending tool message before calling streamResponses, e.g. convert its text into a user message or remove the orphaned tool output.","Check the source of the Conversation data: if it comes from persisted state, re-project it through readOutput/rendering so segments use the current model."],"exampleFix":"// before: plain text on a tool-role message\n{ id: 'm1', role: 'tool', segments: [{ type: 'text', text: '42' }] }\n\n// after: correlated tool-result segment\n{ id: 'm1', role: 'tool', segments: [{ type: 'tool-result', callId: 'call_1', content: [{ type: 'text', text: '42' }] }] }","handlingStrategy":"validation","validationCode":"function isValidToolMessage(m) {\n  return m.role !== 'tool' || m.segments.every(s => s.type === 'tool-result')\n}\nconversation.turns.flatMap(t => (t.type !== 'assistant' ? [projectInput(t)] : t.rounds.flatMap(projectRound))).forEach(m => { if (!isValidToolMessage(m)) throw new Error('tool message must only contain tool-result segments') })","typeGuard":"const isToolResultSegment = (s) => s.type === 'tool-result'\nconst hasPlainToolText = (m) => m.role === 'tool' && m.segments.some(s => s.type !== 'tool-result')","tryCatchPattern":"try {\n  await streamResponses({ config, conversation, scope, onEvent })\n} catch (e) {\n  if (e.message === 'Tool messages require a correlated tool result') {\n    // re-project or drop the offending tool message before retrying\n  } else throw e\n}","preventionTips":["Always build tool outputs through the runtime's tool-result segment API, never as raw role:'tool' text messages.","Keep callId pairing between tool-call and tool-result segments when constructing conversations.","When importing history from chat-completions formats, convert role:'tool' string content into tool-result segments.","Add a unit test that renders any conversation you persist through renderConversation."],"tags":["responses-api","conversation-projection","tool-messages"],"backgroundTag":"invalid-argument-value","analyzedSha":"438a067dde47aa0bdb46c2323d1fe293dc805218","analyzedAt":"2026-09-17T01:14:42.644Z","contentChangedAt":"2026-09-17T01:14:42.644Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}