Skip to main content

bamboo_agent/agent/skill/
types.rs

1//! Skill types and shared data structures.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6/// Unique identifier for a skill (kebab-case)
7pub type SkillId = String;
8
9/// Visibility level for a skill
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11#[serde(rename_all = "snake_case")]
12#[derive(Default)]
13pub enum SkillVisibility {
14    /// Visible to all users
15    #[default]
16    Public,
17    /// Private to the creator
18    Private,
19}
20
21/// Complete definition of a skill
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct SkillDefinition {
24    /// Unique identifier (kebab-case)
25    pub id: SkillId,
26
27    /// Display name
28    pub name: String,
29
30    /// Human-readable description
31    pub description: String,
32
33    /// Category for grouping
34    pub category: String,
35
36    /// Searchable tags
37    #[serde(default)]
38    pub tags: Vec<String>,
39
40    /// Prompt fragment injected into system prompt
41    pub prompt: String,
42
43    /// Built-in tool references (format: "tool")
44    #[serde(default)]
45    pub tool_refs: Vec<String>,
46
47    /// Associated workflow names
48    #[serde(default)]
49    pub workflow_refs: Vec<String>,
50
51    /// Visibility level
52    #[serde(default)]
53    pub visibility: SkillVisibility,
54
55    /// Semantic version
56    #[serde(default = "default_version")]
57    pub version: String,
58
59    /// Creation timestamp
60    pub created_at: DateTime<Utc>,
61
62    /// Last update timestamp
63    pub updated_at: DateTime<Utc>,
64}
65
66fn default_version() -> String {
67    "1.0.0".to_string()
68}
69
70impl SkillDefinition {
71    /// Create a new skill definition with generated timestamp
72    pub fn new(
73        id: impl Into<String>,
74        name: impl Into<String>,
75        description: impl Into<String>,
76        category: impl Into<String>,
77        prompt: impl Into<String>,
78    ) -> Self {
79        let now = Utc::now();
80        Self {
81            id: id.into(),
82            name: name.into(),
83            description: description.into(),
84            category: category.into(),
85            tags: Vec::new(),
86            prompt: prompt.into(),
87            tool_refs: Vec::new(),
88            workflow_refs: Vec::new(),
89            visibility: SkillVisibility::default(),
90            version: default_version(),
91            created_at: now,
92            updated_at: now,
93        }
94    }
95
96    /// Add a tool reference
97    pub fn with_tool_ref(mut self, tool_ref: impl Into<String>) -> Self {
98        self.tool_refs.push(tool_ref.into());
99        self
100    }
101
102    /// Add a workflow reference
103    pub fn with_workflow_ref(mut self, workflow_ref: impl Into<String>) -> Self {
104        self.workflow_refs.push(workflow_ref.into());
105        self
106    }
107
108    /// Add a tag
109    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
110        self.tags.push(tag.into());
111        self
112    }
113
114    /// Set visibility
115    pub fn with_visibility(mut self, visibility: SkillVisibility) -> Self {
116        self.visibility = visibility;
117        self
118    }
119
120    /// Update the timestamp
121    pub fn touch(&mut self) {
122        self.updated_at = Utc::now();
123    }
124
125    /// Check if this is a built-in skill (based on id prefix or version)
126    pub fn is_builtin(&self) -> bool {
127        self.id.starts_with("builtin-") || self.id.starts_with("system-")
128    }
129}
130
131/// Configuration for skill store persistence
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct SkillStoreConfig {
134    pub skills_dir: std::path::PathBuf,
135}
136
137impl Default for SkillStoreConfig {
138    fn default() -> Self {
139        let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
140        Self {
141            skills_dir: home.join(".bamboo").join("skills"),
142        }
143    }
144}
145
146/// Filter options for listing skills
147#[derive(Debug, Clone, Default)]
148pub struct SkillFilter {
149    /// Filter by category
150    pub category: Option<String>,
151
152    /// Filter by tags (any match)
153    pub tags: Vec<String>,
154
155    /// Search in name and description
156    pub search: Option<String>,
157
158    /// Filter by visibility
159    pub visibility: Option<SkillVisibility>,
160}
161
162impl SkillFilter {
163    /// Create a new empty filter
164    pub fn new() -> Self {
165        Self::default()
166    }
167
168    /// Filter by category
169    pub fn with_category(mut self, category: impl Into<String>) -> Self {
170        self.category = Some(category.into());
171        self
172    }
173
174    /// Add a tag filter
175    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
176        self.tags.push(tag.into());
177        self
178    }
179
180    /// Set search query
181    pub fn with_search(mut self, search: impl Into<String>) -> Self {
182        self.search = Some(search.into());
183        self
184    }
185
186    /// Check if a skill matches this filter
187    pub fn matches(&self, skill: &SkillDefinition) -> bool {
188        if let Some(ref category) = self.category {
189            if skill.category != *category {
190                return false;
191            }
192        }
193
194        if !self.tags.is_empty() {
195            let has_matching_tag = self.tags.iter().any(|tag| skill.tags.contains(tag));
196            if !has_matching_tag {
197                return false;
198            }
199        }
200
201        if let Some(ref search) = self.search {
202            let search_lower = search.to_lowercase();
203            if !skill.name.to_lowercase().contains(&search_lower)
204                && !skill.description.to_lowercase().contains(&search_lower)
205            {
206                return false;
207            }
208        }
209
210        if let Some(ref visibility) = self.visibility {
211            if skill.visibility != *visibility {
212                return false;
213            }
214        }
215
216        true
217    }
218}
219
220/// Error types for skill operations
221#[derive(Debug, thiserror::Error)]
222pub enum SkillError {
223    #[error("Skill not found: {0}")]
224    NotFound(SkillId),
225
226    #[error("Skill already exists: {0}")]
227    AlreadyExists(SkillId),
228
229    #[error("Invalid skill ID: {0}")]
230    InvalidId(String),
231
232    #[error("Validation error: {0}")]
233    Validation(String),
234
235    #[error("Storage error: {0}")]
236    Storage(String),
237
238    #[error("Read-only: {0}")]
239    ReadOnly(String),
240
241    #[error("IO error: {0}")]
242    Io(#[from] std::io::Error),
243
244    #[error("YAML error: {0}")]
245    Yaml(#[from] serde_yaml::Error),
246}
247
248/// Result type for skill operations
249pub type SkillResult<T> = Result<T, SkillError>;