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

  1. Set toolCallId on the tool message, matching the id of the corresponding ToolCall / action execution
  2. If consuming from another runtime, map its call-id field (tool_call_id / actionExecutionId) to toolCallId before conversion
  3. 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

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


AI-assisted analysis of CopilotKit/CopilotKit@68fbe97d87 (2026-08-27). Data as JSON: /api/errors/8e9318aa665e8c28. Report an issue: GitHub.