descry-tool-core 0.3.1

Core traits and types for descry-tool framework
Documentation
//! Core tool trait
//!
//! Provides unified async-first tool abstraction.

use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Serialize};
use std::sync::Arc;

use crate::context::ToolContext;
use crate::error::ToolError;

/// 统一的工具 Trait(async-first)
///
/// 所有工具只需实现这一个 trait,无需区分 sync/async。
/// 使用 `Arc<ToolContext>` 避免借用地狱,可跨 await。
///
/// # Examples
///
/// ```
/// use descry_tool_core::{Tool, ToolContext, ToolError};
/// use serde::{Deserialize, Serialize};
/// use schemars::JsonSchema;
/// use std::sync::Arc;
///
/// #[derive(Deserialize, JsonSchema)]
/// struct AddParams {
///     a: i32,
///     b: i32,
/// }
///
/// #[derive(Serialize, JsonSchema)]
/// struct AddOutput {
///     result: i32,
/// }
///
/// struct AddTool;
///
/// impl Tool for AddTool {
///     type Params = AddParams;
///     type Output = AddOutput;
///
///     const NAME: &'static str = "add";
///     const DESCRIPTION: &'static str = "Add two numbers";
///
///     async fn call(
///         ctx: Arc<ToolContext>,
///         params: Self::Params,
///     ) -> Result<Self::Output, ToolError> {
///         Ok(AddOutput {
///             result: params.a + params.b,
///         })
///     }
/// }
/// ```
pub trait Tool: Send + Sync + 'static {
    /// 参数类型
    type Params: DeserializeOwned + JsonSchema + Send + Sync + 'static;

    /// 输出类型
    type Output: Serialize + JsonSchema + Send + Sync + 'static;

    /// 工具名称(编译期常量)
    const NAME: &'static str;

    /// 工具描述(编译期常量)
    const DESCRIPTION: &'static str;

    /// 输入示例(可选)
    ///
    /// 格式:`[(name, json_str), ...]`
    /// 例如:`[("basic", r#"{"a": 1, "b": 2}"#)]`
    const EXAMPLES: &'static [(&'static str, &'static str)] = &[];

    /// 生成 JSON Schema(使用 thread-local 缓存)
    ///
    /// 使用 thread-local HashMap 缓存每个类型的 schema,
    /// 通过 Box::leak 创建 'static 引用。
    fn schema() -> &'static serde_json::Value {
        use std::any::TypeId;
        use std::cell::RefCell;
        use std::collections::HashMap;
        
        thread_local! {
            static SCHEMAS: RefCell<HashMap<TypeId, &'static serde_json::Value>> = 
                RefCell::new(HashMap::new());
        }
        
        SCHEMAS.with(|schemas| {
            let type_id = TypeId::of::<Self::Params>();
            let mut cache = schemas.borrow_mut();
            
            *cache.entry(type_id).or_insert_with(|| {
                let root_schema = schemars::schema_for!(Self::Params);
                let value = serde_json::to_value(root_schema)
                    .expect("Failed to serialize schema");
                Box::leak(Box::new(value))
            })
        })
    }

    /// 生成输出 JSON Schema(可选)
    fn output_schema() -> Option<&'static serde_json::Value> {
        use std::any::TypeId;
        use std::cell::RefCell;
        use std::collections::HashMap;
        
        thread_local! {
            static SCHEMAS: RefCell<HashMap<TypeId, &'static serde_json::Value>> = 
                RefCell::new(HashMap::new());
        }
        
        Some(SCHEMAS.with(|schemas| {
            let type_id = TypeId::of::<Self::Output>();
            let mut cache = schemas.borrow_mut();
            
            *cache.entry(type_id).or_insert_with(|| {
                let root_schema = schemars::schema_for!(Self::Output);
                let value = serde_json::to_value(root_schema)
                    .expect("Failed to serialize schema");
                Box::leak(Box::new(value))
            })
        }))
    }

    /// 工具执行(核心方法)
    ///
    /// 使用 `Arc<ToolContext>` 避免借用地狱。
    async fn call(ctx: Arc<ToolContext>, params: Self::Params) -> Result<Self::Output, ToolError>;
}

/// 可选:工具行为注解
///
/// 提供工具的行为提示(如只读、幂等、破坏性等)。
pub trait HasAnnotations: Tool {
    /// 是否只读(不修改状态)
    const READ_ONLY: bool = false;

    /// 是否幂等(多次调用结果相同)
    const IDEMPOTENT: bool = false;

    /// 是否破坏性(可能删除/修改数据)
    const DESTRUCTIVE: bool = false;

    /// 是否开放世界(访问外部资源)
    const OPEN_WORLD: bool = false;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
    struct TestParams {
        value: i32,
    }

    #[derive(Debug, serde::Serialize, schemars::JsonSchema)]
    struct TestOutput {
        doubled: i32,
    }

    struct DoubleTool;

    impl Tool for DoubleTool {
        type Params = TestParams;
        type Output = TestOutput;

        const NAME: &'static str = "double";
        const DESCRIPTION: &'static str = "Double the input value";

        async fn call(
            _ctx: Arc<ToolContext>,
            params: Self::Params,
        ) -> Result<Self::Output, ToolError> {
            Ok(TestOutput {
                doubled: params.value * 2,
            })
        }
    }

    impl HasAnnotations for DoubleTool {
        const READ_ONLY: bool = true;
        const IDEMPOTENT: bool = true;
    }

    #[tokio::test]
    async fn test_tool_meta() {
        assert_eq!(DoubleTool::NAME, "double");
        assert_eq!(DoubleTool::DESCRIPTION, "Double the input value");
    }

    #[tokio::test]
    async fn test_schema_generation() {
        let schema = <DoubleTool as Tool>::schema();
        assert!(schema.is_object());
        assert_eq!(schema.get("title").unwrap().as_str().unwrap(), "TestParams");
    }

    #[tokio::test]
    async fn test_tool_call() {
        let ctx = Arc::new(ToolContext::new());
        let params = TestParams { value: 5 };
        let result = DoubleTool::call(ctx, params).await.unwrap();
        assert_eq!(result.doubled, 10);
    }

    #[tokio::test]
    async fn test_annotations() {
        assert!(<DoubleTool as HasAnnotations>::READ_ONLY);
        assert!(<DoubleTool as HasAnnotations>::IDEMPOTENT);
        assert!(!<DoubleTool as HasAnnotations>::DESTRUCTIVE);
    }
}