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:
npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757
2026-07-16 11:09:55 -04:00
parent 2510a4b4c9
commit 72d303fbab
3 changed files with 48 additions and 63 deletions
+12 -42
View File
@@ -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,
+18 -11
View File
@@ -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"
);
}
+18 -10
View File
@@ -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}"