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