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::{
25 CurrentModeUpdate, McpServer, SessionId, SessionModeId, SessionNotification, SessionUpdate,
26};
27use tokio::sync::RwLock;
28use tracing::instrument;
29
30use crate::converter::NotificationConverter;
31use crate::hooks::{HookCallbackRegistry, create_post_tool_use_hook, create_pre_tool_use_hook};
32use crate::mcp::AcpMcpServer;
33use crate::permissions::create_can_use_tool_callback;
34use crate::settings::{PermissionChecker, SettingsManager};
35use crate::terminal::TerminalClient;
36use crate::types::{AgentConfig, AgentError, NewSessionMeta, Result};
37
38use super::BackgroundProcessManager;
39use super::permission::{PermissionHandler, PermissionMode};
40use super::usage::UsageTracker;
41
42fn get_acp_replacement_tools() -> Vec<&'static str> {
50 vec![
51 "Bash",
53 "BashOutput",
54 "KillShell",
55 "Read",
57 "Write",
58 "Edit",
59 ]
60}
61
62pub struct Session {
67 pub session_id: String,
69 pub cwd: PathBuf,
71 client: RwLock<ClaudeClient>,
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 current_model: OnceLock<String>,
87 acp_mcp_server: Arc<AcpMcpServer>,
89 background_processes: Arc<BackgroundProcessManager>,
91 external_mcp_servers: OnceLock<Vec<McpServer>>,
94 external_mcp_connected: AtomicBool,
96 connection_cx_lock: Arc<OnceLock<JrConnectionCx<AgentToClient>>>,
99 cancel_sender: broadcast::Sender<()>,
101 permission_cache: Arc<DashMap<String, bool>>,
106 tool_use_id_cache: Arc<DashMap<String, String>>,
111}
112
113pub fn stable_cache_key(tool_input: &serde_json::Value) -> String {
119 fn canonicalize(value: &serde_json::Value) -> serde_json::Value {
120 match value {
121 serde_json::Value::Object(map) => {
122 let sorted: BTreeMap<_, _> = map
124 .iter()
125 .map(|(k, v)| (k.clone(), canonicalize(v)))
126 .collect();
127 serde_json::Value::Object(sorted.into_iter().collect())
128 }
129 serde_json::Value::Array(arr) => {
130 serde_json::Value::Array(arr.iter().map(canonicalize).collect())
131 }
132 other => other.clone(),
133 }
134 }
135 canonicalize(tool_input).to_string()
136}
137
138impl Session {
139 #[instrument(
151 name = "session_create",
152 skip(config, meta),
153 fields(
154 session_id = %session_id,
155 cwd = ?cwd,
156 has_meta = meta.is_some(),
157 )
158 )]
159 pub fn new(
160 session_id: String,
161 cwd: PathBuf,
162 config: &AgentConfig,
163 meta: Option<&NewSessionMeta>,
164 ) -> Result<Arc<Self>> {
165 let start_time = Instant::now();
166
167 tracing::info!(
168 session_id = %session_id,
169 cwd = ?cwd,
170 "Creating new session"
171 );
172
173 let hook_callback_registry = Arc::new(HookCallbackRegistry::new());
175
176 let settings_manager = SettingsManager::new(&cwd)
179 .unwrap_or_else(|e| {
180 tracing::warn!("Failed to load settings manager from cwd: {}. Using default settings.", e);
181 if let Some(home) = dirs::home_dir() {
183 tracing::info!("Attempting to load settings from home directory");
184 SettingsManager::new(&home).unwrap_or_else(|e2| {
185 tracing::error!("Failed to load settings from home directory: {}. Using minimal default settings.", e2);
186 SettingsManager::new_with_settings(crate::settings::Settings::default(), "/")
188 })
189 } else {
190 tracing::error!("No home directory found. Using minimal default settings.");
191 SettingsManager::new_with_settings(crate::settings::Settings::default(), "/")
192 }
193 });
194 let permission_checker = Arc::new(RwLock::new(PermissionChecker::new(
197 settings_manager.settings().clone(),
198 &cwd,
199 )));
200
201 let permission_handler = Arc::new(RwLock::new(PermissionHandler::with_checker(
205 permission_checker.clone(),
206 )));
207
208 let connection_cx_lock: Arc<OnceLock<JrConnectionCx<AgentToClient>>> =
210 Arc::new(OnceLock::new());
211
212 let permission_cache: Arc<DashMap<String, bool>> = Arc::new(DashMap::new());
215
216 let tool_use_id_cache: Arc<DashMap<String, String>> = Arc::new(DashMap::new());
220
221 let pre_tool_use_hook = create_pre_tool_use_hook(
223 connection_cx_lock.clone(),
224 session_id.clone(),
225 Some(permission_checker.clone()),
226 permission_handler.clone(),
227 permission_cache.clone(),
228 tool_use_id_cache.clone(),
229 );
230 let post_tool_use_hook = create_post_tool_use_hook(hook_callback_registry.clone());
231
232 let mut hooks_map: HashMap<HookEvent, Vec<HookMatcher>> = HashMap::new();
234 hooks_map.insert(
235 HookEvent::PreToolUse,
236 vec![
237 HookMatcher::builder()
238 .hooks(vec![pre_tool_use_hook])
239 .build(),
240 ],
241 );
242 hooks_map.insert(
243 HookEvent::PostToolUse,
244 vec![
245 HookMatcher::builder()
246 .hooks(vec![post_tool_use_hook])
247 .build(),
248 ],
249 );
250
251 tracing::info!(
252 session_id = %session_id,
253 hooks_count = 2,
254 "Hooks configured: PreToolUse, PostToolUse"
255 );
256
257 let session_lock: Arc<OnceLock<Arc<Session>>> = Arc::new(OnceLock::new());
259
260 let acp_mcp_server = Arc::new(AcpMcpServer::new("acp", env!("CARGO_PKG_VERSION")));
262
263 let background_processes = Arc::new(BackgroundProcessManager::new());
265
266 let mut mcp_servers_dict = HashMap::new();
268 mcp_servers_dict.insert(
269 "acp".to_string(),
270 McpServerConfig::Sdk(McpSdkServerConfig {
271 name: "acp".to_string(),
272 instance: acp_mcp_server.clone(),
273 }),
274 );
275
276 tracing::info!(
277 session_id = %session_id,
278 mcp_server_count = mcp_servers_dict.len(),
279 "MCP servers configured"
280 );
281
282 let can_use_tool_callback = create_can_use_tool_callback(session_lock.clone());
284
285 let mut options = ClaudeAgentOptions::builder()
293 .cwd(cwd.clone())
294 .hooks(hooks_map)
295 .mcp_servers(McpServers::Dict(mcp_servers_dict))
296 .can_use_tool(can_use_tool_callback)
297 .permission_mode(SdkPermissionMode::AcceptEdits)
298 .build();
299
300 tracing::info!(
302 session_id = %session_id,
303 has_can_use_tool = options.can_use_tool.is_some(),
304 has_hooks = options.hooks.is_some(),
305 "Options configured after build"
306 );
307
308 match &options.mcp_servers {
310 McpServers::Dict(dict) => {
311 tracing::debug!(
312 session_id = %session_id,
313 servers = ?dict.keys().collect::<Vec<_>>(),
314 "MCP servers registered"
315 );
316 }
317 McpServers::Empty => {
318 tracing::warn!(
319 session_id = %session_id,
320 "MCP servers is Empty - this is unexpected!"
321 );
322 }
323 McpServers::Path(p) => {
324 tracing::warn!(
325 session_id = %session_id,
326 path = ?p,
327 "MCP servers is Path - this is unexpected!"
328 );
329 }
330 }
331
332 let acp_tools = get_acp_replacement_tools();
335 options.use_acp_tools(&acp_tools);
336
337 options.include_partial_messages = true;
340
341 tracing::debug!(
342 session_id = %session_id,
343 acp_tools = ?acp_tools,
344 disallowed_tools = ?options.disallowed_tools,
345 allowed_tools = ?options.allowed_tools,
346 "ACP tools configured"
347 );
348
349 config.apply_to_options(&mut options);
351
352 tracing::debug!(
353 session_id = %session_id,
354 model = ?options.model,
355 fallback_model = ?options.fallback_model,
356 max_thinking_tokens = ?options.max_thinking_tokens,
357 base_url = ?config.base_url,
358 api_key = ?config.masked_api_key(),
359 env_vars_count = options.env.len(),
360 "Agent config applied"
361 );
362
363 if let Some(meta) = meta {
365 if let Some(replace) = meta.get_system_prompt_replace() {
367 options.system_prompt = Some(SystemPrompt::Text(replace.to_string()));
369 tracing::info!(
370 session_id = %session_id,
371 prompt_len = replace.len(),
372 "Using custom system prompt from meta (replace)"
373 );
374 } else if let Some(append) = meta.get_system_prompt_append() {
375 let preset = SystemPromptPreset::with_append("claude_code", append);
377 options.system_prompt = Some(SystemPrompt::Preset(preset));
378 tracing::info!(
379 session_id = %session_id,
380 append_len = append.len(),
381 "Appending to system prompt from meta"
382 );
383 }
384
385 if let Some(resume_id) = meta.get_resume_session_id() {
387 options.resume = Some(resume_id.to_string());
388 tracing::info!(
389 session_id = %session_id,
390 resume_session_id = %resume_id,
391 "Resuming from previous session"
392 );
393 }
394
395 if let Some(tokens) = meta.get_max_thinking_tokens() {
397 options.max_thinking_tokens = Some(tokens);
398 tracing::info!(
399 session_id = %session_id,
400 max_thinking_tokens = tokens,
401 "Extended thinking mode enabled via meta"
402 );
403 }
404 }
405
406 let client = ClaudeClient::new(options);
408
409 let elapsed = start_time.elapsed();
410 tracing::info!(
411 session_id = %session_id,
412 elapsed_ms = elapsed.as_millis(),
413 "Session created successfully"
414 );
415
416 let cwd_for_converter = cwd.clone();
418
419 let session = Self {
421 session_id,
422 cwd,
423 client: RwLock::new(client),
424 permission: permission_handler,
425 usage_tracker: UsageTracker::new(),
426 converter: NotificationConverter::with_cwd(cwd_for_converter),
427 connected: AtomicBool::new(false),
428 hook_callback_registry,
429 permission_checker,
430 current_model: OnceLock::new(),
431 acp_mcp_server,
432 background_processes,
433 external_mcp_servers: OnceLock::new(),
434 external_mcp_connected: AtomicBool::new(false),
435 connection_cx_lock,
436 cancel_sender: broadcast::channel(1).0,
437 permission_cache,
438 tool_use_id_cache,
439 };
440
441 let session_arc = Arc::new(session);
443
444 drop(session_lock.set(session_arc.clone()));
446
447 Ok(session_arc)
448 }
449
450 pub fn set_external_mcp_servers(&self, servers: Vec<McpServer>) {
456 if !servers.is_empty() {
457 tracing::info!(
458 session_id = %self.session_id,
459 server_count = servers.len(),
460 "Storing external MCP servers for later connection"
461 );
462
463 for server in &servers {
464 match server {
465 McpServer::Stdio(s) => {
466 tracing::debug!(
467 session_id = %self.session_id,
468 server_name = %s.name,
469 command = ?s.command,
470 args = ?s.args,
471 "External MCP server (stdio)"
472 );
473 }
474 McpServer::Http(s) => {
475 tracing::debug!(
476 session_id = %self.session_id,
477 server_name = %s.name,
478 url = %s.url,
479 "External MCP server (http)"
480 );
481 }
482 McpServer::Sse(s) => {
483 tracing::debug!(
484 session_id = %self.session_id,
485 server_name = %s.name,
486 url = %s.url,
487 "External MCP server (sse)"
488 );
489 }
490 _ => {
491 tracing::debug!(
492 session_id = %self.session_id,
493 "External MCP server (unknown type)"
494 );
495 }
496 }
497 }
498 }
499
500 if self.external_mcp_servers.get().is_none() {
502 drop(self.external_mcp_servers.set(servers));
503 }
504 }
505
506 pub fn set_connection_cx(&self, cx: JrConnectionCx<AgentToClient>) {
511 if self.connection_cx_lock.get().is_none() {
512 drop(self.connection_cx_lock.set(cx));
513 }
514 }
515
516 pub fn get_connection_cx(&self) -> Option<&JrConnectionCx<AgentToClient>> {
520 self.connection_cx_lock.get()
521 }
522
523 pub fn cache_permission(&self, tool_input: &serde_json::Value, allowed: bool) {
528 let key = stable_cache_key(tool_input);
529 tracing::debug!(
530 key_len = key.len(),
531 allowed = allowed,
532 "Caching permission result"
533 );
534 self.permission_cache.insert(key, allowed);
535 }
536
537 pub fn check_cached_permission(&self, tool_input: &serde_json::Value) -> Option<bool> {
543 let key = stable_cache_key(tool_input);
544 self.permission_cache.remove(&key).map(|(_, v)| v)
545 }
546
547 pub fn permission_cache(&self) -> Arc<DashMap<String, bool>> {
549 Arc::clone(&self.permission_cache)
550 }
551
552 pub fn cache_tool_use_id(&self, tool_input: &serde_json::Value, tool_use_id: &str) {
557 let key = stable_cache_key(tool_input);
558 tracing::debug!(
559 key_len = key.len(),
560 tool_use_id = %tool_use_id,
561 "Caching tool_use_id"
562 );
563 self.tool_use_id_cache.insert(key, tool_use_id.to_string());
564 }
565
566 pub fn get_cached_tool_use_id(&self, tool_input: &serde_json::Value) -> Option<String> {
572 let key = stable_cache_key(tool_input);
573 self.tool_use_id_cache.remove(&key).map(|(_, v)| v)
574 }
575
576 pub fn tool_use_id_cache(&self) -> Arc<DashMap<String, String>> {
578 Arc::clone(&self.tool_use_id_cache)
579 }
580
581 #[instrument(
586 name = "connect_external_mcp_servers",
587 skip(self),
588 fields(session_id = %self.session_id)
589 )]
590 pub async fn connect_external_mcp_servers(&self) -> Result<()> {
591 if self.external_mcp_connected.load(Ordering::SeqCst) {
593 tracing::debug!(
594 session_id = %self.session_id,
595 "External MCP servers already connected"
596 );
597 return Ok(());
598 }
599
600 let Some(servers) = self.external_mcp_servers.get() else {
602 tracing::debug!(
603 session_id = %self.session_id,
604 "No external MCP servers to connect"
605 );
606 self.external_mcp_connected.store(true, Ordering::SeqCst);
607 return Ok(());
608 };
609
610 let servers_vec: Vec<_> = servers.clone();
612
613 let server_count = servers_vec.len();
614 let start_time = Instant::now();
615
616 tracing::info!(
617 session_id = %self.session_id,
618 server_count = server_count,
619 "Connecting to external MCP servers"
620 );
621
622 let external_manager = self.acp_mcp_server.mcp_server().external_manager();
623
624 let mut success_count = 0;
625 let mut error_count = 0;
626
627 for server in &servers_vec {
628 match server {
629 McpServer::Stdio(s) => {
630 let server_start = Instant::now();
631
632 tracing::info!(
633 session_id = %self.session_id,
634 server_name = %s.name,
635 command = ?s.command,
636 args = ?s.args,
637 "Connecting to external MCP server (stdio)"
638 );
639
640 let env: Option<HashMap<String, String>> = if s.env.is_empty() {
642 None
643 } else {
644 Some(
645 s.env
646 .iter()
647 .map(|e| (e.name.clone(), e.value.clone()))
648 .collect(),
649 )
650 };
651
652 match external_manager
653 .connect(
654 s.name.clone(),
655 s.command.to_string_lossy().as_ref(),
656 &s.args,
657 env.as_ref(),
658 Some(self.cwd.as_path()),
659 )
660 .await
661 {
662 Ok(()) => {
663 success_count += 1;
664 let elapsed = server_start.elapsed();
665 tracing::info!(
666 session_id = %self.session_id,
667 server_name = %s.name,
668 elapsed_ms = elapsed.as_millis(),
669 "Successfully connected to external MCP server"
670 );
671 }
672 Err(e) => {
673 error_count += 1;
674 let elapsed = server_start.elapsed();
675 tracing::error!(
676 session_id = %self.session_id,
677 server_name = %s.name,
678 error = %e,
679 elapsed_ms = elapsed.as_millis(),
680 "Failed to connect to external MCP server"
681 );
682 }
683 }
684 }
685 McpServer::Http(s) => {
686 tracing::warn!(
687 session_id = %self.session_id,
688 server_name = %s.name,
689 url = %s.url,
690 "HTTP MCP servers not yet supported"
691 );
692 }
693 McpServer::Sse(s) => {
694 tracing::warn!(
695 session_id = %self.session_id,
696 server_name = %s.name,
697 url = %s.url,
698 "SSE MCP servers not yet supported"
699 );
700 }
701 _ => {
702 tracing::warn!(
703 session_id = %self.session_id,
704 "Unknown MCP server type - not supported"
705 );
706 }
707 }
708 }
709
710 let total_elapsed = start_time.elapsed();
711 tracing::info!(
712 session_id = %self.session_id,
713 total_servers = server_count,
714 success_count = success_count,
715 error_count = error_count,
716 total_elapsed_ms = total_elapsed.as_millis(),
717 "Finished connecting external MCP servers"
718 );
719
720 self.external_mcp_connected.store(true, Ordering::SeqCst);
721 Ok(())
722 }
723
724 #[instrument(
728 name = "session_connect",
729 skip(self),
730 fields(session_id = %self.session_id)
731 )]
732 pub async fn connect(&self) -> Result<()> {
733 if self.connected.load(Ordering::SeqCst) {
734 tracing::debug!(
735 session_id = %self.session_id,
736 "Already connected to Claude CLI"
737 );
738 return Ok(());
739 }
740
741 let start_time = Instant::now();
742 tracing::info!(
743 session_id = %self.session_id,
744 cwd = ?self.cwd,
745 "Connecting to Claude CLI..."
746 );
747
748 let mut client = self.client.write().await;
749 client.connect().await.map_err(|e| {
750 let agent_error = AgentError::from(e);
751 tracing::error!(
752 session_id = %self.session_id,
753 error = %agent_error,
754 error_code = ?agent_error.error_code(),
755 is_retryable = %agent_error.is_retryable(),
756 error_chain = ?agent_error.source(),
757 "Failed to connect to Claude CLI"
758 );
759 agent_error
760 })?;
761
762 self.connected.store(true, Ordering::SeqCst);
763
764 let elapsed = start_time.elapsed();
765 tracing::info!(
766 session_id = %self.session_id,
767 elapsed_ms = elapsed.as_millis(),
768 "Successfully connected to Claude CLI"
769 );
770
771 Ok(())
772 }
773
774 #[instrument(
776 name = "session_disconnect",
777 skip(self),
778 fields(session_id = %self.session_id)
779 )]
780 pub async fn disconnect(&self) -> Result<()> {
781 if !self.connected.load(Ordering::SeqCst) {
782 tracing::debug!(
783 session_id = %self.session_id,
784 "Already disconnected from Claude CLI"
785 );
786 return Ok(());
787 }
788
789 let start_time = Instant::now();
790 tracing::info!(
791 session_id = %self.session_id,
792 "Disconnecting from Claude CLI..."
793 );
794
795 let mut client = self.client.write().await;
796 client.disconnect().await.map_err(|e| {
797 let agent_error = AgentError::from(e);
798 tracing::error!(
799 session_id = %self.session_id,
800 error = %agent_error,
801 error_code = ?agent_error.error_code(),
802 is_retryable = %agent_error.is_retryable(),
803 error_chain = ?agent_error.source(),
804 "Failed to disconnect from Claude CLI"
805 );
806 agent_error
807 })?;
808
809 self.connected.store(false, Ordering::SeqCst);
810
811 let elapsed = start_time.elapsed();
812 tracing::info!(
813 session_id = %self.session_id,
814 elapsed_ms = elapsed.as_millis(),
815 "Disconnected from Claude CLI"
816 );
817
818 Ok(())
819 }
820
821 pub fn is_connected(&self) -> bool {
823 self.connected.load(Ordering::SeqCst)
824 }
825
826 pub async fn client(&self) -> tokio::sync::RwLockReadGuard<'_, ClaudeClient> {
828 self.client.read().await
829 }
830
831 pub async fn client_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, ClaudeClient> {
833 self.client.write().await
834 }
835
836 pub fn cancel_receiver(&self) -> broadcast::Receiver<()> {
841 self.cancel_sender.subscribe()
842 }
843
844 #[instrument(
850 name = "session_cancel",
851 skip(self),
852 fields(session_id = %self.session_id)
853 )]
854 pub async fn cancel(&self) {
855 tracing::info!(
856 session_id = %self.session_id,
857 "Sending interrupt signal to Claude CLI"
858 );
859
860 if let Ok(client) = self.client.try_read() {
862 if let Err(e) = client.interrupt().await {
863 tracing::warn!(
864 session_id = %self.session_id,
865 error = %e,
866 "Failed to send interrupt signal to Claude CLI"
867 );
868 } else {
869 tracing::info!(
870 session_id = %self.session_id,
871 "Interrupt signal sent to Claude CLI"
872 );
873 }
874 } else {
875 tracing::warn!(
876 session_id = %self.session_id,
877 "Could not acquire client lock for interrupt"
878 );
879 }
880 }
881
882 pub async fn permission(&self) -> tokio::sync::RwLockReadGuard<'_, PermissionHandler> {
884 self.permission.read().await
885 }
886
887 pub async fn permission_mode(&self) -> PermissionMode {
889 self.permission.read().await.mode()
890 }
891
892 pub async fn set_permission_mode(&self, mode: PermissionMode) {
897 self.permission.write().await.set_mode(mode);
899
900 tracing::info!(
901 session_id = %self.session_id,
902 mode = mode.as_str(),
903 "Permission mode updated"
904 );
905 }
906
907 pub fn send_mode_update(&self, mode: &str) {
913 let Some(connection_cx) = self.get_connection_cx() else {
914 tracing::warn!(
915 session_id = %self.session_id,
916 mode = %mode,
917 "Connection not ready for mode update notification"
918 );
919 return;
920 };
921
922 let mode_update = CurrentModeUpdate::new(SessionModeId::new(mode));
923 let notification = SessionNotification::new(
924 SessionId::new(self.session_id.clone()),
925 SessionUpdate::CurrentModeUpdate(mode_update),
926 );
927
928 if let Err(e) = connection_cx.send_notification(notification) {
929 tracing::warn!(
930 session_id = %self.session_id,
931 mode = %mode,
932 error = %e,
933 "Failed to send CurrentModeUpdate notification"
934 );
935 } else {
936 tracing::info!(
937 session_id = %self.session_id,
938 mode = %mode,
939 "Sent CurrentModeUpdate notification"
940 );
941 }
942 }
943
944 pub async fn add_permission_allow_rule(&self, tool_name: &str) {
948 self.permission.read().await.add_allow_rule(tool_name).await;
949 }
950
951 #[allow(dead_code)]
955 pub fn current_model(&self) -> Option<String> {
956 self.current_model.get().cloned()
957 }
958
959 #[allow(dead_code)]
963 pub fn set_model(&self, model_id: String) {
964 if self.current_model.get().is_none() {
966 drop(self.current_model.set(model_id));
967 }
968 }
969
970 pub fn usage_tracker(&self) -> &UsageTracker {
972 &self.usage_tracker
973 }
974
975 pub fn converter(&self) -> &NotificationConverter {
977 &self.converter
978 }
979
980 pub fn hook_callback_registry(&self) -> &Arc<HookCallbackRegistry> {
982 &self.hook_callback_registry
983 }
984
985 pub fn permission_checker(&self) -> &Arc<RwLock<PermissionChecker>> {
987 &self.permission_checker
988 }
989
990 pub fn register_post_tool_use_callback(
992 &self,
993 tool_use_id: String,
994 callback: crate::hooks::PostToolUseCallback,
995 ) {
996 self.hook_callback_registry
997 .register_post_tool_use(tool_use_id, callback);
998 }
999
1000 pub fn acp_mcp_server(&self) -> &Arc<AcpMcpServer> {
1002 &self.acp_mcp_server
1003 }
1004
1005 pub fn background_processes(&self) -> &Arc<BackgroundProcessManager> {
1007 &self.background_processes
1008 }
1009
1010 pub async fn configure_acp_server(
1015 &self,
1016 connection_cx: JrConnectionCx<AgentToClient>,
1017 terminal_client: Option<Arc<TerminalClient>>,
1018 ) {
1019 self.acp_mcp_server.set_session_id(&self.session_id);
1020 self.acp_mcp_server.set_connection(connection_cx);
1021 self.acp_mcp_server.set_cwd(self.cwd.clone()).await;
1022 self.acp_mcp_server
1023 .set_background_processes(self.background_processes.clone());
1024 self.acp_mcp_server
1025 .set_permission_checker(self.permission_checker.clone());
1026
1027 if let Some(client) = terminal_client {
1028 self.acp_mcp_server.set_terminal_client(client);
1029 }
1030
1031 let session_id = self.session_id.clone();
1033 let cancel_sender = self.cancel_sender.clone();
1034
1035 self.acp_mcp_server
1036 .set_cancel_callback(move || {
1037 tracing::info!(
1038 session_id = %session_id,
1039 "MCP cancel callback invoked, sending cancel signal"
1040 );
1041 let _ = cancel_sender.send(());
1044 })
1045 .await;
1046 }
1047}
1048
1049#[allow(clippy::missing_fields_in_debug)]
1050impl std::fmt::Debug for Session {
1051 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1052 f.debug_struct("Session")
1053 .field("session_id", &self.session_id)
1054 .field("cwd", &self.cwd)
1055 .field("connected", &self.connected.load(Ordering::Relaxed))
1056 .finish()
1057 }
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062 use super::*;
1063
1064 fn test_config() -> AgentConfig {
1065 AgentConfig {
1066 base_url: None,
1067 api_key: None,
1068 model: None,
1069 small_fast_model: None,
1070 max_thinking_tokens: None,
1071 }
1072 }
1073
1074 #[test]
1075 fn test_session_new() {
1076 let session = Session::new(
1077 "test-session-1".to_string(),
1078 PathBuf::from("/tmp"),
1079 &test_config(),
1080 None,
1081 )
1082 .unwrap();
1083
1084 assert_eq!(session.session_id, "test-session-1");
1085 assert_eq!(session.cwd, PathBuf::from("/tmp"));
1086 assert!(!session.is_connected());
1087 }
1088
1089 #[tokio::test]
1090 async fn test_session_cancel() {
1091 let session = Session::new(
1092 "test-session-2".to_string(),
1093 PathBuf::from("/tmp"),
1094 &test_config(),
1095 None,
1096 )
1097 .unwrap();
1098
1099 session.cancel().await;
1102 }
1103
1104 #[tokio::test]
1105 async fn test_session_permission_mode() {
1106 let session = Session::new(
1107 "test-session-3".to_string(),
1108 PathBuf::from("/tmp"),
1109 &test_config(),
1110 None,
1111 )
1112 .unwrap();
1113
1114 assert_eq!(session.permission_mode().await, PermissionMode::Default);
1116 session.set_permission_mode(PermissionMode::DontAsk).await;
1117 assert_eq!(session.permission_mode().await, PermissionMode::DontAsk);
1118 }
1119
1120 #[test]
1121 fn test_stable_cache_key_ordering() {
1122 use serde_json::json;
1123
1124 let json1 = json!({"a": 1, "b": 2, "c": 3});
1126 let json2 = json!({"c": 3, "b": 2, "a": 1});
1127 let json3 = json!({"b": 2, "a": 1, "c": 3});
1128
1129 let key1 = stable_cache_key(&json1);
1130 let key2 = stable_cache_key(&json2);
1131 let key3 = stable_cache_key(&json3);
1132
1133 assert_eq!(
1134 key1, key2,
1135 "Different key ordering should produce same cache key"
1136 );
1137 assert_eq!(
1138 key2, key3,
1139 "Different key ordering should produce same cache key"
1140 );
1141 }
1142
1143 #[test]
1144 fn test_stable_cache_key_nested_objects() {
1145 use serde_json::json;
1146
1147 let json1 = json!({
1149 "command": "cargo build",
1150 "options": {"a": 1, "b": 2}
1151 });
1152 let json2 = json!({
1153 "options": {"b": 2, "a": 1},
1154 "command": "cargo build"
1155 });
1156
1157 let key1 = stable_cache_key(&json1);
1158 let key2 = stable_cache_key(&json2);
1159
1160 assert_eq!(key1, key2, "Nested objects should also produce stable keys");
1161 }
1162
1163 #[test]
1164 fn test_stable_cache_key_arrays() {
1165 use serde_json::json;
1166
1167 let json1 = json!({
1169 "items": [{"a": 1, "b": 2}, {"c": 3, "d": 4}]
1170 });
1171 let json2 = json!({
1172 "items": [{"b": 2, "a": 1}, {"d": 4, "c": 3}]
1173 });
1174
1175 let key1 = stable_cache_key(&json1);
1176 let key2 = stable_cache_key(&json2);
1177
1178 assert_eq!(key1, key2, "Arrays with objects should produce stable keys");
1179 }
1180
1181 #[test]
1182 fn test_stable_cache_key_different_content() {
1183 use serde_json::json;
1184
1185 let json1 = json!({"command": "cargo build"});
1187 let json2 = json!({"command": "cargo test"});
1188
1189 let key1 = stable_cache_key(&json1);
1190 let key2 = stable_cache_key(&json2);
1191
1192 assert_ne!(
1193 key1, key2,
1194 "Different content should produce different keys"
1195 );
1196 }
1197}