Skip to main content

lellm_agent/runtime/tools/
executor.rs

1//! 工具执行器 — 注册、分派、批量执行、并行安全分级。
2//!
3//! 通过 `ToolCatalog` 消费工具快照,不持有工具所有权。
4
5use std::sync::Arc;
6
7use lellm_core::{
8    Message, ParallelSafety, ToolCall, ToolCategory, ToolError, ToolErrorKind, ToolResult,
9};
10
11use super::super::event::AgentEvent;
12use super::super::retry::RetryPolicy;
13use super::{ExecutableTool, ToolCatalog, ToolFn, ToolSnapshot};
14use tokio::sync::mpsc::Sender;
15
16/// 批量执行结果 — 长度、顺序、完整性三重保证。
17///
18/// **不变量:**
19/// 1. `results.len() == calls.len()` 永远成立
20/// 2. `results[i]` 对应 `calls[i]` 的执行结果(原始顺序)
21/// 3. panic 永远被转换成 `ToolResult(is_error: true)`,不会丢失
22/// 4. `panicked` 仅作为观测信号,不改变结果完整性
23#[derive(Debug)]
24pub struct BatchExecutionResult {
25    /// 按原始调用顺序排列的工具结果,长度等于输入 calls 长度。
26    pub results: Vec<Message>,
27    /// 是否有任意 spawned task panic(仅作为观测信号)
28    pub panicked: bool,
29}
30
31/// 工具执行器 — 按名称分派 ToolCall 到实际工具函数。
32///
33/// 内部持有 `ToolCatalog`,通过 `snapshot()` 获取冻结工具快照。
34/// Clone 为 O(1)(Arc 浅拷贝)。
35#[derive(Clone)]
36pub struct ToolExecutor {
37    catalog: Arc<dyn ToolCatalog>,
38    retry_policy: RetryPolicy,
39}
40
41impl ToolExecutor {
42    /// 绑定工具目录。
43    pub fn new(catalog: Arc<dyn ToolCatalog>) -> Self {
44        Self {
45            catalog,
46            retry_policy: RetryPolicy::default(),
47        }
48    }
49
50    /// 绑定工具目录,使用默认重试策略。
51    pub fn with_catalog(catalog: Arc<dyn ToolCatalog>) -> Self {
52        Self::new(catalog)
53    }
54
55    /// 构造时绑定全局重试策略。
56    pub fn with_retry_policy(catalog: Arc<dyn ToolCatalog>, policy: RetryPolicy) -> Self {
57        Self {
58            catalog,
59            retry_policy: policy,
60        }
61    }
62
63    /// 设置/替换重试策略。
64    pub fn set_retry_policy(&mut self, policy: RetryPolicy) {
65        self.retry_policy = policy;
66    }
67
68    /// 获取当前重试策略的克隆。
69    pub fn retry_policy(&self) -> RetryPolicy {
70        self.retry_policy.clone()
71    }
72
73    /// 获取冻结工具快照。
74    ///
75    /// 每轮迭代调用一次,固定本轮工具集。
76    pub async fn snapshot(&self) -> Arc<ToolSnapshot> {
77        self.catalog.snapshot().await
78    }
79
80    /// 执行单个工具调用,自带重试。
81    ///
82    /// 使用预解析的快照执行。
83    pub async fn execute_with_snapshot(
84        &self,
85        call: &ToolCall,
86        snapshot: &ToolSnapshot,
87    ) -> ToolResult {
88        match snapshot.get(&call.name) {
89            Some(entry) => {
90                let tool_fn = tool_fn_from_reg(entry);
91                self.retry_policy
92                    .execute_with_retry(&tool_fn, &call.arguments)
93                    .await
94            }
95            None => Err(ToolError::not_found(format!("unknown tool: {}", call.name))),
96        }
97    }
98
99    /// 执行单个工具调用,自带重试 + Retry 事件发射。
100    pub async fn execute_with_emission(
101        &self,
102        call: &ToolCall,
103        snapshot: &ToolSnapshot,
104        tx: &Sender<AgentEvent>,
105    ) -> ToolResult {
106        match snapshot.get(&call.name) {
107            Some(entry) => {
108                let tool_fn = tool_fn_from_reg(entry);
109                self.retry_policy
110                    .execute_with_retry_and_emission(&tool_fn, &call.arguments, tx, &call.id)
111                    .await
112            }
113            None => Err(ToolError::not_found(format!("unknown tool: {}", call.name))),
114        }
115    }
116}
117
118/// 使用预解析的快照批量执行 tool_calls。
119///
120/// 这是动态目录模式的核心执行函数。
121///
122/// # 用法
123///
124/// ```ignore
125/// let snapshot = executor.snapshot().await;
126/// let result = execute_batch_with(&tool_calls, &snapshot, &executor.retry_policy()).await;
127/// ```
128///
129/// # ParallelSafety 契约
130///
131/// - `Safe`: 全并发(每个 tool 独立 spawn)
132/// - `CategoryExclusive(cat)`: 组内串行,组间并发
133/// - `Exclusive`: 全串行
134///
135/// # 一致性保证
136///
137/// `snapshot` 快照在函数执行期间固定不变,不会因目录刷新而漂移。
138pub async fn execute_batch_with(
139    calls: &[ToolCall],
140    snapshot: &ToolSnapshot,
141    retry_policy: &RetryPolicy,
142) -> BatchExecutionResult {
143    if calls.is_empty() {
144        return BatchExecutionResult {
145            results: Vec::new(),
146            panicked: false,
147        };
148    }
149
150    // 分组时保留原始索引
151    let mut safe_calls: Vec<(usize, ToolCall)> = Vec::new();
152    let mut category_calls: std::collections::HashMap<ToolCategory, Vec<(usize, ToolCall)>> =
153        std::collections::HashMap::new();
154    let mut exclusive_calls: Vec<(usize, ToolCall)> = Vec::new();
155
156    for (idx, call) in calls.iter().enumerate() {
157        let safety = snapshot
158            .get(&call.name)
159            .map(|t| t.safety.clone())
160            .unwrap_or(ParallelSafety::Exclusive);
161
162        match safety {
163            ParallelSafety::Safe => safe_calls.push((idx, call.clone())),
164            ParallelSafety::CategoryExclusive => {
165                if let Some(cat) = snapshot.get(&call.name).and_then(|t| t.category.clone()) {
166                    category_calls
167                        .entry(cat)
168                        .or_default()
169                        .push((idx, call.clone()));
170                } else {
171                    exclusive_calls.push((idx, call.clone()));
172                }
173            }
174            ParallelSafety::Exclusive => exclusive_calls.push((idx, call.clone())),
175        }
176    }
177
178    // 构建 group handles
179    let mut group_handles: Vec<tokio::task::JoinHandle<Vec<(usize, Message)>>> = Vec::new();
180    let mut group_indices: Vec<Vec<usize>> = Vec::new();
181
182    let snapshot = Arc::new(snapshot.clone_for_spawn());
183    let retry_policy = retry_policy.clone();
184
185    // Safe: 每个 tool 独立 spawn(全并发)
186    if !safe_calls.is_empty() {
187        let s = Arc::clone(&snapshot);
188        let rp = retry_policy.clone();
189        let indices: Vec<usize> = safe_calls.iter().map(|(i, _)| *i).collect();
190        group_handles.push(tokio::spawn(async move {
191            run_parallel_indexed_with(&s, &rp, safe_calls).await
192        }));
193        group_indices.push(indices);
194    }
195
196    // CategoryExclusive: 按 category 分组,组内串行、组间并发
197    for group_calls in category_calls.into_values() {
198        let s = Arc::clone(&snapshot);
199        let rp = retry_policy.clone();
200        let indices: Vec<usize> = group_calls.iter().map(|(i, _)| *i).collect();
201        group_handles.push(tokio::spawn(async move {
202            run_serial_indexed_with(&s, &rp, group_calls).await
203        }));
204        group_indices.push(indices);
205    }
206
207    // Exclusive: 全部串行,一个 task
208    if !exclusive_calls.is_empty() {
209        let s = Arc::clone(&snapshot);
210        let rp = retry_policy.clone();
211        let indices: Vec<usize> = exclusive_calls.iter().map(|(i, _)| *i).collect();
212        group_handles.push(tokio::spawn(async move {
213            run_serial_indexed_with(&s, &rp, exclusive_calls).await
214        }));
215        group_indices.push(indices);
216    }
217
218    // 按原始索引回填结果;panic 的 group 按索引列表精准回填错误
219    let mut results: Vec<Option<Message>> = vec![None; calls.len()];
220    let mut panicked = false;
221    let all_handles = futures_util::future::join_all(group_handles).await;
222
223    for (handle_result, indices) in all_handles.into_iter().zip(group_indices.into_iter()) {
224        match handle_result {
225            Ok(indexed_messages) => {
226                for (idx, msg) in indexed_messages {
227                    results[idx] = Some(msg);
228                }
229            }
230            Err(join_err) => {
231                panicked = true;
232                for idx in indices {
233                    let call = &calls[idx];
234                    results[idx] = Some(Message::tool_result(
235                        call,
236                        &Err(ToolError {
237                            kind: ToolErrorKind::Internal,
238                            message: format!("tool group task panicked: {join_err}"),
239                        }),
240                    ));
241                }
242            }
243        }
244    }
245
246    BatchExecutionResult {
247        results: results.into_iter().flatten().collect(),
248        panicked,
249    }
250}
251
252// ─── 内部辅助:快照克隆 ──────────────────────────────────────────
253
254impl ToolSnapshot {
255    /// 克隆内部工具映射,供 spawn 使用。
256    pub fn clone_for_spawn(&self) -> Arc<indexmap::IndexMap<String, ExecutableTool>> {
257        self.tools.clone()
258    }
259}
260
261// ─── Safe group: 全并发 ──────────────────────────────────────────
262
263async fn run_parallel_indexed_with(
264    tools: &Arc<indexmap::IndexMap<String, ExecutableTool>>,
265    retry_policy: &RetryPolicy,
266    calls: Vec<(usize, ToolCall)>,
267) -> Vec<(usize, Message)> {
268    let handles: Vec<_> = calls
269        .iter()
270        .map(|(idx, call)| {
271            let tools = Arc::clone(tools);
272            let rp = retry_policy.clone();
273            let call = call.clone();
274            let idx = *idx;
275            tokio::spawn(async move {
276                let result = match tools.get(&call.name) {
277                    Some(entry) => {
278                        let tool_fn = tool_fn_from_reg(entry);
279                        rp.execute_with_retry(&tool_fn, &call.arguments).await
280                    }
281                    None => Err(ToolError::not_found(format!("unknown tool: {}", call.name))),
282                };
283                (idx, Message::tool_result(&call, &result))
284            })
285        })
286        .collect();
287
288    let raw = futures_util::future::join_all(handles).await;
289    raw.into_iter()
290        .zip(calls.into_iter())
291        .map(|(h, (idx, call))| match h {
292            Ok((_, msg)) => (idx, msg),
293            Err(join_err) => (
294                idx,
295                Message::tool_result(
296                    &call,
297                    &Err(ToolError {
298                        kind: ToolErrorKind::Internal,
299                        message: format!("tool '{}' task panicked: {join_err}", call.name),
300                    }),
301                ),
302            ),
303        })
304        .collect()
305}
306
307// ─── 组内串行 ────────────────────────────────────────────────────
308
309async fn run_serial_indexed_with(
310    tools: &Arc<indexmap::IndexMap<String, ExecutableTool>>,
311    retry_policy: &RetryPolicy,
312    calls: Vec<(usize, ToolCall)>,
313) -> Vec<(usize, Message)> {
314    let mut results = Vec::with_capacity(calls.len());
315    for (idx, call) in calls {
316        let exec_result = match tools.get(&call.name) {
317            Some(entry) => {
318                let tool_fn = tool_fn_from_reg(entry);
319                retry_policy
320                    .execute_with_retry(&tool_fn, &call.arguments)
321                    .await
322            }
323            None => Err(ToolError::not_found(format!("unknown tool: {}", call.name))),
324        };
325        results.push((idx, Message::tool_result(&call, &exec_result)));
326    }
327    results
328}
329
330// ─── Helper: 从 ExecutableTool 获取 ToolFn ──────────────────────
331
332fn tool_fn_from_reg(entry: &ExecutableTool) -> ToolFn {
333    let entry = entry.clone();
334    Arc::new(move |args: &serde_json::Value| entry.execute(args))
335}