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};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15#[derive(Default)]
16pub enum SessionState {
17 #[default]
19 Active,
20 Paused,
22 Completed,
24 TimedOut,
26 Error { reason: String },
28}
29
30#[derive(Debug, Clone)]
32pub struct AgentCapabilities {
33 pub can_traverse: bool,
35 pub can_search: bool,
37 pub can_modify_context: bool,
39 pub can_coordinate: bool,
41 pub allowed_edge_types: HashSet<EdgeType>,
43 pub max_expand_depth: usize,
45}
46
47impl Default for AgentCapabilities {
48 fn default() -> Self {
49 Self {
50 can_traverse: true,
51 can_search: true,
52 can_modify_context: true,
53 can_coordinate: true,
54 allowed_edge_types: HashSet::new(), max_expand_depth: 10,
56 }
57 }
58}
59
60impl AgentCapabilities {
61 pub fn is_edge_allowed(&self, edge_type: &EdgeType) -> bool {
63 self.allowed_edge_types.is_empty() || self.allowed_edge_types.contains(edge_type)
64 }
65
66 pub fn full() -> Self {
68 Self::default()
69 }
70
71 pub fn read_only() -> Self {
73 Self {
74 can_traverse: true,
75 can_search: true,
76 can_modify_context: false,
77 can_coordinate: false,
78 ..Default::default()
79 }
80 }
81}
82
83#[derive(Debug, Clone, Default)]
85pub struct SessionConfig {
86 pub name: Option<String>,
88 pub start_block: Option<BlockId>,
90 pub limits: SessionLimits,
92 pub capabilities: AgentCapabilities,
94 pub view_mode: ViewMode,
96}
97
98impl SessionConfig {
99 pub fn new() -> Self {
100 Self::default()
101 }
102
103 pub fn with_name(mut self, name: &str) -> Self {
104 self.name = Some(name.to_string());
105 self
106 }
107
108 pub fn with_start_block(mut self, block: BlockId) -> Self {
109 self.start_block = Some(block);
110 self
111 }
112
113 pub fn with_limits(mut self, limits: SessionLimits) -> Self {
114 self.limits = limits;
115 self
116 }
117
118 pub fn with_capabilities(mut self, capabilities: AgentCapabilities) -> Self {
119 self.capabilities = capabilities;
120 self
121 }
122
123 pub fn with_view_mode(mut self, mode: ViewMode) -> Self {
124 self.view_mode = mode;
125 self
126 }
127}
128
129pub struct AgentSession {
131 pub id: AgentSessionId,
133 pub name: Option<String>,
135 pub cursor: TraversalCursor,
137 pub capabilities: AgentCapabilities,
139 pub limits: SessionLimits,
141 pub budget: BudgetTracker,
143 pub metrics: SessionMetrics,
145 pub state: SessionState,
147 pub created_at: DateTime<Utc>,
149 pub last_active: DateTime<Utc>,
151 pub last_results: Vec<BlockId>,
153 pub focus_block: Option<BlockId>,
155}
156
157impl AgentSession {
158 pub fn new(start_block: BlockId, config: SessionConfig) -> Self {
159 let now = Utc::now();
160 Self {
161 id: AgentSessionId::new(),
162 name: config.name,
163 cursor: TraversalCursor::new(start_block, config.limits.max_history_size),
164 capabilities: config.capabilities,
165 limits: config.limits,
166 budget: BudgetTracker::new(),
167 metrics: SessionMetrics::new(),
168 state: SessionState::Active,
169 created_at: now,
170 last_active: now,
171 last_results: Vec::new(),
172 focus_block: None,
173 }
174 }
175
176 pub fn is_active(&self) -> bool {
178 matches!(self.state, SessionState::Active)
179 }
180
181 pub fn is_timed_out(&self) -> bool {
183 let elapsed = Utc::now()
184 .signed_duration_since(self.last_active)
185 .to_std()
186 .unwrap_or_default();
187 elapsed >= self.limits.session_timeout
188 }
189
190 pub fn touch(&mut self) {
192 self.last_active = Utc::now();
193 }
194
195 pub fn complete(&mut self) {
197 self.state = SessionState::Completed;
198 }
199
200 pub fn error(&mut self, reason: String) {
202 self.state = SessionState::Error { reason };
203 }
204
205 pub fn pause(&mut self) {
207 self.state = SessionState::Paused;
208 }
209
210 pub fn resume(&mut self) -> Result<()> {
212 match &self.state {
213 SessionState::Paused => {
214 self.state = SessionState::Active;
215 self.touch();
216 Ok(())
217 }
218 SessionState::Active => Ok(()),
219 _ => Err(AgentError::SessionClosed(self.id.clone())),
220 }
221 }
222
223 pub fn check_active(&self) -> Result<()> {
225 if !self.is_active() {
226 return Err(AgentError::SessionClosed(self.id.clone()));
227 }
228 if self.is_timed_out() {
229 return Err(AgentError::SessionExpired(self.id.clone()));
230 }
231 Ok(())
232 }
233
234 pub fn check_can_traverse(&self) -> Result<()> {
236 self.check_active()?;
237 if !self.capabilities.can_traverse {
238 return Err(AgentError::OperationNotPermitted {
239 operation: "traverse".to_string(),
240 });
241 }
242 Ok(())
243 }
244
245 pub fn check_can_search(&self) -> Result<()> {
247 self.check_active()?;
248 if !self.capabilities.can_search {
249 return Err(AgentError::OperationNotPermitted {
250 operation: "search".to_string(),
251 });
252 }
253 Ok(())
254 }
255
256 pub fn check_can_modify_context(&self) -> Result<()> {
258 self.check_active()?;
259 if !self.capabilities.can_modify_context {
260 return Err(AgentError::OperationNotPermitted {
261 operation: "modify_context".to_string(),
262 });
263 }
264 Ok(())
265 }
266
267 pub fn store_results(&mut self, results: Vec<BlockId>) {
269 self.last_results = results;
270 }
271
272 pub fn get_last_results(&self) -> Result<&[BlockId]> {
274 if self.last_results.is_empty() {
275 return Err(AgentError::NoResultsAvailable);
276 }
277 Ok(&self.last_results)
278 }
279
280 pub fn set_focus(&mut self, block_id: Option<BlockId>) {
282 self.focus_block = block_id;
283 }
284
285 pub fn info(&self) -> SessionInfo {
287 SessionInfo {
288 id: self.id.to_string(),
289 name: self.name.clone(),
290 position: self.cursor.position.to_string(),
291 state: self.state.clone(),
292 created_at: self.created_at,
293 last_active: self.last_active,
294 history_depth: self.cursor.history_depth(),
295 metrics: self.metrics.snapshot(),
296 }
297 }
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct SessionInfo {
303 pub id: String,
304 pub name: Option<String>,
305 pub position: String,
306 pub state: SessionState,
307 pub created_at: DateTime<Utc>,
308 pub last_active: DateTime<Utc>,
309 pub history_depth: usize,
310 pub metrics: crate::metrics::MetricsSnapshot,
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 fn block_id(s: &str) -> BlockId {
318 s.parse().unwrap_or_else(|_| {
319 let mut bytes = [0u8; 12];
321 let s_bytes = s.as_bytes();
322 for (i, b) in s_bytes.iter().enumerate() {
323 bytes[i % 12] ^= *b;
324 }
325 BlockId::from_bytes(bytes)
326 })
327 }
328
329 #[test]
330 fn test_session_creation() {
331 let session = AgentSession::new(
332 block_id("blk_000000000001"),
333 SessionConfig::new().with_name("test"),
334 );
335
336 assert!(session.is_active());
337 assert_eq!(session.name, Some("test".to_string()));
338 }
339
340 #[test]
341 fn test_session_state_transitions() {
342 let mut session = AgentSession::new(block_id("blk_000000000001"), SessionConfig::default());
343
344 assert!(session.is_active());
345
346 session.pause();
347 assert!(!session.is_active());
348 assert!(matches!(session.state, SessionState::Paused));
349
350 session.resume().unwrap();
351 assert!(session.is_active());
352
353 session.complete();
354 assert!(!session.is_active());
355 assert!(session.resume().is_err());
356 }
357
358 #[test]
359 fn test_capabilities_check() {
360 let session = AgentSession::new(
361 block_id("blk_000000000001"),
362 SessionConfig::new().with_capabilities(AgentCapabilities::read_only()),
363 );
364
365 assert!(session.check_can_traverse().is_ok());
366 assert!(session.check_can_search().is_ok());
367 assert!(session.check_can_modify_context().is_err());
368 }
369
370 #[test]
371 fn test_last_results() {
372 let mut session = AgentSession::new(block_id("blk_000000000001"), SessionConfig::default());
373
374 assert!(session.get_last_results().is_err());
376
377 session.store_results(vec![
379 block_id("blk_000000000002"),
380 block_id("blk_000000000003"),
381 ]);
382
383 let results = session.get_last_results().unwrap();
384 assert_eq!(results.len(), 2);
385 }
386}