bamboo_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 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}