AG-UI

CopilotKit is built on the AG-UI protocol — a lightweight, event-based standard that defines how AI agents communicate with user-facing applications over Server-Sent Events (SSE).

Everything in CopilotKit — messages, state updates, tool calls, and more — flows through AG-UI events. Understanding this layer helps you debug, extend, and build on top of CopilotKit more effectively.

Accessing Your Agent with useAgent

The useAgent hook is your primary interface to the AG-UI agent powering your copilot. It returns an AbstractAgent from the AG-UI client library — the same base type that all AG-UI agents implement.



function MyComponent() {
  const { agent } = useAgent();

  // agent.messages - conversation history
  // agent.state - current agent state
  // agent.isRunning - whether the agent is currently running
}

If you have multiple agents, pass the agentId to select one:

const { agent } = useAgent({ agentId: "research-agent" });

The returned agent is a standard AG-UI AbstractAgent. You can subscribe to its events, read its state, and interact with it using the same interface defined by the AG-UI specification.

Subscribing to AG-UI Events

Every agent exposes a subscribe method that lets you listen for specific AG-UI events as they stream in. Each callback receives the event and the current agent state:




function MyComponent() {
  const { agent } = useAgent();

  useEffect(() => {
    const subscription = agent.subscribe({
      // Called on every event
      onEvent({ event, agent }) {
        console.log("Event:", event.type, event);
      },

      // Text message streaming
      onTextMessageContentEvent({ event, textMessageBuffer, agent }) {
        console.log("Streaming text:", textMessageBuffer);
      },

      // Tool calls
      onToolCallEndEvent({ event, toolCallName, toolCallArgs, agent }) {
        console.log("Tool called:", toolCallName, toolCallArgs);
      },

      // State updates
      onStateSnapshotEvent({ event, agent }) {
        console.log("State snapshot:", agent.state);
      },

      // High-level lifecycle
      onMessagesChanged({ agent }) {
        console.log("Messages updated:", agent.messages);
      },
      onStateChanged({ agent }) {
        console.log("State changed:", agent.state);
      },
    });

    return () => subscription.unsubscribe();
  }, [agent]);
}

The full list of subscribable events maps directly to the AG-UI event types:

EventCallbackDescription
Run lifecycleonRunStartedEvent, onRunFinishedEvent, onRunErrorEventAgent run start, completion, and errors
StepsonStepStartedEvent, onStepFinishedEventIndividual step boundaries within a run
Text messagesonTextMessageStartEvent, onTextMessageContentEvent, onTextMessageEndEventStreaming text content from the agent
Tool callsonToolCallStartEvent, onToolCallArgsEvent, onToolCallEndEvent, onToolCallResultEventTool invocation lifecycle
StateonStateSnapshotEvent, onStateDeltaEventFull state snapshots and incremental deltas
MessagesonMessagesSnapshotEventFull message list snapshots
CustomonCustomEvent, onRawEventCustom and raw events for extensibility
High-levelonMessagesChanged, onStateChangedAggregate notifications after any message or state mutation

The Proxy Pattern

When you use CopilotKit with a runtime, your frontend never talks directly to your agent. Instead, CopilotKit creates a proxy agent on the frontend that forwards requests through the Copilot Runtime.

On startup, CopilotKit calls the runtime's /info endpoint to discover which agents are available. Each agent is wrapped in a ProxiedCopilotRuntimeAgent — a thin client that extends AG-UI's HttpAgent. From your component's perspective, this proxy behaves identically to a local AG-UI agent: same AbstractAgent interface, same subscribe API, same properties. But under the hood, every run call is an HTTP request to your server, and every response is an SSE stream of AG-UI events flowing back.

const { agent } = useAgent(); // Returns an AbstractAgent
agent.messages;               // Read messages
agent.state;                  // Read state
agent.subscribe({ ... });     // Subscribe to events
// useAgent() → AgentRegistry checks /info → wraps each agent in ProxiedCopilotRuntimeAgent
// agent.runAgent() → HTTP POST to runtime → runtime routes to your agent → SSE stream back

This indirection is what enables the runtime to provide authentication, middleware, agent routing, and ecosystem features like threads and observability — without changing how you interact with agents on the frontend.

How Agents Slot into the Runtime

On the server side, the CopilotRuntime accepts a map of AG-UI AbstractAgent instances. Each agent framework provides its own implementation, but they all extend the same base type:


  CopilotRuntime,
  copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";


const runtime = new CopilotRuntime({
  agents: {
    "my-agent": new HttpAgent({
      url: "https://my-agent-server.example.com",
    }),
  },
});

export const POST = async (req: NextRequest) => {
  const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
    runtime,
    endpoint: "/api/copilotkit",
  });
  return handleRequest(req);
};

When a request comes in:

  1. The runtime resolves the target agent by ID
  2. It clones the agent (for thread safety) and sets messages, state, and thread context from the request
  3. The AgentRunner executes the agent, which produces a stream of AG-UI BaseEvents
  4. Events are encoded as SSE and streamed back to the frontend proxy

Because every agent is an AbstractAgent, you can register any AG-UI-compatible agent — whether it's an HttpAgent pointing at a remote server, a framework-specific adapter, or a custom implementation — and the runtime handles routing, middleware, and delivery uniformly.

2087950ee