From 401f8e86ba0092076b43161b8f4693edee67ceec Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Thu, 14 May 2026 16:00:04 -0400 Subject: [PATCH] Dynamically refresh skill instructions each turn (#9217) Signed-off-by: Douwe Osinga Co-authored-by: Douwe Osinga --- .../src/scenario_tests/mock_client.rs | 2 +- crates/goose/src/agents/extension_manager.rs | 4 +- crates/goose/src/agents/mcp_client.rs | 7 +++ crates/goose/src/agents/prompt_manager.rs | 8 ++-- crates/goose/src/skills/client.rs | 47 ++++++++++--------- 5 files changed, 36 insertions(+), 32 deletions(-) diff --git a/crates/goose-cli/src/scenario_tests/mock_client.rs b/crates/goose-cli/src/scenario_tests/mock_client.rs index 31932e5bef..dce42c24ca 100644 --- a/crates/goose-cli/src/scenario_tests/mock_client.rs +++ b/crates/goose-cli/src/scenario_tests/mock_client.rs @@ -56,7 +56,7 @@ impl McpClientTrait for MockClient { } fn get_info(&self) -> std::option::Option<&rmcp::model::InitializeResult> { - todo!() + None } async fn read_resource( diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index fe03598be6..cb2c88db46 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -106,9 +106,7 @@ impl Extension { } fn get_instructions(&self) -> Option { - self.server_info - .as_ref() - .and_then(|info| info.instructions.clone()) + self.client.get_instructions() } fn get_client(&self) -> McpClientBox { diff --git a/crates/goose/src/agents/mcp_client.rs b/crates/goose/src/agents/mcp_client.rs index a0a7ac8fc1..c801b37ac8 100644 --- a/crates/goose/src/agents/mcp_client.rs +++ b/crates/goose/src/agents/mcp_client.rs @@ -84,6 +84,13 @@ pub trait McpClientTrait: Send + Sync { fn get_info(&self) -> Option<&InitializeResult>; + /// Return the extension's current instructions. The default reads from + /// `get_info()`, but platform extensions can override this to provide + /// dynamically computed instructions (e.g. freshly discovered skills). + fn get_instructions(&self) -> Option { + self.get_info().and_then(|info| info.instructions.clone()) + } + async fn list_resources( &self, _session_id: &str, diff --git a/crates/goose/src/agents/prompt_manager.rs b/crates/goose/src/agents/prompt_manager.rs index b6ae713cbf..4ce45fd352 100644 --- a/crates/goose/src/agents/prompt_manager.rs +++ b/crates/goose/src/agents/prompt_manager.rs @@ -441,11 +441,9 @@ mod tests { .values() .map(|def| { let client = (def.client_factory)(context.clone()); - let info = client.get_info(); - let instructions = info - .and_then(|i| i.instructions.clone()) - .unwrap_or_default(); - let has_resources = info + let instructions = client.get_instructions().unwrap_or_default(); + let has_resources = client + .get_info() .and_then(|i| i.capabilities.resources.as_ref()) .is_some(); ExtensionInfo::new(def.name, &instructions, has_resources) diff --git a/crates/goose/src/skills/client.rs b/crates/goose/src/skills/client.rs index 888dfd45d8..b52b3fadd0 100644 --- a/crates/goose/src/skills/client.rs +++ b/crates/goose/src/skills/client.rs @@ -27,30 +27,8 @@ impl SkillsClient { .map(|s| s.working_dir.clone()) .unwrap_or_else(|| std::env::current_dir().unwrap_or_default()); - let mut instructions = String::new(); - if context.session.is_some() { - let sources = discover_skills(Some(&working_dir)); - let mut skills: Vec<&SourceEntry> = sources - .iter() - .filter(|s| { - s.source_type == SourceType::Skill || s.source_type == SourceType::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)); - } - } - } - let info = InitializeResult::new(ServerCapabilities::builder().enable_tools().build()) - .with_server_info(Implementation::new(EXTENSION_NAME, "1.0.0").with_title("Skills")) - .with_instructions(instructions); + .with_server_info(Implementation::new(EXTENSION_NAME, "1.0.0").with_title("Skills")); Ok(Self { info, working_dir }) } @@ -252,6 +230,29 @@ impl McpClientTrait for SkillsClient { Some(&self.info) } + fn get_instructions(&self) -> Option { + let sources = discover_skills(Some(&self.working_dir)); + let mut skills: Vec<&SourceEntry> = sources + .iter() + .filter(|s| { + s.source_type == SourceType::Skill || s.source_type == SourceType::BuiltinSkill + }) + .collect(); + skills.sort_by(|a, b| (&a.name, &a.path).cmp(&(&b.name, &b.path))); + + if skills.is_empty() { + return None; + } + + let mut instructions = String::from( + "\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) + } + async fn subscribe(&self) -> mpsc::Receiver { let (_tx, rx) = mpsc::channel(1); rx