Dynamically refresh skill instructions each turn (#9217)

Signed-off-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Douwe Osinga
2026-05-14 16:00:04 -04:00
committed by GitHub
parent cac334b30f
commit 401f8e86ba
5 changed files with 36 additions and 32 deletions
@@ -56,7 +56,7 @@ impl McpClientTrait for MockClient {
}
fn get_info(&self) -> std::option::Option<&rmcp::model::InitializeResult> {
todo!()
None
}
async fn read_resource(
+1 -3
View File
@@ -106,9 +106,7 @@ impl Extension {
}
fn get_instructions(&self) -> Option<String> {
self.server_info
.as_ref()
.and_then(|info| info.instructions.clone())
self.client.get_instructions()
}
fn get_client(&self) -> McpClientBox {
+7
View File
@@ -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<String> {
self.get_info().and_then(|info| info.instructions.clone())
}
async fn list_resources(
&self,
_session_id: &str,
+3 -5
View File
@@ -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)
+24 -23
View File
@@ -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<String> {
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<ServerNotification> {
let (_tx, rx) = mpsc::channel(1);
rx