diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index 74037c1f7c..8fee660edd 100644
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -24,6 +24,7 @@ import { ReadFileTool } from '../tools/read-file.js';
import { GrepTool } from '../tools/grep.js';
import { canUseRipgrep, RipGrepTool } from '../tools/ripGrep.js';
import { GlobTool } from '../tools/glob.js';
+import { ActivateSkillTool } from '../tools/activate-skill.js';
import { EditTool } from '../tools/edit.js';
import { SmartEditTool } from '../tools/smart-edit.js';
import { ShellTool } from '../tools/shell.js';
@@ -737,6 +738,13 @@ export class Config {
if (this.skillsSupport) {
await this.getSkillManager().discoverSkills(this.storage);
this.getSkillManager().setDisabledSkills(this.disabledSkills);
+
+ // Re-register ActivateSkillTool to update its schema with the discovered enabled skill enums
+ if (this.getSkillManager().getSkills().length > 0) {
+ this.getToolRegistry().registerTool(
+ new ActivateSkillTool(this, this.messageBus),
+ );
+ }
}
// Initialize hook system if enabled
@@ -1690,6 +1698,7 @@ export class Config {
}
registerCoreTool(GlobTool, this);
+ registerCoreTool(ActivateSkillTool, this);
if (this.getUseSmartEdit()) {
registerCoreTool(SmartEditTool, this);
} else {
diff --git a/packages/core/src/policy/policies/write.toml b/packages/core/src/policy/policies/write.toml
index 09387b59c1..991424cebc 100644
--- a/packages/core/src/policy/policies/write.toml
+++ b/packages/core/src/policy/policies/write.toml
@@ -56,6 +56,11 @@ toolName = "write_file"
decision = "ask_user"
priority = 10
+[[rule]]
+toolName = "activate_skill"
+decision = "ask_user"
+priority = 10
+
[[rule]]
toolName = "write_file"
decision = "allow"
diff --git a/packages/core/src/tools/activate-skill.test.ts b/packages/core/src/tools/activate-skill.test.ts
new file mode 100644
index 0000000000..80f4dc6885
--- /dev/null
+++ b/packages/core/src/tools/activate-skill.test.ts
@@ -0,0 +1,118 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { ActivateSkillTool } from './activate-skill.js';
+import type { Config } from '../config/config.js';
+import type { MessageBus } from '../confirmation-bus/message-bus.js';
+
+vi.mock('../utils/getFolderStructure.js', () => ({
+ getFolderStructure: vi.fn().mockResolvedValue('Mock folder structure'),
+}));
+
+describe('ActivateSkillTool', () => {
+ let mockConfig: Config;
+ let tool: ActivateSkillTool;
+ const mockMessageBus = {
+ publish: vi.fn(),
+ subscribe: vi.fn(),
+ unsubscribe: vi.fn(),
+ } as unknown as MessageBus;
+
+ beforeEach(() => {
+ const skills = [
+ {
+ name: 'test-skill',
+ description: 'A test skill',
+ location: '/path/to/test-skill/SKILL.md',
+ },
+ ];
+ mockConfig = {
+ getSkillManager: vi.fn().mockReturnValue({
+ getSkills: vi.fn().mockReturnValue(skills),
+ getAllSkills: vi.fn().mockReturnValue(skills),
+ getSkill: vi.fn().mockImplementation((name: string) => {
+ if (name === 'test-skill') {
+ return {
+ name: 'test-skill',
+ description: 'A test skill',
+ location: '/path/to/test-skill/SKILL.md',
+ body: 'Skill instructions content.',
+ };
+ }
+ return null;
+ }),
+ activateSkill: vi.fn(),
+ }),
+ } as unknown as Config;
+ tool = new ActivateSkillTool(mockConfig, mockMessageBus);
+ });
+
+ it('should return enhanced description', () => {
+ const params = { name: 'test-skill' };
+ const invocation = tool.build(params);
+ expect(invocation.getDescription()).toBe('"test-skill": A test skill');
+ });
+
+ it('should return enhanced confirmation details', async () => {
+ const params = { name: 'test-skill' };
+ const invocation = tool.build(params);
+ const details = await (
+ invocation as unknown as {
+ getConfirmationDetails: (signal: AbortSignal) => Promise<{
+ prompt: string;
+ title: string;
+ }>;
+ }
+ ).getConfirmationDetails(new AbortController().signal);
+
+ expect(details.title).toBe('Activate Skill: test-skill');
+ expect(details.prompt).toContain('enable the specialized agent skill');
+ expect(details.prompt).toContain('A test skill');
+ expect(details.prompt).toContain('Mock folder structure');
+ });
+
+ it('should activate a valid skill and return its content in XML tags', async () => {
+ const params = { name: 'test-skill' };
+ const invocation = tool.build(params);
+ const result = await invocation.execute(new AbortController().signal);
+
+ expect(mockConfig.getSkillManager().activateSkill).toHaveBeenCalledWith(
+ 'test-skill',
+ );
+ expect(result.llmContent).toContain('');
+ expect(result.llmContent).toContain('');
+ expect(result.llmContent).toContain('Skill instructions content.');
+ expect(result.llmContent).toContain('');
+ expect(result.llmContent).toContain('');
+ expect(result.llmContent).toContain('Mock folder structure');
+ expect(result.llmContent).toContain('');
+ expect(result.llmContent).toContain('');
+ expect(result.returnDisplay).toContain('Skill **test-skill** activated');
+ expect(result.returnDisplay).toContain('Mock folder structure');
+ });
+
+ it('should throw error if skill is not in enum', async () => {
+ const params = { name: 'non-existent' };
+ expect(() => tool.build(params as { name: string })).toThrow();
+ });
+
+ it('should return an error if skill content cannot be read', async () => {
+ vi.mocked(mockConfig.getSkillManager().getSkill).mockReturnValue(null);
+ const params = { name: 'test-skill' };
+ const invocation = tool.build(params);
+ const result = await invocation.execute(new AbortController().signal);
+
+ expect(result.llmContent).toContain('Error: Skill "test-skill" not found.');
+ expect(mockConfig.getSkillManager().activateSkill).not.toHaveBeenCalled();
+ });
+
+ it('should validate that name is provided', () => {
+ expect(() =>
+ tool.build({ name: '' } as unknown as { name: string }),
+ ).toThrow();
+ });
+});
diff --git a/packages/core/src/tools/activate-skill.ts b/packages/core/src/tools/activate-skill.ts
new file mode 100644
index 0000000000..afea50316c
--- /dev/null
+++ b/packages/core/src/tools/activate-skill.ts
@@ -0,0 +1,192 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { z } from 'zod';
+import { zodToJsonSchema } from 'zod-to-json-schema';
+import * as path from 'node:path';
+import { getFolderStructure } from '../utils/getFolderStructure.js';
+import type { MessageBus } from '../confirmation-bus/message-bus.js';
+import type {
+ ToolResult,
+ ToolCallConfirmationDetails,
+ ToolInvocation,
+ ToolConfirmationOutcome,
+} from './tools.js';
+import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
+import type { Config } from '../config/config.js';
+import { ACTIVATE_SKILL_TOOL_NAME } from './tool-names.js';
+
+/**
+ * Parameters for the ActivateSkill tool
+ */
+export interface ActivateSkillToolParams {
+ /**
+ * The name of the skill to activate
+ */
+ name: string;
+}
+
+class ActivateSkillToolInvocation extends BaseToolInvocation<
+ ActivateSkillToolParams,
+ ToolResult
+> {
+ private cachedFolderStructure: string | undefined;
+
+ constructor(
+ private config: Config,
+ params: ActivateSkillToolParams,
+ messageBus: MessageBus | undefined,
+ _toolName?: string,
+ _toolDisplayName?: string,
+ ) {
+ super(params, messageBus, _toolName, _toolDisplayName);
+ }
+
+ getDescription(): string {
+ const skillName = this.params.name;
+ const skill = this.config.getSkillManager().getSkill(skillName);
+ if (skill) {
+ return `"${skillName}": ${skill.description}`;
+ }
+ return `"${skillName}" (⚠️ unknown skill)`;
+ }
+
+ private async getOrFetchFolderStructure(
+ skillLocation: string,
+ ): Promise {
+ if (this.cachedFolderStructure === undefined) {
+ this.cachedFolderStructure = await getFolderStructure(
+ path.dirname(skillLocation),
+ );
+ }
+ return this.cachedFolderStructure;
+ }
+
+ protected override async getConfirmationDetails(
+ _abortSignal: AbortSignal,
+ ): Promise {
+ if (!this.messageBus) {
+ return false;
+ }
+
+ const skillName = this.params.name;
+ const skill = this.config.getSkillManager().getSkill(skillName);
+
+ if (!skill) {
+ return false;
+ }
+
+ const folderStructure = await this.getOrFetchFolderStructure(
+ skill.location,
+ );
+
+ const confirmationDetails: ToolCallConfirmationDetails = {
+ type: 'info',
+ title: `Activate Skill: ${skillName}`,
+ prompt: `You are about to enable the specialized agent skill **${skillName}**.
+
+**Description:**
+${skill.description}
+
+**Resources to be shared with the model:**
+${folderStructure}`,
+ onConfirm: async (outcome: ToolConfirmationOutcome) => {
+ await this.publishPolicyUpdate(outcome);
+ },
+ };
+ return confirmationDetails;
+ }
+
+ async execute(_signal: AbortSignal): Promise {
+ const skillName = this.params.name;
+ const skillManager = this.config.getSkillManager();
+ const skill = skillManager.getSkill(skillName);
+
+ if (!skill) {
+ const skills = skillManager.getSkills();
+ return {
+ llmContent: `Error: Skill "${skillName}" not found. Available skills are: ${skills.map((s) => s.name).join(', ')}`,
+ returnDisplay: `Skill "${skillName}" not found.`,
+ };
+ }
+
+ skillManager.activateSkill(skillName);
+
+ const folderStructure = await this.getOrFetchFolderStructure(
+ skill.location,
+ );
+
+ return {
+ llmContent: `
+
+ ${skill.body}
+
+
+
+ ${folderStructure}
+
+`,
+ returnDisplay: `Skill **${skillName}** activated. Resources loaded from \`${path.dirname(skill.location)}\`:\n\n${folderStructure}`,
+ };
+ }
+}
+
+/**
+ * Implementation of the ActivateSkill tool logic
+ */
+export class ActivateSkillTool extends BaseDeclarativeTool<
+ ActivateSkillToolParams,
+ ToolResult
+> {
+ static readonly Name = ACTIVATE_SKILL_TOOL_NAME;
+
+ constructor(
+ private config: Config,
+ messageBus?: MessageBus,
+ ) {
+ const skills = config.getSkillManager().getSkills();
+ const skillNames = skills.map((s) => s.name);
+
+ let schema: z.ZodTypeAny;
+ if (skillNames.length === 0) {
+ schema = z.object({
+ name: z.string().describe('No skills are currently available.'),
+ });
+ } else {
+ schema = z.object({
+ name: z
+ .enum(skillNames as [string, ...string[]])
+ .describe('The name of the skill to activate.'),
+ });
+ }
+
+ super(
+ ActivateSkillTool.Name,
+ 'Activate Skill',
+ "Activates a specialized agent skill by name. Returns the skill's instructions wrapped in `` tags. These provide specialized guidance for the current task. Use this when you identify a task that matches a skill's description.",
+ Kind.Other,
+ zodToJsonSchema(schema),
+ true,
+ false,
+ messageBus,
+ );
+ }
+
+ protected createInvocation(
+ params: ActivateSkillToolParams,
+ messageBus?: MessageBus,
+ _toolName?: string,
+ _toolDisplayName?: string,
+ ): ToolInvocation {
+ return new ActivateSkillToolInvocation(
+ this.config,
+ params,
+ messageBus,
+ _toolName,
+ _toolDisplayName ?? 'Activate Skill',
+ );
+ }
+}
diff --git a/packages/core/src/tools/tool-names.ts b/packages/core/src/tools/tool-names.ts
index db2967405e..41e4be8dec 100644
--- a/packages/core/src/tools/tool-names.ts
+++ b/packages/core/src/tools/tool-names.ts
@@ -21,6 +21,7 @@ export const READ_FILE_TOOL_NAME = 'read_file';
export const LS_TOOL_NAME = 'list_directory';
export const MEMORY_TOOL_NAME = 'save_memory';
export const GET_INTERNAL_DOCS_TOOL_NAME = 'get_internal_docs';
+export const ACTIVATE_SKILL_TOOL_NAME = 'activate_skill';
export const EDIT_TOOL_NAMES = new Set([EDIT_TOOL_NAME, WRITE_FILE_TOOL_NAME]);
export const DELEGATE_TO_AGENT_TOOL_NAME = 'delegate_to_agent';
@@ -43,6 +44,7 @@ export const ALL_BUILTIN_TOOL_NAMES = [
READ_FILE_TOOL_NAME,
LS_TOOL_NAME,
MEMORY_TOOL_NAME,
+ ACTIVATE_SKILL_TOOL_NAME,
DELEGATE_TO_AGENT_TOOL_NAME,
] as const;
diff --git a/packages/core/src/utils/getFolderStructure.ts b/packages/core/src/utils/getFolderStructure.ts
index 0544f4655d..8f871e1283 100644
--- a/packages/core/src/utils/getFolderStructure.ts
+++ b/packages/core/src/utils/getFolderStructure.ts
@@ -18,7 +18,12 @@ import { debugLogger } from './debugLogger.js';
const MAX_ITEMS = 200;
const TRUNCATION_INDICATOR = '...';
-const DEFAULT_IGNORED_FOLDERS = new Set(['node_modules', '.git', 'dist']);
+const DEFAULT_IGNORED_FOLDERS = new Set([
+ 'node_modules',
+ '.git',
+ 'dist',
+ '__pycache__',
+]);
// --- Interfaces ---