{"record":{"id":"64658eff8194a02e","repo":"moeru-ai/airi","slug":"only-assistant-messages-can-invoke-tools","errorCode":null,"errorMessage":"Only assistant messages can invoke tools","messagePattern":"Only assistant messages can invoke tools","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/core-agent/src/runtime/responses.ts","lineNumber":59,"sourceCode":"    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')\n        throw new Error('Only assistant messages can invoke tools')\n      flush()\n      items.push({ type: 'function_call', call_id: segment.callId, name: segment.name, arguments: segment.arguments })\n    }\n    else if (segment.type === 'tool-result') {\n      flush()\n      const content = segment.content.map(inputPart)\n      items.push({ type: 'function_call_output', call_id: segment.callId, output: content.every(part => part.type === 'input_text') ? content.map(part => part.text).join('') : content })\n    }\n    else {\n      parts.push(segment)\n    }\n  }\n  flush()\n  return items\n}\n\nfunction renderConversation(conversation: Conversation, scope: string): ItemParam[] {\n  return conversation.turns.flatMap((turn) => {","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/packages/core-agent/src/runtime/responses.ts#L41-L77","documentation":"The Responses adapter requires that a segment of type 'tool-call' (rendered as a Responses `function_call` item) appears inside an assistant-role message. OpenAI's Responses API only allows the model/assistant to emit function calls, so a non-assistant message containing a tool-call segment is invalid and this library throws at renderMessage time.","triggerScenarios":"A ProjectionEntry whose role is 'user', 'tool', 'system', 'context', 'event', or 'summary' contains a segment with type 'tool-call'. Occurs when messages are constructed or merged manually, when transcript import mis-attributes a tool call to the user/system role, or when projection logic (projectInput/projectRound) maps a turn into the wrong role while keeping its segments.","commonSituations":"Importing chat logs where tool calls were stored under a generic role; custom projection code that flattens rounds and loses the assistant ownership of tool-call segments; buggy turn merging that concatenates a user message with an assistant tool call.","solutions":["Move the 'tool-call' segment into an assistant-role message; keep user/system messages free of tool-call segments.","If the segment is actually the outcome of the call, use type 'tool-result' (with the matching callId) in the tool-role message instead of 'tool-call'.","Fix the projection code (projectInput/projectRound or your Conversation builder) so assistant tool calls retain role 'assistant'.","If importing history, re-emit the call as an assistant message followed by a tool message containing the correlated 'tool-result' segment."],"exampleFix":"// before: tool call attached to a user message\n{ id: 'm1', role: 'user', segments: [{ type: 'tool-call', callId: 'c1', name: 'getWeather', arguments: '{}' }] }\n\n// after: assistant owns the call\n{ id: 'm1', role: 'assistant', segments: [{ type: 'tool-call', callId: 'c1', name: 'getWeather', arguments: '{}' }] }","handlingStrategy":"validation","validationCode":"function assertToolCallsAreAssistant(conversation) {\n  for (const turn of conversation.turns) {\n    const entries = turn.type !== 'assistant' ? [projectInput(turn)] : turn.rounds.flatMap(projectRound)\n    for (const m of entries) {\n      if (m.role !== 'assistant' && m.segments.some(s => s.type === 'tool-call'))\n        throw new Error(`tool-call segment on non-assistant message (role=${m.role})`)\n    }\n  }\n}","typeGuard":"const hasToolCall = (m) => m.segments.some(s => s.type === 'tool-call')\nconst renderable = (m) => !hasToolCall(m) || m.role === 'assistant'","tryCatchPattern":"try {\n  await streamResponses({ config, conversation, scope, onEvent })\n} catch (e) {\n  if (e.message === 'Only assistant messages can invoke tools') {\n    // re-attribute tool-call segments to an assistant message, then retry once\n  } else throw e\n}","preventionTips":["Treat tool-call segments as assistant-owned by construction; have your conversation builder enforce it at insertion time.","When merging or flattening messages, split tool-call segments out into their own assistant message.","Test imported transcripts through renderConversation before sending them to the API."],"tags":["responses-api","tool-calls","conversation-projection"],"backgroundTag":"invalid-state-transition","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"}