Skip to main content

telos_agent/tools/builtin/
skill.rs

1use async_trait::async_trait;
2use serde_json::{Value, json};
3use std::sync::Arc;
4
5use crate::error::AgentError;
6use crate::knowledge::skills::SkillRegistry;
7use crate::tools::api::{PermissionDecision, Tool, ToolContext, ToolDefinition, ToolOutput};
8
9/// Tool that lets the model invoke a user-defined skill by name.
10/// The skill's prompt (with {{args}} substituted) is returned to the model.
11pub struct SkillTool {
12    registry: Arc<SkillRegistry>,
13}
14
15impl SkillTool {
16    pub fn new(registry: Arc<SkillRegistry>) -> Self {
17        Self { registry }
18    }
19}
20
21#[async_trait]
22impl Tool for SkillTool {
23    fn definition(&self) -> ToolDefinition {
24        ToolDefinition {
25            name: "Skill".into(),
26            description: "Invoke a user-defined skill by name. Returns the skill's prompt for the model to follow.".into(),
27            input_schema: json!({
28                "type": "object",
29                "properties": {
30                    "skill": { "type": "string", "description": "The name of the skill to invoke" },
31                    "args": { "type": "string", "description": "Optional arguments to pass to the skill" }
32                },
33                "required": ["skill"]
34            }),
35        }
36    }
37
38    fn prompt_text(&self) -> Option<&'static str> {
39        Some(
40            "Use the Skill tool to invoke loaded skills by name. Only invoke skills that were explicitly listed in the prompt or recommended via system reminders; do not guess. \
41Pass `args` when the skill expects arguments. The skill returns its prompt and body for you to follow.",
42        )
43    }
44
45    async fn check_permission(
46        &self,
47        _arguments: &Value,
48        _context: &ToolContext,
49    ) -> Result<PermissionDecision, AgentError> {
50        Ok(PermissionDecision::Allow)
51    }
52
53    fn is_concurrency_safe(&self, _arguments: &Value) -> bool {
54        true
55    }
56
57    async fn invoke(
58        &self,
59        arguments: Value,
60        _context: ToolContext,
61    ) -> Result<ToolOutput, AgentError> {
62        let skill_name = arguments
63            .get("skill")
64            .and_then(|v| v.as_str())
65            .ok_or_else(|| AgentError::Validation("missing string `skill`".into()))?;
66        let skill = self
67            .registry
68            .get(skill_name)
69            .ok_or_else(|| AgentError::ToolNotFound(format!("skill `{skill_name}` not found")))?;
70        let args = arguments.get("args").and_then(|v| v.as_str()).unwrap_or("");
71        let rendered_prompt = skill.prompt.replace("{{args}}", args);
72        let full = format!("{}\n\n---\n\n{}", rendered_prompt, skill.body);
73        Ok(ToolOutput::json(json!({
74            "text": full,
75            "skill_name": skill_name,
76            "skill_description": skill.description,
77        })))
78    }
79}