fix(skills): inject skill prompts and tools into agent system prompt

Skill prompts and tool definitions from SKILL.toml were parsed and stored
correctly but never included in the agent's system prompt. Both prompt-building
paths (channels/mod.rs and agent/prompt.rs) only emitted skill metadata (name,
description, location), telling the LLM to "read" the SKILL.toml on demand.
This caused the agent to attempt manual file reads that often failed, leaving
skills effectively ignored.

Now both paths inline <instructions> and <tools> blocks inside each <skill>
XML element, so the agent receives full skill context without extra tool calls.

Closes #877

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Edvard 2026-02-18 22:50:35 -05:00 committed by Chummy
parent 14fb3fbcae
commit 8a4da141d6
2 changed files with 69 additions and 15 deletions

View file

@ -165,13 +165,35 @@ impl PromptSection for SkillsSection {
.join(&skill.name)
.join("SKILL.md")
});
let _ = writeln!(prompt, " <skill>");
let _ = writeln!(prompt, " <name>{}</name>", skill.name);
let _ = writeln!(
prompt,
" <skill>\n <name>{}</name>\n <description>{}</description>\n <location>{}</location>\n </skill>",
skill.name,
skill.description,
location.display()
" <description>{}</description>",
skill.description
);
let _ = writeln!(prompt, " <location>{}</location>", location.display());
if !skill.tools.is_empty() {
let _ = writeln!(prompt, " <tools>");
for tool in &skill.tools {
let _ = writeln!(
prompt,
" <tool name=\"{}\" kind=\"{}\">{}</tool>",
tool.name, tool.kind, tool.description
);
}
let _ = writeln!(prompt, " </tools>");
}
if !skill.prompts.is_empty() {
let _ = writeln!(prompt, " <instructions>");
for p in &skill.prompts {
prompt.push_str(" ");
prompt.push_str(p);
prompt.push('\n');
}
let _ = writeln!(prompt, " </instructions>");
}
let _ = writeln!(prompt, " </skill>");
}
prompt.push_str("</available_skills>");
Ok(prompt)

View file

@ -1096,12 +1096,9 @@ pub fn build_system_prompt(
- When in doubt, ask before acting externally.\n\n",
);
// ── 3. Skills (compact list — load on-demand) ───────────────
// ── 3. Skills (inline prompts + tools) ──────────────────────
if !skills.is_empty() {
prompt.push_str("## Available Skills\n\n");
prompt.push_str(
"Skills are loaded on demand. Use `read` on the skill path to get full instructions.\n\n",
);
prompt.push_str("<available_skills>\n");
for skill in skills {
let _ = writeln!(prompt, " <skill>");
@ -1118,6 +1115,26 @@ pub fn build_system_prompt(
.join("SKILL.md")
});
let _ = writeln!(prompt, " <location>{}</location>", location.display());
if !skill.tools.is_empty() {
let _ = writeln!(prompt, " <tools>");
for tool in &skill.tools {
let _ = writeln!(
prompt,
" <tool name=\"{}\" kind=\"{}\">{}</tool>",
tool.name, tool.kind, tool.description
);
}
let _ = writeln!(prompt, " </tools>");
}
if !skill.prompts.is_empty() {
let _ = writeln!(prompt, " <instructions>");
for p in &skill.prompts {
prompt.push_str(" ");
prompt.push_str(p);
prompt.push('\n');
}
let _ = writeln!(prompt, " </instructions>");
}
let _ = writeln!(prompt, " </skill>");
}
prompt.push_str("</available_skills>\n\n");
@ -3263,7 +3280,7 @@ mod tests {
}
#[test]
fn prompt_skills_compact_list() {
fn prompt_skills_inline_prompts_and_tools() {
let ws = make_workspace();
let skills = vec![crate::skills::Skill {
name: "code-review".into(),
@ -3271,8 +3288,14 @@ mod tests {
version: "1.0.0".into(),
author: None,
tags: vec![],
tools: vec![],
prompts: vec!["Long prompt content that should NOT appear in system prompt".into()],
tools: vec![crate::skills::SkillTool {
name: "run_linter".into(),
description: "Run linter on code".into(),
kind: "shell".into(),
command: "cargo clippy".into(),
args: std::collections::HashMap::new(),
}],
prompts: vec!["When reviewing code, check for common bugs and style issues.".into()],
location: None,
}];
@ -3282,12 +3305,21 @@ mod tests {
assert!(prompt.contains("<name>code-review</name>"));
assert!(prompt.contains("<description>Review code for bugs</description>"));
assert!(prompt.contains("SKILL.md</location>"));
// Skill prompts should be inlined
assert!(
prompt.contains("loaded on demand"),
"should mention on-demand loading"
prompt.contains("When reviewing code, check for common bugs"),
"skill prompt should be inlined in system prompt"
);
// Full prompt content should NOT be dumped
assert!(!prompt.contains("Long prompt content that should NOT appear"));
assert!(
prompt.contains("<instructions>"),
"should have instructions block"
);
// Skill tools should be inlined
assert!(
prompt.contains("run_linter"),
"skill tool should be inlined"
);
assert!(prompt.contains("<tools>"), "should have tools block");
}
#[test]