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.
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
From src/tools/traits.rs:5-10:
pub struct ToolResult {
pub success: bool,
pub output: String,
pub error: Option<String>,
}
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>,
}
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>,
}
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"]
})
}
From src/tools/mod.rs:
| Tool | Name | Description |
|---|
| ShellTool | shell | Execute shell commands |
| FileReadTool | file_read | Read file contents |
| FileWriteTool | file_write | Write file contents |
| MemoryStoreTool | memory_store | Save memories |
| MemoryRecallTool | memory_recall | Search memories |
| MemoryForgetTool | memory_forget | Delete memories |
| BrowserOpenTool | browser_open | Open URLs (opt-in) |
| ComposioTool | composio_* | 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
// ...
}
}
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
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 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>
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.