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
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
# CopilotKit - Runtime Client GQL
<img src="https://github.com/user-attachments/assets/0a6b64d9-e193-4940-a3f6-60334ac34084" alt="banner" style="border-radius: 12px; border: 2px solid #d6d4fa;" />
<br>
<div align="center" style="display:flex;justify-content:center;gap:16px;height:20px;margin: 0;">
<a href="https://www.npmjs.com/package/@copilotkit/react-core" target="_blank">
<img src="https://img.shields.io/npm/v/%40copilotkit%2Fruntime-client-gql?logo=npm&logoColor=%23FFFFFF&label=Version&color=%236963ff" alt="NPM">
</a>
<a href="https://github.com/copilotkit/copilotkit/blob/main/LICENSE" target="_blank">
<img src="https://img.shields.io/github/license/copilotkit/copilotkit?color=%236963ff&label=License" alt="MIT">
</a>
<a href="https://discord.gg/6dffbvGU3D" target="_blank">
<img src="https://img.shields.io/discord/1122926057641742418?logo=discord&logoColor=%23FFFFFF&label=Discord&color=%236963ff" alt="Discord">
</a>
</div>
<br/>
<div align="center">
<a href="https://www.producthunt.com/posts/copilotkit" target="_blank">
<img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=428778&theme=light&period=daily">
</a>
</div>
## ✨ Why CopilotKit?
- Minutes to integrate - Get started quickly with our CLI
- Framework agnostic - Works with React, Next.js, AGUI and more
- Production-ready UI - Use customizable components or build with headless UI
- Built-in security - Prompt injection protection
- Open source - Full transparency and community-driven
<img src="https://github.com/user-attachments/assets/6cb425f8-ffcb-49d2-9bbb-87cab5995b78" alt="class-support-ecosystem" style="border-radius: 12px; border: 2px solid #d6d4fa;">
## 🧑‍💻 Real life use cases
<span>Deploy deeply-integrated AI assistants & agents that work alongside your users inside your applications.</span>
<img src="https://github.com/user-attachments/assets/3b810240-e9f8-43ae-acec-31a58095e223" alt="headless-ui" style="border-radius: 12px; border: 2px solid #d6d4fa;">
## 🏆 Featured Examples
<p align="center">
<a href="https://www.copilotkit.ai/examples/form-filling-copilot">
<img src="https://github.com/user-attachments/assets/874da84a-67ff-47fa-a6b4-cbc3c65eb704" width="300" style="border-radius: 16px;" />
</a>
<a href="https://www.copilotkit.ai/examples/state-machine-copilot">
<img src="https://github.com/user-attachments/assets/0b5e45b3-2704-4678-82dc-2f3e1c58e2dd" width="300" style="border-radius: 16px;" />
</a>
<a href="https://www.copilotkit.ai/examples/chat-with-your-data">
<img src="https://github.com/user-attachments/assets/0fed66be-a4c2-4093-8eab-75c0b27a62f6" width="300" style="border-radius: 16px;" />
</a>
</p>
## Documentation
To get started with CopilotKit, please check out the [documentation](https://docs.copilotkit.ai).
+25
View File
@@ -0,0 +1,25 @@
import type { CodegenConfig } from "@graphql-codegen/cli";
import path from "node:path";
const schema = path.resolve(
__dirname,
"../runtime/__snapshots__/schema/schema.graphql",
);
const config: CodegenConfig = {
schema,
documents: ["src/graphql/definitions/**/*.{ts,tsx}"],
generates: {
"./src/graphql/@generated/": {
preset: "client",
config: {
useTypeImports: true,
withHooks: false,
},
plugins: [],
},
},
hooks: {},
};
export default config;
+81
View File
@@ -0,0 +1,81 @@
{
"name": "@copilotkit/runtime-client-gql",
"version": "1.62.3",
"private": false,
"keywords": [
"ai",
"assistant",
"automation",
"copilot",
"copilotkit",
"javascript",
"nextjs",
"nodejs",
"react",
"textarea"
],
"homepage": "https://github.com/CopilotKit/CopilotKit",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/CopilotKit/CopilotKit.git"
},
"sideEffects": false,
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.cts",
"unpkg": "./dist/index.umd.js",
"jsdelivr": "./dist/index.umd.js",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./package.json": "./package.json"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "pnpm run graphql-codegen && tsdown",
"compat-check": "es-check es2022 --module 'dist/**/!(*.umd).{mjs,cjs,js}' && es-check es2018 'dist/**/*.umd.js'",
"dev": "concurrently \"pnpm run graphql-codegen:watch\" \"tsdown --watch\"",
"test": "pnpm run graphql-codegen && vitest run",
"test:watch": "pnpm run graphql-codegen && vitest",
"check-types": "tsc --noEmit",
"graphql-codegen": "graphql-codegen -c codegen.ts",
"graphql-codegen:watch": "graphql-codegen -c codegen.ts --watch",
"link:global": "pnpm link --global",
"unlink:global": "pnpm unlink --global",
"publint": "publint .",
"attw": "attw --pack . --profile node16"
},
"dependencies": {
"@copilotkit/shared": "workspace:*",
"@urql/core": "^5.0.3",
"untruncate-json": "^0.0.1",
"urql": "^4.1.0"
},
"devDependencies": {
"@graphql-codegen/cli": "^5.0.2",
"@graphql-codegen/client-preset": "^4.2.6",
"@graphql-codegen/introspection": "^4.0.3",
"@graphql-codegen/typescript": "^4.0.7",
"@graphql-codegen/typescript-operations": "^4.2.1",
"@graphql-codegen/typescript-urql": "^4.0.0",
"@graphql-codegen/urql-introspection": "^3.0.0",
"@graphql-typed-document-node/core": "^3.2.0",
"@parcel/watcher": "^2.4.1",
"@types/node": "^20.12.12",
"concurrently": "^8.2.2",
"esbuild": "^0.23.0",
"graphql": "^16.8.1",
"ts-node": "^10.9.2",
"tsdown": "^0.20.3",
"typescript": "^5.4.5",
"vitest": "^3.2.4"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc"
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"name": "@copilotkit/runtime-client-gql",
"implicitDependencies": ["@copilotkit/runtime"]
}
@@ -0,0 +1,231 @@
import { Client, cacheExchange, fetchExchange } from "@urql/core";
import * as packageJson from "../../package.json";
import {
AvailableAgentsQuery,
GenerateCopilotResponseMutation,
GenerateCopilotResponseMutationVariables,
LoadAgentStateQuery,
} from "../graphql/@generated/graphql";
import { generateCopilotResponseMutation } from "../graphql/definitions/mutations";
import {
getAvailableAgentsQuery,
loadAgentStateQuery,
} from "../graphql/definitions/queries";
import { OperationResultSource, OperationResult } from "urql";
import {
ResolvedCopilotKitError,
CopilotKitLowLevelError,
CopilotKitError,
CopilotKitVersionMismatchError,
getPossibleVersionMismatch,
} from "@copilotkit/shared";
const createFetchFn =
(signal?: AbortSignal, handleGQLWarning?: (warning: string) => void) =>
async (...args: Parameters<typeof fetch>) => {
// @ts-expect-error -- since this is our own header, TS will not recognize
const publicApiKey = args[1]?.headers?.["x-copilotcloud-public-api-key"];
try {
const result = await fetch(args[0], { ...args[1], signal });
// No mismatch checking if cloud is being used
const mismatch = publicApiKey
? null
: await getPossibleVersionMismatch({
runtimeVersion: result.headers.get("X-CopilotKit-Runtime-Version")!,
runtimeClientGqlVersion: packageJson.version,
});
if (result.status !== 200) {
if (result.status >= 400 && result.status <= 500) {
if (mismatch) {
throw new CopilotKitVersionMismatchError(mismatch);
}
throw new ResolvedCopilotKitError({ status: result.status });
}
}
if (mismatch && handleGQLWarning) {
handleGQLWarning(mismatch.message);
}
return result;
} catch (error) {
// Let abort error pass through. It will be suppressed later
if (
(error as Error).message.includes("BodyStreamBuffer was aborted") ||
(error as Error).message.includes("signal is aborted without reason")
) {
throw error;
}
if (error instanceof CopilotKitError) {
throw error;
}
throw new CopilotKitLowLevelError({
error: error as Error,
url: args[0] as string,
});
}
};
export interface CopilotRuntimeClientOptions {
url: string;
publicApiKey?: string;
headers?: Record<string, string>;
credentials?: RequestCredentials;
handleGQLErrors?: (error: Error) => void;
handleGQLWarning?: (warning: string) => void;
}
export class CopilotRuntimeClient {
client: Client;
public handleGQLErrors?: (error: Error) => void;
public handleGQLWarning?: (warning: string) => void;
constructor(options: CopilotRuntimeClientOptions) {
const headers: Record<string, string> = {};
this.handleGQLErrors = options.handleGQLErrors;
this.handleGQLWarning = options.handleGQLWarning;
if (options.headers) {
Object.assign(headers, options.headers);
}
if (options.publicApiKey) {
headers["x-copilotcloud-public-api-key"] = options.publicApiKey;
}
this.client = new Client({
url: options.url,
exchanges: [cacheExchange, fetchExchange],
fetchOptions: {
headers: {
...headers,
"X-CopilotKit-Runtime-Client-GQL-Version": packageJson.version,
},
...(options.credentials ? { credentials: options.credentials } : {}),
},
});
}
generateCopilotResponse({
data,
properties,
signal,
}: {
data: GenerateCopilotResponseMutationVariables["data"];
properties?: GenerateCopilotResponseMutationVariables["properties"];
signal?: AbortSignal;
}) {
const fetchFn = createFetchFn(signal, this.handleGQLWarning);
const result = this.client.mutation<
GenerateCopilotResponseMutation,
GenerateCopilotResponseMutationVariables
>(
generateCopilotResponseMutation,
{ data, properties },
{ fetch: fetchFn },
);
return result;
}
public asStream<S, T>(
source: OperationResultSource<OperationResult<S, { data: T }>>,
) {
const handleGQLErrors = this.handleGQLErrors;
return new ReadableStream<S>({
start(controller) {
source.subscribe(({ data, hasNext, error }) => {
if (error) {
if (
error.message.includes("BodyStreamBuffer was aborted") ||
error.message.includes("signal is aborted without reason")
) {
// close the stream if there is no next item
if (!hasNext) controller.close();
//suppress this specific error
console.warn("Abort error suppressed");
return;
}
// Handle structured errors specially - check if it's a CopilotKitError with visibility
if ((error as any).extensions?.visibility) {
// Create a synthetic GraphQL error with the structured error info
const syntheticError = {
...error,
graphQLErrors: [
{
message: error.message,
extensions: (error as any).extensions,
},
],
};
if (handleGQLErrors) {
handleGQLErrors(syntheticError);
}
return; // Don't close the stream for structured errors, let the error handler decide
}
controller.error(error);
if (handleGQLErrors) {
handleGQLErrors(error);
}
} else {
controller.enqueue(data);
if (!hasNext) {
controller.close();
}
}
});
},
});
}
availableAgents() {
const fetchFn = createFetchFn();
return this.client.query<AvailableAgentsQuery>(
getAvailableAgentsQuery,
{},
{ fetch: fetchFn },
);
}
loadAgentState(data: { threadId: string; agentName: string }) {
const fetchFn = createFetchFn();
const result = this.client.query<LoadAgentStateQuery>(
loadAgentStateQuery,
{ data },
{ fetch: fetchFn },
);
// Add error handling for GraphQL errors - similar to generateCopilotResponse
result
.toPromise()
.then(({ error }) => {
if (error && this.handleGQLErrors) {
this.handleGQLErrors(error);
}
})
.catch(() => {}); // Suppress promise rejection warnings
return result;
}
static removeGraphQLTypename(data: any) {
if (Array.isArray(data)) {
data.forEach((item) => CopilotRuntimeClient.removeGraphQLTypename(item));
} else if (typeof data === "object" && data !== null) {
delete data.__typename;
Object.keys(data).forEach((key) => {
if (typeof data[key] === "object" && data[key] !== null) {
CopilotRuntimeClient.removeGraphQLTypename(data[key]);
}
});
}
return data;
}
}
@@ -0,0 +1,64 @@
import { describe, it, expect } from "vitest";
import { convertGqlOutputToMessages } from "../conversion";
describe("getPartialArguments non-object guard", () => {
function makeActionOutput(argsFragments: string[]) {
return [
{
__typename: "ActionExecutionMessageOutput" as const,
id: "msg-1",
name: "myAction",
arguments: argsFragments,
parentMessageId: undefined,
status: { code: "Pending" },
},
];
}
it("passes through valid object arguments", () => {
const messages = convertGqlOutputToMessages(
makeActionOutput(['{"key":"val"}']) as any,
);
expect((messages[0] as any).arguments).toEqual({ key: "val" });
});
it("replaces a string argument with an empty object", () => {
const messages = convertGqlOutputToMessages(
makeActionOutput(['""']) as any,
);
expect((messages[0] as any).arguments).toEqual({});
});
it("replaces an array argument with an empty object", () => {
const messages = convertGqlOutputToMessages(
makeActionOutput(["[1,2]"]) as any,
);
expect((messages[0] as any).arguments).toEqual({});
});
it("replaces null with an empty object", () => {
const messages = convertGqlOutputToMessages(
makeActionOutput(["null"]) as any,
);
expect((messages[0] as any).arguments).toEqual({});
});
it("replaces a number with an empty object", () => {
const messages = convertGqlOutputToMessages(
makeActionOutput(["99"]) as any,
);
expect((messages[0] as any).arguments).toEqual({});
});
it("returns empty object for empty arguments array", () => {
const messages = convertGqlOutputToMessages(makeActionOutput([]) as any);
expect((messages[0] as any).arguments).toEqual({});
});
it("returns empty object for unparseable JSON", () => {
const messages = convertGqlOutputToMessages(
makeActionOutput(["{broken"]) as any,
);
expect((messages[0] as any).arguments).toEqual({});
});
});
@@ -0,0 +1,262 @@
import {
GenerateCopilotResponseMutation,
MessageInput,
MessageStatusCode,
} from "../graphql/@generated/graphql";
import {
ActionExecutionMessage,
AgentStateMessage,
Message,
ResultMessage,
TextMessage,
ImageMessage,
} from "./types";
import untruncateJson from "untruncate-json";
import { parseJson } from "@copilotkit/shared";
export function filterAgentStateMessages(messages: Message[]): Message[] {
return messages.filter((message) => !message.isAgentStateMessage());
}
export function convertMessagesToGqlInput(messages: Message[]): MessageInput[] {
return messages.map((message) => {
if (message.isTextMessage()) {
return {
id: message.id,
createdAt: message.createdAt,
textMessage: {
content: message.content,
role: message.role as any,
parentMessageId: message.parentMessageId,
},
};
} else if (message.isActionExecutionMessage()) {
return {
id: message.id,
createdAt: message.createdAt,
actionExecutionMessage: {
name: message.name,
arguments: JSON.stringify(message.arguments),
parentMessageId: message.parentMessageId,
},
};
} else if (message.isResultMessage()) {
return {
id: message.id,
createdAt: message.createdAt,
resultMessage: {
result: message.result,
actionExecutionId: message.actionExecutionId,
actionName: message.actionName,
},
};
} else if (message.isAgentStateMessage()) {
return {
id: message.id,
createdAt: message.createdAt,
agentStateMessage: {
threadId: message.threadId,
role: message.role,
agentName: message.agentName,
nodeName: message.nodeName,
runId: message.runId,
active: message.active,
running: message.running,
state: JSON.stringify(message.state),
},
};
} else if (message.isImageMessage()) {
return {
id: message.id,
createdAt: message.createdAt,
imageMessage: {
format: message.format,
bytes: message.bytes,
role: message.role as any,
parentMessageId: message.parentMessageId,
},
};
} else {
throw new Error("Unknown message type");
}
});
}
export function filterAdjacentAgentStateMessages(
messages: GenerateCopilotResponseMutation["generateCopilotResponse"]["messages"],
): GenerateCopilotResponseMutation["generateCopilotResponse"]["messages"] {
const filteredMessages: GenerateCopilotResponseMutation["generateCopilotResponse"]["messages"] =
[];
messages.forEach((message, i) => {
// keep all other message types
if (message.__typename !== "AgentStateMessageOutput") {
filteredMessages.push(message);
} else {
const prevAgentStateMessageIndex = filteredMessages.findIndex(
// TODO: also check runId
(m) =>
m.__typename === "AgentStateMessageOutput" &&
m.agentName === message.agentName,
);
if (prevAgentStateMessageIndex === -1) {
filteredMessages.push(message);
} else {
filteredMessages[prevAgentStateMessageIndex] = message;
}
}
});
return filteredMessages;
}
export function convertGqlOutputToMessages(
messages: GenerateCopilotResponseMutation["generateCopilotResponse"]["messages"],
): Message[] {
return messages.map((message) => {
if (message.__typename === "TextMessageOutput") {
return new TextMessage({
id: message.id,
role: message.role,
content: message.content.join(""),
parentMessageId: message.parentMessageId,
createdAt: new Date(),
status: message.status || { code: MessageStatusCode.Pending },
});
} else if (message.__typename === "ActionExecutionMessageOutput") {
return new ActionExecutionMessage({
id: message.id,
name: message.name,
arguments: getPartialArguments(message.arguments),
parentMessageId: message.parentMessageId,
createdAt: new Date(),
status: message.status || { code: MessageStatusCode.Pending },
});
} else if (message.__typename === "ResultMessageOutput") {
return new ResultMessage({
id: message.id,
result: message.result,
actionExecutionId: message.actionExecutionId,
actionName: message.actionName,
createdAt: new Date(),
status: message.status || { code: MessageStatusCode.Pending },
});
} else if (message.__typename === "AgentStateMessageOutput") {
return new AgentStateMessage({
id: message.id,
threadId: message.threadId,
role: message.role,
agentName: message.agentName,
nodeName: message.nodeName,
runId: message.runId,
active: message.active,
running: message.running,
state: parseJson(message.state, {}),
createdAt: new Date(),
});
} else if (message.__typename === "ImageMessageOutput") {
return new ImageMessage({
id: message.id,
format: message.format,
bytes: message.bytes,
role: message.role,
parentMessageId: message.parentMessageId,
createdAt: new Date(),
status: message.status || { code: MessageStatusCode.Pending },
});
}
throw new Error("Unknown message type");
});
}
export function loadMessagesFromJsonRepresentation(json: any[]): Message[] {
const result: Message[] = [];
for (const item of json) {
if ("content" in item) {
result.push(
new TextMessage({
id: item.id,
role: item.role,
content: item.content,
parentMessageId: item.parentMessageId,
createdAt: item.createdAt || new Date(),
status: item.status || { code: MessageStatusCode.Success },
}),
);
} else if ("arguments" in item) {
result.push(
new ActionExecutionMessage({
id: item.id,
name: item.name,
arguments: item.arguments,
parentMessageId: item.parentMessageId,
createdAt: item.createdAt || new Date(),
status: item.status || { code: MessageStatusCode.Success },
}),
);
} else if ("result" in item) {
result.push(
new ResultMessage({
id: item.id,
result: item.result,
actionExecutionId: item.actionExecutionId,
actionName: item.actionName,
createdAt: item.createdAt || new Date(),
status: item.status || { code: MessageStatusCode.Success },
}),
);
} else if ("state" in item) {
result.push(
new AgentStateMessage({
id: item.id,
threadId: item.threadId,
role: item.role,
agentName: item.agentName,
nodeName: item.nodeName,
runId: item.runId,
active: item.active,
running: item.running,
state: item.state,
createdAt: item.createdAt || new Date(),
}),
);
} else if ("format" in item && "bytes" in item) {
result.push(
new ImageMessage({
id: item.id,
format: item.format,
bytes: item.bytes,
role: item.role,
parentMessageId: item.parentMessageId,
createdAt: item.createdAt || new Date(),
status: item.status || { code: MessageStatusCode.Success },
}),
);
}
}
return result;
}
function getPartialArguments(args: string[]) {
try {
if (!args.length) return {};
const parsed = JSON.parse(untruncateJson(args.join("")));
if (
typeof parsed !== "object" ||
parsed === null ||
Array.isArray(parsed)
) {
console.warn(
`[CopilotKit] Tool arguments parsed to non-object (${typeof parsed}), falling back to empty object`,
);
return {};
}
return parsed;
} catch (e) {
// Incomplete JSON is expected during streaming — no warning needed
return {};
}
}
@@ -0,0 +1,10 @@
export * from "./CopilotRuntimeClient";
export {
convertMessagesToGqlInput,
convertGqlOutputToMessages,
filterAdjacentAgentStateMessages,
filterAgentStateMessages,
loadMessagesFromJsonRepresentation,
} from "./conversion";
export * from "./types";
export type { GraphQLError } from "graphql";
@@ -0,0 +1,191 @@
import { randomId } from "@copilotkit/shared";
import {
ActionExecutionMessageInput,
MessageRole,
MessageStatus,
ResultMessageInput,
TextMessageInput,
BaseMessageOutput,
AgentStateMessageInput,
MessageStatusCode,
LangGraphInterruptEvent as GqlLangGraphInterruptEvent,
MetaEventName,
CopilotKitLangGraphInterruptEvent as GqlCopilotKitLangGraphInterruptEvent,
ImageMessageInput,
} from "../graphql/@generated/graphql";
import { parseJson } from "@copilotkit/shared";
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";
}
}
type ActionExecutionMessageConstructorOptions = MessageConstructorOptions &
Omit<ActionExecutionMessageInput, "arguments"> & {
arguments: Record<string, any>;
};
export class ActionExecutionMessage
extends Message
implements Omit<ActionExecutionMessageInput, "arguments" | "scope">
{
name: ActionExecutionMessageInput["name"];
arguments: Record<string, any>;
parentMessageId: ActionExecutionMessageInput["parentMessageId"];
constructor(props: ActionExecutionMessageConstructorOptions) {
super(props);
this.type = "ActionExecutionMessage";
}
}
type ResultMessageConstructorOptions = MessageConstructorOptions &
ResultMessageInput;
export class ResultMessage
extends Message
implements ResultMessageConstructorOptions
{
actionExecutionId: ResultMessageInput["actionExecutionId"];
actionName: ResultMessageInput["actionName"];
result: ResultMessageInput["result"];
constructor(props: ResultMessageConstructorOptions) {
super(props);
this.type = "ResultMessage";
}
static decodeResult(result: string): any {
return parseJson(result, result);
}
static encodeResult(result: any): string {
if (result === undefined) {
return "";
} else if (typeof result === "string") {
return result;
} else {
return JSON.stringify(result);
}
}
}
export class AgentStateMessage
extends Message
implements Omit<AgentStateMessageInput, "state">
{
agentName: AgentStateMessageInput["agentName"];
state: any;
running: AgentStateMessageInput["running"];
threadId: AgentStateMessageInput["threadId"];
role: AgentStateMessageInput["role"];
nodeName: AgentStateMessageInput["nodeName"];
runId: AgentStateMessageInput["runId"];
active: AgentStateMessageInput["active"];
constructor(props: any) {
super(props);
this.type = "AgentStateMessage";
}
}
type ImageMessageConstructorOptions = MessageConstructorOptions &
ImageMessageInput;
export class ImageMessage
extends Message
implements ImageMessageConstructorOptions
{
format: ImageMessageInput["format"];
bytes: ImageMessageInput["bytes"];
role: ImageMessageInput["role"];
parentMessageId: ImageMessageInput["parentMessageId"];
constructor(props: ImageMessageConstructorOptions) {
super(props);
this.type = "ImageMessage";
}
}
export function langGraphInterruptEvent(
eventProps: Omit<LangGraphInterruptEvent, "name" | "type" | "__typename">,
): LangGraphInterruptEvent {
return {
...eventProps,
name: MetaEventName.LangGraphInterruptEvent,
type: "MetaEvent",
};
}
export type LangGraphInterruptEvent<TValue extends any = any> =
GqlLangGraphInterruptEvent & {
value: TValue;
};
type CopilotKitLangGraphInterruptEvent<TValue extends any = any> =
GqlCopilotKitLangGraphInterruptEvent & {
data: GqlCopilotKitLangGraphInterruptEvent["data"] & { value: TValue };
};
export type MetaEvent =
| LangGraphInterruptEvent
| CopilotKitLangGraphInterruptEvent;
@@ -0,0 +1,133 @@
import { graphql } from "../@generated/gql";
export const generateCopilotResponseMutation = graphql(/** GraphQL **/ `
mutation generateCopilotResponse(
$data: GenerateCopilotResponseInput!
$properties: JSONObject
) {
generateCopilotResponse(data: $data, properties: $properties) {
threadId
runId
extensions {
openaiAssistantAPI {
runId
threadId
}
}
... on CopilotResponse @defer {
status {
... on BaseResponseStatus {
code
}
... on FailedResponseStatus {
reason
details
}
}
}
messages @stream {
__typename
... on BaseMessageOutput {
id
createdAt
}
... on BaseMessageOutput @defer {
status {
... on SuccessMessageStatus {
code
}
... on FailedMessageStatus {
code
reason
}
... on PendingMessageStatus {
code
}
}
}
... on TextMessageOutput {
content @stream
role
parentMessageId
}
... on ImageMessageOutput {
format
bytes
role
parentMessageId
}
... on ActionExecutionMessageOutput {
name
arguments @stream
parentMessageId
}
... on ResultMessageOutput {
result
actionExecutionId
actionName
}
... on AgentStateMessageOutput {
threadId
state
running
agentName
nodeName
runId
active
role
}
}
metaEvents @stream {
... on LangGraphInterruptEvent {
type
name
value
}
... on CopilotKitLangGraphInterruptEvent {
type
name
data {
messages {
__typename
... on BaseMessageOutput {
id
createdAt
}
... on BaseMessageOutput @defer {
status {
... on SuccessMessageStatus {
code
}
... on FailedMessageStatus {
code
reason
}
... on PendingMessageStatus {
code
}
}
}
... on TextMessageOutput {
content
role
parentMessageId
}
... on ActionExecutionMessageOutput {
name
arguments
parentMessageId
}
... on ResultMessageOutput {
result
actionExecutionId
actionName
}
}
value
}
}
}
}
}
`);
@@ -0,0 +1,24 @@
import { graphql } from "../@generated/gql";
export const getAvailableAgentsQuery = graphql(/** GraphQL **/ `
query availableAgents {
availableAgents {
agents {
name
id
description
}
}
}
`);
export const loadAgentStateQuery = graphql(/** GraphQL **/ `
query loadAgentState($data: LoadAgentStateInput!) {
loadAgentState(data: $data) {
threadId
threadExists
state
messages
}
}
`);
+4
View File
@@ -0,0 +1,4 @@
export * from "./client";
export * from "./graphql/@generated/graphql";
export * from "./message-conversion";
export type { LangGraphInterruptEvent } from "./client";
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,355 @@
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,
});
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,297 @@
import * as gql from "../client";
import * as agui from "@copilotkit/shared";
import { MessageStatusCode } from "../graphql/@generated/graphql";
// 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 { describe, test, expect, vi } from "vitest";
import * as gql from "../client";
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: agui.Message = {
id: "img-user-1",
role: "user",
image: {
format: "png",
bytes: "userbase64data",
},
content: "", // required for type
};
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");
}
});
});
+17
View File
@@ -0,0 +1,17 @@
{
"extends": "../tsconfig/base.json",
"compilerOptions": {
"strictPropertyInitialization": false,
"resolveJsonModule": true,
"types": ["node"]
},
"include": ["."],
"exclude": [
"dist",
"build",
"node_modules",
"**/*.test.ts",
"**/*.test.tsx",
"**/__tests__/*"
]
}
@@ -0,0 +1,46 @@
import { defineConfig } from "tsdown";
export default defineConfig([
{
entry: ["src/index.ts"],
format: ["esm", "cjs"],
dts: true,
sourcemap: true,
target: "es2022",
outDir: "dist",
unbundle: true,
external: ["react", "@graphql-typed-document-node/core"],
exports: true,
},
{
entry: ["src/index.ts"],
format: ["umd"],
globalName: "CopilotKitRuntimeClientGQL",
sourcemap: true,
target: "es2018",
outDir: "dist",
external: [
"react",
"@copilotkit/shared",
"urql",
"@urql/core",
"graphql",
"@graphql-typed-document-node/core",
"untruncate-json",
],
outputOptions(options) {
options.codeSplitting = false;
options.entryFileNames = "[name].umd.js";
options.globals = {
react: "React",
"@copilotkit/shared": "CopilotKitShared",
urql: "Urql",
"@urql/core": "UrqlCore",
graphql: "GraphQL",
"@graphql-typed-document-node/core": "GraphQLTypedDocumentNode",
"untruncate-json": "untruncateJson",
};
return options;
},
},
]);
+4
View File
@@ -0,0 +1,4 @@
{
"extends": ["../../typedoc.base.json"],
"entryPoints": ["src/index.tsx"]
}
@@ -0,0 +1,12 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
globals: true,
include: ["src/**/*.{test,spec}.ts"],
reporters: [["default", { summary: false }]],
silent: true,
retry: 2,
},
});