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::core::ocla::types::{IntentRequest, OclaRequestContext};
5use crate::tools::CrpMode;
6
7pub fn handle(
8    _cache: &mut SessionCache,
9    query: &str,
10    project_root: &str,
11    _crp_mode: CrpMode,
12    format: Option<&str>,
13) -> String {
14    if query.trim().is_empty() {
15        return "ERROR: ctx_intent requires query".to_string();
16    }
17
18    let intent = crate::core::intent_protocol::intent_from_query(query, Some(project_root));
19    let classification = classify(query);
20    let route = route_intent(query, &classification);
21    let mut route_v1 = crate::core::intent_router::route_v1(query);
22    if let Some(mode) = classify_read_mode(query, project_root, &route_v1) {
23        route_v1.decision.effective_read_mode = mode;
24    }
25
26    if matches!(format.map(|s| s.trim().to_lowercase()), Some(ref f) if f == "json") {
27        return serde_json::to_string_pretty(&route_v1).unwrap_or_else(|e| format!("ERROR: {e}"));
28    }
29
30    format_ack(&intent, &route, &route_v1)
31}
32
33fn classify_read_mode(
34    query: &str,
35    project_root: &str,
36    route: &crate::core::intent_router::IntentRouteV1,
37) -> Option<String> {
38    let mut candidate_intents = vec![route.decision.effective_read_mode.clone()];
39    if route.decision.recommended_read_mode != candidate_intents[0] {
40        candidate_intents.push(route.decision.recommended_read_mode.clone());
41    }
42
43    let request = IntentRequest {
44        context: OclaRequestContext {
45            request_id: format!("ctx-intent:{}", crate::core::hasher::hash_str(query)),
46            session_id: format!("project:{}", crate::core::hasher::hash_str(project_root)),
47            agent_id: "ctx_intent".to_string(),
48            content_ref: format!("query:{}", crate::core::hasher::hash_str(query)),
49            tenant_id: None,
50            trace_id: "tr-unit".into(),
51        },
52        candidate_intents,
53    };
54
55    crate::core::ocla::OclaRegistry::global()
56        .intent_classifier
57        .classify_intent(request)
58        .ok()
59        .map(|decision| decision.intent)
60}
61
62fn format_ack(
63    intent: &IntentRecord,
64    route: &crate::core::intent_engine::IntentRoute,
65    route_v1: &crate::core::intent_router::IntentRouteV1,
66) -> String {
67    format!(
68        "INTENT_OK id={} type={} source={} conf={:.0}% subj={} | route_v1: task={} dimension={} model_tier={}→{} read={} reason={}",
69        intent.id,
70        intent.intent_type.as_str(),
71        intent.source.as_str(),
72        (intent.confidence.clamp(0.0, 1.0) * 100.0).round(),
73        subject_short(&intent.subject),
74        route_v1.inputs.task_type.as_str(),
75        route.dimension.as_str(),
76        route.model_tier.as_str(),
77        route_v1.decision.effective_model_tier.as_str(),
78        route_v1.decision.effective_read_mode,
79        route.reasoning,
80    )
81}
82
83fn subject_short(s: &IntentSubject) -> String {
84    match s {
85        IntentSubject::Project { root } => format!("project({})", root.as_deref().unwrap_or(".")),
86        IntentSubject::Command { command } => format!("cmd({})", truncate(command, 80)),
87        IntentSubject::Workflow { action } => format!("workflow({})", truncate(action, 60)),
88        IntentSubject::KnowledgeFact { category, key, .. } => format!("fact({category}/{key})"),
89        IntentSubject::KnowledgeQuery { category, query } => format!(
90            "recall({}/{})",
91            category.as_deref().unwrap_or("-"),
92            query.as_deref().unwrap_or("-")
93        ),
94        IntentSubject::Tool { name } => format!("tool({name})"),
95    }
96}
97
98fn truncate(s: &str, max: usize) -> String {
99    if s.chars().count() <= max {
100        return s.to_string();
101    }
102    let mut out = String::new();
103    for (i, ch) in s.chars().enumerate() {
104        if i + 1 >= max {
105            break;
106        }
107        out.push(ch);
108    }
109    out.push('…');
110    out
111}