Skip to main content

lean_ctx/server/
prompts.rs

1use rmcp::model::{
2    GetPromptRequestParams, GetPromptResult, Prompt, PromptArgument, PromptMessage, Role,
3};
4
5fn required_arg(name: &str, desc: &str) -> PromptArgument {
6    PromptArgument::new(name)
7        .with_description(desc)
8        .with_required(true)
9}
10
11pub fn list_prompts() -> Vec<Prompt> {
12    vec![
13        Prompt::new(
14            "context-focus",
15            Some("Set task intent and optimize context for a specific task"),
16            Some(vec![required_arg("task", "What you are working on")]),
17        ),
18        Prompt::new(
19            "context-review",
20            Some("Review current context state: items, pressure, budget, recommendations"),
21            None,
22        ),
23        Prompt::new(
24            "context-reset",
25            Some("Clear all overlays and reset context state"),
26            None,
27        ),
28        Prompt::new(
29            "context-pin",
30            Some("Pin a file to keep it in full context"),
31            Some(vec![required_arg("path", "Path to the file to pin")]),
32        ),
33        Prompt::new(
34            "context-budget",
35            Some("Set the token budget for this session"),
36            Some(vec![required_arg(
37                "tokens",
38                "Max tokens for the context window",
39            )]),
40        ),
41    ]
42}
43
44pub fn get_prompt(
45    params: &GetPromptRequestParams,
46    ledger: &crate::core::context_ledger::ContextLedger,
47) -> Option<GetPromptResult> {
48    match params.name.as_str() {
49        "context-focus" => {
50            let task = params
51                .arguments
52                .as_ref()
53                .and_then(|a| a.get("task"))
54                .and_then(|v| v.as_str())
55                .unwrap_or("general development");
56            Some(get_context_focus(task, ledger))
57        }
58        "context-review" => Some(get_context_review(ledger)),
59        "context-reset" => Some(get_context_reset()),
60        "context-pin" => {
61            let path = params
62                .arguments
63                .as_ref()
64                .and_then(|a| a.get("path"))
65                .and_then(|v| v.as_str())
66                .unwrap_or("");
67            Some(get_context_pin(path))
68        }
69        "context-budget" => {
70            let tokens = params
71                .arguments
72                .as_ref()
73                .and_then(|a| a.get("tokens"))
74                .and_then(|v| v.as_str())
75                .unwrap_or("128000");
76            Some(get_context_budget(tokens))
77        }
78        _ => None,
79    }
80}
81
82fn get_context_focus(
83    task: &str,
84    ledger: &crate::core::context_ledger::ContextLedger,
85) -> GetPromptResult {
86    let pressure = ledger.pressure();
87    let msg = format!(
88        "Focus context on task: {task}\n\
89         Current state: {} files, {:.0}% pressure\n\
90         Use ctx_plan(task=\"{task}\") to compute optimal modes for all tracked files.\n\
91         Files matching this task's intent targets should be read as 'full', others compressed.",
92        ledger.entries.len(),
93        pressure.utilization * 100.0,
94    );
95    GetPromptResult::new(vec![PromptMessage::new_text(Role::Assistant, msg)])
96}
97
98fn get_context_review(ledger: &crate::core::context_ledger::ContextLedger) -> GetPromptResult {
99    let summary = ledger.format_summary();
100    let adjusted = ledger.adjusted_total_saved();
101    let bounce_info = match crate::core::bounce_tracker::global().lock() {
102        Ok(bt) => bt.format_summary(),
103        _ => String::new(),
104    };
105    let msg = format!(
106        "Context Review:\n{summary}\nAdjusted savings: {adjusted} tokens\n{bounce_info}\n\n\
107         Use ctx_metrics() for detailed breakdown or ctx_plan(task=\"review context state\") for mode recommendations.",
108    );
109    GetPromptResult::new(vec![PromptMessage::new_text(Role::Assistant, msg)])
110}
111
112fn get_context_reset() -> GetPromptResult {
113    GetPromptResult::new(vec![PromptMessage::new_text(
114        Role::Assistant,
115        "Reset context: Use ctx_control(action=\"reset\") to clear all overlays and reset ledger states.",
116    )])
117}
118
119fn get_context_pin(path: &str) -> GetPromptResult {
120    let msg = format!(
121        "Pin file: Use ctx_control(action=\"pin\", target=\"{path}\") to keep this file in full context regardless of pressure."
122    );
123    GetPromptResult::new(vec![PromptMessage::new_text(Role::Assistant, msg)])
124}
125
126fn get_context_budget(tokens: &str) -> GetPromptResult {
127    let msg = format!(
128        "Set budget: Configure the context window to {tokens} tokens. \
129         Use ctx_session(action=\"budget\", value=\"{tokens}\") or set LCTX_CONTEXT_BUDGET={tokens} in your environment."
130    );
131    GetPromptResult::new(vec![PromptMessage::new_text(Role::Assistant, msg)])
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn list_returns_five_prompts() {
140        let prompts = list_prompts();
141        assert_eq!(prompts.len(), 5);
142    }
143
144    #[test]
145    fn context_focus_has_task_arg() {
146        let prompts = list_prompts();
147        let focus = prompts.iter().find(|p| p.name == "context-focus").unwrap();
148        let args = focus.arguments.as_ref().unwrap();
149        assert_eq!(args[0].name, "task");
150    }
151
152    #[test]
153    fn get_unknown_prompt_returns_none() {
154        let ledger = crate::core::context_ledger::ContextLedger::new();
155        let params = GetPromptRequestParams::new("unknown-prompt");
156        assert!(get_prompt(&params, &ledger).is_none());
157    }
158
159    #[test]
160    fn get_context_review_returns_result() {
161        let ledger = crate::core::context_ledger::ContextLedger::new();
162        let params = GetPromptRequestParams::new("context-review");
163        let result = get_prompt(&params, &ledger);
164        assert!(result.is_some());
165    }
166}