Skip to content
Open
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
60 changes: 60 additions & 0 deletions core/config/markdown/loadMarkdownSkills.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
6 changes: 4 additions & 2 deletions core/config/profile/doLoadConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ async function loadRules(ide: IDE) {

return { rules, errors };
}

export default async function doLoadConfig(options: {
ide: IDE;
controlPlaneClient: ControlPlaneClient;
Expand Down Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Tool>;

export interface BaseCompletionOptions {
temperature?: number;
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions core/tools/builtIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions core/tools/callTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions core/tools/definitions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
34 changes: 34 additions & 0 deletions core/tools/definitions/readSkill.ts
Original file line number Diff line number Diff line change
@@ -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.",
},
},
},
},
};
};
2 changes: 1 addition & 1 deletion core/tools/definitions/requestRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}}",
Expand Down
13 changes: 8 additions & 5 deletions core/tools/definitions/toolDefinitions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Tool> => {
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");
Expand Down Expand Up @@ -52,6 +55,6 @@ describe("Tool Definitions", () => {
expect(typeof property.type).toBe("string");
});
}
});
}
});
});
32 changes: 32 additions & 0 deletions core/tools/implementations/readSkill.ts
Original file line number Diff line number Diff line change
@@ -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,
},
},
];
};
7 changes: 4 additions & 3 deletions core/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ export const getBaseToolDefinitions = () => [
toolDefinitions.fetchUrlContentTool,
];

export const getConfigDependentToolDefinitions = (
export const getConfigDependentToolDefinitions = async (
params: ConfigDependentToolParams,
): Tool[] => {
): Promise<Tool[]> => {
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
Expand Down
8 changes: 5 additions & 3 deletions core/tools/searchWebGating.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down
3 changes: 3 additions & 0 deletions core/util/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading