use std::fmt; pub type Result = std::result::Result; /// A failure in a pipeline stage. The orchestrator decides whether it is /// fatal or whether aborting the current turn is enough, so the error carries /// that distinction instead of leaving it to whoever receives it. #[derive(Debug, thiserror::Error)] pub enum Error { #[error("invalid configuration: {0}")] Config(String), #[error("audio: {0}")] Audio(String), #[error("ASR: {0}")] Asr(String), #[error("LLM: {0}")] Llm(String), #[error("TTS: {0}")] Tts(String), #[error("HTTP {status} at {url}: {body}")] Http { status: u16, url: String, body: String, }, #[error("transport to {url}: {source}")] Transport { url: String, #[source] source: std::io::Error, }, #[error("tool «{tool}»: {message}")] Tool { tool: String, message: String }, #[error("operation cancelled")] Cancelled, #[error(transparent)] Json(#[from] serde_json::Error), #[error(transparent)] Io(#[from] std::io::Error), } impl Error { /// A fatal error brings the assistant down; the rest only abort the turn. /// /// The local servers crash and restart, and the user would rather hear «no /// te he entendido» than have the process die, so only configuration and /// audio (which cannot be retried) are fatal. pub fn is_fatal(&self) -> bool { matches!(self, Error::Config(_) | Error::Audio(_)) } /// `true` when retrying makes sense: the server is still loading the /// model, or the connection dropped mid-request. pub fn is_retryable(&self) -> bool { match self { Error::Transport { .. } => true, Error::Http { status, .. } => *status == 503 || *status == 429 || *status >= 500, _ => false, } } } /// Readable context for `Result`s that cross a crate boundary. pub trait Context { fn ctx(self, f: impl FnOnce() -> String) -> Result; } impl Context for std::result::Result { fn ctx(self, f: impl FnOnce() -> String) -> Result { self.map_err(|e| Error::Config(format!("{}: {}", f(), e))) } }