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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## [v5.3.0] - 2026-01-29

### Added

- New skill tool functionality for enhanced code assistance
- Skill parser and type definitions for skill management
- Comprehensive test coverage for skill tool components
- Enhanced code index orchestrator with skill integration
- State manager for skill tool operations

### Changed

- Updated tool type definitions to support skill tools
- Enhanced assistant message presentation with skill support
- Improved code index manager with skill-related functionality
- Updated dependencies in package.json

---

## [v5.2.5] - 2026-01-21

### Added
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const toolNames = [
"run_slash_command",
"generate_image",
"check_past_chat_memories",
"use_skill",
] as const

export const toolNamesSchema = z.enum(toolNames)
Expand Down
4 changes: 4 additions & 0 deletions src/core/assistant-message/presentAssistantMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { generateImageTool } from "../tools/generateImageTool"
import { planFileEditTool } from "../tools/planFileEditTool"
import { runSlashCommandTool } from "../tools/runSlashCommandTool"
import { updateTodoListTool } from "../tools/updateTodoListTool"
import { useSkillTool } from "../tools/useSkillTool"

import Anthropic from "@anthropic-ai/sdk" // kilocode_change
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
Expand Down Expand Up @@ -626,6 +627,9 @@ export async function presentAssistantMessage(cline: Task) {
case "plan_file_edit":
await planFileEditTool(cline, block, handleError, pushToolResult, removeClosingTag)
break
case "use_skill":
await useSkillTool(cline, block, handleError, pushToolResult)
break
}

break
Expand Down
26 changes: 25 additions & 1 deletion src/core/prompts/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { PromptVariables, loadSystemPromptFile } from "./sections/custom-system-
import { type ClineProviderState } from "../webview/ClineProvider" // kilocode_change
import { addCustomInstructions, getMcpServersSection, getSystemInfoSection } from "./sections"
import { getToolDescriptionsForMode } from "./tools"
import { discoverSkills } from "../tools/skills"

// Helper function to get prompt component, filtering out empty objects
export function getPromptComponent(
Expand Down Expand Up @@ -60,6 +61,26 @@ function getPreviousChatTitlesSection(history?: HistoryItem[]): string {
return `Previous Chat Titles: ${titles.join(", ")}`
}

/**
* Get available skills section for system prompt
*/
async function getSkillsSection(workspacePath: string): Promise<string> {
const skills = await discoverSkills({ workspacePath })

if (skills.length === 0) {
return ""
}

const skillList = skills
.map((skill) => {
return ` - ${skill.metadata.name}: ${skill.metadata.description}`
})
.join("\n")

return `You are provided Skills below, these skills are to be used by you as per your descretion. The purpose of these skills is to provide you additional niche context for you tasks. You might get skills for React, Security or even third-party tools. Use the tool use_skill to get the skill context:
${skillList}`
Comment on lines +74 to +81
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Logic/Performance

Issue: Listing all skills in the system prompt is redundant as they are already listed in the use_skill tool description (which is required for the model to know valid arguments). This consumes unnecessary tokens. Additionally, there are typos in the text ("descretion", "for you tasks").

Fix: Remove the detailed list from the system prompt and direct the model to refer to the use_skill tool definition.

Impact: Reduces token usage and improves prompt clarity.

Suggested change
const skillList = skills
.map((skill) => {
return ` - ${skill.metadata.name}: ${skill.metadata.description}`
})
.join("\n")
return `You are provided Skills below, these skills are to be used by you as per your descretion. The purpose of these skills is to provide you additional niche context for you tasks. You might get skills for React, Security or even third-party tools. Use the tool use_skill to get the skill context:
${skillList}`
// Skills are already listed in the use_skill tool description
return `You have access to a library of skills that provide specialized instructions for tasks. Use the 'use_skill' tool to access them. Refer to the 'use_skill' tool definition for the list of available skills.`

}

const applyDiffToolDescription = `
Common tool calls and explanations

Expand Down Expand Up @@ -464,11 +485,12 @@ async function generatePrompt(
const hasMcpServers = mcpHub && mcpHub.getServers().length > 0
const shouldIncludeMcp = hasMcpGroup && hasMcpServers

const [mcpServersSection] = await Promise.all([
const [mcpServersSection, skillsSection] = await Promise.all([
// getModesSection(context, toolUseStyle /*kilocode_change*/),
shouldIncludeMcp
? getMcpServersSection(mcpHub, effectiveDiffStrategy, enableMcpServerCreation)
: Promise.resolve(""),
getSkillsSection(cwd),
])

const codeIndexManager = CodeIndexManager.getInstance(context, cwd)
Expand Down Expand Up @@ -502,6 +524,8 @@ ${applyDiffToolDescription}

${mcpServersSection}

${skillsSection}

${previousChatTitlesSection}

${getSystemInfoSection(cwd)}
Expand Down
33 changes: 20 additions & 13 deletions src/core/prompts/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { getUpdateTodoListDescription } from "./update-todo-list"
import { getRunSlashCommandDescription } from "./run-slash-command"
import { getGenerateImageDescription } from "./generate-image"
import { getCheckPastChatMemoriesDescription } from "./check-past-chat-memories"
import { getUseSkillDescription } from "./use-skill"
import { CodeIndexManager } from "../../../services/code-index/manager"

// kilocode_change start: Morph fast apply
Expand All @@ -42,7 +43,7 @@ import { type ClineProviderState } from "../../webview/ClineProvider"
// kilocode_change end

// Map of tool names to their description functions
const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined> = {
const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined | Promise<string>> = {
execute_command: (args) => getExecuteCommandDescription(args),
read_file: (args) => {
// Check if the current model should use the simplified read_file tool
Expand Down Expand Up @@ -75,9 +76,10 @@ const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined>
run_slash_command: () => getRunSlashCommandDescription(),
generate_image: (args) => getGenerateImageDescription(args),
check_past_chat_memories: (args) => getCheckPastChatMemoriesDescription(args),
use_skill: (args) => getUseSkillDescription(args),
}

export function getToolDescriptionsForMode(
export async function getToolDescriptionsForMode(
mode: Mode,
cwd: string,
supportsComputerUse: boolean,
Expand All @@ -92,7 +94,7 @@ export function getToolDescriptionsForMode(
enableMcpServerCreation?: boolean,
modelId?: string,
clineProviderState?: ClineProviderState, // kilocode_change
): string {
): Promise<string> {
const config = getModeConfig(mode, customModes)
const args: ToolArgs = {
cwd,
Expand Down Expand Up @@ -176,17 +178,22 @@ export function getToolDescriptionsForMode(
}

// Map tool descriptions for allowed tools
const descriptions = Array.from(tools).map((toolName) => {
const descriptionFn = toolDescriptionMap[toolName]
if (!descriptionFn) {
return undefined
}
const descriptions = await Promise.all(
Array.from(tools).map(async (toolName) => {
const descriptionFn = toolDescriptionMap[toolName]
if (!descriptionFn) {
return undefined
}

const result = descriptionFn({
...args,
toolOptions: undefined, // No tool options in group-based approach
})

return descriptionFn({
...args,
toolOptions: undefined, // No tool options in group-based approach
})
})
// Handle both sync and async description functions
return result instanceof Promise ? await result : result
}),
)

return `# Tools\n\n${descriptions.filter(Boolean).join("\n\n")}`
}
Expand Down
2 changes: 2 additions & 0 deletions src/core/prompts/tools/native-tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import fileEdit from "./file_edit"
import updateTodoList from "./update_todo_list"
import codebaseSearch from "./codebase_search"
import planFileEdit from "./plan_file_edit"
import useSkill from "./use_skill"

export const nativeTools = [
// apply_diff_single_file,
Expand All @@ -40,5 +41,6 @@ export const nativeTools = [
// searchAndReplace,
searchFiles,
updateTodoList,
useSkill,
// writeToFile,
] satisfies OpenAI.Chat.ChatCompletionTool[]
23 changes: 23 additions & 0 deletions src/core/prompts/tools/native-tools/use_skill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type OpenAI from "openai"

export default {
type: "function",
function: {
name: "use_skill",
description:
"Use a specific skill to guide the task execution. This tool allows you to apply predefined skills stored in the workspace's .agent/skills directory. Each skill contains specialized instructions for performing specific tasks or following particular patterns.",
strict: true,
parameters: {
type: "object",
properties: {
skill_name: {
type: "string",
description:
"The name of the skill to use. Must match one of the available skills listed in the tool description.",
},
},
required: ["skill_name"],
additionalProperties: false,
},
},
} satisfies OpenAI.Chat.ChatCompletionTool
37 changes: 37 additions & 0 deletions src/core/prompts/tools/use-skill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { discoverSkills } from "../../tools/skills"
import type { ToolArgs } from "./types"

export async function getUseSkillDescription(args: ToolArgs): Promise<string> {
const skills = await discoverSkills({ workspacePath: args.cwd })

if (skills.length === 0) {
return `## use_skill
Description: Use a specific skill to guide the task execution. This tool allows you to apply predefined skills stored in the workspace's .agent/skills directory.
Parameters:
- skill_name: (required) The name of the skill to use.

No skills are currently available in this workspace. To add skills, create SKILL.md files in .agent/skills/<skill-name>/ directories.`
}

const skillList = skills
.map((skill) => {
return ` - ${skill.metadata.name}: ${skill.metadata.description}`
})
.join("\n")

const example = skills[0]
? `Example: Using the "${skills[0].metadata.name}" skill

<use_skill>
<skill_name>${skills[0].metadata.name}</skill_name>
</use_skill>`
: ""

return `## use_skill
Description: Use a specific skill to guide the task execution. This tool allows you to apply predefined skills stored in the workspace's .agent/skills directory. Each skill contains specialized instructions for performing specific tasks or following particular patterns.
Parameters:
- skill_name: (required) The name of the skill to use. Available skills:
${skillList}

${example}`
}
130 changes: 130 additions & 0 deletions src/core/tools/__tests__/useSkillTool.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { useSkillTool } from "../useSkillTool"
import { Task } from "../../task/Task"
import { formatResponse } from "../../prompts/responses"

// Mock dependencies
vi.mock("../../prompts/responses", () => ({
formatResponse: {
toolError: vi.fn((msg) => `<error>${msg}</error>`),
},
}))

describe("useSkillTool", () => {
let mockCline: any
let mockHandleError: any
let mockPushToolResult: any

beforeEach(() => {
mockPushToolResult = vi.fn()
mockHandleError = vi.fn()

mockCline = {
workspacePath: "/workspace",
consecutiveMistakeCount: 0,
recordToolError: vi.fn(),
sayAndCreateMissingParamError: vi.fn().mockResolvedValue("Missing param error"),
ask: vi.fn().mockResolvedValue({ text: "", images: [] }),
say: vi.fn().mockResolvedValue(undefined),
} as const
})

it("should handle partial tool call", async () => {
const block = {
type: "tool_use",
id: "test-id-1",
name: "use_skill",
params: {},
partial: true,
} as const

await useSkillTool(mockCline, block, mockHandleError, mockPushToolResult)

expect(mockCline.ask).toHaveBeenCalledWith("tool", expect.any(String), true)
expect(mockPushToolResult).not.toHaveBeenCalled()
})

it("should return error when skill_name is missing", async () => {
const block = {
type: "tool_use",
name: "use_skill",
params: {},
partial: false,
} as const

await useSkillTool(mockCline, block, mockHandleError, mockPushToolResult)

expect(mockCline.consecutiveMistakeCount).toBe(1)
expect(mockCline.recordToolError).toHaveBeenCalledWith("use_skill")
expect(mockCline.sayAndCreateMissingParamError).toHaveBeenCalledWith("use_skill", "skill_name")
expect(mockPushToolResult).toHaveBeenCalledWith("Missing param error")
})

it("should return error when skill is not found", async () => {
const block = {
type: "tool_use",
name: "use_skill",
params: { skill_name: "non-existent-skill" },
partial: false,
} as const

await useSkillTool(mockCline, block, mockHandleError, mockPushToolResult)

expect(mockCline.say).toHaveBeenCalled()
expect(mockPushToolResult).toHaveBeenCalledWith(
'<error>Skill "non-existent-skill" not found. Make sure the skill exists in .agent/skills/<skill-name>/SKILL.md</error>',
)
})

it("should return skill content when skill is found", async () => {
const block = {
type: "tool_use",
name: "use_skill",
params: { skill_name: "test-skill" },
partial: false,
} as const

// Mock the skill discovery to return a skill
vi.doMock(
"../skills",
() =>
({
getSkillByName: vi.fn().mockResolvedValue({
metadata: { name: "test-skill", description: "Test skill" },
content: "# Test Skill\n\nThis is the skill content.",
folderName: "test",
path: "/workspace/.agent/skills/test/SKILL.md",
} as const),
}) as const,
)

await useSkillTool(mockCline, block, mockHandleError, mockPushToolResult)

expect(mockCline.say).toHaveBeenCalled()
expect(mockPushToolResult).toHaveBeenCalledWith(
"You are requested to follow the below instructions\n\n# Test Skill\n\nThis is the skill content.",
)
})

it("should handle errors gracefully", async () => {
const block = {
type: "tool_use",
name: "use_skill",
params: { skill_name: "test-skill" },
partial: false,
} as const

// Mock an error in skill discovery
vi.doMock(
"../skills",
() =>
({
getSkillByName: vi.fn().mockRejectedValue(new Error("Discovery error")),
}) as const,
)

await useSkillTool(mockCline, block, mockHandleError, mockPushToolResult)

expect(mockHandleError).toHaveBeenCalledWith("using skill", expect.any(Error))
})
})
Loading
Loading