Description
Running into an issue with missing tool_result blocks after approving / denying a tool use with approval.
Error: undefined: The model returned the following errors: messages.2: tool_use ids were found without tool_result blocks immediately after: tooluse_HgfwsaZXS-qWznevI6y_kw. Each tool_use block must have a corresponding tool_result block in the next message.
Client code for approval:
// Within handleApprove
await addToolApprovalResponse({
id: approvalId,
approved: true,
reason: `Applied ${input.operations.length} operation(s): ${input.reasoning}`,
});
// JSX
// Handle tool invocations for update_current_theme_tokens
if (part.type === "tool-update_current_theme_tokens") {
return (
<div key={`${message.id}-tool-${index}`} className="mt-2 space-y-1">
{part.input?.operations?.map((op, opIndex) => (
<TokenChangePreview key={opIndex} operation={op} />
))}
{part.state === "approval-requested" && (
<div className="flex gap-1.5 mt-1.5">
<button
type="button"
onClick={() => handleApprove(part.approval.id, part.input)}
className="flex items-center gap-1 px-2 py-1 text-[9px] font-medium bg-green-500 text-white rounded hover:bg-green-600 transition-colors"
>
<Check className="h-2.5 w-2.5" />
Apply
</button>
<button
type="button"
onClick={() => handleDeny(part.approval.id)}
className="flex items-center gap-1 px-2 py-1 text-[9px] font-medium bg-[#e5e5e5] text-[#666] rounded hover:bg-[#d5d5d5] transition-colors"
>
<XIcon className="h-2.5 w-2.5" />
Reject
</button>
</div>
)}
{part.state === "output-available" && (
<div className="text-[9px] text-green-600">✓ Applied</div>
)}
{part.state === "output-denied" && (
<div className="text-[9px] text-red-600">✗ Rejected</div>
)}
</div>
);
}
Server code:
const result = streamText({
model: bedrock("global.anthropic.claude-opus-4-5-20251101-v1:0"),
system: systemPrompt,
messages: convertToModelMessages(processedMessages),
tools: {
update_current_theme_tokens: updateCurrentThemeTokensTool,
},
// Allow up to 3 tool execution steps
stopWhen: stepCountIs(3),
});
return result.toUIMessageStreamResponse({
originalMessages: processedMessages,
});
Right now my workaround is pre-processing the messages on the server:
/**
* Process messages to handle tool approval responses.
* When a tool has needsApproval: true, the client sends back an approval response
* but we need to inject the actual tool output for the model to continue.
*/
function processToolApprovalResponses(
messages: ThemeStudioUIMessage[],
): ThemeStudioUIMessage[] {
// Deep clone messages to avoid mutation issues
const clonedMessages = JSON.parse(
JSON.stringify(messages),
) as ThemeStudioUIMessage[];
// Process all messages to ensure all approval responses have outputs
for (const message of clonedMessages) {
if (message.role !== "assistant" || !message.parts) {
continue;
}
for (let i = 0; i < message.parts.length; i++) {
const part = message.parts[i];
// Only process tool UI parts (they have a type starting with "tool-")
if (
!part ||
typeof part.type !== "string" ||
!part.type.startsWith("tool-")
) {
continue;
}
// Cast to access tool-specific properties
const toolPart = part as {
type: string;
state?: string;
approval?: { id: string; approved?: boolean };
output?: unknown;
input?: { operations?: unknown[]; reasoning?: string };
};
// Check if this is an approval-responded state without an output
const hasApprovalResponse =
toolPart.state === "approval-responded" ||
(toolPart.approval && "approved" in toolPart.approval);
if (!hasApprovalResponse) {
continue;
}
// If it already has an output, don't overwrite it
if (toolPart.output !== undefined) {
continue;
}
// Generate the tool output based on approval status
const wasApproved = toolPart.approval?.approved === true;
const operationCount = toolPart.input?.operations?.length ?? 0;
if (wasApproved) {
// Tool was approved - provide success output
toolPart.state = "output-available";
toolPart.output = {
success: true,
message: `Applied ${operationCount} token operation(s)${toolPart.input?.reasoning ? `: ${toolPart.input.reasoning}` : ""}`,
};
} else {
// Tool was denied - provide error output
toolPart.state = "output-available";
toolPart.output = {
success: false,
message: "User denied the token changes",
};
}
}
}
return clonedMessages;
}
Which doesn't seem like the right approach
AI SDK Version
Code of Conduct
Description
Running into an issue with missing tool_result blocks after approving / denying a tool use with approval.
Error: undefined: The model returned the following errors: messages.2:
tool_useids were found withouttool_resultblocks immediately after: tooluse_HgfwsaZXS-qWznevI6y_kw. Eachtool_useblock must have a correspondingtool_resultblock in the next message.Client code for approval:
Server code:
Right now my workaround is pre-processing the messages on the server:
Which doesn't seem like the right approach
AI SDK Version
Code of Conduct