use crate::error::{Result, TrajectoryError};
use crate::trajectory::TrajectoryEntry;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tokio::fs;
use tokio::sync::RwLock;
pub struct TrajectoryRecorder {
entries: RwLock<Vec<TrajectoryEntry>>,
file_path: Option<PathBuf>,
auto_save: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Trajectory {
pub metadata: TrajectoryMetadata,
pub entries: Vec<TrajectoryEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrajectoryMetadata {
pub id: String,
pub started_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
pub version: String,
pub agent_type: String,
pub task: Option<String>,
pub success: Option<bool>,
pub total_steps: usize,
pub duration_ms: Option<u64>,
}
impl TrajectoryRecorder {
pub fn new() -> Self {
Self {
entries: RwLock::new(Vec::new()),
file_path: None,
auto_save: false,
}
}
pub fn with_file<P: AsRef<Path>>(path: P) -> Self {
Self {
entries: RwLock::new(Vec::new()),
file_path: Some(path.as_ref().to_path_buf()),
auto_save: true,
}
}
pub fn with_auto_filename() -> Self {
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
let filename = format!("trajectory_{}.json", timestamp);
let trajectories_dir = Path::new("trajectories");
if !trajectories_dir.exists() {
std::fs::create_dir_all(trajectories_dir).ok();
}
let path = trajectories_dir.join(filename);
Self::with_file(path)
}
pub async fn record(&self, entry: TrajectoryEntry) -> Result<()> {
{
let mut entries = self.entries.write().await;
entries.push(entry);
}
if self.auto_save {
self.save().await?;
}
Ok(())
}
pub async fn get_entries(&self) -> Vec<TrajectoryEntry> {
self.entries.read().await.clone()
}
pub async fn entry_count(&self) -> usize {
self.entries.read().await.len()
}
pub async fn save(&self) -> Result<()> {
if let Some(path) = &self.file_path {
let trajectory = self.build_trajectory().await;
let json = serde_json::to_string_pretty(&trajectory)
.map_err(|e| TrajectoryError::RecordingFailed {
message: format!("Failed to serialize trajectory: {}", e),
})?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
}
fs::write(path, json).await?;
}
Ok(())
}
pub async fn load<P: AsRef<Path>>(path: P) -> Result<Trajectory> {
let path = path.as_ref();
if !path.exists() {
return Err(TrajectoryError::LoadFailed {
path: path.to_string_lossy().to_string(),
}.into());
}
let content = fs::read_to_string(path).await?;
let trajectory: Trajectory = serde_json::from_str(&content)
.map_err(|_| TrajectoryError::InvalidFormat)?;
Ok(trajectory)
}
async fn build_trajectory(&self) -> Trajectory {
let entries = self.entries.read().await.clone();
let started_at = entries.first()
.map(|e| e.timestamp)
.unwrap_or_else(Utc::now);
let completed_at = entries.last()
.map(|e| e.timestamp);
let duration_ms = completed_at.map(|end| {
(end - started_at).num_milliseconds() as u64
});
let mut task = None;
let mut success = None;
for entry in &entries {
match &entry.entry_type {
crate::trajectory::EntryType::TaskStart { task: t, .. } => {
task = Some(t.clone());
}
crate::trajectory::EntryType::TaskComplete { success: s, .. } => {
success = Some(*s);
}
_ => {}
}
}
let metadata = TrajectoryMetadata {
id: uuid::Uuid::new_v4().to_string(),
started_at,
completed_at,
version: "1.0".to_string(),
agent_type: "trae_agent".to_string(),
task,
success,
total_steps: entries.len(),
duration_ms,
};
Trajectory { metadata, entries }
}
pub async fn clear(&self) {
let mut entries = self.entries.write().await;
entries.clear();
}
pub fn file_path(&self) -> Option<&Path> {
self.file_path.as_deref()
}
}
impl Default for TrajectoryRecorder {
fn default() -> Self {
Self::new()
}
}