1use async_trait::async_trait;
4use serde::Deserialize;
5use serde_json::Value;
6
7use super::{resolve_path, Tool, ToolContext, ToolResult};
8use crate::error::Result;
9
10pub struct ReadTool {
11 max_output_lines: usize,
13 max_output_bytes: usize,
15}
16
17#[derive(Debug, Deserialize)]
18struct ReadArgs {
19 file_path: String,
20 #[serde(default)]
21 offset: Option<usize>,
22 #[serde(default)]
23 limit: Option<usize>,
24}
25
26impl ReadTool {
27 pub fn new(max_output_lines: usize, max_output_bytes: usize) -> Self {
28 Self {
29 max_output_lines,
30 max_output_bytes,
31 }
32 }
33}
34
35#[async_trait]
36impl Tool for ReadTool {
37 fn name(&self) -> &str {
38 "read"
39 }
40
41 fn description(&self) -> &str {
42 "Read file contents. Supports text files. Large files can be read in segments using offset/limit. Output includes line numbers."
43 }
44
45 fn parameters_schema(&self) -> Value {
46 serde_json::json!({
47 "type": "object",
48 "properties": {
49 "file_path": {
50 "type": "string",
51 "description": "File path (relative or absolute)"
52 },
53 "offset": {
54 "type": "integer",
55 "description": "Starting line number (0-based, default 0)"
56 },
57 "limit": {
58 "type": "integer",
59 "description": "Max number of lines to read (default: read all)"
60 }
61 },
62 "required": ["file_path"]
63 })
64 }
65
66 fn requires_confirmation(&self) -> bool {
67 false
68 }
69
70 async fn execute(&self, args: Value, ctx: &ToolContext) -> Result<ToolResult> {
71 let parsed: ReadArgs = match serde_json::from_value(args) {
72 Ok(a) => a,
73 Err(e) => return Ok(ToolResult::error(format!("Argument parsing failed: {}", e))),
74 };
75
76 let path = resolve_path(&parsed.file_path, &ctx.working_dir);
78
79 if !path.exists() {
81 return Ok(ToolResult::error(format!("File not found: {}", path.display())));
82 }
83
84 if path.is_dir() {
85 return Ok(ToolResult::error(format!(
86 "'{}' is a directory, not a file",
87 path.display()
88 )));
89 }
90
91 let content = match tokio::fs::read_to_string(&path).await {
93 Ok(c) => c,
94 Err(e) => {
95 return Ok(ToolResult::error(format!(
96 "Failed to read file '{}': {}",
97 path.display(),
98 e
99 )));
100 }
101 };
102
103 let all_lines: Vec<&str> = content.lines().collect();
104 let total_lines = all_lines.len();
105 let offset = parsed.offset.unwrap_or(0);
106 let limit = parsed.limit.unwrap_or(total_lines);
107
108 if offset > total_lines {
110 return Ok(ToolResult::error(format!(
111 "offset {} is out of range, file has {} lines",
112 offset, total_lines
113 )));
114 }
115
116 let end = (offset + limit).min(total_lines);
117 let selected_lines = &all_lines[offset..end];
118
119 let mut output = String::new();
121 let mut byte_count = 0;
122
123 for (i, line) in selected_lines.iter().enumerate() {
124 let line_num = offset + i + 1; let formatted = format!("{:>6}\t{}\n", line_num, line);
126
127 if byte_count + formatted.len() > self.max_output_bytes {
129 output.push_str(&format!(
130 "\n... (Output truncated, byte limit of {} bytes reached)\n",
131 self.max_output_bytes
132 ));
133 return Ok(ToolResult::success(output));
134 }
135
136 if i >= self.max_output_lines {
138 output.push_str(&format!(
139 "\n... (Output truncated, {} lines total, showing first {}. Use offset/limit to read more)\n",
140 total_lines, self.max_output_lines
141 ));
142 return Ok(ToolResult::success(output));
143 }
144
145 byte_count += formatted.len();
146 output.push_str(&formatted);
147 }
148
149 if offset > 0 || end < total_lines {
151 output.push_str(&format!(
152 "\n(Showing lines {}-{} of {})",
153 offset + 1,
154 end,
155 total_lines
156 ));
157 }
158
159 Ok(ToolResult::success(output))
160 }
161}