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