zeroclaw/src/tools/traits.rs
Chummy 49fcc7a2c4
test: deepen and complete project-wide test coverage (#297)
* test: deepen coverage for health doctor provider and tunnels

* test: add broad trait and module re-export coverage
2026-02-16 05:58:24 -05:00

121 lines
3.2 KiB
Rust

use async_trait::async_trait;
use serde::{Deserialize, Serialize};
/// Result of a tool execution
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
pub success: bool,
pub output: String,
pub error: Option<String>,
}
/// Description of a tool for the LLM
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolSpec {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
/// Core tool trait — implement for any capability
#[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) -> serde_json::Value;
/// Execute the tool with given arguments
async fn execute(&self, args: serde_json::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(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct DummyTool;
#[async_trait]
impl Tool for DummyTool {
fn name(&self) -> &str {
"dummy_tool"
}
fn description(&self) -> &str {
"A deterministic test tool"
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"value": { "type": "string" }
}
})
}
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
Ok(ToolResult {
success: true,
output: args
.get("value")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_string(),
error: None,
})
}
}
#[test]
fn spec_uses_tool_metadata_and_schema() {
let tool = DummyTool;
let spec = tool.spec();
assert_eq!(spec.name, "dummy_tool");
assert_eq!(spec.description, "A deterministic test tool");
assert_eq!(spec.parameters["type"], "object");
assert_eq!(spec.parameters["properties"]["value"]["type"], "string");
}
#[tokio::test]
async fn execute_returns_expected_output() {
let tool = DummyTool;
let result = tool
.execute(serde_json::json!({ "value": "hello-tool" }))
.await
.unwrap();
assert!(result.success);
assert_eq!(result.output, "hello-tool");
assert!(result.error.is_none());
}
#[test]
fn tool_result_serialization_roundtrip() {
let result = ToolResult {
success: false,
output: String::new(),
error: Some("boom".into()),
};
let json = serde_json::to_string(&result).unwrap();
let parsed: ToolResult = serde_json::from_str(&json).unwrap();
assert!(!parsed.success);
assert_eq!(parsed.error.as_deref(), Some("boom"));
}
}