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 > 3000 && is_code(ext) {
175 return "map".to_string();
176 }
177 "full".to_string()
178}
179
180fn compute_hash_from_disk(path: &str) -> Option<String> {
181 let content = std::fs::read_to_string(path).ok()?;
182 use md5::{Digest, Md5};
183 let mut hasher = Md5::new();
184 hasher.update(content.as_bytes());
185 Some(format!("{:x}", hasher.finalize()))
186}
187
188fn is_code(ext: &str) -> bool {
189 matches!(
190 ext,
191 "rs" | "ts"
192 | "tsx"
193 | "js"
194 | "jsx"
195 | "py"
196 | "go"
197 | "java"
198 | "c"
199 | "cpp"
200 | "cc"
201 | "h"
202 | "hpp"
203 | "rb"
204 | "cs"
205 | "kt"
206 | "swift"
207 | "php"
208 | "zig"
209 | "ex"
210 | "exs"
211 | "scala"
212 | "sc"
213 | "dart"
214 | "sh"
215 | "bash"
216 | "svelte"
217 | "vue"
218 )
219}
220
221fn is_config_or_data(ext: &str, path: &str) -> bool {
222 if matches!(ext, "xml" | "ini" | "cfg" | "env") {
223 return true;
224 }
225 let name = std::path::Path::new(path)
226 .file_name()
227 .and_then(|n| n.to_str())
228 .unwrap_or("");
229 matches!(
230 name,
231 "Cargo.toml"
232 | "package.json"
233 | "tsconfig.json"
234 | "Makefile"
235 | "Dockerfile"
236 | "docker-compose.yml"
237 | ".gitignore"
238 | ".env"
239 | "pyproject.toml"
240 | "go.mod"
241 | "build.gradle"
242 | "pom.xml"
243 )
244}
245
246fn resolved(mode: &str, source: &'static str) -> ResolvedMode {
247 ResolvedMode {
248 mode: mode.to_string(),
249 source,
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn pressure_suggest_full_to_map() {
259 assert_eq!(
260 pressure_downgrade("full", &PressureAction::SuggestCompression),
261 Some("map".to_string())
262 );
263 }
264
265 #[test]
266 fn pressure_suggest_auto_to_map() {
267 assert_eq!(
268 pressure_downgrade("auto", &PressureAction::SuggestCompression),
269 Some("map".to_string())
270 );
271 }
272
273 #[test]
274 fn pressure_suggest_does_not_touch_signatures() {
275 assert!(pressure_downgrade("signatures", &PressureAction::SuggestCompression).is_none());
276 }
277
278 #[test]
279 fn pressure_force_full_to_map() {
280 assert_eq!(
281 pressure_downgrade("full", &PressureAction::ForceCompression),
282 Some("map".to_string())
283 );
284 }
285
286 #[test]
287 fn pressure_force_map_to_signatures() {
288 assert_eq!(
289 pressure_downgrade("map", &PressureAction::ForceCompression),
290 Some("signatures".to_string())
291 );
292 }
293
294 #[test]
295 fn pressure_evict_signatures_to_reference() {
296 assert_eq!(
297 pressure_downgrade("signatures", &PressureAction::EvictLeastRelevant),
298 Some("reference".to_string())
299 );
300 }
301
302 #[test]
303 fn pressure_noaction_returns_none() {
304 assert!(pressure_downgrade("full", &PressureAction::NoAction).is_none());
305 }
306
307 #[test]
308 fn small_file_always_full() {
309 let ctx = AutoModeContext {
310 path: "test.rs",
311 token_count: 100,
312 task: None,
313 cache: None,
314 };
315 let result = resolve(&ctx);
316 assert_eq!(result.mode, "full");
317 assert_eq!(result.source, "small_file");
318 }
319
320 #[test]
321 fn config_file_returns_full() {
322 let ctx = AutoModeContext {
323 path: "config.ini",
324 token_count: 500,
325 task: None,
326 cache: None,
327 };
328 let result = resolve(&ctx);
329 assert_eq!(result.mode, "full");
330 assert_eq!(result.source, "config_data");
331 }
332
333 #[test]
334 fn intent_explore_returns_map() {
335 let ctx = AutoModeContext {
336 path: "large.rs",
337 token_count: 5000,
338 task: Some("how does the cache work?"),
339 cache: None,
340 };
341 let result = resolve(&ctx);
342 assert_eq!(result.mode, "map");
343 assert_eq!(result.source, "intent");
344 }
345}