chore: remove read_file handler (#15773)

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
jif-oai
2026-03-25 16:27:32 +00:00
committed by GitHub
parent c6ffe9abab
commit 14c35a16a8
3 changed files with 0 additions and 611 deletions

View File

@@ -10,7 +10,6 @@ pub(crate) mod multi_agents;
pub(crate) mod multi_agents_common;
pub(crate) mod multi_agents_v2;
mod plan;
mod read_file;
mod request_permissions;
mod request_user_input;
mod shell;
@@ -46,7 +45,6 @@ pub use list_dir::ListDirHandler;
pub use mcp::McpHandler;
pub use mcp_resource::McpResourceHandler;
pub use plan::PlanHandler;
pub use read_file::ReadFileHandler;
pub use request_permissions::RequestPermissionsHandler;
pub(crate) use request_permissions::request_permissions_tool_description;
pub use request_user_input::RequestUserInputHandler;

View File

@@ -1,489 +0,0 @@
use std::collections::VecDeque;
use std::path::PathBuf;
use async_trait::async_trait;
use codex_utils_string::take_bytes_at_char_boundary;
use serde::Deserialize;
use crate::function_tool::FunctionCallError;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
pub struct ReadFileHandler;
const MAX_LINE_LENGTH: usize = 500;
const TAB_WIDTH: usize = 4;
// TODO(jif) add support for block comments
const COMMENT_PREFIXES: &[&str] = &["#", "//", "--"];
/// JSON arguments accepted by the `read_file` tool handler.
#[derive(Deserialize)]
struct ReadFileArgs {
/// Absolute path to the file that will be read.
file_path: String,
/// 1-indexed line number to start reading from; defaults to 1.
#[serde(default = "defaults::offset")]
offset: usize,
/// Maximum number of lines to return; defaults to 2000.
#[serde(default = "defaults::limit")]
limit: usize,
/// Determines whether the handler reads a simple slice or indentation-aware block.
#[serde(default)]
mode: ReadMode,
/// Optional indentation configuration used when `mode` is `Indentation`.
#[serde(default)]
indentation: Option<IndentationArgs>,
}
#[derive(Deserialize, Default)]
#[serde(rename_all = "snake_case")]
enum ReadMode {
#[default]
Slice,
Indentation,
}
/// Additional configuration for indentation-aware reads.
#[derive(Deserialize, Clone)]
struct IndentationArgs {
/// Optional explicit anchor line; defaults to `offset` when omitted.
#[serde(default)]
anchor_line: Option<usize>,
/// Maximum indentation depth to collect; `0` means unlimited.
#[serde(default = "defaults::max_levels")]
max_levels: usize,
/// Whether to include sibling blocks at the same indentation level.
#[serde(default = "defaults::include_siblings")]
include_siblings: bool,
/// Whether to include header lines above the anchor block. This made on a best effort basis.
#[serde(default = "defaults::include_header")]
include_header: bool,
/// Optional hard cap on returned lines; defaults to the global `limit`.
#[serde(default)]
max_lines: Option<usize>,
}
#[derive(Clone, Debug)]
struct LineRecord {
number: usize,
raw: String,
display: String,
indent: usize,
}
impl LineRecord {
fn trimmed(&self) -> &str {
self.raw.trim_start()
}
fn is_blank(&self) -> bool {
self.trimmed().is_empty()
}
fn is_comment(&self) -> bool {
COMMENT_PREFIXES
.iter()
.any(|prefix| self.raw.trim().starts_with(prefix))
}
}
#[async_trait]
impl ToolHandler for ReadFileHandler {
type Output = FunctionToolOutput;
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation { payload, .. } = invocation;
let arguments = match payload {
ToolPayload::Function { arguments } => arguments,
_ => {
return Err(FunctionCallError::RespondToModel(
"read_file handler received unsupported payload".to_string(),
));
}
};
let args: ReadFileArgs = parse_arguments(&arguments)?;
let ReadFileArgs {
file_path,
offset,
limit,
mode,
indentation,
} = args;
if offset == 0 {
return Err(FunctionCallError::RespondToModel(
"offset must be a 1-indexed line number".to_string(),
));
}
if limit == 0 {
return Err(FunctionCallError::RespondToModel(
"limit must be greater than zero".to_string(),
));
}
let path = PathBuf::from(&file_path);
if !path.is_absolute() {
return Err(FunctionCallError::RespondToModel(
"file_path must be an absolute path".to_string(),
));
}
let collected = match mode {
ReadMode::Slice => slice::read(&path, offset, limit).await?,
ReadMode::Indentation => {
let indentation = indentation.unwrap_or_default();
indentation::read_block(&path, offset, limit, indentation).await?
}
};
Ok(FunctionToolOutput::from_text(
collected.join("\n"),
Some(true),
))
}
}
mod slice {
use crate::function_tool::FunctionCallError;
use crate::tools::handlers::read_file::format_line;
use std::path::Path;
use tokio::fs::File;
use tokio::io::AsyncBufReadExt;
use tokio::io::BufReader;
pub async fn read(
path: &Path,
offset: usize,
limit: usize,
) -> Result<Vec<String>, FunctionCallError> {
let file = File::open(path).await.map_err(|err| {
FunctionCallError::RespondToModel(format!("failed to read file: {err}"))
})?;
let mut reader = BufReader::new(file);
let mut collected = Vec::new();
let mut seen = 0usize;
let mut buffer = Vec::new();
loop {
buffer.clear();
let bytes_read = reader.read_until(b'\n', &mut buffer).await.map_err(|err| {
FunctionCallError::RespondToModel(format!("failed to read file: {err}"))
})?;
if bytes_read == 0 {
break;
}
if buffer.last() == Some(&b'\n') {
buffer.pop();
if buffer.last() == Some(&b'\r') {
buffer.pop();
}
}
seen += 1;
if seen < offset {
continue;
}
if collected.len() == limit {
break;
}
let formatted = format_line(&buffer);
collected.push(format!("L{seen}: {formatted}"));
if collected.len() == limit {
break;
}
}
if seen < offset {
return Err(FunctionCallError::RespondToModel(
"offset exceeds file length".to_string(),
));
}
Ok(collected)
}
}
mod indentation {
use crate::function_tool::FunctionCallError;
use crate::tools::handlers::read_file::IndentationArgs;
use crate::tools::handlers::read_file::LineRecord;
use crate::tools::handlers::read_file::TAB_WIDTH;
use crate::tools::handlers::read_file::format_line;
use crate::tools::handlers::read_file::trim_empty_lines;
use std::collections::VecDeque;
use std::path::Path;
use tokio::fs::File;
use tokio::io::AsyncBufReadExt;
use tokio::io::BufReader;
pub async fn read_block(
path: &Path,
offset: usize,
limit: usize,
options: IndentationArgs,
) -> Result<Vec<String>, FunctionCallError> {
let anchor_line = options.anchor_line.unwrap_or(offset);
if anchor_line == 0 {
return Err(FunctionCallError::RespondToModel(
"anchor_line must be a 1-indexed line number".to_string(),
));
}
let guard_limit = options.max_lines.unwrap_or(limit);
if guard_limit == 0 {
return Err(FunctionCallError::RespondToModel(
"max_lines must be greater than zero".to_string(),
));
}
let collected = collect_file_lines(path).await?;
if collected.is_empty() || anchor_line > collected.len() {
return Err(FunctionCallError::RespondToModel(
"anchor_line exceeds file length".to_string(),
));
}
let anchor_index = anchor_line - 1;
let effective_indents = compute_effective_indents(&collected);
let anchor_indent = effective_indents[anchor_index];
// Compute the min indent
let min_indent = if options.max_levels == 0 {
0
} else {
anchor_indent.saturating_sub(options.max_levels * TAB_WIDTH)
};
// Cap requested lines by guard_limit and file length
let final_limit = limit.min(guard_limit).min(collected.len());
if final_limit == 1 {
return Ok(vec![format!(
"L{}: {}",
collected[anchor_index].number, collected[anchor_index].display
)]);
}
// Cursors
let mut i: isize = anchor_index as isize - 1; // up (inclusive)
let mut j: usize = anchor_index + 1; // down (inclusive)
let mut i_counter_min_indent = 0;
let mut j_counter_min_indent = 0;
let mut out = VecDeque::with_capacity(limit);
out.push_back(&collected[anchor_index]);
while out.len() < final_limit {
let mut progressed = 0;
// Up.
if i >= 0 {
let iu = i as usize;
if effective_indents[iu] >= min_indent {
out.push_front(&collected[iu]);
progressed += 1;
i -= 1;
// We do not include the siblings (not applied to comments).
if effective_indents[iu] == min_indent && !options.include_siblings {
let allow_header_comment =
options.include_header && collected[iu].is_comment();
let can_take_line = allow_header_comment || i_counter_min_indent == 0;
if can_take_line {
i_counter_min_indent += 1;
} else {
// This line shouldn't have been taken.
out.pop_front();
progressed -= 1;
i = -1; // consider using Option<usize> or a control flag instead of a sentinel
}
}
// Short-cut.
if out.len() >= final_limit {
break;
}
} else {
// Stop moving up.
i = -1;
}
}
// Down.
if j < collected.len() {
let ju = j;
if effective_indents[ju] >= min_indent {
out.push_back(&collected[ju]);
progressed += 1;
j += 1;
// We do not include the siblings (applied to comments).
if effective_indents[ju] == min_indent && !options.include_siblings {
if j_counter_min_indent > 0 {
// This line shouldn't have been taken.
out.pop_back();
progressed -= 1;
j = collected.len();
}
j_counter_min_indent += 1;
}
} else {
// Stop moving down.
j = collected.len();
}
}
if progressed == 0 {
break;
}
}
// Trim empty lines
trim_empty_lines(&mut out);
Ok(out
.into_iter()
.map(|record| format!("L{}: {}", record.number, record.display))
.collect())
}
async fn collect_file_lines(path: &Path) -> Result<Vec<LineRecord>, FunctionCallError> {
let file = File::open(path).await.map_err(|err| {
FunctionCallError::RespondToModel(format!("failed to read file: {err}"))
})?;
let mut reader = BufReader::new(file);
let mut buffer = Vec::new();
let mut lines = Vec::new();
let mut number = 0usize;
loop {
buffer.clear();
let bytes_read = reader.read_until(b'\n', &mut buffer).await.map_err(|err| {
FunctionCallError::RespondToModel(format!("failed to read file: {err}"))
})?;
if bytes_read == 0 {
break;
}
if buffer.last() == Some(&b'\n') {
buffer.pop();
if buffer.last() == Some(&b'\r') {
buffer.pop();
}
}
number += 1;
let raw = String::from_utf8_lossy(&buffer).into_owned();
let indent = measure_indent(&raw);
let display = format_line(&buffer);
lines.push(LineRecord {
number,
raw,
display,
indent,
});
}
Ok(lines)
}
fn compute_effective_indents(records: &[LineRecord]) -> Vec<usize> {
let mut effective = Vec::with_capacity(records.len());
let mut previous_indent = 0usize;
for record in records {
if record.is_blank() {
effective.push(previous_indent);
} else {
previous_indent = record.indent;
effective.push(previous_indent);
}
}
effective
}
fn measure_indent(line: &str) -> usize {
line.chars()
.take_while(|c| matches!(c, ' ' | '\t'))
.map(|c| if c == '\t' { TAB_WIDTH } else { 1 })
.sum()
}
}
fn format_line(bytes: &[u8]) -> String {
let decoded = String::from_utf8_lossy(bytes);
if decoded.len() > MAX_LINE_LENGTH {
take_bytes_at_char_boundary(&decoded, MAX_LINE_LENGTH).to_string()
} else {
decoded.into_owned()
}
}
fn trim_empty_lines(out: &mut VecDeque<&LineRecord>) {
while matches!(out.front(), Some(line) if line.raw.trim().is_empty()) {
out.pop_front();
}
while matches!(out.back(), Some(line) if line.raw.trim().is_empty()) {
out.pop_back();
}
}
mod defaults {
use super::*;
impl Default for IndentationArgs {
fn default() -> Self {
Self {
anchor_line: None,
max_levels: max_levels(),
include_siblings: include_siblings(),
include_header: include_header(),
max_lines: None,
}
}
}
pub fn offset() -> usize {
1
}
pub fn limit() -> usize {
2000
}
pub fn max_levels() -> usize {
0
}
pub fn include_siblings() -> bool {
false
}
pub fn include_header() -> bool {
true
}
}
#[cfg(test)]
#[path = "read_file_tests.rs"]
mod tests;

View File

@@ -2046,111 +2046,6 @@ fn format_plugin_summary(plugin: &DiscoverablePluginInfo) -> String {
}
}
fn create_read_file_tool() -> ToolSpec {
let indentation_properties = BTreeMap::from([
(
"anchor_line".to_string(),
JsonSchema::Number {
description: Some(
"Anchor line to center the indentation lookup on (defaults to offset)."
.to_string(),
),
},
),
(
"max_levels".to_string(),
JsonSchema::Number {
description: Some(
"How many parent indentation levels (smaller indents) to include.".to_string(),
),
},
),
(
"include_siblings".to_string(),
JsonSchema::Boolean {
description: Some(
"When true, include additional blocks that share the anchor indentation."
.to_string(),
),
},
),
(
"include_header".to_string(),
JsonSchema::Boolean {
description: Some(
"Include doc comments or attributes directly above the selected block."
.to_string(),
),
},
),
(
"max_lines".to_string(),
JsonSchema::Number {
description: Some(
"Hard cap on the number of lines returned when using indentation mode."
.to_string(),
),
},
),
]);
let properties = BTreeMap::from([
(
"file_path".to_string(),
JsonSchema::String {
description: Some("Absolute path to the file".to_string()),
},
),
(
"offset".to_string(),
JsonSchema::Number {
description: Some(
"The line number to start reading from. Must be 1 or greater.".to_string(),
),
},
),
(
"limit".to_string(),
JsonSchema::Number {
description: Some("The maximum number of lines to return.".to_string()),
},
),
(
"mode".to_string(),
JsonSchema::String {
description: Some(
"Optional mode selector: \"slice\" for simple ranges (default) or \"indentation\" \
to expand around an anchor line."
.to_string(),
),
},
),
(
"indentation".to_string(),
JsonSchema::Object {
properties: indentation_properties,
required: None,
additional_properties: Some(false.into()),
},
),
]);
ToolSpec::Function(ResponsesApiTool {
name: "read_file".to_string(),
description:
"Reads a local file with 1-indexed line numbers, supporting slice and indentation-aware block modes."
.to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::Object {
properties,
required: Some(vec!["file_path".to_string()]),
additional_properties: Some(false.into()),
},
output_schema: None,
})
}
fn create_list_dir_tool() -> ToolSpec {
let properties = BTreeMap::from([
(
@@ -2704,7 +2599,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
use crate::tools::handlers::McpHandler;
use crate::tools::handlers::McpResourceHandler;
use crate::tools::handlers::PlanHandler;
use crate::tools::handlers::ReadFileHandler;
use crate::tools::handlers::RequestPermissionsHandler;
use crate::tools::handlers::RequestUserInputHandler;
use crate::tools::handlers::ShellCommandHandler;
@@ -2975,20 +2869,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
builder.register_handler("apply_patch", apply_patch_handler);
}
if config
.experimental_supported_tools
.contains(&"read_file".to_string())
{
let read_file_handler = Arc::new(ReadFileHandler);
push_tool_spec(
&mut builder,
create_read_file_tool(),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
builder.register_handler("read_file", read_file_handler);
}
if config
.experimental_supported_tools
.iter()