Builder Pattern

AgentAir uses a fluent builder pattern for configuration. This allows agents to be customized incrementally before being run, with sensible defaults for unconfigured options.

Overview

The builder pattern enables method chaining for configuration:

let mut core = AgentAir::new(&MyConfig)?;

core.set_version(env!("CARGO_PKG_VERSION"))
    .set_layout(LayoutTemplate::with_sidebar("files", 30))
    .set_key_bindings(KeyBindings::vim())
    .set_exit_handler(SaveOnExitHandler::new())
    .register_widget(PermissionPanel::new())
    .register_widget(QuestionPanel::new());

core.register_tools(register_all_tools)?;
core.run()

All configuration is stored and applied when run() is called.

Configuration Storage

AgentAir stores configuration in optional fields:

pub struct AgentAir {
    // Configuration fields
    layout_template: Option<LayoutTemplate>,
    key_handler: Option<Box<dyn KeyHandler>>,
    exit_handler: Option<Box<dyn ExitHandler>>,
    commands: Option<Vec<Box<dyn SlashCommand>>>,
    command_extension: Option<Box<dyn Any + Send>>,
    custom_status_bar: Option<Box<dyn Widget>>,
    hide_status_bar: bool,
    conversation_factory: Option<ConversationViewFactory>,
    widgets_to_register: Vec<Box<dyn Widget>>,
    tool_definitions: Vec<LLMTool>,
    // ...
}

When run() is called, these values are passed to the App for TUI initialization.

Builder Methods

Layout Configuration

set_layout()

Sets the TUI layout template:

pub fn set_layout(&mut self, template: LayoutTemplate) -> &mut Self {
    self.layout_template = Some(template);
    self
}

Usage:

// Standard layout with chat and status bar
core.set_layout(LayoutTemplate::standard());

// Layout with sidebar panel
core.set_layout(LayoutTemplate::with_sidebar("files", 30));

// Minimal layout without decorations
core.set_layout(LayoutTemplate::minimal());

// Split layout with two panels
core.set_layout(LayoutTemplate::split("left", "right", 50));

Key Handling

set_key_handler()

Sets a custom key handler implementing the KeyHandler trait:

pub fn set_key_handler<H: KeyHandler + 'static>(&mut self, handler: H) -> &mut Self {
    self.key_handler = Some(Box::new(handler));
    self
}

Usage:

core.set_key_handler(VimKeyHandler::new());
core.set_key_handler(CustomKeyHandler::with_bindings(my_bindings));

set_key_bindings()

Convenience method using DefaultKeyHandler with custom bindings:

pub fn set_key_bindings(&mut self, bindings: KeyBindings) -> &mut Self {
    self.key_handler = Some(Box::new(DefaultKeyHandler::new(bindings)));
    self
}

Usage:

core.set_key_bindings(KeyBindings::vim());
core.set_key_bindings(KeyBindings::emacs());
core.set_key_bindings(KeyBindings::minimal());

Exit Handling

set_exit_handler()

Sets a handler for cleanup before exit:

pub fn set_exit_handler<H: ExitHandler + 'static>(&mut self, handler: H) -> &mut Self {
    self.exit_handler = Some(Box::new(handler));
    self
}

Usage:

core.set_exit_handler(SaveSessionHandler::new());

Command Configuration

set_commands()

Replaces the default slash commands:

pub fn set_commands(&mut self, commands: Vec<Box<dyn SlashCommand>>) -> &mut Self {
    self.commands = Some(commands);
    self
}

Usage:

let commands = CommandRegistry::with_defaults()
    .add(CustomCommand::new("deploy", "Deploy app", handler))
    .remove("quit")
    .build();

core.set_commands(commands);

set_command_extension()

Sets data available to commands via CommandContext:

pub fn set_command_extension<T: Any + Send + 'static>(&mut self, ext: T) -> &mut Self {
    self.command_extension = Some(Box::new(ext));
    self
}

Usage:

struct MyContext {
    api_url: String,
    auth_token: String,
}

core.set_command_extension(MyContext {
    api_url: "https://api.example.com".to_string(),
    auth_token: env::var("AUTH_TOKEN").unwrap(),
});

Conversation View

set_conversation_factory()

Sets a factory for creating custom conversation views:

pub fn set_conversation_factory<F>(&mut self, factory: F) -> &mut Self
where
    F: Fn() -> Box<dyn ConversationView> + Send + Sync + 'static,
{
    self.conversation_factory = Some(Box::new(factory));
    self
}

Usage:

core.set_conversation_factory(|| {
    Box::new(ChatView::new()
        .with_title("My Agent")
        .with_initial_content(welcome_renderer))
});

Widget Registration

register_widget()

Registers a widget with the TUI:

pub fn register_widget<W: Widget + 'static>(&mut self, widget: W) -> &mut Self {
    self.widgets_to_register.push(Box::new(widget));
    self
}

Unlike other builder methods, widgets accumulate rather than replace:

core.register_widget(PermissionPanel::new())
    .register_widget(QuestionPanel::new())
    .register_widget(FileBrowserWidget::new());

Status Bar

set_status_bar()

Replaces the default status bar:

pub fn set_status_bar<W: Widget + 'static>(&mut self, status_bar: W) -> &mut Self {
    self.custom_status_bar = Some(Box::new(status_bar));
    self
}

hide_status_bar()

Removes the status bar entirely:

pub fn hide_status_bar(&mut self) -> &mut Self {
    self.hide_status_bar = true;
    self
}

Identity

set_version()

Sets the agent version for display:

pub fn set_version(&mut self, version: impl Into<String>) {
    self.version = version.into();
}

Usage:

core.set_version(env!("CARGO_PKG_VERSION"));
core.set_version("1.2.3-beta");

Tool Registration

Tool registration uses a callback pattern rather than simple builder methods:

register_tools()

Synchronous tool registration:

pub fn register_tools<F>(&mut self, f: F) -> Result<(), AgentError>
where
    F: FnOnce(
        &Arc<ToolRegistry>,
        &Arc<UserInteractionRegistry>,
        &Arc<PermissionRegistry>,
    ) -> Result<Vec<LLMTool>, String>,
{
    let registry = self.controller.tool_registry();
    let user_reg = self.user_interaction_registry.clone();
    let perm_reg = self.permission_registry.clone();

    let tool_defs = f(&registry, &user_reg, &perm_reg)
        .map_err(AgentError::ToolRegistration)?;

    self.tool_definitions = tool_defs;
    Ok(())
}

Usage:

core.register_tools(|registry, user_reg, perm_reg| {
    // Register tool handlers
    register_file_tools(registry)?;
    register_search_tools(registry)?;

    // Return LLM tool definitions
    Ok(tool_definitions())
})?;

register_tools_async()

Asynchronous tool registration:

pub fn register_tools_async<F, Fut>(&mut self, f: F) -> Result<(), AgentError>
where
    F: FnOnce(
        Arc<ToolRegistry>,
        Arc<UserInteractionRegistry>,
        Arc<PermissionRegistry>,
    ) -> Fut,
    Fut: Future<Output = Result<Vec<LLMTool>, String>>,
{
    let registry = self.controller.tool_registry();
    let user_reg = self.user_interaction_registry.clone();
    let perm_reg = self.permission_registry.clone();

    let tool_defs = self.runtime.block_on(f(registry, user_reg, perm_reg))
        .map_err(AgentError::ToolRegistration)?;

    self.tool_definitions = tool_defs;
    Ok(())
}

Usage:

core.register_tools_async(|registry, user_reg, perm_reg| async move {
    register_tools_with_validation(&registry).await?;
    Ok(tool_definitions())
})?;

Method Chaining

Most builder methods return &mut Self enabling fluent chaining:

core.set_layout(LayoutTemplate::standard())
    .set_key_bindings(KeyBindings::vim())
    .set_exit_handler(handler)
    .register_widget(widget1)
    .register_widget(widget2);

Methods that can fail return Result:

core.register_tools(register_fn)?;  // Must handle error
core.run()?;                         // Must handle error

Configuration Application

Configuration is applied when run() is called:

pub fn run(&mut self) -> io::Result<()> {
    self.start_background_tasks();

    // Create App with stored configuration
    let mut app = App::new(
        self.name.clone(),
        self.version.clone(),
        self.to_controller_tx.clone(),
        self.controller.clone(),
    );

    // Apply 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

    // Register widgets
    for widget in self.widgets_to_register.drain(..) {
        app.register_widget(widget);
    }

    // Run the TUI
    app.run()
}

Default Values

Unconfigured options use sensible defaults:

OptionDefault
LayoutStandard layout
Key HandlerDefault handler with standard bindings
Exit HandlerNone (no cleanup)
CommandsBuilt-in commands (quit, help, clear, etc.)
Status BarDefault status bar with token counts
Conversation ViewDefault ChatView

Design Rationale

The builder pattern provides:

  1. Flexibility: Configure only what you need
  2. Discoverability: IDE autocompletion shows available options
  3. Safety: Type-checked configuration at compile time
  4. Clarity: Configuration is explicit and readable
  5. Defaults: Sensible behavior without configuration

Next Steps