Skip to main content

bamboo_engine/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 crate::access_control::{SkillAccessError, SkillSessionPort};
9use crate::SkillManager;
10use bamboo_infrastructure::Config;
11
12use bamboo_agent_core::storage::Storage;
13use bamboo_agent_core::tools::ToolError;
14use bamboo_agent_core::Session;
15use bamboo_infrastructure::LockedSessionStore;
16
17mod load_skill;
18mod read_resource;
19
20#[cfg(test)]
21mod tests;
22
23pub use load_skill::LoadSkillTool;
24pub use read_resource::ReadSkillResourceTool;
25
26pub(super) const MAX_RESOURCE_CONTENT_CHARS: usize = 50_000;
27
28#[derive(Clone)]
29pub(super) struct SkillToolAccess {
30    pub(super) skill_manager: Arc<SkillManager>,
31    config: Arc<RwLock<Config>>,
32    pub(super) sessions: Arc<RwLock<HashMap<String, Session>>>,
33    storage: Arc<dyn Storage>,
34    pub(super) persistence: Arc<LockedSessionStore>,
35}
36
37impl SkillToolAccess {
38    pub(super) fn new(
39        skill_manager: Arc<SkillManager>,
40        config: Arc<RwLock<Config>>,
41        sessions: Arc<RwLock<HashMap<String, Session>>>,
42        storage: Arc<dyn Storage>,
43        persistence: Arc<LockedSessionStore>,
44    ) -> Self {
45        Self {
46            skill_manager,
47            config,
48            sessions,
49            storage,
50            persistence,
51        }
52    }
53
54    pub(super) async fn session_for_context(&self, session_id: Option<&str>) -> Option<Session> {
55        let session_id = session_id?;
56
57        let in_memory = {
58            let sessions = self.sessions.read().await;
59            sessions.get(session_id).cloned()
60        };
61
62        match in_memory {
63            Some(session) => Some(session),
64            None => self.storage.load_session(session_id).await.ok().flatten(),
65        }
66    }
67
68    pub(super) async fn skill_root(
69        &self,
70        skill_id: &str,
71        skill_mode: Option<&str>,
72    ) -> Result<PathBuf, ToolError> {
73        self.skill_manager
74            .store()
75            .get_skill_root_for_mode(skill_id, skill_mode)
76            .await
77            .map_err(|err| ToolError::Execution(format!("Failed to resolve skill root: {err}")))
78    }
79}
80
81#[async_trait]
82impl SkillSessionPort for SkillToolAccess {
83    async fn load_session_metadata(&self, session_id: &str) -> Option<HashMap<String, String>> {
84        self.session_for_context(Some(session_id))
85            .await
86            .map(|session| session.metadata.clone())
87    }
88
89    async fn save_metadata_updates(
90        &self,
91        session_id: &str,
92        updates: &[(String, Option<String>)],
93    ) -> Result<(), String> {
94        let mut session = {
95            let sessions = self.sessions.read().await;
96            sessions.get(session_id).cloned()
97        };
98
99        if session.is_none() {
100            session = self
101                .storage
102                .load_session(session_id)
103                .await
104                .map_err(|e| e.to_string())?;
105        }
106
107        let mut session = session.ok_or_else(|| format!("Session '{session_id}' not found"))?;
108
109        for (key, value) in updates {
110            if let Some(val) = value {
111                session.metadata.insert(key.clone(), val.clone());
112            } else {
113                session.metadata.remove(key);
114            }
115        }
116
117        self.persistence
118            .merge_save_runtime(&mut session)
119            .await
120            .map_err(|e| e.to_string())?;
121
122        let mut sessions = self.sessions.write().await;
123        sessions.insert(session_id.to_string(), session);
124
125        Ok(())
126    }
127
128    async fn disabled_skill_ids(&self) -> HashSet<String> {
129        let config = self.config.read().await;
130        config.disabled_skill_ids().into_iter().collect()
131    }
132}
133
134pub(super) fn skill_access_error_to_tool_error(error: SkillAccessError) -> ToolError {
135    match error {
136        SkillAccessError::NotAllowed(msg)
137        | SkillAccessError::NotLoaded(msg)
138        | SkillAccessError::SessionRequired(msg)
139        | SkillAccessError::SessionNotFound(msg)
140        | SkillAccessError::PersistenceError(msg) => ToolError::Execution(msg),
141    }
142}