Skip to content

Commit 73ef45e

Browse files
committed
Custom Tool descriptions
Add ability to Override tools descriptions system prompt. Example usage add your description in .roo/tools/read_file.md it will override default tool description it supports args param replacement
1 parent 39f69f2 commit 73ef45e

File tree

5 files changed

+387
-26
lines changed

5 files changed

+387
-26
lines changed

.changeset/swift-webs-peel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": minor
3+
---
4+
5+
Add ability to Override tools descriptions system prompt. Example usage add your description in .roo/tools/read_file.md it will override default tool description it supports args param replacement with ${args.cwd}
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { getToolDescriptionsForMode } from "../tools/index"
2+
import { defaultModeSlug } from "../../../shared/modes"
3+
import * as fs from "fs/promises"
4+
import { toPosix } from "./utils"
5+
6+
// Mock the fs/promises module
7+
jest.mock("fs/promises", () => ({
8+
readFile: jest.fn(),
9+
mkdir: jest.fn().mockResolvedValue(undefined),
10+
access: jest.fn().mockResolvedValue(undefined),
11+
}))
12+
13+
// Get the mocked fs module
14+
const mockedFs = fs as jest.Mocked<typeof fs>
15+
16+
describe("Tool Override System", () => {
17+
beforeEach(() => {
18+
// Reset mocks before each test
19+
jest.clearAllMocks()
20+
21+
// Default behavior: file doesn't exist
22+
mockedFs.readFile.mockRejectedValue({ code: "ENOENT" })
23+
})
24+
25+
it("should return default read_file tool description when no override file exists", async () => {
26+
const toolDescriptions = await getToolDescriptionsForMode(
27+
defaultModeSlug,
28+
"test/workspace",
29+
false, // supportsComputerUse
30+
undefined, // codeIndexManager
31+
undefined, // diffStrategy
32+
undefined, // browserViewportSize
33+
undefined, // mcpHub
34+
undefined, // customModes
35+
undefined, // experiments
36+
)
37+
38+
// Should contain the default read_file description
39+
expect(toolDescriptions).toContain("## read_file")
40+
expect(toolDescriptions).toContain("Request to read the contents of one or more files")
41+
expect(toolDescriptions).toContain("relative to the current workspace directory test/workspace")
42+
expect(toolDescriptions).toContain("<read_file>")
43+
expect(toolDescriptions).toContain("Examples:")
44+
})
45+
46+
it("should use custom read_file tool description when override file exists", async () => {
47+
// Mock the readFile to return content from an override file
48+
const customReadFileDescription = `## read_file
49+
Description: Custom read file description for testing
50+
Parameters:
51+
- path: (required) Custom path description for \${args.cwd}
52+
- start_line: (optional) Custom start line description
53+
- end_line: (optional) Custom end line description
54+
Usage:
55+
<read_file>
56+
<path>Custom file path</path>
57+
</read_file>
58+
Custom example:
59+
<read_file>
60+
<path>custom-example.txt</path>
61+
</read_file>`
62+
63+
mockedFs.readFile.mockImplementation((filePath, options) => {
64+
if (toPosix(filePath).includes(".roo/tools/read_file.md") && options === "utf-8") {
65+
return Promise.resolve(customReadFileDescription)
66+
}
67+
return Promise.reject({ code: "ENOENT" })
68+
})
69+
70+
const toolDescriptions = await getToolDescriptionsForMode(
71+
defaultModeSlug,
72+
"test/workspace",
73+
false, // supportsComputerUse
74+
undefined, // codeIndexManager
75+
undefined, // diffStrategy
76+
undefined, // browserViewportSize
77+
undefined, // mcpHub
78+
undefined, // customModes
79+
undefined, // experiments
80+
)
81+
82+
// Should contain the custom read_file description
83+
expect(toolDescriptions).toContain("Custom read file description for testing")
84+
expect(toolDescriptions).toContain("Custom path description for test/workspace")
85+
expect(toolDescriptions).toContain("Custom example:")
86+
expect(toolDescriptions).toContain("custom-example.txt")
87+
88+
// Should not contain the default description text
89+
expect(toolDescriptions).not.toContain("Request to read the contents of one or more files")
90+
expect(toolDescriptions).not.toContain("3. Reading lines 500-1000 of a CSV file:")
91+
})
92+
93+
it("should interpolate args properties in override content", async () => {
94+
// Mock the readFile to return content with args interpolation
95+
const customReadFileDescription = `## read_file
96+
Description: Custom read file description with interpolated args
97+
Parameters:
98+
- path: (required) File path relative to workspace \${args.cwd}
99+
- workspace: Current workspace is \${args.cwd}
100+
Usage:
101+
<read_file>
102+
<path>File relative to \${args.cwd}</path>
103+
</read_file>`
104+
105+
mockedFs.readFile.mockImplementation((filePath, options) => {
106+
if (toPosix(filePath).includes(".roo/tools/read_file.md") && options === "utf-8") {
107+
return Promise.resolve(customReadFileDescription)
108+
}
109+
return Promise.reject({ code: "ENOENT" })
110+
})
111+
112+
const toolDescriptions = await getToolDescriptionsForMode(
113+
defaultModeSlug,
114+
"test/workspace",
115+
false, // supportsComputerUse
116+
undefined, // codeIndexManager
117+
undefined, // diffStrategy
118+
undefined, // browserViewportSize
119+
undefined, // mcpHub
120+
undefined, // customModes
121+
undefined, // experiments
122+
)
123+
124+
// Should contain interpolated values
125+
expect(toolDescriptions).toContain("File path relative to workspace test/workspace")
126+
expect(toolDescriptions).toContain("Current workspace is test/workspace")
127+
expect(toolDescriptions).toContain("File relative to test/workspace")
128+
129+
// Should not contain the placeholder text
130+
expect(toolDescriptions).not.toContain("\${args.cwd}")
131+
})
132+
133+
it("should return multiple tool descriptions including read_file", async () => {
134+
const toolDescriptions = await getToolDescriptionsForMode(
135+
defaultModeSlug,
136+
"test/workspace",
137+
false, // supportsComputerUse
138+
undefined, // codeIndexManager
139+
undefined, // diffStrategy
140+
undefined, // browserViewportSize
141+
undefined, // mcpHub
142+
undefined, // customModes
143+
undefined, // experiments
144+
)
145+
146+
// Should contain multiple tools from the default mode
147+
expect(toolDescriptions).toContain("# Tools")
148+
expect(toolDescriptions).toContain("## read_file")
149+
expect(toolDescriptions).toContain("## write_to_file")
150+
expect(toolDescriptions).toContain("## list_files")
151+
expect(toolDescriptions).toContain("## search_files")
152+
153+
// Tools should be separated by double newlines
154+
const toolSections = toolDescriptions.split("\n\n")
155+
expect(toolSections.length).toBeGreaterThan(1)
156+
})
157+
158+
it("should handle empty override file gracefully", async () => {
159+
// Mock the readFile to return empty content
160+
mockedFs.readFile.mockImplementation((filePath, options) => {
161+
if (toPosix(filePath).includes(".roo/tools/read_file.md") && options === "utf-8") {
162+
return Promise.resolve("")
163+
}
164+
return Promise.reject({ code: "ENOENT" })
165+
})
166+
167+
const toolDescriptions = await getToolDescriptionsForMode(
168+
defaultModeSlug,
169+
"test/workspace",
170+
false, // supportsComputerUse
171+
undefined, // codeIndexManager
172+
undefined, // diffStrategy
173+
undefined, // browserViewportSize
174+
undefined, // mcpHub
175+
undefined, // customModes
176+
undefined, // experiments
177+
)
178+
179+
// Should fall back to default description when override file is empty
180+
expect(toolDescriptions).toContain("## read_file")
181+
expect(toolDescriptions).toContain("Request to read the contents of one or more files")
182+
})
183+
184+
it("should handle whitespace-only override file gracefully", async () => {
185+
// Mock the readFile to return whitespace-only content
186+
mockedFs.readFile.mockImplementation((filePath, options) => {
187+
if (toPosix(filePath).includes(".roo/tools/read_file.md") && options === "utf-8") {
188+
return Promise.resolve(" \n \t \n ")
189+
}
190+
return Promise.reject({ code: "ENOENT" })
191+
})
192+
193+
const toolDescriptions = await getToolDescriptionsForMode(
194+
defaultModeSlug,
195+
"test/workspace",
196+
false, // supportsComputerUse
197+
undefined, // codeIndexManager
198+
undefined, // diffStrategy
199+
undefined, // browserViewportSize
200+
undefined, // mcpHub
201+
undefined, // customModes
202+
undefined, // experiments
203+
)
204+
205+
// Should fall back to default description when override file contains only whitespace
206+
expect(toolDescriptions).toContain("## read_file")
207+
expect(toolDescriptions).toContain("Request to read the contents of one or more files")
208+
})
209+
})

src/core/prompts/system.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ ${markdownFormattingSection()}
7171
7272
${getSharedToolUseSection()}
7373
74-
${getToolDescriptionsForMode(
74+
${await getToolDescriptionsForMode(
7575
mode,
7676
cwd,
7777
supportsComputerUse,

src/core/prompts/tools/index.ts

Lines changed: 88 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ToolName, ModeConfig } from "@roo-code/types"
22

3-
import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS, DiffStrategy } from "../../../shared/tools"
3+
import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS, DiffStrategy, readToolOverrideWithArgs } from "../../../shared/tools"
44
import { McpHub } from "../../../services/mcp/McpHub"
55
import { Mode, getModeConfig, isToolAllowedForMode, getGroupName } from "../../../shared/modes"
66

@@ -25,29 +25,91 @@ import { getCodebaseSearchDescription } from "./codebase-search"
2525
import { CodeIndexManager } from "../../../services/code-index/manager"
2626

2727
// Map of tool names to their description functions
28-
const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined> = {
29-
execute_command: (args) => getExecuteCommandDescription(args),
30-
read_file: (args) => getReadFileDescription(args),
31-
fetch_instructions: () => getFetchInstructionsDescription(),
32-
write_to_file: (args) => getWriteToFileDescription(args),
33-
search_files: (args) => getSearchFilesDescription(args),
34-
list_files: (args) => getListFilesDescription(args),
35-
list_code_definition_names: (args) => getListCodeDefinitionNamesDescription(args),
36-
browser_action: (args) => getBrowserActionDescription(args),
37-
ask_followup_question: () => getAskFollowupQuestionDescription(),
38-
attempt_completion: () => getAttemptCompletionDescription(),
39-
use_mcp_tool: (args) => getUseMcpToolDescription(args),
40-
access_mcp_resource: (args) => getAccessMcpResourceDescription(args),
41-
codebase_search: () => getCodebaseSearchDescription(),
42-
switch_mode: () => getSwitchModeDescription(),
43-
new_task: (args) => getNewTaskDescription(args),
44-
insert_content: (args) => getInsertContentDescription(args),
45-
search_and_replace: (args) => getSearchAndReplaceDescription(args),
46-
apply_diff: (args) =>
47-
args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "",
28+
const toolDescriptionMap: Record<string, (args: ToolArgs) => Promise<string | undefined>> = {
29+
execute_command: async (args) => {
30+
const overrideContent = await readToolOverrideWithArgs(args.cwd, "execute_command", args)
31+
return overrideContent || getExecuteCommandDescription(args)
32+
},
33+
read_file: async (args) => {
34+
const overrideContent = await readToolOverrideWithArgs(args.cwd, "read_file", args)
35+
return overrideContent || getReadFileDescription(args)
36+
},
37+
fetch_instructions: async (args) => {
38+
const overrideContent = await readToolOverrideWithArgs(args.cwd, "fetch_instructions", args)
39+
return overrideContent || getFetchInstructionsDescription()
40+
},
41+
write_to_file: async (args) => {
42+
const overrideContent = await readToolOverrideWithArgs(args.cwd, "write_to_file", args)
43+
return overrideContent || getWriteToFileDescription(args)
44+
},
45+
search_files: async (args) => {
46+
const overrideContent = await readToolOverrideWithArgs(args.cwd, "search_files", args)
47+
return overrideContent || getSearchFilesDescription(args)
48+
},
49+
list_files: async (args) => {
50+
const overrideContent = await readToolOverrideWithArgs(args.cwd, "list_files", args)
51+
return overrideContent || getListFilesDescription(args)
52+
},
53+
list_code_definition_names: async (args) => {
54+
const overrideContent = await readToolOverrideWithArgs(args.cwd, "list_code_definition_names", args)
55+
return overrideContent || getListCodeDefinitionNamesDescription(args)
56+
},
57+
browser_action: async (args) => {
58+
const overrideContent = await readToolOverrideWithArgs(args.cwd, "browser_action", args)
59+
return overrideContent || getBrowserActionDescription(args)
60+
},
61+
ask_followup_question: async (args) => {
62+
const overrideContent = await readToolOverrideWithArgs(args.cwd, "ask_followup_question", args)
63+
return overrideContent || getAskFollowupQuestionDescription()
64+
},
65+
attempt_completion: async (args) => {
66+
const overrideContent = await readToolOverrideWithArgs(args.cwd, "attempt_completion", args)
67+
return overrideContent || getAttemptCompletionDescription()
68+
},
69+
use_mcp_tool: async (args) => {
70+
const overrideContent = await readToolOverrideWithArgs(args.cwd, "use_mcp_tool", args)
71+
return overrideContent || getUseMcpToolDescription(args)
72+
},
73+
access_mcp_resource: async (args) => {
74+
const overrideContent = await readToolOverrideWithArgs(args.cwd, "access_mcp_resource", args)
75+
return overrideContent || getAccessMcpResourceDescription(args)
76+
},
77+
codebase_search: async (args) => {
78+
const overrideContent = await readToolOverrideWithArgs(args.cwd, "codebase_search", args)
79+
return overrideContent || getCodebaseSearchDescription()
80+
},
81+
switch_mode: async (args) => {
82+
const overrideContent = await readToolOverrideWithArgs(args.cwd, "switch_mode", args)
83+
return overrideContent || getSwitchModeDescription()
84+
},
85+
new_task: async (args) => {
86+
const overrideContent = await readToolOverrideWithArgs(args.cwd, "new_task", args)
87+
return overrideContent || getNewTaskDescription(args)
88+
},
89+
insert_content: async (args) => {
90+
const overrideContent = await readToolOverrideWithArgs(args.cwd, "insert_content", args)
91+
return overrideContent || getInsertContentDescription(args)
92+
},
93+
search_and_replace: async (args) => {
94+
const overrideContent = await readToolOverrideWithArgs(args.cwd, "search_and_replace", args)
95+
return overrideContent || getSearchAndReplaceDescription(args)
96+
},
97+
apply_diff: async (args) => {
98+
const overrideContent = await readToolOverrideWithArgs(
99+
args.cwd,
100+
`apply_diff${args.diffStrategy?.getName()}`,
101+
args,
102+
)
103+
return (
104+
overrideContent ||
105+
(args.diffStrategy
106+
? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions })
107+
: "")
108+
)
109+
},
48110
}
49111

50-
export function getToolDescriptionsForMode(
112+
export async function getToolDescriptionsForMode(
51113
mode: Mode,
52114
cwd: string,
53115
supportsComputerUse: boolean,
@@ -59,7 +121,7 @@ export function getToolDescriptionsForMode(
59121
experiments?: Record<string, boolean>,
60122
partialReadsEnabled?: boolean,
61123
settings?: Record<string, any>,
62-
): string {
124+
): Promise<string> {
63125
const config = getModeConfig(mode, customModes)
64126
const args: ToolArgs = {
65127
cwd,
@@ -107,18 +169,19 @@ export function getToolDescriptionsForMode(
107169
}
108170

109171
// Map tool descriptions for allowed tools
110-
const descriptions = Array.from(tools).map((toolName) => {
172+
const descriptionPromises = Array.from(tools).map(async (toolName) => {
111173
const descriptionFn = toolDescriptionMap[toolName]
112174
if (!descriptionFn) {
113175
return undefined
114176
}
115177

116-
return descriptionFn({
178+
return await descriptionFn({
117179
...args,
118180
toolOptions: undefined, // No tool options in group-based approach
119181
})
120182
})
121183

184+
const descriptions = await Promise.all(descriptionPromises)
122185
return `# Tools\n\n${descriptions.filter(Boolean).join("\n\n")}`
123186
}
124187

0 commit comments

Comments
 (0)