Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/dallay/corvus/llms.txt

Use this file to discover all available pages before exploring further.

Custom Tool

Build a custom tool to extend the agent’s capabilities.

Overview

Tools implement the Tool trait from src/tools/traits.rs. Every tool defines a name, description, parameter schema, and execution logic.

Step 1: Create the Tool

Create src/tools/my_tool.rs:
use crate::tools::traits::{Tool, ToolResult};
use anyhow::Result;
use async_trait::async_trait;
use serde_json::{json, Value};

pub struct MyTool {
    // Dependencies (security, memory, runtime, etc.)
}

impl MyTool {
    pub fn new() -> Self {
        Self {}
    }
}

#[async_trait]
impl Tool for MyTool {
    fn name(&self) -> &str {
        "my_tool"
    }
    
    fn description(&self) -> &str {
        "Does something useful with the given input"
    }
    
    fn parameters_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "input": {
                    "type": "string",
                    "description": "The input to process"
                },
                "mode": {
                    "type": "string",
                    "enum": ["fast", "thorough"],
                    "description": "Processing mode",
                    "default": "fast"
                }
            },
            "required": ["input"]
        })
    }
    
    async fn execute(&self, args: Value) -> Result<ToolResult> {
        // 1. Validate inputs
        let input = args["input"]
            .as_str()
            .ok_or_else(|| anyhow::anyhow!("Missing 'input' parameter"))?;
        
        let mode = args["mode"]
            .as_str()
            .unwrap_or("fast");
        
        // 2. Execute logic
        match do_something(input, mode).await {
            Ok(result) => Ok(ToolResult {
                success: true,
                output: result,
                error: None,
            }),
            Err(e) => Ok(ToolResult {
                success: false,
                output: String::new(),
                error: Some(e.to_string()),
            }),
        }
    }
}

async fn do_something(input: &str, mode: &str) -> Result<String> {
    // Your tool logic here
    Ok(format!("Processed '{}' in {} mode", input, mode))
}

Step 2: Register the Tool

Add to src/tools/mod.rs:
pub mod my_tool;

pub fn default_tools(
    security: Arc<SecurityPolicy>,
    memory: Arc<dyn Memory>,
    runtime: Arc<dyn RuntimeAdapter>,
    config: &Config,
) -> Vec<Arc<dyn Tool>> {
    let mut tools = vec![
        Arc::new(shell::ShellTool::new(security.clone(), runtime.clone())),
        Arc::new(file_read::FileReadTool::new(security.clone())),
        // ... existing tools
        Arc::new(my_tool::MyTool::new()),  // Add here
    ];
    
    tools
}

Step 3: Test

Add tests in src/tools/my_tool.rs:
#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_my_tool_success() {
        let tool = MyTool::new();
        let result = tool.execute(json!({
            "input": "test data",
            "mode": "fast"
        })).await.unwrap();
        
        assert!(result.success);
        assert!(result.output.contains("Processed"));
    }
    
    #[tokio::test]
    async fn test_my_tool_missing_param() {
        let tool = MyTool::new();
        let result = tool.execute(json!({})).await;
        
        assert!(result.is_err());
    }
}
Run tests:
cargo test tools::my_tool

Step 4: Use

The agent automatically discovers the tool:
corvus agent -m "Use my_tool to process this text"

Security Integration

For tools that need security checks:
use crate::security::SecurityPolicy;
use std::sync::Arc;

pub struct MyTool {
    security: Arc<SecurityPolicy>,
}

impl MyTool {
    pub fn new(security: Arc<SecurityPolicy>) -> Self {
        Self { security }
    }
}

#[async_trait]
impl Tool for MyTool {
    async fn execute(&self, args: Value) -> Result<ToolResult> {
        // Rate limit check
        if self.security.is_rate_limited() {
            return Ok(ToolResult {
                success: false,
                error: Some("Rate limit exceeded".into()),
                ..Default::default()
            });
        }
        
        // Record action
        if !self.security.record_action() {
            return Ok(ToolResult {
                success: false,
                error: Some("Action budget exhausted".into()),
                ..Default::default()
            });
        }
        
        // Execute...
    }
}

Best Practices

1. Descriptive Schemas

Provide detailed descriptions in the JSON schema:
json!({
    "type": "object",
    "properties": {
        "url": {
            "type": "string",
            "description": "Full URL to fetch (must be https://)",
            "pattern": "^https://"
        }
    },
    "required": ["url"]
})

2. Error Handling

Return errors via ToolResult, not Result::Err:
match operation().await {
    Ok(output) => Ok(ToolResult {
        success: true,
        output,
        error: None,
    }),
    Err(e) => Ok(ToolResult {
        success: false,
        output: String::new(),
        error: Some(e.to_string()),  // Structured error
    }),
}

3. Structured Output

For parseable data, return JSON strings:
let data = serde_json::json!({
    "status": "success",
    "count": 42,
    "items": ["a", "b", "c"]
});

Ok(ToolResult {
    success: true,
    output: data.to_string(),
    error: None,
})

Full Example

See examples/custom_tool.rs:27-71 for a complete HTTP GET tool.

Build docs developers (and LLMs) love