CopilotKit

Events

The AG-UI protocol defines 25 event types that represent the complete lifecycle of agent interactions. Events are emitted as streaming responses and provide real-time updates about message content, tool calls, thinking processes, and state changes.

Event Categories

Message Events (4)

Handle text message streaming from agents.

TextMessageStartEvent

Agent begins a text message response.

data class TextMessageStartEvent(
    val messageId: String,
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

Usage:

when (event) {
    is TextMessageStartEvent -> {
        println("Agent started message: ${event.messageId}")
        // Initialize message buffer
        messageBuffer[event.messageId] = StringBuilder()
    }
}

TextMessageContentEvent

Streaming text content chunk.

data class TextMessageContentEvent(
    val messageId: String,
    val delta: String,
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

Usage:

when (event) {
    is TextMessageContentEvent -> {
        print(event.delta) // Stream to UI
        messageBuffer[event.messageId]?.append(event.delta)
    }
}

TextMessageEndEvent

Agent completes text message.

data class TextMessageEndEvent(
    val messageId: String,
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

Usage:

when (event) {
    is TextMessageEndEvent -> {
        val fullMessage = messageBuffer[event.messageId]?.toString()
        println("\nMessage complete: $fullMessage")
    }
}

TextMessageChunkEvent

Chunk-based text streaming (automatically converted to start/content/end sequence).

data class TextMessageChunkEvent(
    val messageId: String? = null,
    val delta: String? = null,
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

Thinking Events (5)

Handle agent internal reasoning processes.

ThinkingStartEvent

Agent begins internal reasoning.

data class ThinkingStartEvent(
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

Usage:

when (event) {
    is ThinkingStartEvent -> {
        showThinkingIndicator(true)
        println("🤔 Agent is thinking...")
    }
}

ThinkingTextMessageStartEvent

Agent starts thinking text message.

data class ThinkingTextMessageStartEvent(
    val messageId: String,
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

ThinkingTextMessageContentEvent

Streaming thinking content.

data class ThinkingTextMessageContentEvent(
    val messageId: String,
    val delta: String,
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

Usage:

when (event) {
    is ThinkingTextMessageContentEvent -> {
        // Show thinking process to user (optional)
        displayThinking(event.delta)
    }
}

ThinkingTextMessageEndEvent

Agent completes thinking text.

data class ThinkingTextMessageEndEvent(
    val messageId: String,
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

ThinkingEndEvent

Agent finishes reasoning process.

data class ThinkingEndEvent(
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

Usage:

when (event) {
    is ThinkingEndEvent -> {
        showThinkingIndicator(false)
        println("💡 Agent finished thinking")
    }
}

Tool Events (5)

Handle client-side tool execution lifecycle.

ToolCallStartEvent

Agent requests tool execution.

data class ToolCallStartEvent(
    val toolCallId: String,
    val toolCallName: String,
    val parentMessageId: String? = null,
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

Usage:

when (event) {
    is ToolCallStartEvent -> {
        println("🔧 Tool call: ${event.toolCallName}")
        showToolIndicator(event.toolCallName)
    }
}

ToolCallArgsEvent

Streaming tool arguments.

data class ToolCallArgsEvent(
    val toolCallId: String,
    val delta: String,
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

Usage:

when (event) {
    is ToolCallArgsEvent -> {
        // Accumulate arguments for tool execution
        toolArgsBuffer[event.toolCallId] =
            (toolArgsBuffer[event.toolCallId] ?: "") + event.delta
    }
}

ToolCallEndEvent

Tool call arguments complete.

data class ToolCallEndEvent(
    val toolCallId: String,
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

ToolCallChunkEvent

Chunk-based tool streaming (automatically converted to start/args/end sequence).

data class ToolCallChunkEvent(
    val toolCallId: String? = null,
    val toolCallName: String? = null,
    val parentMessageId: String? = null,
    val delta: String? = null,
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

ToolCallResultEvent

Contains the result of a tool execution.

data class ToolCallResultEvent(
    val toolCallId: String,
    val result: String,
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

ToolResultEvent

Tool execution result.

data class ToolResultEvent(
    val messageId: String,
    val toolCallId: String,
    val content: String,
    val role: String = "tool",
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

Usage:

when (event) {
    is ToolResultEvent -> {
        println("🔧 Tool result: ${event.content}")
        hideToolIndicator()
    }
}

State Events (2)

Handle agent state synchronization.

StateSnapshotEvent

Complete state snapshot.

data class StateSnapshotEvent(
    val snapshot: JsonElement,
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

Usage:

when (event) {
    is StateSnapshotEvent -> {
        currentState = event.snapshot
        updateUI(currentState)
    }
}

StateDeltaEvent

Incremental state change using JSON Patch.

data class StateDeltaEvent(
    val delta: JsonElement,
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

Usage:

when (event) {
    is StateDeltaEvent -> {
        // Apply JSON patch to current state
        currentState = applyJsonPatch(currentState, event.delta)
        updateUI(currentState)
    }
}

Lifecycle Events (2)

Handle run lifecycle management.

RunStartedEvent

Agent run begins.

data class RunStartedEvent(
    val threadId: String,
    val runId: String,
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

Usage:

when (event) {
    is RunStartedEvent -> {
        println("🚀 Run started: ${event.runId}")
        showLoadingIndicator(true)
    }
}

RunFinishedEvent

Agent run completes.

data class RunFinishedEvent(
    val threadId: String,
    val runId: String,
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

Usage:

when (event) {
    is RunFinishedEvent -> {
        println("✅ Run finished: ${event.runId}")
        showLoadingIndicator(false)
    }
}

Error Events (1)

Handle protocol and execution errors.

ErrorEvent

Protocol or execution error.

data class ErrorEvent(
    val error: AgentError,
    val timestamp: Long? = null,
    val rawEvent: JsonElement? = null
) : BaseEvent()

Usage:

when (event) {
    is ErrorEvent -> {
        println("❌ Error: ${event.error.message}")
        when (event.error.type) {
            "network" -> handleNetworkError(event.error)
            "authentication" -> handleAuthError(event.error)
            "protocol" -> handleProtocolError(event.error)
            else -> handleGenericError(event.error)
        }
    }
}

Complete Event Handling

Comprehensive Handler

class EventHandler {
    private val messageBuffers = mutableMapOf<String, StringBuilder>()
    private val toolArgsBuffers = mutableMapOf<String, StringBuilder>()
    private var currentState: JsonElement = JsonObject(emptyMap())

    fun handleEvent(event: BaseEvent) {
        when (event) {
            // Message Events
            is TextMessageStartEvent -> {
                println("📝 Agent message started: ${event.messageId}")
                messageBuffers[event.messageId] = StringBuilder()
            }
            is TextMessageContentEvent -> {
                print(event.delta)
                messageBuffers[event.messageId]?.append(event.delta)
            }
            is TextMessageEndEvent -> {
                val fullMessage = messageBuffers.remove(event.messageId)?.toString()
                println("\n📝 Message complete: $fullMessage")
            }

            // Thinking Events
            is ThinkingStartEvent -> println("🤔 Agent thinking...")
            is ThinkingTextMessageContentEvent -> println("💭 ${event.delta}")
            is ThinkingEndEvent -> println("💡 Thinking complete")

            // Tool Events
            is ToolCallStartEvent -> {
                println("🔧 Tool call: ${event.toolCallName} (${event.toolCallId})")
                toolArgsBuffers[event.toolCallId] = StringBuilder()
            }
            is ToolCallArgsEvent -> {
                toolArgsBuffers[event.toolCallId]?.append(event.delta)
            }
            is ToolCallEndEvent -> {
                val args = toolArgsBuffers.remove(event.toolCallId)?.toString()
                println("🔧 Tool args complete: $args")
            }
            is ToolResultEvent -> {
                println("🔧 Tool result: ${event.content}")
            }

            // State Events
            is StateSnapshotEvent -> {
                currentState = event.snapshot
                println("📊 State updated")
            }
            is StateDeltaEvent -> {
                // Apply JSON patch (simplified)
                println("📊 State delta applied")
            }

            // Lifecycle Events
            is RunStartedEvent -> println("🚀 Run ${event.runId} started")
            is RunFinishedEvent -> println("✅ Run ${event.runId} finished")

            // Error Events
            is ErrorEvent -> {
                println("❌ Error: ${event.error.message}")
                println("   Type: ${event.error.type}")
            }

            // Chunk Events (handled by ChunkTransform)
            is TextMessageChunkEvent -> {
                // Usually processed by transform before reaching handler
                println("📦 Text chunk: ${event.delta}")
            }
            is ToolCallChunkEvent -> {
                // Usually processed by transform before reaching handler
                println("📦 Tool chunk: ${event.delta}")
            }
        }
    }
}

Event Flow Examples

Text Message Flow

// Typical text message event sequence:
// 1. RunStartedEvent
// 2. TextMessageStartEvent
// 3. TextMessageContentEvent (multiple)
// 4. TextMessageEndEvent
// 5. RunFinishedEvent

agent.sendMessage("Hello").collect { event ->
    when (event) {
        is RunStartedEvent -> startConversation()
        is TextMessageStartEvent -> beginResponse()
        is TextMessageContentEvent -> streamContent(event.delta)
        is TextMessageEndEvent -> completeResponse()
        is RunFinishedEvent -> endConversation()
    }
}

Tool Call Flow

// Typical tool call event sequence:
// 1. RunStartedEvent
// 2. ToolCallStartEvent
// 3. ToolCallArgsEvent (multiple)
// 4. ToolCallEndEvent
// 5. ToolResultEvent
// 6. TextMessageStartEvent (agent response)
// 7. TextMessageContentEvent (multiple)
// 8. TextMessageEndEvent
// 9. RunFinishedEvent

agent.sendMessage("What's the weather?").collect { event ->
    when (event) {
        is ToolCallStartEvent -> showToolExecution(event.toolCallName)
        is ToolResultEvent -> showToolResult(event.content)
        is TextMessageContentEvent -> showAgentResponse(event.delta)
    }
}

Thinking Flow

// Agents may think before responding:
// 1. RunStartedEvent
// 2. ThinkingStartEvent
// 3. ThinkingTextMessageStartEvent
// 4. ThinkingTextMessageContentEvent (multiple)
// 5. ThinkingTextMessageEndEvent
// 6. ThinkingEndEvent
// 7. TextMessageStartEvent
// 8. TextMessageContentEvent (multiple)
// 9. TextMessageEndEvent
// 10. RunFinishedEvent

agent.sendMessage("Complex question").collect { event ->
    when (event) {
        is ThinkingStartEvent -> showThinkingUI()
        is ThinkingTextMessageContentEvent -> showThinking(event.delta)
        is ThinkingEndEvent -> hideThinkingUI()
        is TextMessageContentEvent -> showResponse(event.delta)
    }
}

Best Practices

Exhaustive Handling

// Always handle all event types
fun processEvent(event: BaseEvent): Unit = when (event) {
    is TextMessageStartEvent -> handleTextStart(event)
    is TextMessageContentEvent -> handleTextContent(event)
    is TextMessageEndEvent -> handleTextEnd(event)
    is TextMessageChunkEvent -> handleTextChunk(event)
    is ThinkingStartEvent -> handleThinkingStart(event)
    is ThinkingTextMessageStartEvent -> handleThinkingTextStart(event)
    is ThinkingTextMessageContentEvent -> handleThinkingTextContent(event)
    is ThinkingTextMessageEndEvent -> handleThinkingTextEnd(event)
    is ThinkingEndEvent -> handleThinkingEnd(event)
    is ToolCallStartEvent -> handleToolStart(event)
    is ToolCallArgsEvent -> handleToolArgs(event)
    is ToolCallEndEvent -> handleToolEnd(event)
    is ToolCallChunkEvent -> handleToolChunk(event)
    is ToolResultEvent -> handleToolResult(event)
    is StateSnapshotEvent -> handleStateSnapshot(event)
    is StateDeltaEvent -> handleStateDelta(event)
    is RunStartedEvent -> handleRunStart(event)
    is RunFinishedEvent -> handleRunEnd(event)
    is ErrorEvent -> handleError(event)
    // Compiler ensures all cases covered
}

Event Buffering

class EventBuffer {
    private val textBuffers = mutableMapOf<String, StringBuilder>()
    private val toolArgsBuffers = mutableMapOf<String, StringBuilder>()

    fun bufferTextContent(event: TextMessageContentEvent) {
        textBuffers.getOrPut(event.messageId) { StringBuilder() }
            .append(event.delta)
    }

    fun getCompleteText(messageId: String): String? {
        return textBuffers.remove(messageId)?.toString()
    }
}

Error Handling

fun handleEventSafely(event: BaseEvent) {
    try {
        processEvent(event)
    } catch (e: Exception) {
        logger.error("Error processing event ${event.eventType}", e)
        // Handle gracefully - don't crash the stream
    }
}