Skip to main content

matrixcode_core/agent/context/
manager.rs

1//! Agent context management.
2//!
3//! This module manages the context of the Agent, including:
4//! - Skills and project overview
5//! - Memory summary
6//! - System prompt building
7//! - CodeGraph tool management
8//!
9//! By extracting context into a dedicated struct, we enable:
10//! - Clear separation between context and state
11//! - Easier testing of prompt building
12//! - Better management of project-specific information
13
14use std::path::PathBuf;
15
16use crate::prompt::PromptProfile;
17use crate::skills::Skill;
18
19/// Agent context information.
20///
21/// Manages all context-related information that influences agent behavior.
22/// This includes project overview, memory, skills, and system prompts.
23pub struct AgentContext {
24    /// Skills available to the agent.
25    skills: Vec<Skill>,
26
27    /// Project overview (generated by /init).
28    project_overview: Option<String>,
29
30    /// Memory summary from previous conversations.
31    memory_summary: Option<String>,
32
33    /// Project root path (for CodeGraph and file operations).
34    project_path: Option<PathBuf>,
35
36    /// System prompt sent to LLM.
37    system_prompt: String,
38
39    /// Prompt profile (defines agent personality and rules).
40    profile: PromptProfile,
41}
42
43impl AgentContext {
44    /// Create a new context with given profile.
45    pub fn new(profile: PromptProfile) -> Self {
46        let system_prompt = crate::prompt::build_system_prompt(
47            &profile,
48            &[], // No skills initially
49            None,
50            None,
51        );
52
53        Self {
54            skills: Vec::new(),
55            project_overview: None,
56            memory_summary: None,
57            project_path: None,
58            system_prompt,
59            profile,
60        }
61    }
62
63    /// Create a new context with skills and overview.
64    pub fn with_context(
65        profile: PromptProfile,
66        skills: Vec<Skill>,
67        project_overview: Option<String>,
68        memory_summary: Option<String>,
69        project_path: Option<PathBuf>,
70    ) -> Self {
71        let system_prompt = crate::prompt::build_system_prompt(
72            &profile,
73            &skills,
74            project_overview.as_deref(),
75            memory_summary.as_deref(),
76        );
77
78        Self {
79            skills,
80            project_overview,
81            memory_summary,
82            project_path,
83            system_prompt,
84            profile,
85        }
86    }
87
88    /// Get reference to skills.
89    pub fn skills(&self) -> &[Skill] {
90        &self.skills
91    }
92
93    /// Set skills (rebuilds system prompt).
94    pub fn set_skills(&mut self, skills: Vec<Skill>) {
95        self.skills = skills;
96        self.rebuild_system_prompt();
97    }
98
99    /// Get project overview.
100    pub fn project_overview(&self) -> Option<&str> {
101        self.project_overview.as_deref()
102    }
103
104    /// Set project overview (rebuilds system prompt).
105    pub fn set_project_overview(&mut self, overview: Option<String>) {
106        self.project_overview = overview;
107        self.rebuild_system_prompt();
108    }
109
110    /// Get memory summary.
111    pub fn memory_summary(&self) -> Option<&str> {
112        self.memory_summary.as_deref()
113    }
114
115    /// Update memory summary (rebuilds system prompt).
116    pub fn update_memory(&mut self, summary: Option<String>) {
117        self.memory_summary = summary;
118        self.rebuild_system_prompt();
119    }
120
121    /// Get project path.
122    pub fn project_path(&self) -> Option<&PathBuf> {
123        self.project_path.as_ref()
124    }
125
126    /// Set project path (may trigger CodeGraph injection).
127    pub fn set_project_path(&mut self, path: Option<PathBuf>) {
128        self.project_path = path;
129        // Note: CodeGraph injection will be handled in Phase 4
130        // when we integrate with tool management
131    }
132
133    /// Get system prompt.
134    pub fn system_prompt(&self) -> &str {
135        &self.system_prompt
136    }
137
138    /// Get prompt profile.
139    pub fn profile(&self) -> &PromptProfile {
140        &self.profile
141    }
142
143    /// Rebuild system prompt with current context.
144    fn rebuild_system_prompt(&mut self) {
145        self.system_prompt = crate::prompt::build_system_prompt(
146            &self.profile,
147            &self.skills,
148            self.project_overview.as_deref(),
149            self.memory_summary.as_deref(),
150        );
151    }
152
153    /// Set system prompt directly (for workflows with project_path).
154    pub fn set_system_prompt(&mut self, prompt: String) {
155        self.system_prompt = prompt;
156    }
157
158    /// Rebuild system prompt with workflows (includes project_path for CodeGraph).
159    pub fn rebuild_system_prompt_with_workflows(&mut self, project_path: Option<PathBuf>) {
160        self.system_prompt = crate::prompt::build_system_prompt_with_workflows(
161            &self.profile,
162            &self.skills,
163            self.project_overview.as_deref(),
164            self.memory_summary.as_deref(),
165            project_path.as_ref(),
166            None, // LSP servers not available in agent context
167        );
168    }
169
170    /// Clear all context (reset to initial state).
171    pub fn clear(&mut self) {
172        self.skills.clear();
173        self.project_overview = None;
174        self.memory_summary = None;
175        self.project_path = None;
176        self.rebuild_system_prompt();
177    }
178}
179
180impl Default for AgentContext {
181    fn default() -> Self {
182        Self::new(PromptProfile::default())
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn test_context_new_creates_empty_context() {
192        let context = AgentContext::new(PromptProfile::default());
193
194        assert_eq!(context.skills().len(), 0);
195        assert!(context.project_overview().is_none());
196        assert!(context.memory_summary().is_none());
197        assert!(context.project_path().is_none());
198        assert!(!context.system_prompt().is_empty(), "system prompt should not be empty");
199    }
200
201    #[test]
202    fn test_context_set_skills_updates_prompt() {
203        let mut context = AgentContext::new(PromptProfile::default());
204        let initial_prompt = context.system_prompt().to_string();
205
206        // Add a skill
207        let skills = vec![Skill {
208            name: "test_skill".to_string(),
209            description: "Test skill description".to_string(),
210            trigger: Some("/test".to_string()),
211            skill_type: crate::skills::SkillType::Flexible,
212            priority: crate::skills::SkillPriority::Implementation,
213            mandatory: false,
214            dir: PathBuf::from("/skills/test_skill"),
215            body: "Test skill content".to_string(),
216            source_file: PathBuf::from("/skills/test_skill/SKILL.md"),
217        }];
218        context.set_skills(skills);
219
220        // System prompt should change
221        assert_ne!(context.system_prompt(), initial_prompt, "prompt should change when skills added");
222        assert!(context.system_prompt().contains("test_skill"), "prompt should include skill name");
223        assert!(context.system_prompt().contains("Test skill description"), "prompt should include skill description");
224    }
225
226    #[test]
227    fn test_context_update_memory_rebuilds_prompt() {
228        let mut context = AgentContext::new(PromptProfile::default());
229        let initial_prompt = context.system_prompt().to_string();
230
231        // Update memory
232        context.update_memory(Some("Previous conversation summary".to_string()));
233
234        // System prompt should change
235        assert_ne!(context.system_prompt(), initial_prompt, "prompt should change when memory updated");
236        assert!(context.system_prompt().contains("Previous conversation summary"), "prompt should include memory");
237    }
238
239    #[test]
240    fn test_context_set_project_overview() {
241        let mut context = AgentContext::new(PromptProfile::default());
242
243        context.set_project_overview(Some("Project overview text".to_string()));
244
245        assert_eq!(context.project_overview(), Some("Project overview text"));
246    }
247
248    #[test]
249    fn test_context_clear_resets_state() {
250        let mut context = AgentContext::new(PromptProfile::default());
251
252        // Add some context
253        context.set_skills(vec![Skill {
254            name: "skill".to_string(),
255            description: "description".to_string(),
256            trigger: Some("trigger".to_string()),
257            skill_type: crate::skills::SkillType::Flexible,
258            priority: crate::skills::SkillPriority::Implementation,
259            mandatory: false,
260            dir: PathBuf::from("/skills/skill"),
261            body: "content".to_string(),
262            source_file: PathBuf::from("/skills/skill/SKILL.md"),
263        }]);
264        context.set_project_overview(Some("overview".to_string()));
265        context.update_memory(Some("memory".to_string()));
266        context.set_project_path(Some(PathBuf::from("/path")));
267
268        // Clear
269        context.clear();
270
271        // Verify all cleared
272        assert_eq!(context.skills().len(), 0);
273        assert!(context.project_overview().is_none());
274        assert!(context.memory_summary().is_none());
275        assert!(context.project_path().is_none());
276    }
277
278    #[test]
279    fn test_context_with_context_creates_full_context() {
280        let skills = vec![Skill {
281            name: "skill".to_string(),
282            description: "skill description".to_string(),
283            trigger: Some("trigger".to_string()),
284            skill_type: crate::skills::SkillType::Flexible,
285            priority: crate::skills::SkillPriority::Implementation,
286            mandatory: false,
287            dir: PathBuf::from("/skills/skill"),
288            body: "skill content".to_string(),
289            source_file: PathBuf::from("/skills/skill/SKILL.md"),
290        }];
291        let overview = Some("Project overview".to_string());
292        let memory = Some("Memory summary".to_string());
293        let path = Some(PathBuf::from("/project"));
294
295        let context = AgentContext::with_context(
296            PromptProfile::default(),
297            skills.clone(),
298            overview.clone(),
299            memory.clone(),
300            path.clone(),
301        );
302
303        assert_eq!(context.skills(), &skills);
304        assert_eq!(context.project_overview(), overview.as_deref());
305        assert_eq!(context.memory_summary(), memory.as_deref());
306        assert_eq!(context.project_path(), path.as_ref());
307    }
308}