Skip to main content

thulp_workspace/
lib.rs

1//! # thulp-workspace
2//!
3//! Workspace and session management for thulp.
4//!
5//! This crate provides functionality for managing agent workspaces,
6//! including context, state, and session persistence.
7//!
8//! ## Features
9//!
10//! - **Workspace Management**: Create, load, and manage workspaces with metadata and context
11//! - **Session Management**: Track conversation history, tool calls, and skill executions
12//! - **Turn Counting**: Monitor conversation turns with configurable limits
13//! - **Persistence**: File-based storage for sessions with in-memory caching
14//! - **Filtering**: Query sessions by status, type, tags, and timestamps
15//!
16//! ## Example
17//!
18//! ```ignore
19//! use thulp_workspace::{Workspace, SessionManager, SessionType, SessionFilter};
20//! use std::path::PathBuf;
21//!
22//! #[tokio::main]
23//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
24//!     // Create a workspace
25//!     let workspace = Workspace::new("my-workspace", "My Workspace", PathBuf::from("."));
26//!
27//!     // Create a session manager
28//!     let manager = SessionManager::new(&workspace).await?;
29//!
30//!     // Create a session
31//!     let session = manager.create_session("Chat Session", SessionType::Conversation {
32//!         purpose: "User assistance".to_string(),
33//!     }).await?;
34//!
35//!     // Add entries
36//!     manager.add_entry(
37//!         session.id(),
38//!         EntryType::UserMessage,
39//!         serde_json::json!({"text": "Hello!"}),
40//!     ).await?;
41//!
42//!     // Query sessions
43//!     let active_sessions = manager.find_by_status(SessionStatus::Active).await?;
44//!
45//!     Ok(())
46//! }
47//! ```
48
49pub mod filter;
50pub mod session;
51pub mod session_manager;
52
53pub use filter::SessionFilter;
54pub use session::{
55    EntryType, LimitAction, LimitCheck, LimitExceeded, Session, SessionConfig, SessionEntry,
56    SessionId, SessionMetadata, SessionStatus, SessionType, Timestamp,
57};
58pub use session_manager::SessionManager;
59
60use serde::{Deserialize, Serialize};
61use serde_json::Value;
62use std::collections::HashMap;
63use std::path::PathBuf;
64
65use std::fs;
66use std::path::Path;
67
68/// Result type for workspace operations
69pub type Result<T> = std::result::Result<T, WorkspaceError>;
70
71/// Errors that can occur in workspace operations
72#[derive(Debug, thiserror::Error)]
73pub enum WorkspaceError {
74    #[error("IO error: {0}")]
75    Io(#[from] std::io::Error),
76
77    #[error("Serialization error: {0}")]
78    Serialization(String),
79
80    #[error("Workspace not found: {0}")]
81    NotFound(String),
82}
83
84/// A workspace for an agent session
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct Workspace {
87    /// Workspace ID
88    pub id: String,
89
90    /// Workspace name
91    pub name: String,
92
93    /// Root directory path
94    pub root: PathBuf,
95
96    /// Workspace metadata
97    #[serde(default)]
98    pub metadata: HashMap<String, String>,
99
100    /// Context data
101    #[serde(default)]
102    pub context: HashMap<String, Value>,
103}
104
105impl Workspace {
106    /// Create a new workspace
107    pub fn new(id: impl Into<String>, name: impl Into<String>, root: PathBuf) -> Self {
108        Self {
109            id: id.into(),
110            name: name.into(),
111            root,
112            metadata: HashMap::new(),
113            context: HashMap::new(),
114        }
115    }
116
117    /// Set metadata
118    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
119        self.metadata.insert(key.into(), value.into());
120        self
121    }
122
123    /// Set context data
124    pub fn with_context(mut self, key: impl Into<String>, value: Value) -> Self {
125        self.context.insert(key.into(), value);
126        self
127    }
128
129    /// Get context value
130    pub fn get_context(&self, key: &str) -> Option<&Value> {
131        self.context.get(key)
132    }
133
134    /// Get metadata value
135    pub fn get_metadata(&self, key: &str) -> Option<&String> {
136        self.metadata.get(key)
137    }
138}
139
140impl Workspace {
141    /// Save the workspace to a JSON file
142    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
143        let json = serde_json::to_string_pretty(self)
144            .map_err(|e| WorkspaceError::Serialization(e.to_string()))?;
145        fs::write(path, json).map_err(WorkspaceError::Io)?;
146        Ok(())
147    }
148
149    /// Load a workspace from a JSON file
150    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
151        let json = fs::read_to_string(path).map_err(WorkspaceError::Io)?;
152        let workspace = serde_json::from_str(&json)
153            .map_err(|e| WorkspaceError::Serialization(e.to_string()))?;
154        Ok(workspace)
155    }
156}
157
158/// Manager for multiple workspaces
159#[derive(Debug, Default)]
160pub struct WorkspaceManager {
161    workspaces: HashMap<String, Workspace>,
162    active_workspace: Option<String>,
163}
164
165impl WorkspaceManager {
166    /// Create a new workspace manager
167    pub fn new() -> Self {
168        Self::default()
169    }
170
171    /// Create and register a new workspace
172    pub fn create(&mut self, workspace: Workspace) {
173        self.workspaces.insert(workspace.id.clone(), workspace);
174    }
175
176    /// Get a workspace by ID
177    pub fn get(&self, id: &str) -> Option<&Workspace> {
178        self.workspaces.get(id)
179    }
180
181    /// Get a mutable workspace by ID
182    pub fn get_mut(&mut self, id: &str) -> Option<&mut Workspace> {
183        self.workspaces.get_mut(id)
184    }
185
186    /// Set the active workspace
187    pub fn set_active(&mut self, id: &str) -> Result<()> {
188        if !self.workspaces.contains_key(id) {
189            return Err(WorkspaceError::NotFound(id.to_string()));
190        }
191        self.active_workspace = Some(id.to_string());
192        Ok(())
193    }
194
195    /// Get the active workspace
196    pub fn get_active(&self) -> Option<&Workspace> {
197        self.active_workspace
198            .as_ref()
199            .and_then(|id| self.workspaces.get(id))
200    }
201
202    /// Get the active workspace mutably
203    pub fn get_active_mut(&mut self) -> Option<&mut Workspace> {
204        self.active_workspace
205            .as_ref()
206            .and_then(|id| self.workspaces.get_mut(id))
207    }
208
209    /// List all workspace IDs
210    pub fn list(&self) -> Vec<String> {
211        self.workspaces.keys().cloned().collect()
212    }
213
214    /// Remove a workspace
215    pub fn remove(&mut self, id: &str) -> Option<Workspace> {
216        if self.active_workspace.as_deref() == Some(id) {
217            self.active_workspace = None;
218        }
219        self.workspaces.remove(id)
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_workspace_creation() {
229        let workspace = Workspace::new("test", "Test Workspace", PathBuf::from("/tmp/test"));
230        assert_eq!(workspace.id, "test");
231        assert_eq!(workspace.name, "Test Workspace");
232    }
233
234    #[test]
235    fn test_workspace_builder() {
236        let workspace = Workspace::new("test", "Test", PathBuf::from("/tmp"))
237            .with_metadata("version", "1.0")
238            .with_context("key", serde_json::json!({"value": 42}));
239
240        assert_eq!(workspace.get_metadata("version"), Some(&"1.0".to_string()));
241        assert!(workspace.get_context("key").is_some());
242    }
243
244    #[test]
245    fn test_workspace_manager() {
246        let mut manager = WorkspaceManager::new();
247
248        let workspace = Workspace::new("test", "Test", PathBuf::from("/tmp"));
249        manager.create(workspace);
250
251        assert!(manager.get("test").is_some());
252        assert_eq!(manager.list().len(), 1);
253    }
254
255    #[test]
256    fn test_active_workspace() {
257        let mut manager = WorkspaceManager::new();
258
259        let workspace = Workspace::new("test", "Test", PathBuf::from("/tmp"));
260        manager.create(workspace);
261
262        manager.set_active("test").unwrap();
263        assert!(manager.get_active().is_some());
264        assert_eq!(manager.get_active().unwrap().id, "test");
265    }
266
267    #[test]
268    fn test_remove_workspace() {
269        let mut manager = WorkspaceManager::new();
270
271        let workspace = Workspace::new("test", "Test", PathBuf::from("/tmp"));
272        manager.create(workspace);
273
274        manager.set_active("test").unwrap();
275        assert!(manager.remove("test").is_some());
276        assert!(manager.get_active().is_none());
277    }
278
279    #[test]
280    fn test_workspace_save_load() {
281        let workspace = Workspace::new("test", "Test Workspace", PathBuf::from("/tmp/test"))
282            .with_metadata("version", "1.0")
283            .with_context("key", serde_json::json!({"value": 42}));
284
285        let temp_file = tempfile::NamedTempFile::new().unwrap();
286        let path = temp_file.path();
287
288        // Save workspace
289        workspace.save_to_file(path).unwrap();
290
291        // Load workspace
292        let loaded = Workspace::load_from_file(path).unwrap();
293
294        assert_eq!(workspace.id, loaded.id);
295        assert_eq!(workspace.name, loaded.name);
296        assert_eq!(workspace.metadata, loaded.metadata);
297        assert_eq!(workspace.context, loaded.context);
298    }
299}