Skip to main content

lean_ctx/tools/registered/
ctx_compose.rs

1use crate::core::ocla::cache_types::{CacheKeyBuilder, ComposedContextKey};
2use rmcp::ErrorData;
3use rmcp::model::Tool;
4use serde_json::{Map, Value, json};
5
6use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput, get_str};
7use crate::tool_defs::tool_def;
8
9pub struct CtxComposeTool;
10
11impl McpTool for CtxComposeTool {
12    fn name(&self) -> &'static str {
13        "ctx_compose"
14    }
15
16    fn tool_def(&self) -> Tool {
17        tool_def(
18            "ctx_compose",
19            "PRIMARY TOOL — call FIRST for understanding code (before editing/debugging/'how does X work').\n\
20             Returns ranked files with relevant symbol source inline grouped by file.\n\
21             Combines BM25 lexical+semantic+associative retrieval+submodular optimization.\n\
22             ANTIPATTERN: Do NOT chain search→read→symbol — one compose replaces the whole chain.\n\
23             ANTIPATTERN: Do NOT Read files whose source compose already returned — it IS the source.\n\
24             WORKFLOW: Fire parallel ctx_read or ctx_compose for different areas.",
25            json!({
26                "type": "object",
27                "properties": {
28                    "task": { "type": "string", "description": "Short English task/question or symbol names" },
29                    "path": { "type": "string", "description": "Project root" }
30                },
31                "required": ["task"]
32            }),
33        )
34    }
35
36    fn handle(
37        &self,
38        args: &Map<String, Value>,
39        ctx: &ToolContext,
40    ) -> Result<ToolOutput, ErrorData> {
41        let task = get_str(args, "task")
42            .ok_or_else(|| ErrorData::invalid_params("task is required", None))?;
43        let path = if let Some(p) = ctx.resolved_path("path") {
44            p.to_string()
45        } else if let Some(err) = ctx.path_error("path") {
46            return Err(ErrorData::invalid_params(format!("path: {err}"), None));
47        } else {
48            ctx.project_root.clone()
49        };
50
51        // Share the resident BM25 cache with the composed semantic search.
52        if let Some(ref cache) = ctx.bm25_cache {
53            crate::tools::ctx_semantic_search::set_thread_cache(cache.clone());
54        }
55
56        let cache_enabled = crate::core::config::Config::load()
57            .cache
58            .compose_cache_enabled;
59        let cached = cache_enabled
60            .then(|| crate::core::ocla::compose_cache::global().check(&task, &path))
61            .flatten();
62        let (text, sent) = if let Some(text) = cached {
63            let sent = crate::core::tokens::count_tokens(&text);
64            (text, sent)
65        } else {
66            // Cross-process delivery check before expensive computation
67            let compose_builder = ComposedContextKey {
68                task: task.clone(),
69                path: path.clone(),
70                source_digests: Vec::new(),
71            };
72            let ck = compose_builder.cache_key();
73            let cv = compose_builder.validator();
74            if let Some(entry) = crate::core::ocla::cache_delivery::check(&ck, &cv, "ctx_compose") {
75                let stub = crate::core::ocla::cache_delivery::stub(&entry, "compose");
76                let sent = crate::core::tokens::count_tokens(&stub);
77                (stub, sent)
78            } else {
79                let (text, sent) = tokio::task::block_in_place(|| {
80                    crate::tools::ctx_compose::handle(&task, &path, ctx.crp_mode)
81                });
82                if cache_enabled && !text.starts_with("ERROR") {
83                    crate::core::ocla::compose_cache::global().record(&task, &path, text.clone());
84                    crate::core::ocla::cache_delivery::record(
85                        ck,
86                        crate::core::ocla::cache_types::DeliveryKind::ComposedContext,
87                        cv,
88                        Some(path.clone()),
89                        &text,
90                        "ctx_compose",
91                    );
92                }
93                (text, sent)
94            }
95        };
96
97        if text.starts_with("ERROR") {
98            return Err(ErrorData::invalid_params(text, None));
99        }
100
101        Ok(ToolOutput {
102            text,
103            original_tokens: sent,
104            saved_tokens: 0,
105            mode: Some("compose".to_string()),
106            path: Some(path),
107            changed: false,
108            shell_outcome: None,
109            content_blocks: None,
110        })
111    }
112}