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