1pub(crate) mod compaction;
14pub mod manager;
15
16pub use manager::SessionManager;
17
18#[cfg(test)]
19#[path = "tests.rs"]
20mod tests_file;
21
22use crate::agent::AgentEvent;
23use crate::hitl::{ConfirmationManager, ConfirmationPolicy, ConfirmationProvider};
24use crate::llm::{LlmClient, Message, TokenUsage, ToolDefinition};
25use crate::permissions::{PermissionChecker, PermissionDecision, PermissionPolicy};
26use crate::planning::Task;
27use crate::prompts::PlanningMode;
28use crate::queue::{ExternalTaskResult, LaneHandlerConfig, SessionQueueConfig};
29use crate::session_lane_queue::SessionLaneQueue;
30use crate::store::{LlmConfigData, SessionData};
31use anyhow::Result;
32use serde::{Deserialize, Serialize};
33use std::sync::Arc;
34use tokio::sync::{broadcast, RwLock};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
38pub enum SessionState {
39 #[default]
40 Unknown = 0,
41 Active = 1,
42 Paused = 2,
43 Completed = 3,
44 Error = 4,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct ContextUsage {
50 pub used_tokens: usize,
51 pub max_tokens: usize,
52 pub percent: f32,
53 pub turns: usize,
54}
55
56impl Default for ContextUsage {
57 fn default() -> Self {
58 Self {
59 used_tokens: 0,
60 max_tokens: 200_000,
61 percent: 0.0,
62 turns: 0,
63 }
64 }
65}
66
67pub const DEFAULT_AUTO_COMPACT_THRESHOLD: f32 = 0.80;
69
70fn default_auto_compact_threshold() -> f32 {
72 DEFAULT_AUTO_COMPACT_THRESHOLD
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct SessionConfig {
78 pub name: String,
79 pub workspace: String,
80 pub system_prompt: Option<String>,
81 pub max_context_length: u32,
82 pub auto_compact: bool,
83 #[serde(default = "default_auto_compact_threshold")]
86 pub auto_compact_threshold: f32,
87 #[serde(default)]
89 pub storage_type: crate::config::StorageBackend,
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub queue_config: Option<SessionQueueConfig>,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub confirmation_policy: Option<ConfirmationPolicy>,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub permission_policy: Option<PermissionPolicy>,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub parent_id: Option<String>,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub security_config: Option<crate::security::SecurityConfig>,
105 #[serde(skip)]
107 pub hook_engine: Option<std::sync::Arc<dyn crate::hooks::HookExecutor>>,
108 #[serde(default)]
110 pub planning_mode: PlanningMode,
111 #[serde(default)]
113 pub goal_tracking: bool,
114}
115
116impl Default for SessionConfig {
117 fn default() -> Self {
118 Self {
119 name: String::new(),
120 workspace: String::new(),
121 system_prompt: None,
122 max_context_length: 0,
123 auto_compact: false,
124 auto_compact_threshold: DEFAULT_AUTO_COMPACT_THRESHOLD,
125 storage_type: crate::config::StorageBackend::default(),
126 queue_config: None,
127 confirmation_policy: None,
128 permission_policy: None,
129 parent_id: None,
130 security_config: None,
131 hook_engine: None,
132 planning_mode: PlanningMode::default(),
133 goal_tracking: false,
134 }
135 }
136}
137
138pub struct Session {
139 pub id: String,
140 pub config: SessionConfig,
141 pub state: SessionState,
142 pub messages: Vec<Message>,
143 pub context_usage: ContextUsage,
144 pub total_usage: TokenUsage,
145 pub total_cost: f64,
147 pub model_name: Option<String>,
149 pub tools: Vec<ToolDefinition>,
150 pub thinking_enabled: bool,
151 pub thinking_budget: Option<usize>,
152 pub llm_client: Option<Arc<dyn LlmClient>>,
154 pub created_at: i64,
156 pub updated_at: i64,
158 pub command_queue: SessionLaneQueue,
160 pub confirmation_manager: Arc<dyn ConfirmationProvider>,
162 pub permission_checker: Arc<dyn PermissionChecker>,
164 event_tx: broadcast::Sender<AgentEvent>,
166 pub context_providers: Vec<Arc<dyn crate::context::ContextProvider>>,
168 pub tasks: Vec<Task>,
170 pub parent_id: Option<String>,
172 pub memory: Option<Arc<RwLock<crate::memory::AgentMemory>>>,
174 pub current_plan: Arc<RwLock<Option<crate::planning::ExecutionPlan>>>,
176 pub security_provider: Option<Arc<dyn crate::security::SecurityProvider>>,
178 pub tool_metrics: Arc<RwLock<crate::telemetry::ToolMetrics>>,
180 pub cost_records: Vec<crate::telemetry::LlmCostRecord>,
182}
183
184fn validate_path_safe_id(id: &str, label: &str) -> Result<()> {
187 if id.is_empty() {
188 anyhow::bail!("{label} must not be empty");
189 }
190 let is_safe = id
192 .chars()
193 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
194 && !id.starts_with('.')
195 && !id.contains("..");
196 if !is_safe {
197 anyhow::bail!("{label} contains unsafe characters: {id:?}");
198 }
199 Ok(())
200}
201
202impl Session {
203 pub async fn new(
205 id: String,
206 config: SessionConfig,
207 tools: Vec<ToolDefinition>,
208 ) -> Result<Self> {
209 validate_path_safe_id(&id, "Session ID")?;
211
212 let now = std::time::SystemTime::now()
213 .duration_since(std::time::UNIX_EPOCH)
214 .map(|d| d.as_secs() as i64)
215 .unwrap_or(0);
216
217 let (event_tx, _) = broadcast::channel(100);
219
220 let queue_config = config.queue_config.clone().unwrap_or_default();
222 let command_queue = SessionLaneQueue::new(&id, queue_config, event_tx.clone()).await?;
223
224 let confirmation_policy = config
226 .confirmation_policy
227 .clone()
228 .unwrap_or_else(ConfirmationPolicy::enabled);
229 let confirmation_manager = Arc::new(ConfirmationManager::new(
230 confirmation_policy,
231 event_tx.clone(),
232 ));
233
234 let permission_checker: Arc<dyn PermissionChecker> =
236 Arc::new(config.permission_policy.clone().unwrap_or_default());
237
238 let parent_id = config.parent_id.clone();
240
241 let memory = None;
243
244 let context_providers: Vec<Arc<dyn crate::context::ContextProvider>> = vec![];
245
246 let current_plan = Arc::new(RwLock::new(None));
248
249 let security_provider: Option<Arc<dyn crate::security::SecurityProvider>> =
251 config.security_config.as_ref().and_then(|sc| {
252 if sc.enabled {
253 Some(Arc::new(crate::security::NoOpSecurityProvider)
254 as Arc<dyn crate::security::SecurityProvider>)
255 } else {
256 None
257 }
258 });
259
260 Ok(Self {
261 id,
262 config,
263 state: SessionState::Active,
264 messages: Vec::new(),
265 context_usage: ContextUsage::default(),
266 total_usage: TokenUsage::default(),
267 total_cost: 0.0,
268 model_name: None,
269 tools,
270 thinking_enabled: false,
271 thinking_budget: None,
272 llm_client: None,
273 created_at: now,
274 updated_at: now,
275 command_queue,
276 confirmation_manager,
277 permission_checker,
278 event_tx,
279 context_providers,
280 tasks: Vec::new(),
281 parent_id,
282 memory,
283 current_plan,
284
285 security_provider,
286 tool_metrics: Arc::new(RwLock::new(crate::telemetry::ToolMetrics::new())),
287 cost_records: Vec::new(),
288 })
289 }
290
291 pub fn is_child_session(&self) -> bool {
293 self.parent_id.is_some()
294 }
295
296 pub fn parent_session_id(&self) -> Option<&str> {
298 self.parent_id.as_deref()
299 }
300
301 pub fn subscribe_events(&self) -> broadcast::Receiver<AgentEvent> {
303 self.event_tx.subscribe()
304 }
305
306 pub fn event_tx(&self) -> broadcast::Sender<AgentEvent> {
308 self.event_tx.clone()
309 }
310
311 pub async fn set_confirmation_policy(&self, policy: ConfirmationPolicy) {
313 self.confirmation_manager.set_policy(policy).await;
314 }
315
316 pub fn set_permission_policy(&mut self, policy: PermissionPolicy) {
318 self.permission_checker = Arc::new(policy.clone());
319 self.config.permission_policy = Some(policy);
320 }
321
322 pub async fn confirmation_policy(&self) -> ConfirmationPolicy {
324 self.confirmation_manager.policy().await
325 }
326
327 pub fn check_permission(
329 &self,
330 tool_name: &str,
331 args: &serde_json::Value,
332 ) -> PermissionDecision {
333 self.permission_checker.check(tool_name, args)
334 }
335
336 pub fn add_context_provider(&mut self, provider: Arc<dyn crate::context::ContextProvider>) {
338 self.context_providers.push(provider);
339 }
340
341 pub fn remove_context_provider(&mut self, name: &str) -> bool {
345 let initial_len = self.context_providers.len();
346 self.context_providers.retain(|p| p.name() != name);
347 self.context_providers.len() < initial_len
348 }
349
350 pub fn context_provider_names(&self) -> Vec<String> {
352 self.context_providers
353 .iter()
354 .map(|p| p.name().to_string())
355 .collect()
356 }
357
358 pub fn get_tasks(&self) -> &[Task] {
364 &self.tasks
365 }
366
367 pub fn set_tasks(&mut self, tasks: Vec<Task>) {
371 self.tasks = tasks.clone();
372 self.touch();
373
374 let _ = self.event_tx.send(AgentEvent::TaskUpdated {
376 session_id: self.id.clone(),
377 tasks,
378 });
379 }
380
381 pub fn active_task_count(&self) -> usize {
383 self.tasks.iter().filter(|t| t.is_active()).count()
384 }
385
386 pub async fn set_lane_handler(
388 &self,
389 lane: crate::hitl::SessionLane,
390 config: LaneHandlerConfig,
391 ) {
392 self.command_queue.set_lane_handler(lane, config).await;
393 }
394
395 pub async fn get_lane_handler(&self, lane: crate::hitl::SessionLane) -> LaneHandlerConfig {
397 self.command_queue.get_lane_handler(lane).await
398 }
399
400 pub async fn complete_external_task(&self, task_id: &str, result: ExternalTaskResult) -> bool {
402 self.command_queue
403 .complete_external_task(task_id, result)
404 .await
405 }
406
407 pub async fn pending_external_tasks(&self) -> Vec<crate::queue::ExternalTask> {
409 self.command_queue.pending_external_tasks().await
410 }
411
412 pub async fn dead_letters(&self) -> Vec<a3s_lane::DeadLetter> {
414 self.command_queue.dead_letters().await
415 }
416
417 pub async fn queue_metrics(&self) -> Option<a3s_lane::MetricsSnapshot> {
419 self.command_queue.metrics_snapshot().await
420 }
421
422 pub async fn queue_stats(&self) -> crate::queue::SessionQueueStats {
424 self.command_queue.stats().await
425 }
426
427 pub async fn start_queue(&self) -> Result<()> {
429 self.command_queue.start().await
430 }
431
432 pub async fn stop_queue(&self) {
434 self.command_queue.stop().await;
435 }
436
437 pub fn system(&self) -> Option<&str> {
439 self.config.system_prompt.as_deref()
440 }
441
442 pub fn history(&self) -> &[Message] {
444 &self.messages
445 }
446
447 pub fn add_message(&mut self, message: Message) {
449 self.messages.push(message);
450 self.context_usage.turns = self.messages.len();
451 self.touch();
452 }
453
454 pub fn update_usage(&mut self, usage: &TokenUsage) {
456 self.total_usage.prompt_tokens += usage.prompt_tokens;
457 self.total_usage.completion_tokens += usage.completion_tokens;
458 self.total_usage.total_tokens += usage.total_tokens;
459
460 let cost_usd = if let Some(ref model) = self.model_name {
462 let pricing_map = crate::telemetry::default_model_pricing();
463 if let Some(pricing) = pricing_map.get(model) {
464 let cost = pricing.calculate_cost(usage.prompt_tokens, usage.completion_tokens);
465 self.total_cost += cost;
466 Some(cost)
467 } else {
468 None
469 }
470 } else {
471 None
472 };
473
474 let model_str = self.model_name.clone().unwrap_or_default();
476 self.cost_records.push(crate::telemetry::LlmCostRecord {
477 model: model_str.clone(),
478 provider: String::new(),
479 prompt_tokens: usage.prompt_tokens,
480 completion_tokens: usage.completion_tokens,
481 total_tokens: usage.total_tokens,
482 cost_usd,
483 timestamp: chrono::Utc::now(),
484 session_id: Some(self.id.clone()),
485 });
486
487 crate::telemetry::record_llm_metrics(
489 if model_str.is_empty() {
490 "unknown"
491 } else {
492 &model_str
493 },
494 usage.prompt_tokens,
495 usage.completion_tokens,
496 cost_usd.unwrap_or(0.0),
497 0.0, );
499
500 self.context_usage.used_tokens = usage.prompt_tokens;
502 self.context_usage.percent =
503 self.context_usage.used_tokens as f32 / self.context_usage.max_tokens as f32;
504 self.touch();
505 }
506
507 pub fn clear(&mut self) {
509 self.messages.clear();
510 self.context_usage = ContextUsage::default();
511 self.touch();
512 }
513
514 pub async fn compact(&mut self, llm_client: &Arc<dyn LlmClient>) -> Result<()> {
516 if let Some(new_messages) =
517 compaction::compact_messages(&self.id, &self.messages, llm_client).await?
518 {
519 self.messages = new_messages;
520 self.touch();
521 }
522 Ok(())
523 }
524
525 pub fn pause(&mut self) -> bool {
527 if self.state == SessionState::Active {
528 self.state = SessionState::Paused;
529 self.touch();
530 true
531 } else {
532 false
533 }
534 }
535
536 pub fn resume(&mut self) -> bool {
538 if self.state == SessionState::Paused {
539 self.state = SessionState::Active;
540 self.touch();
541 true
542 } else {
543 false
544 }
545 }
546
547 pub fn set_error(&mut self) {
549 self.state = SessionState::Error;
550 self.touch();
551 }
552
553 pub fn set_completed(&mut self) {
555 self.state = SessionState::Completed;
556 self.touch();
557 }
558
559 fn touch(&mut self) {
561 self.updated_at = std::time::SystemTime::now()
562 .duration_since(std::time::UNIX_EPOCH)
563 .map(|d| d.as_secs() as i64)
564 .unwrap_or(0);
565 }
566
567 pub fn to_session_data(&self, llm_config: Option<LlmConfigData>) -> SessionData {
569 SessionData {
570 id: self.id.clone(),
571 config: self.config.clone(),
572 state: self.state,
573 messages: self.messages.clone(),
574 context_usage: self.context_usage.clone(),
575 total_usage: self.total_usage.clone(),
576 total_cost: self.total_cost,
577 model_name: self.model_name.clone(),
578 cost_records: self.cost_records.clone(),
579 tool_names: SessionData::tool_names_from_definitions(&self.tools),
580 thinking_enabled: self.thinking_enabled,
581 thinking_budget: self.thinking_budget,
582 created_at: self.created_at,
583 updated_at: self.updated_at,
584 llm_config,
585 tasks: self.tasks.clone(),
586 parent_id: self.parent_id.clone(),
587 }
588 }
589
590 pub fn restore_from_data(&mut self, data: &SessionData) {
596 self.state = data.state;
597 self.messages = data.messages.clone();
598 self.context_usage = data.context_usage.clone();
599 self.total_usage = data.total_usage.clone();
600 self.total_cost = data.total_cost;
601 self.model_name = data.model_name.clone();
602 self.cost_records = data.cost_records.clone();
603 self.thinking_enabled = data.thinking_enabled;
604 self.thinking_budget = data.thinking_budget;
605 self.created_at = data.created_at;
606 self.updated_at = data.updated_at;
607 self.tasks = data.tasks.clone();
608 self.parent_id = data.parent_id.clone();
609 }
610}