Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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?.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