bamboo_agent_core/tools/
executor.rs1use std::sync::Arc;
7
8use async_trait::async_trait;
9use thiserror::Error;
10
11use crate::composition::{CompositionExecutor, ExecutionContext, ToolExpr};
12use crate::tools::{ToolCall, ToolResult, ToolSchema};
13
14use super::result_handler::parse_tool_args_best_effort;
15use super::ToolExecutionContext;
16
17#[derive(Error, Debug, Clone)]
19pub enum ToolError {
20 #[error("Tool not found: {0}")]
22 NotFound(String),
23
24 #[error("Execution failed: {0}")]
26 Execution(String),
27
28 #[error("Invalid arguments: {0}")]
30 InvalidArguments(String),
31}
32
33pub type Result<T> = std::result::Result<T, ToolError>;
35
36#[async_trait]
66pub trait ToolExecutor: Send + Sync {
67 async fn execute(&self, call: &ToolCall) -> Result<ToolResult>;
77
78 async fn execute_with_context(
83 &self,
84 call: &ToolCall,
85 _ctx: ToolExecutionContext<'_>,
86 ) -> Result<ToolResult> {
87 self.execute(call).await
88 }
89
90 fn list_tools(&self) -> Vec<ToolSchema>;
94
95 fn tool_mutability(&self, tool_name: &str) -> crate::tools::ToolMutability {
98 crate::tools::classify_tool(tool_name)
99 }
100
101 fn call_mutability(&self, call: &ToolCall) -> crate::tools::ToolMutability {
104 self.tool_mutability(call.function.name.trim())
105 }
106
107 fn tool_concurrency_safe(&self, tool_name: &str) -> bool {
111 self.tool_mutability(tool_name) == crate::tools::ToolMutability::ReadOnly
112 }
113
114 fn call_concurrency_safe(&self, call: &ToolCall) -> bool {
117 self.tool_concurrency_safe(call.function.name.trim())
118 }
119}
120
121pub async fn execute_tool_call(
154 tool_call: &ToolCall,
155 tools: &dyn ToolExecutor,
156 composition_executor: Option<Arc<CompositionExecutor>>,
157) -> Result<ToolResult> {
158 execute_tool_call_with_context(
159 tool_call,
160 tools,
161 composition_executor,
162 ToolExecutionContext::none(&tool_call.id),
163 )
164 .await
165}
166
167pub async fn execute_tool_call_with_context(
169 tool_call: &ToolCall,
170 tools: &dyn ToolExecutor,
171 composition_executor: Option<Arc<CompositionExecutor>>,
172 ctx: ToolExecutionContext<'_>,
173) -> Result<ToolResult> {
174 if let Some(executor) = composition_executor {
175 let args_raw = tool_call.function.arguments.trim();
176 let (args, parse_warning) = parse_tool_args_best_effort(&tool_call.function.arguments);
177 if let Some(warning) = parse_warning {
178 tracing::warn!(
179 "Composition executor tool args fallback applied: tool_call_id={}, tool_name={}, args_len={}, warning={}",
180 tool_call.id,
181 tool_call.function.name,
182 args_raw.len(),
183 warning
184 );
185 }
186 let expr = ToolExpr::call(tool_call.function.name.clone(), args);
187 let mut exec_ctx = ExecutionContext::new();
188
189 match executor.execute(&expr, &mut exec_ctx).await {
190 Ok(result) => return Ok(result),
191 Err(ToolError::NotFound(_)) => {}
192 Err(error) => return Err(error),
193 }
194 }
195
196 tools.execute_with_context(tool_call, ctx).await
197}
198
199#[cfg(test)]
200mod tests {
201 use std::collections::HashMap;
202
203 use async_trait::async_trait;
204 use serde_json::json;
205
206 use crate::tools::{FunctionCall, Tool, ToolRegistry};
207
208 use super::*;
209
210 struct StaticExecutor {
211 results: HashMap<String, ToolResult>,
212 }
213
214 #[async_trait]
215 impl ToolExecutor for StaticExecutor {
216 async fn execute(&self, call: &ToolCall) -> Result<ToolResult> {
217 self.results
218 .get(&call.function.name)
219 .cloned()
220 .ok_or_else(|| ToolError::NotFound(call.function.name.clone()))
221 }
222
223 fn list_tools(&self) -> Vec<ToolSchema> {
224 Vec::new()
225 }
226 }
227
228 struct RegistryTool;
229
230 #[async_trait]
231 impl Tool for RegistryTool {
232 fn name(&self) -> &str {
233 "registry_tool"
234 }
235
236 fn description(&self) -> &str {
237 "registry tool"
238 }
239
240 fn parameters_schema(&self) -> serde_json::Value {
241 json!({
242 "type": "object",
243 "properties": {}
244 })
245 }
246
247 async fn execute(
248 &self,
249 _args: serde_json::Value,
250 ) -> std::result::Result<ToolResult, ToolError> {
251 Ok(ToolResult {
252 success: true,
253 result: "from-composition".to_string(),
254 display_preference: None,
255 })
256 }
257 }
258
259 fn make_tool_call(name: &str) -> ToolCall {
260 ToolCall {
261 id: "call_1".to_string(),
262 tool_type: "function".to_string(),
263 function: FunctionCall {
264 name: name.to_string(),
265 arguments: "{}".to_string(),
266 },
267 }
268 }
269
270 #[tokio::test]
271 async fn execute_tool_call_falls_back_when_composition_misses_tool() {
272 let mut results = HashMap::new();
273 results.insert(
274 "fallback_tool".to_string(),
275 ToolResult {
276 success: true,
277 result: "from-fallback".to_string(),
278 display_preference: None,
279 },
280 );
281
282 let tools = StaticExecutor { results };
283 let composition_executor =
284 Arc::new(CompositionExecutor::new(Arc::new(ToolRegistry::new())));
285 let tool_call = make_tool_call("fallback_tool");
286
287 let result = execute_tool_call(&tool_call, &tools, Some(composition_executor))
288 .await
289 .expect("fallback execution should succeed");
290
291 assert_eq!(result.result, "from-fallback");
292 }
293
294 #[tokio::test]
295 async fn execute_tool_call_uses_composition_when_available() {
296 let registry = Arc::new(ToolRegistry::new());
297 registry.register(RegistryTool).expect("register tool");
298
299 let tools = StaticExecutor {
300 results: HashMap::new(),
301 };
302 let composition_executor = Arc::new(CompositionExecutor::new(registry));
303 let tool_call = make_tool_call("registry_tool");
304
305 let result = execute_tool_call(&tool_call, &tools, Some(composition_executor))
306 .await
307 .expect("composition execution should succeed");
308
309 assert_eq!(result.result, "from-composition");
310 }
311}