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}
30
31#[derive(Debug, Clone, Error)]
33#[non_exhaustive]
34pub enum BackendError {
35 #[error("not found: {0}")]
36 NotFound(String),
37 #[error("already exists: {0}")]
38 AlreadyExists(String),
39 #[error("permission denied: {0}")]
40 PermissionDenied(String),
41 #[error("is a directory: {0}")]
42 IsDirectory(String),
43 #[error("not a directory: {0}")]
44 NotDirectory(String),
45 #[error("read-only filesystem")]
46 ReadOnly,
47 #[error("conflict: {0}")]
48 Conflict(ConflictError),
49 #[error("tool not found: {0}")]
50 ToolNotFound(String),
51 #[error("io error: {0}")]
52 Io(String),
53 #[error("invalid operation: {0}")]
54 InvalidOperation(String),
55}
56
57impl From<std::io::Error> for BackendError {
58 fn from(err: std::io::Error) -> Self {
59 use std::io::ErrorKind;
60 match err.kind() {
61 ErrorKind::NotFound => BackendError::NotFound(err.to_string()),
62 ErrorKind::AlreadyExists => BackendError::AlreadyExists(err.to_string()),
63 ErrorKind::PermissionDenied => BackendError::PermissionDenied(err.to_string()),
64 ErrorKind::IsADirectory => BackendError::IsDirectory(err.to_string()),
65 ErrorKind::NotADirectory => BackendError::NotDirectory(err.to_string()),
66 ErrorKind::ReadOnlyFilesystem => BackendError::ReadOnly,
67 _ => BackendError::Io(err.to_string()),
68 }
69 }
70}
71
72#[derive(Debug, Clone, Error)]
74#[error("conflict at {location}: expected {expected:?}, found {actual:?}")]
75pub struct ConflictError {
76 pub location: String,
78 pub expected: String,
80 pub actual: String,
82}
83
84#[derive(Debug, Clone)]
99pub enum PatchOp {
100 Insert { offset: usize, content: String },
102
103 Delete {
106 offset: usize,
107 len: usize,
108 expected: Option<String>,
109 },
110
111 Replace {
114 offset: usize,
115 len: usize,
116 content: String,
117 expected: Option<String>,
118 },
119
120 InsertLine { line: usize, content: String },
122
123 DeleteLine { line: usize, expected: Option<String> },
126
127 ReplaceLine {
130 line: usize,
131 content: String,
132 expected: Option<String>,
133 },
134
135 Append { content: String },
137}
138
139#[derive(Debug, Clone, Default)]
141pub struct ReadRange {
142 pub start_line: Option<usize>,
144 pub end_line: Option<usize>,
146 pub offset: Option<u64>,
148 pub limit: Option<u64>,
150}
151
152impl ReadRange {
153 pub fn lines(start: usize, end: usize) -> Self {
155 Self {
156 start_line: Some(start),
157 end_line: Some(end),
158 ..Default::default()
159 }
160 }
161
162 pub fn bytes(offset: u64, limit: u64) -> Self {
164 Self {
165 offset: Some(offset),
166 limit: Some(limit),
167 ..Default::default()
168 }
169 }
170}
171
172#[non_exhaustive]
174#[derive(Debug, Clone, Copy, Default)]
175pub enum WriteMode {
176 CreateNew,
178 #[default]
180 Overwrite,
181 UpdateOnly,
183 Truncate,
185}
186
187#[derive(Debug, Clone)]
189pub struct ToolResult {
190 pub code: i32,
192 pub stdout: String,
194 pub stderr: String,
196 pub data: Option<JsonValue>,
198 pub output: Option<OutputData>,
200 pub content_type: Option<String>,
202 pub baggage: BTreeMap<String, String>,
204}
205
206impl ToolResult {
207 pub fn success(stdout: impl Into<String>) -> Self {
209 Self {
210 code: 0,
211 stdout: stdout.into(),
212 stderr: String::new(),
213 data: None,
214 output: None,
215 content_type: None,
216 baggage: BTreeMap::new(),
217 }
218 }
219
220 pub fn failure(code: i32, stderr: impl Into<String>) -> Self {
222 Self {
223 code,
224 stdout: String::new(),
225 stderr: stderr.into(),
226 data: None,
227 output: None,
228 content_type: None,
229 baggage: BTreeMap::new(),
230 }
231 }
232
233 pub fn with_data(stdout: impl Into<String>, data: JsonValue) -> Self {
235 Self {
236 code: 0,
237 stdout: stdout.into(),
238 stderr: String::new(),
239 data: Some(data),
240 output: None,
241 content_type: None,
242 baggage: BTreeMap::new(),
243 }
244 }
245
246 pub fn ok(&self) -> bool {
248 self.code == 0
249 }
250}
251
252impl From<ExecResult> for ToolResult {
253 fn from(mut exec: ExecResult) -> Self {
254 let code = exec.code.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
256
257 let stdout = exec.text_out().into_owned();
259 let output = exec.take_output();
260
261 let data = exec.data.map(|v| value_to_json(&v));
263
264 Self {
265 code,
266 stdout,
267 stderr: exec.err,
268 data,
269 output,
270 content_type: exec.content_type,
271 baggage: exec.baggage,
272 }
273 }
274}
275
276#[derive(Debug, Clone)]
278pub struct ToolInfo {
279 pub name: String,
281 pub description: String,
283 pub schema: ToolSchema,
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn tool_result_from_exec_result_preserves_content_type_and_baggage() {
293 let mut exec = ExecResult::success("hello");
294 exec.content_type = Some("text/markdown".to_string());
295 exec.baggage.insert("traceparent".to_string(), "00-abc-def-01".to_string());
296
297 let tool_result = ToolResult::from(exec);
298 assert_eq!(tool_result.content_type.as_deref(), Some("text/markdown"));
299 assert_eq!(
300 tool_result.baggage.get("traceparent").map(|s| s.as_str()),
301 Some("00-abc-def-01")
302 );
303 }
304
305 #[test]
306 fn tool_result_constructors_default_to_empty_baggage() {
307 let success = ToolResult::success("ok");
308 assert!(success.baggage.is_empty());
309 assert!(success.content_type.is_none());
310
311 let failure = ToolResult::failure(1, "err");
312 assert!(failure.baggage.is_empty());
313 assert!(failure.content_type.is_none());
314 }
315}