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    ///
311    /// Returns only the main stream. For multi-stream sessions (with sidechains
312    /// or subagents), use `assemble_all()` instead.
313    pub fn assemble(&self) -> Result<AgentSession> {
314        let events = self.events()?;
315        agtrace_engine::assemble_session(&events).ok_or_else(|| {
316            Error::InvalidInput(
317                "Failed to assemble session: insufficient or invalid events".to_string(),
318            )
319        })
320    }
321
322    /// Assemble all streams from events into separate sessions.
323    ///
324    /// Unlike `assemble()` which returns only the main stream, this method
325    /// returns all streams (Main, Sidechain, Subagent) found in the session's events.
326    pub fn assemble_all(&self) -> Result<Vec<AgentSession>> {
327        let events = self.events()?;
328        let sessions = agtrace_engine::assemble_sessions(&events);
329        if sessions.is_empty() {
330            return Err(Error::InvalidInput(
331                "Failed to assemble session: insufficient or invalid events".to_string(),
332            ));
333        }
334        Ok(sessions)
335    }
336
337    /// Export session with specified strategy.
338    pub fn export(&self, strategy: ExportStrategy) -> Result<Vec<AgentEvent>> {
339        let events = self.events()?;
340        Ok(agtrace_engine::export::transform(&events, strategy))
341    }
342
343    /// Get session metadata (DB-derived: project_hash, provider).
344    ///
345    /// Returns None for standalone sessions (created from events without workspace).
346    pub fn metadata(&self) -> Result<Option<crate::types::SessionMetadata>> {
347        match &self.source {
348            SessionSource::Workspace { inner, id } => {
349                let runtime_handle = inner
350                    .sessions()
351                    .find(id)
352                    .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
353
354                runtime_handle.metadata().map(Some).map_err(Error::Runtime)
355            }
356            SessionSource::Events { .. } => Ok(None),
357        }
358    }
359
360    /// Get raw log files for this session.
361    ///
362    /// Returns the list of raw log file paths and their contents.
363    /// Returns empty vector for standalone sessions (created from events without workspace).
364    pub fn raw_files(&self) -> Result<Vec<crate::types::RawFileContent>> {
365        match &self.source {
366            SessionSource::Workspace { inner, id } => {
367                let runtime_handle = inner
368                    .sessions()
369                    .find(id)
370                    .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
371
372                runtime_handle.raw_files().map_err(Error::Runtime)
373            }
374            SessionSource::Events { .. } => Ok(vec![]),
375        }
376    }
377
378    /// Summarize session statistics.
379    pub fn summarize(&self) -> Result<agtrace_engine::SessionSummary> {
380        let session = self.assemble()?;
381        Ok(agtrace_engine::session::summarize(&session))
382    }
383
384    /// Analyze session with diagnostic lenses.
385    pub fn analyze(&self) -> Result<crate::analysis::SessionAnalyzer> {
386        let session = self.assemble()?;
387        Ok(crate::analysis::SessionAnalyzer::new(session))
388    }
389
390    /// Get child sessions (subagents) that were spawned from this session.
391    ///
392    /// Returns a list of child session summaries with their spawn context
393    /// (turn_index, step_index). Returns empty vector for standalone sessions.
394    pub fn child_sessions(&self) -> Result<Vec<ChildSessionInfo>> {
395        match &self.source {
396            SessionSource::Workspace { inner, id } => {
397                let runtime_handle = inner
398                    .sessions()
399                    .find(id)
400                    .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
401
402                let children = runtime_handle.child_sessions().map_err(Error::Runtime)?;
403                Ok(children
404                    .into_iter()
405                    .map(|c| ChildSessionInfo {
406                        session_id: c.id,
407                        provider: c.provider,
408                        spawned_by: c.spawned_by,
409                        snippet: c.snippet,
410                    })
411                    .collect())
412            }
413            SessionSource::Events { .. } => Ok(vec![]),
414        }
415    }
416}
417
418/// Information about a child session (subagent) spawned from a parent session.
419#[derive(Debug, Clone)]
420pub struct ChildSessionInfo {
421    pub session_id: String,
422    pub provider: String,
423    pub spawned_by: Option<agtrace_types::SpawnContext>,
424    pub snippet: Option<String>,
425}
426
427// ============================================================================
428// ProjectClient
429// ============================================================================
430
431/// Client for project-related operations.
432pub struct ProjectClient {
433    inner: Arc<agtrace_runtime::AgTrace>,
434}
435
436impl ProjectClient {
437    /// List all projects in the workspace.
438    pub fn list(&self) -> Result<Vec<ProjectInfo>> {
439        self.inner.projects().list().map_err(Error::Runtime)
440    }
441}
442
443// ============================================================================
444// WatchClient
445// ============================================================================
446
447/// Client for live monitoring operations.
448pub struct WatchClient {
449    inner: Arc<agtrace_runtime::AgTrace>,
450}
451
452impl WatchClient {
453    /// Create a watch builder for configuring monitoring.
454    pub fn builder(&self) -> WatchBuilder {
455        WatchBuilder::new(self.inner.clone())
456    }
457
458    /// Watch all providers (convenience method).
459    pub fn all_providers(&self) -> WatchBuilder {
460        WatchBuilder::new(self.inner.clone()).all_providers()
461    }
462
463    /// Watch a specific provider (convenience method).
464    pub fn provider(&self, name: &str) -> WatchBuilder {
465        WatchBuilder::new(self.inner.clone()).provider(name)
466    }
467
468    /// Watch a specific session (convenience method).
469    pub fn session(&self, _id: &str) -> WatchBuilder {
470        // WatchBuilder doesn't have a session method yet, return builder for now
471        WatchBuilder::new(self.inner.clone())
472    }
473}
474
475// ============================================================================
476// InsightClient
477// ============================================================================
478
479/// Client for analysis and insights operations.
480pub struct InsightClient {
481    inner: Arc<agtrace_runtime::AgTrace>,
482}
483
484impl InsightClient {
485    /// Get corpus statistics.
486    pub fn corpus_stats(
487        &self,
488        project_hash: Option<&agtrace_types::ProjectHash>,
489        limit: usize,
490    ) -> Result<CorpusStats> {
491        self.inner
492            .insights()
493            .corpus_stats(project_hash, limit)
494            .map_err(Error::Runtime)
495    }
496
497    /// Get tool usage statistics.
498    pub fn tool_usage(
499        &self,
500        limit: Option<usize>,
501        provider: Option<String>,
502    ) -> Result<agtrace_runtime::StatsResult> {
503        self.inner
504            .insights()
505            .tool_usage(limit, provider)
506            .map_err(Error::Runtime)
507    }
508
509    /// Pack sessions for analysis (placeholder - needs runtime implementation).
510    pub fn pack(&self, _limit: usize) -> Result<PackResult> {
511        // TODO: This needs to be implemented in agtrace-runtime
512        Err(Error::InvalidInput(
513            "Pack operation not yet implemented in runtime".to_string(),
514        ))
515    }
516
517    /// Grep through tool calls (placeholder - needs runtime implementation).
518    pub fn grep(
519        &self,
520        _pattern: &str,
521        _filter: &SessionFilter,
522        _limit: usize,
523    ) -> Result<Vec<AgentEvent>> {
524        // TODO: This needs to be implemented in agtrace-runtime
525        Err(Error::InvalidInput(
526            "Grep operation not yet implemented in runtime".to_string(),
527        ))
528    }
529}
530
531// ============================================================================
532// SystemClient
533// ============================================================================
534
535/// Client for system-level operations (init, index, doctor, provider).
536pub struct SystemClient {
537    inner: Arc<agtrace_runtime::AgTrace>,
538}
539
540impl SystemClient {
541    /// Initialize a new workspace (static method).
542    pub fn initialize<F>(config: InitConfig, on_progress: Option<F>) -> Result<InitResult>
543    where
544        F: FnMut(InitProgress),
545    {
546        agtrace_runtime::AgTrace::setup(config, on_progress).map_err(Error::Runtime)
547    }
548
549    /// Run diagnostics on all providers.
550    pub fn diagnose(&self) -> Result<Vec<DiagnoseResult>> {
551        self.inner.diagnose().map_err(Error::Runtime)
552    }
553
554    /// Check if a file can be parsed (requires workspace context).
555    pub fn check_file(&self, path: &Path, provider: Option<&str>) -> Result<CheckResult> {
556        let path_str = path
557            .to_str()
558            .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
559
560        // Detect adapter
561        let (adapter, provider_name) = if let Some(name) = provider {
562            let adapter = agtrace_providers::create_adapter(name)
563                .map_err(|_| Error::NotFound(format!("Provider: {}", name)))?;
564            (adapter, name.to_string())
565        } else {
566            let adapter = agtrace_providers::detect_adapter_from_path(path_str)
567                .map_err(|_| Error::NotFound("No suitable provider detected".to_string()))?;
568            let name = format!("{} (auto-detected)", adapter.id());
569            (adapter, name)
570        };
571
572        agtrace_runtime::AgTrace::check_file(path_str, &adapter, &provider_name)
573            .map_err(Error::Runtime)
574    }
575
576    /// Inspect file contents with parsing.
577    pub fn inspect_file(path: &Path, lines: usize, json_format: bool) -> Result<InspectResult> {
578        let path_str = path
579            .to_str()
580            .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
581
582        agtrace_runtime::AgTrace::inspect_file(path_str, lines, json_format).map_err(Error::Runtime)
583    }
584
585    /// Reindex the workspace.
586    pub fn reindex<F>(
587        &self,
588        scope: agtrace_types::ProjectScope,
589        force: bool,
590        provider_filter: Option<&str>,
591        on_progress: F,
592    ) -> Result<()>
593    where
594        F: FnMut(IndexProgress),
595    {
596        self.inner
597            .projects()
598            .scan(scope, force, provider_filter, on_progress)
599            .map(|_| ()) // Discard the ScanSummary for now
600            .map_err(Error::Runtime)
601    }
602
603    /// Vacuum the database to reclaim space.
604    pub fn vacuum(&self) -> Result<()> {
605        let db = self.inner.database();
606        let db = db.lock().unwrap();
607        db.vacuum().map_err(|e| Error::Runtime(e.into()))
608    }
609
610    /// List provider configurations.
611    pub fn list_providers(&self) -> Result<Vec<ProviderConfig>> {
612        Ok(self.inner.config().providers.values().cloned().collect())
613    }
614
615    /// Detect providers in current environment.
616    pub fn detect_providers() -> Result<Config> {
617        agtrace_runtime::Config::detect_providers().map_err(Error::Runtime)
618    }
619
620    /// Get current configuration.
621    pub fn config(&self) -> Config {
622        self.inner.config().clone()
623    }
624}