vercel/ai · error · ToolCallNotFoundForApprovalError

Tool call "${toolCallId}" not found for approval request "${

Error message

Tool call "${toolCallId}" not found for approval request "${approvalId}".

What it means

Each tool-approval-request in history must correspond to a tool-call part with the same toolCallId. If collectToolApprovals finds the approval request but cannot find its associated tool call, ToolCallNotFoundForApprovalError is thrown. This ensures approvals always act on an actual recorded tool call.

Source

Thrown at packages/ai/src/generate-text/collect-tool-approvals.ts:113

    if (approvalRequest == null) {
      throw new InvalidToolApprovalError({
        approvalId: approvalResponse.approvalId,
      });
    }

    const existingToolResult = toolResults[approvalRequest.toolCallId];
    if (
      existingToolResult != null &&
      (approvalResponse.approved ||
        existingToolResult.output.type !== 'execution-denied')
    ) {
      continue;
    }

    const toolCall = toolCallsByToolCallId[approvalRequest.toolCallId];
    if (toolCall == null) {
      throw new ToolCallNotFoundForApprovalError({
        toolCallId: approvalRequest.toolCallId,
        approvalId: approvalRequest.approvalId,
      });
    }

    const approval: CollectedToolApprovals<TOOLS> = {
      approvalRequest,
      approvalResponse,
      toolCall,
      ...(existingToolResult != null ? { existingToolResult } : {}),
    };

    if (approvalResponse.approved) {
      approvedToolApprovals.push(approval);
    } else {
      deniedToolApprovals.push(approval);
    }
  }

View on GitHub (pinned to 69428b1f8b)

Solutions

  1. Keep tool-call parts together with their tool-approval-request parts in persisted history
  2. Rebuild the conversation so the tool call preceding the approval request is present
  3. If history is unrecoverable, restart the tool flow from the original user turn
  4. Validate each approval request has a matching toolCallId before resuming

Example fix

// before
messages: parts.filter(p => p.type !== 'tool-call')
// after
messages: parts // retain tool-call parts referenced by approval requests
Defensive patterns

Strategy: validation

Validate before calling

const toolCallIds = new Set(messages.flatMap(m => m.parts ?? []).filter(p => p.type === 'tool-call').map(p => p.toolCallId));
const dangling = messages.flatMap(m => m.parts ?? [])
  .filter(p => p.type === 'tool-approval-request' && !toolCallIds.has(p.toolCallId));
if (dangling.length) throw new Error('Approval requests without matching tool calls');

Type guard

function hasToolCallsForApprovals(messages: Message[]): boolean {
  const calls = new Set(messages.flatMap(m => m.parts ?? []).filter(p => p.type === 'tool-call').map(p => p.toolCallId));
  return messages.flatMap(m => m.parts ?? []).filter(p => p.type === 'tool-approval-request').every(p => calls.has(p.toolCallId));
}

Try / catch

try {
  return await generateText({ messages, experimental_toolApprovals });
} catch (e) {
  if (ToolCallNotFoundForApprovalError.isInstance(e)) {
    console.error('missing tool call', e.toolCallId, 'for approval', e.approvalId);
  }
  throw e;
}

Prevention

When it happens

Trigger: Message history contains a tool-approval-request whose toolCallId has no matching tool-call part; history was pruned to remove tool calls but kept approval requests; approval request and tool call came from divergent conversation copies.

Common situations: Selective persistence of message parts (saving approvals but not raw tool calls); migrating between SDK versions where part shapes changed; manually assembling resume messages from partial logs.

Related errors


AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30). Data as JSON: /api/errors/3a12edd44a7d1b74. Report an issue: GitHub.