diff --git a/core/config/markdown/loadMarkdownSkills.ts b/core/config/markdown/loadMarkdownSkills.ts new file mode 100644 index 00000000000..a06d64408c4 --- /dev/null +++ b/core/config/markdown/loadMarkdownSkills.ts @@ -0,0 +1,60 @@ +import { + ConfigValidationError, + parseMarkdownRule, +} from "@continuedev/config-yaml"; +import z from "zod"; +import { IDE, Skill } from "../.."; +import { getAllDotContinueDefinitionFiles } from "../loadLocalAssistants"; + +const skillFrontmatterSchema = z.object({ + name: z.string().min(1), + description: z.string().min(1), +}); + +export async function loadMarkdownSkills(ide: IDE) { + const errors: ConfigValidationError[] = []; + const skills: Skill[] = []; + + try { + const yamlAndMarkdownFiles = await getAllDotContinueDefinitionFiles( + ide, + { + includeGlobal: true, + includeWorkspace: true, + fileExtType: "markdown", + }, + "", // SKILL.md can exist in any .continue subdirectory + ); + + const skillFiles = yamlAndMarkdownFiles.filter((file) => + file.path.endsWith("SKILL.md"), + ); + for (const file of skillFiles) { + try { + const { frontmatter, markdown } = parseMarkdownRule( + file.content, + ) as unknown as { frontmatter: Skill; markdown: string }; + + const validatedFrontmatter = skillFrontmatterSchema.parse(frontmatter); + + skills.push({ + ...validatedFrontmatter, + content: markdown, + path: file.path.slice(7), + }); + } catch (error) { + errors.push({ + fatal: false, + message: `Failed to parse markdown skill file: ${error instanceof Error ? error.message : error}`, + }); + } + } + } catch (err) { + errors.push({ + fatal: false, + message: `Error loading markdown skill files: ${err instanceof Error ? err.message : err}`, + }); + } + + return { skills, errors }; +} diff --git a/core/config/profile/doLoadConfig.ts b/core/config/profile/doLoadConfig.ts index f33c8a1e9f4..5ead1d91441 100644 --- a/core/config/profile/doLoadConfig.ts +++ b/core/config/profile/doLoadConfig.ts @@ -69,6 +69,7 @@ async function loadRules(ide: IDE) { return { rules, errors }; } + export default async function doLoadConfig(options: { ide: IDE; controlPlaneClient: ControlPlaneClient; @@ -299,14 +300,15 @@ export default async function doLoadConfig(options: { } newConfig.tools.push( - ...getConfigDependentToolDefinitions({ + ...(await getConfigDependentToolDefinitions({ rules: newConfig.rules, enableExperimentalTools: newConfig.experimental?.enableExperimentalTools ?? false, isSignedIn, isRemote: await ide.isWorkspaceRemote(), modelName: newConfig.selectedModelByRole.chat?.model, - }), + ide, + })), ); // Detect duplicate tool names diff --git a/core/index.d.ts b/core/index.d.ts index 197b5516d0c..ed9dfb0aaf2 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -1152,9 +1152,10 @@ export interface ConfigDependentToolParams { isSignedIn: boolean; isRemote: boolean; modelName: string | undefined; + ide: IDE; } -export type GetTool = (params: ConfigDependentToolParams) => Tool; +export type GetTool = (params: ConfigDependentToolParams) => Promise; export interface BaseCompletionOptions { temperature?: number; @@ -1895,6 +1896,14 @@ export interface RuleWithSource extends RuleMetadata { rule: string; } +export interface Skill { + name: string; + description: string; + path: string; + content: string; + license?: string; +} + export interface CompleteOnboardingPayload { mode: OnboardingModes; provider?: string; diff --git a/core/tools/builtIn.ts b/core/tools/builtIn.ts index 47cd6769fcc..0b1346bbca2 100644 --- a/core/tools/builtIn.ts +++ b/core/tools/builtIn.ts @@ -16,6 +16,7 @@ export enum BuiltInToolNames { RequestRule = "request_rule", FetchUrlContent = "fetch_url_content", CodebaseTool = "codebase", + ReadSkill = "read_skill", // excluded from allTools for now ViewRepoMap = "view_repo_map", diff --git a/core/tools/callTool.ts b/core/tools/callTool.ts index 8d695e1e192..2dd53474c3e 100644 --- a/core/tools/callTool.ts +++ b/core/tools/callTool.ts @@ -16,6 +16,7 @@ import { readCurrentlyOpenFileImpl } from "./implementations/readCurrentlyOpenFi import { readFileImpl } from "./implementations/readFile"; import { readFileRangeImpl } from "./implementations/readFileRange"; +import { readSkillImpl } from "./implementations/readSkill"; import { requestRuleImpl } from "./implementations/requestRule"; import { runTerminalCommandImpl } from "./implementations/runTerminalCommand"; import { searchWebImpl } from "./implementations/searchWeb"; @@ -179,6 +180,8 @@ export async function callBuiltInTool( return await requestRuleImpl(args, extras); case BuiltInToolNames.CodebaseTool: return await codebaseToolImpl(args, extras); + case BuiltInToolNames.ReadSkill: + return await readSkillImpl(args, extras); case BuiltInToolNames.ViewRepoMap: return await viewRepoMapImpl(args, extras); case BuiltInToolNames.ViewSubdirectory: diff --git a/core/tools/definitions/index.ts b/core/tools/definitions/index.ts index bf3dc02d96c..bfc78ac3d2e 100644 --- a/core/tools/definitions/index.ts +++ b/core/tools/definitions/index.ts @@ -11,6 +11,7 @@ export { readCurrentlyOpenFileTool } from "./readCurrentlyOpenFile"; export { readFileTool } from "./readFile"; export { readFileRangeTool } from "./readFileRange"; +export { readSkillTool } from "./readSkill"; export { requestRuleTool } from "./requestRule"; export { runTerminalCommandTool } from "./runTerminalCommand"; export { searchWebTool } from "./searchWeb"; diff --git a/core/tools/definitions/readSkill.ts b/core/tools/definitions/readSkill.ts new file mode 100644 index 00000000000..7d0b18218e3 --- /dev/null +++ b/core/tools/definitions/readSkill.ts @@ -0,0 +1,34 @@ +import { GetTool } from "../.."; +import { loadMarkdownSkills } from "../../config/markdown/loadMarkdownSkills"; +import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn"; + +export const readSkillTool: GetTool = async (params) => { + const { skills } = await loadMarkdownSkills(params.ide); + return { + type: "function", + displayTitle: "Read Skill", + wouldLikeTo: "read skill {{{ skillName }}}", + isCurrently: "reading skill {{{ skillName }}}", + hasAlready: "read skill {{{ skillName }}}", + readonly: true, + isInstant: true, + group: BUILT_IN_GROUP_NAME, + function: { + name: BuiltInToolNames.ReadSkill, + description: ` +Use this tool to read the content of a skill by its name. Skills contain detailed instructions for specific tasks. The skill name should match one of the available skills listed below: +${skills.map((skill) => `\nname: ${skill.name}\ndescription: ${skill.description}\n`)}`, + parameters: { + type: "object", + required: ["skillName"], + properties: { + skillName: { + type: "string", + description: + "The name of the skill to read. This should match the name from the available skills.", + }, + }, + }, + }, + }; +}; diff --git a/core/tools/definitions/requestRule.ts b/core/tools/definitions/requestRule.ts index 30443506c54..7b2d2e42c91 100644 --- a/core/tools/definitions/requestRule.ts +++ b/core/tools/definitions/requestRule.ts @@ -37,7 +37,7 @@ function getRequestRuleSystemMessageDescription( return prefix + availableRules + suffix; } -export const requestRuleTool: GetTool = ({ rules }) => ({ +export const requestRuleTool: GetTool = async ({ rules }) => ({ type: "function", displayTitle: "Request Rules", wouldLikeTo: "request rule {{{ name }}}", diff --git a/core/tools/definitions/toolDefinitions.test.ts b/core/tools/definitions/toolDefinitions.test.ts index 265ebe5e12f..6b64f542ca6 100644 --- a/core/tools/definitions/toolDefinitions.test.ts +++ b/core/tools/definitions/toolDefinitions.test.ts @@ -10,21 +10,24 @@ describe("Tool Definitions", () => { isSignedIn: false, isRemote: false, modelName: "a model", + ide: {} as any, }; // Helper function to get the actual tool object - const getToolObject = (toolDefinition: Tool | GetTool): Tool => { + const getToolObject = async ( + toolDefinition: Tool | GetTool, + ): Promise => { if (typeof toolDefinition === "function") { return toolDefinition(mockParams); } return toolDefinition; }; - it("should have all required parameters defined in properties for each tool", () => { + it("should have all required parameters defined in properties for each tool", async () => { const exportedTools = Object.values(toolDefinitions); - exportedTools.forEach((toolDefinition) => { - const tool = getToolObject(toolDefinition); + for (const toolDefinition of exportedTools) { + const tool = await getToolObject(toolDefinition); // Each tool should have the required structure expect(tool).toHaveProperty("type", "function"); @@ -52,6 +55,6 @@ describe("Tool Definitions", () => { expect(typeof property.type).toBe("string"); }); } - }); + } }); }); diff --git a/core/tools/implementations/readSkill.ts b/core/tools/implementations/readSkill.ts new file mode 100644 index 00000000000..5cb471581a3 --- /dev/null +++ b/core/tools/implementations/readSkill.ts @@ -0,0 +1,32 @@ +import { ToolImpl } from "."; +import { loadMarkdownSkills } from "../../config/markdown/loadMarkdownSkills"; +import { ContinueError, ContinueErrorReason } from "../../util/errors"; +import { getStringArg } from "../parseArgs"; + +export const readSkillImpl: ToolImpl = async (args, extras) => { + const skillName = getStringArg(args, "skillName"); + + const { skills } = await loadMarkdownSkills(extras.ide); + + const skill = skills.find((s) => s.name === skillName); + + if (!skill) { + const availableSkills = skills.map((s) => s.name).join(", "); + throw new ContinueError( + ContinueErrorReason.SkillNotFound, + `Skill "${skillName}" not found. Available skills: ${availableSkills || "none"}`, + ); + } + + return [ + { + name: `Skill: ${skill.name}`, + description: skill.description, + content: `# ${skill.name}\n\n${skill.description}\n\n## Instructions\n\n${skill.content}`, + uri: { + type: "file", + value: skill.path, + }, + }, + ]; +}; diff --git a/core/tools/index.ts b/core/tools/index.ts index bdc519dc9ab..15d52d3aca4 100644 --- a/core/tools/index.ts +++ b/core/tools/index.ts @@ -15,13 +15,14 @@ export const getBaseToolDefinitions = () => [ toolDefinitions.fetchUrlContentTool, ]; -export const getConfigDependentToolDefinitions = ( +export const getConfigDependentToolDefinitions = async ( params: ConfigDependentToolParams, -): Tool[] => { +): Promise => { const { modelName, isSignedIn, enableExperimentalTools, isRemote } = params; const tools: Tool[] = []; - tools.push(toolDefinitions.requestRuleTool(params)); + tools.push(await toolDefinitions.requestRuleTool(params)); + tools.push(await toolDefinitions.readSkillTool(params)); if (isSignedIn) { // Web search is only available for signed-in users diff --git a/core/tools/searchWebGating.vitest.ts b/core/tools/searchWebGating.vitest.ts index 4ebb6e5b45f..e49984bea60 100644 --- a/core/tools/searchWebGating.vitest.ts +++ b/core/tools/searchWebGating.vitest.ts @@ -2,14 +2,15 @@ import { expect, test } from "vitest"; import { BuiltInToolNames } from "./builtIn"; import { getConfigDependentToolDefinitions } from "./index"; -test("searchWeb tool is only available when user is signed in", () => { +test("searchWeb tool is only available when user is signed in", async () => { // Test with signed-in user - const signedInTools = getConfigDependentToolDefinitions({ + const signedInTools = await getConfigDependentToolDefinitions({ rules: [], enableExperimentalTools: false, isSignedIn: true, isRemote: false, modelName: "", + ide: {} as any, }); const searchWebToolSignedIn = signedInTools.find( @@ -19,12 +20,13 @@ test("searchWeb tool is only available when user is signed in", () => { expect(searchWebToolSignedIn?.displayTitle).toBe("Search Web"); // Test with non-signed-in user - const notSignedInTools = getConfigDependentToolDefinitions({ + const notSignedInTools = await getConfigDependentToolDefinitions({ rules: [], enableExperimentalTools: false, isSignedIn: false, isRemote: false, modelName: "", + ide: {} as any, }); const searchWebToolNotSignedIn = notSignedInTools.find( diff --git a/core/util/errors.ts b/core/util/errors.ts index 69df7c81423..b94b5ac003f 100644 --- a/core/util/errors.ts +++ b/core/util/errors.ts @@ -62,6 +62,9 @@ export enum ContinueErrorReason { // Rules RuleNotFound = "rule_not_found", + // Skills + SkillNotFound = "skill_not_found", + // Other Unspecified = "unspecified", // I.e. a known error but no specific code for it Unknown = "unknown", // I.e. an unexpected error