telos_agent/tools/builtin/
skill.rs1use 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
9pub 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}