agtrace_providers/
traits.rs

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