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, ToolCtx, ToolError, ToolOutcome, 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 invoke(
62        &self,
63        args: serde_json::Value,
64        ctx: ToolCtx,
65    ) -> Result<ToolOutcome, ToolError> {
66        let parsed: LoadSkillArgs = serde_json::from_value(args).map_err(|err| {
67            ToolError::InvalidArguments(format!("Invalid load_skill args: {err}"))
68        })?;
69        let skill_id = parsed.skill_id.trim();
70        if skill_id.is_empty() {
71            return Err(ToolError::InvalidArguments(
72                "skill_id must be a non-empty string".to_string(),
73            ));
74        }
75
76        access_control::ensure_skill_allowed(&self.access, skill_id, ctx.session_id())
77            .await
78            .map_err(skill_access_error_to_tool_error)?;
79        let skill_mode = access_control::selected_skill_mode(&self.access, ctx.session_id()).await;
80
81        let skill = self
82            .access
83            .skill_manager
84            .store()
85            .get_skill_for_mode(skill_id, skill_mode.as_deref())
86            .await
87            .map_err(|err| {
88                ToolError::Execution(format!("Failed to load skill '{skill_id}': {err}"))
89            })?;
90        let skill_root = self
91            .access
92            .skill_root(skill_id, skill_mode.as_deref())
93            .await?;
94        let resources = list_skill_resource_paths(&skill_root).map_err(|err| {
95            ToolError::Execution(format!("Failed to list skill resources: {err}"))
96        })?;
97        let canonical_skill_root = tokio::fs::canonicalize(&skill_root)
98            .await
99            .unwrap_or(skill_root);
100        access_control::mark_skill_loaded(&self.access, skill_id, ctx.session_id())
101            .await
102            .map_err(skill_access_error_to_tool_error)?;
103
104        Ok(ToolOutcome::Completed(ToolResult {
105            success: true,
106            result: json!({
107                "skill_id": skill.id,
108                "name": skill.name,
109                "description": skill.description,
110                "license": skill.license,
111                "compatibility": skill.compatibility,
112                "allowed_tools": skill.tool_refs,
113                "instructions": skill.prompt,
114                "skill_base_dir": bamboo_config::paths::path_to_display_string(&canonical_skill_root),
115                "resource_files": resources
116            })
117            .to_string(),
118            display_preference: Some("Collapsible".to_string()),
119            images: Vec::new(),
120        }))
121    }
122}