diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index c65d070086..b151067d1d 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -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" diff --git a/packages/types/src/single-file-read-models.ts b/packages/types/src/single-file-read-models.ts new file mode 100644 index 0000000000..4e6f4e1f57 --- /dev/null +++ b/packages/types/src/single-file-read-models.ts @@ -0,0 +1,32 @@ +/** + * Configuration for models that should use simplified single-file read_file tool + * These models will use the simpler ... 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(["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 +} diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index acdc7f5412..a8b90728b1 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -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) diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 4ed1185da7..3cc327c815 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -61,6 +61,7 @@ async function generatePrompt( partialReadsEnabled?: boolean, settings?: SystemPromptSettings, todoList?: TodoItem[], + modelId?: string, ): Promise { 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 => { if (!context) { throw new Error("Extension context is required for generating system prompt") @@ -221,5 +224,6 @@ ${customInstructions}` partialReadsEnabled, settings, todoList, + modelId, ) } diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts index d455abb8d7..3eb112d270 100644 --- a/src/core/prompts/tools/index.ts +++ b/src/core/prompts/tools/index.ts @@ -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 | 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, 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, diff --git a/src/core/prompts/tools/simple-read-file.ts b/src/core/prompts/tools/simple-read-file.ts new file mode 100644 index 0000000000..28f4f1129e --- /dev/null +++ b/src/core/prompts/tools/simple-read-file.ts @@ -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: file/path.ext + */ +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: + +path/to/file + + +Examples: + +1. Reading a TypeScript file: + +src/app.ts + + +2. Reading a configuration file: + +config.json + + +3. Reading a markdown file: + +README.md +` +} diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index cff8d5aec3..34f3218236 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -2226,6 +2226,8 @@ export class Task extends EventEmitter implements TaskLike { todoListEnabled: apiConfiguration?.todoListEnabled ?? true, useAgentRules: vscode.workspace.getConfiguration("roo-cline").get("useAgentRules") ?? true, }, + undefined, // todoList + this.api.getModel().id, ) })() } diff --git a/src/core/tools/simpleReadFileTool.ts b/src/core/tools/simpleReadFileTool.ts new file mode 100644 index 0000000000..ee6656c5c8 --- /dev/null +++ b/src/core/tools/simpleReadFileTool.ts @@ -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: file/path.ext + * + * 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(`${errorMsg}`) + 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(`${relPath}${errorMsg}`) + 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${relPath}Denied by user`) + 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( + `${relPath}\n${validationResult.notice}\n`, + ) + 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( + `${relPath}\n${imageResult.notice}\n`, + 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( + `${relPath}Error reading image file: ${errorMsg}`, + ) + 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( + `${relPath}\nBinary file - content not displayed\n`, + ) + return + } + } + + // Handle definitions-only mode + if (maxReadFileLine === 0) { + try { + const defResult = await parseSourceCodeDefinitionsForFile(fullPath, cline.rooIgnoreController) + if (defResult) { + let xmlInfo = `Showing only definitions. Use standard read_file if you need to read actual content\n` + pushToolResult( + `${relPath}\n${defResult}\n${xmlInfo}`, + ) + } + } 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 = `\n${content}\n` + + try { + const defResult = await parseSourceCodeDefinitionsForFile(fullPath, cline.rooIgnoreController) + if (defResult) { + xmlInfo += `${defResult}\n` + } + xmlInfo += `Showing only ${maxReadFileLine} of ${totalLines} total lines. File is too large for complete display\n` + pushToolResult(`${relPath}\n${xmlInfo}`) + } 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 ? `\n${content}\n` : `` + + if (totalLines === 0) { + xmlInfo += `File is empty\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${relPath}\n${xmlInfo}`) + } else { + pushToolResult(`${relPath}\n${xmlInfo}`) + } + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error) + pushToolResult(`${relPath}Error reading file: ${errorMsg}`) + 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]` + } +}