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// Main Client
10// ============================================================================
11
12/// Main entry point for interacting with an agtrace workspace.
13pub struct Client {
14    inner: Arc<agtrace_runtime::AgTrace>,
15}
16
17impl Client {
18    /// Connect to an agtrace workspace at the given path.
19    pub fn connect(path: impl Into<PathBuf>) -> Result<Self> {
20        let path = path.into();
21        let runtime = agtrace_runtime::AgTrace::open(path).map_err(Error::Internal)?;
22        Ok(Self {
23            inner: Arc::new(runtime),
24        })
25    }
26
27    /// Access session operations.
28    pub fn sessions(&self) -> SessionClient {
29        SessionClient {
30            inner: self.inner.clone(),
31        }
32    }
33
34    /// Access project operations.
35    pub fn projects(&self) -> ProjectClient {
36        ProjectClient {
37            inner: self.inner.clone(),
38        }
39    }
40
41    /// Access watch/monitoring operations.
42    pub fn watch(&self) -> WatchClient {
43        WatchClient {
44            inner: self.inner.clone(),
45        }
46    }
47
48    /// Access insights/analysis operations.
49    pub fn insights(&self) -> InsightClient {
50        InsightClient {
51            inner: self.inner.clone(),
52        }
53    }
54
55    /// Access system operations (init, index, doctor, provider).
56    pub fn system(&self) -> SystemClient {
57        SystemClient {
58            inner: self.inner.clone(),
59        }
60    }
61
62    /// Get the watch service for low-level watch operations.
63    /// Prefer using `client.watch()` for most use cases.
64    pub fn watch_service(&self) -> crate::types::WatchService {
65        self.inner.watch_service()
66    }
67
68    // Legacy compatibility methods (to be deprecated)
69    #[deprecated(note = "Use client.sessions().get(id) instead")]
70    pub fn session(&self, id: &str) -> SessionHandle {
71        SessionHandle {
72            source: SessionSource::Workspace {
73                inner: self.inner.clone(),
74                id: id.to_string(),
75            },
76        }
77    }
78
79    #[deprecated(note = "Use client.sessions().list(filter) instead")]
80    pub fn list_sessions(&self) -> Result<Vec<SessionSummary>> {
81        let filter = SessionFilter::new().limit(100);
82        self.inner.sessions().list(filter).map_err(Error::Internal)
83    }
84}
85
86// ============================================================================
87// SessionClient
88// ============================================================================
89
90/// Client for session-related operations.
91pub struct SessionClient {
92    inner: Arc<agtrace_runtime::AgTrace>,
93}
94
95impl SessionClient {
96    /// List sessions with optional filtering.
97    pub fn list(&self, filter: SessionFilter) -> Result<Vec<SessionSummary>> {
98        self.inner.sessions().list(filter).map_err(Error::Internal)
99    }
100
101    /// List sessions without triggering auto-refresh.
102    pub fn list_without_refresh(&self, filter: SessionFilter) -> Result<Vec<SessionSummary>> {
103        self.inner
104            .sessions()
105            .list_without_refresh(filter)
106            .map_err(Error::Internal)
107    }
108
109    /// Pack sessions for context window analysis.
110    pub fn pack_context(
111        &self,
112        project_hash: Option<&crate::types::ProjectHash>,
113        limit: usize,
114    ) -> Result<crate::types::PackResult> {
115        self.inner
116            .sessions()
117            .pack_context(project_hash, limit)
118            .map_err(Error::Internal)
119    }
120
121    /// Get a session handle by ID or prefix.
122    pub fn get(&self, id_or_prefix: &str) -> Result<SessionHandle> {
123        // Validate the session exists by trying to find it
124        self.inner
125            .sessions()
126            .find(id_or_prefix)
127            .map_err(|e| Error::NotFound(format!("Session {}: {}", id_or_prefix, e)))?;
128
129        Ok(SessionHandle {
130            source: SessionSource::Workspace {
131                inner: self.inner.clone(),
132                id: id_or_prefix.to_string(),
133            },
134        })
135    }
136}
137
138// ============================================================================
139// SessionHandle
140// ============================================================================
141
142/// Handle to a specific session, providing access to its data.
143pub struct SessionHandle {
144    source: SessionSource,
145}
146
147enum SessionSource {
148    /// Session from a workspace (Client-based)
149    Workspace {
150        inner: Arc<agtrace_runtime::AgTrace>,
151        id: String,
152    },
153    /// Session from raw events (Standalone)
154    Events {
155        events: Vec<crate::types::AgentEvent>,
156    },
157}
158
159impl SessionHandle {
160    /// Create a SessionHandle from raw events (for testing, simulations, custom pipelines).
161    ///
162    /// This allows you to use SessionHandle API without a Client connection.
163    ///
164    /// # Examples
165    ///
166    /// ```no_run
167    /// use agtrace_sdk::{SessionHandle, types::AgentEvent};
168    ///
169    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
170    /// let events: Vec<AgentEvent> = vec![/* ... */];
171    /// let handle = SessionHandle::from_events(events);
172    ///
173    /// let session = handle.assemble()?;
174    /// let summary = handle.summarize()?;
175    /// # Ok(())
176    /// # }
177    /// ```
178    pub fn from_events(events: Vec<AgentEvent>) -> Self {
179        Self {
180            source: SessionSource::Events { events },
181        }
182    }
183
184    /// Load raw events for this session.
185    pub fn events(&self) -> Result<Vec<AgentEvent>> {
186        match &self.source {
187            SessionSource::Workspace { inner, id } => {
188                let session_handle = inner
189                    .sessions()
190                    .find(id)
191                    .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
192
193                session_handle.events().map_err(Error::Internal)
194            }
195            SessionSource::Events { events } => Ok(events.clone()),
196        }
197    }
198
199    /// Assemble events into a structured session.
200    pub fn assemble(&self) -> Result<AgentSession> {
201        let events = self.events()?;
202        agtrace_engine::assemble_session(&events)
203            .ok_or_else(|| Error::Internal(anyhow::anyhow!("Failed to assemble session")))
204    }
205
206    /// Export session with specified strategy.
207    pub fn export(&self, strategy: ExportStrategy) -> Result<Vec<AgentEvent>> {
208        let events = self.events()?;
209        Ok(agtrace_engine::export::transform(&events, strategy))
210    }
211
212    /// Summarize session statistics.
213    pub fn summarize(&self) -> Result<agtrace_engine::SessionSummary> {
214        let session = self.assemble()?;
215        Ok(agtrace_engine::session::summarize(&session))
216    }
217
218    /// Analyze session with diagnostic lenses.
219    pub fn analyze(&self) -> Result<crate::analysis::SessionAnalyzer> {
220        let session = self.assemble()?;
221        Ok(crate::analysis::SessionAnalyzer::new(session))
222    }
223
224    /// Get session summary (legacy compatibility).
225    #[deprecated(note = "Use summarize() instead")]
226    pub fn summary(&self) -> Result<agtrace_engine::SessionSummary> {
227        self.summarize()
228    }
229}
230
231// ============================================================================
232// ProjectClient
233// ============================================================================
234
235/// Client for project-related operations.
236pub struct ProjectClient {
237    inner: Arc<agtrace_runtime::AgTrace>,
238}
239
240impl ProjectClient {
241    /// List all projects in the workspace.
242    pub fn list(&self) -> Result<Vec<ProjectInfo>> {
243        self.inner.projects().list().map_err(Error::Internal)
244    }
245}
246
247// ============================================================================
248// WatchClient
249// ============================================================================
250
251/// Client for live monitoring operations.
252pub struct WatchClient {
253    inner: Arc<agtrace_runtime::AgTrace>,
254}
255
256impl WatchClient {
257    /// Create a watch builder for configuring monitoring.
258    pub fn builder(&self) -> WatchBuilder {
259        WatchBuilder::new(self.inner.clone())
260    }
261
262    /// Watch all providers (convenience method).
263    pub fn all_providers(&self) -> WatchBuilder {
264        WatchBuilder::new(self.inner.clone()).all_providers()
265    }
266
267    /// Watch a specific provider (convenience method).
268    pub fn provider(&self, name: &str) -> WatchBuilder {
269        WatchBuilder::new(self.inner.clone()).provider(name)
270    }
271
272    /// Watch a specific session (convenience method).
273    pub fn session(&self, _id: &str) -> WatchBuilder {
274        // WatchBuilder doesn't have a session method yet, return builder for now
275        WatchBuilder::new(self.inner.clone())
276    }
277}
278
279// ============================================================================
280// InsightClient
281// ============================================================================
282
283/// Client for analysis and insights operations.
284pub struct InsightClient {
285    inner: Arc<agtrace_runtime::AgTrace>,
286}
287
288impl InsightClient {
289    /// Get corpus statistics.
290    pub fn corpus_stats(
291        &self,
292        project_hash: Option<&agtrace_types::ProjectHash>,
293        limit: usize,
294    ) -> Result<CorpusStats> {
295        self.inner
296            .insights()
297            .corpus_stats(project_hash, limit)
298            .map_err(Error::Internal)
299    }
300
301    /// Get tool usage statistics.
302    pub fn tool_usage(
303        &self,
304        limit: Option<usize>,
305        provider: Option<String>,
306    ) -> Result<agtrace_runtime::StatsResult> {
307        self.inner
308            .insights()
309            .tool_usage(limit, provider)
310            .map_err(Error::Internal)
311    }
312
313    /// Pack sessions for analysis (placeholder - needs runtime implementation).
314    pub fn pack(&self, _limit: usize) -> Result<PackResult> {
315        // TODO: This needs to be implemented in agtrace-runtime
316        Err(Error::InvalidInput(
317            "Pack operation not yet implemented in runtime".to_string(),
318        ))
319    }
320
321    /// Grep through tool calls (placeholder - needs runtime implementation).
322    pub fn grep(
323        &self,
324        _pattern: &str,
325        _filter: &SessionFilter,
326        _limit: usize,
327    ) -> Result<Vec<AgentEvent>> {
328        // TODO: This needs to be implemented in agtrace-runtime
329        Err(Error::InvalidInput(
330            "Grep operation not yet implemented in runtime".to_string(),
331        ))
332    }
333}
334
335// ============================================================================
336// SystemClient
337// ============================================================================
338
339/// Client for system-level operations (init, index, doctor, provider).
340pub struct SystemClient {
341    inner: Arc<agtrace_runtime::AgTrace>,
342}
343
344impl SystemClient {
345    /// Initialize a new workspace (static method).
346    pub fn initialize<F>(config: InitConfig, on_progress: Option<F>) -> Result<InitResult>
347    where
348        F: FnMut(InitProgress),
349    {
350        agtrace_runtime::AgTrace::setup(config, on_progress).map_err(Error::Internal)
351    }
352
353    /// Run diagnostics on all providers.
354    pub fn diagnose(&self) -> Result<Vec<DiagnoseResult>> {
355        self.inner.diagnose().map_err(Error::Internal)
356    }
357
358    /// Check if a file can be parsed (requires workspace context).
359    pub fn check_file(&self, path: &Path, provider: Option<&str>) -> Result<CheckResult> {
360        let path_str = path
361            .to_str()
362            .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
363
364        // Detect adapter
365        let (adapter, provider_name) = if let Some(name) = provider {
366            let adapter = agtrace_providers::create_adapter(name)
367                .map_err(|_| Error::NotFound(format!("Provider: {}", name)))?;
368            (adapter, name.to_string())
369        } else {
370            let adapter = agtrace_providers::detect_adapter_from_path(path_str)
371                .map_err(|_| Error::NotFound("No suitable provider detected".to_string()))?;
372            let name = format!("{} (auto-detected)", adapter.id());
373            (adapter, name)
374        };
375
376        agtrace_runtime::AgTrace::check_file(path_str, &adapter, &provider_name)
377            .map_err(Error::Internal)
378    }
379
380    /// Inspect file contents with parsing.
381    pub fn inspect_file(path: &Path, lines: usize, json_format: bool) -> Result<InspectResult> {
382        let path_str = path
383            .to_str()
384            .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
385
386        agtrace_runtime::AgTrace::inspect_file(path_str, lines, json_format)
387            .map_err(Error::Internal)
388    }
389
390    /// Reindex the workspace.
391    pub fn reindex<F>(
392        &self,
393        scope: agtrace_types::ProjectScope,
394        force: bool,
395        provider_filter: Option<&str>,
396        on_progress: F,
397    ) -> Result<()>
398    where
399        F: FnMut(IndexProgress),
400    {
401        self.inner
402            .projects()
403            .scan(scope, force, provider_filter, on_progress)
404            .map(|_| ()) // Discard the ScanSummary for now
405            .map_err(Error::Internal)
406    }
407
408    /// Vacuum the database to reclaim space.
409    pub fn vacuum(&self) -> Result<()> {
410        let db = self.inner.database();
411        let db = db.lock().unwrap();
412        db.vacuum().map_err(Error::Internal)
413    }
414
415    /// List provider configurations.
416    pub fn list_providers(&self) -> Result<Vec<ProviderConfig>> {
417        Ok(self.inner.config().providers.values().cloned().collect())
418    }
419
420    /// Detect providers in current environment.
421    pub fn detect_providers() -> Result<Config> {
422        agtrace_runtime::Config::detect_providers().map_err(Error::Internal)
423    }
424
425    /// Get current configuration.
426    pub fn config(&self) -> Config {
427        self.inner.config().clone()
428    }
429}