Overview
CopilotKit provides framework-specific integrations that wrap the core CopilotKitCore orchestrator. All frameworks share the same underlying architecture but expose different APIs tailored to each framework’s patterns.
The frontend layer manages local state, registers tools and context, and handles agent communication through the AG-UI protocol.
Architecture
CopilotKitCore
CopilotKitCore is the framework-agnostic orchestrator at the heart of every CopilotKit frontend integration:
import { CopilotKitCore } from "@copilotkitnext/core" ;
const core = new CopilotKitCore ({
// Connect to runtime
runtimeUrl: "http://localhost:4000" ,
runtimeTransport: "rest" , // or "single"
// Or register agents directly (dev only)
agents__unsafe_dev_only: {
"assistant" : myAgent
},
// HTTP configuration
headers: {
"Authorization" : "Bearer token"
},
credentials: "include" ,
// Custom properties sent to agents
properties: {
userId: "user_123"
},
// Initial tools
tools: [],
// Initial suggestions config
suggestionsConfig: []
});
Reference : packages/v2/core/src/core/core.ts:179
Core Responsibilities
Agent Registry - Manages available agents and creates ProxiedAgents
Tool Registry - Tracks frontend tools and their handlers
Context Store - Maintains application context sent to agents
Event System - Pub/sub for state changes
Run Handler - Coordinates agent runs and tool execution
React Integration
Setup
import { CopilotKitProvider } from "@copilotkitnext/react" ;
function App () {
return (
< CopilotKitProvider runtimeUrl = "http://localhost:4000" >
< YourApp />
</ CopilotKitProvider >
);
}
The provider creates a CopilotKitCore instance and makes it available to all child components via React context.
useAgent Hook
Access and interact with agents:
import { useAgent } from "@copilotkitnext/react" ;
function ChatComponent () {
// Default agent
const { agent } = useAgent ();
// Specific agent
const { agent : codeAgent } = useAgent ({
agentId: "code-assistant"
});
// Access agent state
const messages = agent . messages ;
const isRunning = agent . isRunning ;
const state = agent . state ;
// Run the agent
const handleSend = async ( message : string ) => {
agent . addMessage ({ role: "user" , content: message });
await agent . runAgent ({
threadId: threadId ,
runId: generateId (),
messages: agent . messages ,
tools: [],
context: {},
state: agent . state
});
};
return (
< div >
{ messages . map ( msg => (
< div key = { msg . id } > { msg . content } </ div >
)) }
</ div >
);
}
Reference : packages/v2/react/src/hooks/use-agent.tsx:27
Selective Re-rendering
Control which changes trigger re-renders:
import { useAgent , UseAgentUpdate } from "@copilotkitnext/react" ;
// Only re-render on message changes
const { agent } = useAgent ({
agentId: "assistant" ,
updates: [ UseAgentUpdate . OnMessagesChanged ]
});
// Re-render on state and run status changes
const { agent } = useAgent ({
agentId: "assistant" ,
updates: [
UseAgentUpdate . OnStateChanged ,
UseAgentUpdate . OnRunStatusChanged
]
});
Reference : packages/v2/react/src/hooks/use-agent.tsx:10
Register tools that execute in the browser:
import { useFrontendTool } from "@copilotkitnext/react" ;
import { z } from "zod" ;
function DataTable () {
const [ data , setData ] = useState ([]);
useFrontendTool ({
name: "updateTable" ,
description: "Update the data table with new entries" ,
parameters: z . object ({
entries: z . array ( z . object ({
id: z . string (),
name: z . string (),
value: z . number ()
}))
}),
execute : async ({ entries }) => {
setData ( entries );
return `Updated table with ${ entries . length } entries` ;
}
});
return < table > { /* render data */ } </ table > ;
}
Reference : packages/v2/react/src/hooks/use-frontend-tool.tsx:8
Render custom UI while a tool is executing:
useFrontendTool ({
name: "searchDocuments" ,
parameters: z . object ({
query: z . string ()
}),
execute : async ({ query }) => {
const results = await search ( query );
return JSON . stringify ( results );
},
render : ({ args , result , status }) => {
if ( status === "executing" ) {
return (
< div className = "tool-card" >
< Spinner />
< p > Searching for: { args . query } </ p >
</ div >
);
}
if ( status === "complete" ) {
const results = JSON . parse ( result );
return (
< div className = "tool-card" >
< h3 > Search Results </ h3 >
{ results . map ( r => < div key = { r . id } > { r . title } </ div > ) }
</ div >
);
}
return null ;
}
});
Scope tools to specific agents:
// Only available to code-assistant agent
useFrontendTool ({
name: "formatCode" ,
agentId: "code-assistant" ,
parameters: z . object ({
code: z . string (),
language: z . string ()
}),
execute : async ({ code , language }) => {
return await prettier . format ( code , { parser: language });
}
});
// Global tool (available to all agents)
useFrontendTool ({
name: "getCurrentUser" ,
parameters: z . object ({}),
execute : async () => {
return JSON . stringify ( currentUser );
}
});
useAgentContext Hook
Provide application state to agents:
import { useAgentContext } from "@copilotkitnext/react" ;
function UserDashboard () {
const user = useUser ();
const settings = useSettings ();
// Share user data with agents
useAgentContext (
"Current user information" ,
{
id: user . id ,
name: user . name ,
email: user . email ,
preferences: settings
}
);
return < Dashboard /> ;
}
Context is automatically included in every agent run:
// Agent receives:
{
context : {
"ctx_abc123" : {
description: "Current user information" ,
data: {
id: "user_123" ,
name: "John Doe" ,
email: "john@example.com" ,
preferences: { theme: "dark" }
}
}
}
}
Dynamic Context
Context updates automatically when dependencies change:
function DocumentEditor () {
const [ document , setDocument ] = useState ( null );
const [ selection , setSelection ] = useState ( null );
// Context updates when document or selection changes
useAgentContext (
"Current document and selection" ,
{
documentId: document ?. id ,
title: document ?. title ,
selectedText: selection ?. text ,
cursorPosition: selection ?. position
},
[ document , selection ] // Dependencies
);
return < Editor /> ;
}
useCopilotKit Hook
Access the underlying core directly:
import { useCopilotKit } from "@copilotkitnext/react" ;
function AdvancedComponent () {
const { copilotkit } = useCopilotKit ();
// Access all core functionality
const agents = copilotkit . agents ;
const tools = copilotkit . tools ;
const context = copilotkit . context ;
// Subscribe to events
useEffect (() => {
const subscription = copilotkit . subscribe ({
onAgentsChanged : ({ agents }) => {
console . log ( "Agents updated:" , agents );
},
onToolExecutionStart : ({ toolName , args }) => {
console . log ( `Tool ${ toolName } started with` , args );
},
onError : ({ error , code }) => {
console . error ( `Error ${ code } :` , error );
}
});
return () => subscription . unsubscribe ();
}, [ copilotkit ]);
return < div > { /* ... */ } </ div > ;
}
Reference : packages/v2/core/src/core/core.ts:446
Angular Integration
Setup
import { provideCopilotKit } from "@copilotkitnext/angular" ;
import { ApplicationConfig } from "@angular/core" ;
export const appConfig : ApplicationConfig = {
providers: [
provideCopilotKit ({
runtimeUrl: "http://localhost:4000"
})
]
};
Agent Service
import { Component , inject , signal , effect } from "@angular/core" ;
import { AgentStore } from "@copilotkitnext/angular" ;
@ Component ({
selector: "app-chat" ,
template: `
<div>
@for (message of messages(); track message.id) {
<div>{{ message.content }}</div>
}
<button (click)="sendMessage()">Send</button>
</div>
`
})
export class ChatComponent {
private agentStore = inject ( AgentStore );
messages = signal ([]);
constructor () {
// Get agent
const agent = this . agentStore . getAgent ( "assistant" );
// Subscribe to messages
effect (() => {
this . messages . set ( agent (). messages );
});
}
async sendMessage () {
const agent = this . agentStore . getAgent ( "assistant" );
agent (). addMessage ({
role: "user" ,
content: "Hello"
});
await agent (). runAgent ({
threadId: "thread_123" ,
runId: "run_456" ,
messages: agent (). messages ,
tools: [],
context: {},
state: {}
});
}
}
import { Directive , effect , inject } from "@angular/core" ;
import { CopilotKitService } from "@copilotkitnext/angular" ;
@ Directive ({
selector: "[appDataTable]" ,
standalone: true
})
export class DataTableToolDirective {
private copilotKit = inject ( CopilotKitService );
constructor () {
effect (() => {
this . copilotKit . addTool ({
name: "updateTable" ,
description: "Update the data table" ,
parameters: z . object ({
entries: z . array ( z . any ())
}),
execute : async ({ entries }) => {
// Update table
return "Table updated" ;
}
});
});
}
}
Context (Angular)
import { Directive , Input , effect , inject } from "@angular/core" ;
import { CopilotKitService } from "@copilotkitnext/angular" ;
@ Directive ({
selector: "[appAgentContext]" ,
standalone: true
})
export class AgentContextDirective {
@ Input () description !: string ;
@ Input () data !: any ;
private copilotKit = inject ( CopilotKitService );
private contextId ?: string ;
constructor () {
effect (() => {
// Remove old context
if ( this . contextId ) {
this . copilotKit . removeContext ( this . contextId );
}
// Add new context
this . contextId = this . copilotKit . addContext ({
description: this . description ,
data: this . data
});
});
}
ngOnDestroy () {
if ( this . contextId ) {
this . copilotKit . removeContext ( this . contextId );
}
}
}
Usage:
< div [appAgentContext] = "'User info'" [data] = "user" >
<!-- User dashboard -->
</ div >
Vanilla JS Integration
For non-framework applications:
import { CopilotKitCore } from "@copilotkitnext/core" ;
// Create core instance
const core = new CopilotKitCore ({
runtimeUrl: "http://localhost:4000"
});
// Get agent
const agent = core . getAgent ( "assistant" );
// Add message
agent . addMessage ({ role: "user" , content: "Hello" });
// Run agent
agent . runAgent ({
threadId: "thread_123" ,
runId: "run_456" ,
messages: agent . messages ,
tools: [],
context: core . context ,
state: {}
}). subscribe ({
next : ( event ) => {
console . log ( "Event:" , event );
if ( event . type === "TEXT_MESSAGE_CONTENT" ) {
updateUI ( event . content );
}
},
error : ( error ) => {
console . error ( "Error:" , error );
},
complete : () => {
console . log ( "Run complete" );
}
});
// Register tool
core . addTool ({
name: "getData" ,
description: "Get data from the page" ,
parameters: z . object ({
selector: z . string ()
}),
execute : async ({ selector }) => {
const element = document . querySelector ( selector );
return element ?. textContent || "" ;
}
});
// Add context
core . addContext ({
description: "Current page URL" ,
data: { url: window . location . href }
});
Event Subscription
Subscribe to core events for fine-grained control:
const subscription = core . subscribe ({
// Runtime connection status
onRuntimeConnectionStatusChanged : ({ status }) => {
console . log ( "Runtime status:" , status );
},
// Tool execution
onToolExecutionStart : ({ toolName , args }) => {
showToolIndicator ( toolName );
},
onToolExecutionEnd : ({ toolName , result , error }) => {
hideToolIndicator ( toolName );
if ( error ) showError ( error );
},
// Agent changes
onAgentsChanged : ({ agents }) => {
updateAgentList ( Object . keys ( agents ));
},
// Context changes
onContextChanged : ({ context }) => {
console . log ( "Context updated:" , context );
},
// Errors
onError : ({ error , code , context }) => {
console . error ( `Error [ ${ code } ]:` , error , context );
showErrorNotification ( error . message );
}
});
// Cleanup
subscription . unsubscribe ();
Reference : packages/v2/core/src/core/core.ts:72
Understanding how frontend tools are executed:
Reference : packages/v2/core/src/core/run-handler.ts
// 1. Agent emits TOOL_CALL_START
{
type : "TOOL_CALL_START" ,
toolCallId : "call_123" ,
toolName : "searchDocuments" ,
args : { query : "architecture" }
}
// 2. Core looks up tool
const tool = core . getTool ({
toolName: "searchDocuments" ,
agentId: "assistant"
});
// 3. Core executes handler
const result = await tool . execute ({
query: "architecture"
});
// 4. Core sends result to runtime
await fetch ( "/copilotkit/agent/assistant/tool-result" , {
method: "POST" ,
body: JSON . stringify ({
toolCallId: "call_123" ,
result: JSON . stringify ( result )
})
});
// 5. Agent continues with result
{
type : "TOOL_CALL_RESULT" ,
toolCallId : "call_123" ,
result : "[{...}]"
}
Runtime Connection
Connection Lifecycle
// 1. Core initialized
const core = new CopilotKitCore ({
runtimeUrl: "http://localhost:4000"
});
// Status: Disconnected
// 2. Fetch runtime info
await core . fetchRuntimeInfo ();
// Status: Connecting
// 3. Info received, agents created
{
agents : [ "assistant" , "code-helper" ],
version : "1.0.0"
}
// Status: Connected
// 4. ProxiedAgents created automatically
const agent = core . getAgent ( "assistant" ); // ProxiedCopilotRuntimeAgent
Error Handling
core . subscribe ({
onError : ({ error , code , context }) => {
switch ( code ) {
case "runtime_info_fetch_failed" :
// Failed to connect to runtime
showRetryButton ();
break ;
case "agent_run_failed" :
// Agent run failed
showErrorMessage ( "Agent failed to process your request" );
break ;
case "tool_handler_failed" :
// Tool execution failed
console . error ( "Tool error:" , context . toolName , error );
break ;
case "transcription_failed" :
// Voice transcription failed
showVoiceError ();
break ;
}
}
});
Reference : packages/v2/core/src/core/core.ts:55
Minimize Re-renders
// Only re-render when messages change
const { agent } = useAgent ({
updates: [ UseAgentUpdate . OnMessagesChanged ]
});
// Don't re-render on state changes
const { agent } = useAgent ({
updates: [] // No automatic re-renders
});
// Manual updates when needed
const forceUpdate = useReducer ( x => x + 1 , 0 )[ 1 ];
useEffect (() => {
agent . subscribe ({
onStateChanged: forceUpdate
});
}, [ agent ]);
const executeSearch = useCallback ( async ({ query }) => {
return await searchAPI ( query );
}, []);
useFrontendTool ({
name: "search" ,
parameters: z . object ({ query: z . string () }),
execute: executeSearch
}, [ executeSearch ]); // Stable dependency
Batch Context Updates
// Bad: Multiple context registrations
useAgentContext ( "User name" , user . name );
useAgentContext ( "User email" , user . email );
useAgentContext ( "User settings" , user . settings );
// Good: Single context object
useAgentContext ( "User information" , {
name: user . name ,
email: user . email ,
settings: user . settings
});
Best Practices
Follow these guidelines for robust frontend integration:
Keep tools focused - One tool = one responsibility
Validate inputs - Use Zod schemas for type safety
Return serializable data - JSON-compatible objects only
Handle errors gracefully - Catch and return error messages
Provide good descriptions - Help agents understand when to use tools
Context Management
Send relevant data only - Don’t send entire app state
Update incrementally - Use multiple useAgentContext calls for different concerns
Clean up on unmount - Context is automatically removed
Avoid sensitive data - Don’t send passwords, tokens, etc.
State Management
Use selective updates - Only re-render when necessary
Memoize handlers - Prevent unnecessary re-registrations
Clean up subscriptions - Always unsubscribe
Handle connection states - Show loading/error states appropriately
Error Handling
Subscribe to errors - Use onError event
Show user feedback - Display error messages
Log for debugging - Include context in logs
Implement retry logic - For transient failures
Next Steps
Architecture Understand the overall architecture
Agents Learn about agent types and implementations
Runtime Set up your CopilotRuntime
AG-UI Protocol Deep dive into the event protocol