mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Stop inlining skill descriptions
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
parent
2510a4b4c9
commit
72d303fbab
@@ -17,8 +17,8 @@ pub fn load_skill_def() -> ToolDef {
|
||||
ToolDef {
|
||||
name: LOAD_SKILL_TOOL.to_owned(),
|
||||
description: "Load the full content of a skill by name. \
|
||||
Call this before using a skill — the system prompt lists skill names \
|
||||
and descriptions only; the full instructions are loaded on demand. \
|
||||
Call this before using a skill — the system prompt lists skill names only; \
|
||||
full instructions are loaded on demand. \
|
||||
To load a supporting file within a skill, use the form \
|
||||
\"skill-name/relative/path\" (e.g. \"my-skill/references/foo.md\")."
|
||||
.to_owned(),
|
||||
@@ -76,8 +76,7 @@ pub async fn call_load_skill(arguments: &Value, skills: &[SkillEntry]) -> ToolRe
|
||||
}
|
||||
};
|
||||
|
||||
// Strip the YAML frontmatter — the agent already knows name/description
|
||||
// from the system prompt; return only the body.
|
||||
// Strip the YAML frontmatter; return the skill instructions only on demand.
|
||||
let body = strip_frontmatter(&raw);
|
||||
|
||||
let mut output = body.to_owned();
|
||||
@@ -250,10 +249,9 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_skill(name: &str, description: &str, path: PathBuf) -> SkillEntry {
|
||||
fn make_skill(name: &str, path: PathBuf) -> SkillEntry {
|
||||
SkillEntry {
|
||||
name: name.to_owned(),
|
||||
description: description.to_owned(),
|
||||
path,
|
||||
supporting_files: Vec::new(),
|
||||
}
|
||||
@@ -261,13 +259,11 @@ mod tests {
|
||||
|
||||
fn make_skill_with_files(
|
||||
name: &str,
|
||||
description: &str,
|
||||
path: PathBuf,
|
||||
supporting_files: Vec<PathBuf>,
|
||||
) -> SkillEntry {
|
||||
SkillEntry {
|
||||
name: name.to_owned(),
|
||||
description: description.to_owned(),
|
||||
path,
|
||||
supporting_files,
|
||||
}
|
||||
@@ -298,7 +294,7 @@ mod tests {
|
||||
"---\nname: test\ndescription: A test\n---\nSkill body here.\n",
|
||||
)
|
||||
.unwrap();
|
||||
let skills = vec![make_skill("test", "A test", skill_md)];
|
||||
let skills = vec![make_skill("test", skill_md)];
|
||||
let result = call_load_skill(&serde_json::json!({"name": "test"}), &skills).await;
|
||||
assert!(!result.is_error);
|
||||
let text = text_content(&result);
|
||||
@@ -324,12 +320,7 @@ mod tests {
|
||||
let ref_file = refs_dir.join("foo.md");
|
||||
std::fs::write(&ref_file, "Reference content.").unwrap();
|
||||
|
||||
let skills = vec![make_skill_with_files(
|
||||
"my-skill",
|
||||
"desc",
|
||||
skill_md,
|
||||
vec![ref_file],
|
||||
)];
|
||||
let skills = vec![make_skill_with_files("my-skill", skill_md, vec![ref_file])];
|
||||
let result = call_load_skill(&serde_json::json!({"name": "my-skill"}), &skills).await;
|
||||
assert!(!result.is_error);
|
||||
let text = text_content(&result);
|
||||
@@ -357,7 +348,7 @@ mod tests {
|
||||
"---\nname: bare\ndescription: desc\n---\nBody.\n",
|
||||
)
|
||||
.unwrap();
|
||||
let skills = vec![make_skill("bare", "desc", skill_md)];
|
||||
let skills = vec![make_skill("bare", skill_md)];
|
||||
let result = call_load_skill(&serde_json::json!({"name": "bare"}), &skills).await;
|
||||
assert!(!result.is_error);
|
||||
let text = text_content(&result);
|
||||
@@ -382,12 +373,7 @@ mod tests {
|
||||
let ref_file = refs_dir.join("foo.md");
|
||||
std::fs::write(&ref_file, "Reference content here.").unwrap();
|
||||
|
||||
let skills = vec![make_skill_with_files(
|
||||
"my-skill",
|
||||
"desc",
|
||||
skill_md,
|
||||
vec![ref_file],
|
||||
)];
|
||||
let skills = vec![make_skill_with_files("my-skill", skill_md, vec![ref_file])];
|
||||
let result = call_load_skill(
|
||||
&serde_json::json!({"name": "my-skill/references/foo.md"}),
|
||||
&skills,
|
||||
@@ -420,12 +406,7 @@ mod tests {
|
||||
let ref_file = refs_dir.join("foo.md");
|
||||
std::fs::write(&ref_file, "content").unwrap();
|
||||
|
||||
let skills = vec![make_skill_with_files(
|
||||
"my-skill",
|
||||
"desc",
|
||||
skill_md,
|
||||
vec![ref_file],
|
||||
)];
|
||||
let skills = vec![make_skill_with_files("my-skill", skill_md, vec![ref_file])];
|
||||
let result = call_load_skill(
|
||||
&serde_json::json!({"name": "my-skill/references/missing.md"}),
|
||||
&skills,
|
||||
@@ -449,7 +430,7 @@ mod tests {
|
||||
"---\nname: bare\ndescription: desc\n---\nBody.\n",
|
||||
)
|
||||
.unwrap();
|
||||
let skills = vec![make_skill("bare", "desc", skill_md)];
|
||||
let skills = vec![make_skill("bare", skill_md)];
|
||||
let result =
|
||||
call_load_skill(&serde_json::json!({"name": "bare/anything.md"}), &skills).await;
|
||||
assert!(result.is_error);
|
||||
@@ -478,7 +459,6 @@ mod tests {
|
||||
// The traversal guard should catch this.
|
||||
let skills = vec![make_skill_with_files(
|
||||
"my-skill",
|
||||
"desc",
|
||||
skill_md.clone(),
|
||||
vec![outside_file.clone()],
|
||||
)];
|
||||
@@ -519,12 +499,7 @@ mod tests {
|
||||
let ref_file = refs_dir.join("extra.md");
|
||||
std::fs::write(&ref_file, "extra content").unwrap();
|
||||
|
||||
let skills = vec![make_skill_with_files(
|
||||
"big",
|
||||
"desc",
|
||||
skill_md,
|
||||
vec![ref_file],
|
||||
)];
|
||||
let skills = vec![make_skill_with_files("big", skill_md, vec![ref_file])];
|
||||
let result = call_load_skill(&serde_json::json!({"name": "big"}), &skills).await;
|
||||
assert!(!result.is_error);
|
||||
let text = text_content(&result);
|
||||
@@ -548,12 +523,7 @@ mod tests {
|
||||
let ref_file = refs_dir.join("huge.md");
|
||||
std::fs::write(&ref_file, "x".repeat(MAX_SKILL_BODY_BYTES * 2)).unwrap();
|
||||
|
||||
let skills = vec![make_skill_with_files(
|
||||
"big",
|
||||
"desc",
|
||||
skill_md,
|
||||
vec![ref_file],
|
||||
)];
|
||||
let skills = vec![make_skill_with_files("big", skill_md, vec![ref_file])];
|
||||
let result = call_load_skill(
|
||||
&serde_json::json!({"name": "big/references/huge.md"}),
|
||||
&skills,
|
||||
|
||||
@@ -14,7 +14,6 @@ fn home_dir() -> Option<PathBuf> {
|
||||
#[derive(Clone)]
|
||||
pub struct SkillEntry {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
/// Absolute path to the SKILL.md file; used by `load_skill` to read on demand.
|
||||
pub path: PathBuf,
|
||||
/// Absolute paths to every non-SKILL.md file in the skill directory tree.
|
||||
@@ -130,7 +129,7 @@ fn scan_skill_dir(dir: &Path, seen: &mut HashSet<String>, skills: &mut Vec<Skill
|
||||
let Ok(content) = std::fs::read_to_string(&skill_md) else {
|
||||
continue;
|
||||
};
|
||||
let Some((name, description)) = parse_skill_frontmatter(&content) else {
|
||||
let Some((name, _description)) = parse_skill_frontmatter(&content) else {
|
||||
continue;
|
||||
};
|
||||
if seen.contains(&name) {
|
||||
@@ -145,7 +144,6 @@ fn scan_skill_dir(dir: &Path, seen: &mut HashSet<String>, skills: &mut Vec<Skill
|
||||
|
||||
skills.push(SkillEntry {
|
||||
name,
|
||||
description,
|
||||
path: skill_md,
|
||||
supporting_files,
|
||||
});
|
||||
@@ -239,10 +237,10 @@ fn build_hints_section_impl(cwd: &Path, home: Option<&Path>) -> (String, Vec<Ski
|
||||
if !skills.is_empty() {
|
||||
out.push_str("\n## Available Skills\n");
|
||||
for skill in &skills {
|
||||
out.push_str(&format!("- {}: {}\n", skill.name, skill.description));
|
||||
out.push_str(&format!("- {}\n", skill.name));
|
||||
}
|
||||
out.push_str(
|
||||
"\nUse the `load_skill` tool to read the full content of a skill before using it.\n",
|
||||
"\nUse the `load_skill` tool to read a skill's instructions before using it.\n",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -404,8 +402,12 @@ mod tests {
|
||||
|
||||
let skills = discover_skills_impl(cwd, None);
|
||||
assert_eq!(skills.len(), 1, "duplicate name should be deduplicated");
|
||||
assert_eq!(
|
||||
skills[0].description, "from agents",
|
||||
assert!(
|
||||
skills[0]
|
||||
.path
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.contains(".agents/skills/shared"),
|
||||
"first wins (.agents/)"
|
||||
);
|
||||
// Path should point to the .agents/ version (first wins).
|
||||
@@ -471,9 +473,10 @@ mod tests {
|
||||
result.contains("## Available Skills"),
|
||||
"missing Available Skills"
|
||||
);
|
||||
assert!(result.contains("- buzz-cli\n"), "missing skill name");
|
||||
assert!(
|
||||
result.contains("buzz-cli: CLI reference for Buzz managed agents"),
|
||||
"missing skill bullet"
|
||||
!result.contains("CLI reference for Buzz managed agents"),
|
||||
"skill description must not be inlined in system prompt"
|
||||
);
|
||||
// Body must NOT be inlined — lazy loading only.
|
||||
assert!(
|
||||
@@ -595,8 +598,12 @@ mod tests {
|
||||
|
||||
let skills = discover_skills_impl(cwd.path(), Some(home.path()));
|
||||
assert_eq!(skills.len(), 1, "duplicate name should be deduplicated");
|
||||
assert_eq!(
|
||||
skills[0].description, "from project",
|
||||
assert!(
|
||||
skills[0]
|
||||
.path
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.contains(".agents/skills/shared"),
|
||||
"project-level should win over global"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -248,8 +248,8 @@ async fn hints_suppressed_with_env_var() {
|
||||
h.shutdown().await;
|
||||
}
|
||||
|
||||
/// SKILL.md files in .agents/skills/ are loaded into the system prompt as metadata only.
|
||||
/// The body is NOT inlined; the agent uses `load_skill` to fetch it on demand.
|
||||
/// SKILL.md files in .agents/skills/ are loaded into the system prompt as names only.
|
||||
/// The description and body are NOT inlined; the agent uses `load_skill` to fetch them on demand.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn skills_loaded_from_agents_skills_dir() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
@@ -277,12 +277,16 @@ async fn skills_loaded_from_agents_skills_dir() {
|
||||
let captured = llm.captured.lock().await;
|
||||
assert!(!captured.is_empty(), "no LLM request captured");
|
||||
let system = captured[0]["messages"][0]["content"].as_str().unwrap_or("");
|
||||
// Skill name must appear in the metadata listing.
|
||||
// Skill name must appear in the listing.
|
||||
assert!(
|
||||
system.contains("test-skill"),
|
||||
"system prompt missing skill name: {system}"
|
||||
);
|
||||
// Body must NOT be inlined — lazy loading only.
|
||||
// Description and body must NOT be inlined — lazy loading only.
|
||||
assert!(
|
||||
!system.contains("A test skill"),
|
||||
"skill description must not be inlined in system prompt: {system}"
|
||||
);
|
||||
assert!(
|
||||
!system.contains("SKILL_BODY_MARKER_77"),
|
||||
"skill body must not be inlined in system prompt: {system}"
|
||||
@@ -380,7 +384,7 @@ async fn global_agents_md_loaded() {
|
||||
}
|
||||
|
||||
/// Global skills from ~/.agents/skills/ are loaded; project-level wins on name conflict.
|
||||
/// Bodies are NOT inlined — only metadata (name + description) appears in the system prompt.
|
||||
/// Descriptions and bodies are NOT inlined — only names appear in the system prompt.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn global_skills_loaded_and_project_wins() {
|
||||
let home_tmp = tempfile::TempDir::new().unwrap();
|
||||
@@ -426,7 +430,7 @@ async fn global_skills_loaded_and_project_wins() {
|
||||
let captured = llm.captured.lock().await;
|
||||
assert!(!captured.is_empty(), "no LLM request captured");
|
||||
let system = captured[0]["messages"][0]["content"].as_str().unwrap_or("");
|
||||
// Both skill names must appear in the metadata listing.
|
||||
// Both skill names must appear in the listing.
|
||||
assert!(
|
||||
system.contains("global-only"),
|
||||
"system prompt missing global-only skill name: {system}"
|
||||
@@ -435,10 +439,14 @@ async fn global_skills_loaded_and_project_wins() {
|
||||
system.contains("shared-name"),
|
||||
"system prompt missing shared-name skill: {system}"
|
||||
);
|
||||
// Project description wins over global for the shared name.
|
||||
// Descriptions must NOT be inlined.
|
||||
assert!(
|
||||
system.contains("Project version"),
|
||||
"system prompt should show project description for shared-name: {system}"
|
||||
!system.contains("A global skill"),
|
||||
"system prompt should NOT show global description: {system}"
|
||||
);
|
||||
assert!(
|
||||
!system.contains("Project version"),
|
||||
"system prompt should NOT show project description for shared-name: {system}"
|
||||
);
|
||||
assert!(
|
||||
!system.contains("Global version"),
|
||||
@@ -499,7 +507,7 @@ async fn symlinked_skill_dir_is_discovered() {
|
||||
let captured = llm.captured.lock().await;
|
||||
assert!(!captured.is_empty(), "no LLM request captured");
|
||||
let system = captured[0]["messages"][0]["content"].as_str().unwrap_or("");
|
||||
// The symlinked skill name must appear in the metadata listing.
|
||||
// The symlinked skill name must appear in the listing.
|
||||
assert!(
|
||||
system.contains("symlinked-skill"),
|
||||
"system prompt missing symlinked skill name: {system}"
|
||||
|
||||
Reference in New Issue
Block a user