@copilotkit/runtime
Version:
<img src="https://github.com/user-attachments/assets/0a6b64d9-e193-4940-a3f6-60334ac34084" alt="banner" style="border-radius: 12px; border: 2px solid #d6d4fa;" />
319 lines (317 loc) • 10.8 kB
JavaScript
require("reflect-metadata");
const require_runtime = require('../../_virtual/_rolldown/runtime.cjs');
let _copilotkit_shared = require("@copilotkit/shared");
let _ag_ui_client = require("@ag-ui/client");
//#region src/agent/converters/tanstack.ts
/**
* Converts AG-UI user message content to TanStack AI format.
* Handles plain strings, multimodal parts (image/audio/video/document),
* and legacy BinaryInputContent for backward compatibility.
*/
function convertUserContent(content) {
if (!content) return null;
if (typeof content === "string") return content;
if (!Array.isArray(content)) return null;
if (content.length === 0) return "";
const parts = [];
for (const part of content) {
if (!part || typeof part !== "object" || !("type" in part)) continue;
switch (part.type) {
case "text": {
const text = part.text;
if (text != null) parts.push({
type: "text",
content: text
});
break;
}
case "image":
case "audio":
case "video":
case "document": {
const source = part.source;
if (!source) break;
const partType = part.type;
if (source.type === "data") parts.push({
type: partType,
source: {
type: "data",
value: source.value,
mimeType: source.mimeType
}
});
else if (source.type === "url") parts.push({
type: partType,
source: {
type: "url",
value: source.value,
...source.mimeType ? { mimeType: source.mimeType } : {}
}
});
break;
}
case "binary": {
const legacy = part;
const mimeType = legacy.mimeType ?? "application/octet-stream";
const isImage = mimeType.startsWith("image/");
if (legacy.data) {
const partType = isImage ? "image" : "document";
parts.push({
type: partType,
source: {
type: "data",
value: legacy.data,
mimeType
}
});
} else if (legacy.url) {
const partType = isImage ? "image" : "document";
parts.push({
type: partType,
source: {
type: "url",
value: legacy.url,
mimeType
}
});
}
break;
}
}
}
return parts.length > 0 ? parts : "";
}
/**
* Recursively normalizes a frontend tool's JSON Schema so OpenAI accepts it as
* a function-tool schema.
*
* Frontend tools are often authored with permissive Zod (`z.any()`,
* `z.record(...)`, `.passthrough()`), which serialize to open objects —
* `additionalProperties: {}` (an empty sub-schema) or `additionalProperties:
* true`. OpenAI rejects both: strict mode requires `additionalProperties:
* false`, and an empty `{}` sub-schema fails base validation ("schema must
* have a 'type' key"). The classic (Vercel AI SDK) path sanitized these
* implicitly via a Zod round-trip; the TanStack path forwards the raw schema,
* so we close open objects here to match. (Models can't supply free-form extra
* keys either way — same as the classic path.)
*/
function sanitizeClientToolSchema(schema) {
if (Array.isArray(schema)) return schema.map(sanitizeClientToolSchema);
if (!schema || typeof schema !== "object") return schema;
const node = { ...schema };
if ("additionalProperties" in node) node.additionalProperties = false;
if (node.properties && typeof node.properties === "object") {
const props = {};
for (const [key, value] of Object.entries(node.properties)) props[key] = sanitizeClientToolSchema(value);
node.properties = props;
}
if ("items" in node) node.items = sanitizeClientToolSchema(node.items);
for (const combinator of [
"anyOf",
"allOf",
"oneOf"
]) if (Array.isArray(node[combinator])) node[combinator] = node[combinator].map(sanitizeClientToolSchema);
return node;
}
/**
* Converts a RunAgentInput into the format expected by TanStack AI's `chat()`.
*
* - Keeps only user/assistant/tool messages (activity, reasoning, and other roles are also excluded)
* - Extracts system/developer messages into `systemPrompts`
* - Appends context entries and application state to `systemPrompts`
* - Preserves tool calls on assistant messages and toolCallId on tool messages
*/
function convertInputToTanStackAI(input) {
const chatRoles = new Set([
"user",
"assistant",
"tool"
]);
const messages = input.messages.filter((m) => chatRoles.has(m.role)).map((m) => {
const msg = {
role: m.role,
content: m.role === "user" ? convertUserContent(m.content) : typeof m.content === "string" ? m.content : null
};
if (m.role === "assistant" && "toolCalls" in m && m.toolCalls) msg.toolCalls = m.toolCalls.map((tc) => ({
id: tc.id,
type: "function",
function: {
name: tc.function.name,
arguments: tc.function.arguments
}
}));
if (m.role === "tool" && "toolCallId" in m) msg.toolCallId = m.toolCallId;
return msg;
});
const systemPrompts = [];
for (const m of input.messages) if ((m.role === "system" || m.role === "developer") && m.content) systemPrompts.push(typeof m.content === "string" ? m.content : JSON.stringify(m.content));
if (input.context?.length) for (const ctx of input.context) systemPrompts.push(`${ctx.description}:\n${ctx.value}`);
if (input.state !== void 0 && input.state !== null && typeof input.state === "object" && Object.keys(input.state).length > 0) systemPrompts.push(`Application State:\n\`\`\`json\n${JSON.stringify(input.state, null, 2)}\n\`\`\``);
return {
messages,
systemPrompts,
tools: (input.tools ?? []).map((t) => ({
__toolSide: "client",
name: t.name,
description: t.description,
inputSchema: sanitizeClientToolSchema(t.parameters)
}))
};
}
/**
* Converts a TanStack AI stream into AG-UI `BaseEvent` objects.
*
* This is a pure converter — it does NOT emit lifecycle events
* (RUN_STARTED / RUN_FINISHED / RUN_ERROR). The caller (Agent class)
* is responsible for those.
*/
async function* convertTanStackStream(stream, abortSignal) {
const messageId = (0, _copilotkit_shared.randomUUID)();
const toolNamesById = /* @__PURE__ */ new Map();
let reasoningRunOpen = false;
let reasoningMessageOpen = false;
let reasoningMessageId = (0, _copilotkit_shared.randomUUID)();
function* closeReasoningIfOpen() {
if (reasoningMessageOpen) {
reasoningMessageOpen = false;
yield {
type: _ag_ui_client.EventType.REASONING_MESSAGE_END,
messageId: reasoningMessageId
};
}
if (reasoningRunOpen) {
reasoningRunOpen = false;
yield {
type: _ag_ui_client.EventType.REASONING_END,
messageId: reasoningMessageId
};
}
}
const startedToolCalls = /* @__PURE__ */ new Set();
const endedToolCalls = /* @__PURE__ */ new Set();
for await (const chunk of stream) {
if (abortSignal.aborted) break;
const raw = chunk;
const type = raw.type;
if (type === "RUN_STARTED" || type === "RUN_FINISHED") continue;
if (type === "RUN_ERROR") throw new Error(typeof raw.message === "string" ? raw.message : "TanStack AI run error");
if (type === "TEXT_MESSAGE_CONTENT" && raw.delta != null) {
yield* closeReasoningIfOpen();
yield {
type: _ag_ui_client.EventType.TEXT_MESSAGE_CHUNK,
role: "assistant",
messageId,
delta: raw.delta
};
} else if (type === "TOOL_CALL_START") {
const toolCallId = raw.toolCallId;
if (startedToolCalls.has(toolCallId)) continue;
startedToolCalls.add(toolCallId);
yield* closeReasoningIfOpen();
toolNamesById.set(toolCallId, raw.toolCallName);
yield {
type: _ag_ui_client.EventType.TOOL_CALL_START,
parentMessageId: messageId,
toolCallId,
toolCallName: raw.toolCallName
};
} else if (type === "TOOL_CALL_ARGS") {
if (endedToolCalls.has(raw.toolCallId)) continue;
yield* closeReasoningIfOpen();
yield {
type: _ag_ui_client.EventType.TOOL_CALL_ARGS,
toolCallId: raw.toolCallId,
delta: raw.delta
};
} else if (type === "TOOL_CALL_END") {
const toolCallId = raw.toolCallId;
if (endedToolCalls.has(toolCallId)) continue;
endedToolCalls.add(toolCallId);
yield* closeReasoningIfOpen();
yield {
type: _ag_ui_client.EventType.TOOL_CALL_END,
toolCallId
};
} else if (type === "TOOL_CALL_RESULT") {
yield* closeReasoningIfOpen();
const toolCallId = raw.toolCallId;
const toolName = toolNamesById.get(toolCallId);
const rawPayload = raw.content ?? raw.result;
const parsedContent = typeof rawPayload === "string" ? safeParse(rawPayload) : rawPayload;
if (toolName === "AGUISendStateSnapshot" && parsedContent && typeof parsedContent === "object" && "snapshot" in parsedContent) yield {
type: _ag_ui_client.EventType.STATE_SNAPSHOT,
snapshot: parsedContent.snapshot
};
if (toolName === "AGUISendStateDelta" && parsedContent && typeof parsedContent === "object" && "delta" in parsedContent) yield {
type: _ag_ui_client.EventType.STATE_DELTA,
delta: parsedContent.delta
};
let serializedContent;
if (typeof rawPayload === "string") serializedContent = rawPayload;
else try {
serializedContent = JSON.stringify(rawPayload ?? null);
} catch {
serializedContent = "[Unserializable tool result]";
}
yield {
type: _ag_ui_client.EventType.TOOL_CALL_RESULT,
role: "tool",
messageId: (0, _copilotkit_shared.randomUUID)(),
toolCallId,
content: serializedContent
};
toolNamesById.delete(toolCallId);
} else if (type === "REASONING_START") {
yield* closeReasoningIfOpen();
reasoningRunOpen = true;
reasoningMessageId = raw.messageId ?? (0, _copilotkit_shared.randomUUID)();
yield {
type: _ag_ui_client.EventType.REASONING_START,
messageId: reasoningMessageId
};
} else if (type === "REASONING_MESSAGE_START") {
reasoningMessageOpen = true;
yield {
type: _ag_ui_client.EventType.REASONING_MESSAGE_START,
messageId: reasoningMessageId,
role: "reasoning"
};
} else if (type === "REASONING_MESSAGE_CONTENT") yield {
type: _ag_ui_client.EventType.REASONING_MESSAGE_CONTENT,
messageId: reasoningMessageId,
delta: raw.delta
};
else if (type === "REASONING_MESSAGE_END") {
reasoningMessageOpen = false;
yield {
type: _ag_ui_client.EventType.REASONING_MESSAGE_END,
messageId: reasoningMessageId
};
} else if (type === "REASONING_END") {
if (reasoningMessageOpen) {
reasoningMessageOpen = false;
yield {
type: _ag_ui_client.EventType.REASONING_MESSAGE_END,
messageId: reasoningMessageId
};
}
reasoningRunOpen = false;
yield {
type: _ag_ui_client.EventType.REASONING_END,
messageId: reasoningMessageId
};
}
}
yield* closeReasoningIfOpen();
}
function safeParse(value) {
try {
return JSON.parse(value);
} catch {
return value;
}
}
//#endregion
exports.convertInputToTanStackAI = convertInputToTanStackAI;
exports.convertTanStackStream = convertTanStackStream;
//# sourceMappingURL=tanstack.cjs.map