Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
title: Fix duplicate messages with addToolApprovalResponse
description: Prevent duplicate assistant messages when using addToolApprovalResponse for tools requiring approval
products:
- agents
- workers
date: 2026-02-02
---

Fixed a bug where using `addToolApprovalResponse` with tools that have `needsApproval` would create duplicate assistant messages in the conversation history.

## What changed

When a user approved a tool via `addToolApprovalResponse` and then called `sendMessage`, two assistant messages were persisted with the same content but different IDs:

- Original message with server-generated ID stuck in pending state
- New message with client-generated ID in completed state

The fix ensures that `addToolApprovalResponse` now notifies the server to update the message in place using the existing ID, preventing duplicates when `sendMessage` is called afterward.

## How it works

The `addToolApprovalResponse` function now automatically:

1. Sends a `CF_AGENT_TOOL_APPROVAL` message to the server with the tool call ID and approval decision
2. Server updates the existing message state from `approval-requested` to `approval-responded`
3. Message is updated in place with the existing server-generated ID
4. When `sendMessage` is called next, no duplicate is created

No code changes are required in your application - the fix is automatic when using `addToolApprovalResponse` from the `useAgentChat` hook.

Refer to the [Human-in-the-Loop guide](/agents/guides/human-in-the-loop/) for examples of using tool approvals in your agents.
76 changes: 76 additions & 0 deletions src/content/docs/agents/api-reference/agents-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,14 @@ function useAgentChat(options: UseAgentChatOptions): {
toolCallId: string;
result: any;
}) => void;
// Respond to a tool approval request (for tools with needsApproval)
addToolApprovalResponse: ({
id,
approved,
}: {
id: string;
approved: boolean;
}) => void;
// Current error if any
error: Error | undefined;
};
Expand Down Expand Up @@ -1074,6 +1082,74 @@ function ChatInterface() {

</TypeScriptExample>

#### Tool approval handling

The `useAgentChat` hook provides `addToolApprovalResponse` for handling tool approval workflows. This function is used with tools that have `needsApproval` set to `true`.

When a tool requires approval:

1. The tool part will have `state: "approval-requested"` or `state: "input-available"`
2. Display approval UI to the user (Approve/Reject buttons)
3. Call `addToolApprovalResponse` with the approval decision
4. The server updates the message in place, preventing duplicate messages

<TypeScriptExample>

```tsx
import { useAgentChat } from "agents/ai-react";
import { useAgent } from "agents/react";
import { isToolUIPart } from "ai";

function ChatWithApproval() {
const agent = useAgent({ agent: "my-agent" });
const { messages, addToolApprovalResponse } = useAgentChat({ agent });

return (
<div>
{messages.map((message) =>
message.parts?.map((part) => {
if (isToolUIPart(part) && part.state === "input-available") {
// Tool needs approval - get the approval ID from the part
const approvalId = part.approval?.id;

return (
<div key={part.toolCallId}>
<p>Approve {part.type}?</p>
<button
onClick={() =>
addToolApprovalResponse({
id: approvalId,
approved: true,
})
}
>
Approve
</button>
<button
onClick={() =>
addToolApprovalResponse({
id: approvalId,
approved: false,
})
}
>
Reject
</button>
</div>
);
}
return null;
}),
)}
</div>
);
}
```

</TypeScriptExample>

The `addToolApprovalResponse` function automatically notifies the server before updating local state. This ensures that when you call other functions like `sendMessage` afterward, duplicate messages are prevented because the server has already updated the message with the existing ID.

### Next steps

- [Build a chat Agent](/agents/getting-started/build-a-chat-agent/) using the Agents SDK and deploy it to Workers.
Expand Down
2 changes: 2 additions & 0 deletions src/content/docs/agents/guides/human-in-the-loop.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@
inputSchema: z.object({ location: z.string() }),
execute: async ({ location }) => {
console.log(`Getting local time for ${location}`);
await new Promise((res) => setTimeout(res, 2000));

Check warning on line 131 in src/content/docs/agents/guides/human-in-the-loop.mdx

View workflow job for this annotation

GitHub Actions / Semgrep

semgrep.style-guide-potential-date-year

Potential year found. Documentation should strive to represent universal truth, not something time-bound. (add [skip style guide checks] to commit message to skip)

Check warning on line 131 in src/content/docs/agents/guides/human-in-the-loop.mdx

View workflow job for this annotation

GitHub Actions / Semgrep

semgrep.style-guide-potential-date-year

Potential year found. Documentation should strive to represent universal truth, not something time-bound. (add [skip style guide checks] to commit message to skip)
return "10am";
}
});
Expand All @@ -139,7 +139,7 @@
inputSchema: z.object({ location: z.string() }),
execute: async ({ location }) => {
console.log(`Getting local news for ${location}`);
await new Promise((res) => setTimeout(res, 2000));

Check warning on line 142 in src/content/docs/agents/guides/human-in-the-loop.mdx

View workflow job for this annotation

GitHub Actions / Semgrep

semgrep.style-guide-potential-date-year

Potential year found. Documentation should strive to represent universal truth, not something time-bound. (add [skip style guide checks] to commit message to skip)

Check warning on line 142 in src/content/docs/agents/guides/human-in-the-loop.mdx

View workflow job for this annotation

GitHub Actions / Semgrep

semgrep.style-guide-potential-date-year

Potential year found. Documentation should strive to represent universal truth, not something time-bound. (add [skip style guide checks] to commit message to skip)
return `${location} kittens found drinking tea this last weekend`;
}
});
Expand Down Expand Up @@ -540,6 +540,8 @@
4. **User decision**: The user reviews the action and makes a decision.
5. **Execution or rejection**: Based on the user's choice, the tool either executes or returns a rejection message.

When using `addToolResult` for tool approvals (lines 435-439 and 447-455), the approval response is sent to the server, which updates the message state in place. This prevents duplicate messages from being created when the conversation continues, ensuring each tool call has a single associated message with a consistent ID.

### Message streaming with confirmations

The agent uses the Vercel AI SDK's streaming capabilities:
Expand Down