adk-tool 1.0.0

Tool system for Rust Agent Development Kit (ADK-Rust) agents (FunctionTool, MCP, Google Search)
Documentation
use adk_core::{
    CallbackContext, Content, EventActions, MemoryEntry, ReadonlyContext, Result, Tool, ToolContext,
};
use adk_tool::{
    AnthropicBashTool20250124, AnthropicTextEditorTool20250728, ExitLoopTool, GoogleSearchTool,
    OpenAIWebSearchTool, WebSearchTool,
};
use async_trait::async_trait;
use serde_json::json;
use std::sync::{Arc, Mutex};

struct MockToolContext {
    actions: Mutex<EventActions>,
    content: Content,
}

impl MockToolContext {
    fn new() -> Self {
        Self { actions: Mutex::new(EventActions::default()), content: Content::new("user") }
    }
}

#[async_trait]
impl ReadonlyContext for MockToolContext {
    fn invocation_id(&self) -> &str {
        "inv-1"
    }
    fn agent_name(&self) -> &str {
        "test-agent"
    }
    fn user_id(&self) -> &str {
        "user-1"
    }
    fn app_name(&self) -> &str {
        "test-app"
    }
    fn session_id(&self) -> &str {
        "session-1"
    }
    fn branch(&self) -> &str {
        ""
    }
    fn user_content(&self) -> &Content {
        &self.content
    }
}

#[async_trait]
impl CallbackContext for MockToolContext {
    fn artifacts(&self) -> Option<Arc<dyn adk_core::Artifacts>> {
        None
    }
}

#[async_trait]
impl ToolContext for MockToolContext {
    fn function_call_id(&self) -> &str {
        "call-1"
    }
    fn actions(&self) -> EventActions {
        self.actions.lock().unwrap().clone()
    }
    fn set_actions(&self, actions: EventActions) {
        *self.actions.lock().unwrap() = actions;
    }
    async fn search_memory(&self, _query: &str) -> Result<Vec<MemoryEntry>> {
        Ok(vec![])
    }
}

#[test]
fn test_exit_loop_tool_metadata() {
    let tool = ExitLoopTool::new();
    assert_eq!(tool.name(), "exit_loop");
    assert!(tool.description().contains("Exits the loop"));
    assert!(!tool.is_long_running());
}

#[tokio::test]
async fn test_exit_loop_execute() {
    let tool = ExitLoopTool::new();
    let ctx = Arc::new(MockToolContext::new()) as Arc<dyn ToolContext>;
    let result = tool.execute(ctx, json!({})).await;
    assert!(result.is_ok());
}

#[test]
fn test_google_search_tool_metadata() {
    let tool = GoogleSearchTool::new();
    assert_eq!(tool.name(), "google_search");
    assert!(tool.description().contains("Google search"));
    assert!(!tool.is_long_running());
}

#[tokio::test]
async fn test_google_search_not_executable() {
    let tool = GoogleSearchTool::new();
    let ctx = Arc::new(MockToolContext::new()) as Arc<dyn ToolContext>;
    let result = tool.execute(ctx, json!({})).await;
    assert!(result.is_err());
}

#[test]
fn test_native_declarations_are_exposed() {
    let gemini_decl = GoogleSearchTool::new().declaration();
    assert_eq!(gemini_decl["x-adk-gemini-tool"]["google_search"], json!({}));

    let openai_decl = OpenAIWebSearchTool::new().declaration();
    assert_eq!(openai_decl["x-adk-openai-tool"]["type"], "web_search_2025_08_26");

    let anthropic_decl = WebSearchTool::new().declaration();
    assert_eq!(anthropic_decl["x-adk-anthropic-tool"]["type"], "web_search_20250305");
}

#[tokio::test]
async fn test_anthropic_bash_tool_executes_shell_command() {
    let tool = AnthropicBashTool20250124::new();
    let ctx = Arc::new(MockToolContext::new()) as Arc<dyn ToolContext>;
    let result = tool
        .execute(ctx, json!({ "command": "printf hello", "restart": false }))
        .await
        .expect("bash should succeed");

    assert_eq!(result, json!("hello\nexit_code: 0\n"));
}

#[tokio::test]
async fn test_anthropic_text_editor_tool_executes_local_edits() {
    let tool = AnthropicTextEditorTool20250728::new();
    let ctx = Arc::new(MockToolContext::new()) as Arc<dyn ToolContext>;
    let file_path = std::env::temp_dir().join(format!("adk-tool-{}.txt", uuid::Uuid::new_v4()));
    let file_path_str = file_path.to_string_lossy().to_string();

    tool.execute(
        Arc::clone(&ctx),
        json!({ "command": "create", "path": file_path_str, "file_text": "alpha\nbeta\n" }),
    )
    .await
    .expect("create should succeed");

    let viewed = tool
        .execute(
            Arc::clone(&ctx),
            json!({ "command": "view", "path": file_path.to_string_lossy() }),
        )
        .await
        .expect("view should succeed");
    assert_eq!(viewed, json!("alpha\nbeta\n"));

    tool.execute(
        Arc::clone(&ctx),
        json!({
            "command": "str_replace",
            "path": file_path.to_string_lossy(),
            "old_str": "beta",
            "new_str": "gamma"
        }),
    )
    .await
    .expect("replace should succeed");

    let updated = tool
        .execute(ctx, json!({ "command": "view", "path": file_path.to_string_lossy() }))
        .await
        .expect("view after replace should succeed");
    assert_eq!(updated, json!("alpha\ngamma\n"));

    let _ = std::fs::remove_file(file_path);
}