use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicU64, Ordering};
use uuid::Uuid;
use super::Verdict;
static TRAJECTORY_COUNTER: AtomicU64 = AtomicU64::new(0);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TrajectoryId(pub u64);
impl TrajectoryId {
pub fn new() -> Self {
Self(TRAJECTORY_COUNTER.fetch_add(1, Ordering::SeqCst))
}
pub fn from_u64(id: u64) -> Self {
Self(id)
}
pub fn as_u64(&self) -> u64 {
self.0
}
}
impl Default for TrajectoryId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for TrajectoryId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "traj-{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum StepOutcome {
Success,
Partial {
achieved: String,
missing: String,
},
Failure {
error: String,
},
Skipped {
reason: String,
},
NeedsRetry {
reason: String,
suggestions: Vec<String>,
},
}
impl StepOutcome {
pub fn is_success(&self) -> bool {
matches!(self, StepOutcome::Success)
}
pub fn is_failure(&self) -> bool {
matches!(self, StepOutcome::Failure { .. })
}
pub fn quality_score(&self) -> f32 {
match self {
StepOutcome::Success => 1.0,
StepOutcome::Partial { .. } => 0.6,
StepOutcome::Failure { .. } => 0.0,
StepOutcome::Skipped { .. } => 0.3,
StepOutcome::NeedsRetry { .. } => 0.2,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrajectoryStep {
pub index: usize,
pub action: String,
pub rationale: String,
pub outcome: StepOutcome,
pub confidence: f32,
pub latency_ms: u64,
pub timestamp: DateTime<Utc>,
pub context_embedding: Option<Vec<f32>>,
pub metadata: Option<StepMetadata>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StepMetadata {
pub tool_used: Option<String>,
pub input_tokens: Option<u32>,
pub output_tokens: Option<u32>,
pub model: Option<String>,
pub tags: Vec<String>,
pub attributes: std::collections::HashMap<String, String>,
}
impl TrajectoryStep {
pub fn new(
index: usize,
action: String,
rationale: String,
outcome: StepOutcome,
confidence: f32,
) -> Self {
Self {
index,
action,
rationale,
outcome,
confidence,
latency_ms: 0,
timestamp: Utc::now(),
context_embedding: None,
metadata: None,
}
}
pub fn with_latency(mut self, latency_ms: u64) -> Self {
self.latency_ms = latency_ms;
self
}
pub fn with_embedding(mut self, embedding: Vec<f32>) -> Self {
self.context_embedding = Some(embedding);
self
}
pub fn with_metadata(mut self, metadata: StepMetadata) -> Self {
self.metadata = Some(metadata);
self
}
pub fn quality(&self) -> f32 {
self.outcome.quality_score() * self.confidence
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TrajectoryMetadata {
pub session_id: Option<String>,
pub user_id: Option<String>,
pub request_type: Option<String>,
pub total_input_tokens: u32,
pub total_output_tokens: u32,
pub models_used: Vec<String>,
pub tools_invoked: Vec<String>,
pub tags: Vec<String>,
pub attributes: std::collections::HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Trajectory {
pub id: TrajectoryId,
pub uuid: Uuid,
pub query_embedding: Vec<f32>,
pub response_embedding: Option<Vec<f32>>,
pub steps: Vec<TrajectoryStep>,
pub verdict: Verdict,
pub quality: f32,
pub total_latency_ms: u64,
pub started_at: DateTime<Utc>,
pub completed_at: DateTime<Utc>,
pub metadata: TrajectoryMetadata,
pub lessons: Vec<String>,
}
impl Trajectory {
pub fn new(query_embedding: Vec<f32>) -> Self {
let now = Utc::now();
Self {
id: TrajectoryId::new(),
uuid: Uuid::new_v4(),
query_embedding,
response_embedding: None,
steps: Vec::new(),
verdict: Verdict::Partial {
completion_ratio: 0.0,
},
quality: 0.0,
total_latency_ms: 0,
started_at: now,
completed_at: now,
metadata: TrajectoryMetadata::default(),
lessons: Vec::new(),
}
}
pub fn from_compressed(compressed: &super::CompressedTrajectory) -> Self {
let now = Utc::now();
Self {
id: TrajectoryId::from_u64(compressed.original_id),
uuid: Uuid::new_v4(),
query_embedding: compressed.key_embedding.clone(),
response_embedding: None,
steps: Vec::new(), verdict: compressed.verdict.clone(),
quality: compressed.quality,
total_latency_ms: 0,
started_at: now,
completed_at: now,
metadata: TrajectoryMetadata::default(),
lessons: compressed.preserved_lessons.clone(),
}
}
pub fn add_step(&mut self, step: TrajectoryStep) {
self.steps.push(step);
}
pub fn complete(&mut self, verdict: Verdict) {
self.verdict = verdict;
self.completed_at = Utc::now();
self.total_latency_ms = (self.completed_at - self.started_at).num_milliseconds() as u64;
self.quality = self.compute_quality();
}
fn compute_quality(&self) -> f32 {
if self.steps.is_empty() {
return match &self.verdict {
Verdict::Success => 1.0,
Verdict::Failure(_) => 0.0,
Verdict::Partial { completion_ratio } => *completion_ratio,
Verdict::RecoveredViaReflection { final_quality, .. } => *final_quality,
};
}
let step_quality: f32 = self.steps.iter().map(|s| s.quality()).sum();
let avg_step_quality = step_quality / self.steps.len() as f32;
let verdict_factor = match &self.verdict {
Verdict::Success => 1.0,
Verdict::Failure(_) => 0.3,
Verdict::Partial { completion_ratio } => 0.5 + 0.5 * completion_ratio,
Verdict::RecoveredViaReflection { final_quality, .. } => *final_quality,
};
avg_step_quality * verdict_factor
}
pub fn total_tokens(&self) -> u32 {
self.metadata.total_input_tokens + self.metadata.total_output_tokens
}
pub fn step_count(&self) -> usize {
self.steps.len()
}
pub fn step_success_rate(&self) -> f32 {
if self.steps.is_empty() {
return 0.0;
}
let successes = self.steps.iter().filter(|s| s.outcome.is_success()).count();
successes as f32 / self.steps.len() as f32
}
pub fn avg_confidence(&self) -> f32 {
if self.steps.is_empty() {
return 0.0;
}
let total: f32 = self.steps.iter().map(|s| s.confidence).sum();
total / self.steps.len() as f32
}
pub fn is_success(&self) -> bool {
matches!(
self.verdict,
Verdict::Success | Verdict::RecoveredViaReflection { .. }
)
}
pub fn is_failure(&self) -> bool {
matches!(self.verdict, Verdict::Failure(_))
}
pub fn add_lesson(&mut self, lesson: String) {
self.lessons.push(lesson);
}
pub fn set_response_embedding(&mut self, embedding: Vec<f32>) {
self.response_embedding = Some(embedding);
}
}
pub struct TrajectoryRecorder {
trajectory: Trajectory,
current_step: usize,
step_start: Option<std::time::Instant>,
}
impl TrajectoryRecorder {
pub fn new(query_embedding: Vec<f32>) -> Self {
Self {
trajectory: Trajectory::new(query_embedding),
current_step: 0,
step_start: None,
}
}
pub fn start_step(&mut self) {
self.step_start = Some(std::time::Instant::now());
}
pub fn add_step(
&mut self,
action: String,
rationale: String,
outcome: StepOutcome,
confidence: f32,
) {
let latency_ms = self
.step_start
.map(|start| start.elapsed().as_millis() as u64)
.unwrap_or(0);
let step = TrajectoryStep::new(self.current_step, action, rationale, outcome, confidence)
.with_latency(latency_ms);
self.trajectory.add_step(step);
self.current_step += 1;
self.step_start = None;
}
pub fn add_full_step(&mut self, mut step: TrajectoryStep) {
step.index = self.current_step;
self.trajectory.add_step(step);
self.current_step += 1;
}
pub fn set_session_id(&mut self, session_id: String) {
self.trajectory.metadata.session_id = Some(session_id);
}
pub fn set_user_id(&mut self, user_id: String) {
self.trajectory.metadata.user_id = Some(user_id);
}
pub fn set_request_type(&mut self, request_type: String) {
self.trajectory.metadata.request_type = Some(request_type);
}
pub fn add_tag(&mut self, tag: String) {
self.trajectory.metadata.tags.push(tag);
}
pub fn record_tokens(&mut self, input_tokens: u32, output_tokens: u32) {
self.trajectory.metadata.total_input_tokens += input_tokens;
self.trajectory.metadata.total_output_tokens += output_tokens;
}
pub fn record_model(&mut self, model: String) {
if !self.trajectory.metadata.models_used.contains(&model) {
self.trajectory.metadata.models_used.push(model);
}
}
pub fn record_tool(&mut self, tool: String) {
if !self.trajectory.metadata.tools_invoked.contains(&tool) {
self.trajectory.metadata.tools_invoked.push(tool);
}
}
pub fn add_lesson(&mut self, lesson: String) {
self.trajectory.add_lesson(lesson);
}
pub fn set_response_embedding(&mut self, embedding: Vec<f32>) {
self.trajectory.set_response_embedding(embedding);
}
pub fn complete(mut self, verdict: Verdict) -> Trajectory {
self.trajectory.complete(verdict);
self.trajectory
}
pub fn step_count(&self) -> usize {
self.current_step
}
pub fn trajectory(&self) -> &Trajectory {
&self.trajectory
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_trajectory_id_generation() {
let id1 = TrajectoryId::new();
let id2 = TrajectoryId::new();
assert_ne!(id1, id2);
}
#[test]
fn test_step_outcome_quality() {
assert_eq!(StepOutcome::Success.quality_score(), 1.0);
assert_eq!(
StepOutcome::Failure {
error: "test".into()
}
.quality_score(),
0.0
);
}
#[test]
fn test_trajectory_step_creation() {
let step = TrajectoryStep::new(
0,
"analyze".to_string(),
"Need to understand context".to_string(),
StepOutcome::Success,
0.9,
);
assert_eq!(step.index, 0);
assert_eq!(step.action, "analyze");
assert_eq!(step.quality(), 0.9); }
#[test]
fn test_trajectory_creation() {
let trajectory = Trajectory::new(vec![0.1; 768]);
assert_eq!(trajectory.steps.len(), 0);
assert!(!trajectory.is_success());
}
#[test]
fn test_trajectory_recorder() {
let mut recorder = TrajectoryRecorder::new(vec![0.1; 768]);
recorder.set_session_id("session-1".to_string());
recorder.set_user_id("user-1".to_string());
recorder.add_step(
"search".to_string(),
"Finding relevant context".to_string(),
StepOutcome::Success,
0.95,
);
recorder.add_step(
"generate".to_string(),
"Creating response".to_string(),
StepOutcome::Success,
0.9,
);
let trajectory = recorder.complete(Verdict::Success);
assert_eq!(trajectory.steps.len(), 2);
assert!(trajectory.is_success());
assert!(trajectory.quality > 0.8);
}
#[test]
fn test_trajectory_quality_computation() {
let mut trajectory = Trajectory::new(vec![0.1; 768]);
trajectory.add_step(TrajectoryStep::new(
0,
"step1".to_string(),
"rationale1".to_string(),
StepOutcome::Success,
1.0,
));
trajectory.add_step(TrajectoryStep::new(
1,
"step2".to_string(),
"rationale2".to_string(),
StepOutcome::Failure {
error: "test".to_string(),
},
0.5,
));
trajectory.complete(Verdict::Partial {
completion_ratio: 0.5,
});
assert!(trajectory.quality < 1.0);
assert!(trajectory.quality > 0.0);
}
#[test]
fn test_trajectory_stats() {
let mut recorder = TrajectoryRecorder::new(vec![0.1; 768]);
recorder.add_step(
"step1".to_string(),
"r1".to_string(),
StepOutcome::Success,
0.9,
);
recorder.add_step(
"step2".to_string(),
"r2".to_string(),
StepOutcome::Success,
0.8,
);
recorder.add_step(
"step3".to_string(),
"r3".to_string(),
StepOutcome::Failure {
error: "e".to_string(),
},
0.7,
);
let trajectory = recorder.complete(Verdict::Partial {
completion_ratio: 0.67,
});
assert_eq!(trajectory.step_count(), 3);
assert!((trajectory.step_success_rate() - 0.666).abs() < 0.01);
assert!((trajectory.avg_confidence() - 0.8).abs() < 0.01);
}
}