bamboo_server_tools/skill_runtime/
load_skill.rs1use 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}