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