Support for Built-in Agent Skills (#16045)

This commit is contained in:
N. Taylor Mullen
2026-01-09 22:26:58 -08:00
committed by GitHub
parent b54e688c75
commit 461c277bf2
17 changed files with 755 additions and 451 deletions
+2
View File
@@ -25,6 +25,8 @@ export interface SkillDefinition {
body: string;
/** Whether the skill is currently disabled. */
disabled?: boolean;
/** Whether the skill is a built-in skill. */
isBuiltin?: boolean;
}
const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)/;
@@ -163,4 +163,45 @@ description: desc1
expect(service.getAllSkills()).toHaveLength(1);
expect(service.getAllSkills()[0].disabled).toBe(true);
});
it('should filter built-in skills in getDisplayableSkills', async () => {
const service = new SkillManager();
// @ts-expect-error accessing private property for testing
service.skills = [
{
name: 'regular-skill',
description: 'regular',
location: 'loc1',
body: 'body',
isBuiltin: false,
},
{
name: 'builtin-skill',
description: 'builtin',
location: 'loc2',
body: 'body',
isBuiltin: true,
},
{
name: 'disabled-builtin',
description: 'disabled builtin',
location: 'loc3',
body: 'body',
isBuiltin: true,
disabled: true,
},
];
const displayable = service.getDisplayableSkills();
expect(displayable).toHaveLength(1);
expect(displayable[0].name).toBe('regular-skill');
const all = service.getAllSkills();
expect(all).toHaveLength(3);
const enabled = service.getSkills();
expect(enabled).toHaveLength(2);
expect(enabled.map((s) => s.name)).toContain('builtin-skill');
});
});
+31 -5
View File
@@ -31,24 +31,36 @@ export class SkillManager {
): Promise<void> {
this.clearSkills();
// 1. Extension skills (lowest precedence)
// 1. Built-in skills (lowest precedence)
await this.discoverBuiltinSkills();
// 2. Extension skills
for (const extension of extensions) {
if (extension.isActive && extension.skills) {
this.addSkillsWithPrecedence(extension.skills);
}
}
// 2. User skills
// 3. User skills
const userSkills = await loadSkillsFromDir(Storage.getUserSkillsDir());
this.addSkillsWithPrecedence(userSkills);
// 3. Project skills (highest precedence)
// 4. Project skills (highest precedence)
const projectSkills = await loadSkillsFromDir(
storage.getProjectSkillsDir(),
);
this.addSkillsWithPrecedence(projectSkills);
}
/**
* Discovers built-in skills.
*/
private async discoverBuiltinSkills(): Promise<void> {
// Built-in skills can be added here.
// For now, this is a placeholder for where built-in skills will be loaded from.
// They could be loaded from a specific directory within the package.
}
private addSkillsWithPrecedence(newSkills: SkillDefinition[]): void {
const skillMap = new Map<string, SkillDefinition>();
for (const skill of [...this.skills, ...newSkills]) {
@@ -64,6 +76,14 @@ export class SkillManager {
return this.skills.filter((s) => !s.disabled);
}
/**
* Returns the list of enabled discovered skills that should be displayed in the UI.
* This excludes built-in skills.
*/
getDisplayableSkills(): SkillDefinition[] {
return this.skills.filter((s) => !s.disabled && !s.isBuiltin);
}
/**
* Returns all discovered skills, including disabled ones.
*/
@@ -82,8 +102,11 @@ export class SkillManager {
* Sets the list of disabled skill names.
*/
setDisabledSkills(disabledNames: string[]): void {
const lowercaseDisabledNames = disabledNames.map((n) => n.toLowerCase());
for (const skill of this.skills) {
skill.disabled = disabledNames.includes(skill.name);
skill.disabled = lowercaseDisabledNames.includes(
skill.name.toLowerCase(),
);
}
}
@@ -91,7 +114,10 @@ export class SkillManager {
* Reads the full content (metadata + body) of a skill by name.
*/
getSkill(name: string): SkillDefinition | null {
return this.skills.find((s) => s.name === name) ?? null;
const lowercaseName = name.toLowerCase();
return (
this.skills.find((s) => s.name.toLowerCase() === lowercaseName) ?? null
);
}
/**
@@ -76,6 +76,34 @@ describe('ActivateSkillTool', () => {
expect(details.prompt).toContain('Mock folder structure');
});
it('should skip confirmation for built-in skills', async () => {
const builtinSkill = {
name: 'builtin-skill',
description: 'A built-in skill',
location: '/path/to/builtin/SKILL.md',
isBuiltin: true,
body: 'Built-in instructions',
};
vi.mocked(mockConfig.getSkillManager().getSkill).mockReturnValue(
builtinSkill,
);
vi.mocked(mockConfig.getSkillManager().getSkills).mockReturnValue([
builtinSkill,
]);
const params = { name: 'builtin-skill' };
const toolWithBuiltin = new ActivateSkillTool(mockConfig, mockMessageBus);
const invocation = toolWithBuiltin.build(params);
const details = await (
invocation as unknown as {
getConfirmationDetails: (signal: AbortSignal) => Promise<unknown>;
}
).getConfirmationDetails(new AbortController().signal);
expect(details).toBe(false);
});
it('should activate a valid skill and return its content in XML tags', async () => {
const params = { name: 'test-skill' };
const invocation = tool.build(params);
@@ -80,6 +80,10 @@ class ActivateSkillToolInvocation extends BaseToolInvocation<
return false;
}
if (skill.isBuiltin) {
return false;
}
const folderStructure = await this.getOrFetchFolderStructure(
skill.location,
);