1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use crate::error::{Error, Result};
5use crate::providers::Providers;
6use crate::query::{
7 Cursor, EventMatch, GetTurnsArgs, GetTurnsResponse, ListTurnsArgs, ListTurnsResponse,
8 SearchEventsArgs, SearchEventsResponse,
9};
10use crate::types::*;
11use crate::watch::WatchBuilder;
12
13#[derive(Default)]
45pub struct ClientBuilder {
46 path: Option<PathBuf>,
47}
48
49impl ClientBuilder {
50 pub fn new() -> Self {
52 Self::default()
53 }
54
55 pub fn path(mut self, path: impl Into<PathBuf>) -> Self {
57 self.path = Some(path.into());
58 self
59 }
60
61 pub async fn connect(self) -> Result<Client> {
64 let path = self.resolve_path()?;
65 let runtime = agtrace_runtime::AgTrace::connect_or_create(path)
66 .await
67 .map_err(Error::Runtime)?;
68 Ok(Client {
69 inner: Arc::new(runtime),
70 })
71 }
72
73 fn resolve_path(&self) -> Result<PathBuf> {
78 let explicit_path = self.path.as_ref().and_then(|p| p.to_str());
79 agtrace_runtime::resolve_workspace_path(explicit_path).map_err(Error::Runtime)
80 }
81}
82
83#[derive(Clone)]
89pub struct Client {
90 inner: Arc<agtrace_runtime::AgTrace>,
91}
92
93impl Client {
94 pub fn builder() -> ClientBuilder {
116 ClientBuilder::new()
117 }
118
119 pub async fn connect_default() -> Result<Self> {
137 Self::builder().connect().await
138 }
139
140 pub async fn connect(path: impl Into<PathBuf>) -> Result<Self> {
156 Self::builder().path(path).connect().await
157 }
158
159 pub fn sessions(&self) -> SessionClient {
161 SessionClient {
162 inner: self.inner.clone(),
163 }
164 }
165
166 pub fn projects(&self) -> ProjectClient {
168 ProjectClient {
169 inner: self.inner.clone(),
170 }
171 }
172
173 pub fn watch(&self) -> WatchClient {
175 WatchClient {
176 inner: self.inner.clone(),
177 }
178 }
179
180 pub fn insights(&self) -> InsightClient {
182 InsightClient {
183 inner: self.inner.clone(),
184 }
185 }
186
187 pub fn system(&self) -> SystemClient {
189 SystemClient {
190 inner: self.inner.clone(),
191 }
192 }
193
194 pub fn watch_service(&self) -> crate::types::WatchService {
197 self.inner.watch_service()
198 }
199
200 pub fn providers(&self) -> Providers {
225 Providers::with_config(self.inner.config().clone())
226 }
227}
228
229pub struct SessionClient {
235 inner: Arc<agtrace_runtime::AgTrace>,
236}
237
238impl SessionClient {
239 pub fn list(&self, filter: SessionFilter) -> Result<Vec<SessionSummary>> {
241 self.inner.sessions().list(filter).map_err(Error::Runtime)
242 }
243
244 pub fn list_without_refresh(&self, filter: SessionFilter) -> Result<Vec<SessionSummary>> {
246 self.inner
247 .sessions()
248 .list_without_refresh(filter)
249 .map_err(Error::Runtime)
250 }
251
252 pub fn pack_context(
254 &self,
255 project_hash: Option<&crate::types::ProjectHash>,
256 limit: usize,
257 ) -> Result<crate::types::PackResult> {
258 self.inner
259 .sessions()
260 .pack_context(project_hash, limit)
261 .map_err(Error::Runtime)
262 }
263
264 pub fn get(&self, id_or_prefix: &str) -> Result<SessionHandle> {
266 self.inner
268 .sessions()
269 .find(id_or_prefix)
270 .map_err(|e| Error::NotFound(format!("Session {}: {}", id_or_prefix, e)))?;
271
272 Ok(SessionHandle {
273 source: SessionSource::Workspace {
274 inner: self.inner.clone(),
275 id: id_or_prefix.to_string(),
276 },
277 })
278 }
279
280 pub fn search_events(&self, args: SearchEventsArgs) -> Result<SearchEventsResponse> {
286 let limit = args.limit();
287 let offset = args
288 .cursor
289 .as_ref()
290 .and_then(|c| Cursor::decode(c))
291 .map(|c| c.offset)
292 .unwrap_or(0);
293
294 let project_hash_filter = if let Some(ref root) = args.project_root {
295 Some(crate::utils::project_hash_from_root(root))
296 } else {
297 args.project_hash.clone().map(|h| h.into())
298 };
299
300 let mut filter = if let Some(hash) = project_hash_filter {
301 SessionFilter::project(hash).limit(1000)
302 } else {
303 SessionFilter::all().limit(1000)
304 };
305
306 if let Some(ref provider) = args.provider {
307 filter = filter.provider(provider.as_str().to_string());
308 }
309
310 let sessions = if let Some(ref session_id) = args.session_id {
311 let _handle = self.get(session_id)?;
312
313 vec![SessionSummary {
314 id: session_id.clone(),
315 provider: String::new(),
316 project_hash: ProjectHash::from(String::new()),
317 repository_hash: None,
318 project_root: None,
319 start_ts: None,
320 snippet: None,
321 parent_session_id: None,
322 spawned_by: None,
323 }]
324 } else {
325 self.list_without_refresh(filter)?
326 };
327
328 let mut all_matches = Vec::new();
329
330 for session_summary in sessions {
331 let handle = match self.get(&session_summary.id) {
332 Ok(h) => h,
333 Err(_) => continue,
334 };
335
336 let session = match handle.assemble() {
337 Ok(s) => s,
338 Err(_) => continue,
339 };
340
341 let events = match handle.events() {
342 Ok(e) => e,
343 Err(_) => continue,
344 };
345
346 for (event_index, event) in events.iter().enumerate() {
347 if let Some(ref event_type_filter) = args.event_type
348 && !event_type_filter.matches_payload(&event.payload)
349 {
350 continue;
351 }
352
353 let event_json = match serde_json::to_string(&event.payload) {
354 Ok(j) => j,
355 Err(_) => continue,
356 };
357
358 if event_json.contains(&args.query) {
359 let (turn_index, step_index) = Self::find_event_location(&session, event_index);
360
361 let event_match = EventMatch::new(
362 session_summary.id.clone(),
363 event_index,
364 turn_index,
365 step_index,
366 event,
367 );
368 all_matches.push(event_match);
369 }
370 }
371 }
372
373 let fetch_limit = limit + 1;
374 let mut matches: Vec<_> = all_matches
375 .into_iter()
376 .skip(offset)
377 .take(fetch_limit)
378 .collect();
379
380 let has_more = matches.len() > limit;
381 if has_more {
382 matches.pop();
383 }
384
385 let next_cursor = if has_more {
386 Some(
387 Cursor {
388 offset: offset + limit,
389 }
390 .encode(),
391 )
392 } else {
393 None
394 };
395
396 Ok(SearchEventsResponse {
397 matches,
398 next_cursor,
399 })
400 }
401
402 pub fn list_turns(&self, args: ListTurnsArgs) -> Result<ListTurnsResponse> {
404 let handle = self.get(&args.session_id)?;
405
406 let session = handle.assemble()?;
407
408 let limit = args.limit();
409 let offset = args
410 .cursor
411 .as_ref()
412 .and_then(|c| Cursor::decode(c))
413 .map(|c| c.offset)
414 .unwrap_or(0);
415
416 let total_turns = session.turns.len();
417 let remaining = total_turns.saturating_sub(offset);
418 let has_more = remaining > limit;
419
420 let next_cursor = if has_more {
421 Some(
422 Cursor {
423 offset: offset + limit,
424 }
425 .encode(),
426 )
427 } else {
428 None
429 };
430
431 Ok(ListTurnsResponse::new(session, offset, limit, next_cursor))
432 }
433
434 pub fn get_turns(&self, args: GetTurnsArgs) -> Result<GetTurnsResponse> {
436 let handle = self.get(&args.session_id)?;
437
438 let session = handle.assemble()?;
439
440 GetTurnsResponse::new(session, &args).map_err(Error::InvalidInput)
441 }
442
443 fn find_event_location(session: &AgentSession, event_index: usize) -> (usize, usize) {
444 let mut current_event_idx = 0;
445
446 for (turn_idx, turn) in session.turns.iter().enumerate() {
447 for (step_idx, step) in turn.steps.iter().enumerate() {
448 let step_event_count = Self::count_step_events(step);
449
450 if current_event_idx + step_event_count > event_index {
451 return (turn_idx, step_idx);
452 }
453
454 current_event_idx += step_event_count;
455 }
456 }
457
458 (0, 0)
459 }
460
461 fn count_step_events(step: &AgentStep) -> usize {
462 let mut count = 0;
463
464 if step.reasoning.is_some() {
465 count += 1;
466 }
467
468 count += step.tools.len() * 2;
469
470 if step.message.is_some() {
471 count += 1;
472 }
473
474 count
475 }
476}
477
478pub struct SessionHandle {
484 source: SessionSource,
485}
486
487enum SessionSource {
488 Workspace {
490 inner: Arc<agtrace_runtime::AgTrace>,
491 id: String,
492 },
493 Events {
495 events: Vec<crate::types::AgentEvent>,
496 },
497}
498
499impl SessionHandle {
500 pub fn from_events(events: Vec<AgentEvent>) -> Self {
519 Self {
520 source: SessionSource::Events { events },
521 }
522 }
523
524 pub fn events(&self) -> Result<Vec<AgentEvent>> {
526 match &self.source {
527 SessionSource::Workspace { inner, id } => {
528 let session_handle = inner
529 .sessions()
530 .find(id)
531 .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
532
533 session_handle.events().map_err(Error::Runtime)
534 }
535 SessionSource::Events { events } => Ok(events.clone()),
536 }
537 }
538
539 pub fn assemble(&self) -> Result<AgentSession> {
544 let events = self.events()?;
545 agtrace_engine::assemble_session(&events).ok_or_else(|| {
546 Error::InvalidInput(
547 "Failed to assemble session: insufficient or invalid events".to_string(),
548 )
549 })
550 }
551
552 pub fn assemble_all(&self) -> Result<Vec<AgentSession>> {
557 let events = self.events()?;
558 let sessions = agtrace_engine::assemble_sessions(&events);
559 if sessions.is_empty() {
560 return Err(Error::InvalidInput(
561 "Failed to assemble session: insufficient or invalid events".to_string(),
562 ));
563 }
564 Ok(sessions)
565 }
566
567 pub fn export(&self, strategy: ExportStrategy) -> Result<Vec<AgentEvent>> {
569 let events = self.events()?;
570 Ok(agtrace_engine::export::transform(&events, strategy))
571 }
572
573 pub fn metadata(&self) -> Result<Option<crate::types::SessionMetadata>> {
577 match &self.source {
578 SessionSource::Workspace { inner, id } => {
579 let runtime_handle = inner
580 .sessions()
581 .find(id)
582 .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
583
584 runtime_handle.metadata().map(Some).map_err(Error::Runtime)
585 }
586 SessionSource::Events { .. } => Ok(None),
587 }
588 }
589
590 pub fn raw_files(&self) -> Result<Vec<crate::types::RawFileContent>> {
595 match &self.source {
596 SessionSource::Workspace { inner, id } => {
597 let runtime_handle = inner
598 .sessions()
599 .find(id)
600 .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
601
602 runtime_handle.raw_files().map_err(Error::Runtime)
603 }
604 SessionSource::Events { .. } => Ok(vec![]),
605 }
606 }
607
608 pub fn summarize(&self) -> Result<agtrace_engine::SessionSummary> {
610 let session = self.assemble()?;
611 Ok(agtrace_engine::session::summarize(&session))
612 }
613
614 pub fn analyze(&self) -> Result<crate::analysis::SessionAnalyzer> {
616 let session = self.assemble()?;
617 Ok(crate::analysis::SessionAnalyzer::new(session))
618 }
619
620 pub fn child_sessions(&self) -> Result<Vec<ChildSessionInfo>> {
625 match &self.source {
626 SessionSource::Workspace { inner, id } => {
627 let runtime_handle = inner
628 .sessions()
629 .find(id)
630 .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
631
632 let children = runtime_handle.child_sessions().map_err(Error::Runtime)?;
633 Ok(children
634 .into_iter()
635 .map(|c| ChildSessionInfo {
636 session_id: c.id,
637 provider: c.provider,
638 spawned_by: c.spawned_by,
639 snippet: c.snippet,
640 })
641 .collect())
642 }
643 SessionSource::Events { .. } => Ok(vec![]),
644 }
645 }
646}
647
648#[derive(Debug, Clone)]
650pub struct ChildSessionInfo {
651 pub session_id: String,
652 pub provider: String,
653 pub spawned_by: Option<agtrace_types::SpawnContext>,
654 pub snippet: Option<String>,
655}
656
657pub struct ProjectClient {
663 inner: Arc<agtrace_runtime::AgTrace>,
664}
665
666impl ProjectClient {
667 pub fn list(&self) -> Result<Vec<ProjectInfo>> {
669 self.inner.projects().list().map_err(Error::Runtime)
670 }
671}
672
673pub struct WatchClient {
679 inner: Arc<agtrace_runtime::AgTrace>,
680}
681
682impl WatchClient {
683 pub fn builder(&self) -> WatchBuilder {
685 WatchBuilder::new(self.inner.clone())
686 }
687
688 pub fn all_providers(&self) -> WatchBuilder {
690 WatchBuilder::new(self.inner.clone()).all_providers()
691 }
692
693 pub fn provider(&self, name: &str) -> WatchBuilder {
695 WatchBuilder::new(self.inner.clone()).provider(name)
696 }
697
698 pub fn session(&self, _id: &str) -> WatchBuilder {
700 WatchBuilder::new(self.inner.clone())
702 }
703}
704
705pub struct InsightClient {
711 inner: Arc<agtrace_runtime::AgTrace>,
712}
713
714impl InsightClient {
715 pub fn corpus_stats(
717 &self,
718 project_hash: Option<&agtrace_types::ProjectHash>,
719 limit: usize,
720 ) -> Result<CorpusStats> {
721 self.inner
722 .insights()
723 .corpus_stats(project_hash, limit)
724 .map_err(Error::Runtime)
725 }
726
727 pub fn tool_usage(
729 &self,
730 limit: Option<usize>,
731 provider: Option<String>,
732 ) -> Result<agtrace_runtime::StatsResult> {
733 self.inner
734 .insights()
735 .tool_usage(limit, provider)
736 .map_err(Error::Runtime)
737 }
738
739 pub fn pack(&self, _limit: usize) -> Result<PackResult> {
741 Err(Error::InvalidInput(
743 "Pack operation not yet implemented in runtime".to_string(),
744 ))
745 }
746
747 pub fn grep(
749 &self,
750 _pattern: &str,
751 _filter: &SessionFilter,
752 _limit: usize,
753 ) -> Result<Vec<AgentEvent>> {
754 Err(Error::InvalidInput(
756 "Grep operation not yet implemented in runtime".to_string(),
757 ))
758 }
759}
760
761pub struct SystemClient {
767 inner: Arc<agtrace_runtime::AgTrace>,
768}
769
770impl SystemClient {
771 pub fn initialize<F>(config: InitConfig, on_progress: Option<F>) -> Result<InitResult>
773 where
774 F: FnMut(InitProgress),
775 {
776 agtrace_runtime::AgTrace::setup(config, on_progress).map_err(Error::Runtime)
777 }
778
779 pub fn diagnose(&self) -> Result<Vec<DiagnoseResult>> {
781 self.inner.diagnose().map_err(Error::Runtime)
782 }
783
784 pub fn check_file(&self, path: &Path, provider: Option<&str>) -> Result<CheckResult> {
786 let path_str = path
787 .to_str()
788 .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
789
790 let (adapter, provider_name) = if let Some(name) = provider {
792 let adapter = agtrace_providers::create_adapter(name)
793 .map_err(|_| Error::NotFound(format!("Provider: {}", name)))?;
794 (adapter, name.to_string())
795 } else {
796 let adapter = agtrace_providers::detect_adapter_from_path(path_str)
797 .map_err(|_| Error::NotFound("No suitable provider detected".to_string()))?;
798 let name = format!("{} (auto-detected)", adapter.id());
799 (adapter, name)
800 };
801
802 agtrace_runtime::AgTrace::check_file(path_str, &adapter, &provider_name)
803 .map_err(Error::Runtime)
804 }
805
806 pub fn inspect_file(path: &Path, lines: usize, json_format: bool) -> Result<InspectResult> {
808 let path_str = path
809 .to_str()
810 .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
811
812 agtrace_runtime::AgTrace::inspect_file(path_str, lines, json_format).map_err(Error::Runtime)
813 }
814
815 pub fn reindex<F>(
817 &self,
818 scope: agtrace_types::ProjectScope,
819 force: bool,
820 provider_filter: Option<&str>,
821 on_progress: F,
822 ) -> Result<()>
823 where
824 F: FnMut(IndexProgress),
825 {
826 self.inner
827 .projects()
828 .scan(scope, force, provider_filter, on_progress)
829 .map(|_| ()) .map_err(Error::Runtime)
831 }
832
833 pub fn vacuum(&self) -> Result<()> {
835 let db = self.inner.database();
836 let db = db.lock().unwrap();
837 db.vacuum().map_err(|e| Error::Runtime(e.into()))
838 }
839
840 pub fn list_providers(&self) -> Result<Vec<ProviderConfig>> {
842 Ok(self.inner.config().providers.values().cloned().collect())
843 }
844
845 pub fn detect_providers() -> Result<Config> {
847 agtrace_runtime::Config::detect_providers().map_err(Error::Runtime)
848 }
849
850 pub fn config(&self) -> Config {
852 self.inner.config().clone()
853 }
854}