The Tool trait defines the interface for all agent capabilities in ZeroClaw. Implement this trait to expose new functions to the LLM for execution during agent loops.
Trait Definition
use async_trait::async_trait;
#[async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn parameters_schema(&self) -> serde_json::Value;
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult>;
// Provided method
fn spec(&self) -> ToolSpec {
ToolSpec {
name: self.name().to_string(),
description: self.description().to_string(),
parameters: self.parameters_schema(),
}
}
}
Required Methods
name
Return the tool name used in LLM function calling.
Stable lowercase identifier (e.g., “shell”, “file_read”, “memory_recall”)
Note: This name is used by the LLM to invoke the tool. Keep it stable across versions.
description
Return a human-readable description for the LLM.
Clear description of what the tool does, used by the LLM to decide when to call it
Example: "Execute a shell command in the workspace directory"
parameters_schema
Return JSON Schema for tool parameters.
JSON Schema object defining required and optional parameters
Example:
{
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Shell command to execute"
},
"timeout": {
"type": "number",
"description": "Timeout in seconds (optional)"
}
},
"required": ["command"]
}
execute
Execute the tool with provided arguments.
args
serde_json::Value
required
Tool arguments as JSON value. Validate against your schema before execution.
ToolResult
anyhow::Result<ToolResult>
pub struct ToolResult {
pub success: bool,
pub output: String,
pub error: Option<String>,
}
Important:
- Validate all inputs before execution
- Never panic - return errors via
ToolResult::error
- Keep execution time reasonable (use timeouts)
- Return structured output when possible
Provided Methods
spec
Get the full tool specification for LLM registration.
pub struct ToolSpec {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
Default: Combines name(), description(), and parameters_schema().
Types
pub struct ToolResult {
pub success: bool,
pub output: String,
pub error: Option<String>,
}
Usage:
- Set
success: true for successful execution
- Put result data in
output as string (JSON if structured)
- Set
success: false and populate error for failures
- The LLM receives this result and can continue reasoning
pub struct ToolSpec {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
Implementation Example
Here’s a complete shell tool implementation with security:
use async_trait::async_trait;
use zeroclaw::tools::traits::{Tool, ToolResult};
use zeroclaw::security::SecurityPolicy;
use zeroclaw::runtime::RuntimeAdapter;
use serde_json::json;
use std::sync::Arc;
use std::time::Duration;
const SHELL_TIMEOUT_SECS: u64 = 60;
const MAX_OUTPUT_BYTES: usize = 1_048_576;
pub struct ShellTool {
security: Arc<SecurityPolicy>,
runtime: Arc<dyn RuntimeAdapter>,
}
impl ShellTool {
pub fn new(security: Arc<SecurityPolicy>, runtime: Arc<dyn RuntimeAdapter>) -> Self {
Self { security, runtime }
}
}
#[async_trait]
impl Tool for ShellTool {
fn name(&self) -> &str {
"shell"
}
fn description(&self) -> &str {
"Execute a shell command in the workspace directory"
}
fn parameters_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Shell command to execute"
}
},
"required": ["command"]
})
}
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
// Extract and validate command
let command = args
.get("command")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'command' parameter"))?;
if command.trim().is_empty() {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some("Command cannot be empty".to_string()),
});
}
// Security check
if !self.security.is_command_allowed(command) {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some("Command blocked by security policy".to_string()),
});
}
// Execute with timeout
let timeout = Duration::from_secs(SHELL_TIMEOUT_SECS);
match tokio::time::timeout(timeout, self.runtime.execute_command(command)).await {
Ok(Ok(output)) => {
let mut output = output;
if output.len() > MAX_OUTPUT_BYTES {
output.truncate(MAX_OUTPUT_BYTES);
output.push_str("\n[Output truncated]");
}
Ok(ToolResult {
success: true,
output,
error: None,
})
}
Ok(Err(e)) => Ok(ToolResult {
success: false,
output: String::new(),
error: Some(format!("Execution failed: {}", e)),
}),
Err(_) => Ok(ToolResult {
success: false,
output: String::new(),
error: Some("Command timed out".to_string()),
}),
}
}
}
Factory Registration
Register your tool in the agent’s tool registry:
// src/tools/mod.rs
pub fn create_tool_registry(
security: Arc<SecurityPolicy>,
runtime: Arc<dyn RuntimeAdapter>,
) -> HashMap<String, Arc<dyn Tool>> {
let mut tools = HashMap::new();
tools.insert(
"shell".to_string(),
Arc::new(ShellTool::new(security.clone(), runtime.clone())) as Arc<dyn Tool>
);
tools.insert(
"file_read".to_string(),
Arc::new(FileReadTool::new(security.clone())) as Arc<dyn Tool>
);
// ... more tools
tools
}
Security Best Practices
Input Validation: Always validate and sanitize inputs. Never trust LLM-generated arguments blindly.
Command Injection: For shell tools, validate commands against blocklists and use proper escaping. Never concatenate user input directly into shell commands.
Timeouts: Implement reasonable timeouts to prevent resource exhaustion from long-running operations.
Output Size: Limit output size to prevent memory issues. Truncate large outputs with clear indicators.
Error Messages: Return helpful error messages in ToolResult.error so the LLM can understand what went wrong and retry intelligently.
String Parameter
let value = args
.get("param_name")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'param_name'"))?;
Optional String Parameter
let value = args
.get("param_name")
.and_then(|v| v.as_str())
.map(ToString::to_string);
Number Parameter
let value = args
.get("param_name")
.and_then(|v| v.as_f64())
.unwrap_or(10.0);
Boolean Parameter
let value = args
.get("param_name")
.and_then(|v| v.as_bool())
.unwrap_or(false);
Object Parameter
let obj = args
.get("param_name")
.and_then(|v| v.as_object())
.ok_or_else(|| anyhow::anyhow!("Missing object parameter"))?;
Testing
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_tool_execution() {
let tool = MyTool::new();
let args = serde_json::json!({
"param": "value"
});
let result = tool.execute(args).await.unwrap();
assert!(result.success);
assert!(!result.output.is_empty());
}
#[test]
fn test_tool_spec() {
let tool = MyTool::new();
let spec = tool.spec();
assert_eq!(spec.name, "my_tool");
assert!(!spec.description.is_empty());
assert_eq!(spec.parameters["type"], "object");
}
}