use std::sync::Arc;
use async_trait::async_trait;
use thiserror::Error;
use crate::composition::{CompositionExecutor, ExecutionContext, ToolExpr};
use crate::tools::{ToolCall, ToolResult, ToolSchema};
use super::result_handler::parse_tool_args_best_effort;
use super::ToolExecutionContext;
#[derive(Error, Debug, Clone)]
pub enum ToolError {
#[error("Tool not found: {0}")]
NotFound(String),
#[error("Execution failed: {0}")]
Execution(String),
#[error("Invalid arguments: {0}")]
InvalidArguments(String),
}
pub type Result<T> = std::result::Result<T, ToolError>;
#[async_trait]
pub trait ToolExecutor: Send + Sync {
async fn execute(&self, call: &ToolCall) -> Result<ToolResult>;
async fn execute_with_context(
&self,
call: &ToolCall,
_ctx: ToolExecutionContext<'_>,
) -> Result<ToolResult> {
self.execute(call).await
}
fn list_tools(&self) -> Vec<ToolSchema>;
fn tool_mutability(&self, tool_name: &str) -> crate::tools::ToolMutability {
crate::tools::classify_tool(tool_name)
}
fn call_mutability(&self, call: &ToolCall) -> crate::tools::ToolMutability {
self.tool_mutability(call.function.name.trim())
}
fn tool_concurrency_safe(&self, tool_name: &str) -> bool {
self.tool_mutability(tool_name) == crate::tools::ToolMutability::ReadOnly
}
fn call_concurrency_safe(&self, call: &ToolCall) -> bool {
self.tool_concurrency_safe(call.function.name.trim())
}
}
pub async fn execute_tool_call(
tool_call: &ToolCall,
tools: &dyn ToolExecutor,
composition_executor: Option<Arc<CompositionExecutor>>,
) -> Result<ToolResult> {
execute_tool_call_with_context(
tool_call,
tools,
composition_executor,
ToolExecutionContext::none(&tool_call.id),
)
.await
}
pub async fn execute_tool_call_with_context(
tool_call: &ToolCall,
tools: &dyn ToolExecutor,
composition_executor: Option<Arc<CompositionExecutor>>,
ctx: ToolExecutionContext<'_>,
) -> Result<ToolResult> {
if let Some(executor) = composition_executor {
let args_raw = tool_call.function.arguments.trim();
let (args, parse_warning) = parse_tool_args_best_effort(&tool_call.function.arguments);
if let Some(warning) = parse_warning {
tracing::warn!(
"Composition executor tool args fallback applied: tool_call_id={}, tool_name={}, args_len={}, warning={}",
tool_call.id,
tool_call.function.name,
args_raw.len(),
warning
);
}
let expr = ToolExpr::call(tool_call.function.name.clone(), args);
let mut exec_ctx = ExecutionContext::new();
match executor.execute(&expr, &mut exec_ctx).await {
Ok(result) => return Ok(result),
Err(ToolError::NotFound(_)) => {}
Err(error) => return Err(error),
}
}
tools.execute_with_context(tool_call, ctx).await
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use async_trait::async_trait;
use serde_json::json;
use crate::tools::{FunctionCall, Tool, ToolRegistry};
use super::*;
struct StaticExecutor {
results: HashMap<String, ToolResult>,
}
#[async_trait]
impl ToolExecutor for StaticExecutor {
async fn execute(&self, call: &ToolCall) -> Result<ToolResult> {
self.results
.get(&call.function.name)
.cloned()
.ok_or_else(|| ToolError::NotFound(call.function.name.clone()))
}
fn list_tools(&self) -> Vec<ToolSchema> {
Vec::new()
}
}
struct RegistryTool;
#[async_trait]
impl Tool for RegistryTool {
fn name(&self) -> &str {
"registry_tool"
}
fn description(&self) -> &str {
"registry tool"
}
fn parameters_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {}
})
}
async fn execute(
&self,
_args: serde_json::Value,
) -> std::result::Result<ToolResult, ToolError> {
Ok(ToolResult {
success: true,
result: "from-composition".to_string(),
display_preference: None,
})
}
}
fn make_tool_call(name: &str) -> ToolCall {
ToolCall {
id: "call_1".to_string(),
tool_type: "function".to_string(),
function: FunctionCall {
name: name.to_string(),
arguments: "{}".to_string(),
},
}
}
#[tokio::test]
async fn execute_tool_call_falls_back_when_composition_misses_tool() {
let mut results = HashMap::new();
results.insert(
"fallback_tool".to_string(),
ToolResult {
success: true,
result: "from-fallback".to_string(),
display_preference: None,
},
);
let tools = StaticExecutor { results };
let composition_executor =
Arc::new(CompositionExecutor::new(Arc::new(ToolRegistry::new())));
let tool_call = make_tool_call("fallback_tool");
let result = execute_tool_call(&tool_call, &tools, Some(composition_executor))
.await
.expect("fallback execution should succeed");
assert_eq!(result.result, "from-fallback");
}
#[tokio::test]
async fn execute_tool_call_uses_composition_when_available() {
let registry = Arc::new(ToolRegistry::new());
registry.register(RegistryTool).expect("register tool");
let tools = StaticExecutor {
results: HashMap::new(),
};
let composition_executor = Arc::new(CompositionExecutor::new(registry));
let tool_call = make_tool_call("registry_tool");
let result = execute_tool_call(&tool_call, &tools, Some(composition_executor))
.await
.expect("composition execution should succeed");
assert_eq!(result.result, "from-composition");
}
}