♻️ Reproduction Steps
- Use CopilotKit v2 with
InMemoryAgentRunner and register at least one frontend tool via useFrontendTool
- Send a message that triggers a tool call and let the run complete (the tool call should appear in the chat UI)
- Reload the page — or otherwise cause
connectAgent() to be called on an agent whose messages state already contains the completed assistant message with the tool call
- Open the browser DevTools console
✅ Expected Behavior
No console warnings. Each tool call should appear exactly once in the chat.
❌ Actual Behavior
The browser console shows:
Warning: Encountered two children with the same key, `call_uUF5OZtugHE6tyoC9PPtpAs6`.
Keys should be unique so that components maintain their identity across updates.
Non-unique keys may cause children to be duplicated and/or omitted —
the behavior is unsupported and could change in a future version.
at CopilotChatToolCallsView
at MemoizedSlotWrapper
at CopilotChatAssistantMessage
Root cause: When connectAgent() is called, InMemoryAgentRunner.connect() replays compacted historic events (including TOOL_CALL_START for completed tool calls) via @ag-ui/client's defaultApplyEvents. However, at that point the agent's messages state already contains those tool calls from the prior run. Because defaultApplyEvents pushes to message.toolCalls unconditionally — with no duplicate guard — the same toolCallId ends up in the array twice:
message.toolCalls = [
{ id: "call_abc123", ... }, // ← already in initial state
{ id: "call_abc123", ... }, // ← pushed again by replayed TOOL_CALL_START
]
CopilotChatToolCallsView then maps this array using toolCall.id as a React key, producing the duplicate-key warning.
The existing deduplicateMessages() in CopilotChatMessageView only deduplicates at the message level; it does not dedup entries within each message's toolCalls array.
Fix suggestion: In CopilotChatToolCallsView, deduplicate message.toolCalls by id before rendering:
const uniqueToolCalls = [
...new Map(message.toolCalls.map((tc) => [tc.id, tc])).values(),
];
Or equivalently, extend deduplicateMessages() to also dedup within toolCalls arrays.
Note: the underlying idempotency issue in @ag-ui/client's defaultApplyEvents has been separately reported to ag-ui-protocol/ag-ui.
Screenshots
No response
𝌚 CopilotKit Version
❯ pnpm ls | grep "@copilotkit"
├── @copilotkit/a2ui-renderer@1.55.3
├── @copilotkit/react-core@1.55.3
📄 Logs (Optional)
♻️ Reproduction Steps
InMemoryAgentRunnerand register at least one frontend tool viauseFrontendToolconnectAgent()to be called on an agent whosemessagesstate already contains the completed assistant message with the tool call✅ Expected Behavior
No console warnings. Each tool call should appear exactly once in the chat.
❌ Actual Behavior
The browser console shows:
Root cause: When
connectAgent()is called,InMemoryAgentRunner.connect()replays compacted historic events (includingTOOL_CALL_STARTfor completed tool calls) via@ag-ui/client'sdefaultApplyEvents. However, at that point the agent'smessagesstate already contains those tool calls from the prior run. BecausedefaultApplyEventspushes tomessage.toolCallsunconditionally — with no duplicate guard — the sametoolCallIdends up in the array twice:CopilotChatToolCallsViewthen maps this array usingtoolCall.idas a Reactkey, producing the duplicate-key warning.The existing
deduplicateMessages()inCopilotChatMessageViewonly deduplicates at the message level; it does not dedup entries within each message'stoolCallsarray.Fix suggestion: In
CopilotChatToolCallsView, deduplicatemessage.toolCallsbyidbefore rendering:Or equivalently, extend
deduplicateMessages()to also dedup withintoolCallsarrays.Note: the underlying idempotency issue in
@ag-ui/client'sdefaultApplyEventshas been separately reported to ag-ui-protocol/ag-ui.Screenshots
No response
𝌚 CopilotKit Version
📄 Logs (Optional)