use std::collections::HashMap;
use std::fmt;
use std::future::Future;
use std::path::Path;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::{LazyLock, Mutex};
use regex::Regex;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio_util::sync::CancellationToken;
use crate::agent_options::{ApproveToolFn, ApproveToolFuture};
use crate::schema::schema_for;
use crate::transfer::TransferSignal;
use crate::types::ContentBlock;
static SCHEMA_VALIDATOR_CACHE: LazyLock<Mutex<HashMap<String, Arc<jsonschema::Validator>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentToolResult {
pub content: Vec<ContentBlock>,
pub details: Value,
pub is_error: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transfer_signal: Option<TransferSignal>,
}
impl AgentToolResult {
#[must_use]
pub fn new(content: Vec<ContentBlock>, is_error: bool) -> Self {
Self {
content,
details: Value::Null,
is_error,
transfer_signal: None,
}
}
pub fn text(text: impl Into<String>) -> Self {
Self {
content: vec![ContentBlock::Text { text: text.into() }],
details: Value::Null,
is_error: false,
transfer_signal: None,
}
}
pub fn error(message: impl Into<String>) -> Self {
Self {
content: vec![ContentBlock::Text {
text: message.into(),
}],
details: Value::Null,
is_error: true,
transfer_signal: None,
}
}
pub fn transfer(signal: TransferSignal) -> Self {
let text = format!("Transfer to {} initiated.", signal.target_agent());
Self {
content: vec![ContentBlock::Text { text }],
details: Value::Null,
is_error: false,
transfer_signal: Some(signal),
}
}
pub const fn is_transfer(&self) -> bool {
self.transfer_signal.is_some()
}
}
pub type ToolFuture<'a> = Pin<Box<dyn Future<Output = AgentToolResult> + Send + 'a>>;
#[non_exhaustive]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ToolMetadata {
pub namespace: Option<String>,
pub version: Option<String>,
}
impl ToolMetadata {
#[must_use]
pub fn with_namespace(namespace: impl Into<String>) -> Self {
Self {
namespace: Some(namespace.into()),
version: None,
}
}
#[must_use]
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
}
pub trait AgentTool: Send + Sync {
fn name(&self) -> &str;
fn label(&self) -> &str;
fn description(&self) -> &str;
fn parameters_schema(&self) -> &Value;
fn requires_approval(&self) -> bool {
false
}
fn metadata(&self) -> Option<ToolMetadata> {
None
}
fn execution_root(&self) -> Option<&Path> {
None
}
fn approval_context(&self, _params: &Value) -> Option<Value> {
None
}
fn auth_config(&self) -> Option<crate::credential::AuthConfig> {
None
}
fn execute(
&self,
tool_call_id: &str,
params: Value,
cancellation_token: CancellationToken,
on_update: Option<Box<dyn Fn(AgentToolResult) + Send + Sync>>,
state: Arc<std::sync::RwLock<crate::SessionState>>,
credential: Option<crate::credential::ResolvedCredential>,
) -> ToolFuture<'_>;
}
pub trait IntoTool {
fn into_tool(self) -> Arc<dyn AgentTool>;
}
impl<T: AgentTool + 'static> IntoTool for T {
fn into_tool(self) -> Arc<dyn AgentTool> {
Arc::new(self)
}
}
pub fn validate_schema(schema: &Value) -> Result<(), String> {
compiled_validator(schema)?;
Ok(())
}
pub fn validate_tool_arguments(schema: &Value, arguments: &Value) -> Result<(), Vec<String>> {
let validator = compiled_validator(schema).map_err(|e| vec![e])?;
let errors: Vec<String> = validator
.iter_errors(arguments)
.map(|e| e.to_string())
.collect();
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
#[must_use]
pub(crate) fn permissive_object_schema() -> Value {
serde_json::json!({
"type": "object",
"properties": {},
"additionalProperties": true
})
}
#[must_use]
pub(crate) fn debug_validated_schema(schema: Value) -> Value {
debug_assert!(validate_schema(&schema).is_ok());
schema
}
#[must_use]
pub(crate) fn validated_schema_for<T: schemars::JsonSchema>() -> Value {
debug_validated_schema(schema_for::<T>())
}
fn compiled_validator(schema: &Value) -> Result<Arc<jsonschema::Validator>, String> {
let cache_key = serde_json::to_string(schema).map_err(|e| e.to_string())?;
{
let cache = SCHEMA_VALIDATOR_CACHE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(validator) = cache.get(&cache_key) {
return Ok(Arc::clone(validator));
}
}
let compiled = Arc::new(jsonschema::validator_for(schema).map_err(|e| e.to_string())?);
let mut cache = SCHEMA_VALIDATOR_CACHE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
Ok(Arc::clone(
cache
.entry(cache_key)
.or_insert_with(|| Arc::clone(&compiled)),
))
}
#[must_use]
pub fn unknown_tool_result(tool_name: &str) -> AgentToolResult {
AgentToolResult::error(format!("unknown tool: {tool_name}"))
}
#[must_use]
pub fn validation_error_result(errors: &[String]) -> AgentToolResult {
let message = errors.join("\n");
AgentToolResult::error(message)
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ToolApproval {
Approved,
Rejected,
ApprovedWith(serde_json::Value),
}
#[non_exhaustive]
#[derive(Clone)]
pub struct ToolApprovalRequest {
pub tool_call_id: String,
pub tool_name: String,
pub arguments: Value,
pub requires_approval: bool,
pub context: Option<Value>,
}
impl ToolApprovalRequest {
#[must_use]
pub fn new(
tool_call_id: impl Into<String>,
tool_name: impl Into<String>,
arguments: Value,
requires_approval: bool,
) -> Self {
Self {
tool_call_id: tool_call_id.into(),
tool_name: tool_name.into(),
arguments,
requires_approval,
context: None,
}
}
#[must_use]
pub fn with_context(mut self, context: Value) -> Self {
self.context = Some(context);
self
}
}
impl fmt::Debug for ToolApprovalRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let redacted_context = self.context.as_ref().map(redact_sensitive_values);
f.debug_struct("ToolApprovalRequest")
.field("tool_call_id", &self.tool_call_id)
.field("tool_name", &self.tool_name)
.field("arguments", &"[REDACTED]")
.field("requires_approval", &self.requires_approval)
.field("context", &redacted_context)
.finish()
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ApprovalMode {
Enabled,
#[default]
Smart,
Bypassed,
}
#[allow(clippy::type_complexity)]
pub fn selective_approve<F>(inner: F) -> Box<ApproveToolFn>
where
F: Fn(ToolApprovalRequest) -> ApproveToolFuture + Send + Sync + 'static,
{
Box::new(move |req: ToolApprovalRequest| {
if req.requires_approval {
inner(req)
} else {
Box::pin(async { ToolApproval::Approved })
}
})
}
const REDACTED: &str = "[REDACTED]";
const SENSITIVE_KEYS: &[&str] = &[
"password",
"secret",
"token",
"api_key",
"apikey",
"authorization",
];
#[must_use]
pub fn redact_sensitive_values(value: &Value) -> Value {
redact_value(value, None)
}
fn redact_value(value: &Value, parent_key: Option<&str>) -> Value {
if let Some(key) = parent_key
&& SENSITIVE_KEYS.iter().any(|&s| key.eq_ignore_ascii_case(s))
{
return Value::String(REDACTED.to_string());
}
match value {
Value::String(s) => {
if is_sensitive_string(s) {
Value::String(REDACTED.to_string())
} else {
value.clone()
}
}
Value::Array(arr) => Value::Array(arr.iter().map(|v| redact_value(v, None)).collect()),
Value::Object(map) => {
let redacted = map
.iter()
.map(|(k, v)| (k.clone(), redact_value(v, Some(k))))
.collect();
Value::Object(redacted)
}
_ => value.clone(),
}
}
fn is_sensitive_string(s: &str) -> bool {
if s.starts_with("sk-")
|| s.starts_with("key-")
|| s.starts_with("token-")
|| s.to_ascii_lowercase().starts_with("bearer ")
|| s.to_ascii_lowercase().starts_with("basic ")
{
return true;
}
thread_local! {
static ENV_VAR_RE: Regex =
Regex::new(r"^\$\{?[A-Z_][A-Z0-9_]*\}?$").expect("valid regex");
}
ENV_VAR_RE.with(|re| re.is_match(s))
}
pub trait ToolParameters {
fn json_schema() -> Value;
}
const _: () = {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<AgentToolResult>();
assert_send_sync::<ToolApproval>();
assert_send_sync::<ToolApprovalRequest>();
assert_send_sync::<ApprovalMode>();
};
#[cfg(test)]
#[path = "tool_tests.rs"]
mod tests;