Skip to main content

lean_ctx/core/ib/
intent.rs

1//! Task-intent classification from session metadata and findings.
2//!
3//! Maps agent task descriptions to [`TaskIntent`] categories used by
4//! information-bottleneck compression to select query terms.
5
6use std::fmt;
7
8use crate::core::session::SessionState;
9
10/// Task intent categories derived from cognitive task analysis.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
12pub enum TaskIntent {
13    /// Diagnose and correct erroneous behavior.
14    Debug,
15    /// Restructure existing code without changing its behavior.
16    Refactor,
17    /// Add or build new behavior.
18    Implement,
19    /// Assess code for correctness or quality.
20    Review,
21    /// Investigate an unfamiliar codebase or problem.
22    Explore,
23    /// No intent signal was found.
24    #[default]
25    Unknown,
26}
27
28const INTENT_KEYWORDS: &[(TaskIntent, &[&str])] = &[
29    (
30        TaskIntent::Debug,
31        &["fix", "bug", "error", "debug", "crash", "panic"],
32    ),
33    (
34        TaskIntent::Refactor,
35        &[
36            "refactor",
37            "rename",
38            "move",
39            "extract",
40            "restructure",
41            "clean",
42        ],
43    ),
44    (
45        TaskIntent::Implement,
46        &["implement", "add", "create", "build", "feature", "new"],
47    ),
48    (
49        TaskIntent::Review,
50        &["review", "check", "audit", "verify", "inspect"],
51    ),
52    (
53        TaskIntent::Explore,
54        &["understand", "explore", "analyze", "investigate", "find"],
55    ),
56];
57
58const ERROR_KEYWORDS: &[&str] = &["error", "panic", "crash", "failed", "failure", "bug"];
59
60/// Classify the agent's current task intent from session state.
61///
62/// Task metadata has priority over findings; error-bearing findings supply a
63/// debug signal when the task itself does not identify an intent.
64pub(crate) fn classify_intent(session: &SessionState) -> TaskIntent {
65    if let Some(task) = &session.task {
66        if let Some(intent) = classify_text(task.intent.as_deref().unwrap_or_default()) {
67            return intent;
68        }
69        if let Some(intent) = classify_text(&task.description) {
70            return intent;
71        }
72    }
73
74    if session
75        .findings
76        .iter()
77        .rev()
78        .any(|finding| contains_keyword(&finding.summary, ERROR_KEYWORDS))
79    {
80        return TaskIntent::Debug;
81    }
82
83    TaskIntent::Unknown
84}
85
86fn classify_text(text: &str) -> Option<TaskIntent> {
87    INTENT_KEYWORDS
88        .iter()
89        .find_map(|(intent, keywords)| contains_keyword(text, keywords).then_some(*intent))
90}
91
92fn contains_keyword(text: &str, keywords: &[&str]) -> bool {
93    text.split(|character: char| !character.is_alphanumeric())
94        .filter(|word| !word.is_empty())
95        .any(|word| {
96            keywords
97                .iter()
98                .any(|keyword| word.eq_ignore_ascii_case(keyword))
99        })
100}
101
102impl fmt::Display for TaskIntent {
103    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
104        let name = match self {
105            Self::Debug => "debug",
106            Self::Refactor => "refactor",
107            Self::Implement => "implement",
108            Self::Review => "review",
109            Self::Explore => "explore",
110            Self::Unknown => "unknown",
111        };
112        formatter.write_str(name)
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use crate::core::session::{SessionState, TaskInfo};
119
120    use super::{TaskIntent, classify_intent};
121
122    fn session_with_task(description: &str) -> SessionState {
123        let mut session = SessionState::new();
124        session.task = Some(TaskInfo {
125            description: description.to_owned(),
126            intent: None,
127            progress_pct: None,
128        });
129        session
130    }
131
132    #[test]
133    fn classify_debug_from_task() {
134        let session = session_with_task("Fix the parser crash");
135        assert_eq!(classify_intent(&session), TaskIntent::Debug);
136    }
137
138    #[test]
139    fn classify_refactor_from_task() {
140        let session = session_with_task("Refactor the session store");
141        assert_eq!(classify_intent(&session), TaskIntent::Refactor);
142    }
143
144    #[test]
145    fn classify_unknown_when_empty() {
146        assert_eq!(classify_intent(&SessionState::new()), TaskIntent::Unknown);
147    }
148
149    #[test]
150    fn display_format() {
151        assert_eq!(TaskIntent::Implement.to_string(), "implement");
152    }
153}