Skip to main content

bamboo_server_tools/skill_runtime/
load_skill.rs

1use std::sync::Arc;
2
3use async_trait::async_trait;
4use serde::Deserialize;
5use serde_json::json;
6use tokio::sync::RwLock;
7
8use bamboo_llm::Config;
9use bamboo_skills::access_control;
10use bamboo_skills::resource_helpers::list_skill_resource_paths;
11use bamboo_skills::SkillManager;
12
13use bamboo_agent_core::tools::{Tool, ToolError, ToolExecutionContext, ToolResult};
14
15use super::{skill_access_error_to_tool_error, SkillToolAccess};
16
17#[derive(Debug, Deserialize)]
18struct LoadSkillArgs {
19    skill_id: String,
20}
21
22pub struct LoadSkillTool {
23    access: SkillToolAccess,
24}
25
26impl LoadSkillTool {
27    pub fn new(
28        skill_manager: Arc<SkillManager>,
29        config: Arc<RwLock<Config>>,
30        session_repo: bamboo_engine::SessionRepository,
31    ) -> Self {
32        Self {
33            access: SkillToolAccess::new(skill_manager, config, session_repo),
34        }
35    }
36}
37
38#[async_trait]
39impl Tool for LoadSkillTool {
40    fn name(&self) -> &str {
41        "load_skill"
42    }
43
44    fn description(&self) -> &str {
45        "Load a skill's detailed SKILL.md instructions by skill_id."
46    }
47
48    fn parameters_schema(&self) -> serde_json::Value {
49        json!({
50            "type": "object",
51            "properties": {
52                "skill_id": {
53                    "type": "string",
54                    "description": "Skill ID from the advertised skill list (for example: skill-creator)."
55                }
56            },
57            "required": ["skill_id"]
58        })
59    }
60
61    async fn execute(&self, args: serde_json::Value) -> Result<ToolResult, ToolError> {
62        self.execute_with_context(args, ToolExecutionContext::none("tool_call"))
63            .await
64    }
65
66    async fn execute_with_context(
67        &self,
68        args: serde_json::Value,
69        ctx: ToolExecutionContext<'_>,
70    ) -> Result<ToolResult, ToolError> {
71        let parsed: LoadSkillArgs = serde_json::from_value(args).map_err(|err| {
72            ToolError::InvalidArguments(format!("Invalid load_skill args: {err}"))
73        })?;
74        let skill_id = parsed.skill_id.trim();
75        if skill_id.is_empty() {
76            return Err(ToolError::InvalidArguments(
77                "skill_id must be a non-empty string".to_string(),
78            ));
79        }
80
81        access_control::ensure_skill_allowed(&self.access, skill_id, ctx.session_id)
82            .await
83            .map_err(skill_access_error_to_tool_error)?;
84        let skill_mode = access_control::selected_skill_mode(&self.access, ctx.session_id).await;
85
86        let skill = self
87            .access
88            .skill_manager
89            .store()
90            .get_skill_for_mode(skill_id, skill_mode.as_deref())
91            .await
92            .map_err(|err| {
93                ToolError::Execution(format!("Failed to load skill '{skill_id}': {err}"))
94            })?;
95        let skill_root = self
96            .access
97            .skill_root(skill_id, skill_mode.as_deref())
98            .await?;
99        let resources = list_skill_resource_paths(&skill_root).map_err(|err| {
100            ToolError::Execution(format!("Failed to list skill resources: {err}"))
101        })?;
102        let canonical_skill_root = tokio::fs::canonicalize(&skill_root)
103            .await
104            .unwrap_or(skill_root);
105        access_control::mark_skill_loaded(&self.access, skill_id, ctx.session_id)
106            .await
107            .map_err(skill_access_error_to_tool_error)?;
108
109        Ok(ToolResult {
110            success: true,
111            result: json!({
112                "skill_id": skill.id,
113                "name": skill.name,
114                "description": skill.description,
115                "license": skill.license,
116                "compatibility": skill.compatibility,
117                "allowed_tools": skill.tool_refs,
118                "instructions": skill.prompt,
119                "skill_base_dir": bamboo_config::paths::path_to_display_string(&canonical_skill_root),
120                "resource_files": resources
121            })
122            .to_string(),
123            display_preference: Some("Collapsible".to_string()),
124            images: Vec::new(),
125        })
126    }
127}