Skip to main content

lean_ctx/tools/
ctx_intent.rs

1use crate::core::cache::SessionCache;
2use crate::core::intent_engine::{classify, route_intent};
3use crate::core::intent_protocol::{IntentRecord, IntentSubject};
4use crate::tools::CrpMode;
5
6pub fn handle(
7    _cache: &mut SessionCache,
8    query: &str,
9    project_root: &str,
10    _crp_mode: CrpMode,
11    format: Option<&str>,
12) -> String {
13    if query.trim().is_empty() {
14        return "ERROR: ctx_intent requires query".to_string();
15    }
16
17    let intent = crate::core::intent_protocol::intent_from_query(query, Some(project_root));
18    let classification = classify(query);
19    let route = route_intent(query, &classification);
20    let route_v1 = crate::core::intent_router::route_v1(query);
21
22    if matches!(format.map(|s| s.trim().to_lowercase()), Some(ref f) if f == "json") {
23        return serde_json::to_string_pretty(&route_v1).unwrap_or_else(|e| format!("ERROR: {e}"));
24    }
25
26    format_ack(&intent, &route, &route_v1)
27}
28
29fn format_ack(
30    intent: &IntentRecord,
31    route: &crate::core::intent_engine::IntentRoute,
32    route_v1: &crate::core::intent_router::IntentRouteV1,
33) -> String {
34    format!(
35        "INTENT_OK id={} type={} source={} conf={:.0}% subj={} | route_v1: task={} dimension={} model_tier={}→{} read={} reason={}",
36        intent.id,
37        intent.intent_type.as_str(),
38        intent.source.as_str(),
39        (intent.confidence.clamp(0.0, 1.0) * 100.0).round(),
40        subject_short(&intent.subject),
41        route_v1.inputs.task_type.as_str(),
42        route.dimension.as_str(),
43        route.model_tier.as_str(),
44        route_v1.decision.effective_model_tier.as_str(),
45        route_v1.decision.effective_read_mode,
46        route.reasoning,
47    )
48}
49
50fn subject_short(s: &IntentSubject) -> String {
51    match s {
52        IntentSubject::Project { root } => format!("project({})", root.as_deref().unwrap_or(".")),
53        IntentSubject::Command { command } => format!("cmd({})", truncate(command, 80)),
54        IntentSubject::Workflow { action } => format!("workflow({})", truncate(action, 60)),
55        IntentSubject::KnowledgeFact { category, key, .. } => format!("fact({category}/{key})"),
56        IntentSubject::KnowledgeQuery { category, query } => format!(
57            "recall({}/{})",
58            category.as_deref().unwrap_or("-"),
59            query.as_deref().unwrap_or("-")
60        ),
61        IntentSubject::Tool { name } => format!("tool({name})"),
62    }
63}
64
65fn truncate(s: &str, max: usize) -> String {
66    if s.chars().count() <= max {
67        return s.to_string();
68    }
69    let mut out = String::new();
70    for (i, ch) in s.chars().enumerate() {
71        if i + 1 >= max {
72            break;
73        }
74        out.push(ch);
75    }
76    out.push('…');
77    out
78}