use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::ctx::ToolCtx;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ToolTier {
Agent,
Operator,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ResponseRedaction {
#[default]
Apply,
BypassByOperator,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ToolDescriptor {
name: String,
tier: ToolTier,
schema: serde_json::Value,
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
output_schema: Option<serde_json::Value>,
#[serde(skip)]
response_redaction: ResponseRedaction,
}
impl ToolDescriptor {
pub fn agent(name: impl Into<String>, schema: serde_json::Value) -> Self {
Self {
name: name.into(),
tier: ToolTier::Agent,
schema,
description: None,
output_schema: None,
response_redaction: ResponseRedaction::Apply,
}
}
pub fn operator(name: impl Into<String>, schema: serde_json::Value) -> Self {
Self {
name: name.into(),
tier: ToolTier::Operator,
schema,
description: None,
output_schema: None,
response_redaction: ResponseRedaction::Apply,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_response_redaction(mut self, response_redaction: ResponseRedaction) -> Self {
self.response_redaction = response_redaction;
self
}
pub fn with_output_schema(mut self, output_schema: serde_json::Value) -> Self {
self.output_schema = Some(output_schema);
self
}
pub fn name(&self) -> &str {
&self.name
}
pub fn tier(&self) -> ToolTier {
self.tier
}
pub fn schema(&self) -> &serde_json::Value {
&self.schema
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn output_schema(&self) -> Option<&serde_json::Value> {
self.output_schema.as_ref()
}
pub fn response_redaction(&self) -> ResponseRedaction {
self.response_redaction
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ToolResponse {
pub payload: serde_json::Value,
}
impl ToolResponse {
pub fn json(payload: serde_json::Value) -> Self {
Self { payload }
}
pub fn text(text: impl Into<String>) -> Self {
Self {
payload: serde_json::Value::String(text.into()),
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ToolError {
#[error("invalid args: {0}")]
InvalidArgs(String),
#[error("not found: {0}")]
NotFound(String),
#[error("limit exceeded: {0}")]
LimitExceeded(String),
#[error("backend unavailable: {0}")]
BackendUnavailable(String),
#[error("backend failure: {0}")]
BackendFailure(String),
#[error("tool internal error: {0}")]
Internal(#[source] Box<dyn std::error::Error + Send + Sync>),
}
impl ToolError {
pub fn class(&self) -> &'static str {
match self {
Self::InvalidArgs(_) => "invalid-args",
Self::NotFound(_) => "not-found",
Self::LimitExceeded(_) => "limit-exceeded",
Self::BackendUnavailable(_) => "backend-unavailable",
Self::BackendFailure(_) => "backend-failure",
Self::Internal(_) => "internal",
}
}
pub fn internal<E>(err: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
Self::Internal(Box::new(err))
}
}
#[async_trait]
pub trait Tool: Send + Sync {
fn descriptor(&self) -> &ToolDescriptor;
async fn invoke(&self, ctx: &ToolCtx<'_>) -> Result<ToolResponse, ToolError>;
}