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:
| Event | Description |
|---|---|
TextChunk | Streaming text from the LLM response |
Complete | Response finished, includes stop reason |
Error | An error occurred |
ToolExecuting | A tool is being executed |
ToolCompleted | Tool execution finished |
TokenUpdate | Token usage statistics |
UserInteractionRequired | Agent needs user input (from AskUserQuestions tool) |
PermissionRequired | Agent 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
| Aspect | TUI Agent | Server Agent |
|---|---|---|
| Entry point | agent.into_tui().run() | Manual channel management |
| Event handling | Built-in rendering | Custom event loop |
| User interaction | TUI widgets | API/WebSocket responses |
| Runtime | Managed by TuiRunner | You manage the event loop |
Next Steps
- Tool System - Add tools to your agent
- LLM Providers - Configure LLM providers
- Session Management - Manage multiple sessions
