Skip to main content

lean_ctx/tools/registered/
ctx_metrics.rs

1use rmcp::ErrorData;
2use rmcp::model::Tool;
3use serde_json::{Map, Value, json};
4
5use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput};
6use crate::tool_defs::tool_def;
7
8pub struct CtxMetricsTool;
9
10impl McpTool for CtxMetricsTool {
11    fn name(&self) -> &'static str {
12        "ctx_metrics"
13    }
14
15    fn tool_def(&self) -> Tool {
16        tool_def(
17            "ctx_metrics",
18            "Session token statistics — cache hit rates, per-tool savings, pipeline metrics,\n\
19 and signature backend ratios.\n\
20 ANTI-PATTERN: not for real-time monitoring — snapshot of current session.\n\
21 Complements ctx_radar for budget analysis.",
22            json!({
23                "type": "object",
24                "properties": {}
25            }),
26        )
27    }
28
29    fn handle(
30        &self,
31        _args: &Map<String, Value>,
32        ctx: &ToolContext,
33    ) -> Result<ToolOutput, ErrorData> {
34        let cache = ctx
35            .cache
36            .as_ref()
37            .ok_or_else(|| ErrorData::internal_error("cache not available", None))?;
38        let Some(cache_guard) = crate::server::bounded_lock::read(cache, "ctx_metrics:cache")
39        else {
40            return Ok(ToolOutput::simple(
41                "[metrics unavailable — cache busy, retry]".to_string(),
42            ));
43        };
44        let calls = ctx
45            .tool_calls
46            .as_ref()
47            .ok_or_else(|| ErrorData::internal_error("tool_calls not available", None))?;
48        let Some(calls_guard) = crate::server::bounded_lock::read(calls, "ctx_metrics:calls")
49        else {
50            return Ok(ToolOutput::simple(
51                "[metrics unavailable — calls lock busy, retry]".to_string(),
52            ));
53        };
54        let mut result =
55            crate::tools::ctx_metrics::handle(&cache_guard, &calls_guard, ctx.crp_mode);
56        drop(cache_guard);
57        drop(calls_guard);
58
59        if let Some(ref ps) = ctx.pipeline_stats {
60            let Some(stats) = crate::server::bounded_lock::read(ps, "ctx_metrics:pipeline") else {
61                return Ok(ToolOutput::simple(result));
62            };
63            if stats.runs > 0 {
64                result.push_str("\n\n--- PIPELINE METRICS ---\n");
65                result.push_str(&stats.format_summary());
66            }
67        }
68
69        let (ts_hits, regex_hits) = crate::core::signatures::signature_backend_stats();
70        if ts_hits + regex_hits > 0 {
71            result.push_str("\n--- SIGNATURE BACKEND ---\n");
72            result.push_str(&format!(
73                "tree-sitter: {} | regex fallback: {} | ratio: {:.0}%\n",
74                ts_hits,
75                regex_hits,
76                if ts_hits + regex_hits > 0 {
77                    ts_hits as f64 / (ts_hits + regex_hits) as f64 * 100.0
78                } else {
79                    0.0
80                }
81            ));
82            // Persistent per-extension split (GH #690 Phase 2 telemetry) —
83            // top rows only; full history lives in grammar_usage.json.
84            let ranked = crate::core::grammar_usage::live_ranked();
85            if !ranked.is_empty() {
86                let rows: Vec<String> = ranked
87                    .iter()
88                    .take(8)
89                    .map(|(ext, u)| {
90                        format!(".{ext}: ts={} rx={}", u.tree_sitter_hits, u.regex_hits)
91                    })
92                    .collect();
93                result.push_str(&format!("by extension (all-time): {}\n", rows.join(" | ")));
94            }
95        }
96
97        Ok(ToolOutput::simple(result))
98    }
99}