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 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}