agtrace_runtime/client/
sessions.rs

1use crate::ops::{
2    ExportService, IndexProgress, IndexService, ListSessionsRequest, PackResult, PackService,
3    SessionService,
4};
5use crate::storage::{RawFileContent, get_raw_files};
6use crate::{Error, Result};
7use agtrace_engine::export::ExportStrategy;
8use agtrace_index::{Database, SessionSummary};
9use agtrace_providers::ProviderAdapter;
10use agtrace_types::AgentEvent;
11use std::path::PathBuf;
12use std::sync::{Arc, Mutex};
13
14#[derive(Debug, Clone)]
15pub struct SessionFilter {
16    pub scope: agtrace_types::ProjectScope,
17    pub limit: Option<usize>,
18    pub provider: Option<String>,
19    pub order: agtrace_types::SessionOrder,
20    pub since: Option<String>,
21    pub until: Option<String>,
22}
23
24impl SessionFilter {
25    /// Create a filter for all projects
26    pub fn all() -> Self {
27        Self {
28            scope: agtrace_types::ProjectScope::All,
29            limit: None,
30            provider: None,
31            order: agtrace_types::SessionOrder::default(),
32            since: None,
33            until: None,
34        }
35    }
36
37    /// Create a filter for a specific project
38    pub fn project(project_hash: agtrace_types::ProjectHash) -> Self {
39        Self {
40            scope: agtrace_types::ProjectScope::Specific(project_hash),
41            limit: None,
42            provider: None,
43            order: agtrace_types::SessionOrder::default(),
44            since: None,
45            until: None,
46        }
47    }
48
49    pub fn limit(mut self, limit: usize) -> Self {
50        self.limit = Some(limit);
51        self
52    }
53
54    pub fn provider(mut self, provider: String) -> Self {
55        self.provider = Some(provider);
56        self
57    }
58
59    pub fn order(mut self, order: agtrace_types::SessionOrder) -> Self {
60        self.order = order;
61        self
62    }
63
64    pub fn since(mut self, since: String) -> Self {
65        self.since = Some(since);
66        self
67    }
68
69    pub fn until(mut self, until: String) -> Self {
70        self.until = Some(until);
71        self
72    }
73}
74
75pub struct SessionOps {
76    db: Arc<Mutex<Database>>,
77    provider_configs: Arc<Vec<(String, PathBuf)>>,
78}
79
80impl SessionOps {
81    pub fn new(db: Arc<Mutex<Database>>, provider_configs: Arc<Vec<(String, PathBuf)>>) -> Self {
82        Self {
83            db,
84            provider_configs,
85        }
86    }
87
88    pub fn list(&self, filter: SessionFilter) -> Result<Vec<SessionSummary>> {
89        self.ensure_index_is_fresh()?;
90        self.list_without_refresh(filter)
91    }
92
93    pub fn list_without_refresh(&self, filter: SessionFilter) -> Result<Vec<SessionSummary>> {
94        let db = self.db.lock().unwrap();
95        let service = SessionService::new(&db);
96        let request = ListSessionsRequest {
97            scope: filter.scope,
98            limit: filter.limit,
99            provider: filter.provider,
100            order: filter.order,
101            since: filter.since,
102            until: filter.until,
103        };
104        service.list_sessions(request)
105    }
106
107    fn ensure_index_is_fresh(&self) -> Result<()> {
108        let db = self.db.lock().unwrap();
109
110        let providers: Vec<(ProviderAdapter, PathBuf)> = self
111            .provider_configs
112            .iter()
113            .filter_map(|(name, path)| {
114                agtrace_providers::create_adapter(name)
115                    .ok()
116                    .map(|p| (p, path.clone()))
117            })
118            .collect();
119
120        let service = IndexService::new(&db, providers);
121
122        // Scan all projects without filtering
123        let scope = agtrace_types::ProjectScope::All;
124
125        service.run(scope, false, |_progress: IndexProgress| {})?;
126
127        Ok(())
128    }
129
130    pub fn find(&self, session_id: &str) -> Result<SessionHandle> {
131        if let Some(resolved_id) = self.resolve_session_id(session_id)? {
132            return Ok(SessionHandle {
133                id: resolved_id,
134                db: self.db.clone(),
135            });
136        }
137
138        self.ensure_index_is_fresh()?;
139
140        if let Some(resolved_id) = self.resolve_session_id(session_id)? {
141            return Ok(SessionHandle {
142                id: resolved_id,
143                db: self.db.clone(),
144            });
145        }
146
147        Err(Error::InvalidOperation(format!(
148            "Session not found: {}",
149            session_id
150        )))
151    }
152
153    fn resolve_session_id(&self, session_id: &str) -> Result<Option<String>> {
154        let db = self.db.lock().unwrap();
155
156        if let Some(session) = db.get_session_by_id(session_id)? {
157            return Ok(Some(session.id));
158        }
159
160        Ok(db.find_session_by_prefix(session_id)?)
161    }
162
163    pub fn pack_context(
164        &self,
165        project_hash: Option<&agtrace_types::ProjectHash>,
166        limit: usize,
167    ) -> Result<PackResult> {
168        self.ensure_index_is_fresh()?;
169
170        let db = self.db.lock().unwrap();
171        let service = PackService::new(&db);
172        service.select_sessions(project_hash, limit)
173    }
174}
175
176pub struct SessionHandle {
177    id: String,
178    db: Arc<Mutex<Database>>,
179}
180
181impl SessionHandle {
182    pub fn events(&self) -> Result<Vec<AgentEvent>> {
183        let db = self.db.lock().unwrap();
184        let service = SessionService::new(&db);
185        service.get_session_events(&self.id)
186    }
187
188    pub fn raw_files(&self) -> Result<Vec<RawFileContent>> {
189        let db = self.db.lock().unwrap();
190        get_raw_files(&db, &self.id)
191    }
192
193    pub fn export(&self, strategy: ExportStrategy) -> Result<Vec<AgentEvent>> {
194        let db = self.db.lock().unwrap();
195        let service = ExportService::new(&db);
196        service.export_session(&self.id, strategy)
197    }
198
199    pub fn metadata(&self) -> Result<agtrace_types::SessionMetadata> {
200        let db = self.db.lock().unwrap();
201        let index_summary = db.get_session_by_id(&self.id)?.ok_or_else(|| {
202            Error::InvalidOperation(format!("Session metadata not found: {}", self.id))
203        })?;
204
205        // Resolve project_root from project_hash
206        let project_root = db
207            .get_project(index_summary.project_hash.as_str())?
208            .and_then(|p| p.root_path);
209
210        Ok(agtrace_types::SessionMetadata {
211            session_id: index_summary.id.clone(),
212            project_hash: index_summary.project_hash,
213            project_root,
214            provider: index_summary.provider,
215        })
216    }
217
218    pub fn id(&self) -> &str {
219        &self.id
220    }
221}