use std::fmt; pub type Result = std::result::Result; /// Un fallo de una etapa del pipeline. El orquestador decide si es fatal o si /// basta con abortar el turno en curso, así que el error lleva esa distinción /// en lugar de dejarla al criterio de quien lo recibe. #[derive(Debug, thiserror::Error)] pub enum Error { #[error("configuración inválida: {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} en {url}: {body}")] Http { status: u16, url: String, body: String, }, #[error("transporte hacia {url}: {source}")] Transport { url: String, #[source] source: std::io::Error, }, #[error("herramienta «{tool}»: {message}")] Tool { tool: String, message: String }, #[error("operación cancelada")] Cancelled, #[error(transparent)] Json(#[from] serde_json::Error), #[error(transparent)] Io(#[from] std::io::Error), } impl Error { /// Un error fatal tumba el asistente; el resto sólo aborta el turno. /// /// Los servidores locales se caen y se reinician, y el usuario prefiere /// oír «no te he entendido» a que el proceso muera, así que sólo la /// configuración y el audio (que no se pueden reintentar) son fatales. pub fn is_fatal(&self) -> bool { matches!(self, Error::Config(_) | Error::Audio(_)) } /// `true` cuando reintentar tiene sentido: el servidor aún está cargando /// el modelo, o se ha caído la conexión a mitad de una petición. pub fn is_retryable(&self) -> bool { match self { Error::Transport { .. } => true, Error::Http { status, .. } => *status == 503 || *status == 429 || *status >= 500, _ => false, } } } /// Contexto legible para los `Result` que cruzan una frontera de crate. 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))) } }