use std::path::PathBuf;
use std::sync::Arc;
use robit_ai::config::{
resolve_image_provider, resolve_memory_mode, resolve_profile, MemoryMode, RobitConfig,
};
use crate::image_gen::ImageGenClient;
use crate::skill::{load_skills, Skill, SkillRegistry};
use crate::tool::bash::BashTool;
use crate::tool::edit::EditTool;
use crate::tool::find::FindTool;
use crate::tool::generate_image::GenerateImageTool;
use crate::tool::grep::GrepTool;
use crate::tool::load_skill::LoadSkillTool;
use crate::tool::ls::LsTool;
use crate::tool::memory::{ForgetTool, ListMemoriesTool, MemorizeTool, RecallTool};
use crate::tool::query_task::QueryTaskTool;
use crate::tool::read::ReadTool;
use crate::tool::search_history::SearchHistoryTool;
use crate::tool::write::WriteTool;
use crate::tool::ToolRegistry;
use crate::SkillLoadError;
pub struct BootstrapResult {
pub skill_registry: Arc<SkillRegistry>,
pub tool_registry: Arc<ToolRegistry>,
pub total_skills_loaded: usize,
pub skill_load_errors: Vec<SkillLoadError>,
}
pub fn bootstrap(
config: &RobitConfig,
working_dir: &PathBuf,
base_tool_names: &[&str],
) -> BootstrapResult {
let (skills, skill_load_errors) = load_all_skills(working_dir);
let total_skills_loaded = skills.len();
let filtered_skills = filter_skills_by_config(skills, config);
let skill_registry = Arc::new(SkillRegistry::new(filtered_skills, base_tool_names));
let tool_registry = Arc::new(create_tools_from_config(config, Arc::clone(&skill_registry)));
BootstrapResult {
skill_registry,
tool_registry,
total_skills_loaded,
skill_load_errors,
}
}
pub fn load_all_skills(working_dir: &PathBuf) -> (Vec<Skill>, Vec<SkillLoadError>) {
let global_skills_dir = dirs::home_dir().map(|h| h.join(".robit/skills"));
let project_skills_dir = Some(working_dir.join(".robit/skills"));
load_skills(global_skills_dir, project_skills_dir)
}
pub fn filter_skills_by_config(skills: Vec<Skill>, config: &RobitConfig) -> Vec<Skill> {
let enabled_skills = config.app.as_ref().and_then(|a| a.enabled_skills.as_ref());
match enabled_skills {
Some(list) => skills
.into_iter()
.filter(|s| list.contains(&s.frontmatter.name))
.collect(),
None => skills,
}
}
pub fn create_tools_from_config(
config: &RobitConfig,
skill_registry: Arc<SkillRegistry>,
) -> ToolRegistry {
let mut tools = ToolRegistry::new();
let context_config = config.app.as_ref().and_then(|a| a.context.as_ref());
let max_lines = context_config.and_then(|c| c.max_output_lines).unwrap_or(500);
let max_bytes = context_config
.and_then(|c| c.max_output_bytes)
.unwrap_or(51200);
let supports_images = resolve_profile(config, None)
.map(|m| m.supports_images)
.unwrap_or(false);
let max_image_dimension = context_config
.and_then(|c| c.max_image_dimension)
.unwrap_or(1024);
let memory_mode = resolve_memory_mode(config);
let memory_tools_enabled = memory_mode == MemoryMode::Tools;
tools.register(ReadTool::new(
max_lines,
max_bytes,
supports_images,
max_image_dimension,
));
tools.register(LoadSkillTool::new(skill_registry));
if memory_tools_enabled {
tools.register(MemorizeTool::new());
tools.register(RecallTool::new());
tools.register(ForgetTool::new());
tools.register(ListMemoriesTool::new());
}
tools.register(SearchHistoryTool::new());
tools.register(QueryTaskTool::new());
let mut image_client = build_image_client(config);
let enabled_tools = config.app.as_ref().and_then(|a| a.enabled_tools.as_ref());
match enabled_tools {
Some(list) => {
for tool_name in list {
match tool_name.as_str() {
"read" => {} "load_skill" => {} "memorize" | "recall" | "forget" | "list_memories" => {
if !memory_tools_enabled {
tracing::warn!(
"memory tool {} listed in enabled_tools but memory_mode \
!= \"tools\", skipping",
tool_name
);
}
}
"search_history" => {} "query_task" => {} "bash" => tools.register(BashTool::new(max_bytes)),
"write" => tools.register(WriteTool::new()),
"edit" => tools.register(EditTool::new()),
"ls" => tools.register(LsTool::new()),
"find" => tools.register(FindTool::new(max_bytes)),
"grep" => tools.register(GrepTool::new(max_lines, max_bytes)),
"generate_image" => match image_client.take() {
Some(client) => tools.register(GenerateImageTool::new(client)),
None => tracing::warn!(
"generate_image listed in enabled_tools but no image_provider \
is configured, skipping"
),
},
_ => tracing::warn!("Unknown tool in enabled_tools config: {}", tool_name),
}
}
}
None => {
tools.register(BashTool::new(max_bytes));
tools.register(WriteTool::new());
tools.register(EditTool::new());
tools.register(LsTool::new());
tools.register(FindTool::new(max_bytes));
tools.register(GrepTool::new(max_lines, max_bytes));
if let Some(client) = image_client.take() {
tools.register(GenerateImageTool::new(client));
}
}
}
if memory_mode == MemoryMode::File {
let missing = missing_file_memory_tools(&tools.tool_names());
if !missing.is_empty() {
tracing::warn!(
"memory_mode = \"file\" but tool(s) [{}] are not enabled; the agent \
cannot persist memory files (the system prompt instructs it to use \
write/edit). Enable them, or set memory_mode = \"off\" to hide the \
mechanism.",
missing.join(", ")
);
}
}
tools
}
const FILE_MEMORY_REQUIRED_TOOLS: [&str; 2] = ["write", "edit"];
fn missing_file_memory_tools(names: &[&str]) -> Vec<&'static str> {
FILE_MEMORY_REQUIRED_TOOLS
.into_iter()
.filter(|tool| !names.contains(tool))
.collect()
}
fn build_image_client(config: &RobitConfig) -> Option<ImageGenClient> {
if config.image_providers.is_empty() {
return None;
}
if config.default_image_model.is_none() {
return None;
}
match resolve_image_provider(config) {
Ok(provider) => {
tracing::info!(
"Image generation provider configured: {}/{} (protocol: {:?}, mode: {:?})",
provider.provider_name,
provider.model_id,
provider.protocol,
provider.mode
);
Some(ImageGenClient::new(provider))
}
Err(e) => {
tracing::warn!("Failed to resolve image generation provider: {}", e);
None
}
}
}
pub fn log_skill_errors(errors: &[SkillLoadError]) {
for err in errors {
tracing::warn!("Skill load error: {:?}", err);
}
}
#[cfg(test)]
mod tests {
use super::*;
use robit_ai::config::{AppConfig, MemoryMode, RobitConfig};
use std::collections::HashMap;
const MEMORY_TOOL_NAMES: [&str; 4] = ["memorize", "recall", "forget", "list_memories"];
fn config_with_memory_mode(mode: Option<MemoryMode>) -> RobitConfig {
RobitConfig {
default_model: None,
providers: HashMap::new(),
app: Some(AppConfig {
memory_mode: mode,
..Default::default()
}),
channels: None,
default_image_model: None,
image_providers: HashMap::new(),
}
}
fn registry_for(config: &RobitConfig) -> ToolRegistry {
let skills = Arc::new(SkillRegistry::new(Vec::new(), &[]));
create_tools_from_config(config, skills)
}
#[test]
fn memory_tools_registered_in_tools_mode() {
let registry = registry_for(&config_with_memory_mode(Some(MemoryMode::Tools)));
let names = registry.tool_names();
for tool in MEMORY_TOOL_NAMES {
assert!(names.contains(&tool), "missing {}", tool);
}
}
#[test]
fn memory_tools_not_registered_in_file_mode() {
let registry = registry_for(&config_with_memory_mode(Some(MemoryMode::File)));
let names = registry.tool_names();
for tool in MEMORY_TOOL_NAMES {
assert!(!names.contains(&tool), "{} should not be registered", tool);
}
assert!(names.contains(&"read"));
}
#[test]
fn memory_tools_not_registered_by_default() {
let registry = registry_for(&config_with_memory_mode(None));
let names = registry.tool_names();
for tool in MEMORY_TOOL_NAMES {
assert!(!names.contains(&tool));
}
}
#[test]
fn memory_tools_not_registered_in_off_mode() {
let registry = registry_for(&config_with_memory_mode(Some(MemoryMode::Off)));
for tool in MEMORY_TOOL_NAMES {
assert!(!registry.tool_names().contains(&tool));
}
}
#[test]
fn memory_tools_in_enabled_tools_list_ignored_in_file_mode() {
let mut config = config_with_memory_mode(Some(MemoryMode::File));
config.app.as_mut().unwrap().enabled_tools =
Some(vec!["read".into(), "memorize".into(), "recall".into()]);
let registry = registry_for(&config);
let names = registry.tool_names();
assert!(names.contains(&"read"));
for tool in MEMORY_TOOL_NAMES {
assert!(!names.contains(&tool));
}
}
#[test]
fn missing_file_memory_tools_empty_when_all_present() {
assert!(missing_file_memory_tools(&["read", "write", "edit", "bash"]).is_empty());
}
#[test]
fn missing_file_memory_tools_reports_each_missing() {
assert_eq!(missing_file_memory_tools(&["read", "edit"]), vec!["write"]);
assert_eq!(missing_file_memory_tools(&["read", "write"]), vec!["edit"]);
assert_eq!(missing_file_memory_tools(&["read"]), vec!["write", "edit"]);
}
}