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

StepActionResult
1Logger createdLog file opened, tracing configured
2Config loadedLLMRegistry populated from file or env
3Runtime createdTokio runtime ready for async tasks
4Channels createdTUI-Controller communication ready
5Event handler createdController events route to TUI
6Controller createdLLMController with 6-channel loop
7Cancel token createdShutdown coordination ready
8Registries obtainedUser interaction and permission handling
9Forwarders spawnedRegistry 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(&registry, &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:

ResourceCleanup Mechanism
Log filesLogger guard drop
Tokio runtimeRuntime drop after shutdown
ChannelsSender/receiver drops
Terminalcrossterm cleanup in App::run()
LLM sessionsCancelled via token

Next Steps