Restore old system prompt behavior around subagents (#7576)

This commit is contained in:
Jack Amadeo
2026-02-27 15:42:54 -05:00
committed by GitHub
parent b43302a058
commit c8a7189cda
6 changed files with 264 additions and 222 deletions
@@ -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
@@ -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