lean_ctx_server/tools/registered/
ctx_feedback.rs1use rmcp::ErrorData;
3use rmcp::model::Tool;
4use serde_json::{Map, Value, json};
5
6use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput, get_int, get_str};
7use lean_ctx::tool_defs::tool_def;
8
9pub struct CtxFeedbackTool;
10
11impl McpTool for CtxFeedbackTool {
12 fn name(&self) -> &'static str {
13 "ctx_feedback"
14 }
15
16 fn tool_def(&self) -> Tool {
17 tool_def(
18 "ctx_feedback",
19 "Record and report LLM token/latency metrics — use to track efficiency and optimize context usage.\n\
20 WORKFLOW: action=record during each LLM call, then action=report for readable summary.\n\
21 Actions: record (log event), report (readable summary), json (machine-readable),\n\
22 reset (clear data), status (storage info).\n\
23 ANTIPATTERN: not for debugging code behavior — this tracks token/latency stats only.\n\
24 record requires llm_input_tokens + llm_output_tokens.",
25 json!({
26 "type": "object",
27 "properties": {
28 "action": {
29 "type": "string",
30 "enum": ["record", "report", "json", "reset", "status"],
31 "description": "record (log event) | report (summary) | json (data) | reset (clear) | status (storage)"
32 },
33 "agent_id": { "type": "string", "description": "Agent ID (default: current agent)" },
34 "intent": { "type": "string", "description": "Intent/task string" },
35 "model": { "type": "string", "description": "Model identifier" },
36 "llm_input_tokens": { "type": "integer", "description": "Required for action=record" },
37 "llm_output_tokens": { "type": "integer", "description": "Required for action=record" },
38 "latency_ms": { "type": "integer", "description": "Latency in ms (for record)" },
39 "note": { "type": "string", "description": "Note (no prompts/PII)" },
40 "limit": { "type": "integer", "description": "Max recent events (default: 500)" }
41 },
42 "allOf": [
43 {
44 "if": { "properties": { "action": { "const": "record" } }, "required": ["action"] },
45 "then": { "required": ["action", "llm_input_tokens", "llm_output_tokens"] }
46 }
47 ]
48 }),
49 )
50 }
51
52 fn handle(
53 &self,
54 args: &Map<String, Value>,
55 ctx: &ToolContext,
56 ) -> Result<ToolOutput, ErrorData> {
57 let action = get_str(args, "action").unwrap_or_else(|| "report".to_string());
58 let limit = get_int(args, "limit").map_or(500, |n| n.max(1) as usize);
59
60 let result = match action.as_str() {
61 "record" => {
62 let current_agent_id = ctx
63 .agent_id
64 .as_ref()
65 .and_then(|a| tokio::task::block_in_place(|| a.blocking_read()).clone());
66 let agent_id = get_str(args, "agent_id").or(current_agent_id);
67 let agent_id = agent_id.ok_or_else(|| {
68 ErrorData::invalid_params(
69 "agent_id is required (or register an agent via project_root detection first)",
70 None,
71 )
72 })?;
73
74 let (ctx_read_last_mode, ctx_read_modes) = if let Some(ref tc) = ctx.tool_calls {
75 let calls = tokio::task::block_in_place(|| tc.blocking_read());
76 let mut last: Option<String> = None;
77 let mut modes: std::collections::BTreeMap<String, u64> =
78 std::collections::BTreeMap::new();
79 for rec in calls.iter().rev().take(50) {
80 if rec.tool != "ctx_read" {
81 continue;
82 }
83 if let Some(m) = rec.mode.as_ref() {
84 *modes.entry(m.clone()).or_insert(0) += 1;
85 if last.is_none() {
86 last = Some(m.clone());
87 }
88 }
89 }
90 (last, if modes.is_empty() { None } else { Some(modes) })
91 } else {
92 (None, None)
93 };
94
95 let llm_input_tokens = get_int(args, "llm_input_tokens").ok_or_else(|| {
96 ErrorData::invalid_params("llm_input_tokens is required", None)
97 })?;
98 let llm_output_tokens = get_int(args, "llm_output_tokens").ok_or_else(|| {
99 ErrorData::invalid_params("llm_output_tokens is required", None)
100 })?;
101 if llm_input_tokens <= 0 || llm_output_tokens <= 0 {
102 return Err(ErrorData::invalid_params(
103 "llm_input_tokens and llm_output_tokens must be > 0",
104 None,
105 ));
106 }
107
108 let ev = lean_ctx::core::llm_feedback::LlmFeedbackEvent {
109 agent_id,
110 intent: get_str(args, "intent"),
111 model: get_str(args, "model"),
112 llm_input_tokens: llm_input_tokens as u64,
113 llm_output_tokens: llm_output_tokens as u64,
114 latency_ms: get_int(args, "latency_ms").map(|n| n.max(0) as u64),
115 note: get_str(args, "note"),
116 ctx_read_last_mode,
117 ctx_read_modes,
118 timestamp: chrono::Local::now().to_rfc3339(),
119 };
120 crate::tools::ctx_feedback::record(&ev)
121 .unwrap_or_else(|e| format!("Error recording feedback: {e}"))
122 }
123 "status" => crate::tools::ctx_feedback::status(),
124 "json" => crate::tools::ctx_feedback::json(limit),
125 "reset" => crate::tools::ctx_feedback::reset(),
126 _ => crate::tools::ctx_feedback::report(limit),
127 };
128
129 Ok(ToolOutput {
130 text: result,
131 original_tokens: 0,
132 saved_tokens: 0,
133 mode: Some(action),
134 path: None,
135 changed: false,
136 shell_outcome: None,
137 content_blocks: None,
138 })
139 }
140}