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