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};
13
14/// Callback type for session end events
15pub type SessionEndCallback = Arc<dyn Fn(SessionContext) + Send + Sync>;
16
17/// Result of a hook operation
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct HookResult {
20    /// Whether the operation succeeded
21    pub success: bool,
22
23    /// Agent type
24    pub agent_type: String,
25
26    /// Source of the hook trigger
27    pub source: ExtractionSource,
28
29    /// Extracted context (if any)
30    pub context: Option<SessionContext>,
31
32    /// Error message (if failed)
33    pub error: Option<String>,
34
35    /// When this result was created
36    pub timestamp: DateTime<Utc>,
37}
38
39impl HookResult {
40    /// Create a successful result
41    pub fn success(agent_type: impl Into<String>, source: ExtractionSource) -> Self {
42        Self {
43            success: true,
44            agent_type: agent_type.into(),
45            source,
46            context: None,
47            error: None,
48            timestamp: Utc::now(),
49        }
50    }
51
52    /// Create a successful result with context
53    pub fn success_with_context(
54        agent_type: impl Into<String>,
55        source: ExtractionSource,
56        context: SessionContext,
57    ) -> Self {
58        Self {
59            success: true,
60            agent_type: agent_type.into(),
61            source,
62            context: Some(context),
63            error: None,
64            timestamp: Utc::now(),
65        }
66    }
67
68    /// Create a failed result
69    pub fn failure(
70        agent_type: impl Into<String>,
71        source: ExtractionSource,
72        error: impl Into<String>,
73    ) -> Self {
74        Self {
75            success: false,
76            agent_type: agent_type.into(),
77            source,
78            context: None,
79            error: Some(error.into()),
80            timestamp: Utc::now(),
81        }
82    }
83}
84
85/// AgentHook trait - all agent hooks must implement this
86///
87/// This trait defines the interface for agent-specific hooks that enable
88/// automated memory extraction from agent sessions.
89///
90/// # Implementation Notes
91///
92/// - All methods are async for non-blocking operation
93/// - Hooks should be thread-safe (Send + Sync)
94/// - Installation should be idempotent
95///
96/// # Example
97///
98/// ```rust,ignore
99/// use nexus_hooks::{AgentHook, SessionContext};
100/// use async_trait::async_trait;
101///
102/// struct MyAgentHook {
103///     agent_type: String,
104/// }
105///
106/// #[async_trait]
107/// impl AgentHook for MyAgentHook {
108///     fn agent_type(&self) -> &str {
109///         &self.agent_type
110///     }
111///
112///     async fn install_session_end_hook(&mut self, callback: SessionEndCallback) -> Result<()> {
113///         // Install hook...
114///         Ok(())
115///     }
116///
117///     async fn detect_session_activity(&self) -> Result<SessionActivity> {
118///         // Detect activity...
119///         Ok(SessionActivity::new(AgentType::Generic))
120///     }
121///
122///     async fn extract_session_context(&self) -> Result<SessionContext> {
123///         // Extract context...
124///         Ok(SessionContext::new(self.agent_type.clone()))
125///     }
126/// }
127/// ```
128#[async_trait]
129pub trait AgentHook: Send + Sync {
130    /// Get the agent type this hook handles
131    fn agent_type(&self) -> &str;
132
133    /// Install the session end hook
134    ///
135    /// This sets up the native hook mechanism for the agent. When a session
136    /// ends, the callback will be invoked with the extracted context.
137    ///
138    /// # Arguments
139    ///
140    /// * `callback` - Function to call when session ends
141    ///
142    /// # Returns
143    ///
144    /// Ok(()) if hook was installed successfully
145    async fn install_session_end_hook(&mut self, callback: SessionEndCallback) -> Result<()>;
146
147    /// Detect if the agent session is currently active
148    ///
149    /// This method checks for agent activity through various means:
150    /// - Process detection
151    /// - Session file monitoring
152    /// - Agent-specific indicators
153    ///
154    /// # Returns
155    ///
156    /// SessionActivity with current state
157    async fn detect_session_activity(&self) -> Result<SessionActivity>;
158
159    /// Extract session context from the agent
160    ///
161    /// This method extracts all relevant context from the current or
162    /// recent agent session, including:
163    /// - Conversation history
164    /// - Decisions made
165    /// - Files modified
166    /// - Commands executed
167    /// - Errors encountered
168    ///
169    /// # Returns
170    ///
171    /// SessionContext with extracted data
172    async fn extract_session_context(&self) -> Result<SessionContext>;
173
174    /// Optional: Install a checkpoint hook
175    ///
176    /// Some agents support periodic checkpointing during long sessions.
177    /// This allows for incremental context extraction.
178    async fn install_checkpoint_hook(&mut self, _callback: SessionEndCallback) -> Result<()> {
179        // Default: not supported
180        Err(crate::error::HookError::NotSupported(
181            "Checkpoint hooks not supported for this agent".to_string(),
182        ))
183    }
184
185    /// Optional: Install an error hook
186    ///
187    /// Some agents can trigger hooks when errors occur.
188    async fn install_error_hook(&mut self, _callback: SessionEndCallback) -> Result<()> {
189        // Default: not supported
190        Err(crate::error::HookError::NotSupported(
191            "Error hooks not supported for this agent".to_string(),
192        ))
193    }
194
195    /// Optional: Check if native hook is installed
196    fn is_hook_installed(&self) -> bool {
197        false
198    }
199
200    /// Optional: Uninstall all hooks
201    async fn uninstall_hooks(&mut self) -> Result<()> {
202        Ok(())
203    }
204
205    /// Optional: Get hook reliability score (0.0-1.0)
206    fn reliability_score(&self) -> f32 {
207        1.0
208    }
209}
210
211/// Base hook implementation with common functionality
212pub struct BaseHook {
213    /// Agent type name
214    pub agent_type: String,
215
216    /// Whether hook is installed
217    pub installed: bool,
218
219    /// Registered callbacks
220    pub callbacks: Vec<SessionEndCallback>,
221}
222
223impl BaseHook {
224    /// Create a new base hook
225    pub fn new(agent_type: impl Into<String>) -> Self {
226        Self {
227            agent_type: agent_type.into(),
228            installed: false,
229            callbacks: Vec::new(),
230        }
231    }
232
233    /// Add a callback
234    pub fn add_callback(&mut self, callback: SessionEndCallback) {
235        self.callbacks.push(callback);
236    }
237
238    /// Trigger all callbacks
239    pub fn trigger_callbacks(&self, context: SessionContext) {
240        for callback in &self.callbacks {
241            callback(context.clone());
242        }
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn test_hook_result_success() {
252        let result = HookResult::success("test-agent", ExtractionSource::Manual);
253        assert!(result.success);
254        assert!(result.error.is_none());
255    }
256
257    #[test]
258    fn test_hook_result_failure() {
259        let result = HookResult::failure(
260            "test-agent",
261            ExtractionSource::Manual,
262            "Something went wrong",
263        );
264        assert!(!result.success);
265        assert!(result.error.is_some());
266        assert_eq!(result.error.unwrap(), "Something went wrong");
267    }
268
269    #[test]
270    fn test_hook_result_with_context() {
271        let ctx = SessionContext::new("test");
272        let result = HookResult::success_with_context(
273            "test-agent",
274            ExtractionSource::NativeHook("skill".to_string()),
275            ctx,
276        );
277        assert!(result.success);
278        assert!(result.context.is_some());
279    }
280
281    #[test]
282    fn test_base_hook() {
283        let mut hook = BaseHook::new("test");
284        assert_eq!(hook.agent_type, "test");
285        assert!(!hook.installed);
286
287        let called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
288        let called_clone = called.clone();
289        hook.add_callback(Arc::new(move |_ctx| {
290            called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
291        }));
292
293        hook.trigger_callbacks(SessionContext::new("test"));
294        assert!(called.load(std::sync::atomic::Ordering::SeqCst));
295    }
296}