use crate::tools::ToolDefinition;
use crate::types::Message;
pub const HOOK_TIMING_DISPLAY_THRESHOLD_MS: u64 = 500;
pub const SLOW_PHASE_LOG_THRESHOLD_MS: u64 = 2000;
pub fn classify_tool_error(error: &(dyn std::error::Error + 'static)) -> String {
let error_name = std::any::type_name_of_val(error);
if let Some(downcast) = error.downcast_ref::<std::io::Error>() {
let errno = downcast.raw_os_error();
if let Some(code) = errno {
return format!("Error:{}", code);
}
}
let name_len = error_name.len();
if name_len > 3 && !error_name.contains("std::io::Error") {
let short_name = error_name
.rsplit("::")
.next()
.unwrap_or(error_name)
.chars()
.take(60)
.collect::<String>();
return short_name;
}
"Error".to_string()
}
pub fn classify_tool_error_from_message(message: &str) -> String {
let lower = message.to_lowercase();
if lower.contains("enoent") || lower.contains("file not found") {
return "Error:ENOENT".to_string();
}
if lower.contains("eacces") || lower.contains("permission denied") {
return "Error:EACCES".to_string();
}
if lower.contains("timeout") {
return "Error:ETIMEDOUT".to_string();
}
"Error".to_string()
}
pub fn build_schema_not_sent_hint(
tool_name: &str,
messages: &[Message],
tools: &[ToolDefinition],
) -> Option<String> {
let tool_available = tools.iter().any(|t| t.name == tool_name);
if tool_available {
return None;
}
let discovered_in_messages = messages.iter().any(|m| m.content.contains(tool_name));
if discovered_in_messages {
return Some(format!(
"\n\nThis tool's schema was not sent to the API — it was not in the discovered-tool set derived from message history. \
Without the schema in your prompt, typed parameters (arrays, numbers, booleans) get emitted as strings and the client-side parser rejects them. \
Load the tool first: call tool_search with query \"select:{}\", then retry this call.",
tool_name
));
}
None
}
#[derive(Debug, Clone)]
pub struct MessageUpdateLazy {
pub message: Message,
pub context_modifier: Option<ContextModifier>,
}
#[derive(Debug, Clone)]
pub struct ContextModifier {
pub tool_use_id: String,
}
#[derive(Debug, Clone)]
pub struct ToolProgress {
pub tool_use_id: String,
pub data: serde_json::Value,
}
#[derive(Debug, Clone)]
pub enum ToolExecutionError {
ToolNotFound(String),
InputValidation(String),
PermissionDenied(String),
ExecutionFailed(String),
Aborted,
}
impl std::fmt::Display for ToolExecutionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ToolExecutionError::ToolNotFound(name) => write!(f, "No such tool available: {}", name),
ToolExecutionError::InputValidation(msg) => write!(f, "InputValidationError: {}", msg),
ToolExecutionError::PermissionDenied(msg) => write!(f, "Permission denied: {}", msg),
ToolExecutionError::ExecutionFailed(msg) => write!(f, "Error calling tool: {}", msg),
ToolExecutionError::Aborted => write!(f, "Tool execution was aborted"),
}
}
}
impl std::error::Error for ToolExecutionError {}
pub fn create_tool_error_message(tool_use_id: &str, error: &str, is_error: bool) -> Message {
Message {
role: crate::types::MessageRole::Tool,
content: format!("<tool_use_error>{}</tool_use_error>", error),
tool_call_id: Some(tool_use_id.to_string()),
is_error: Some(is_error),
..Default::default()
}
}
pub fn create_progress_message(tool_use_id: &str, data: serde_json::Value) -> Message {
Message {
role: crate::types::MessageRole::User,
content: serde_json::json!({
"type": "progress",
"tool_use_id": tool_use_id,
"data": data,
})
.to_string(),
..Default::default()
}
}
pub fn format_input_validation_error(tool_name: &str, error_message: &str) -> String {
format!("Error parsing {} input: {}", tool_name, error_message)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_classify_tool_error_io() {
let error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let classified = classify_tool_error(&error);
assert!(classified.contains("Error:") || classified == "Error");
}
#[test]
fn test_classify_tool_error_from_message() {
assert_eq!(
classify_tool_error_from_message("File not found"),
"Error:ENOENT"
);
assert_eq!(
classify_tool_error_from_message("Permission denied"),
"Error:EACCES"
);
assert_eq!(
classify_tool_error_from_message("timeout error"),
"Error:ETIMEDOUT"
);
assert_eq!(
classify_tool_error_from_message("Some other error"),
"Error"
);
}
#[test]
fn test_build_schema_not_sent_hint_tool_available() {
let tools = vec![ToolDefinition {
name: "test_tool".to_string(),
description: "Test tool".to_string(),
input_schema: crate::types::ToolInputSchema {
schema_type: "object".to_string(),
properties: serde_json::json!({}),
required: None,
},
annotations: None,
should_defer: None,
always_load: None,
is_mcp: None,
search_hint: None,
aliases: None,
user_facing_name: None,
interrupt_behavior: None,
}];
let messages = vec![];
let hint = build_schema_not_sent_hint("test_tool", &messages, &tools);
assert!(hint.is_none());
}
#[test]
fn test_build_schema_not_sent_hint_discovered() {
let tools = vec![];
let messages = vec![Message {
role: crate::types::MessageRole::Assistant,
content: "Using discovered_tool".to_string(),
..Default::default()
}];
let hint = build_schema_not_sent_hint("discovered_tool", &messages, &tools);
assert!(hint.is_some());
assert!(hint.unwrap().contains("discovered_tool"));
}
#[test]
fn test_create_tool_error_message() {
let msg = create_tool_error_message("tool_123", "Test error", true);
assert!(msg.content.contains("tool_use_error"));
assert!(msg.content.contains("Test error"));
assert!(msg.is_error == Some(true));
}
#[test]
fn test_format_input_validation_error() {
let error = format_input_validation_error("Read", "expected string, got number");
assert!(error.contains("Read"));
assert!(error.contains("expected string"));
}
#[test]
fn test_constants() {
assert_eq!(HOOK_TIMING_DISPLAY_THRESHOLD_MS, 500);
assert_eq!(SLOW_PHASE_LOG_THRESHOLD_MS, 2000);
}
}