1use 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct PatternApplication {
15 pub pattern_id: PatternId,
17 pub applied_at_step: usize,
19 pub outcome: ApplicationOutcome,
21 pub notes: Option<String>,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27pub enum ApplicationOutcome {
28 Helped,
30 NoEffect,
32 Hindered,
34 Pending,
36}
37
38impl ApplicationOutcome {
39 #[must_use]
41 pub fn is_success(&self) -> bool {
42 matches!(self, ApplicationOutcome::Helped)
43 }
44}
45
46pub type PatternId = Uuid;
48
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53pub struct ExecutionStep {
54 pub step_number: usize,
56 pub timestamp: DateTime<Utc>,
58 pub tool: String,
60 pub action: String,
62 pub parameters: serde_json::Value,
64 pub result: Option<ExecutionResult>,
66 pub latency_ms: u64,
68 pub tokens_used: Option<usize>,
70 pub metadata: HashMap<String, String>,
72}
73
74impl ExecutionStep {
75 #[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 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
100pub struct Episode {
101 pub episode_id: Uuid,
103 pub task_type: TaskType,
105 pub task_description: String,
107 pub context: TaskContext,
109 pub start_time: DateTime<Utc>,
111 pub end_time: Option<DateTime<Utc>>,
113 pub steps: Vec<ExecutionStep>,
115 pub outcome: Option<TaskOutcome>,
117 pub reward: Option<RewardScore>,
119 pub reflection: Option<Reflection>,
121 pub patterns: Vec<PatternId>,
123 pub heuristics: Vec<Uuid>,
125 #[serde(default)]
127 pub applied_patterns: Vec<PatternApplication>,
128 #[serde(default)]
130 pub salient_features: Option<SalientFeatures>,
131 pub metadata: HashMap<String, String>,
133 #[serde(default)]
135 pub tags: Vec<String>,
136 #[serde(default)]
138 pub checkpoints: Vec<CheckpointMeta>,
139}
140
141impl Episode {
142 #[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 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 #[must_use]
184 pub fn is_complete(&self) -> bool {
185 self.end_time.is_some() && self.outcome.is_some()
186 }
187
188 #[must_use]
190 pub fn duration(&self) -> Option<chrono::Duration> {
191 self.end_time.map(|end| end - self.start_time)
192 }
193
194 pub fn add_step(&mut self, step: ExecutionStep) {
196 self.steps.push(step);
197 }
198
199 pub fn complete(&mut self, outcome: TaskOutcome) {
201 self.end_time = Some(Utc::now());
202 self.outcome = Some(outcome);
203 }
204
205 #[must_use]
207 pub fn successful_steps_count(&self) -> usize {
208 self.steps.iter().filter(|s| s.is_success()).count()
209 }
210
211 #[must_use]
213 pub fn failed_steps_count(&self) -> usize {
214 self.steps.iter().filter(|s| !s.is_success()).count()
215 }
216
217 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 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 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 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 #[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 pub fn clear_tags(&mut self) {
283 self.tags.clear();
284 }
285
286 #[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;