use async_trait::async_trait;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use tracing::error;
use crate::background::BackgroundExecutableTool;
use crate::tool_types::{
BuiltinTool, DeferrablePolicy, ToolCall, ToolDefinition, ToolHints, ToolPolicy, ToolResult,
ToolResultImage,
};
use crate::{
tool_context::ToolContext, tool_context::ToolContextService, tool_context::ToolContextServices,
};
use crate::error::{AgentLoopError, Result};
use crate::tool_execution::ToolExecutor;
#[derive(Debug)]
pub enum ToolExecutionResult {
Success(Value),
SuccessWithImages {
result: Value,
images: Vec<ToolResultImage>,
},
ToolError(String),
InternalError(ToolInternalError),
ConnectionRequired {
provider: String,
},
}
impl ToolExecutionResult {
pub fn success(value: impl Into<Value>) -> Self {
ToolExecutionResult::Success(value.into())
}
pub fn success_with_raw_output(value: impl Into<Value>, raw_output: String) -> Self {
let mut value = value.into();
match value.as_object_mut() {
Some(obj)
if !obj.contains_key("_raw_output") && !obj.contains_key("_raw_output_scalar") =>
{
obj.insert("_raw_output".to_string(), Value::String(raw_output));
}
_ => {
value = serde_json::json!({
"_raw_output_scalar": value,
"_raw_output": raw_output,
});
}
}
ToolExecutionResult::Success(value)
}
pub fn success_with_images(value: impl Into<Value>, images: Vec<ToolResultImage>) -> Self {
ToolExecutionResult::SuccessWithImages {
result: value.into(),
images,
}
}
pub fn tool_error(message: impl Into<String>) -> Self {
ToolExecutionResult::ToolError(message.into())
}
pub fn internal_error(error: impl std::error::Error + Send + Sync + 'static) -> Self {
ToolExecutionResult::InternalError(ToolInternalError::new(error))
}
pub fn internal_error_msg(message: impl Into<String>) -> Self {
ToolExecutionResult::InternalError(ToolInternalError::from_message(message))
}
pub fn connection_required(provider: impl Into<String>) -> Self {
ToolExecutionResult::ConnectionRequired {
provider: provider.into(),
}
}
pub fn is_success(&self) -> bool {
matches!(
self,
ToolExecutionResult::Success(_) | ToolExecutionResult::SuccessWithImages { .. }
)
}
pub fn is_error(&self) -> bool {
matches!(
self,
ToolExecutionResult::ToolError(_) | ToolExecutionResult::InternalError(_)
)
}
pub fn is_connection_required(&self) -> bool {
matches!(self, ToolExecutionResult::ConnectionRequired { .. })
}
pub fn into_tool_result(self, tool_call_id: &str, tool_name: &str) -> ToolResult {
match self {
ToolExecutionResult::Success(mut value) => {
let raw_output = value
.as_object_mut()
.and_then(|obj| obj.remove("_raw_output"))
.and_then(|v| v.as_str().map(|s| s.to_string()));
let result_value = if let Some(obj) = value.as_object_mut() {
let is_scalar_carrier = raw_output.is_some()
&& obj.len() == 1
&& obj.contains_key("_raw_output_scalar");
if is_scalar_carrier {
obj.remove("_raw_output_scalar").unwrap_or(Value::Null)
} else {
value
}
} else {
value
};
ToolResult {
tool_call_id: tool_call_id.to_string(),
result: Some(result_value),
images: None,
error: None,
connection_required: None,
raw_output,
}
}
ToolExecutionResult::SuccessWithImages { result, images } => ToolResult {
tool_call_id: tool_call_id.to_string(),
result: Some(result),
images: if images.is_empty() {
None
} else {
Some(images)
},
error: None,
connection_required: None,
raw_output: None,
},
ToolExecutionResult::ToolError(message) => ToolResult {
tool_call_id: tool_call_id.to_string(),
result: Some(serde_json::json!({ "error": &message })),
images: None,
error: Some(message),
connection_required: None,
raw_output: None,
},
ToolExecutionResult::InternalError(err) => {
error!(
tool_name = %tool_name,
tool_call_id = %tool_call_id,
error = %err.message,
error_chain = %err.chain_string(),
"Tool internal error (details hidden from LLM)"
);
let generic_msg = "An internal error occurred while executing the tool";
ToolResult {
tool_call_id: tool_call_id.to_string(),
result: Some(serde_json::json!({
"error": generic_msg
})),
images: None,
error: Some(generic_msg.to_string()),
connection_required: None,
raw_output: None,
}
}
ToolExecutionResult::ConnectionRequired { ref provider } => ToolResult {
tool_call_id: tool_call_id.to_string(),
result: Some(serde_json::json!({
"connection_required": provider,
})),
images: None,
error: None,
connection_required: Some(provider.clone()),
raw_output: None,
},
}
}
}
#[derive(Debug)]
pub struct ToolInternalError {
pub message: String,
pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
impl ToolInternalError {
pub fn new(error: impl std::error::Error + Send + Sync + 'static) -> Self {
Self {
message: error.to_string(),
source: Some(Box::new(error)),
}
}
pub fn from_message(message: impl Into<String>) -> Self {
Self {
message: message.into(),
source: None,
}
}
pub fn chain_string(&self) -> String {
let mut parts = vec![self.message.clone()];
let mut current = <Self as std::error::Error>::source(self);
while let Some(source) = current {
let message = source.to_string();
if parts.last() != Some(&message) {
parts.push(message);
}
current = source.source();
}
parts.join(": ")
}
}
impl std::fmt::Display for ToolInternalError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ToolInternalError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_ref()
.map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CliSpelling {
pub root: &'static str,
pub script_parameter: &'static str,
}
impl CliSpelling {
pub const fn new(root: &'static str, script_parameter: &'static str) -> Self {
Self {
root,
script_parameter,
}
}
}
#[async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn display_name(&self) -> Option<&str> {
None
}
fn description(&self) -> &str;
fn parameters_schema(&self) -> Value;
async fn execute(&self, arguments: Value) -> ToolExecutionResult;
async fn execute_with_context(
&self,
arguments: Value,
_context: &ToolContext,
) -> ToolExecutionResult {
self.execute(arguments).await
}
fn cli_spelling(&self) -> Option<CliSpelling> {
None
}
fn requires_context(&self) -> bool {
false
}
fn required_context_services(&self) -> &'static [ToolContextService] {
&[]
}
fn policy(&self) -> ToolPolicy {
ToolPolicy::Auto
}
fn hints(&self) -> ToolHints {
ToolHints::default()
}
fn narrate(
&self,
_tool_call: &crate::tool_types::ToolCall,
_phase: crate::tool_narration::ToolNarrationPhase,
_locale: Option<&str>,
_ctx: crate::tool_narration::ToolNarrationContext<'_>,
) -> Option<String> {
None
}
fn as_background_executable(&self) -> Option<&dyn BackgroundExecutableTool> {
None
}
fn deferrable_policy(&self) -> DeferrablePolicy {
DeferrablePolicy::default()
}
fn to_definition(&self) -> ToolDefinition {
ToolDefinition::Builtin(BuiltinTool {
name: self.name().to_string(),
display_name: self.display_name().map(|s| s.to_string()),
description: self.description().to_string(),
parameters: self.parameters_schema(),
policy: self.policy(),
category: None,
deferrable: self.deferrable_policy(),
hints: self.hints(),
full_parameters: None,
})
}
}
#[derive(Default, Clone)]
pub struct ToolRegistry {
tools: HashMap<String, Arc<dyn Tool>>,
}
impl ToolRegistry {
pub fn new() -> Self {
Self {
tools: HashMap::new(),
}
}
pub fn with_defaults() -> Self {
use crate::progress_reporting::ReportProgressTool;
let builder = ToolRegistry::builder()
.tool(ReportProgressTool);
builder.build()
}
pub fn with_monitor_probe_defaults() -> Self {
Self::new()
}
pub fn register(&mut self, tool: impl Tool + 'static) {
self.tools.insert(tool.name().to_string(), Arc::new(tool));
}
pub fn register_boxed(&mut self, tool: Box<dyn Tool>) {
self.tools.insert(tool.name().to_string(), Arc::from(tool));
}
pub fn register_arc(&mut self, tool: Arc<dyn Tool>) {
self.tools.insert(tool.name().to_string(), tool);
}
pub fn get(&self, name: &str) -> Option<&Arc<dyn Tool>> {
self.tools.get(name)
}
pub fn has(&self, name: &str) -> bool {
self.tools.contains_key(name)
}
pub fn len(&self) -> usize {
self.tools.len()
}
pub fn is_empty(&self) -> bool {
self.tools.is_empty()
}
pub fn tool_names(&self) -> Vec<&str> {
self.tools.keys().map(|s| s.as_str()).collect()
}
pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
self.tools.values().map(|t| t.to_definition()).collect()
}
pub fn validate_context_services(&self, services: &ToolContextServices) -> Result<()> {
let mut tools: Vec<_> = self.tools.values().collect();
tools.sort_by_key(|tool| tool.name());
for tool in tools {
for service in tool.required_context_services() {
if !services.provides(*service) {
return Err(crate::error::AgentLoopError::config(format!(
"tool \"{}\" requires unavailable ToolContext service {}",
tool.name(),
service.name(),
)));
}
}
}
Ok(())
}
pub fn unregister(&mut self, name: &str) -> Option<Arc<dyn Tool>> {
self.tools.remove(name)
}
pub fn clear(&mut self) {
self.tools.clear();
}
pub fn builder() -> ToolRegistryBuilder {
ToolRegistryBuilder::new()
}
}
impl std::fmt::Debug for ToolRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ToolRegistry")
.field("tools", &self.tool_names())
.finish()
}
}
fn validate_tool_arguments(tool: &dyn Tool, tool_call: &ToolCall) -> Result<Option<String>> {
let arguments = tool_call.execution_arguments();
let definition = tool.to_definition();
let validator = jsonschema::validator_for(definition.parameters()).map_err(|error| {
AgentLoopError::config(format!(
"Tool '{}' has an invalid parameters schema: {error}",
tool_call.name
))
})?;
let issues: Vec<_> = validator
.iter_errors(&arguments)
.map(|error| {
serde_json::json!({
"instance_path": error.instance_path().to_string(),
"message": error.to_string(),
"schema_path": error.schema_path().to_string(),
})
})
.collect();
if issues.is_empty() {
return Ok(None);
}
Ok(Some(
serde_json::json!({
"code": "invalid_tool_arguments",
"tool": tool_call.name,
"issues": issues,
})
.to_string(),
))
}
#[async_trait]
impl ToolExecutor for ToolRegistry {
async fn execute(
&self,
tool_call: &ToolCall,
_tool_def: &ToolDefinition,
) -> Result<ToolResult> {
let tool = self.tools.get(&tool_call.name).ok_or_else(|| {
crate::error::AgentLoopError::tool(format!("Tool not found: {}", tool_call.name))
})?;
if let Some(error) = validate_tool_arguments(tool.as_ref(), tool_call)? {
return Ok(ToolExecutionResult::tool_error(error)
.into_tool_result(&tool_call.id, &tool_call.name));
}
let result = tool.execute(tool_call.execution_arguments()).await;
Ok(result.into_tool_result(&tool_call.id, &tool_call.name))
}
async fn execute_with_context(
&self,
tool_call: &ToolCall,
_tool_def: &ToolDefinition,
context: &ToolContext,
) -> Result<ToolResult> {
let tool = self.tools.get(&tool_call.name).ok_or_else(|| {
crate::error::AgentLoopError::tool(format!("Tool not found: {}", tool_call.name))
})?;
if let Some(error) = validate_tool_arguments(tool.as_ref(), tool_call)? {
return Ok(ToolExecutionResult::tool_error(error)
.into_tool_result(&tool_call.id, &tool_call.name));
}
let result = tool
.execute_with_context(tool_call.execution_arguments(), context)
.await;
Ok(result.into_tool_result(&tool_call.id, &tool_call.name))
}
}
pub struct ToolRegistryBuilder {
registry: ToolRegistry,
}
impl ToolRegistryBuilder {
pub fn new() -> Self {
Self {
registry: ToolRegistry::new(),
}
}
pub fn tool(mut self, tool: impl Tool + 'static) -> Self {
self.registry.register(tool);
self
}
pub fn tool_boxed(mut self, tool: Box<dyn Tool>) -> Self {
self.registry.register_boxed(tool);
self
}
pub fn tool_arc(mut self, tool: Arc<dyn Tool>) -> Self {
self.registry.register_arc(tool);
self
}
pub fn build(self) -> ToolRegistry {
self.registry
}
}
impl Default for ToolRegistryBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
pub struct EchoTool;
#[cfg(test)]
#[async_trait]
impl Tool for EchoTool {
fn name(&self) -> &str {
"echo"
}
fn display_name(&self) -> Option<&str> {
Some("Echo")
}
fn description(&self) -> &str {
"Echo back the provided message. Useful for testing tool execution."
}
fn parameters_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "The message to echo back"
}
},
"required": ["message"],
"additionalProperties": false
})
}
fn hints(&self) -> ToolHints {
ToolHints::default()
.with_readonly(true)
.with_idempotent(true)
}
async fn execute(&self, arguments: Value) -> ToolExecutionResult {
let message = arguments
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("");
ToolExecutionResult::success(serde_json::json!({
"echoed": message,
"length": message.len()
}))
}
}
#[cfg(test)]
pub struct FailingTool {
error_message: String,
use_internal_error: bool,
}
#[cfg(test)]
impl FailingTool {
pub fn with_tool_error(message: impl Into<String>) -> Self {
Self {
error_message: message.into(),
use_internal_error: false,
}
}
pub fn with_internal_error(message: impl Into<String>) -> Self {
Self {
error_message: message.into(),
use_internal_error: true,
}
}
}
#[cfg(test)]
impl Default for FailingTool {
fn default() -> Self {
Self::with_tool_error("Tool execution failed")
}
}
#[cfg(test)]
#[async_trait]
impl Tool for FailingTool {
fn name(&self) -> &str {
"failing_tool"
}
fn display_name(&self) -> Option<&str> {
Some("Failing Tool")
}
fn description(&self) -> &str {
"A tool that always fails (for testing error handling)"
}
fn parameters_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {},
"additionalProperties": false
})
}
fn hints(&self) -> ToolHints {
ToolHints::default()
.with_readonly(true)
.with_idempotent(true)
}
async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
if self.use_internal_error {
ToolExecutionResult::internal_error_msg(&self.error_message)
} else {
ToolExecutionResult::tool_error(&self.error_message)
}
}
}
#[cfg(test)]
mod tests;