1use crate::core::cache::SessionCache;
2use crate::core::context_ledger::PressureAction;
3use crate::core::mode_predictor::{FileSignature, ModePredictor};
4
5pub struct AutoModeContext<'a> {
6 pub path: &'a str,
7 pub token_count: usize,
8 pub task: Option<&'a str>,
9 pub cache: Option<&'a SessionCache>,
10}
11
12pub struct ResolvedMode {
13 pub mode: String,
14 pub source: &'static str,
15}
16
17pub fn resolve(ctx: &AutoModeContext) -> ResolvedMode {
20 if crate::tools::ctx_read::is_instruction_file(ctx.path) {
21 return resolved("full", "instruction_file");
22 }
23
24 if crate::core::binary_detect::is_binary_file(ctx.path) {
25 return resolved("full", "binary");
26 }
27
28 if let Some(cache) = ctx.cache {
29 if let Some(cached) = cache.get(ctx.path) {
30 let current_hash = compute_hash_from_disk(ctx.path);
31 if let Some(hash) = current_hash {
32 if cached.hash == hash {
33 return resolved("full", "cache_hit");
34 }
35 return resolved("diff", "cache_changed");
36 }
37 }
38 }
39
40 if ctx.token_count <= 200 {
41 return resolved("full", "small_file");
42 }
43
44 let ext = std::path::Path::new(ctx.path)
45 .extension()
46 .and_then(|e| e.to_str())
47 .unwrap_or("");
48
49 if is_config_or_data(ext, ctx.path) {
50 return resolved("full", "config_data");
51 }
52
53 if let Ok(bt) = crate::core::bounce_tracker::global().lock() {
54 if bt.should_force_full(ctx.path) {
55 return resolved("full", "bounce_tracker");
56 }
57 }
58
59 if let Some(mode) = intent_recommended_mode(ctx.task) {
60 return resolved(&mode, "intent");
61 }
62
63 let sig = FileSignature::from_path(ctx.path, ctx.token_count);
64 let predictor = ModePredictor::new();
65 let mut predicted = predictor
66 .predict_best_mode(&sig)
67 .unwrap_or_else(|| "full".to_string());
68 if predicted == "auto" {
69 predicted = "full".to_string();
70 }
71
72 if predicted != "full" {
73 if let Some(bandit_override) = bandit_explore(ctx.path, ctx.token_count) {
74 predicted = bandit_override;
75 }
76 }
77
78 let policy = crate::core::adaptive_mode_policy::AdaptiveModePolicyStore::load();
79 let chosen = policy.choose_auto_mode(ctx.task, &predicted);
80
81 if ctx.token_count > 2000 {
82 if (predicted == "map" || predicted == "signatures")
83 && chosen != "map"
84 && chosen != "signatures"
85 {
86 return resolved(&predicted, "predictor_guard");
87 }
88 if chosen == "full" && predicted != "full" {
89 return resolved(&predicted, "predictor_override");
90 }
91 }
92
93 if chosen != predicted {
94 return resolved(&chosen, "adaptive_policy");
95 }
96
97 if predicted != "full" {
98 return resolved(&predicted, "predictor");
99 }
100
101 let heuristic = heuristic_mode(ext, ctx.token_count);
102 resolved(&heuristic, "heuristic")
103}
104
105pub fn pressure_downgrade(requested_mode: &str, action: &PressureAction) -> Option<String> {
108 match action {
109 PressureAction::SuggestCompression => match requested_mode {
110 "auto" | "full" => Some("map".to_string()),
111 _ => None,
112 },
113 PressureAction::ForceCompression => match requested_mode {
114 "full" => Some("map".to_string()),
115 "auto" | "map" => Some("signatures".to_string()),
116 _ => None,
117 },
118 PressureAction::EvictLeastRelevant => match requested_mode {
119 "full" => Some("map".to_string()),
120 "auto" | "map" => Some("signatures".to_string()),
121 "signatures" => Some("reference".to_string()),
122 _ => None,
123 },
124 PressureAction::NoAction => None,
125 }
126}
127
128fn intent_recommended_mode(task: Option<&str>) -> Option<String> {
129 let task_desc = task?;
130 let classification = crate::core::intent_engine::classify(task_desc);
131 if classification.confidence < 0.4 {
132 return None;
133 }
134 let route = crate::core::intent_engine::route_intent(task_desc, &classification);
135 let mode =
136 crate::core::intent_router::read_mode_for_tier(route.model_tier, classification.task_type);
137 if mode == "auto" {
138 return None;
139 }
140 Some(mode)
141}
142
143fn bandit_explore(file_path: &str, token_count: usize) -> Option<String> {
144 let project_root =
145 crate::core::session::SessionState::load_latest().and_then(|s| s.project_root)?;
146 let ext = std::path::Path::new(file_path)
147 .extension()
148 .and_then(|e| e.to_str())
149 .unwrap_or("");
150 let bucket = match token_count {
151 0..=2000 => "sm",
152 2001..=10000 => "md",
153 10001..=50000 => "lg",
154 _ => "xl",
155 };
156 let bandit_key = format!("{ext}_{bucket}");
157 let mut store = crate::core::bandit::BanditStore::load(&project_root);
158 let bandit = store.get_or_create(&bandit_key);
159 let arm = bandit.select_arm();
160 if arm.budget_ratio < 0.25 && token_count > 2000 {
161 Some("aggressive".to_string())
162 } else {
163 None
164 }
165}
166
167fn heuristic_mode(ext: &str, token_count: usize) -> String {
168 if token_count > 8000 {
169 if is_code(ext) {
170 return "map".to_string();
171 }
172 return "aggressive".to_string();
173 }
174 if token_count > 6000 && is_code(ext) {
179 return "map".to_string();
180 }
181 "full".to_string()
182}
183
184fn compute_hash_from_disk(path: &str) -> Option<String> {
185 let content = std::fs::read_to_string(path).ok()?;
186 use md5::{Digest, Md5};
187 let mut hasher = Md5::new();
188 hasher.update(content.as_bytes());
189 Some(format!("{:x}", hasher.finalize()))
190}
191
192fn is_code(ext: &str) -> bool {
193 matches!(
194 ext,
195 "rs" | "ts"
196 | "tsx"
197 | "js"
198 | "jsx"
199 | "py"
200 | "go"
201 | "java"
202 | "c"
203 | "cpp"
204 | "cc"
205 | "h"
206 | "hpp"
207 | "rb"
208 | "cs"
209 | "kt"
210 | "swift"
211 | "php"
212 | "zig"
213 | "ex"
214 | "exs"
215 | "scala"
216 | "sc"
217 | "dart"
218 | "sh"
219 | "bash"
220 | "svelte"
221 | "vue"
222 )
223}
224
225fn is_config_or_data(ext: &str, path: &str) -> bool {
226 if matches!(ext, "xml" | "ini" | "cfg" | "env") {
227 return true;
228 }
229 let name = std::path::Path::new(path)
230 .file_name()
231 .and_then(|n| n.to_str())
232 .unwrap_or("");
233 matches!(
234 name,
235 "Cargo.toml"
236 | "package.json"
237 | "tsconfig.json"
238 | "Makefile"
239 | "Dockerfile"
240 | "docker-compose.yml"
241 | ".gitignore"
242 | ".env"
243 | "pyproject.toml"
244 | "go.mod"
245 | "build.gradle"
246 | "pom.xml"
247 )
248}
249
250fn resolved(mode: &str, source: &'static str) -> ResolvedMode {
251 ResolvedMode {
252 mode: mode.to_string(),
253 source,
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn pressure_suggest_full_to_map() {
263 assert_eq!(
264 pressure_downgrade("full", &PressureAction::SuggestCompression),
265 Some("map".to_string())
266 );
267 }
268
269 #[test]
270 fn pressure_suggest_auto_to_map() {
271 assert_eq!(
272 pressure_downgrade("auto", &PressureAction::SuggestCompression),
273 Some("map".to_string())
274 );
275 }
276
277 #[test]
278 fn pressure_suggest_does_not_touch_signatures() {
279 assert!(pressure_downgrade("signatures", &PressureAction::SuggestCompression).is_none());
280 }
281
282 #[test]
283 fn pressure_force_full_to_map() {
284 assert_eq!(
285 pressure_downgrade("full", &PressureAction::ForceCompression),
286 Some("map".to_string())
287 );
288 }
289
290 #[test]
291 fn pressure_force_map_to_signatures() {
292 assert_eq!(
293 pressure_downgrade("map", &PressureAction::ForceCompression),
294 Some("signatures".to_string())
295 );
296 }
297
298 #[test]
299 fn pressure_evict_signatures_to_reference() {
300 assert_eq!(
301 pressure_downgrade("signatures", &PressureAction::EvictLeastRelevant),
302 Some("reference".to_string())
303 );
304 }
305
306 #[test]
307 fn pressure_noaction_returns_none() {
308 assert!(pressure_downgrade("full", &PressureAction::NoAction).is_none());
309 }
310
311 #[test]
312 fn small_file_always_full() {
313 let ctx = AutoModeContext {
314 path: "test.rs",
315 token_count: 100,
316 task: None,
317 cache: None,
318 };
319 let result = resolve(&ctx);
320 assert_eq!(result.mode, "full");
321 assert_eq!(result.source, "small_file");
322 }
323
324 #[test]
325 fn config_file_returns_full() {
326 let ctx = AutoModeContext {
327 path: "config.ini",
328 token_count: 500,
329 task: None,
330 cache: None,
331 };
332 let result = resolve(&ctx);
333 assert_eq!(result.mode, "full");
334 assert_eq!(result.source, "config_data");
335 }
336
337 #[test]
338 fn intent_explore_returns_map() {
339 let ctx = AutoModeContext {
340 path: "large.rs",
341 token_count: 5000,
342 task: Some("how does the cache work?"),
343 cache: None,
344 };
345 let result = resolve(&ctx);
346 assert_eq!(result.mode, "map");
347 assert_eq!(result.source, "intent");
348 }
349}