mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
feat: simple read_file tool for single-file-only models (#7222)
This commit is contained in:
parent
865fb18334
commit
9a7ddab1bc
8 changed files with 397 additions and 4 deletions
|
|
@ -12,6 +12,7 @@ export * from "./message.js"
|
|||
export * from "./mode.js"
|
||||
export * from "./model.js"
|
||||
export * from "./provider-settings.js"
|
||||
export * from "./single-file-read-models.js"
|
||||
export * from "./task.js"
|
||||
export * from "./todo.js"
|
||||
export * from "./telemetry.js"
|
||||
|
|
|
|||
32
packages/types/src/single-file-read-models.ts
Normal file
32
packages/types/src/single-file-read-models.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* Configuration for models that should use simplified single-file read_file tool
|
||||
* These models will use the simpler <read_file><path>...</path></read_file> format
|
||||
* instead of the more complex multi-file args format
|
||||
*/
|
||||
|
||||
// List of model IDs (or patterns) that should use single file reads only
|
||||
export const SINGLE_FILE_READ_MODELS = new Set<string>(["roo/sonic"])
|
||||
|
||||
/**
|
||||
* Check if a model should use single file read format
|
||||
* @param modelId The model ID to check
|
||||
* @returns true if the model should use single file reads
|
||||
*/
|
||||
export function shouldUseSingleFileRead(modelId: string): boolean {
|
||||
// Direct match
|
||||
if (SINGLE_FILE_READ_MODELS.has(modelId)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Pattern matching for model families
|
||||
// Check if model ID starts with any configured pattern
|
||||
// Using Array.from for compatibility with older TypeScript targets
|
||||
const patterns = Array.from(SINGLE_FILE_READ_MODELS)
|
||||
for (const pattern of patterns) {
|
||||
if (pattern.endsWith("*") && modelId.startsWith(pattern.slice(0, -1))) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
@ -10,6 +10,8 @@ import type { ToolParamName, ToolResponse } from "../../shared/tools"
|
|||
import { fetchInstructionsTool } from "../tools/fetchInstructionsTool"
|
||||
import { listFilesTool } from "../tools/listFilesTool"
|
||||
import { getReadFileToolDescription, readFileTool } from "../tools/readFileTool"
|
||||
import { getSimpleReadFileToolDescription, simpleReadFileTool } from "../tools/simpleReadFileTool"
|
||||
import { shouldUseSingleFileRead } from "@roo-code/types"
|
||||
import { writeToFileTool } from "../tools/writeToFileTool"
|
||||
import { applyDiffTool } from "../tools/multiApplyDiffTool"
|
||||
import { insertContentTool } from "../tools/insertContentTool"
|
||||
|
|
@ -155,7 +157,13 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
case "execute_command":
|
||||
return `[${block.name} for '${block.params.command}']`
|
||||
case "read_file":
|
||||
return getReadFileToolDescription(block.name, block.params)
|
||||
// Check if this model should use the simplified description
|
||||
const modelId = cline.api.getModel().id
|
||||
if (shouldUseSingleFileRead(modelId)) {
|
||||
return getSimpleReadFileToolDescription(block.name, block.params)
|
||||
} else {
|
||||
return getReadFileToolDescription(block.name, block.params)
|
||||
}
|
||||
case "fetch_instructions":
|
||||
return `[${block.name} for '${block.params.task}']`
|
||||
case "write_to_file":
|
||||
|
|
@ -454,8 +462,20 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
await searchAndReplaceTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
break
|
||||
case "read_file":
|
||||
await readFileTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
|
||||
// Check if this model should use the simplified single-file read tool
|
||||
const modelId = cline.api.getModel().id
|
||||
if (shouldUseSingleFileRead(modelId)) {
|
||||
await simpleReadFileTool(
|
||||
cline,
|
||||
block,
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
)
|
||||
} else {
|
||||
await readFileTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
}
|
||||
break
|
||||
case "fetch_instructions":
|
||||
await fetchInstructionsTool(cline, block, askApproval, handleError, pushToolResult)
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ async function generatePrompt(
|
|||
partialReadsEnabled?: boolean,
|
||||
settings?: SystemPromptSettings,
|
||||
todoList?: TodoItem[],
|
||||
modelId?: string,
|
||||
): Promise<string> {
|
||||
if (!context) {
|
||||
throw new Error("Extension context is required for generating system prompt")
|
||||
|
|
@ -106,6 +107,7 @@ ${getToolDescriptionsForMode(
|
|||
partialReadsEnabled,
|
||||
settings,
|
||||
enableMcpServerCreation,
|
||||
modelId,
|
||||
)}
|
||||
|
||||
${getToolUseGuidelinesSection(codeIndexManager)}
|
||||
|
|
@ -150,6 +152,7 @@ export const SYSTEM_PROMPT = async (
|
|||
partialReadsEnabled?: boolean,
|
||||
settings?: SystemPromptSettings,
|
||||
todoList?: TodoItem[],
|
||||
modelId?: string,
|
||||
): Promise<string> => {
|
||||
if (!context) {
|
||||
throw new Error("Extension context is required for generating system prompt")
|
||||
|
|
@ -221,5 +224,6 @@ ${customInstructions}`
|
|||
partialReadsEnabled,
|
||||
settings,
|
||||
todoList,
|
||||
modelId,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import { Mode, getModeConfig, isToolAllowedForMode, getGroupName } from "../../.
|
|||
import { ToolArgs } from "./types"
|
||||
import { getExecuteCommandDescription } from "./execute-command"
|
||||
import { getReadFileDescription } from "./read-file"
|
||||
import { getSimpleReadFileDescription } from "./simple-read-file"
|
||||
import { getFetchInstructionsDescription } from "./fetch-instructions"
|
||||
import { shouldUseSingleFileRead } from "@roo-code/types"
|
||||
import { getWriteToFileDescription } from "./write-to-file"
|
||||
import { getSearchFilesDescription } from "./search-files"
|
||||
import { getListFilesDescription } from "./list-files"
|
||||
|
|
@ -28,7 +30,14 @@ 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),
|
||||
read_file: (args) => {
|
||||
// Check if the current model should use the simplified read_file tool
|
||||
const modelId = args.settings?.modelId
|
||||
if (modelId && shouldUseSingleFileRead(modelId)) {
|
||||
return getSimpleReadFileDescription(args)
|
||||
}
|
||||
return getReadFileDescription(args)
|
||||
},
|
||||
fetch_instructions: (args) => getFetchInstructionsDescription(args.settings?.enableMcpServerCreation),
|
||||
write_to_file: (args) => getWriteToFileDescription(args),
|
||||
search_files: (args) => getSearchFilesDescription(args),
|
||||
|
|
@ -62,6 +71,7 @@ export function getToolDescriptionsForMode(
|
|||
partialReadsEnabled?: boolean,
|
||||
settings?: Record<string, any>,
|
||||
enableMcpServerCreation?: boolean,
|
||||
modelId?: string,
|
||||
): string {
|
||||
const config = getModeConfig(mode, customModes)
|
||||
const args: ToolArgs = {
|
||||
|
|
@ -74,6 +84,7 @@ export function getToolDescriptionsForMode(
|
|||
settings: {
|
||||
...settings,
|
||||
enableMcpServerCreation,
|
||||
modelId,
|
||||
},
|
||||
experiments,
|
||||
}
|
||||
|
|
@ -138,6 +149,7 @@ export function getToolDescriptionsForMode(
|
|||
export {
|
||||
getExecuteCommandDescription,
|
||||
getReadFileDescription,
|
||||
getSimpleReadFileDescription,
|
||||
getFetchInstructionsDescription,
|
||||
getWriteToFileDescription,
|
||||
getSearchFilesDescription,
|
||||
|
|
|
|||
35
src/core/prompts/tools/simple-read-file.ts
Normal file
35
src/core/prompts/tools/simple-read-file.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { ToolArgs } from "./types"
|
||||
|
||||
/**
|
||||
* Generate a simplified read_file tool description for models that only support single file reads
|
||||
* Uses the simpler format: <read_file><path>file/path.ext</path></read_file>
|
||||
*/
|
||||
export function getSimpleReadFileDescription(args: ToolArgs): string {
|
||||
return `## read_file
|
||||
Description: Request to read the contents of a file. The tool outputs line-numbered content (e.g. "1 | const x = 1") for easy reference when discussing code.
|
||||
|
||||
Parameters:
|
||||
- path: (required) File path (relative to workspace directory ${args.cwd})
|
||||
|
||||
Usage:
|
||||
<read_file>
|
||||
<path>path/to/file</path>
|
||||
</read_file>
|
||||
|
||||
Examples:
|
||||
|
||||
1. Reading a TypeScript file:
|
||||
<read_file>
|
||||
<path>src/app.ts</path>
|
||||
</read_file>
|
||||
|
||||
2. Reading a configuration file:
|
||||
<read_file>
|
||||
<path>config.json</path>
|
||||
</read_file>
|
||||
|
||||
3. Reading a markdown file:
|
||||
<read_file>
|
||||
<path>README.md</path>
|
||||
</read_file>`
|
||||
}
|
||||
|
|
@ -2226,6 +2226,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
todoListEnabled: apiConfiguration?.todoListEnabled ?? true,
|
||||
useAgentRules: vscode.workspace.getConfiguration("roo-cline").get<boolean>("useAgentRules") ?? true,
|
||||
},
|
||||
undefined, // todoList
|
||||
this.api.getModel().id,
|
||||
)
|
||||
})()
|
||||
}
|
||||
|
|
|
|||
287
src/core/tools/simpleReadFileTool.ts
Normal file
287
src/core/tools/simpleReadFileTool.ts
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
import path from "path"
|
||||
import { isBinaryFile } from "isbinaryfile"
|
||||
|
||||
import { Task } from "../task/Task"
|
||||
import { ClineSayTool } from "../../shared/ExtensionMessage"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { t } from "../../i18n"
|
||||
import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
|
||||
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
|
||||
import { isPathOutsideWorkspace } from "../../utils/pathUtils"
|
||||
import { getReadablePath } from "../../utils/path"
|
||||
import { countFileLines } from "../../integrations/misc/line-counter"
|
||||
import { readLines } from "../../integrations/misc/read-lines"
|
||||
import { extractTextFromFile, addLineNumbers, getSupportedBinaryFormats } from "../../integrations/misc/extract-text"
|
||||
import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter"
|
||||
import {
|
||||
DEFAULT_MAX_IMAGE_FILE_SIZE_MB,
|
||||
DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB,
|
||||
isSupportedImageFormat,
|
||||
validateImageForProcessing,
|
||||
processImageFile,
|
||||
} from "./helpers/imageHelpers"
|
||||
|
||||
/**
|
||||
* Simplified read file tool for models that only support single file reads
|
||||
* Uses the format: <read_file><path>file/path.ext</path></read_file>
|
||||
*
|
||||
* This is a streamlined version of readFileTool that:
|
||||
* - Only accepts a single path parameter
|
||||
* - Does not support multiple files
|
||||
* - Does not support line ranges
|
||||
* - Has simpler XML parsing
|
||||
*/
|
||||
export async function simpleReadFileTool(
|
||||
cline: Task,
|
||||
block: ToolUse,
|
||||
askApproval: AskApproval,
|
||||
handleError: HandleError,
|
||||
pushToolResult: PushToolResult,
|
||||
_removeClosingTag: RemoveClosingTag,
|
||||
) {
|
||||
const filePath: string | undefined = block.params.path
|
||||
|
||||
// Check if the current model supports images
|
||||
const modelInfo = cline.api.getModel().info
|
||||
const supportsImages = modelInfo.supportsImages ?? false
|
||||
|
||||
// Handle partial message
|
||||
if (block.partial) {
|
||||
const fullPath = filePath ? path.resolve(cline.cwd, filePath) : ""
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
tool: "readFile",
|
||||
path: getReadablePath(cline.cwd, filePath || ""),
|
||||
isOutsideWorkspace: filePath ? isPathOutsideWorkspace(fullPath) : false,
|
||||
}
|
||||
const partialMessage = JSON.stringify({
|
||||
...sharedMessageProps,
|
||||
content: undefined,
|
||||
} satisfies ClineSayTool)
|
||||
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate path parameter
|
||||
if (!filePath) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolError("read_file")
|
||||
const errorMsg = await cline.sayAndCreateMissingParamError("read_file", "path")
|
||||
pushToolResult(`<file><error>${errorMsg}</error></file>`)
|
||||
return
|
||||
}
|
||||
|
||||
const relPath = filePath
|
||||
const fullPath = path.resolve(cline.cwd, relPath)
|
||||
|
||||
try {
|
||||
// Check RooIgnore validation
|
||||
const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath)
|
||||
if (!accessAllowed) {
|
||||
await cline.say("rooignore_error", relPath)
|
||||
const errorMsg = formatResponse.rooIgnoreError(relPath)
|
||||
pushToolResult(`<file><path>${relPath}</path><error>${errorMsg}</error></file>`)
|
||||
return
|
||||
}
|
||||
|
||||
// Get max read file line setting
|
||||
const { maxReadFileLine = -1 } = (await cline.providerRef.deref()?.getState()) ?? {}
|
||||
|
||||
// Create approval message
|
||||
const isOutsideWorkspace = isPathOutsideWorkspace(fullPath)
|
||||
let lineSnippet = ""
|
||||
if (maxReadFileLine === 0) {
|
||||
lineSnippet = t("tools:readFile.definitionsOnly")
|
||||
} else if (maxReadFileLine > 0) {
|
||||
lineSnippet = t("tools:readFile.maxLines", { max: maxReadFileLine })
|
||||
}
|
||||
|
||||
const completeMessage = JSON.stringify({
|
||||
tool: "readFile",
|
||||
path: getReadablePath(cline.cwd, relPath),
|
||||
isOutsideWorkspace,
|
||||
content: fullPath,
|
||||
reason: lineSnippet,
|
||||
} satisfies ClineSayTool)
|
||||
|
||||
const { response, text, images } = await cline.ask("tool", completeMessage, false)
|
||||
|
||||
if (response !== "yesButtonClicked") {
|
||||
// Handle denial
|
||||
if (text) {
|
||||
await cline.say("user_feedback", text, images)
|
||||
}
|
||||
cline.didRejectTool = true
|
||||
|
||||
const statusMessage = text ? formatResponse.toolDeniedWithFeedback(text) : formatResponse.toolDenied()
|
||||
|
||||
pushToolResult(`${statusMessage}\n<file><path>${relPath}</path><status>Denied by user</status></file>`)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle approval with feedback
|
||||
if (text) {
|
||||
await cline.say("user_feedback", text, images)
|
||||
}
|
||||
|
||||
// Process the file
|
||||
const [totalLines, isBinary] = await Promise.all([countFileLines(fullPath), isBinaryFile(fullPath)])
|
||||
|
||||
// Handle binary files
|
||||
if (isBinary) {
|
||||
const fileExtension = path.extname(relPath).toLowerCase()
|
||||
const supportedBinaryFormats = getSupportedBinaryFormats()
|
||||
|
||||
// Check if it's a supported image format
|
||||
if (isSupportedImageFormat(fileExtension)) {
|
||||
try {
|
||||
const {
|
||||
maxImageFileSize = DEFAULT_MAX_IMAGE_FILE_SIZE_MB,
|
||||
maxTotalImageSize = DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB,
|
||||
} = (await cline.providerRef.deref()?.getState()) ?? {}
|
||||
|
||||
// Validate image for processing
|
||||
const validationResult = await validateImageForProcessing(
|
||||
fullPath,
|
||||
supportsImages,
|
||||
maxImageFileSize,
|
||||
maxTotalImageSize,
|
||||
0, // No cumulative memory for single file
|
||||
)
|
||||
|
||||
if (!validationResult.isValid) {
|
||||
await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource)
|
||||
pushToolResult(
|
||||
`<file><path>${relPath}</path>\n<notice>${validationResult.notice}</notice>\n</file>`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Process the image
|
||||
const imageResult = await processImageFile(fullPath)
|
||||
await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource)
|
||||
|
||||
// Return result with image data
|
||||
const result = formatResponse.toolResult(
|
||||
`<file><path>${relPath}</path>\n<notice>${imageResult.notice}</notice>\n</file>`,
|
||||
supportsImages ? [imageResult.dataUrl] : undefined,
|
||||
)
|
||||
|
||||
if (typeof result === "string") {
|
||||
pushToolResult(result)
|
||||
} else {
|
||||
pushToolResult(result)
|
||||
}
|
||||
return
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
pushToolResult(
|
||||
`<file><path>${relPath}</path><error>Error reading image file: ${errorMsg}</error></file>`,
|
||||
)
|
||||
await handleError(
|
||||
`reading image file ${relPath}`,
|
||||
error instanceof Error ? error : new Error(errorMsg),
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a supported binary format that can be processed
|
||||
if (supportedBinaryFormats && supportedBinaryFormats.includes(fileExtension)) {
|
||||
// For supported binary formats (.pdf, .docx, .ipynb), continue to extractTextFromFile
|
||||
// Fall through to the normal extractTextFromFile processing below
|
||||
} else {
|
||||
// Handle unknown binary format
|
||||
const fileFormat = fileExtension.slice(1) || "bin"
|
||||
pushToolResult(
|
||||
`<file><path>${relPath}</path>\n<binary_file format="${fileFormat}">Binary file - content not displayed</binary_file>\n</file>`,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handle definitions-only mode
|
||||
if (maxReadFileLine === 0) {
|
||||
try {
|
||||
const defResult = await parseSourceCodeDefinitionsForFile(fullPath, cline.rooIgnoreController)
|
||||
if (defResult) {
|
||||
let xmlInfo = `<notice>Showing only definitions. Use standard read_file if you need to read actual content</notice>\n`
|
||||
pushToolResult(
|
||||
`<file><path>${relPath}</path>\n<list_code_definition_names>${defResult}</list_code_definition_names>\n${xmlInfo}</file>`,
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.startsWith("Unsupported language:")) {
|
||||
console.warn(`[simple_read_file] Warning: ${error.message}`)
|
||||
} else {
|
||||
console.error(
|
||||
`[simple_read_file] Unhandled error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle files exceeding line threshold
|
||||
if (maxReadFileLine > 0 && totalLines > maxReadFileLine) {
|
||||
const content = addLineNumbers(await readLines(fullPath, maxReadFileLine - 1, 0))
|
||||
const lineRangeAttr = ` lines="1-${maxReadFileLine}"`
|
||||
let xmlInfo = `<content${lineRangeAttr}>\n${content}</content>\n`
|
||||
|
||||
try {
|
||||
const defResult = await parseSourceCodeDefinitionsForFile(fullPath, cline.rooIgnoreController)
|
||||
if (defResult) {
|
||||
xmlInfo += `<list_code_definition_names>${defResult}</list_code_definition_names>\n`
|
||||
}
|
||||
xmlInfo += `<notice>Showing only ${maxReadFileLine} of ${totalLines} total lines. File is too large for complete display</notice>\n`
|
||||
pushToolResult(`<file><path>${relPath}</path>\n${xmlInfo}</file>`)
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.startsWith("Unsupported language:")) {
|
||||
console.warn(`[simple_read_file] Warning: ${error.message}`)
|
||||
} else {
|
||||
console.error(
|
||||
`[simple_read_file] Unhandled error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle normal file read
|
||||
const content = await extractTextFromFile(fullPath)
|
||||
const lineRangeAttr = ` lines="1-${totalLines}"`
|
||||
let xmlInfo = totalLines > 0 ? `<content${lineRangeAttr}>\n${content}</content>\n` : `<content/>`
|
||||
|
||||
if (totalLines === 0) {
|
||||
xmlInfo += `<notice>File is empty</notice>\n`
|
||||
}
|
||||
|
||||
// Track file read
|
||||
await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource)
|
||||
|
||||
// Return the result
|
||||
if (text) {
|
||||
const statusMessage = formatResponse.toolApprovedWithFeedback(text)
|
||||
pushToolResult(`${statusMessage}\n<file><path>${relPath}</path>\n${xmlInfo}</file>`)
|
||||
} else {
|
||||
pushToolResult(`<file><path>${relPath}</path>\n${xmlInfo}</file>`)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
pushToolResult(`<file><path>${relPath}</path><error>Error reading file: ${errorMsg}</error></file>`)
|
||||
await handleError(`reading file ${relPath}`, error instanceof Error ? error : new Error(errorMsg))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get description for the simple read file tool
|
||||
* @param blockName The name of the tool block
|
||||
* @param blockParams The parameters passed to the tool
|
||||
* @returns A description string for the tool use
|
||||
*/
|
||||
export function getSimpleReadFileToolDescription(blockName: string, blockParams: any): string {
|
||||
if (blockParams.path) {
|
||||
return `[${blockName} for '${blockParams.path}']`
|
||||
} else {
|
||||
return `[${blockName} with missing path]`
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue