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, LatchRequest};
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 pub latch: Option<Box<LatchRequest>>,
263}
264
265impl ToolResult {
266 pub fn success(stdout: impl Into<String>) -> Self {
268 Self {
269 code: 0,
270 stdout: stdout.into(),
271 stderr: String::new(),
272 data: None,
273 output: None,
274 did_spill: false,
275 original_code: None,
276 content_type: None,
277 baggage: BTreeMap::new(),
278 latch: None,
279 }
280 }
281
282 pub fn failure(code: i32, stderr: impl Into<String>) -> Self {
284 Self {
285 code,
286 stdout: String::new(),
287 stderr: stderr.into(),
288 data: None,
289 output: None,
290 did_spill: false,
291 original_code: None,
292 content_type: None,
293 baggage: BTreeMap::new(),
294 latch: None,
295 }
296 }
297
298 pub fn with_data(stdout: impl Into<String>, data: JsonValue) -> Self {
300 Self {
301 code: 0,
302 stdout: stdout.into(),
303 stderr: String::new(),
304 data: Some(data),
305 output: None,
306 did_spill: false,
307 original_code: None,
308 content_type: None,
309 baggage: BTreeMap::new(),
310 latch: None,
311 }
312 }
313
314 pub fn ok(&self) -> bool {
316 self.code == 0
317 }
318
319 pub fn with_output(mut self, output: Option<OutputData>) -> Self {
321 self.output = output;
322 self
323 }
324
325 pub fn with_content_type(mut self, ct: impl Into<String>) -> Self {
327 self.content_type = Some(ct.into());
328 self
329 }
330
331 pub fn with_baggage(mut self, baggage: BTreeMap<String, String>) -> Self {
333 self.baggage = baggage;
334 self
335 }
336
337 pub fn with_latch(mut self, latch: Option<LatchRequest>) -> Self {
339 self.latch = latch.map(Box::new);
340 self
341 }
342
343 pub fn with_did_spill(mut self, did_spill: bool) -> Self {
345 self.did_spill = did_spill;
346 self
347 }
348
349 pub fn with_original_code(mut self, original_code: Option<i64>) -> Self {
351 self.original_code = original_code;
352 self
353 }
354}
355
356impl From<ExecResult> for ToolResult {
357 fn from(mut exec: ExecResult) -> Self {
358 let code = exec.code.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
360
361 let stdout = exec.text_out().into_owned();
375 let output = exec.take_output();
376
377 let data = exec.data.map(|v| value_to_json(&v));
379
380 Self {
381 code,
382 stdout,
383 stderr: exec.err,
384 data,
385 output,
386 did_spill: exec.did_spill,
387 original_code: exec.original_code,
388 content_type: exec.content_type,
389 baggage: exec.baggage,
390 latch: exec.latch,
391 }
392 }
393}
394
395impl From<ToolResult> for ExecResult {
396 fn from(result: ToolResult) -> Self {
408 let mut exec = ExecResult::from_output(result.code as i64, result.stdout, result.stderr);
409 exec.set_output(result.output);
410 exec.data = result.data.map(json_to_value_no_envelope);
411 exec.did_spill = result.did_spill;
412 exec.original_code = result.original_code;
413 exec.content_type = result.content_type;
414 exec.baggage = result.baggage;
415 exec.latch = result.latch;
416 exec
417 }
418}
419
420#[derive(Debug, Clone)]
422pub struct ToolInfo {
423 pub name: String,
425 pub description: String,
427 pub schema: ToolSchema,
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 #[test]
436 fn tool_result_from_exec_result_preserves_content_type_and_baggage() {
437 let mut exec = ExecResult::success("hello");
438 exec.content_type = Some("text/markdown".to_string());
439 exec.baggage.insert("traceparent".to_string(), "00-abc-def-01".to_string());
440
441 let tool_result = ToolResult::from(exec);
442 assert_eq!(tool_result.content_type.as_deref(), Some("text/markdown"));
443 assert_eq!(
444 tool_result.baggage.get("traceparent").map(|s| s.as_str()),
445 Some("00-abc-def-01")
446 );
447 }
448
449 #[test]
450 fn tool_result_constructors_default_to_empty_baggage() {
451 let success = ToolResult::success("ok");
452 assert!(success.baggage.is_empty());
453 assert!(success.content_type.is_none());
454
455 let failure = ToolResult::failure(1, "err");
456 assert!(failure.baggage.is_empty());
457 assert!(failure.content_type.is_none());
458 }
459
460 #[test]
461 fn tool_result_constructors_default_did_spill_and_original_code() {
462 assert!(!ToolResult::success("ok").did_spill);
465 assert!(ToolResult::success("ok").original_code.is_none());
466 assert!(!ToolResult::failure(1, "err").did_spill);
467 assert!(!ToolResult::with_data("ok", serde_json::json!(1)).did_spill);
468 }
469
470 #[test]
471 fn tool_result_from_exec_result_preserves_did_spill_and_original_code() {
472 let mut exec = ExecResult::success("hello");
476 exec.did_spill = true;
477 exec.original_code = Some(0);
478
479 let tool_result = ToolResult::from(exec);
480 assert!(tool_result.did_spill);
481 assert_eq!(tool_result.original_code, Some(0));
482 }
483
484 #[test]
485 fn exec_result_from_tool_result_preserves_did_spill_and_original_code() {
486 let tool_result = ToolResult::success("hello")
490 .with_did_spill(true)
491 .with_original_code(Some(5));
492
493 let exec = ExecResult::from(tool_result);
494 assert!(exec.did_spill);
495 assert_eq!(exec.original_code, Some(5));
496 }
497
498 #[test]
499 fn tool_result_builder_setters_chain() {
500 let mut baggage = BTreeMap::new();
504 baggage.insert("k".to_string(), "v".to_string());
505
506 let latch = LatchRequest {
507 nonce: "n".to_string(),
508 command: "rm".to_string(),
509 paths: vec!["f".to_string()],
510 hint: "rm --confirm=n f".to_string(),
511 tool: "rm".to_string(),
512 argv: vec!["f".to_string()],
513 ttl: 60,
514 job_id: None,
515 };
516
517 let result = ToolResult::success("hi")
518 .with_output(Some(OutputData::text("hi")))
519 .with_content_type("text/plain")
520 .with_baggage(baggage.clone())
521 .with_latch(Some(latch.clone()))
522 .with_did_spill(true)
523 .with_original_code(Some(2));
524
525 assert!(result.output.is_some());
526 assert_eq!(result.content_type.as_deref(), Some("text/plain"));
527 assert_eq!(result.baggage, baggage);
528 assert_eq!(result.latch.as_deref(), Some(&latch));
529 assert!(result.did_spill);
530 assert_eq!(result.original_code, Some(2));
531 }
532}