Skip to main content

do_memory_core/reward/
efficiency.rs

1//! Efficiency multiplier calculation based on duration and step count
2
3use crate::episode::Episode;
4use crate::reward::constants::{
5    EFFICIENT_DURATION_SECS, EFFICIENT_STEP_COUNT, MAX_EFFICIENCY_MULTIPLIER,
6    MIN_EFFICIENCY_MULTIPLIER,
7};
8use crate::types::TaskOutcome;
9
10/// Computes an abstention timeliness bonus/penalty.
11///
12/// - Early abstention (stopped_at_step < EARLY_ABSTENTION_THRESHOLD) → positive bonus
13/// - Late abstention (stopped_at_step >= LATE_ABSTENTION_THRESHOLD) → penalty
14/// - Failure with many steps (should have abstained but didn't) → penalty
15const EARLY_ABSTENTION_THRESHOLD: usize = 3;
16const LATE_ABSTENTION_THRESHOLD: usize = 10;
17
18pub fn calculate_abstention_score(episode: &Episode) -> f32 {
19    match &episode.outcome {
20        Some(TaskOutcome::Abstained {
21            stopped_at_step, ..
22        }) => {
23            if *stopped_at_step <= EARLY_ABSTENTION_THRESHOLD {
24                0.5 // Rewarded: agent stopped early
25            } else if *stopped_at_step >= LATE_ABSTENTION_THRESHOLD {
26                -0.3 // Penalized: agent waited too long
27            } else {
28                // Linear interpolation in the middle zone
29                let range = (LATE_ABSTENTION_THRESHOLD - EARLY_ABSTENTION_THRESHOLD) as f32;
30                let pos = (*stopped_at_step - EARLY_ABSTENTION_THRESHOLD) as f32;
31                0.5 - (0.8 * pos / range)
32            }
33        }
34        Some(TaskOutcome::Failure { .. }) if episode.steps.len() > LATE_ABSTENTION_THRESHOLD => {
35            // Should-have-abstained penalty: ran many steps before failing
36            -0.2
37        }
38        _ => 0.0,
39    }
40}
41
42/// Calculator for efficiency metrics
43pub struct EfficiencyCalculator {
44    /// Weight for duration in efficiency calculation
45    pub duration_weight: f32,
46    /// Weight for step count in efficiency calculation
47    pub step_count_weight: f32,
48}
49
50impl EfficiencyCalculator {
51    /// Create a new efficiency calculator with given weights
52    pub fn new(duration_weight: f32, step_count_weight: f32) -> Self {
53        Self {
54            duration_weight,
55            step_count_weight,
56        }
57    }
58
59    /// Calculate efficiency multiplier based on duration and step count
60    pub fn calculate(&self, episode: &Episode) -> f32 {
61        let duration_score = self.calculate_duration_efficiency(episode);
62        let step_count_score = self.calculate_step_count_efficiency(episode);
63
64        let combined =
65            (duration_score * self.duration_weight) + (step_count_score * self.step_count_weight);
66
67        // Clamp to reasonable bounds
68        combined.clamp(MIN_EFFICIENCY_MULTIPLIER, MAX_EFFICIENCY_MULTIPLIER)
69    }
70
71    /// Calculate duration efficiency score
72    fn calculate_duration_efficiency(&self, episode: &Episode) -> f32 {
73        if let Some(duration) = episode.duration() {
74            let duration_secs = duration.num_seconds() as f32;
75
76            if duration_secs <= 0.0 {
77                return MAX_EFFICIENCY_MULTIPLIER;
78            }
79
80            // Efficiency decreases as duration increases
81            // Exponential decay: e^(-x/threshold)
82            let ratio = duration_secs / EFFICIENT_DURATION_SECS;
83            let score = (-ratio / 2.0).exp();
84
85            // Map to multiplier range
86            MIN_EFFICIENCY_MULTIPLIER
87                + (score * (MAX_EFFICIENCY_MULTIPLIER - MIN_EFFICIENCY_MULTIPLIER))
88        } else {
89            1.0 // Default if no duration
90        }
91    }
92
93    /// Calculate step count efficiency score
94    fn calculate_step_count_efficiency(&self, episode: &Episode) -> f32 {
95        let step_count = episode.steps.len();
96
97        if step_count == 0 {
98            return MIN_EFFICIENCY_MULTIPLIER;
99        }
100
101        // Efficiency decreases as step count increases
102        let ratio = step_count as f32 / EFFICIENT_STEP_COUNT as f32;
103        let score = (-ratio / 2.0).exp();
104
105        // Map to multiplier range
106        MIN_EFFICIENCY_MULTIPLIER
107            + (score * (MAX_EFFICIENCY_MULTIPLIER - MIN_EFFICIENCY_MULTIPLIER))
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::episode::ExecutionStep;
115    use crate::types::{ComplexityLevel, ExecutionResult, TaskContext, TaskOutcome, TaskType};
116
117    fn create_test_episode() -> Episode {
118        let context = TaskContext {
119            language: Some("rust".to_string()),
120            framework: None,
121            complexity: ComplexityLevel::Simple,
122            domain: "testing".to_string(),
123            tags: vec![],
124        };
125        Episode::new("Test task".to_string(), context, TaskType::Testing)
126    }
127
128    #[test]
129    fn test_efficiency_fast_execution() {
130        let calculator = EfficiencyCalculator::new(0.5, 0.5);
131        let mut episode = create_test_episode();
132
133        // Add just a few steps
134        for i in 0..3 {
135            let mut step = ExecutionStep::new(i + 1, format!("tool_{i}"), "Action".to_string());
136            step.result = Some(ExecutionResult::Success {
137                output: "OK".to_string(),
138            });
139            episode.add_step(step);
140        }
141
142        episode.complete(TaskOutcome::Success {
143            verdict: "Quick".to_string(),
144            artifacts: vec![],
145        });
146
147        let efficiency = calculator.calculate(&episode);
148        assert!(efficiency > 1.0);
149    }
150
151    #[test]
152    fn test_efficiency_slow_execution() {
153        let calculator = EfficiencyCalculator::new(0.5, 0.5);
154        let mut episode = create_test_episode();
155
156        // Simulate old start time
157        episode.start_time = chrono::Utc::now() - chrono::Duration::minutes(5);
158
159        // Add many steps
160        for i in 0..50 {
161            let mut step = ExecutionStep::new(i + 1, format!("tool_{i}"), "Action".to_string());
162            step.result = Some(ExecutionResult::Success {
163                output: "OK".to_string(),
164            });
165            episode.add_step(step);
166        }
167
168        episode.complete(TaskOutcome::Success {
169            verdict: "Slow".to_string(),
170            artifacts: vec![],
171        });
172
173        let efficiency = calculator.calculate(&episode);
174        assert!(efficiency < 1.0);
175    }
176}