use std::collections::BTreeMap;
use std::path::PathBuf;
use serde_json::Value as JsonValue;
use thiserror::Error;
use crate::output::OutputData;
use crate::result::{value_to_json, ExecResult};
use crate::tool::ToolSchema;
pub type BackendResult<T> = Result<T, BackendError>;
#[derive(Debug, Clone)]
pub struct MountInfo {
pub path: PathBuf,
pub read_only: bool,
pub resident_bytes: Option<u64>,
}
#[derive(Debug, Clone, Error)]
#[non_exhaustive]
pub enum BackendError {
#[error("not found: {0}")]
NotFound(String),
#[error("already exists: {0}")]
AlreadyExists(String),
#[error("permission denied: {0}")]
PermissionDenied(String),
#[error("is a directory: {0}")]
IsDirectory(String),
#[error("not a directory: {0}")]
NotDirectory(String),
#[error("read-only filesystem")]
ReadOnly,
#[error("conflict: {0}")]
Conflict(ConflictError),
#[error("tool not found: {0}")]
ToolNotFound(String),
#[error("io error: {0}")]
Io(String),
#[error("invalid operation: {0}")]
InvalidOperation(String),
}
impl From<std::io::Error> for BackendError {
fn from(err: std::io::Error) -> Self {
use std::io::ErrorKind;
match err.kind() {
ErrorKind::NotFound => BackendError::NotFound(err.to_string()),
ErrorKind::AlreadyExists => BackendError::AlreadyExists(err.to_string()),
ErrorKind::PermissionDenied => BackendError::PermissionDenied(err.to_string()),
ErrorKind::IsADirectory => BackendError::IsDirectory(err.to_string()),
ErrorKind::NotADirectory => BackendError::NotDirectory(err.to_string()),
ErrorKind::ReadOnlyFilesystem => BackendError::ReadOnly,
_ => BackendError::Io(err.to_string()),
}
}
}
#[derive(Debug, Clone, Error)]
#[error("conflict at {location}: expected {expected:?}, found {actual:?}")]
pub struct ConflictError {
pub location: String,
pub expected: String,
pub actual: String,
}
#[derive(Debug, Clone)]
pub enum PatchOp {
Insert { offset: usize, content: String },
Delete {
offset: usize,
len: usize,
expected: Option<String>,
},
Replace {
offset: usize,
len: usize,
content: String,
expected: Option<String>,
},
InsertLine { line: usize, content: String },
DeleteLine { line: usize, expected: Option<String> },
ReplaceLine {
line: usize,
content: String,
expected: Option<String>,
},
Append { content: String },
}
#[derive(Debug, Clone, Default)]
pub struct ReadRange {
pub start_line: Option<usize>,
pub end_line: Option<usize>,
pub offset: Option<u64>,
pub limit: Option<u64>,
}
impl ReadRange {
pub fn lines(start: usize, end: usize) -> Self {
Self {
start_line: Some(start),
end_line: Some(end),
..Default::default()
}
}
pub fn bytes(offset: u64, limit: u64) -> Self {
Self {
offset: Some(offset),
limit: Some(limit),
..Default::default()
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default)]
pub enum WriteMode {
CreateNew,
#[default]
Overwrite,
UpdateOnly,
Truncate,
}
#[derive(Debug, Clone)]
pub struct ToolResult {
pub code: i32,
pub stdout: String,
pub stderr: String,
pub data: Option<JsonValue>,
pub output: Option<OutputData>,
pub content_type: Option<String>,
pub baggage: BTreeMap<String, String>,
}
impl ToolResult {
pub fn success(stdout: impl Into<String>) -> Self {
Self {
code: 0,
stdout: stdout.into(),
stderr: String::new(),
data: None,
output: None,
content_type: None,
baggage: BTreeMap::new(),
}
}
pub fn failure(code: i32, stderr: impl Into<String>) -> Self {
Self {
code,
stdout: String::new(),
stderr: stderr.into(),
data: None,
output: None,
content_type: None,
baggage: BTreeMap::new(),
}
}
pub fn with_data(stdout: impl Into<String>, data: JsonValue) -> Self {
Self {
code: 0,
stdout: stdout.into(),
stderr: String::new(),
data: Some(data),
output: None,
content_type: None,
baggage: BTreeMap::new(),
}
}
pub fn ok(&self) -> bool {
self.code == 0
}
}
impl From<ExecResult> for ToolResult {
fn from(mut exec: ExecResult) -> Self {
let code = exec.code.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
let stdout = exec.text_out().into_owned();
let output = exec.take_output();
let data = exec.data.map(|v| value_to_json(&v));
Self {
code,
stdout,
stderr: exec.err,
data,
output,
content_type: exec.content_type,
baggage: exec.baggage,
}
}
}
#[derive(Debug, Clone)]
pub struct ToolInfo {
pub name: String,
pub description: String,
pub schema: ToolSchema,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tool_result_from_exec_result_preserves_content_type_and_baggage() {
let mut exec = ExecResult::success("hello");
exec.content_type = Some("text/markdown".to_string());
exec.baggage.insert("traceparent".to_string(), "00-abc-def-01".to_string());
let tool_result = ToolResult::from(exec);
assert_eq!(tool_result.content_type.as_deref(), Some("text/markdown"));
assert_eq!(
tool_result.baggage.get("traceparent").map(|s| s.as_str()),
Some("00-abc-def-01")
);
}
#[test]
fn tool_result_constructors_default_to_empty_baggage() {
let success = ToolResult::success("ok");
assert!(success.baggage.is_empty());
assert!(success.content_type.is_none());
let failure = ToolResult::failure(1, "err");
assert!(failure.baggage.is_empty());
assert!(failure.content_type.is_none());
}
}