use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use super::dynamic_tools::{ToolCategory, categorize_tool};
use crate::core::tool_profiles::ToolProfile;
static AUTO_TURN_COUNT: AtomicU64 = AtomicU64::new(0);
static AUTO_CTX_TOOLS_USED: AtomicBool = AtomicBool::new(false);
static AUTO_SYSTEM_PROMPT_TOKENS: AtomicUsize = AtomicUsize::new(0);
pub fn record_auto_turn() {
AUTO_TURN_COUNT.fetch_add(1, Ordering::Relaxed);
}
pub fn mark_auto_ctx_tool_used() {
AUTO_CTX_TOOLS_USED.store(true, Ordering::Relaxed);
}
#[allow(dead_code)]
pub fn set_auto_system_prompt_tokens(tokens: usize) {
AUTO_SYSTEM_PROMPT_TOKENS.store(tokens, Ordering::Relaxed);
}
#[must_use]
pub fn resolve_auto_profile(profile: &ToolProfile) -> ToolProfile {
if *profile != ToolProfile::Auto {
return profile.clone();
}
ToolProfile::resolve_auto(
AUTO_TURN_COUNT.load(Ordering::Relaxed),
AUTO_CTX_TOOLS_USED.load(Ordering::Relaxed),
AUTO_SYSTEM_PROMPT_TOKENS.load(Ordering::Relaxed),
)
}
pub const INVOKER: &str = "ctx_call";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CandidateSet {
Full,
Unified,
ProfileAuthoritative,
LazyCore,
}
#[must_use]
pub fn candidate_set(full_mode: bool, unified_env: bool, explicit_profile: bool) -> CandidateSet {
if full_mode {
CandidateSet::Full
} else if unified_env {
CandidateSet::Unified
} else if explicit_profile {
CandidateSet::ProfileAuthoritative
} else {
CandidateSet::LazyCore
}
}
#[must_use]
pub fn explicit_profile(cfg: &crate::core::config::Config) -> bool {
cfg.tool_profile.is_some()
|| !cfg.tools_enabled.is_empty()
|| std::env::var("LEAN_CTX_TOOL_PROFILE").is_ok()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ClientQuirks {
pub hide_ctx_edit: bool,
pub hide_ctx_patch: bool,
}
impl ClientQuirks {
#[must_use]
pub fn resolve(client_name: &str, candidate: CandidateSet) -> Self {
let lower = client_name.to_lowercase();
Self {
hide_ctx_edit: lower.contains("zed"),
hide_ctx_patch: candidate == CandidateSet::LazyCore && has_native_editor(&lower),
}
}
}
fn has_native_editor(lower_client_name: &str) -> bool {
[
"cursor",
"zed",
"windsurf",
"codeium",
"antigravity",
"opencode",
]
.iter()
.any(|c| lower_client_name.contains(c))
}
#[must_use]
pub fn is_tool_visible(
name: &str,
profile: &ToolProfile,
disabled: &[String],
quirks: ClientQuirks,
role_allows: bool,
) -> bool {
if categorize_tool(name) == ToolCategory::Internal {
return false;
}
if super::dynamic_tools::is_deprecated_alias(name) {
return false;
}
if !profile.is_tool_enabled(name) {
return false;
}
if disabled.iter().any(|d| d == name) {
return false;
}
if quirks.hide_ctx_edit && name == "ctx_edit" {
return false;
}
if quirks.hide_ctx_patch && name == "ctx_patch" {
return false;
}
role_allows
}
#[must_use]
pub fn advertised_tool_defs_default() -> Vec<rmcp::model::Tool> {
let cfg = crate::core::config::Config::load();
let disabled = cfg.disabled_tools_effective();
let profile = cfg.tool_profile_effective();
let full_mode = crate::tool_defs::is_full_mode();
let registry = crate::server::registry::build_registry();
let candidate = candidate_set(
full_mode,
std::env::var("LEAN_CTX_UNIFIED").is_ok(),
explicit_profile(&cfg),
);
let pool: Vec<rmcp::model::Tool> = match candidate {
CandidateSet::Full | CandidateSet::ProfileAuthoritative => registry.tool_defs(),
CandidateSet::Unified => crate::tool_defs::unified_tool_defs(),
CandidateSet::LazyCore => {
let core = crate::tool_defs::core_tool_names();
registry
.tool_defs()
.into_iter()
.filter(|t| core.contains(&t.name.as_ref()))
.collect()
}
};
let mut tools: Vec<_> = pool
.into_iter()
.filter(|t| {
is_tool_visible(
t.name.as_ref(),
&profile,
&disabled,
ClientQuirks::default(),
true,
)
})
.collect();
let already = tools.iter().any(|t| t.name.as_ref() == INVOKER);
if needs_invoker(full_mode, already, true, &disabled)
&& let Some(def) = registry
.tool_defs()
.into_iter()
.find(|t| t.name.as_ref() == INVOKER)
{
tools.push(def);
}
let level = crate::core::config::CompressionLevel::effective(&cfg);
let mode = crate::core::terse::mcp_compress::DescriptionMode::from_compression_level(&level);
if mode == crate::core::terse::mcp_compress::DescriptionMode::Full {
return tools;
}
tools
.into_iter()
.map(|mut t| {
let compressed = crate::core::terse::mcp_compress::compress_description(
t.name.as_ref(),
t.description.as_deref().unwrap_or(""),
mode,
);
t.description = Some(compressed.into());
t
})
.collect()
}
#[must_use]
pub fn category_gate_applies(supports_list_changed: bool, explicit_profile: bool) -> bool {
supports_list_changed && !explicit_profile
}
#[must_use]
pub fn needs_invoker(
full_mode: bool,
already_present: bool,
invoker_role_allowed: bool,
disabled: &[String],
) -> bool {
!full_mode && !already_present && invoker_role_allowed && !disabled.iter().any(|d| d == INVOKER)
}
#[cfg(test)]
mod tests {
use super::*;
fn no_quirks() -> ClientQuirks {
ClientQuirks::default()
}
#[test]
fn internal_tools_never_visible_even_in_power() {
let p = ToolProfile::Power;
assert!(!is_tool_visible("ctx_metrics", &p, &[], no_quirks(), true));
assert!(!is_tool_visible("ctx_cost", &p, &[], no_quirks(), true));
assert!(!is_tool_visible(
"ctx_discover_tools",
&p,
&[],
no_quirks(),
true
));
}
#[test]
fn deprecated_aliases_never_visible_even_in_power() {
let p = ToolProfile::Power;
assert!(!is_tool_visible(
"ctx_smart_read",
&p,
&[],
no_quirks(),
true
));
assert!(!is_tool_visible(
"ctx_multi_read",
&p,
&[],
no_quirks(),
true
));
}
#[test]
fn deprecated_aliases_stay_registered_and_callable() {
let _guard = crate::core::data_dir::isolated_data_dir();
let defs = crate::server::registry::build_registry().tool_defs();
for name in [
"ctx_smart_read",
"ctx_multi_read",
"ctx_semantic_search",
"ctx_symbol",
] {
assert!(
defs.iter().any(|t| t.name.as_ref() == name),
"{name} must stay registered (callable) even though hidden"
);
assert!(
!is_tool_visible(name, &ToolProfile::Power, &[], no_quirks(), true),
"{name} must be hidden from tools/list"
);
}
}
#[test]
fn core_tool_visible_under_power() {
assert!(is_tool_visible(
"ctx_read",
&ToolProfile::Power,
&[],
no_quirks(),
true
));
}
#[test]
fn standard_exposes_its_advertised_tools() {
let p = ToolProfile::Standard;
assert!(is_tool_visible("ctx_execute", &p, &[], no_quirks(), true));
assert!(is_tool_visible("ctx_explore", &p, &[], no_quirks(), true));
assert!(is_tool_visible("ctx_callgraph", &p, &[], no_quirks(), true));
assert!(is_tool_visible("ctx_graph", &p, &[], no_quirks(), true));
assert!(is_tool_visible("ctx_patch", &p, &[], no_quirks(), true));
}
#[test]
fn folded_search_aliases_never_visible() {
let p = ToolProfile::Power;
assert!(!is_tool_visible(
"ctx_semantic_search",
&p,
&[],
no_quirks(),
true
));
assert!(!is_tool_visible("ctx_symbol", &p, &[], no_quirks(), true));
assert!(is_tool_visible("ctx_search", &p, &[], no_quirks(), true));
}
#[test]
fn minimal_hides_non_minimal_tools() {
let p = ToolProfile::Minimal;
assert!(is_tool_visible("ctx_read", &p, &[], no_quirks(), true));
assert!(!is_tool_visible(
"ctx_architecture",
&p,
&[],
no_quirks(),
true
));
}
#[test]
fn disabled_list_filters() {
let disabled = vec!["ctx_read".to_string()];
assert!(!is_tool_visible(
"ctx_read",
&ToolProfile::Power,
&disabled,
no_quirks(),
true
));
}
#[test]
fn zed_hides_ctx_edit_only() {
let p = ToolProfile::Power;
let zed = ClientQuirks {
hide_ctx_edit: true,
hide_ctx_patch: false,
};
assert!(!is_tool_visible("ctx_edit", &p, &[], zed, true));
assert!(is_tool_visible("ctx_read", &p, &[], zed, true));
}
#[test]
fn native_editor_quirk_hides_ctx_patch_only() {
let p = ToolProfile::Power;
let native = ClientQuirks {
hide_ctx_edit: false,
hide_ctx_patch: true,
};
assert!(!is_tool_visible("ctx_patch", &p, &[], native, true));
assert!(is_tool_visible("ctx_read", &p, &[], native, true));
assert!(is_tool_visible("ctx_edit", &p, &[], native, true));
}
#[test]
fn quirks_resolution_is_client_and_candidate_aware() {
for client in ["Cursor", "zed 0.164", "Windsurf", "antigravity", "opencode"] {
let q = ClientQuirks::resolve(client, CandidateSet::LazyCore);
assert!(q.hide_ctx_patch, "{client}: lazy core must hide ctx_patch");
}
for client in ["claude-code", "CodeBuddy", "pi", "", "my-sdk-harness"] {
let q = ClientQuirks::resolve(client, CandidateSet::LazyCore);
assert!(
!q.hide_ctx_patch,
"{client:?}: lazy core must show ctx_patch"
);
}
for candidate in [
CandidateSet::ProfileAuthoritative,
CandidateSet::Full,
CandidateSet::Unified,
] {
let q = ClientQuirks::resolve("Cursor", candidate);
assert!(
!q.hide_ctx_patch,
"{candidate:?}: pinned/full surfaces are client-agnostic"
);
}
assert!(ClientQuirks::resolve("zed", CandidateSet::Full).hide_ctx_edit);
assert!(!ClientQuirks::resolve("Cursor", CandidateSet::Full).hide_ctx_edit);
}
#[test]
fn role_block_hides_tool() {
assert!(!is_tool_visible(
"ctx_read",
&ToolProfile::Power,
&[],
no_quirks(),
false
));
}
#[test]
fn category_gate_only_in_default_lean_mode() {
assert!(category_gate_applies(true, false));
assert!(!category_gate_applies(true, true));
assert!(!category_gate_applies(false, false));
assert!(!category_gate_applies(false, true));
}
#[test]
fn invoker_added_when_missing_in_lazy_mode() {
assert!(needs_invoker(false, false, true, &[]));
}
#[test]
fn invoker_not_added_in_full_mode() {
assert!(!needs_invoker(true, false, true, &[]));
}
#[test]
fn invoker_not_duplicated_when_present() {
assert!(!needs_invoker(false, true, true, &[]));
}
#[test]
fn invoker_respects_role_and_disabled() {
assert!(!needs_invoker(false, false, false, &[]));
assert!(!needs_invoker(
false,
false,
true,
&["ctx_call".to_string()]
));
}
#[test]
fn core_tool_surface_stays_within_budget() {
const PER_TOOL_BUDGET: usize = 410;
const TOTAL_BUDGET: usize = 3000;
let _guard = crate::core::data_dir::isolated_data_dir();
let core = crate::tool_defs::core_tool_names();
let defs: Vec<_> = crate::server::registry::build_registry()
.tool_defs()
.into_iter()
.filter(|t| core.contains(&t.name.as_ref()))
.collect();
assert_eq!(defs.len(), core.len(), "every core tool must be registered");
let mut total = 0usize;
for t in &defs {
let desc = t.description.as_deref().unwrap_or("");
let schema = serde_json::to_string(&t.input_schema).unwrap_or_default();
let cost = crate::core::tokens::count_tokens(desc)
+ crate::core::tokens::count_tokens(&schema);
eprintln!("{:24} {cost:4} tok", t.name.as_ref());
assert!(
cost <= PER_TOOL_BUDGET,
"{} costs {cost} tok (budget {PER_TOOL_BUDGET}) — trim its description/schema",
t.name
);
total += cost;
}
eprintln!("CORE TOTAL: {total} tok / {} tools", defs.len());
assert!(
total <= TOTAL_BUDGET,
"core surface costs {total} tok (budget {TOTAL_BUDGET})"
);
}
#[test]
fn resolve_auto_returns_non_auto_unchanged() {
assert_eq!(
resolve_auto_profile(&ToolProfile::Power),
ToolProfile::Power
);
assert_eq!(
resolve_auto_profile(&ToolProfile::Minimal),
ToolProfile::Minimal
);
}
#[test]
fn resolve_auto_resolves_to_concrete_profile() {
let resolved = resolve_auto_profile(&ToolProfile::Auto);
assert_ne!(resolved, ToolProfile::Auto);
}
}