diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index db66e4e9b383..1ac7437a38d6 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -488,6 +488,7 @@ impl ExtensionManager { context: PlatformExtensionContext { extension_manager: None, session_manager, + session: None, }, provider, tools_cache: Mutex::new(None), @@ -715,6 +716,11 @@ impl ExtensionManager { })?; let mut context = self.context.clone(); context.extension_manager = Some(Arc::downgrade(self)); + if let Some(id) = session_id { + if let Ok(session) = self.context.session_manager.get_session(id, false).await { + context.session = Some(Arc::new(session)); + } + } (def.client_factory)(context) } diff --git a/crates/goose/src/agents/platform_extensions/developer/mod.rs b/crates/goose/src/agents/platform_extensions/developer/mod.rs index c902343585e8..b03827baf001 100644 --- a/crates/goose/src/agents/platform_extensions/developer/mod.rs +++ b/crates/goose/src/agents/platform_extensions/developer/mod.rs @@ -234,6 +234,7 @@ mod tests { PlatformExtensionContext { extension_manager: None, session_manager: Arc::new(SessionManager::new(data_dir)), + session: None, } } diff --git a/crates/goose/src/agents/platform_extensions/mod.rs b/crates/goose/src/agents/platform_extensions/mod.rs index 1b8ff10f0fed..384a8e24a170 100644 --- a/crates/goose/src/agents/platform_extensions/mod.rs +++ b/crates/goose/src/agents/platform_extensions/mod.rs @@ -11,6 +11,7 @@ pub mod tom; use std::collections::HashMap; use crate::agents::mcp_client::McpClientTrait; +use crate::session::Session; use once_cell::sync::Lazy; pub use ext_manager::MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE; @@ -139,6 +140,7 @@ pub struct PlatformExtensionContext { pub extension_manager: Option>, pub session_manager: std::sync::Arc, + pub session: Option>, } impl PlatformExtensionContext { diff --git a/crates/goose/src/agents/platform_extensions/summon.rs b/crates/goose/src/agents/platform_extensions/summon.rs index de01e5960c77..cfb5fd6da94d 100644 --- a/crates/goose/src/agents/platform_extensions/summon.rs +++ b/crates/goose/src/agents/platform_extensions/summon.rs @@ -201,6 +201,223 @@ fn parse_agent_content(content: &str, path: PathBuf) -> Option { }) } +/// Scan a directory for skill subdirectories containing SKILL.md files. +/// Returns discovered skills, skipping any whose names are already in `seen`. +fn scan_skills_from_dir(dir: &Path, seen: &mut std::collections::HashSet) -> Vec { + let mut sources = Vec::new(); + let entries = match std::fs::read_dir(dir) { + Ok(e) => e, + Err(_) => return sources, + }; + + for entry in entries.flatten() { + let skill_dir = entry.path(); + if !skill_dir.is_dir() { + continue; + } + + let skill_file = skill_dir.join("SKILL.md"); + if !skill_file.exists() { + continue; + } + + let content = match std::fs::read_to_string(&skill_file) { + Ok(c) => c, + Err(e) => { + warn!("Failed to read skill file {}: {}", skill_file.display(), e); + continue; + } + }; + + if let Some(mut source) = parse_skill_content(&content, skill_dir.clone()) { + if !seen.contains(&source.name) { + source.supporting_files = find_supporting_files(&skill_dir, &skill_file); + seen.insert(source.name.clone()); + sources.push(source); + } + } + } + sources +} + +fn scan_recipes_from_dir( + dir: &Path, + kind: SourceKind, + sources: &mut Vec, + seen: &mut std::collections::HashSet, +) { + let entries = match std::fs::read_dir(dir) { + Ok(e) => e, + Err(_) => return, + }; + + for entry in entries.flatten() { + let path = entry.path(); + if !path.is_file() { + continue; + } + + let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); + if !RECIPE_FILE_EXTENSIONS.contains(&ext) { + continue; + } + + let name = path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_string(); + + if name.is_empty() || seen.contains(&name) { + continue; + } + + match Recipe::from_file_path(&path) { + Ok(recipe) => { + seen.insert(name.clone()); + sources.push(Source { + name, + kind, + description: recipe.description.clone(), + path: path.clone(), + content: recipe.instructions.clone().unwrap_or_default(), + supporting_files: Vec::new(), + }); + } + Err(e) => { + warn!("Failed to parse recipe {}: {}", path.display(), e); + } + } + } +} + +fn scan_agents_from_dir( + dir: &Path, + sources: &mut Vec, + seen: &mut std::collections::HashSet, +) { + let entries = match std::fs::read_dir(dir) { + Ok(e) => e, + Err(_) => return, + }; + + for entry in entries.flatten() { + let path = entry.path(); + if !path.is_file() { + continue; + } + + let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); + if ext != "md" { + continue; + } + + let content = match std::fs::read_to_string(&path) { + Ok(c) => c, + Err(e) => { + warn!("Failed to read agent file {}: {}", path.display(), e); + continue; + } + }; + + if let Some(source) = parse_agent_content(&content, path) { + if !seen.contains(&source.name) { + seen.insert(source.name.clone()); + sources.push(source); + } + } + } +} + +fn discover_filesystem_sources(working_dir: &Path) -> Vec { + let mut sources: Vec = Vec::new(); + let mut seen: std::collections::HashSet = std::collections::HashSet::new(); + + let home = dirs::home_dir(); + let config = Paths::config_dir(); + + let local_recipe_dirs: Vec = vec![ + working_dir.to_path_buf(), + working_dir.join(".goose/recipes"), + ]; + + let global_recipe_dirs: Vec = std::env::var("GOOSE_RECIPE_PATH") + .ok() + .into_iter() + .flat_map(|p| { + let sep = if cfg!(windows) { ';' } else { ':' }; + p.split(sep).map(PathBuf::from).collect::>() + }) + .chain([config.join("recipes")]) + .collect(); + + let local_skill_dirs: Vec = vec![ + working_dir.join(".goose/skills"), + working_dir.join(".claude/skills"), + working_dir.join(".agents/skills"), + ]; + + let global_skill_dirs: Vec = [ + Some(config.join("skills")), + home.as_ref().map(|h| h.join(".claude/skills")), + home.as_ref().map(|h| h.join(".config/agents/skills")), + ] + .into_iter() + .flatten() + .collect(); + + let local_agent_dirs: Vec = vec![ + working_dir.join(".goose/agents"), + working_dir.join(".claude/agents"), + ]; + + let global_agent_dirs: Vec = [ + Some(config.join("agents")), + home.as_ref().map(|h| h.join(".claude/agents")), + ] + .into_iter() + .flatten() + .collect(); + + for dir in local_recipe_dirs { + scan_recipes_from_dir(&dir, SourceKind::Recipe, &mut sources, &mut seen); + } + + for dir in local_skill_dirs { + sources.extend(scan_skills_from_dir(&dir, &mut seen)); + } + + for dir in local_agent_dirs { + scan_agents_from_dir(&dir, &mut sources, &mut seen); + } + + for dir in global_recipe_dirs { + scan_recipes_from_dir(&dir, SourceKind::Recipe, &mut sources, &mut seen); + } + + for dir in global_skill_dirs { + sources.extend(scan_skills_from_dir(&dir, &mut seen)); + } + + for dir in global_agent_dirs { + scan_agents_from_dir(&dir, &mut sources, &mut seen); + } + + for content in builtin_skills::get_all() { + if let Some(source) = parse_skill_content(content, PathBuf::new()) { + if !seen.contains(&source.name) { + seen.insert(source.name.clone()); + sources.push(Source { + kind: SourceKind::BuiltinSkill, + ..source + }); + } + } + } + + sources +} + /// Collect all files in a skill directory (excluding SKILL.md itself), /// recursing one level into subdirectories. fn find_supporting_files(directory: &Path, skill_file: &Path) -> Vec { @@ -278,6 +495,28 @@ impl Drop for SummonClient { impl SummonClient { pub fn new(context: PlatformExtensionContext) -> Result { + let instructions = if let Some(session) = &context.session { + let mut instructions = "".to_string(); + let sources = discover_filesystem_sources(&session.working_dir); + + let mut skills: Vec<&Source> = sources + .iter() + .filter(|s| s.kind == SourceKind::Skill || s.kind == SourceKind::BuiltinSkill) + .collect(); + + skills.sort_by(|a, b| (&a.name, &a.path).cmp(&(&b.name, &b.path))); + + if !skills.is_empty() { + instructions.push_str("\n\nYou have these skills at your disposal, when it is clear they can help you solve a problem or you are asked to use them:"); + for skill in &skills { + instructions.push_str(&format!("\n• {} - {}", skill.name, skill.description)); + } + } + Some(instructions) + } else { + None + }; + let info = InitializeResult { protocol_version: ProtocolVersion::V_2025_03_26, capabilities: ServerCapabilities { @@ -300,10 +539,7 @@ impl SummonClient { icons: None, website_url: None, }, - instructions: Some( - "Load knowledge and delegate tasks to subagents using the summon extension." - .to_string(), - ), + instructions, }; Ok(Self { @@ -520,92 +756,7 @@ impl SummonClient { } fn discover_filesystem_sources(&self, working_dir: &Path) -> Vec { - let mut sources: Vec = Vec::new(); - let mut seen: std::collections::HashSet = std::collections::HashSet::new(); - - let home = dirs::home_dir(); - let config = Paths::config_dir(); - - let local_recipe_dirs: Vec = vec![ - working_dir.to_path_buf(), - working_dir.join(".goose/recipes"), - ]; - - let global_recipe_dirs: Vec = std::env::var("GOOSE_RECIPE_PATH") - .ok() - .into_iter() - .flat_map(|p| { - let sep = if cfg!(windows) { ';' } else { ':' }; - p.split(sep).map(PathBuf::from).collect::>() - }) - .chain([config.join("recipes")]) - .collect(); - - let local_skill_dirs: Vec = vec![ - working_dir.join(".goose/skills"), - working_dir.join(".claude/skills"), - working_dir.join(".agents/skills"), - ]; - - let global_skill_dirs: Vec = [ - Some(config.join("skills")), - home.as_ref().map(|h| h.join(".claude/skills")), - home.as_ref().map(|h| h.join(".config/agents/skills")), - ] - .into_iter() - .flatten() - .collect(); - - let local_agent_dirs: Vec = vec![ - working_dir.join(".goose/agents"), - working_dir.join(".claude/agents"), - ]; - - let global_agent_dirs: Vec = [ - Some(config.join("agents")), - home.as_ref().map(|h| h.join(".claude/agents")), - ] - .into_iter() - .flatten() - .collect(); - - for dir in local_recipe_dirs { - self.scan_recipes_dir(&dir, SourceKind::Recipe, &mut sources, &mut seen); - } - - for dir in local_skill_dirs { - self.scan_skills_dir(&dir, &mut sources, &mut seen); - } - - for dir in local_agent_dirs { - self.scan_agents_dir(&dir, &mut sources, &mut seen); - } - - for dir in global_recipe_dirs { - self.scan_recipes_dir(&dir, SourceKind::Recipe, &mut sources, &mut seen); - } - - for dir in global_skill_dirs { - self.scan_skills_dir(&dir, &mut sources, &mut seen); - } - - for dir in global_agent_dirs { - self.scan_agents_dir(&dir, &mut sources, &mut seen); - } - - for content in builtin_skills::get_all() { - if let Some(source) = parse_skill_content(content, PathBuf::new()) { - if !seen.contains(&source.name) { - seen.insert(source.name.clone()); - sources.push(Source { - kind: SourceKind::BuiltinSkill, - ..source - }); - } - } - } - - sources + discover_filesystem_sources(working_dir) } async fn add_subrecipes( @@ -672,137 +823,6 @@ impl SummonClient { format!("Subrecipe from {}", sr.path) } - fn scan_recipes_dir( - &self, - dir: &Path, - kind: SourceKind, - sources: &mut Vec, - seen: &mut std::collections::HashSet, - ) { - let entries = match std::fs::read_dir(dir) { - Ok(e) => e, - Err(_) => return, - }; - - for entry in entries.flatten() { - let path = entry.path(); - if !path.is_file() { - continue; - } - - let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); - if !RECIPE_FILE_EXTENSIONS.contains(&ext) { - continue; - } - - let name = path - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("") - .to_string(); - - if name.is_empty() || seen.contains(&name) { - continue; - } - - match Recipe::from_file_path(&path) { - Ok(recipe) => { - seen.insert(name.clone()); - sources.push(Source { - name, - kind, - description: recipe.description.clone(), - path: path.clone(), - content: recipe.instructions.clone().unwrap_or_default(), - supporting_files: Vec::new(), - }); - } - Err(e) => { - warn!("Failed to parse recipe {}: {}", path.display(), e); - } - } - } - } - - fn scan_skills_dir( - &self, - dir: &Path, - sources: &mut Vec, - seen: &mut std::collections::HashSet, - ) { - let entries = match std::fs::read_dir(dir) { - Ok(e) => e, - Err(_) => return, - }; - - for entry in entries.flatten() { - let skill_dir = entry.path(); - if !skill_dir.is_dir() { - continue; - } - - let skill_file = skill_dir.join("SKILL.md"); - if !skill_file.exists() { - continue; - } - - let content = match std::fs::read_to_string(&skill_file) { - Ok(c) => c, - Err(e) => { - warn!("Failed to read skill file {}: {}", skill_file.display(), e); - continue; - } - }; - - if let Some(mut source) = parse_skill_content(&content, skill_dir.clone()) { - if !seen.contains(&source.name) { - source.supporting_files = find_supporting_files(&skill_dir, &skill_file); - seen.insert(source.name.clone()); - sources.push(source); - } - } - } - } - - fn scan_agents_dir( - &self, - dir: &Path, - sources: &mut Vec, - seen: &mut std::collections::HashSet, - ) { - let entries = match std::fs::read_dir(dir) { - Ok(e) => e, - Err(_) => return, - }; - - for entry in entries.flatten() { - let path = entry.path(); - if !path.is_file() { - continue; - } - - let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); - if ext != "md" { - continue; - } - - let content = match std::fs::read_to_string(&path) { - Ok(c) => c, - Err(e) => { - warn!("Failed to read agent file {}: {}", path.display(), e); - continue; - } - }; - - if let Some(source) = parse_agent_content(&content, path) { - if !seen.contains(&source.name) { - seen.insert(source.name.clone()); - sources.push(source); - } - } - } - } - async fn handle_load( &self, session_id: &str, @@ -1817,6 +1837,7 @@ mod tests { PlatformExtensionContext { extension_manager: None, session_manager: Arc::new(crate::session::SessionManager::instance()), + session: None, } } diff --git a/crates/goose/src/agents/prompt_manager.rs b/crates/goose/src/agents/prompt_manager.rs index c2b807dde14b..d3bfc115e1e2 100644 --- a/crates/goose/src/agents/prompt_manager.rs +++ b/crates/goose/src/agents/prompt_manager.rs @@ -401,9 +401,18 @@ mod tests { let tmp_dir = tempfile::tempdir().unwrap(); let session_manager = Arc::new(SessionManager::new(tmp_dir.path().to_path_buf())); + let session = session_manager + .create_session( + tmp_dir.path().to_path_buf(), + "test session".to_owned(), + crate::session::SessionType::Hidden, + ) + .await + .unwrap(); let context = PlatformExtensionContext { extension_manager: None, session_manager, + session: Some(Arc::new(session)), }; let mut extensions: Vec = PLATFORM_EXTENSIONS diff --git a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap index a14f0d1c787f..73ad29d38c9c 100644 --- a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap +++ b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap @@ -87,7 +87,10 @@ Use write and edit to efficiently make changes. Test and verify as appropriate. ## summon ### Instructions -Load knowledge and delegate tasks to subagents using the summon extension. + + +You have these skills at your disposal, when it is clear they can help you solve a problem or you are asked to use them: +• goose-doc-guide - Reference goose documentation to create, configure, or explain goose-specific features like recipes, extensions, sessions, and providers. You MUST fetch relevant goose docs before answering. You MUST NOT rely on training data or assumptions for any goose-specific fields, values, names, syntax, or commands. ## todo ### Instructions