Agent Lifecycle
This page documents the complete lifecycle of an agent from initialization through shutdown. Understanding these phases is essential for proper resource management and customization.
Lifecycle Overview
┌─────────────────────────────────────────────────────────────────┐
│ INITIALIZATION PHASE │
│ AgentAir::new() │
│ - Logger setup │
│ - Config loading │
│ - Runtime creation │
│ - Channel creation │
│ - Controller creation │
│ - Registry creation │
└─────────────────────────────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ CONFIGURATION PHASE │
│ Builder methods called │
│ - set_layout() │
│ - set_key_handler() │
│ - register_widget() │
│ - register_tools() │
└─────────────────────────────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ STARTUP PHASE │
│ run() called │
│ - Background tasks spawned │
│ - App created with configuration │
│ - Initial session created │
└─────────────────────────────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ RUNNING PHASE │
│ App event loop running │
│ - User input processed │
│ - LLM responses streamed │
│ - Tools executed │
└─────────────────────────────────────────┬───────────────────────┘
│ User quits
▼
┌─────────────────────────────────────────────────────────────────┐
│ SHUTDOWN PHASE │
│ shutdown() called │
│ - Cancel token triggered │
│ - Background tasks stop │
│ - Exit handler called │
│ - Resources cleaned up │
└─────────────────────────────────────────────────────────────────┘
Phase 1: Initialization
The initialization phase occurs in AgentAir::new():
pub fn new<C: AgentConfig>(config: &C) -> io::Result<Self> {
// Step 1: Initialize logging
let logger = Logger::new(config.log_prefix())?;
tracing::info!("Initializing {}", config.name());
// Step 2: Load LLM configuration
let llm_registry = load_config(config);
// Step 3: Create Tokio runtime
let runtime = Runtime::new().map_err(|e| {
io::Error::new(io::ErrorKind::Other, format!("Runtime error: {}", e))
})?;
// Step 4: Create communication channels
let (to_controller_tx, to_controller_rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE);
let (from_controller_tx, from_controller_rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE);
// Step 5: Create event handler for controller
let ui_tx = from_controller_tx.clone();
let event_handler = Box::new(move |event: ControllerEvent| {
let msg = convert_controller_event_to_ui_message(event);
if let Err(e) = ui_tx.try_send(msg) {
tracing::warn!("Failed to send event to UI: {}", e);
}
});
// Step 6: Create LLMController
let controller = Arc::new(LLMController::new(Some(event_handler)));
// Step 7: Create cancellation token
let cancel_token = CancellationToken::new();
// Step 8: Create registries
let user_interaction_registry = controller.user_interaction_registry();
let permission_registry = controller.permission_registry();
// Step 9: Spawn registry event forwarders
let (interaction_rx, permission_rx) = setup_registry_forwarders(&runtime, ...);
Ok(Self {
logger,
name: config.name().to_string(),
version: String::new(),
runtime,
controller,
llm_registry,
to_controller_tx,
to_controller_rx: Some(to_controller_rx),
from_controller_tx,
from_controller_rx: Some(from_controller_rx),
cancel_token,
user_interaction_registry,
permission_registry,
// ... default None values for optional configuration
})
}
What Happens at Initialization
| Step | Action | Result |
|---|---|---|
| 1 | Logger created | Log file opened, tracing configured |
| 2 | Config loaded | LLMRegistry populated from file or env |
| 3 | Runtime created | Tokio runtime ready for async tasks |
| 4 | Channels created | TUI-Controller communication ready |
| 5 | Event handler created | Controller events route to TUI |
| 6 | Controller created | LLMController with 6-channel loop |
| 7 | Cancel token created | Shutdown coordination ready |
| 8 | Registries obtained | User interaction and permission handling |
| 9 | Forwarders spawned | Registry events route to TUI |
Phase 2: Configuration
After initialization, builder methods configure the agent:
let mut core = AgentAir::new(&MyConfig)?;
// Optional configuration
core.set_version(env!("CARGO_PKG_VERSION"))
.set_layout(LayoutTemplate::with_sidebar("files", 30))
.set_key_bindings(KeyBindings::vim())
.set_exit_handler(SaveOnExitHandler::new());
// Widget registration
core.register_widget(PermissionPanel::new())
.register_widget(QuestionPanel::new());
// Tool registration (required for agentic behavior)
core.register_tools(|registry, user_reg, perm_reg| {
register_all_tools(registry, user_reg, perm_reg)
})?;
Configuration Storage
Configuration is stored but not applied:
pub fn set_layout(&mut self, template: LayoutTemplate) -> &mut Self {
self.layout_template = Some(template); // Stored for later
self
}
Tool Registration
Tools are registered immediately with the controller:
pub fn register_tools<F>(&mut self, f: F) -> Result<(), AgentError> {
let registry = self.controller.tool_registry();
let tool_defs = f(®istry, &self.user_interaction_registry, &self.permission_registry)?;
self.tool_definitions = tool_defs; // Stored for session creation
Ok(())
}
Phase 3: Startup
The startup phase begins when run() is called:
pub fn run(&mut self) -> io::Result<()> {
// Step 1: Start background tasks
self.start_background_tasks();
// Step 2: Create App with basic configuration
let mut app = App::new(
self.name.clone(),
self.version.clone(),
self.to_controller_tx.clone(),
self.controller.clone(),
);
// Step 3: Apply stored configuration
if let Some(template) = self.layout_template.take() {
app.set_layout(template);
}
if let Some(handler) = self.key_handler.take() {
app.set_key_handler(handler);
}
if let Some(exit_handler) = self.exit_handler.take() {
app.set_exit_handler(exit_handler);
}
// ... more configuration
// Step 4: Wire up channels
if let Some(rx) = self.from_controller_rx.take() {
app.set_controller_receiver(rx);
}
// Step 5: Register widgets
for widget in self.widgets_to_register.drain(..) {
app.register_widget(widget);
}
// Step 6: Create initial session (if LLM configured)
if self.llm_registry.is_some() {
let (session_id, model, context_limit) = self.create_initial_session()?;
app.set_session(session_id, &model, context_limit);
}
// Step 7: Run the TUI (blocking)
let result = app.run();
// Step 8: Cleanup on exit
self.shutdown();
result
}
Background Tasks
start_background_tasks() spawns long-running tasks:
pub fn start_background_tasks(&mut self) {
// Task 1: Controller event loop
let controller = self.controller.clone();
self.runtime.spawn(async move {
controller.start().await;
});
// Task 2: Input router (TUI -> Controller)
if let Some(rx) = self.to_controller_rx.take() {
let router = InputRouter::new(
self.controller.clone(),
rx,
self.cancel_token.clone(),
);
self.runtime.spawn(async move {
router.run().await;
});
}
}
Initial Session Creation
pub fn create_initial_session(&mut self) -> Result<(i64, String, i32), AgentError> {
let registry = self.llm_registry.as_ref()
.ok_or_else(|| AgentError::NoConfiguration("No LLM config".into()))?;
let config = registry.get_default()
.ok_or_else(|| AgentError::NoConfiguration("No default provider".into()))?;
let session_id = self.runtime.block_on(
Self::create_session_internal(&self.controller, config.clone(), &self.tool_definitions)
)?;
Ok((session_id, config.model, config.context_limit))
}
Phase 4: Running
During the running phase, the TUI event loop processes events:
// Inside App::run()
loop {
// 1. Check for terminal events (key presses, resize)
if crossterm::event::poll(Duration::from_millis(100))? {
let event = crossterm::event::read()?;
if let Event::Key(key) = event {
if self.handle_key_event(key)? == KeyResult::Quit {
break;
}
}
}
// 2. Check for controller messages
while let Ok(msg) = self.controller_rx.try_recv() {
self.handle_controller_message(msg);
}
// 3. Render the UI
self.terminal.draw(|frame| {
self.render(frame);
})?;
}
Event Flow During Running
User Input (keyboard)
↓
App::handle_key_event()
↓
ControllerInputPayload created
↓
to_controller_tx.send()
↓
InputRouter receives
↓
controller.send_input()
↓
LLMController processes
↓
LLMSession sends to API
↓
Response streamed
↓
ControllerEvent emitted
↓
UiMessage sent to App
↓
ChatView updated
↓
Terminal rendered
Phase 5: Shutdown
Shutdown is triggered when the user quits:
pub fn shutdown(&self) {
tracing::info!("{} shutting down", self.name);
// Step 1: Cancel the cancellation token
self.cancel_token.cancel();
// Step 2: Wait for controller shutdown
let controller = self.controller.clone();
self.runtime.block_on(async move {
controller.shutdown().await;
});
tracing::info!("{} shutdown complete", self.name);
// Logger guard dropped here, flushing remaining logs
}
Shutdown Sequence
User quits (Ctrl+Q, /quit, etc.)
↓
App::run() returns
↓
AgentAir::shutdown() called
↓
cancel_token.cancel()
↓
Background tasks check cancellation
↓
├─ InputRouter::run() breaks
├─ LLMController::start() breaks
└─ Registry forwarders break
↓
controller.shutdown() awaited
↓
Sessions stopped
↓
Channels closed
↓
Exit handler called (if configured)
↓
Logger flushed on drop
↓
Process exits
Exit Handler
If an exit handler was configured, it runs during shutdown:
impl ExitHandler for SaveSessionHandler {
fn on_exit(&self, ctx: &ExitContext) {
// Save conversation history, cleanup temp files, etc.
if let Some(session) = ctx.current_session() {
save_session_to_file(session);
}
}
}
State Machine
The agent progresses through discrete states:
enum AgentState {
Uninitialized, // Before new()
Initialized, // After new(), before run()
Running, // During run()
ShuttingDown, // During shutdown()
Terminated, // After shutdown completes
}
State transitions:
Uninitialized ──new()──> Initialized ──run()──> Running
│
│ user quits
▼
Terminated <──── ShuttingDown
Error Handling During Lifecycle
Initialization Errors
let core = AgentAir::new(&MyConfig)?; // Returns io::Result
Initialization can fail due to:
- Logger file creation failure
- Tokio runtime creation failure
Configuration Errors
core.register_tools(register_fn)?; // Returns Result<(), AgentError>
Tool registration can fail due to:
- Invalid tool definitions
- Registration callback errors
Runtime Errors
core.run()?; // Returns io::Result
Runtime can fail due to:
- Terminal initialization failure
- Session creation failure (no LLM config)
- TUI rendering errors
Resource Management
Resources are cleaned up automatically:
| Resource | Cleanup Mechanism |
|---|---|
| Log files | Logger guard drop |
| Tokio runtime | Runtime drop after shutdown |
| Channels | Sender/receiver drops |
| Terminal | crossterm cleanup in App::run() |
| LLM sessions | Cancelled via token |
Next Steps
- Logger Setup - Logging configuration details
- LLMController - Controller internals
- Builder Pattern - Configuration methods
