lellm_agent/runtime/
retry.rs1use std::time::Duration;
7
8use lellm_core::ToolResult;
9
10use super::event::AgentEvent;
11use super::tools::ToolFn;
12use tokio::sync::mpsc::Sender;
13
14#[derive(Debug, Clone)]
16pub enum BackoffStrategy {
17 Fixed(Duration),
19 Exponential { base: Duration, max: Duration },
21}
22
23impl BackoffStrategy {
24 pub fn delay(&self, attempt: u32) -> Duration {
26 match self {
27 BackoffStrategy::Fixed(d) => *d,
28 BackoffStrategy::Exponential { base, max } => {
29 let d = base.saturating_mul(2_u32.pow(attempt));
30 d.min(*max)
31 }
32 }
33 }
34}
35
36#[derive(Debug, Clone)]
42pub struct RetryPolicy {
43 max_attempts: u32,
45 backoff: BackoffStrategy,
47}
48
49impl Default for RetryPolicy {
50 fn default() -> Self {
51 Self {
52 max_attempts: 3,
53 backoff: BackoffStrategy::Exponential {
54 base: Duration::from_millis(500),
55 max: Duration::from_secs(30),
56 },
57 }
58 }
59}
60
61impl RetryPolicy {
62 pub fn new(max_attempts: u32, backoff: BackoffStrategy) -> Self {
63 Self {
64 max_attempts,
65 backoff,
66 }
67 }
68
69 pub fn max_attempts(&self) -> u32 {
71 self.max_attempts
72 }
73
74 pub fn backoff(&self) -> &BackoffStrategy {
76 &self.backoff
77 }
78
79 pub async fn execute_with_retry(
84 &self,
85 tool_fn: &ToolFn,
86 args: &serde_json::Value,
87 ) -> ToolResult {
88 let mut last_result = tool_fn(args).await;
89 if last_result.is_ok() {
90 return last_result;
91 }
92
93 for attempt in 1..self.max_attempts {
94 match &last_result {
95 Err(e) if e.kind.is_retryable() => {}
96 _ => return last_result,
97 }
98
99 let delay = self.backoff.delay(attempt);
100 tracing::warn!(
101 attempt,
102 max = self.max_attempts,
103 delay_ms = delay.as_millis(),
104 "tool execution failed, retrying"
105 );
106 tokio::time::sleep(delay).await;
107
108 last_result = tool_fn(args).await;
109 if last_result.is_ok() {
110 return last_result;
111 }
112 }
113
114 last_result
115 }
116
117 pub async fn execute_with_retry_and_emission(
121 &self,
122 tool_fn: &ToolFn,
123 args: &serde_json::Value,
124 tx: &Sender<AgentEvent>,
125 tool_call_id: &str,
126 ) -> ToolResult {
127 let mut last_result = tool_fn(args).await;
128 if last_result.is_ok() {
129 return last_result;
130 }
131
132 for attempt in 1..self.max_attempts {
133 let reason = match &last_result {
134 Err(e) if e.kind.is_retryable() => format!("[{}] {}", e.kind, e.message),
135 _ => return last_result,
136 };
137 let _ = tx
138 .send(AgentEvent::Retry {
139 tool_call_id: tool_call_id.to_string(),
140 attempt: (attempt + 1) as usize,
141 max_attempts: self.max_attempts as usize,
142 reason: reason.clone(),
143 })
144 .await;
145
146 let delay = self.backoff.delay(attempt);
147 tracing::warn!(
148 attempt,
149 max = self.max_attempts,
150 delay_ms = delay.as_millis(),
151 "tool execution failed, retrying"
152 );
153 tokio::time::sleep(delay).await;
154
155 last_result = tool_fn(args).await;
156 if last_result.is_ok() {
157 return last_result;
158 }
159 }
160
161 last_result
162 }
163}