agent_base/engine/runtime/
session_manager.rs1use std::collections::HashMap;
2use std::sync::Arc;
3use std::time::Instant;
4
5use tokio::sync::{Mutex, RwLock};
6
7use crate::engine::AgentSession;
8use crate::engine::context::ContextWindowManager;
9use crate::engine::session_store::SessionStore;
10use crate::types::{
11 AgentError, AgentResult, MessageRole, SessionConfig, SessionId, SessionIdGenerator,
12};
13
14#[derive(Clone)]
15pub struct SessionManager {
16 session_id_generator: Arc<dyn SessionIdGenerator>,
17 sessions: Arc<RwLock<HashMap<SessionId, AgentSession>>>,
18 lru_times: Arc<Mutex<HashMap<SessionId, Instant>>>,
22 session_store: Arc<dyn SessionStore>,
23 config: SessionConfig,
24}
25
26impl SessionManager {
27 pub fn new(
28 session_id_generator: Arc<dyn SessionIdGenerator>,
29 session_store: Arc<dyn SessionStore>,
30 config: SessionConfig,
31 ) -> Self {
32 Self {
33 session_id_generator,
34 sessions: Arc::new(RwLock::new(HashMap::new())),
35 lru_times: Arc::new(Mutex::new(HashMap::new())),
36 session_store,
37 config,
38 }
39 }
40
41 pub async fn create_session(&self, system_prompt: Option<&str>) -> SessionId {
42 if let Err(e) = self.evict_if_needed().await {
44 tracing::warn!(error = %e, "session eviction failed, proceeding with creation");
45 }
46
47 let id = self.session_id_generator.generate();
48 let mut session = AgentSession::new(id.clone());
49 if let Some(prompt) = system_prompt {
50 session.push_message(MessageRole::System, prompt);
51 }
52 {
53 let mut sessions = self.sessions.write().await;
54 sessions.insert(id.clone(), session);
55 }
56 {
57 let mut lru = self.lru_times.lock().await;
58 lru.insert(id.clone(), Instant::now());
59 }
60 tracing::debug!(session_id = id.id, "session created");
61 id
62 }
63
64 pub async fn restore_session(&self, session_id: &SessionId) -> Option<AgentSession> {
65 {
66 let sessions = self.sessions.read().await;
67 if sessions.contains_key(session_id) {
68 let mut lru = self.lru_times.lock().await;
69 lru.insert(session_id.clone(), Instant::now());
70 tracing::debug!(session_id = session_id.id, "session restore cache hit");
71 return sessions.get(session_id).cloned();
72 }
73 }
74 match self.session_store.load(session_id).await {
75 Ok(Some(session)) => {
76 let msg_count = session.chat_messages().len();
77 if let Err(e) =
80 crate::engine::session::validate_message_sequence(session.chat_messages())
81 {
82 tracing::warn!(session_id = session_id.id, error = %e, "restored session has invalid message sequence");
83 }
84 self.evict_if_needed().await.ok();
86 {
87 let mut sessions = self.sessions.write().await;
88 sessions.insert(session_id.clone(), session.clone());
89 }
90 {
91 let mut lru = self.lru_times.lock().await;
92 lru.insert(session_id.clone(), Instant::now());
93 }
94 tracing::debug!(
95 session_id = session_id.id,
96 msg_count,
97 "session restored from store"
98 );
99 Some(session)
100 }
101 Ok(None) => {
102 tracing::debug!(session_id = session_id.id, "session not found in store");
103 None
104 }
105 Err(e) => {
106 tracing::warn!(session_id = session_id.id, error = %e, "session restore failed");
107 None
108 }
109 }
110 }
111
112 pub async fn session(&self, session_id: &SessionId) -> Option<AgentSession> {
113 let sessions = self.sessions.read().await;
114 let result = sessions.get(session_id).cloned();
115 if result.is_some() {
116 let mut lru = self.lru_times.lock().await;
117 lru.insert(session_id.clone(), Instant::now());
118 }
119 result
120 }
121
122 pub async fn session_or_err(&self, session_id: &SessionId) -> AgentResult<AgentSession> {
123 let sessions = self.sessions.read().await;
124 let result = sessions
125 .get(session_id)
126 .cloned()
127 .ok_or_else(|| AgentError::session_not_found(session_id.id));
128 if result.is_ok() {
129 let mut lru = self.lru_times.lock().await;
130 lru.insert(session_id.clone(), Instant::now());
131 }
132 result
133 }
134
135 pub async fn with_session_mut<F, R>(&self, session_id: &SessionId, f: F) -> AgentResult<R>
136 where
137 F: FnOnce(&mut AgentSession) -> R,
138 {
139 let result = {
145 let mut sessions = self.sessions.write().await;
146 let session = sessions
147 .get_mut(session_id)
148 .ok_or_else(|| AgentError::session_not_found(session_id.id))?;
149 f(session)
150 };
151 {
153 let mut lru = self.lru_times.lock().await;
154 lru.insert(session_id.clone(), Instant::now());
155 }
156 self.enforce_session_limits(session_id).await;
157 Ok(result)
158 }
159
160 pub async fn cached_approval(&self, session_id: &SessionId, action_key: &str) -> bool {
161 let sessions = self.sessions.read().await;
162 sessions
163 .get(session_id)
164 .is_some_and(|session| session.is_action_allowed(action_key))
165 }
166
167 pub async fn cache_approval(&self, session_id: &SessionId, action_key: String) {
168 let mut sessions = self.sessions.write().await;
169 if let Some(session) = sessions.get_mut(session_id) {
170 session.allow_action(action_key);
171 }
172 }
173
174 pub async fn save_session(&self, session_id: &SessionId) -> AgentResult<()> {
175 let session = self.session_or_err(session_id).await?;
176 let msg_count = session.chat_messages().len();
177 tracing::debug!(session_id = session_id.id, msg_count, "saving session");
178 self.session_store
179 .save(&session)
180 .await
181 .map_err(|e| AgentError::internal(format!("Session persistence failed: {e}")))
182 }
183
184 pub fn session_store(&self) -> &Arc<dyn SessionStore> {
185 &self.session_store
186 }
187
188 async fn evict_if_needed(&self) -> AgentResult<()> {
195 let max = match self.config.max_sessions {
196 Some(m) => m,
197 None => return Ok(()),
198 };
199
200 let victim = {
202 let sessions = self.sessions.read().await;
203 if sessions.len() < max {
204 return Ok(());
205 }
206 let lru = self.lru_times.lock().await;
207 sessions
208 .keys()
209 .min_by_key(|id| lru.get(*id).copied().unwrap_or(Instant::now()))
210 .cloned()
211 };
212
213 let Some(victim_id) = victim else {
214 return Ok(());
215 };
216
217 if let Err(e) = self.save_session(&victim_id).await {
219 tracing::warn!(session_id = victim_id.id, error = %e, "failed to persist session before eviction");
220 }
221
222 {
224 let mut sessions = self.sessions.write().await;
225 sessions.remove(&victim_id);
226 }
227 {
228 let mut lru = self.lru_times.lock().await;
229 lru.remove(&victim_id);
230 }
231 tracing::info!(session_id = victim_id.id, "session evicted (LRU)");
232
233 Ok(())
234 }
235
236 async fn enforce_session_limits(&self, session_id: &SessionId) {
238 if let Some(max_turns) = self.config.max_turns_per_session {
240 let needs_trim = {
241 let sessions = self.sessions.read().await;
242 sessions
243 .get(session_id)
244 .is_some_and(|session| session.turn_count() > max_turns)
245 };
246
247 if needs_trim {
248 if let Err(e) = self.save_session(session_id).await {
250 tracing::warn!(session_id = session_id.id, error = %e, "failed to persist before turn trim");
251 }
252 let mut sessions = self.sessions.write().await;
254 if let Some(session) = sessions.get_mut(session_id) {
255 let before = session.turn_count();
256 session.trim_oldest_turns(max_turns);
257 tracing::info!(
258 session_id = session_id.id,
259 before,
260 after = session.turn_count(),
261 max_turns,
262 "session turns trimmed"
263 );
264 }
265 }
266 }
267
268 if let Some(max_tokens) = self.config.max_message_tokens {
270 let mut sessions = self.sessions.write().await;
271 if let Some(session) = sessions.get_mut(session_id)
272 && let Some(last) = session.chat_messages().last()
273 {
274 let tokens = ContextWindowManager::message_tokens(last);
275 if tokens > max_tokens {
276 session.pop_last_message();
277 tracing::warn!(
278 session_id = session_id.id,
279 tokens,
280 max_tokens,
281 "oversized message removed from session (safety valve)"
282 );
283 }
284 }
285 }
286 }
287}