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(®istry, &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(®istry).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:
| Option | Default |
|---|---|
| Layout | Standard layout |
| Key Handler | Default handler with standard bindings |
| Exit Handler | None (no cleanup) |
| Commands | Built-in commands (quit, help, clear, etc.) |
| Status Bar | Default status bar with token counts |
| Conversation View | Default ChatView |
Design Rationale
The builder pattern provides:
- Flexibility: Configure only what you need
- Discoverability: IDE autocompletion shows available options
- Safety: Type-checked configuration at compile time
- Clarity: Configuration is explicit and readable
- Defaults: Sensible behavior without configuration
Next Steps
- AgentConfig Internals - Configuration loading details
- Agent Lifecycle - When configuration is applied
- Tool Registration - Registering tools in detail
