moeru-ai/airi · error · Error
Tool messages require a correlated tool result
Error message
Tool messages require a correlated tool result
What it means
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.
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.
Example fix
// before: plain text on a tool-role message
{ id: 'm1', role: 'tool', segments: [{ type: 'text', text: '42' }] }
// after: correlated tool-result segment
{ id: 'm1', role: 'tool', segments: [{ type: 'tool-result', callId: 'call_1', content: [{ type: 'text', text: '42' }] }] } Defensive patterns
Strategy: validation
Validate before calling
function isValidToolMessage(m) {
return m.role !== 'tool' || m.segments.every(s => s.type === 'tool-result')
}
conversation.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') }) Type guard
const isToolResultSegment = (s) => s.type === 'tool-result' const hasPlainToolText = (m) => m.role === 'tool' && m.segments.some(s => s.type !== 'tool-result')
Try / catch
try {
await streamResponses({ config, conversation, scope, onEvent })
} catch (e) {
if (e.message === 'Tool messages require a correlated tool result') {
// re-project or drop the offending tool message before retrying
} else throw e
} Prevention
- 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.
When it happens
Trigger: 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'.
Common situations: 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.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Only assistant messages can invoke tools
- Responses continuation must contain an item array
- Responses file requires exactly one source
- Responses image output requires a URL
- Tool messages require a correlated tool result
AI-assisted analysis of moeru-ai/airi@438a067dde (2026-09-17).
Data as JSON: /api/errors/2b878191013d864b.
Report an issue: GitHub.
Appendix: source
Thrown at packages/core-agent/src/runtime/responses.ts:40
case 'image': return { type: 'input_image', image_url: segment.url, detail: segment.detail }
case 'file':
if (segment.providerFileId)
throw new Error('Responses file references require native input Items')
return { type: 'input_file', file_data: segment.data, filename: segment.name, file_url: segment.url }
case 'audio': throw new Error('This Responses adapter does not support audio input')
default: return { type: 'input_text', text: renderSegmentText(segment) }
}
}
function renderMessage(message: ProjectionEntry): ItemParam[] {
const items: ItemParam[] = []
const role = message.role === 'context' || message.role === 'event' || message.role === 'summary' ? 'user' : message.role
let parts: MessageSegment[] = []
function flush() {
if (!parts.length)
return
if (role === 'tool')
throw new Error('Tool messages require a correlated tool result')
if (role === 'assistant') {
const content = parts.map(part => part.type === 'refusal'
? { type: 'refusal' as const, refusal: part.text }
: { type: 'output_text' as const, text: renderSegmentText(part) })
items.push({ type: 'message', role, content })
}
else if (role === 'user') {
const content = parts.map(inputPart)
items.push({ type: 'message', role, content: content.every(part => part.type === 'input_text') ? content.map(part => part.text).join('') : content })
}
else {
items.push({ type: 'message', role, content: parts.map(part => ({ type: 'input_text', text: renderSegmentText(part) })) })
}
parts = []
}
for (const segment of message.segments) {
if (segment.type === 'tool-call') {
if (role !== 'assistant')View on GitHub (pinned to 438a067dde)