use std::future::Future;
use std::pin::Pin;
pub mod agent;
pub mod backend;
pub mod config;
pub mod identifier;
pub mod middleware;
#[cfg(unix)]
pub mod owner_only;
pub mod protocol;
pub mod sync;
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
#[derive(Debug, thiserror::Error)]
#[error("{message}")]
pub struct ProviderError {
message: String,
status: Option<u16>,
retryable: bool,
retry_after: Option<String>,
kind: ProviderErrorKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProviderErrorKind {
Other,
StreamInterrupted,
}
impl ProviderError {
#[must_use]
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
status: None,
retryable: false,
retry_after: None,
kind: ProviderErrorKind::Other,
}
}
#[must_use]
pub fn retryable(message: impl Into<String>) -> Self {
Self {
retryable: true,
..Self::new(message)
}
}
#[must_use]
pub fn stream_interrupted(retry_after: Option<String>) -> Self {
Self {
message: "model response stream was interrupted".into(),
status: None,
retryable: true,
retry_after,
kind: ProviderErrorKind::StreamInterrupted,
}
}
pub(crate) fn http(
message: impl Into<String>,
status: u16,
retry_after: Option<String>,
) -> Self {
Self {
message: message.into(),
status: Some(status),
retryable: status == 408 || status == 429 || (500..=599).contains(&status),
retry_after,
kind: ProviderErrorKind::Other,
}
}
pub(crate) fn with_code(mut self, code: Option<&str>) -> Self {
if code.is_some_and(Self::is_quota_code) {
self.retryable = false;
}
self
}
pub(crate) fn is_quota_code(code: &str) -> bool {
matches!(
code,
"insufficient_quota"
| "quota_exceeded"
| "billing_hard_limit_reached"
| "billing_not_active"
| "usage_limit_reached"
| "credit_limit_exceeded"
)
}
#[must_use]
pub fn status(&self) -> Option<u16> {
self.status
}
#[must_use]
pub fn is_retryable(&self) -> bool {
self.retryable
}
#[must_use]
pub fn is_stream_interrupted(&self) -> bool {
self.kind == ProviderErrorKind::StreamInterrupted
}
#[must_use]
pub fn retry_after(&self) -> Option<&str> {
self.retry_after.as_deref()
}
}
impl From<String> for ProviderError {
fn from(message: String) -> Self {
Self::new(message)
}
}
impl From<&str> for ProviderError {
fn from(message: &str) -> Self {
Self::new(message)
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("internal lock poisoned: {0}")]
Poisoned(&'static str),
#[error("storage is full; free up space before adding files")]
StorageFull,
#[error("configuration error: {0}")]
Config(String),
#[error("duplicate registration: {0}")]
Duplicate(String),
#[error("unknown registration: {0}")]
Unknown(String),
#[error("provider error: {0}")]
Provider(#[from] ProviderError),
#[error("authentication error: {0}")]
Auth(String),
#[error("sandbox rejected path: {0}")]
Sandbox(String),
#[error("tool error: {0}")]
Tool(String),
#[error("checkpoint error: {0}")]
Checkpoint(String),
#[error("agent busy: {0}")]
Busy(String),
#[error("agent stopped: {0}")]
Stopped(String),
#[error("{primary}; rollback failed: {rollback}")]
Rollback {
primary: Box<Error>,
rollback: Box<Error>,
},
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Http(#[from] reqwest::Error),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error("checkpoint storage error")]
Sqlite(
#[source]
#[from]
rusqlite::Error,
),
}
pub type Result<T> = std::result::Result<T, Error>;
pub(crate) const MAX_IDENTIFIER_BYTES: usize = 4 * 1024;
pub(crate) fn validate_identifier(name: &str, value: &str, limit: usize) -> Result<()> {
if value.trim().is_empty() {
return Err(Error::Config(format!("{name} cannot be empty")));
}
if value.len() > limit {
return Err(Error::Config(format!("{name} exceeds size limit")));
}
Ok(())
}
pub(crate) fn preview_json(value: &serde_json::Value) -> String {
let value = value.to_string();
if value.len() <= 10_000 {
return value;
}
format!("{}…", truncate_utf8(&value, 10_000))
}
pub(crate) fn truncate_utf8(value: &str, max_bytes: usize) -> &str {
&value[..value.floor_char_boundary(max_bytes)]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sqlite_errors_do_not_expose_engine_messages() {
let error = Error::from(rusqlite::Error::InvalidQuery);
assert_eq!(error.to_string(), "checkpoint storage error");
}
}