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}