Skip to main content

lean_ctx/tools/registered/
ctx_preload.rs

1use rmcp::model::Tool;
2use rmcp::ErrorData;
3use serde_json::{json, Map, Value};
4
5use crate::server::tool_trait::{get_str, McpTool, ToolContext, ToolOutput};
6use crate::tool_defs::tool_def;
7
8pub struct CtxPreloadTool;
9
10impl McpTool for CtxPreloadTool {
11    fn name(&self) -> &'static str {
12        "ctx_preload"
13    }
14
15    fn tool_def(&self) -> Tool {
16        tool_def(
17            "ctx_preload",
18            "Proactive context loader — caches task-relevant files, returns L-curve-optimized summary (~50-100 tokens vs ~5000 for individual reads).",
19            json!({
20                "type": "object",
21                "properties": {
22                    "task": {
23                        "type": "string",
24                        "description": "Task description (e.g. 'fix auth bug in validate_token')"
25                    },
26                    "path": {
27                        "type": "string",
28                        "description": "Project root (default: .)"
29                    }
30                },
31                "required": ["task"]
32            }),
33        )
34    }
35
36    fn handle(
37        &self,
38        args: &Map<String, Value>,
39        ctx: &ToolContext,
40    ) -> Result<ToolOutput, ErrorData> {
41        let task = get_str(args, "task").unwrap_or_default();
42
43        let resolved_path = if get_str(args, "path").is_some() {
44            if let Some(p) = ctx.resolved_path("path") {
45                Some(p.to_string())
46            } else if let Some(err) = ctx.path_error("path") {
47                return Err(ErrorData::invalid_params(format!("path: {err}"), None));
48            } else {
49                None
50            }
51        } else if let Some(ref session) = ctx.session {
52            let guard = crate::server::bounded_lock::read(session, "ctx_preload:session_root");
53            guard.as_ref().and_then(|g| g.project_root.clone())
54        } else {
55            None
56        };
57
58        let cache = ctx.cache.as_ref().unwrap();
59        let Some(mut cache_guard) = crate::server::bounded_lock::write(cache, "ctx_preload:cache")
60        else {
61            return Ok(ToolOutput::simple(
62                "[preload skipped — cache temporarily unavailable]".to_string(),
63            ));
64        };
65        let mut result = crate::tools::ctx_preload::handle(
66            &mut cache_guard,
67            &task,
68            resolved_path.as_deref(),
69            ctx.crp_mode,
70        );
71
72        // Active Inference: predict which provider data will be needed and
73        // prefetch it into the session cache for fast subsequent access.
74        let provider_hints = predict_and_prefetch(&task, &mut cache_guard);
75        if !provider_hints.is_empty() {
76            result.push_str(&provider_hints);
77        }
78
79        drop(cache_guard);
80
81        if let Some(ref session_lock) = ctx.session {
82            if let Some(mut session_guard) =
83                crate::server::bounded_lock::write(session_lock, "ctx_preload:session_write")
84            {
85                if session_guard.active_structured_intent.is_none()
86                    || session_guard
87                        .active_structured_intent
88                        .as_ref()
89                        .is_none_or(|i| i.confidence < 0.6)
90                {
91                    session_guard.set_task(&task, Some("preload"));
92                }
93            }
94
95            if let Some(session_guard) =
96                crate::server::bounded_lock::read(session_lock, "ctx_preload:session_read")
97            {
98                if let Some(ref intent) = session_guard.active_structured_intent {
99                    if let Some(ref ledger_lock) = ctx.ledger {
100                        let Some(ledger) =
101                            crate::server::bounded_lock::read(ledger_lock, "ctx_preload:ledger")
102                        else {
103                            return Ok(ToolOutput::simple(result));
104                        };
105                        if !ledger.entries.is_empty() {
106                            let known: Vec<String> = session_guard
107                                .files_touched
108                                .iter()
109                                .map(|f| f.path.clone())
110                                .collect();
111                            let deficit = crate::core::context_deficit::detect_deficit(
112                                &ledger, intent, &known,
113                            );
114                            if !deficit.suggested_files.is_empty() {
115                                result.push_str("\n\n--- SUGGESTED FILES ---");
116                                for s in &deficit.suggested_files {
117                                    result.push_str(&format!(
118                                        "\n  {} ({:?}, ~{} tok, mode: {})",
119                                        s.path, s.reason, s.estimated_tokens, s.recommended_mode
120                                    ));
121                                }
122                            }
123
124                            let pressure = ledger.pressure();
125                            if pressure.utilization > 0.7 {
126                                let plan = ledger.reinjection_plan(intent, 0.6);
127                                if !plan.actions.is_empty() {
128                                    result.push_str("\n\n--- REINJECTION PLAN ---");
129                                    result.push_str(&format!(
130                                        "\n  Context pressure: {:.0}% -> target: 60%",
131                                        pressure.utilization * 100.0
132                                    ));
133                                    for a in &plan.actions {
134                                        result.push_str(&format!(
135                                            "\n  {} : {} -> {} (frees ~{} tokens)",
136                                            a.path, a.current_mode, a.new_mode, a.tokens_freed
137                                        ));
138                                    }
139                                    result.push_str(&format!(
140                                        "\n  Total freeable: {} tokens",
141                                        plan.total_tokens_freed
142                                    ));
143                                }
144                            }
145                        }
146                    }
147                }
148            }
149        }
150
151        Ok(ToolOutput {
152            text: result,
153            original_tokens: 0,
154            saved_tokens: 0,
155            mode: Some("preload".to_string()),
156            path: None,
157            changed: false,
158        })
159    }
160}
161
162/// Use Active Inference to predict useful provider data and prefetch it.
163/// Returns a summary string to append to the preload output, or empty if
164/// no providers are available or predictions are empty.
165fn predict_and_prefetch(task: &str, cache: &mut crate::core::cache::SessionCache) -> String {
166    crate::core::providers::init::init_builtin_providers();
167    let registry = crate::core::providers::registry::global_registry();
168    let available = registry.available_provider_ids();
169    if available.is_empty() {
170        return String::new();
171    }
172
173    let mut bandit = crate::core::provider_bandit::ProviderBandit::new();
174    let predictions =
175        crate::core::active_inference::predict_preloads(task, &available, &mut bandit, 2);
176
177    if predictions.is_empty() {
178        return String::new();
179    }
180
181    let mut out = String::from("\n\n--- PROVIDER PRELOAD ---");
182    let mut prefetched = 0usize;
183
184    for pred in &predictions {
185        let params = crate::core::providers::provider_trait::ProviderParams {
186            limit: Some(5),
187            ..Default::default()
188        };
189
190        match registry.execute_as_chunks(&pred.provider_id, &pred.action, &params) {
191            Ok(chunks) => {
192                let artifacts = crate::core::consolidation::consolidate(&chunks);
193                for entry in &artifacts.cache_entries {
194                    cache.store(&entry.uri, &entry.content);
195                    prefetched += 1;
196                }
197                out.push_str(&format!(
198                    "\n  {} {} → {} items cached (confidence: {:.0}%)",
199                    pred.provider_id,
200                    pred.action,
201                    chunks.len(),
202                    pred.confidence * 100.0,
203                ));
204            }
205            Err(e) => {
206                tracing::debug!(
207                    "[preload] provider {}/{} failed: {e}",
208                    pred.provider_id,
209                    pred.action,
210                );
211            }
212        }
213    }
214
215    if prefetched == 0 {
216        return String::new();
217    }
218
219    out
220}