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    ///
71    /// `max_attempts` = 总尝试次数(初始执行 + 重试),与 AWS SDK / reqwest 等语义一致。
72    /// 执行链:`ToolUseLoop → ToolExecutor → RetryPolicy → tool_fn()`
73    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    /// 执行工具函数并自动重试可重试的错误,同时发射 Retry 事件。
108    ///
109    /// 与 [`execute_with_retry`] 的区别:每次重试前通过 `tx` 发射 `AgentEvent::Retry`。
110    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}