1use std::collections::BTreeMap;
7
8use serde_json::Value as JsonValue;
9use thiserror::Error;
10
11use crate::output::OutputData;
12use crate::result::{value_to_json, ExecResult};
13use crate::tool::ToolSchema;
14
15pub type BackendResult<T> = Result<T, BackendError>;
17
18#[derive(Debug, Clone, Error)]
20pub enum BackendError {
21 #[error("not found: {0}")]
22 NotFound(String),
23 #[error("already exists: {0}")]
24 AlreadyExists(String),
25 #[error("permission denied: {0}")]
26 PermissionDenied(String),
27 #[error("is a directory: {0}")]
28 IsDirectory(String),
29 #[error("not a directory: {0}")]
30 NotDirectory(String),
31 #[error("read-only filesystem")]
32 ReadOnly,
33 #[error("conflict: {0}")]
34 Conflict(ConflictError),
35 #[error("tool not found: {0}")]
36 ToolNotFound(String),
37 #[error("io error: {0}")]
38 Io(String),
39 #[error("invalid operation: {0}")]
40 InvalidOperation(String),
41}
42
43impl From<std::io::Error> for BackendError {
44 fn from(err: std::io::Error) -> Self {
45 use std::io::ErrorKind;
46 match err.kind() {
47 ErrorKind::NotFound => BackendError::NotFound(err.to_string()),
48 ErrorKind::AlreadyExists => BackendError::AlreadyExists(err.to_string()),
49 ErrorKind::PermissionDenied => BackendError::PermissionDenied(err.to_string()),
50 ErrorKind::IsADirectory => BackendError::IsDirectory(err.to_string()),
51 ErrorKind::NotADirectory => BackendError::NotDirectory(err.to_string()),
52 ErrorKind::ReadOnlyFilesystem => BackendError::ReadOnly,
53 _ => BackendError::Io(err.to_string()),
54 }
55 }
56}
57
58#[derive(Debug, Clone, Error)]
60#[error("conflict at {location}: expected {expected:?}, found {actual:?}")]
61pub struct ConflictError {
62 pub location: String,
64 pub expected: String,
66 pub actual: String,
68}
69
70#[derive(Debug, Clone)]
85pub enum PatchOp {
86 Insert { offset: usize, content: String },
88
89 Delete {
92 offset: usize,
93 len: usize,
94 expected: Option<String>,
95 },
96
97 Replace {
100 offset: usize,
101 len: usize,
102 content: String,
103 expected: Option<String>,
104 },
105
106 InsertLine { line: usize, content: String },
108
109 DeleteLine { line: usize, expected: Option<String> },
112
113 ReplaceLine {
116 line: usize,
117 content: String,
118 expected: Option<String>,
119 },
120
121 Append { content: String },
123}
124
125#[derive(Debug, Clone, Default)]
127pub struct ReadRange {
128 pub start_line: Option<usize>,
130 pub end_line: Option<usize>,
132 pub offset: Option<u64>,
134 pub limit: Option<u64>,
136}
137
138impl ReadRange {
139 pub fn lines(start: usize, end: usize) -> Self {
141 Self {
142 start_line: Some(start),
143 end_line: Some(end),
144 ..Default::default()
145 }
146 }
147
148 pub fn bytes(offset: u64, limit: u64) -> Self {
150 Self {
151 offset: Some(offset),
152 limit: Some(limit),
153 ..Default::default()
154 }
155 }
156}
157
158#[non_exhaustive]
160#[derive(Debug, Clone, Copy, Default)]
161pub enum WriteMode {
162 CreateNew,
164 #[default]
166 Overwrite,
167 UpdateOnly,
169 Truncate,
171}
172
173#[derive(Debug, Clone)]
175pub struct ToolResult {
176 pub code: i32,
178 pub stdout: String,
180 pub stderr: String,
182 pub data: Option<JsonValue>,
184 pub output: Option<OutputData>,
186 pub content_type: Option<String>,
188 pub baggage: BTreeMap<String, String>,
190}
191
192impl ToolResult {
193 pub fn success(stdout: impl Into<String>) -> Self {
195 Self {
196 code: 0,
197 stdout: stdout.into(),
198 stderr: String::new(),
199 data: None,
200 output: None,
201 content_type: None,
202 baggage: BTreeMap::new(),
203 }
204 }
205
206 pub fn failure(code: i32, stderr: impl Into<String>) -> Self {
208 Self {
209 code,
210 stdout: String::new(),
211 stderr: stderr.into(),
212 data: None,
213 output: None,
214 content_type: None,
215 baggage: BTreeMap::new(),
216 }
217 }
218
219 pub fn with_data(stdout: impl Into<String>, data: JsonValue) -> Self {
221 Self {
222 code: 0,
223 stdout: stdout.into(),
224 stderr: String::new(),
225 data: Some(data),
226 output: None,
227 content_type: None,
228 baggage: BTreeMap::new(),
229 }
230 }
231
232 pub fn ok(&self) -> bool {
234 self.code == 0
235 }
236}
237
238impl From<ExecResult> for ToolResult {
239 fn from(mut exec: ExecResult) -> Self {
240 let code = exec.code.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
242
243 let stdout = exec.text_out().into_owned();
245 let output = exec.take_output();
246
247 let data = exec.data.map(|v| value_to_json(&v));
249
250 Self {
251 code,
252 stdout,
253 stderr: exec.err,
254 data,
255 output,
256 content_type: exec.content_type,
257 baggage: exec.baggage,
258 }
259 }
260}
261
262#[derive(Debug, Clone)]
264pub struct ToolInfo {
265 pub name: String,
267 pub description: String,
269 pub schema: ToolSchema,
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn tool_result_from_exec_result_preserves_content_type_and_baggage() {
279 let mut exec = ExecResult::success("hello");
280 exec.content_type = Some("text/markdown".to_string());
281 exec.baggage.insert("traceparent".to_string(), "00-abc-def-01".to_string());
282
283 let tool_result = ToolResult::from(exec);
284 assert_eq!(tool_result.content_type.as_deref(), Some("text/markdown"));
285 assert_eq!(
286 tool_result.baggage.get("traceparent").map(|s| s.as_str()),
287 Some("00-abc-def-01")
288 );
289 }
290
291 #[test]
292 fn tool_result_constructors_default_to_empty_baggage() {
293 let success = ToolResult::success("ok");
294 assert!(success.baggage.is_empty());
295 assert!(success.content_type.is_none());
296
297 let failure = ToolResult::failure(1, "err");
298 assert!(failure.baggage.is_empty());
299 assert!(failure.content_type.is_none());
300 }
301}