{"record":{"id":"195f630f849822d0","repo":"mastra-ai/mastra","slug":"tool-result-must-be-preceded-by-a-tool-call","errorCode":null,"errorMessage":"tool_result must be preceded by a tool_call","messagePattern":"tool_result must be preceded by a tool_call","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client-sdks/client-js/src/resources/agent.ts","lineNumber":1577,"sourceCode":"              step,\n              ...value,\n              result,\n            } as const;\n\n            // store the result in the tool invocation\n            message.toolInvocations![message.toolInvocations!.length - 1] = invocation;\n\n            updateToolInvocationPart(value.toolCallId, invocation, (value as MaybeProviderMetadata).providerMetadata);\n\n            execUpdate();\n          }\n        }\n      },\n      onToolResultPart(value) {\n        const toolInvocations = message.toolInvocations;\n\n        if (toolInvocations == null) {\n          throw new Error('tool_result must be preceded by a tool_call');\n        }\n\n        // find if there is any tool invocation with the same toolCallId\n        // and replace it with the result\n        const toolInvocationIndex = toolInvocations.findIndex(invocation => invocation.toolCallId === value.toolCallId);\n\n        if (toolInvocationIndex === -1) {\n          throw new Error('tool_result must be preceded by a tool_call with the same toolCallId');\n        }\n\n        const invocation = {\n          ...toolInvocations[toolInvocationIndex],\n          state: 'result' as const,\n          ...value,\n        } as const;\n\n        toolInvocations[toolInvocationIndex] = invocation as ToolInvocation;\n","sourceCodeStart":1559,"sourceCodeEnd":1595,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/client-sdks/client-js/src/resources/agent.ts#L1559-L1595","documentation":"In agent.ts, onToolResultPart throws 'tool_result must be preceded by a tool_call' when a stream chunk carries a tool result but message.toolInvocations is null, meaning no earlier tool_call chunk created an invocation entry. The stream protocol requires each tool_result to correspond to a tracked tool invocation so the result can replace the pending call by toolCallId.","triggerScenarios":"Consuming a partially persisted or truncated message stream where the tool_call chunk was missed (joined mid-stream, resumed from an offset, or the server emitted results out of order); feeding historical/loaded messages through the processor without their preceding tool_call chunks.","commonSituations":"Reconnecting to a thread stream after the tool call already streamed; custom servers or middleware dropping tool_call chunks; loading stored threads whose serialization omitted toolInvocations; version mismatches where the client expects toolInvocations but the server emits a different shape.","solutions":["Ensure you consume the stream from the beginning (or from a checkpoint that includes the tool_call chunk).","Verify server/middleware isn't dropping or reordering tool_call parts.","Initialize message.toolInvocations to [] on the message object before processing chunks if you process partial streams defensively.","Check SDK/server version compatibility for the stream protocol's tool part shapes.","Guard with a null check and skip/log orphan tool results instead of throwing, if out-of-order data is acceptable in your app."],"exampleFix":"// before\nonToolResultPart(value) {\n  const toolInvocations = message.toolInvocations;\n  if (toolInvocations == null) throw new Error('tool_result must be preceded by a tool_call');\n// after (app-side guard when processing partial streams)\nif (!message.toolInvocations) { console.warn('Skipping orphan tool_result', value.toolCallId); return; }\nconst idx = message.toolInvocations.findIndex(i => i.toolCallId === value.toolCallId);","handlingStrategy":"validation","validationCode":"// before processing a partial/historical stream, ensure invocation state exists\nif (!message.toolInvocations) message.toolInvocations = [];\nconst knownCallIds = new Set(message.toolInvocations.map(i => i.toolCallId));\nif ('toolCallId' in incomingResult && !knownCallIds.has(incomingResult.toolCallId)) {\n  console.warn('tool_result without matching tool_call — stream may be partial/out of order');\n}","typeGuard":"function hasToolInvocations(m: unknown): m is { toolInvocations: Array<{ toolCallId: string }> } {\n  return !!m && typeof m === 'object' && Array.isArray((m as any).toolInvocations);\n}","tryCatchPattern":"try {\n  await consumeAgentStream(stream);\n} catch (err) {\n  if ((err as Error).message === 'tool_result must be preceded by a tool_call') {\n    console.error('Stream was joined late or tool_call chunk was dropped — restart consumption from the beginning');\n  }\n  throw err;\n}","preventionTips":["Consume streams from the first chunk or a checkpoint containing tool_call parts","Never drop or reorder tool_call chunks in middleware","Initialize toolInvocations defensively when processing partial message history","Keep client and server stream protocol versions aligned"],"tags":["streaming","protocol","tool-call","state"],"backgroundTag":"tool-result-without-tool-call","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T08:17:16.595Z"}