1use std::path::PathBuf;
7use std::sync::Arc;
8
9use robit_ai::config::{
10 resolve_image_provider, resolve_memory_mode, resolve_profile, MemoryMode, RobitConfig,
11};
12
13use crate::image_gen::ImageGenClient;
14use crate::skill::{load_skills, Skill, SkillRegistry};
15use crate::tool::bash::BashTool;
16use crate::tool::edit::EditTool;
17use crate::tool::find::FindTool;
18use crate::tool::generate_image::GenerateImageTool;
19use crate::tool::grep::GrepTool;
20use crate::tool::load_skill::LoadSkillTool;
21use crate::tool::ls::LsTool;
22use crate::tool::memory::{ForgetTool, ListMemoriesTool, MemorizeTool, RecallTool};
23use crate::tool::query_task::QueryTaskTool;
24use crate::tool::read::ReadTool;
25use crate::tool::search_history::SearchHistoryTool;
26use crate::tool::write::WriteTool;
27use crate::tool::ToolRegistry;
28use crate::SkillLoadError;
29
30pub struct BootstrapResult {
36 pub skill_registry: Arc<SkillRegistry>,
38 pub tool_registry: Arc<ToolRegistry>,
40 pub total_skills_loaded: usize,
42 pub skill_load_errors: Vec<SkillLoadError>,
44}
45
46pub fn bootstrap(
60 config: &RobitConfig,
61 working_dir: &PathBuf,
62 base_tool_names: &[&str],
63) -> BootstrapResult {
64 let (skills, skill_load_errors) = load_all_skills(working_dir);
65 let total_skills_loaded = skills.len();
66
67 let filtered_skills = filter_skills_by_config(skills, config);
68
69 let skill_registry = Arc::new(SkillRegistry::new(filtered_skills, base_tool_names));
70 let tool_registry = Arc::new(create_tools_from_config(config, Arc::clone(&skill_registry)));
71
72 BootstrapResult {
73 skill_registry,
74 tool_registry,
75 total_skills_loaded,
76 skill_load_errors,
77 }
78}
79
80pub fn load_all_skills(working_dir: &PathBuf) -> (Vec<Skill>, Vec<SkillLoadError>) {
84 let global_skills_dir = dirs::home_dir().map(|h| h.join(".robit/skills"));
85 let project_skills_dir = Some(working_dir.join(".robit/skills"));
86
87 load_skills(global_skills_dir, project_skills_dir)
88}
89
90pub fn filter_skills_by_config(skills: Vec<Skill>, config: &RobitConfig) -> Vec<Skill> {
92 let enabled_skills = config.app.as_ref().and_then(|a| a.enabled_skills.as_ref());
93
94 match enabled_skills {
95 Some(list) => skills
96 .into_iter()
97 .filter(|s| list.contains(&s.frontmatter.name))
98 .collect(),
99 None => skills,
100 }
101}
102
103pub fn create_tools_from_config(
111 config: &RobitConfig,
112 skill_registry: Arc<SkillRegistry>,
113) -> ToolRegistry {
114 let mut tools = ToolRegistry::new();
115 let context_config = config.app.as_ref().and_then(|a| a.context.as_ref());
116 let max_lines = context_config.and_then(|c| c.max_output_lines).unwrap_or(500);
117 let max_bytes = context_config
118 .and_then(|c| c.max_output_bytes)
119 .unwrap_or(51200);
120
121 let supports_images = resolve_profile(config, None)
125 .map(|m| m.supports_images)
126 .unwrap_or(false);
127
128 let max_image_dimension = context_config
131 .and_then(|c| c.max_image_dimension)
132 .unwrap_or(1024);
133
134 let memory_mode = resolve_memory_mode(config);
138 let memory_tools_enabled = memory_mode == MemoryMode::Tools;
139
140 tools.register(ReadTool::new(
143 max_lines,
144 max_bytes,
145 supports_images,
146 max_image_dimension,
147 ));
148 tools.register(LoadSkillTool::new(skill_registry));
149 if memory_tools_enabled {
150 tools.register(MemorizeTool::new());
151 tools.register(RecallTool::new());
152 tools.register(ForgetTool::new());
153 tools.register(ListMemoriesTool::new());
154 }
155 tools.register(SearchHistoryTool::new());
156 tools.register(QueryTaskTool::new());
157
158 let mut image_client = build_image_client(config);
161
162 let enabled_tools = config.app.as_ref().and_then(|a| a.enabled_tools.as_ref());
164
165 match enabled_tools {
166 Some(list) => {
167 for tool_name in list {
169 match tool_name.as_str() {
170 "read" => {} "load_skill" => {} "memorize" | "recall" | "forget" | "list_memories" => {
173 if !memory_tools_enabled {
175 tracing::warn!(
176 "memory tool {} listed in enabled_tools but memory_mode \
177 != \"tools\", skipping",
178 tool_name
179 );
180 }
181 }
182 "search_history" => {} "query_task" => {} "bash" => tools.register(BashTool::new(max_bytes)),
185 "write" => tools.register(WriteTool::new()),
186 "edit" => tools.register(EditTool::new()),
187 "ls" => tools.register(LsTool::new()),
188 "find" => tools.register(FindTool::new(max_bytes)),
189 "grep" => tools.register(GrepTool::new(max_lines, max_bytes)),
190 "generate_image" => match image_client.take() {
191 Some(client) => tools.register(GenerateImageTool::new(client)),
192 None => tracing::warn!(
193 "generate_image listed in enabled_tools but no image_provider \
194 is configured, skipping"
195 ),
196 },
197 _ => tracing::warn!("Unknown tool in enabled_tools config: {}", tool_name),
198 }
199 }
200 }
201 None => {
202 tools.register(BashTool::new(max_bytes));
204 tools.register(WriteTool::new());
205 tools.register(EditTool::new());
206 tools.register(LsTool::new());
207 tools.register(FindTool::new(max_bytes));
208 tools.register(GrepTool::new(max_lines, max_bytes));
209 if let Some(client) = image_client.take() {
210 tools.register(GenerateImageTool::new(client));
211 }
212 }
213 }
214
215 if memory_mode == MemoryMode::File {
220 let missing = missing_file_memory_tools(&tools.tool_names());
221 if !missing.is_empty() {
222 tracing::warn!(
223 "memory_mode = \"file\" but tool(s) [{}] are not enabled; the agent \
224 cannot persist memory files (the system prompt instructs it to use \
225 write/edit). Enable them, or set memory_mode = \"off\" to hide the \
226 mechanism.",
227 missing.join(", ")
228 );
229 }
230 }
231
232 tools
233}
234
235const FILE_MEMORY_REQUIRED_TOOLS: [&str; 2] = ["write", "edit"];
237
238fn missing_file_memory_tools(names: &[&str]) -> Vec<&'static str> {
241 FILE_MEMORY_REQUIRED_TOOLS
242 .into_iter()
243 .filter(|tool| !names.contains(tool))
244 .collect()
245}
246
247fn build_image_client(config: &RobitConfig) -> Option<ImageGenClient> {
255 if config.image_providers.is_empty() {
256 return None;
257 }
258 if config.default_image_model.is_none() {
261 return None;
262 }
263 match resolve_image_provider(config) {
264 Ok(provider) => {
265 tracing::info!(
266 "Image generation provider configured: {}/{} (protocol: {:?}, mode: {:?})",
267 provider.provider_name,
268 provider.model_id,
269 provider.protocol,
270 provider.mode
271 );
272 Some(ImageGenClient::new(provider))
273 }
274 Err(e) => {
275 tracing::warn!("Failed to resolve image generation provider: {}", e);
276 None
277 }
278 }
279}
280
281pub fn log_skill_errors(errors: &[SkillLoadError]) {
285 for err in errors {
286 tracing::warn!("Skill load error: {:?}", err);
287 }
288}
289
290#[cfg(test)]
295mod tests {
296 use super::*;
297 use robit_ai::config::{AppConfig, MemoryMode, RobitConfig};
298 use std::collections::HashMap;
299
300 const MEMORY_TOOL_NAMES: [&str; 4] = ["memorize", "recall", "forget", "list_memories"];
301
302 fn config_with_memory_mode(mode: Option<MemoryMode>) -> RobitConfig {
303 RobitConfig {
304 default_model: None,
305 providers: HashMap::new(),
306 app: Some(AppConfig {
307 memory_mode: mode,
308 ..Default::default()
309 }),
310 channels: None,
311 default_image_model: None,
312 image_providers: HashMap::new(),
313 }
314 }
315
316 fn registry_for(config: &RobitConfig) -> ToolRegistry {
317 let skills = Arc::new(SkillRegistry::new(Vec::new(), &[]));
318 create_tools_from_config(config, skills)
319 }
320
321 #[test]
322 fn memory_tools_registered_in_tools_mode() {
323 let registry = registry_for(&config_with_memory_mode(Some(MemoryMode::Tools)));
324 let names = registry.tool_names();
325 for tool in MEMORY_TOOL_NAMES {
326 assert!(names.contains(&tool), "missing {}", tool);
327 }
328 }
329
330 #[test]
331 fn memory_tools_not_registered_in_file_mode() {
332 let registry = registry_for(&config_with_memory_mode(Some(MemoryMode::File)));
333 let names = registry.tool_names();
334 for tool in MEMORY_TOOL_NAMES {
335 assert!(!names.contains(&tool), "{} should not be registered", tool);
336 }
337 assert!(names.contains(&"read"));
338 }
339
340 #[test]
341 fn memory_tools_not_registered_by_default() {
342 let registry = registry_for(&config_with_memory_mode(None));
344 let names = registry.tool_names();
345 for tool in MEMORY_TOOL_NAMES {
346 assert!(!names.contains(&tool));
347 }
348 }
349
350 #[test]
351 fn memory_tools_not_registered_in_off_mode() {
352 let registry = registry_for(&config_with_memory_mode(Some(MemoryMode::Off)));
353 for tool in MEMORY_TOOL_NAMES {
354 assert!(!registry.tool_names().contains(&tool));
355 }
356 }
357
358 #[test]
359 fn memory_tools_in_enabled_tools_list_ignored_in_file_mode() {
360 let mut config = config_with_memory_mode(Some(MemoryMode::File));
362 config.app.as_mut().unwrap().enabled_tools =
363 Some(vec!["read".into(), "memorize".into(), "recall".into()]);
364 let registry = registry_for(&config);
365 let names = registry.tool_names();
366 assert!(names.contains(&"read"));
367 for tool in MEMORY_TOOL_NAMES {
368 assert!(!names.contains(&tool));
369 }
370 }
371
372 #[test]
373 fn missing_file_memory_tools_empty_when_all_present() {
374 assert!(missing_file_memory_tools(&["read", "write", "edit", "bash"]).is_empty());
375 }
376
377 #[test]
378 fn missing_file_memory_tools_reports_each_missing() {
379 assert_eq!(missing_file_memory_tools(&["read", "edit"]), vec!["write"]);
380 assert_eq!(missing_file_memory_tools(&["read", "write"]), vec!["edit"]);
381 assert_eq!(missing_file_memory_tools(&["read"]), vec!["write", "edit"]);
382 }
383}