diff --git a/core/config/json/loadRcConfigs.ts b/core/config/json/loadRcConfigs.ts index ceb7de77b54..cf73eb4aae3 100644 --- a/core/config/json/loadRcConfigs.ts +++ b/core/config/json/loadRcConfigs.ts @@ -18,7 +18,7 @@ export async function getWorkspaceRcConfigs( entry[0].endsWith(".continuerc.json"), ) .map((entry) => joinPathsToUri(dir, entry[0])); - return await Promise.all(rcFiles.map(ide.readFile)); + return await Promise.all(rcFiles.map((uri) => ide.readFile(uri))); }), ); return rcFiles diff --git a/core/config/load.ts b/core/config/load.ts index 35d12d421fc..b570d2047bc 100644 --- a/core/config/load.ts +++ b/core/config/load.ts @@ -109,6 +109,7 @@ const configMergeKeys = { }, slashCommands: (a: any, b: any) => a.name === b.name, customCommands: (a: any, b: any) => a.name === b.name, + tools: (a: any, b: any) => a.name === b.name, }; function loadSerializedConfig( @@ -504,6 +505,7 @@ async function intermediateToFinalConfig({ ...config, contextProviders, tools: getBaseToolDefinitions(), + toolOverrides: config.tools, // Pass through tool overrides from config mcpServerStatuses: [], slashCommands: [], modelsByRole: { diff --git a/core/config/profile/doLoadConfig.ts b/core/config/profile/doLoadConfig.ts index f33c8a1e9f4..1b090234712 100644 --- a/core/config/profile/doLoadConfig.ts +++ b/core/config/profile/doLoadConfig.ts @@ -30,6 +30,7 @@ import { TeamAnalytics } from "../../control-plane/TeamAnalytics.js"; import ContinueProxy from "../../llm/llms/stubs/ContinueProxy"; import { initSlashCommand } from "../../promptFiles/initPrompt"; import { getConfigDependentToolDefinitions } from "../../tools"; +import { applyToolOverrides } from "../../tools/applyToolOverrides"; import { encodeMCPToolUri } from "../../tools/callTool"; import { getMCPToolName } from "../../tools/mcpToolName"; import { GlobalContext } from "../../util/GlobalContext"; @@ -39,6 +40,7 @@ import { Telemetry } from "../../util/posthog"; import { SentryLogger } from "../../util/sentry/SentryLogger"; import { TTS } from "../../util/tts"; import { getWorkspaceContinueRuleDotFiles } from "../getWorkspaceContinueRuleDotFiles"; +import { getWorkspaceRcConfigs } from "../json/loadRcConfigs"; import { loadContinueConfigFromJson } from "../load"; import { CodebaseRulesCache } from "../markdown/loadCodebaseRules"; import { loadMarkdownRules } from "../markdown/loadMarkdownRules"; @@ -309,6 +311,32 @@ export default async function doLoadConfig(options: { }), ); + // Load workspace .continuerc.json files for tool overrides + // (JSON path loads these earlier during config merge, YAML path doesn't) + if (!newConfig.toolOverrides?.length) { + const workspaceRcConfigs = await getWorkspaceRcConfigs(ide); + for (const rcConfig of workspaceRcConfigs) { + if (rcConfig.tools?.length) { + newConfig.toolOverrides = [ + ...(newConfig.toolOverrides ?? []), + ...rcConfig.tools, + ]; + } + } + } + + // Apply tool overrides from config (e.g., .continuerc.json) + if (newConfig.toolOverrides?.length) { + const toolOverridesResult = applyToolOverrides( + newConfig.tools, + newConfig.toolOverrides, + ); + newConfig.tools = toolOverridesResult.tools; + errors.push(...toolOverridesResult.errors); + // Clear toolOverrides after applying (not needed at runtime) + delete newConfig.toolOverrides; + } + // Detect duplicate tool names const counts: Record = {}; newConfig.tools.forEach((tool) => { diff --git a/core/config/types.ts b/core/config/types.ts index d9f58aca6e2..9a2a3ad1f4c 100644 --- a/core/config/types.ts +++ b/core/config/types.ts @@ -1133,7 +1133,31 @@ declare global { url?: string; clientKey?: string; } - + + /** + * Configuration for overriding built-in tool prompts. + * Allows customization of tool descriptions and behavior at the repo level. + */ + export interface ToolOverride { + /** Tool name to override (matches function.name, e.g., "read_file", "run_terminal_command") */ + name: string; + /** Override the tool's description shown to the LLM */ + description?: string; + /** Override the display title shown in UI */ + displayTitle?: string; + /** Override the action phrases */ + wouldLikeTo?: string; + isCurrently?: string; + hasAlready?: string; + /** Override system message description for non-native tool calling */ + systemMessageDescription?: { + prefix?: string; + exampleArgs?: Array<[string, string | number]>; + }; + /** Set to true to disable this tool */ + disabled?: boolean; + } + // config.json export interface SerializedContinueConfig { env?: string[]; @@ -1156,8 +1180,10 @@ declare global { experimental?: ExperimentalConfig; analytics?: AnalyticsConfig; docs?: SiteIndexingConfig[]; + /** Tool overrides for customizing built-in tool prompts at the repo level */ + tools?: ToolOverride[]; } - + export type ConfigMergeType = "merge" | "overwrite"; export type ContinueRcJson = Partial & { @@ -1208,8 +1234,10 @@ declare global { experimental?: ExperimentalConfig; /** Analytics configuration */ analytics?: AnalyticsConfig; + /** Tool overrides for customizing built-in tool prompts */ + tools?: ToolOverride[]; } - + // in the actual Continue source code export interface ContinueConfig { allowAnonymousTelemetry?: boolean; @@ -1231,8 +1259,10 @@ declare global { analytics?: AnalyticsConfig; docs?: SiteIndexingConfig[]; tools: Tool[]; + /** Tool overrides from config, applied after all tools are loaded */ + toolOverrides?: ToolOverride[]; } - + export interface BrowserSerializedContinueConfig { allowAnonymousTelemetry?: boolean; models: ModelDescription[]; diff --git a/core/index.d.ts b/core/index.d.ts index 197b5516d0c..4fa6efefa90 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -1139,6 +1139,30 @@ export interface Tool { ) => ToolPolicy; } +/** + * Configuration for overriding built-in tool prompts. + * Allows customization of tool descriptions and behavior at the repo level. + */ +export interface ToolOverride { + /** Tool name to override (matches function.name, e.g., "read_file", "run_terminal_command") */ + name: string; + /** Override the tool's description shown to the LLM */ + description?: string; + /** Override the display title shown in UI */ + displayTitle?: string; + /** Override the action phrases */ + wouldLikeTo?: string; + isCurrently?: string; + hasAlready?: string; + /** Override system message description for non-native tool calling */ + systemMessageDescription?: { + prefix?: string; + exampleArgs?: Array<[string, string | number]>; + }; + /** Set to true to disable this tool */ + disabled?: boolean; +} + interface ToolChoice { type: "function"; function: { @@ -1721,6 +1745,8 @@ export interface SerializedContinueConfig { analytics?: AnalyticsConfig; docs?: SiteIndexingConfig[]; data?: DataDestination[]; + /** Tool overrides for customizing built-in tool prompts at the repo level */ + tools?: ToolOverride[]; } export type ConfigMergeType = "merge" | "overwrite"; @@ -1775,6 +1801,8 @@ export interface Config { analytics?: AnalyticsConfig; docs?: SiteIndexingConfig[]; data?: DataDestination[]; + /** Tool overrides for customizing built-in tool prompts */ + tools?: ToolOverride[]; } // in the actual Continue source code @@ -1794,6 +1822,8 @@ export interface ContinueConfig { analytics?: AnalyticsConfig; docs?: SiteIndexingConfig[]; tools: Tool[]; + /** Tool overrides from config, applied after all tools are loaded */ + toolOverrides?: ToolOverride[]; mcpServerStatuses: MCPServerStatus[]; rules: RuleWithSource[]; modelsByRole: Record; diff --git a/core/tools/applyToolOverrides.test.ts b/core/tools/applyToolOverrides.test.ts new file mode 100644 index 00000000000..a0834f9857b --- /dev/null +++ b/core/tools/applyToolOverrides.test.ts @@ -0,0 +1,184 @@ +import { Tool, ToolOverride } from ".."; +import { applyToolOverrides } from "./applyToolOverrides"; + +const mockTool = (name: string, description: string): Tool => ({ + type: "function", + displayTitle: name, + readonly: true, + group: "test", + function: { name, description }, +}); + +describe("applyToolOverrides", () => { + it("should return tools unchanged when no overrides provided", () => { + const tools = [mockTool("read_file", "Read a file")]; + const result = applyToolOverrides(tools, undefined); + expect(result.tools).toEqual(tools); + expect(result.errors).toHaveLength(0); + }); + + it("should return tools unchanged when empty overrides array provided", () => { + const tools = [mockTool("read_file", "Read a file")]; + const result = applyToolOverrides(tools, []); + expect(result.tools).toEqual(tools); + expect(result.errors).toHaveLength(0); + }); + + it("should override description when specified", () => { + const tools = [mockTool("read_file", "Original description")]; + const overrides: ToolOverride[] = [ + { name: "read_file", description: "New description" }, + ]; + const result = applyToolOverrides(tools, overrides); + expect(result.tools[0].function.description).toBe("New description"); + expect(result.errors).toHaveLength(0); + }); + + it("should override displayTitle when specified", () => { + const tools = [mockTool("read_file", "Read a file")]; + const overrides: ToolOverride[] = [ + { name: "read_file", displayTitle: "Custom Read File" }, + ]; + const result = applyToolOverrides(tools, overrides); + expect(result.tools[0].displayTitle).toBe("Custom Read File"); + }); + + it("should override action phrases when specified", () => { + const tools = [mockTool("read_file", "Read a file")]; + tools[0].wouldLikeTo = "read {{{ filepath }}}"; + tools[0].isCurrently = "reading {{{ filepath }}}"; + tools[0].hasAlready = "read {{{ filepath }}}"; + + const overrides: ToolOverride[] = [ + { + name: "read_file", + wouldLikeTo: "open {{{ filepath }}}", + isCurrently: "opening {{{ filepath }}}", + hasAlready: "opened {{{ filepath }}}", + }, + ]; + const result = applyToolOverrides(tools, overrides); + expect(result.tools[0].wouldLikeTo).toBe("open {{{ filepath }}}"); + expect(result.tools[0].isCurrently).toBe("opening {{{ filepath }}}"); + expect(result.tools[0].hasAlready).toBe("opened {{{ filepath }}}"); + }); + + it("should disable tools when disabled: true", () => { + const tools = [ + mockTool("read_file", "Read"), + mockTool("write_file", "Write"), + ]; + const overrides: ToolOverride[] = [{ name: "read_file", disabled: true }]; + const result = applyToolOverrides(tools, overrides); + expect(result.tools).toHaveLength(1); + expect(result.tools[0].function.name).toBe("write_file"); + expect(result.errors).toHaveLength(0); + }); + + it("should warn when override references unknown tool", () => { + const tools = [mockTool("read_file", "Read")]; + const overrides: ToolOverride[] = [ + { name: "unknown_tool", description: "test" }, + ]; + const result = applyToolOverrides(tools, overrides); + expect(result.tools).toHaveLength(1); + expect(result.errors).toHaveLength(1); + expect(result.errors[0].message).toContain("unknown_tool"); + expect(result.errors[0].fatal).toBe(false); + }); + + it("should preserve unmodified fields", () => { + const tools = [mockTool("read_file", "Original")]; + tools[0].readonly = true; + tools[0].group = "Built-In"; + + const overrides: ToolOverride[] = [ + { name: "read_file", description: "New description" }, + ]; + const result = applyToolOverrides(tools, overrides); + expect(result.tools[0].readonly).toBe(true); + expect(result.tools[0].group).toBe("Built-In"); + expect(result.tools[0].displayTitle).toBe("read_file"); + }); + + it("should override systemMessageDescription", () => { + const tools = [mockTool("read_file", "Read")]; + tools[0].systemMessageDescription = { + prefix: "old prefix", + exampleArgs: [["filepath", "/old/path"]], + }; + + const overrides: ToolOverride[] = [ + { + name: "read_file", + systemMessageDescription: { + prefix: "new prefix", + exampleArgs: [["filepath", "/new/path"]], + }, + }, + ]; + const result = applyToolOverrides(tools, overrides); + expect(result.tools[0].systemMessageDescription?.prefix).toBe("new prefix"); + expect(result.tools[0].systemMessageDescription?.exampleArgs).toEqual([ + ["filepath", "/new/path"], + ]); + }); + + it("should partially override systemMessageDescription", () => { + const tools = [mockTool("read_file", "Read")]; + tools[0].systemMessageDescription = { + prefix: "old prefix", + exampleArgs: [["filepath", "/old/path"]], + }; + + const overrides: ToolOverride[] = [ + { + name: "read_file", + systemMessageDescription: { + prefix: "new prefix", + // exampleArgs not specified - should preserve original + }, + }, + ]; + const result = applyToolOverrides(tools, overrides); + expect(result.tools[0].systemMessageDescription?.prefix).toBe("new prefix"); + expect(result.tools[0].systemMessageDescription?.exampleArgs).toEqual([ + ["filepath", "/old/path"], + ]); + }); + + it("should apply multiple overrides", () => { + const tools = [ + mockTool("read_file", "Read"), + mockTool("write_file", "Write"), + mockTool("delete_file", "Delete"), + ]; + + const overrides: ToolOverride[] = [ + { name: "read_file", description: "Custom read" }, + { name: "write_file", disabled: true }, + { name: "delete_file", displayTitle: "Remove File" }, + ]; + + const result = applyToolOverrides(tools, overrides); + expect(result.tools).toHaveLength(2); + expect(result.tools[0].function.description).toBe("Custom read"); + expect(result.tools[1].displayTitle).toBe("Remove File"); + expect(result.errors).toHaveLength(0); + }); + + it("should not mutate original tools array", () => { + const tools = [mockTool("read_file", "Original")]; + const originalDescription = tools[0].function.description; + + const overrides: ToolOverride[] = [ + { name: "read_file", description: "New description" }, + ]; + const result = applyToolOverrides(tools, overrides); + + // Original should be unchanged + expect(tools[0].function.description).toBe(originalDescription); + // Result should have new description + expect(result.tools[0].function.description).toBe("New description"); + }); +}); diff --git a/core/tools/applyToolOverrides.ts b/core/tools/applyToolOverrides.ts new file mode 100644 index 00000000000..52c7b94cb1f --- /dev/null +++ b/core/tools/applyToolOverrides.ts @@ -0,0 +1,69 @@ +import { ConfigValidationError } from "@continuedev/config-yaml"; +import { Tool, ToolOverride } from ".."; + +export interface ApplyToolOverridesResult { + tools: Tool[]; + errors: ConfigValidationError[]; +} + +/** + * Applies tool overrides from config to the list of tools. + * Overrides can modify tool descriptions, display titles, action phrases, + * system message descriptions, or disable tools entirely. + */ +export function applyToolOverrides( + tools: Tool[], + overrides: ToolOverride[] | undefined, +): ApplyToolOverridesResult { + if (!overrides?.length) { + return { tools, errors: [] }; + } + + const errors: ConfigValidationError[] = []; + const toolsByName = new Map(tools.map((t) => [t.function.name, t])); + + for (const override of overrides) { + const tool = toolsByName.get(override.name); + + if (!tool) { + errors.push({ + fatal: false, + message: `Tool override "${override.name}" does not match any known tool. Available tools: ${Array.from(toolsByName.keys()).join(", ")}`, + }); + continue; + } + + if (override.disabled) { + toolsByName.delete(override.name); + continue; + } + + const updatedTool: Tool = { + ...tool, + function: { + ...tool.function, + description: override.description ?? tool.function.description, + }, + displayTitle: override.displayTitle ?? tool.displayTitle, + wouldLikeTo: override.wouldLikeTo ?? tool.wouldLikeTo, + isCurrently: override.isCurrently ?? tool.isCurrently, + hasAlready: override.hasAlready ?? tool.hasAlready, + }; + + if (override.systemMessageDescription) { + updatedTool.systemMessageDescription = { + prefix: + override.systemMessageDescription.prefix ?? + tool.systemMessageDescription?.prefix ?? + "", + exampleArgs: + override.systemMessageDescription.exampleArgs ?? + tool.systemMessageDescription?.exampleArgs, + }; + } + + toolsByName.set(override.name, updatedTool); + } + + return { tools: Array.from(toolsByName.values()), errors }; +} diff --git a/docs/customize/deep-dives/configuration.mdx b/docs/customize/deep-dives/configuration.mdx index 694b7543013..88df2822cf7 100644 --- a/docs/customize/deep-dives/configuration.mdx +++ b/docs/customize/deep-dives/configuration.mdx @@ -44,7 +44,87 @@ See the full reference for `config.yaml` [here](/reference). The format of `.continuerc.json` is the same as `config.json`, plus one _additional_ property `mergeBehavior`, which can be set to either "merge" or "overwrite". If set to "merge" (the default), `.continuerc.json` will be applied on top of `config.json` (arrays and objects are merged). If set to "overwrite", then every top-level property of `.continuerc.json` will overwrite that property from `config.json`. -Example +#### Customizing Tool Prompts + +You can customize built-in tool prompts at the workspace level using the `tools` array in `.continuerc.json`. This is especially useful when working with local models that may struggle with Continue's default tool prompts. + + + + Customize how tools are described to the LLM: + + ```json title=".continuerc.json" + { + "mergeBehavior": "merge", + "tools": [ + { + "name": "read_file", + "description": "Read file contents. Use relative paths from workspace root." + } + ] + } + ``` + + + + Prevent certain tools from being available: + + ```json title=".continuerc.json" + { + "mergeBehavior": "merge", + "tools": [ + { + "name": "view_diff", + "disabled": true + } + ] + } + ``` + + + + Override the system message descriptions for non-native tool calling: + + ```json title=".continuerc.json" + { + "mergeBehavior": "merge", + "tools": [ + { + "name": "read_file", + "systemMessageDescription": { + "prefix": "To read a file, use read_file with a relative path:", + "exampleArgs": [["filepath", "./docs/README.md"]] + } + } + ] + } + ``` + + + + Customize the UI display and action phrases: + + ```json title=".continuerc.json" + { + "mergeBehavior": "merge", + "tools": [ + { + "name": "read_file", + "displayTitle": "View File", + "wouldLikeTo": "view {{{ filepath }}}", + "isCurrently": "viewing {{{ filepath }}}", + "hasAlready": "viewed {{{ filepath }}}" + } + ] + } + ``` + + + + + Tool names match the built-in function names like `read_file`, `run_terminal_command`, `view_diff`, etc. Tools are merged by name during configuration loading. + + +#### Basic Configuration Example ```json title=".continuerc.json" {