mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-05 08:10:39 +00:00
Add use_skill tool for agent-initiated skill loading
Let the agent autonomously load skill templates when it recognizes a matching task, instead of requiring users to type /skill-name. The system prompt now instructs the agent to call `use_skill` and skill names use backtick formatting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f6c711fdd9
commit
35005ca0fe
2 changed files with 99 additions and 6 deletions
|
|
@ -7,7 +7,7 @@ use crate::loop_detection::detect_loop;
|
|||
use crate::profiles::EnvContext;
|
||||
use crate::project_docs::discover_project_docs;
|
||||
use crate::provider_profile::ProviderProfile;
|
||||
use crate::skills::{default_skill_dirs, discover_skills, expand_skill, Skill};
|
||||
use crate::skills::{default_skill_dirs, discover_skills, expand_skill, make_use_skill_tool, Skill};
|
||||
use crate::tool_registry::ToolRegistry;
|
||||
use crate::truncation::truncate_tool_output;
|
||||
use crate::types::{AgentEvent, SessionState, Turn};
|
||||
|
|
@ -95,6 +95,14 @@ impl Session {
|
|||
};
|
||||
self.skills = discover_skills(self.execution_env.as_ref(), &skill_dirs).await;
|
||||
|
||||
// Register use_skill tool when skills are available
|
||||
if !self.skills.is_empty() {
|
||||
let skills_arc = Arc::new(self.skills.clone());
|
||||
if let Some(profile) = Arc::get_mut(&mut self.provider_profile) {
|
||||
profile.tool_registry_mut().register(make_use_skill_tool(skills_arc));
|
||||
}
|
||||
}
|
||||
|
||||
// Populate environment context
|
||||
self.env_context = self.build_env_context().await;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
use crate::execution_env::ExecutionEnvironment;
|
||||
use crate::tool_registry::RegisteredTool;
|
||||
use crate::tools::required_str;
|
||||
use llm::types::ToolDefinition;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Skill {
|
||||
|
|
@ -148,17 +152,54 @@ pub fn expand_skill(skills: &[Skill], input: &str) -> Result<ExpandedInput, Stri
|
|||
})
|
||||
}
|
||||
|
||||
pub fn make_use_skill_tool(skills: Arc<Vec<Skill>>) -> RegisteredTool {
|
||||
RegisteredTool {
|
||||
definition: ToolDefinition {
|
||||
name: "use_skill".into(),
|
||||
description: "Load a skill's instructions by name. Call this when the user's \
|
||||
request matches an available skill."
|
||||
.into(),
|
||||
parameters: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"skill_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the skill to load (without the / prefix)"
|
||||
}
|
||||
},
|
||||
"required": ["skill_name"]
|
||||
}),
|
||||
},
|
||||
executor: Arc::new(move |args, _env, _cancel| {
|
||||
let skills = skills.clone();
|
||||
Box::pin(async move {
|
||||
let name = required_str(&args, "skill_name")?;
|
||||
let skill = skills
|
||||
.iter()
|
||||
.find(|s| s.name == name)
|
||||
.ok_or_else(|| format!("Unknown skill: {name}"))?;
|
||||
Ok(skill.template.clone())
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_skills_prompt_section(skills: &[Skill]) -> String {
|
||||
if skills.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let mut lines = vec!["# Available Skills".to_string()];
|
||||
let mut lines = vec![
|
||||
"# Available Skills".to_string(),
|
||||
"When the user's request matches a skill below, call the `use_skill` tool \
|
||||
to load its instructions, then follow them."
|
||||
.to_string(),
|
||||
];
|
||||
for skill in skills {
|
||||
if skill.description.is_empty() {
|
||||
lines.push(format!("- /{}", skill.name));
|
||||
lines.push(format!("- `{}`", skill.name));
|
||||
} else {
|
||||
lines.push(format!("- /{}: {}", skill.name, skill.description));
|
||||
lines.push(format!("- `{}`: {}", skill.name, skill.description));
|
||||
}
|
||||
}
|
||||
lines.join("\n")
|
||||
|
|
@ -392,8 +433,9 @@ name: trimmed
|
|||
let skills = test_skills();
|
||||
let section = format_skills_prompt_section(&skills);
|
||||
assert!(section.contains("# Available Skills"));
|
||||
assert!(section.contains("- /commit: Create a commit"));
|
||||
assert!(section.contains("- /test: Run tests"));
|
||||
assert!(section.contains("call the `use_skill` tool"));
|
||||
assert!(section.contains("- `commit`: Create a commit"));
|
||||
assert!(section.contains("- `test`: Run tests"));
|
||||
}
|
||||
|
||||
// --- discover_skills tests ---
|
||||
|
|
@ -491,4 +533,47 @@ name: trimmed
|
|||
let dirs = default_skill_dirs(Some("/home/user"), None);
|
||||
assert_eq!(dirs, vec!["/home/user/.attractor/skills"]);
|
||||
}
|
||||
|
||||
// --- make_use_skill_tool tests ---
|
||||
|
||||
#[tokio::test]
|
||||
async fn use_skill_tool_returns_template() {
|
||||
let skills = Arc::new(test_skills());
|
||||
let tool = make_use_skill_tool(skills);
|
||||
|
||||
let env: Arc<dyn crate::execution_env::ExecutionEnvironment> =
|
||||
Arc::new(MockExecutionEnvironment::default());
|
||||
let args = serde_json::json!({"skill_name": "commit"});
|
||||
let result = (tool.executor)(args, env, tokio_util::sync::CancellationToken::new()).await;
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
"Review changes and commit.\n\n{{user_input}}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn use_skill_tool_unknown_skill_errors() {
|
||||
let skills = Arc::new(test_skills());
|
||||
let tool = make_use_skill_tool(skills);
|
||||
|
||||
let env: Arc<dyn crate::execution_env::ExecutionEnvironment> =
|
||||
Arc::new(MockExecutionEnvironment::default());
|
||||
let args = serde_json::json!({"skill_name": "nonexistent"});
|
||||
let result = (tool.executor)(args, env, tokio_util::sync::CancellationToken::new()).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("Unknown skill"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn use_skill_tool_missing_param_errors() {
|
||||
let skills = Arc::new(test_skills());
|
||||
let tool = make_use_skill_tool(skills);
|
||||
|
||||
let env: Arc<dyn crate::execution_env::ExecutionEnvironment> =
|
||||
Arc::new(MockExecutionEnvironment::default());
|
||||
let args = serde_json::json!({});
|
||||
let result = (tool.executor)(args, env, tokio_util::sync::CancellationToken::new()).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("Missing required parameter"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue