heddle_object_model/object/
session.rs1use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7use super::Principal;
8
9pub fn generate_session_id() -> String {
10 let random_bytes: [u8; 10] = rand::random();
11 format!(
12 "sess-{}",
13 base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &random_bytes).to_lowercase()
14 )
15}
16
17pub fn generate_segment_id(session_id: &str, segment_number: u32) -> String {
18 format!("{}-seg-{}", session_id, segment_number)
19}
20
21#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
22pub struct Session {
23 pub id: String,
24 pub principal: Principal,
25 pub created_at: DateTime<Utc>,
26 pub ended_at: Option<DateTime<Utc>>,
27 pub segments: Vec<SessionSegment>,
28 pub current_segment_id: Option<String>,
29}
30
31impl Session {
32 pub fn new(
33 id: String,
34 principal: Principal,
35 provider: String,
36 model: String,
37 policy_id: Option<String>,
38 ) -> Self {
39 let segment_id = generate_segment_id(&id, 1);
40 let segment = SessionSegment {
41 id: segment_id.clone(),
42 provider,
43 model,
44 started_at: Utc::now(),
45 policy_id,
46 thought_level: None,
47 };
48 Self {
49 id,
50 principal,
51 created_at: Utc::now(),
52 ended_at: None,
53 segments: vec![segment],
54 current_segment_id: Some(segment_id),
55 }
56 }
57
58 pub fn is_active(&self) -> bool {
59 self.ended_at.is_none()
60 }
61
62 pub fn current_segment(&self) -> Option<&SessionSegment> {
63 self.current_segment_id
64 .as_ref()
65 .and_then(|id| self.segments.iter().find(|s| &s.id == id))
66 }
67
68 pub fn add_segment(
69 &mut self,
70 provider: String,
71 model: String,
72 policy_id: Option<String>,
73 ) -> &SessionSegment {
74 let segment_number = self.segments.len() as u32 + 1;
75 let segment_id = generate_segment_id(&self.id, segment_number);
76 let segment = SessionSegment {
77 id: segment_id.clone(),
78 provider,
79 model,
80 started_at: Utc::now(),
81 policy_id,
82 thought_level: None,
83 };
84 self.segments.push(segment);
85 self.current_segment_id = Some(segment_id);
86 self.segments.last().expect("segment was just pushed")
87 }
88
89 pub fn attach_thought_level(&mut self, thought_level: impl Into<String>) {
91 let Some(id) = self.current_segment_id.clone() else {
92 return;
93 };
94 if let Some(segment) = self.segments.iter_mut().find(|segment| segment.id == id)
95 && segment.thought_level.is_none()
96 {
97 segment.thought_level = Some(thought_level.into());
98 }
99 }
100
101 pub fn current_segment_mut(&mut self) -> Option<&mut SessionSegment> {
102 let id = self.current_segment_id.as_ref()?;
103 self.segments.iter_mut().find(|segment| &segment.id == id)
104 }
105
106 pub fn end(&mut self) {
107 self.ended_at = Some(Utc::now());
108 self.current_segment_id = None;
109 }
110}
111
112#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
113pub struct SessionSegment {
114 pub id: String,
115 pub provider: String,
116 pub model: String,
117 pub started_at: DateTime<Utc>,
118 pub policy_id: Option<String>,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub thought_level: Option<String>,
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn test_session_creation() {
130 let principal = Principal::new("Test User", "test@example.com");
131 let session = Session::new(
132 "sess-test123".to_string(),
133 principal.clone(),
134 "anthropic".to_string(),
135 "claude-opus-4".to_string(),
136 None,
137 );
138
139 assert_eq!(session.id, "sess-test123");
140 assert_eq!(session.principal, principal);
141 assert!(session.is_active());
142 assert!(session.ended_at.is_none());
143 assert_eq!(session.segments.len(), 1);
144 assert!(session.current_segment_id.is_some());
145 }
146
147 #[test]
148 fn test_segment_id_format() {
149 let segment_id = generate_segment_id("sess-test123", 1);
150 assert_eq!(segment_id, "sess-test123-seg-1");
151
152 let segment_id = generate_segment_id("sess-test123", 2);
153 assert_eq!(segment_id, "sess-test123-seg-2");
154 }
155
156 #[test]
157 fn test_current_segment() {
158 let principal = Principal::new("Test User", "test@example.com");
159 let session = Session::new(
160 "sess-test123".to_string(),
161 principal,
162 "anthropic".to_string(),
163 "claude-opus-4".to_string(),
164 None,
165 );
166
167 let segment = session.current_segment().unwrap();
168 assert_eq!(segment.provider, "anthropic");
169 assert_eq!(segment.model, "claude-opus-4");
170 }
171
172 #[test]
173 fn test_add_segment() {
174 let principal = Principal::new("Test User", "test@example.com");
175 let mut session = Session::new(
176 "sess-test123".to_string(),
177 principal,
178 "anthropic".to_string(),
179 "claude-opus-4".to_string(),
180 None,
181 );
182
183 let segment = session.add_segment(
184 "openai".to_string(),
185 "gpt-4".to_string(),
186 Some("policy-123".to_string()),
187 );
188
189 let segment_id = segment.id.clone();
190 let segment_provider = segment.provider.clone();
191 let segment_model = segment.model.clone();
192 let segment_policy_id = segment.policy_id.clone();
193
194 assert_eq!(session.segments.len(), 2);
195 assert_eq!(segment_id, "sess-test123-seg-2");
196 assert_eq!(segment_provider, "openai");
197 assert_eq!(segment_model, "gpt-4");
198 assert_eq!(segment_policy_id, Some("policy-123".to_string()));
199
200 let current = session.current_segment().unwrap();
201 assert_eq!(current.id, "sess-test123-seg-2");
202 }
203
204 #[test]
205 fn test_end_session() {
206 let principal = Principal::new("Test User", "test@example.com");
207 let mut session = Session::new(
208 "sess-test123".to_string(),
209 principal,
210 "anthropic".to_string(),
211 "claude-opus-4".to_string(),
212 None,
213 );
214
215 assert!(session.is_active());
216
217 session.end();
218
219 assert!(!session.is_active());
220 assert!(session.ended_at.is_some());
221 assert!(session.current_segment_id.is_none());
222 }
223
224 #[test]
225 fn test_session_serialization() {
226 let principal = Principal::new("Test User", "test@example.com");
227 let session = Session::new(
228 "sess-test123".to_string(),
229 principal,
230 "anthropic".to_string(),
231 "claude-opus-4".to_string(),
232 Some("policy-abc".to_string()),
233 );
234
235 let json = serde_json::to_string(&session).unwrap();
236 let deserialized: Session = serde_json::from_str(&json).unwrap();
237
238 assert_eq!(session, deserialized);
239 }
240}