mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
chore: remove read_file handler (#15773)
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user