nexus_memory_hooks/agents/
qwen.rs1use 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
14pub struct QwenHook {
16 base: BaseHook,
18
19 config_path: PathBuf,
21
22 process_monitor: ProcessMonitor,
24}
25
26impl QwenHook {
27 pub const CONFIG_DIR: &'static str = ".qwen";
29
30 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 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 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 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 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}