Skip to main content

nexus_memory_hooks/
base.rs

1//! AgentHook trait definition
2//!
3//! This module defines the core AgentHook trait that all agent hooks must implement.
4
5use 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
14/// Callback type for session end events
15pub type SessionEndCallback = Arc<dyn Fn(SessionContext) + Send + Sync>;
16
17/// Describes which lifecycle events an agent hook can handle.
18///
19/// Each field indicates whether the agent's native hook/config model
20/// supports that particular lifecycle event. Agents should override
21/// `AgentHook::lifecycle_capabilities()` to report their honest support.
22#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
23pub struct LifecycleCapabilities {
24    /// Native session start hook support (e.g. Claude Code's SessionStart hook)
25    pub session_start: bool,
26    /// Session end hook support (via skills, native hooks, or atexit)
27    pub session_end: bool,
28    /// Periodic checkpoint hook support
29    pub checkpoint: bool,
30    /// Error-triggered hook support
31    pub error_hook: bool,
32    /// Compact/compression hook support
33    pub compact: bool,
34}
35
36impl LifecycleCapabilities {
37    /// Helper to create a capabilities set with only session end support.
38    pub fn end_only() -> Self {
39        Self {
40            session_end: true,
41            ..Default::default()
42        }
43    }
44
45    /// Helper to create a monitor-only capabilities set (process detection, atexit).
46    pub fn monitor_only() -> Self {
47        Self::default()
48    }
49}
50
51/// Result of a hook operation
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct HookResult {
54    /// Whether the operation succeeded
55    pub success: bool,
56
57    /// Agent type
58    pub agent_type: String,
59
60    /// Source of the hook trigger
61    pub source: ExtractionSource,
62
63    /// Extracted context (if any)
64    pub context: Option<SessionContext>,
65
66    /// Error message (if failed)
67    pub error: Option<String>,
68
69    /// When this result was created
70    pub timestamp: DateTime<Utc>,
71}
72
73impl HookResult {
74    /// Create a successful result
75    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    /// Create a successful result with context
87    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    /// Create a failed result
103    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/// AgentHook trait - all agent hooks must implement this
120///
121/// This trait defines the interface for agent-specific hooks that enable
122/// automated memory extraction from agent sessions.
123///
124/// # Implementation Notes
125///
126/// - All methods are async for non-blocking operation
127/// - Hooks should be thread-safe (Send + Sync)
128/// - Installation should be idempotent
129///
130/// # Example
131///
132/// ```rust,ignore
133/// use nexus_memory_hooks::{AgentHook, SessionContext};
134/// use async_trait::async_trait;
135///
136/// struct MyAgentHook {
137///     agent_type: String,
138/// }
139///
140/// #[async_trait]
141/// impl AgentHook for MyAgentHook {
142///     fn agent_type(&self) -> &str {
143///         &self.agent_type
144///     }
145///
146///     async fn install_session_end_hook(&mut self, callback: SessionEndCallback) -> Result<()> {
147///         // Install hook...
148///         Ok(())
149///     }
150///
151///     async fn detect_session_activity(&self) -> Result<SessionActivity> {
152///         // Detect activity...
153///         Ok(SessionActivity::new(AgentType::Generic))
154///     }
155///
156///     async fn extract_session_context(&self) -> Result<SessionContext> {
157///         // Extract context...
158///         Ok(SessionContext::new(self.agent_type.clone()))
159///     }
160/// }
161/// ```
162#[async_trait]
163pub trait AgentHook: Send + Sync {
164    /// Get the agent type this hook handles
165    fn agent_type(&self) -> &str;
166
167    /// Install the session end hook
168    ///
169    /// This sets up the native hook mechanism for the agent. When a session
170    /// ends, the callback will be invoked with the extracted context.
171    ///
172    /// # Arguments
173    ///
174    /// * `callback` - Function to call when session ends
175    ///
176    /// # Returns
177    ///
178    /// Ok(()) if hook was installed successfully
179    async fn install_session_end_hook(&mut self, callback: SessionEndCallback) -> Result<()>;
180
181    /// Optional: Install the session start hook.
182    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    /// Optional: Install a compact/checkpoint hook.
189    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    /// Detect if the agent session is currently active
196    ///
197    /// This method checks for agent activity through various means:
198    /// - Process detection
199    /// - Session file monitoring
200    /// - Agent-specific indicators
201    ///
202    /// # Returns
203    ///
204    /// SessionActivity with current state
205    async fn detect_session_activity(&self) -> Result<SessionActivity>;
206
207    /// Extract session context from the agent
208    ///
209    /// This method extracts all relevant context from the current or
210    /// recent agent session, including:
211    /// - Conversation history
212    /// - Decisions made
213    /// - Files modified
214    /// - Commands executed
215    /// - Errors encountered
216    ///
217    /// # Returns
218    ///
219    /// SessionContext with extracted data
220    async fn extract_session_context(&self) -> Result<SessionContext>;
221
222    /// Optional: Install a checkpoint hook
223    ///
224    /// Some agents support periodic checkpointing during long sessions.
225    /// This allows for incremental context extraction.
226    async fn install_checkpoint_hook(&mut self, _callback: SessionEndCallback) -> Result<()> {
227        // Default: not supported
228        Err(crate::error::HookError::NotSupported(
229            "Checkpoint hooks not supported for this agent".to_string(),
230        ))
231    }
232
233    /// Optional: Install an error hook
234    ///
235    /// Some agents can trigger hooks when errors occur.
236    async fn install_error_hook(&mut self, _callback: SessionEndCallback) -> Result<()> {
237        // Default: not supported
238        Err(crate::error::HookError::NotSupported(
239            "Error hooks not supported for this agent".to_string(),
240        ))
241    }
242
243    /// Optional: Check if native hook is installed
244    fn is_hook_installed(&self) -> bool {
245        false
246    }
247
248    /// Optional: Uninstall all hooks
249    async fn uninstall_hooks(&mut self) -> Result<()> {
250        Ok(())
251    }
252
253    /// Optional: Get hook reliability score (0.0-1.0)
254    fn reliability_score(&self) -> f32 {
255        1.0
256    }
257
258    /// Report which lifecycle events this agent hook supports.
259    ///
260    /// Default assumes session-end only. Agents with richer native hook
261    /// support (e.g. Claude Code's SessionStart hook) should override this.
262    fn lifecycle_capabilities(&self) -> LifecycleCapabilities {
263        LifecycleCapabilities::end_only()
264    }
265
266    /// Report the support tier for this agent hook.
267    ///
268    /// Default assumes monitor-only. Agents with dedicated hook files
269    /// and skill installation should override this.
270    fn support_tier(&self) -> SupportTier {
271        SupportTier::MonitorOnly
272    }
273}
274
275/// Base hook implementation with common functionality
276pub struct BaseHook {
277    /// Agent type name
278    pub agent_type: String,
279
280    /// Whether hook is installed
281    pub installed: bool,
282
283    /// Registered callbacks
284    pub callbacks: Vec<SessionEndCallback>,
285}
286
287impl BaseHook {
288    /// Create a new base hook
289    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    /// Add a callback
298    pub fn add_callback(&mut self, callback: SessionEndCallback) {
299        self.callbacks.push(callback);
300    }
301
302    /// Trigger all callbacks
303    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}