agtrace_providers/
traits.rs

1use agtrace_types::{AgentEvent, ToolCallPayload, ToolKind, ToolOrigin};
2use serde_json::Value;
3use std::path::{Path, PathBuf};
4
5use crate::{Error, Result};
6
7/// Provider discovery and lifecycle management
8///
9/// Responsibilities:
10/// - Identify provider from file paths/patterns
11/// - Locate session files on filesystem
12/// - Extract session metadata
13pub trait LogDiscovery: Send + Sync {
14    /// Unique provider ID (e.g., "claude", "codex", "gemini")
15    fn id(&self) -> &'static str;
16
17    /// Check if a file belongs to this provider
18    fn probe(&self, path: &Path) -> ProbeResult;
19
20    /// Resolve log root directory for a given project root
21    /// Returns None if provider doesn't organize by project
22    fn resolve_log_root(&self, project_root: &Path) -> Option<PathBuf>;
23
24    /// Scan for sessions in the log root
25    fn scan_sessions(&self, log_root: &Path) -> Result<Vec<SessionIndex>>;
26
27    /// Extract session ID from file header (lightweight, no full parse)
28    fn extract_session_id(&self, path: &Path) -> Result<String>;
29
30    /// Extract project hash from file header (lightweight, no full parse)
31    /// Returns None if the file doesn't contain project information
32    fn extract_project_hash(&self, path: &Path) -> Result<Option<agtrace_types::ProjectHash>>;
33
34    /// Find all files belonging to a session (main + sidechains)
35    fn find_session_files(&self, log_root: &Path, session_id: &str) -> Result<Vec<PathBuf>>;
36}
37
38/// Session data normalization
39///
40/// Responsibilities:
41/// - Parse raw log files into structured events
42/// - Handle format differences (JSONL, JSON array, custom)
43/// - Support streaming/incremental parsing
44pub trait SessionParser: Send + Sync {
45    /// Parse entire file into event stream
46    fn parse_file(&self, path: &Path) -> Result<Vec<AgentEvent>>;
47
48    /// Parse single record for streaming (e.g., tail -f mode)
49    /// Returns None for malformed/incomplete lines (non-fatal)
50    fn parse_record(&self, content: &str) -> Result<Option<AgentEvent>>;
51}
52
53/// Tool call semantic interpretation
54///
55/// Responsibilities:
56/// - Classify tools by origin and kind
57/// - Normalize provider-specific tool arguments to domain model
58/// - Extract UI summaries for display
59pub trait ToolMapper: Send + Sync {
60    /// Classify tool by origin (System/Mcp) and kind (Read/Write/Execute/etc.)
61    fn classify(&self, tool_name: &str) -> (ToolOrigin, ToolKind);
62
63    /// Normalize provider-specific tool call to domain ToolCallPayload
64    fn normalize_call(&self, name: &str, args: Value, call_id: Option<String>) -> ToolCallPayload;
65
66    /// Extract short summary for UI display
67    fn summarize(&self, kind: ToolKind, args: &Value) -> String;
68}
69
70// --- Helper types ---
71
72/// Probe result with confidence score
73#[derive(Debug, Clone, Copy, PartialEq)]
74pub enum ProbeResult {
75    /// Provider can handle this file with given confidence (0.0 - 1.0)
76    Confidence(f32),
77    /// Provider cannot handle this file
78    NoMatch,
79}
80
81impl ProbeResult {
82    /// Create high confidence match (1.0)
83    pub fn match_high() -> Self {
84        ProbeResult::Confidence(1.0)
85    }
86
87    /// Create medium confidence match (0.5)
88    pub fn match_medium() -> Self {
89        ProbeResult::Confidence(0.5)
90    }
91
92    /// Create low confidence match (0.3)
93    pub fn match_low() -> Self {
94        ProbeResult::Confidence(0.3)
95    }
96
97    /// Check if this is a match (confidence > 0)
98    pub fn is_match(&self) -> bool {
99        matches!(self, ProbeResult::Confidence(c) if *c > 0.0)
100    }
101
102    /// Get confidence score (0.0 if NoMatch)
103    pub fn confidence(&self) -> f32 {
104        match self {
105            ProbeResult::Confidence(c) => *c,
106            ProbeResult::NoMatch => 0.0,
107        }
108    }
109}
110
111/// Session index metadata
112///
113/// NOTE: Design rationale for latest_mod_time
114/// - `timestamp` represents session creation time (when first event was logged)
115/// - `latest_mod_time` represents last file update time (when session was last active)
116/// - For watch mode, we need to track "most recently updated" session, not just "most recently created"
117/// - This enables switching to sessions that are actively being updated, even if they were created earlier
118/// - Since watch bypasses DB indexing for real-time monitoring, we derive this directly from filesystem
119#[derive(Debug, Clone)]
120pub struct SessionIndex {
121    pub session_id: String,
122    pub timestamp: Option<String>,
123    pub latest_mod_time: Option<String>,
124    pub main_file: PathBuf,
125    pub sidechain_files: Vec<PathBuf>,
126    pub project_root: Option<PathBuf>,
127    pub snippet: Option<String>,
128}
129
130// --- Provider Adapter ---
131
132/// Adapter that bundles the three trait implementations
133///
134/// This provides a unified interface for working with provider functionality
135/// while maintaining clean separation of concerns internally.
136pub struct ProviderAdapter {
137    pub discovery: Box<dyn LogDiscovery>,
138    pub parser: Box<dyn SessionParser>,
139    pub mapper: Box<dyn ToolMapper>,
140}
141
142impl ProviderAdapter {
143    pub fn new(
144        discovery: Box<dyn LogDiscovery>,
145        parser: Box<dyn SessionParser>,
146        mapper: Box<dyn ToolMapper>,
147    ) -> Self {
148        Self {
149            discovery,
150            parser,
151            mapper,
152        }
153    }
154
155    /// Create adapter for a provider by name
156    pub fn from_name(provider_name: &str) -> Result<Self> {
157        match provider_name {
158            "claude_code" | "claude" => Ok(Self::claude()),
159            "codex" => Ok(Self::codex()),
160            "gemini" => Ok(Self::gemini()),
161            _ => Err(Error::Provider(format!(
162                "Unknown provider: {}",
163                provider_name
164            ))),
165        }
166    }
167
168    /// Create Claude provider adapter
169    pub fn claude() -> Self {
170        Self::new(
171            Box::new(crate::claude::ClaudeDiscovery),
172            Box::new(crate::claude::ClaudeParser),
173            Box::new(crate::claude::ClaudeToolMapper),
174        )
175    }
176
177    /// Create Codex provider adapter
178    pub fn codex() -> Self {
179        Self::new(
180            Box::new(crate::codex::CodexDiscovery),
181            Box::new(crate::codex::CodexParser),
182            Box::new(crate::codex::CodexToolMapper),
183        )
184    }
185
186    /// Create Gemini provider adapter
187    pub fn gemini() -> Self {
188        Self::new(
189            Box::new(crate::gemini::GeminiDiscovery),
190            Box::new(crate::gemini::GeminiParser),
191            Box::new(crate::gemini::GeminiToolMapper),
192        )
193    }
194
195    /// Get provider ID
196    pub fn id(&self) -> &'static str {
197        self.discovery.id()
198    }
199
200    /// Process a file through the adapter (convenience method)
201    pub fn process_file(&self, path: &Path) -> Result<Vec<AgentEvent>> {
202        if !self.discovery.probe(path).is_match() {
203            return Err(Error::Provider(format!(
204                "Provider {} cannot handle file: {}",
205                self.id(),
206                path.display()
207            )));
208        }
209        self.parser.parse_file(path)
210    }
211}
212
213/// Get the latest modification time from a list of file paths in RFC3339 format.
214///
215/// This utility function is used by discovery implementations to track when
216/// a session was last active (most recent file modification).
217///
218/// Returns None if no files have a valid modification time.
219pub fn get_latest_mod_time_rfc3339(files: &[&std::path::Path]) -> Option<String> {
220    use chrono::{DateTime, Utc};
221    use std::time::SystemTime;
222
223    let mut latest: Option<SystemTime> = None;
224
225    for path in files {
226        if let Ok(metadata) = std::fs::metadata(path)
227            && let Ok(modified) = metadata.modified()
228            && (latest.is_none() || Some(modified) > latest)
229        {
230            latest = Some(modified);
231        }
232    }
233
234    latest.map(|t| {
235        let dt: DateTime<Utc> = t.into();
236        dt.to_rfc3339()
237    })
238}