lellm_agent/runtime/tools/
executor.rs1use 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#[derive(Debug)]
24pub struct BatchExecutionResult {
25 pub results: Vec<Message>,
27 pub panicked: bool,
29}
30
31#[derive(Clone)]
36pub struct ToolExecutor {
37 catalog: Arc<dyn ToolCatalog>,
38 retry_policy: RetryPolicy,
39}
40
41impl ToolExecutor {
42 pub fn new(catalog: Arc<dyn ToolCatalog>) -> Self {
44 Self {
45 catalog,
46 retry_policy: RetryPolicy::default(),
47 }
48 }
49
50 pub fn with_catalog(catalog: Arc<dyn ToolCatalog>) -> Self {
52 Self::new(catalog)
53 }
54
55 pub fn with_retry_policy(catalog: Arc<dyn ToolCatalog>, policy: RetryPolicy) -> Self {
57 Self {
58 catalog,
59 retry_policy: policy,
60 }
61 }
62
63 pub fn set_retry_policy(&mut self, policy: RetryPolicy) {
65 self.retry_policy = policy;
66 }
67
68 pub fn retry_policy(&self) -> RetryPolicy {
70 self.retry_policy.clone()
71 }
72
73 pub async fn snapshot(&self) -> Arc<ToolSnapshot> {
77 self.catalog.snapshot().await
78 }
79
80 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 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
118pub 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 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 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 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 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 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 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
252impl ToolSnapshot {
255 pub fn clone_for_spawn(&self) -> Arc<indexmap::IndexMap<String, ExecutableTool>> {
257 self.tools.clone()
258 }
259}
260
261async 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
307async 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
330fn tool_fn_from_reg(entry: &ExecutableTool) -> ToolFn {
333 let entry = entry.clone();
334 Arc::new(move |args: &serde_json::Value| entry.execute(args))
335}