moeru-ai/airi · error · Error

Assistant message does not contain tool call "${payload.tool

Error message

Assistant message does not contain tool call "${payload.toolCallId}" for "${payload.toolName}".

What it means

The target assistant message exists but contains no tool call matching payload.toolCallId and payload.toolName. hasMatchingToolCall compares ids stored on the assistant message; replaceToolCallResult needs an exact match to splice the new result in, so a mismatch aborts the rerun before any tool executes.

Source

Thrown at packages/stage-ui/src/stores/tool-call-rerun.ts:88

/**
 * Re-executes a stored tool call with supplied arguments and returns updated chat history.
 *
 * The resolver is injected so callers can choose the runtime-specific tool list
 * without coupling this helper to app-local stores, Electron IPC, or browser state.
 */
export async function executeToolCallRerun<TToolset extends string = string>(
  options: ExecuteToolCallRerunOptions<TToolset>,
): Promise<ChatHistoryItem[]> {
  const { messages, payload } = options
  const targetIndex = findTargetMessageIndex(messages, payload)
  const targetMessage = messages[targetIndex]

  if (targetMessage?.role !== 'assistant')
    throw new Error('Tool call rerun target must be an assistant message.')

  if (!hasMatchingToolCall(targetMessage, payload))
    throw new Error(`Assistant message does not contain tool call "${payload.toolCallId}" for "${payload.toolName}".`)

  const replaceTargetMessage = (result: ToolCallResultInput) => messages.map((item, itemIndex) => {
    if (itemIndex !== targetIndex)
      return item

    return replaceToolCallResult(targetMessage, result)
  })

  const tools = await options.resolveTools()
  const tool = tools.find(candidate => toolNameFrom(candidate) === payload.toolName)
  if (tool == null) {
    return replaceTargetMessage({
      id: payload.toolCallId,
      isError: true,
      result: `Tool "${payload.toolName}" is not available for rerun in this runtime.`,
    })
  }

View on GitHub (pinned to 677329427f)

Solutions

  1. Refresh history and trigger the rerun from the current assistant message's tool-call block.
  2. Ensure the UI passes the toolCallId exactly as stored (no trimming or re-casing).
  3. If messages are transformed, preserve toolCallId values or invalidate rerun actions.
  4. Log the tool call ids present on the target message versus the payload to confirm the mismatch.
Defensive patterns

Strategy: type-guard

Validate before calling

const target = messages[findTargetMessageIndex(messages, payload)]
const hasCall = !!target && target.role === 'assistant'
  && listToolCallIds(target).includes(payload.toolCallId)
if (!hasCall) {
  // hide/disable the rerun action
}

Type guard

function assistantHasToolCall(m: ChatHistoryItem | undefined, toolCallId: string): m is ChatHistoryItem & { role: 'assistant' } {
  return !!m && m.role === 'assistant' && listToolCallIds(m).includes(toolCallId)
}

Try / catch

catch (e) {
  const msg = errorMessageFrom(e) ?? ''
  if (msg.includes('does not contain tool call'))
    await reloadHistory()
  else
    throw e
}

Prevention

When it happens

Trigger: The assistant message was regenerated so its tool call got a new id while the payload kept the old one; streaming stopped mid-response leaving the message without tool-call parts; history sync or merge dropped/rekeyed tool call ids.

Common situations: Clicking a rerun affordance rendered from a pre-regeneration snapshot; providers emitting different toolCallId formats across versions; history dedupe rewriting ids.

Related errors


AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18). Data as JSON: /api/errors/3f68beb29545691c. Report an issue: GitHub.