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