Skip to main content

robit_agent/
bootstrap.rs

1//! Bootstrap module — common setup for loading skills and creating tools.
2//!
3//! This module provides reusable functions for frontends (robit-tui, robit-gui, etc.)
4//! to avoid duplicating skill loading and tool creation code.
5
6use std::path::PathBuf;
7use std::sync::Arc;
8
9use robit_ai::config::{resolve_image_provider, resolve_profile, RobitConfig};
10
11use crate::image_gen::ImageGenClient;
12use crate::skill::{load_skills, Skill, SkillRegistry};
13use crate::tool::bash::BashTool;
14use crate::tool::edit::EditTool;
15use crate::tool::find::FindTool;
16use crate::tool::generate_image::GenerateImageTool;
17use crate::tool::grep::GrepTool;
18use crate::tool::load_skill::LoadSkillTool;
19use crate::tool::ls::LsTool;
20use crate::tool::memory::{ForgetTool, ListMemoriesTool, MemorizeTool, RecallTool};
21use crate::tool::query_task::QueryTaskTool;
22use crate::tool::read::ReadTool;
23use crate::tool::search_history::SearchHistoryTool;
24use crate::tool::write::WriteTool;
25use crate::tool::ToolRegistry;
26use crate::SkillLoadError;
27
28// ============================================================================
29// BootstrapResult
30// ============================================================================
31
32/// Result of bootstrapping skills and tools.
33pub struct BootstrapResult {
34    /// The skill registry, ready for use.
35    pub skill_registry: Arc<SkillRegistry>,
36    /// The tool registry, ready for use.
37    pub tool_registry: Arc<ToolRegistry>,
38    /// Total skills loaded (before filtering by enabled_skills).
39    pub total_skills_loaded: usize,
40    /// Any errors that occurred during skill loading (non-fatal).
41    pub skill_load_errors: Vec<SkillLoadError>,
42}
43
44// ============================================================================
45// Bootstrap functions
46// ============================================================================
47
48/// Bootstrap both skills and tools in one call.
49///
50/// This is the main entry point for frontends. It:
51/// 1. Loads skills from global and project directories
52/// 2. Filters skills by config.enabled_skills
53/// 3. Creates SkillRegistry
54/// 4. Creates ToolRegistry with all standard tools
55///
56/// Returns a BootstrapResult with both registries and metadata.
57pub fn bootstrap(
58    config: &RobitConfig,
59    working_dir: &PathBuf,
60    base_tool_names: &[&str],
61) -> BootstrapResult {
62    let (skills, skill_load_errors) = load_all_skills(working_dir);
63    let total_skills_loaded = skills.len();
64
65    let filtered_skills = filter_skills_by_config(skills, config);
66
67    let skill_registry = Arc::new(SkillRegistry::new(filtered_skills, base_tool_names));
68    let tool_registry = Arc::new(create_tools_from_config(config, Arc::clone(&skill_registry)));
69
70    BootstrapResult {
71        skill_registry,
72        tool_registry,
73        total_skills_loaded,
74        skill_load_errors,
75    }
76}
77
78/// Load skills from standard locations (global ~/.robit/skills and project .robit/skills).
79///
80/// Returns (loaded_skills, load_errors).
81pub fn load_all_skills(working_dir: &PathBuf) -> (Vec<Skill>, Vec<SkillLoadError>) {
82    let global_skills_dir = dirs::home_dir().map(|h| h.join(".robit/skills"));
83    let project_skills_dir = Some(working_dir.join(".robit/skills"));
84
85    load_skills(global_skills_dir, project_skills_dir)
86}
87
88/// Filter skills by the enabled_skills list in config, if present.
89pub fn filter_skills_by_config(skills: Vec<Skill>, config: &RobitConfig) -> Vec<Skill> {
90    let enabled_skills = config.app.as_ref().and_then(|a| a.enabled_skills.as_ref());
91
92    match enabled_skills {
93        Some(list) => skills
94            .into_iter()
95            .filter(|s| list.contains(&s.frontmatter.name))
96            .collect(),
97        None => skills,
98    }
99}
100
101/// Create a ToolRegistry with tools filtered by config.enabled_tools.
102///
103/// - If enabled_tools is not specified: all tools are registered
104/// - If enabled_tools is specified: only register tools in the list
105/// - `read`, `load_skill`, and memory tools are always registered (required for basic functionality)
106pub fn create_tools_from_config(
107    config: &RobitConfig,
108    skill_registry: Arc<SkillRegistry>,
109) -> ToolRegistry {
110    let mut tools = ToolRegistry::new();
111    let context_config = config.app.as_ref().and_then(|a| a.context.as_ref());
112    let max_lines = context_config.and_then(|c| c.max_output_lines).unwrap_or(500);
113    let max_bytes = context_config
114        .and_then(|c| c.max_output_bytes)
115        .unwrap_or(51200);
116
117    // Whether the configured default model supports image inputs. The `read`
118    // tool uses this to decide whether to encode image files and to advertise
119    // image support in its description.
120    let supports_images = resolve_profile(config, None)
121        .map(|m| m.supports_images)
122        .unwrap_or(false);
123
124    // Always register read, load_skill, memory, history, and query_task tools
125    // (required for basic functionality / async task visibility)
126    tools.register(ReadTool::new(max_lines, max_bytes, supports_images));
127    tools.register(LoadSkillTool::new(skill_registry));
128    tools.register(MemorizeTool::new());
129    tools.register(RecallTool::new());
130    tools.register(ForgetTool::new());
131    tools.register(ListMemoriesTool::new());
132    tools.register(SearchHistoryTool::new());
133    tools.register(QueryTaskTool::new());
134
135    // Try to build the image generation client. Returns None when no image
136    // providers are configured (the tool is simply not registered in that case).
137    let mut image_client = build_image_client(config);
138
139    // Get enabled tools from config
140    let enabled_tools = config.app.as_ref().and_then(|a| a.enabled_tools.as_ref());
141
142    match enabled_tools {
143        Some(list) => {
144            // Configured: only register specified tools (always available tools already registered)
145            for tool_name in list {
146                match tool_name.as_str() {
147                    "read" => {} // already registered
148                    "load_skill" => {} // already registered
149                    "memorize" => {} // already registered
150                    "recall" => {} // already registered
151                    "forget" => {} // already registered
152                    "list_memories" => {} // already registered
153                    "search_history" => {} // already registered
154                    "query_task" => {} // already registered
155                    "bash" => tools.register(BashTool::new(max_bytes)),
156                    "write" => tools.register(WriteTool::new()),
157                    "edit" => tools.register(EditTool::new()),
158                    "ls" => tools.register(LsTool::new()),
159                    "find" => tools.register(FindTool::new(max_bytes)),
160                    "grep" => tools.register(GrepTool::new(max_lines, max_bytes)),
161                    "generate_image" => match image_client.take() {
162                        Some(client) => tools.register(GenerateImageTool::new(client)),
163                        None => tracing::warn!(
164                            "generate_image listed in enabled_tools but no image_provider \
165                             is configured, skipping"
166                        ),
167                    },
168                    _ => tracing::warn!("Unknown tool in enabled_tools config: {}", tool_name),
169                }
170            }
171        }
172        None => {
173            // Not configured: register all remaining tools
174            tools.register(BashTool::new(max_bytes));
175            tools.register(WriteTool::new());
176            tools.register(EditTool::new());
177            tools.register(LsTool::new());
178            tools.register(FindTool::new(max_bytes));
179            tools.register(GrepTool::new(max_lines, max_bytes));
180            if let Some(client) = image_client.take() {
181                tools.register(GenerateImageTool::new(client));
182            }
183        }
184    }
185
186    tools
187}
188
189/// Build the image generation client from config.
190///
191/// Returns `None` (without warning) when image generation is not configured -
192/// either no image providers are defined, or `default_image_model` is absent.
193/// Both are normal "user doesn't need image generation" states. Returns `None`
194/// with a warning only when configuration is present but invalid (e.g. missing
195/// API key or invalid model reference).
196fn build_image_client(config: &RobitConfig) -> Option<ImageGenClient> {
197    if config.image_providers.is_empty() {
198        return None;
199    }
200    // default_image_model is required; if absent, image generation is
201    // considered disabled (no warning - this is a valid "not using it" state).
202    if config.default_image_model.is_none() {
203        return None;
204    }
205    match resolve_image_provider(config) {
206        Ok(provider) => {
207            tracing::info!(
208                "Image generation provider configured: {}/{} (protocol: {:?}, mode: {:?})",
209                provider.provider_name,
210                provider.model_id,
211                provider.protocol,
212                provider.mode
213            );
214            Some(ImageGenClient::new(provider))
215        }
216        Err(e) => {
217            tracing::warn!("Failed to resolve image generation provider: {}", e);
218            None
219        }
220    }
221}
222
223/// Log any skill load errors as warnings.
224///
225/// Convenience function for frontends to log errors without duplicating code.
226pub fn log_skill_errors(errors: &[SkillLoadError]) {
227    for err in errors {
228        tracing::warn!("Skill load error: {:?}", err);
229    }
230}