{"record":{"id":"bceea9b54e454f83","repo":"vercel/ai","slug":"no-tool-invocation-found-for-tool-call-id-toolc","errorCode":null,"errorMessage":"No tool invocation found for tool call ID \"${toolCallId}\".","messagePattern":"No tool invocation found for tool call ID \"(.+?)\"\\.","errorType":"exception","errorClass":"UIMessageStreamError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/ui/process-ui-message-stream.ts","lineNumber":149,"sourceCode":"\n            let toolInvocation = toolInvocations.find(\n              invocation => invocation.toolCallId === toolCallId,\n            );\n\n            if (toolInvocation == null) {\n              const parts = state.message.parts;\n\n              for (let i = parts.length - 1; i >= 0; i--) {\n                const part = parts[i];\n                if (isToolUIPart(part) && part.toolCallId === toolCallId) {\n                  toolInvocation = part;\n                  break;\n                }\n              }\n            }\n\n            if (toolInvocation == null) {\n              throw new UIMessageStreamError({\n                chunkType: 'tool-invocation',\n                chunkId: toolCallId,\n                message: `No tool invocation found for tool call ID \"${toolCallId}\".`,\n              });\n            }\n\n            return toolInvocation;\n          }\n\n          function getToolInvocationByApprovalId(approvalId: string) {\n            const toolInvocations = state.message.parts.filter(isToolUIPart);\n\n            const toolInvocation = toolInvocations.find(\n              invocation => invocation.approval?.id === approvalId,\n            );\n\n            if (toolInvocation == null) {\n              throw new UIMessageStreamError({","sourceCodeStart":131,"sourceCodeEnd":167,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/ai/src/ui/process-ui-message-stream.ts#L131-L167","documentation":"In process-ui-message-stream.ts, when a tool-result or related chunk arrives, getToolInvocation looks up the existing tool invocation by toolCallId in the message's toolInvocations. If none matches, it throws a UIMessageStreamError. The stream references a tool call that was never started (no tool-input-start / tool call part created first).","triggerScenarios":"Consuming a UI message stream where a 'tool-input-available'/'tool-result' chunk carries a toolCallId that has no matching tool invocation in the message state — e.g. manually constructed streams, stream resumed mid-way dropping earlier chunks, or server sending results for calls it never announced.","commonSituations":"Custom hand-rolled stream writers on the server emitting tool results without tool-input-start; resuming a stream where the original tool-start chunks were already consumed; stream chunk loss in persistence/replay; mismatched toolCallIds after server-side regeneration.","solutions":["Ensure the server always sends a 'tool-input-start' (tool call announcement) chunk before any tool-input-delta or tool-result chunks for that toolCallId.","Verify toolCallIds are consistent between the stream chunks and any persisted message history being appended to.","When resuming, replay or include the earlier tool-start chunks so state contains the invocation.","Catch UIMessageStreamError in the stream processing and skip/handle orphan tool-result chunks."],"exampleFix":"// before (server custom stream)\nwriter.write({ type: 'tool-result', toolCallId: 'call_1', result });\n// after\nwriter.write({ type: 'tool-input-start', toolCallId: 'call_1', toolName: 'getWeather' });\nwriter.write({ type: 'tool-input-available', toolCallId: 'call_1', input });\nwriter.write({ type: 'tool-result', toolCallId: 'call_1', result });","handlingStrategy":"validation","validationCode":"const exists = message.parts.some(\n  p => p.type === 'tool' && p.toolCallId === toolCallId,\n);\nif (!exists) throw new Error(`Cannot emit result: unknown toolCallId ${toolCallId}`);","typeGuard":"function hasToolInvocation(msg: UIMessage, toolCallId: string): boolean {\n  return msg.parts.some(\n    (p): p is Extract<UIMessage['parts'][number], { type: 'tool' }> =>\n      p.type === 'tool' && p.toolCallId === toolCallId,\n  );\n}","tryCatchPattern":"try {\n  await processUIMessageStream(...);\n} catch (e) {\n  if (UIMessageStreamError.isInstance?.(e) || (e instanceof Error && /No tool invocation found for tool call ID/.test(e.message))) {\n    console.warn('Orphan tool result chunk skipped', e.message);\n  } else throw e;\n}","preventionTips":["Always emit tool-input-start before tool results in custom stream writers.","Reuse the writer helpers from the SDK instead of hand-writing raw chunks.","Persist and replay the full chunk sequence including start chunks on resume.","Generate toolCallIds in one place to avoid mismatches."],"tags":["streaming","tool-calls","ui-message-stream","state-mismatch"],"backgroundTag":"missing-stream-chunk","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}