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