Skip to main content

lean_ctx/proxy/
break_even.rs

1use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
2
3/// Tracks whether MCP schema overhead is justified by proxy compression savings.
4pub struct BreakEvenCalculator {
5    proxy_tokens_saved: AtomicI64,
6    schema_overhead_per_turn: u64,
7    turn_count: AtomicU64,
8    has_used_ctx_tools: AtomicBool,
9}
10
11/// Snapshot of a session's MCP break-even state.
12#[derive(Debug, Clone)]
13pub struct BreakEvenSummary {
14    pub proxy_savings: i64,
15    pub estimated_mcp_overhead: i64,
16    pub net: i64,
17    pub mcp_recommended: bool,
18    pub reason: &'static str,
19}
20
21impl BreakEvenCalculator {
22    /// Create with the estimated schema overhead per turn (in tokens).
23    /// Typical values: 1500 (minimal profile) to 3500 (full profile).
24    pub fn new(schema_overhead_per_turn: u64) -> Self {
25        Self {
26            proxy_tokens_saved: AtomicI64::new(0),
27            schema_overhead_per_turn,
28            turn_count: AtomicU64::new(0),
29            has_used_ctx_tools: AtomicBool::new(false),
30        }
31    }
32
33    /// Record tokens saved by proxy-level compression this turn.
34    pub fn record_proxy_savings(&self, tokens: i64) {
35        self.proxy_tokens_saved.fetch_add(tokens, Ordering::Relaxed);
36    }
37
38    /// Advance the turn counter. Call once per API request.
39    pub fn record_turn(&self) {
40        self.turn_count.fetch_add(1, Ordering::Relaxed);
41    }
42
43    /// Mark that the agent has used a ctx_* MCP tool in this session.
44    /// Once set, MCP is never disabled mid-session.
45    pub fn mark_ctx_tool_used(&self) {
46        self.has_used_ctx_tools.store(true, Ordering::Relaxed);
47    }
48
49    /// Whether MCP tools should be enabled for this session.
50    pub fn should_enable_mcp(&self) -> bool {
51        if self.has_used_ctx_tools.load(Ordering::Relaxed) {
52            return true;
53        }
54
55        let turns = self.turn_count.load(Ordering::Relaxed);
56        if turns < 2 {
57            return false;
58        }
59
60        let savings = self.proxy_tokens_saved.load(Ordering::Relaxed);
61        let overhead = self.schema_overhead_per_turn as i64 * turns as i64;
62        savings > overhead
63    }
64
65    /// Snapshot of the current break-even state.
66    pub fn summary(&self) -> BreakEvenSummary {
67        let turns = self.turn_count.load(Ordering::Relaxed);
68        let savings = self.proxy_tokens_saved.load(Ordering::Relaxed);
69        let overhead = self.schema_overhead_per_turn as i64 * turns as i64;
70        let net = savings - overhead;
71        let mcp_recommended = self.should_enable_mcp();
72
73        let reason = if self.has_used_ctx_tools.load(Ordering::Relaxed) {
74            "ctx_tools used — MCP locked on"
75        } else if turns < 2 {
76            "too few turns — proxy-only"
77        } else if mcp_recommended {
78            "savings exceed schema overhead"
79        } else {
80            "schema overhead exceeds savings"
81        };
82
83        BreakEvenSummary {
84            proxy_savings: savings,
85            estimated_mcp_overhead: overhead,
86            net,
87            mcp_recommended,
88            reason,
89        }
90    }
91
92    /// Current turn count.
93    pub fn turn_count(&self) -> u64 {
94        self.turn_count.load(Ordering::Relaxed)
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::BreakEvenCalculator;
101
102    #[test]
103    fn new_calculator_starts_proxy_only() {
104        let calc = BreakEvenCalculator::new(3500);
105        assert!(!calc.should_enable_mcp());
106        assert_eq!(calc.turn_count(), 0);
107    }
108
109    #[test]
110    fn first_two_turns_always_proxy_only() {
111        let calc = BreakEvenCalculator::new(1500);
112        calc.record_proxy_savings(50000);
113        calc.record_turn();
114        assert!(!calc.should_enable_mcp());
115    }
116
117    #[test]
118    fn high_savings_enables_mcp() {
119        let calc = BreakEvenCalculator::new(1500);
120        for _ in 0..5 {
121            calc.record_proxy_savings(5000);
122            calc.record_turn();
123        }
124        assert!(calc.should_enable_mcp());
125    }
126
127    #[test]
128    fn low_savings_stays_proxy_only() {
129        let calc = BreakEvenCalculator::new(3500);
130        for _ in 0..3 {
131            calc.record_proxy_savings(500);
132            calc.record_turn();
133        }
134        assert!(!calc.should_enable_mcp());
135    }
136
137    #[test]
138    fn ctx_tool_usage_locks_mcp_on() {
139        let calc = BreakEvenCalculator::new(3500);
140        calc.mark_ctx_tool_used();
141        assert!(calc.should_enable_mcp());
142        assert!(calc.should_enable_mcp());
143    }
144
145    #[test]
146    fn summary_reflects_state() {
147        let calc = BreakEvenCalculator::new(2000);
148        calc.record_turn();
149        calc.record_turn();
150        calc.record_proxy_savings(3000);
151        let summary = calc.summary();
152        assert_eq!(summary.proxy_savings, 3000);
153        assert_eq!(summary.estimated_mcp_overhead, 4000);
154        assert_eq!(summary.net, -1000);
155        assert!(!summary.mcp_recommended);
156    }
157
158    #[test]
159    fn summary_reason_for_ctx_tools() {
160        let calc = BreakEvenCalculator::new(2000);
161        calc.mark_ctx_tool_used();
162        let summary = calc.summary();
163        assert_eq!(summary.reason, "ctx_tools used — MCP locked on");
164        assert!(summary.mcp_recommended);
165    }
166}