Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/swift-webs-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"roo-cline": minor
---

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}
209 changes: 209 additions & 0 deletions src/core/prompts/__tests__/tool-overrides.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { getToolDescriptionsForMode } from "../tools/index"
import { defaultModeSlug } from "../../../shared/modes"
import * as fs from "fs/promises"
import { toPosix } from "./utils"

// Mock the fs/promises module
jest.mock("fs/promises", () => ({
readFile: jest.fn(),
mkdir: jest.fn().mockResolvedValue(undefined),
access: jest.fn().mockResolvedValue(undefined),
}))

// Get the mocked fs module
const mockedFs = fs as jest.Mocked<typeof fs>

describe("Tool Override System", () => {
beforeEach(() => {
// Reset mocks before each test
jest.clearAllMocks()

// Default behavior: file doesn't exist
mockedFs.readFile.mockRejectedValue({ code: "ENOENT" })
})

it("should return default read_file tool description when no override file exists", async () => {
const toolDescriptions = await getToolDescriptionsForMode(
defaultModeSlug,
"test/workspace",
false, // supportsComputerUse
undefined, // codeIndexManager
undefined, // diffStrategy
undefined, // browserViewportSize
undefined, // mcpHub
undefined, // customModes
undefined, // experiments
)

// Should contain the default read_file description
expect(toolDescriptions).toContain("## read_file")
expect(toolDescriptions).toContain("Request to read the contents of one or more files")
expect(toolDescriptions).toContain("relative to the current workspace directory test/workspace")
expect(toolDescriptions).toContain("<read_file>")
expect(toolDescriptions).toContain("Examples:")
})

it("should use custom read_file tool description when override file exists", async () => {
// Mock the readFile to return content from an override file
const customReadFileDescription = `## read_file
Description: Custom read file description for testing
Parameters:
- path: (required) Custom path description for \${args.cwd}
- start_line: (optional) Custom start line description
- end_line: (optional) Custom end line description
Usage:
<read_file>
<path>Custom file path</path>
</read_file>
Custom example:
<read_file>
<path>custom-example.txt</path>
</read_file>`

mockedFs.readFile.mockImplementation((filePath, options) => {
if (toPosix(filePath).includes(".roo/tools/read_file.md") && options === "utf-8") {
return Promise.resolve(customReadFileDescription)
}
return Promise.reject({ code: "ENOENT" })
})

const toolDescriptions = await getToolDescriptionsForMode(
defaultModeSlug,
"test/workspace",
false, // supportsComputerUse
undefined, // codeIndexManager
undefined, // diffStrategy
undefined, // browserViewportSize
undefined, // mcpHub
undefined, // customModes
undefined, // experiments
)

// Should contain the custom read_file description
expect(toolDescriptions).toContain("Custom read file description for testing")
expect(toolDescriptions).toContain("Custom path description for test/workspace")
expect(toolDescriptions).toContain("Custom example:")
expect(toolDescriptions).toContain("custom-example.txt")

// Should not contain the default description text
expect(toolDescriptions).not.toContain("Request to read the contents of one or more files")
expect(toolDescriptions).not.toContain("3. Reading lines 500-1000 of a CSV file:")
})

it("should interpolate args properties in override content", async () => {
// Mock the readFile to return content with args interpolation
const customReadFileDescription = `## read_file
Description: Custom read file description with interpolated args
Parameters:
- path: (required) File path relative to workspace \${args.cwd}
- workspace: Current workspace is \${args.cwd}
Usage:
<read_file>
<path>File relative to \${args.cwd}</path>
</read_file>`

mockedFs.readFile.mockImplementation((filePath, options) => {
if (toPosix(filePath).includes(".roo/tools/read_file.md") && options === "utf-8") {
return Promise.resolve(customReadFileDescription)
}
return Promise.reject({ code: "ENOENT" })
})

const toolDescriptions = await getToolDescriptionsForMode(
defaultModeSlug,
"test/workspace",
false, // supportsComputerUse
undefined, // codeIndexManager
undefined, // diffStrategy
undefined, // browserViewportSize
undefined, // mcpHub
undefined, // customModes
undefined, // experiments
)

// Should contain interpolated values
expect(toolDescriptions).toContain("File path relative to workspace test/workspace")
expect(toolDescriptions).toContain("Current workspace is test/workspace")
expect(toolDescriptions).toContain("File relative to test/workspace")

// Should not contain the placeholder text
expect(toolDescriptions).not.toContain("\${args.cwd}")
})

it("should return multiple tool descriptions including read_file", async () => {
const toolDescriptions = await getToolDescriptionsForMode(
defaultModeSlug,
"test/workspace",
false, // supportsComputerUse
undefined, // codeIndexManager
undefined, // diffStrategy
undefined, // browserViewportSize
undefined, // mcpHub
undefined, // customModes
undefined, // experiments
)

// Should contain multiple tools from the default mode
expect(toolDescriptions).toContain("# Tools")
expect(toolDescriptions).toContain("## read_file")
expect(toolDescriptions).toContain("## write_to_file")
expect(toolDescriptions).toContain("## list_files")
expect(toolDescriptions).toContain("## search_files")

// Tools should be separated by double newlines
const toolSections = toolDescriptions.split("\n\n")
expect(toolSections.length).toBeGreaterThan(1)
})

it("should handle empty override file gracefully", async () => {
// Mock the readFile to return empty content
mockedFs.readFile.mockImplementation((filePath, options) => {
if (toPosix(filePath).includes(".roo/tools/read_file.md") && options === "utf-8") {
return Promise.resolve("")
}
return Promise.reject({ code: "ENOENT" })
})

const toolDescriptions = await getToolDescriptionsForMode(
defaultModeSlug,
"test/workspace",
false, // supportsComputerUse
undefined, // codeIndexManager
undefined, // diffStrategy
undefined, // browserViewportSize
undefined, // mcpHub
undefined, // customModes
undefined, // experiments
)

// Should fall back to default description when override file is empty
expect(toolDescriptions).toContain("## read_file")
expect(toolDescriptions).toContain("Request to read the contents of one or more files")
})

it("should handle whitespace-only override file gracefully", async () => {
// Mock the readFile to return whitespace-only content
mockedFs.readFile.mockImplementation((filePath, options) => {
if (toPosix(filePath).includes(".roo/tools/read_file.md") && options === "utf-8") {
return Promise.resolve(" \n \t \n ")
}
return Promise.reject({ code: "ENOENT" })
})

const toolDescriptions = await getToolDescriptionsForMode(
defaultModeSlug,
"test/workspace",
false, // supportsComputerUse
undefined, // codeIndexManager
undefined, // diffStrategy
undefined, // browserViewportSize
undefined, // mcpHub
undefined, // customModes
undefined, // experiments
)

// Should fall back to default description when override file contains only whitespace
expect(toolDescriptions).toContain("## read_file")
expect(toolDescriptions).toContain("Request to read the contents of one or more files")
})
})
2 changes: 1 addition & 1 deletion src/core/prompts/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ ${markdownFormattingSection()}

${getSharedToolUseSection()}

${getToolDescriptionsForMode(
${await getToolDescriptionsForMode(
mode,
cwd,
supportsComputerUse,
Expand Down
113 changes: 88 additions & 25 deletions src/core/prompts/tools/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ToolName, ModeConfig } from "@roo-code/types"

import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS, DiffStrategy } from "../../../shared/tools"
import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS, DiffStrategy, readToolOverrideWithArgs } from "../../../shared/tools"
import { McpHub } from "../../../services/mcp/McpHub"
import { Mode, getModeConfig, isToolAllowedForMode, getGroupName } from "../../../shared/modes"

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

// Map of tool names to their description functions
const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined> = {
execute_command: (args) => getExecuteCommandDescription(args),
read_file: (args) => getReadFileDescription(args),
fetch_instructions: () => getFetchInstructionsDescription(),
write_to_file: (args) => getWriteToFileDescription(args),
search_files: (args) => getSearchFilesDescription(args),
list_files: (args) => getListFilesDescription(args),
list_code_definition_names: (args) => getListCodeDefinitionNamesDescription(args),
browser_action: (args) => getBrowserActionDescription(args),
ask_followup_question: () => getAskFollowupQuestionDescription(),
attempt_completion: () => getAttemptCompletionDescription(),
use_mcp_tool: (args) => getUseMcpToolDescription(args),
access_mcp_resource: (args) => getAccessMcpResourceDescription(args),
codebase_search: () => getCodebaseSearchDescription(),
switch_mode: () => getSwitchModeDescription(),
new_task: (args) => getNewTaskDescription(args),
insert_content: (args) => getInsertContentDescription(args),
search_and_replace: (args) => getSearchAndReplaceDescription(args),
apply_diff: (args) =>
args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "",
const toolDescriptionMap: Record<string, (args: ToolArgs) => Promise<string | undefined>> = {
execute_command: async (args) => {
const overrideContent = await readToolOverrideWithArgs(args.cwd, "execute_command", args)
return overrideContent || getExecuteCommandDescription(args)
},
read_file: async (args) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is a huge amount of duplication here you should create a helper function to simplify this

if you can use the existing toolDescriptionMap without changing it, when you can map your changes in directly.

const overrideContent = await readToolOverrideWithArgs(args.cwd, "read_file", args)
return overrideContent || getReadFileDescription(args)
},
fetch_instructions: async (args) => {
const overrideContent = await readToolOverrideWithArgs(args.cwd, "fetch_instructions", args)
return overrideContent || getFetchInstructionsDescription()
},
write_to_file: async (args) => {
const overrideContent = await readToolOverrideWithArgs(args.cwd, "write_to_file", args)
return overrideContent || getWriteToFileDescription(args)
},
search_files: async (args) => {
const overrideContent = await readToolOverrideWithArgs(args.cwd, "search_files", args)
return overrideContent || getSearchFilesDescription(args)
},
list_files: async (args) => {
const overrideContent = await readToolOverrideWithArgs(args.cwd, "list_files", args)
return overrideContent || getListFilesDescription(args)
},
list_code_definition_names: async (args) => {
const overrideContent = await readToolOverrideWithArgs(args.cwd, "list_code_definition_names", args)
return overrideContent || getListCodeDefinitionNamesDescription(args)
},
browser_action: async (args) => {
const overrideContent = await readToolOverrideWithArgs(args.cwd, "browser_action", args)
return overrideContent || getBrowserActionDescription(args)
},
ask_followup_question: async (args) => {
const overrideContent = await readToolOverrideWithArgs(args.cwd, "ask_followup_question", args)
return overrideContent || getAskFollowupQuestionDescription()
},
attempt_completion: async (args) => {
const overrideContent = await readToolOverrideWithArgs(args.cwd, "attempt_completion", args)
return overrideContent || getAttemptCompletionDescription()
},
use_mcp_tool: async (args) => {
const overrideContent = await readToolOverrideWithArgs(args.cwd, "use_mcp_tool", args)
return overrideContent || getUseMcpToolDescription(args)
},
access_mcp_resource: async (args) => {
const overrideContent = await readToolOverrideWithArgs(args.cwd, "access_mcp_resource", args)
return overrideContent || getAccessMcpResourceDescription(args)
},
codebase_search: async (args) => {
const overrideContent = await readToolOverrideWithArgs(args.cwd, "codebase_search", args)
return overrideContent || getCodebaseSearchDescription()
},
switch_mode: async (args) => {
const overrideContent = await readToolOverrideWithArgs(args.cwd, "switch_mode", args)
return overrideContent || getSwitchModeDescription()
},
new_task: async (args) => {
const overrideContent = await readToolOverrideWithArgs(args.cwd, "new_task", args)
return overrideContent || getNewTaskDescription(args)
},
insert_content: async (args) => {
const overrideContent = await readToolOverrideWithArgs(args.cwd, "insert_content", args)
return overrideContent || getInsertContentDescription(args)
},
search_and_replace: async (args) => {
const overrideContent = await readToolOverrideWithArgs(args.cwd, "search_and_replace", args)
return overrideContent || getSearchAndReplaceDescription(args)
},
apply_diff: async (args) => {
const overrideContent = await readToolOverrideWithArgs(
args.cwd,
`apply_diff${args.diffStrategy ? args.diffStrategy.getName() : ''}`,
args,
)
return (
overrideContent ||
(args.diffStrategy
? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions })
: "")
)
},
}

export function getToolDescriptionsForMode(
export async function getToolDescriptionsForMode(
mode: Mode,
cwd: string,
supportsComputerUse: boolean,
Expand All @@ -59,7 +121,7 @@ export function getToolDescriptionsForMode(
experiments?: Record<string, boolean>,
partialReadsEnabled?: boolean,
settings?: Record<string, any>,
): string {
): Promise<string> {
const config = getModeConfig(mode, customModes)
const args: ToolArgs = {
cwd,
Expand Down Expand Up @@ -107,18 +169,19 @@ export function getToolDescriptionsForMode(
}

// Map tool descriptions for allowed tools
const descriptions = Array.from(tools).map((toolName) => {
const descriptionPromises = Array.from(tools).map(async (toolName) => {
const descriptionFn = toolDescriptionMap[toolName]
if (!descriptionFn) {
return undefined
}

return descriptionFn({
return await descriptionFn({
...args,
toolOptions: undefined, // No tool options in group-based approach
})
})

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

Expand Down
Loading