Skip to main content

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

  1. Agent Registry - Manages available agents and creates ProxiedAgents
  2. Tool Registry - Tracks frontend tools and their handlers
  3. Context Store - Maintains application context sent to agents
  4. Event System - Pub/sub for state changes
  5. 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

useFrontendTool Hook

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

Tool Rendering

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;
  }
});

Agent-Specific Tools

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: {}
    });
  }
}

Frontend Tools (Angular)

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

Tool Execution Flow

Understanding how frontend tools are executed: Reference: packages/v2/core/src/core/run-handler.ts

Tool Execution Example

// 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

Performance Optimization

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]);

Memoize Tool Handlers

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:

Tool Design

  1. Keep tools focused - One tool = one responsibility
  2. Validate inputs - Use Zod schemas for type safety
  3. Return serializable data - JSON-compatible objects only
  4. Handle errors gracefully - Catch and return error messages
  5. Provide good descriptions - Help agents understand when to use tools

Context Management

  1. Send relevant data only - Don’t send entire app state
  2. Update incrementally - Use multiple useAgentContext calls for different concerns
  3. Clean up on unmount - Context is automatically removed
  4. Avoid sensitive data - Don’t send passwords, tokens, etc.

State Management

  1. Use selective updates - Only re-render when necessary
  2. Memoize handlers - Prevent unnecessary re-registrations
  3. Clean up subscriptions - Always unsubscribe
  4. Handle connection states - Show loading/error states appropriately

Error Handling

  1. Subscribe to errors - Use onError event
  2. Show user feedback - Display error messages
  3. Log for debugging - Include context in logs
  4. 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