Skip to main content

lean_ctx/tools/ctx_cognitive/
mod.rs

1//! ctx_cognitive — science-driven context intelligence MCP tool.
2
3use rmcp::ErrorData;
4use rmcp::model::Tool;
5use serde::{Deserialize, Serialize};
6use serde_json::{Map, Value, json};
7
8use crate::core::config::CognitiveMode;
9use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput};
10use crate::tool_defs::tool_def;
11
12#[derive(Debug, Deserialize)]
13pub struct CognitiveParams {
14    pub action: String,
15}
16
17#[derive(Debug, Serialize)]
18#[serde(rename_all = "camelCase")]
19struct FeatureStatus {
20    name: &'static str,
21    enabled: bool,
22}
23
24#[derive(Debug, Serialize)]
25#[serde(rename_all = "camelCase")]
26struct CognitiveStatus {
27    action: &'static str,
28    cognitive_mode: String,
29    features: Vec<&'static str>,
30}
31
32#[derive(Debug, Serialize)]
33#[serde(rename_all = "camelCase")]
34struct CognitiveFeatures {
35    action: &'static str,
36    cognitive_mode: String,
37    features: Vec<FeatureStatus>,
38}
39
40pub struct CtxCognitiveTool;
41
42impl McpTool for CtxCognitiveTool {
43    fn name(&self) -> &'static str {
44        "ctx_cognitive"
45    }
46
47    fn tool_def(&self) -> Tool {
48        tool_def(
49            "ctx_cognitive",
50            "Read science-driven context intelligence status and cognitive impact.\n\
51             action=status reports the active mode; impact reports interruption savings; \
52             features lists every science feature and whether it is enabled.",
53            json!({
54                "type": "object",
55                "properties": {
56                    "action": {
57                        "type": "string",
58                        "enum": ["status", "impact", "features"],
59                        "description": "Cognitive information to return"
60                    }
61                },
62                "required": ["action"]
63            }),
64        )
65    }
66
67    fn handle(
68        &self,
69        args: &Map<String, Value>,
70        _ctx: &ToolContext,
71    ) -> Result<ToolOutput, ErrorData> {
72        let params: CognitiveParams = serde_json::from_value(Value::Object(args.clone()))
73            .map_err(|error| ErrorData::invalid_params(error.to_string(), None))?;
74        let mode = crate::core::config::Config::load().cognitive_mode;
75
76        let value = match params.action.as_str() {
77            "status" => serde_json::to_value(CognitiveStatus {
78                action: "status",
79                cognitive_mode: mode.to_string(),
80                features: feature_statuses(mode)
81                    .into_iter()
82                    .filter_map(|feature| feature.enabled.then_some(feature.name))
83                    .collect(),
84            }),
85            "impact" => {
86                let report = crate::core::anti_interrupt::compute_impact();
87                Ok(json!({
88                    "action": "impact",
89                    "interruptionsPrevented": report.interruptions_prevented,
90                    "contextSwitchesSaved": report.context_switches_saved,
91                    "echoTokensSaved": report.echo_tokens_saved,
92                    "cognitiveLoadReduction": report.cognitive_load_reduction,
93                    "focusTimeSavedMinutes": report.focus_time_saved_minutes,
94                    "score": report.score
95                }))
96            }
97            "features" => serde_json::to_value(CognitiveFeatures {
98                action: "features",
99                cognitive_mode: mode.to_string(),
100                features: feature_statuses(mode),
101            }),
102            _ => {
103                return Err(ErrorData::invalid_params(
104                    "action must be one of: status, impact, features",
105                    None,
106                ));
107            }
108        }
109        .map_err(|error| ErrorData::internal_error(error.to_string(), None))?;
110
111        let text = serde_json::to_string_pretty(&value)
112            .map_err(|error| ErrorData::internal_error(error.to_string(), None))?;
113        Ok(ToolOutput::simple(text))
114    }
115
116    fn produces_machine_readable(&self, _args: Option<&Map<String, Value>>) -> bool {
117        true
118    }
119}
120
121fn feature_statuses(mode: CognitiveMode) -> Vec<FeatureStatus> {
122    let basic = !matches!(mode, CognitiveMode::Off);
123    let full = matches!(mode, CognitiveMode::Full);
124
125    vec![
126        FeatureStatus {
127            name: "intent_classification",
128            enabled: basic,
129        },
130        FeatureStatus {
131            name: "semantic_chunking",
132            enabled: basic,
133        },
134        FeatureStatus {
135            name: "memory_scheduling",
136            enabled: full,
137        },
138        FeatureStatus {
139            name: "anti_interruption",
140            enabled: full,
141        },
142        FeatureStatus {
143            name: "optimal_transport_allocation",
144            enabled: full,
145        },
146        FeatureStatus {
147            name: "graph_expansion",
148            enabled: full,
149        },
150        FeatureStatus {
151            name: "structural_descriptions",
152            enabled: full,
153        },
154        FeatureStatus {
155            name: "verbosity_learning",
156            enabled: full,
157        },
158        FeatureStatus {
159            name: "context_prefetch",
160            enabled: full,
161        },
162        FeatureStatus {
163            name: "stigmergic_coordination",
164            enabled: full,
165        },
166    ]
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn basic_mode_enables_only_basic_features() {
175        let features = feature_statuses(CognitiveMode::Basic);
176        assert_eq!(features.iter().filter(|feature| feature.enabled).count(), 2);
177        assert!(features[0].enabled);
178        assert!(features[1].enabled);
179    }
180
181    #[test]
182    fn full_mode_enables_every_feature() {
183        let features = feature_statuses(CognitiveMode::Full);
184        assert_eq!(features.len(), 10);
185        assert!(features.iter().all(|feature| feature.enabled));
186    }
187}