@@ -17,8 +17,9 @@ use crate::context::{ContextProvider, ContextQuery, ContextResult};
1717use crate :: hitl:: ConfirmationProvider ;
1818use crate :: hooks:: {
1919 ErrorType , GenerateEndEvent , GenerateStartEvent , HookEvent , HookExecutor , HookResult ,
20- OnErrorEvent , PostResponseEvent , PostToolUseEvent , PreContextPerceptionEvent , PrePromptEvent ,
21- PreToolUseEvent , TokenUsageInfo , ToolCallInfo , ToolResultData ,
20+ IntentDetectionEvent , OnErrorEvent , PostResponseEvent , PostToolUseEvent ,
21+ PreContextPerceptionEvent , PrePromptEvent , PreToolUseEvent , TokenUsageInfo , ToolCallInfo ,
22+ ToolResultData ,
2223} ;
2324use crate :: llm:: { LlmClient , LlmResponse , Message , TokenUsage , ToolDefinition } ;
2425use crate :: permissions:: { PermissionChecker , PermissionDecision } ;
@@ -767,6 +768,86 @@ fn detect_domain_from_prompt(prompt: &str) -> String {
767768 }
768769}
769770
771+ /// Result from IntentDetection harness
772+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
773+ pub struct IntentDetectionResult {
774+ /// Detected intent: "locate" | "understand" | "retrieve" | "explore" | "reason" | "validate" | "compare" | "track"
775+ pub detected_intent : String ,
776+ /// Confidence score 0.0 - 1.0
777+ pub confidence : f32 ,
778+ /// Optional target hints from the harness
779+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
780+ pub target_hints : Option < TargetHints > ,
781+ }
782+
783+ /// Target hints from IntentDetection harness
784+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
785+ pub struct TargetHints {
786+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
787+ pub target_type : Option < String > ,
788+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
789+ pub target_name : Option < String > ,
790+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
791+ pub domain : Option < String > ,
792+ }
793+
794+ /// Detect language hint from prompt characters.
795+ fn detect_language_hint ( prompt : & str ) -> Option < String > {
796+ // Check for Chinese characters
797+ if prompt. chars ( ) . any ( |c| '\u{4e00}' <= c && c <= '\u{9fff}' ) {
798+ return Some ( "zh" . to_string ( ) ) ;
799+ }
800+ // Check for Japanese characters (Hiragana, Katakana, or CJK unified ideographs outside Chinese range)
801+ if prompt
802+ . chars ( )
803+ . any ( |c| ( '\u{3040}' <= c && c <= '\u{309f}' ) || ( '\u{30a0}' <= c && c <= '\u{30ff}' ) )
804+ {
805+ return Some ( "ja" . to_string ( ) ) ;
806+ }
807+ // Check for Korean characters
808+ if prompt. chars ( ) . any ( |c| '\u{ac00}' <= c && c <= '\u{d7af}' ) {
809+ return Some ( "ko" . to_string ( ) ) ;
810+ }
811+ // Check for Arabic
812+ if prompt. chars ( ) . any ( |c| '\u{0600}' <= c && c <= '\u{06ff}' ) {
813+ return Some ( "ar" . to_string ( ) ) ;
814+ }
815+ // Check for Russian/Cyrillic
816+ if prompt. chars ( ) . any ( |c| '\u{0400}' <= c && c <= '\u{04ff}' ) {
817+ return Some ( "ru" . to_string ( ) ) ;
818+ }
819+ None
820+ }
821+
822+ /// Build PreContextPerceptionEvent from IntentDetection result.
823+ fn build_pre_context_perception_from_intent (
824+ result : IntentDetectionResult ,
825+ prompt : & str ,
826+ session_id : & str ,
827+ workspace : & str ,
828+ ) -> PreContextPerceptionEvent {
829+ let target_hints = result. target_hints ;
830+ PreContextPerceptionEvent {
831+ session_id : session_id. to_string ( ) ,
832+ intent : result. detected_intent ,
833+ target_type : target_hints
834+ . as_ref ( )
835+ . and_then ( |h| h. target_type . clone ( ) )
836+ . unwrap_or_else ( || "unknown" . to_string ( ) ) ,
837+ target_name : target_hints
838+ . as_ref ( )
839+ . and_then ( |h| h. target_name . clone ( ) )
840+ . unwrap_or_else ( || extract_target_name_from_prompt ( prompt, & [ ] ) ) ,
841+ domain : target_hints
842+ . as_ref ( )
843+ . and_then ( |h| h. domain . clone ( ) )
844+ . unwrap_or_else ( || detect_domain_from_prompt ( prompt) ) ,
845+ query : Some ( prompt. to_string ( ) ) ,
846+ working_directory : workspace. to_string ( ) ,
847+ urgency : "normal" . to_string ( ) ,
848+ }
849+ }
850+
770851/// Rough token estimation (~4 chars per token for English/code).
771852#[ cfg( feature = "ahp" ) ]
772853fn estimate_tokens ( text : & str ) -> usize {
@@ -1495,157 +1576,55 @@ impl AgentLoop {
14951576 None
14961577 }
14971578
1498- /// Detect if context perception is needed based on user prompt.
1579+ /// Fire PreContextPerception hook and wait for harness decision.
1580+ async fn fire_pre_context_perception ( & self , event : & PreContextPerceptionEvent ) -> HookResult {
1581+ if let Some ( he) = & self . config . hook_engine {
1582+ let hook_event = HookEvent :: PreContextPerception ( event. clone ( ) ) ;
1583+ he. fire ( & hook_event) . await
1584+ } else {
1585+ HookResult :: continue_ ( )
1586+ }
1587+ }
1588+
1589+ /// Fire IntentDetection hook and wait for harness decision.
14991590 ///
1500- /// Returns `Some(PreContextPerceptionEvent)` if the prompt suggests the model
1501- /// needs workspace knowledge (finding files, understanding code, etc.) .
1502- pub fn detect_context_perception_intent (
1591+ /// This is called on every prompt to detect user intent via the AHP harness.
1592+ /// Returns the detected intent if the harness provides one, or None if blocked/failed .
1593+ async fn fire_intent_detection (
15031594 & self ,
15041595 prompt : & str ,
15051596 session_id : & str ,
15061597 workspace : & str ,
1507- ) -> Option < PreContextPerceptionEvent > {
1508- let lower = prompt. to_lowercase ( ) ;
1509-
1510- // Pattern matching for different intents that suggest context perception is needed
1511- let intents: & [ ( & [ & str ] , & str ) ] = & [
1512- // Locate: finding files, functions, resources
1513- (
1514- & [
1515- "where is" ,
1516- "where are" ,
1517- "find the file" ,
1518- "find all" ,
1519- "find files" ,
1520- "who wrote" ,
1521- "locate" ,
1522- "search for" ,
1523- "look for" ,
1524- "search" ,
1525- ] ,
1526- "locate" ,
1527- ) ,
1528- // Understand: explaining how something works
1529- (
1530- & [
1531- "how does" ,
1532- "what does" ,
1533- "explain" ,
1534- "understand" ,
1535- "what is this" ,
1536- "how does this work" ,
1537- ] ,
1538- "understand" ,
1539- ) ,
1540- // Retrieve: recalling from memory/past
1541- (
1542- & [
1543- "remember" ,
1544- "earlier" ,
1545- "before" ,
1546- "previously" ,
1547- "last time" ,
1548- "past" ,
1549- "previous" ,
1550- ] ,
1551- "retrieve" ,
1552- ) ,
1553- // Explore: understanding structure
1554- (
1555- & [
1556- "how is organized" ,
1557- "project structure" ,
1558- "what files" ,
1559- "show me the structure" ,
1560- "explore" ,
1561- ] ,
1562- "explore" ,
1563- ) ,
1564- // Reason: asking why/causality
1565- (
1566- & [
1567- "why did" ,
1568- "why is" ,
1569- "cause" ,
1570- "reason" ,
1571- "what happened" ,
1572- "why does" ,
1573- ] ,
1574- "reason" ,
1575- ) ,
1576- // Validate: checking correctness
1577- (
1578- & [ "is this correct" , "verify" , "validate" , "check if" , "debug" ] ,
1579- "validate" ,
1580- ) ,
1581- // Compare: comparing things
1582- (
1583- & [
1584- "difference between" ,
1585- "compare" ,
1586- "versus" ,
1587- " vs " ,
1588- "different from" ,
1589- ] ,
1590- "compare" ,
1591- ) ,
1592- // Track: status/history
1593- (
1594- & [
1595- "status" ,
1596- "progress" ,
1597- "how far" ,
1598- "history" ,
1599- "what's the current" ,
1600- ] ,
1601- "track" ,
1602- ) ,
1603- ] ;
1598+ ) -> Option < IntentDetectionResult > {
1599+ let event = IntentDetectionEvent {
1600+ session_id : session_id. to_string ( ) ,
1601+ prompt : prompt. to_string ( ) ,
1602+ workspace : workspace. to_string ( ) ,
1603+ language_hint : detect_language_hint ( prompt) ,
1604+ } ;
16041605
1605- // Detect target type from keywords
1606- let target_type = if lower. contains ( "function" ) || lower. contains ( "method" ) {
1607- "function"
1608- } else if lower. contains ( "file" ) || lower. contains ( "config" ) {
1609- "file"
1610- } else if lower. contains ( "class" ) {
1611- "entity"
1612- } else if lower. contains ( "module" ) || lower. contains ( "package" ) {
1613- "module"
1614- } else if lower. contains ( "test" ) {
1615- "test"
1606+ let hook_result = if let Some ( he) = & self . config . hook_engine {
1607+ let hook_event = HookEvent :: IntentDetection ( event) ;
1608+ he. fire ( & hook_event) . await
16161609 } else {
1617- "unknown"
1610+ return None ;
16181611 } ;
16191612
1620- // Find matching intent
1621- let matched_intent = intents
1622- . iter ( )
1623- . find ( |( patterns, _) | patterns. iter ( ) . any ( |p| lower. contains ( p) ) ) ;
1624-
1625- matched_intent. map ( |( patterns, intent) | {
1626- // Extract target name if possible (simplified extraction)
1627- let target_name = extract_target_name_from_prompt ( prompt, patterns) ;
1628-
1629- PreContextPerceptionEvent {
1630- session_id : session_id. to_string ( ) ,
1631- intent : intent. to_string ( ) ,
1632- target_type : target_type. to_string ( ) ,
1633- target_name,
1634- domain : detect_domain_from_prompt ( prompt) ,
1635- query : Some ( prompt. to_string ( ) ) ,
1636- working_directory : workspace. to_string ( ) ,
1637- urgency : "normal" . to_string ( ) ,
1613+ match hook_result {
1614+ HookResult :: Continue ( Some ( modified) ) => {
1615+ // Parse the intent detection result
1616+ if let Ok ( result) = serde_json:: from_value :: < IntentDetectionResult > ( modified) {
1617+ Some ( result)
1618+ } else {
1619+ None
1620+ }
16381621 }
1639- } )
1640- }
1641-
1642- /// Fire PreContextPerception hook and wait for harness decision.
1643- async fn fire_pre_context_perception ( & self , event : & PreContextPerceptionEvent ) -> HookResult {
1644- if let Some ( he) = & self . config . hook_engine {
1645- let hook_event = HookEvent :: PreContextPerception ( event. clone ( ) ) ;
1646- he. fire ( & hook_event) . await
1647- } else {
1648- HookResult :: continue_ ( )
1622+ HookResult :: Block ( _) => {
1623+ // Harness blocked intent detection - use fallback
1624+ tracing:: info!( "AHP harness blocked intent detection" ) ;
1625+ None
1626+ }
1627+ _ => None ,
16491628 }
16501629 }
16511630
@@ -2424,22 +2403,35 @@ impl AgentLoop {
24242403 } ;
24252404
24262405 // Resolve context from providers on first turn (before adding user message)
2427- // Intent-driven: detect context perception need first, then optionally fire AHP hook
2406+ // Intent-driven: detect context perception need first via AHP harness , then fire hook
24282407 let workspace = self . tool_context . workspace . display ( ) . to_string ( ) ;
2408+ let session_id_str = session_id. unwrap_or ( "" ) ;
24292409 let context_results = if !self . config . context_providers . is_empty ( ) {
2430- // Step 1: Detect if context perception is needed based on prompt
2431- let needs_context = self . detect_context_perception_intent (
2432- effective_prompt,
2433- session_id. unwrap_or ( "" ) ,
2434- & workspace,
2435- ) ;
2410+ // Step 1: Fire IntentDetection harness point on EVERY prompt
2411+ let harness_intent = self
2412+ . fire_intent_detection ( effective_prompt, & session_id_str, & workspace)
2413+ . await ;
24362414
2437- if let Some ( perception_event) = needs_context {
2438- // Step 2: Fire PreContextPerception hook to get harness decision
2415+ // Step 2: If harness returned an intent, use it; otherwise skip context perception
2416+ if let Some ( detected) = harness_intent {
2417+ tracing:: info!(
2418+ intent = %detected. detected_intent,
2419+ confidence = %detected. confidence,
2420+ "Intent detected from AHP harness"
2421+ ) ;
2422+
2423+ let perception_event = build_pre_context_perception_from_intent (
2424+ detected,
2425+ effective_prompt,
2426+ & session_id_str,
2427+ & workspace,
2428+ ) ;
2429+
2430+ // Step 3: Fire PreContextPerception hook to get harness decision
24392431 tracing:: info!(
24402432 intent = %perception_event. intent,
24412433 target_type = %perception_event. target_type,
2442- "Context perception intent detected, firing AHP hook"
2434+ "Firing PreContextPerception hook"
24432435 ) ;
24442436
24452437 let hook_result = self . fire_pre_context_perception ( & perception_event) . await ;
@@ -2458,33 +2450,32 @@ impl AgentLoop {
24582450 ) ;
24592451 self . apply_injected_context ( injected)
24602452 } else {
2461- // Fall back to normal providers if parsing fails
2462- tracing:: warn!(
2463- "Failed to parse injected context, falling back to providers"
2464- ) ;
2465- self . resolve_context ( effective_prompt, session_id) . await
2453+ // Failed to parse, skip context injection
2454+ tracing:: warn!( "Failed to parse injected context, skipping" ) ;
2455+ Vec :: new ( )
24662456 }
24672457 }
24682458 #[ cfg( not( feature = "ahp" ) ) ]
24692459 {
2470- // Without AHP, fall back to normal providers
2471- let _ = modified_context; // suppress unused warning
2460+ let _ = modified_context;
24722461 self . resolve_context ( effective_prompt, session_id) . await
24732462 }
24742463 }
24752464 HookResult :: Block ( _) => {
2476- // Harness blocked context injection - skip
24772465 tracing:: info!( "AHP harness blocked context injection" ) ;
24782466 Vec :: new ( )
24792467 }
24802468 _ => {
2481- // No modification or unknown result, proceed with normal providers
2482- self . resolve_context ( effective_prompt , session_id ) . await
2469+ // No modification, skip context injection
2470+ Vec :: new ( )
24832471 }
24842472 }
24852473 } else {
2486- // No intent detected, proceed with normal providers
2487- self . resolve_context ( effective_prompt, session_id) . await
2474+ // No harness registered for IntentDetection - skip context perception entirely
2475+ tracing:: debug!(
2476+ "No IntentDetection harness registered, skipping context perception"
2477+ ) ;
2478+ Vec :: new ( )
24882479 }
24892480 } else {
24902481 Vec :: new ( )
0 commit comments