Skip to main content

smelt_memory/
types.rs

1//! Core types for the memory system
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// Outcome of an episode
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9pub enum EpisodeOutcome {
10    /// Successfully completed
11    Success,
12    /// Partially completed
13    Partial,
14    /// Failed
15    Failure,
16}
17
18impl EpisodeOutcome {
19    /// Convert to a numeric value for scoring
20    pub fn score(&self) -> f64 {
21        match self {
22            EpisodeOutcome::Success => 1.0,
23            EpisodeOutcome::Partial => 0.5,
24            EpisodeOutcome::Failure => 0.0,
25        }
26    }
27}
28
29/// An episode captures a coding experience for future reference
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct Episode {
32    /// Unique identifier
33    pub id: Uuid,
34
35    /// When the episode was captured
36    pub created_at: DateTime<Utc>,
37
38    /// Project this episode belongs to
39    pub project: Option<String>,
40
41    /// Brief summary of what was accomplished
42    pub summary: String,
43
44    /// Type of task (bugfix, feature, refactor, etc.)
45    pub task_type: String,
46
47    /// Outcome of the episode
48    pub outcome: EpisodeOutcome,
49
50    /// Files that were modified
51    pub files_modified: Vec<String>,
52
53    /// Errors encountered and how they were resolved
54    pub errors_resolved: Vec<ErrorResolution>,
55
56    /// Domain tags for categorization
57    pub tags: Vec<String>,
58
59    /// Associated intent ID (if any)
60    pub intent_id: Option<Uuid>,
61
62    /// Associated delta ID (if any)
63    pub delta_id: Option<Uuid>,
64
65    /// Git commit SHA (if committed)
66    pub commit_sha: Option<String>,
67
68    /// Utility score (updated by feedback and propagation)
69    pub utility: f64,
70
71    /// Number of times this episode was helpful
72    pub helpful_count: u32,
73
74    /// Total number of feedback events
75    pub feedback_count: u32,
76}
77
78impl Episode {
79    /// Create a new episode
80    pub fn new(summary: String, task_type: String, outcome: EpisodeOutcome) -> Self {
81        Self {
82            id: Uuid::new_v4(),
83            created_at: Utc::now(),
84            project: None,
85            summary,
86            task_type,
87            outcome,
88            files_modified: Vec::new(),
89            errors_resolved: Vec::new(),
90            tags: Vec::new(),
91            intent_id: None,
92            delta_id: None,
93            commit_sha: None,
94            utility: outcome.score(),
95            helpful_count: 0,
96            feedback_count: 0,
97        }
98    }
99
100    /// Set the project
101    pub fn with_project(mut self, project: String) -> Self {
102        self.project = Some(project);
103        self
104    }
105
106    /// Set files modified
107    pub fn with_files(mut self, files: Vec<String>) -> Self {
108        self.files_modified = files;
109        self
110    }
111
112    /// Set errors resolved
113    pub fn with_errors(mut self, errors: Vec<ErrorResolution>) -> Self {
114        self.errors_resolved = errors;
115        self
116    }
117
118    /// Set tags
119    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
120        self.tags = tags;
121        self
122    }
123
124    /// Set intent ID
125    pub fn with_intent(mut self, intent_id: Uuid) -> Self {
126        self.intent_id = Some(intent_id);
127        self
128    }
129
130    /// Set delta ID
131    pub fn with_delta(mut self, delta_id: Uuid) -> Self {
132        self.delta_id = Some(delta_id);
133        self
134    }
135
136    /// Set commit SHA
137    pub fn with_commit(mut self, sha: String) -> Self {
138        self.commit_sha = Some(sha);
139        self
140    }
141
142    /// Build text representation for embedding
143    pub fn to_embedding_text(&self) -> String {
144        let mut parts = Vec::new();
145
146        parts.push(self.summary.clone());
147        parts.push(format!("Task: {}", self.task_type));
148
149        if !self.tags.is_empty() {
150            parts.push(format!("Tags: {}", self.tags.join(", ")));
151        }
152
153        if !self.files_modified.is_empty() {
154            // Include file names without full paths for better matching
155            let file_names: Vec<&str> = self
156                .files_modified
157                .iter()
158                .filter_map(|f| f.split('/').next_back())
159                .collect();
160            parts.push(format!("Files: {}", file_names.join(", ")));
161        }
162
163        for error in &self.errors_resolved {
164            parts.push(format!("Error: {} -> {}", error.error, error.resolution));
165        }
166
167        parts.join(". ")
168    }
169}
170
171/// An error that was resolved during the episode
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct ErrorResolution {
174    /// The error message or description
175    pub error: String,
176    /// How the error was resolved
177    pub resolution: String,
178}
179
180/// Feedback on an episode's helpfulness
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct Feedback {
183    /// Episode ID
184    pub episode_id: Uuid,
185    /// When the feedback was given
186    pub timestamp: DateTime<Utc>,
187    /// Whether the episode was helpful
188    pub helpful: bool,
189}
190
191/// An episode with retrieval ranking information
192#[derive(Debug, Clone)]
193pub struct RankedEpisode {
194    /// The episode
195    pub episode: Episode,
196    /// Semantic similarity score (0.0 - 1.0)
197    pub similarity: f64,
198    /// Combined ranking score
199    pub score: f64,
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn test_episode_creation() {
208        let episode = Episode::new(
209            "Fixed authentication bug".to_string(),
210            "bugfix".to_string(),
211            EpisodeOutcome::Success,
212        );
213
214        assert_eq!(episode.summary, "Fixed authentication bug");
215        assert_eq!(episode.task_type, "bugfix");
216        assert_eq!(episode.outcome, EpisodeOutcome::Success);
217        assert_eq!(episode.utility, 1.0);
218    }
219
220    #[test]
221    fn test_episode_builder() {
222        let episode = Episode::new(
223            "Added user profile feature".to_string(),
224            "feature".to_string(),
225            EpisodeOutcome::Success,
226        )
227        .with_project("my-app".to_string())
228        .with_tags(vec!["auth".to_string(), "user".to_string()])
229        .with_files(vec!["src/user.rs".to_string()]);
230
231        assert_eq!(episode.project, Some("my-app".to_string()));
232        assert_eq!(episode.tags.len(), 2);
233        assert_eq!(episode.files_modified.len(), 1);
234    }
235
236    #[test]
237    fn test_embedding_text() {
238        let episode = Episode::new(
239            "Fixed login timeout".to_string(),
240            "bugfix".to_string(),
241            EpisodeOutcome::Success,
242        )
243        .with_tags(vec!["auth".to_string()])
244        .with_files(vec!["src/auth/login.rs".to_string()])
245        .with_errors(vec![ErrorResolution {
246            error: "Connection timeout".to_string(),
247            resolution: "Increased timeout to 30s".to_string(),
248        }]);
249
250        let text = episode.to_embedding_text();
251        assert!(text.contains("Fixed login timeout"));
252        assert!(text.contains("bugfix"));
253        assert!(text.contains("auth"));
254        assert!(text.contains("login.rs"));
255        assert!(text.contains("Connection timeout"));
256    }
257
258    #[test]
259    fn test_outcome_score() {
260        assert_eq!(EpisodeOutcome::Success.score(), 1.0);
261        assert_eq!(EpisodeOutcome::Partial.score(), 0.5);
262        assert_eq!(EpisodeOutcome::Failure.score(), 0.0);
263    }
264}