1use std::collections::BTreeMap;
7use std::path::PathBuf;
8
9use serde_json::Value as JsonValue;
10use thiserror::Error;
11
12use crate::output::OutputData;
13use crate::result::{value_to_json, ExecResult};
14use crate::tool::ToolSchema;
15
16pub type BackendResult<T> = Result<T, BackendError>;
18
19#[derive(Debug, Clone)]
24pub struct MountInfo {
25 pub path: PathBuf,
27 pub read_only: bool,
29 pub resident_bytes: Option<u64>,
37}
38
39#[derive(Debug, Clone, Error)]
41#[non_exhaustive]
42pub enum BackendError {
43 #[error("not found: {0}")]
44 NotFound(String),
45 #[error("already exists: {0}")]
46 AlreadyExists(String),
47 #[error("permission denied: {0}")]
48 PermissionDenied(String),
49 #[error("is a directory: {0}")]
50 IsDirectory(String),
51 #[error("not a directory: {0}")]
52 NotDirectory(String),
53 #[error("read-only filesystem")]
54 ReadOnly,
55 #[error("conflict: {0}")]
56 Conflict(ConflictError),
57 #[error("tool not found: {0}")]
58 ToolNotFound(String),
59 #[error("io error: {0}")]
60 Io(String),
61 #[error("invalid operation: {0}")]
62 InvalidOperation(String),
63}
64
65impl From<std::io::Error> for BackendError {
66 fn from(err: std::io::Error) -> Self {
67 use std::io::ErrorKind;
68 match err.kind() {
69 ErrorKind::NotFound => BackendError::NotFound(err.to_string()),
70 ErrorKind::AlreadyExists => BackendError::AlreadyExists(err.to_string()),
71 ErrorKind::PermissionDenied => BackendError::PermissionDenied(err.to_string()),
72 ErrorKind::IsADirectory => BackendError::IsDirectory(err.to_string()),
73 ErrorKind::NotADirectory => BackendError::NotDirectory(err.to_string()),
74 ErrorKind::ReadOnlyFilesystem => BackendError::ReadOnly,
75 _ => BackendError::Io(err.to_string()),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Error)]
82#[error("conflict at {location}: expected {expected:?}, found {actual:?}")]
83pub struct ConflictError {
84 pub location: String,
86 pub expected: String,
88 pub actual: String,
90}
91
92#[derive(Debug, Clone)]
107pub enum PatchOp {
108 Insert { offset: usize, content: String },
110
111 Delete {
114 offset: usize,
115 len: usize,
116 expected: Option<String>,
117 },
118
119 Replace {
122 offset: usize,
123 len: usize,
124 content: String,
125 expected: Option<String>,
126 },
127
128 InsertLine { line: usize, content: String },
130
131 DeleteLine { line: usize, expected: Option<String> },
134
135 ReplaceLine {
138 line: usize,
139 content: String,
140 expected: Option<String>,
141 },
142
143 Append { content: String },
145}
146
147#[derive(Debug, Clone, Default)]
149pub struct ReadRange {
150 pub start_line: Option<usize>,
152 pub end_line: Option<usize>,
154 pub offset: Option<u64>,
156 pub limit: Option<u64>,
158}
159
160impl ReadRange {
161 pub fn lines(start: usize, end: usize) -> Self {
163 Self {
164 start_line: Some(start),
165 end_line: Some(end),
166 ..Default::default()
167 }
168 }
169
170 pub fn bytes(offset: u64, limit: u64) -> Self {
172 Self {
173 offset: Some(offset),
174 limit: Some(limit),
175 ..Default::default()
176 }
177 }
178
179 pub fn apply(&self, content: &[u8]) -> Vec<u8> {
186 if self.offset.is_some() || self.limit.is_some() {
188 let offset = self.offset.unwrap_or(0) as usize;
189 let limit = self.limit.map(|l| l as usize).unwrap_or(content.len());
190 let end = offset.saturating_add(limit).min(content.len());
191 return content.get(offset..end).unwrap_or(&[]).to_vec();
192 }
193
194 if self.start_line.is_some() || self.end_line.is_some() {
196 let content_str = match std::str::from_utf8(content) {
197 Ok(s) => s,
198 Err(_) => return content.to_vec(),
199 };
200 let lines: Vec<&str> = content_str.lines().collect();
201 let start = self.start_line.unwrap_or(1).saturating_sub(1);
202 let end = self.end_line.unwrap_or(lines.len()).min(lines.len());
203 let selected: Vec<&str> = lines.get(start..end).unwrap_or(&[]).to_vec();
204 let mut result = selected.join("\n");
205 if self.end_line.is_none() && content_str.ends_with('\n') && !result.is_empty() {
208 result.push('\n');
209 }
210 return result.into_bytes();
211 }
212
213 content.to_vec()
214 }
215}
216
217#[non_exhaustive]
219#[derive(Debug, Clone, Copy, Default)]
220pub enum WriteMode {
221 CreateNew,
223 #[default]
225 Overwrite,
226 UpdateOnly,
228 Truncate,
230}
231
232#[derive(Debug, Clone)]
234pub struct ToolResult {
235 pub code: i32,
237 pub stdout: String,
239 pub stderr: String,
241 pub data: Option<JsonValue>,
243 pub output: Option<OutputData>,
245 pub content_type: Option<String>,
247 pub baggage: BTreeMap<String, String>,
249}
250
251impl ToolResult {
252 pub fn success(stdout: impl Into<String>) -> Self {
254 Self {
255 code: 0,
256 stdout: stdout.into(),
257 stderr: String::new(),
258 data: None,
259 output: None,
260 content_type: None,
261 baggage: BTreeMap::new(),
262 }
263 }
264
265 pub fn failure(code: i32, stderr: impl Into<String>) -> Self {
267 Self {
268 code,
269 stdout: String::new(),
270 stderr: stderr.into(),
271 data: None,
272 output: None,
273 content_type: None,
274 baggage: BTreeMap::new(),
275 }
276 }
277
278 pub fn with_data(stdout: impl Into<String>, data: JsonValue) -> Self {
280 Self {
281 code: 0,
282 stdout: stdout.into(),
283 stderr: String::new(),
284 data: Some(data),
285 output: None,
286 content_type: None,
287 baggage: BTreeMap::new(),
288 }
289 }
290
291 pub fn ok(&self) -> bool {
293 self.code == 0
294 }
295}
296
297impl From<ExecResult> for ToolResult {
298 fn from(mut exec: ExecResult) -> Self {
299 let code = exec.code.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
301
302 let stdout = exec.text_out().into_owned();
304 let output = exec.take_output();
305
306 let data = exec.data.map(|v| value_to_json(&v));
308
309 Self {
310 code,
311 stdout,
312 stderr: exec.err,
313 data,
314 output,
315 content_type: exec.content_type,
316 baggage: exec.baggage,
317 }
318 }
319}
320
321#[derive(Debug, Clone)]
323pub struct ToolInfo {
324 pub name: String,
326 pub description: String,
328 pub schema: ToolSchema,
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn tool_result_from_exec_result_preserves_content_type_and_baggage() {
338 let mut exec = ExecResult::success("hello");
339 exec.content_type = Some("text/markdown".to_string());
340 exec.baggage.insert("traceparent".to_string(), "00-abc-def-01".to_string());
341
342 let tool_result = ToolResult::from(exec);
343 assert_eq!(tool_result.content_type.as_deref(), Some("text/markdown"));
344 assert_eq!(
345 tool_result.baggage.get("traceparent").map(|s| s.as_str()),
346 Some("00-abc-def-01")
347 );
348 }
349
350 #[test]
351 fn tool_result_constructors_default_to_empty_baggage() {
352 let success = ToolResult::success("ok");
353 assert!(success.baggage.is_empty());
354 assert!(success.content_type.is_none());
355
356 let failure = ToolResult::failure(1, "err");
357 assert!(failure.baggage.is_empty());
358 assert!(failure.content_type.is_none());
359 }
360}