Skip to main content

What is AG-UI?

AG-UI (Agent-UI) is an event-based communication protocol that enables standardized, streaming communication between AI agents and user interfaces. All three layers of CopilotKit communicate exclusively through AG-UI events streamed over Server-Sent Events (SSE).
AG-UI events follow a structured lifecycle and are validated with Zod schemas to ensure type safety and consistency across the stack.

Protocol Fundamentals

Event-Based Architecture

Instead of traditional request-response patterns, AG-UI uses a unidirectional event stream:
Agent → AG-UI Events → Runtime → SSE Stream → Frontend
This enables:
  • Real-time streaming - Text and data stream as they’re generated
  • Progressive rendering - UI updates incrementally as events arrive
  • Tool execution visibility - Track tool calls in real-time
  • Structured lifecycle - Clear start/end boundaries for runs and steps

Transport Layer

Events are transported via Server-Sent Events (SSE) with these characteristics:
  • HTTP-based streaming - Works with standard HTTP infrastructure
  • Automatic reconnection - Browser automatically reconnects on connection loss
  • Text-based format - Easy to debug and monitor
  • Event-type routing - Events are tagged with their type for client-side handling
Reference: packages/v2/core/src/agent.ts:148

Event Lifecycle

Every agent run follows a predictable event sequence:
The lifecycle provides clear boundaries for tracking progress, handling errors, and managing state persistence.

Core Event Types

Run Lifecycle Events

These events mark the boundaries of an agent execution:

RUN_STARTED

Emitted when the agent begins processing a request.
{
  type: "RUN_STARTED",
  runId: "run_abc123",
  threadId: "thread_xyz789",
  input: {
    messages: [...],
    tools: [...],
    context: {...},
    state: {...}
  }
}
Reference: packages/v2/runtime/src/runner/in-memory.ts:129

RUN_FINISHED

Emitted when the agent completes successfully.
{
  type: "RUN_FINISHED",
  runId: "run_abc123",
  threadId: "thread_xyz789"
}
This is a terminal event that completes the Observable stream. Reference: packages/v2/core/src/intelligence-agent.ts:118

RUN_ERROR

Emitted when the agent encounters an unrecoverable error.
{
  type: "RUN_ERROR",
  runId: "run_abc123",
  threadId: "thread_xyz789",
  message: "Error description"
}
This is also a terminal event that errors the Observable stream. Reference: packages/v2/core/src/intelligence-agent.ts:121

Step Events

Some agent frameworks (like LangGraph) execute in discrete steps:

STEP_STARTED

Marks the beginning of a processing step.
{
  type: "STEP_STARTED",
  stepId: "step_1",
  stepName: "analyze_query"
}

STEP_FINISHED

Marks the completion of a step.
{
  type: "STEP_FINISHED",
  stepId: "step_1"
}

Message Events

Message events enable real-time streaming of agent responses:

TEXT_MESSAGE_START

Begins a new message in the conversation.
{
  type: "TEXT_MESSAGE_START",
  messageId: "msg_abc123",
  role: "assistant"
}
Reference: packages/v2/sqlite-runner/src/__tests__/sqlite-runner.test.ts:132

TEXT_MESSAGE_CONTENT

Streams content chunks as the agent generates text.
{
  type: "TEXT_MESSAGE_CONTENT",
  messageId: "msg_abc123",
  content: "Here is the answer to your question..."
}
This event can be emitted multiple times for a single message, enabling word-by-word or sentence-by-sentence streaming. Reference: packages/v2/sqlite-runner/src/__tests__/sqlite-runner.test.ts:140

TEXT_MESSAGE_END

Marks the completion of a message.
{
  type: "TEXT_MESSAGE_END",
  messageId: "msg_abc123"
}
The frontend can start rendering the message as soon as TEXT_MESSAGE_START arrives, then update it progressively with each TEXT_MESSAGE_CONTENT chunk.

Tool Events

Tool events provide visibility into function calling:

TOOL_CALL_START

Emitted when the agent begins calling a tool.
{
  type: "TOOL_CALL_START",
  toolCallId: "call_123",
  toolName: "search_database",
  messageId: "msg_abc123"
}
Reference: packages/v2/sqlite-runner/src/__tests__/sqlite-runner.test.ts:148

TOOL_CALL_ARGS

Provides the arguments for the tool call.
{
  type: "TOOL_CALL_ARGS",
  toolCallId: "call_123",
  args: {
    query: "CopilotKit architecture",
    limit: 10
  }
}
Reference: packages/v2/sqlite-runner/src/__tests__/sqlite-runner.e2e.test.ts:265

TOOL_CALL_END

Marks the end of a tool call (arguments are complete).
{
  type: "TOOL_CALL_END",
  toolCallId: "call_123"
}
Reference: packages/v2/sqlite-runner/src/__tests__/sqlite-runner.e2e.test.ts:270

TOOL_CALL_RESULT

Contains the result returned by the tool.
{
  type: "TOOL_CALL_RESULT",
  toolCallId: "call_123",
  result: JSON.stringify({
    items: [...],
    count: 5
  })
}
Reference: packages/v2/sqlite-runner/src/__tests__/sqlite-runner.e2e.test.ts:274

Custom Events

The protocol supports custom events for domain-specific needs:

CUSTOM

{
  type: "CUSTOM",
  name: "progress_update",
  value: {
    percent: 75,
    stage: "analyzing"
  }
}
Custom events allow agents to communicate application-specific state without extending the core protocol. Reference: packages/v2/core/src/intelligence-agent.ts:59

Event Flow Examples

Simple Q&A Interaction

1. RUN_STARTED
2. STEP_STARTED
3. TEXT_MESSAGE_START
4. TEXT_MESSAGE_CONTENT (chunk 1)
5. TEXT_MESSAGE_CONTENT (chunk 2)
6. TEXT_MESSAGE_CONTENT (chunk 3)
7. TEXT_MESSAGE_END
8. STEP_FINISHED
9. RUN_FINISHED

Tool-Assisted Response

1. RUN_STARTED
2. STEP_STARTED
3. TOOL_CALL_START (search_database)
4. TOOL_CALL_ARGS
5. TOOL_CALL_END
6. TOOL_CALL_RESULT
7. TEXT_MESSAGE_START
8. TEXT_MESSAGE_CONTENT (using search results...)
9. TEXT_MESSAGE_END
10. STEP_FINISHED
11. RUN_FINISHED

Multi-Step Agent Workflow

1. RUN_STARTED
2. STEP_STARTED (step: analyze)
3. TEXT_MESSAGE_START
4. TEXT_MESSAGE_CONTENT
5. TEXT_MESSAGE_END
6. STEP_FINISHED
7. STEP_STARTED (step: search)
8. TOOL_CALL_START
9. TOOL_CALL_ARGS
10. TOOL_CALL_END
11. TOOL_CALL_RESULT
12. STEP_FINISHED
13. STEP_STARTED (step: synthesize)
14. TEXT_MESSAGE_START
15. TEXT_MESSAGE_CONTENT
16. TEXT_MESSAGE_END
17. STEP_FINISHED
18. RUN_FINISHED

Event Processing

Frontend Event Handling

The frontend subscribes to the event stream via Observables:
agent.run(input).subscribe({
  next: (event: BaseEvent) => {
    switch (event.type) {
      case EventType.RUN_STARTED:
        console.log("Agent started");
        break;
      case EventType.TEXT_MESSAGE_CONTENT:
        appendToUI(event.content);
        break;
      case EventType.TOOL_CALL_START:
        showToolIndicator(event.toolName);
        break;
      case EventType.RUN_FINISHED:
        console.log("Agent finished");
        break;
    }
  },
  error: (error) => {
    console.error("Agent error:", error);
  },
  complete: () => {
    console.log("Stream complete");
  }
});
Reference: packages/v2/core/src/agent.ts:172

Runtime Event Emission

Agents emit events through the onEvent callback:
await agent.runAgent(input, {
  onEvent: ({ event }) => {
    // Event is streamed to frontend
    stream.write(formatSSE(event));
  },
  onNewMessage: ({ message }) => {
    // Track new messages
  },
  onRunStartedEvent: () => {
    // Run has started
  }
});
Reference: packages/v2/runtime/src/runner/in-memory.ts:157

Error Handling

Graceful Degradation

The protocol includes mechanisms for handling incomplete or interrupted streams:
  • Event finalization - Missing _END events are automatically generated
  • Zod validation - Events are validated, with invalid events logged but not crashing
  • Abort handling - ZodErrors from aborted streams are suppressed
Reference: packages/v2/core/src/agent.ts:30

Event Validation

All events are validated against Zod schemas before processing:
import { BaseEvent, EventType } from "@ag-ui/client";

// Events are type-checked and runtime-validated
const event: BaseEvent = { /* ... */ };
Malformed events will cause the stream to error. Always ensure your custom agents emit valid AG-UI events.

State Management

Event Compaction

To optimize storage and transmission, the runtime can compact event streams:
  • Merge consecutive TEXT_MESSAGE_CONTENT events
  • Deduplicate redundant state updates
  • Remove intermediate events that don’t affect final state
Reference: packages/v2/runtime/src/runner/in-memory.ts:213

History Replay

The AgentRunner can replay historic events when reconnecting:
await runner.connect({
  threadId: "thread_123",
  headers: {}
});
// Emits all historic events from the thread
This enables:
  • Session restoration - Users can refresh without losing context
  • Multi-device sync - Same conversation across devices
  • Audit trails - Complete history of agent interactions
Reference: packages/v2/runtime/src/runner/in-memory.ts:294

Best Practices

Follow these guidelines when working with AG-UI events:

For Agent Developers

  1. Emit events in order - Follow the lifecycle sequence
  2. Always emit terminal events - Every RUN_STARTED needs a RUN_FINISHED or RUN_ERROR
  3. Close all messages - Every TEXT_MESSAGE_START needs a TEXT_MESSAGE_END
  4. Include context - Attach messageId, runId, threadId where relevant
  5. Validate early - Check event structure before emitting

For Frontend Developers

  1. Handle all event types - Don’t assume a specific sequence
  2. Expect streaming - Process content incrementally
  3. Show progress - Use lifecycle events to display loading states
  4. Handle errors gracefully - Subscribe to both next and error
  5. Clean up subscriptions - Unsubscribe when component unmounts

For Runtime Developers

  1. Preserve event order - Don’t reorder or batch events incorrectly
  2. Monitor stream health - Log errors and disconnections
  3. Implement backpressure - Don’t overwhelm slow clients
  4. Compact wisely - Only compact when safe (not during active runs)

Next Steps

Architecture

Understand the three-layer architecture

Agents

Learn how agents emit AG-UI events

Runtime

Explore AgentRunner and event streaming

Frontend Integration

Handle events in your UI