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 = tokio::task::block_in_place(|| session.blocking_read());
53            guard.project_root.clone()
54        } else {
55            None
56        };
57
58        let cache = ctx.cache.as_ref().unwrap();
59        let mut cache_guard = tokio::task::block_in_place(|| cache.blocking_write());
60        let mut result = crate::tools::ctx_preload::handle(
61            &mut cache_guard,
62            &task,
63            resolved_path.as_deref(),
64            ctx.crp_mode,
65        );
66        drop(cache_guard);
67
68        if let Some(ref session_lock) = ctx.session {
69            let mut session_guard = tokio::task::block_in_place(|| session_lock.blocking_write());
70            if session_guard.active_structured_intent.is_none()
71                || session_guard
72                    .active_structured_intent
73                    .as_ref()
74                    .is_none_or(|i| i.confidence < 0.6)
75            {
76                session_guard.set_task(&task, Some("preload"));
77            }
78            drop(session_guard);
79
80            let session_guard = tokio::task::block_in_place(|| session_lock.blocking_read());
81            if let Some(ref intent) = session_guard.active_structured_intent {
82                if let Some(ref ledger_lock) = ctx.ledger {
83                    let ledger = tokio::task::block_in_place(|| ledger_lock.blocking_read());
84                    if !ledger.entries.is_empty() {
85                        let known: Vec<String> = session_guard
86                            .files_touched
87                            .iter()
88                            .map(|f| f.path.clone())
89                            .collect();
90                        let deficit =
91                            crate::core::context_deficit::detect_deficit(&ledger, intent, &known);
92                        if !deficit.suggested_files.is_empty() {
93                            result.push_str("\n\n--- SUGGESTED FILES ---");
94                            for s in &deficit.suggested_files {
95                                result.push_str(&format!(
96                                    "\n  {} ({:?}, ~{} tok, mode: {})",
97                                    s.path, s.reason, s.estimated_tokens, s.recommended_mode
98                                ));
99                            }
100                        }
101
102                        let pressure = ledger.pressure();
103                        if pressure.utilization > 0.7 {
104                            let plan = ledger.reinjection_plan(intent, 0.6);
105                            if !plan.actions.is_empty() {
106                                result.push_str("\n\n--- REINJECTION PLAN ---");
107                                result.push_str(&format!(
108                                    "\n  Context pressure: {:.0}% -> target: 60%",
109                                    pressure.utilization * 100.0
110                                ));
111                                for a in &plan.actions {
112                                    result.push_str(&format!(
113                                        "\n  {} : {} -> {} (frees ~{} tokens)",
114                                        a.path, a.current_mode, a.new_mode, a.tokens_freed
115                                    ));
116                                }
117                                result.push_str(&format!(
118                                    "\n  Total freeable: {} tokens",
119                                    plan.total_tokens_freed
120                                ));
121                            }
122                        }
123                    }
124                }
125            }
126        }
127
128        Ok(ToolOutput {
129            text: result,
130            original_tokens: 0,
131            saved_tokens: 0,
132            mode: Some("preload".to_string()),
133            path: None,
134            changed: false,
135        })
136    }
137}