agentwerk 0.1.3

A minimal Rust crate that gives any application agentic capabilities.
Documentation
use std::future::Future;
use std::pin::Pin;

use serde_json::Value;

use crate::error::Result;
use crate::tools::tool::{Tool, ToolContext, ToolResult};

/// Tool that searches the tool registry by query string.
pub struct ToolSearchTool;

const DESCRIPTION: &str = "\
Search for available tools by name or keyword. Returns tool names, descriptions, and input schemas.";

impl Tool for ToolSearchTool {
    fn name(&self) -> &str {
        "tool_search"
    }

    fn description(&self) -> &str {
        DESCRIPTION
    }

    fn input_schema(&self) -> Value {
        serde_json::json!({
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "Search query to find tools by name or keyword"
                }
            },
            "required": ["query"]
        })
    }

    fn is_read_only(&self) -> bool {
        true
    }

    fn should_defer(&self) -> bool {
        false
    }

    fn call<'a>(
        &'a self,
        input: Value,
        ctx: &'a ToolContext,
    ) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send + 'a>> {
        Box::pin(async move {
            let query = match input.get("query").and_then(|v| v.as_str()) {
                Some(q) => q,
                None => {
                    return Ok(ToolResult::error("Missing required field: query"));
                }
            };

            let registry = match ctx.tool_registry.as_ref() {
                Some(r) => r,
                None => {
                    return Ok(ToolResult::error("No tool registry available"));
                }
            };

            let results = registry.search(query);

            if results.is_empty() {
                return Ok(ToolResult::success(format!("No tools found matching '{query}'.")));
            }

            let count = results.len();
            let mut content = format!("Found {count} tool(s) matching '{query}':\n\n");

            for def in &results {
                let pretty_schema =
                    serde_json::to_string_pretty(&def.input_schema).unwrap_or_default();

                content.push_str(&format!("## {}\n\n", def.name));
                content.push_str(&format!("{}\n\n", def.description));
                content.push_str("Input schema:\n```json\n");
                content.push_str(&pretty_schema);
                content.push_str("\n```\n\n---\n\n");
            }

            let discovered: Vec<String> = results.iter().map(|d| d.name.clone()).collect();
            Ok(ToolResult::success(content).with_discovered_tools(discovered))
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::testutil::MockTool;
    use crate::tools::tool::ToolRegistry;
    use std::sync::Arc;

    fn registry_with_mock_tools() -> Arc<ToolRegistry> {
        let mut registry = ToolRegistry::new();
        registry.register(MockTool::new("read_file", true, "file contents"));
        registry.register(MockTool::new("write_file", false, "written"));
        registry.register(MockTool::new("list_directory", true, "listing"));
        Arc::new(registry)
    }

    fn ctx_registry(registry: Arc<ToolRegistry>) -> ToolContext {
        ToolContext::new(std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")))
            .registry(registry)
    }

    #[tokio::test]
    async fn find_by_name() {
        let registry = registry_with_mock_tools();
        let ctx = ctx_registry(registry);
        let tool = ToolSearchTool;
        let input = serde_json::json!({ "query": "read_file" });
        let result = tool.call(input, &ctx).await.unwrap();
        assert!(!result.is_error);
        assert!(result.content.contains("read_file"));
    }

    #[tokio::test]
    async fn find_by_keyword() {
        let registry = registry_with_mock_tools();
        let ctx = ctx_registry(registry);
        let tool = ToolSearchTool;
        let input = serde_json::json!({ "query": "file" });
        let result = tool.call(input, &ctx).await.unwrap();
        assert!(!result.is_error);
        assert!(result.content.contains("read_file"));
        assert!(result.content.contains("write_file"));
    }

    #[tokio::test]
    async fn no_results() {
        let registry = registry_with_mock_tools();
        let ctx = ctx_registry(registry);
        let tool = ToolSearchTool;
        let input = serde_json::json!({ "query": "nonexistent_xyz" });
        let result = tool.call(input, &ctx).await.unwrap();
        assert!(!result.is_error);
        assert!(result.content.contains("No tools found"));
    }

    #[tokio::test]
    async fn returns_schema() {
        let registry = registry_with_mock_tools();
        let ctx = ctx_registry(registry);
        let tool = ToolSearchTool;
        let input = serde_json::json!({ "query": "read_file" });
        let result = tool.call(input, &ctx).await.unwrap();
        assert!(result.content.contains("```json"));
        assert!(result.content.contains("\"type\""));
    }

    #[tokio::test]
    async fn missing_query_errors() {
        let registry = registry_with_mock_tools();
        let ctx = ctx_registry(registry);
        let tool = ToolSearchTool;
        let input = serde_json::json!({});
        let result = tool.call(input, &ctx).await.unwrap();
        assert!(result.is_error);
        assert!(result.content.contains("Missing required field: query"));
    }

    #[tokio::test]
    async fn no_registry_errors() {
        let ctx =
            ToolContext::new(std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")));
        let tool = ToolSearchTool;
        let input = serde_json::json!({ "query": "anything" });
        let result = tool.call(input, &ctx).await.unwrap();
        assert!(result.is_error);
        assert!(result.content.contains("No tool registry available"));
    }

    #[tokio::test]
    async fn is_never_deferred() {
        let tool = ToolSearchTool;
        assert!(!tool.should_defer());
    }

    #[tokio::test]
    async fn is_read_only() {
        let tool = ToolSearchTool;
        assert!(tool.is_read_only());
    }
}