Skip to main content

crabtalk_runtime/os/
read.rs

1//! read tool — paginated file reading with line numbers.
2
3use crate::{Env, host::Host};
4use schemars::JsonSchema;
5use serde::Deserialize;
6use std::fmt::Write;
7use wcore::{
8    agent::{AsTool, ToolDescription},
9    model::Tool,
10};
11
12/// Default maximum number of lines to return per read.
13const DEFAULT_LIMIT: usize = 2000;
14
15/// Maximum file size in bytes before refusing to read (50 MB).
16pub const MAX_FILE_SIZE: u64 = 50 * 1024 * 1024;
17
18#[derive(Deserialize, JsonSchema)]
19pub struct Read {
20    /// Absolute or relative file path to read.
21    pub path: String,
22    /// Line number to start reading from (1-based). Defaults to 1.
23    #[serde(default)]
24    pub offset: Option<usize>,
25    /// Maximum number of lines to read. Defaults to 2000.
26    #[serde(default)]
27    pub limit: Option<usize>,
28}
29
30impl ToolDescription for Read {
31    const DESCRIPTION: &'static str =
32        "Read a file with line numbers. Supports offset/limit for pagination.";
33}
34
35pub fn tools() -> Vec<Tool> {
36    vec![Read::as_tool()]
37}
38
39impl<H: Host> Env<H> {
40    pub async fn dispatch_read(
41        &self,
42        args: &str,
43        conversation_id: Option<u64>,
44    ) -> Result<String, String> {
45        let input: Read =
46            serde_json::from_str(args).map_err(|e| format!("invalid arguments: {e}"))?;
47
48        let conversation_cwd = if let Some(id) = conversation_id {
49            self.host.conversation_cwd(id)
50        } else {
51            None
52        };
53        let cwd = conversation_cwd.as_deref().unwrap_or(&self.cwd);
54
55        let path = if std::path::Path::new(&input.path).is_absolute() {
56            std::path::PathBuf::from(&input.path)
57        } else {
58            cwd.join(&input.path)
59        };
60
61        // Size guard — refuse to read files that could OOM the process.
62        match std::fs::metadata(&path) {
63            Ok(m) if m.len() > MAX_FILE_SIZE => {
64                return Err(format!(
65                    "file is too large ({} bytes, max {})",
66                    m.len(),
67                    MAX_FILE_SIZE
68                ));
69            }
70            Err(e) => return Err(format!("error reading {}: {e}", path.display())),
71            _ => {}
72        }
73
74        let content = std::fs::read_to_string(&path)
75            .map_err(|e| format!("error reading {}: {e}", path.display()))?;
76
77        let total = content.lines().count();
78        let offset = input.offset.unwrap_or(1).max(1);
79        let limit = input.limit.unwrap_or(DEFAULT_LIMIT);
80        let start = offset - 1; // convert 1-based to 0-based
81
82        if start >= total {
83            return Ok(format!(
84                "--- {total} total lines (offset {offset} is past end of file) ---"
85            ));
86        }
87
88        let mut buf = String::new();
89        let mut shown = 0;
90        for (line_num, line) in content.lines().skip(start).take(limit).enumerate() {
91            let _ = writeln!(buf, "{}\t{line}", start + line_num + 1);
92            shown += 1;
93        }
94
95        let end = start + shown;
96        if start > 0 || end < total {
97            let _ = write!(
98                buf,
99                "\n--- {total} total lines (showing lines {}-{end}) ---",
100                start + 1,
101            );
102        } else {
103            let _ = write!(buf, "\n--- {total} total lines ---");
104        }
105
106        Ok(buf)
107    }
108}