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 project_root: None,
318 start_ts: None,
319 snippet: None,
320 parent_session_id: None,
321 spawned_by: None,
322 }]
323 } else {
324 self.list_without_refresh(filter)?
325 };
326
327 let mut all_matches = Vec::new();
328
329 for session_summary in sessions {
330 let handle = match self.get(&session_summary.id) {
331 Ok(h) => h,
332 Err(_) => continue,
333 };
334
335 let session = match handle.assemble() {
336 Ok(s) => s,
337 Err(_) => continue,
338 };
339
340 let events = match handle.events() {
341 Ok(e) => e,
342 Err(_) => continue,
343 };
344
345 for (event_index, event) in events.iter().enumerate() {
346 if let Some(ref event_type_filter) = args.event_type
347 && !event_type_filter.matches_payload(&event.payload)
348 {
349 continue;
350 }
351
352 let event_json = match serde_json::to_string(&event.payload) {
353 Ok(j) => j,
354 Err(_) => continue,
355 };
356
357 if event_json.contains(&args.query) {
358 let (turn_index, step_index) = Self::find_event_location(&session, event_index);
359
360 let event_match = EventMatch::new(
361 session_summary.id.clone(),
362 event_index,
363 turn_index,
364 step_index,
365 event,
366 );
367 all_matches.push(event_match);
368 }
369 }
370 }
371
372 let fetch_limit = limit + 1;
373 let mut matches: Vec<_> = all_matches
374 .into_iter()
375 .skip(offset)
376 .take(fetch_limit)
377 .collect();
378
379 let has_more = matches.len() > limit;
380 if has_more {
381 matches.pop();
382 }
383
384 let next_cursor = if has_more {
385 Some(
386 Cursor {
387 offset: offset + limit,
388 }
389 .encode(),
390 )
391 } else {
392 None
393 };
394
395 Ok(SearchEventsResponse {
396 matches,
397 next_cursor,
398 })
399 }
400
401 pub fn list_turns(&self, args: ListTurnsArgs) -> Result<ListTurnsResponse> {
403 let handle = self.get(&args.session_id)?;
404
405 let session = handle.assemble()?;
406
407 let limit = args.limit();
408 let offset = args
409 .cursor
410 .as_ref()
411 .and_then(|c| Cursor::decode(c))
412 .map(|c| c.offset)
413 .unwrap_or(0);
414
415 let total_turns = session.turns.len();
416 let remaining = total_turns.saturating_sub(offset);
417 let has_more = remaining > limit;
418
419 let next_cursor = if has_more {
420 Some(
421 Cursor {
422 offset: offset + limit,
423 }
424 .encode(),
425 )
426 } else {
427 None
428 };
429
430 Ok(ListTurnsResponse::new(session, offset, limit, next_cursor))
431 }
432
433 pub fn get_turns(&self, args: GetTurnsArgs) -> Result<GetTurnsResponse> {
435 let handle = self.get(&args.session_id)?;
436
437 let session = handle.assemble()?;
438
439 GetTurnsResponse::new(session, &args).map_err(Error::InvalidInput)
440 }
441
442 fn find_event_location(session: &AgentSession, event_index: usize) -> (usize, usize) {
443 let mut current_event_idx = 0;
444
445 for (turn_idx, turn) in session.turns.iter().enumerate() {
446 for (step_idx, step) in turn.steps.iter().enumerate() {
447 let step_event_count = Self::count_step_events(step);
448
449 if current_event_idx + step_event_count > event_index {
450 return (turn_idx, step_idx);
451 }
452
453 current_event_idx += step_event_count;
454 }
455 }
456
457 (0, 0)
458 }
459
460 fn count_step_events(step: &AgentStep) -> usize {
461 let mut count = 0;
462
463 if step.reasoning.is_some() {
464 count += 1;
465 }
466
467 count += step.tools.len() * 2;
468
469 if step.message.is_some() {
470 count += 1;
471 }
472
473 count
474 }
475}
476
477pub struct SessionHandle {
483 source: SessionSource,
484}
485
486enum SessionSource {
487 Workspace {
489 inner: Arc<agtrace_runtime::AgTrace>,
490 id: String,
491 },
492 Events {
494 events: Vec<crate::types::AgentEvent>,
495 },
496}
497
498impl SessionHandle {
499 pub fn from_events(events: Vec<AgentEvent>) -> Self {
518 Self {
519 source: SessionSource::Events { events },
520 }
521 }
522
523 pub fn events(&self) -> Result<Vec<AgentEvent>> {
525 match &self.source {
526 SessionSource::Workspace { inner, id } => {
527 let session_handle = inner
528 .sessions()
529 .find(id)
530 .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
531
532 session_handle.events().map_err(Error::Runtime)
533 }
534 SessionSource::Events { events } => Ok(events.clone()),
535 }
536 }
537
538 pub fn assemble(&self) -> Result<AgentSession> {
543 let events = self.events()?;
544 agtrace_engine::assemble_session(&events).ok_or_else(|| {
545 Error::InvalidInput(
546 "Failed to assemble session: insufficient or invalid events".to_string(),
547 )
548 })
549 }
550
551 pub fn assemble_all(&self) -> Result<Vec<AgentSession>> {
556 let events = self.events()?;
557 let sessions = agtrace_engine::assemble_sessions(&events);
558 if sessions.is_empty() {
559 return Err(Error::InvalidInput(
560 "Failed to assemble session: insufficient or invalid events".to_string(),
561 ));
562 }
563 Ok(sessions)
564 }
565
566 pub fn export(&self, strategy: ExportStrategy) -> Result<Vec<AgentEvent>> {
568 let events = self.events()?;
569 Ok(agtrace_engine::export::transform(&events, strategy))
570 }
571
572 pub fn metadata(&self) -> Result<Option<crate::types::SessionMetadata>> {
576 match &self.source {
577 SessionSource::Workspace { inner, id } => {
578 let runtime_handle = inner
579 .sessions()
580 .find(id)
581 .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
582
583 runtime_handle.metadata().map(Some).map_err(Error::Runtime)
584 }
585 SessionSource::Events { .. } => Ok(None),
586 }
587 }
588
589 pub fn raw_files(&self) -> Result<Vec<crate::types::RawFileContent>> {
594 match &self.source {
595 SessionSource::Workspace { inner, id } => {
596 let runtime_handle = inner
597 .sessions()
598 .find(id)
599 .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
600
601 runtime_handle.raw_files().map_err(Error::Runtime)
602 }
603 SessionSource::Events { .. } => Ok(vec![]),
604 }
605 }
606
607 pub fn summarize(&self) -> Result<agtrace_engine::SessionSummary> {
609 let session = self.assemble()?;
610 Ok(agtrace_engine::session::summarize(&session))
611 }
612
613 pub fn analyze(&self) -> Result<crate::analysis::SessionAnalyzer> {
615 let session = self.assemble()?;
616 Ok(crate::analysis::SessionAnalyzer::new(session))
617 }
618
619 pub fn child_sessions(&self) -> Result<Vec<ChildSessionInfo>> {
624 match &self.source {
625 SessionSource::Workspace { inner, id } => {
626 let runtime_handle = inner
627 .sessions()
628 .find(id)
629 .map_err(|e| Error::NotFound(format!("Session {}: {}", id, e)))?;
630
631 let children = runtime_handle.child_sessions().map_err(Error::Runtime)?;
632 Ok(children
633 .into_iter()
634 .map(|c| ChildSessionInfo {
635 session_id: c.id,
636 provider: c.provider,
637 spawned_by: c.spawned_by,
638 snippet: c.snippet,
639 })
640 .collect())
641 }
642 SessionSource::Events { .. } => Ok(vec![]),
643 }
644 }
645}
646
647#[derive(Debug, Clone)]
649pub struct ChildSessionInfo {
650 pub session_id: String,
651 pub provider: String,
652 pub spawned_by: Option<agtrace_types::SpawnContext>,
653 pub snippet: Option<String>,
654}
655
656pub struct ProjectClient {
662 inner: Arc<agtrace_runtime::AgTrace>,
663}
664
665impl ProjectClient {
666 pub fn list(&self) -> Result<Vec<ProjectInfo>> {
668 self.inner.projects().list().map_err(Error::Runtime)
669 }
670}
671
672pub struct WatchClient {
678 inner: Arc<agtrace_runtime::AgTrace>,
679}
680
681impl WatchClient {
682 pub fn builder(&self) -> WatchBuilder {
684 WatchBuilder::new(self.inner.clone())
685 }
686
687 pub fn all_providers(&self) -> WatchBuilder {
689 WatchBuilder::new(self.inner.clone()).all_providers()
690 }
691
692 pub fn provider(&self, name: &str) -> WatchBuilder {
694 WatchBuilder::new(self.inner.clone()).provider(name)
695 }
696
697 pub fn session(&self, _id: &str) -> WatchBuilder {
699 WatchBuilder::new(self.inner.clone())
701 }
702}
703
704pub struct InsightClient {
710 inner: Arc<agtrace_runtime::AgTrace>,
711}
712
713impl InsightClient {
714 pub fn corpus_stats(
716 &self,
717 project_hash: Option<&agtrace_types::ProjectHash>,
718 limit: usize,
719 ) -> Result<CorpusStats> {
720 self.inner
721 .insights()
722 .corpus_stats(project_hash, limit)
723 .map_err(Error::Runtime)
724 }
725
726 pub fn tool_usage(
728 &self,
729 limit: Option<usize>,
730 provider: Option<String>,
731 ) -> Result<agtrace_runtime::StatsResult> {
732 self.inner
733 .insights()
734 .tool_usage(limit, provider)
735 .map_err(Error::Runtime)
736 }
737
738 pub fn pack(&self, _limit: usize) -> Result<PackResult> {
740 Err(Error::InvalidInput(
742 "Pack operation not yet implemented in runtime".to_string(),
743 ))
744 }
745
746 pub fn grep(
748 &self,
749 _pattern: &str,
750 _filter: &SessionFilter,
751 _limit: usize,
752 ) -> Result<Vec<AgentEvent>> {
753 Err(Error::InvalidInput(
755 "Grep operation not yet implemented in runtime".to_string(),
756 ))
757 }
758}
759
760pub struct SystemClient {
766 inner: Arc<agtrace_runtime::AgTrace>,
767}
768
769impl SystemClient {
770 pub fn initialize<F>(config: InitConfig, on_progress: Option<F>) -> Result<InitResult>
772 where
773 F: FnMut(InitProgress),
774 {
775 agtrace_runtime::AgTrace::setup(config, on_progress).map_err(Error::Runtime)
776 }
777
778 pub fn diagnose(&self) -> Result<Vec<DiagnoseResult>> {
780 self.inner.diagnose().map_err(Error::Runtime)
781 }
782
783 pub fn check_file(&self, path: &Path, provider: Option<&str>) -> Result<CheckResult> {
785 let path_str = path
786 .to_str()
787 .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
788
789 let (adapter, provider_name) = if let Some(name) = provider {
791 let adapter = agtrace_providers::create_adapter(name)
792 .map_err(|_| Error::NotFound(format!("Provider: {}", name)))?;
793 (adapter, name.to_string())
794 } else {
795 let adapter = agtrace_providers::detect_adapter_from_path(path_str)
796 .map_err(|_| Error::NotFound("No suitable provider detected".to_string()))?;
797 let name = format!("{} (auto-detected)", adapter.id());
798 (adapter, name)
799 };
800
801 agtrace_runtime::AgTrace::check_file(path_str, &adapter, &provider_name)
802 .map_err(Error::Runtime)
803 }
804
805 pub fn inspect_file(path: &Path, lines: usize, json_format: bool) -> Result<InspectResult> {
807 let path_str = path
808 .to_str()
809 .ok_or_else(|| Error::InvalidInput("Path contains invalid UTF-8".to_string()))?;
810
811 agtrace_runtime::AgTrace::inspect_file(path_str, lines, json_format).map_err(Error::Runtime)
812 }
813
814 pub fn reindex<F>(
816 &self,
817 scope: agtrace_types::ProjectScope,
818 force: bool,
819 provider_filter: Option<&str>,
820 on_progress: F,
821 ) -> Result<()>
822 where
823 F: FnMut(IndexProgress),
824 {
825 self.inner
826 .projects()
827 .scan(scope, force, provider_filter, on_progress)
828 .map(|_| ()) .map_err(Error::Runtime)
830 }
831
832 pub fn vacuum(&self) -> Result<()> {
834 let db = self.inner.database();
835 let db = db.lock().unwrap();
836 db.vacuum().map_err(|e| Error::Runtime(e.into()))
837 }
838
839 pub fn list_providers(&self) -> Result<Vec<ProviderConfig>> {
841 Ok(self.inner.config().providers.values().cloned().collect())
842 }
843
844 pub fn detect_providers() -> Result<Config> {
846 agtrace_runtime::Config::detect_providers().map_err(Error::Runtime)
847 }
848
849 pub fn config(&self) -> Config {
851 self.inner.config().clone()
852 }
853}