Skip to main content

Tool Trait

The Tool trait defines the interface for all agent capabilities in Corvus. Every tool (shell, file operations, memory, etc.) implements this trait. Source: src/tools/traits.rs:34-57

Trait Definition

use async_trait::async_trait;
use serde_json::Value;

#[async_trait]
pub trait Tool: Send + Sync {
    /// Tool name (used in LLM function calling)
    fn name(&self) -> &str;
    
    /// Human-readable description
    fn description(&self) -> &str;
    
    /// JSON schema for parameters
    fn parameters_schema(&self) -> Value;
    
    /// Execute the tool with given arguments
    async fn execute(&self, args: Value) -> anyhow::Result<ToolResult>;
    
    /// Get the full spec for LLM registration
    fn spec(&self) -> ToolSpec {
        ToolSpec {
            name: self.name().to_string(),
            description: self.description().to_string(),
            parameters: self.parameters_schema(),
            source: None,
        }
    }
}

Core Types

ToolResult

From src/tools/traits.rs:5-10:
pub struct ToolResult {
    pub success: bool,
    pub output: String,
    pub error: Option<String>,
}

ToolSpec

From src/tools/traits.rs:13-20:
pub struct ToolSpec {
    pub name: String,
    pub description: String,
    pub parameters: Value,  // JSON Schema
    pub source: Option<ToolSourceMetadata>,
}

ToolSourceMetadata

For MCP/Composio tools:
pub struct ToolSourceMetadata {
    pub kind: String,           // "mcp", "composio", "builtin"
    pub provider: Option<String>,
    pub server: Option<String>,
    pub original_name: Option<String>,
}

Implementing a Tool

Minimal example from examples/custom_tool.rs:27-71:
use async_trait::async_trait;
use serde_json::{json, Value};

pub struct HttpGetTool;

#[async_trait]
impl Tool for HttpGetTool {
    fn name(&self) -> &str {
        "http_get"
    }
    
    fn description(&self) -> &str {
        "Fetch a URL and return the HTTP status code and content length"
    }
    
    fn parameters_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "url": { 
                    "type": "string", 
                    "description": "URL to fetch" 
                }
            },
            "required": ["url"]
        })
    }
    
    async fn execute(&self, args: Value) -> anyhow::Result<ToolResult> {
        let url = args["url"]
            .as_str()
            .ok_or_else(|| anyhow::anyhow!("Missing 'url' parameter"))?;
        
        match reqwest::get(url).await {
            Ok(resp) => {
                let status = resp.status().as_u16();
                let len = resp.content_length().unwrap_or(0);
                Ok(ToolResult {
                    success: status < 400,
                    output: format!("HTTP {} — {} bytes", status, len),
                    error: None,
                })
            }
            Err(e) => Ok(ToolResult {
                success: false,
                output: String::new(),
                error: Some(format!("Request failed: {}", e)),
            }),
        }
    }
}

Parameter Schema (JSON Schema)

All tools use JSON Schema for parameter validation:
fn parameters_schema(&self) -> Value {
    json!({
        "type": "object",
        "properties": {
            "path": {
                "type": "string",
                "description": "Relative path to the file"
            },
            "approved": {
                "type": "boolean",
                "description": "Explicit approval for risky operations",
                "default": false
            }
        },
        "required": ["path"]
    })
}

Built-in Tools

From src/tools/mod.rs:
ToolNameDescription
ShellToolshellExecute shell commands
FileReadToolfile_readRead file contents
FileWriteToolfile_writeWrite file contents
MemoryStoreToolmemory_storeSave memories
MemoryRecallToolmemory_recallSearch memories
MemoryForgetToolmemory_forgetDelete memories
BrowserOpenToolbrowser_openOpen URLs (opt-in)
ComposioToolcomposio_*1000+ OAuth apps (opt-in)

Security Integration

Tools integrate with SecurityPolicy:
pub struct FileReadTool {
    security: Arc<SecurityPolicy>,
}

#[async_trait]
impl Tool for FileReadTool {
    async fn execute(&self, args: Value) -> anyhow::Result<ToolResult> {
        let path = args["path"].as_str()?;
        
        // 1. Rate limit check
        if self.security.is_rate_limited() {
            return Ok(ToolResult {
                success: false,
                error: Some("Rate limit exceeded".into()),
                ..Default::default()
            });
        }
        
        // 2. Path validation
        if !self.security.is_path_allowed(path) {
            return Ok(ToolResult {
                success: false,
                error: Some(format!("Path not allowed: {}", path)),
                ..Default::default()
            });
        }
        
        // 3. Record action (consumes rate limit budget)
        if !self.security.record_action() {
            return Ok(ToolResult {
                success: false,
                error: Some("Action budget exhausted".into()),
                ..Default::default()
            });
        }
        
        // 4. Execute
        // ...
    }
}

Tool Registration

Register your tool in src/tools/mod.rs:
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<dyn Tool>> = vec![
        Arc::new(shell::ShellTool::new(security.clone(), runtime.clone())),
        Arc::new(file_read::FileReadTool::new(security.clone())),
        Arc::new(file_write::FileWriteTool::new(security.clone())),
        Arc::new(memory_store::MemoryStoreTool::new(memory.clone())),
        Arc::new(memory_recall::MemoryRecallTool::new(memory.clone())),
        Arc::new(memory_forget::MemoryForgetTool::new(memory.clone())),
    ];
    
    // Optional tools
    if config.browser.enabled {
        tools.push(Arc::new(browser_open::BrowserOpenTool::new(config)));
    }
    
    tools
}

Error Handling

Return Errors as ToolResult

async fn execute(&self, args: Value) -> anyhow::Result<ToolResult> {
    // Validation errors: return Result::Err
    let param = args["param"]
        .as_str()
        .ok_or_else(|| anyhow::anyhow!("Missing 'param'"))?;
    
    // Execution errors: return ToolResult with error field
    match do_something(param).await {
        Ok(output) => Ok(ToolResult {
            success: true,
            output,
            error: None,
        }),
        Err(e) => Ok(ToolResult {
            success: false,
            output: String::new(),
            error: Some(e.to_string()),
        }),
    }
}

Never Panic in Tools

Never use .unwrap(), .expect(), or panic!() in tool execution paths. Return errors via ToolResult or Result::Err.

LLM Integration

Tools are exposed to the LLM via:

1. Native Function Calling (OpenAI, Anthropic, Gemini)

{
  "type": "function",
  "function": {
    "name": "file_read",
    "description": "Read the contents of a file in the workspace",
    "parameters": {
      "type": "object",
      "properties": {
        "path": {
          "type": "string",
          "description": "Relative path to the file"
        }
      },
      "required": ["path"]
    }
  }
}

2. Prompt-Guided (Ollama, local models)

## Available Tools

**file_read**: Read the contents of a file in the workspace
Parameters: `{"type":"object","properties":{"path":{"type":"string"}}}`

To use a tool, wrap JSON in <tool_call></tool_call> tags:

<tool_call>
{"name": "file_read", "arguments": {"path": "README.md"}}
</tool_call>

Testing Tools

From src/tools/shell.rs:206-216:
#[tokio::test]
async fn shell_executes_allowed_command() {
    let tool = ShellTool::new(
        test_security(AutonomyLevel::Supervised),
        test_runtime(),
    );
    
    let result = tool
        .execute(json!({"command": "echo hello"}))
        .await
        .unwrap();
    
    assert!(result.success);
    assert!(result.output.trim().contains("hello"));
    assert!(result.error.is_none());
}

Best Practices

Use descriptive names without prefixes: file_read, not tool_file_read
Provide detailed descriptions with examples in the JSON schema
Return structured output in ToolResult.output (e.g., JSON strings for parseable data)
Always validate inputs before executing. Don’t trust LLM-provided parameters.