Skip to content

Système de skills

源码版本v0.73.1

skills.ts implémente la spec Agent Skills (voir https://agentskills.io/integrate-skills). Une skill est un fichier markdown avec un frontmatter qui décrit son usage ; le LLM voit la liste des skills dans le system prompt, puis utilise l'outil read pour lire le SKILL.md correspondant et obtenir des instructions détaillées. Ce fichier gère la découverte, le chargement, la validation, la déduplication et le formatage en bloc de prompt des skills.

Responsabilités

  1. Règles de découverte : loadSkillsFromDirInternal scanne récursivement un répertoire — quand il rencontre SKILL.md, il le considère comme la racine d'une skill et ne descend plus ; sinon, il descend dans les sous-répertoires et scanne les fichiers .md à la racine. Voir packages/coding-agent/src/core/skills.ts:173-280.
  2. Validation du frontmatter : validateName vérifie le nom (a-z0-9-, ≤ 64 caractères, ni ne commence ni ne finit par un tiret, pas de --, identique au nom du répertoire parent) ; validateDescription vérifie que la description est obligatoire et ≤ 1024 caractères. Voir packages/coding-agent/src/core/skills.ts:93-117 et packages/coding-agent/src/core/skills.ts:122-132.
  3. Chargement multi-source : loadSkills charge depuis trois sources — global utilisateur (~/.pi/agent/skills), projet (./.pi/skills), et skillPaths explicites ; en cas de conflit, le premier enregistré l'emporte et un diagnostic collision est produit. Voir packages/coding-agent/src/core/skills.ts:405-504.
  4. Déduplication : utilise realpath pour résoudre les liens symboliques ; un même fichier enregistré via des chemins différents ne compte qu'une fois ; en cas de skills homonymes, premier arrivé premier servi. Voir packages/coding-agent/src/core/skills.ts:416-445.
  5. Formatage du prompt : formatSkillsForPrompt génère un bloc XML <available_skills>, avec un triplet <name>/<description>/<location> par skill ; les skills disableModelInvocation: true n'entrent pas dans le prompt (elles ne peuvent être appelées que via /skill:name explicite). Voir packages/coding-agent/src/core/skills.ts:340-366.
  6. Règles d'ignore : respecte .gitignore / .ignore / .fdignore ; le scan récursif préfixe les motifs par le chemin du répertoire. Voir packages/coding-agent/src/core/skills.ts:48-66 et packages/coding-agent/src/core/skills.ts:25-46.

Motifs de conception

Pourquoi SKILL.md plutôt que de scanner tous les .md ? Parce qu'une skill peut contenir plusieurs fichiers (scripts, sous-docs) : SKILL.md est le marqueur d'entrée. Une fois SKILL.md rencontré, on arrête la récursion et on traite le répertoire entier comme un pack de skills, ce qui permet à un auteur d'organiser plusieurs fichiers sans qu'ils soient vus comme plusieurs skills indépendantes.

Pourquoi un frontmatter plutôt que des commentaires ? Parce que le frontmatter est du YAML, structuré, parsable, validable. Les champs name, description, disable-model-invocation doivent être lisibles tant par le LLM que par le programme. validateName force le nom à correspondre au nom du répertoire parent (name "x" does not match parent directory "y"), pour éviter que le nom ne dérive quand le fichier de skill est déplacé.

Pourquoi disableModelInvocation ? Certaines skills sont des templates de prompt à usage utilisateur et ne doivent pas être déclenchées automatiquement par le LLM (par ex. « quand tu écris du code, utilise ce style ») ; elles ne se chargent que lorsque l'utilisateur fait explicitement /skill:name. formatSkillsForPrompt filtre ces skills, le prompt n'expose que celles que le LLM peut appeler de lui-même.

Pourquoi dédupliquer via realpath ? Parce que l'utilisateur peut passer --skills ./my-skills tout en ayant symlinké ./my-skills vers ~/.pi/agent/skills/my-skills ; deux scans ramèneraient le même fichier via des chemins différents. canonicalizePath résout les liens symboliques avant comparaison, pour éviter de charger deux fois la même skill.

Fichiers clés

formatSkillsForPrompt enveloppe la liste des skills dans des balises XML et échappe les caractères spéciaux :

typescript
// packages/coding-agent/src/core/skills.ts:340-366
export function formatSkillsForPrompt(skills: Skill[]): string {
	const visibleSkills = skills.filter((s) => !s.disableModelInvocation);

	if (visibleSkills.length === 0) {
		return "";
	}

	const lines = [
		"\n\nThe following skills provide specialized instructions for specific tasks.",
		"Use the read tool to load a skill's file when the task matches its description.",
		"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
		"",
		"<available_skills>",
	];

	for (const skill of visibleSkills) {
		lines.push("  <skill>");
		lines.push(`    <name>${escapeXml(skill.name)}</name>`);
		lines.push(`    <description>${escapeXml(skill.description)}</description>`);
		lines.push(`    <location>${escapeXml(skill.filePath)}</location>`);
		lines.push("  </skill>");
	}
	lines.push("</available_skills>");
	return lines.join("\n");
}

loadSkillsFromDirInternal retourne immédiatement dès qu'il rencontre SKILL.md, sans récuser plus loin :

typescript
// packages/coding-agent/src/core/skills.ts:199-226
for (const entry of entries) {
	if (entry.name !== "SKILL.md") {
		continue;
	}
	const fullPath = join(dir, entry.name);
	let isFile = entry.isFile();
	if (entry.isSymbolicLink()) {
		try {
			isFile = statSync(fullPath).isFile();
		} catch {
			continue;
		}
	}
	const relPath = toPosixPath(relative(root, fullPath));
	if (!isFile || ig.ignores(relPath)) {
		continue;
	}
	const result = loadSkillFromFile(fullPath, source);
	if (result.skill) {
		skills.push(result.skill);
	}
	diagnostics.push(...result.diagnostics);
	return { skills, diagnostics };
}

Flux de données

Une skill du disque jusqu'au prompt :

Limites et cas d'échec

Synthèse

skills.ts implémente la spec Agent Skills : chargement depuis trois sources (global utilisateur, projet, chemin explicite), validation du frontmatter, déduplication par realpath, formatage en XML injecté dans le prompt. Une skill référencée par le system prompt est ensuite lue via l'outil read — voir construction du system prompt et ensemble d'outils read/bash/edit/write/grep/find/ls. L'entrée qui assemble les chemins de skills est dans entrée CLI et dispatch.