use std::sync::Arc;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::Value;
use sgr_agent_core::agent_tool::{Tool, ToolError, ToolOutput, parse_args};
use sgr_agent_core::context::AgentContext;
use sgr_agent_core::schema::json_schema_for;
use crate::backend::FileBackend;
use crate::helpers::backend_err;
use crate::trust::wrap_with_meta;
pub struct ReadTool<B: FileBackend>(pub Arc<B>);
#[derive(Deserialize, JsonSchema)]
struct ReadArgs {
path: String,
#[serde(default)]
number: bool,
#[serde(default)]
start_line: i32,
#[serde(default)]
end_line: i32,
#[serde(default)]
mode: Option<String>,
#[serde(default)]
anchor_line: Option<usize>,
#[serde(default)]
max_levels: Option<usize>,
}
const TAB_WIDTH: usize = 4;
fn compute_effective_indents(lines: &[&str]) -> Vec<usize> {
let mut indents: Vec<usize> = Vec::with_capacity(lines.len());
let mut prev_indent: usize = 0;
for line in lines {
if line.trim().is_empty() {
indents.push(prev_indent);
} else {
let indent = line
.chars()
.take_while(|c| c.is_whitespace())
.map(|c| if c == '\t' { TAB_WIDTH } else { 1 })
.sum();
indents.push(indent);
prev_indent = indent;
}
}
indents
}
fn read_indentation_block(content: &str, anchor: usize, max_levels: usize) -> String {
let lines: Vec<&str> = content.lines().collect();
if lines.is_empty() || anchor == 0 || anchor > lines.len() {
return content.to_string();
}
let indents = compute_effective_indents(&lines);
let anchor_idx = anchor - 1; let anchor_indent = indents[anchor_idx];
let min_indent = if max_levels == 0 {
0
} else {
anchor_indent.saturating_sub(max_levels * TAB_WIDTH)
};
let mut start = anchor_idx;
for i in (0..anchor_idx).rev() {
if indents[i] < min_indent {
break;
}
if indents[i] == min_indent {
start = i;
break;
}
start = i;
}
let mut end = anchor_idx;
for i in (anchor_idx + 1)..lines.len() {
if indents[i] < min_indent {
break;
}
if indents[i] == min_indent {
break;
}
end = i;
}
while start <= end && lines[start].trim().is_empty() {
start += 1;
}
while end > start && lines[end].trim().is_empty() {
end -= 1;
}
let mut result = String::new();
for i in start..=end {
result.push_str(&format!("L{}: {}\n", i + 1, lines[i]));
}
result
}
#[async_trait::async_trait]
impl<B: FileBackend> Tool for ReadTool<B> {
fn name(&self) -> &str {
"read"
}
fn description(&self) -> &str {
"Read file contents. Use number=true to see line numbers (like cat -n). \
Use start_line/end_line to read a specific range (like sed -n '5,10p'). \
For large files: first read with number=true, then read specific ranges. \
Indentation mode: mode=\"indentation\", anchor_line=N expands around line N \
by indent level (max_levels=0 for full scope)."
}
fn is_read_only(&self) -> bool {
true
}
fn parameters_schema(&self) -> Value {
json_schema_for::<ReadArgs>()
}
async fn execute(&self, args: Value, ctx: &mut AgentContext) -> Result<ToolOutput, ToolError> {
self.execute_readonly(args, ctx).await
}
async fn execute_readonly(
&self,
args: Value,
_ctx: &AgentContext,
) -> Result<ToolOutput, ToolError> {
let a: ReadArgs = parse_args(&args)?;
if a.mode.as_deref() == Some("indentation") {
let anchor = a.anchor_line.unwrap_or(1);
let max_levels = a.max_levels.unwrap_or(0);
let content = self
.0
.read(&a.path, false, 0, 0)
.await
.map_err(backend_err)?;
let block = read_indentation_block(&content, anchor, max_levels);
return Ok(ToolOutput::text(wrap_with_meta(&a.path, &block)));
}
let result = self
.0
.read(&a.path, a.number, a.start_line, a.end_line)
.await
.map_err(backend_err)?;
Ok(ToolOutput::text(wrap_with_meta(&a.path, &result)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn effective_indents_basic() {
let lines = vec!["def foo():", " x = 1", " y = 2", ""];
let indents = compute_effective_indents(&lines);
assert_eq!(indents, vec![0, 4, 4, 4]); }
#[test]
fn effective_indents_tabs() {
let lines = vec!["\tdef foo():", "\t\tx = 1"];
let indents = compute_effective_indents(&lines);
assert_eq!(indents, vec![4, 8]);
}
#[test]
fn indentation_block_simple() {
let content = "class Foo:\n def bar(self):\n x = 1\n y = 2\n def baz(self):\n z = 3\n";
let result = read_indentation_block(content, 3, 1);
assert!(result.contains("def bar"));
assert!(result.contains("x = 1"));
assert!(result.contains("y = 2"));
assert!(!result.contains("baz"));
}
#[test]
fn indentation_block_unlimited() {
let content = "a\n b\n c\n d\ne\n";
let result = read_indentation_block(content, 3, 0);
assert!(result.contains("L1: a"));
assert!(result.contains("L3: c"));
}
#[test]
fn indentation_block_anchor_out_of_range() {
let content = "line1\nline2";
let result = read_indentation_block(content, 99, 0);
assert_eq!(result, content);
}
#[test]
fn indentation_block_blank_lines_trimmed() {
let content = "\n\ndef foo():\n x = 1\n\n\n";
let result = read_indentation_block(content, 4, 1);
assert!(result.contains("def foo()"));
assert!(result.contains("x = 1"));
assert!(!result.starts_with("L1: \n"));
}
#[test]
fn indentation_block_nested() {
let content = "\
fn outer() {
fn inner() {
let x = 1;
let y = 2;
}
fn other() {
let z = 3;
}
}";
let result = read_indentation_block(content, 3, 1);
assert!(result.contains("fn inner()"));
assert!(result.contains("let x = 1"));
assert!(result.contains("let y = 2"));
assert!(!result.contains("other"));
}
}