gestura_core_context/
lib.rs

1//! Smart context analysis, resolution, and caching for Gestura.
2//!
3//! `gestura-core-context` helps the runtime decide what context is relevant for
4//! a request before invoking an LLM. It provides lightweight request analysis,
5//! entity extraction, category classification, tool-aware context resolution,
6//! and cache-backed reuse of previous results.
7//!
8//! ## Resolution model
9//!
10//! The context system follows a three-stage approach:
11//!
12//! 1. **Request analysis**: infer intent and extract entities without needing a
13//!    model round-trip
14//! 2. **Context resolution**: load only the categories and tool descriptors that
15//!    are likely relevant to the request
16//! 3. **Smart caching**: reuse recent analysis and resolved context with TTL- and
17//!    size-based cache limits
18//!
19//! ## Main entry points
20//!
21//! - `RequestAnalyzer`: parses requests into categories, entities, and intent
22//! - `ContextManager`: orchestrates analysis, resolution, and cache lookup
23//! - `ContextCache`: reusable caching layer with observable stats
24//! - foundation re-exports: shared context data structures from
25//!   `gestura-core-foundation`
26//!
27//! ## Architecture boundary
28//!
29//! This crate owns context-domain behavior. It does not decide how the pipeline
30//! uses the resolved context inside a full agent run; that orchestration remains
31//! in `gestura-core`.
32//!
33//! Most application code should import these types through
34//! `gestura_core::context::*`, while code inside the workspace may depend on
35//! this domain crate directly when evolving context analysis itself.
36//!
37//! ## Example
38//!
39//! ```rust,ignore
40//! use gestura_core::context::ContextManager;
41//!
42//! let manager = ContextManager::new();
43//! let analysis = manager.analyze("Read the file src/main.rs");
44//! let context = manager.resolve_simple("Show git status", None);
45//! println!("{:?} -> {} tools", analysis.categories, context.tools.len());
46//! ```
47
48mod analyzer;
49mod cache;
50mod manager;
51
52pub use analyzer::RequestAnalyzer;
53pub use cache::{CacheStats, ContextCache};
54pub use gestura_core_foundation::context::*;
55pub use manager::{ContextManager, ContextManagerStats, ToolProviderFn, estimate_tokens};
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn test_full_context_flow() {
63        let manager = ContextManager::new();
64
65        // Test file-related request
66        let ctx = manager.resolve_simple("Read src/lib.rs and show its contents", None);
67        assert!(ctx.categories.contains(&ContextCategory::FileSystem));
68
69        // Test git request
70        let ctx = manager.resolve_simple("Show me the git log", None);
71        assert!(ctx.categories.contains(&ContextCategory::Git));
72
73        // Test general conversation
74        let ctx = manager.resolve_simple("Hello, how are you?", None);
75        assert!(ctx.categories.contains(&ContextCategory::General));
76        assert!(ctx.tools.is_empty());
77    }
78
79    #[test]
80    fn test_caching_behavior() {
81        let manager = ContextManager::new();
82
83        // First call
84        let ctx1 = manager.resolve_simple("Run a shell command", None);
85
86        // Second call should be cached
87        let ctx2 = manager.resolve_simple("Execute a terminal command", None);
88
89        // Both should have shell category
90        assert!(ctx1.categories.contains(&ContextCategory::Shell));
91        assert!(ctx2.categories.contains(&ContextCategory::Shell));
92
93        // Check cache has entries
94        let stats = manager.cache_stats();
95        assert!(stats.context_cache.size > 0);
96    }
97
98    #[test]
99    fn test_entity_extraction() {
100        let analyzer = RequestAnalyzer::new();
101
102        let analysis = analyzer.analyze("Fetch https://api.example.com/data");
103        assert!(
104            analysis
105                .entities
106                .iter()
107                .any(|e| e.entity_type == EntityType::Url)
108        );
109        assert!(analysis.categories.contains(&ContextCategory::Web));
110    }
111
112    #[test]
113    fn test_resolve_context_with_tool_provider() {
114        // Create a manager with a mock tool provider
115        let manager = ContextManager::new().with_tool_provider(Box::new(|| {
116            vec![
117                ("file".to_string(), "Read/write files".to_string()),
118                ("shell".to_string(), "Run shell commands".to_string()),
119                ("git".to_string(), "Git operations".to_string()),
120            ]
121        }));
122        let context = manager.resolve_simple("List files in the current directory", None);
123
124        assert!(context.categories.contains(&ContextCategory::FileSystem));
125        assert!(!context.tools.is_empty());
126    }
127}