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
180#[non_exhaustive]
182#[derive(Debug, Clone, Copy, Default)]
183pub enum WriteMode {
184 CreateNew,
186 #[default]
188 Overwrite,
189 UpdateOnly,
191 Truncate,
193}
194
195#[derive(Debug, Clone)]
197pub struct ToolResult {
198 pub code: i32,
200 pub stdout: String,
202 pub stderr: String,
204 pub data: Option<JsonValue>,
206 pub output: Option<OutputData>,
208 pub content_type: Option<String>,
210 pub baggage: BTreeMap<String, String>,
212}
213
214impl ToolResult {
215 pub fn success(stdout: impl Into<String>) -> Self {
217 Self {
218 code: 0,
219 stdout: stdout.into(),
220 stderr: String::new(),
221 data: None,
222 output: None,
223 content_type: None,
224 baggage: BTreeMap::new(),
225 }
226 }
227
228 pub fn failure(code: i32, stderr: impl Into<String>) -> Self {
230 Self {
231 code,
232 stdout: String::new(),
233 stderr: stderr.into(),
234 data: None,
235 output: None,
236 content_type: None,
237 baggage: BTreeMap::new(),
238 }
239 }
240
241 pub fn with_data(stdout: impl Into<String>, data: JsonValue) -> Self {
243 Self {
244 code: 0,
245 stdout: stdout.into(),
246 stderr: String::new(),
247 data: Some(data),
248 output: None,
249 content_type: None,
250 baggage: BTreeMap::new(),
251 }
252 }
253
254 pub fn ok(&self) -> bool {
256 self.code == 0
257 }
258}
259
260impl From<ExecResult> for ToolResult {
261 fn from(mut exec: ExecResult) -> Self {
262 let code = exec.code.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
264
265 let stdout = exec.text_out().into_owned();
267 let output = exec.take_output();
268
269 let data = exec.data.map(|v| value_to_json(&v));
271
272 Self {
273 code,
274 stdout,
275 stderr: exec.err,
276 data,
277 output,
278 content_type: exec.content_type,
279 baggage: exec.baggage,
280 }
281 }
282}
283
284#[derive(Debug, Clone)]
286pub struct ToolInfo {
287 pub name: String,
289 pub description: String,
291 pub schema: ToolSchema,
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn tool_result_from_exec_result_preserves_content_type_and_baggage() {
301 let mut exec = ExecResult::success("hello");
302 exec.content_type = Some("text/markdown".to_string());
303 exec.baggage.insert("traceparent".to_string(), "00-abc-def-01".to_string());
304
305 let tool_result = ToolResult::from(exec);
306 assert_eq!(tool_result.content_type.as_deref(), Some("text/markdown"));
307 assert_eq!(
308 tool_result.baggage.get("traceparent").map(|s| s.as_str()),
309 Some("00-abc-def-01")
310 );
311 }
312
313 #[test]
314 fn tool_result_constructors_default_to_empty_baggage() {
315 let success = ToolResult::success("ok");
316 assert!(success.baggage.is_empty());
317 assert!(success.content_type.is_none());
318
319 let failure = ToolResult::failure(1, "err");
320 assert!(failure.baggage.is_empty());
321 assert!(failure.content_type.is_none());
322 }
323}