CopilotKit

Programmatic Control

Chat with an agent using CopilotKit's UI components.


Overview#

The useAgent hook provides direct access to your LangGraph agent from any React component. It gives you real-time access to the agent's state, messages, execution status, and allows you to subscribe to custom events like interrupts.

This enables you to build custom agent dashboards, monitoring tools, and interactive features that respond to your agent's behavior.

This page covers everything you need to know about using useAgent with LangGraph. Select where you'd like to get started below.

Getting started#

Let's start by building a simple component that displays agent information.

Import the hook#

First, import useAgent from the v2 package:

page.tsx
import { useAgent } from "@copilotkit/react-core/v2"; 

Access your agent#

Call the hook to get a reference to your agent:

page.tsx
export function AgentInfo() {
  const { agent } = useAgent(); 

  return (
    <div>
      <p>Agent ID: {agent.agentId}</p>
      <p>Thread ID: {agent.threadId}</p>
      <p>Status: {agent.isRunning ? "Running" : "Idle"}</p>
      <p>Messages: {agent.messages.length}</p>
    </div>
  );
}

If you are not using CopilotKit Cloud's public access/license key, pass your agentId to the useAgent() hook:

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

The hook will throw an error if no agent is configured, so you can safely use agent without null checks.

Display messages#

Access the agent's conversation history:

page.tsx
export function MessageList() {
  const { agent } = useAgent();

  return (
    <div>
      {agent.messages.map((msg) => (
        <div key={msg.id}>
          <strong>{msg.role}:</strong>
          <span>{msg.content}</span>
        </div>
      ))}
    </div>
  );
}

Show running status#

Add a loading indicator when the agent is processing:

page.tsx
export function AgentStatus() {
  const { agent } = useAgent();

  return (
    <div>
      {agent.isRunning ? (
        <div>
          <div className="spinner" />
          <span>Agent is processing...</span>
        </div>
      ) : (
        <span>Ready</span>
      )}
    </div>
  );
}

Run the agent#

Use copilotkit.runAgent() to trigger your agent programmatically:

page.tsx
import { useAgent } from "@copilotkit/react-core/v2";
import { useCopilotKit } from "@copilotkit/react-core/v2";
import { randomUUID } from "@copilotkit/shared/v2";

export function RunAgent() {
  const { agent } = useAgent();
  const { copilotkit } = useCopilotKit();

  const handleRun = async () => {
    agent.addMessage({
      id: randomUUID(),
      role: "user",
      content: "Hello, agent!",
    });

    await copilotkit.runAgent({ agent });
  };

  return <button onClick={handleRun}>Send</button>;
}

copilotkit.runAgent() orchestrates the full agent lifecycle — executing frontend tools, handling follow-up runs, and streaming results. This is the same method <CopilotChat /> uses internally.

Working with State#

Agents expose their state through the agent.state property. This state is shared between your application and the agent - both can read and modify it.

Reading State#

Access your agent's current state:

page.tsx
export function StateDisplay() {
  const { agent } = useAgent();

  return (
    <div>
      <h3>Agent State</h3>
      <pre>{JSON.stringify(agent.state, null, 2)}</pre>

      {/* Access specific properties */}
      {agent.state.user_name && <p>User: {agent.state.user_name}</p>}
      {agent.state.preferences && (
        <p>Preferences: {JSON.stringify(agent.state.preferences)}</p>
      )}
    </div>
  );
}

Your component automatically re-renders when the agent's state changes.

Updating State#

Update state that your agent can access:

page.tsx
export function ThemeSelector() {
  const { agent } = useAgent();

  const updateTheme = (theme: string) => {
    agent.setState({
      ...agent.state,
      user_theme: theme,
    });
  };

  return (
    <div>
      <button onClick={() => updateTheme("dark")}>Dark Mode</button>
      <button onClick={() => updateTheme("light")}>Light Mode</button>
      <p>Current: {agent.state.user_theme || "default"}</p>
    </div>
  );
}

State updates are immediately available to your agent in its next execution.

Subscribing to Agent Events#

You can subscribe to agent events using the subscribe() method. This is useful for logging, monitoring, or responding to specific agent behaviors.

Basic Event Subscription#

page.tsx
import { useEffect } from "react";
import { useAgent } from "@copilotkit/react-core/v2";
import type { AgentSubscriber } from "@ag-ui/client";

export function EventLogger() {
  const { agent } = useAgent();

  useEffect(() => {
    const subscriber: AgentSubscriber = {
      onCustomEvent: ({ event }) => {
        console.log("Custom event:", event.name, event.value);
      },
      onRunStartedEvent: () => {
        console.log("Agent started running");
      },
      onRunFinalized: () => {
        console.log("Agent finished running");
      },
      onStateChanged: (state) => {
        console.log("State changed:", state);
      },
    };

    const { unsubscribe } = agent.subscribe(subscriber);
    return () => unsubscribe();
  }, [agent]);

  return null;
}

Available Events#

The AgentSubscriber interface provides:

  • onCustomEvent - Custom events emitted by the agent
  • onRunStartedEvent - Agent starts executing
  • onRunFinalized - Agent completes execution
  • onStateChanged - Agent's state changes
  • onMessagesChanged - Messages are added or modified

Rendering Tool Calls#

You can customize how agent tool calls are displayed in your UI. First, define your tool renderers:

components/weather-tool.tsx
import { defineToolCallRenderer } from "@copilotkit/react-core/v2";

export const weatherToolRender = defineToolCallRenderer({
  name: "get_weather",
  render: ({ args, status }) => {
    return <WeatherCard location={args.location} status={status} />;
  },
});

function WeatherCard({
  location,
  status,
}: {
  location?: string;
  status: string;
}) {
  return (
    <div className="rounded-lg border p-6 shadow-sm">
      <h3 className="text-xl font-semibold">Weather in {location}</h3>
      <div className="mt-4">
        <span className="text-5xl font-light">70°F</span>
      </div>
      {status === "executing" && <div className="spinner">Loading...</div>}
    </div>
  );
}

Register your tool renderers with CopilotKit:

layout.tsx
import { CopilotKit } from "@copilotkit/react-core/v2";
import { weatherToolRender } from "./components/weather-tool";

export default function RootLayout({ children }) {
  return (
    <CopilotKit
      runtimeUrl="/api/copilotkit"
      renderToolCalls={[weatherToolRender]}
    >
      {children}
    </CopilotKit>
  );
}

Then use useRenderToolCall to render tool calls from agent messages:

components/message-list.tsx
import { useAgent, useRenderToolCall } from "@copilotkit/react-core/v2";

export function MessageList() {
  const { agent } = useAgent();
  const renderToolCall = useRenderToolCall();

  return (
    <div className="messages">
      {agent.messages.map((message) => (
        <div key={message.id}>
          {/* Display message content */}
          {message.content && <p>{message.content}</p>}

          {/* Render tool calls if present */}
          {message.role === "assistant" &&
            message.toolCalls?.map((toolCall) => {
              const toolMessage = agent.messages.find(
                (m) => m.role === "tool" && m.toolCallId === toolCall.id,
              );
              return (
                <div key={toolCall.id}>
                  {renderToolCall({ toolCall, toolMessage })}
                </div>
              );
            })}
        </div>
      ))}
    </div>
  );
}

Building a Complete Dashboard#

Here's a full example combining all concepts into an interactive agent dashboard:

page.tsx
"use client";

import { useAgent } from "@copilotkit/react-core/v2";

export default function AgentDashboard() {
  const { agent } = useAgent();

  return (
    <div className="p-8 max-w-4xl mx-auto space-y-6">
      {/* Status */}
      <div className="p-6 bg-white rounded-lg shadow">
        <h2 className="text-xl font-bold mb-4">Agent Status</h2>
        <div className="space-y-2">
          <div className="flex items-center gap-2">
            <div
              className={`w-3 h-3 rounded-full ${
                agent.isRunning ? "bg-yellow-500 animate-pulse" : "bg-green-500"
              }`}
            />
            <span>{agent.isRunning ? "Running" : "Idle"}</span>
          </div>
          <div>Thread: {agent.threadId}</div>
          <div>Messages: {agent.messages.length}</div>
        </div>
      </div>

      {/* State */}
      <div className="p-6 bg-white rounded-lg shadow">
        <h2 className="text-xl font-bold mb-4">Agent State</h2>
        <pre className="bg-gray-50 p-4 rounded text-sm overflow-auto">
          {JSON.stringify(agent.state, null, 2)}
        </pre>
      </div>

      {/* Messages */}
      <div className="p-6 bg-white rounded-lg shadow">
        <h2 className="text-xl font-bold mb-4">Conversation</h2>
        <div className="space-y-3">
          {agent.messages.map((msg) => (
            <div
              key={msg.id}
              className={`p-3 rounded-lg ${
                msg.role === "user" ? "bg-blue-50 ml-8" : "bg-gray-50 mr-8"
              }`}
            >
              <div className="font-semibold text-sm mb-1">
                {msg.role === "user" ? "You" : "Agent"}
              </div>
              <div>{msg.content}</div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

Node-Specific State#

If your LangGraph agent tracks which node it's in, you can show contextual UI:

page.tsx
export function NodeStatus() {
  const { agent } = useAgent();

  const currentNode = agent.state.currentNode;

  return (
    <div>
      {currentNode === "research_node" && (
        <div className="alert">Agent is researching your query...</div>
      )}
      {currentNode === "summarize_node" && (
        <div className="alert">Agent is summarizing findings...</div>
      )}
    </div>
  );
}

Running the Agent Programmatically#

Use copilotkit.runAgent() to trigger your agent from any component — no chat UI required. This is the same method CopilotKit's built-in <CopilotChat /> uses internally.

page.tsx
export function AgentTrigger() {
  const { agent } = useAgent();
  const { copilotkit } = useCopilotKit();

  const handleRun = async () => {
    // Add a user message to the agent's conversation
    agent.addMessage({
      id: randomUUID(),
      role: "user",
      content: "Summarize the latest sales data",
    });

    // Run the agent — handles tool execution, follow-ups, and streaming
    await copilotkit.runAgent({ agent });
  };

  return <button onClick={handleRun}>Run Agent</button>;
}

copilotkit.runAgent() vs agent.runAgent()#

Both methods trigger the agent, but they operate at different levels:

  • copilotkit.runAgent({ agent }) — The recommended approach. Orchestrates the full agent lifecycle: executes frontend tools, handles follow-up runs when tools request them, and manages errors through the subscriber system.
  • agent.runAgent() — Low-level method on the agent instance. Sends the request to the runtime but does not execute frontend tools or handle follow-ups. Use this only when you need direct control over the agent execution (e.g., resuming from an interrupt with forwardedProps).

Stopping a Run#

You can stop a running agent using copilotkit.stopAgent():

page.tsx
const handleStop = () => {
  copilotkit.stopAgent({ agent });
};

Handling LangGraph Interrupts#

LangGraph's interrupt() function emits custom events that you can capture and respond to.

Simple Interrupt Handler#

page.tsx
export function InterruptHandler() {
  const { agent } = useAgent();

  useEffect(() => {
    const subscriber: AgentSubscriber = {
      onCustomEvent: ({ event }) => {
        if (event.name === "on_interrupt") {
          // LangGraph interrupt() was called
          const response = prompt(event.value);

          if (response) {
            // Resume the agent with the user's response
            agent.runAgent({
              forwardedProps: {
                command: { resume: response },
              },
            });
          }
        }
      },
    };

    const { unsubscribe } = agent.subscribe(subscriber);
    return () => unsubscribe();
  }, []);

  return null;
}

Custom Interrupt UI#

For a more sophisticated UI, you can render a custom component:

page.tsx
export function CustomInterruptHandler() {
  const { agent } = useAgent();
  const [interrupt, setInterrupt] = useState<{ message: string } | null>(null);

  useEffect(() => {
    const subscriber: AgentSubscriber = {
      onCustomEvent: ({ event }) => {
        if (event.name === "on_interrupt") {
          setInterrupt({ message: event.value });
        }
      },
    };

    const { unsubscribe } = agent.subscribe(subscriber);
    return () => unsubscribe();
  }, []);

  const handleResponse = (response: string) => {
    agent.runAgent({
      forwardedProps: {
        command: { resume: response },
      },
    });
    setInterrupt(null);
  };

  if (!interrupt) return null;

  return (
    <div className="interrupt-modal">
      <h3>Agent Needs Your Input</h3>
      <p>{interrupt.message}</p>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          const formData = new FormData(e.currentTarget);
          handleResponse(formData.get("response") as string);
        }}
      >
        <input type="text" name="response" placeholder="Your response" />
        <button type="submit">Submit</button>
      </form>
    </div>
  );
}

For a more declarative approach, see useInterrupt.

See Also#