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