mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
Restore old system prompt behavior around subagents (#7576)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -234,6 +234,7 @@ mod tests {
|
||||
PlatformExtensionContext {
|
||||
extension_manager: None,
|
||||
session_manager: Arc::new(SessionManager::new(data_dir)),
|
||||
session: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<std::sync::Weak<crate::agents::extension_manager::ExtensionManager>>,
|
||||
pub session_manager: std::sync::Arc<crate::session::SessionManager>,
|
||||
pub session: Option<std::sync::Arc<Session>>,
|
||||
}
|
||||
|
||||
impl PlatformExtensionContext {
|
||||
|
||||
@@ -201,6 +201,223 @@ fn parse_agent_content(content: &str, path: PathBuf) -> Option<Source> {
|
||||
})
|
||||
}
|
||||
|
||||
/// 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<String>) -> Vec<Source> {
|
||||
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<Source>,
|
||||
seen: &mut std::collections::HashSet<String>,
|
||||
) {
|
||||
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<Source>,
|
||||
seen: &mut std::collections::HashSet<String>,
|
||||
) {
|
||||
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<Source> {
|
||||
let mut sources: Vec<Source> = Vec::new();
|
||||
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
|
||||
let home = dirs::home_dir();
|
||||
let config = Paths::config_dir();
|
||||
|
||||
let local_recipe_dirs: Vec<PathBuf> = vec![
|
||||
working_dir.to_path_buf(),
|
||||
working_dir.join(".goose/recipes"),
|
||||
];
|
||||
|
||||
let global_recipe_dirs: Vec<PathBuf> = 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::<Vec<_>>()
|
||||
})
|
||||
.chain([config.join("recipes")])
|
||||
.collect();
|
||||
|
||||
let local_skill_dirs: Vec<PathBuf> = vec![
|
||||
working_dir.join(".goose/skills"),
|
||||
working_dir.join(".claude/skills"),
|
||||
working_dir.join(".agents/skills"),
|
||||
];
|
||||
|
||||
let global_skill_dirs: Vec<PathBuf> = [
|
||||
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<PathBuf> = vec![
|
||||
working_dir.join(".goose/agents"),
|
||||
working_dir.join(".claude/agents"),
|
||||
];
|
||||
|
||||
let global_agent_dirs: Vec<PathBuf> = [
|
||||
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<PathBuf> {
|
||||
@@ -278,6 +495,28 @@ impl Drop for SummonClient {
|
||||
|
||||
impl SummonClient {
|
||||
pub fn new(context: PlatformExtensionContext) -> Result<Self> {
|
||||
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<Source> {
|
||||
let mut sources: Vec<Source> = Vec::new();
|
||||
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
|
||||
let home = dirs::home_dir();
|
||||
let config = Paths::config_dir();
|
||||
|
||||
let local_recipe_dirs: Vec<PathBuf> = vec![
|
||||
working_dir.to_path_buf(),
|
||||
working_dir.join(".goose/recipes"),
|
||||
];
|
||||
|
||||
let global_recipe_dirs: Vec<PathBuf> = 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::<Vec<_>>()
|
||||
})
|
||||
.chain([config.join("recipes")])
|
||||
.collect();
|
||||
|
||||
let local_skill_dirs: Vec<PathBuf> = vec![
|
||||
working_dir.join(".goose/skills"),
|
||||
working_dir.join(".claude/skills"),
|
||||
working_dir.join(".agents/skills"),
|
||||
];
|
||||
|
||||
let global_skill_dirs: Vec<PathBuf> = [
|
||||
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<PathBuf> = vec![
|
||||
working_dir.join(".goose/agents"),
|
||||
working_dir.join(".claude/agents"),
|
||||
];
|
||||
|
||||
let global_agent_dirs: Vec<PathBuf> = [
|
||||
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<Source>,
|
||||
seen: &mut std::collections::HashSet<String>,
|
||||
) {
|
||||
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<Source>,
|
||||
seen: &mut std::collections::HashSet<String>,
|
||||
) {
|
||||
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<Source>,
|
||||
seen: &mut std::collections::HashSet<String>,
|
||||
) {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ExtensionInfo> = PLATFORM_EXTENSIONS
|
||||
|
||||
+4
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user