1use crate::cursor::{TraversalCursor, ViewMode};
4use crate::error::{AgentError, AgentSessionId, Result};
5use crate::metrics::SessionMetrics;
6use crate::safety::{BudgetTracker, SessionLimits};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashSet;
10use ucm_core::{BlockId, EdgeType};
11use ucp_codegraph::CodeGraphContextSession;
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16#[derive(Default)]
17pub enum SessionState {
18 #[default]
20 Active,
21 Paused,
23 Completed,
25 TimedOut,
27 Error { reason: String },
29}
30
31#[derive(Debug, Clone)]
33pub struct AgentCapabilities {
34 pub can_traverse: bool,
36 pub can_search: bool,
38 pub can_modify_context: bool,
40 pub can_coordinate: bool,
42 pub allowed_edge_types: HashSet<EdgeType>,
44 pub max_expand_depth: usize,
46}
47
48impl Default for AgentCapabilities {
49 fn default() -> Self {
50 Self {
51 can_traverse: true,
52 can_search: true,
53 can_modify_context: true,
54 can_coordinate: true,
55 allowed_edge_types: HashSet::new(), max_expand_depth: 10,
57 }
58 }
59}
60
61impl AgentCapabilities {
62 pub fn is_edge_allowed(&self, edge_type: &EdgeType) -> bool {
64 self.allowed_edge_types.is_empty() || self.allowed_edge_types.contains(edge_type)
65 }
66
67 pub fn full() -> Self {
69 Self::default()
70 }
71
72 pub fn read_only() -> Self {
74 Self {
75 can_traverse: true,
76 can_search: true,
77 can_modify_context: false,
78 can_coordinate: false,
79 ..Default::default()
80 }
81 }
82}
83
84#[derive(Debug, Clone, Default)]
86pub struct SessionConfig {
87 pub name: Option<String>,
89 pub start_block: Option<BlockId>,
91 pub limits: SessionLimits,
93 pub capabilities: AgentCapabilities,
95 pub view_mode: ViewMode,
97}
98
99impl SessionConfig {
100 pub fn new() -> Self {
101 Self::default()
102 }
103
104 pub fn with_name(mut self, name: &str) -> Self {
105 self.name = Some(name.to_string());
106 self
107 }
108
109 pub fn with_start_block(mut self, block: BlockId) -> Self {
110 self.start_block = Some(block);
111 self
112 }
113
114 pub fn with_limits(mut self, limits: SessionLimits) -> Self {
115 self.limits = limits;
116 self
117 }
118
119 pub fn with_capabilities(mut self, capabilities: AgentCapabilities) -> Self {
120 self.capabilities = capabilities;
121 self
122 }
123
124 pub fn with_view_mode(mut self, mode: ViewMode) -> Self {
125 self.view_mode = mode;
126 self
127 }
128}
129
130pub struct AgentSession {
132 pub id: AgentSessionId,
134 pub name: Option<String>,
136 pub cursor: TraversalCursor,
138 pub capabilities: AgentCapabilities,
140 pub limits: SessionLimits,
142 pub budget: BudgetTracker,
144 pub metrics: SessionMetrics,
146 pub state: SessionState,
148 pub created_at: DateTime<Utc>,
150 pub last_active: DateTime<Utc>,
152 pub last_results: Vec<BlockId>,
154 pub context_blocks: HashSet<BlockId>,
156 pub focus_block: Option<BlockId>,
158 pub codegraph_context: Option<CodeGraphContextSession>,
160}
161
162impl AgentSession {
163 pub fn new(start_block: BlockId, config: SessionConfig) -> Self {
164 let now = Utc::now();
165 Self {
166 id: AgentSessionId::new(),
167 name: config.name,
168 cursor: TraversalCursor::new(start_block, config.limits.max_history_size),
169 capabilities: config.capabilities,
170 limits: config.limits,
171 budget: BudgetTracker::new(),
172 metrics: SessionMetrics::new(),
173 state: SessionState::Active,
174 created_at: now,
175 last_active: now,
176 last_results: Vec::new(),
177 context_blocks: HashSet::new(),
178 focus_block: None,
179 codegraph_context: None,
180 }
181 }
182
183 pub fn is_active(&self) -> bool {
185 matches!(self.state, SessionState::Active)
186 }
187
188 pub fn is_timed_out(&self) -> bool {
190 let elapsed = Utc::now()
191 .signed_duration_since(self.last_active)
192 .to_std()
193 .unwrap_or_default();
194 elapsed >= self.limits.session_timeout
195 }
196
197 pub fn touch(&mut self) {
199 self.last_active = Utc::now();
200 }
201
202 pub fn complete(&mut self) {
204 self.state = SessionState::Completed;
205 }
206
207 pub fn error(&mut self, reason: String) {
209 self.state = SessionState::Error { reason };
210 }
211
212 pub fn pause(&mut self) {
214 self.state = SessionState::Paused;
215 }
216
217 pub fn resume(&mut self) -> Result<()> {
219 match &self.state {
220 SessionState::Paused => {
221 self.state = SessionState::Active;
222 self.touch();
223 Ok(())
224 }
225 SessionState::Active => Ok(()),
226 _ => Err(AgentError::SessionClosed(self.id.clone())),
227 }
228 }
229
230 pub fn check_active(&self) -> Result<()> {
232 if !self.is_active() {
233 return Err(AgentError::SessionClosed(self.id.clone()));
234 }
235 if self.is_timed_out() {
236 return Err(AgentError::SessionExpired(self.id.clone()));
237 }
238 Ok(())
239 }
240
241 pub fn check_can_traverse(&self) -> Result<()> {
243 self.check_active()?;
244 if !self.capabilities.can_traverse {
245 return Err(AgentError::OperationNotPermitted {
246 operation: "traverse".to_string(),
247 });
248 }
249 Ok(())
250 }
251
252 pub fn check_can_search(&self) -> Result<()> {
254 self.check_active()?;
255 if !self.capabilities.can_search {
256 return Err(AgentError::OperationNotPermitted {
257 operation: "search".to_string(),
258 });
259 }
260 Ok(())
261 }
262
263 pub fn check_can_modify_context(&self) -> Result<()> {
265 self.check_active()?;
266 if !self.capabilities.can_modify_context {
267 return Err(AgentError::OperationNotPermitted {
268 operation: "modify_context".to_string(),
269 });
270 }
271 Ok(())
272 }
273
274 pub fn store_results(&mut self, results: Vec<BlockId>) {
276 self.last_results = results;
277 }
278
279 pub fn get_last_results(&self) -> Result<&[BlockId]> {
281 if self.last_results.is_empty() {
282 return Err(AgentError::NoResultsAvailable);
283 }
284 Ok(&self.last_results)
285 }
286
287 pub fn set_focus(&mut self, block_id: Option<BlockId>) {
289 self.focus_block = block_id;
290 }
291
292 pub fn ensure_codegraph_context(&mut self) -> &mut CodeGraphContextSession {
293 self.codegraph_context
294 .get_or_insert_with(CodeGraphContextSession::new)
295 }
296
297 pub fn info(&self) -> SessionInfo {
299 SessionInfo {
300 id: self.id.to_string(),
301 name: self.name.clone(),
302 position: self.cursor.position.to_string(),
303 state: self.state.clone(),
304 created_at: self.created_at,
305 last_active: self.last_active,
306 history_depth: self.cursor.history_depth(),
307 metrics: self.metrics.snapshot(),
308 context_blocks: self.context_blocks.len(),
309 has_codegraph_context: self.codegraph_context.is_some(),
310 }
311 }
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct SessionInfo {
317 pub id: String,
318 pub name: Option<String>,
319 pub position: String,
320 pub state: SessionState,
321 pub created_at: DateTime<Utc>,
322 pub last_active: DateTime<Utc>,
323 pub history_depth: usize,
324 pub metrics: crate::metrics::MetricsSnapshot,
325 pub context_blocks: usize,
326 pub has_codegraph_context: bool,
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 fn block_id(s: &str) -> BlockId {
334 s.parse().unwrap_or_else(|_| {
335 let mut bytes = [0u8; 12];
337 let s_bytes = s.as_bytes();
338 for (i, b) in s_bytes.iter().enumerate() {
339 bytes[i % 12] ^= *b;
340 }
341 BlockId::from_bytes(bytes)
342 })
343 }
344
345 #[test]
346 fn test_session_creation() {
347 let session = AgentSession::new(
348 block_id("blk_000000000001"),
349 SessionConfig::new().with_name("test"),
350 );
351
352 assert!(session.is_active());
353 assert_eq!(session.name, Some("test".to_string()));
354 }
355
356 #[test]
357 fn test_session_state_transitions() {
358 let mut session = AgentSession::new(block_id("blk_000000000001"), SessionConfig::default());
359
360 assert!(session.is_active());
361
362 session.pause();
363 assert!(!session.is_active());
364 assert!(matches!(session.state, SessionState::Paused));
365
366 session.resume().unwrap();
367 assert!(session.is_active());
368
369 session.complete();
370 assert!(!session.is_active());
371 assert!(session.resume().is_err());
372 }
373
374 #[test]
375 fn test_capabilities_check() {
376 let session = AgentSession::new(
377 block_id("blk_000000000001"),
378 SessionConfig::new().with_capabilities(AgentCapabilities::read_only()),
379 );
380
381 assert!(session.check_can_traverse().is_ok());
382 assert!(session.check_can_search().is_ok());
383 assert!(session.check_can_modify_context().is_err());
384 }
385
386 #[test]
387 fn test_last_results() {
388 let mut session = AgentSession::new(block_id("blk_000000000001"), SessionConfig::default());
389
390 assert!(session.get_last_results().is_err());
392
393 session.store_results(vec![
395 block_id("blk_000000000002"),
396 block_id("blk_000000000003"),
397 ]);
398
399 let results = session.get_last_results().unwrap();
400 assert_eq!(results.len(), 2);
401 }
402}