Server Messages

Server mode communication uses two primary message types: UiMessage for events from the agent and ControllerInputPayload for input to the agent. Understanding these types is essential for building integrations that correctly handle all the events an agent can produce and all the input it can accept.

The message types are designed for serialization, making them suitable for transmission over network protocols. Most fields use simple types that map directly to JSON, and the enum variants provide clear semantics for each event type.


UiMessage Enum

The UiMessage enum represents all events that flow from the agent to your EventSink. Each variant carries the data specific to that event type, plus common fields like session ID for routing.

pub enum UiMessage {
    TextChunk { text: String, session_id: i64, turn_id: TurnId },
    ToolExecuting { tool_use_id: String, tool_name: String, input: Value, session_id: i64 },
    ToolCompleted { tool_use_id: String, result: String, session_id: i64 },
    Complete { session_id: i64, turn_id: TurnId, stop_reason: StopReason },
    PermissionRequired { tool_use_id: String, request: PermissionRequest, session_id: i64 },
    BatchPermissionRequired { batch_id: String, requests: Vec<PermissionRequest>, session_id: i64 },
    UserInteractionRequired { tool_use_id: String, request: AskUserQuestionsRequest, session_id: i64 },
    TokenUpdate { input_tokens: i64, output_tokens: i64, session_id: i64 },
    Error { message: String, session_id: Option<i64> },
    System { message: String, session_id: Option<i64> },
    Display { content: String, session_id: i64 },
    CommandComplete { command: String, session_id: i64 },
}

Streaming Events

These events represent the real-time flow of agent responses.

TextChunk

Streaming text from the LLM response. These arrive frequently during generation, each containing a small piece of the response.

UiMessage::TextChunk {
    text: String,       // The text fragment
    session_id: i64,    // Which session
    turn_id: TurnId,    // Which turn in the conversation
}

Concatenate chunks to build the complete response. For real-time display, render each chunk as it arrives. The text may break at any character boundary, including mid-word or mid-sentence.

match event {
    UiMessage::TextChunk { text, .. } => {
        response_buffer.push_str(&text);
        send_to_client(&text);  // Stream to frontend
    }
    // ...
}

Complete

Signals that a turn has finished. This arrives after all TextChunks and tool executions for a turn are complete.

UiMessage::Complete {
    session_id: i64,
    turn_id: TurnId,
    stop_reason: StopReason,
}

The stop_reason indicates why generation stopped:

ReasonDescription
EndTurnNormal completion
MaxTokensHit the token limit
StopSequenceHit a stop sequence
ToolUsePaused for tool execution

Use Complete to know when it’s safe to accept the next user message.


Tool Events

These events track tool execution lifecycle.

ToolExecuting

A tool has started executing. Sent when the LLM requests a tool call and the agent begins execution.

UiMessage::ToolExecuting {
    tool_use_id: String,  // Unique identifier for this invocation
    tool_name: String,    // Which tool (e.g., "read_file")
    input: Value,         // Tool input parameters as JSON
    session_id: i64,
}

Display this to show users what the agent is doing. The tool_use_id correlates with the subsequent ToolCompleted event.

match event {
    UiMessage::ToolExecuting { tool_name, input, .. } => {
        println!("Running {}: {:?}", tool_name, input);
    }
    // ...
}

ToolCompleted

A tool has finished executing. Sent after the tool returns, whether successfully or with an error.

UiMessage::ToolCompleted {
    tool_use_id: String,  // Matches the ToolExecuting event
    result: String,       // Tool output or error message
    session_id: i64,
}

The result contains the tool’s output. For successful executions, this is the tool’s return value. For failures, it’s an error message.

match event {
    UiMessage::ToolCompleted { tool_use_id, result, .. } => {
        println!("Tool {} completed: {}", tool_use_id, result);
    }
    // ...
}

Permission Events

These events request permission for sensitive operations.

PermissionRequired

A tool needs permission for a single operation. The agent pauses until you respond through the PermissionRegistry.

UiMessage::PermissionRequired {
    tool_use_id: String,
    request: PermissionRequest,
    session_id: i64,
}

The PermissionRequest contains details about what the tool wants to do:

pub struct PermissionRequest {
    pub target_type: TargetType,   // Path, Domain, or Command
    pub target: String,            // The specific resource
    pub level: PermissionLevel,    // Read, Write, Execute, Admin
    pub description: String,       // Human-readable description
    pub reason: Option<String>,    // Why permission is needed
    pub recursive: bool,           // Covers subdirectories?
}

If your policy returns AskUser, you must respond to this event. See Server Registries for response handling.

BatchPermissionRequired

Multiple tools need permissions simultaneously. This happens when the LLM requests several tool calls in one turn.

UiMessage::BatchPermissionRequired {
    batch_id: String,
    requests: Vec<PermissionRequest>,
    session_id: i64,
}

Respond with a batch response covering all requests. Users can approve some and deny others.


User Interaction Events

UserInteractionRequired

The ask_user_questions tool needs user input. This event contains questions that the user should answer.

UiMessage::UserInteractionRequired {
    tool_use_id: String,
    request: AskUserQuestionsRequest,
    session_id: i64,
}

The request contains structured questions:

pub struct AskUserQuestionsRequest {
    pub questions: Vec<Question>,
}

pub struct Question {
    pub id: String,
    pub text: String,
    pub question_type: QuestionType,
    pub options: Option<Vec<String>>,
    pub required: bool,
}

If your policy supports user interaction, collect answers and respond through the UserInteractionRegistry.


Status Events

These events provide operational information.

TokenUpdate

Real-time token usage statistics. Sent periodically during generation to track context consumption.

UiMessage::TokenUpdate {
    input_tokens: i64,   // Tokens in the prompt
    output_tokens: i64,  // Tokens generated so far
    session_id: i64,
}

Use this to display usage meters or trigger warnings when approaching limits.

Error

An error occurred during processing. May or may not be fatal depending on the error type.

UiMessage::Error {
    message: String,
    session_id: Option<i64>,  // None for agent-wide errors
}

Log errors and optionally display them to users. Some errors are recoverable (rate limits, transient network issues); others require intervention.

System

System-level informational messages. Not typically shown to end users but useful for debugging.

UiMessage::System {
    message: String,
    session_id: Option<i64>,
}

Display

Content that should be displayed to the user, typically from slash commands or system messages.

UiMessage::Display {
    content: String,
    session_id: i64,
}

CommandComplete

A slash command has finished executing.

UiMessage::CommandComplete {
    command: String,
    session_id: i64,
}

ControllerInputPayload

Input messages to the agent use the ControllerInputPayload struct.

pub struct ControllerInputPayload {
    pub session_id: i64,
    pub content: String,
    pub turn_id: TurnId,
    pub metadata: Option<HashMap<String, Value>>,
}

Fields

FieldDescription
session_idRoutes the message to the correct session
contentThe user’s message text
turn_idSequence number for this turn
metadataOptional key-value pairs for custom data

Creating Payloads

Use the constructor for common cases:

let payload = ControllerInputPayload::data(
    session_id,
    "What's the weather like?",
    TurnId::new_user_turn(1)
);

Or construct directly for full control:

let mut metadata = HashMap::new();
metadata.insert("client_timestamp".to_string(), json!(1234567890));
metadata.insert("client_version".to_string(), json!("1.2.3"));

let payload = ControllerInputPayload {
    session_id: 1,
    content: "Hello".to_string(),
    turn_id: TurnId::new_user_turn(1),
    metadata: Some(metadata),
};

TurnId

Turn IDs sequence messages within a session. They help correlate requests with responses and maintain conversation order.

pub struct TurnId {
    pub role: Role,
    pub sequence: u64,
}

pub enum Role {
    User,
    Assistant,
}

Create turn IDs with the constructor methods:

// User's first message
TurnId::new_user_turn(1)

// User's second message
TurnId::new_user_turn(2)

// Assistant's response (used internally)
TurnId::new_assistant_turn(1)

Increment the sequence for each user message. The agent assigns its own turn IDs for responses.


Serialization

All message types implement Serialize and Deserialize for JSON encoding. This makes them suitable for network transmission.

use serde_json;

// Serialize an event
let event = UiMessage::TextChunk {
    text: "Hello".to_string(),
    session_id: 1,
    turn_id: TurnId::new_assistant_turn(1),
};
let json = serde_json::to_string(&event)?;
// {"TextChunk":{"text":"Hello","session_id":1,"turn_id":{"role":"Assistant","sequence":1}}}

// Deserialize a payload
let json = r#"{"session_id":1,"content":"Hi","turn_id":{"role":"User","sequence":1}}"#;
let payload: ControllerInputPayload = serde_json::from_str(json)?;

The JSON format uses tagged enums for UiMessage, with the variant name as the key. Adjust your client-side parsing accordingly.


Event Handling Pattern

A typical event handler switches on the variant and processes each type appropriately:

async fn handle_event(event: UiMessage, client: &Client) {
    match event {
        UiMessage::TextChunk { text, session_id, .. } => {
            client.send_text(session_id, &text).await;
        }

        UiMessage::ToolExecuting { tool_name, session_id, .. } => {
            client.send_status(session_id, &format!("Running {}...", tool_name)).await;
        }

        UiMessage::ToolCompleted { session_id, .. } => {
            client.send_status(session_id, "Tool complete").await;
        }

        UiMessage::Complete { session_id, .. } => {
            client.send_complete(session_id).await;
        }

        UiMessage::PermissionRequired { tool_use_id, request, session_id } => {
            // Auto-approve or forward to user based on your policy
            handle_permission(tool_use_id, request, session_id).await;
        }

        UiMessage::Error { message, session_id } => {
            log::error!("Agent error: {}", message);
            if let Some(sid) = session_id {
                client.send_error(sid, &message).await;
            }
        }

        UiMessage::TokenUpdate { input_tokens, output_tokens, session_id } => {
            client.send_usage(session_id, input_tokens, output_tokens).await;
        }

        _ => {}  // Ignore other events
    }
}

Handle all variants you care about and ignore the rest. New variants may be added in future versions, so use a catch-all pattern.