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}