UNPKG

@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;" />

1 lines 89.9 kB
{"version":3,"file":"index.mjs","names":["convertJsonSchemaToZodSchema","tool","createVercelAISDKTool","aiJsonSchema"],"sources":["../../src/agent/index.ts"],"sourcesContent":["import type {\n BaseEvent,\n RunAgentInput,\n Message,\n ReasoningEndEvent,\n ReasoningMessageContentEvent,\n ReasoningMessageEndEvent,\n ReasoningMessageStartEvent,\n ReasoningStartEvent,\n RunFinishedEvent,\n RunStartedEvent,\n TextMessageChunkEvent,\n ToolCallArgsEvent,\n ToolCallEndEvent,\n ToolCallStartEvent,\n ToolCallResultEvent,\n RunErrorEvent,\n StateSnapshotEvent,\n StateDeltaEvent,\n Interrupt,\n ResumeEntry,\n} from \"@ag-ui/client\";\nimport { AbstractAgent, EventType } from \"@ag-ui/client\";\nimport type { AgentCapabilities } from \"@ag-ui/core\";\nimport type {\n LanguageModel,\n ModelMessage,\n AssistantModelMessage,\n UserModelMessage,\n ToolModelMessage,\n SystemModelMessage,\n ToolCallPart,\n ToolResultPart,\n TextPart,\n ImagePart,\n FilePart,\n ToolChoice,\n ToolSet,\n Schema,\n} from \"ai\";\nimport { streamText, tool as createVercelAISDKTool, stepCountIs } from \"ai\";\nimport { createMCPClient } from \"@ai-sdk/mcp\";\nimport type { MCPClient } from \"@ai-sdk/mcp\";\nimport { Observable } from \"rxjs\";\nimport { createOpenAI } from \"@ai-sdk/openai\";\nimport { createAnthropic } from \"@ai-sdk/anthropic\";\nimport { createGoogleGenerativeAI } from \"@ai-sdk/google\";\nimport { createVertex } from \"@ai-sdk/google-vertex\";\nimport { safeParseToolArgs } from \"@copilotkit/shared\";\nimport { z } from \"zod\";\nimport type { StandardSchemaV1, InferSchemaOutput } from \"@copilotkit/shared\";\nimport { schemaToJsonSchema } from \"@copilotkit/shared\";\nimport { jsonSchema as aiJsonSchema } from \"ai\";\nimport { convertAISDKStream } from \"./converters/aisdk\";\nimport { convertTanStackStream } from \"./converters/tanstack\";\nimport type { StreamableHTTPClientTransportOptions } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport { SSEClientTransport } from \"@modelcontextprotocol/sdk/client/sse.js\";\nimport { randomUUID } from \"@copilotkit/shared\";\n\n/**\n * Properties that can be overridden by forwardedProps\n * These match the exact parameter names in streamText\n */\nexport type OverridableProperty =\n | \"model\"\n | \"toolChoice\"\n | \"maxOutputTokens\"\n | \"temperature\"\n | \"topP\"\n | \"topK\"\n | \"presencePenalty\"\n | \"frequencyPenalty\"\n | \"stopSequences\"\n | \"seed\"\n | \"maxRetries\"\n | \"prompt\"\n | \"providerOptions\";\n\n/**\n * Supported model identifiers for BuiltInAgent\n */\nexport type BuiltInAgentModel =\n // OpenAI models\n | \"openai/gpt-5\"\n | \"openai/gpt-5-mini\"\n | \"openai/gpt-4.1\"\n | \"openai/gpt-4.1-mini\"\n | \"openai/gpt-4.1-nano\"\n | \"openai/gpt-4o\"\n | \"openai/gpt-4o-mini\"\n // OpenAI reasoning series\n | \"openai/o3\"\n | \"openai/o3-mini\"\n | \"openai/o4-mini\"\n // Anthropic (Claude) models\n | \"anthropic/claude-sonnet-4.5\"\n | \"anthropic/claude-sonnet-4\"\n | \"anthropic/claude-3.7-sonnet\"\n | \"anthropic/claude-opus-4.1\"\n | \"anthropic/claude-opus-4\"\n | \"anthropic/claude-3.5-haiku\"\n // Google (Gemini) models\n | \"google/gemini-2.5-pro\"\n | \"google/gemini-2.5-flash\"\n | \"google/gemini-2.5-flash-lite\"\n // Allow any LanguageModel instance\n | (string & {});\n\n/**\n * Model specifier - can be a string like \"openai/gpt-4o\" or a LanguageModel instance\n */\nexport type ModelSpecifier = string | LanguageModel;\n\n/**\n * MCP Client configuration for HTTP transport\n */\nexport interface MCPClientConfigHTTP {\n /** Type of MCP client */\n type: \"http\";\n /** URL of the MCP server */\n url: string;\n /**\n * Optional transport options for the underlying\n * `StreamableHTTPClientTransport`. The SDK's documented extension point\n * for per-request customization is `options.fetch` — pass a wrapped fetch\n * here if you need static + dynamic headers on outbound MCP requests.\n */\n options?: StreamableHTTPClientTransportOptions;\n}\n\n/**\n * MCP Client configuration for SSE transport\n */\nexport interface MCPClientConfigSSE {\n /** Type of MCP client */\n type: \"sse\";\n /** URL of the MCP server */\n url: string;\n /** Optional HTTP headers (e.g., for authentication) */\n headers?: Record<string, string>;\n}\n\n/**\n * MCP Client configuration\n */\nexport type MCPClientConfig = MCPClientConfigHTTP | MCPClientConfigSSE;\n\n/**\n * A user-managed MCP client that provides tools to the agent.\n * The user is responsible for creating, configuring, and closing the client.\n * Compatible with the return type of @ai-sdk/mcp's createMCPClient().\n *\n * Unlike mcpServers, the agent does NOT create or close these clients.\n * This allows persistent connections, custom auth, and tool caching.\n */\nexport interface MCPClientProvider {\n /** Return tools to be merged into the agent's tool set. */\n tools(): Promise<ToolSet>;\n}\n\n/**\n * Resolves a model specifier to a LanguageModel instance\n * @param spec - Model string (e.g., \"openai/gpt-4o\") or LanguageModel instance\n * @param apiKey - Optional API key to use instead of environment variables\n * @returns LanguageModel instance\n */\nexport function resolveModel(\n spec: ModelSpecifier,\n apiKey?: string,\n): LanguageModel {\n // If already a LanguageModel instance, pass through\n if (typeof spec !== \"string\") {\n return spec;\n }\n\n // Normalize \"provider/model\" or \"provider:model\" format\n const normalized = spec.replace(\"/\", \":\").trim();\n const parts = normalized.split(\":\");\n const rawProvider = parts[0];\n const rest = parts.slice(1);\n\n if (!rawProvider) {\n throw new Error(\n `Invalid model string \"${spec}\". Use \"openai/gpt-5\", \"anthropic/claude-sonnet-4.5\", or \"google/gemini-2.5-pro\".`,\n );\n }\n\n const provider = rawProvider.toLowerCase();\n const model = rest.join(\":\").trim();\n\n if (!model) {\n throw new Error(\n `Invalid model string \"${spec}\". Use \"openai/gpt-5\", \"anthropic/claude-sonnet-4.5\", or \"google/gemini-2.5-pro\".`,\n );\n }\n\n switch (provider) {\n case \"openai\": {\n // Lazily create OpenAI provider\n // Use provided apiKey, or fall back to environment variable\n const openai = createOpenAI({\n apiKey: apiKey || process.env.OPENAI_API_KEY!,\n });\n // Accepts any OpenAI model id, e.g. \"gpt-4o\", \"gpt-4.1-mini\", \"o3-mini\"\n return openai(model);\n }\n\n case \"anthropic\": {\n // Lazily create Anthropic provider\n // Use provided apiKey, or fall back to environment variable\n const anthropic = createAnthropic({\n apiKey: apiKey || process.env.ANTHROPIC_API_KEY!,\n });\n // Accepts any Claude id, e.g. \"claude-3.7-sonnet\", \"claude-3.5-haiku\"\n return anthropic(model);\n }\n\n case \"google\":\n case \"gemini\":\n case \"google-gemini\": {\n // Lazily create Google provider\n // Use provided apiKey, or fall back to environment variable\n const google = createGoogleGenerativeAI({\n apiKey: apiKey || process.env.GOOGLE_API_KEY!,\n });\n // Accepts any Gemini id, e.g. \"gemini-2.5-pro\", \"gemini-2.5-flash\"\n return google(model);\n }\n\n case \"vertex\": {\n const vertex = createVertex();\n return vertex(model);\n }\n\n default:\n throw new Error(\n `Unknown provider \"${provider}\" in \"${spec}\". Supported: openai, anthropic, google (gemini).`,\n );\n }\n}\n\n/**\n * Thrown by `AgentFactoryContext.interrupt()` on a fresh (non-resume) run to\n * pause the factory. Caught in `runFactory` and translated into a RUN_FINISHED\n * event carrying `outcome:{type:\"interrupt\",interrupts}`. Not a real error.\n */\nexport class InterruptSignal extends Error {\n constructor(public readonly interrupts: Interrupt[]) {\n super(\"CopilotKit interrupt: run paused awaiting human input\");\n this.name = \"InterruptSignal\";\n }\n}\n\n/**\n * Tool definition for BuiltInAgent\n */\nexport interface ToolDefinition<\n TParameters extends StandardSchemaV1 = StandardSchemaV1,\n> {\n name: string;\n description: string;\n parameters: TParameters;\n /**\n * Server-side executor. Optional ONLY for interrupt tools (`interrupt:true`),\n * which pause the run instead of executing.\n */\n execute?: (args: InferSchemaOutput<TParameters>) => Promise<unknown>;\n /**\n * When true, calling this tool pauses the run and emits a standard AG-UI\n * interrupt (RUN_FINISHED outcome:interrupt) keyed by the tool call's id.\n * The human response (resume payload) is injected as this tool call's result\n * on the resume run. Interrupt tools must NOT define `execute`, and require\n * the default `maxSteps: 1` — with `maxSteps > 1` the AI SDK's agentic loop\n * would try to continue past the unexecuted tool call instead of pausing.\n */\n interrupt?: boolean;\n /** Optional categorical reason surfaced on the Interrupt (default: \"tool_call\"). */\n interruptReason?: string;\n /** Optional human-readable prompt surfaced on the Interrupt. */\n interruptMessage?: string;\n}\n\n/**\n * Define a tool for use with BuiltInAgent\n * @param name - The name of the tool\n * @param description - Description of what the tool does\n * @param parameters - Schema for the tool's input parameters (any Standard Schema V1 compatible library: Zod, Valibot, ArkType, etc.)\n * @param execute - Function to execute the tool server-side\n * @returns Tool definition\n */\nexport function defineTool<TParameters extends StandardSchemaV1>(config: {\n name: string;\n description: string;\n parameters: TParameters;\n execute?: (args: InferSchemaOutput<TParameters>) => Promise<unknown>;\n interrupt?: boolean;\n interruptReason?: string;\n interruptMessage?: string;\n}): ToolDefinition<TParameters> {\n return {\n name: config.name,\n description: config.description,\n parameters: config.parameters,\n execute: config.execute,\n interrupt: config.interrupt,\n interruptReason: config.interruptReason,\n interruptMessage: config.interruptMessage,\n };\n}\n\ntype AGUIUserMessage = Extract<Message, { role: \"user\" }>;\n\n/**\n * Converts AG-UI user message content to Vercel AI SDK UserContent format.\n * Handles plain strings, new modality-specific parts (image/audio/video/document),\n * and legacy BinaryInputContent for backward compatibility.\n */\nfunction convertUserMessageContent(\n content: AGUIUserMessage[\"content\"],\n): string | Array<TextPart | ImagePart | FilePart> {\n if (!content) {\n return \"\";\n }\n\n if (typeof content === \"string\") {\n return content;\n }\n\n const parts: Array<TextPart | ImagePart | FilePart> = [];\n\n for (const part of content) {\n if (!part || typeof part !== \"object\" || !(\"type\" in part)) {\n continue;\n }\n\n switch (part.type) {\n case \"text\": {\n const text = (part as { text?: string }).text;\n if (text) {\n parts.push({ type: \"text\", text });\n }\n break;\n }\n\n case \"image\": {\n const source = (part as { source?: any }).source;\n if (!source) break;\n if (source.type === \"data\") {\n parts.push({\n type: \"image\",\n image: source.value,\n mediaType: source.mimeType,\n });\n } else if (source.type === \"url\") {\n try {\n parts.push({\n type: \"image\",\n image: new URL(source.value),\n mediaType: source.mimeType,\n });\n } catch {\n console.error(\n `[CopilotKit] convertUserMessageContent: invalid URL \"${source.value}\" in image part — skipping`,\n );\n }\n }\n break;\n }\n\n case \"audio\":\n case \"video\":\n case \"document\": {\n const source = (part as { source?: any }).source;\n if (!source) break;\n if (source.type === \"data\") {\n parts.push({\n type: \"file\",\n data: source.value,\n mediaType: source.mimeType,\n });\n } else if (source.type === \"url\") {\n try {\n parts.push({\n type: \"file\",\n data: new URL(source.value),\n mediaType: source.mimeType ?? \"application/octet-stream\",\n });\n } catch {\n console.error(\n `[CopilotKit] convertUserMessageContent: invalid URL \"${source.value}\" in ${part.type} part — skipping`,\n );\n }\n }\n break;\n }\n\n // Legacy BinaryInputContent backward compatibility\n case \"binary\": {\n const legacy = part as {\n mimeType?: string;\n data?: string;\n url?: string;\n };\n const mimeType = legacy.mimeType ?? \"application/octet-stream\";\n const isImage = mimeType.startsWith(\"image/\");\n\n if (legacy.data) {\n if (isImage) {\n parts.push({\n type: \"image\",\n image: legacy.data,\n mediaType: mimeType,\n });\n } else {\n parts.push({\n type: \"file\",\n data: legacy.data,\n mediaType: mimeType,\n });\n }\n } else if (legacy.url) {\n try {\n const url = new URL(legacy.url);\n if (isImage) {\n parts.push({ type: \"image\", image: url, mediaType: mimeType });\n } else {\n parts.push({ type: \"file\", data: url, mediaType: mimeType });\n }\n } catch {\n console.error(\n `[CopilotKit] convertUserMessageContent: invalid URL \"${legacy.url}\" in binary part — skipping`,\n );\n }\n }\n break;\n }\n\n default: {\n console.error(\n `[CopilotKit] convertUserMessageContent: unrecognized content part type \"${(part as { type: string }).type}\" — skipping`,\n );\n break;\n }\n }\n }\n\n return parts.length > 0 ? parts : \"\";\n}\n\n/**\n * Options for converting AG-UI messages to Vercel AI SDK format\n */\nexport interface MessageConversionOptions {\n forwardSystemMessages?: boolean;\n forwardDeveloperMessages?: boolean;\n}\n\n/**\n * Converts AG-UI messages to Vercel AI SDK ModelMessage format\n */\nexport function convertMessagesToVercelAISDKMessages(\n messages: Message[],\n options: MessageConversionOptions = {},\n): ModelMessage[] {\n const result: ModelMessage[] = [];\n\n for (const message of messages) {\n if (message.role === \"system\" && options.forwardSystemMessages) {\n const systemMsg: SystemModelMessage = {\n role: \"system\",\n content: message.content ?? \"\",\n };\n result.push(systemMsg);\n } else if (\n message.role === \"developer\" &&\n options.forwardDeveloperMessages\n ) {\n const systemMsg: SystemModelMessage = {\n role: \"system\",\n content: message.content ?? \"\",\n };\n result.push(systemMsg);\n } else if (message.role === \"assistant\") {\n const parts: Array<TextPart | ToolCallPart> = message.content\n ? [{ type: \"text\", text: message.content }]\n : [];\n\n for (const toolCall of message.toolCalls ?? []) {\n const toolCallPart: ToolCallPart = {\n type: \"tool-call\",\n toolCallId: toolCall.id,\n toolName: toolCall.function.name,\n input: safeParseToolArgs(toolCall.function.arguments),\n };\n parts.push(toolCallPart);\n }\n\n const assistantMsg: AssistantModelMessage = {\n role: \"assistant\",\n content: parts,\n };\n result.push(assistantMsg);\n } else if (message.role === \"user\") {\n const userMsg: UserModelMessage = {\n role: \"user\",\n content: convertUserMessageContent(message.content),\n };\n result.push(userMsg);\n } else if (message.role === \"tool\") {\n let toolName = \"unknown\";\n // Find the tool name from the corresponding tool call\n for (const msg of messages) {\n if (msg.role === \"assistant\") {\n for (const toolCall of msg.toolCalls ?? []) {\n if (toolCall.id === message.toolCallId) {\n toolName = toolCall.function.name;\n break;\n }\n }\n }\n }\n\n const toolResultPart: ToolResultPart = {\n type: \"tool-result\",\n toolCallId: message.toolCallId,\n toolName: toolName,\n output: {\n type: \"text\",\n value: message.content,\n },\n };\n\n const toolMsg: ToolModelMessage = {\n role: \"tool\",\n content: [toolResultPart],\n };\n result.push(toolMsg);\n }\n }\n\n return result;\n}\n\n/**\n * JSON Schema type definition\n */\ninterface JsonSchema {\n type: \"object\" | \"string\" | \"number\" | \"integer\" | \"boolean\" | \"array\";\n description?: string;\n properties?: Record<string, JsonSchema>;\n required?: string[];\n items?: JsonSchema;\n enum?: string[];\n}\n\n/**\n * Converts JSON Schema to Zod schema\n */\nexport function convertJsonSchemaToZodSchema(\n jsonSchema: JsonSchema,\n required: boolean,\n): z.ZodSchema {\n // Handle empty schemas {} (no input required) - treat as empty object\n if (!jsonSchema.type) {\n return required ? z.object({}) : z.object({}).optional();\n }\n if (jsonSchema.type === \"object\") {\n const spec: { [key: string]: z.ZodSchema } = {};\n\n if (!jsonSchema.properties || !Object.keys(jsonSchema.properties).length) {\n return !required ? z.object(spec).optional() : z.object(spec);\n }\n\n for (const [key, value] of Object.entries(jsonSchema.properties)) {\n spec[key] = convertJsonSchemaToZodSchema(\n value,\n jsonSchema.required ? jsonSchema.required.includes(key) : false,\n );\n }\n const schema = z.object(spec).describe(jsonSchema.description ?? \"\");\n return required ? schema : schema.optional();\n } else if (jsonSchema.type === \"string\") {\n if (jsonSchema.enum && jsonSchema.enum.length > 0) {\n const schema = z\n .enum(jsonSchema.enum as [string, ...string[]])\n .describe(jsonSchema.description ?? \"\");\n return required ? schema : schema.optional();\n }\n const schema = z.string().describe(jsonSchema.description ?? \"\");\n return required ? schema : schema.optional();\n } else if (jsonSchema.type === \"number\" || jsonSchema.type === \"integer\") {\n const schema = z.number().describe(jsonSchema.description ?? \"\");\n return required ? schema : schema.optional();\n } else if (jsonSchema.type === \"boolean\") {\n const schema = z.boolean().describe(jsonSchema.description ?? \"\");\n return required ? schema : schema.optional();\n } else if (jsonSchema.type === \"array\") {\n if (!jsonSchema.items) {\n throw new Error(\"Array type must have items property\");\n }\n const itemSchema = convertJsonSchemaToZodSchema(jsonSchema.items, true);\n const schema = z.array(itemSchema).describe(jsonSchema.description ?? \"\");\n return required ? schema : schema.optional();\n }\n console.error(\"Invalid JSON schema:\", JSON.stringify(jsonSchema, null, 2));\n throw new Error(\"Invalid JSON schema\");\n}\n\n/**\n * Converts AG-UI tools to Vercel AI SDK ToolSet\n */\nfunction isJsonSchema(obj: unknown): obj is JsonSchema {\n if (typeof obj !== \"object\" || obj === null) return false;\n const schema = obj as Record<string, unknown>;\n // Empty objects {} are valid JSON schemas (no input required)\n if (Object.keys(schema).length === 0) return true;\n return (\n typeof schema.type === \"string\" &&\n [\"object\", \"string\", \"number\", \"integer\", \"boolean\", \"array\"].includes(\n schema.type,\n )\n );\n}\n\n/**\n * Type-only pass-through for handing a Zod schema to the AI SDK's `tool()`.\n * The raw Zod schema is returned unchanged at runtime — the SDK's `asSchema()`\n * converts and validates it internally exactly as before.\n *\n * The Zod type is erased through `unknown` deliberately: letting tsc relate\n * Zod schema types to the AI SDK's `FlexibleSchema` union (conditional types\n * spanning zod v3/v4) makes type instantiation explode (TS2589 / compiler\n * OOM) under this package's `moduleResolution: node`.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction toLanguageModelSchema(schema: z.ZodSchema): Schema<any> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return schema as unknown as Schema<any>;\n}\n\nexport function convertToolsToVercelAITools(\n tools: RunAgentInput[\"tools\"],\n): ToolSet {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const result: Record<string, any> = {};\n\n for (const tool of tools) {\n if (!isJsonSchema(tool.parameters)) {\n throw new Error(`Invalid JSON schema for tool ${tool.name}`);\n }\n const zodSchema = convertJsonSchemaToZodSchema(tool.parameters, true);\n result[tool.name] = createVercelAISDKTool({\n description: tool.description,\n inputSchema: toLanguageModelSchema(zodSchema),\n });\n }\n\n return result;\n}\n\n/**\n * Check whether a schema is a Zod schema by inspecting its Standard Schema vendor.\n */\nfunction isZodSchema(schema: StandardSchemaV1): boolean {\n return schema[\"~standard\"]?.vendor === \"zod\";\n}\n\n/**\n * Converts ToolDefinition array to Vercel AI SDK ToolSet.\n *\n * For Zod schemas, passes them directly to the AI SDK (Zod satisfies FlexibleSchema).\n * For non-Zod schemas, converts to JSON Schema via schemaToJsonSchema() and wraps\n * with the AI SDK's jsonSchema() helper.\n */\nexport function convertToolDefinitionsToVercelAITools(\n tools: ToolDefinition[],\n): ToolSet {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const result: Record<string, any> = {};\n\n for (const tool of tools) {\n if (isZodSchema(tool.parameters)) {\n // Zod schemas can be passed directly to AI SDK (satisfies FlexibleSchema)\n result[tool.name] = createVercelAISDKTool({\n description: tool.description,\n inputSchema: tool.parameters as any,\n execute: tool.execute,\n });\n } else {\n // Non-Zod: convert to JSON Schema and wrap with AI SDK's jsonSchema()\n const jsonSchemaObj = schemaToJsonSchema(tool.parameters);\n result[tool.name] = createVercelAISDKTool({\n description: tool.description,\n inputSchema: aiJsonSchema(jsonSchemaObj),\n execute: tool.execute,\n });\n }\n }\n\n return result;\n}\n\n/**\n * Context passed to the user-supplied factory function in factory mode.\n */\nexport interface AgentFactoryContext {\n input: RunAgentInput;\n /**\n * Prefer `abortSignal` for most use cases (AI SDK, fetch, custom backends).\n * Provided for backends like TanStack AI that require the full AbortController.\n * Do NOT call `.abort()` on this controller — use `abortRun()` on the agent instead.\n */\n abortController: AbortController;\n abortSignal: AbortSignal;\n /**\n * Pause the run for human input (AG-UI standard interrupt). On a fresh run\n * this throws an InterruptSignal (caught by the runtime) and the run finishes\n * with `outcome:{type:\"interrupt\",interrupts}`. On the resume run (when\n * `input.resume` covers these interrupts) it returns the matching ResumeEntry\n * responses so the factory can continue. Each `interrupts[i].id` is matched to\n * `input.resume[].interruptId`.\n */\n interrupt: (interrupts: Interrupt[]) => Promise<ResumeEntry[]>;\n}\n\n/**\n * Factory config for AI SDK backend.\n * The factory must return an object with a `fullStream` async iterable\n * (compatible with the result of `streamText()` — only `fullStream` is consumed).\n */\nexport interface BuiltInAgentAISDKFactoryConfig {\n type: \"aisdk\";\n factory: (\n ctx: AgentFactoryContext,\n ) =>\n | { fullStream: AsyncIterable<unknown> }\n | Promise<{ fullStream: AsyncIterable<unknown> }>;\n}\n\n/**\n * Factory config for TanStack AI backend.\n * The factory must return an async iterable of TanStack AI stream chunks.\n */\nexport interface BuiltInAgentTanStackFactoryConfig {\n type: \"tanstack\";\n factory: (\n ctx: AgentFactoryContext,\n ) => AsyncIterable<unknown> | Promise<AsyncIterable<unknown>>;\n}\n\n/**\n * Factory config for a custom backend that directly yields AG-UI events.\n */\nexport interface BuiltInAgentCustomFactoryConfig {\n type: \"custom\";\n factory: (\n ctx: AgentFactoryContext,\n ) => AsyncIterable<BaseEvent> | Promise<AsyncIterable<BaseEvent>>;\n}\n\n/**\n * Union of all factory-mode configurations.\n */\nexport type BuiltInAgentFactoryConfig =\n | BuiltInAgentAISDKFactoryConfig\n | BuiltInAgentTanStackFactoryConfig\n | BuiltInAgentCustomFactoryConfig;\n\n/**\n * Classic config — BuiltInAgent handles streamText, tools, MCP, state tools, prompt building.\n */\nexport interface BuiltInAgentClassicConfig {\n /**\n * The model to use\n */\n model: BuiltInAgentModel | LanguageModel;\n /**\n * API key for the model provider (OpenAI, Anthropic, Google)\n * If not provided, falls back to environment variables:\n * - OPENAI_API_KEY for OpenAI models\n * - ANTHROPIC_API_KEY for Anthropic models\n * - GOOGLE_API_KEY for Google models\n */\n apiKey?: string;\n /**\n * Maximum number of steps/iterations for tool calling (default: 1)\n */\n maxSteps?: number;\n /**\n * Tool choice setting - how tools are selected for execution (default: \"auto\")\n */\n toolChoice?: ToolChoice<Record<string, unknown>>;\n /**\n * Maximum number of tokens to generate\n */\n maxOutputTokens?: number;\n /**\n * Temperature setting (range depends on provider)\n */\n temperature?: number;\n /**\n * Nucleus sampling (topP)\n */\n topP?: number;\n /**\n * Top K sampling\n */\n topK?: number;\n /**\n * Presence penalty\n */\n presencePenalty?: number;\n /**\n * Frequency penalty\n */\n frequencyPenalty?: number;\n /**\n * Sequences that will stop the generation\n */\n stopSequences?: string[];\n /**\n * Seed for deterministic results\n */\n seed?: number;\n /**\n * Maximum number of retries\n */\n maxRetries?: number;\n /**\n * Prompt for the agent\n */\n prompt?: string;\n /**\n * List of properties that can be overridden by forwardedProps.\n */\n overridableProperties?: OverridableProperty[];\n /**\n * Optional list of MCP server configurations\n */\n mcpServers?: MCPClientConfig[];\n /**\n * Optional list of user-managed MCP clients.\n * Unlike mcpServers, the agent does NOT create or close these clients.\n * The user controls the lifecycle, persistence, auth, and caching.\n *\n * Compatible with @ai-sdk/mcp's createMCPClient() return type:\n * ```typescript\n * const client = await createMCPClient({ transport });\n * const agent = new BuiltInAgent({ model: \"...\", mcpClients: [client] });\n * ```\n */\n mcpClients?: MCPClientProvider[];\n /**\n * Optional tools available to the agent\n */\n tools?: ToolDefinition[];\n /**\n * Forward system-role messages from input to the LLM.\n * Default: false\n */\n forwardSystemMessages?: boolean;\n /**\n * Forward developer-role messages from input to the LLM (as system messages).\n * Default: false\n */\n forwardDeveloperMessages?: boolean;\n /**\n * Provider-specific options passed to the model (e.g., OpenAI reasoningEffort).\n * Example: `{ openai: { reasoningEffort: \"high\" } }`\n */\n providerOptions?: Record<string, any>;\n /**\n * Explicit agent capabilities. **Shallow-merged** at the category level on\n * top of auto-inferred defaults — providing a category (e.g. `tools`)\n * replaces that entire category, not individual fields within it.\n *\n * For example, `{ tools: { supported: true } }` will drop the inferred\n * `clientProvided` value. Include all fields for any category you override.\n */\n capabilities?: Partial<AgentCapabilities>;\n}\n\n/**\n * Configuration for BuiltInAgent.\n *\n * Two modes:\n * - **Classic** (model + params): BuiltInAgent handles everything — streamText, tools, MCP, state tools.\n * - **Factory** (type + factory): You own the LLM call. BuiltInAgent handles lifecycle only.\n */\nexport type BuiltInAgentConfiguration =\n | BuiltInAgentClassicConfig\n | BuiltInAgentFactoryConfig;\n\n/**\n * Type guard: returns true if this is a factory-mode config.\n */\nfunction isFactoryConfig(\n config: BuiltInAgentConfiguration,\n): config is BuiltInAgentFactoryConfig {\n return \"factory\" in config;\n}\n\nexport class BuiltInAgent extends AbstractAgent {\n private abortController?: AbortController;\n\n constructor(private config: BuiltInAgentConfiguration) {\n super();\n }\n\n /**\n * Check if a property can be overridden by forwardedProps\n */\n canOverride(property: OverridableProperty): boolean {\n if (isFactoryConfig(this.config)) return false;\n return this.config?.overridableProperties?.includes(property) ?? false;\n }\n\n async getCapabilities(): Promise<AgentCapabilities> {\n const inferred: AgentCapabilities = {\n tools: {\n supported: true,\n clientProvided: true,\n },\n transport: {\n streaming: true,\n },\n humanInTheLoop: {\n interrupts: true,\n },\n };\n\n // Factory-mode configs have no `capabilities` property.\n const capabilities = isFactoryConfig(this.config)\n ? undefined\n : this.config.capabilities;\n\n if (!capabilities) {\n return inferred;\n }\n\n // Shallow merge at the category level — explicit overrides replace\n // entire categories when provided, inferred defaults fill the rest.\n return {\n ...inferred,\n ...capabilities,\n };\n }\n\n run(input: RunAgentInput): Observable<BaseEvent> {\n if (isFactoryConfig(this.config)) {\n return this.runFactory(input, this.config);\n }\n\n // Capture the narrowed classic config — the narrowing of `this.config`\n // above does not survive into the Observable/async closures below.\n const config = this.config;\n\n if (this.abortController) {\n throw new Error(\n \"Agent is already running. Call abortRun() first or create a new instance.\",\n );\n }\n\n // Set synchronously before Observable creation to close TOCTOU window\n this.abortController = new AbortController();\n const abortController = this.abortController;\n\n return new Observable<BaseEvent>((subscriber) => {\n // Emit RUN_STARTED event\n const startEvent: RunStartedEvent = {\n type: EventType.RUN_STARTED,\n threadId: input.threadId,\n runId: input.runId,\n };\n subscriber.next(startEvent);\n\n // Resolve the model, passing API key if provided\n const model = resolveModel(config.model, config.apiKey);\n\n // Build prompt based on conditions\n let systemPrompt: string | undefined = undefined;\n\n // Check if we should build a prompt:\n // - config.prompt is set, OR\n // - input.context is non-empty, OR\n // - input.state is non-empty and not an empty object\n const hasPrompt = !!config.prompt;\n const hasContext = input.context && input.context.length > 0;\n const hasState =\n input.state !== undefined &&\n input.state !== null &&\n !(\n typeof input.state === \"object\" &&\n Object.keys(input.state).length === 0\n );\n\n if (hasPrompt || hasContext || hasState) {\n const parts: string[] = [];\n\n // First: the prompt if any\n if (hasPrompt) {\n parts.push(config.prompt!);\n }\n\n // Second: context from the application\n if (hasContext) {\n parts.push(\"\\n## Context from the application\\n\");\n for (const ctx of input.context) {\n parts.push(`${ctx.description}:\\n${ctx.value}\\n`);\n }\n }\n\n // Third: state from the application that can be edited\n if (hasState) {\n parts.push(\n \"\\n## Application State\\n\" +\n \"This is state from the application that you can edit by calling AGUISendStateSnapshot or AGUISendStateDelta.\\n\" +\n `\\`\\`\\`json\\n${JSON.stringify(input.state, null, 2)}\\n\\`\\`\\`\\n`,\n );\n }\n\n systemPrompt = parts.join(\"\");\n }\n\n // Convert messages and prepend system message if we have a prompt\n const messages = convertMessagesToVercelAISDKMessages(input.messages, {\n forwardSystemMessages: config.forwardSystemMessages,\n forwardDeveloperMessages: config.forwardDeveloperMessages,\n });\n if (systemPrompt) {\n messages.unshift({\n role: \"system\",\n content: systemPrompt,\n });\n }\n\n // Resume injection: each ResumeEntry maps to the interrupt tool call it\n // addresses (interruptId === toolCallId) and is appended as that call's\n // tool-role result so the model can continue the agentic loop.\n const resumeEntries: ResumeEntry[] = input.resume ?? [];\n if (resumeEntries.length > 0) {\n // Recover the originating tool name for each interrupt tool call so the\n // injected tool-result carries the real name. Providers like Anthropic\n // and Google reject a tool-result whose name doesn't match the prior\n // tool-call; an empty name fails there (OpenAI tolerates it).\n const toolNameById = new Map<string, string>();\n for (const m of input.messages) {\n if (m.role !== \"assistant\") continue;\n for (const tc of m.toolCalls ?? []) {\n if (tc.id && tc.function?.name) {\n toolNameById.set(tc.id, tc.function.name);\n }\n }\n }\n // Idempotent: a client (useInterrupt) may already have appended the\n // resolution as a tool message — converted into a tool-result above.\n // Skip those so we don't answer the same tool call twice.\n const alreadyAnswered = new Set<string>();\n for (const m of messages) {\n if (m.role !== \"tool\" || !Array.isArray(m.content)) continue;\n for (const part of m.content) {\n if (part && typeof part === \"object\" && \"toolCallId\" in part) {\n alreadyAnswered.add((part as { toolCallId: string }).toolCallId);\n }\n }\n }\n for (const entry of resumeEntries) {\n if (alreadyAnswered.has(entry.interruptId)) continue;\n const value =\n entry.status === \"cancelled\"\n ? { status: \"cancelled\" }\n : (entry.payload ?? { status: \"resolved\" });\n const toolResultMessage: ToolModelMessage = {\n role: \"tool\",\n content: [\n {\n type: \"tool-result\",\n toolCallId: entry.interruptId,\n toolName: toolNameById.get(entry.interruptId) ?? \"\",\n output: { type: \"json\", value },\n },\n ],\n };\n messages.push(toolResultMessage);\n }\n }\n\n // Merge tools from input and config\n let allTools: ToolSet = convertToolsToVercelAITools(input.tools);\n if (config.tools && config.tools.length > 0) {\n const configTools = convertToolDefinitionsToVercelAITools(config.tools);\n allTools = { ...allTools, ...configTools };\n }\n\n const streamTextParams: Parameters<typeof streamText>[0] = {\n model,\n messages,\n tools: allTools,\n toolChoice: config.toolChoice,\n stopWhen: config.maxSteps ? stepCountIs(config.maxSteps) : undefined,\n maxOutputTokens: config.maxOutputTokens,\n temperature: config.temperature,\n topP: config.topP,\n topK: config.topK,\n presencePenalty: config.presencePenalty,\n frequencyPenalty: config.frequencyPenalty,\n stopSequences: config.stopSequences,\n seed: config.seed,\n providerOptions: config.providerOptions,\n maxRetries: config.maxRetries,\n };\n\n // Apply forwardedProps overrides (if allowed)\n if (input.forwardedProps && typeof input.forwardedProps === \"object\") {\n const props = input.forwardedProps as Record<string, unknown>;\n\n // Check and apply each overridable property\n if (props.model !== undefined && this.canOverride(\"model\")) {\n if (\n typeof props.model === \"string\" ||\n typeof props.model === \"object\"\n ) {\n // Accept any string or LanguageModel instance for model override\n // Use the configured API key when resolving overridden models\n streamTextParams.model = resolveModel(\n props.model as string | LanguageModel,\n config.apiKey,\n );\n }\n }\n if (props.toolChoice !== undefined && this.canOverride(\"toolChoice\")) {\n // ToolChoice can be 'auto', 'required', 'none', or { type: 'tool', toolName: string }\n const toolChoice = props.toolChoice;\n if (\n toolChoice === \"auto\" ||\n toolChoice === \"required\" ||\n toolChoice === \"none\" ||\n (typeof toolChoice === \"object\" &&\n toolChoice !== null &&\n \"type\" in toolChoice &&\n toolChoice.type === \"tool\")\n ) {\n streamTextParams.toolChoice = toolChoice as ToolChoice<\n Record<string, unknown>\n >;\n }\n }\n if (\n typeof props.maxOutputTokens === \"number\" &&\n this.canOverride(\"maxOutputTokens\")\n ) {\n streamTextParams.maxOutputTokens = props.maxOutputTokens;\n }\n if (\n typeof props.temperature === \"number\" &&\n this.canOverride(\"temperature\")\n ) {\n streamTextParams.temperature = props.temperature;\n }\n if (typeof props.topP === \"number\" && this.canOverride(\"topP\")) {\n streamTextParams.topP = props.topP;\n }\n if (typeof props.topK === \"number\" && this.canOverride(\"topK\")) {\n streamTextParams.topK = props.topK;\n }\n if (\n typeof props.presencePenalty === \"number\" &&\n this.canOverride(\"presencePenalty\")\n ) {\n streamTextParams.presencePenalty = props.presencePenalty;\n }\n if (\n typeof props.frequencyPenalty === \"number\" &&\n this.canOverride(\"frequencyPenalty\")\n ) {\n streamTextParams.frequencyPenalty = props.frequencyPenalty;\n }\n if (\n Array.isArray(props.stopSequences) &&\n this.canOverride(\"stopSequences\")\n ) {\n // Validate all elements are strings\n if (\n props.stopSequences.every(\n (item): item is string => typeof item === \"string\",\n )\n ) {\n streamTextParams.stopSequences = props.stopSequences;\n }\n }\n if (typeof props.seed === \"number\" && this.canOverride(\"seed\")) {\n streamTextParams.seed = props.seed;\n }\n if (\n typeof props.maxRetries === \"number\" &&\n this.canOverride(\"maxRetries\")\n ) {\n streamTextParams.maxRetries = props.maxRetries;\n }\n if (\n props.providerOptions !== undefined &&\n this.canOverride(\"providerOptions\")\n ) {\n if (\n typeof props.providerOptions === \"object\" &&\n props.providerOptions !== null\n ) {\n streamTextParams.providerOptions = props.providerOptions as Record<\n string,\n any\n >;\n }\n }\n }\n\n // Set up MCP clients if configured and process the stream\n const mcpClients: MCPClient[] = [];\n\n (async () => {\n let terminalEventEmitted = false;\n let messageId = randomUUID();\n let reasoningMessageId = randomUUID();\n let isInReasoning = false;\n\n // Auto-close an open reasoning lifecycle.\n // Some AI SDK providers (notably @ai-sdk/anthropic) never emit \"reasoning-end\",\n // which leaves downstream state machines stuck. This helper emits the\n // missing REASONING_MESSAGE_END + REASONING_END events so the stream\n // can transition to text, tool-call, or finish phases.\n // Declared before try/catch so it is accessible in the catch block.\n const closeReasoningIfOpen = () => {\n if (!isInReasoning) return;\n isInReasoning = false;\n const reasoningMsgEnd: ReasoningMessageEndEvent = {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningMessageId,\n };\n subscriber.next(reasoningMsgEnd);\n const reasoningEnd: ReasoningEndEvent = {\n type: EventType.REASONING_END,\n messageId: reasoningMessageId,\n };\n subscriber.next(reasoningEnd);\n };\n\n try {\n // Add AG-UI state update tools\n streamTextParams.tools = {\n ...streamTextParams.tools,\n AGUISendStateSnapshot: createVercelAISDKTool({\n description:\n \"Replace the entire application state with a new snapshot\",\n inputSchema: toLanguageModelSchema(\n z.object({\n snapshot: z.any().describe(\"The complete new state object\"),\n }),\n ),\n execute: async ({ snapshot }: { snapshot?: unknown }) => {\n return { success: true, snapshot };\n },\n }),\n AGUISendStateDelta: createVercelAISDKTool({\n description:\n \"Apply incremental updates to application state using JSON Patch operations\",\n inputSchema: toLanguageModelSchema(\n z.object({\n delta: z\n .array(\n z.object({\n op: z\n .enum([\"add\", \"replace\", \"remove\"])\n .describe(\"The operation to perform\"),\n path: z\n .string()\n .describe(\"JSON Pointer path (e.g., '/foo/bar')\"),\n value: z\n .any()\n .optional()\n .describe(\n \"The value to set. Required for 'add' and 'replace' operations, ignored for 'remove'.\",\n ),\n }),\n )\n .describe(\"Array of JSON Patch operations\"),\n }),\n ),\n execute: async ({\n delta,\n }: {\n delta: {\n op: \"add\" | \"replace\" | \"remove\";\n path: string;\n value?: unknown;\n }[];\n }) => {\n return { success: true, delta };\n },\n }),\n };\n\n // Merge tools from user-managed MCP clients (user controls lifecycle)\n if (config.mcpClients && config.mcpClients.length > 0) {\n for (const client of config.mcpClients) {\n const mcpTools = await client.tools();\n streamTextParams.tools = {\n ...streamTextParams.tools,\n ...mcpTools,\n } as ToolSet;\n }\n }\n\n // Initialize MCP clients and get their tools from\n // `config.mcpServers` — the user-supplied static array.\n const allMcpServers: MCPClientConfig[] = [\n ...(config.mcpServers ?? []),\n ];\n if (allMcpServers.length > 0) {\n for (const serverConfig of allMcpServers) {\n let transport;\n\n if (serverConfig.type === \"http\") {\n const url = new URL(serverConfig.url);\n transport = new StreamableHTTPClientTransport(\n url,\n serverConfig.options,\n );\n } else if (serverConfig.type === \"sse\") {\n transport = new SSEClientTransport(\n new URL(serverConfig.url),\n serverConfig.headers,\n );\n }\n\n if (transport) {\n // A single MCP server being unavailable (down, 5xx, timeout,\n // bad auth) must NOT fail the whole run — skip it and continue\n // with the healthy servers and the agent's own tools. The run\n // degrades gracefully instead of erroring out.\n let mcpClient;\n try {\n mcpClient = await createMCPClient({ transport });\n } catch (err) {\n console.error(\n `[CopilotKit] MCP server ${serverConfig.url} failed to connect — skipping it for this run:`,\n err,\n );\n continue;\n }\n // Track it so it's closed on cleanup even if tools() fails.\n mcpClients.push(mcpClient);\n try {\n // Get tools from this MCP server and merge with existing tools\n const mcpTools = await mcpClient.tools();\n streamTextParams.tools = {\n ...streamTextParams.tools,\n ...mcpTools,\n } as ToolSet;\n } catch (err) {\n console.error(\n `[CopilotKit] MCP server ${serverConfig.url} tools() failed — skipping its tools for this run:`,\n err,\n );\n }\n }\n }\n }\n\n // Call streamText and process the stream\n const response = streamText({\n ...streamTextParams,\n abortSignal: abortController.signal,\n });\n\n // Names of tools flagged as interrupt tools (classic config only).\n const interruptToolNames = new Set(\n (isFactoryConfig(this.config) ? [] : (this.config.tools ?? []))\n .filter((t) => t.interrupt)\n .map((t) => t.name),\n );\n const interruptToolMeta = new Map(\n (isFactoryConfig(this.config) ? [] : (this.config.tools ?? []))\n .filter((t) => t.interrupt)\n .map((t) => [\n t.name,\n {\n reason: t.interruptReason ?? \"tool_call\",\n message: t.interruptMessage,\n },\n ]),\n );\n const pendingInterrupts: Interrupt[] = [];\n\n const toolCallStates = new Map<\n string,\n {\n started: boolean;\n hasArgsDelta: boolean;\n ended: boolean;\n toolName?: string;\n }\n >();\n\n const ensureToolCallState = (toolCallId: string) => {\n let state = toolCallStates.get(toolCallId);\n if (!state) {\n state = { started: false, hasArgsDelta: false, ended: false };\n toolCallStates.set(toolCallId, state);\n }\n return state;\n };\n\n // Process fullStream events\n for await (const part of response.fullStream) {\n // Close any open reasoning lifecycle on every event except\n // reasoning-delta, which arrives mid-block and must not interrupt it.\n if (part.type !== \"reasoning-delta\") {\n closeReasoningIfOpen();\n }\n\n switch (part.type) {\n case \"abort\": {\n const abortEndEvent: RunFinishedEvent = {\n type: EventType.RUN_FINISHED,\n threadId: input.threadId,\n runId: input.runId,\n };\n subscriber.next(abortEndEvent);\n terminalEventEmitted = true;\n\n // Complete the observable\n subscriber.complete();\n break;\n }\n case \"reasoning-start\": {\n // Use SDK-provided id, or generate a fresh UUID if the id is falsy,\n // \"0\", or matches the non-unique pattern emitted by @ai-sdk/openai-compatible\n // (e.g. \"txt-0\", \"reasoning-0\", \"msg-0\").\n const providedId = \"id\" in part ? part.id : undefined;\n const isNonUniqueId =\n !providedId ||\n providedId === \"0\" ||\n /^(txt|reasoning|msg)-0$/.test(providedId);\n reasoningMessageId = isNonUniqueId\n ? randomUUID()\n : (providedId as typeof reasoningMessageId);\n