use std::path::{Path, PathBuf};
use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDefinition {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
}
impl ToolDefinition {
pub fn new(name: impl Into<String>, description: impl Into<String>, input_schema: serde_json::Value) -> Self {
Self {
name: name.into(),
description: description.into(),
input_schema,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillResult {
pub output: String,
pub artifacts_created: Vec<PathBuf>,
pub tool_calls_made: usize,
pub tokens_used: u64,
}
impl SkillResult {
pub fn success(output: impl Into<String>) -> Self {
Self {
output: output.into(),
artifacts_created: vec![],
tool_calls_made: 0,
tokens_used: 0,
}
}
pub fn with_artifacts(mut self, artifacts: Vec<PathBuf>) -> Self {
self.artifacts_created = artifacts;
self
}
pub fn with_tool_calls(mut self, count: usize) -> Self {
self.tool_calls_made = count;
self
}
pub fn with_tokens(mut self, tokens: u64) -> Self {
self.tokens_used = tokens;
self
}
}
#[async_trait]
pub trait LlmClient: Send + Sync {
async fn run_skill(
&self,
skill_prompt: &str,
tools: Vec<ToolDefinition>,
model: &str,
working_dir: &Path,
max_iterations: usize,
) -> Result<SkillResult>;
async fn chat(&self, prompt: &str, model: &str) -> Result<String> {
let result = self.run_skill(prompt, vec![], model, Path::new("."), 20).await?;
Ok(result.output)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_definition_new() {
let tool = ToolDefinition::new(
"Read",
"Read a file",
serde_json::json!({
"type": "object",
"properties": {
"path": { "type": "string" }
},
"required": ["path"]
}),
);
assert_eq!(tool.name, "Read");
assert_eq!(tool.description, "Read a file");
}
#[test]
fn test_skill_result_builder() {
let result = SkillResult::success("Done")
.with_artifacts(vec![PathBuf::from("output.txt")])
.with_tool_calls(5)
.with_tokens(1000);
assert_eq!(result.output, "Done");
assert_eq!(result.artifacts_created, vec![PathBuf::from("output.txt")]);
assert_eq!(result.tool_calls_made, 5);
assert_eq!(result.tokens_used, 1000);
}
}
#[derive(Debug, Clone)]
pub struct ToolOutput {
pub content: String,
pub is_error: bool,
}
impl ToolOutput {
pub fn success(content: impl Into<String>) -> Self {
Self { content: content.into(), is_error: false }
}
pub fn error(content: impl Into<String>) -> Self {
Self { content: content.into(), is_error: true }
}
}
#[derive(Debug, Clone)]
pub struct Tool {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
}
impl Tool {
pub fn new(name: impl Into<String>, description: impl Into<String>, input_schema: serde_json::Value) -> Self {
Self {
name: name.into(),
description: description.into(),
input_schema,
}
}
}
#[async_trait]
pub trait ToolHandler: Send + Sync {
async fn handle(&self, name: &str, input: &serde_json::Value) -> Result<ToolOutput>;
}