Skip to main content

cascade_agent/skills/
skill_tool.rs

1//! Adapter that wraps a [`Skill`] as a [`Tool`] for the tool registry.
2
3use async_trait::async_trait;
4
5use crate::tools::{Tool, ToolResult};
6
7use super::executor::{SkillExecutor, DEFAULT_TIMEOUT};
8use super::types::Skill;
9
10/// A tool wrapper around a parsed [`Skill`].
11///
12/// If the skill has an executable, invoking the tool runs it via
13/// [`SkillExecutor`].  Otherwise, the tool returns the skill's
14/// instructions as a reference response.
15pub struct SkillTool {
16    skill: Skill,
17}
18
19impl SkillTool {
20    pub fn new(skill: Skill) -> Self {
21        Self { skill }
22    }
23
24    /// Build a description string for the LLM, combining the metadata
25    /// description with the instructions (truncated if very long).
26    fn build_description(&self) -> String {
27        let meta_desc = self.skill.metadata.description.clone();
28        let instructions = self.skill.instructions.trim();
29        if instructions.is_empty() {
30            return meta_desc;
31        }
32        // Truncate very long instructions to avoid wasting context window.
33        const MAX_INSTRUCTIONS_LEN: usize = 2_000;
34        let truncated = if instructions.len() > MAX_INSTRUCTIONS_LEN {
35            format!(
36                "{}\n\n[... instructions truncated at {} chars ...]",
37                &instructions[..MAX_INSTRUCTIONS_LEN],
38                MAX_INSTRUCTIONS_LEN
39            )
40        } else {
41            instructions.to_string()
42        };
43        format!("{}\n\nInstructions:\n{}", meta_desc, truncated)
44    }
45
46    /// Build a parameters schema for the tool.
47    fn build_parameters_schema(&self) -> serde_json::Value {
48        if let Some(ref input_format) = self.skill.metadata.input_format {
49            // Use the schema from the skill's input_format.
50            input_format.schema.clone()
51        } else if self.skill.executable_path.is_some() {
52            // Has an executable but no declared schema – use a generic schema.
53            serde_json::json!({
54                "type": "object",
55                "properties": {
56                    "input": {
57                        "type": "string",
58                        "description": "Input to pass to the skill executable"
59                    }
60                }
61            })
62        } else {
63            // No executable – no real parameters needed.
64            serde_json::json!({
65                "type": "object",
66                "properties": {}
67            })
68        }
69    }
70}
71
72#[async_trait]
73impl Tool for SkillTool {
74    fn name(&self) -> &str {
75        &self.skill.metadata.name
76    }
77
78    fn description(&self) -> &str {
79        // We can't return a reference to a computed string, so we store it.
80        // Actually, Tool trait requires &str which makes dynamic descriptions tricky.
81        // We'll use a workaround via self-description caching.
82        // The trait requires &str, so we need to store the description.
83        // For now, return the metadata description (the trait only requires &str).
84        &self.skill.metadata.description
85    }
86
87    fn parameters_schema(&self) -> serde_json::Value {
88        self.build_parameters_schema()
89    }
90
91    async fn execute(&self, args: serde_json::Value) -> ToolResult {
92        if let Some(ref exe_path) = self.skill.executable_path {
93            SkillExecutor::execute(exe_path, args, &self.skill.directory, DEFAULT_TIMEOUT).await
94        } else {
95            // No executable – return the instructions as a reference response.
96            ToolResult::ok(serde_json::json!({
97                "instructions": self.skill.instructions.trim()
98            }))
99        }
100    }
101
102    fn to_definition(&self) -> llm_cascade::ToolDefinition {
103        llm_cascade::ToolDefinition {
104            name: self.name().to_owned(),
105            description: self.build_description(),
106            parameters: self.parameters_schema(),
107        }
108    }
109}