CopilotKit/CopilotKit · error · Error
Tool message must have a toolCallId
Error message
Tool message must have a toolCallId
What it means
Thrown when converting an AG-UI message with role "tool" into a GraphQL ResultMessage but toolCallId is falsy. ResultMessage requires actionExecutionId to correlate the tool result with the originating tool call; without it the result cannot be matched. This is a malformed tool message, not a transient failure.
Source
Thrown at packages/runtime-client-gql/src/message-conversion/agui-to-gql.ts:259
id: toolCall.id,
name: toolCall.function.name,
arguments: argumentsObj,
parentMessageId: parentMessageId,
});
}
export function aguiToolMessageToGQLResultMessage(
message: agui.Message,
toolCallNames: Record<string, string>,
): gql.ResultMessage {
if (message.role !== "tool") {
throw new Error(
`Cannot convert message with role ${message.role} to ResultMessage`,
);
}
if (!message.toolCallId) {
throw new Error("Tool message must have a toolCallId");
}
const actionName = toolCallNames[message.toolCallId] || "unknown";
// Handle result content - it could be a string or an object that needs serialization
let resultContent: string;
const messageContent = message.content || "";
if (typeof messageContent === "string") {
// Expected case: content is already a string
resultContent = messageContent;
} else if (typeof messageContent === "object" && messageContent !== null) {
// Handle case where content is an object that needs to be serialized
try {
resultContent = JSON.stringify(messageContent);
} catch (error) {
console.warn(`Failed to stringify tool result for ${actionName}:`, error);
resultContent = String(messageContent);View on GitHub (pinned to 68fbe97d87)
Solutions
- Set toolCallId on the tool message, matching the id of the corresponding ToolCall / action execution
- If consuming from another runtime, map its call-id field (tool_call_id / actionExecutionId) to toolCallId before conversion
- Validate messages against the AG-UI Message schema before conversion
Example fix
// before
const msg = { id: "1", role: "tool", content: "42" };
// after
const msg = { id: "1", role: "tool", content: "42", toolCallId: "call-abc" }; Defensive patterns
Strategy: validation
Validate before calling
if (msg.role === "tool" && !msg.toolCallId) {
console.warn("Dropping tool message without toolCallId", msg.id);
continue;
} Type guard
function isCompleteToolMessage(m: agui.Message): m is Extract<agui.Message, { role: "tool"; toolCallId: string }> {
return m.role === "tool" && typeof m.toolCallId === "string" && m.toolCallId.length > 0;
} Try / catch
try {
const rm = aguiToolMessageToGQLResultMessage(msg, names);
} catch (e) {
if (e instanceof Error && e.message.includes("toolCallId")) { /* drop or re-link message */ }
else throw e;
} Prevention
- Always pair tool results with the originating ToolCall id
- Validate inbound messages against the AG-UI schema before conversion
When it happens
Trigger: A tool message object created without a toolCallId (e.g. { role: "tool", content: "..." }); toolCallId set to empty string, undefined, or null; deserializing AG-UI messages from JSON where the field was dropped or camelCased incorrectly.
Common situations: Hand-built tool result messages in custom agent bridges; LLM/agent runtimes that emit tool outputs without echoing the call ID; data migration between message schemas that renamed toolCallId (e.g. tool_call_id).
Related errors
- Cannot convert message with role ${message.role} to ResultMe
- Cannot convert message to ImageMessage: missing format or by
- Cannot convert message with role ${message.role} to ImageMes
- Unknown message type
- Unsupported tool call type: ${toolCall.type}
AI-assisted analysis of CopilotKit/CopilotKit@68fbe97d87 (2026-08-27).
Data as JSON: /api/errors/8e9318aa665e8c28.
Report an issue: GitHub.