Skip to main content

do_memory_core/episode/
structs.rs

1//! Episode and `ExecutionStep` structs and implementations.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use uuid::Uuid;
7
8use crate::memory::checkpoint::CheckpointMeta;
9use crate::pre_storage::SalientFeatures;
10use crate::types::{ExecutionResult, Reflection, RewardScore, TaskContext, TaskOutcome, TaskType};
11
12/// Records when a pattern was applied during episode execution
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct PatternApplication {
15    /// ID of the pattern that was applied
16    pub pattern_id: PatternId,
17    /// Step number when pattern was applied
18    pub applied_at_step: usize,
19    /// Outcome of applying this pattern
20    pub outcome: ApplicationOutcome,
21    /// Optional notes about the application
22    pub notes: Option<String>,
23}
24
25/// Outcome of applying a pattern
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27pub enum ApplicationOutcome {
28    /// Pattern helped achieve the desired outcome
29    Helped,
30    /// Pattern was applied but had no noticeable effect
31    NoEffect,
32    /// Pattern hindered progress or caused issues
33    Hindered,
34    /// Outcome not yet determined
35    Pending,
36}
37
38impl ApplicationOutcome {
39    /// Check if this outcome counts as a success
40    #[must_use]
41    pub fn is_success(&self) -> bool {
42        matches!(self, ApplicationOutcome::Helped)
43    }
44}
45
46/// Unique identifier for patterns extracted from episodes.
47pub type PatternId = Uuid;
48
49/// A single execution step within an episode.
50///
51/// Represents one discrete action or operation performed during task execution.
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53pub struct ExecutionStep {
54    /// Step number in sequence (1-indexed)
55    pub step_number: usize,
56    /// When this step was executed
57    pub timestamp: DateTime<Utc>,
58    /// Tool or function used
59    pub tool: String,
60    /// Description of action taken
61    pub action: String,
62    /// Input parameters (as JSON)
63    pub parameters: serde_json::Value,
64    /// Result of execution
65    pub result: Option<ExecutionResult>,
66    /// Execution time in milliseconds
67    pub latency_ms: u64,
68    /// Number of tokens used (if applicable)
69    pub tokens_used: Option<usize>,
70    /// Additional metadata
71    pub metadata: HashMap<String, String>,
72}
73
74impl ExecutionStep {
75    /// Create a new execution step with default values.
76    #[must_use]
77    pub fn new(step_number: usize, tool: String, action: String) -> Self {
78        Self {
79            step_number,
80            timestamp: Utc::now(),
81            tool,
82            action,
83            parameters: serde_json::json!({}),
84            result: None,
85            latency_ms: 0,
86            tokens_used: None,
87            metadata: HashMap::new(),
88        }
89    }
90
91    /// Check if this step was successful.
92    #[must_use]
93    pub fn is_success(&self) -> bool {
94        self.result.as_ref().is_some_and(|r| r.is_success())
95    }
96}
97
98/// Complete record of a task execution from start to finish.
99#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
100pub struct Episode {
101    /// Unique episode identifier
102    pub episode_id: Uuid,
103    /// Type of task
104    pub task_type: TaskType,
105    /// Description of the task
106    pub task_description: String,
107    /// Task context and metadata
108    pub context: TaskContext,
109    /// When episode started
110    pub start_time: DateTime<Utc>,
111    /// When episode completed (None if in progress)
112    pub end_time: Option<DateTime<Utc>>,
113    /// Execution steps
114    pub steps: Vec<ExecutionStep>,
115    /// Final outcome
116    pub outcome: Option<TaskOutcome>,
117    /// Reward score
118    pub reward: Option<RewardScore>,
119    /// Reflection on execution
120    pub reflection: Option<Reflection>,
121    /// Extracted pattern IDs
122    pub patterns: Vec<PatternId>,
123    /// Extracted heuristic IDs
124    pub heuristics: Vec<Uuid>,
125    /// Record of patterns applied during execution
126    #[serde(default)]
127    pub applied_patterns: Vec<PatternApplication>,
128    /// Salient features extracted during pre-storage reasoning (`PREMem`)
129    #[serde(default)]
130    pub salient_features: Option<SalientFeatures>,
131    /// Additional metadata
132    pub metadata: HashMap<String, String>,
133    /// Tags for episode categorization (e.g., "bug-fix", "feature", "refactor")
134    #[serde(default)]
135    pub tags: Vec<String>,
136    /// Checkpoints created during episode execution (ADR-044 Feature 3)
137    #[serde(default)]
138    pub checkpoints: Vec<CheckpointMeta>,
139}
140
141impl Episode {
142    /// Create a new episode for a task.
143    #[must_use]
144    pub fn new(task_description: String, context: TaskContext, task_type: TaskType) -> Self {
145        Self {
146            episode_id: Uuid::new_v4(),
147            task_type,
148            task_description,
149            context,
150            start_time: Utc::now(),
151            end_time: None,
152            steps: Vec::new(),
153            outcome: None,
154            reward: None,
155            reflection: None,
156            patterns: Vec::new(),
157            heuristics: Vec::new(),
158            applied_patterns: Vec::new(),
159            salient_features: None,
160            metadata: HashMap::new(),
161            tags: Vec::new(),
162            checkpoints: Vec::new(),
163        }
164    }
165
166    /// Record that a pattern was applied during this episode
167    pub fn record_pattern_application(
168        &mut self,
169        pattern_id: PatternId,
170        applied_at_step: usize,
171        outcome: ApplicationOutcome,
172        notes: Option<String>,
173    ) {
174        self.applied_patterns.push(PatternApplication {
175            pattern_id,
176            applied_at_step,
177            outcome,
178            notes,
179        });
180    }
181
182    /// Check if the episode has been completed.
183    #[must_use]
184    pub fn is_complete(&self) -> bool {
185        self.end_time.is_some() && self.outcome.is_some()
186    }
187
188    /// Get the total duration of the episode.
189    #[must_use]
190    pub fn duration(&self) -> Option<chrono::Duration> {
191        self.end_time.map(|end| end - self.start_time)
192    }
193
194    /// Add a new execution step to this episode.
195    pub fn add_step(&mut self, step: ExecutionStep) {
196        self.steps.push(step);
197    }
198
199    /// Mark the episode as complete with a final outcome.
200    pub fn complete(&mut self, outcome: TaskOutcome) {
201        self.end_time = Some(Utc::now());
202        self.outcome = Some(outcome);
203    }
204
205    /// Count the number of successful execution steps.
206    #[must_use]
207    pub fn successful_steps_count(&self) -> usize {
208        self.steps.iter().filter(|s| s.is_success()).count()
209    }
210
211    /// Count the number of failed execution steps.
212    #[must_use]
213    pub fn failed_steps_count(&self) -> usize {
214        self.steps.iter().filter(|s| !s.is_success()).count()
215    }
216
217    /// Normalize a tag: lowercase, trim whitespace, validate characters
218    fn normalize_tag(tag: &str) -> Result<String, String> {
219        let normalized = tag.trim().to_lowercase();
220
221        if normalized.is_empty() {
222            return Err("Tag cannot be empty".to_string());
223        }
224
225        if normalized.len() < 2 {
226            return Err("Tag must be at least 2 characters long".to_string());
227        }
228
229        if normalized.len() > 100 {
230            return Err("Tag cannot exceed 100 characters".to_string());
231        }
232
233        // Allow alphanumeric, hyphens, underscores
234        if !normalized
235            .chars()
236            .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
237        {
238            return Err(format!(
239                "Tag '{tag}' contains invalid characters. Only alphanumeric, hyphens, and underscores allowed"
240            ));
241        }
242
243        Ok(normalized)
244    }
245
246    /// Add a tag to this episode (normalized, no duplicates)
247    /// Returns `Ok(true)` if tag was added, `Ok(false)` if already exists, `Err` if invalid
248    pub fn add_tag(&mut self, tag: String) -> Result<bool, String> {
249        let normalized = Self::normalize_tag(&tag)?;
250
251        if self.tags.contains(&normalized) {
252            return Ok(false);
253        }
254
255        self.tags.push(normalized);
256        Ok(true)
257    }
258
259    /// Remove a tag from this episode
260    /// Returns `true` if tag was removed, `false` if not found
261    pub fn remove_tag(&mut self, tag: &str) -> bool {
262        if let Ok(normalized) = Self::normalize_tag(tag) {
263            if let Some(pos) = self.tags.iter().position(|t| t == &normalized) {
264                self.tags.remove(pos);
265                return true;
266            }
267        }
268        false
269    }
270
271    /// Check if episode has a specific tag
272    #[must_use]
273    pub fn has_tag(&self, tag: &str) -> bool {
274        if let Ok(normalized) = Self::normalize_tag(tag) {
275            self.tags.contains(&normalized)
276        } else {
277            false
278        }
279    }
280
281    /// Clear all tags from this episode
282    pub fn clear_tags(&mut self) {
283        self.tags.clear();
284    }
285
286    /// Get all tags for this episode
287    #[must_use]
288    pub fn get_tags(&self) -> &[String] {
289        &self.tags
290    }
291}
292
293#[cfg(test)]
294mod tests;
295
296#[cfg(test)]
297mod tests_edge;