技能系統
skills.ts 實作 Agent Skills 規範(見 https://agentskills.io/integrate-skills)。技能是 markdown 檔案,帶 frontmatter 描述技能用途,LLM 在系統提示裡看到技能列表後,透過 read 工具讀取對應 SKILL.md 取得詳細指令。這個檔案負責技能的發現、載入、校驗、去重、格式化為 prompt 段落。
職責
- 發現規則:
loadSkillsFromDirInternal遞迴掃目錄——遇到SKILL.md視為技能根不再遞迴,否則掃子目錄和根.md檔案。見packages/coding-agent/src/core/skills.ts:173-280。 - frontmatter 校驗:
validateName檢查名稱(a-z0-9-、不超 64 字元、不連字號開頭/結尾、無--、與父目錄名一致),validateDescription檢查描述必填且不超 1024 字元。見packages/coding-agent/src/core/skills.ts:93-117、packages/coding-agent/src/core/skills.ts:122-132。 - 多來源載入:
loadSkills從使用者全域(~/.pi/agent/skills)、專案(./.pi/skills)、顯式skillPaths三個來源載入,衝突時保留先註冊的並產生collision診斷。見packages/coding-agent/src/core/skills.ts:405-504。 - 去重:用
realpath解析符號連結,同一檔案透過不同路徑註冊只算一次;同名技能先到先得。見packages/coding-agent/src/core/skills.ts:416-445。 - prompt 格式化:
formatSkillsForPrompt產生<available_skills>XML 段落,每個技能<name>/<description>/<location>三元組,disableModelInvocation: true的技能不進 prompt(只能透過/skill:name顯式呼叫)。見packages/coding-agent/src/core/skills.ts:340-366。 - ignore 規則:尊重
.gitignore/.ignore/.fdignore,遞迴掃描時按目錄前綴拼路徑模式。見packages/coding-agent/src/core/skills.ts:48-66、packages/coding-agent/src/core/skills.ts:25-46。
設計動機
為什麼用 SKILL.md 而不是直接掃所有 .md?因為一個技能可能包含多個檔案(腳本、子文件),SKILL.md 是入口標誌。遇到 SKILL.md 後停止遞迴,把整個目錄當成技能包,讓技能作者可以組織多個檔案而不被當作多個獨立技能。
為什麼用 frontmatter 而不是註解?因為 frontmatter 是 YAML,結構化、可解析、可校驗。名稱、描述、disable-model-invocation 這些欄位需要 LLM 和程式都讀得懂。validateName 強制名稱與父目錄名一致(name "x" does not match parent directory "y"),避免技能檔案被移動後名稱漂移。
為什麼需要 disableModelInvocation?有些技能是使用者自用的提示範本,不應該被 LLM 自動觸發(比如「寫程式時用這個風格」),只在使用者顯式 /skill:name 時才載入。formatSkillsForPrompt 過濾掉這類技能,prompt 裡只暴露允許 LLM 自動呼叫的。
為什麼用 realpath 去重?因為使用者可能透過 --skills ./my-skills 同時又把 ./my-skills 軟鏈到 ~/.pi/agent/skills/my-skills,兩次掃描會拿到同一檔案的不同路徑。canonicalizePath 解析符號連結後比較,避免重複載入同一技能。
關鍵檔案
packages/coding-agent/src/core/skills.ts:11-19— 常數:MAX_NAME_LENGTH64、MAX_DESCRIPTION_LENGTH1024、IGNORE_FILE_NAMES。packages/coding-agent/src/core/skills.ts:21-46—toPosixPath和prefixIgnorePattern,把子目錄的 ignore 規則加上目錄前綴。packages/coding-agent/src/core/skills.ts:75-87—Skill介面:name、description、filePath、baseDir、sourceInfo、disableModelInvocation。packages/coding-agent/src/core/skills.ts:93-117—validateName,5 條規則校驗。packages/coding-agent/src/core/skills.ts:173-280—loadSkillsFromDirInternal,遞迴掃描與 ignore 過濾。packages/coding-agent/src/core/skills.ts:282-330—loadSkillFromFile,frontmatter 解析與校驗。packages/coding-agent/src/core/skills.ts:340-366—formatSkillsForPrompt,XML 段落產生。packages/coding-agent/src/core/skills.ts:405-504—loadSkills,多來源載入與去重。
formatSkillsForPrompt 用 XML 標籤包裹技能列表,跳脫 XML 特殊字元:
// 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 遇到 SKILL.md 立即回傳,不再遞迴:
// 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 };
}資料流
技能從磁碟到 prompt:
邊界與失敗
- 無描述則拒絕載入:
frontmatter.description為空或 trim 後為空時回傳{ skill: null },不進 skillMap,見packages/coding-agent/src/core/skills.ts:310-312。 - 名稱不符仍載入:名稱校驗只產生 warning 診斷,不阻止載入,讓使用者能看到問題但技能仍可用,見
packages/coding-agent/src/core/skills.ts:300-307。 - 損壞符號連結:
statSync拋錯時continue跳過,不中斷整個掃描,見packages/coding-agent/src/core/skills.ts:207-213、packages/coding-agent/src/core/skills.ts:241-252。 - node_modules 跳過:遞迴時顯式跳過
node_modules,避免掃相依套件裡的.md,見packages/coding-agent/src/core/skills.ts:233-236。 - path 類型解析:顯式
skillPaths可以是檔案或目錄,目錄走loadSkillsFromDirInternal,檔案必須是.md,見packages/coding-agent/src/core/skills.ts:478-498。 - source 識別:顯式 path 在
includeDefaults: false時仍嘗試識別為 user/project 來源(基於路徑前綴),否則標為path,見packages/coding-agent/src/core/skills.ts:464-470。
小結
skills.ts 實作 Agent Skills 規範,三來源載入(使用者全域、專案、顯式路徑),frontmatter 校驗,realpath 去重,XML 格式化注入 prompt。技能被系統提示引用後透過 read 工具讀取,看 系統提示建構 和 工具集 read/bash/edit/write/grep/find/ls。組裝技能路徑的入口看 CLI 入口與分派。