1use super::ToolGate;
4use anyhow::{Result, anyhow, bail};
5use helpers::TruncatedDisplay;
6use llimorse::{Agent, CallableTool};
7use std::fmt;
8use std::fs::{self, File};
9use std::io::{self, BufRead, BufReader, Read as _, Seek as _, Write as _};
10use std::path::PathBuf;
11
12llimorse::tool! {
13 'name: "view";
14
15 #[derive(Debug)]
17 'params: pub struct ViewParams {
18 filename: PathBuf,
20
21 range: Option<(usize, usize)>,
24 }
25
26 #[derive(Debug)]
28 'result: pub struct ViewResult {
29 filename: PathBuf,
31
32 content: String,
34 }
35
36 #[derive(Debug)]
38 'state: pub struct View<G: ToolGate> {
39 gate: G,
41 }
42}
43
44impl fmt::Display for ViewParams {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 write!(f, "filename={}", self.filename.display())?;
47
48 if let Some((start, end)) = self.range {
49 write!(f, " range=[{start}, {end}]")?
50 }
51
52 Ok(())
53 }
54}
55
56impl fmt::Display for ViewResult {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 write!(
59 f,
60 "filename={} content={:?}",
61 self.filename.display(),
62 self.content.truncated_display(100)
63 )
64 }
65}
66
67impl<G: ToolGate> View<G> {
68 pub fn new(gate: G) -> Self {
70 View { gate }
71 }
72}
73
74impl<G: ToolGate> CallableTool for View<G> {
75 async fn execute(&self, _agent: &Agent, params: ViewParams) -> Result<ViewResult> {
76 self.gate
77 .permitted(¶ms)
78 .await
79 .map_err(|e| anyhow!("View tool call rejected: {e}"))?;
80
81 let file = File::open(¶ms.filename)
82 .map_err(|err| anyhow!("Failed to open {}: {err}", params.filename.display()))?;
83 let reader = BufReader::new(file);
84
85 let range = params.range.unwrap_or((1, usize::MAX));
86 let skip = range
87 .0
88 .checked_sub(1) .ok_or_else(|| anyhow!("Range {range:?}: First line is 1, not 0"))?;
90
91 let take = range
92 .1
93 .checked_sub(range.0)
94 .ok_or_else(|| {
95 anyhow!(
96 "Range end must be greater or equal to the range start: {} >= {}",
97 range.1,
98 range.0
99 )
100 })?
101 .saturating_add(1); let content = reader
104 .lines()
105 .skip(skip)
106 .take(take)
107 .try_fold(String::new(), |mut acc, line| -> Result<String> {
108 if !acc.is_empty() {
109 acc.push('\n');
110 }
111 acc.push_str(&line?);
112 Ok(acc)
113 })
114 .map_err(|err| anyhow!("Failed to read {}: {err}", params.filename.display()))?;
115
116 Ok(ViewResult {
117 filename: params.filename,
118 content,
119 })
120 }
121}
122
123llimorse::tool! {
124 'name: "write";
125
126 #[derive(Debug)]
128 'params: pub struct WriteParams {
129 filename: PathBuf,
131
132 content: String,
134 }
135
136 #[derive(Debug)]
138 'result: pub struct WriteResult {
139 filename: PathBuf,
141
142 bytes_written: usize,
144 }
145
146 #[derive(Debug)]
148 'state: pub struct Write<G: ToolGate> {
149 gate: G,
151 }
152}
153
154impl fmt::Display for WriteParams {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 write!(
157 f,
158 "filename={} content={:?}",
159 self.filename.display(),
160 self.content.truncated_display(100)
161 )
162 }
163}
164
165impl fmt::Display for WriteResult {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 write!(
168 f,
169 "filename={} bytes_written={}",
170 self.filename.display(),
171 self.bytes_written
172 )
173 }
174}
175
176impl<G: ToolGate> Write<G> {
177 pub fn new(gate: G) -> Self {
179 Write { gate }
180 }
181}
182
183impl<G: ToolGate> CallableTool for Write<G> {
184 async fn execute(&self, _agent: &Agent, params: WriteParams) -> Result<WriteResult> {
185 self.gate
186 .permitted(¶ms)
187 .await
188 .map_err(|e| anyhow!("Write tool call rejected: {e}"))?;
189
190 let mut file = fs::OpenOptions::new()
191 .write(true)
192 .create(true)
193 .truncate(true)
194 .open(¶ms.filename)
195 .map_err(|err| anyhow!("Failed to open {}: {err}", params.filename.display()))?;
196
197 let len = params.content.len(); file.write_all(params.content.as_bytes())
199 .map_err(|err| anyhow!("Failed to write to {}: {err}", params.filename.display()))?;
200
201 Ok(WriteResult {
202 filename: params.filename,
203 bytes_written: len,
204 })
205 }
206}
207
208llimorse::tool! {
209 'name: "edit";
210
211 #[derive(Debug)]
213 'params: pub struct EditParams {
214 filename: PathBuf,
216
217 old_content: String,
219
220 new_content: String,
222 }
223
224 #[derive(Debug)]
226 'result: pub struct EditResult {
227 filename: PathBuf,
229
230 byte_size_changed: isize,
232 }
233
234 #[derive(Debug)]
236 'state: pub struct Edit<G: ToolGate> {
237 gate: G,
239 }
240}
241
242impl fmt::Display for EditParams {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 write!(
245 f,
246 "filename={} {:?} -> {:?}",
247 self.filename.display(),
248 self.old_content.truncated_display(100),
249 self.new_content.truncated_display(100)
250 )
251 }
252}
253
254impl fmt::Display for EditResult {
255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256 write!(
257 f,
258 "filename={} byte_size_changed={}",
259 self.filename.display(),
260 self.byte_size_changed
261 )
262 }
263}
264
265impl<G: ToolGate> Edit<G> {
266 pub fn new(gate: G) -> Self {
268 Edit { gate }
269 }
270}
271
272impl<G: ToolGate> CallableTool for Edit<G> {
273 async fn execute(&self, _agent: &Agent, params: EditParams) -> Result<EditResult> {
274 self.gate
275 .permitted(¶ms)
276 .await
277 .map_err(|e| anyhow!("Edit tool call rejected: {e}"))?;
278
279 let mut file = fs::OpenOptions::new()
280 .read(true)
281 .write(true)
282 .open(¶ms.filename)
283 .map_err(|err| anyhow!("Failed to open {}: {err}", params.filename.display()))?;
284
285 let mut content = String::new();
286 file.read_to_string(&mut content)
287 .map_err(|err| anyhow!("Failed to read from {}: {err}", params.filename.display()))?;
288
289 let first_match = content.find(¶ms.old_content).ok_or_else(|| {
290 anyhow!(
291 "Did not find old_content string in {}",
292 params.filename.display()
293 )
294 })?;
295
296 let end_match = first_match + params.old_content.len();
297 if content[end_match..].find(¶ms.old_content).is_some() {
298 bail!(
299 "Multiple old_content matches in {}",
300 params.filename.display()
301 )
302 }
303
304 file.set_len(first_match as u64)
305 .map_err(|err| anyhow!("Failed to write to {}: {err}", params.filename.display()))?;
306 file.seek(io::SeekFrom::End(0))
307 .map_err(|err| anyhow!("Failed to write to {}: {err}", params.filename.display()))?;
308 file.write_all(params.new_content.as_bytes())
309 .map_err(|err| anyhow!("Failed to write to {}: {err}", params.filename.display()))?;
310 file.write_all(&content.as_bytes()[end_match..])
311 .map_err(|err| anyhow!("Failed to write to {}: {err}", params.filename.display()))?;
312
313 let byte_size_changed =
314 params.new_content.len() as isize - params.old_content.len() as isize;
315 Ok(EditResult {
316 filename: params.filename,
317 byte_size_changed,
318 })
319 }
320}