/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import type { MessageBus } from '../confirmation-bus/message-bus.js'; import path from 'node:path'; import { makeRelative, shortenPath } from '../utils/paths.js'; import type { ToolInvocation, ToolLocation, ToolResult } from './tools.js'; import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js'; import { ToolErrorType } from './tool-error.js'; import type { PartUnion } from '@google/genai'; import { processSingleFileContent, getSpecificMimeType, } from '../utils/fileUtils.js'; import type { Config } from '../config/config.js'; import { FileOperation } from '../telemetry/metrics.js'; import { getProgrammingLanguage } from '../telemetry/telemetry-utils.js'; import { logFileOperation } from '../telemetry/loggers.js'; import { FileOperationEvent } from '../telemetry/types.js'; import { READ_FILE_TOOL_NAME } from './tool-names.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import { READ_FILE_DEFINITION } from './definitions/coreTools.js'; import { resolveToolDeclaration } from './definitions/resolver.js'; /** * Parameters for the ReadFile tool */ export interface ReadFileToolParams { /** * The path to the file to read */ file_path: string; /** * The line number to start reading from (optional) */ offset?: number; /** * The number of lines to read (optional) */ limit?: number; } class ReadFileToolInvocation extends BaseToolInvocation< ReadFileToolParams, ToolResult > { private readonly resolvedPath: string; constructor( private config: Config, params: ReadFileToolParams, messageBus: MessageBus, _toolName?: string, _toolDisplayName?: string, ) { super(params, messageBus, _toolName, _toolDisplayName); this.resolvedPath = path.resolve( this.config.getTargetDir(), this.params.file_path, ); } getDescription(): string { const relativePath = makeRelative( this.resolvedPath, this.config.getTargetDir(), ); return shortenPath(relativePath); } override toolLocations(): ToolLocation[] { return [{ path: this.resolvedPath, line: this.params.offset }]; } async execute(): Promise { const validationError = this.config.validatePathAccess(this.resolvedPath); if (validationError) { return { llmContent: validationError, returnDisplay: 'Path not in workspace.', error: { message: validationError, type: ToolErrorType.PATH_NOT_IN_WORKSPACE, }, }; } const result = await processSingleFileContent( this.resolvedPath, this.config.getTargetDir(), this.config.getFileSystemService(), this.params.offset, this.params.limit, ); if (result.error) { return { llmContent: result.llmContent, returnDisplay: result.returnDisplay || 'Error reading file', error: { message: result.error, type: result.errorType, }, }; } let llmContent: PartUnion; if (result.isTruncated) { const [start, end] = result.linesShown!; const total = result.originalLineCount!; const nextOffset = this.params.offset ? this.params.offset + end - start + 1 : end; llmContent = ` IMPORTANT: The file content has been truncated. Status: Showing lines ${start}-${end} of ${total} total lines. Action: To read more of the file, you can use the 'offset' and 'limit' parameters in a subsequent 'read_file' call. For example, to read the next section of the file, use offset: ${nextOffset}. --- FILE CONTENT (truncated) --- ${result.llmContent}`; } else { llmContent = result.llmContent || ''; } const lines = typeof result.llmContent === 'string' ? result.llmContent.split('\n').length : undefined; const mimetype = getSpecificMimeType(this.resolvedPath); const programming_language = getProgrammingLanguage({ file_path: this.resolvedPath, }); logFileOperation( this.config, new FileOperationEvent( READ_FILE_TOOL_NAME, FileOperation.READ, lines, mimetype, path.extname(this.resolvedPath), programming_language, ), ); return { llmContent, returnDisplay: result.returnDisplay || '', }; } } /** * Implementation of the ReadFile tool logic */ export class ReadFileTool extends BaseDeclarativeTool< ReadFileToolParams, ToolResult > { static readonly Name = READ_FILE_TOOL_NAME; private readonly fileDiscoveryService: FileDiscoveryService; constructor( private config: Config, messageBus: MessageBus, ) { super( ReadFileTool.Name, 'ReadFile', READ_FILE_DEFINITION.base.description!, Kind.Read, READ_FILE_DEFINITION.base.parameters!, messageBus, true, false, ); this.fileDiscoveryService = new FileDiscoveryService( config.getTargetDir(), config.getFileFilteringOptions(), ); } protected override validateToolParamValues( params: ReadFileToolParams, ): string | null { if (params.file_path.trim() === '') { return "The 'file_path' parameter must be non-empty."; } const resolvedPath = path.resolve( this.config.getTargetDir(), params.file_path, ); const validationError = this.config.validatePathAccess(resolvedPath); if (validationError) { return validationError; } if (params.offset !== undefined && params.offset < 0) { return 'Offset must be a non-negative number'; } if (params.limit !== undefined && params.limit <= 0) { return 'Limit must be a positive number'; } const fileFilteringOptions = this.config.getFileFilteringOptions(); if ( this.fileDiscoveryService.shouldIgnoreFile( resolvedPath, fileFilteringOptions, ) ) { return `File path '${resolvedPath}' is ignored by configured ignore patterns.`; } return null; } protected createInvocation( params: ReadFileToolParams, messageBus: MessageBus, _toolName?: string, _toolDisplayName?: string, ): ToolInvocation { return new ReadFileToolInvocation( this.config, params, messageBus, _toolName, _toolDisplayName, ); } override getSchema(modelId?: string) { if (!modelId) { return super.getSchema(); } return resolveToolDeclaration(READ_FILE_DEFINITION, modelId); } }