lean_ctx/tools/registered/
ctx_knowledge.rs1use rmcp::ErrorData;
2use rmcp::model::Tool;
3use serde_json::{Map, Value, json};
4
5use crate::server::tool_trait::{
6 McpTool, ToolContext, ToolOutput, get_f64, get_str, get_str_array,
7};
8use crate::tool_defs::tool_def;
9
10pub struct CtxKnowledgeTool;
11
12impl McpTool for CtxKnowledgeTool {
13 fn name(&self) -> &'static str {
14 "ctx_knowledge"
15 }
16
17 fn tool_def(&self) -> Tool {
18 tool_def(
19 "ctx_knowledge",
20 "Persistent memory across sessions — remember decisions, patterns, and facts for recall.\n\
21 WORKFLOW: save after completing significant tasks; recall at session start.\n\
22 action=remember key='X' value='Y' saves a fact (both required).\n\
23 action=recall query='X' retrieves it. action=status shows all categories.\n\
24 action=gotcha trigger='X' resolution='Y' for known pitfalls.\n\
25 mode=semantic|exact for recall. category groups related facts.",
26 json!({
27 "type": "object",
28 "properties": {
29 "action": {
30 "type": "string",
31 "description": "remember|recall|search|pattern|gotcha|relate|relations|consolidate|status|timeline|rooms|wakeup|remove|export (also: feedback, unrelate, relations_diagram, health, lifecycle_report, policy, embeddings_*)"
32 },
33 "trigger": { "type": "string", "description": "gotcha trigger pattern" },
34 "resolution": { "type": "string", "description": "gotcha resolution/fix" },
35 "severity": { "type": "string", "description": "gotcha: critical|warning|info" },
36 "category": { "type": "string", "description": "Fact category" },
37 "key": { "type": "string" },
38 "value": { "type": "string" },
39 "query": { "type": "string", "description": "Query for recall/search/relate" },
40 "mode": { "type": "string", "description": "auto|exact|semantic|hybrid" },
41 "as_of": { "type": "string", "description": "YYYY-MM-DD date filter" },
42 "pattern_type": { "type": "string" },
43 "examples": { "type": "array", "items": { "type": "string" } },
44 "confidence": { "type": "number", "description": "0.0-1.0" }
45 },
46 "required": ["action"]
47 }),
48 )
49 }
50
51 fn handle(
52 &self,
53 args: &Map<String, Value>,
54 ctx: &ToolContext,
55 ) -> Result<ToolOutput, ErrorData> {
56 let action = get_str(args, "action")
57 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
58 let category = get_str(args, "category");
59 let key = get_str(args, "key");
60 let value = get_str(args, "value");
61 let query = get_str(args, "query");
62 let mode = get_str(args, "mode");
63 let as_of = get_str(args, "as_of");
64 let pattern_type = get_str(args, "pattern_type");
65 let examples = get_str_array(args, "examples");
66 let confidence = get_f64(args, "confidence").map(|v| v as f32);
67
68 let session_handle = ctx
69 .session
70 .as_ref()
71 .ok_or_else(|| ErrorData::internal_error("session not available", None))?;
72 let (session_id, project_root) = {
73 let timeout_dur =
74 crate::core::io_health::adaptive_timeout(std::time::Duration::from_secs(10));
75 let read_result = tokio::task::block_in_place(|| {
76 tokio::runtime::Handle::current()
77 .block_on(tokio::time::timeout(timeout_dur, session_handle.read()))
78 });
79 if let Ok(session) = read_result {
80 let sid = session.id.clone();
81 let root = session
82 .project_root
83 .clone()
84 .unwrap_or_else(|| ctx.project_root.clone());
85 (sid, root)
86 } else {
87 tracing::warn!("ctx_knowledge: session read-lock timeout, using fallback");
88 ("unknown".to_string(), ctx.project_root.clone())
89 }
90 };
91
92 if action == "gotcha" {
93 let trigger = get_str(args, "trigger").unwrap_or_default();
94 let resolution = get_str(args, "resolution").unwrap_or_default();
95 let severity = get_str(args, "severity").unwrap_or_default();
96 let cat = category.as_deref().unwrap_or("convention");
97
98 if trigger.is_empty() || resolution.is_empty() {
99 return Ok(ToolOutput {
100 text: "ERROR: trigger and resolution are required for gotcha action"
101 .to_string(),
102 original_tokens: 0,
103 saved_tokens: 0,
104 mode: Some(action),
105 path: None,
106 changed: false,
107 shell_outcome: None,
108 });
109 }
110
111 let mut store = crate::core::gotcha_tracker::GotchaStore::load(&project_root);
112 let msg = match store.report_gotcha(&trigger, &resolution, cat, &severity, &session_id)
113 {
114 Some(gotcha) => {
115 let conf = (gotcha.confidence * 100.0) as u32;
116 let label = gotcha.category.short_label();
117 format!("Gotcha recorded: [{label}] {trigger} (confidence: {conf}%)")
118 }
119 None => {
120 format!("Gotcha noted: {trigger} (evicted by higher-confidence entries)")
121 }
122 };
123 let _ = store.save(&project_root);
124 return Ok(ToolOutput {
125 text: msg,
126 original_tokens: 0,
127 saved_tokens: 0,
128 mode: Some(action),
129 path: None,
130 changed: false,
131 shell_outcome: None,
132 });
133 }
134
135 let result = crate::tools::ctx_knowledge::handle(
136 &project_root,
137 &action,
138 category.as_deref(),
139 key.as_deref(),
140 value.as_deref(),
141 query.as_deref(),
142 &session_id,
143 pattern_type.as_deref(),
144 examples,
145 confidence,
146 mode.as_deref(),
147 as_of.as_deref(),
148 );
149
150 Ok(ToolOutput {
151 text: result,
152 original_tokens: 0,
153 saved_tokens: 0,
154 mode: Some(action),
155 path: None,
156 changed: false,
157 shell_outcome: None,
158 })
159 }
160}