use std::future::Future;
use oino_ai::{AssistantMessage, StopReason};
use serde::{Deserialize, Serialize};
use tokio_util::sync::CancellationToken;
use super::errors::is_retryable_assistant_error;
pub const DEFAULT_MAX_AGENT_RETRY_DELAY_MS: u64 = 60_000;
const MAX_SAFE_INTEGER: u64 = (1 << 53) - 1;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RetryPolicy {
pub enabled: bool,
pub max_retries: u32,
pub base_delay_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_agent_delay_ms: Option<u64>,
}
#[must_use]
pub fn retry_delay_ms(base_delay_ms: u64, max_agent_delay_ms: Option<u64>, attempt: u32) -> u64 {
let exponent = attempt.saturating_sub(1);
let delay = 2u64
.checked_pow(exponent)
.and_then(|factor| base_delay_ms.checked_mul(factor))
.filter(|delay| *delay <= MAX_SAFE_INTEGER)
.unwrap_or(MAX_SAFE_INTEGER);
delay.min(max_agent_delay_ms.unwrap_or(DEFAULT_MAX_AGENT_RETRY_DELAY_MS))
}
pub trait SummarizationRetryObserver: Send + Sync {
fn retry_scheduled(&self, attempt: u32, max_attempts: u32, delay_ms: u64, error_message: &str);
fn retry_attempt_start(&self);
fn retry_finished(&self, success: bool, attempt: u32, final_error: Option<&str>);
}
pub async fn retry_assistant_call<F, Fut>(
mut produce: F,
policy: Option<&RetryPolicy>,
cancel: &CancellationToken,
observer: Option<&dyn SummarizationRetryObserver>,
) -> AssistantMessage
where
F: FnMut() -> Fut,
Fut: Future<Output = AssistantMessage>,
{
let max_attempts = policy
.filter(|policy| policy.enabled)
.map_or(0, |policy| policy.max_retries);
let mut attempt = 0;
let mut last_retry: Option<String> = None;
loop {
let mut response = produce().await;
let finished = |success: bool, final_error: Option<&str>| {
if let (Some(observer), Some(_)) = (observer, &last_retry) {
observer.retry_finished(success, attempt, final_error);
}
};
match response.stop_reason {
StopReason::Aborted => {
finished(false, None);
return response;
}
StopReason::Error => {}
_ => {
finished(true, None);
return response;
}
}
let Some(policy) =
policy.filter(|_| attempt < max_attempts && is_retryable_assistant_error(&response))
else {
finished(false, response.error_message.as_deref());
return response;
};
attempt += 1;
let error_message = response
.error_message
.clone()
.filter(|text| !text.is_empty())
.unwrap_or_else(|| "Unknown error".to_owned());
let delay_ms = retry_delay_ms(policy.base_delay_ms, policy.max_agent_delay_ms, attempt);
if let Some(observer) = observer {
observer.retry_scheduled(attempt, max_attempts, delay_ms, &error_message);
}
let cancelled = tokio::select! {
biased;
() = cancel.cancelled() => true,
() = tokio::time::sleep(std::time::Duration::from_millis(delay_ms)) => false,
};
if cancelled {
if let Some(observer) = observer {
observer.retry_finished(false, attempt, Some(&error_message));
}
response.error_message = None;
response.stop_reason = StopReason::Aborted;
return response;
}
last_retry = Some(error_message);
if let Some(observer) = observer {
observer.retry_attempt_start();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn delay_doubles_and_caps() {
assert_eq!(retry_delay_ms(2000, None, 0), 2000);
assert_eq!(retry_delay_ms(2000, None, 1), 2000);
assert_eq!(retry_delay_ms(2000, None, 3), 8000);
assert_eq!(retry_delay_ms(2000, None, 10), 60_000);
assert_eq!(retry_delay_ms(2000, Some(u64::MAX), 80), MAX_SAFE_INTEGER);
}
}