lean_ctx/tools/registered/
ctx_ledger.rs1use rmcp::model::Tool;
2use rmcp::ErrorData;
3use serde_json::{json, Map, Value};
4
5use crate::core::context_field::ContextItemId;
6use crate::core::context_overlay::{
7 ContextOverlay, OverlayAuthor, OverlayOp, OverlayScope, OverlayStore,
8};
9use crate::server::tool_trait::{get_str, McpTool, ToolContext, ToolOutput};
10use crate::tool_defs::tool_def;
11
12pub struct CtxLedgerTool;
13
14impl McpTool for CtxLedgerTool {
15 fn name(&self) -> &'static str {
16 "ctx_ledger"
17 }
18
19 fn tool_def(&self) -> Tool {
20 tool_def(
21 "ctx_ledger",
22 "Context ledger ops: status|reset|evict. Manages persistent context pressure.",
23 json!({
24 "type": "object",
25 "properties": {
26 "action": {
27 "type": "string",
28 "enum": ["status", "reset", "evict"],
29 "description": "Ledger operation: status (show pressure), reset (clear all), evict (remove specific files)"
30 },
31 "targets": {
32 "type": "string",
33 "description": "Comma-separated file paths to evict (required for 'evict' action)"
34 }
35 },
36 "required": ["action"]
37 }),
38 )
39 }
40
41 fn handle(
42 &self,
43 args: &Map<String, Value>,
44 ctx: &ToolContext,
45 ) -> Result<ToolOutput, ErrorData> {
46 let action = get_str(args, "action")
47 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
48
49 let ledger_arc = ctx
50 .ledger
51 .as_ref()
52 .ok_or_else(|| ErrorData::internal_error("ledger not available", None))?;
53
54 let result = match action.as_str() {
55 "status" => {
56 let Some(ledger) =
57 crate::server::bounded_lock::read(ledger_arc, "ctx_ledger:status")
58 else {
59 return Ok(ToolOutput::simple(
60 "[ledger status unavailable — busy, retry]".to_string(),
61 ));
62 };
63 let pressure = ledger.pressure();
64 let top_files: Vec<String> = ledger
65 .files_by_token_cost()
66 .iter()
67 .take(5)
68 .map(|(path, tokens)| {
69 format!(
70 " {} ({} tok)",
71 crate::core::protocol::shorten_path(path),
72 tokens
73 )
74 })
75 .collect();
76
77 let mut lines = vec![
78 format!(
79 "Context pressure: {:.0}% ({}/{} tokens)",
80 pressure.utilization * 100.0,
81 ledger.total_tokens_sent,
82 ledger.window_size,
83 ),
84 format!("Entries: {}", ledger.entries.len()),
85 format!("Recommendation: {:?}", pressure.recommendation),
86 ];
87 if !top_files.is_empty() {
88 lines.push("Top files by cost:".to_string());
89 lines.extend(top_files);
90 }
91 lines.join("\n")
92 }
93
94 "reset" => {
95 let Some(mut ledger) =
96 crate::server::bounded_lock::write(ledger_arc, "ctx_ledger:reset")
97 else {
98 return Ok(ToolOutput::simple(
99 "[ledger reset unavailable — busy, retry]".to_string(),
100 ));
101 };
102 let prev_entries = ledger.entries.len();
103 let prev_tokens = ledger.total_tokens_sent;
104 ledger.reset();
105 ledger.save();
106 format!(
107 "Ledger reset. Removed {prev_entries} entries, freed {prev_tokens} tracked tokens. Pressure: 0%."
108 )
109 }
110
111 "evict" => {
112 let targets_str = get_str(args, "targets").ok_or_else(|| {
113 ErrorData::invalid_params(
114 "targets is required for evict action (comma-separated paths)",
115 None,
116 )
117 })?;
118
119 let targets: Vec<&str> = targets_str.split(',').map(str::trim).collect();
120 if targets.is_empty() {
121 return Ok(ToolOutput::simple(
122 "No targets specified for eviction.".to_string(),
123 ));
124 }
125
126 let Some(mut ledger) =
127 crate::server::bounded_lock::write(ledger_arc, "ctx_ledger:evict")
128 else {
129 return Ok(ToolOutput::simple(
130 "[ledger evict unavailable — busy, retry]".to_string(),
131 ));
132 };
133 let removed = ledger.evict_paths(&targets);
134
135 let root = if ctx.project_root.is_empty() {
137 "."
138 } else {
139 &ctx.project_root
140 };
141 let mut overlays = OverlayStore::load_project(&std::path::PathBuf::from(root));
142 for target in &targets {
143 let item_id = ContextItemId::from_file(target);
144 let overlay = ContextOverlay::new(
145 item_id,
146 OverlayOp::Exclude {
147 reason: "evicted by ctx_ledger".into(),
148 },
149 OverlayScope::Session,
150 String::new(),
151 OverlayAuthor::Policy("ctx_ledger_evict".into()),
152 );
153 overlays.add(overlay);
154 }
155 let _ = overlays.save_project(&std::path::PathBuf::from(root));
156
157 ledger.save();
158
159 let pressure = ledger.pressure();
160 format!(
161 "Evicted {removed}/{} target(s). Pressure now: {:.0}%. Files excluded from re-accumulation until session reset.",
162 targets.len(),
163 pressure.utilization * 100.0,
164 )
165 }
166
167 _ => "Unknown action. Use: status, reset, evict".to_string(),
168 };
169
170 let changed = action != "status";
171 Ok(ToolOutput {
172 text: result,
173 original_tokens: 0,
174 saved_tokens: 0,
175 mode: Some(action),
176 path: None,
177 changed,
178 })
179 }
180}