Skip to content
2 changes: 2 additions & 0 deletions core/config/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@ async function intermediateToFinalConfig({
summarize: null, // Not implemented
},
rules: [],
skills: [],
};

for (const cmd of config.slashCommands ?? []) {
Expand Down Expand Up @@ -670,6 +671,7 @@ async function finalToBrowserConfig(
ui: final.ui,
experimental: final.experimental,
rules: final.rules,
skills: final.skills,
docs: final.docs,
tools: final.tools.map(serializeTool),
mcpServerStatuses: final.mcpServerStatuses,
Expand Down
64 changes: 64 additions & 0 deletions core/config/markdown/loadMarkdownSkills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { ConfigValidationError } from "@continuedev/config-yaml";
import * as YAML from "yaml";
import { IDE, Skill } from "../..";
import { getAllDotContinueDefinitionFiles } from "../loadLocalAssistants";

// todo: refactor this to packages/config-yaml (like parseMarkdownRule)
function parseSkillMarkdown(content: string): Skill {
const normalizedContent = content.replace(/\r\n/g, "\n");

const parts = normalizedContent.split(/^---\s*$/m);

if (parts.length < 3) {
throw new Error("Invalid skill markdown file");
}
const frontmatterStr = parts[1];
const markdownContent = parts.slice(2).join("---");

const frontmatter = YAML.parse(frontmatterStr) || {}; // Handle empty frontmatter
// todo: validate frontmatter with zod

return {
...frontmatter,
content: markdownContent,
};
}

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 skill = parseSkillMarkdown(file.content);
skills.push({ ...skill, 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 };
}
8 changes: 8 additions & 0 deletions core/config/profile/doLoadConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { getWorkspaceContinueRuleDotFiles } from "../getWorkspaceContinueRuleDot
import { loadContinueConfigFromJson } from "../load";
import { CodebaseRulesCache } from "../markdown/loadCodebaseRules";
import { loadMarkdownRules } from "../markdown/loadMarkdownRules";
import { loadMarkdownSkills } from "../markdown/loadMarkdownSkills";
import { migrateJsonSharedConfig } from "../migrateSharedConfig";
import { rectifySelectedModelsFromGlobalContext } from "../selectedModels";
import { loadContinueConfigFromYaml } from "../yaml/loadYaml";
Expand Down Expand Up @@ -69,6 +70,7 @@ async function loadRules(ide: IDE) {

return { rules, errors };
}

export default async function doLoadConfig(options: {
ide: IDE;
controlPlaneClient: ControlPlaneClient;
Expand Down Expand Up @@ -157,6 +159,11 @@ export default async function doLoadConfig(options: {
errors.push(...rulesErrors);
newConfig.rules.unshift(...rules);

// load skills
const { skills, errors: skillsErrors } = await loadMarkdownSkills(ide);
errors.push(...skillsErrors);
newConfig.skills.unshift(...skills);

// Convert invokable rules to slash commands
for (const rule of newConfig.rules) {
if (rule.invokable) {
Expand Down Expand Up @@ -301,6 +308,7 @@ export default async function doLoadConfig(options: {
newConfig.tools.push(
...getConfigDependentToolDefinitions({
rules: newConfig.rules,
skills: newConfig.skills,
enableExperimentalTools:
newConfig.experimental?.enableExperimentalTools ?? false,
isSignedIn,
Expand Down
1 change: 1 addition & 0 deletions core/config/yaml/loadYaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ export async function configYamlToContinueConfig(options: {
summarize: null,
},
rules: [],
skills: [],
requestOptions: { ...config.requestOptions },
};

Expand Down
12 changes: 12 additions & 0 deletions core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,7 @@ interface ToolChoice {

export interface ConfigDependentToolParams {
rules: RuleWithSource[];
skills: Skill[];
enableExperimentalTools: boolean;
isSignedIn: boolean;
isRemote: boolean;
Expand Down Expand Up @@ -1796,6 +1797,7 @@ export interface ContinueConfig {
tools: Tool[];
mcpServerStatuses: MCPServerStatus[];
rules: RuleWithSource[];
skills: Skill[];
modelsByRole: Record<ModelRole, ILLM[]>;
selectedModelByRole: Record<ModelRole, ILLM | null>;
data?: DataDestination[];
Expand All @@ -1818,6 +1820,7 @@ export interface BrowserSerializedContinueConfig {
tools: Omit<Tool, "preprocessArgs", "evaluateToolCallPolicy">[];
mcpServerStatuses: MCPServerStatus[];
rules: RuleWithSource[];
skills: Skill[];
usePlatform: boolean;
tabAutocompleteOptions?: Partial<TabAutocompleteOptions>;
modelsByRole: Record<ModelRole, ModelDescription[]>;
Expand Down Expand Up @@ -1895,6 +1898,15 @@ export interface RuleWithSource extends RuleMetadata {
rule: string;
}

export interface Skill {
name: string;
description: string;
path: string;
toolName: string;
content: string;
license?: string;
}

export interface CompleteOnboardingPayload {
mode: OnboardingModes;
provider?: string;
Expand Down
23 changes: 23 additions & 0 deletions core/llm/skills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Skill } from "..";

export function getSystemMessageWithSkills(
systemMessage: string,
skills: Skill[],
) {
const lines = [
"You have access to skills listed in `<available_skills>`. When a task matches a skill's description, read its SKILL.md file to get detailed instructions.",
"",
"<available_skills>",
];
for (const skill of skills) {
lines.push(" <skill>");
lines.push(` <name>${skill.name}</name>`);
lines.push(` <description>${skill.description}</description>`);
lines.push(" </skill>");
}
lines.push("</available_skills>");

console.log("debug1 with skills", lines.join("\n"));

return systemMessage + "\n\n" + lines.join("\n");
}
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
29 changes: 29 additions & 0 deletions core/tools/definitions/readSkill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Tool } from "../..";
import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn";

export const readSkillTool: Tool = {
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 in the system message.",
parameters: {
type: "object",
required: ["skillName"],
properties: {
skillName: {
type: "string",
description:
"The name of the skill to read. This should match the name field from the available skills.",
},
},
},
},
};
1 change: 1 addition & 0 deletions core/tools/definitions/toolDefinitions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe("Tool Definitions", () => {
// Mock params for tools that need them
const mockParams: ConfigDependentToolParams = {
rules: [],
skills: [],
enableExperimentalTools: false,
isSignedIn: false,
isRemote: false,
Expand Down
29 changes: 29 additions & 0 deletions core/tools/implementations/readSkill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ToolImpl } from ".";
import { ContinueError, ContinueErrorReason } from "../../util/errors";
import { getStringArg } from "../parseArgs";

export const readSkillImpl: ToolImpl = async (args, extras) => {
const skillName = getStringArg(args, "skillName");

const skill = extras.config.skills.find((s) => s.name === skillName);

if (!skill) {
const availableSkills = extras.config.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: 6 additions & 1 deletion core/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,16 @@ export const getBaseToolDefinitions = () => [
export const getConfigDependentToolDefinitions = (
params: ConfigDependentToolParams,
): Tool[] => {
const { modelName, isSignedIn, enableExperimentalTools, isRemote } = params;
const { modelName, isSignedIn, enableExperimentalTools, isRemote, skills } =
params;
const tools: Tool[] = [];

tools.push(toolDefinitions.requestRuleTool(params));

if (skills.length > 0) {
tools.push(toolDefinitions.readSkillTool);
}

if (isSignedIn) {
// Web search is only available for signed-in users
tools.push(toolDefinitions.searchWebTool);
Expand Down
2 changes: 2 additions & 0 deletions core/tools/searchWebGating.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ test("searchWeb tool is only available when user is signed in", () => {
// Test with signed-in user
const signedInTools = getConfigDependentToolDefinitions({
rules: [],
skills: [],
enableExperimentalTools: false,
isSignedIn: true,
isRemote: false,
Expand All @@ -21,6 +22,7 @@ test("searchWeb tool is only available when user is signed in", () => {
// Test with non-signed-in user
const notSignedInTools = getConfigDependentToolDefinitions({
rules: [],
skills: [],
enableExperimentalTools: false,
isSignedIn: false,
isRemote: false,
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
1 change: 1 addition & 0 deletions gui/src/redux/thunks/streamNormalInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export const streamNormalInput = createAsyncThunk<
systemMessage,
state.config.config.rules,
state.ui.ruleSettings,
state.config.config.skills,
systemToolsFramework,
);

Expand Down
Loading
Loading