Files
gemini-cli/packages/core/src/tools/read-file.ts

265 lines
6.7 KiB
TypeScript

/**
* @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 {
BaseDeclarativeTool,
BaseToolInvocation,
Kind,
type ToolInvocation,
type ToolLocation,
type ToolResult,
} 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, READ_FILE_DISPLAY_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, 1-based)
*/
start_line?: number;
/**
* The line number to end reading at (optional, 1-based, inclusive)
*/
end_line?: 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.start_line,
},
];
}
async execute(): Promise<ToolResult> {
const validationError = this.config.validatePathAccess(
this.resolvedPath,
'read',
);
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.start_line,
this.params.end_line,
);
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!;
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 'start_line' and 'end_line' parameters in a subsequent 'read_file' call. For example, to read the next section of the file, use start_line: ${end + 1}.
--- 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,
READ_FILE_DISPLAY_NAME,
READ_FILE_DEFINITION.base.description!,
Kind.Read,
READ_FILE_DEFINITION.base.parametersJsonSchema,
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,
'read',
);
if (validationError) {
return validationError;
}
if (params.start_line !== undefined && params.start_line < 1) {
return 'start_line must be at least 1';
}
if (params.end_line !== undefined && params.end_line < 1) {
return 'end_line must be at least 1';
}
if (
params.start_line !== undefined &&
params.end_line !== undefined &&
params.start_line > params.end_line
) {
return 'start_line cannot be greater than end_line';
}
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<ReadFileToolParams, ToolResult> {
return new ReadFileToolInvocation(
this.config,
params,
messageBus,
_toolName,
_toolDisplayName,
);
}
override getSchema(modelId?: string) {
return resolveToolDeclaration(READ_FILE_DEFINITION, modelId);
}
}