1use crate::agent::branch_summary::{collect_entries_for_branch_summary, generate_branch_summary};
2use crate::agent::compaction::{
3 self, CompactionReason, CompactionResult, CompactionSettings, compact, prepare_compaction,
4};
5use crate::agent::extension::Extension;
6use crate::agent::session::SessionManager;
7use crate::agent::session::model::MessageCost;
8use crate::agent::types::{message_text, user_message};
9use std::sync::Arc;
10
11use crate::provider::ProviderRegistry;
12use yoagent::types::AgentMessage;
13use yoagent::types::Message;
14
15#[derive(Debug, Clone)]
20pub enum CompactionEvent {
21 Start { reason: CompactionReason },
23 End {
25 reason: CompactionReason,
26 result: CompactionResult,
27 aborted: bool,
28 will_retry: bool,
29 error_message: Option<String>,
30 },
31}
32
33pub type CompactionEventCallback = Box<dyn Fn(&CompactionEvent) + Send + Sync>;
35
36pub struct AgentSession {
42 mgr: SessionManager,
44 last_model: Option<(String, String)>,
46 last_thinking_level: String,
48 last_active_tools: Option<Vec<String>>,
50 compaction_settings: CompactionSettings,
52 context_window: u64,
54 model_name: String,
56 compaction_api_key: Option<String>,
58 model_config: Option<yoagent::provider::model::ModelConfig>,
60 thinking_level: yoagent::types::ThinkingLevel,
62 extensions: Vec<Box<dyn Extension>>,
64 event_listeners: Vec<CompactionEventCallback>,
66 overflow_recovery_attempted: bool,
68 compaction_cancel: crate::agent::extension::Cancel,
70 registry: Option<Arc<ProviderRegistry>>,
72}
73
74impl AgentSession {
75 pub fn new(mgr: SessionManager) -> Self {
77 let ctx = mgr.session().build_context();
79
80 let has_thinking_entries = !mgr
83 .session()
84 .find_entries("thinking_level_change")
85 .is_empty();
86 let last_thinking_level = if has_thinking_entries {
87 ctx.thinking_level
88 } else {
89 String::new()
90 };
91
92 Self {
93 mgr,
94 last_model: ctx.model,
95 last_thinking_level,
96 last_active_tools: ctx.active_tool_names,
97 compaction_settings: CompactionSettings::default(),
98 context_window: 200_000,
99 model_name: String::new(),
100 compaction_api_key: None,
101 model_config: None,
102 thinking_level: yoagent::types::ThinkingLevel::Off,
103 extensions: Vec::new(),
104 event_listeners: Vec::new(),
105 overflow_recovery_attempted: false,
106 compaction_cancel: crate::agent::extension::Cancel::new(),
107 registry: None,
108 }
109 }
110
111 pub fn create(cwd: &std::path::Path, session_dir: Option<&std::path::Path>) -> Self {
115 Self::new(SessionManager::create(cwd, session_dir))
116 }
117
118 pub fn open(
120 path: &std::path::Path,
121 session_dir: Option<&std::path::Path>,
122 cwd_override: Option<&std::path::Path>,
123 ) -> Self {
124 Self::new(SessionManager::open(path, session_dir, cwd_override))
125 }
126
127 pub fn in_memory(cwd: &std::path::Path) -> Self {
129 Self::new(SessionManager::in_memory(cwd))
130 }
131
132 pub fn continue_recent(cwd: &std::path::Path, session_dir: Option<&std::path::Path>) -> Self {
134 Self::new(SessionManager::continue_recent(cwd, session_dir))
135 }
136
137 pub fn fork_from(
139 source_path: &std::path::Path,
140 target_cwd: &std::path::Path,
141 session_dir: Option<&std::path::Path>,
142 options: Option<&crate::agent::session::NewSessionOptions>,
143 ) -> std::io::Result<Self> {
144 SessionManager::fork_from(source_path, target_cwd, session_dir, options).map(Self::new)
145 }
146
147 pub fn set_compaction_config(
149 &mut self,
150 api_key: String,
151 model_name: &str,
152 context_window: u64,
153 model_config: Option<yoagent::provider::model::ModelConfig>,
154 ) {
155 self.compaction_api_key = Some(api_key);
156 self.model_name = model_name.to_string();
157 self.context_window = context_window;
158 self.model_config = model_config;
159 }
160
161 pub fn set_auto_compact(&mut self, enabled: bool) {
163 self.compaction_settings.enabled = enabled;
164 }
165
166 pub fn set_registry(&mut self, registry: Arc<ProviderRegistry>) {
168 self.registry = Some(registry);
169 }
170
171 pub fn sync_thinking_level(&mut self) {
174 let ctx = self.mgr.session().build_context();
175 let level_str = ctx.thinking_level.to_lowercase();
176 self.thinking_level = match level_str.as_str() {
177 "off" => yoagent::types::ThinkingLevel::Off,
178 "minimal" => yoagent::types::ThinkingLevel::Minimal,
179 "low" => yoagent::types::ThinkingLevel::Low,
180 "medium" => yoagent::types::ThinkingLevel::Medium,
181 "high" => yoagent::types::ThinkingLevel::High,
182 _ => yoagent::types::ThinkingLevel::Off,
183 };
184 }
185
186 pub fn compaction_settings_mut(&mut self) -> &mut CompactionSettings {
188 &mut self.compaction_settings
189 }
190
191 pub fn compaction_settings(&self) -> &CompactionSettings {
193 &self.compaction_settings
194 }
195
196 pub fn set_extensions(&mut self, extensions: Vec<Box<dyn Extension>>) {
198 self.extensions = extensions;
199 }
200
201 pub fn abort_compaction(&self) {
205 self.compaction_cancel.cancel();
206 }
207
208 pub fn on_compaction_event(&mut self, callback: CompactionEventCallback) {
210 self.event_listeners.push(callback);
211 }
212
213 fn emit_compaction_event(&self, event: &CompactionEvent) {
215 for listener in &self.event_listeners {
216 listener(event);
217 }
218 }
219
220 pub fn reset_overflow_recovery(&mut self) {
224 self.overflow_recovery_attempted = false;
225 self.compaction_cancel = crate::agent::extension::Cancel::new();
226 }
227
228 pub fn is_context_overflow_error(msg: &AgentMessage) -> bool {
231 let text = message_text(msg);
232 let lower = text.to_lowercase();
233 lower.contains("413")
235 || lower.contains("request_too_large")
236 || lower.contains("prompt too long")
237 || lower.contains("context_length_exceeded")
238 || lower.contains("context overflow")
239 || lower.contains("max context length")
240 || lower.contains("exceeded max tokens")
241 || lower.contains("maximum context length")
242 }
243
244 pub fn session(&self) -> &crate::agent::session::Session {
249 self.mgr.session()
250 }
251
252 pub fn session_manager(&self) -> &crate::agent::session::SessionManager {
254 &self.mgr
255 }
256
257 pub fn session_mut(&mut self) -> &mut crate::agent::session::Session {
259 self.mgr.session_mut()
260 }
261
262 pub fn into_session(self) -> crate::agent::session::Session {
264 self.mgr.into_session()
265 }
266
267 pub fn ensure_flushed(&mut self) {
270 self.mgr.ensure_flushed();
271 }
272
273 pub fn cwd(&self) -> &std::path::Path {
276 self.mgr.cwd()
277 }
278
279 pub fn session_dir(&self) -> &std::path::Path {
280 self.mgr.session_dir()
281 }
282
283 pub fn is_persisted(&self) -> bool {
284 self.mgr.is_persisted()
285 }
286
287 pub fn session_id(&self) -> String {
288 self.mgr.session().session_id()
289 }
290
291 pub fn session_file(&self) -> Option<std::path::PathBuf> {
292 self.mgr.session().session_file()
293 }
294
295 pub fn session_name(&self) -> Option<String> {
296 self.mgr.session().session_name()
297 }
298
299 pub fn on_model_change(&mut self, provider: &str, model_id: &str) -> bool {
304 let new = (provider.to_string(), model_id.to_string());
305 if self.last_model.as_ref() != Some(&new) {
306 self.mgr
307 .session_mut()
308 .append_model_change(provider, model_id);
309 self.last_model = Some(new);
310 true
311 } else {
312 false
313 }
314 }
315
316 pub fn on_thinking_level_change(&mut self, level: &str) -> bool {
319 if self.last_thinking_level != level {
320 self.mgr.session_mut().append_thinking_level_change(level);
321 self.last_thinking_level = level.to_string();
322 true
323 } else {
324 false
325 }
326 }
327
328 pub fn on_active_tools_change(&mut self, tools: &[String]) -> bool {
331 let tools_vec = tools.to_vec();
332 if self.last_active_tools.as_ref() != Some(&tools_vec) {
333 self.mgr
334 .session_mut()
335 .append_active_tools_change(&tools_vec);
336 self.last_active_tools = Some(tools_vec);
337 true
338 } else {
339 false
340 }
341 }
342
343 pub fn new_session(&mut self) {
348 self.mgr.new_session(None);
349 self.last_model = None;
350 self.last_thinking_level = String::new();
351 self.last_active_tools = None;
352 self.compaction_cancel = crate::agent::extension::Cancel::new();
353 }
354
355 pub fn send_user_message(&mut self, content: &str) -> String {
358 let msg = user_message(content);
359 self.mgr.append_message(&msg)
360 }
361
362 pub fn send_user_message_obj(&mut self, msg: &AgentMessage) -> String {
365 self.mgr.append_message(msg)
366 }
367
368 pub fn on_agent_event(&mut self, event: &yoagent::types::AgentEvent) {
382 if let yoagent::types::AgentEvent::MessageEnd { message } = event {
384 if crate::agent::types::message_is_user(message) {
387 self.reset_overflow_recovery();
388 }
389 if crate::agent::types::message_is_extension(message) {
393 self.persist_extension_message(message);
394 } else {
395 let cost = self.compute_message_cost(message);
397 self.mgr.append_message_with_cost(message, cost);
398 }
399 }
400 }
401
402 fn compute_message_cost(&self, message: &AgentMessage) -> MessageCost {
406 let (provider, model_id, usage) = match message {
408 AgentMessage::Llm(Message::Assistant {
409 provider,
410 model,
411 usage,
412 ..
413 }) => (provider.as_str(), model.as_str(), usage),
414 _ => return MessageCost::ZERO,
415 };
416
417 let Some(ref registry) = self.registry else {
418 return MessageCost::ZERO;
419 };
420
421 let Ok(resolved) = registry.resolve(model_id, Some(provider)) else {
423 return MessageCost::ZERO;
424 };
425
426 let cost_config = &resolved.model_config.cost;
427 let (input, output, cache_read, cache_write, _total) =
428 crate::provider::calculate_cost(cost_config, usage);
429 MessageCost::new(input, output, cache_read, cache_write)
430 }
431
432 pub async fn check_auto_compact(&mut self) -> Result<bool, String> {
438 Ok(self
439 ._run_compaction(CompactionReason::Threshold, None, false)
440 .await?
441 .is_some())
442 }
443
444 pub async fn check_overflow_compact(&mut self, will_retry: bool) -> Result<bool, String> {
448 if self.overflow_recovery_attempted {
449 return Ok(false);
450 }
451 self.overflow_recovery_attempted = true;
452 Ok(self
453 ._run_compaction(CompactionReason::Overflow, None, will_retry)
454 .await?
455 .is_some())
456 }
457
458 pub async fn run_manual_compact(
461 &mut self,
462 custom_instructions: Option<&str>,
463 ) -> Result<String, String> {
464 let result = self
465 ._run_compaction(CompactionReason::Manual, custom_instructions, false)
466 .await?;
467 Ok(result.map(|r| r.summary).unwrap_or_default())
468 }
469
470 async fn _run_compaction(
473 &mut self,
474 reason: CompactionReason,
475 custom_instructions: Option<&str>,
476 will_retry: bool,
477 ) -> Result<Option<CompactionResult>, String> {
478 if reason == CompactionReason::Threshold && !self.compaction_settings.enabled {
480 return Ok(None);
481 }
482
483 if self.compaction_api_key.is_none() || self.model_name.is_empty() {
484 return Ok(None);
485 }
486
487 self.compaction_cancel = crate::agent::extension::Cancel::new();
490 let cancel = self.compaction_cancel.clone();
491
492 self.emit_compaction_event(&CompactionEvent::Start { reason });
494
495 if cancel.is_cancelled() {
497 return Ok(None);
498 }
499
500 let entries = self.mgr.get_entries();
501
502 if reason == CompactionReason::Threshold {
504 let context_msgs = self.mgr.session().build_context().messages;
505 let context_tokens = compaction::estimate_context_tokens(&context_msgs);
506 if !compaction::should_compact(
507 context_tokens,
508 self.context_window,
509 &self.compaction_settings,
510 ) {
511 return Ok(None);
512 }
513 }
514
515 let Some(prep) = prepare_compaction(&entries, &self.compaction_settings) else {
516 return Ok(None);
517 };
518
519 let mut from_hook = false;
521 let mut hook_summary: Option<String> = None;
522 let mut hook_details: Option<serde_json::Value> = None;
523
524 for ext in &self.extensions {
525 if cancel.is_cancelled() {
526 break;
527 }
528 if let Some(result) = ext.before_compact(
529 &prep.first_kept_entry_id,
530 prep.tokens_before,
531 &reason.to_string(),
532 &cancel,
533 ) {
534 if result.cancel {
535 self.emit_compaction_event(&CompactionEvent::End {
536 reason,
537 aborted: true,
538 will_retry: false,
539 error_message: Some("Compaction cancelled by extension".to_string()),
540 result: CompactionResult {
541 summary: String::new(),
542 first_kept_entry_id: prep.first_kept_entry_id.clone(),
543 tokens_before: prep.tokens_before,
544 estimated_tokens_after: 0,
545 details: None,
546 },
547 });
548 return Ok(None);
549 }
550 if result.summary.is_some() {
551 hook_summary = result.summary;
552 hook_details = result.details;
553 from_hook = true;
554 break;
555 }
556 }
557 }
558
559 let result = if let Some(summary) = hook_summary {
560 CompactionResult {
562 summary,
563 first_kept_entry_id: prep.first_kept_entry_id.clone(),
564 tokens_before: prep.tokens_before,
565 estimated_tokens_after: 0, details: hook_details,
567 }
568 } else {
569 let api_key = self.compaction_api_key.as_ref().unwrap();
571 compact(
572 &prep,
573 api_key,
574 &self.model_name,
575 custom_instructions,
576 self.thinking_level,
577 self.model_config.clone(),
578 )
579 .await?
580 };
581
582 self.mgr.session_mut().append_compaction(
584 &result.summary,
585 &result.first_kept_entry_id,
586 result.tokens_before,
587 result.details.clone(),
588 Some(from_hook),
589 );
590
591 let context_after = self.mgr.session().build_context().messages;
593 let estimated_tokens_after = compaction::estimate_context_tokens(&context_after);
594
595 let final_result = CompactionResult {
596 estimated_tokens_after,
597 ..result
598 };
599
600 for ext in &self.extensions {
602 if cancel.is_cancelled() {
603 break;
604 }
605 ext.after_compact(
606 &final_result.summary,
607 &final_result.first_kept_entry_id,
608 final_result.tokens_before,
609 final_result.estimated_tokens_after,
610 from_hook,
611 &reason.to_string(),
612 &cancel,
613 );
614 }
615
616 self.emit_compaction_event(&CompactionEvent::End {
618 reason,
619 result: final_result.clone(),
620 aborted: false,
621 will_retry,
622 error_message: None,
623 });
624
625 Ok(Some(final_result))
626 }
627
628 pub async fn summarize_branch_navigation(
638 &mut self,
639 old_leaf_id: Option<&str>,
640 target_id: &str,
641 custom_instructions: Option<&str>,
642 ) -> Result<String, String> {
643 if self.compaction_api_key.is_none() || self.model_name.is_empty() {
644 return Err("No provider configured for summarization".to_string());
645 }
646
647 let (entries, _common_ancestor) =
648 collect_entries_for_branch_summary(self.session(), old_leaf_id, target_id);
649
650 if entries.is_empty() {
651 return Err("No abandoned entries to summarize".to_string());
652 }
653
654 let api_key = self.compaction_api_key.as_ref().unwrap();
655 generate_branch_summary(
656 self.mgr.session_mut(),
657 &entries,
658 target_id,
659 api_key,
660 &self.model_name,
661 self.thinking_level,
662 self.model_config.clone(),
663 custom_instructions,
664 )
665 .await
666 }
667
668 pub async fn set_branch(
673 &mut self,
674 branch_from_id: &str,
675 custom_instructions: Option<&str>,
676 ) -> Result<Option<String>, String> {
677 let old_leaf = self.mgr.session().get_leaf_id();
678
679 let summary = if self.compaction_api_key.is_some()
680 && !self.model_name.is_empty()
681 && let Some(ref old) = old_leaf
682 && old != branch_from_id
683 {
684 match self
686 .summarize_branch_navigation(Some(old), branch_from_id, custom_instructions)
687 .await
688 {
689 Ok(s) => Some(s),
690 Err(e) => {
691 eprintln!("Warning: branch summarization failed: {}", e);
693 None
694 }
695 }
696 } else {
697 None
698 };
699
700 self.mgr
701 .session_mut()
702 .set_leaf_id(Some(branch_from_id))
703 .map_err(|e| format!("Failed to set branch: {}", e))?;
704
705 Ok(summary)
706 }
707
708 pub fn persist_extension_message(&mut self, msg: &AgentMessage) {
714 let Some(kind) = crate::agent::types::message_extension_kind(msg) else {
715 return;
716 };
717 let text = crate::agent::types::message_extension_text(msg)
718 .unwrap_or_else(|| crate::agent::types::message_text(msg));
719 let content = serde_json::json!({"text": text});
720 self.mgr
721 .session_mut()
722 .append_custom_message_entry(kind, content, true, None);
723 }
724}