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::{json_to_value_no_envelope, 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#[non_exhaustive]
234#[derive(Debug, Clone)]
235pub struct ToolResult {
236 pub code: i32,
238 pub stdout: String,
240 pub stderr: String,
242 pub data: Option<JsonValue>,
244 pub output: Option<OutputData>,
246 pub did_spill: bool,
250 pub original_code: Option<i64>,
254 pub content_type: Option<String>,
256 pub baggage: BTreeMap<String, String>,
258}
259
260impl ToolResult {
261 pub fn success(stdout: impl Into<String>) -> Self {
263 Self {
264 code: 0,
265 stdout: stdout.into(),
266 stderr: String::new(),
267 data: None,
268 output: None,
269 did_spill: false,
270 original_code: None,
271 content_type: None,
272 baggage: BTreeMap::new(),
273 }
274 }
275
276 pub fn failure(code: i32, stderr: impl Into<String>) -> Self {
278 Self {
279 code,
280 stdout: String::new(),
281 stderr: stderr.into(),
282 data: None,
283 output: None,
284 did_spill: false,
285 original_code: None,
286 content_type: None,
287 baggage: BTreeMap::new(),
288 }
289 }
290
291 pub fn with_data(stdout: impl Into<String>, data: JsonValue) -> Self {
293 Self {
294 code: 0,
295 stdout: stdout.into(),
296 stderr: String::new(),
297 data: Some(data),
298 output: None,
299 did_spill: false,
300 original_code: None,
301 content_type: None,
302 baggage: BTreeMap::new(),
303 }
304 }
305
306 pub fn ok(&self) -> bool {
308 self.code == 0
309 }
310
311 pub fn with_output(mut self, output: Option<OutputData>) -> Self {
313 self.output = output;
314 self
315 }
316
317 pub fn with_content_type(mut self, ct: impl Into<String>) -> Self {
319 self.content_type = Some(ct.into());
320 self
321 }
322
323 pub fn with_baggage(mut self, baggage: BTreeMap<String, String>) -> Self {
325 self.baggage = baggage;
326 self
327 }
328
329 pub fn with_did_spill(mut self, did_spill: bool) -> Self {
331 self.did_spill = did_spill;
332 self
333 }
334
335 pub fn with_original_code(mut self, original_code: Option<i64>) -> Self {
337 self.original_code = original_code;
338 self
339 }
340}
341
342impl From<ExecResult> for ToolResult {
343 fn from(mut exec: ExecResult) -> Self {
344 let code = exec.code.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
346
347 let stdout = exec.text_out().into_owned();
361 let output = exec.take_output();
362
363 let data = exec.data.map(|v| value_to_json(&v));
365
366 Self {
367 code,
368 stdout,
369 stderr: exec.err,
370 data,
371 output,
372 did_spill: exec.did_spill,
373 original_code: exec.original_code,
374 content_type: exec.content_type,
375 baggage: exec.baggage,
376 }
377 }
378}
379
380impl From<ToolResult> for ExecResult {
381 fn from(result: ToolResult) -> Self {
393 let mut exec = ExecResult::from_output(result.code as i64, result.stdout, result.stderr);
394 exec.set_output(result.output);
395 exec.data = result.data.map(json_to_value_no_envelope);
396 exec.did_spill = result.did_spill;
397 exec.original_code = result.original_code;
398 exec.content_type = result.content_type;
399 exec.baggage = result.baggage;
400 exec
401 }
402}
403
404#[derive(Debug, Clone)]
406pub struct ToolInfo {
407 pub name: String,
409 pub description: String,
411 pub schema: ToolSchema,
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[test]
420 fn tool_result_from_exec_result_preserves_content_type_and_baggage() {
421 let mut exec = ExecResult::success("hello");
422 exec.content_type = Some("text/markdown".to_string());
423 exec.baggage.insert("traceparent".to_string(), "00-abc-def-01".to_string());
424
425 let tool_result = ToolResult::from(exec);
426 assert_eq!(tool_result.content_type.as_deref(), Some("text/markdown"));
427 assert_eq!(
428 tool_result.baggage.get("traceparent").map(|s| s.as_str()),
429 Some("00-abc-def-01")
430 );
431 }
432
433 #[test]
434 fn tool_result_constructors_default_to_empty_baggage() {
435 let success = ToolResult::success("ok");
436 assert!(success.baggage.is_empty());
437 assert!(success.content_type.is_none());
438
439 let failure = ToolResult::failure(1, "err");
440 assert!(failure.baggage.is_empty());
441 assert!(failure.content_type.is_none());
442 }
443
444 #[test]
445 fn tool_result_constructors_default_did_spill_and_original_code() {
446 assert!(!ToolResult::success("ok").did_spill);
449 assert!(ToolResult::success("ok").original_code.is_none());
450 assert!(!ToolResult::failure(1, "err").did_spill);
451 assert!(!ToolResult::with_data("ok", serde_json::json!(1)).did_spill);
452 }
453
454 #[test]
455 fn tool_result_from_exec_result_preserves_did_spill_and_original_code() {
456 let mut exec = ExecResult::success("hello");
460 exec.did_spill = true;
461 exec.original_code = Some(0);
462
463 let tool_result = ToolResult::from(exec);
464 assert!(tool_result.did_spill);
465 assert_eq!(tool_result.original_code, Some(0));
466 }
467
468 #[test]
469 fn exec_result_from_tool_result_preserves_did_spill_and_original_code() {
470 let tool_result = ToolResult::success("hello")
474 .with_did_spill(true)
475 .with_original_code(Some(5));
476
477 let exec = ExecResult::from(tool_result);
478 assert!(exec.did_spill);
479 assert_eq!(exec.original_code, Some(5));
480 }
481
482 #[test]
483 fn tool_result_builder_setters_chain() {
484 let mut baggage = BTreeMap::new();
488 baggage.insert("k".to_string(), "v".to_string());
489
490
491 let result = ToolResult::success("hi")
492 .with_output(Some(OutputData::text("hi")))
493 .with_content_type("text/plain")
494 .with_baggage(baggage.clone())
495 .with_did_spill(true)
496 .with_original_code(Some(2));
497
498 assert!(result.output.is_some());
499 assert_eq!(result.content_type.as_deref(), Some("text/plain"));
500 assert_eq!(result.baggage, baggage);
501 assert!(result.did_spill);
502 assert_eq!(result.original_code, Some(2));
503 }
504
505}