agtrace_sdk/providers.rs
1//! Lightweight provider operations without database.
2//!
3//! The [`Providers`] type provides access to provider operations (parsing, diagnostics)
4//! without requiring a full workspace with database. Use this when you only need to:
5//!
6//! - Parse log files directly
7//! - Run diagnostics on providers
8//! - Check file parseability
9//! - Inspect file contents
10//!
11//! For session querying and indexing, use [`Client`](crate::Client) instead.
12//!
13//! # Examples
14//!
15//! ```no_run
16//! use agtrace_sdk::Providers;
17//! use std::path::Path;
18//!
19//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
20//! // Auto-detect providers from system paths
21//! let providers = Providers::detect()?;
22//!
23//! // Parse a log file
24//! let events = providers.parse_auto(Path::new("/path/to/log.jsonl"))?;
25//! println!("Parsed {} events", events.len());
26//!
27//! // Run diagnostics
28//! let results = providers.diagnose()?;
29//! for result in &results {
30//! println!("{}: {} files, {} successful",
31//! result.provider_name,
32//! result.total_files,
33//! result.successful);
34//! }
35//! # Ok(())
36//! # }
37//! ```
38//!
39//! ## Custom Provider Configuration
40//!
41//! ```no_run
42//! use agtrace_sdk::Providers;
43//!
44//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
45//! let providers = Providers::builder()
46//! .provider("claude_code", "/custom/.claude/projects")
47//! .provider("codex", "/custom/.codex/sessions")
48//! .build()?;
49//! # Ok(())
50//! # }
51//! ```
52
53use std::path::{Path, PathBuf};
54use std::sync::Arc;
55
56use crate::error::{Error, Result};
57use crate::types::{
58 AgentEvent, CheckResult, Config, DiagnoseResult, InspectResult, ProviderConfig,
59};
60
61/// Lightweight client for provider operations without database.
62///
63/// This type provides access to provider-level operations (parsing, diagnostics)
64/// without requiring a full workspace with database indexing.
65///
66/// # When to use `Providers` vs `Client`
67///
68/// | Operation | `Providers` | `Client` |
69/// |-----------|-------------|----------|
70/// | Parse log files | Yes | Yes (via `.providers()`) |
71/// | Run diagnostics | Yes | Yes (via `.system()`) |
72/// | Check/inspect files | Yes | Yes (via `.system()`) |
73/// | List sessions | No | Yes |
74/// | Query sessions | No | Yes |
75/// | Watch events | No | Yes |
76/// | Index operations | No | Yes |
77///
78/// Use `Providers` for:
79/// - Quick file parsing without workspace setup
80/// - Diagnostics on provider log directories
81/// - CI/CD validation of log files
82/// - Tools that only need read-only file access
83///
84/// Use `Client` for:
85/// - Session browsing and querying
86/// - Real-time event monitoring
87/// - Full workspace operations
88#[derive(Clone)]
89pub struct Providers {
90 config: Arc<Config>,
91 /// (provider_name, log_root)
92 provider_configs: Vec<(String, PathBuf)>,
93}
94
95impl Providers {
96 /// Create with auto-detected providers from system paths.
97 ///
98 /// Scans default log directories for each supported provider
99 /// (Claude Code, Codex, Gemini) and enables those that exist.
100 ///
101 /// # Examples
102 ///
103 /// ```no_run
104 /// use agtrace_sdk::Providers;
105 ///
106 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
107 /// let providers = Providers::detect()?;
108 /// println!("Detected {} providers", providers.list().len());
109 /// # Ok(())
110 /// # }
111 /// ```
112 pub fn detect() -> Result<Self> {
113 let config = Config::detect_providers().map_err(Error::Runtime)?;
114 Ok(Self::with_config(config))
115 }
116
117 /// Create with explicit configuration.
118 ///
119 /// # Examples
120 ///
121 /// ```no_run
122 /// use agtrace_sdk::{Providers, types::Config};
123 ///
124 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
125 /// let config = Config::detect_providers()?;
126 /// let providers = Providers::with_config(config);
127 /// # Ok(())
128 /// # }
129 /// ```
130 pub fn with_config(config: Config) -> Self {
131 let provider_configs: Vec<(String, PathBuf)> = config
132 .enabled_providers()
133 .into_iter()
134 .map(|(name, cfg)| (name.clone(), cfg.log_root.clone()))
135 .collect();
136
137 Self {
138 config: Arc::new(config),
139 provider_configs,
140 }
141 }
142
143 /// Create a builder for fine-grained configuration.
144 ///
145 /// # Examples
146 ///
147 /// ```no_run
148 /// use agtrace_sdk::Providers;
149 ///
150 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
151 /// let providers = Providers::builder()
152 /// .provider("claude_code", "/custom/.claude/projects")
153 /// .build()?;
154 /// # Ok(())
155 /// # }
156 /// ```
157 pub fn builder() -> ProvidersBuilder {
158 ProvidersBuilder::new()
159 }
160
161 // =========================================================================
162 // Operations
163 // =========================================================================
164
165 /// Parse a log file with auto-detected provider.
166 ///
167 /// Automatically detects the appropriate provider based on file path
168 /// and parses it into events.
169 ///
170 /// # Examples
171 ///
172 /// ```no_run
173 /// use agtrace_sdk::Providers;
174 /// use std::path::Path;
175 ///
176 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
177 /// let providers = Providers::detect()?;
178 /// let events = providers.parse_auto(Path::new("/path/to/session.jsonl"))?;
179 /// for event in &events {
180 /// println!("{:?}", event.payload);
181 /// }
182 /// # Ok(())
183 /// # }
184 /// ```
185 pub fn parse_auto(&self, path: &Path) -> Result<Vec<AgentEvent>> {
186 let path_str = path
187 .to_str()
188 .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
189
190 let adapter = agtrace_providers::detect_adapter_from_path(path_str)
191 .map_err(|_| Error::NotFound("No suitable provider detected for file".to_string()))?;
192
193 adapter
194 .parser
195 .parse_file(path)
196 .map_err(|e| Error::InvalidInput(format!("Parse error: {}", e)))
197 }
198
199 /// Parse a log file with a specific provider.
200 ///
201 /// # Examples
202 ///
203 /// ```no_run
204 /// use agtrace_sdk::Providers;
205 /// use std::path::Path;
206 ///
207 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
208 /// let providers = Providers::detect()?;
209 /// let events = providers.parse_file(Path::new("/path/to/log.jsonl"), "claude_code")?;
210 /// # Ok(())
211 /// # }
212 /// ```
213 pub fn parse_file(&self, path: &Path, provider_name: &str) -> Result<Vec<AgentEvent>> {
214 let adapter = agtrace_providers::create_adapter(provider_name)
215 .map_err(|_| Error::NotFound(format!("Unknown provider: {}", provider_name)))?;
216
217 adapter
218 .parser
219 .parse_file(path)
220 .map_err(|e| Error::InvalidInput(format!("Parse error: {}", e)))
221 }
222
223 /// Run diagnostics on all configured providers.
224 ///
225 /// Scans each provider's log directory and reports parsing statistics.
226 ///
227 /// # Examples
228 ///
229 /// ```no_run
230 /// use agtrace_sdk::Providers;
231 ///
232 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
233 /// let providers = Providers::detect()?;
234 /// let results = providers.diagnose()?;
235 ///
236 /// for result in &results {
237 /// let success_rate = if result.total_files > 0 {
238 /// (result.successful as f64 / result.total_files as f64) * 100.0
239 /// } else {
240 /// 100.0
241 /// };
242 /// println!("{}: {:.1}% success ({}/{})",
243 /// result.provider_name,
244 /// success_rate,
245 /// result.successful,
246 /// result.total_files);
247 /// }
248 /// # Ok(())
249 /// # }
250 /// ```
251 pub fn diagnose(&self) -> Result<Vec<DiagnoseResult>> {
252 let providers: Vec<_> = self
253 .provider_configs
254 .iter()
255 .filter_map(|(name, path)| {
256 agtrace_providers::create_adapter(name)
257 .ok()
258 .map(|adapter| (adapter, path.clone()))
259 })
260 .collect();
261
262 agtrace_runtime::DoctorService::diagnose_all(&providers).map_err(Error::Runtime)
263 }
264
265 /// Check if a file can be parsed.
266 ///
267 /// # Examples
268 ///
269 /// ```no_run
270 /// use agtrace_sdk::Providers;
271 /// use std::path::Path;
272 ///
273 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
274 /// let providers = Providers::detect()?;
275 /// let result = providers.check_file(Path::new("/path/to/log.jsonl"), None)?;
276 /// println!("Status: {:?}", result.status);
277 /// # Ok(())
278 /// # }
279 /// ```
280 pub fn check_file(&self, path: &Path, provider: Option<&str>) -> Result<CheckResult> {
281 let path_str = path
282 .to_str()
283 .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
284
285 let (adapter, provider_name) = if let Some(name) = provider {
286 let adapter = agtrace_providers::create_adapter(name)
287 .map_err(|_| Error::NotFound(format!("Provider: {}", name)))?;
288 (adapter, name.to_string())
289 } else {
290 let adapter = agtrace_providers::detect_adapter_from_path(path_str)
291 .map_err(|_| Error::NotFound("No suitable provider detected".to_string()))?;
292 let name = format!("{} (auto-detected)", adapter.id());
293 (adapter, name)
294 };
295
296 agtrace_runtime::DoctorService::check_file(path_str, &adapter, &provider_name)
297 .map_err(Error::Runtime)
298 }
299
300 /// Inspect raw file contents.
301 ///
302 /// Returns the first N lines of the file, optionally parsed as JSON.
303 ///
304 /// # Examples
305 ///
306 /// ```no_run
307 /// use agtrace_sdk::Providers;
308 /// use std::path::Path;
309 ///
310 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
311 /// let result = Providers::inspect_file(Path::new("/path/to/log.jsonl"), 10, true)?;
312 /// println!("Showing {} of {} lines", result.shown_lines, result.total_lines);
313 /// # Ok(())
314 /// # }
315 /// ```
316 pub fn inspect_file(path: &Path, lines: usize, json_format: bool) -> Result<InspectResult> {
317 let path_str = path
318 .to_str()
319 .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
320
321 agtrace_runtime::DoctorService::inspect_file(path_str, lines, json_format)
322 .map_err(Error::Runtime)
323 }
324
325 /// List configured providers.
326 ///
327 /// # Examples
328 ///
329 /// ```no_run
330 /// use agtrace_sdk::Providers;
331 ///
332 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
333 /// let providers = Providers::detect()?;
334 /// for (name, config) in providers.list() {
335 /// println!("{}: {:?}", name, config.log_root);
336 /// }
337 /// # Ok(())
338 /// # }
339 /// ```
340 pub fn list(&self) -> Vec<(&String, &ProviderConfig)> {
341 self.config.enabled_providers()
342 }
343
344 /// Get current configuration.
345 pub fn config(&self) -> &Config {
346 &self.config
347 }
348
349 /// Get provider configurations as (name, log_root) pairs.
350 pub fn provider_configs(&self) -> &[(String, PathBuf)] {
351 &self.provider_configs
352 }
353}
354
355// =============================================================================
356// ProvidersBuilder
357// =============================================================================
358
359/// Builder for configuring [`Providers`].
360///
361/// Allows programmatic configuration of providers without relying on
362/// filesystem detection or TOML files.
363///
364/// # Examples
365///
366/// ```no_run
367/// use agtrace_sdk::Providers;
368///
369/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
370/// // Start from auto-detected providers and add custom ones
371/// let providers = Providers::builder()
372/// .auto_detect()
373/// .provider("claude_code", "/custom/claude/path")
374/// .build()?;
375///
376/// // Or configure entirely manually
377/// let providers = Providers::builder()
378/// .provider("claude_code", "/path/to/.claude/projects")
379/// .provider("codex", "/path/to/.codex/sessions")
380/// .build()?;
381/// # Ok(())
382/// # }
383/// ```
384#[derive(Default)]
385pub struct ProvidersBuilder {
386 config: Option<Config>,
387 providers: Vec<(String, PathBuf)>,
388}
389
390impl ProvidersBuilder {
391 /// Create a new builder with no providers configured.
392 pub fn new() -> Self {
393 Self::default()
394 }
395
396 /// Load configuration from a TOML file.
397 ///
398 /// # Examples
399 ///
400 /// ```no_run
401 /// use agtrace_sdk::Providers;
402 /// use std::path::Path;
403 ///
404 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
405 /// let providers = Providers::builder()
406 /// .config_file(Path::new("/path/to/config.toml"))?
407 /// .build()?;
408 /// # Ok(())
409 /// # }
410 /// ```
411 pub fn config_file(mut self, path: impl AsRef<Path>) -> Result<Self> {
412 let config = Config::load_from(&path.as_ref().to_path_buf()).map_err(Error::Runtime)?;
413 self.config = Some(config);
414 Ok(self)
415 }
416
417 /// Use explicit configuration.
418 pub fn config(mut self, config: Config) -> Self {
419 self.config = Some(config);
420 self
421 }
422
423 /// Add a provider with custom log root.
424 ///
425 /// This overrides any provider with the same name from config file
426 /// or auto-detection.
427 ///
428 /// # Examples
429 ///
430 /// ```no_run
431 /// use agtrace_sdk::Providers;
432 ///
433 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
434 /// let providers = Providers::builder()
435 /// .provider("claude_code", "/custom/.claude/projects")
436 /// .build()?;
437 /// # Ok(())
438 /// # }
439 /// ```
440 pub fn provider(mut self, name: &str, log_root: impl Into<PathBuf>) -> Self {
441 self.providers.push((name.to_string(), log_root.into()));
442 self
443 }
444
445 /// Enable auto-detection of providers.
446 ///
447 /// Scans default log directories for each supported provider
448 /// and enables those that exist.
449 pub fn auto_detect(mut self) -> Self {
450 match Config::detect_providers() {
451 Ok(config) => {
452 self.config = Some(config);
453 }
454 Err(_) => {
455 // Silently ignore detection errors
456 }
457 }
458 self
459 }
460
461 /// Build the `Providers` instance.
462 pub fn build(self) -> Result<Providers> {
463 let mut config = self.config.unwrap_or_default();
464
465 // Apply manual provider overrides
466 for (name, log_root) in self.providers {
467 config.set_provider(
468 name,
469 ProviderConfig {
470 enabled: true,
471 log_root,
472 context_window_override: None,
473 },
474 );
475 }
476
477 Ok(Providers::with_config(config))
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484
485 #[test]
486 fn test_builder_creates_empty_providers() {
487 let providers = Providers::builder().build().unwrap();
488 assert!(providers.list().is_empty());
489 }
490
491 #[test]
492 fn test_builder_with_manual_provider() {
493 let providers = Providers::builder()
494 .provider("claude_code", "/tmp/test")
495 .build()
496 .unwrap();
497
498 assert_eq!(providers.list().len(), 1);
499 let (name, config) = &providers.list()[0];
500 assert_eq!(*name, "claude_code");
501 assert_eq!(config.log_root, PathBuf::from("/tmp/test"));
502 }
503
504 #[test]
505 fn test_with_config() {
506 let mut config = Config::default();
507 config.set_provider(
508 "test_provider".to_string(),
509 ProviderConfig {
510 enabled: true,
511 log_root: PathBuf::from("/test/path"),
512 context_window_override: None,
513 },
514 );
515
516 let providers = Providers::with_config(config);
517 assert_eq!(providers.list().len(), 1);
518 }
519}