agtrace_sdk/
client.rs

1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use crate::error::{Error, Result};
5use crate::types::*;
6use crate::watch::WatchBuilder;
7
8// ============================================================================
9// ClientBuilder
10// ============================================================================
11
12/// Builder for configuring and connecting to an agtrace workspace.
13///
14/// Provides flexible path resolution with the following priority:
15/// 1. Explicit path via `builder.path()`
16/// 2. `AGTRACE_PATH` environment variable
17/// 3. System data directory (e.g., `~/.local/share/agtrace` on Linux, `~/Library/Application Support/agtrace` on macOS)
18///
19/// # Examples
20///
21/// ```no_run
22/// # use agtrace_sdk::Client;
23/// # #[tokio::main]
24/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
25/// // Use default system path
26/// let client = Client::connect_default().await?;
27///
28/// // Use explicit path
29/// let client = Client::builder()
30///     .path("/custom/path")
31///     .connect().await?;
32///
33/// // Use AGTRACE_PATH environment variable
34/// // $ export AGTRACE_PATH=/tmp/agtrace
35/// let client = Client::builder().connect().await?;
36/// # Ok(())
37/// # }
38/// ```
39#[derive(Default)]
40pub struct ClientBuilder {
41    path: Option<PathBuf>,
42}
43
44impl ClientBuilder {
45    /// Create a new ClientBuilder with default settings.
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Set an explicit workspace path (highest priority).
51    pub fn path(mut self, path: impl Into<PathBuf>) -> Self {
52        self.path = Some(path.into());
53        self
54    }
55
56    /// Connect to the workspace using the configured or resolved path.
57    /// If the workspace does not exist, it will be automatically initialized.
58    pub async fn connect(self) -> Result<Client> {
59        let path = self.resolve_path()?;
60        let runtime = agtrace_runtime::AgTrace::connect_or_create(path)
61            .await
62            .map_err(Error::Runtime)?;
63        Ok(Client {
64            inner: Arc::new(runtime),
65        })
66    }
67
68    /// Resolve the workspace path based on priority:
69    /// 1. Explicit path from builder
70    /// 2. AGTRACE_PATH environment variable
71    /// 3. System data directory
72    fn resolve_path(&self) -> Result<PathBuf> {
73        let explicit_path = self.path.as_ref().and_then(|p| p.to_str());
74        agtrace_runtime::resolve_workspace_path(explicit_path).map_err(Error::Runtime)
75    }
76}
77
78// ============================================================================
79// Main Client
80// ============================================================================
81
82/// Main entry point for interacting with an agtrace workspace.
83#[derive(Clone)]
84pub struct Client {
85    inner: Arc<agtrace_runtime::AgTrace>,
86}
87
88impl Client {
89    /// Create a new ClientBuilder for configuring workspace connection.
90    ///
91    /// This is the recommended way to connect to a workspace as it supports
92    /// platform-standard path resolution and environment variable configuration.
93    ///
94    /// # Examples
95    ///
96    /// ```no_run
97    /// # use agtrace_sdk::Client;
98    /// # #[tokio::main]
99    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
100    /// // Use default path
101    /// let client = Client::builder().connect().await?;
102    ///
103    /// // Use custom path
104    /// let client = Client::builder()
105    ///     .path("/custom/agtrace")
106    ///     .connect().await?;
107    /// # Ok(())
108    /// # }
109    /// ```
110    pub fn builder() -> ClientBuilder {
111        ClientBuilder::new()
112    }
113
114    /// Connect to the default agtrace workspace.
115    ///
116    /// This is a convenience method that uses platform-standard path resolution.
117    /// It checks (in order):
118    /// 1. `AGTRACE_PATH` environment variable
119    /// 2. System data directory + "agtrace" (e.g., `~/.local/share/agtrace`)
120    ///
121    /// # Examples
122    ///
123    /// ```no_run
124    /// # use agtrace_sdk::Client;
125    /// # #[tokio::main]
126    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
127    /// let client = Client::connect_default().await?;
128    /// # Ok(())
129    /// # }
130    /// ```
131    pub async fn connect_default() -> Result<Self> {
132        Self::builder().connect().await
133    }
134
135    /// Connect to an agtrace workspace at the given path.
136    ///
137    /// This is a low-level API. Consider using `Client::builder()` or
138    /// `Client::connect_default()` for better ergonomics and system path support.
139    ///
140    /// # Examples
141    ///
142    /// ```no_run
143    /// # use agtrace_sdk::Client;
144    /// # #[tokio::main]
145    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
146    /// let client = Client::connect("/custom/agtrace/path").await?;
147    /// # Ok(())
148    /// # }
149    /// ```
150    pub async fn connect(path: impl Into<PathBuf>) -> Result<Self> {
151        Self::builder().path(path).connect().await
152    }
153
154    /// Access session operations.
155    pub fn sessions(&self) -> SessionClient {
156        SessionClient {
157            inner: self.inner.clone(),
158        }
159    }
160
161    /// Access project operations.
162    pub fn projects(&self) -> ProjectClient {
163        ProjectClient {
164            inner: self.inner.clone(),
165        }
166    }
167
168    /// Access watch/monitoring operations.
169    pub fn watch(&self) -> WatchClient {
170        WatchClient {
171            inner: self.inner.clone(),
172        }
173    }
174
175    /// Access insights/analysis operations.
176    pub fn insights(&self) -> InsightClient {
177        InsightClient {
178            inner: self.inner.clone(),
179        }
180    }
181
182    /// Access system operations (init, index, doctor, provider).
183    pub fn system(&self) -> SystemClient {
184        SystemClient {
185            inner: self.inner.clone(),
186        }
187    }
188
189    /// Get the watch service for low-level watch operations.
190    /// Prefer using `client.watch()` for most use cases.
191    pub fn watch_service(&self) -> crate::types::WatchService {
192        self.inner.watch_service()
193    }
194}
195
196// ============================================================================
197// SessionClient
198// ============================================================================
199
200/// Client for session-related operations.
201pub struct SessionClient {
202    inner: Arc<agtrace_runtime::AgTrace>,
203}
204
205impl SessionClient {
206    /// List sessions with optional filtering.
207    pub fn list(&self, filter: SessionFilter) -> Result<Vec<SessionSummary>> {
208        self.inner.sessions().list(filter).map_err(Error::Runtime)
209    }
210
211    /// List sessions without triggering auto-refresh.
212    pub fn list_without_refresh(&self, filter: SessionFilter) -> Result<Vec<SessionSummary>> {
213        self.inner
214            .sessions()
215            .list_without_refresh(filter)
216            .map_err(Error::Runtime)
217    }
218
219    /// Pack sessions for context window analysis.
220    pub fn pack_context(
221        &self,
222        project_hash: Option<&crate::types::ProjectHash>,
223        limit: usize,
224    ) -> Result<crate::types::PackResult> {
225        self.inner
226            .sessions()
227            .pack_context(project_hash, limit)
228            .map_err(Error::Runtime)
229    }
230
231    /// Get a session handle by ID or prefix.
232    pub fn get(&self, id_or_prefix: &str) -> Result<SessionHandle> {
233        // Validate the session exists by trying to find it
234        self.inner
235            .sessions()
236            .find(id_or_prefix)
237            .map_err(|e| Error::NotFound(format!("Session {}: {}", id_or_prefix, e)))?;
238
239        Ok(SessionHandle {
240            source: SessionSource::Workspace {
241                inner: self.inner.clone(),
242                id: id_or_prefix.to_string(),
243            },
244        })
245    }
246}
247
248// ============================================================================
249// SessionHandle
250// ============================================================================
251
252/// Handle to a specific session, providing access to its data.
253pub struct SessionHandle {
254    source: SessionSource,
255}
256
257enum SessionSource {
258    /// Session from a workspace (Client-based)
259    Workspace {
260        inner: Arc<agtrace_runtime::AgTrace>,
261        id: String,
262    },
263    /// Session from raw events (Standalone)
264    Events {
265        events: Vec<crate::types::AgentEvent>,
266    },
267}
268
269impl SessionHandle {
270    /// Create a SessionHandle from raw events (for testing, simulations, custom pipelines).
271    ///
272    /// This allows you to use SessionHandle API without a Client connection.
273    ///
274    /// # Examples
275    ///
276    /// ```no_run
277    /// use agtrace_sdk::{SessionHandle, types::AgentEvent};
278    ///
279    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
280    /// let events: Vec<AgentEvent> = vec![/* ... */];
281    /// let handle = SessionHandle::from_events(events);
282    ///
283    /// let session = handle.assemble()?;
284    /// let summary = handle.summarize()?;
285    /// # Ok(())
286    /// # }
287    /// ```
288    pub fn from_events(events: Vec<AgentEvent>) -> Self {
289        Self {
290            source: SessionSource::Events { events },
291        }
292    }
293
294    /// Load raw events for this session.
295    pub fn events(&self) -> Result<Vec<AgentEvent>> {
296        match &self.source {
297            SessionSource::Workspace { inner, id } => {
298                let session_handle = inner
299                    .sessions()
300                    .find(id)
301                    .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
302
303                session_handle.events().map_err(Error::Runtime)
304            }
305            SessionSource::Events { events } => Ok(events.clone()),
306        }
307    }
308
309    /// Assemble events into a structured session.
310    pub fn assemble(&self) -> Result<AgentSession> {
311        let events = self.events()?;
312        agtrace_engine::assemble_session(&events).ok_or_else(|| {
313            Error::InvalidInput(
314                "Failed to assemble session: insufficient or invalid events".to_string(),
315            )
316        })
317    }
318
319    /// Export session with specified strategy.
320    pub fn export(&self, strategy: ExportStrategy) -> Result<Vec<AgentEvent>> {
321        let events = self.events()?;
322        Ok(agtrace_engine::export::transform(&events, strategy))
323    }
324
325    /// Get session metadata (DB-derived: project_hash, provider).
326    ///
327    /// Returns None for standalone sessions (created from events without workspace).
328    pub fn metadata(&self) -> Result<Option<crate::types::SessionMetadata>> {
329        match &self.source {
330            SessionSource::Workspace { inner, id } => {
331                let runtime_handle = inner
332                    .sessions()
333                    .find(id)
334                    .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
335
336                runtime_handle.metadata().map(Some).map_err(Error::Runtime)
337            }
338            SessionSource::Events { .. } => Ok(None),
339        }
340    }
341
342    /// Summarize session statistics.
343    pub fn summarize(&self) -> Result<agtrace_engine::SessionSummary> {
344        let session = self.assemble()?;
345        Ok(agtrace_engine::session::summarize(&session))
346    }
347
348    /// Analyze session with diagnostic lenses.
349    pub fn analyze(&self) -> Result<crate::analysis::SessionAnalyzer> {
350        let session = self.assemble()?;
351        Ok(crate::analysis::SessionAnalyzer::new(session))
352    }
353}
354
355// ============================================================================
356// ProjectClient
357// ============================================================================
358
359/// Client for project-related operations.
360pub struct ProjectClient {
361    inner: Arc<agtrace_runtime::AgTrace>,
362}
363
364impl ProjectClient {
365    /// List all projects in the workspace.
366    pub fn list(&self) -> Result<Vec<ProjectInfo>> {
367        self.inner.projects().list().map_err(Error::Runtime)
368    }
369}
370
371// ============================================================================
372// WatchClient
373// ============================================================================
374
375/// Client for live monitoring operations.
376pub struct WatchClient {
377    inner: Arc<agtrace_runtime::AgTrace>,
378}
379
380impl WatchClient {
381    /// Create a watch builder for configuring monitoring.
382    pub fn builder(&self) -> WatchBuilder {
383        WatchBuilder::new(self.inner.clone())
384    }
385
386    /// Watch all providers (convenience method).
387    pub fn all_providers(&self) -> WatchBuilder {
388        WatchBuilder::new(self.inner.clone()).all_providers()
389    }
390
391    /// Watch a specific provider (convenience method).
392    pub fn provider(&self, name: &str) -> WatchBuilder {
393        WatchBuilder::new(self.inner.clone()).provider(name)
394    }
395
396    /// Watch a specific session (convenience method).
397    pub fn session(&self, _id: &str) -> WatchBuilder {
398        // WatchBuilder doesn't have a session method yet, return builder for now
399        WatchBuilder::new(self.inner.clone())
400    }
401}
402
403// ============================================================================
404// InsightClient
405// ============================================================================
406
407/// Client for analysis and insights operations.
408pub struct InsightClient {
409    inner: Arc<agtrace_runtime::AgTrace>,
410}
411
412impl InsightClient {
413    /// Get corpus statistics.
414    pub fn corpus_stats(
415        &self,
416        project_hash: Option<&agtrace_types::ProjectHash>,
417        limit: usize,
418    ) -> Result<CorpusStats> {
419        self.inner
420            .insights()
421            .corpus_stats(project_hash, limit)
422            .map_err(Error::Runtime)
423    }
424
425    /// Get tool usage statistics.
426    pub fn tool_usage(
427        &self,
428        limit: Option<usize>,
429        provider: Option<String>,
430    ) -> Result<agtrace_runtime::StatsResult> {
431        self.inner
432            .insights()
433            .tool_usage(limit, provider)
434            .map_err(Error::Runtime)
435    }
436
437    /// Pack sessions for analysis (placeholder - needs runtime implementation).
438    pub fn pack(&self, _limit: usize) -> Result<PackResult> {
439        // TODO: This needs to be implemented in agtrace-runtime
440        Err(Error::InvalidInput(
441            "Pack operation not yet implemented in runtime".to_string(),
442        ))
443    }
444
445    /// Grep through tool calls (placeholder - needs runtime implementation).
446    pub fn grep(
447        &self,
448        _pattern: &str,
449        _filter: &SessionFilter,
450        _limit: usize,
451    ) -> Result<Vec<AgentEvent>> {
452        // TODO: This needs to be implemented in agtrace-runtime
453        Err(Error::InvalidInput(
454            "Grep operation not yet implemented in runtime".to_string(),
455        ))
456    }
457}
458
459// ============================================================================
460// SystemClient
461// ============================================================================
462
463/// Client for system-level operations (init, index, doctor, provider).
464pub struct SystemClient {
465    inner: Arc<agtrace_runtime::AgTrace>,
466}
467
468impl SystemClient {
469    /// Initialize a new workspace (static method).
470    pub fn initialize<F>(config: InitConfig, on_progress: Option<F>) -> Result<InitResult>
471    where
472        F: FnMut(InitProgress),
473    {
474        agtrace_runtime::AgTrace::setup(config, on_progress).map_err(Error::Runtime)
475    }
476
477    /// Run diagnostics on all providers.
478    pub fn diagnose(&self) -> Result<Vec<DiagnoseResult>> {
479        self.inner.diagnose().map_err(Error::Runtime)
480    }
481
482    /// Check if a file can be parsed (requires workspace context).
483    pub fn check_file(&self, path: &Path, provider: Option<&str>) -> Result<CheckResult> {
484        let path_str = path
485            .to_str()
486            .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
487
488        // Detect adapter
489        let (adapter, provider_name) = if let Some(name) = provider {
490            let adapter = agtrace_providers::create_adapter(name)
491                .map_err(|_| Error::NotFound(format!("Provider: {}", name)))?;
492            (adapter, name.to_string())
493        } else {
494            let adapter = agtrace_providers::detect_adapter_from_path(path_str)
495                .map_err(|_| Error::NotFound("No suitable provider detected".to_string()))?;
496            let name = format!("{} (auto-detected)", adapter.id());
497            (adapter, name)
498        };
499
500        agtrace_runtime::AgTrace::check_file(path_str, &adapter, &provider_name)
501            .map_err(Error::Runtime)
502    }
503
504    /// Inspect file contents with parsing.
505    pub fn inspect_file(path: &Path, lines: usize, json_format: bool) -> Result<InspectResult> {
506        let path_str = path
507            .to_str()
508            .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
509
510        agtrace_runtime::AgTrace::inspect_file(path_str, lines, json_format).map_err(Error::Runtime)
511    }
512
513    /// Reindex the workspace.
514    pub fn reindex<F>(
515        &self,
516        scope: agtrace_types::ProjectScope,
517        force: bool,
518        provider_filter: Option<&str>,
519        on_progress: F,
520    ) -> Result<()>
521    where
522        F: FnMut(IndexProgress),
523    {
524        self.inner
525            .projects()
526            .scan(scope, force, provider_filter, on_progress)
527            .map(|_| ()) // Discard the ScanSummary for now
528            .map_err(Error::Runtime)
529    }
530
531    /// Vacuum the database to reclaim space.
532    pub fn vacuum(&self) -> Result<()> {
533        let db = self.inner.database();
534        let db = db.lock().unwrap();
535        db.vacuum().map_err(|e| Error::Runtime(e.into()))
536    }
537
538    /// List provider configurations.
539    pub fn list_providers(&self) -> Result<Vec<ProviderConfig>> {
540        Ok(self.inner.config().providers.values().cloned().collect())
541    }
542
543    /// Detect providers in current environment.
544    pub fn detect_providers() -> Result<Config> {
545        agtrace_runtime::Config::detect_providers().map_err(Error::Runtime)
546    }
547
548    /// Get current configuration.
549    pub fn config(&self) -> Config {
550        self.inner.config().clone()
551    }
552}