Lifecycle Hooks
The agent lifecycle encompasses startup, runtime, and shutdown phases. Agent Air provides hooks for customizing behavior at key lifecycle points, particularly around application exit and status display. Exit handlers let you run cleanup code or cancel shutdown, while the status bar configuration controls what information is shown to users during operation.
Understanding these lifecycle hooks helps you build robust agents that properly clean up resources, save state, and provide useful feedback to users throughout their session.
Exit Handlers
Exit handlers run cleanup code when the application exits. They can save state, close connections, or even cancel the exit if needed. The handler is called after the user confirms exit but before the application terminates.
ExitHandler Trait
The trait defines a single method:
pub trait ExitHandler: Send + 'static {
/// Called when exit is confirmed.
///
/// Return `true` to proceed with exit, or `false` to cancel the exit.
fn on_exit(&mut self) -> bool {
true
}
}
The default implementation returns true, allowing exit to proceed.
Setting an Exit Handler
Register an exit handler using set_exit_handler() on AgentAir:
impl AgentAir {
pub fn set_exit_handler<H: ExitHandler>(&mut self, handler: H) -> &mut Self;
}
Example:
use agent_air::AgentAir;
use agent_air::tui::ExitHandler;
struct MyExitHandler;
impl ExitHandler for MyExitHandler {
fn on_exit(&mut self) -> bool {
println!("Cleaning up...");
true
}
}
let mut agent = AgentAir::new(&MyConfig)?;
agent.set_exit_handler(MyExitHandler);
Saving State on Exit
A common use case is saving session state:
use agent_air::tui::ExitHandler;
use std::path::PathBuf;
use std::fs;
struct SaveOnExitHandler {
session_file: PathBuf,
state: serde_json::Value,
}
impl SaveOnExitHandler {
fn new(session_file: PathBuf) -> Self {
Self {
session_file,
state: serde_json::json!({}),
}
}
fn save_session(&self) -> std::io::Result<()> {
let content = serde_json::to_string_pretty(&self.state)?;
fs::write(&self.session_file, content)?;
Ok(())
}
}
impl ExitHandler for SaveOnExitHandler {
fn on_exit(&mut self) -> bool {
if let Err(e) = self.save_session() {
eprintln!("Failed to save session: {}", e);
}
true // Still proceed with exit
}
}
Vetoing Exit
Return false from on_exit() to cancel the exit:
use std::sync::{Arc, RwLock};
struct UnsavedChangesHandler {
has_unsaved_changes: Arc<RwLock<bool>>,
}
impl ExitHandler for UnsavedChangesHandler {
fn on_exit(&mut self) -> bool {
let unsaved = *self.has_unsaved_changes.read().unwrap();
if unsaved {
eprintln!("Warning: Unsaved changes will be lost!");
false // Cancel exit
} else {
true
}
}
}
Closing Connections
Clean up external resources:
use std::sync::Arc;
struct ResourceCleanupHandler {
db_connection: Arc<DatabaseConnection>,
cache_client: Arc<CacheClient>,
}
impl ExitHandler for ResourceCleanupHandler {
fn on_exit(&mut self) -> bool {
if let Err(e) = self.db_connection.close() {
tracing::error!("Failed to close database: {}", e);
}
if let Err(e) = self.cache_client.flush() {
tracing::error!("Failed to flush cache: {}", e);
}
tracing::info!("Resources cleaned up");
true
}
}
Composite Handler
Combine multiple exit behaviors:
struct CompositeExitHandler {
handlers: Vec<Box<dyn ExitHandler>>,
}
impl CompositeExitHandler {
fn new() -> Self {
Self { handlers: Vec::new() }
}
fn add<H: ExitHandler + 'static>(mut self, handler: H) -> Self {
self.handlers.push(Box::new(handler));
self
}
}
impl ExitHandler for CompositeExitHandler {
fn on_exit(&mut self) -> bool {
// Run all handlers, cancel if any returns false
for handler in &mut self.handlers {
if !handler.on_exit() {
return false;
}
}
true
}
}
// Usage
let exit_handler = CompositeExitHandler::new()
.add(SaveStateHandler::new())
.add(CloseConnectionsHandler::new())
.add(LogExitHandler::new());
agent.set_exit_handler(exit_handler);
Exit Flow
- User initiates exit (Ctrl+D twice or Esc with empty input)
- App checks if exit handler exists
- If handler exists,
on_exit()is called - If
on_exit()returnstrue, app exits - If
on_exit()returnsfalse, exit is cancelled
Status Bar Configuration
The status bar displays application state at the bottom of the TUI. You can customize what information is shown, provide a custom renderer, or hide it entirely. The status bar is a key feedback mechanism for users, showing model information, context usage, and helpful hints.
StatusBarConfig
Configuration options:
pub struct StatusBarConfig {
/// Height of the status bar (default: 2)
pub height: u16,
/// Show current working directory (default: true)
pub show_cwd: bool,
/// Show model name (default: true)
pub show_model: bool,
/// Show context usage (default: true)
pub show_context: bool,
/// Show help hints (default: true)
pub show_hints: bool,
/// Custom content renderer (overrides all flags)
pub content_renderer: Option<StatusBarRenderer>,
}
Default Configuration
let config = StatusBarConfig::new();
// Equivalent to:
let config = StatusBarConfig {
height: 2,
show_cwd: true,
show_model: true,
show_context: true,
show_hints: true,
content_renderer: None,
};
Hiding Elements
Disable specific elements:
use agent_air::tui::widgets::{StatusBar, StatusBarConfig};
let status_bar = StatusBar::with_config(StatusBarConfig {
show_cwd: false, // Hide working directory
show_context: false, // Hide context usage
..Default::default()
});
agent.set_status_bar(status_bar);
StatusBarData
The data passed to renderers:
pub struct StatusBarData {
/// Current working directory
pub cwd: String,
/// Model name
pub model_name: String,
/// Context tokens used
pub context_used: i64,
/// Context token limit
pub context_limit: i32,
/// Current session ID
pub session_id: i64,
/// Status hint from key handler
pub status_hint: Option<String>,
/// Whether the app is waiting for a response
pub is_waiting: bool,
/// Time elapsed since waiting started
pub waiting_elapsed: Option<Duration>,
/// Whether the input is empty
pub input_empty: bool,
/// Whether panels are active (suppress hints)
pub panels_active: bool,
}
Custom Renderer
For complete control, provide a custom renderer:
pub type StatusBarRenderer = Box<dyn Fn(&StatusBarData, &Theme) -> Vec<Line<'static>> + Send>;
Example:
use agent_air::tui::widgets::{StatusBar, StatusBarData};
use ratatui::text::{Line, Span};
let status_bar = StatusBar::new()
.with_renderer(|data: &StatusBarData, theme: &Theme| {
let line1 = Line::from(vec![
Span::raw(" "),
Span::styled(&data.model_name, theme.status_model),
Span::raw(" | Session "),
Span::raw(data.session_id.to_string()),
]);
let line2 = if data.is_waiting {
Line::from(Span::styled(" Processing...", theme.status_help))
} else {
Line::from(Span::styled(" Ready", theme.status_help))
};
vec![line1, line2]
});
agent.set_status_bar(status_bar);
Minimal Status Bar
A single-line minimal status bar:
let status_bar = StatusBar::with_config(StatusBarConfig {
height: 1,
show_cwd: false,
show_model: true,
show_context: true,
show_hints: false,
content_renderer: None,
});
Hiding the Status Bar
Hide the status bar entirely:
agent.hide_status_bar();
Context Usage Display
The default renderer shows context usage with color coding:
// Default format:
// "Context: 4.3K/200K (2%)" - Normal (default color)
// "Context Low: 180K/200K (90%)" - Warning (yellow)
Status Hints
The key handler provides status hints that appear in the status bar:
// When exit confirmation is pending:
// " Press again to exit"
// When waiting for LLM:
// " escape to interrupt (5s)"
// When input is empty:
// " Ctrl-D to exit"
// When typing:
// " Shift-Enter to add a new line"
Complete Exit Handler Example
use agent_air::AgentAir;
use agent_air::tui::ExitHandler;
use std::path::PathBuf;
use std::fs;
use std::sync::{Arc, RwLock};
use std::time::Instant;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Default)]
struct SessionState {
last_query: Option<String>,
command_history: Vec<String>,
preferences: std::collections::HashMap<String, String>,
}
struct AgentExitHandler {
state_file: PathBuf,
state: Arc<RwLock<SessionState>>,
start_time: Instant,
agent_name: String,
}
impl AgentExitHandler {
fn new(
state_file: PathBuf,
state: Arc<RwLock<SessionState>>,
agent_name: &str,
) -> Self {
Self {
state_file,
state,
start_time: Instant::now(),
agent_name: agent_name.to_string(),
}
}
fn save_state(&self) -> Result<(), Box<dyn std::error::Error>> {
let state = self.state.read().unwrap();
let content = serde_json::to_string_pretty(&*state)?;
if let Some(parent) = self.state_file.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&self.state_file, content)?;
Ok(())
}
}
impl ExitHandler for AgentExitHandler {
fn on_exit(&mut self) -> bool {
let duration = self.start_time.elapsed();
tracing::info!(
"{} exiting after {:.1}s",
self.agent_name,
duration.as_secs_f64()
);
match self.save_state() {
Ok(()) => {
tracing::info!("Session state saved to {:?}", self.state_file);
}
Err(e) => {
tracing::error!("Failed to save state: {}", e);
}
}
true
}
}
fn main() -> std::io::Result<()> {
let mut agent = AgentAir::new(&MyConfig)?;
let state = Arc::new(RwLock::new(SessionState::default()));
let state_file = dirs::data_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("myagent")
.join("session.json");
let exit_handler = AgentExitHandler::new(
state_file,
state.clone(),
"MyAgent",
);
agent.set_exit_handler(exit_handler);
agent.run()
}
Complete Status Bar Example
use agent_air::AgentAir;
use agent_air::tui::widgets::{StatusBar, StatusBarConfig, StatusBarData};
use agent_air::tui::themes::Theme;
use ratatui::{
style::{Color, Style},
text::{Line, Span},
};
use std::time::Duration;
fn format_tokens(tokens: i64) -> String {
if tokens >= 1000 {
format!("{:.1}K", tokens as f64 / 1000.0)
} else {
format!("{}", tokens)
}
}
fn format_elapsed(duration: Duration) -> String {
let secs = duration.as_secs();
if secs < 60 {
format!("{}s", secs)
} else {
format!("{}m {}s", secs / 60, secs % 60)
}
}
fn main() -> std::io::Result<()> {
let mut agent = AgentAir::new(&MyConfig)?;
let status_bar = StatusBar::with_config(StatusBarConfig {
height: 2,
..Default::default()
})
.with_renderer(|data: &StatusBarData, theme: &Theme| {
// Line 1: Model and context
let context_str = if data.context_limit > 0 {
let pct = (data.context_used as f64 / data.context_limit as f64) * 100.0;
let style = if pct > 80.0 {
Style::default().fg(Color::Yellow)
} else {
theme.status_help
};
Span::styled(
format!(
" {}/{} ({:.0}%)",
format_tokens(data.context_used),
format_tokens(data.context_limit as i64),
pct
),
style,
)
} else {
Span::raw("")
};
let line1 = Line::from(vec![
Span::styled(format!(" {} ", data.model_name), theme.status_model),
Span::raw("|"),
context_str,
]);
// Line 2: Status or hints
let line2 = if let Some(hint) = &data.status_hint {
Line::from(Span::styled(format!(" {}", hint), theme.status_help))
} else if data.is_waiting {
let elapsed = data.waiting_elapsed
.map(format_elapsed)
.unwrap_or_else(|| "0s".to_string());
Line::from(Span::styled(
format!(" Processing... ({})", elapsed),
theme.status_help,
))
} else if data.session_id == 0 {
Line::from(Span::styled(
" No session - /new-session to start",
Style::default().fg(Color::Yellow),
))
} else {
Line::from(Span::styled(
" Ready - Ctrl+D to exit",
theme.status_help,
))
};
vec![line1, line2]
});
agent.set_status_bar(status_bar);
agent.run()
} 