diff --git a/apps/docs/mint.json b/apps/docs/mint.json index 0a76a8a1..2d7dfa9e 100644 --- a/apps/docs/mint.json +++ b/apps/docs/mint.json @@ -27,11 +27,6 @@ }, "anchors": [ - { - "name": "Documentation", - "icon": "book-open-cover", - "url": "https://docs.copilotkit.ai" - }, { "name": "Community", "icon": "slack", @@ -50,7 +45,35 @@ }, { "group": "API Reference", - "pages": ["reference/copilotkit-core"] + "pages": [ + { + "group": "Core", + "pages": [ + "reference/copilotkit-core", + "reference/proxied-copilot-runtime-agent", + "reference/frontend-tool" + ] + }, + { + "group": "React", + "pages": [ + { + "group": "Providers", + "pages": [ + "reference/copilotkit-provider", + "reference/copilot-chat-configuration-provider" + ] + }, + { + "group": "Hooks", + "pages": [ + "reference/use-copilot-kit", + "reference/use-agent" + ] + } + ] + } + ] } ], "footerSocials": { diff --git a/apps/docs/reference/copilot-chat-configuration-provider.mdx b/apps/docs/reference/copilot-chat-configuration-provider.mdx new file mode 100644 index 00000000..8348daee --- /dev/null +++ b/apps/docs/reference/copilot-chat-configuration-provider.mdx @@ -0,0 +1,179 @@ +--- +title: CopilotChatConfigurationProvider +description: "CopilotChatConfigurationProvider API Reference" +--- + +`CopilotChatConfigurationProvider` is a React context provider that manages configuration for chat UI components, +including labels, agent ID, and thread ID. It enables customization of all text labels in the chat interface and +provides context for chat components to access configuration values. + +## What is CopilotChatConfigurationProvider? + +The CopilotChatConfigurationProvider: + +- Manages UI labels for all chat components (placeholders, button labels, etc.), allowing for localization or + customization of the chat interface. +- Lets you to access the current `agentId` and `threadId` +- Supports nested configuration with proper inheritance + +## Basic Usage + +Wrap your chat components with the provider to customize configuration: + +```tsx +import { CopilotChatConfigurationProvider } from "@copilotkitnext/react"; + +function App() { + return ( + + {/* Your chat components */} + + ); +} +``` + +## Props + +### threadId + +`string` **(required)** + +A unique identifier for the conversation thread. This is used to maintain conversation history and context. + +```tsx +{children} +``` + +### agentId + +`string` **(optional)** + +The ID of the agent to use for this chat context. If not provided, defaults to `"default"`. + +```tsx + + {children} + +``` + +### labels + +`Partial` **(optional)** + +Custom labels for chat UI elements. Any labels not provided will use the default values. + +```tsx + + {children} + +``` + +### children + +`ReactNode` **(required)** + +The React components that will have access to the chat configuration context. + +## Using with CopilotChat + +The `CopilotChat` component automatically creates its own `CopilotChatConfigurationProvider` internally. When using +`CopilotChat`, you can pass configuration directly as props: + +```tsx + +``` + +### Priority System + +When `CopilotChat` is used within an existing `CopilotChatConfigurationProvider`, values are merged with the following +priority (highest to lowest): + +1. Props passed directly to `CopilotChat` +2. Values from the outer `CopilotChatConfigurationProvider` +3. Default values + +Example: + +```tsx + + + +``` + +## Accessing Configuration + +Use the `useCopilotChatConfiguration` hook to access configuration values in your components: + +```tsx +import { useCopilotChatConfiguration } from "@copilotkitnext/react"; + +function MyComponent() { + const config = useCopilotChatConfiguration(); + + if (!config) { + // No provider found + return null; + } + + return ( +
+

Agent: {config.agentId}

+

Thread: {config.threadId}

+

Labels: {JSON.stringify(config.labels)}

+
+ ); +} +``` + +Note: The hook returns `null` if no provider is found in the component tree. + +## Localization Example + +The provider is ideal for implementing localization: + +```tsx +import { useTranslation } from "react-i18next"; + +function LocalizedChat() { + const { t } = useTranslation(); + + return ( + + + + ); +} +``` diff --git a/apps/docs/reference/copilot-chat.mdx b/apps/docs/reference/copilot-chat.mdx new file mode 100644 index 00000000..656c6267 --- /dev/null +++ b/apps/docs/reference/copilot-chat.mdx @@ -0,0 +1,116 @@ +--- +title: CopilotChat +description: "CopilotChat Component API Reference" +--- + +`CopilotChat` is a React component that provides a complete chat interface for interacting with AI agents. It handles +message display, user input, tool execution rendering, and agent communication automatically. + +## What is CopilotChat? + +The CopilotChat component: + +- Provides a complete chat UI out of the box +- Manages conversation threads and message history +- Automatically connects to agents and handles message routing +- Renders tool executions with visual feedback +- Supports customization through props and slots +- Handles auto-scrolling and responsive layouts + +## Basic Usage + +```tsx +import { CopilotChat, CopilotKitProvider } from "@copilotkitnext/react"; + +function App() { + return ( + + + + ); +} +``` + +## Props + +### agentId + +`string` **(optional)** + +The ID of the agent to connect to. Defaults to `"default"`. + +```tsx + +``` + +### threadId + +`string` **(optional)** + +The conversation thread ID. If not provided, a new thread ID is automatically generated. + +```tsx + +``` + +### labels + +`Partial` **(optional)** + +Customize the text labels used throughout the chat interface. + +```tsx + +``` + +### autoScroll + +`boolean` **(optional, default: true)** + +Automatically scroll to the bottom when new messages appear. + +```tsx + +``` + +### className + +`string` **(optional)** + +CSS class name for the root container. + +```tsx + +``` + +### Component Slots + +CopilotChat supports slot customization for various parts of the UI: + +```tsx + +``` + +## Auto-scrolling Behavior + +CopilotChat automatically scrolls to the bottom when: + +- New messages are added +- The user is already near the bottom +- `autoScroll` prop is true (default) + +The scroll-to-bottom button appears when the user scrolls up and new content is available. diff --git a/apps/docs/reference/copilotkit-core.mdx b/apps/docs/reference/copilotkit-core.mdx index c8db2aea..2fffd078 100644 --- a/apps/docs/reference/copilotkit-core.mdx +++ b/apps/docs/reference/copilotkit-core.mdx @@ -4,47 +4,86 @@ description: "CopilotKitCore API Reference" --- `CopilotKitCore` is the client-side cross-framework orchestration layer that coordinates communication between your -application's UI, the remote Copilot runtime and using locally defined agents for development. +application's UI, agents abd the remote Copilot runtime. + +## Architecture Overview + +```mermaid +graph TB + subgraph Framework["Framework Layer"] + React["**CopilotKit React**
Components & Hooks"] + Angular["**CopilotKit Angular**
Directives & Services"] + Vue["**CopilotKit ...**
Future frameworks"] + end + + subgraph CoreLayer["Core Layer"] + Core["**CopilotKitCore**
Cross-framework Orchestration
State Management & Agent Coordination"] + end + + subgraph RuntimeLayer["Runtime Layer"] + Runtime["**CopilotRuntime**
AI Agent Execution
Backend Services"] + end + + React --> Core + Angular --> Core + Vue -.-> Core + Core <--> Runtime +``` ## What does CopilotKitCore do? CopilotKitCore solves several complex challenges in building AI-powered applications: -1. **Tool Execution Orchestration**: When agents need to call frontend tools (like updating UI or accessing browser - APIs), CopilotKitCore handles the entire lifecycle - parsing arguments, executing handlers, and automatically - re-running agents with tool results -2. **Unified Agent Management**: It manages remote server-side agents in production, while allowing you to define local - agents for development. -3. **State Synchronization**: It maintains consistent state across your application, ensuring all components have access - to the latest agents, tools, and context +1. **Agent Orchestration**: Handles the entire lifecycle of running AI agents, from starting the agent and executing + tools, to providing tool results, handling follow-ups and handling streaming updates to the UI. +2. **Runtime Connection**: Manages connecting to and synchronizing with the remote `CopilotRuntime`. +3. **State Synchronization**: Maintains consistent state across your application, ensuring all components have access to + the latest agents, tools, and context 4. **Framework Independence**: Works with any frontend framework, providing a consistent API whether you're using React, Angular, Vue, or vanilla JavaScript -At its heart, CopilotKitCore maintains the authoritative state for three critical elements: +At its heart, CopilotKitCore maintains the authoritative state for these critical elements: - **Agents**: Both local and remote AI agents that power your application's intelligence +- **Context**: Additional information that agents can use to make more informed decisions - **Frontend Tools**: Functions that agents can call to interact with your UI +- **Properties and Headers**: Application-specific data forwarded to agents as additional properties or HTTP headers - **Runtime Metadata**: Connection status, versioning, and configuration details ## Creating a CopilotKitCore Instance -When you instantiate CopilotKitCore, you're setting up the foundation for all AI interactions in your application. Here -are the configuration options you can provide: + + In most applications, you don’t need to create a `CopilotKitCore` instance yourself—CopilotKit initializes and manages + it behind the scenes. + -- `runtimeUrl?`: `string` The endpoint where your CopilotRuntime server is hosted. When provided, CopilotKitCore will - automatically connect and discover available remote agents. +Instantiate `CopilotKitCore` to initialize the client that manages runtime connections, agents, and tools. -- `headers?`: `Record` Custom HTTP headers to include with every request. Useful for authentication - tokens or tracking headers. +```typescript +new CopilotKitCore(config?: CopilotKitCoreConfig) +```` -- `properties?`: `Record` Application-specific data that gets forwarded to agents as context. This - might include user preferences, application state, or feature flags. +#### Parameters -- `agents?`: `Record` Local agents that run directly in the browser. The key becomes the agent's - identifier. +```typescript +interface CopilotKitCoreConfig { + runtimeUrl?: string; // The endpoint where your CopilotRuntime server is hosted + headers?: Record; // Custom HTTP headers to include with every request + properties?: Record; // Application-specific data forwarded to agents as context + agents__unsafe_dev_only?: Record; // Local agents for development only - production requires CopilotRuntime + tools?: FrontendTool[]; // Frontend functions that agents can invoke +} +``` -- `tools?`: `FrontendTool[]` Frontend functions that agents can invoke, such as updating UI elements or fetching - client-side data. +Configuration details: + +- `runtimeUrl`: When provided, CopilotKitCore will automatically connect and discover available remote agents. +- `headers`: Headers to include with every request. +- `properties`: Properties forwarded to agents as `forwardedProps`. +- `tools`: Frontend tools that agents can invoke. +- `agents__unsafe_dev_only`: **Development only** - This property is intended solely for rapid prototyping during + development. Production deployments require the security, reliability, and performance guarantees that only the + CopilotRuntime can provide. The key becomes the agent's identifier. ## Core Properties @@ -63,84 +102,194 @@ Once initialized, CopilotKitCore exposes several properties that provide insight ### State Management -- `headers`: `Record` The current request headers. You can modify these directly to update - authentication or add new headers on the fly. +- `headers`: `Readonly>` The current request headers sent with every request. -- `properties`: `Record` The application properties being forwarded to agents. Update these when your - application state changes. +- `properties`: `Readonly>` The application properties being forwarded to agents. -- `agents`: `Readonly>` A combined view of all available agents, merging local agents with - those discovered from the runtime. This is read-only to prevent accidental modifications. +- `agents`: `Readonly>` A combined view of all available agents, merging local development + agents with those discovered from the runtime. -- `tools`: `Readonly[]>` The complete list of registered frontend tools available to agents. +- `tools`: `Readonly[]>` The list of registered frontend tools available to agents. - `context`: `Readonly>` Contextual information that agents can use to make more informed decisions. -## Working with Configuration +## Understanding Runtime Connections -These methods allow you to dynamically update CopilotKitCore's configuration after initialization: +When you provide a `runtimeUrl`, CopilotKitCore initiates a connection to your CopilotRuntime server. Here's what +happens behind the scenes: -### `setRuntimeUrl(runtimeUrl: string | undefined): void` +1. CopilotKitCore sends a GET request to `{runtimeUrl}/info` +2. The runtime responds with available agents and metadata +3. For each remote agent, `CopilotKitCore` creates a `ProxiedCopilotRuntimeAgent` instance +4. These remote agents are merged with your local agents +5. Subscribers are notified of the connection status and agent availability -Changes the runtime endpoint and manages the connection lifecycle. This is useful when switching between different -environments or disconnecting from the runtime entirely. +### Runtime Connection States -### `setHeaders(headers: Record): void` +The connection to your runtime can be in one of four states: -Replaces all HTTP headers. Subscribers are notified so they can react to authentication changes or other header updates. +- `Disconnected`: No runtime URL is configured, or the connection was intentionally closed +- `Connecting`: Actively attempting to establish a connection with the runtime +- `Connected`: Successfully connected and remote agents are available +- `Error`: The connection attempt failed, but CopilotKitCore will continue with local agents only -### `setProperties(properties: Record): void` +## Event Subscription System -Updates the properties forwarded to agents. Use this when your application state changes in ways that might affect agent -behavior. +CopilotKitCore implements a robust event system that allows you to react to state changes and agent activities: -## Managing Agents +### subscribe() -Agents are the AI-powered components that process requests and generate responses. CopilotKitCore provides several -methods to manage them: +Registers a subscriber to receive events. The returned function can be called to unsubscribe, making cleanup +straightforward. -### `setAgents(agents: Record): void` +```typescript +subscribe(subscriber: CopilotKitCoreSubscriber): () => void +``` -Replaces all local agents while preserving remote agents from the runtime. This is useful for completely reconfiguring -your local agent setup. +### unsubscribe() -### `addAgent(params: CopilotKitCoreAddAgentParams): void` +Manually removes a subscriber if you prefer explicit cleanup over using the returned function. -Adds a single agent to your application. The agent is immediately available for use. +```typescript +unsubscribe(subscriber: CopilotKitCoreSubscriber): void +``` -- `params.id`: A unique identifier for the agent -- `params.agent`: The agent instance to add +## Subscribing to Events -### `removeAgent(id: string): void` +The event subscription system allows you to build reactive UIs that respond to CopilotKitCore's state changes. Each +event provides both the CopilotKitCore instance and relevant data: -Removes a local agent by its identifier. Remote agents from the runtime cannot be removed this way. +### onRuntimeConnectionStatusChanged() -### `getAgent(id: string): AbstractAgent | undefined` +Notified whenever the connection to the runtime changes state. Use this to show connection indicators in your UI. -Retrieves an agent by its identifier. Returns `undefined` if the agent doesn't exist or if the runtime is still -connecting. +```typescript +onRuntimeConnectionStatusChanged?: (event: { + copilotkit: CopilotKitCore; + status: CopilotKitCoreRuntimeConnectionStatus; +}) => void | Promise +``` -### `connectAgent(params: CopilotKitCoreConnectAgentParams): Promise` +### onToolExecutionStart() -Establishes a live connection with an agent, restoring any existing conversation history and subscribing to real-time -events. This method is particularly useful when: +Fired when an agent begins executing a tool. Useful for showing loading states or activity indicators. -- Reconnecting to an agent that's already running (displays new events as they occur) -- Restoring conversation history after a page refresh -- Setting up an agent that will receive updates from background processes +```typescript +onToolExecutionStart?: (event: { + copilotkit: CopilotKitCore; + toolCallId: string; + agentId: string; + toolName: string; + args: unknown; +}) => void | Promise +``` -The connection process: +### onToolExecutionEnd() -1. Provides the agent with current properties and available tools -2. Sets up error event subscribers for proper error handling -3. Restores any existing message history -4. Returns immediately without triggering a new agent run +Fired when tool execution completes, whether successfully or with an error. Use this to update your UI based on the +results. + +```typescript +onToolExecutionEnd?: (event: { + copilotkit: CopilotKitCore; + toolCallId: string; + agentId: string; + toolName: string; + result: string; + error?: string; +}) => void | Promise +``` + +### onAgentsChanged() + +Notified when agents are added, removed, or updated. This includes both local changes and runtime discovery. + +```typescript +onAgentsChanged?: (event: { + copilotkit: CopilotKitCore; + agents: Readonly>; +}) => void | Promise +``` + +### onContextChanged() + +Fired when context items are added or removed. Use this to keep UI elements in sync with available context. + +```typescript +onContextChanged?: (event: { + copilotkit: CopilotKitCore; + context: Readonly>; +}) => void | Promise +``` + +### onPropertiesChanged() + +Notified when application properties are updated. Useful for debugging or showing current configuration. + +```typescript +onPropertiesChanged?: (event: { + copilotkit: CopilotKitCore; + properties: Readonly>; +}) => void | Promise +``` -- `params.agent`: The agent to connect -- `params.agentId?`: Override the agent's default identifier +### onHeadersChanged() -### `runAgent(params: CopilotKitCoreRunAgentParams): Promise` +Fired when HTTP headers are modified. Important for tracking authentication state changes. + +```typescript +onHeadersChanged?: (event: { + copilotkit: CopilotKitCore; + headers: Readonly>; +}) => void | Promise +``` + +### onError() + +The central error handler for all CopilotKitCore operations. Every error includes a code and contextual information to +help with debugging. + +```typescript +onError?: (event: { + copilotkit: CopilotKitCore; + error: Error; + code: CopilotKitCoreErrorCode; + context: Record; +}) => void | Promise +``` + +## Error Handling + +CopilotKitCore provides detailed error information through typed error codes, making it easier to handle different +failure scenarios appropriately: + +### Understanding Error Codes + +Each error is categorized with a specific code that indicates what went wrong: + +- `RUNTIME_INFO_FETCH_FAILED`: The attempt to fetch runtime information failed. This might indicate network issues or an + incorrect runtime URL. + +- `AGENT_CONNECT_FAILED`: Failed to establish a connection with an agent. Check that the agent is properly configured. + +- `AGENT_RUN_FAILED`: The agent execution failed. This could be due to invalid messages or agent-side errors. + +- `AGENT_RUN_FAILED_EVENT`: The agent reported a failure through its event stream. + +- `AGENT_RUN_ERROR_EVENT`: The agent emitted an error event during execution. + +- `TOOL_ARGUMENT_PARSE_FAILED`: The arguments provided to a tool couldn't be parsed. This usually indicates a mismatch + between expected and actual parameters. + +- `TOOL_HANDLER_FAILED`: A tool's handler function threw an error during execution. + +## Running Agents + +CopilotKitCore provides two primary methods for executing agents: `runAgent` for immediate execution with message +handling, and `connectAgent` for establishing live connections to existing agent sessions. + +### runAgent() Executes an agent and intelligently handles the complete request-response cycle, including automatic tool execution and follow-up runs. @@ -164,65 +313,137 @@ Key behaviors: gracefully - **Wildcard Tools**: Supports a special "\*" tool that can handle any undefined tool request -- `params.agent`: The agent to execute -- `params.withMessages?`: Initial messages to send to the agent -- `params.agentId?`: Override the agent's default identifier +```typescript +runAgent(params: CopilotKitCoreRunAgentParams): Promise +``` + +#### Parameters + +```typescript +interface CopilotKitCoreRunAgentParams { + agent: AbstractAgent; // The agent to execute + withMessages?: Message[]; // Initial messages to send to the agent + agentId?: string; // Override the agent's default identifier +} +``` + +### connectAgent() + +Establishes a live connection with an agent, restoring any existing conversation history and subscribing to real-time +events. This method is particularly useful when: + +- Reconnecting to an agent that's already running (displays new events as they occur) +- Restoring conversation history after a page refresh +- Setting up an agent that will receive updates from background processes + +The connection process: + +1. Provides the agent with current properties and available tools +2. Sets up error event subscribers for proper error handling +3. Restores any existing message history +4. Returns immediately without triggering a new agent run + +```typescript +connectAgent(params: CopilotKitCoreConnectAgentParams): Promise +``` + +#### Parameters + +```typescript +interface CopilotKitCoreConnectAgentParams { + agent: AbstractAgent; // The agent to connect + agentId?: string; // Override the agent's default identifier +} +``` + +### getAgent() + +Retrieves an agent by its identifier. Returns `undefined` if the agent doesn't exist or if the runtime is still +connecting. + +```typescript +getAgent(id: string): AbstractAgent | undefined +``` ## Managing Context Context provides agents with additional information about the current state of your application: -### `addContext(context: Context): string` +### addContext() Adds contextual information that agents can reference. Returns a unique identifier for later removal. -- `context.description`: A human-readable description of what this context represents -- `context.value`: The actual context data +```typescript +addContext(context: Context): string +``` -### `removeContext(id: string): void` +#### Parameters + +```typescript +interface Context { + description: string; // A human-readable description of what this context represents + value: any; // The actual context data +} +``` + +### removeContext() Removes previously added context using its identifier. +```typescript +removeContext(id: string): void +``` + ## Managing Tools Tools are functions that agents can call to interact with your application: -### `addTool(tool: FrontendTool): void` +### addTool() Registers a new frontend tool. If a tool with the same name and agent scope already exists, the registration is skipped to prevent duplicates. -### `removeTool(id: string, agentId?: string): void` +```typescript +addTool(tool: FrontendTool): void +``` + +### removeTool() Removes a tool from the registry. You can remove tools globally or for specific agents: - When `agentId` is provided, removes the tool only for that agent - When `agentId` is omitted, removes only global tools with the matching name -### `getTool(params: CopilotKitCoreGetToolParams): FrontendTool | undefined` +```typescript +removeTool(id: string, agentId?: string): void +``` + +### getTool() Retrieves a tool by name, with intelligent fallback behavior: -- `params.toolName`: The tool's name -- `params.agentId?`: Look for agent-specific tools first - If an agent-specific tool isn't found, it falls back to global tools -### `setTools(tools: FrontendTool[]): void` - -Replaces all tools at once. Useful for completely reconfiguring available tools. - -## Event Subscription System +```typescript +getTool(params: CopilotKitCoreGetToolParams): FrontendTool | undefined +``` -CopilotKitCore implements a robust event system that allows you to react to state changes and agent activities: +#### Parameters -### `subscribe(subscriber: CopilotKitCoreSubscriber): () => void` +```typescript +interface CopilotKitCoreGetToolParams { + toolName: string; // The tool's name + agentId?: string; // Look for agent-specific tools first +} +``` -Registers a subscriber to receive events. The returned function can be called to unsubscribe, making cleanup -straightforward. +### setTools() -### `unsubscribe(subscriber: CopilotKitCoreSubscriber): void` +Replaces all tools at once. Useful for completely reconfiguring available tools. -Manually removes a subscriber if you prefer explicit cleanup over using the returned function. +```typescript +setTools(tools: FrontendTool[]): void +``` ## Tool Execution Lifecycle @@ -262,169 +483,77 @@ By default, after tool execution completes: - This continues until no more tools are requested - You can disable follow-up by setting `followUp: false` on specific tools -## Understanding Runtime Connections - -When you provide a `runtimeUrl`, CopilotKitCore initiates a connection to your CopilotRuntime server. Here's what -happens behind the scenes: - -1. CopilotKitCore sends a GET request to `{runtimeUrl}/info` -2. The runtime responds with available agents and metadata -3. For each remote agent, CopilotKitCore creates a ProxiedCopilotRuntimeAgent instance -4. These remote agents are merged with your local agents -5. Subscribers are notified of the connection status and agent availability - -## Runtime Connection States - -The connection to your runtime can be in one of four states: - -- `Disconnected`: No runtime URL is configured, or the connection was intentionally closed -- `Connecting`: Actively attempting to establish a connection with the runtime -- `Connected`: Successfully connected and remote agents are available -- `Error`: The connection attempt failed, but CopilotKitCore will continue with local agents only - -## Subscribing to Events - -The event subscription system allows you to build reactive UIs that respond to CopilotKitCore's state changes. Each -event provides both the CopilotKitCore instance and relevant data: - -### `onRuntimeConnectionStatusChanged` - -Notified whenever the connection to the runtime changes state. Use this to show connection indicators in your UI. +## Working with Configuration -```typescript -onRuntimeConnectionStatusChanged?: (event: { - copilotkit: CopilotKitCore; - status: CopilotKitCoreRuntimeConnectionStatus; -}) => void | Promise; -``` +These methods allow you to dynamically update CopilotKitCore's configuration after initialization: -### `onToolExecutionStart` +### setRuntimeUrl() -Fired when an agent begins executing a tool. Useful for showing loading states or activity indicators. +Changes the runtime endpoint and manages the connection lifecycle. This is useful when switching between different +environments or disconnecting from the runtime entirely. ```typescript -onToolExecutionStart?: (event: { - copilotkit: CopilotKitCore; - toolCallId: string; - agentId: string; - toolName: string; - args: unknown; -}) => void | Promise; +setRuntimeUrl(runtimeUrl: string | undefined): void ``` -### `onToolExecutionEnd` +### setHeaders() -Fired when tool execution completes, whether successfully or with an error. Use this to update your UI based on the -results. +Replaces all HTTP headers. Subscribers are notified so they can react to authentication changes or other header updates. ```typescript -onToolExecutionEnd?: (event: { - copilotkit: CopilotKitCore; - toolCallId: string; - agentId: string; - toolName: string; - result: string; - error?: string; -}) => void | Promise; +setHeaders(headers: Record): void ``` -### `onAgentsChanged` +### setProperties() -Notified when agents are added, removed, or updated. This includes both local changes and runtime discovery. +Updates the properties forwarded to agents. Use this when your application state changes in ways that might affect agent +behavior. ```typescript -onAgentsChanged?: (event: { - copilotkit: CopilotKitCore; - agents: Readonly>; -}) => void | Promise; +setProperties(properties: Record): void ``` -### `onContextChanged` +## Managing Local Agents -Fired when context items are added or removed. Use this to keep UI elements in sync with available context. + + In production applications, agents must be managed through the CopilotRuntime, which provides proper isolation, + monitoring, and scalability. The local agent methods below are marked `__unsafe_dev_only` as they're intended solely + for rapid prototyping during development. Production deployments require the security and performance guarantees that + only the CopilotRuntime can provide. + -```typescript -onContextChanged?: (event: { - copilotkit: CopilotKitCore; - context: Readonly>; -}) => void | Promise; -``` +Agents are the AI-powered components that process requests and generate responses. CopilotKitCore provides several +methods to manage them locally during development: -### `onPropertiesChanged` +### setAgents\_\_unsafe_dev_only() -Notified when application properties are updated. Useful for debugging or showing current configuration. +Replaces all local development agents while preserving remote agents from the runtime. ```typescript -onPropertiesChanged?: (event: { - copilotkit: CopilotKitCore; - properties: Readonly>; -}) => void | Promise; +setAgents__unsafe_dev_only(agents: Record): void ``` -### `onHeadersChanged` +### addAgent\_\_unsafe_dev_only() -Fired when HTTP headers are modified. Important for tracking authentication state changes. +Adds a single agent for development testing. ```typescript -onHeadersChanged?: (event: { - copilotkit: CopilotKitCore; - headers: Readonly>; -}) => void | Promise; +addAgent__unsafe_dev_only(params: CopilotKitCoreAddAgentParams): void ``` -### `onError` - -The central error handler for all CopilotKitCore operations. Every error includes a code and contextual information to -help with debugging. +#### Parameters ```typescript -onError?: (event: { - copilotkit: CopilotKitCore; - error: Error; - code: CopilotKitCoreErrorCode; - context: Record; -}) => void | Promise; +interface CopilotKitCoreAddAgentParams { + id: string; // A unique identifier for the agent + agent: AbstractAgent; // The agent instance to add +} ``` -## Error Handling - -CopilotKitCore provides detailed error information through typed error codes, making it easier to handle different -failure scenarios appropriately: - -### Understanding Error Codes +### removeAgent\_\_unsafe_dev_only() -Each error is categorized with a specific code that indicates what went wrong: - -- `RUNTIME_INFO_FETCH_FAILED`: The attempt to fetch runtime information failed. This might indicate network issues or an - incorrect runtime URL. - -- `AGENT_CONNECT_FAILED`: Failed to establish a connection with an agent. Check that the agent is properly configured. - -- `AGENT_RUN_FAILED`: The agent execution failed. This could be due to invalid messages or agent-side errors. - -- `AGENT_RUN_FAILED_EVENT`: The agent reported a failure through its event stream. - -- `AGENT_RUN_ERROR_EVENT`: The agent emitted an error event during execution. - -- `TOOL_ARGUMENT_PARSE_FAILED`: The arguments provided to a tool couldn't be parsed. This usually indicates a mismatch - between expected and actual parameters. - -- `TOOL_HANDLER_FAILED`: A tool's handler function threw an error during execution. - -## TypeScript Types and Interfaces - -CopilotKitCore is fully typed to provide excellent IDE support and catch errors at compile time: - -### `CopilotKitCoreConfig` - -The configuration object used when creating a new CopilotKitCore instance: +Removes a local development agent by its identifier. Remote agents from the runtime cannot be removed this way. ```typescript -interface CopilotKitCoreConfig { - runtimeUrl?: string; - agents?: Record; - headers?: Record; - properties?: Record; - tools?: FrontendTool[]; -} +removeAgent__unsafe_dev_only(id: string): void ``` diff --git a/apps/docs/reference/copilotkit-provider.mdx b/apps/docs/reference/copilotkit-provider.mdx new file mode 100644 index 00000000..cc7bd58f --- /dev/null +++ b/apps/docs/reference/copilotkit-provider.mdx @@ -0,0 +1,245 @@ +--- +title: CopilotKitProvider +description: "CopilotKitProvider API Reference" +--- + +`CopilotKitProvider` is the React context provider that initializes and manages `CopilotKitCore` for your React +application. It provides all child components with access to agents, tools, and copilot functionality through React's +context API. + +## What is CopilotKitProvider? + +The CopilotKitProvider is the root component that: + +- Creates and manages a `CopilotKitCore` instance +- Provides React-specific features like hooks and render components +- Manages tool rendering and human-in-the-loop interactions +- Handles state synchronization between your React app and AI agents + +## Basic Usage + +Typically you would wrap your application with `CopilotKitProvider` at the root level: + +```tsx +import { CopilotKitProvider } from "@copilotkitnext/react"; + +function App() { + return ( + + {/* Your app components */} + + ); +} +``` + +## Props + +### runtimeUrl + +`string` **(optional)** + +The URL of your CopilotRuntime server. The provider will automatically connect to the runtime and discover available +agents. + +```tsx +{children} +``` + +### headers + +`Record` **(optional)** + +Custom HTTP headers to include with every request to the runtime. Useful for authentication and custom metadata. + +```tsx + + {children} + +``` + +### properties + +`Record` **(optional)** + +Application-specific data that gets forwarded to agents as additional context. Agents receive these as `forwardedProps`. + +```tsx + + {children} + +``` + +### agents\_\_unsafe_dev_only + +`Record` **(optional, development only)** + + + This property is intended solely for rapid prototyping during development. Production deployments require the + security, reliability, and performance guarantees that only the CopilotRuntime can provide. + + +Local agents for development testing. The key becomes the agent's identifier. + +```tsx +import { HttpAgent } from "@ag-ui/client"; + +const devAgent = new HttpAgent({ + url: "http://localhost:8000", +}); + + + {children} +; +``` + +### renderToolCalls + +`ReactToolCallRender[]` **(optional)** + +A static list of components to render when specific tools are called. Enables visual feedback for tool execution. + +```tsx +const renderToolCalls = [ + { + name: "searchProducts", + args: z.object({ + query: z.string(), + }), + render: ({ args }) =>
Searching for: {args.query}
, + }, +]; + +{children}; +``` + + + The `renderToolCalls` array must be stable across renders. Define it outside your component or use `useMemo`. For + dynamic tool rendering, use the `useRenderToolCall` hook instead. + + +### frontendTools + +`ReactFrontendTool[]` **(optional)** + +A static list of frontend tools that agents can invoke. These are React-specific wrappers around the base `FrontendTool` +type with additional rendering capabilities. + +```tsx +const tools = [ + { + name: "showNotification", + description: "Display a notification to the user", + parameters: z.object({ + message: z.string(), + type: z.enum(["info", "success", "warning", "error"]), + }), + handler: async ({ message, type }) => { + toast[type](message); + return "Notification displayed"; + }, + }, +]; + +{children}; +``` + + + The `frontendTools` array must be stable across renders. For dynamically adding/removing tools, use the + `useFrontendTool` hook. + + +### humanInTheLoop + +`ReactHumanInTheLoop[]` **(optional)** + +Tools that require human interaction or approval before execution. These tools pause agent execution until the user +responds. + +```tsx +const humanInTheLoop = [ + { + name: "confirmAction", + description: "Request user confirmation for an action", + parameters: z.object({ + action: z.string(), + details: z.string(), + }), + render: ({ args, resolve }) => ( + resolve({ confirmed: true })} + onCancel={() => resolve({ confirmed: false })} + /> + ), + }, +]; + +{children}; +``` + +### children + +`ReactNode` **(required)** + +The React components that will have access to the CopilotKit context. + +## Context Value + +The provider makes a `CopilotKitContextValue` available to child components through React context: + +```typescript +interface CopilotKitContextValue { + copilotkit: CopilotKitCore; + renderToolCalls: ReactToolCallRender[]; + currentRenderToolCalls: ReactToolCallRender[]; + setCurrentRenderToolCalls: React.Dispatch[]>>; +} +``` + +Access this context using the `useCopilotKit` hook: + +```tsx +import { useCopilotKit } from "@copilotkitnext/react"; + +function MyComponent() { + const { copilotkit } = useCopilotKit(); + + // Access CopilotKitCore instance + const agent = copilotkit.getAgent("assistant"); +} +``` + +## Considerations + +### Server-Side Rendering (SSR) + +The provider is compatible with SSR but won't fetch runtime information during server-side rendering. The runtime +connection is established only on the client side to prevent blocking SSR. + +### Dynamic Updates + +You can dynamically update the following props: + +- `runtimeUrl`: Changing this will disconnect from the current runtime and connect to the new one +- `headers`: Updates are applied to all future requests +- `properties`: Changes are immediately available to agents diff --git a/apps/docs/reference/frontend-tool.mdx b/apps/docs/reference/frontend-tool.mdx new file mode 100644 index 00000000..4dd32964 --- /dev/null +++ b/apps/docs/reference/frontend-tool.mdx @@ -0,0 +1,150 @@ +--- +title: FrontendTool +description: "FrontendTool API Reference" +--- + +`FrontendTool` is a cross-platform type that defines tools (functions) that AI agents can invoke in your frontend +application. These tools enable agents to interact the user, retrieve data, perform actions, and integrate with your +application's functionality. + +## What is a FrontendTool? + +A FrontendTool represents a capability you expose to AI agents, allowing them to: + +- Interact with the user +- Fetch or manipulate application data +- Trigger application workflows + +Frontend tools are framework-agnostic and work consistently across all frameworks through `CopilotKitCore`. + +## Type Definition + +```typescript +type FrontendTool = Record> = { + name: string; + description?: string; + parameters?: z.ZodType; + handler?: (args: T, toolCall: ToolCall) => Promise; + followUp?: boolean; + agentId?: string; +}; +``` + +## Properties + +### name + +`string` **(required)** + +A unique identifier for the tool. This is the name agents will use to request this tool's execution. Avoid spaces and +special characters. + +```typescript +{ + name: "searchProducts"; +} +``` + +A special wildcard tool is available with the name `*`. This tool will handle any unmatched tool requests. + +### description + +`string` **(optional)** + +A human-readable description that helps agents understand when and how to use this tool. This description is sent to the +LLM to guide its decision-making. + +```typescript +{ + name: "searchProducts", + description: "Search for products in the catalog by name, category, or price range" +} +``` + +### parameters + +`z.ZodType` **(optional)** + +A Zod schema that defines and validates the tool's input parameters. This ensures type safety and provides automatic +validation of agent-provided arguments. + +```typescript +import { z } from "zod"; + +{ + name: "updateUserProfile", + parameters: z.object({ + firstName: z.string().optional(), + lastName: z.string().optional(), + email: z.string().email().optional(), + preferences: z.object({ + theme: z.enum(["light", "dark"]).optional(), + notifications: z.boolean().optional() + }).optional() + }) +} +``` + +### handler + +`(args: T, toolCall: ToolCall) => Promise` **(optional)** + +The async function that executes when the agent invokes this tool. It receives the validated arguments and a ToolCall +object containing metadata about the invocation. + +```typescript +{ + name: "addToCart", + parameters: z.object({ + productId: z.string(), + quantity: z.number().min(1) + }), + handler: async ({productId, quantity}) => { + // Add product to cart + const result = await cartService.addItem(productId, quantity); + + // Return a string or serializable object + return { + success: true, + cartTotal: result.total, + itemCount: result.itemCount + }; + } +} +``` + +### followUp + +`boolean` **(optional, default: true)** + +Controls whether the agent should be automatically re-run after this tool completes. When `true`, the tool's result is +added to the conversation and the agent continues processing. Disable follow-up for final actions that complete a +workflow. + +```typescript +{ + name: "saveDocument", + handler: async (args) => { + await documentService.save(args); + return "Document saved successfully"; + }, + followUp: false // Don't re-run agent after saving +} +``` + +### agentId + +`string` **(optional)** + +Restricts this tool to a specific agent. When set, only the specified agent can invoke this tool. + +```typescript +{ + name: "adminAction", + agentId: "admin-assistant", + handler: async (args) => { + // Only the admin-assistant agent can call this + return await performAdminAction(args); + } +} +``` diff --git a/apps/docs/reference/proxied-copilot-runtime-agent.mdx b/apps/docs/reference/proxied-copilot-runtime-agent.mdx new file mode 100644 index 00000000..475f22f1 --- /dev/null +++ b/apps/docs/reference/proxied-copilot-runtime-agent.mdx @@ -0,0 +1,80 @@ +--- +title: ProxiedCopilotRuntimeAgent +description: "ProxiedCopilotRuntimeAgent API Reference" +--- + +`ProxiedCopilotRuntimeAgent` is a specialized HTTP agent that acts as a proxy between your client application and remote +agents hosted on the `CopilotRuntime` server. It extends the base `HttpAgent` class to provide seamless communication +with runtime-hosted agents. + +## Architecture Overview + +```mermaid +graph LR + subgraph Client["Client Application"] + Core[CopilotKitCore] + PRA1[ProxiedCopilotRuntimeAgent
customer-support] + PRA2[ProxiedCopilotRuntimeAgent
code-assistant] + PRA3[ProxiedCopilotRuntimeAgent
data-analyst] + + Core --> PRA1 + Core --> PRA2 + Core --> PRA3 + end + + subgraph Server["CopilotRuntime Server"] + Runtime[Runtime API] + Agent1[Agent: customer-support] + Agent2[Agent: code-assistant] + Agent3[Agent: data-analyst] + + Runtime --> Agent1 + Runtime --> Agent2 + Runtime --> Agent3 + end + + PRA1 -.-> Runtime + PRA2 -.-> Runtime + PRA3 -.-> Runtime +``` + +## What is ProxiedCopilotRuntimeAgent? + +When `CopilotKitCore` connects to a `CopilotRuntime` server, it discovers available remote agents. For each remote +agent, it creates a `ProxiedCopilotRuntimeAgent` instance that handles all communication with that specific agent +through the runtime's API endpoints. + +Key characteristics: + +- **Remote Agent Communication**: Handles communication with the remote agent through the runtime's API endpoints +- **Header and Property Forwarding**: Inherits and forwards authentication headers and properties from `CopilotKitCore` +- **Connection and History Management**: Handles loading history and reconnecting to existing live agent sessions + +## How does it work? + +`CopilotKitCore` automatically creates `ProxiedCopilotRuntimeAgent` instances during runtime discovery: + +1. `CopilotKitCore` fetches `/info` from the runtime +2. The runtime responds with available agents +3. For each agent, `CopilotKitCore` creates a `ProxiedCopilotRuntimeAgent` +4. These agents are merged with any local agents +5. The agents become available through `copilotKit.getAgent()` + +## Creating a ProxiedCopilotRuntimeAgent + + + In typical usage, you don't create `ProxiedCopilotRuntimeAgent` instances directly. `CopilotKitCore` automatically + creates them when discovering agents from the runtime. + + +```typescript +import { ProxiedCopilotRuntimeAgent } from "@copilotkitnext/core"; + +const agent = new ProxiedCopilotRuntimeAgent({ + runtimeUrl: "https://your-runtime.example.com", + agentId: "my-agent", + headers: { + Authorization: "Bearer your-token", + }, +}); +``` diff --git a/apps/docs/reference/use-agent-context.mdx b/apps/docs/reference/use-agent-context.mdx new file mode 100644 index 00000000..7bebffdb --- /dev/null +++ b/apps/docs/reference/use-agent-context.mdx @@ -0,0 +1,384 @@ +--- +title: useAgentContext +description: "useAgentContext Hook API Reference" +--- + +`useAgentContext` is a React hook that provides contextual information to AI agents during their execution. It allows +you to dynamically add relevant data that agents can use to make more informed decisions and provide better responses. + +## What is useAgentContext? + +The useAgentContext hook: + +- Provides contextual information to agents +- Automatically manages context lifecycle (add on mount, remove on unmount) +- Updates context when values change +- Helps agents understand application state and user data + +## Basic Usage + +```tsx +import { useAgentContext } from "@copilotkitnext/react"; + +function UserPreferences() { + const userSettings = { + theme: "dark", + language: "en", + timezone: "UTC-5", + }; + + useAgentContext({ + description: "User preferences and settings", + value: userSettings, + }); + + return
User preferences loaded
; +} +``` + +## Parameters + +The hook accepts a single `Context` object with the following properties: + +### description + +`string` **(required)** + +A clear description of what this context represents. This helps agents understand how to use the provided information. + +```tsx +useAgentContext({ + description: "Current shopping cart contents", + value: cartItems, +}); +``` + +### value + +`any` **(required)** + +The actual data to provide as context. Can be any serializable value including objects, arrays, strings, or numbers. + +```tsx +useAgentContext({ + description: "Current form validation state", + value: { + hasErrors: false, + touchedFields: ["email", "name"], + dirtyFields: ["email"], + isSubmitting: false, + }, +}); +``` + +## Examples + +### User Preferences Context + +```tsx +import { useAgentContext } from "@copilotkitnext/react"; +import { useUserPreferences } from "./hooks/useUserPreferences"; + +function UserPreferencesContext() { + const { preferences, isLoading } = useUserPreferences(); + + useAgentContext({ + description: "User display preferences and settings", + value: { + theme: preferences?.theme || "light", + language: preferences?.language || "en", + timezone: preferences?.timezone || "UTC", + displayDensity: preferences?.displayDensity || "comfortable", + isLoading, + }, + }); + + return null; // Context-only component +} +``` + +### Form State Context + +```tsx +import { useAgentContext } from "@copilotkitnext/react"; +import { useState } from "react"; + +function ContactForm() { + const [formData, setFormData] = useState({ + name: "", + email: "", + subject: "", + message: "", + }); + + // Provide form state to agent for assistance + useAgentContext({ + description: "Contact form current state", + value: { + formData, + hasUnsavedChanges: Object.values(formData).some((v) => v !== ""), + isValid: formData.email.includes("@") && formData.name.length > 0, + }, + }); + + return ( +
+ setFormData({ ...formData, name: e.target.value })} + placeholder="Name" + /> + {/* Rest of form fields */} +
+ ); +} +``` + +### Application State Context + +```tsx +import { useAgentContext } from "@copilotkitnext/react"; +import { useLocation } from "react-router-dom"; + +function AppStateContext() { + const location = useLocation(); + const currentTime = new Date().toISOString(); + + useAgentContext({ + description: "Current application state and navigation", + value: { + currentPath: location.pathname, + queryParams: Object.fromEntries(new URLSearchParams(location.search)), + timestamp: currentTime, + }, + }); + + return null; +} +``` + +### Dynamic Data Context + +```tsx +import { useAgentContext } from "@copilotkitnext/react"; +import { useEffect, useState } from "react"; + +function DynamicDataContext() { + const [data, setData] = useState(null); + + useEffect(() => { + const fetchData = async () => { + const response = await fetch("/api/context-data"); + setData(await response.json()); + }; + fetchData(); + }, []); + + // Context updates automatically when data changes + useAgentContext({ + description: "Dynamic application data", + value: data || { loading: true }, + }); + + return null; +} +``` + +### Multiple Contexts + +```tsx +import { useAgentContext } from "@copilotkitnext/react"; + +function MultipleContexts() { + const userContext = { id: "123", name: "John" }; + const appContext = { version: "1.0.0", features: ["chat", "search"] }; + + // Use multiple hooks for different contexts + useAgentContext({ + description: "User information", + value: userContext, + }); + + useAgentContext({ + description: "Application configuration", + value: appContext, + }); + + return
Multiple contexts provided
; +} +``` + +## Context Lifecycle + +### Automatic Management + +Context is automatically managed throughout the component lifecycle: + +```tsx +function ManagedContext() { + const [count, setCount] = useState(0); + + useAgentContext({ + description: "Counter state", + value: { count, lastUpdated: Date.now() }, + }); + + // Context is: + // 1. Added when component mounts + // 2. Updated when count changes + // 3. Removed when component unmounts + + return ; +} +``` + +### Updates on Change + +Context automatically updates when values change: + +```tsx +function ReactiveContext() { + const [filters, setFilters] = useState({ + category: "all", + priceRange: [0, 100], + }); + + // Context updates whenever filters change + useAgentContext({ + description: "Active search filters", + value: filters, + }); + + return ( +
+ +
+ ); +} +``` + +## Best Practices + +### Descriptive Context Names + +Provide clear, descriptive names for your context: + +```tsx +// ✅ Good - Clear and specific +useAgentContext({ + description: "E-commerce shopping cart with items and totals", + value: cartData, +}); + +// ❌ Avoid - Too vague +useAgentContext({ + description: "Data", + value: cartData, +}); +``` + +### Structured Data + +Organize context data in a structured format: + +```tsx +// ✅ Good - Well-structured data +useAgentContext({ + description: "Order processing state", + value: { + orderId: "ORD-123", + status: "processing", + items: [{ id: "1", name: "Product", quantity: 2, price: 29.99 }], + customer: { + id: "CUST-456", + email: "user@example.com", + }, + timestamps: { + created: "2024-01-01T10:00:00Z", + updated: "2024-01-01T10:30:00Z", + }, + }, +}); + +// ❌ Avoid - Unstructured data +useAgentContext({ + description: "Order info", + value: "Order ORD-123 for user@example.com with 2 items", +}); +``` + +### Performance Optimization + +Memoize complex computed values: + +```tsx +import { useMemo } from "react"; + +function OptimizedContext({ items }) { + const contextValue = useMemo( + () => ({ + itemCount: items.length, + totalValue: items.reduce((sum, item) => sum + item.price, 0), + categories: [...new Set(items.map((item) => item.category))], + }), + [items], + ); + + useAgentContext({ + description: "Computed inventory statistics", + value: contextValue, + }); + + return null; +} +``` + +## Integration with Agents + +Context provided through this hook is available to agents during execution: + +```tsx +import { useAgentContext, useAgent, useCopilotKit } from "@copilotkitnext/react"; + +function IntegratedExample() { + const { agent } = useAgent(); + const { copilotkit } = useCopilotKit(); + const [productSearch, setProductSearch] = useState(""); + + // Provide search context + useAgentContext({ + description: "Current product search parameters", + value: { + searchQuery: productSearch, + resultsPerPage: 20, + sortBy: "relevance", + }, + }); + + const handleSearch = async () => { + // Agent has access to the context when running + agent?.addMessage({ + id: crypto.randomUUID(), + role: "user", + content: `Help me refine my search for: ${productSearch}`, + }); + + await copilotkit.runAgent({ agent }); + }; + + return ( +
+ setProductSearch(e.target.value)} + placeholder="Search products..." + /> + +
+ ); +} +``` diff --git a/apps/docs/reference/use-agent.mdx b/apps/docs/reference/use-agent.mdx new file mode 100644 index 00000000..92bd7971 --- /dev/null +++ b/apps/docs/reference/use-agent.mdx @@ -0,0 +1,502 @@ +--- +title: useAgent +description: "useAgent Hook API Reference" +--- + +`useAgent` is a React hook that provides access to [AG-UI](https://ag-ui.com) agents and subscribes to their state +changes. It enables components to interact with agents, access their messages, and respond to updates in real-time. + +## What is useAgent? + +The useAgent hook: + +- Retrieves an agent by ID from the CopilotKit context +- Subscribes to agent state changes (messages, state, run status) +- Automatically triggers component re-renders when the agent updates +- Handles cleanup of subscriptions when the component unmounts + +## Basic Usage + +```tsx +import { useAgent } from "@copilotkitnext/react"; + +function ChatComponent() { + const { agent } = useAgent({ agentId: "assistant" }); + + if (!agent) { + return
Loading agent...
; + } + + return ( +
+

Agent: {agent.id}

+
Messages: {agent.messages.length}
+
Running: {agent.isRunning ? "Yes" : "No"}
+
+ ); +} +``` + +## Parameters + +### agentId + +`string` **(optional)** + +The ID of the agent to retrieve. If not provided, defaults to `"default"`. + +```tsx +const { agent } = useAgent({ agentId: "customer-support" }); +``` + +### updates + +`UseAgentUpdate[]` **(optional)** + +An array of update types to subscribe to. This allows you to optimize re-renders by only subscribing to specific +changes. + +```tsx +import { useAgent, UseAgentUpdate } from "@copilotkitnext/react"; + +const { agent } = useAgent({ + agentId: "assistant", + updates: [UseAgentUpdate.OnMessagesChanged], +}); +``` + +Available update types: + +- `UseAgentUpdate.OnMessagesChanged` - Updates when messages are added or modified +- `UseAgentUpdate.OnStateChanged` - Updates when agent state changes +- `UseAgentUpdate.OnRunStatusChanged` - Updates when agent starts or stops running + +If `updates` is not provided, the hook subscribes to all update types by default. + +## Return Value + +The hook returns an object with a single property: + +### agent + +`AbstractAgent | undefined` + +The agent instance if found, or `undefined` if: + +- The agent with the specified ID doesn't exist +- The runtime is still connecting +- No agents are available + +## Accessing Agent Messages + +The agent's message history is available through the `messages` property. You can read messages and add new ones to +create interactive conversations. + +### Reading Messages + +```tsx +function SimpleChat() { + const { agent } = useAgent(); + + if (!agent) { + return
No agent available
; + } + + return ( +
+ {agent.messages.map((message) => ( +
+ {message.role}: {message.content} +
+ ))} +
+ ); +} +``` + +### Sending Messages + +To send a message, add it to the agent and then run the agent: + +```tsx +import { useAgent } from "@copilotkitnext/react"; +import { useCopilotKit } from "@copilotkitnext/react"; + +function MessageSender() { + const { agent } = useAgent({ agentId: "assistant" }); + const { copilotkit } = useCopilotKit(); + + const sendMessage = async (content: string) => { + if (!agent) return; + + // Add message to agent + agent.addMessage({ + id: crypto.randomUUID(), + role: "user", + content, + }); + + // Run the agent to get a response + await copilotkit.runAgent({ + agent, + agentId: "assistant", + }); + }; + + return ( +
+ +
+ ); +} +``` + +## Accessing and Updating Shared State + +Shared state enables real-time collaboration between users and agents. Both can read and modify the state, creating a +synchronized workspace for interactive features. + +### Understanding Shared State + +The agent's state is a shared data structure that: + +- Can be read by both your application and the agent +- Can be modified by both parties +- Automatically triggers re-renders when changed +- Persists throughout the conversation + +State updates cause re-renders when: + +- You call `agent.setState()` from your application +- The agent modifies the state during execution +- Any state change occurs, regardless of source + +### Reading State + +```tsx +function StateDisplay() { + const { agent } = useAgent({ + updates: [UseAgentUpdate.OnStateChanged], // Subscribe to state changes + }); + + if (!agent) return null; + + return ( +
+

Current State

+
{JSON.stringify(agent.state, null, 2)}
+
+ ); +} +``` + +### Updating State + +```tsx +function StateController() { + const { agent } = useAgent({ + updates: [UseAgentUpdate.OnStateChanged], + }); + + const updateState = (key: string, value: any) => { + if (!agent) return; + + // Update the shared state + agent.setState({ + ...agent.state, + [key]: value, + }); + + // This will trigger re-renders for all components + // subscribed to OnStateChanged + }; + + return ( +
+ +
+ ); +} +``` + +### Collaborative Features + +Shared state enables collaborative features where users and agents work together: + +```tsx +function CollaborativeTodo() { + const { agent } = useAgent({ + updates: [UseAgentUpdate.OnStateChanged], + }); + const { copilotkit } = useCopilotKit(); + + if (!agent) return null; + + const todos = agent.state.todos || []; + + const addTodo = (text: string) => { + agent.setState({ + ...agent.state, + todos: [...todos, { id: crypto.randomUUID(), text, done: false }], + }); + }; + + const toggleTodo = (id: string) => { + agent.setState({ + ...agent.state, + todos: todos.map((todo) => (todo.id === id ? { ...todo, done: !todo.done } : todo)), + }); + }; + + const askAgentToOrganize = async () => { + // Add a message asking the agent to organize todos + agent.addMessage({ + id: crypto.randomUUID(), + role: "user", + content: "Please organize my todos by priority", + }); + + // The agent can read and modify the todos in agent.state + await copilotkit.runAgent({ agent }); + + // After the agent runs, the state will be updated + // and the component will re-render automatically + }; + + return ( +
+

Shared Todo List

+
    + {todos.map((todo) => ( +
  • + toggleTodo(todo.id)} /> + {todo.text} +
  • + ))} +
+ + + + +
+ ); +} +``` + +## Examples + +### Optimized Updates + +Subscribe only to message changes to avoid unnecessary re-renders: + +```tsx +import { useAgent, UseAgentUpdate } from "@copilotkitnext/react"; + +function MessageList() { + const { agent } = useAgent({ + updates: [UseAgentUpdate.OnMessagesChanged], + }); + + if (!agent) return null; + + return ( +
    + {agent.messages.map((msg) => ( +
  • {msg.content}
  • + ))} +
+ ); +} +``` + +### Run Status Indicator + +Show a loading indicator when the agent is processing: + +```tsx +import { useAgent, UseAgentUpdate } from "@copilotkitnext/react"; + +function RunStatus() { + const { agent } = useAgent({ + agentId: "assistant", + updates: [UseAgentUpdate.OnRunStatusChanged], + }); + + if (!agent) return null; + + return ( +
+ {agent.isRunning ? "🔄 Processing..." : "✅ Ready"} +
+ ); +} +``` + +### Multiple Agents + +Access different agents in different components: + +```tsx +function DualAgentView() { + const { agent: primaryAgent } = useAgent({ + agentId: "primary-assistant", + }); + + const { agent: supportAgent } = useAgent({ + agentId: "support-assistant", + }); + + return ( +
+
+

Primary Assistant

+ {primaryAgent ?
Messages: {primaryAgent.messages.length}
:
Loading...
} +
+ +
+

Support Assistant

+ {supportAgent ?
Messages: {supportAgent.messages.length}
:
Loading...
} +
+
+ ); +} +``` + +### No Updates Subscription + +For static access without subscribing to updates: + +```tsx +const { agent } = useAgent({ + agentId: "assistant", + updates: [], // No subscriptions +}); + +// Component won't re-render on agent changes +``` + +### Custom Message Rendering with Optimized Updates + +```tsx +import { useAgent, UseAgentUpdate } from "@copilotkitnext/react"; +import { Message } from "@ag-ui/core"; + +function MessageRenderer() { + const { agent } = useAgent({ + updates: [UseAgentUpdate.OnMessagesChanged], // Only re-render on message changes + }); + + if (!agent || agent.messages.length === 0) { + return
No messages yet
; + } + + const renderMessage = (message: Message) => { + switch (message.role) { + case "user": + return ( +
+ 👤 + {message.content} +
+ ); + case "assistant": + return ( +
+ 🤖 + {message.content} +
+ ); + default: + return null; + } + }; + + return ( +
+ {agent.messages.map((msg) => ( +
{renderMessage(msg)}
+ ))} +
+ ); +} +``` + +## Performance Considerations + +### Selective Updates + +By default, `useAgent` subscribes to all update types, which can cause frequent re-renders. Use the `updates` parameter +to subscribe only to the changes you need: + +```tsx +// ❌ Subscribes to all updates (may cause unnecessary re-renders) +const { agent } = useAgent({ agentId: "assistant" }); + +// ✅ Only subscribes to message changes +const { agent } = useAgent({ + agentId: "assistant", + updates: [UseAgentUpdate.OnMessagesChanged], +}); + +// ✅ Only subscribes to run status changes +const { agent } = useAgent({ + agentId: "assistant", + updates: [UseAgentUpdate.OnRunStatusChanged], +}); +``` + +### Component Splitting + +Split components by update type to optimize rendering: + +```tsx +// Message display component - only updates on message changes +function Messages() { + const { agent } = useAgent({ + updates: [UseAgentUpdate.OnMessagesChanged], + }); + + // Render messages... +} + +// Status indicator - only updates on run status changes +function StatusIndicator() { + const { agent } = useAgent({ + updates: [UseAgentUpdate.OnRunStatusChanged], + }); + + // Render status... +} + +// Parent component doesn't need to subscribe +function ChatView() { + return ( + <> + + + + ); +} +``` + +## Error Handling + +The hook returns `undefined` if the agent is not available. Always check for this: + +```tsx +function SafeAgentComponent() { + const { agent } = useAgent({ agentId: "my-agent" }); + + if (!agent) { + // Handle missing agent gracefully + return
Agent not available. Please check your configuration.
; + } + + // Safe to use agent here + return
{agent.messages.length} messages
; +} +``` diff --git a/apps/docs/reference/use-copilot-kit.mdx b/apps/docs/reference/use-copilot-kit.mdx new file mode 100644 index 00000000..5c4ad03c --- /dev/null +++ b/apps/docs/reference/use-copilot-kit.mdx @@ -0,0 +1,147 @@ +--- +title: useCopilotKit +description: "useCopilotKit Hook API Reference" +--- + +`useCopilotKit` is a React hook that provides access to the CopilotKit context, including the core instance, tool +rendering capabilities, and runtime connection status. It's the primary way to interact with CopilotKit's core +functionality from React components. + +## What is useCopilotKit? + +The useCopilotKit hook: + +- Provides access to the `CopilotKitCore` instance for agent and tool management +- Exposes render tool calls for visual tool execution feedback +- Subscribes to runtime connection status changes +- Enables programmatic control over agents, tools, and context + +## Basic Usage + +```tsx +import { useCopilotKit } from "@copilotkitnext/react"; + +function MyComponent() { + const { copilotkit } = useCopilotKit(); + + // Access runtime status + console.log("Runtime status:", copilotkit.runtimeConnectionStatus); + + // Get an agent + const agent = copilotkit.getAgent("assistant"); + + return
Connected: {copilotkit.runtimeConnectionStatus === "Connected"}
; +} +``` + +## Return Value + +The hook returns a `CopilotKitContextValue` object with the following properties: + +### copilotkit + +`CopilotKitCore` + +The core CopilotKit instance that manages agents, tools, context, and runtime connections. This is the main interface +for interacting with CopilotKit programmatically. + +### renderToolCalls + +`ReactToolCallRender[]` + +An array of tool call render configurations defined at the provider level. These are used to render visual feedback when +tools are executed. + +### currentRenderToolCalls + +`ReactToolCallRender[]` + +The current list of render tool calls, including both static configurations and dynamically registered ones. + +### setCurrentRenderToolCalls + +`React.Dispatch[]>>` + +A setter function to update the current render tool calls. Useful for dynamically adding or removing tool renderers. + +## Examples + +### Running an Agent + +```tsx +import { useCopilotKit } from "@copilotkitnext/react"; +import { useAgent } from "@copilotkitnext/react"; + +function AgentRunner() { + const { copilotkit } = useCopilotKit(); + const { agent } = useAgent({ agentId: "assistant" }); + const [message, setMessage] = useState(""); + + const handleSubmit = async () => { + if (!agent || !message) return; + + // Add user message + agent.addMessage({ + id: crypto.randomUUID(), + role: "user", + content: message, + }); + + // Run the agent + await copilotkit.runAgent({ + agent, + agentId: "assistant", + }); + + setMessage(""); + }; + + return ( +
+ setMessage(e.target.value)} placeholder="Type a message..." /> + +
+ ); +} +``` + +### Monitoring Runtime Connection + +```tsx +import { useCopilotKit } from "@copilotkitnext/react"; + +function ConnectionStatus() { + const { copilotkit } = useCopilotKit(); + + const getStatusColor = () => { + switch (copilotkit.runtimeConnectionStatus) { + case "Connected": + return "green"; + case "Connecting": + return "yellow"; + case "Error": + return "red"; + default: + return "gray"; + } + }; + + return ( +
+
+ Runtime: {copilotkit.runtimeConnectionStatus} + {copilotkit.runtimeVersion && v{copilotkit.runtimeVersion}} +
+ ); +} +``` + +## Automatic Re-renders + +The hook automatically subscribes to runtime connection status changes and triggers re-renders when: + +- The runtime connects or disconnects +- The connection status changes +- Connection errors occur + +This ensures your components always display the current connection state without manual subscriptions. diff --git a/apps/docs/reference/use-frontend-tool.mdx b/apps/docs/reference/use-frontend-tool.mdx new file mode 100644 index 00000000..91f94fe8 --- /dev/null +++ b/apps/docs/reference/use-frontend-tool.mdx @@ -0,0 +1,253 @@ +--- +title: useFrontendTool +description: "useFrontendTool Hook API Reference" +--- + +`useFrontendTool` is a React hook that dynamically registers tools (functions) that AI agents can invoke in your +application. It enables components to expose interactive capabilities to agents, with optional visual rendering of tool +execution. + +## What is useFrontendTool? + +The useFrontendTool hook: + +- Registers tools dynamically when components mount +- Automatically cleans up tools when components unmount +- Optionally registers visual renderers for tool execution feedback in the chat interface +- Supports agent-specific tool registration +- Handles tool lifecycle management automatically + +## Basic Usage + +```tsx +import { useFrontendTool } from "@copilotkitnext/react"; +import { z } from "zod"; + +function SearchComponent() { + useFrontendTool({ + name: "searchProducts", + description: "Search for products in the catalog", + parameters: z.object({ + query: z.string(), + category: z.string().optional(), + }), + handler: async ({ query, category }) => { + const results = await searchAPI(query, category); + return results; + }, + }); + + return
Rest of your component...
; +} +``` + +## Parameters + +The hook accepts a single `ReactFrontendTool` object with the following properties: + +### name + +`string` **(required)** + +A unique identifier for the tool. This is the name agents will use to request this tool's execution. + +```tsx +useFrontendTool({ + name: "calculateTotal", + // ... +}); +``` + +### description + +`string` **(optional)** + +A description that helps agents understand when and how to use this tool. This is sent to the LLM to guide its +decision-making. + +```tsx +useFrontendTool({ + name: "fetchUserData", + description: "Retrieve detailed user profile information including preferences and history", + // ... +}); +``` + +### parameters + +`z.ZodType` **(optional)** + +A Zod schema defining the tool's input parameters. Provides type safety and automatic validation. + +```tsx +import { z } from "zod"; + +useFrontendTool({ + name: "updateSettings", + parameters: z.object({ + theme: z.enum(["light", "dark", "auto"]), + language: z.string(), + notifications: z.boolean(), + }), + handler: async ({ theme, language, notifications }) => { + // settings is fully typed based on the schema + await updateUserSettings({ theme, language, notifications }); + return "Settings updated successfully"; + }, +}); +``` + +### handler + +`(args: T, toolCall: ToolCall) => Promise` **(optional)** + +The async function executed when the agent invokes this tool. Receives validated arguments and metadata about the +invocation. + +```tsx +useFrontendTool({ + name: "addToCart", + parameters: z.object({ + productId: z.string(), + quantity: z.number().min(1), + }), + handler: async ({ productId, quantity }, toolCall) => { + console.log(`Tool called by agent at ${toolCall.id}`); + const result = await cartService.addItem(productId, quantity); + return { + success: true, + cartTotal: result.total, + }; + }, +}); +``` + +### render + +`(props: { name: string; args: T; result?: unknown; status: ToolCallStatus }) => React.ReactNode` **(optional)** + +A React component that renders visual feedback when the tool is executed. This appears in the chat interface to show +tool execution progress and results. + +```tsx +import { ToolCallStatus } from "@copilotkitnext/core"; + +useFrontendTool({ + name: "generateChart", + parameters: z.object({ + data: z.array(z.number()), + type: z.enum(["bar", "line", "pie"]), + }), + handler: async ({ data, type }) => { + return generateChartData(data, type); + }, + render: ({ name, args, result, status }) => ( +
+

Generating {args.type} chart...

+ {status === ToolCallStatus.InProgress && } + {status === ToolCallStatus.Complete && result && } +
+ ), +}); +``` + +### followUp + +`boolean` **(optional, default: true)** + +Controls whether the agent should automatically continue after this tool completes. Set to `false` for final actions. + +```tsx +useFrontendTool({ + name: "submitForm", + handler: async (formData) => { + await submitToServer(formData); + return "Form submitted successfully"; + }, + followUp: false, // Don't continue after submission +}); +``` + +### agentId + +`string` **(optional)** + +Restricts this tool to a specific agent. Only the specified agent can invoke this tool. + +```tsx +useFrontendTool({ + name: "adminAction", + agentId: "admin-assistant", + handler: async (args) => { + return await performAdminAction(args); + }, +}); +``` + +## Lifecycle Management + +### Automatic Registration + +Tools are automatically registered when the component mounts: + +```tsx +function DynamicTool() { + // Tool is registered when this component mounts + useFrontendTool({ + name: "dynamicAction", + handler: async () => "Action performed", + }); + + return
Tool available
; +} + +// Usage +function App() { + const [showTool, setShowTool] = useState(false); + + return ( +
+ + {showTool && } {/* Tool only available when mounted */} +
+ ); +} +``` + +### Cleanup on Unmount + +Tools are automatically removed when components unmount, but their renderers persist to maintain chat history: + +```tsx +function TemporaryTool() { + useFrontendTool({ + name: "tempAction", + handler: async () => "Temporary action", + render: ({ status }) =>
Temp tool: {status}
, + }); + + // When this component unmounts: + // - The tool handler is removed (agent can't call it anymore) + // - The renderer persists (previous executions still visible in chat) + + return null; +} +``` + +### Tool Override Warning + +If a tool with the same name already exists, it will be overridden with a console warning: + +```tsx +// First registration +useFrontendTool({ + name: "search", + handler: async () => "Search v1", +}); + +// Second registration (overrides first) +useFrontendTool({ + name: "search", // Same name - will override with warning + handler: async () => "Search v2", +}); +``` diff --git a/apps/docs/reference/use-human-in-the-loop.mdx b/apps/docs/reference/use-human-in-the-loop.mdx new file mode 100644 index 00000000..dbdbb5db --- /dev/null +++ b/apps/docs/reference/use-human-in-the-loop.mdx @@ -0,0 +1,194 @@ +--- +title: useHumanInTheLoop +description: "useHumanInTheLoop Hook API Reference" +--- + +`useHumanInTheLoop` is a React hook that creates interactive tools requiring human approval or input before the agent +can proceed. It enables you to build approval workflows, confirmation dialogs, and interactive decision points where +human oversight is needed. + +## What is useHumanInTheLoop? + +The useHumanInTheLoop hook: + +- Creates tools that pause execution until human input is received +- Manages status transitions (InProgress → Executing → Complete) +- Provides a `respond` callback for user interactions +- Automatically handles promise resolution for agent continuation +- Renders interactive UI components in the chat interface + +## Basic Usage + +```tsx +import { useHumanInTheLoop } from "@copilotkitnext/react"; +import { ToolCallStatus } from "@copilotkitnext/core"; +import { z } from "zod"; + +function ApprovalComponent() { + useHumanInTheLoop({ + name: "requireApproval", + description: "Requires human approval before proceeding", + parameters: z.object({ + action: z.string(), + reason: z.string(), + }), + render: ({ status, args, respond }) => { + if (status === ToolCallStatus.Executing && respond) { + return ( +
+

Approve action: {args.action}

+

Reason: {args.reason}

+ + +
+ ); + } + return
Status: {status}
; + }, + }); + + return
Approval system active
; +} +``` + +## Parameters + +The hook accepts a single `ReactHumanInTheLoop` object with the following properties: + +### name + +`string` **(required)** + +A unique identifier for the human-in-the-loop tool. + +```tsx +useHumanInTheLoop({ + name: "confirmDeletion", + // ... +}); +``` + +### description + +`string` **(optional)** + +Describes the tool's purpose to help agents understand when human approval is needed. + +```tsx +useHumanInTheLoop({ + name: "sensitiveAction", + description: "Requires human confirmation for sensitive operations", + // ... +}); +``` + +### parameters + +`z.ZodType` **(optional)** + +A Zod schema defining the parameters the agent will provide when requesting human input. + +```tsx +import { z } from "zod"; + +useHumanInTheLoop({ + name: "reviewChanges", + parameters: z.object({ + changes: z.array(z.string()), + impact: z.enum(["low", "medium", "high"]), + estimatedTime: z.number(), + }), + // ... +}); +``` + +### render + +`React.ComponentType` **(required)** + +A React component that renders the interactive UI. It receives different props based on the current status: + +#### Status: InProgress + +Initial state when the tool is called but not yet executing: + +```tsx +{ + name: string; + description: string; + args: Partial; // Partial arguments as they're being streamed + status: ToolCallStatus.InProgress; + result: undefined; + respond: undefined; +} +``` + +#### Status: Executing + +Active state where user interaction is required: + +```tsx +{ + name: string; + description: string; + args: T; // Complete arguments + status: ToolCallStatus.Executing; + result: undefined; + respond: (result: unknown) => Promise; // Callback to provide response +} +``` + +#### Status: Complete + +Final state after user has responded: + +```tsx +{ + name: string; + description: string; + args: T; // Complete arguments + status: ToolCallStatus.Complete; + result: string; // The response provided via respond() + respond: undefined; +} +``` + +### followUp + +`boolean` **(optional, default: true)** + +Controls whether the agent continues after receiving the human response. Provided for completeness, but typically you +would want the agent to continue (default behavior). + +```tsx +useHumanInTheLoop({ + name: "finalConfirmation", + followUp: false, // Don't continue after confirmation + // ... +}); +``` + +### agentId + +`string` **(optional)** + +Restricts this tool to a specific agent. + +```tsx +useHumanInTheLoop({ + name: "adminApproval", + agentId: "admin-assistant", + // ... +}); +``` + +## Differences from useFrontendTool + +While `useFrontendTool` executes immediately and returns results, `useHumanInTheLoop`: + +- Pauses execution until human input is received +- Provides a `respond` callback during the Executing state +- Manages status transitions automatically +- Is designed specifically for approval workflows and interactive decisions + +Choose `useHumanInTheLoop` when you need human oversight, and `useFrontendTool` for automated tool execution. diff --git a/apps/docs/reference/use-render-tool-call.mdx b/apps/docs/reference/use-render-tool-call.mdx new file mode 100644 index 00000000..3cb49f32 --- /dev/null +++ b/apps/docs/reference/use-render-tool-call.mdx @@ -0,0 +1,262 @@ +--- +title: useRenderToolCall +description: "useRenderToolCall Hook API Reference" +--- + + + You would use this hook if you use the headless functionality of CopilotKit, rendering your own chat UI instead of the + default `CopilotChat`. If you are using `CopilotChat`, you don't need to use this hook. + + +`useRenderToolCall` is a React hook that provides a function to render visual representations of tool calls in the chat +interface. It manages the rendering of tool execution states (InProgress, Executing, Complete) based on configured +render functions. + +## What is useRenderToolCall? + +The useRenderToolCall hook: + +- Returns a render function for tool calls +- Automatically determines the appropriate status (InProgress, Executing, or Complete) +- Manages tool execution state transitions +- Supports agent-specific and wildcard renderers +- Integrates with CopilotKit's tool rendering system + +## Basic Usage + +```tsx +import { useRenderToolCall } from "@copilotkitnext/react"; +import { ToolCall } from "@ag-ui/core"; + +function ToolCallDisplay({ toolCall, toolMessage }) { + const renderToolCall = useRenderToolCall(); + + return
{renderToolCall({ toolCall, toolMessage })}
; +} +``` + +## Return Value + +The hook returns a function with the following signature: + +```tsx +(props: UseRenderToolCallProps) => React.ReactElement | null; +``` + +### UseRenderToolCallProps + +```tsx +interface UseRenderToolCallProps { + toolCall: ToolCall; // The tool call to render + toolMessage?: ToolMessage; // Optional result message +} +``` + +## How It Works + +### Status Determination + +The render function automatically determines the tool's status: + +1. **Complete**: When a `toolMessage` is provided +2. **Executing**: When the tool is currently running (tracked internally) +3. **InProgress**: Default state when neither complete nor executing + +### Renderer Selection + +The function selects renderers based on priority: + +1. **Exact match** with matching agentId +2. **Exact match** without agentId (global) +3. **Exact match** (any agentId) +4. **Wildcard** renderer (`*`) +5. **No render** (returns null) + +## Examples + +### Basic Tool Call Rendering + +```tsx +import { useRenderToolCall } from "@copilotkitnext/react"; +import { AssistantMessage } from "@ag-ui/core"; + +function ChatMessage({ message }: { message: AssistantMessage }) { + const renderToolCall = useRenderToolCall(); + + if (!message.toolCalls) { + return
{message.content}
; + } + + return ( +
+ {message.content} + {message.toolCalls.map((toolCall) => ( +
+ {renderToolCall({ toolCall })} +
+ ))} +
+ ); +} +``` + +### With Tool Results + +```tsx +import { useRenderToolCall } from "@copilotkitnext/react"; +import { Message, ToolMessage } from "@ag-ui/core"; + +function ChatWithResults({ message, allMessages }: { message: AssistantMessage; allMessages: Message[] }) { + const renderToolCall = useRenderToolCall(); + + return ( + <> + {message.toolCalls?.map((toolCall) => { + // Find the corresponding result message + const toolMessage = allMessages.find( + (m): m is ToolMessage => m.role === "tool" && m.toolCallId === toolCall.id, + ); + + return ( +
+ {renderToolCall({ + toolCall, + toolMessage, // Pass result if available + })} +
+ ); + })} + + ); +} +``` + +## Integration with Tool Renderers + +The hook works with tool renderers defined at various levels: + +### Provider-Level Renderers + +Renderers defined in `CopilotKitProvider`: + +```tsx +import { CopilotKitProvider, defineToolCallRender } from "@copilotkitnext/react"; + +const searchRenderer = defineToolCallRender({ + name: "search", + render: ({ args, status }) => , +}); + +function App() { + return ( + + {/* Components using useRenderToolCall will use this renderer */} + + ); +} +``` + +### Dynamic Tool Renderers + +Renderers registered via `useFrontendTool`: + +```tsx +function DynamicTool() { + useFrontendTool({ + name: "dynamicAction", + handler: async (args) => { + /* ... */ + }, + render: ({ args, status }) =>
Dynamic tool: {status}
, + }); + + // This renderer is automatically available to useRenderToolCall + return null; +} +``` + +### Wildcard Renderer + +A fallback renderer for unmatched tools: + +```tsx +const wildcardRenderer = defineToolCallRender({ + name: "*", + render: ({ name, args, status }) => ( +
+ Unknown tool: {name} + Status: {status} +
+ ), +}); +``` + +## Status Lifecycle + +The hook manages three status states automatically: + +### InProgress + +Initial state when tool is called but not executing: + +```tsx +// Renderer receives: +{ + name: string; + args: Partial; // May be incomplete during streaming + status: ToolCallStatus.InProgress; + result: undefined; +} +``` + +### Executing + +Active execution state: + +```tsx +// Renderer receives: +{ + name: string; + args: T; // Complete arguments + status: ToolCallStatus.Executing; + result: undefined; +} +``` + +### Complete + +Final state with results: + +```tsx +// Renderer receives: +{ + name: string; + args: T; // Complete arguments + status: ToolCallStatus.Complete; + result: string; // Tool execution result +} +``` + +## Agent-Specific Rendering + +The hook supports agent-specific renderers: + +```tsx +import { useCopilotChatConfiguration } from "@copilotkitnext/react"; + +function AgentAwareRendering() { + const renderToolCall = useRenderToolCall(); + const config = useCopilotChatConfiguration(); + + // The hook automatically selects renderers based on the current agent + // Priority: agent-specific > global > wildcard + + return ( +
+

Agent: {config?.agentId || "default"}

+ {/* Renders will use agent-appropriate renderers */} + {toolCalls.map((tc) => renderToolCall({ toolCall: tc }))} +
+ ); +} +``` diff --git a/packages/angular/src/lib/copilotkit.spec.ts b/packages/angular/src/lib/copilotkit.spec.ts index dccebe43..27b80f1e 100644 --- a/packages/angular/src/lib/copilotkit.spec.ts +++ b/packages/angular/src/lib/copilotkit.spec.ts @@ -1,4 +1,4 @@ -import { Component, Injector } from "@angular/core"; +import { Component, Injector, signal } from "@angular/core"; import { TestBed } from "@angular/core/testing"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { z } from "zod"; @@ -26,7 +26,7 @@ vi.mock("@copilotkitnext/core", () => { readonly setRuntimeUrl = mockSetRuntimeUrl; readonly setHeaders = mockSetHeaders; readonly setProperties = mockSetProperties; - readonly setAgents = mockSetAgents; + readonly setAgents__unsafe_dev_only = mockSetAgents; readonly getAgent = mockGetAgent; agents: Record = {}; listener?: Parameters[0]; @@ -56,7 +56,9 @@ describe("CopilotKit", () => { selector: "dummy-tool", template: "", }) - class DummyToolComponent {} + class DummyToolComponent { + toolCall = signal({} as any); + } TestBed.configureTestingModule({ providers: [ @@ -130,7 +132,7 @@ describe("CopilotKit", () => { name: "client", description: "Client tool", args: z.object({ value: z.string() }), - component: class {}, + component: class { toolCall = signal({} as any); }, handler: handlerSpy, injector, }); @@ -159,7 +161,7 @@ describe("CopilotKit", () => { const toolConfig = { name: "approval", args: z.object({ summary: z.string() }), - component: class {}, + component: class { toolCall = signal({} as any); }, toolCall: vi.fn(), agentId: "agent-1", } as const; @@ -190,7 +192,7 @@ describe("CopilotKit", () => { copilotKit.addRenderToolCall({ name: "temp", args: z.object({}), - component: class {}, + component: class { toolCall = signal({} as any); }, agentId: undefined, }); diff --git a/packages/angular/src/lib/copilotkit.ts b/packages/angular/src/lib/copilotkit.ts index f6b08b80..8df87fd3 100644 --- a/packages/angular/src/lib/copilotkit.ts +++ b/packages/angular/src/lib/copilotkit.ts @@ -31,7 +31,7 @@ export class CopilotKit { runtimeUrl: this.#config.runtimeUrl, headers: this.#config.headers, properties: this.#config.properties, - agents: this.#config.agents, + agents__unsafe_dev_only: this.#config.agents, tools: this.#config.tools, }); @@ -191,7 +191,7 @@ export class CopilotKit { this.core.setProperties(options.properties); } if (options.agents !== undefined) { - this.core.setAgents(options.agents); + this.core.setAgents__unsafe_dev_only(options.agents); } } } diff --git a/packages/core/src/__tests__/core-full.test.ts b/packages/core/src/__tests__/core-full.test.ts index 529ca39c..b554e50e 100644 --- a/packages/core/src/__tests__/core-full.test.ts +++ b/packages/core/src/__tests__/core-full.test.ts @@ -25,10 +25,7 @@ describe("CopilotKitCore.runAgent - Full Test Suite", () => { describe("Tests that should pass", () => { it("TEST 1: should run agent without tools", async () => { - const messages = [ - createMessage({ content: "Hello" }), - createAssistantMessage({ content: "Hi there!" }), - ]; + const messages = [createMessage({ content: "Hello" }), createAssistantMessage({ content: "Hi there!" })]; const agent = new MockAgent({ newMessages: messages }); const result = await copilotKitCore.runAgent({ agent: agent as any }); @@ -51,14 +48,17 @@ describe("CopilotKitCore.runAgent - Full Test Suite", () => { await copilotKitCore.runAgent({ agent: agent as any }); - expect(tool.handler).toHaveBeenCalledTimes(1); - const [firstCallArgs] = tool.handler.mock.calls; - expect(firstCallArgs?.[0]).toEqual({ input: "test" }); - expect(firstCallArgs?.[1]).toMatchObject({ - function: { name: toolName }, - type: "function", - }); - expect(agent.messages.some(m => m.role === "tool")).toBe(true); + expect(tool.handler).toHaveBeenCalledWith( + { input: "test" }, + expect.objectContaining({ + id: expect.any(String), + function: expect.objectContaining({ + name: toolName, + arguments: '{"input":"test"}', + }), + }), + ); + expect(agent.messages.some((m) => m.role === "tool")).toBe(true); }); it("TEST 3: should skip tool when not found", async () => { @@ -67,7 +67,7 @@ describe("CopilotKitCore.runAgent - Full Test Suite", () => { await copilotKitCore.runAgent({ agent: agent as any }); - expect(agent.messages.filter(m => m.role === "tool")).toHaveLength(0); + expect(agent.messages.filter((m) => m.role === "tool")).toHaveLength(0); }); }); @@ -83,7 +83,7 @@ describe("CopilotKitCore.runAgent - Full Test Suite", () => { const message = createToolCallMessage("followUpTool"); const followUpMessage = createAssistantMessage({ content: "Follow-up response" }); - + const agent = new MockAgent({ newMessages: [message] }); let callCount = 0; agent.runAgentCallback = () => { @@ -120,11 +120,8 @@ describe("CopilotKitCore.runAgent - Full Test Suite", () => { copilotKitCore.addTool(tool1); copilotKitCore.addTool(tool2); - const message = createMultipleToolCallsMessage([ - { name: "tool1" }, - { name: "tool2" }, - ]); - + const message = createMultipleToolCallsMessage([{ name: "tool1" }, { name: "tool2" }]); + const agent = new MockAgent({ newMessages: [message] }); let callCount = 0; agent.runAgentCallback = () => { @@ -156,7 +153,7 @@ describe("CopilotKitCore.runAgent - Full Test Suite", () => { const message = createToolCallMessage("defaultFollowUpTool"); const followUpMessage = createAssistantMessage({ content: "Follow-up" }); - + const agent = new MockAgent({ newMessages: [message] }); let callCount = 0; agent.runAgentCallback = () => { @@ -188,14 +185,16 @@ describe("CopilotKitCore.runAgent - Full Test Suite", () => { const message = createAssistantMessage({ content: "", - toolCalls: [{ - id: "tool-call-1", - type: "function", - function: { - name: toolName, - arguments: "{ invalid json", + toolCalls: [ + { + id: "tool-call-1", + type: "function", + function: { + name: toolName, + arguments: "{ invalid json", + }, }, - }], + ], }); const agent = new MockAgent({ newMessages: [message] }); @@ -219,14 +218,16 @@ describe("CopilotKitCore.runAgent - Full Test Suite", () => { const message = createAssistantMessage({ content: "", - toolCalls: [{ - id: "empty-args-call", - type: "function", - function: { - name: "emptyArgsTool", - arguments: "", + toolCalls: [ + { + id: "empty-args-call", + type: "function", + function: { + name: "emptyArgsTool", + arguments: "", + }, }, - }], + ], }); const agent = new MockAgent({ newMessages: [message] }); @@ -259,7 +260,7 @@ describe("CopilotKitCore.runAgent - Full Test Suite", () => { const msg1 = createToolCallMessage("chainTool1"); const msg2 = createToolCallMessage("chainTool2"); const finalMsg = createAssistantMessage({ content: "Done" }); - + const agent = new MockAgent({ newMessages: [msg1] }); let callCount = 0; agent.runAgentCallback = () => { @@ -286,22 +287,20 @@ describe("CopilotKitCore.runAgent - Full Test Suite", () => { it("TEST 10: should handle concurrent tool calls", async () => { console.log("TEST 10: Starting concurrent tools test"); const delays = [50, 30, 70]; - const tools = delays.map((delay, i) => + const tools = delays.map((delay, i) => createTool({ name: `concurrentTool${i}`, handler: vi.fn(async () => { - await new Promise(resolve => setTimeout(resolve, delay)); + await new Promise((resolve) => setTimeout(resolve, delay)); return `Result ${i} after ${delay}ms`; }), followUp: false, - }) + }), ); - - tools.forEach(tool => copilotKitCore.addTool(tool)); - const message = createMultipleToolCallsMessage( - delays.map((_, i) => ({ name: `concurrentTool${i}` })) - ); + tools.forEach((tool) => copilotKitCore.addTool(tool)); + + const message = createMultipleToolCallsMessage(delays.map((_, i) => ({ name: `concurrentTool${i}` }))); const agent = new MockAgent({ newMessages: [message] }); const startTime = Date.now(); @@ -309,12 +308,12 @@ describe("CopilotKitCore.runAgent - Full Test Suite", () => { await copilotKitCore.runAgent({ agent: agent as any }); const duration = Date.now() - startTime; console.log(`TEST 10: Success - duration: ${duration}ms`); - + // Should execute sequentially const expectedMinDuration = delays.reduce((a, b) => a + b, 0); expect(duration).toBeGreaterThanOrEqual(expectedMinDuration - 10); - - tools.forEach(tool => { + + tools.forEach((tool) => { expect(tool.handler).toHaveBeenCalled(); }); } catch (error) { @@ -323,4 +322,4 @@ describe("CopilotKitCore.runAgent - Full Test Suite", () => { } }); }); -}); \ No newline at end of file +}); diff --git a/packages/core/src/__tests__/core-tool-minimal.test.ts b/packages/core/src/__tests__/core-tool-minimal.test.ts index 6e8e8c9d..6a9b52dd 100644 --- a/packages/core/src/__tests__/core-tool-minimal.test.ts +++ b/packages/core/src/__tests__/core-tool-minimal.test.ts @@ -1,10 +1,6 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import { CopilotKitCore } from "../core"; -import { - MockAgent, - createToolCallMessage, - createTool, -} from "./test-utils"; +import { MockAgent, createToolCallMessage, createTool } from "./test-utils"; describe("CopilotKitCore Tool Minimal", () => { let copilotKitCore: CopilotKitCore; @@ -26,14 +22,17 @@ describe("CopilotKitCore Tool Minimal", () => { await copilotKitCore.runAgent({ agent: agent as any }); - expect(tool.handler).toHaveBeenCalledTimes(1); - const [firstCallArgs] = tool.handler.mock.calls; - expect(firstCallArgs?.[0]).toEqual({ input: "test" }); - expect(firstCallArgs?.[1]).toMatchObject({ - function: { name: toolName }, - type: "function", - }); - expect(agent.messages.some(m => m.role === "tool")).toBe(true); + expect(tool.handler).toHaveBeenCalledWith( + { input: "test" }, + expect.objectContaining({ + id: expect.any(String), + function: expect.objectContaining({ + name: toolName, + arguments: '{"input":"test"}', + }), + }), + ); + expect(agent.messages.some((m) => m.role === "tool")).toBe(true); }); it("should skip tool call when tool not found", async () => { @@ -42,6 +41,6 @@ describe("CopilotKitCore Tool Minimal", () => { await copilotKitCore.runAgent({ agent: agent as any }); - expect(agent.messages.filter(m => m.role === "tool")).toHaveLength(0); + expect(agent.messages.filter((m) => m.role === "tool")).toHaveLength(0); }); -}); \ No newline at end of file +}); diff --git a/packages/core/src/__tests__/core-tool-simple.test.ts b/packages/core/src/__tests__/core-tool-simple.test.ts index 874a594d..69ba962e 100644 --- a/packages/core/src/__tests__/core-tool-simple.test.ts +++ b/packages/core/src/__tests__/core-tool-simple.test.ts @@ -1,10 +1,6 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import { CopilotKitCore } from "../core"; -import { - MockAgent, - createToolCallMessage, - createTool, -} from "./test-utils"; +import { MockAgent, createToolCallMessage, createTool } from "./test-utils"; describe("CopilotKitCore Tool Simple", () => { let copilotKitCore: CopilotKitCore; @@ -15,7 +11,7 @@ describe("CopilotKitCore Tool Simple", () => { it("should execute a simple tool", async () => { console.log("Starting simple tool test"); - + const toolName = "simpleTool"; const tool = createTool({ name: toolName, @@ -34,13 +30,16 @@ describe("CopilotKitCore Tool Simple", () => { await copilotKitCore.runAgent({ agent: agent as any }); console.log("Agent run complete"); - expect(tool.handler).toHaveBeenCalledTimes(1); - const [firstCallArgs] = tool.handler.mock.calls; - expect(firstCallArgs?.[0]).toEqual({ input: "test" }); - expect(firstCallArgs?.[1]).toMatchObject({ - function: { name: toolName }, - type: "function", - }); + expect(tool.handler).toHaveBeenCalledWith( + { input: "test" }, + expect.objectContaining({ + id: expect.any(String), + function: expect.objectContaining({ + name: toolName, + arguments: '{"input":"test"}', + }), + }), + ); expect(agent.messages.length).toBeGreaterThan(0); }); -}); \ No newline at end of file +}); diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index e9345eac..467d4ad1 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -1,18 +1,5 @@ -import { - AgentDescription, - DEFAULT_AGENT_ID, - randomUUID, - RuntimeInfo, - logger, -} from "@copilotkitnext/shared"; -import { - AbstractAgent, - AgentSubscriber, - Context, - HttpAgent, - Message, - RunAgentResult, -} from "@ag-ui/client"; +import { AgentDescription, DEFAULT_AGENT_ID, randomUUID, RuntimeInfo, logger } from "@copilotkitnext/shared"; +import { AbstractAgent, AgentSubscriber, Context, HttpAgent, Message, RunAgentResult } from "@ag-ui/client"; import { FrontendTool } from "./types"; import { ProxiedCopilotRuntimeAgent } from "./agent"; import { zodToJsonSchema } from "zod-to-json-schema"; @@ -21,8 +8,8 @@ import { zodToJsonSchema } from "zod-to-json-schema"; export interface CopilotKitCoreConfig { /** The endpoint of the CopilotRuntime. */ runtimeUrl?: string; - /** Mapping from agent name to its `AbstractAgent` instance. */ - agents?: Record; + /** Mapping from agent name to its `AbstractAgent` instance. For development only - production requires CopilotRuntime. */ + agents__unsafe_dev_only?: Record; /** Headers appended to every HTTP request made by `CopilotKitCore`. */ headers?: Record; /** Properties sent as `forwardedProps` to the AG-UI agent. */ @@ -114,8 +101,8 @@ export enum CopilotKitCoreRuntimeConnectionStatus { } export class CopilotKitCore { - headers: Record; - properties: Record; + private _headers: Record; + private _properties: Record; private _context: Record = {}; private _agents: Record = {}; @@ -135,12 +122,12 @@ export class CopilotKitCore { runtimeUrl, headers = {}, properties = {}, - agents = {}, + agents__unsafe_dev_only = {}, tools = [], }: CopilotKitCoreConfig) { - this.headers = headers; - this.properties = properties; - this.localAgents = this.assignAgentIds(agents); + this._headers = headers; + this._properties = properties; + this.localAgents = this.assignAgentIds(agents__unsafe_dev_only); this.applyHeadersToAgents(this.localAgents); this._agents = this.localAgents; this._tools = tools; @@ -170,7 +157,7 @@ export class CopilotKitCore { private async notifySubscribers( handler: (subscriber: CopilotKitCoreSubscriber) => void | Promise, - errorMessage: string + errorMessage: string, ) { await Promise.all( Array.from(this.subscribers).map(async (subscriber) => { @@ -179,7 +166,7 @@ export class CopilotKitCore { } catch (error) { logger.error(errorMessage, error); } - }) + }), ); } @@ -200,14 +187,11 @@ export class CopilotKitCore { code, context, }), - "Subscriber onError error:" + "Subscriber onError error:", ); } - private resolveAgentId( - agent: AbstractAgent, - providedAgentId?: string - ): string { + private resolveAgentId(agent: AbstractAgent, providedAgentId?: string): string { if (providedAgentId) { return providedAgentId; } @@ -245,9 +229,7 @@ export class CopilotKitCore { } setRuntimeUrl(runtimeUrl: string | undefined) { - const normalizedRuntimeUrl = runtimeUrl - ? runtimeUrl.replace(/\/$/, "") - : undefined; + const normalizedRuntimeUrl = runtimeUrl ? runtimeUrl.replace(/\/$/, "") : undefined; if (this._runtimeUrl === normalizedRuntimeUrl) { return; @@ -261,6 +243,14 @@ export class CopilotKitCore { return this._runtimeVersion; } + get headers(): Readonly> { + return this._headers; + } + + get properties(): Readonly> { + return this._properties; + } + get runtimeConnectionStatus(): CopilotKitCoreRuntimeConnectionStatus { return this._runtimeConnectionStatus; } @@ -270,8 +260,7 @@ export class CopilotKitCore { */ private async updateRuntimeConnection() { if (!this.runtimeUrl) { - this._runtimeConnectionStatus = - CopilotKitCoreRuntimeConnectionStatus.Disconnected; + this._runtimeConnectionStatus = CopilotKitCoreRuntimeConnectionStatus.Disconnected; this._runtimeVersion = undefined; this.remoteAgents = {}; this._agents = this.localAgents; @@ -282,7 +271,7 @@ export class CopilotKitCore { copilotkit: this, status: CopilotKitCoreRuntimeConnectionStatus.Disconnected, }), - "Error in CopilotKitCore subscriber (onRuntimeConnectionStatusChanged):" + "Error in CopilotKitCore subscriber (onRuntimeConnectionStatusChanged):", ); await this.notifySubscribers( @@ -291,13 +280,12 @@ export class CopilotKitCore { copilotkit: this, agents: this._agents, }), - "Subscriber onAgentsChanged error:" + "Subscriber onAgentsChanged error:", ); return; } - this._runtimeConnectionStatus = - CopilotKitCoreRuntimeConnectionStatus.Connecting; + this._runtimeConnectionStatus = CopilotKitCoreRuntimeConnectionStatus.Connecting; await this.notifySubscribers( (subscriber) => @@ -305,7 +293,7 @@ export class CopilotKitCore { copilotkit: this, status: CopilotKitCoreRuntimeConnectionStatus.Connecting, }), - "Error in CopilotKitCore subscriber (onRuntimeConnectionStatusChanged):" + "Error in CopilotKitCore subscriber (onRuntimeConnectionStatusChanged):", ); try { @@ -329,13 +317,12 @@ export class CopilotKitCore { }); this.applyHeadersToAgent(agent); return [id, agent]; - }) + }), ); this.remoteAgents = agents; this._agents = { ...this.localAgents, ...this.remoteAgents }; - this._runtimeConnectionStatus = - CopilotKitCoreRuntimeConnectionStatus.Connected; + this._runtimeConnectionStatus = CopilotKitCoreRuntimeConnectionStatus.Connected; this._runtimeVersion = version; await this.notifySubscribers( @@ -344,7 +331,7 @@ export class CopilotKitCore { copilotkit: this, status: CopilotKitCoreRuntimeConnectionStatus.Connected, }), - "Error in CopilotKitCore subscriber (onRuntimeConnectionStatusChanged):" + "Error in CopilotKitCore subscriber (onRuntimeConnectionStatusChanged):", ); await this.notifySubscribers( @@ -353,11 +340,10 @@ export class CopilotKitCore { copilotkit: this, agents: this._agents, }), - "Subscriber onAgentsChanged error:" + "Subscriber onAgentsChanged error:", ); } catch (error) { - this._runtimeConnectionStatus = - CopilotKitCoreRuntimeConnectionStatus.Error; + this._runtimeConnectionStatus = CopilotKitCoreRuntimeConnectionStatus.Error; this._runtimeVersion = undefined; this.remoteAgents = {}; this._agents = this.localAgents; @@ -368,7 +354,7 @@ export class CopilotKitCore { copilotkit: this, status: CopilotKitCoreRuntimeConnectionStatus.Error, }), - "Error in CopilotKitCore subscriber (onRuntimeConnectionStatusChanged):" + "Error in CopilotKitCore subscriber (onRuntimeConnectionStatusChanged):", ); await this.notifySubscribers( @@ -377,15 +363,11 @@ export class CopilotKitCore { copilotkit: this, agents: this._agents, }), - "Subscriber onAgentsChanged error:" - ); - const message = - error instanceof Error ? error.message : JSON.stringify(error); - logger.warn( - `Failed to load runtime info (${this.runtimeUrl}/info): ${message}` + "Subscriber onAgentsChanged error:", ); - const runtimeError = - error instanceof Error ? error : new Error(String(error)); + const message = error instanceof Error ? error.message : JSON.stringify(error); + logger.warn(`Failed to load runtime info (${this.runtimeUrl}/info): ${message}`); + const runtimeError = error instanceof Error ? error : new Error(String(error)); await this.emitError({ error: runtimeError, code: CopilotKitCoreErrorCode.RUNTIME_INFO_FETCH_FAILED, @@ -400,7 +382,7 @@ export class CopilotKitCore { * Configuration updates */ setHeaders(headers: Record) { - this.headers = headers; + this._headers = headers; this.applyHeadersToAgents(this._agents); void this.notifySubscribers( (subscriber) => @@ -408,23 +390,23 @@ export class CopilotKitCore { copilotkit: this, headers: this.headers, }), - "Subscriber onHeadersChanged error:" + "Subscriber onHeadersChanged error:", ); } setProperties(properties: Record) { - this.properties = properties; + this._properties = properties; void this.notifySubscribers( (subscriber) => subscriber.onPropertiesChanged?.({ copilotkit: this, properties: this.properties, }), - "Subscriber onPropertiesChanged error:" + "Subscriber onPropertiesChanged error:", ); } - setAgents(agents: Record) { + setAgents__unsafe_dev_only(agents: Record) { this.localAgents = this.assignAgentIds(agents); this._agents = { ...this.localAgents, ...this.remoteAgents }; this.applyHeadersToAgents(this._agents); @@ -434,11 +416,11 @@ export class CopilotKitCore { copilotkit: this, agents: this._agents, }), - "Subscriber onAgentsChanged error:" + "Subscriber onAgentsChanged error:", ); } - addAgent({ id, agent }: CopilotKitCoreAddAgentParams) { + addAgent__unsafe_dev_only({ id, agent }: CopilotKitCoreAddAgentParams) { this.localAgents[id] = agent; if (!agent.agentId) { agent.agentId = id; @@ -451,11 +433,11 @@ export class CopilotKitCore { copilotkit: this, agents: this._agents, }), - "Subscriber onAgentsChanged error:" + "Subscriber onAgentsChanged error:", ); } - removeAgent(id: string) { + removeAgent__unsafe_dev_only(id: string) { delete this.localAgents[id]; this._agents = { ...this.localAgents, ...this.remoteAgents }; void this.notifySubscribers( @@ -464,7 +446,7 @@ export class CopilotKitCore { copilotkit: this, agents: this._agents, }), - "Subscriber onAgentsChanged error:" + "Subscriber onAgentsChanged error:", ); } @@ -475,10 +457,8 @@ export class CopilotKitCore { if ( this.runtimeUrl !== undefined && - (this.runtimeConnectionStatus === - CopilotKitCoreRuntimeConnectionStatus.Disconnected || - this.runtimeConnectionStatus === - CopilotKitCoreRuntimeConnectionStatus.Connecting) + (this.runtimeConnectionStatus === CopilotKitCoreRuntimeConnectionStatus.Disconnected || + this.runtimeConnectionStatus === CopilotKitCoreRuntimeConnectionStatus.Connecting) ) { return undefined; } else { @@ -499,7 +479,7 @@ export class CopilotKitCore { copilotkit: this, context: this._context, }), - "Subscriber onContextChanged error:" + "Subscriber onContextChanged error:", ); return id; } @@ -512,25 +492,19 @@ export class CopilotKitCore { copilotkit: this, context: this._context, }), - "Subscriber onContextChanged error:" + "Subscriber onContextChanged error:", ); } /** * Tool management */ - addTool = Record>( - tool: FrontendTool - ) { + addTool = Record>(tool: FrontendTool) { // Check if a tool with the same name and agentId already exists - const existingToolIndex = this._tools.findIndex( - (t) => t.name === tool.name && t.agentId === tool.agentId - ); + const existingToolIndex = this._tools.findIndex((t) => t.name === tool.name && t.agentId === tool.agentId); if (existingToolIndex !== -1) { - logger.warn( - `Tool already exists: '${tool.name}' for agent '${tool.agentId || "global"}', skipping.` - ); + logger.warn(`Tool already exists: '${tool.name}' for agent '${tool.agentId || "global"}', skipping.`); return; } @@ -558,9 +532,7 @@ export class CopilotKitCore { // If agentId is provided, first look for agent-specific tool if (agentId) { - const agentTool = this._tools.find( - (tool) => tool.name === toolName && tool.agentId === agentId - ); + const agentTool = this._tools.find((tool) => tool.name === toolName && tool.agentId === agentId); if (agentTool) { return agentTool; } @@ -596,10 +568,7 @@ export class CopilotKitCore { /** * Agent connectivity */ - async connectAgent({ - agent, - agentId, - }: CopilotKitCoreConnectAgentParams): Promise { + async connectAgent({ agent, agentId }: CopilotKitCoreConnectAgentParams): Promise { try { if (agent instanceof HttpAgent) { agent.headers = { ...this.headers }; @@ -610,13 +579,12 @@ export class CopilotKitCore { forwardedProps: this.properties, tools: this.buildFrontendTools(agentId), }, - this.createAgentErrorSubscriber(agent, agentId) + this.createAgentErrorSubscriber(agent, agentId), ); return this.processAgentResult({ runAgentResult, agent, agentId }); } catch (error) { - const connectError = - error instanceof Error ? error : new Error(String(error)); + const connectError = error instanceof Error ? error : new Error(String(error)); const context: Record = {}; if (agentId ?? agent.agentId) { context.agentId = agentId ?? agent.agentId; @@ -630,11 +598,7 @@ export class CopilotKitCore { } } - async runAgent({ - agent, - withMessages, - agentId, - }: CopilotKitCoreRunAgentParams): Promise { + async runAgent({ agent, withMessages, agentId }: CopilotKitCoreRunAgentParams): Promise { if (agent instanceof HttpAgent) { agent.headers = { ...this.headers }; } @@ -648,12 +612,11 @@ export class CopilotKitCore { forwardedProps: this.properties, tools: this.buildFrontendTools(agentId), }, - this.createAgentErrorSubscriber(agent, agentId) + this.createAgentErrorSubscriber(agent, agentId), ); return this.processAgentResult({ runAgentResult, agent, agentId }); } catch (error) { - const runError = - error instanceof Error ? error : new Error(String(error)); + const runError = error instanceof Error ? error : new Error(String(error)); const context: Record = {}; if (agentId ?? agent.agentId) { context.agentId = agentId ?? agent.agentId; @@ -687,11 +650,7 @@ export class CopilotKitCore { for (const message of newMessages) { if (message.role === "assistant") { for (const toolCall of message.toolCalls || []) { - if ( - newMessages.findIndex( - (m) => m.role === "tool" && m.toolCallId === toolCall.id - ) === -1 - ) { + if (newMessages.findIndex((m) => m.role === "tool" && m.toolCallId === toolCall.id) === -1) { const tool = this.getTool({ toolName: toolCall.function.name, agentId, @@ -711,8 +670,7 @@ export class CopilotKitCore { try { parsedArgs = JSON.parse(toolCall.function.arguments); } catch (error) { - const parseError = - error instanceof Error ? error : new Error(String(error)); + const parseError = error instanceof Error ? error : new Error(String(error)); errorMessage = parseError.message; isArgumentError = true; await this.emitError({ @@ -738,15 +696,12 @@ export class CopilotKitCore { toolName: toolCall.function.name, args: parsedArgs, }), - "Subscriber onToolExecutionStart error:" + "Subscriber onToolExecutionStart error:", ); if (!errorMessage) { try { - const result = await tool.handler( - parsedArgs as any, - toolCall - ); + const result = await tool.handler(parsedArgs as any, toolCall); if (result === undefined || result === null) { toolCallResult = ""; } else if (typeof result === "string") { @@ -755,8 +710,7 @@ export class CopilotKitCore { toolCallResult = JSON.stringify(result); } } catch (error) { - const handlerError = - error instanceof Error ? error : new Error(String(error)); + const handlerError = error instanceof Error ? error : new Error(String(error)); errorMessage = handlerError.message; await this.emitError({ error: handlerError, @@ -787,7 +741,7 @@ export class CopilotKitCore { result: errorMessage ? "" : toolCallResult, error: errorMessage, }), - "Subscriber onToolExecutionEnd error:" + "Subscriber onToolExecutionEnd error:", ); if (isArgumentError) { @@ -796,9 +750,7 @@ export class CopilotKitCore { } if (!errorMessage || !isArgumentError) { - const messageIndex = agent.messages.findIndex( - (m) => m.id === message.id - ); + const messageIndex = agent.messages.findIndex((m) => m.id === message.id); const toolMessage = { id: randomUUID(), role: "tool" as const, @@ -829,8 +781,7 @@ export class CopilotKitCore { try { parsedArgs = JSON.parse(toolCall.function.arguments); } catch (error) { - const parseError = - error instanceof Error ? error : new Error(String(error)); + const parseError = error instanceof Error ? error : new Error(String(error)); errorMessage = parseError.message; isArgumentError = true; await this.emitError({ @@ -861,15 +812,12 @@ export class CopilotKitCore { toolName: toolCall.function.name, args: wildcardArgs, }), - "Subscriber onToolExecutionStart error:" + "Subscriber onToolExecutionStart error:", ); if (!errorMessage) { try { - const result = await wildcardTool.handler( - wildcardArgs as any, - toolCall - ); + const result = await wildcardTool.handler(wildcardArgs as any, toolCall); if (result === undefined || result === null) { toolCallResult = ""; } else if (typeof result === "string") { @@ -878,10 +826,7 @@ export class CopilotKitCore { toolCallResult = JSON.stringify(result); } } catch (error) { - const handlerError = - error instanceof Error - ? error - : new Error(String(error)); + const handlerError = error instanceof Error ? error : new Error(String(error)); errorMessage = handlerError.message; await this.emitError({ error: handlerError, @@ -912,7 +857,7 @@ export class CopilotKitCore { result: errorMessage ? "" : toolCallResult, error: errorMessage, }), - "Subscriber onToolExecutionEnd error:" + "Subscriber onToolExecutionEnd error:", ); if (isArgumentError) { @@ -921,9 +866,7 @@ export class CopilotKitCore { } if (!errorMessage || !isArgumentError) { - const messageIndex = agent.messages.findIndex( - (m) => m.id === message.id - ); + const messageIndex = agent.messages.findIndex((m) => m.id === message.id); const toolMessage = { id: randomUUID(), role: "tool" as const, @@ -960,14 +903,11 @@ export class CopilotKitCore { })); } - private createAgentErrorSubscriber( - agent: AbstractAgent, - agentId?: string - ): AgentSubscriber { + private createAgentErrorSubscriber(agent: AbstractAgent, agentId?: string): AgentSubscriber { const emitAgentError = async ( error: Error, code: CopilotKitCoreErrorCode, - extraContext: Record = {} + extraContext: Record = {}, ) => { const context: Record = { ...extraContext }; if (agentId ?? agent.agentId) { @@ -982,13 +922,9 @@ export class CopilotKitCore { return { onRunFailed: async ({ error }: { error: Error }) => { - await emitAgentError( - error, - CopilotKitCoreErrorCode.AGENT_RUN_FAILED_EVENT, - { - source: "onRunFailed", - } - ); + await emitAgentError(error, CopilotKitCoreErrorCode.AGENT_RUN_FAILED_EVENT, { + source: "onRunFailed", + }); }, onRunErrorEvent: async ({ event }) => { const eventError = @@ -999,9 +935,7 @@ export class CopilotKitCore { : undefined; const errorMessage = - typeof event?.rawEvent?.error === "string" - ? event.rawEvent.error - : (event?.message ?? "Agent run error"); + typeof event?.rawEvent?.error === "string" ? event.rawEvent.error : (event?.message ?? "Agent run error"); const rawError = eventError ?? new Error(errorMessage); @@ -1009,15 +943,11 @@ export class CopilotKitCore { (rawError as any).code = event.code; } - await emitAgentError( - rawError, - CopilotKitCoreErrorCode.AGENT_RUN_ERROR_EVENT, - { - source: "onRunErrorEvent", - event, - runtimeErrorCode: event?.code, - } - ); + await emitAgentError(rawError, CopilotKitCoreErrorCode.AGENT_RUN_ERROR_EVENT, { + source: "onRunErrorEvent", + event, + runtimeErrorCode: event?.code, + }); }, }; } diff --git a/packages/react/src/components/chat/CopilotChat.tsx b/packages/react/src/components/chat/CopilotChat.tsx index 6493a41e..5abd6b21 100644 --- a/packages/react/src/components/chat/CopilotChat.tsx +++ b/packages/react/src/components/chat/CopilotChat.tsx @@ -1,30 +1,49 @@ import { useAgent } from "@/hooks/use-agent"; import { CopilotChatView, CopilotChatViewProps } from "./CopilotChatView"; -import { CopilotChatConfigurationProvider } from "@/providers/CopilotChatConfigurationProvider"; +import { + CopilotChatConfigurationProvider, + CopilotChatLabels, + CopilotChatDefaultLabels, + useCopilotChatConfiguration, +} from "@/providers/CopilotChatConfigurationProvider"; import { DEFAULT_AGENT_ID, randomUUID } from "@copilotkitnext/shared"; import { useCallback, useEffect, useMemo } from "react"; import { merge } from "ts-deepmerge"; import { useCopilotKit } from "@/providers/CopilotKitProvider"; import { AbstractAgent, AGUIConnectNotImplementedError } from "@ag-ui/client"; -export type CopilotChatProps = Omit & { +export type CopilotChatProps = Omit & { agentId?: string; threadId?: string; + labels?: Partial; }; -export function CopilotChat({ - agentId = DEFAULT_AGENT_ID, - threadId, - ...props -}: CopilotChatProps) { - const { agent } = useAgent({ agentId }); +export function CopilotChat({ agentId, threadId, labels, ...props }: CopilotChatProps) { + // Check for existing configuration provider + const existingConfig = useCopilotChatConfiguration(); + + // Apply priority: props > existing config > defaults + const resolvedAgentId = agentId ?? existingConfig?.agentId ?? DEFAULT_AGENT_ID; + const resolvedThreadId = useMemo( + () => threadId ?? existingConfig?.threadId ?? randomUUID(), + [threadId, existingConfig?.threadId], + ); + const resolvedLabels: CopilotChatLabels = useMemo( + () => ({ + ...CopilotChatDefaultLabels, + ...(existingConfig?.labels || {}), + ...(labels || {}), + }), + [existingConfig?.labels, labels], + ); + + const { agent } = useAgent({ agentId: resolvedAgentId }); const { copilotkit } = useCopilotKit(); - const resolvedThreadId = useMemo(() => threadId ?? randomUUID(), [threadId]); useEffect(() => { const connect = async (agent: AbstractAgent) => { try { - await copilotkit.connectAgent({ agent, agentId }); + await copilotkit.connectAgent({ agent, agentId: resolvedAgentId }); } catch (error) { if (error instanceof AGUIConnectNotImplementedError) { // connect not implemented, ignore @@ -38,7 +57,7 @@ export function CopilotChat({ connect(agent); } return () => {}; - }, [resolvedThreadId, agent, copilotkit, agentId]); + }, [resolvedThreadId, agent, copilotkit, resolvedAgentId]); const onSubmitInput = useCallback( async (value: string) => { @@ -49,20 +68,16 @@ export function CopilotChat({ }); if (agent) { try { - await copilotkit.runAgent({ agent, agentId }); + await copilotkit.runAgent({ agent, agentId: resolvedAgentId }); } catch (error) { console.error("CopilotChat: runAgent failed", error); } } }, - [agent, copilotkit, agentId] + [agent, copilotkit, resolvedAgentId], ); - const { - inputProps: providedInputProps, - messageView: providedMessageView, - ...restProps - } = props; + const { inputProps: providedInputProps, messageView: providedMessageView, ...restProps } = props; const mergedProps = merge( { @@ -75,14 +90,13 @@ export function CopilotChat({ : providedMessageView !== undefined ? { messageView: providedMessageView } : {}), - } + }, ); + // Always create a provider with merged values + // This ensures priority: props > existing config > defaults return ( - + { - const { labels } = useCopilotChatConfiguration(); + const config = useCopilotChatConfiguration(); + const labels = config?.labels ?? CopilotChatDefaultLabels; const [copied, setCopied] = useState(false); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -378,7 +382,8 @@ export namespace CopilotChatAssistantMessage { export const CopyButton: React.FC< React.ButtonHTMLAttributes > = ({ className, title, onClick, ...props }) => { - const { labels } = useCopilotChatConfiguration(); + const config = useCopilotChatConfiguration(); + const labels = config?.labels ?? CopilotChatDefaultLabels; const [copied, setCopied] = useState(false); const handleClick = (event: React.MouseEvent) => { @@ -409,7 +414,8 @@ export namespace CopilotChatAssistantMessage { export const ThumbsUpButton: React.FC< React.ButtonHTMLAttributes > = ({ title, ...props }) => { - const { labels } = useCopilotChatConfiguration(); + const config = useCopilotChatConfiguration(); + const labels = config?.labels ?? CopilotChatDefaultLabels; return ( > = ({ title, ...props }) => { - const { labels } = useCopilotChatConfiguration(); + const config = useCopilotChatConfiguration(); + const labels = config?.labels ?? CopilotChatDefaultLabels; return ( > = ({ title, ...props }) => { - const { labels } = useCopilotChatConfiguration(); + const config = useCopilotChatConfiguration(); + const labels = config?.labels ?? CopilotChatDefaultLabels; return ( > = ({ title, ...props }) => { - const { labels } = useCopilotChatConfiguration(); + const config = useCopilotChatConfiguration(); + const labels = config?.labels ?? CopilotChatDefaultLabels; return ( = ({ icon, labelKey, defaultClassName, className, ...props }) => { - const { labels } = useCopilotChatConfiguration(); + const config = useCopilotChatConfiguration(); + const labels = config?.labels ?? CopilotChatDefaultLabels; return ( @@ -413,7 +415,8 @@ export namespace CopilotChatInput { toolsMenu?: (ToolsMenuItem | "-")[]; } > = ({ className, toolsMenu, ...props }) => { - const { labels } = useCopilotChatConfiguration(); + const config = useCopilotChatConfiguration(); + const labels = config?.labels ?? CopilotChatDefaultLabels; const renderMenuItems = ( items: (ToolsMenuItem | "-")[] @@ -495,7 +498,8 @@ export namespace CopilotChatInput { const internalTextareaRef = useRef(null); const [maxHeight, setMaxHeight] = useState(0); - const { labels } = useCopilotChatConfiguration(); + const config = useCopilotChatConfiguration(); + const labels = config?.labels ?? CopilotChatDefaultLabels; useImperativeHandle( ref, diff --git a/packages/react/src/components/chat/CopilotChatUserMessage.tsx b/packages/react/src/components/chat/CopilotChatUserMessage.tsx index 92b3e273..3d91df19 100644 --- a/packages/react/src/components/chat/CopilotChatUserMessage.tsx +++ b/packages/react/src/components/chat/CopilotChatUserMessage.tsx @@ -1,6 +1,9 @@ import { useState } from "react"; import { Copy, Check, Edit, ChevronLeft, ChevronRight } from "lucide-react"; -import { useCopilotChatConfiguration } from "@/providers/CopilotChatConfigurationProvider"; +import { + useCopilotChatConfiguration, + CopilotChatDefaultLabels, +} from "@/providers/CopilotChatConfigurationProvider"; import { twMerge } from "tailwind-merge"; import { Button } from "@/components/ui/button"; import { UserMessage } from "@ag-ui/core"; @@ -213,7 +216,8 @@ export namespace CopilotChatUserMessage { export const CopyButton: React.FC< React.ButtonHTMLAttributes & { copied?: boolean } > = ({ className, title, onClick, ...props }) => { - const { labels } = useCopilotChatConfiguration(); + const config = useCopilotChatConfiguration(); + const labels = config?.labels ?? CopilotChatDefaultLabels; const [copied, setCopied] = useState(false); const handleClick = (event: React.MouseEvent) => { @@ -244,7 +248,8 @@ export namespace CopilotChatUserMessage { export const EditButton: React.FC< React.ButtonHTMLAttributes > = ({ className, title, ...props }) => { - const { labels } = useCopilotChatConfiguration(); + const config = useCopilotChatConfiguration(); + const labels = config?.labels ?? CopilotChatDefaultLabels; return ( { - const { labels } = useCopilotChatConfiguration(); + const config = useCopilotChatConfiguration(); + const labels = config?.labels ?? CopilotChatDefaultLabels; return (
>(() => new Set()); diff --git a/packages/react/src/providers/CopilotChatConfigurationProvider.tsx b/packages/react/src/providers/CopilotChatConfigurationProvider.tsx index 7f229b5c..3052b0fe 100644 --- a/packages/react/src/providers/CopilotChatConfigurationProvider.tsx +++ b/packages/react/src/providers/CopilotChatConfigurationProvider.tsx @@ -73,12 +73,7 @@ export const CopilotChatConfigurationProvider: React.FC< // Hook to use the full configuration export const useCopilotChatConfiguration = - (): CopilotChatConfigurationValue => { + (): CopilotChatConfigurationValue | null => { const configuration = useContext(CopilotChatConfiguration); - if (!configuration) { - throw new Error( - "useCopilotChatConfiguration must be used within CopilotChatConfigurationProvider" - ); - } return configuration; }; diff --git a/packages/react/src/providers/CopilotKitProvider.tsx b/packages/react/src/providers/CopilotKitProvider.tsx index 16fe9a84..5d2e288f 100644 --- a/packages/react/src/providers/CopilotKitProvider.tsx +++ b/packages/react/src/providers/CopilotKitProvider.tsx @@ -1,24 +1,11 @@ "use client"; -import React, { - createContext, - useContext, - ReactNode, - useMemo, - useEffect, - useState, - useReducer, - useRef, -} from "react"; +import React, { createContext, useContext, ReactNode, useMemo, useEffect, useState, useReducer, useRef } from "react"; import { ReactToolCallRender } from "../types/react-tool-call-render"; import { ReactFrontendTool } from "../types/frontend-tool"; import { ReactHumanInTheLoop } from "../types/human-in-the-loop"; import { z } from "zod"; -import { - CopilotKitCore, - CopilotKitCoreConfig, - FrontendTool, -} from "@copilotkitnext/core"; +import { CopilotKitCore, CopilotKitCoreConfig, FrontendTool } from "@copilotkitnext/core"; import { AbstractAgent } from "@ag-ui/client"; // Define the context value interface - idiomatic React naming @@ -26,9 +13,7 @@ export interface CopilotKitContextValue { copilotkit: CopilotKitCore; renderToolCalls: ReactToolCallRender[]; currentRenderToolCalls: ReactToolCallRender[]; - setCurrentRenderToolCalls: React.Dispatch< - React.SetStateAction[]> - >; + setCurrentRenderToolCalls: React.Dispatch[]>>; } // Create the CopilotKit context @@ -55,7 +40,7 @@ export interface CopilotKitProviderProps { function useStableArrayProp( prop: T[] | undefined, warningMessage?: string, - isMeaningfulChange?: (initial: T[], next: T[]) => boolean + isMeaningfulChange?: (initial: T[], next: T[]) => boolean, ): T[] { const empty = useMemo(() => [], []); const value = prop ?? empty; @@ -92,30 +77,26 @@ export const CopilotKitProvider: React.FC = ({ (initial, next) => { // Only warn if the shape (names+agentId) changed. Allow identity changes // to support updated closures from parents (e.g., Storybook state). - const key = (rc?: ReactToolCallRender) => - `${rc?.agentId ?? ""}:${rc?.name ?? ""}`; - const setFrom = (arr: ReactToolCallRender[]) => - new Set(arr.map(key)); + const key = (rc?: ReactToolCallRender) => `${rc?.agentId ?? ""}:${rc?.name ?? ""}`; + const setFrom = (arr: ReactToolCallRender[]) => new Set(arr.map(key)); const a = setFrom(initial); const b = setFrom(next); if (a.size !== b.size) return true; for (const k of a) if (!b.has(k)) return true; return false; - } + }, ); const frontendToolsList = useStableArrayProp( frontendTools, - "frontendTools must be a stable array. If you want to dynamically add or remove tools, use `useFrontendTool` instead." + "frontendTools must be a stable array. If you want to dynamically add or remove tools, use `useFrontendTool` instead.", ); const humanInTheLoopList = useStableArrayProp( humanInTheLoop, - "humanInTheLoop must be a stable array. If you want to dynamically add or remove human-in-the-loop tools, use `useHumanInTheLoop` instead." + "humanInTheLoop must be a stable array. If you want to dynamically add or remove human-in-the-loop tools, use `useHumanInTheLoop` instead.", ); const initialRenderToolCalls = useMemo(() => renderToolCallsList, []); - const [currentRenderToolCalls, setCurrentRenderToolCalls] = useState< - ReactToolCallRender[] - >([]); + const [currentRenderToolCalls, setCurrentRenderToolCalls] = useState[]>([]); // Note: warnings for array identity changes are handled by useStableArrayProp @@ -138,9 +119,7 @@ export const CopilotKitProvider: React.FC = ({ return new Promise((resolve) => { // The actual implementation will be handled by the render component // This is a placeholder that the hook will override - console.warn( - `Human-in-the-loop tool '${tool.name}' called but no interactive handler is set up.` - ); + console.warn(`Human-in-the-loop tool '${tool.name}' called but no interactive handler is set up.`); resolve(undefined); }); }, @@ -182,8 +161,7 @@ export const CopilotKitProvider: React.FC = ({ frontendToolsList.forEach((tool) => { if (tool.render) { // For wildcard tools without parameters, default to z.any() - const args = - tool.parameters || (tool.name === "*" ? z.any() : undefined); + const args = tool.parameters || (tool.name === "*" ? z.any() : undefined); if (args) { combined.push({ name: tool.name, @@ -206,7 +184,7 @@ export const CopilotKitProvider: React.FC = ({ runtimeUrl: undefined, headers, properties, - agents, + agents__unsafe_dev_only: agents, tools: allTools, }; const copilotkit = new CopilotKitCore(config); @@ -220,8 +198,7 @@ export const CopilotKitProvider: React.FC = ({ useEffect(() => { setCurrentRenderToolCalls((prev) => { // Build a map from computed entries - const keyOf = (rc?: ReactToolCallRender) => - `${rc?.agentId ?? ""}:${rc?.name ?? ""}`; + const keyOf = (rc?: ReactToolCallRender) => `${rc?.agentId ?? ""}:${rc?.name ?? ""}`; const computedMap = new Map>(); for (const rc of allRenderToolCalls) { computedMap.set(keyOf(rc), rc); @@ -254,7 +231,7 @@ export const CopilotKitProvider: React.FC = ({ copilotkit.setRuntimeUrl(runtimeUrl); copilotkit.setHeaders(headers); copilotkit.setProperties(properties); - copilotkit.setAgents(agents); + copilotkit.setAgents__unsafe_dev_only(agents); }, [runtimeUrl, headers, properties, agents]); return ( diff --git a/packages/react/src/providers/__tests__/CopilotChatConfigurationProvider.test.tsx b/packages/react/src/providers/__tests__/CopilotChatConfigurationProvider.test.tsx new file mode 100644 index 00000000..4865abbb --- /dev/null +++ b/packages/react/src/providers/__tests__/CopilotChatConfigurationProvider.test.tsx @@ -0,0 +1,256 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { describe, it, expect } from "vitest"; +import { + CopilotChatConfigurationProvider, + CopilotChatDefaultLabels, + useCopilotChatConfiguration, +} from "../CopilotChatConfigurationProvider"; +import { DEFAULT_AGENT_ID } from "@copilotkitnext/shared"; +import { CopilotKitProvider } from "../CopilotKitProvider"; +import { CopilotChat } from "../../components/chat/CopilotChat"; + +// Test component to access configuration +function ConfigurationDisplay() { + const config = useCopilotChatConfiguration(); + return ( +
+
{config?.agentId || "no-config"}
+
{config?.threadId || "no-config"}
+
+ {config?.labels.chatInputPlaceholder || "no-config"} +
+
+ {config?.labels.assistantMessageToolbarCopyMessageLabel || "no-config"} +
+
+ ); +} + +describe("CopilotChatConfigurationProvider", () => { + describe("Basic functionality", () => { + it("should provide default configuration", () => { + render( + + + + ); + + expect(screen.getByTestId("agentId").textContent).toBe(DEFAULT_AGENT_ID); + expect(screen.getByTestId("threadId").textContent).toBe("test-thread"); + expect(screen.getByTestId("placeholder").textContent).toBe( + CopilotChatDefaultLabels.chatInputPlaceholder + ); + }); + + it("should accept custom agentId", () => { + render( + + + + ); + + expect(screen.getByTestId("agentId").textContent).toBe("custom-agent"); + }); + + it("should merge custom labels with defaults", () => { + const customLabels = { + chatInputPlaceholder: "Custom placeholder", + }; + + render( + + + + ); + + expect(screen.getByTestId("placeholder").textContent).toBe( + "Custom placeholder" + ); + // Other labels should still have defaults + expect(screen.getByTestId("copyLabel").textContent).toBe( + CopilotChatDefaultLabels.assistantMessageToolbarCopyMessageLabel + ); + }); + }); + + describe("Hook behavior", () => { + it("should return null when no provider exists", () => { + render(); + + expect(screen.getByTestId("agentId").textContent).toBe("no-config"); + expect(screen.getByTestId("threadId").textContent).toBe("no-config"); + expect(screen.getByTestId("placeholder").textContent).toBe("no-config"); + }); + }); + + describe("CopilotChat priority merging", () => { + it("should use defaults when no provider exists and no props passed", () => { + // CopilotChat creates its own provider, so we need to check inside it + // We'll check the input placeholder which uses the configuration + const { container } = render( + + + + ); + + // Find the input element and check its placeholder + const input = container.querySelector('textarea, input[type="text"]'); + expect(input?.getAttribute("placeholder")).toBe( + CopilotChatDefaultLabels.chatInputPlaceholder + ); + }); + + it("should inherit from existing provider when CopilotChat has no props", () => { + const { container } = render( + + + + + + ); + + // Check that the input inherits the outer placeholder + const input = container.querySelector('textarea, input[type="text"]'); + expect(input?.getAttribute("placeholder")).toBe("Outer placeholder"); + }); + + it("should override existing provider with CopilotChat props", () => { + const { container } = render( + + + + + + ); + + // CopilotChat props should win - check the input placeholder + const input = container.querySelector('textarea, input[type="text"]'); + expect(input?.getAttribute("placeholder")).toBe("Inner placeholder"); + }); + + it("should merge labels correctly with priority: default < existing < props", () => { + const { container } = render( + + + + + + ); + + const input = container.querySelector('textarea, input[type="text"]'); + expect(input?.getAttribute("placeholder")).toBe("Inner placeholder"); + // The copy label would be tested if we had assistant messages + }); + + it("should handle partial overrides correctly", () => { + const { container } = render( + + + + + + ); + + // Check the placeholder was overridden + const input = container.querySelector('textarea, input[type="text"]'); + expect(input?.getAttribute("placeholder")).toBe("Inner placeholder"); + // agentId and other properties would be tested through agent behavior + }); + + it("should allow accessing configuration outside CopilotChat in same provider", () => { + // This shows that ConfigurationDisplay outside CopilotChat + // sees the outer provider values, not the inner merged ones + render( + + + + + + + ); + + // ConfigurationDisplay is outside CopilotChat, so it sees outer values + expect(screen.getByTestId("agentId").textContent).toBe("outer-agent"); + expect(screen.getByTestId("threadId").textContent).toBe("outer-thread"); + expect(screen.getByTestId("placeholder").textContent).toBe("Outer placeholder"); + }); + }); + + describe("Nested providers", () => { + it("should handle multiple nested providers correctly", () => { + render( + + + + + + + + ); + + // Innermost provider should win + expect(screen.getByTestId("agentId").textContent).toBe("inner-agent"); + expect(screen.getByTestId("threadId").textContent).toBe("inner-thread"); + expect(screen.getByTestId("placeholder").textContent).toBe("Inner"); + }); + }); +}); \ No newline at end of file