use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
use crate::memory::checkpoint::CheckpointMeta;
use crate::pre_storage::SalientFeatures;
use crate::types::{ExecutionResult, Reflection, RewardScore, TaskContext, TaskOutcome, TaskType};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PatternApplication {
pub pattern_id: PatternId,
pub applied_at_step: usize,
pub outcome: ApplicationOutcome,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ApplicationOutcome {
Helped,
NoEffect,
Hindered,
Pending,
}
impl ApplicationOutcome {
#[must_use]
pub fn is_success(&self) -> bool {
matches!(self, ApplicationOutcome::Helped)
}
}
pub type PatternId = Uuid;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExecutionStep {
pub step_number: usize,
pub timestamp: DateTime<Utc>,
pub tool: String,
pub action: String,
pub parameters: serde_json::Value,
pub result: Option<ExecutionResult>,
pub latency_ms: u64,
pub tokens_used: Option<usize>,
pub metadata: HashMap<String, String>,
}
impl ExecutionStep {
#[must_use]
pub fn new(step_number: usize, tool: String, action: String) -> Self {
Self {
step_number,
timestamp: Utc::now(),
tool,
action,
parameters: serde_json::json!({}),
result: None,
latency_ms: 0,
tokens_used: None,
metadata: HashMap::new(),
}
}
#[must_use]
pub fn is_success(&self) -> bool {
self.result.as_ref().is_some_and(|r| r.is_success())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Episode {
pub episode_id: Uuid,
pub task_type: TaskType,
pub task_description: String,
pub context: TaskContext,
pub start_time: DateTime<Utc>,
pub end_time: Option<DateTime<Utc>>,
pub steps: Vec<ExecutionStep>,
pub outcome: Option<TaskOutcome>,
pub reward: Option<RewardScore>,
pub reflection: Option<Reflection>,
pub patterns: Vec<PatternId>,
pub heuristics: Vec<Uuid>,
#[serde(default)]
pub applied_patterns: Vec<PatternApplication>,
#[serde(default)]
pub salient_features: Option<SalientFeatures>,
pub metadata: HashMap<String, String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub checkpoints: Vec<CheckpointMeta>,
}
impl Episode {
#[must_use]
pub fn new(task_description: String, context: TaskContext, task_type: TaskType) -> Self {
Self {
episode_id: Uuid::new_v4(),
task_type,
task_description,
context,
start_time: Utc::now(),
end_time: None,
steps: Vec::new(),
outcome: None,
reward: None,
reflection: None,
patterns: Vec::new(),
heuristics: Vec::new(),
applied_patterns: Vec::new(),
salient_features: None,
metadata: HashMap::new(),
tags: Vec::new(),
checkpoints: Vec::new(),
}
}
pub fn record_pattern_application(
&mut self,
pattern_id: PatternId,
applied_at_step: usize,
outcome: ApplicationOutcome,
notes: Option<String>,
) {
self.applied_patterns.push(PatternApplication {
pattern_id,
applied_at_step,
outcome,
notes,
});
}
#[must_use]
pub fn is_complete(&self) -> bool {
self.end_time.is_some() && self.outcome.is_some()
}
#[must_use]
pub fn duration(&self) -> Option<chrono::Duration> {
self.end_time.map(|end| end - self.start_time)
}
pub fn add_step(&mut self, step: ExecutionStep) {
self.steps.push(step);
}
pub fn complete(&mut self, outcome: TaskOutcome) {
self.end_time = Some(Utc::now());
self.outcome = Some(outcome);
}
#[must_use]
pub fn successful_steps_count(&self) -> usize {
self.steps.iter().filter(|s| s.is_success()).count()
}
#[must_use]
pub fn failed_steps_count(&self) -> usize {
self.steps.iter().filter(|s| !s.is_success()).count()
}
fn normalize_tag(tag: &str) -> Result<String, String> {
let normalized = tag.trim().to_lowercase();
if normalized.is_empty() {
return Err("Tag cannot be empty".to_string());
}
if normalized.len() < 2 {
return Err("Tag must be at least 2 characters long".to_string());
}
if normalized.len() > 100 {
return Err("Tag cannot exceed 100 characters".to_string());
}
if !normalized
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Err(format!(
"Tag '{tag}' contains invalid characters. Only alphanumeric, hyphens, and underscores allowed"
));
}
Ok(normalized)
}
pub fn add_tag(&mut self, tag: String) -> Result<bool, String> {
let normalized = Self::normalize_tag(&tag)?;
if self.tags.contains(&normalized) {
return Ok(false);
}
self.tags.push(normalized);
Ok(true)
}
pub fn remove_tag(&mut self, tag: &str) -> bool {
if let Ok(normalized) = Self::normalize_tag(tag) {
if let Some(pos) = self.tags.iter().position(|t| t == &normalized) {
self.tags.remove(pos);
return true;
}
}
false
}
#[must_use]
pub fn has_tag(&self, tag: &str) -> bool {
if let Ok(normalized) = Self::normalize_tag(tag) {
self.tags.contains(&normalized)
} else {
false
}
}
pub fn clear_tags(&mut self) {
self.tags.clear();
}
#[must_use]
pub fn get_tags(&self) -> &[String] {
&self.tags
}
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_edge;