Skip to main content

opendev_repl/
tool_executor.rs

1//! Execute tools and format results.
2//!
3//! Mirrors `opendev/repl/tool_executor.py`.
4
5use std::collections::HashMap;
6use std::time::Instant;
7
8use futures::future::join_all;
9use serde_json::Value;
10use tracing::{debug, info, warn};
11
12use opendev_tools_core::parallel::{ParallelPolicy, ToolCall as PolicyToolCall};
13use opendev_tools_core::{ToolContext, ToolRegistry, ToolResult};
14
15use crate::error::ReplError;
16
17/// Result of a tool execution with timing metadata.
18#[derive(Debug, Clone)]
19pub struct ToolExecutionResult {
20    /// Tool name that was executed.
21    pub tool_name: String,
22    /// Whether the tool succeeded.
23    pub success: bool,
24    /// Tool output (on success).
25    pub output: Option<String>,
26    /// Error message (on failure).
27    pub error: Option<String>,
28    /// Execution duration in milliseconds.
29    pub duration_ms: u64,
30}
31
32/// Handles tool execution with approval, undo, and result formatting.
33pub struct ToolExecutor {
34    /// Number of tools executed in this session.
35    execution_count: u64,
36}
37
38impl ToolExecutor {
39    /// Create a new tool executor.
40    pub fn new() -> Self {
41        Self { execution_count: 0 }
42    }
43
44    /// Execute a single tool call.
45    ///
46    /// Parses the tool call JSON, dispatches via the registry,
47    /// and returns a formatted result.
48    pub async fn execute(
49        &mut self,
50        tool_call: &Value,
51        registry: &ToolRegistry,
52        context: &ToolContext,
53    ) -> Result<ToolExecutionResult, ReplError> {
54        let tool_name = tool_call["function"]["name"]
55            .as_str()
56            .unwrap_or("unknown")
57            .to_string();
58
59        self.execution_count += 1;
60        info!(
61            tool = %tool_name,
62            execution_num = self.execution_count,
63            "Executing tool"
64        );
65
66        let start = Instant::now();
67
68        // Parse arguments to HashMap<String, Value>
69        let args_value = &tool_call["function"]["arguments"];
70        let args: HashMap<String, Value> = if let Some(args_str) = args_value.as_str() {
71            serde_json::from_str(args_str).unwrap_or_default()
72        } else if let Some(obj) = args_value.as_object() {
73            obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
74        } else {
75            HashMap::new()
76        };
77
78        // Execute via registry (returns ToolResult directly)
79        let result: ToolResult = registry.execute(&tool_name, args, context).await;
80        let duration_ms = start.elapsed().as_millis() as u64;
81
82        if result.success {
83            debug!(
84                tool = %tool_name,
85                duration_ms,
86                "Tool execution succeeded"
87            );
88        } else {
89            warn!(
90                tool = %tool_name,
91                error = ?result.error,
92                duration_ms,
93                "Tool execution failed"
94            );
95        }
96
97        Ok(ToolExecutionResult {
98            tool_name,
99            success: result.success,
100            output: result.output,
101            error: result.error,
102            duration_ms,
103        })
104    }
105
106    /// Execute multiple tool calls, potentially in parallel for read-only tools.
107    ///
108    /// Uses [`ParallelPolicy`] to partition tool calls into execution groups.
109    /// Read-only tools within the same group run concurrently via `join_all`.
110    /// Groups are executed in order to preserve write-after-read semantics.
111    pub async fn execute_batch(
112        &mut self,
113        tool_calls: &[Value],
114        registry: &ToolRegistry,
115        context: &ToolContext,
116    ) -> Vec<Result<ToolExecutionResult, ReplError>> {
117        if tool_calls.is_empty() {
118            return vec![];
119        }
120
121        // Build PolicyToolCall descriptors for partitioning.
122        let policy_calls: Vec<PolicyToolCall> = tool_calls
123            .iter()
124            .map(|tc| {
125                let name = tc["function"]["name"]
126                    .as_str()
127                    .unwrap_or("unknown")
128                    .to_string();
129                let arguments = tc["function"]["arguments"].clone();
130                let args_val = if let Some(s) = arguments.as_str() {
131                    serde_json::from_str(s).unwrap_or(Value::Object(Default::default()))
132                } else {
133                    arguments
134                };
135                PolicyToolCall::new(name, args_val)
136            })
137            .collect();
138
139        let groups = ParallelPolicy::partition(&policy_calls);
140
141        // Pre-allocate result slots (filled out-of-order for parallel groups).
142        let mut results: Vec<Option<Result<ToolExecutionResult, ReplError>>> =
143            (0..tool_calls.len()).map(|_| None).collect();
144
145        for group in &groups {
146            if group.len() == 1 {
147                // Single tool -- run sequentially (avoids spawn overhead).
148                let idx = group[0];
149                self.execution_count += 1;
150                let res = self
151                    .execute_single(&tool_calls[idx], registry, context)
152                    .await;
153                results[idx] = Some(res);
154            } else {
155                // Multiple tools in this group -- run concurrently.
156                let futs: Vec<_> = group
157                    .iter()
158                    .map(|&idx| {
159                        let tc = &tool_calls[idx];
160                        Self::execute_standalone(tc, registry, context)
161                    })
162                    .collect();
163
164                let group_results = join_all(futs).await;
165                for (&idx, res) in group.iter().zip(group_results) {
166                    self.execution_count += 1;
167                    results[idx] = Some(res);
168                }
169            }
170        }
171
172        // Unwrap Option wrappers (all slots should be filled).
173        results
174            .into_iter()
175            .enumerate()
176            .map(|(i, opt)| {
177                opt.unwrap_or_else(|| {
178                    Ok(ToolExecutionResult {
179                        tool_name: format!("tool_{}", i),
180                        success: false,
181                        output: None,
182                        error: Some("tool was not scheduled for execution".to_string()),
183                        duration_ms: 0,
184                    })
185                })
186            })
187            .collect()
188    }
189
190    /// Execute a single tool call (updates internal execution_count).
191    async fn execute_single(
192        &mut self,
193        tool_call: &Value,
194        registry: &ToolRegistry,
195        context: &ToolContext,
196    ) -> Result<ToolExecutionResult, ReplError> {
197        self.execute(tool_call, registry, context).await
198    }
199
200    /// Execute a tool call without borrowing &mut self (for parallel use).
201    async fn execute_standalone(
202        tool_call: &Value,
203        registry: &ToolRegistry,
204        context: &ToolContext,
205    ) -> Result<ToolExecutionResult, ReplError> {
206        let tool_name = tool_call["function"]["name"]
207            .as_str()
208            .unwrap_or("unknown")
209            .to_string();
210
211        let start = Instant::now();
212
213        let args_value = &tool_call["function"]["arguments"];
214        let args: HashMap<String, Value> = if let Some(args_str) = args_value.as_str() {
215            serde_json::from_str(args_str).unwrap_or_default()
216        } else if let Some(obj) = args_value.as_object() {
217            obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
218        } else {
219            HashMap::new()
220        };
221
222        let result: ToolResult = registry.execute(&tool_name, args, context).await;
223        let duration_ms = start.elapsed().as_millis() as u64;
224
225        if result.success {
226            debug!(tool = %tool_name, duration_ms, "Tool execution succeeded (parallel)");
227        } else {
228            warn!(tool = %tool_name, error = ?result.error, duration_ms, "Tool execution failed (parallel)");
229        }
230
231        Ok(ToolExecutionResult {
232            tool_name,
233            success: result.success,
234            output: result.output,
235            error: result.error,
236            duration_ms,
237        })
238    }
239
240    /// Format a tool execution result for display.
241    pub fn format_result(result: &ToolExecutionResult) -> String {
242        if result.success {
243            format!(
244                "  {} ({}ms)\n{}",
245                result.tool_name,
246                result.duration_ms,
247                result.output.as_deref().unwrap_or("")
248            )
249        } else {
250            format!(
251                "  {} FAILED ({}ms)\n  Error: {}",
252                result.tool_name,
253                result.duration_ms,
254                result.error.as_deref().unwrap_or("unknown error")
255            )
256        }
257    }
258}
259
260impl Default for ToolExecutor {
261    fn default() -> Self {
262        Self::new()
263    }
264}
265
266#[cfg(test)]
267#[path = "tool_executor_tests.rs"]
268mod tests;