Skip to main content

bamboo_engine/server_tools/skill_runtime/
load_skill.rs

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