Skip to main content

What is an Agent?

In CopilotKit, an agent is any component that implements the AbstractAgent interface and can process user messages, execute tools, and emit AG-UI events. Agents are the “brain” of your AI application - they contain the logic for understanding user intent and generating responses.
CopilotKit is agent-framework agnostic. You can use LangGraph, CrewAI, the built-in agent, or build your own custom agent.

AbstractAgent Interface

All agents in CopilotKit extend the AbstractAgent base class from @ag-ui/client:
abstract class AbstractAgent {
  // Unique identifier for this agent
  agentId?: string;
  
  // Current thread ID
  threadId?: string;
  
  // Execute the agent with input and receive events
  abstract run(input: RunAgentInput): Observable<BaseEvent>;
  
  // Abort the current run
  abstract abortRun(): void;
  
  // Create a fresh instance of this agent
  abstract clone(): AbstractAgent;
  
  // Subscribe to agent state changes
  subscribe(handlers: AgentSubscriptionHandlers): Subscription;
  
  // Message and state management
  messages: Message[];
  state: State;
  isRunning: boolean;
}
This interface ensures all agents work consistently across the three-layer architecture.

Agent Types

ProxiedAgent

A ProxiedCopilotRuntimeAgent is the frontend representation of a remote agent. It translates agent operations into HTTP requests to the runtime and streams SSE events back.
const agent = new ProxiedCopilotRuntimeAgent({
  runtimeUrl: "http://localhost:4000",
  agentId: "my-agent",
  transport: "rest"
});

// Running the agent makes an HTTP request
const observable = agent.run({
  threadId: "thread_123",
  runId: "run_456",
  messages: [{ role: "user", content: "Hello!" }],
  tools: [],
  context: {},
  state: {}
});
Reference: packages/v2/core/src/agent.ts:55

How ProxiedAgent Works

ProxiedAgent is created automatically by CopilotKitCore when you configure a runtimeUrl. You typically don’t instantiate it directly.

Transport Modes

ProxiedAgent supports two transport modes: REST Transport (default) Separate endpoints for each operation:
  • /agent/:agentId/run - Execute the agent
  • /agent/:agentId/connect - Reconnect to existing thread
  • /agent/:agentId/stop/:threadId - Stop running agent
const agent = new ProxiedCopilotRuntimeAgent({
  runtimeUrl: "http://localhost:4000",
  agentId: "my-agent",
  transport: "rest" // default
});
Single Endpoint Transport All operations go through one endpoint with method envelopes:
const agent = new ProxiedCopilotRuntimeAgent({
  runtimeUrl: "http://localhost:4000",
  agentId: "my-agent",
  transport: "single"
});
Request envelope:
{
  "method": "agent/run",
  "params": { "agentId": "my-agent" },
  "body": { /* RunAgentInput */ }
}
Reference: packages/v2/core/src/agent.ts:66

IntelligenceAgent

IntelligenceAgent is a specialized agent implementation that uses Phoenix WebSockets for bidirectional, real-time communication instead of HTTP + SSE.
const agent = new IntelligenceAgent({
  url: "ws://localhost:4000/socket",
  runtimeUrl: "http://localhost:4000",
  agentId: "my-agent",
  socketParams: { token: "auth_token" }
});
Key differences from ProxiedAgent:
  • Uses WebSocket for persistent connection
  • Supports custom socket parameters (for auth)
  • Automatic reconnection with exponential backoff
  • Phoenix channel-based event streaming
Reference: packages/v2/core/src/intelligence-agent.ts:29

WebSocket Event Flow

// 1. Connect to Phoenix socket
const socket = new Socket("ws://localhost:4000/socket");
socket.connect();

// 2. Join agent-specific channel
const channel = socket.channel(`agent:${threadId}`);
channel.join();

// 3. Listen for AG-UI events
channel.on("ag_ui_event", (event: BaseEvent) => {
  observer.next(event);
});

// 4. Trigger run via REST
fetch("/agent/my-agent/run", { 
  method: "POST", 
  body: JSON.stringify(input) 
});
Reference: packages/v2/core/src/intelligence-agent.ts:80
Use IntelligenceAgent when you need lower latency, bidirectional communication, or more reliable connections in challenging network conditions.

BuiltInAgent

The BuiltInAgent is CopilotKit’s default agent implementation, powered by the Vercel AI SDK. It’s perfect for getting started or when you don’t need a complex agent framework.
import { BuiltInAgent } from "@copilotkitnext/agent";

const agent = new BuiltInAgent({
  agentId: "assistant",
  model: openai("gpt-4"),
  tools: {
    searchDatabase: {
      description: "Search the database",
      parameters: z.object({
        query: z.string()
      }),
      execute: async ({ query }) => {
        // Tool implementation
        return results;
      }
    }
  }
});
Features:
  • Stream responses token-by-token
  • Execute tools with type-safe parameters
  • Manage conversation state
  • Support for multi-turn conversations
Reference: packages/v2/agent/src/__tests__/basic-agent.test.ts

Custom Agents

You can build custom agents by extending AbstractAgent:
import { AbstractAgent, BaseEvent, EventType } from "@ag-ui/client";
import { Observable } from "rxjs";

class MyCustomAgent extends AbstractAgent {
  constructor(public agentId: string) {
    super();
  }

  run(input: RunAgentInput): Observable<BaseEvent> {
    return new Observable((observer) => {
      // Emit RUN_STARTED
      observer.next({
        type: EventType.RUN_STARTED,
        runId: input.runId,
        threadId: input.threadId,
        input
      });

      // Process the request
      this.processRequest(input).then((response) => {
        // Emit message events
        observer.next({
          type: EventType.TEXT_MESSAGE_START,
          messageId: "msg_123",
          role: "assistant"
        });
        
        observer.next({
          type: EventType.TEXT_MESSAGE_CONTENT,
          messageId: "msg_123",
          content: response
        });
        
        observer.next({
          type: EventType.TEXT_MESSAGE_END,
          messageId: "msg_123"
        });

        // Emit RUN_FINISHED
        observer.next({
          type: EventType.RUN_FINISHED,
          runId: input.runId,
          threadId: input.threadId
        });

        observer.complete();
      });

      // Cleanup on unsubscribe
      return () => {
        // Cancel any ongoing work
      };
    });
  }

  abortRun(): void {
    // Implement abort logic
  }

  clone(): MyCustomAgent {
    return new MyCustomAgent(this.agentId);
  }
}
Custom agents must emit events in the correct lifecycle order. See AG-UI Protocol for the event sequence.

Agent Registration

Frontend Registration (Development Only)

For development and testing, you can register agents directly on the frontend:
import { CopilotKitCore } from "@copilotkitnext/core";

const core = new CopilotKitCore({
  agents__unsafe_dev_only: {
    "my-agent": new MyCustomAgent("my-agent")
  }
});
The agents__unsafe_dev_only option exposes your agent logic to the client. Only use this in development or when your agent contains no sensitive logic.

Runtime Registration (Production)

In production, agents are registered on the runtime:
import { CopilotRuntime } from "@copilotkitnext/runtime";
import { BuiltInAgent } from "@copilotkitnext/agent";

const runtime = new CopilotRuntime({
  agents: {
    "assistant": new BuiltInAgent({
      agentId: "assistant",
      model: openai("gpt-4")
    }),
    "researcher": new MyCustomAgent("researcher")
  }
});
The frontend discovers these agents automatically:
// Frontend code
const core = new CopilotKitCore({
  runtimeUrl: "http://localhost:4000"
});

// Runtime fetches /info and creates ProxiedAgents
// Now you can use the agents:
const agent = core.getAgent("assistant");
Reference: packages/v2/runtime/src/runtime.ts:57

Multi-Agent Systems

CopilotKit supports multiple agents in a single application:

Agent Scoping

Each agent can have its own:
  • Message thread - Separate conversation history
  • State - Isolated agent state
  • Tools - Agent-specific tools via agentId parameter
// Agent-specific tool
useFrontendTool({
  name: "analyzeCode",
  agentId: "code-assistant", // Only available to this agent
  parameters: z.object({
    code: z.string()
  }),
  execute: async ({ code }) => {
    return analysis;
  }
});

// Global tool (available to all agents)
useFrontendTool({
  name: "getUser",
  // No agentId - available to all agents
  parameters: z.object({}),
  execute: async () => {
    return currentUser;
  }
});
Reference: packages/v2/react/src/hooks/use-frontend-tool.tsx:14

Selecting Agents

Use the useAgent hook to work with specific agents:
// Default agent
const { agent: defaultAgent } = useAgent();

// Specific agent
const { agent: codeAgent } = useAgent({ 
  agentId: "code-assistant" 
});

// Use different agents in different components
function ChatInterface() {
  const { agent } = useAgent({ agentId: "assistant" });
  return <CopilotChat agent={agent} />;
}

function CodeHelper() {
  const { agent } = useAgent({ agentId: "code-assistant" });
  return <CodeAssistant agent={agent} />;
}
Reference: packages/v2/react/src/hooks/use-agent.tsx:27

Agent State Management

Message Management

Agents maintain a message history:
// Access messages
const messages = agent.messages;

// Add message
agent.addMessage({
  role: "user",
  content: "Hello!"
});

// Subscribe to message changes
agent.subscribe({
  onMessagesChanged: (messages) => {
    console.log("Messages updated:", messages);
  }
});

State Persistence

Agents can maintain custom state across runs:
// Agent state is passed with each run
const result = await agent.runAgent({
  threadId: "thread_123",
  messages: [...],
  state: {
    userPreferences: { theme: "dark" },
    sessionData: { ... }
  }
});

// State is returned and can be persisted
const updatedState = result.state;

Run Status Tracking

Track agent execution status:
agent.subscribe({
  onRunInitialized: () => {
    console.log("Agent started");
  },
  onRunFinalized: () => {
    console.log("Agent finished");
  },
  onRunFailed: (error) => {
    console.error("Agent failed:", error);
  }
});

// Check current status
if (agent.isRunning) {
  console.log("Agent is processing...");
}
Reference: packages/v2/react/src/hooks/use-agent.tsx:139

Agent Cloning

The runtime clones agents for each request to ensure isolation:
// Runtime handler
const clonedAgent = agent.clone();
const result = await runner.run({
  agent: clonedAgent,
  threadId: "thread_123",
  input: { ... }
});
This prevents:
  • State leakage between concurrent requests
  • Race conditions in multi-threaded environments
  • Memory leaks from lingering references
Why cloning matters:
  • Each request gets a fresh agent instance
  • Original agent configuration is preserved
  • Thread safety is guaranteed

Agent Tools

Agents can execute two types of tools:

Frontend Tools

Execute in the browser, useful for:
  • Accessing local state
  • Triggering UI actions
  • Reading browser APIs
useFrontendTool({
  name: "updateUI",
  agentId: "assistant",
  parameters: z.object({
    component: z.string(),
    props: z.record(z.any())
  }),
  execute: async ({ component, props }) => {
    // Execute in browser
    updateComponent(component, props);
    return "UI updated";
  }
});

Backend Tools

Defined in the agent configuration, execute on the server:
const agent = new BuiltInAgent({
  agentId: "assistant",
  tools: {
    queryDatabase: {
      description: "Query the database",
      parameters: z.object({
        sql: z.string()
      }),
      execute: async ({ sql }) => {
        // Execute on server
        return await db.query(sql);
      }
    }
  }
});
Use frontend tools for UI interactions and local data. Use backend tools for secure operations, database access, and external APIs.

Best Practices

Agent Design

  1. Single responsibility - Each agent should have a clear purpose
  2. Stateless where possible - Minimize agent-specific state
  3. Emit events properly - Follow the AG-UI lifecycle
  4. Handle interrupts - Implement abortRun() correctly
  5. Clone safely - Ensure clone() creates true copies

Error Handling

agent.run(input).subscribe({
  next: (event) => {
    // Handle events
  },
  error: (error) => {
    // Handle errors gracefully
    console.error("Agent error:", error);
    showErrorToUser(error.message);
  },
  complete: () => {
    // Clean up
  }
});

Performance

  1. Stream responses - Don’t buffer entire responses
  2. Use cloning - Let the runtime clone agents
  3. Clean up subscriptions - Unsubscribe when done
  4. Abort long-running tasks - Respond to abortRun()

Integration Examples

LangGraph Agent

import { LangGraphAgent } from "@copilotkit/sdk-js";

const agent = new LangGraphAgent({
  agentId: "researcher",
  url: "http://localhost:8000/graph"
});

CrewAI Agent

import { CrewAIAgent } from "@copilotkit/sdk-js";

const agent = new CrewAIAgent({
  agentId: "crew",
  endpoint: "http://localhost:8000/crew"
});

Next Steps

AG-UI Protocol

Learn about the event protocol agents use

Runtime

Understand how agents are executed

Frontend Integration

Use agents in your UI

Architecture

See how agents fit in the architecture