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
59            .cache
60            .as_ref()
61            .ok_or_else(|| ErrorData::internal_error("cache not available", None))?;
62        let Some(mut cache_guard) = crate::server::bounded_lock::write(cache, "ctx_preload:cache")
63        else {
64            return Ok(ToolOutput::simple(
65                "[preload skipped — cache temporarily unavailable]".to_string(),
66            ));
67        };
68        let mut result = crate::tools::ctx_preload::handle(
69            &mut cache_guard,
70            &task,
71            resolved_path.as_deref(),
72            ctx.crp_mode,
73        );
74
75        let provider_hints = predict_and_prefetch(&task, &mut cache_guard, &ctx.project_root);
76        if !provider_hints.is_empty() {
77            result.push_str(&provider_hints);
78        }
79
80        drop(cache_guard);
81
82        if let Some(ref session_lock) = ctx.session {
83            if let Some(mut session_guard) =
84                crate::server::bounded_lock::write(session_lock, "ctx_preload:session_write")
85            {
86                if session_guard.active_structured_intent.is_none()
87                    || session_guard
88                        .active_structured_intent
89                        .as_ref()
90                        .is_none_or(|i| i.confidence < 0.6)
91                {
92                    session_guard.set_task(&task, Some("preload"));
93                }
94            }
95
96            if let Some(session_guard) =
97                crate::server::bounded_lock::read(session_lock, "ctx_preload:session_read")
98            {
99                if let Some(ref intent) = session_guard.active_structured_intent {
100                    if let Some(ref ledger_lock) = ctx.ledger {
101                        let Some(ledger) =
102                            crate::server::bounded_lock::read(ledger_lock, "ctx_preload:ledger")
103                        else {
104                            return Ok(ToolOutput::simple(result));
105                        };
106                        if !ledger.entries.is_empty() {
107                            let known: Vec<String> = session_guard
108                                .files_touched
109                                .iter()
110                                .map(|f| f.path.clone())
111                                .collect();
112                            let deficit = crate::core::context_deficit::detect_deficit(
113                                &ledger, intent, &known,
114                            );
115                            if !deficit.suggested_files.is_empty() {
116                                result.push_str("\n\n--- SUGGESTED FILES ---");
117                                for s in &deficit.suggested_files {
118                                    result.push_str(&format!(
119                                        "\n  {} ({:?}, ~{} tok, mode: {})",
120                                        s.path, s.reason, s.estimated_tokens, s.recommended_mode
121                                    ));
122                                }
123                            }
124
125                            let pressure = ledger.pressure();
126                            if pressure.utilization > 0.7 {
127                                let plan = ledger.reinjection_plan(intent, 0.6);
128                                if !plan.actions.is_empty() {
129                                    result.push_str("\n\n--- REINJECTION PLAN ---");
130                                    result.push_str(&format!(
131                                        "\n  Context pressure: {:.0}% -> target: 60%",
132                                        pressure.utilization * 100.0
133                                    ));
134                                    for a in &plan.actions {
135                                        result.push_str(&format!(
136                                            "\n  {} : {} -> {} (frees ~{} tokens)",
137                                            a.path, a.current_mode, a.new_mode, a.tokens_freed
138                                        ));
139                                    }
140                                    result.push_str(&format!(
141                                        "\n  Total freeable: {} tokens",
142                                        plan.total_tokens_freed
143                                    ));
144                                }
145                            }
146                        }
147                    }
148                }
149            }
150        }
151
152        Ok(ToolOutput {
153            text: result,
154            original_tokens: 0,
155            saved_tokens: 0,
156            mode: Some("preload".to_string()),
157            path: None,
158            changed: false,
159        })
160    }
161}
162
163/// Use Active Inference to predict useful provider data and prefetch it.
164/// Stores results in session cache (synchronous) and triggers deep
165/// indexing (BM25, Graph, Knowledge) in a background thread when
166/// `providers.auto_index` is enabled.
167fn predict_and_prefetch(
168    task: &str,
169    cache: &mut crate::core::cache::SessionCache,
170    project_root: &str,
171) -> String {
172    crate::core::providers::init::init_with_project_root(Some(std::path::Path::new(project_root)));
173    let registry = crate::core::providers::registry::global_registry();
174    let available = registry.available_provider_ids();
175    if available.is_empty() {
176        return String::new();
177    }
178
179    let mut bandit = crate::core::provider_bandit::ProviderBandit::new();
180    let predictions =
181        crate::core::active_inference::predict_preloads(task, &available, &mut bandit, 2);
182
183    if predictions.is_empty() {
184        return String::new();
185    }
186
187    let cfg = crate::core::config::Config::load();
188    let auto_index = cfg.providers.auto_index;
189    let mut all_artifacts = Vec::new();
190
191    let mut out = String::from("\n\n--- PROVIDER PRELOAD ---");
192    let mut prefetched = 0usize;
193
194    for pred in &predictions {
195        let params = crate::core::providers::provider_trait::ProviderParams {
196            limit: Some(5),
197            ..Default::default()
198        };
199
200        match registry.execute_as_chunks(&pred.provider_id, &pred.action, &params) {
201            Ok(chunks) => {
202                let artifacts = crate::core::consolidation::consolidate(&chunks);
203                for entry in &artifacts.cache_entries {
204                    cache.store(&entry.uri, &entry.content);
205                    prefetched += 1;
206                }
207                if auto_index && !artifacts.is_empty() {
208                    all_artifacts.push(artifacts);
209                }
210                out.push_str(&format!(
211                    "\n  {} {} → {} items cached (confidence: {:.0}%)",
212                    pred.provider_id,
213                    pred.action,
214                    chunks.len(),
215                    pred.confidence * 100.0,
216                ));
217            }
218            Err(e) => {
219                tracing::debug!(
220                    "[preload] provider {}/{} failed: {e}",
221                    pred.provider_id,
222                    pred.action,
223                );
224            }
225        }
226    }
227
228    if prefetched == 0 {
229        return String::new();
230    }
231
232    if !all_artifacts.is_empty() {
233        let root = project_root.to_string();
234        std::thread::spawn(move || {
235            let merged = merge_preload_artifacts(&all_artifacts);
236            crate::tools::ctx_provider::apply_artifacts_to_stores(&merged, &root);
237        });
238    }
239
240    out
241}
242
243fn merge_preload_artifacts(
244    all: &[crate::core::consolidation::ConsolidationArtifacts],
245) -> crate::core::consolidation::ConsolidationArtifacts {
246    let mut merged = crate::core::consolidation::ConsolidationArtifacts::default();
247    for a in all {
248        merged.bm25_chunks.extend(a.bm25_chunks.clone());
249        merged.edges.extend(a.edges.clone());
250        merged.facts.extend(a.facts.clone());
251        merged.cache_entries.extend(a.cache_entries.clone());
252    }
253    merged
254}