lean_ctx/tools/registered/
ctx_preload.rs1use 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 changed: false,
129 })
130 }
131}