Server Quickstart

While terminal agents with TUI are great for interactive use, you often need agents that run as backend services - handling API requests, processing webhooks, or running automated tasks. agent-air supports this by exposing the underlying channels for direct integration.

Overview

Instead of calling into_tui().run(), server agents work directly with the controller channels:

  • Input channel - Send user messages and control commands to the agent
  • Output channel - Receive streaming responses, tool events, and status updates

Basic Server Agent

Here’s a minimal headless agent based on the headless-agent example:

use agent_air::agent::{AgentConfig, AgentAir};

struct MyAgentConfig;

impl AgentConfig for MyAgentConfig {
    fn name(&self) -> &str { "ServerAgent" }
    fn config_path(&self) -> &str { ".serveragent/config.yaml" }
    fn default_system_prompt(&self) -> &str { "You are a helpful assistant." }
    fn log_prefix(&self) -> &str { "serveragent" }
}

fn main() {
    // Create the agent
    let mut agent = match AgentAir::new(&MyAgentConfig) {
        Ok(agent) => agent,
        Err(e) => {
            eprintln!("Failed to create agent: {}", e);
            return;
        }
    };

    // Start background tasks (controller, input router)
    agent.start_background_tasks();

    // Get the channels for communication
    let tx = agent.to_controller_tx();
    let rx = agent.take_from_controller_rx();

    // Create an initial session
    match agent.create_initial_session() {
        Ok((session_id, model, _limit)) => {
            println!("Session {} created using {}", session_id, model);

            // In a real application:
            // 1. Send messages via tx using ControllerInputPayload::data()
            // 2. Receive responses via rx (UiMessage enum)
            // 3. Handle tool results, errors, and completion events
        }
        Err(e) => {
            eprintln!("Failed to create session: {}", e);
        }
    }

    // Shutdown when done
    agent.shutdown();
}

Sending Messages

To send a user message to the agent, use ControllerInputPayload::data():

use agent_air::agent::{ControllerInputPayload, TurnId};

// Create a message payload
let payload = ControllerInputPayload::data(
    session_id,
    "Hello, agent!",
    TurnId::new_user_turn(1),  // Increment for each user turn
);

// Send via the channel
tx.send(payload).await.expect("Failed to send message");

Receiving Events

The output channel sends UiMessage events. Handle them in a loop:

use agent_air::agent::UiMessage;

while let Some(msg) = rx.recv().await {
    match msg {
        UiMessage::TextChunk { text, .. } => {
            // Streaming text from LLM
            print!("{}", text);
        }
        UiMessage::Complete { .. } => {
            // Response finished
            println!("\n[Complete]");
            break;
        }
        UiMessage::Error { error, .. } => {
            eprintln!("Error: {}", error);
            break;
        }
        UiMessage::ToolExecuting { display_name, .. } => {
            println!("[Tool: {}]", display_name);
        }
        UiMessage::ToolCompleted { .. } => {
            println!("[Tool completed]");
        }
        _ => {}
    }
}

UiMessage Events

The UiMessage enum provides all events you need to track agent activity:

EventDescription
TextChunkStreaming text from the LLM response
CompleteResponse finished, includes stop reason
ErrorAn error occurred
ToolExecutingA tool is being executed
ToolCompletedTool execution finished
TokenUpdateToken usage statistics
UserInteractionRequiredAgent needs user input (from AskUserQuestions tool)
PermissionRequiredAgent needs permission for an action

Handling User Interactions

When tools require user input or permissions, your server agent receives UserInteractionRequired or PermissionRequired events. Respond through the registries:

use agent_air::controller::{AskUserQuestionsResponse, PermissionPanelResponse};

match msg {
    UiMessage::UserInteractionRequired { tool_use_id, request, .. } => {
        // Get user response via your API
        let response: AskUserQuestionsResponse = get_user_response(&request);

        // Submit the response
        agent.user_interaction_registry()
            .respond(&tool_use_id, response)
            .await;
    }
    UiMessage::PermissionRequired { tool_use_id, request, .. } => {
        // Check or request permission
        let response = if check_permission(&request) {
            PermissionPanelResponse::allow()
        } else {
            PermissionPanelResponse::deny()
        };

        agent.permission_registry()
            .respond_to_request(&tool_use_id, response)
            .await
            .ok();
    }
    _ => {}
}

Key Differences from TUI Agents

AspectTUI AgentServer Agent
Entry pointagent.into_tui().run()Manual channel management
Event handlingBuilt-in renderingCustom event loop
User interactionTUI widgetsAPI/WebSocket responses
RuntimeManaged by TuiRunnerYou manage the event loop

Next Steps