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 async fn execute_with_retry(
74 &self,
75 tool_fn: &ToolFn,
76 args: &serde_json::Value,
77 ) -> ToolResult {
78 let mut last_result = tool_fn(args).await;
79 if last_result.is_ok() {
80 return last_result;
81 }
82
83 for attempt in 1..self.max_attempts {
84 match &last_result {
85 Err(e) if e.kind.is_retryable() => {}
86 _ => return last_result,
87 }
88
89 let delay = self.backoff.delay(attempt);
90 tracing::warn!(
91 attempt,
92 max = self.max_attempts,
93 delay_ms = delay.as_millis(),
94 "tool execution failed, retrying"
95 );
96 tokio::time::sleep(delay).await;
97
98 last_result = tool_fn(args).await;
99 if last_result.is_ok() {
100 return last_result;
101 }
102 }
103
104 last_result
105 }
106
107 pub async fn execute_with_retry_and_emission(
111 &self,
112 tool_fn: &ToolFn,
113 args: &serde_json::Value,
114 tx: &Sender<AgentEvent>,
115 tool_call_id: &str,
116 ) -> ToolResult {
117 let mut last_result = tool_fn(args).await;
118 if last_result.is_ok() {
119 return last_result;
120 }
121
122 for attempt in 1..self.max_attempts {
123 let reason = match &last_result {
124 Err(e) if e.kind.is_retryable() => format!("[{}] {}", e.kind, e.message),
125 _ => return last_result,
126 };
127 let _ = tx
128 .send(AgentEvent::Retry {
129 tool_call_id: tool_call_id.to_string(),
130 attempt: (attempt + 1) as usize,
131 max_attempts: self.max_attempts as usize,
132 reason: reason.clone(),
133 })
134 .await;
135
136 let delay = self.backoff.delay(attempt);
137 tracing::warn!(
138 attempt,
139 max = self.max_attempts,
140 delay_ms = delay.as_millis(),
141 "tool execution failed, retrying"
142 );
143 tokio::time::sleep(delay).await;
144
145 last_result = tool_fn(args).await;
146 if last_result.is_ok() {
147 return last_result;
148 }
149 }
150
151 last_result
152 }
153}