systemprompt-agent 0.1.22

Core Agent protocol module for systemprompt.io
Documentation
use crate::services::shared::error::{AgentServiceError, Result};
use std::time::Duration;
use tokio::time::{sleep, timeout};

#[derive(Debug, Clone, Copy)]
pub struct RetryConfiguration {
    pub max_attempts: u32,
    pub initial_delay: Duration,
    pub max_delay: Duration,
    pub exponential_base: u32,
}

impl Default for RetryConfiguration {
    fn default() -> Self {
        Self {
            max_attempts: 3,
            initial_delay: Duration::from_millis(100),
            max_delay: Duration::from_secs(10),
            exponential_base: 2,
        }
    }
}

pub async fn retry_operation<F, Fut, T>(operation: F, config: RetryConfiguration) -> Result<T>
where
    F: Fn() -> Fut,
    Fut: Future<Output = Result<T>>,
{
    let mut current_delay = config.initial_delay;

    for attempt in 1..=config.max_attempts {
        match operation().await {
            Ok(result) => return Ok(result),
            Err(error) if attempt == config.max_attempts => return Err(error),
            Err(_) => {
                sleep(current_delay).await;
                current_delay = calculate_next_delay(current_delay, &config);
            },
        }
    }

    Err(AgentServiceError::Configuration(
        "RetryConfiguration".to_string(),
        "Retry configuration resulted in no attempts".to_string(),
    ))
}

fn calculate_next_delay(current: Duration, config: &RetryConfiguration) -> Duration {
    let next = current.saturating_mul(config.exponential_base);
    if next > config.max_delay {
        config.max_delay
    } else {
        next
    }
}

pub async fn retry_operation_with_backoff<F, Fut, T>(
    operation: F,
    max_attempts: u32,
    initial_delay: Duration,
) -> Result<T>
where
    F: Fn() -> Fut,
    Fut: Future<Output = Result<T>>,
{
    let config = RetryConfiguration {
        max_attempts,
        initial_delay,
        ..Default::default()
    };
    retry_operation(operation, config).await
}

pub async fn execute_with_timeout<F, T>(duration: Duration, operation: F) -> Result<T>
where
    F: Future<Output = Result<T>>,
{
    timeout(duration, operation)
        .await
        .unwrap_or_else(|_| Err(AgentServiceError::Timeout(duration.as_millis() as u64)))
}

#[derive(Debug, Clone, Copy)]
pub struct TimeoutConfiguration {
    pub default: Duration,
    pub connect: Duration,
    pub read: Duration,
    pub write: Duration,
}

impl Default for TimeoutConfiguration {
    fn default() -> Self {
        Self {
            default: Duration::from_secs(30),
            connect: Duration::from_secs(10),
            read: Duration::from_secs(30),
            write: Duration::from_secs(30),
        }
    }
}

pub async fn execute_with_custom_timeout<F, T>(
    config: TimeoutConfiguration,
    timeout_type: TimeoutType,
    operation: F,
) -> Result<T>
where
    F: Future<Output = Result<T>>,
{
    let duration = match timeout_type {
        TimeoutType::Connect => config.connect,
        TimeoutType::Read => config.read,
        TimeoutType::Write => config.write,
        TimeoutType::Default => config.default,
    };

    execute_with_timeout(duration, operation).await
}

#[derive(Debug, Clone, Copy)]
pub enum TimeoutType {
    Connect,
    Read,
    Write,
    Default,
}