nexus_memory_hooks/
base.rs1use async_trait::async_trait;
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::sync::Arc;
9
10use crate::error::Result;
11use crate::session::SessionContext;
12use crate::types::{ExtractionSource, SessionActivity, SupportTier};
13
14pub type SessionEndCallback = Arc<dyn Fn(SessionContext) + Send + Sync>;
16
17#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
23pub struct LifecycleCapabilities {
24 pub session_start: bool,
26 pub session_end: bool,
28 pub checkpoint: bool,
30 pub error_hook: bool,
32 pub compact: bool,
34}
35
36impl LifecycleCapabilities {
37 pub fn end_only() -> Self {
39 Self {
40 session_end: true,
41 ..Default::default()
42 }
43 }
44
45 pub fn monitor_only() -> Self {
47 Self::default()
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct HookResult {
54 pub success: bool,
56
57 pub agent_type: String,
59
60 pub source: ExtractionSource,
62
63 pub context: Option<SessionContext>,
65
66 pub error: Option<String>,
68
69 pub timestamp: DateTime<Utc>,
71}
72
73impl HookResult {
74 pub fn success(agent_type: impl Into<String>, source: ExtractionSource) -> Self {
76 Self {
77 success: true,
78 agent_type: agent_type.into(),
79 source,
80 context: None,
81 error: None,
82 timestamp: Utc::now(),
83 }
84 }
85
86 pub fn success_with_context(
88 agent_type: impl Into<String>,
89 source: ExtractionSource,
90 context: SessionContext,
91 ) -> Self {
92 Self {
93 success: true,
94 agent_type: agent_type.into(),
95 source,
96 context: Some(context),
97 error: None,
98 timestamp: Utc::now(),
99 }
100 }
101
102 pub fn failure(
104 agent_type: impl Into<String>,
105 source: ExtractionSource,
106 error: impl Into<String>,
107 ) -> Self {
108 Self {
109 success: false,
110 agent_type: agent_type.into(),
111 source,
112 context: None,
113 error: Some(error.into()),
114 timestamp: Utc::now(),
115 }
116 }
117}
118
119#[async_trait]
163pub trait AgentHook: Send + Sync {
164 fn agent_type(&self) -> &str;
166
167 async fn install_session_end_hook(&mut self, callback: SessionEndCallback) -> Result<()>;
180
181 async fn install_session_start_hook(&mut self, _callback: SessionEndCallback) -> Result<()> {
183 Err(crate::error::HookError::NotSupported(
184 "Session start hooks not supported for this agent".to_string(),
185 ))
186 }
187
188 async fn install_compact_hook(&mut self, _callback: SessionEndCallback) -> Result<()> {
190 Err(crate::error::HookError::NotSupported(
191 "Compact/checkpoint hooks not supported for this agent".to_string(),
192 ))
193 }
194
195 async fn detect_session_activity(&self) -> Result<SessionActivity>;
206
207 async fn extract_session_context(&self) -> Result<SessionContext>;
221
222 async fn install_checkpoint_hook(&mut self, _callback: SessionEndCallback) -> Result<()> {
227 Err(crate::error::HookError::NotSupported(
229 "Checkpoint hooks not supported for this agent".to_string(),
230 ))
231 }
232
233 async fn install_error_hook(&mut self, _callback: SessionEndCallback) -> Result<()> {
237 Err(crate::error::HookError::NotSupported(
239 "Error hooks not supported for this agent".to_string(),
240 ))
241 }
242
243 fn is_hook_installed(&self) -> bool {
245 false
246 }
247
248 async fn uninstall_hooks(&mut self) -> Result<()> {
250 Ok(())
251 }
252
253 fn reliability_score(&self) -> f32 {
255 1.0
256 }
257
258 fn lifecycle_capabilities(&self) -> LifecycleCapabilities {
263 LifecycleCapabilities::end_only()
264 }
265
266 fn support_tier(&self) -> SupportTier {
271 SupportTier::MonitorOnly
272 }
273}
274
275pub struct BaseHook {
277 pub agent_type: String,
279
280 pub installed: bool,
282
283 pub callbacks: Vec<SessionEndCallback>,
285}
286
287impl BaseHook {
288 pub fn new(agent_type: impl Into<String>) -> Self {
290 Self {
291 agent_type: agent_type.into(),
292 installed: false,
293 callbacks: Vec::new(),
294 }
295 }
296
297 pub fn add_callback(&mut self, callback: SessionEndCallback) {
299 self.callbacks.push(callback);
300 }
301
302 pub fn trigger_callbacks(&self, context: SessionContext) {
304 for callback in &self.callbacks {
305 callback(context.clone());
306 }
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn test_hook_result_success() {
316 let result = HookResult::success("test-agent", ExtractionSource::Manual);
317 assert!(result.success);
318 assert!(result.error.is_none());
319 }
320
321 #[test]
322 fn test_hook_result_failure() {
323 let result = HookResult::failure(
324 "test-agent",
325 ExtractionSource::Manual,
326 "Something went wrong",
327 );
328 assert!(!result.success);
329 assert!(result.error.is_some());
330 assert_eq!(result.error.unwrap(), "Something went wrong");
331 }
332
333 #[test]
334 fn test_hook_result_with_context() {
335 let ctx = SessionContext::new("test");
336 let result = HookResult::success_with_context(
337 "test-agent",
338 ExtractionSource::NativeHook("skill".to_string()),
339 ctx,
340 );
341 assert!(result.success);
342 assert!(result.context.is_some());
343 }
344
345 #[test]
346 fn test_base_hook() {
347 let mut hook = BaseHook::new("test");
348 assert_eq!(hook.agent_type, "test");
349 assert!(!hook.installed);
350
351 let called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
352 let called_clone = called.clone();
353 hook.add_callback(Arc::new(move |_ctx| {
354 called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
355 }));
356
357 hook.trigger_callbacks(SessionContext::new("test"));
358 assert!(called.load(std::sync::atomic::Ordering::SeqCst));
359 }
360
361 #[test]
362 fn test_lifecycle_capabilities_default() {
363 let caps = LifecycleCapabilities::default();
364 assert!(!caps.session_start);
365 assert!(!caps.session_end);
366 assert!(!caps.checkpoint);
367 assert!(!caps.error_hook);
368 assert!(!caps.compact);
369 }
370
371 #[test]
372 fn test_lifecycle_capabilities_end_only() {
373 let caps = LifecycleCapabilities::end_only();
374 assert!(!caps.session_start);
375 assert!(caps.session_end);
376 assert!(!caps.checkpoint);
377 assert!(!caps.error_hook);
378 assert!(!caps.compact);
379 }
380
381 #[test]
382 fn test_lifecycle_capabilities_monitor_only() {
383 let caps = LifecycleCapabilities::monitor_only();
384 assert!(!caps.session_end);
385 assert!(!caps.session_start);
386 }
387}