chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -0,0 +1,16 @@
import { Field, InputType } from "type-graphql";
import { ActionInputAvailability } from "../types/enums";
@InputType()
export class ActionInput {
@Field(() => String)
name: string;
@Field(() => String)
description: string;
@Field(() => String)
jsonSchema: string;
@Field(() => ActionInputAvailability, { nullable: true })
available?: ActionInputAvailability;
}
@@ -0,0 +1,13 @@
import { Field, InputType } from "type-graphql";
@InputType()
export class AgentSessionInput {
@Field(() => String)
agentName: string;
@Field(() => String, { nullable: true })
threadId?: string;
@Field(() => String, { nullable: true })
nodeName?: string;
}
@@ -0,0 +1,13 @@
import { Field, InputType } from "type-graphql";
@InputType()
export class AgentStateInput {
@Field(() => String)
agentName: string;
@Field(() => String)
state: string;
@Field(() => String, { nullable: true })
config?: string;
}
@@ -0,0 +1,16 @@
import { Field, InputType } from "type-graphql";
@InputType()
export class GuardrailsRuleInput {
@Field(() => [String], { nullable: true })
allowList?: string[] = [];
@Field(() => [String], { nullable: true })
denyList?: string[] = [];
}
@InputType()
export class GuardrailsInput {
@Field(() => GuardrailsRuleInput, { nullable: false })
inputValidationRules: GuardrailsRuleInput;
}
@@ -0,0 +1,8 @@
import { Field, InputType } from "type-graphql";
import { GuardrailsInput } from "./cloud-guardrails.input";
@InputType()
export class CloudInput {
@Field(() => GuardrailsInput, { nullable: true })
guardrails?: GuardrailsInput;
}
@@ -0,0 +1,10 @@
import { Field, InputType } from "type-graphql";
@InputType()
export class ContextPropertyInput {
@Field(() => String)
value: string;
@Field(() => String)
description: string;
}
@@ -0,0 +1,10 @@
import { Field, InputType } from "type-graphql";
@InputType()
export class CopilotContextInput {
@Field(() => String)
description: string;
@Field(() => String)
value: string;
}
@@ -0,0 +1,15 @@
import { Field, InputType, Int, createUnionType } from "type-graphql";
const PrimitiveUnion = createUnionType({
name: "Primitive",
types: () => [String, Number, Boolean] as const,
});
@InputType()
export class CustomPropertyInput {
@Field(() => String)
key: string;
@Field(() => PrimitiveUnion)
value: string;
}
@@ -0,0 +1,21 @@
import { Field, InputType } from "type-graphql";
/**
* The extensions input is used to pass additional information to the copilot runtime, specific to a
* service adapter or agent framework.
*/
@InputType()
export class OpenAIApiAssistantAPIInput {
@Field(() => String, { nullable: true })
runId?: string;
@Field(() => String, { nullable: true })
threadId?: string;
}
@InputType()
export class ExtensionsInput {
@Field(() => OpenAIApiAssistantAPIInput, { nullable: true })
openaiAssistantAPI?: OpenAIApiAssistantAPIInput;
}
@@ -0,0 +1,22 @@
import { Field, InputType } from "type-graphql";
@InputType()
export class ForwardedParametersInput {
@Field(() => String, { nullable: true })
model?: string;
@Field(() => Number, { nullable: true })
maxTokens?: number;
@Field(() => [String], { nullable: true })
stop?: string[];
@Field(() => String, { nullable: true })
toolChoice?: String;
@Field(() => String, { nullable: true })
toolChoiceFunctionName?: string;
@Field(() => Number, { nullable: true })
temperature?: number;
}
@@ -0,0 +1,14 @@
import { Field, InputType } from "type-graphql";
import { ActionInput } from "./action.input";
@InputType()
export class FrontendInput {
@Field(() => String, { nullable: true })
toDeprecate_fullContext?: string;
@Field(() => [ActionInput])
actions: ActionInput[];
@Field(() => String, { nullable: true })
url?: string;
}
@@ -0,0 +1,59 @@
import { Field, InputType } from "type-graphql";
import { MessageInput } from "./message.input";
import { FrontendInput } from "./frontend.input";
import { CloudInput } from "./cloud.input";
import { CopilotRequestType } from "../types/enums";
import { ForwardedParametersInput } from "./forwarded-parameters.input";
import { AgentSessionInput } from "./agent-session.input";
import { AgentStateInput } from "./agent-state.input";
import { ExtensionsInput } from "./extensions.input";
import { MetaEventInput } from "./meta-event.input";
import { CopilotContextInput } from "./copilot-context.input";
@InputType()
export class GenerateCopilotResponseMetadataInput {
@Field(() => CopilotRequestType, { nullable: true })
requestType: CopilotRequestType;
}
@InputType()
export class GenerateCopilotResponseInput {
@Field(() => GenerateCopilotResponseMetadataInput, { nullable: false })
metadata: GenerateCopilotResponseMetadataInput;
@Field(() => String, { nullable: true })
threadId?: string;
@Field(() => String, { nullable: true })
runId?: string;
@Field(() => [MessageInput])
messages: MessageInput[];
@Field(() => FrontendInput)
frontend: FrontendInput;
@Field(() => CloudInput, { nullable: true })
cloud?: CloudInput;
@Field(() => ForwardedParametersInput, { nullable: true })
forwardedParameters?: ForwardedParametersInput;
@Field(() => AgentSessionInput, { nullable: true })
agentSession?: AgentSessionInput;
@Field(() => AgentStateInput, { nullable: true })
agentState?: AgentStateInput;
@Field(() => [AgentStateInput], { nullable: true })
agentStates?: AgentStateInput[];
@Field(() => ExtensionsInput, { nullable: true })
extensions?: ExtensionsInput;
@Field(() => [MetaEventInput], { nullable: true })
metaEvents?: MetaEventInput[];
@Field(() => [CopilotContextInput], { nullable: true })
context?: CopilotContextInput[];
}
@@ -0,0 +1,10 @@
import { Field, InputType } from "type-graphql";
@InputType()
export class LoadAgentStateInput {
@Field(() => String)
threadId: string;
@Field(() => String)
agentName: string;
}
@@ -0,0 +1,110 @@
import { Field, InputType } from "type-graphql";
import { MessageRole } from "../types/enums";
import { BaseMessageInput } from "../types/base";
@InputType()
export class TextMessageInput {
@Field(() => String)
content: string;
@Field(() => String, { nullable: true })
parentMessageId?: string;
@Field(() => MessageRole)
role: MessageRole;
}
@InputType()
export class ActionExecutionMessageInput {
@Field(() => String)
name: string;
@Field(() => String)
arguments: string;
@Field(() => String, { nullable: true })
parentMessageId?: string;
@Field(() => String, {
nullable: true,
deprecationReason: "This field will be removed in a future version",
})
scope?: String;
}
@InputType()
export class ResultMessageInput {
@Field(() => String)
actionExecutionId: string;
@Field(() => String)
actionName: string;
@Field(() => String, { nullable: true })
parentMessageId?: string;
@Field(() => String)
result: string;
}
@InputType()
export class AgentStateMessageInput {
@Field(() => String)
threadId: string;
@Field(() => String)
agentName: string;
@Field(() => MessageRole)
role: MessageRole;
@Field(() => String)
state: string;
@Field(() => Boolean)
running: boolean;
@Field(() => String)
nodeName: string;
@Field(() => String)
runId: string;
@Field(() => Boolean)
active: boolean;
}
@InputType()
export class ImageMessageInput {
@Field(() => String)
format: string;
@Field(() => String)
bytes: string;
@Field(() => String, { nullable: true })
parentMessageId?: string;
@Field(() => MessageRole)
role: MessageRole;
}
// GraphQL does not support union types in inputs, so we need to use
// optional fields for the different subtypes.
@InputType()
export class MessageInput extends BaseMessageInput {
@Field(() => TextMessageInput, { nullable: true })
textMessage?: TextMessageInput;
@Field(() => ActionExecutionMessageInput, { nullable: true })
actionExecutionMessage?: ActionExecutionMessageInput;
@Field(() => ResultMessageInput, { nullable: true })
resultMessage?: ResultMessageInput;
@Field(() => AgentStateMessageInput, { nullable: true })
agentStateMessage?: AgentStateMessageInput;
@Field(() => ImageMessageInput, { nullable: true })
imageMessage?: ImageMessageInput;
}
@@ -0,0 +1,18 @@
import { Field, InputType } from "type-graphql";
import { MetaEventName } from "../types/meta-events.type";
import { MessageInput } from "./message.input";
@InputType()
export class MetaEventInput {
@Field(() => MetaEventName)
name: MetaEventName;
@Field(() => String)
value?: string;
@Field(() => String, { nullable: true })
response?: string;
@Field(() => [MessageInput], { nullable: true })
messages?: MessageInput[];
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,391 @@
import * as gql from "../types/converted/index";
import { MessageRole } from "../types/enums";
import * 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;
}
function normalizeMessageContent(content: agui.Message["content"]): string {
if (typeof content === "string" || typeof content === "undefined") {
return content || "";
}
if (Array.isArray(content)) {
return content
.map((part) => {
if (part?.type === "text") {
return part.text;
}
if (part?.type === "binary") {
return (
part.data ||
part.url ||
part.filename ||
`[binary:${part.mimeType}]`
);
}
return "";
})
.filter(Boolean)
.join("\n");
}
if (content && typeof content === "object") {
try {
return JSON.stringify(content);
} catch (error) {
console.warn("Failed to serialize message content", error);
}
}
return String(content ?? "");
}
/*
----------------------------
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: normalizeMessageContent(message.content),
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,
});
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,297 @@
import * as gql from "../types/converted/index";
import * as agui from "@copilotkit/shared";
import { MessageStatusCode } from "../types/message-status.type";
// Define valid image formats based on the supported formats in the codebase
const VALID_IMAGE_FORMATS = ["jpeg", "png", "webp", "gif"] as const;
type ValidImageFormat = (typeof VALID_IMAGE_FORMATS)[number];
// Validation function for image format
function validateImageFormat(format: string): format is ValidImageFormat {
return VALID_IMAGE_FORMATS.includes(format as ValidImageFormat);
}
/*
----------------------------
GQL Message -> AGUI Message
----------------------------
*/
export function gqlToAGUI(
messages: gql.Message[] | gql.Message,
actions?: Record<string, any>,
coAgentStateRenders?: Record<string, any>,
): agui.Message[] {
let aguiMessages: agui.Message[] = [];
messages = Array.isArray(messages) ? messages : [messages];
// Create a map of action execution ID to result for completed actions
const actionResults = new Map<string, string>();
for (const message of messages) {
if (message.isResultMessage()) {
actionResults.set(message.actionExecutionId, message.result);
}
}
for (const message of messages) {
if (message.isTextMessage()) {
aguiMessages.push(gqlTextMessageToAGUIMessage(message));
} else if (message.isResultMessage()) {
aguiMessages.push(gqlResultMessageToAGUIMessage(message));
} else if (message.isActionExecutionMessage()) {
aguiMessages.push(
gqlActionExecutionMessageToAGUIMessage(message, actions, actionResults),
);
} else if (message.isAgentStateMessage()) {
aguiMessages.push(
gqlAgentStateMessageToAGUIMessage(message, coAgentStateRenders),
);
} else if (message.isImageMessage()) {
aguiMessages.push(gqlImageMessageToAGUIMessage(message));
} else {
throw new Error("Unknown message type");
}
}
return aguiMessages;
}
export function gqlActionExecutionMessageToAGUIMessage(
message: gql.ActionExecutionMessage,
actions?: Record<string, any>,
actionResults?: Map<string, string>,
): agui.Message {
// Check if we have actions and if there's a specific action or wild card action
const hasSpecificAction =
actions &&
Object.values(actions).some((action: any) => action.name === message.name);
const hasWildcardAction =
actions &&
Object.values(actions).some((action: any) => action.name === "*");
if (!actions || (!hasSpecificAction && !hasWildcardAction)) {
return {
id: message.id,
role: "assistant",
toolCalls: [actionExecutionMessageToAGUIMessage(message)],
name: message.name,
};
}
// Find the specific action first, then fall back to wild card action
const action =
Object.values(actions).find(
(action: any) => action.name === message.name,
) || Object.values(actions).find((action: any) => action.name === "*");
// Create render function wrapper that provides proper props
const createRenderWrapper = (originalRender: any) => {
if (!originalRender) return undefined;
return (props?: any) => {
// Determine the correct status based on the same logic as RenderActionExecutionMessage
let actionResult: any = actionResults?.get(message.id);
let status: "inProgress" | "executing" | "complete" = "inProgress";
if (actionResult !== undefined) {
status = "complete";
} else if (message.status?.code !== MessageStatusCode.Pending) {
status = "executing";
}
// if props.result is a string, parse it as JSON but don't throw an error if it's not valid JSON
if (typeof props?.result === "string") {
try {
props.result = JSON.parse(props.result);
} catch (e) {
/* do nothing */
}
}
// if actionResult is a string, parse it as JSON but don't throw an error if it's not valid JSON
if (typeof actionResult === "string") {
try {
actionResult = JSON.parse(actionResult);
} catch (e) {
/* do nothing */
}
}
// Base props that all actions receive
const baseProps = {
status: props?.status || status,
args: message.arguments || {},
result: props?.result || actionResult || undefined,
messageId: message.id,
};
// Add properties based on action type
if (action.name === "*") {
// Wildcard actions get the tool name; ensure it cannot be overridden by incoming props
return originalRender({
...baseProps,
...props,
name: message.name,
});
} else {
// Regular actions get respond (defaulting to a no-op if not provided)
const respond = props?.respond ?? (() => {});
return originalRender({
...baseProps,
...props,
respond,
});
}
};
};
return {
id: message.id,
role: "assistant",
content: "",
toolCalls: [actionExecutionMessageToAGUIMessage(message)],
generativeUI: createRenderWrapper(action.render),
name: message.name,
} as agui.AIMessage;
}
function gqlAgentStateMessageToAGUIMessage(
message: gql.AgentStateMessage,
coAgentStateRenders?: Record<string, any>,
): agui.Message {
if (
coAgentStateRenders &&
Object.values(coAgentStateRenders).some(
(render: any) => render.name === message.agentName,
)
) {
const render = Object.values(coAgentStateRenders).find(
(render: any) => render.name === message.agentName,
);
// Create render function wrapper that provides proper props
const createRenderWrapper = (originalRender: any) => {
if (!originalRender) return undefined;
return (props?: any) => {
// Determine the correct status based on the same logic as RenderActionExecutionMessage
const state = message.state;
// Provide the full props structure that the render function expects
const renderProps = {
state: state,
};
return originalRender(renderProps);
};
};
return {
id: message.id,
role: "assistant",
generativeUI: createRenderWrapper(render.render),
agentName: message.agentName,
state: message.state,
};
}
return {
id: message.id,
role: "assistant",
agentName: message.agentName,
state: message.state,
};
}
function actionExecutionMessageToAGUIMessage(
actionExecutionMessage: gql.ActionExecutionMessage,
): agui.ToolCall {
return {
id: actionExecutionMessage.id,
function: {
name: actionExecutionMessage.name,
arguments: JSON.stringify(actionExecutionMessage.arguments),
},
type: "function",
};
}
export function gqlTextMessageToAGUIMessage(
message: gql.TextMessage,
): agui.Message {
switch (message.role) {
case gql.Role.developer:
return {
id: message.id,
role: "developer",
content: message.content,
};
case gql.Role.system:
return {
id: message.id,
role: "system",
content: message.content,
};
case gql.Role.assistant:
return {
id: message.id,
role: "assistant",
content: message.content,
};
case gql.Role.user:
return {
id: message.id,
role: "user",
content: message.content,
};
default:
throw new Error("Unknown message role");
}
}
export function gqlResultMessageToAGUIMessage(
message: gql.ResultMessage,
): agui.Message {
return {
id: message.id,
role: "tool",
content: message.result,
toolCallId: message.actionExecutionId,
toolName: message.actionName,
};
}
export function gqlImageMessageToAGUIMessage(
message: gql.ImageMessage,
): agui.Message {
// Validate image format
if (!validateImageFormat(message.format)) {
throw new Error(
`Invalid image format: ${message.format}. Supported formats are: ${VALID_IMAGE_FORMATS.join(", ")}`,
);
}
// Validate that bytes is a non-empty string
if (
!message.bytes ||
typeof message.bytes !== "string" ||
message.bytes.trim() === ""
) {
throw new Error("Image bytes must be a non-empty string");
}
// Determine the role based on the message role
const role = message.role === gql.Role.assistant ? "assistant" : "user";
// Create the image message with proper typing
const imageMessage: agui.Message = {
id: message.id,
role,
content: "",
image: {
format: message.format,
bytes: message.bytes,
},
};
return imageMessage;
}
@@ -0,0 +1,2 @@
export * from "./agui-to-gql";
export * from "./gql-to-agui";
@@ -0,0 +1,561 @@
import { vi } from "vitest";
import * as gql from "../types/converted/index";
import * as agui from "@copilotkit/shared";
import { aguiToGQL } from "./agui-to-gql";
import { gqlToAGUI } from "./gql-to-agui";
// Helper to strip functions for deep equality
function stripFunctions(obj: any): any {
if (typeof obj === "function") return undefined;
if (Array.isArray(obj)) return obj.map(stripFunctions);
if (obj && typeof obj === "object") {
const out: any = {};
for (const k in obj) {
if (typeof obj[k] !== "function") {
out[k] = stripFunctions(obj[k]);
}
}
return out;
}
return obj;
}
describe("roundtrip message conversion", () => {
test("text message AGUI -> GQL -> AGUI", () => {
const aguiMsg: agui.Message = {
id: "user-1",
role: "user",
content: "Hello!",
};
const gqlMsgs = aguiToGQL(aguiMsg);
const aguiMsgs2 = gqlToAGUI(gqlMsgs);
expect(stripFunctions(aguiMsgs2[0])).toEqual(stripFunctions(aguiMsg));
});
test("text message GQL -> AGUI -> GQL", () => {
const gqlMsg = new gql.TextMessage({
id: "assistant-1",
content: "Hi!",
role: gql.Role.assistant,
});
const aguiMsgs = gqlToAGUI(gqlMsg);
const gqlMsgs2 = aguiToGQL(aguiMsgs);
// Should be equivalent in content, id, and role
expect(gqlMsgs2[0].id).toBe(gqlMsg.id);
expect((gqlMsgs2[0] as any).content).toBe(gqlMsg.content);
expect((gqlMsgs2[0] as any).role).toBe(gqlMsg.role);
});
test("tool message AGUI -> GQL -> AGUI", () => {
const aguiMsg: agui.Message = {
id: "tool-1",
role: "tool",
content: "Tool result",
toolCallId: "tool-call-1",
toolName: "testAction",
};
const gqlMsgs = aguiToGQL(aguiMsg);
const aguiMsgs2 = gqlToAGUI(gqlMsgs);
expect(stripFunctions(aguiMsgs2[0])).toEqual(stripFunctions(aguiMsg));
});
test("tool message GQL -> AGUI -> GQL", () => {
const gqlMsg = new gql.ResultMessage({
id: "tool-1",
result: "Tool result",
actionExecutionId: "tool-call-1",
actionName: "testAction",
});
const aguiMsgs = gqlToAGUI(gqlMsg);
const gqlMsgs2 = aguiToGQL(aguiMsgs);
expect(gqlMsgs2[0].id).toBe(gqlMsg.id);
expect((gqlMsgs2[0] as any).result).toBe(gqlMsg.result);
expect((gqlMsgs2[0] as any).actionExecutionId).toBe(
gqlMsg.actionExecutionId,
);
});
test("action execution AGUI -> GQL -> AGUI", () => {
const aguiMsg: agui.Message = {
id: "assistant-1",
role: "assistant",
content: "Running action",
toolCalls: [
{
id: "tool-call-1",
type: "function",
function: {
name: "doSomething",
arguments: JSON.stringify({ foo: "bar" }),
},
},
],
};
const gqlMsgs = aguiToGQL(aguiMsg);
const aguiMsgs2 = gqlToAGUI(gqlMsgs);
// Should have an assistant message and an action execution message
expect(aguiMsgs2[0].role).toBe("assistant");
expect(aguiMsgs2[1].role).toBe("assistant");
// Only check toolCalls if present
if ("toolCalls" in aguiMsgs2[1]) {
expect((aguiMsgs2[1] as any).toolCalls[0].function.name).toBe(
"doSomething",
);
}
});
test("action execution GQL -> AGUI -> GQL", () => {
const actionExecMsg = new gql.ActionExecutionMessage({
id: "tool-call-1",
name: "doSomething",
arguments: { foo: "bar" },
parentMessageId: "assistant-1",
});
const aguiMsgs = gqlToAGUI([actionExecMsg]);
const gqlMsgs2 = aguiToGQL(aguiMsgs);
// The ActionExecutionMessage is at index 1, not index 0
expect(gqlMsgs2[1].id).toBe("tool-call-1");
// The name should be extracted from the toolCall function name
expect((gqlMsgs2[1] as any).name).toBe("doSomething");
expect((gqlMsgs2[1] as any).arguments).toEqual({ foo: "bar" });
});
test("agent state GQL -> AGUI -> GQL", () => {
const agentStateMsg = new gql.AgentStateMessage({
id: "agent-state-1",
agentName: "testAgent",
state: { status: "running" },
role: gql.Role.assistant,
});
const aguiMsgs = gqlToAGUI([agentStateMsg]);
const gqlMsgs2 = aguiToGQL(aguiMsgs);
expect(gqlMsgs2[0].id).toBe("agent-state-1");
// The agentName should be preserved in the roundtrip
expect((gqlMsgs2[0] as any).agentName).toBe("testAgent");
});
test("action execution with render function roundtrip", () => {
const mockRender = vi.fn();
const aguiMsg: agui.Message = {
id: "assistant-1",
role: "assistant",
content: "Running action",
toolCalls: [
{
id: "tool-call-1",
type: "function",
function: {
name: "doSomething",
arguments: JSON.stringify({ foo: "bar" }),
},
},
],
generativeUI: mockRender,
};
const actions: Record<string, any> = {
doSomething: { name: "doSomething" },
};
const gqlMsgs = aguiToGQL(aguiMsg, actions);
const aguiMsgs2 = gqlToAGUI(gqlMsgs, actions);
// The render function should be preserved in actions context
expect(typeof actions.doSomething.render).toBe("function");
// The roundtripped message should have the same tool call
if ("toolCalls" in aguiMsgs2[1]) {
expect((aguiMsgs2[1] as any).toolCalls[0].function.name).toBe(
"doSomething",
);
}
});
test("image message GQL -> AGUI -> GQL", () => {
const gqlMsg = new gql.ImageMessage({
id: "img-1",
format: "jpeg",
bytes: "somebase64string",
role: gql.Role.user,
});
const aguiMsgs = gqlToAGUI(gqlMsg);
const gqlMsgs2 = aguiToGQL(aguiMsgs);
expect(gqlMsgs2[0].id).toBe(gqlMsg.id);
expect((gqlMsgs2[0] as any).format).toBe(gqlMsg.format);
expect((gqlMsgs2[0] as any).bytes).toBe(gqlMsg.bytes);
expect((gqlMsgs2[0] as any).role).toBe(gqlMsg.role);
});
test("image message AGUI -> GQL -> AGUI (assistant and user)", () => {
// Assistant image message
const aguiAssistantImageMsg: agui.Message = {
id: "img-assistant-1",
role: "assistant",
image: {
format: "jpeg",
bytes: "assistantbase64data",
},
content: "", // required for type
};
const gqlAssistantMsgs = aguiToGQL(aguiAssistantImageMsg);
const aguiAssistantMsgs2 = gqlToAGUI(gqlAssistantMsgs);
expect(aguiAssistantMsgs2[0].id).toBe(aguiAssistantImageMsg.id);
expect(aguiAssistantMsgs2[0].role).toBe("assistant");
expect((aguiAssistantMsgs2[0] as any).image.format).toBe("jpeg");
expect((aguiAssistantMsgs2[0] as any).image.bytes).toBe(
"assistantbase64data",
);
// User image message
const aguiUserImageMsg = {
id: "img-user-1",
role: "user",
image: {
format: "png",
bytes: "userbase64data",
},
content: "", // required for type
} as agui.Message;
const gqlUserMsgs = aguiToGQL(aguiUserImageMsg);
const aguiUserMsgs2 = gqlToAGUI(gqlUserMsgs);
expect(aguiUserMsgs2[0].id).toBe(aguiUserImageMsg.id);
expect(aguiUserMsgs2[0].role).toBe("user");
expect((aguiUserMsgs2[0] as any).image.format).toBe("png");
expect((aguiUserMsgs2[0] as any).image.bytes).toBe("userbase64data");
});
test("wild card action roundtrip conversion", () => {
const mockRender = vi.fn(
(props) => `Wildcard rendered: ${props.args.test}`,
);
const aguiMsg: agui.Message = {
id: "assistant-wildcard-1",
role: "assistant",
content: "Running wild card action",
toolCalls: [
{
id: "tool-call-wildcard-1",
type: "function",
function: {
name: "unknownAction",
arguments: JSON.stringify({ test: "wildcard-value" }),
},
},
],
generativeUI: mockRender,
};
const actions: Record<string, any> = {
"*": { name: "*" },
};
// AGUI -> GQL -> AGUI roundtrip
const gqlMsgs = aguiToGQL(aguiMsg, actions);
const aguiMsgs2 = gqlToAGUI(gqlMsgs, actions);
// Verify the wild card action preserved the render function
expect(typeof actions["*"].render).toBe("function");
expect(actions["*"].render).toBe(mockRender);
// Verify the roundtripped message structure
expect(aguiMsgs2).toHaveLength(2);
expect(aguiMsgs2[0].role).toBe("assistant");
expect(aguiMsgs2[1].role).toBe("assistant");
// Check that the tool call is preserved
if ("toolCalls" in aguiMsgs2[1]) {
expect((aguiMsgs2[1] as any).toolCalls[0].function.name).toBe(
"unknownAction",
);
expect((aguiMsgs2[1] as any).toolCalls[0].function.arguments).toBe(
'{"test":"wildcard-value"}',
);
}
});
test("wild card action with specific action priority roundtrip", () => {
const mockRender = vi.fn(
(props) => `Specific action rendered: ${props.args.test}`,
);
const aguiMsg: agui.Message = {
id: "assistant-priority-1",
role: "assistant",
content: "Running specific action",
toolCalls: [
{
id: "tool-call-priority-1",
type: "function",
function: {
name: "specificAction",
arguments: JSON.stringify({ test: "specific-value" }),
},
},
],
generativeUI: mockRender,
};
const actions: Record<string, any> = {
specificAction: { name: "specificAction" },
"*": { name: "*" },
};
// AGUI -> GQL -> AGUI roundtrip
const gqlMsgs = aguiToGQL(aguiMsg, actions);
const aguiMsgs2 = gqlToAGUI(gqlMsgs, actions);
// Verify the specific action preserved the render function (not wild card)
expect(typeof actions.specificAction.render).toBe("function");
expect(actions.specificAction.render).toBe(mockRender);
expect(actions["*"].render).toBeUndefined();
// Verify the roundtripped message structure
expect(aguiMsgs2).toHaveLength(2);
expect(aguiMsgs2[0].role).toBe("assistant");
expect(aguiMsgs2[1].role).toBe("assistant");
// Check that the tool call is preserved
if ("toolCalls" in aguiMsgs2[1]) {
expect((aguiMsgs2[1] as any).toolCalls[0].function.name).toBe(
"specificAction",
);
expect((aguiMsgs2[1] as any).toolCalls[0].function.arguments).toBe(
'{"test":"specific-value"}',
);
}
});
test("wild card action GQL -> AGUI -> GQL roundtrip", () => {
const actionExecMsg = new gql.ActionExecutionMessage({
id: "wildcard-action-1",
name: "unknownAction",
arguments: { test: "wildcard-gql-value" },
parentMessageId: "assistant-1",
});
const actions: Record<string, any> = {
"*": {
name: "*",
render: vi.fn((props) => `GQL wildcard rendered: ${props.args.test}`),
},
};
// GQL -> AGUI -> GQL roundtrip
const aguiMsgs = gqlToAGUI([actionExecMsg], actions);
const gqlMsgs2 = aguiToGQL(aguiMsgs, actions);
// When converting ActionExecutionMessage to AGUI and back, we get:
// 1. A TextMessage (assistant message with toolCalls)
// 2. An ActionExecutionMessage (the tool call itself)
expect(gqlMsgs2).toHaveLength(2);
expect(gqlMsgs2[0].id).toBe("wildcard-action-1");
expect((gqlMsgs2[0] as any).role).toBe(gql.Role.assistant);
expect(gqlMsgs2[1].id).toBe("wildcard-action-1");
expect((gqlMsgs2[1] as any).name).toBe("unknownAction");
expect((gqlMsgs2[1] as any).arguments).toEqual({
test: "wildcard-gql-value",
});
});
test("roundtrip conversion with result parsing edge cases", () => {
// Test with a tool result that contains a JSON string
const toolResultMsg: agui.Message = {
id: "tool-result-json",
role: "tool",
content: '{"status": "success", "data": {"value": 42}}',
toolCallId: "tool-call-json",
toolName: "jsonAction",
};
// Convert AGUI -> GQL -> AGUI
const gqlMsgs = aguiToGQL(toolResultMsg);
const aguiMsgs = gqlToAGUI(gqlMsgs);
expect(gqlMsgs).toHaveLength(1);
expect(gqlMsgs[0]).toBeInstanceOf(gql.ResultMessage);
expect((gqlMsgs[0] as any).result).toBe(
'{"status": "success", "data": {"value": 42}}',
);
expect(aguiMsgs).toHaveLength(1);
expect(aguiMsgs[0].role).toBe("tool");
expect(aguiMsgs[0].content).toBe(
'{"status": "success", "data": {"value": 42}}',
);
});
test("roundtrip conversion with object content in tool results", () => {
// Test with a tool result that has object content (edge case)
const toolResultMsg: agui.Message = {
id: "tool-result-object",
role: "tool",
content: { status: "success", data: { value: 42 } } as any,
toolCallId: "tool-call-object",
toolName: "objectAction",
};
// Convert AGUI -> GQL -> AGUI
const gqlMsgs = aguiToGQL(toolResultMsg);
const aguiMsgs = gqlToAGUI(gqlMsgs);
expect(gqlMsgs).toHaveLength(1);
expect(gqlMsgs[0]).toBeInstanceOf(gql.ResultMessage);
expect((gqlMsgs[0] as any).result).toBe(
'{"status":"success","data":{"value":42}}',
);
expect(aguiMsgs).toHaveLength(1);
expect(aguiMsgs[0].role).toBe("tool");
expect(aguiMsgs[0].content).toBe(
'{"status":"success","data":{"value":42}}',
);
});
test("roundtrip conversion with action execution and result parsing", () => {
const mockRender = vi.fn(
(props) => `Rendered: ${JSON.stringify(props.result)}`,
);
// Create action execution message
const actionExecMsg = new gql.ActionExecutionMessage({
id: "action-with-result",
name: "testAction",
arguments: { input: "test-value" },
parentMessageId: "parent-result",
});
// Create result message
const resultMsg = new gql.ResultMessage({
id: "result-with-json",
result: '{"output": "processed", "count": 5}',
actionExecutionId: "action-with-result",
actionName: "testAction",
});
const actions = {
testAction: {
name: "testAction",
render: mockRender,
},
};
// Convert GQL -> AGUI
const aguiMsgs = gqlToAGUI([actionExecMsg, resultMsg], actions);
// The action execution should have a generativeUI function that parses string results
expect(aguiMsgs).toHaveLength(2);
expect(aguiMsgs[0].role).toBe("assistant");
expect("generativeUI" in aguiMsgs[0]).toBe(true);
expect(aguiMsgs[1].role).toBe("tool");
expect(aguiMsgs[1].content).toBe('{"output": "processed", "count": 5}');
// Test that the render function receives parsed results
if ("generativeUI" in aguiMsgs[0] && aguiMsgs[0].generativeUI) {
aguiMsgs[0].generativeUI({ result: '{"parsed": true}' });
expect(mockRender).toHaveBeenCalledWith(
expect.objectContaining({
result: { parsed: true }, // Should be parsed from string
}),
);
}
// Convert back AGUI -> GQL
const gqlMsgs2 = aguiToGQL(aguiMsgs, actions);
// Should have 3 messages: TextMessage, ActionExecutionMessage, ResultMessage
expect(gqlMsgs2).toHaveLength(3);
expect(gqlMsgs2[0]).toBeInstanceOf(gql.TextMessage);
expect(gqlMsgs2[1]).toBeInstanceOf(gql.ActionExecutionMessage);
expect(gqlMsgs2[2]).toBeInstanceOf(gql.ResultMessage);
// Check that arguments roundtripped correctly
expect((gqlMsgs2[1] as any).arguments).toEqual({ input: "test-value" });
expect((gqlMsgs2[2] as any).result).toBe(
'{"output": "processed", "count": 5}',
);
});
test("roundtrip conversion verifies correct property distribution for regular actions", () => {
const mockRender = vi.fn(
(props) => `Regular action: ${JSON.stringify(props.args)}`,
);
const actionExecMsg = new gql.ActionExecutionMessage({
id: "regular-action-test",
name: "regularAction",
arguments: { test: "regular-value" },
parentMessageId: "parent-regular",
});
const actions = {
regularAction: {
name: "regularAction",
render: mockRender,
},
};
// GQL -> AGUI -> GQL roundtrip
const aguiMsgs = gqlToAGUI([actionExecMsg], actions);
const gqlMsgs2 = aguiToGQL(aguiMsgs, actions);
// Verify the roundtrip preserved the action
expect(gqlMsgs2).toHaveLength(2);
expect(gqlMsgs2[1]).toBeInstanceOf(gql.ActionExecutionMessage);
expect((gqlMsgs2[1] as any).name).toBe("regularAction");
// Test that regular actions do NOT receive the name property in render props
if ("generativeUI" in aguiMsgs[0] && aguiMsgs[0].generativeUI) {
aguiMsgs[0].generativeUI();
expect(mockRender).toHaveBeenCalledWith(
expect.objectContaining({
args: { test: "regular-value" },
// name property should NOT be present for regular actions
}),
);
// Verify name property is NOT present
const callArgs = mockRender.mock.calls[0][0];
expect(callArgs).not.toHaveProperty("name");
}
});
test("roundtrip conversion verifies correct property distribution for wildcard actions", () => {
const mockRender = vi.fn(
(props) =>
`Wildcard action: ${props.name} with ${JSON.stringify(props.args)}`,
);
const actionExecMsg = new gql.ActionExecutionMessage({
id: "wildcard-action-test",
name: "unknownAction",
arguments: { test: "wildcard-value" },
parentMessageId: "parent-wildcard",
});
const actions = {
"*": {
name: "*",
render: mockRender,
},
};
// GQL -> AGUI -> GQL roundtrip
const aguiMsgs = gqlToAGUI([actionExecMsg], actions);
const gqlMsgs2 = aguiToGQL(aguiMsgs, actions);
// Verify the roundtrip preserved the action
expect(gqlMsgs2).toHaveLength(2);
expect(gqlMsgs2[1]).toBeInstanceOf(gql.ActionExecutionMessage);
expect((gqlMsgs2[1] as any).name).toBe("unknownAction");
// Test that wildcard actions DO receive the name property in render props
if ("generativeUI" in aguiMsgs[0] && aguiMsgs[0].generativeUI) {
aguiMsgs[0].generativeUI();
expect(mockRender).toHaveBeenCalledWith(
expect.objectContaining({
args: { test: "wildcard-value" },
name: "unknownAction", // name property SHOULD be present for wildcard actions
}),
);
// Verify name property IS present
const callArgs = mockRender.mock.calls[0][0];
expect(callArgs).toHaveProperty("name", "unknownAction");
}
});
});
@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import { resolveMessageId } from "../resolve-message-id";
describe("resolveMessageId (#2118)", () => {
it("preserves a provided non-empty id verbatim", () => {
expect(resolveMessageId("msg-123")).toBe("msg-123");
});
it.each(["", null, undefined] as const)(
"falls back to a generated id when the event id is %p",
(input) => {
const id = resolveMessageId(input);
// randomId() always produces the "ck-<uuid>" shape; the important
// contract for #2118 is that the returned value is a non-empty string,
// never null/undefined.
expect(id).toMatch(/^ck-[0-9a-f-]{36}$/);
},
);
it("generates a fresh id on each fallback call", () => {
const a = resolveMessageId(undefined);
const b = resolveMessageId(undefined);
expect(a).not.toBe(b);
});
});
@@ -0,0 +1,785 @@
import { Arg, Ctx, Mutation, Query, Resolver } from "type-graphql";
import {
ReplaySubject,
Subject,
Subscription,
filter,
finalize,
firstValueFrom,
shareReplay,
skipWhile,
take,
takeWhile,
tap,
} from "rxjs";
import { GenerateCopilotResponseInput } from "../inputs/generate-copilot-response.input";
import { CopilotResponse } from "../types/copilot-response.type";
import {
CopilotKitLangGraphInterruptEvent,
LangGraphInterruptEvent,
} from "../types/meta-events.type";
import { ActionInputAvailability, MessageRole } from "../types/enums";
import { Repeater } from "graphql-yoga";
import type {
CopilotRequestContextProperties,
GraphQLContext,
} from "../../lib/integrations";
import {
RuntimeEvent,
RuntimeEventTypes,
RuntimeMetaEventName,
} from "../../service-adapters/events";
import {
FailedMessageStatus,
MessageStatusCode,
MessageStatusUnion,
SuccessMessageStatus,
} from "../types/message-status.type";
import {
ResponseStatusUnion,
SuccessResponseStatus,
} from "../types/response-status.type";
import { GraphQLJSONObject } from "graphql-scalars";
import { plainToInstance } from "class-transformer";
import { GuardrailsResult } from "../types/guardrails-result.type";
import { GraphQLError } from "graphql";
import {
GuardrailsValidationFailureResponse,
MessageStreamInterruptedResponse,
UnknownErrorResponse,
} from "../../utils";
import {
ActionExecutionMessage,
AgentStateMessage,
Message,
MessageType,
ResultMessage,
TextMessage,
} from "../types/converted";
import telemetry from "../../lib/telemetry-client";
import { randomId } from "@copilotkit/shared";
import { resolveMessageId } from "./resolve-message-id";
import { AgentsResponse } from "../types/agents-response.type";
import { LangGraphEventTypes } from "../../agents/langgraph/events";
import {
CopilotKitError,
CopilotKitLowLevelError,
isStructuredCopilotKitError,
} from "@copilotkit/shared";
import { CopilotRuntime } from "../../lib";
const invokeGuardrails = async ({
baseUrl,
copilotCloudPublicApiKey,
data,
onResult,
onError,
}: {
baseUrl: string;
copilotCloudPublicApiKey: string;
data: GenerateCopilotResponseInput;
onResult: (result: GuardrailsResult) => void;
onError: (err: Error) => void;
}) => {
if (
data.messages.length &&
data.messages[data.messages.length - 1].textMessage?.role ===
MessageRole.user
) {
const messages = data.messages
.filter(
(m) =>
m.textMessage !== undefined &&
(m.textMessage.role === MessageRole.user ||
m.textMessage.role === MessageRole.assistant),
)
.map((m) => ({
role: m.textMessage!.role,
content: m.textMessage.content,
}));
const lastMessage = messages[messages.length - 1];
const restOfMessages = messages.slice(0, -1);
const body = {
input: lastMessage.content,
validTopics: data.cloud.guardrails.inputValidationRules.allowList,
invalidTopics: data.cloud.guardrails.inputValidationRules.denyList,
messages: restOfMessages,
};
const guardrailsResult = await fetch(`${baseUrl}/guardrails/validate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CopilotCloud-Public-API-Key": copilotCloudPublicApiKey,
},
body: JSON.stringify(body),
});
if (guardrailsResult.ok) {
const resultJson: GuardrailsResult = await guardrailsResult.json();
onResult(resultJson);
} else {
onError(await guardrailsResult.json());
}
}
};
@Resolver(() => CopilotResponse)
export class CopilotResolver {
@Query(() => String)
async hello() {
return "Hello World";
}
@Query(() => AgentsResponse)
async availableAgents(@Ctx() ctx: GraphQLContext) {
let logger = ctx.logger.child({
component: "CopilotResolver.availableAgents",
});
logger.debug("Processing");
const agentsWithEndpoints = [];
logger.debug("Event source created, creating response");
return {
agents: agentsWithEndpoints.map(
({ endpoint, ...agentWithoutEndpoint }) => agentWithoutEndpoint,
),
};
}
@Mutation(() => CopilotResponse)
async generateCopilotResponse(
@Ctx() ctx: GraphQLContext,
@Arg("data") data: GenerateCopilotResponseInput,
@Arg("properties", () => GraphQLJSONObject, { nullable: true })
properties?: CopilotRequestContextProperties,
) {
telemetry.capture("oss.runtime.copilot_request_created", {
"cloud.guardrails.enabled": data.cloud?.guardrails !== undefined,
requestType: data.metadata.requestType,
"cloud.api_key_provided": !!ctx.request.headers.get(
"x-copilotcloud-public-api-key",
),
...(ctx.request.headers.get("x-copilotcloud-public-api-key")
? {
"cloud.public_api_key": ctx.request.headers.get(
"x-copilotcloud-public-api-key",
),
}
: {}),
...(ctx._copilotkit.baseUrl
? {
"cloud.base_url": ctx._copilotkit.baseUrl,
}
: {
"cloud.base_url": "https://api.cloud.copilotkit.ai",
}),
});
let logger = ctx.logger.child({
component: "CopilotResolver.generateCopilotResponse",
});
logger.debug({ data }, "Generating Copilot response");
if (properties) {
logger.debug("Properties provided, merging with context properties");
ctx.properties = { ...ctx.properties, ...properties };
}
const copilotRuntime = ctx._copilotkit.runtime as unknown as CopilotRuntime;
const serviceAdapter = ctx._copilotkit.serviceAdapter;
let copilotCloudPublicApiKey: string | null = null;
let copilotCloudBaseUrl: string;
// Extract publicApiKey from headers for both cloud and non-cloud requests
// This enables onTrace functionality regardless of cloud configuration
const publicApiKeyFromHeaders = ctx.request.headers.get(
"x-copilotcloud-public-api-key",
);
if (publicApiKeyFromHeaders) {
copilotCloudPublicApiKey = publicApiKeyFromHeaders;
}
if (data.cloud) {
logger = logger.child({ cloud: true });
logger.debug(
"Cloud configuration provided, checking for public API key in headers",
);
if (!copilotCloudPublicApiKey) {
logger.error("Public API key not found in headers");
throw new GraphQLError(
"X-CopilotCloud-Public-API-Key header is required",
);
}
if (process.env.COPILOT_CLOUD_BASE_URL) {
copilotCloudBaseUrl = process.env.COPILOT_CLOUD_BASE_URL;
} else if (ctx._copilotkit.cloud?.baseUrl) {
copilotCloudBaseUrl = ctx._copilotkit.cloud?.baseUrl;
} else {
copilotCloudBaseUrl = "https://api.cloud.copilotkit.ai";
}
logger = logger.child({ copilotCloudBaseUrl });
}
logger.debug("Setting up subjects");
const responseStatus$ = new ReplaySubject<typeof ResponseStatusUnion>();
const interruptStreaming$ = new ReplaySubject<{
reason: string;
messageId?: string;
}>();
const guardrailsResult$ = new ReplaySubject<GuardrailsResult>();
let outputMessages: Message[] = [];
let resolveOutputMessagesPromise: (messages: Message[]) => void;
let rejectOutputMessagesPromise: (err: Error) => void;
const outputMessagesPromise = new Promise<Message[]>((resolve, reject) => {
resolveOutputMessagesPromise = resolve;
rejectOutputMessagesPromise = reject;
});
if (copilotCloudPublicApiKey) {
ctx.properties["copilotCloudPublicApiKey"] = copilotCloudPublicApiKey;
}
logger.debug("Processing");
let runtimeResponse;
const {
eventSource,
threadId = randomId(),
runId,
serverSideActions,
actionInputsWithoutAgents,
extensions,
} = runtimeResponse;
logger.debug("Event source created, creating response");
// run and process the event stream
const eventStream = eventSource
.processRuntimeEvents({
serverSideActions,
guardrailsResult$: data.cloud?.guardrails ? guardrailsResult$ : null,
actionInputsWithoutAgents: actionInputsWithoutAgents.filter(
// TODO-AGENTS: do not exclude ALL server side actions
(action) =>
!serverSideActions.find(
(serverSideAction) => serverSideAction.name == action.name,
),
),
threadId,
})
.pipe(
// shareReplay() ensures that later subscribers will see the whole stream instead of
// just the events that were emitted after the subscriber was added.
shareReplay(),
finalize(() => {
logger.debug("Event stream finalized");
}),
);
const response = {
threadId,
runId,
status: firstValueFrom(responseStatus$),
extensions,
metaEvents: new Repeater(async (push, stop) => {
let eventStreamSubscription: Subscription;
eventStreamSubscription = eventStream.subscribe({
next: async (event) => {
if (event.type != RuntimeEventTypes.MetaEvent) {
return;
}
switch (event.name) {
// @ts-ignore
case LangGraphEventTypes.OnInterrupt:
push(
plainToInstance(LangGraphInterruptEvent, {
// @ts-ignore
type: event.type,
// @ts-ignore
name: RuntimeMetaEventName.LangGraphInterruptEvent,
// @ts-ignore
value: event.value,
}),
);
break;
case RuntimeMetaEventName.LangGraphInterruptEvent:
push(
plainToInstance(LangGraphInterruptEvent, {
type: event.type,
name: event.name,
value: event.value,
}),
);
break;
case RuntimeMetaEventName.CopilotKitLangGraphInterruptEvent:
push(
plainToInstance(CopilotKitLangGraphInterruptEvent, {
type: event.type,
name: event.name,
data: {
value: event.data.value,
messages: event.data.messages.map((message) => {
if (
message.type === "TextMessage" ||
("content" in message && "role" in message)
) {
return plainToInstance(TextMessage, {
id: message.id,
createdAt: new Date(),
content: [(message as TextMessage).content],
role: (message as TextMessage).role,
status: new SuccessMessageStatus(),
});
}
if ("arguments" in message) {
return plainToInstance(ActionExecutionMessage, {
name: message.name,
id: message.id,
arguments: [JSON.stringify(message.arguments)],
createdAt: new Date(),
status: new SuccessMessageStatus(),
});
}
throw new Error(
"Unknown message in metaEvents copilot resolver",
);
}),
},
}),
);
break;
}
},
error: (err) => {
// For structured CopilotKit errors, set proper error response status
if (
err?.name?.includes("CopilotKit") ||
err?.extensions?.visibility
) {
responseStatus$.next(
new UnknownErrorResponse({
description: err.message || "Agent error occurred",
}),
);
} else {
responseStatus$.next(
new UnknownErrorResponse({
description: `An unknown error has occurred in the event stream`,
}),
);
}
eventStreamSubscription?.unsubscribe();
stop();
},
complete: async () => {
logger.debug("Meta events stream completed");
responseStatus$.next(new SuccessResponseStatus());
eventStreamSubscription?.unsubscribe();
stop();
},
});
}),
messages: new Repeater(async (pushMessage, stopStreamingMessages) => {
logger.debug("Messages repeater created");
if (data.cloud?.guardrails) {
logger = logger.child({ guardrails: true });
logger.debug("Guardrails is enabled, validating input");
invokeGuardrails({
baseUrl: copilotCloudBaseUrl,
copilotCloudPublicApiKey,
data,
onResult: (result) => {
logger.debug(
{ status: result.status },
"Guardrails validation done",
);
guardrailsResult$.next(result);
// Guardrails validation failed
if (result.status === "denied") {
// send the reason to the client and interrupt streaming
responseStatus$.next(
new GuardrailsValidationFailureResponse({
guardrailsReason: result.reason,
}),
);
interruptStreaming$.next({
reason: `Interrupted due to Guardrails validation failure. Reason: ${result.reason}`,
});
// resolve messages promise to the middleware
outputMessages = [
plainToInstance(TextMessage, {
id: randomId(),
createdAt: new Date(),
content: result.reason,
role: MessageRole.assistant,
}),
];
resolveOutputMessagesPromise(outputMessages);
}
},
onError: (err) => {
logger.error({ err }, "Error in guardrails validation");
responseStatus$.next(
new UnknownErrorResponse({
description: `An unknown error has occurred in the guardrails validation`,
}),
);
interruptStreaming$.next({
reason: `Interrupted due to unknown error in guardrails validation`,
});
// reject the middleware promise
rejectOutputMessagesPromise(err);
},
});
}
let eventStreamSubscription: Subscription;
logger.debug("Event stream created, subscribing to event stream");
eventStreamSubscription = eventStream.subscribe({
next: async (event) => {
switch (event.type) {
case RuntimeEventTypes.MetaEvent:
break;
////////////////////////////////
// TextMessageStart
////////////////////////////////
case RuntimeEventTypes.TextMessageStart:
// create a sub stream that contains the message content
const textMessageContentStream = eventStream.pipe(
// skip until this message start event
skipWhile((e: RuntimeEvent) => e !== event),
// take until the message end event
takeWhile(
(e: RuntimeEvent) =>
!(
e.type === RuntimeEventTypes.TextMessageEnd &&
(e as any).messageId == event.messageId
),
),
// filter out any other message events or message ids
filter(
(e: RuntimeEvent) =>
e.type == RuntimeEventTypes.TextMessageContent &&
(e as any).messageId == event.messageId,
),
);
// signal when we are done streaming
const streamingTextStatus = new Subject<
typeof MessageStatusUnion
>();
const messageId = resolveMessageId(event.messageId);
// push the new message
pushMessage({
id: messageId,
parentMessageId: event.parentMessageId,
status: firstValueFrom(streamingTextStatus),
createdAt: new Date(),
role: MessageRole.assistant,
content: new Repeater(
async (pushTextChunk, stopStreamingText) => {
logger.debug("Text message content repeater created");
const textChunks: string[] = [];
let textSubscription: Subscription;
interruptStreaming$
.pipe(
shareReplay(),
take(1),
tap(({ reason, messageId }) => {
logger.debug(
{ reason, messageId },
"Text streaming interrupted",
);
streamingTextStatus.next(
plainToInstance(FailedMessageStatus, { reason }),
);
responseStatus$.next(
new MessageStreamInterruptedResponse({
messageId,
}),
);
stopStreamingText();
textSubscription?.unsubscribe();
}),
)
.subscribe();
logger.debug(
"Subscribing to text message content stream",
);
textSubscription = textMessageContentStream.subscribe({
next: async (e: RuntimeEvent) => {
if (e.type == RuntimeEventTypes.TextMessageContent) {
await pushTextChunk(e.content);
textChunks.push(e.content);
}
},
error: (err) => {
logger.error(
{ err },
"Error in text message content stream",
);
interruptStreaming$.next({
reason: "Error streaming message content",
messageId,
});
stopStreamingText();
textSubscription?.unsubscribe();
},
complete: () => {
logger.debug("Text message content stream completed");
streamingTextStatus.next(new SuccessMessageStatus());
stopStreamingText();
textSubscription?.unsubscribe();
outputMessages.push(
plainToInstance(TextMessage, {
id: messageId,
createdAt: new Date(),
content: textChunks.join(""),
role: MessageRole.assistant,
}),
);
},
});
},
),
});
break;
////////////////////////////////
// ActionExecutionStart
////////////////////////////////
case RuntimeEventTypes.ActionExecutionStart:
logger.debug("Action execution start event received");
const actionExecutionArgumentStream = eventStream.pipe(
skipWhile((e: RuntimeEvent) => e !== event),
// take until the action execution end event
takeWhile(
(e: RuntimeEvent) =>
!(
e.type === RuntimeEventTypes.ActionExecutionEnd &&
(e as any).actionExecutionId == event.actionExecutionId
),
),
// filter out any other action execution events or action execution ids
filter(
(e: RuntimeEvent) =>
e.type == RuntimeEventTypes.ActionExecutionArgs &&
(e as any).actionExecutionId == event.actionExecutionId,
),
);
const streamingArgumentsStatus = new Subject<
typeof MessageStatusUnion
>();
pushMessage({
id: event.actionExecutionId,
parentMessageId: event.parentMessageId,
status: firstValueFrom(streamingArgumentsStatus),
createdAt: new Date(),
name: event.actionName,
arguments: new Repeater(
async (pushArgumentsChunk, stopStreamingArguments) => {
logger.debug("Action execution argument stream created");
const argumentChunks: string[] = [];
let actionExecutionArgumentSubscription: Subscription;
actionExecutionArgumentSubscription =
actionExecutionArgumentStream.subscribe({
next: async (e: RuntimeEvent) => {
if (
e.type == RuntimeEventTypes.ActionExecutionArgs
) {
await pushArgumentsChunk(e.args);
argumentChunks.push(e.args);
}
},
error: (err) => {
logger.error(
{ err },
"Error in action execution argument stream",
);
streamingArgumentsStatus.next(
plainToInstance(FailedMessageStatus, {
reason:
"An unknown error has occurred in the action execution argument stream",
}),
);
stopStreamingArguments();
actionExecutionArgumentSubscription?.unsubscribe();
},
complete: () => {
logger.debug(
"Action execution argument stream completed",
);
streamingArgumentsStatus.next(
new SuccessMessageStatus(),
);
stopStreamingArguments();
actionExecutionArgumentSubscription?.unsubscribe();
outputMessages.push(
plainToInstance(ActionExecutionMessage, {
id: event.actionExecutionId,
createdAt: new Date(),
name: event.actionName,
arguments: argumentChunks.join(""),
}),
);
},
});
},
),
});
break;
////////////////////////////////
// ActionExecutionResult
////////////////////////////////
case RuntimeEventTypes.ActionExecutionResult:
logger.debug(
{ result: event.result },
"Action execution result event received",
);
pushMessage({
id: "result-" + event.actionExecutionId,
status: new SuccessMessageStatus(),
createdAt: new Date(),
actionExecutionId: event.actionExecutionId,
actionName: event.actionName,
result: event.result,
});
outputMessages.push(
plainToInstance(ResultMessage, {
id: "result-" + event.actionExecutionId,
createdAt: new Date(),
actionExecutionId: event.actionExecutionId,
actionName: event.actionName,
result: event.result,
}),
);
break;
////////////////////////////////
// AgentStateMessage
////////////////////////////////
case RuntimeEventTypes.AgentStateMessage:
logger.debug({ event }, "Agent message event received");
pushMessage({
id: randomId(),
status: new SuccessMessageStatus(),
threadId: event.threadId,
agentName: event.agentName,
nodeName: event.nodeName,
runId: event.runId,
active: event.active,
state: event.state,
running: event.running,
role: MessageRole.assistant,
createdAt: new Date(),
});
outputMessages.push(
plainToInstance(AgentStateMessage, {
id: randomId(),
threadId: event.threadId,
agentName: event.agentName,
nodeName: event.nodeName,
runId: event.runId,
active: event.active,
state: event.state,
running: event.running,
role: MessageRole.assistant,
createdAt: new Date(),
}),
);
break;
}
},
error: (err) => {
// For structured CopilotKit errors, set proper error response status
if (
err instanceof CopilotKitError ||
err instanceof CopilotKitLowLevelError ||
(err instanceof Error &&
err.name &&
err.name.includes("CopilotKit")) ||
err?.extensions?.visibility
) {
responseStatus$.next(
new UnknownErrorResponse({
description: err.message || "Agent error occurred",
// Include original error information for frontend to extract
originalError: {
code: err.code || err.extensions?.code,
statusCode: err.statusCode || err.extensions?.statusCode,
severity: err.severity || err.extensions?.severity,
visibility: err.visibility || err.extensions?.visibility,
originalErrorType:
err.originalErrorType ||
err.extensions?.originalErrorType,
extensions: err.extensions,
},
}),
);
eventStreamSubscription?.unsubscribe();
rejectOutputMessagesPromise(err);
stopStreamingMessages();
return;
}
responseStatus$.next(
new UnknownErrorResponse({
description: `An unknown error has occurred in the event stream`,
}),
);
eventStreamSubscription?.unsubscribe();
stopStreamingMessages();
rejectOutputMessagesPromise(err);
},
complete: async () => {
logger.debug("Event stream completed");
if (data.cloud?.guardrails) {
logger.debug(
"Guardrails is enabled, waiting for guardrails result",
);
await firstValueFrom(guardrailsResult$);
}
responseStatus$.next(new SuccessResponseStatus());
eventStreamSubscription?.unsubscribe();
stopStreamingMessages();
resolveOutputMessagesPromise(outputMessages);
},
});
}),
};
return response;
}
}
@@ -0,0 +1,14 @@
import { randomId } from "@copilotkit/shared";
/**
* Resolve the id to use for a streamed TextMessageOutput.
*
* Upstream events occasionally arrive with `messageId` missing, null, or an
* empty string. We fall back to a freshly generated id in those cases so the
* resulting output never surfaces a null id to the GraphQL client (#2118).
*/
export function resolveMessageId(
eventMessageId: string | null | undefined,
): string {
return eventMessageId || randomId();
}
@@ -0,0 +1,30 @@
import { Arg, Resolver } from "type-graphql";
import { Ctx } from "type-graphql";
import { Query } from "type-graphql";
import { LoadAgentStateResponse } from "../types/load-agent-state-response.type";
import type { GraphQLContext } from "../../lib/integrations";
import { LoadAgentStateInput } from "../inputs/load-agent-state.input";
import { CopilotKitAgentDiscoveryError } from "@copilotkit/shared";
import { CopilotRuntime } from "../../lib";
@Resolver(() => LoadAgentStateResponse)
export class StateResolver {
@Query(() => LoadAgentStateResponse)
async loadAgentState(
@Ctx() ctx: GraphQLContext,
@Arg("data") data: LoadAgentStateInput,
) {
const agents = [];
const hasAgent = agents.some((agent) => agent.name === data.agentName);
if (!hasAgent) {
throw new CopilotKitAgentDiscoveryError({
agentName: data.agentName,
availableAgents: agents.map((a) => ({ name: a.name, id: a.name })),
});
}
const state = {};
return state;
}
}
@@ -0,0 +1,19 @@
import { Field, ObjectType } from "type-graphql";
@ObjectType()
export class Agent {
@Field(() => String)
id: string;
@Field(() => String)
name: string;
@Field(() => String)
description?: string;
}
@ObjectType()
export class AgentsResponse {
@Field(() => [Agent])
agents: Agent[];
}
@@ -0,0 +1,10 @@
import { Field, InputType } from "type-graphql";
@InputType()
export class BaseMessageInput {
@Field(() => String)
id: string;
@Field(() => Date)
createdAt: Date;
}
@@ -0,0 +1,183 @@
import { randomId } from "@copilotkit/shared";
import {
ActionExecutionMessageInput,
ResultMessageInput,
TextMessageInput,
AgentStateMessageInput,
ImageMessageInput,
} from "../../inputs/message.input";
import { BaseMessageInput } from "../base";
import { BaseMessageOutput } from "../copilot-response.type";
import { MessageRole } from "../enums";
import { MessageStatus, MessageStatusCode } from "../message-status.type";
export type MessageType =
| "TextMessage"
| "ActionExecutionMessage"
| "ResultMessage"
| "AgentStateMessage"
| "ImageMessage";
export class Message {
type: MessageType;
id: BaseMessageOutput["id"];
createdAt: BaseMessageOutput["createdAt"];
status: MessageStatus;
constructor(props: any) {
props.id ??= randomId();
props.status ??= { code: MessageStatusCode.Success };
props.createdAt ??= new Date();
Object.assign(this, props);
}
isTextMessage(): this is TextMessage {
return this.type === "TextMessage";
}
isActionExecutionMessage(): this is ActionExecutionMessage {
return this.type === "ActionExecutionMessage";
}
isResultMessage(): this is ResultMessage {
return this.type === "ResultMessage";
}
isAgentStateMessage(): this is AgentStateMessage {
return this.type === "AgentStateMessage";
}
isImageMessage(): this is ImageMessage {
return this.type === "ImageMessage";
}
}
// alias Role to MessageRole
export const Role = MessageRole;
// when constructing any message, the base fields are optional
type MessageConstructorOptions = Partial<Message>;
type TextMessageConstructorOptions = MessageConstructorOptions &
TextMessageInput;
export class TextMessage
extends Message
implements TextMessageConstructorOptions
{
content: TextMessageInput["content"];
parentMessageId: TextMessageInput["parentMessageId"];
role: TextMessageInput["role"];
type = "TextMessage" as const;
constructor(props: TextMessageConstructorOptions) {
super(props);
this.type = "TextMessage";
}
}
export class ActionExecutionMessage
extends Message
implements Omit<ActionExecutionMessageInput, "arguments" | "scope">
{
type: MessageType = "ActionExecutionMessage";
name: string;
arguments: Record<string, any>;
parentMessageId?: string;
}
export class ResultMessage extends Message implements ResultMessageInput {
type: MessageType = "ResultMessage";
actionExecutionId: string;
actionName: string;
result: string;
static encodeResult(
result: any,
error?: { code: string; message: string } | string | Error,
): string {
const errorObj = error
? typeof error === "string"
? { code: "ERROR", message: error }
: error instanceof Error
? { code: "ERROR", message: error.message }
: error
: undefined;
if (errorObj) {
return JSON.stringify({
error: errorObj,
result: result || "",
});
}
if (result === undefined) {
return "";
}
return typeof result === "string" ? result : JSON.stringify(result);
}
static decodeResult(result: string): {
error?: { code: string; message: string };
result: string;
} {
if (!result) {
return { result: "" };
}
try {
const parsed = JSON.parse(result);
if (parsed && typeof parsed === "object") {
if ("error" in parsed) {
return {
error: parsed.error,
result: parsed.result || "",
};
}
return { result: JSON.stringify(parsed) };
}
return { result };
} catch (e) {
return { result };
}
}
hasError(): boolean {
try {
const { error } = ResultMessage.decodeResult(this.result);
return !!error;
} catch {
return false;
}
}
getError(): { code: string; message: string } | undefined {
try {
const { error } = ResultMessage.decodeResult(this.result);
return error;
} catch {
return undefined;
}
}
}
export class AgentStateMessage
extends Message
implements Omit<AgentStateMessageInput, "state">
{
type: MessageType = "AgentStateMessage";
threadId: string;
agentName: string;
nodeName: string;
runId: string;
active: boolean;
role: MessageRole;
state: any;
running: boolean;
}
export class ImageMessage extends Message implements ImageMessageInput {
type: MessageType = "ImageMessage";
format: string;
bytes: string;
role: MessageRole;
parentMessageId?: string;
}
@@ -0,0 +1,141 @@
import { Field, InterfaceType, ObjectType } from "type-graphql";
import { MessageRole } from "./enums";
import { MessageStatusUnion } from "./message-status.type";
import { ResponseStatusUnion } from "./response-status.type";
import { ExtensionsResponse } from "./extensions-response.type";
import { BaseMetaEvent } from "./meta-events.type";
@InterfaceType({
resolveType(value) {
if (value.hasOwnProperty("content")) {
return TextMessageOutput;
} else if (value.hasOwnProperty("name")) {
return ActionExecutionMessageOutput;
} else if (value.hasOwnProperty("result")) {
return ResultMessageOutput;
} else if (value.hasOwnProperty("state")) {
return AgentStateMessageOutput;
} else if (
value.hasOwnProperty("format") &&
value.hasOwnProperty("bytes")
) {
return ImageMessageOutput;
}
return undefined;
},
})
export abstract class BaseMessageOutput {
@Field(() => String)
id: string;
@Field(() => Date)
createdAt: Date;
@Field(() => MessageStatusUnion)
status: typeof MessageStatusUnion;
}
@ObjectType({ implements: BaseMessageOutput })
export class TextMessageOutput {
@Field(() => MessageRole)
role: MessageRole;
@Field(() => [String])
content: string[];
@Field(() => String, { nullable: true })
parentMessageId?: string;
}
@ObjectType({ implements: BaseMessageOutput })
export class ActionExecutionMessageOutput {
@Field(() => String)
name: string;
@Field(() => String, {
nullable: true,
deprecationReason: "This field will be removed in a future version",
})
scope?: string;
@Field(() => [String])
arguments: string[];
@Field(() => String, { nullable: true })
parentMessageId?: string;
}
@ObjectType({ implements: BaseMessageOutput })
export class ResultMessageOutput {
@Field(() => String)
actionExecutionId: string;
@Field(() => String)
actionName: string;
@Field(() => String)
result: string;
}
@ObjectType({ implements: BaseMessageOutput })
export class AgentStateMessageOutput {
@Field(() => String)
threadId: string;
@Field(() => String)
agentName: string;
@Field(() => String)
nodeName: string;
@Field(() => String)
runId: string;
@Field(() => Boolean)
active: boolean;
@Field(() => MessageRole)
role: MessageRole;
@Field(() => String)
state: string;
@Field(() => Boolean)
running: boolean;
}
@ObjectType({ implements: BaseMessageOutput })
export class ImageMessageOutput {
@Field(() => String)
format: string;
@Field(() => String)
bytes: string;
@Field(() => MessageRole)
role: MessageRole;
@Field(() => String, { nullable: true })
parentMessageId?: string;
}
@ObjectType()
export class CopilotResponse {
@Field(() => String)
threadId!: string;
@Field(() => ResponseStatusUnion)
status: typeof ResponseStatusUnion;
@Field({ nullable: true })
runId?: string;
@Field(() => [BaseMessageOutput])
messages: (typeof BaseMessageOutput)[];
@Field(() => ExtensionsResponse, { nullable: true })
extensions?: ExtensionsResponse;
@Field(() => [BaseMetaEvent], { nullable: true })
metaEvents?: (typeof BaseMetaEvent)[];
}
@@ -0,0 +1,38 @@
import { registerEnumType } from "type-graphql";
export enum MessageRole {
assistant = "assistant",
developer = "developer",
system = "system",
tool = "tool",
user = "user",
}
export enum CopilotRequestType {
Chat = "Chat",
Task = "Task",
TextareaCompletion = "TextareaCompletion",
TextareaPopover = "TextareaPopover",
Suggestion = "Suggestion",
}
export enum ActionInputAvailability {
disabled = "disabled",
enabled = "enabled",
remote = "remote",
}
registerEnumType(MessageRole, {
name: "MessageRole",
description: "The role of the message",
});
registerEnumType(CopilotRequestType, {
name: "CopilotRequestType",
description: "The type of Copilot request",
});
registerEnumType(ActionInputAvailability, {
name: "ActionInputAvailability",
description: "The availability of the frontend action",
});
@@ -0,0 +1,23 @@
import { Field, ObjectType } from "type-graphql";
/**
* The extensions response is used to receive additional information from the copilot runtime, specific to a
* service adapter or agent framework.
*
* Next time a request to the runtime is made, the extensions response will be included in the request as input.
*/
@ObjectType()
export class OpenAIApiAssistantAPIResponse {
@Field(() => String, { nullable: true })
runId?: string;
@Field(() => String, { nullable: true })
threadId?: string;
}
@ObjectType()
export class ExtensionsResponse {
@Field(() => OpenAIApiAssistantAPIResponse, { nullable: true })
openaiAssistantAPI?: OpenAIApiAssistantAPIResponse;
}
@@ -0,0 +1,20 @@
import { Field, ObjectType, registerEnumType } from "type-graphql";
export enum GuardrailsResultStatus {
ALLOWED = "allowed",
DENIED = "denied",
}
registerEnumType(GuardrailsResultStatus, {
name: "GuardrailsResultStatus",
description: "The status of the guardrails check",
});
@ObjectType()
export class GuardrailsResult {
@Field(() => GuardrailsResultStatus)
status: GuardrailsResultStatus;
@Field(() => String, { nullable: true })
reason?: string;
}
@@ -0,0 +1,17 @@
import { Field, ObjectType } from "type-graphql";
import { BaseMessageOutput } from "./copilot-response.type";
@ObjectType()
export class LoadAgentStateResponse {
@Field(() => String)
threadId: string;
@Field(() => Boolean)
threadExists: boolean;
@Field(() => String)
state: string;
@Field(() => String)
messages: string;
}
@@ -0,0 +1,48 @@
import {
Field,
ObjectType,
createUnionType,
registerEnumType,
} from "type-graphql";
export enum MessageStatusCode {
Pending = "pending",
Success = "success",
Failed = "failed",
}
registerEnumType(MessageStatusCode, {
name: "MessageStatusCode",
});
@ObjectType()
export class BaseMessageStatus {
@Field(() => MessageStatusCode)
code: MessageStatusCode;
}
@ObjectType()
export class PendingMessageStatus extends BaseMessageStatus {
code: MessageStatusCode = MessageStatusCode.Pending;
}
@ObjectType()
export class SuccessMessageStatus extends BaseMessageStatus {
code: MessageStatusCode = MessageStatusCode.Success;
}
@ObjectType()
export class FailedMessageStatus extends BaseMessageStatus {
code: MessageStatusCode = MessageStatusCode.Failed;
@Field(() => String)
reason: string;
}
export const MessageStatusUnion = createUnionType({
name: "MessageStatus",
types: () =>
[PendingMessageStatus, SuccessMessageStatus, FailedMessageStatus] as const,
});
export type MessageStatus = typeof MessageStatusUnion;
@@ -0,0 +1,78 @@
import {
createUnionType,
Field,
InterfaceType,
ObjectType,
registerEnumType,
} from "type-graphql";
import {
ActionExecutionMessageOutput,
AgentStateMessageOutput,
BaseMessageOutput,
ResultMessageOutput,
TextMessageOutput,
} from "./copilot-response.type";
export enum MetaEventName {
LangGraphInterruptEvent = "LangGraphInterruptEvent",
CopilotKitLangGraphInterruptEvent = "CopilotKitLangGraphInterruptEvent",
}
registerEnumType(MetaEventName, {
name: "MetaEventName",
description: "Meta event types",
});
@InterfaceType({
resolveType(value) {
if (value.name === MetaEventName.LangGraphInterruptEvent) {
return LangGraphInterruptEvent;
} else if (value.name === MetaEventName.CopilotKitLangGraphInterruptEvent) {
return CopilotKitLangGraphInterruptEvent;
}
return undefined;
},
})
@InterfaceType()
export abstract class BaseMetaEvent {
@Field(() => String)
type: "MetaEvent" = "MetaEvent";
@Field(() => MetaEventName)
name: MetaEventName;
}
@ObjectType()
export class CopilotKitLangGraphInterruptEventData {
@Field(() => String)
value: string;
@Field(() => [BaseMessageOutput])
messages: (typeof BaseMessageOutput)[];
}
@ObjectType({ implements: BaseMetaEvent })
export class LangGraphInterruptEvent {
@Field(() => MetaEventName)
name: MetaEventName.LangGraphInterruptEvent =
MetaEventName.LangGraphInterruptEvent;
@Field(() => String)
value: string;
@Field(() => String, { nullable: true })
response?: string;
}
@ObjectType({ implements: BaseMetaEvent })
export class CopilotKitLangGraphInterruptEvent {
@Field(() => MetaEventName)
name: MetaEventName.CopilotKitLangGraphInterruptEvent =
MetaEventName.CopilotKitLangGraphInterruptEvent;
@Field(() => CopilotKitLangGraphInterruptEventData)
data: CopilotKitLangGraphInterruptEventData;
@Field(() => String, { nullable: true })
response?: string;
}
@@ -0,0 +1,77 @@
import { GraphQLJSON } from "graphql-scalars";
import {
Field,
InterfaceType,
ObjectType,
createUnionType,
registerEnumType,
} from "type-graphql";
export enum ResponseStatusCode {
Pending = "pending",
Success = "success",
Failed = "failed",
}
registerEnumType(ResponseStatusCode, {
name: "ResponseStatusCode",
});
@InterfaceType({
resolveType(value) {
if (value.code === ResponseStatusCode.Success) {
return SuccessResponseStatus;
} else if (value.code === ResponseStatusCode.Failed) {
return FailedResponseStatus;
} else if (value.code === ResponseStatusCode.Pending) {
return PendingResponseStatus;
}
return undefined;
},
})
@ObjectType()
abstract class BaseResponseStatus {
@Field(() => ResponseStatusCode)
code: ResponseStatusCode;
}
@ObjectType({ implements: BaseResponseStatus })
export class PendingResponseStatus extends BaseResponseStatus {
code: ResponseStatusCode = ResponseStatusCode.Pending;
}
@ObjectType({ implements: BaseResponseStatus })
export class SuccessResponseStatus extends BaseResponseStatus {
code: ResponseStatusCode = ResponseStatusCode.Success;
}
export enum FailedResponseStatusReason {
GUARDRAILS_VALIDATION_FAILED = "GUARDRAILS_VALIDATION_FAILED",
MESSAGE_STREAM_INTERRUPTED = "MESSAGE_STREAM_INTERRUPTED",
UNKNOWN_ERROR = "UNKNOWN_ERROR",
}
registerEnumType(FailedResponseStatusReason, {
name: "FailedResponseStatusReason",
});
@ObjectType({ implements: BaseResponseStatus })
export class FailedResponseStatus extends BaseResponseStatus {
code: ResponseStatusCode = ResponseStatusCode.Failed;
@Field(() => FailedResponseStatusReason)
reason: FailedResponseStatusReason;
@Field(() => GraphQLJSON, { nullable: true })
details?: Record<string, any> = null;
}
export const ResponseStatusUnion = createUnionType({
name: "ResponseStatus",
types: () =>
[
PendingResponseStatus,
SuccessResponseStatus,
FailedResponseStatus,
] as const,
});