Skip to main content

kaish_types/
backend.rs

1//! Backend data types — errors, results, and operations.
2//!
3//! These types define the data contract for `KernelBackend` implementations.
4//! The trait itself lives in kaish-kernel (it depends on async_trait and ExecContext).
5
6use serde_json::Value as JsonValue;
7use thiserror::Error;
8
9use crate::output::OutputData;
10use crate::result::{value_to_json, ExecResult};
11use crate::tool::ToolSchema;
12
13/// Result type for backend operations.
14pub type BackendResult<T> = Result<T, BackendError>;
15
16/// Backend operation errors.
17#[derive(Debug, Clone, Error)]
18pub enum BackendError {
19    #[error("not found: {0}")]
20    NotFound(String),
21    #[error("already exists: {0}")]
22    AlreadyExists(String),
23    #[error("permission denied: {0}")]
24    PermissionDenied(String),
25    #[error("is a directory: {0}")]
26    IsDirectory(String),
27    #[error("not a directory: {0}")]
28    NotDirectory(String),
29    #[error("read-only filesystem")]
30    ReadOnly,
31    #[error("conflict: {0}")]
32    Conflict(ConflictError),
33    #[error("tool not found: {0}")]
34    ToolNotFound(String),
35    #[error("io error: {0}")]
36    Io(String),
37    #[error("invalid operation: {0}")]
38    InvalidOperation(String),
39}
40
41impl From<std::io::Error> for BackendError {
42    fn from(err: std::io::Error) -> Self {
43        use std::io::ErrorKind;
44        match err.kind() {
45            ErrorKind::NotFound => BackendError::NotFound(err.to_string()),
46            ErrorKind::AlreadyExists => BackendError::AlreadyExists(err.to_string()),
47            ErrorKind::PermissionDenied => BackendError::PermissionDenied(err.to_string()),
48            ErrorKind::IsADirectory => BackendError::IsDirectory(err.to_string()),
49            ErrorKind::NotADirectory => BackendError::NotDirectory(err.to_string()),
50            ErrorKind::ReadOnlyFilesystem => BackendError::ReadOnly,
51            _ => BackendError::Io(err.to_string()),
52        }
53    }
54}
55
56/// Error when CAS (compare-and-set) check fails during patching.
57#[derive(Debug, Clone, Error)]
58#[error("conflict at {location}: expected {expected:?}, found {actual:?}")]
59pub struct ConflictError {
60    /// Location of the conflict (e.g., "offset 42" or "line 7")
61    pub location: String,
62    /// Expected content at that location
63    pub expected: String,
64    /// Actual content found at that location
65    pub actual: String,
66}
67
68/// Generic patch operation for file modifications.
69///
70/// Maps to POSIX operations, CRDTs, or REST APIs. All positional ops
71/// support compare-and-set (CAS) via optional `expected` field.
72/// If `expected` is Some, the operation fails with ConflictError if the
73/// current content at that position doesn't match.
74///
75/// # Line Ending Normalization
76///
77/// Line-based operations (`InsertLine`, `DeleteLine`, `ReplaceLine`) normalize
78/// line endings to Unix-style (`\n`). Files with `\r\n` (Windows) line endings
79/// will be converted to `\n` after a line-based patch. This is intentional for
80/// kaish's Unix-first design. Use byte-based operations (`Insert`, `Delete`,
81/// `Replace`) to preserve original line endings.
82#[derive(Debug, Clone)]
83pub enum PatchOp {
84    /// Insert content at byte offset.
85    Insert { offset: usize, content: String },
86
87    /// Delete bytes from offset to offset+len.
88    /// `expected`: if Some, must match content being deleted (CAS)
89    Delete {
90        offset: usize,
91        len: usize,
92        expected: Option<String>,
93    },
94
95    /// Replace content at offset.
96    /// `expected`: if Some, must match content being replaced (CAS)
97    Replace {
98        offset: usize,
99        len: usize,
100        content: String,
101        expected: Option<String>,
102    },
103
104    /// Insert a line at line number (1-indexed).
105    InsertLine { line: usize, content: String },
106
107    /// Delete a line at line number (1-indexed).
108    /// `expected`: if Some, must match line being deleted (CAS)
109    DeleteLine { line: usize, expected: Option<String> },
110
111    /// Replace a line at line number (1-indexed).
112    /// `expected`: if Some, must match line being replaced (CAS)
113    ReplaceLine {
114        line: usize,
115        content: String,
116        expected: Option<String>,
117    },
118
119    /// Append content to end of file (no CAS needed - always safe).
120    Append { content: String },
121}
122
123/// Range specification for partial file reads.
124#[derive(Debug, Clone, Default)]
125pub struct ReadRange {
126    /// Start line (1-indexed). If set, read from this line.
127    pub start_line: Option<usize>,
128    /// End line (1-indexed, inclusive). If set, read until this line.
129    pub end_line: Option<usize>,
130    /// Byte offset to start reading from.
131    pub offset: Option<u64>,
132    /// Maximum number of bytes to read.
133    pub limit: Option<u64>,
134}
135
136impl ReadRange {
137    /// Create a range for reading specific lines.
138    pub fn lines(start: usize, end: usize) -> Self {
139        Self {
140            start_line: Some(start),
141            end_line: Some(end),
142            ..Default::default()
143        }
144    }
145
146    /// Create a range for reading bytes at an offset.
147    pub fn bytes(offset: u64, limit: u64) -> Self {
148        Self {
149            offset: Some(offset),
150            limit: Some(limit),
151            ..Default::default()
152        }
153    }
154}
155
156/// Write mode for file operations.
157#[non_exhaustive]
158#[derive(Debug, Clone, Copy, Default)]
159pub enum WriteMode {
160    /// Fail if file already exists.
161    CreateNew,
162    /// Overwrite existing file (default, like `>`).
163    #[default]
164    Overwrite,
165    /// Fail if file does not exist.
166    UpdateOnly,
167    /// Explicitly truncate file before writing.
168    Truncate,
169}
170
171/// Result from tool execution via backend.
172#[derive(Debug, Clone)]
173pub struct ToolResult {
174    /// Exit code (0 = success).
175    pub code: i32,
176    /// Standard output.
177    pub stdout: String,
178    /// Standard error.
179    pub stderr: String,
180    /// Structured data (if any).
181    pub data: Option<JsonValue>,
182    /// Structured output data for rendering (preserved from ExecResult).
183    pub output: Option<OutputData>,
184}
185
186impl ToolResult {
187    /// Create a successful result.
188    pub fn success(stdout: impl Into<String>) -> Self {
189        Self {
190            code: 0,
191            stdout: stdout.into(),
192            stderr: String::new(),
193            data: None,
194            output: None,
195        }
196    }
197
198    /// Create a failed result.
199    pub fn failure(code: i32, stderr: impl Into<String>) -> Self {
200        Self {
201            code,
202            stdout: String::new(),
203            stderr: stderr.into(),
204            data: None,
205            output: None,
206        }
207    }
208
209    /// Create a result with structured data.
210    pub fn with_data(stdout: impl Into<String>, data: JsonValue) -> Self {
211        Self {
212            code: 0,
213            stdout: stdout.into(),
214            stderr: String::new(),
215            data: Some(data),
216            output: None,
217        }
218    }
219
220    /// Check if the tool execution succeeded.
221    pub fn ok(&self) -> bool {
222        self.code == 0
223    }
224}
225
226impl From<ExecResult> for ToolResult {
227    fn from(exec: ExecResult) -> Self {
228        // Saturating cast: codes outside i32 range clamp to i32::MIN/MAX
229        let code = exec.code.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
230
231        // Convert ast::Value to serde_json::Value if present
232        let data = exec.data.map(|v| value_to_json(&v));
233
234        Self {
235            code,
236            stdout: exec.out,
237            stderr: exec.err,
238            data,
239            output: exec.output,
240        }
241    }
242}
243
244/// Information about an available tool.
245#[derive(Debug, Clone)]
246pub struct ToolInfo {
247    /// Tool name.
248    pub name: String,
249    /// Tool description.
250    pub description: String,
251    /// Full tool schema.
252    pub schema: ToolSchema,
253}