lean_ctx/tools/
ctx_intent.rs1use 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}