crabtalk_runtime/os/
read.rs1use 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
12const DEFAULT_LIMIT: usize = 2000;
14
15pub const MAX_FILE_SIZE: u64 = 50 * 1024 * 1024;
17
18#[derive(Deserialize, JsonSchema)]
19pub struct Read {
20 pub path: String,
22 #[serde(default)]
24 pub offset: Option<usize>,
25 #[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 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; 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}