1use dashmap::DashMap;
7use std::collections::{BTreeMap, HashMap};
8use std::error::Error;
9use std::path::PathBuf;
10use std::sync::Arc;
11use std::sync::OnceLock;
12use std::sync::atomic::{AtomicBool, Ordering};
13use std::time::Instant;
14use tokio::sync::broadcast;
15
16use claude_code_agent_sdk::types::config::PermissionMode as SdkPermissionMode;
17use claude_code_agent_sdk::types::mcp::McpSdkServerConfig;
18use claude_code_agent_sdk::{
19 ClaudeAgentOptions, ClaudeClient, HookEvent, HookMatcher, McpServerConfig, McpServers,
20 SystemPrompt, SystemPromptPreset,
21};
22use sacp::JrConnectionCx;
23use sacp::link::AgentToClient;
24use sacp::schema::McpServer;
25use tokio::sync::RwLock;
26use tracing::instrument;
27
28use crate::converter::NotificationConverter;
29use crate::hooks::{HookCallbackRegistry, create_post_tool_use_hook, create_pre_tool_use_hook};
30use crate::mcp::AcpMcpServer;
31use crate::permissions::create_can_use_tool_callback;
32use crate::settings::{PermissionChecker, SettingsManager};
33use crate::terminal::TerminalClient;
34use crate::types::{AgentConfig, AgentError, NewSessionMeta, Result};
35
36use super::BackgroundProcessManager;
37use super::permission::{PermissionHandler, PermissionMode};
38use super::usage::UsageTracker;
39
40fn get_acp_replacement_tools() -> Vec<&'static str> {
48 vec![
49 "Bash",
51 "BashOutput",
52 "KillShell",
53 "Read",
55 "Write",
56 "Edit",
57 ]
58}
59
60pub struct Session {
65 pub session_id: String,
67 pub cwd: PathBuf,
69 client: RwLock<ClaudeClient>,
71 cancelled: Arc<AtomicBool>,
73 permission: Arc<RwLock<PermissionHandler>>,
75 usage_tracker: UsageTracker,
77 converter: NotificationConverter,
79 connected: AtomicBool,
81 hook_callback_registry: Arc<HookCallbackRegistry>,
83 permission_checker: Arc<RwLock<PermissionChecker>>,
85 permission_mode: Arc<RwLock<PermissionMode>>,
87 current_model: OnceLock<String>,
89 acp_mcp_server: Arc<AcpMcpServer>,
91 background_processes: Arc<BackgroundProcessManager>,
93 external_mcp_servers: OnceLock<Vec<McpServer>>,
96 external_mcp_connected: AtomicBool,
98 connection_cx_lock: Arc<OnceLock<JrConnectionCx<AgentToClient>>>,
101 cancel_sender: broadcast::Sender<()>,
103 permission_cache: Arc<DashMap<String, bool>>,
108 tool_use_id_cache: Arc<DashMap<String, String>>,
113}
114
115pub fn stable_cache_key(tool_input: &serde_json::Value) -> String {
121 fn canonicalize(value: &serde_json::Value) -> serde_json::Value {
122 match value {
123 serde_json::Value::Object(map) => {
124 let sorted: BTreeMap<_, _> = map
126 .iter()
127 .map(|(k, v)| (k.clone(), canonicalize(v)))
128 .collect();
129 serde_json::Value::Object(sorted.into_iter().collect())
130 }
131 serde_json::Value::Array(arr) => {
132 serde_json::Value::Array(arr.iter().map(canonicalize).collect())
133 }
134 other => other.clone(),
135 }
136 }
137 canonicalize(tool_input).to_string()
138}
139
140impl Session {
141 #[instrument(
153 name = "session_create",
154 skip(config, meta),
155 fields(
156 session_id = %session_id,
157 cwd = ?cwd,
158 has_meta = meta.is_some(),
159 )
160 )]
161 pub fn new(
162 session_id: String,
163 cwd: PathBuf,
164 config: &AgentConfig,
165 meta: Option<&NewSessionMeta>,
166 ) -> Result<Arc<Self>> {
167 let start_time = Instant::now();
168
169 tracing::info!(
170 session_id = %session_id,
171 cwd = ?cwd,
172 "Creating new session"
173 );
174
175 let hook_callback_registry = Arc::new(HookCallbackRegistry::new());
177
178 let settings_manager = SettingsManager::new(&cwd)
181 .unwrap_or_else(|e| {
182 tracing::warn!("Failed to load settings manager from cwd: {}. Using default settings.", e);
183 match dirs::home_dir() {
185 Some(home) => {
186 tracing::info!("Attempting to load settings from home directory");
187 SettingsManager::new(&home).unwrap_or_else(|e2| {
188 tracing::error!("Failed to load settings from home directory: {}. Using minimal default settings.", e2);
189 SettingsManager::new_with_settings(crate::settings::Settings::default(), "/")
191 })
192 }
193 None => {
194 tracing::error!("No home directory found. Using minimal default settings.");
195 SettingsManager::new_with_settings(crate::settings::Settings::default(), "/")
196 }
197 }
198 });
199 let permission_checker = Arc::new(RwLock::new(PermissionChecker::new(
202 settings_manager.settings().clone(),
203 &cwd,
204 )));
205
206 let permission_mode = Arc::new(RwLock::new(PermissionMode::Default));
210
211 let connection_cx_lock: Arc<OnceLock<JrConnectionCx<AgentToClient>>> =
213 Arc::new(OnceLock::new());
214
215 let permission_cache: Arc<DashMap<String, bool>> = Arc::new(DashMap::new());
218
219 let tool_use_id_cache: Arc<DashMap<String, String>> = Arc::new(DashMap::new());
223
224 let pre_tool_use_hook = create_pre_tool_use_hook(
226 connection_cx_lock.clone(),
227 session_id.clone(),
228 Some(permission_checker.clone()),
229 permission_mode.clone(),
230 permission_cache.clone(),
231 tool_use_id_cache.clone(),
232 );
233 let post_tool_use_hook = create_post_tool_use_hook(hook_callback_registry.clone());
234
235 let mut hooks_map: HashMap<HookEvent, Vec<HookMatcher>> = HashMap::new();
237 hooks_map.insert(
238 HookEvent::PreToolUse,
239 vec![
240 HookMatcher::builder()
241 .hooks(vec![pre_tool_use_hook])
242 .build(),
243 ],
244 );
245 hooks_map.insert(
246 HookEvent::PostToolUse,
247 vec![
248 HookMatcher::builder()
249 .hooks(vec![post_tool_use_hook])
250 .build(),
251 ],
252 );
253
254 tracing::info!(
255 session_id = %session_id,
256 hooks_count = 2,
257 "Hooks configured: PreToolUse, PostToolUse"
258 );
259
260 let permission_handler = Arc::new(RwLock::new(PermissionHandler::with_checker(
264 permission_checker.clone(),
265 )));
266
267 let session_lock: Arc<OnceLock<Arc<Session>>> = Arc::new(OnceLock::new());
269
270 let acp_mcp_server = Arc::new(AcpMcpServer::new("acp", env!("CARGO_PKG_VERSION")));
272
273 let background_processes = Arc::new(BackgroundProcessManager::new());
275
276 let mut mcp_servers_dict = HashMap::new();
278 mcp_servers_dict.insert(
279 "acp".to_string(),
280 McpServerConfig::Sdk(McpSdkServerConfig {
281 name: "acp".to_string(),
282 instance: acp_mcp_server.clone(),
283 }),
284 );
285
286 tracing::info!(
287 session_id = %session_id,
288 mcp_server_count = mcp_servers_dict.len(),
289 "MCP servers configured"
290 );
291
292 let can_use_tool_callback = create_can_use_tool_callback(session_lock.clone());
294
295 let mut options = ClaudeAgentOptions::builder()
303 .cwd(cwd.clone())
304 .hooks(hooks_map)
305 .mcp_servers(McpServers::Dict(mcp_servers_dict))
306 .can_use_tool(can_use_tool_callback)
307 .permission_mode(SdkPermissionMode::AcceptEdits)
308 .build();
309
310 tracing::info!(
312 session_id = %session_id,
313 has_can_use_tool = options.can_use_tool.is_some(),
314 has_hooks = options.hooks.is_some(),
315 "Options configured after build"
316 );
317
318 match &options.mcp_servers {
320 McpServers::Dict(dict) => {
321 tracing::debug!(
322 session_id = %session_id,
323 servers = ?dict.keys().collect::<Vec<_>>(),
324 "MCP servers registered"
325 );
326 }
327 McpServers::Empty => {
328 tracing::warn!(
329 session_id = %session_id,
330 "MCP servers is Empty - this is unexpected!"
331 );
332 }
333 McpServers::Path(p) => {
334 tracing::warn!(
335 session_id = %session_id,
336 path = ?p,
337 "MCP servers is Path - this is unexpected!"
338 );
339 }
340 }
341
342 let acp_tools = get_acp_replacement_tools();
345 options.use_acp_tools(&acp_tools);
346
347 options.include_partial_messages = true;
350
351 tracing::debug!(
352 session_id = %session_id,
353 acp_tools = ?acp_tools,
354 disallowed_tools = ?options.disallowed_tools,
355 allowed_tools = ?options.allowed_tools,
356 "ACP tools configured"
357 );
358
359 config.apply_to_options(&mut options);
361
362 tracing::debug!(
363 session_id = %session_id,
364 model = ?options.model,
365 fallback_model = ?options.fallback_model,
366 max_thinking_tokens = ?options.max_thinking_tokens,
367 base_url = ?config.base_url,
368 api_key = ?config.masked_api_key(),
369 env_vars_count = options.env.len(),
370 "Agent config applied"
371 );
372
373 if let Some(meta) = meta {
375 if let Some(replace) = meta.get_system_prompt_replace() {
377 options.system_prompt = Some(SystemPrompt::Text(replace.to_string()));
379 tracing::info!(
380 session_id = %session_id,
381 prompt_len = replace.len(),
382 "Using custom system prompt from meta (replace)"
383 );
384 } else if let Some(append) = meta.get_system_prompt_append() {
385 let preset = SystemPromptPreset::with_append("claude_code", append);
387 options.system_prompt = Some(SystemPrompt::Preset(preset));
388 tracing::info!(
389 session_id = %session_id,
390 append_len = append.len(),
391 "Appending to system prompt from meta"
392 );
393 }
394
395 if let Some(resume_id) = meta.get_resume_session_id() {
397 options.resume = Some(resume_id.to_string());
398 tracing::info!(
399 session_id = %session_id,
400 resume_session_id = %resume_id,
401 "Resuming from previous session"
402 );
403 }
404
405 if let Some(tokens) = meta.get_max_thinking_tokens() {
407 options.max_thinking_tokens = Some(tokens);
408 tracing::info!(
409 session_id = %session_id,
410 max_thinking_tokens = tokens,
411 "Extended thinking mode enabled via meta"
412 );
413 }
414 }
415
416 let client = ClaudeClient::new(options);
418
419 let elapsed = start_time.elapsed();
420 tracing::info!(
421 session_id = %session_id,
422 elapsed_ms = elapsed.as_millis(),
423 "Session created successfully"
424 );
425
426 let cwd_for_converter = cwd.clone();
428
429 let session = Self {
431 session_id,
432 cwd,
433 client: RwLock::new(client),
434 cancelled: Arc::new(AtomicBool::new(false)),
435 permission: permission_handler,
436 usage_tracker: UsageTracker::new(),
437 converter: NotificationConverter::with_cwd(cwd_for_converter),
438 connected: AtomicBool::new(false),
439 hook_callback_registry,
440 permission_checker,
441 permission_mode,
442 current_model: OnceLock::new(),
443 acp_mcp_server,
444 background_processes,
445 external_mcp_servers: OnceLock::new(),
446 external_mcp_connected: AtomicBool::new(false),
447 connection_cx_lock,
448 cancel_sender: broadcast::channel(1).0,
449 permission_cache,
450 tool_use_id_cache,
451 };
452
453 let session_arc = Arc::new(session);
455
456 drop(session_lock.set(session_arc.clone()));
458
459 Ok(session_arc)
460 }
461
462 pub fn set_external_mcp_servers(&self, servers: Vec<McpServer>) {
468 if !servers.is_empty() {
469 tracing::info!(
470 session_id = %self.session_id,
471 server_count = servers.len(),
472 "Storing external MCP servers for later connection"
473 );
474
475 for server in &servers {
476 match server {
477 McpServer::Stdio(s) => {
478 tracing::debug!(
479 session_id = %self.session_id,
480 server_name = %s.name,
481 command = ?s.command,
482 args = ?s.args,
483 "External MCP server (stdio)"
484 );
485 }
486 McpServer::Http(s) => {
487 tracing::debug!(
488 session_id = %self.session_id,
489 server_name = %s.name,
490 url = %s.url,
491 "External MCP server (http)"
492 );
493 }
494 McpServer::Sse(s) => {
495 tracing::debug!(
496 session_id = %self.session_id,
497 server_name = %s.name,
498 url = %s.url,
499 "External MCP server (sse)"
500 );
501 }
502 _ => {
503 tracing::debug!(
504 session_id = %self.session_id,
505 "External MCP server (unknown type)"
506 );
507 }
508 }
509 }
510 }
511
512 if self.external_mcp_servers.get().is_none() {
514 drop(self.external_mcp_servers.set(servers));
515 }
516 }
517
518 pub fn set_connection_cx(&self, cx: JrConnectionCx<AgentToClient>) {
523 if self.connection_cx_lock.get().is_none() {
524 drop(self.connection_cx_lock.set(cx));
525 }
526 }
527
528 pub fn get_connection_cx(&self) -> Option<&JrConnectionCx<AgentToClient>> {
532 self.connection_cx_lock.get()
533 }
534
535 pub fn cache_permission(&self, tool_input: &serde_json::Value, allowed: bool) {
540 let key = stable_cache_key(tool_input);
541 tracing::debug!(
542 key_len = key.len(),
543 allowed = allowed,
544 "Caching permission result"
545 );
546 self.permission_cache.insert(key, allowed);
547 }
548
549 pub fn check_cached_permission(&self, tool_input: &serde_json::Value) -> Option<bool> {
555 let key = stable_cache_key(tool_input);
556 self.permission_cache.remove(&key).map(|(_, v)| v)
557 }
558
559 pub fn permission_cache(&self) -> Arc<DashMap<String, bool>> {
561 Arc::clone(&self.permission_cache)
562 }
563
564 pub fn cache_tool_use_id(&self, tool_input: &serde_json::Value, tool_use_id: &str) {
569 let key = stable_cache_key(tool_input);
570 tracing::debug!(
571 key_len = key.len(),
572 tool_use_id = %tool_use_id,
573 "Caching tool_use_id"
574 );
575 self.tool_use_id_cache.insert(key, tool_use_id.to_string());
576 }
577
578 pub fn get_cached_tool_use_id(&self, tool_input: &serde_json::Value) -> Option<String> {
584 let key = stable_cache_key(tool_input);
585 self.tool_use_id_cache.remove(&key).map(|(_, v)| v)
586 }
587
588 pub fn tool_use_id_cache(&self) -> Arc<DashMap<String, String>> {
590 Arc::clone(&self.tool_use_id_cache)
591 }
592
593 #[instrument(
598 name = "connect_external_mcp_servers",
599 skip(self),
600 fields(session_id = %self.session_id)
601 )]
602 pub async fn connect_external_mcp_servers(&self) -> Result<()> {
603 if self.external_mcp_connected.load(Ordering::SeqCst) {
605 tracing::debug!(
606 session_id = %self.session_id,
607 "External MCP servers already connected"
608 );
609 return Ok(());
610 }
611
612 let servers = match self.external_mcp_servers.get() {
614 Some(s) => s,
615 None => {
616 tracing::debug!(
617 session_id = %self.session_id,
618 "No external MCP servers to connect"
619 );
620 self.external_mcp_connected.store(true, Ordering::SeqCst);
621 return Ok(());
622 }
623 };
624
625 let servers_vec: Vec<_> = servers.iter().cloned().collect();
627
628 let server_count = servers_vec.len();
629 let start_time = Instant::now();
630
631 tracing::info!(
632 session_id = %self.session_id,
633 server_count = server_count,
634 "Connecting to external MCP servers"
635 );
636
637 let external_manager = self.acp_mcp_server.mcp_server().external_manager();
638
639 let mut success_count = 0;
640 let mut error_count = 0;
641
642 for server in servers_vec.iter() {
643 match server {
644 McpServer::Stdio(s) => {
645 let server_start = Instant::now();
646
647 tracing::info!(
648 session_id = %self.session_id,
649 server_name = %s.name,
650 command = ?s.command,
651 args = ?s.args,
652 "Connecting to external MCP server (stdio)"
653 );
654
655 let env: Option<HashMap<String, String>> = if s.env.is_empty() {
657 None
658 } else {
659 Some(
660 s.env
661 .iter()
662 .map(|e| (e.name.clone(), e.value.clone()))
663 .collect(),
664 )
665 };
666
667 match external_manager
668 .connect(
669 s.name.clone(),
670 s.command.to_string_lossy().as_ref(),
671 &s.args,
672 env.as_ref(),
673 Some(self.cwd.as_path()),
674 )
675 .await
676 {
677 Ok(_) => {
678 success_count += 1;
679 let elapsed = server_start.elapsed();
680 tracing::info!(
681 session_id = %self.session_id,
682 server_name = %s.name,
683 elapsed_ms = elapsed.as_millis(),
684 "Successfully connected to external MCP server"
685 );
686 }
687 Err(e) => {
688 error_count += 1;
689 let elapsed = server_start.elapsed();
690 tracing::error!(
691 session_id = %self.session_id,
692 server_name = %s.name,
693 error = %e,
694 elapsed_ms = elapsed.as_millis(),
695 "Failed to connect to external MCP server"
696 );
697 }
698 }
699 }
700 McpServer::Http(s) => {
701 tracing::warn!(
702 session_id = %self.session_id,
703 server_name = %s.name,
704 url = %s.url,
705 "HTTP MCP servers not yet supported"
706 );
707 }
708 McpServer::Sse(s) => {
709 tracing::warn!(
710 session_id = %self.session_id,
711 server_name = %s.name,
712 url = %s.url,
713 "SSE MCP servers not yet supported"
714 );
715 }
716 _ => {
717 tracing::warn!(
718 session_id = %self.session_id,
719 "Unknown MCP server type - not supported"
720 );
721 }
722 }
723 }
724
725 let total_elapsed = start_time.elapsed();
726 tracing::info!(
727 session_id = %self.session_id,
728 total_servers = server_count,
729 success_count = success_count,
730 error_count = error_count,
731 total_elapsed_ms = total_elapsed.as_millis(),
732 "Finished connecting external MCP servers"
733 );
734
735 self.external_mcp_connected.store(true, Ordering::SeqCst);
736 Ok(())
737 }
738
739 #[instrument(
743 name = "session_connect",
744 skip(self),
745 fields(session_id = %self.session_id)
746 )]
747 pub async fn connect(&self) -> Result<()> {
748 if self.connected.load(Ordering::SeqCst) {
749 tracing::debug!(
750 session_id = %self.session_id,
751 "Already connected to Claude CLI"
752 );
753 return Ok(());
754 }
755
756 let start_time = Instant::now();
757 tracing::info!(
758 session_id = %self.session_id,
759 cwd = ?self.cwd,
760 "Connecting to Claude CLI..."
761 );
762
763 let mut client = self.client.write().await;
764 client.connect().await.map_err(|e| {
765 let agent_error = AgentError::from(e);
766 tracing::error!(
767 session_id = %self.session_id,
768 error = %agent_error,
769 error_code = ?agent_error.error_code(),
770 is_retryable = %agent_error.is_retryable(),
771 error_chain = ?agent_error.source(),
772 "Failed to connect to Claude CLI"
773 );
774 agent_error
775 })?;
776
777 self.connected.store(true, Ordering::SeqCst);
778
779 let elapsed = start_time.elapsed();
780 tracing::info!(
781 session_id = %self.session_id,
782 elapsed_ms = elapsed.as_millis(),
783 "Successfully connected to Claude CLI"
784 );
785
786 Ok(())
787 }
788
789 #[instrument(
791 name = "session_disconnect",
792 skip(self),
793 fields(session_id = %self.session_id)
794 )]
795 pub async fn disconnect(&self) -> Result<()> {
796 if !self.connected.load(Ordering::SeqCst) {
797 tracing::debug!(
798 session_id = %self.session_id,
799 "Already disconnected from Claude CLI"
800 );
801 return Ok(());
802 }
803
804 let start_time = Instant::now();
805 tracing::info!(
806 session_id = %self.session_id,
807 "Disconnecting from Claude CLI..."
808 );
809
810 let mut client = self.client.write().await;
811 client.disconnect().await.map_err(|e| {
812 let agent_error = AgentError::from(e);
813 tracing::error!(
814 session_id = %self.session_id,
815 error = %agent_error,
816 error_code = ?agent_error.error_code(),
817 is_retryable = %agent_error.is_retryable(),
818 error_chain = ?agent_error.source(),
819 "Failed to disconnect from Claude CLI"
820 );
821 agent_error
822 })?;
823
824 self.connected.store(false, Ordering::SeqCst);
825
826 let elapsed = start_time.elapsed();
827 tracing::info!(
828 session_id = %self.session_id,
829 elapsed_ms = elapsed.as_millis(),
830 "Disconnected from Claude CLI"
831 );
832
833 Ok(())
834 }
835
836 pub fn is_connected(&self) -> bool {
838 self.connected.load(Ordering::SeqCst)
839 }
840
841 pub async fn client(&self) -> tokio::sync::RwLockReadGuard<'_, ClaudeClient> {
843 self.client.read().await
844 }
845
846 pub async fn client_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, ClaudeClient> {
848 self.client.write().await
849 }
850
851 pub fn cancel_receiver(&self) -> broadcast::Receiver<()> {
856 self.cancel_sender.subscribe()
857 }
858
859 pub fn is_cancelled(&self) -> bool {
861 self.cancelled.load(Ordering::SeqCst)
862 }
863
864 #[instrument(
866 name = "session_cancel",
867 skip(self),
868 fields(session_id = %self.session_id)
869 )]
870 pub async fn cancel(&self) {
871 tracing::info!(
872 session_id = %self.session_id,
873 "Cancelling session and sending interrupt signal"
874 );
875
876 self.cancelled.store(true, Ordering::SeqCst);
877
878 if let Ok(client) = self.client.try_read() {
880 if let Err(e) = client.interrupt().await {
881 tracing::warn!(
882 session_id = %self.session_id,
883 error = %e,
884 "Failed to send interrupt signal to Claude CLI"
885 );
886 } else {
887 tracing::info!(
888 session_id = %self.session_id,
889 "Interrupt signal sent to Claude CLI"
890 );
891 }
892 } else {
893 tracing::warn!(
894 session_id = %self.session_id,
895 "Could not acquire client lock for interrupt"
896 );
897 }
898 }
899
900 pub fn reset_cancelled(&self) {
902 self.cancelled.store(false, Ordering::SeqCst);
903 }
904
905 pub async fn permission(&self) -> tokio::sync::RwLockReadGuard<'_, PermissionHandler> {
907 self.permission.read().await
908 }
909
910 pub async fn permission_mode(&self) -> PermissionMode {
912 self.permission.read().await.mode()
913 }
914
915 pub async fn set_permission_mode(&self, mode: PermissionMode) {
920 self.permission.write().await.set_mode(mode);
922 *self.permission_mode.write().await = mode;
924
925 tracing::info!(
926 session_id = %self.session_id,
927 mode = mode.as_str(),
928 "Permission mode updated"
929 );
930 }
931
932 pub async fn add_permission_allow_rule(&self, tool_name: &str) {
936 self.permission.read().await.add_allow_rule(tool_name).await;
937 }
938
939 #[allow(dead_code)]
943 pub fn current_model(&self) -> Option<String> {
944 self.current_model.get().cloned()
945 }
946
947 #[allow(dead_code)]
951 pub fn set_model(&self, model_id: String) {
952 if self.current_model.get().is_none() {
954 drop(self.current_model.set(model_id));
955 }
956 }
957
958 pub fn usage_tracker(&self) -> &UsageTracker {
960 &self.usage_tracker
961 }
962
963 pub fn converter(&self) -> &NotificationConverter {
965 &self.converter
966 }
967
968 pub fn hook_callback_registry(&self) -> &Arc<HookCallbackRegistry> {
970 &self.hook_callback_registry
971 }
972
973 pub fn permission_checker(&self) -> &Arc<RwLock<PermissionChecker>> {
975 &self.permission_checker
976 }
977
978 pub fn register_post_tool_use_callback(
980 &self,
981 tool_use_id: String,
982 callback: crate::hooks::PostToolUseCallback,
983 ) {
984 self.hook_callback_registry
985 .register_post_tool_use(tool_use_id, callback);
986 }
987
988 pub fn acp_mcp_server(&self) -> &Arc<AcpMcpServer> {
990 &self.acp_mcp_server
991 }
992
993 pub fn background_processes(&self) -> &Arc<BackgroundProcessManager> {
995 &self.background_processes
996 }
997
998 pub async fn configure_acp_server(
1003 &self,
1004 connection_cx: JrConnectionCx<AgentToClient>,
1005 terminal_client: Option<Arc<TerminalClient>>,
1006 ) {
1007 self.acp_mcp_server.set_session_id(&self.session_id);
1008 self.acp_mcp_server.set_connection(connection_cx);
1009 self.acp_mcp_server.set_cwd(self.cwd.clone()).await;
1010 self.acp_mcp_server
1011 .set_background_processes(self.background_processes.clone());
1012 self.acp_mcp_server
1013 .set_permission_checker(self.permission_checker.clone());
1014
1015 if let Some(client) = terminal_client {
1016 self.acp_mcp_server.set_terminal_client(client);
1017 }
1018
1019 let cancelled_flag = self.cancelled.clone();
1021 let session_id = self.session_id.clone();
1022 let cancel_sender = self.cancel_sender.clone();
1023
1024 self.acp_mcp_server
1025 .set_cancel_callback(move || {
1026 tracing::info!(
1027 session_id = %session_id,
1028 "MCP cancel callback invoked, sending cancel signal"
1029 );
1030 cancelled_flag.store(true, Ordering::SeqCst);
1031 let _ = cancel_sender.send(());
1033 })
1034 .await;
1035 }
1036}
1037
1038#[allow(clippy::missing_fields_in_debug)]
1039impl std::fmt::Debug for Session {
1040 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1041 f.debug_struct("Session")
1042 .field("session_id", &self.session_id)
1043 .field("cwd", &self.cwd)
1044 .field("cancelled", &self.cancelled.load(Ordering::Relaxed))
1045 .field("connected", &self.connected.load(Ordering::Relaxed))
1046 .finish()
1047 }
1048}
1049
1050#[cfg(test)]
1051mod tests {
1052 use super::*;
1053
1054 fn test_config() -> AgentConfig {
1055 AgentConfig {
1056 base_url: None,
1057 api_key: None,
1058 model: None,
1059 small_fast_model: None,
1060 max_thinking_tokens: None,
1061 }
1062 }
1063
1064 #[test]
1065 fn test_session_new() {
1066 let session = Session::new(
1067 "test-session-1".to_string(),
1068 PathBuf::from("/tmp"),
1069 &test_config(),
1070 None,
1071 )
1072 .unwrap();
1073
1074 assert_eq!(session.session_id, "test-session-1");
1075 assert_eq!(session.cwd, PathBuf::from("/tmp"));
1076 assert!(!session.is_cancelled());
1077 assert!(!session.is_connected());
1078 }
1079
1080 #[tokio::test]
1081 async fn test_session_cancel() {
1082 let session = Session::new(
1083 "test-session-2".to_string(),
1084 PathBuf::from("/tmp"),
1085 &test_config(),
1086 None,
1087 )
1088 .unwrap();
1089
1090 assert!(!session.is_cancelled());
1091 session.cancel().await;
1092 assert!(session.is_cancelled());
1093 session.reset_cancelled();
1094 assert!(!session.is_cancelled());
1095 }
1096
1097 #[tokio::test]
1098 async fn test_session_permission_mode() {
1099 let session = Session::new(
1100 "test-session-3".to_string(),
1101 PathBuf::from("/tmp"),
1102 &test_config(),
1103 None,
1104 )
1105 .unwrap();
1106
1107 assert_eq!(session.permission_mode().await, PermissionMode::Default);
1109 session.set_permission_mode(PermissionMode::DontAsk).await;
1110 assert_eq!(session.permission_mode().await, PermissionMode::DontAsk);
1111 }
1112
1113 #[test]
1114 fn test_stable_cache_key_ordering() {
1115 use serde_json::json;
1116
1117 let json1 = json!({"a": 1, "b": 2, "c": 3});
1119 let json2 = json!({"c": 3, "b": 2, "a": 1});
1120 let json3 = json!({"b": 2, "a": 1, "c": 3});
1121
1122 let key1 = stable_cache_key(&json1);
1123 let key2 = stable_cache_key(&json2);
1124 let key3 = stable_cache_key(&json3);
1125
1126 assert_eq!(
1127 key1, key2,
1128 "Different key ordering should produce same cache key"
1129 );
1130 assert_eq!(
1131 key2, key3,
1132 "Different key ordering should produce same cache key"
1133 );
1134 }
1135
1136 #[test]
1137 fn test_stable_cache_key_nested_objects() {
1138 use serde_json::json;
1139
1140 let json1 = json!({
1142 "command": "cargo build",
1143 "options": {"a": 1, "b": 2}
1144 });
1145 let json2 = json!({
1146 "options": {"b": 2, "a": 1},
1147 "command": "cargo build"
1148 });
1149
1150 let key1 = stable_cache_key(&json1);
1151 let key2 = stable_cache_key(&json2);
1152
1153 assert_eq!(key1, key2, "Nested objects should also produce stable keys");
1154 }
1155
1156 #[test]
1157 fn test_stable_cache_key_arrays() {
1158 use serde_json::json;
1159
1160 let json1 = json!({
1162 "items": [{"a": 1, "b": 2}, {"c": 3, "d": 4}]
1163 });
1164 let json2 = json!({
1165 "items": [{"b": 2, "a": 1}, {"d": 4, "c": 3}]
1166 });
1167
1168 let key1 = stable_cache_key(&json1);
1169 let key2 = stable_cache_key(&json2);
1170
1171 assert_eq!(key1, key2, "Arrays with objects should produce stable keys");
1172 }
1173
1174 #[test]
1175 fn test_stable_cache_key_different_content() {
1176 use serde_json::json;
1177
1178 let json1 = json!({"command": "cargo build"});
1180 let json2 = json!({"command": "cargo test"});
1181
1182 let key1 = stable_cache_key(&json1);
1183 let key2 = stable_cache_key(&json2);
1184
1185 assert_ne!(
1186 key1, key2,
1187 "Different content should produce different keys"
1188 );
1189 }
1190}