Agent Builder
The AgentAir builder provides methods for registering tools, widgets, and other components before running the agent. Registration happens during setup, before calling run(), and determines what capabilities are available during execution. Understanding these registration methods is key to extending agent functionality.
The builder pattern used by AgentAir allows fluent configuration through method chaining. Each registration method returns &mut Self, so you can configure multiple aspects in a single chain. Tools are registered with the LLM so it knows what operations are available, while widgets are registered with the TUI to extend the user interface.
Tool Registration
Tools are registered through the ToolRegistry and the register_tools() method on AgentAir. The registry provides thread-safe storage and lookup of tools, while the registration methods handle integration with LLM sessions.
ToolRegistry
The ToolRegistry is a thread-safe registry for managing available tools:
pub struct ToolRegistry {
tools: RwLock<HashMap<String, Arc<dyn Executable>>>,
}
It uses a tokio RwLock to allow concurrent reads while ensuring exclusive writes.
Registry Methods
impl ToolRegistry {
/// Create a new empty registry
pub fn new() -> Self;
/// Register a tool (returns error if name already exists)
pub async fn register(&self, tool: Arc<dyn Executable>) -> Result<(), RegistryError>;
/// Get a tool by name
pub async fn get(&self, name: &str) -> Option<Arc<dyn Executable>>;
/// Check if a tool exists
pub async fn has(&self, name: &str) -> bool;
/// List all tool names
pub async fn list(&self) -> Vec<String>;
/// Get all tools
pub async fn get_all(&self) -> Vec<Arc<dyn Executable>>;
/// Remove a tool by name
pub async fn remove(&self, name: &str);
/// Get the number of registered tools
pub async fn len(&self) -> usize;
/// Check if the registry is empty
pub async fn is_empty(&self) -> bool;
}
Registering with AgentAir
Use register_tools() to register tools when building your agent:
impl AgentAir {
pub fn register_tools<F>(&mut self, f: F) -> Result<(), AgentError>
where
F: FnOnce(
&Arc<ToolRegistry>,
&Arc<UserInteractionRegistry>,
&Arc<PermissionRegistry>,
) -> Result<Vec<LLMTool>, String>;
}
The callback receives three registries:
&Arc<ToolRegistry>- For registering Executable implementations&Arc<UserInteractionRegistry>- For tools that ask user questions&Arc<PermissionRegistry>- For tools that request permissions
The callback returns Vec<LLMTool> which are the tool definitions sent to the LLM.
Synchronous Registration
use agent_air::AgentAir;
use agent_air::controller::tools::{ToolRegistry, LLMTool};
use std::sync::Arc;
let mut agent = AgentAir::new(&MyConfig)?;
agent.register_tools(|registry, user_reg, perm_reg| {
let my_tool = Arc::new(MyTool::new());
// Register with the runtime
tokio::runtime::Handle::current().block_on(async {
registry.register(my_tool.clone()).await
}).map_err(|e| e.to_string())?;
// Return the LLM tool definition
Ok(vec![my_tool.to_llm_tool()])
})?;
Async Registration
For tools that require async initialization, use register_tools_async():
impl AgentAir {
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>>;
}
Example:
agent.register_tools_async(|registry, user_reg, perm_reg| async move {
// Async initialization
let client = HttpClient::connect("https://api.example.com").await?;
let my_tool = Arc::new(ApiTool::new(client));
registry.register(my_tool.clone()).await
.map_err(|e| e.to_string())?;
Ok(vec![my_tool.to_llm_tool()])
})?;
Tool Definition Flow
When tools are registered:
- The callback registers
Executableimplementations withToolRegistry - The callback returns
Vec<LLMTool>definitions - AgentAir stores these definitions
- When a session is created, tools are set on the session via
session.set_tools() - The LLM receives tool definitions and can request their execution
Complete Tool Example
use agent_air::AgentAir;
use agent_air::controller::tools::{
Executable, ToolContext, ToolType, LLMTool, ToolRegistry,
};
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
struct WeatherTool {
api_key: String,
}
impl Executable for WeatherTool {
fn name(&self) -> &str { "get_weather" }
fn description(&self) -> &str { "Get weather for a location" }
fn input_schema(&self) -> &str {
r#"{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}"#
}
fn tool_type(&self) -> ToolType { ToolType::ApiCall }
fn execute(
&self,
_ctx: ToolContext,
input: HashMap<String, serde_json::Value>,
) -> Pin<Box<dyn Future<Output = Result<String, String>> + Send>> {
let api_key = self.api_key.clone();
Box::pin(async move {
let location = input.get("location")
.and_then(|v| v.as_str())
.ok_or("Missing location")?;
Ok(format!("Weather in {}: Sunny", location))
})
}
}
fn main() -> std::io::Result<()> {
let mut agent = AgentAir::new(&MyConfig)?;
agent.register_tools_async(|registry, _user_reg, _perm_reg| async move {
let weather = Arc::new(WeatherTool {
api_key: std::env::var("WEATHER_API_KEY")
.map_err(|_| "Missing WEATHER_API_KEY".to_string())?,
});
registry.register(weather.clone()).await
.map_err(|e| e.to_string())?;
Ok(vec![weather.to_llm_tool()])
})?;
agent.run()
}
RegistryError
Registration can fail with:
#[derive(Error, Debug)]
pub enum RegistryError {
#[error("Tool with name {0:?} already exists")]
DuplicateTool(String),
}
Accessing the Registry at Runtime
The registry is accessible through the controller:
let registry = agent.controller().tool_registry();
// Check if a tool exists
let has_weather = registry.has("get_weather").await;
// List all tools
let tool_names = registry.list().await;
// Remove a tool
registry.remove("deprecated_tool").await;
Widget Registration
Widgets are registered with AgentAir to add custom UI components to the TUI. Registered widgets participate in the rendering pipeline and can handle key events based on their priority. The widget system allows extending the interface with custom panels, overlays, and interactive elements.
The register_widget Method
Register widgets using register_widget() on AgentAir:
impl AgentAir {
pub fn register_widget<W: Widget>(&mut self, widget: W) -> &mut Self;
}
This method returns &mut Self, allowing method chaining:
use agent_air::AgentAir;
use agent_air::tui::widgets::{PermissionPanel, QuestionPanel};
let mut agent = AgentAir::new(&MyConfig)?;
agent
.register_widget(PermissionPanel::new())
.register_widget(QuestionPanel::new());
agent.run()
Standard Widget IDs
The framework defines standard widget IDs:
pub mod widget_ids {
// Core widgets (always present)
pub const CHAT_VIEW: &str = "chat_view";
pub const TEXT_INPUT: &str = "text_input";
// Registerable widgets
pub const PERMISSION_PANEL: &str = "permission_panel";
pub const QUESTION_PANEL: &str = "question_panel";
pub const SESSION_PICKER: &str = "session_picker";
pub const SLASH_POPUP: &str = "slash_popup";
pub const THEME_PICKER: &str = "theme_picker";
pub const STATUS_BAR: &str = "status_bar";
}
Use these constants when implementing custom widgets or checking widget presence.
Widget Trait Requirements
Registered widgets must implement the Widget trait:
pub trait Widget: Send + 'static {
fn id(&self) -> &'static str;
fn priority(&self) -> u8 { 100 }
fn is_active(&self) -> bool;
fn handle_key(&mut self, key: KeyEvent, ctx: &WidgetKeyContext) -> WidgetKeyResult;
fn render(&mut self, frame: &mut Frame, area: Rect, theme: &Theme);
fn required_height(&self, available: u16) -> u16 { 0 }
fn blocks_input(&self) -> bool { false }
fn is_overlay(&self) -> bool { false }
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn into_any(self: Box<Self>) -> Box<dyn Any>;
}
Checking Widget Presence
The App provides methods to check if widgets are registered:
if app.has_widget(widget_ids::PERMISSION_PANEL) {
// Permission panel is available
}
Accessing Registered Widgets
Access widgets by ID with type-safe downcasting:
use agent_air::tui::widgets::{widget_ids, PermissionPanel};
// Immutable access
if let Some(panel) = app.widget::<PermissionPanel>(widget_ids::PERMISSION_PANEL) {
let is_active = panel.is_active();
}
// Mutable access
if let Some(panel) = app.widget_mut::<PermissionPanel>(widget_ids::PERMISSION_PANEL) {
panel.set_options(options);
}
Priority-Based Key Handling
Widgets are checked for key handling in priority order (highest first). When a key event occurs:
- App iterates widgets sorted by priority descending
- Each active widget’s
handle_key()is called - If a widget returns
HandledorAction, processing stops - Otherwise, the next widget is checked
impl Widget for MyWidget {
fn priority(&self) -> u8 {
200 // Higher priority, checked before widgets with priority 100
}
fn handle_key(&mut self, key: KeyEvent, ctx: &WidgetKeyContext) -> WidgetKeyResult {
if self.is_active() && key.code == KeyCode::Escape {
self.close();
return WidgetKeyResult::Handled;
}
WidgetKeyResult::NotHandled
}
}
Overlay Widgets
Widgets that return true from is_overlay() are rendered on top of all other content:
impl Widget for FullScreenPicker {
fn is_overlay(&self) -> bool {
true // Rendered last, on top of everything
}
}
Blocking Input
Widgets that return true from blocks_input() prevent text input while active:
impl Widget for ModalDialog {
fn blocks_input(&self) -> bool {
self.is_active() // Block text input when dialog is open
}
}
Complete Widget Example
use agent_air::AgentAir;
use agent_air::tui::widgets::{
Widget, WidgetKeyContext, WidgetKeyResult, widget_ids,
PermissionPanel, QuestionPanel,
};
use crossterm::event::{KeyEvent, KeyCode};
use ratatui::{layout::Rect, Frame};
use std::any::Any;
struct NotificationWidget {
message: Option<String>,
}
impl Widget for NotificationWidget {
fn id(&self) -> &'static str { "notification" }
fn priority(&self) -> u8 { 150 }
fn is_active(&self) -> bool { self.message.is_some() }
fn handle_key(&mut self, key: KeyEvent, _ctx: &WidgetKeyContext) -> WidgetKeyResult {
if self.is_active() && matches!(key.code, KeyCode::Enter | KeyCode::Escape) {
self.message = None;
return WidgetKeyResult::Handled;
}
WidgetKeyResult::NotHandled
}
fn render(&mut self, frame: &mut Frame, area: Rect, theme: &Theme) {
if let Some(msg) = &self.message {
// Render notification...
}
}
fn required_height(&self, _available: u16) -> u16 {
if self.message.is_some() { 3 } else { 0 }
}
fn as_any(&self) -> &dyn Any { self }
fn as_any_mut(&mut self) -> &mut dyn Any { self }
fn into_any(self: Box<Self>) -> Box<dyn Any> { self }
}
fn main() -> std::io::Result<()> {
let mut agent = AgentAir::new(&MyConfig)?;
// Register built-in widgets
agent
.register_widget(PermissionPanel::new())
.register_widget(QuestionPanel::new());
// Register custom widget
agent.register_widget(NotificationWidget { message: None });
agent.run()
}
Widget Registration Order
Widgets are stored in registration order but processed by priority. Register widgets before calling run():
let mut agent = AgentAir::new(&config)?;
// All registrations must happen before run()
agent.register_widget(WidgetA::new());
agent.register_widget(WidgetB::new());
agent.register_widget(WidgetC::new());
// run() consumes the widget list
agent.run()
Builder Method Reference
| Method | Description |
|---|---|
register_tools(f) | Register tools synchronously |
register_tools_async(f) | Register tools with async initialization |
register_widget(widget) | Register a TUI widget |
set_commands(commands) | Set slash commands (see Slash Commands) |
set_command_extension(ext) | Set extension data for commands |
set_conversation_factory(f) | Set conversation view factory (see Conversation Customization) |
set_exit_handler(handler) | Set exit handler (see Agent Lifecycle) |
set_status_bar(bar) | Set custom status bar |
set_key_handler(handler) | Set key handler (see Key Handlers) |
