Skip to main content

lellm_agent/runtime/
retry.rs

1//! 工具重试策略 — 瞬时故障恢复("再试一次")。
2//!
3//! 位于 ToolExecutor 内部,负责 transient failure recovery。
4//! 重试耗尽后,错误向上传播至 FallbackStrategy("换条路走")。
5
6use std::time::Duration;
7
8use lellm_core::ToolResult;
9
10use super::event::AgentEvent;
11use super::tools::ToolFn;
12use tokio::sync::mpsc::Sender;
13
14/// 退避策略
15#[derive(Debug, Clone)]
16pub enum BackoffStrategy {
17    /// 固定间隔
18    Fixed(Duration),
19    /// 指数退避
20    Exponential { base: Duration, max: Duration },
21}
22
23impl BackoffStrategy {
24    /// 计算第 attempt 次的退避时间
25    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/// 重试策略配置。
37///
38/// `max_attempts` 表示**总尝试次数**(初始执行 + 重试),与主流 SDK 语义一致:
39/// - `max_attempts = 1` → 不重试,只执行一次
40/// - `max_attempts = 3` → 初始执行 + 最多 2 次重试
41#[derive(Debug, Clone)]
42pub struct RetryPolicy {
43    /// 总尝试次数(初始 + 重试),默认 3
44    max_attempts: u32,
45    /// 退避策略
46    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    /// 获取总尝试次数。
70    pub fn max_attempts(&self) -> u32 {
71        self.max_attempts
72    }
73
74    /// 获取退避策略引用。
75    pub fn backoff(&self) -> &BackoffStrategy {
76        &self.backoff
77    }
78
79    /// 执行工具函数并自动重试可重试的错误。
80    ///
81    /// `max_attempts` = 总尝试次数(初始执行 + 重试),与 AWS SDK / reqwest 等语义一致。
82    /// 执行链:`ToolUseLoop → ToolExecutor → RetryPolicy → tool_fn()`
83    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    /// 执行工具函数并自动重试可重试的错误,同时发射 Retry 事件。
118    ///
119    /// 与 [`execute_with_retry`] 的区别:每次重试前通过 `tx` 发射 `AgentEvent::Retry`。
120    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}