Skip to main content

bamboo_server_tools/skill_runtime/
mod.rs

1use std::collections::{HashMap, HashSet};
2use std::path::PathBuf;
3use std::sync::Arc;
4
5use async_trait::async_trait;
6use tokio::sync::RwLock;
7
8use bamboo_llm::Config;
9use bamboo_skills::access_control::{SkillAccessError, SkillSessionPort};
10use bamboo_skills::SkillManager;
11
12use bamboo_agent_core::tools::ToolError;
13use bamboo_agent_core::Session;
14
15mod load_skill;
16mod read_resource;
17
18#[cfg(test)]
19mod tests;
20
21pub use load_skill::LoadSkillTool;
22pub use read_resource::ReadSkillResourceTool;
23
24pub(super) const MAX_RESOURCE_CONTENT_CHARS: usize = 50_000;
25
26#[derive(Clone)]
27pub(super) struct SkillToolAccess {
28    pub(super) skill_manager: Arc<SkillManager>,
29    config: Arc<RwLock<Config>>,
30    pub(super) session_repo: bamboo_engine::SessionRepository,
31}
32
33impl SkillToolAccess {
34    pub(super) fn new(
35        skill_manager: Arc<SkillManager>,
36        config: Arc<RwLock<Config>>,
37        session_repo: bamboo_engine::SessionRepository,
38    ) -> Self {
39        Self {
40            skill_manager,
41            config,
42            session_repo,
43        }
44    }
45
46    pub(super) async fn session_for_context(&self, session_id: Option<&str>) -> Option<Session> {
47        self.session_repo.load(session_id?).await
48    }
49
50    pub(super) async fn skill_root(
51        &self,
52        skill_id: &str,
53        skill_mode: Option<&str>,
54    ) -> Result<PathBuf, ToolError> {
55        self.skill_manager
56            .store()
57            .get_skill_root_for_mode(skill_id, skill_mode)
58            .await
59            .map_err(|err| ToolError::Execution(format!("Failed to resolve skill root: {err}")))
60    }
61}
62
63#[async_trait]
64impl SkillSessionPort for SkillToolAccess {
65    async fn load_session_metadata(&self, session_id: &str) -> Option<HashMap<String, String>> {
66        self.session_for_context(Some(session_id))
67            .await
68            .map(|session| session.metadata.clone())
69    }
70
71    async fn save_metadata_updates(
72        &self,
73        session_id: &str,
74        updates: &[(String, Option<String>)],
75    ) -> Result<(), String> {
76        let mut session = self
77            .session_repo
78            .try_load(session_id)
79            .await
80            .map_err(|e| e.to_string())?
81            .ok_or_else(|| format!("Session '{session_id}' not found"))?;
82
83        for (key, value) in updates {
84            if let Some(val) = value {
85                session.metadata.insert(key.clone(), val.clone());
86            } else {
87                session.metadata.remove(key);
88            }
89        }
90
91        self.session_repo
92            .save(&mut session)
93            .await
94            .map_err(|e| e.to_string())?;
95
96        Ok(())
97    }
98
99    async fn disabled_skill_ids(&self) -> HashSet<String> {
100        let config = self.config.read().await;
101        config.disabled_skill_ids().into_iter().collect()
102    }
103}
104
105pub(super) fn skill_access_error_to_tool_error(error: SkillAccessError) -> ToolError {
106    match error {
107        SkillAccessError::NotAllowed(msg)
108        | SkillAccessError::NotLoaded(msg)
109        | SkillAccessError::SessionRequired(msg)
110        | SkillAccessError::SessionNotFound(msg)
111        | SkillAccessError::PersistenceError(msg) => ToolError::Execution(msg),
112    }
113}