Skip to main content

nexus_memory_hooks/agents/
qwen.rs

1//! Qwen hook implementation
2//!
3//! Process-monitor detection with optional session directory scanning.
4
5use async_trait::async_trait;
6use std::path::PathBuf;
7
8use crate::base::{AgentHook, BaseHook, LifecycleCapabilities, SessionEndCallback};
9use crate::error::Result;
10use crate::monitor::ProcessMonitor;
11use crate::session::SessionContext;
12use crate::types::{AgentType, SessionActivity};
13
14/// Qwen hook using process monitoring and session directory detection
15pub struct QwenHook {
16    /// Base hook functionality
17    base: BaseHook,
18
19    /// Config path
20    config_path: PathBuf,
21
22    /// Process monitor for fallback detection
23    process_monitor: ProcessMonitor,
24}
25
26impl QwenHook {
27    /// Config directory
28    pub const CONFIG_DIR: &'static str = ".qwen";
29
30    /// Create a new Qwen hook
31    pub fn new() -> Self {
32        let config_path = dirs::home_dir()
33            .unwrap_or_else(|| PathBuf::from("."))
34            .join(Self::CONFIG_DIR);
35
36        Self {
37            base: BaseHook::new("qwen"),
38            config_path,
39            process_monitor: ProcessMonitor::new(),
40        }
41    }
42
43    /// Read session data
44    fn read_session_data(&self) -> Option<serde_json::Value> {
45        let session_file = self.config_path.join("session.json");
46
47        if session_file.exists() {
48            let content = std::fs::read_to_string(&session_file).ok()?;
49            serde_json::from_str(&content).ok()
50        } else {
51            None
52        }
53    }
54}
55
56impl Default for QwenHook {
57    fn default() -> Self {
58        Self::new()
59    }
60}
61
62#[async_trait]
63impl AgentHook for QwenHook {
64    fn agent_type(&self) -> &str {
65        &self.base.agent_type
66    }
67
68    async fn install_session_end_hook(&mut self, callback: SessionEndCallback) -> Result<()> {
69        self.base.add_callback(callback);
70        self.base.installed = true;
71
72        Ok(())
73    }
74
75    async fn detect_session_activity(&self) -> Result<SessionActivity> {
76        let mut monitor = self.process_monitor.clone();
77        let processes = monitor.find_agent_processes(AgentType::Qwen);
78
79        let mut activity = SessionActivity::new(AgentType::Qwen);
80
81        if !processes.is_empty() {
82            activity.is_active = true;
83            activity.processes = processes;
84        }
85
86        // Check session directory for recent activity files
87        let session_dir = self.config_path.join("sessions");
88        if session_dir.exists() {
89            if let Ok(entries) = std::fs::read_dir(&session_dir) {
90                let most_recent = entries
91                    .filter_map(|e| e.ok())
92                    .filter(|e| {
93                        e.path()
94                            .extension()
95                            .map(|ext| ext == "json")
96                            .unwrap_or(false)
97                    })
98                    .max_by_key(|e| e.metadata().ok().and_then(|m| m.modified().ok()));
99
100                if let Some(entry) = most_recent {
101                    if let Ok(metadata) = entry.metadata() {
102                        if let Ok(modified) = metadata.modified() {
103                            let age = std::time::SystemTime::now()
104                                .duration_since(modified)
105                                .unwrap_or(std::time::Duration::MAX);
106
107                            // Consider active if modified in last 5 minutes
108                            if age.as_secs() < 300 {
109                                activity.is_active = true;
110                                activity.session_id = Some(
111                                    entry
112                                        .path()
113                                        .file_stem()
114                                        .unwrap()
115                                        .to_string_lossy()
116                                        .to_string(),
117                                );
118                            }
119                        }
120                    }
121                }
122            }
123        }
124
125        Ok(activity)
126    }
127
128    async fn extract_session_context(&self) -> Result<SessionContext> {
129        let mut context = SessionContext::new("qwen")
130            .with_source("monitor")
131            .with_reliability(0.95);
132
133        if let Some(session) = self.read_session_data() {
134            if let Some(messages) = session.get("messages").and_then(|m| m.as_array()) {
135                for msg in messages {
136                    let role = msg
137                        .get("role")
138                        .and_then(|r| r.as_str())
139                        .unwrap_or("unknown");
140                    let content = msg.get("content").and_then(|c| c.as_str()).unwrap_or("");
141                    context.add_message(role, content);
142                }
143            }
144
145            if let Some(commands) = session.get("commands").and_then(|c| c.as_array()) {
146                for cmd in commands {
147                    if let Some(cmd_str) = cmd.as_str() {
148                        context.add_command(cmd_str);
149                    }
150                }
151            }
152        }
153
154        // Try to get git status for modified files
155        let git_status = std::process::Command::new("git")
156            .args(["status", "--porcelain"])
157            .output()
158            .ok();
159
160        if let Some(output) = git_status {
161            if output.status.success() {
162                let status = String::from_utf8_lossy(&output.stdout);
163                for line in status.lines() {
164                    if line.len() > 3 {
165                        let status_char = line.chars().next().unwrap_or(' ');
166                        let file_path = &line[3..];
167                        let action = match status_char {
168                            '?' => crate::session::FileAction::Created,
169                            'D' => crate::session::FileAction::Deleted,
170                            _ => crate::session::FileAction::Modified,
171                        };
172                        context.add_file(crate::session::FileInfo::new(file_path, action));
173                    }
174                }
175            }
176        }
177
178        context.complete();
179        Ok(context)
180    }
181
182    fn reliability_score(&self) -> f32 {
183        0.95
184    }
185
186    fn lifecycle_capabilities(&self) -> LifecycleCapabilities {
187        LifecycleCapabilities::monitor_only()
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_qwen_hook_new() {
197        let hook = QwenHook::new();
198        assert_eq!(hook.agent_type(), "qwen");
199    }
200
201    #[tokio::test]
202    async fn test_qwen_hook_detect_activity() {
203        let hook = QwenHook::new();
204        let activity = hook.detect_session_activity().await.unwrap();
205
206        assert_eq!(activity.agent_type, AgentType::Qwen);
207    }
208
209    #[test]
210    fn test_qwen_hook_lifecycle_capabilities() {
211        let hook = QwenHook::new();
212        let caps = hook.lifecycle_capabilities();
213
214        assert!(!caps.session_start, "Qwen does not support session_start");
215        assert!(!caps.session_end, "Qwen is monitor-only");
216        assert!(!caps.checkpoint, "Qwen does not support checkpoint");
217    }
218}