Skip to main content

lean_ctx/tools/registered/
ctx_knowledge.rs

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