1use std::sync::Arc;
11
12use schemars::JsonSchema;
13use serde::Deserialize;
14use serde_json::Value;
15use sgr_agent_core::agent_tool::{Tool, ToolError, ToolOutput, parse_args};
16use sgr_agent_core::context::AgentContext;
17use sgr_agent_core::schema::json_schema_for;
18
19use crate::backend::FileBackend;
20use crate::helpers::backend_err;
21use crate::trust::wrap_with_meta;
22
23pub struct ReadTool<B: FileBackend>(pub Arc<B>);
24
25#[derive(Deserialize, JsonSchema)]
26struct ReadArgs {
27 path: String,
29 #[serde(default)]
31 number: bool,
32 #[serde(default)]
34 start_line: i32,
35 #[serde(default)]
36 end_line: i32,
37 #[serde(default)]
39 mode: Option<String>,
40 #[serde(default)]
42 anchor_line: Option<usize>,
43 #[serde(default)]
45 max_levels: Option<usize>,
46}
47
48const TAB_WIDTH: usize = 4;
50
51fn compute_effective_indents(lines: &[&str]) -> Vec<usize> {
54 let mut indents: Vec<usize> = Vec::with_capacity(lines.len());
55 let mut prev_indent: usize = 0;
56
57 for line in lines {
58 if line.trim().is_empty() {
59 indents.push(prev_indent);
61 } else {
62 let indent = line
63 .chars()
64 .take_while(|c| c.is_whitespace())
65 .map(|c| if c == '\t' { TAB_WIDTH } else { 1 })
66 .sum();
67 indents.push(indent);
68 prev_indent = indent;
69 }
70 }
71
72 indents
73}
74
75fn read_indentation_block(content: &str, anchor: usize, max_levels: usize) -> String {
81 let lines: Vec<&str> = content.lines().collect();
82 if lines.is_empty() || anchor == 0 || anchor > lines.len() {
83 return content.to_string();
84 }
85
86 let indents = compute_effective_indents(&lines);
87 let anchor_idx = anchor - 1; let anchor_indent = indents[anchor_idx];
89
90 let min_indent = if max_levels == 0 {
91 0
92 } else {
93 anchor_indent.saturating_sub(max_levels * TAB_WIDTH)
94 };
95
96 let mut start = anchor_idx;
99 for i in (0..anchor_idx).rev() {
100 if indents[i] < min_indent {
101 break;
102 }
103 if indents[i] == min_indent {
104 start = i;
106 break;
107 }
108 start = i;
109 }
110
111 let mut end = anchor_idx;
114 for i in (anchor_idx + 1)..lines.len() {
115 if indents[i] < min_indent {
116 break;
117 }
118 if indents[i] == min_indent {
119 break;
121 }
122 end = i;
123 }
124
125 while start <= end && lines[start].trim().is_empty() {
127 start += 1;
128 }
129 while end > start && lines[end].trim().is_empty() {
130 end -= 1;
131 }
132
133 let mut result = String::new();
135 for i in start..=end {
136 result.push_str(&format!("L{}: {}\n", i + 1, lines[i]));
137 }
138
139 result
140}
141
142#[async_trait::async_trait]
143impl<B: FileBackend> Tool for ReadTool<B> {
144 fn name(&self) -> &str {
145 "read"
146 }
147 fn description(&self) -> &str {
148 "Read file contents. Use number=true to see line numbers (like cat -n). \
149 Use start_line/end_line to read a specific range (like sed -n '5,10p'). \
150 For large files: first read with number=true, then read specific ranges. \
151 Indentation mode: mode=\"indentation\", anchor_line=N expands around line N \
152 by indent level (max_levels=0 for full scope)."
153 }
154 fn is_read_only(&self) -> bool {
155 true
156 }
157 fn parameters_schema(&self) -> Value {
158 json_schema_for::<ReadArgs>()
159 }
160 async fn execute(&self, args: Value, ctx: &mut AgentContext) -> Result<ToolOutput, ToolError> {
161 self.execute_readonly(args, ctx).await
162 }
163 async fn execute_readonly(
164 &self,
165 args: Value,
166 _ctx: &AgentContext,
167 ) -> Result<ToolOutput, ToolError> {
168 let a: ReadArgs = parse_args(&args)?;
169
170 if a.mode.as_deref() == Some("indentation") {
171 let anchor = a.anchor_line.unwrap_or(1);
172 let max_levels = a.max_levels.unwrap_or(0);
173
174 let content = self
176 .0
177 .read(&a.path, false, 0, 0)
178 .await
179 .map_err(backend_err)?;
180
181 let block = read_indentation_block(&content, anchor, max_levels);
182 return Ok(ToolOutput::text(wrap_with_meta(&a.path, &block)));
183 }
184
185 let result = self
187 .0
188 .read(&a.path, a.number, a.start_line, a.end_line)
189 .await
190 .map_err(backend_err)?;
191 Ok(ToolOutput::text(wrap_with_meta(&a.path, &result)))
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn effective_indents_basic() {
201 let lines = vec!["def foo():", " x = 1", " y = 2", ""];
202 let indents = compute_effective_indents(&lines);
203 assert_eq!(indents, vec![0, 4, 4, 4]); }
205
206 #[test]
207 fn effective_indents_tabs() {
208 let lines = vec!["\tdef foo():", "\t\tx = 1"];
209 let indents = compute_effective_indents(&lines);
210 assert_eq!(indents, vec![4, 8]);
211 }
212
213 #[test]
214 fn indentation_block_simple() {
215 let content = "class Foo:\n def bar(self):\n x = 1\n y = 2\n def baz(self):\n z = 3\n";
216 let result = read_indentation_block(content, 3, 1);
218 assert!(result.contains("def bar"));
219 assert!(result.contains("x = 1"));
220 assert!(result.contains("y = 2"));
221 assert!(!result.contains("baz"));
223 }
224
225 #[test]
226 fn indentation_block_unlimited() {
227 let content = "a\n b\n c\n d\ne\n";
228 let result = read_indentation_block(content, 3, 0);
230 assert!(result.contains("L1: a"));
232 assert!(result.contains("L3: c"));
233 }
234
235 #[test]
236 fn indentation_block_anchor_out_of_range() {
237 let content = "line1\nline2";
238 let result = read_indentation_block(content, 99, 0);
239 assert_eq!(result, content);
241 }
242
243 #[test]
244 fn indentation_block_blank_lines_trimmed() {
245 let content = "\n\ndef foo():\n x = 1\n\n\n";
246 let result = read_indentation_block(content, 4, 1);
248 assert!(result.contains("def foo()"));
249 assert!(result.contains("x = 1"));
250 assert!(!result.starts_with("L1: \n"));
252 }
253
254 #[test]
255 fn indentation_block_nested() {
256 let content = "\
257fn outer() {
258 fn inner() {
259 let x = 1;
260 let y = 2;
261 }
262 fn other() {
263 let z = 3;
264 }
265}";
266 let result = read_indentation_block(content, 3, 1);
268 assert!(result.contains("fn inner()"));
269 assert!(result.contains("let x = 1"));
270 assert!(result.contains("let y = 2"));
271 assert!(!result.contains("other"));
273 }
274}