Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions prompts/en/fragments/skills_branch.md.j2
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
## Available Skills

These skills are procedures this agent knows. When one is relevant to your reasoning, call `read_skill` to load its full instructions.
These skills are procedures this agent knows. Scan this index before acting and call `read_skill` for any skill that is even partially relevant — a skill defines how its task class is done here, even when the task looks familiar. Prefer reading an unnecessary skill over missing established procedure.

When you spawn a worker for a task that matches a skill, pass the skill names as `suggested_skills` instead of inlining the skill's content into the task description — the worker reads the skills it needs itself.

<available_skills>
{%- for skill in skills %}
<skill>
<name>{{ skill.name }}</name>
<description>{{ skill.description }}</description>
</skill>
{%- for category in categories %}
<category name="{{ category.name }}"{% if category.description %} description="{{ category.description }}"{% endif %}>
{%- for skill in category.skills %}
<skill>
<name>{{ skill.name }}</name>
<description>{{ skill.description }}</description>
</skill>
{%- endfor %}
</category>
{%- endfor %}
</available_skills>
16 changes: 10 additions & 6 deletions prompts/en/fragments/skills_channel.md.j2
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## Available Skills

You have access to the following skills. When a user's request matches one or more skills, spawn a worker and pass the relevant skill names as `suggested_skills`. The worker will read the skills it needs automatically.
You have access to the following skills. Before responding, scan this index for any skill that is even partially relevant to the user's request — prefer reading an unnecessary skill over missing an established procedure. When a request matches one or more skills, spawn a worker and pass the relevant skill names as `suggested_skills`. The worker will read the skills it needs automatically.

**Do not include implementation details, tool invocations, or protocol instructions in the task description.** Describe *what* to do, not *how*. The worker reads the skill for the how.

Expand All @@ -9,10 +9,14 @@ Example: `spawn_worker(task="Generate a 30-second downtempo track with these lyr
You may suggest multiple skills if the task spans more than one: `suggested_skills=["github", "coding-agent"]`

<available_skills>
{%- for skill in skills %}
<skill>
<name>{{ skill.name }}</name>
<description>{{ skill.description }}</description>
</skill>
{%- for category in categories %}
<category name="{{ category.name }}"{% if category.description %} description="{{ category.description }}"{% endif %}>
{%- for skill in category.skills %}
<skill>
<name>{{ skill.name }}</name>
<description>{{ skill.description }}</description>
</skill>
{%- endfor %}
</category>
{%- endfor %}
</available_skills>
16 changes: 10 additions & 6 deletions prompts/en/fragments/skills_worker.md.j2
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
## Available Skills

You have access to the following skills. Before starting your task, scan the list and call `read_skill` for any skill that is relevant — you may read more than one.
You have access to the following skills. Before starting your task, scan the list and call `read_skill` for any skill that is even partially relevant — a skill defines how its task class is done here, even when the task looks familiar. Prefer reading an unnecessary skill over missing established procedure. You may read more than one.

Skills marked as **suggested** were recommended by the channel for this specific task. Read those first, then decide if any others apply.

<available_skills>
{%- for skill in skills %}
<skill{% if skill.suggested %} suggested="true"{% endif %}>
<name>{{ skill.name }}</name>
<description>{{ skill.description }}</description>
</skill>
{%- for category in categories %}
<category name="{{ category.name }}"{% if category.description %} description="{{ category.description }}"{% endif %}>
{%- for skill in category.skills %}
<skill{% if skill.suggested %} suggested="true"{% endif %}>
<name>{{ skill.name }}</name>
<description>{{ skill.description }}</description>
</skill>
{%- endfor %}
</category>
{%- endfor %}
</available_skills>
2 changes: 2 additions & 0 deletions src/api/skills.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub(super) struct SkillInfo {
source: String,
#[serde(skip_serializing_if = "Option::is_none")]
source_repo: Option<String>,
category: String,
}

#[derive(Serialize, utoipa::ToSchema)]
Expand Down Expand Up @@ -262,6 +263,7 @@ pub(super) async fn list_skills(
crate::skills::SkillSource::Workspace => "workspace".to_string(),
},
source_repo: s.source_repo,
category: s.category,
})
.collect();

Expand Down
68 changes: 62 additions & 6 deletions src/prompts/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,11 +329,16 @@ impl PromptEngine {
}

/// Convenience method for rendering skills channel fragment.
pub fn render_skills_channel(&self, skills: Vec<SkillInfo>) -> Result<String> {
pub fn render_skills_channel(
&self,
skills: Vec<SkillInfo>,
category_descriptions: &std::collections::HashMap<String, String>,
) -> Result<String> {
let categories = group_skills_by_category(skills, category_descriptions);
self.render(
"fragments/skills_channel",
context! {
skills => skills,
categories => categories,
},
)
}
Expand Down Expand Up @@ -361,11 +366,16 @@ impl PromptEngine {
///
/// Branches read skills directly via `read_skill` or pass names to
/// spawned workers as `suggested_skills`.
pub fn render_skills_branch(&self, skills: Vec<SkillInfo>) -> Result<String> {
pub fn render_skills_branch(
&self,
skills: Vec<SkillInfo>,
category_descriptions: &std::collections::HashMap<String, String>,
) -> Result<String> {
let categories = group_skills_by_category(skills, category_descriptions);
self.render(
"fragments/skills_branch",
context! {
skills => skills,
categories => categories,
},
)
}
Expand Down Expand Up @@ -436,11 +446,16 @@ impl PromptEngine {
///
/// Workers see all available skills with suggestions from the channel flagged.
/// They read whichever skills they need via the read_skill tool.
pub fn render_skills_worker(&self, skills: Vec<SkillInfo>) -> Result<String> {
pub fn render_skills_worker(
&self,
skills: Vec<SkillInfo>,
category_descriptions: &std::collections::HashMap<String, String>,
) -> Result<String> {
let categories = group_skills_by_category(skills, category_descriptions);
self.render(
"fragments/skills_worker",
context! {
skills => skills,
categories => categories,
},
)
}
Expand Down Expand Up @@ -914,6 +929,47 @@ pub struct SkillInfo {
/// Whether the spawning channel suggested this skill for the current task.
/// Workers should prioritise suggested skills but may read others too.
pub suggested: bool,
/// Category derived from the directory path.
pub category: String,
}

/// Group of skills under one category for grouped index rendering.
#[derive(Debug, Clone, serde::Serialize)]
pub struct SkillCategoryGroup {
pub name: String,
/// Description from the category's `index.md`, if any.
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub skills: Vec<SkillInfo>,
}

/// Group skills by category, sorted by category name then skill name within
/// each group. Category descriptions come from `index.md` files loaded
/// during discovery.
fn group_skills_by_category(
skills: Vec<SkillInfo>,
category_descriptions: &std::collections::HashMap<String, String>,
) -> Vec<SkillCategoryGroup> {
let mut groups: std::collections::BTreeMap<String, Vec<SkillInfo>> =
std::collections::BTreeMap::new();
for skill in skills {
groups
.entry(skill.category.clone())
.or_default()
.push(skill);
}
groups
.into_iter()
.map(|(name, mut skills)| {
skills.sort_by(|a, b| a.name.cmp(&b.name));
let description = category_descriptions.get(&name).cloned();
SkillCategoryGroup {
name,
description,
skills,
}
})
.collect()
}

/// Information about a channel for template rendering.
Expand Down
Loading
Loading