@copilotkit/runtime-client-gql
Version:
<img src="https://github.com/user-attachments/assets/0a6b64d9-e193-4940-a3f6-60334ac34084" alt="banner" style="border-radius: 12px; border: 2px solid #d6d4fa;" />
356 lines (319 loc) • 10.5 kB
text/typescript
import * as gql from "../client";
import type { MessageRole } from "../graphql/@generated/graphql";
import type * as agui from "@copilotkit/shared"; // named agui for clarity, but this only includes agui message types
// Helper function to extract agent name from message
function extractAgentName(message: agui.Message): string {
if (message.role !== "assistant") {
throw new Error(
`Cannot extract agent name from message with role ${message.role}`,
);
}
return message.agentName || "unknown";
}
// Type guard for agent state message
function isAgentStateMessage(message: agui.Message): boolean {
return (
message.role === "assistant" && "agentName" in message && "state" in message
);
}
// Type guard for messages with image property
function hasImageProperty(
message: agui.Message,
): message is agui.Message & { image: agui.ImageData } {
const canContainImage =
message.role === "assistant" || message.role === "user";
if (!canContainImage) {
return false;
}
const image: { format?: string; bytes?: string } | undefined =
"image" in message ? message.image : undefined;
if (image === undefined) {
return false;
}
const isMalformed = image.format === undefined || image.bytes === undefined;
if (isMalformed) {
return false;
}
return true;
}
/*
----------------------------
AGUI Message -> GQL Message
----------------------------
*/
export function aguiToGQL(
messages: agui.Message[] | agui.Message,
actions?: Record<string, any>,
coAgentStateRenders?: Record<string, any>,
): gql.Message[] {
const gqlMessages: gql.Message[] = [];
messages = Array.isArray(messages) ? messages : [messages];
// Track tool call names by their IDs for use in result messages
const toolCallNames: Record<string, string> = {};
for (const message of messages) {
// Agent state message support
if (isAgentStateMessage(message)) {
const agentName = extractAgentName(message);
const state = "state" in message && message.state ? message.state : {};
gqlMessages.push(
new gql.AgentStateMessage({
id: message.id,
agentName,
state,
role: gql.Role.Assistant,
}),
);
// Optionally preserve render function
if (
"generativeUI" in message &&
message.generativeUI &&
coAgentStateRenders
) {
coAgentStateRenders[agentName] = {
name: agentName,
render: message.generativeUI,
};
}
continue;
}
if (hasImageProperty(message)) {
gqlMessages.push(aguiMessageWithImageToGQLMessage(message));
continue;
}
// Action execution message support
if (message.role === "assistant" && message.toolCalls) {
gqlMessages.push(aguiTextMessageToGQLMessage(message));
for (const toolCall of message.toolCalls) {
// Track the tool call name by its ID
toolCallNames[toolCall.id] = toolCall.function.name;
const actionExecMsg = aguiToolCallToGQLActionExecution(
toolCall,
message.id,
);
// Preserve render function in actions context
if ("generativeUI" in message && message.generativeUI && actions) {
const actionName = toolCall.function.name;
// Check for specific action first, then wild card action
const specificAction = Object.values(actions).find(
(action: any) => action.name === actionName,
);
const wildcardAction = Object.values(actions).find(
(action: any) => action.name === "*",
);
// Assign render function to the matching action (specific takes priority)
if (specificAction) {
specificAction.render = message.generativeUI;
} else if (wildcardAction) {
wildcardAction.render = message.generativeUI;
}
}
gqlMessages.push(actionExecMsg);
}
continue;
}
// Reasoning messages are ephemeral display-only content with no GQL equivalent — skip them
if (message.role === "reasoning") {
continue;
}
// Regular text messages
if (
message.role === "developer" ||
message.role === "system" ||
message.role === "assistant" ||
message.role === "user"
) {
gqlMessages.push(aguiTextMessageToGQLMessage(message));
continue;
}
// Tool result message
if (message.role === "tool") {
gqlMessages.push(
aguiToolMessageToGQLResultMessage(message, toolCallNames),
);
continue;
}
throw new Error(
`Unknown message role: "${(message as any).role}" in message with id: ${(message as any).id}`,
);
}
return gqlMessages;
}
export function aguiTextMessageToGQLMessage(
message: agui.Message,
): gql.TextMessage {
if (
message.role !== "developer" &&
message.role !== "system" &&
message.role !== "assistant" &&
message.role !== "user"
) {
throw new Error(
`Cannot convert message with role ${message.role} to TextMessage`,
);
}
let roleValue: MessageRole;
if (message.role === "developer") {
roleValue = gql.Role.Developer;
} else if (message.role === "system") {
roleValue = gql.Role.System;
} else if (message.role === "assistant") {
roleValue = gql.Role.Assistant;
} else {
roleValue = gql.Role.User;
}
return new gql.TextMessage({
id: message.id,
content: (message.content as any) || "",
role: roleValue,
});
}
export function aguiToolCallToGQLActionExecution(
toolCall: agui.ToolCall,
parentMessageId: string,
): gql.ActionExecutionMessage {
if (toolCall.type !== "function") {
throw new Error(`Unsupported tool call type: ${toolCall.type}`);
}
// Handle arguments - they should be a JSON string in AGUI format,
// but we need to convert them to an object for GQL format
let argumentsObj: any;
if (typeof toolCall.function.arguments === "string") {
// Expected case: arguments is a JSON string
try {
argumentsObj = JSON.parse(toolCall.function.arguments);
} catch {
console.warn(
`[CopilotKit] Failed to parse tool arguments, falling back to empty object`,
);
// Provide fallback empty object to prevent application crash
argumentsObj = {};
}
} else if (
typeof toolCall.function.arguments === "object" &&
toolCall.function.arguments !== null
) {
// Backward compatibility: arguments is already an object
argumentsObj = toolCall.function.arguments;
} else {
// Fallback for undefined, null, or other types
console.warn(
`[CopilotKit] Tool arguments parsed to non-object (${typeof toolCall.function.arguments}), falling back to empty object`,
);
argumentsObj = {};
}
// Guard against successfully parsed non-object values (e.g. JSON.parse('""') → "")
if (
typeof argumentsObj !== "object" ||
argumentsObj === null ||
Array.isArray(argumentsObj)
) {
console.warn(
`[CopilotKit] Tool arguments parsed to non-object (${typeof argumentsObj}), falling back to empty object`,
);
argumentsObj = {};
}
// Always include name and arguments
return new gql.ActionExecutionMessage({
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);
}
} else {
// Handle other types (number, boolean, etc.)
resultContent = String(messageContent);
}
return new gql.ResultMessage({
id: message.id,
result: resultContent,
actionExecutionId: message.toolCallId,
actionName: message.toolName || actionName,
});
}
// New function to handle AGUI messages with render functions
export function aguiMessageWithRenderToGQL(
message: agui.Message,
actions?: Record<string, any>,
coAgentStateRenders?: Record<string, any>,
): gql.Message[] {
// Handle the special case: assistant messages with render function but no tool calls
if (
message.role === "assistant" &&
"generativeUI" in message &&
message.generativeUI &&
!message.toolCalls
) {
const gqlMessages: gql.Message[] = [];
gqlMessages.push(
new gql.AgentStateMessage({
id: message.id,
agentName: "unknown",
state: {},
role: gql.Role.Assistant,
}),
);
if (coAgentStateRenders) {
coAgentStateRenders.unknown = {
name: "unknown",
render: message.generativeUI,
};
}
return gqlMessages;
}
// For all other cases, delegate to aguiToGQL
return aguiToGQL([message], actions, coAgentStateRenders);
}
export function aguiMessageWithImageToGQLMessage(
message: agui.Message,
): gql.ImageMessage {
if (!hasImageProperty(message)) {
throw new Error(
`Cannot convert message to ImageMessage: missing format or bytes`,
);
}
let roleValue: MessageRole;
if (message.role === "assistant") {
roleValue = gql.Role.Assistant;
} else {
roleValue = gql.Role.User;
}
if (message.role !== "assistant" && message.role !== "user") {
throw new Error(
`Cannot convert message with role ${message.role} to ImageMessage`,
);
}
return new gql.ImageMessage({
id: message.id,
format: message.image.format,
bytes: message.image.bytes,
role: roleValue,
});
}