use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use tokio_util::sync::CancellationToken;
#[derive(Debug, Clone)]
pub struct ValidationError {
pub path: String,
pub message: String,
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.path.is_empty() || self.path == "root" {
write!(f, "{}", self.message)
} else {
write!(f, "{}: {}", self.path, self.message)
}
}
}
#[derive(Debug, Clone)]
pub struct FileDiagnostic {
pub line: u32,
pub column: u32,
pub severity: u32,
pub message: String,
}
impl FileDiagnostic {
pub fn pretty(&self) -> String {
let level = match self.severity {
1 => "ERROR",
2 => "WARN",
3 => "INFO",
_ => "HINT",
};
format!("{level} [{}:{}] {}", self.line, self.column, self.message)
}
}
#[async_trait::async_trait]
pub trait DiagnosticProvider: Send + Sync + std::fmt::Debug {
async fn diagnostics_for_file(
&self,
file_path: &Path,
max_severity: u32,
max_count: usize,
) -> Vec<FileDiagnostic>;
}
#[derive(Debug, thiserror::Error)]
pub enum ToolError {
#[error("Tool execution failed: {0}")]
Execution(String),
#[error("Invalid parameters: {0}")]
InvalidParams(String),
#[error("Tool not found: {0}")]
NotFound(String),
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error("Interrupted by user")]
Interrupted,
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
Other(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub llm_suffix: Option<String>,
}
impl ToolResult {
pub fn ok(output: impl Into<String>) -> Self {
Self {
success: true,
output: Some(output.into()),
error: None,
metadata: HashMap::new(),
duration_ms: None,
llm_suffix: None,
}
}
pub fn ok_with_metadata(
output: impl Into<String>,
metadata: HashMap<String, serde_json::Value>,
) -> Self {
Self {
success: true,
output: Some(output.into()),
error: None,
metadata,
duration_ms: None,
llm_suffix: None,
}
}
pub fn fail(error: impl Into<String>) -> Self {
Self {
success: false,
output: None,
error: Some(error.into()),
metadata: HashMap::new(),
duration_ms: None,
llm_suffix: None,
}
}
pub fn with_llm_suffix(mut self, suffix: impl Into<String>) -> Self {
self.llm_suffix = Some(suffix.into());
self
}
pub fn from_error(err: ToolError) -> Self {
Self::fail(err.to_string())
}
}
#[derive(Debug, Clone)]
pub struct ToolTimeoutConfig {
pub idle_timeout_secs: u64,
pub max_timeout_secs: u64,
}
impl Default for ToolTimeoutConfig {
fn default() -> Self {
Self {
idle_timeout_secs: 60,
max_timeout_secs: 600,
}
}
}
#[derive(Debug, Clone)]
pub struct ToolContext {
pub working_dir: PathBuf,
pub is_subagent: bool,
pub session_id: Option<String>,
pub values: HashMap<String, serde_json::Value>,
pub timeout_config: Option<ToolTimeoutConfig>,
pub cancel_token: Option<CancellationToken>,
pub diagnostic_provider: Option<Arc<dyn DiagnosticProvider>>,
pub shared_state: Option<Arc<Mutex<HashMap<String, serde_json::Value>>>>,
}
impl ToolContext {
pub fn new(working_dir: impl Into<PathBuf>) -> Self {
let raw: PathBuf = working_dir.into();
let resolved = if raw.is_relative() {
if let Ok(cwd) = std::env::current_dir() {
let joined = cwd.join(&raw);
joined.canonicalize().unwrap_or(joined)
} else {
raw.canonicalize().unwrap_or(raw)
}
} else {
raw
};
Self {
working_dir: resolved,
is_subagent: false,
session_id: None,
values: HashMap::new(),
timeout_config: None,
cancel_token: None,
diagnostic_provider: None,
shared_state: None,
}
}
pub fn with_cancel_token(mut self, token: CancellationToken) -> Self {
self.cancel_token = Some(token);
self
}
pub fn with_subagent(mut self, is_subagent: bool) -> Self {
self.is_subagent = is_subagent;
self
}
pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
self.session_id = Some(session_id.into());
self
}
pub fn with_value(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.values.insert(key.into(), value);
self
}
pub fn with_diagnostic_provider(mut self, provider: Arc<dyn DiagnosticProvider>) -> Self {
self.diagnostic_provider = Some(provider);
self
}
pub fn with_timeout_config(mut self, config: ToolTimeoutConfig) -> Self {
self.timeout_config = Some(config);
self
}
pub fn with_shared_state(
mut self,
state: Arc<Mutex<HashMap<String, serde_json::Value>>>,
) -> Self {
self.shared_state = Some(state);
self
}
}
impl Default for ToolContext {
fn default() -> Self {
Self {
working_dir: std::env::current_dir().unwrap_or_default(),
is_subagent: false,
session_id: None,
values: HashMap::new(),
timeout_config: None,
cancel_token: None,
diagnostic_provider: None,
shared_state: None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ToolDisplayMeta {
pub verb: &'static str,
pub label: &'static str,
pub category: &'static str,
pub primary_arg_keys: &'static [&'static str],
}
#[async_trait::async_trait]
pub trait BaseTool: Send + Sync + std::fmt::Debug {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn parameter_schema(&self) -> serde_json::Value;
async fn execute(
&self,
args: HashMap<String, serde_json::Value>,
ctx: &ToolContext,
) -> ToolResult;
fn format_validation_error(&self, errors: &[ValidationError]) -> Option<String> {
let _ = errors;
None
}
fn display_meta(&self) -> Option<ToolDisplayMeta> {
None
}
}
#[cfg(test)]
#[path = "traits_tests.rs"]
mod tests;