//! # ACP Session Manager //! //! 基于 project_id 管理 ACP 会话的核心模块。 //! //! ## SACP 迁移说明 //! //! 本模块已迁移至 SACP 实现: //! - 移除了 `Client` trait 泛型参数(SACP 内部处理) //! - 简化了 `create_session` 参数(不再需要 client 和 shared_api_key_manager) //! - 使用标准 tokio::spawn(无需 LocalSet) //! //! ## 职责范围 //! //! 1. **会话生命周期管理** //! - 创建新会话 (`create_session`) //! - 获取或复用会话 (`get_or_create_session`) //! - 移除会话 (`remove_session`) //! - 检测会话健康状态(channel 是否关闭) //! //! 2. **Prompt 构建** //! - 构建纯文本 Prompt (`build_text_prompt_request`) //! - 构建带附件的 Prompt (`build_prompt_request_with_attachments`) //! - 将 request_id 放入 meta 字段 //! //! 3. **路径处理** //! - 路径规范化 (`normalize_path`) //! - 确保项目目录存在 (`ensure_project_dir`) //! //! ## 架构说明 //! //! 使用依赖注入的 `SessionRegistry` 进行会话存储: //! - 统一使用注入的 `SessionRegistry`(通常是 `AGENT_REGISTRY`) //! //! ## 与 Worker 的协作 //! //! ```text //! AcpAgentWorker (acp_worker.rs) //! │ //! │ 1. 调用 get_or_create_session() //! ▼ //! AcpSessionManager //! │ //! │ 2. 通过 SessionRegistry 获取/创建会话 //! │ 3. 通过 SacpClaudeCodeLauncher 启动 Agent //! ▼ //! SessionRegistry (注入的 AGENT_REGISTRY) //! ``` use std::path::{Component, PathBuf}; use std::sync::Arc; use agent_config::PromptBuilder; use anyhow::Result; use chrono::Utc; use agent_client_protocol::schema::{ContentBlock, PromptRequest, SessionId, TextContent}; use shared_types::{ AgentLifecycle, AgentStatus, ModelProviderConfig, ProjectAndAgentInfo, SessionEntry, }; use tracing::{debug, error, info}; use crate::PromptMessage; use crate::launcher::ClaudeCodeLauncher; use crate::traits::{AgentStartConfig, SessionNotifier, SessionRegistry}; /// ACP 会话管理器 (SACP 版本) /// /// 管理所有活跃的 ACP 会话,提供: /// - 会话创建和复用 /// - 模型配置变化检测 /// - 会话生命周期管理 /// /// ## 泛型参数 /// - `N`: SessionNotifier 实现,用于推送 SSE 消息 /// - `R`: SessionRegistry 实现,用于存储会话数据(通常是 AGENT_REGISTRY) pub struct AcpSessionManager { /// 会话注册表(注入的 SessionRegistry) registry: Arc, /// 会话通知器 notifier: Arc, } impl AcpSessionManager where R::Entry: Into + From, { /// 创建新的会话管理器 /// /// # 参数 /// - `notifier`: 会话通知器 /// - `registry`: 会话注册表(通常注入 AGENT_REGISTRY) pub fn new(notifier: Arc, registry: Arc) -> Self { Self { registry, notifier } } /// 获取会话信息 pub fn get_session(&self, project_id: &str) -> Option { self.registry.get(project_id) } /// 检查会话是否存在 pub fn contains_session(&self, project_id: &str) -> bool { self.registry.contains(project_id) } /// 移除会话 pub fn remove_session(&self, project_id: &str) -> Option { self.registry.remove(project_id) } /// 获取所有会话的 project_id 列表 pub fn list_sessions(&self) -> Vec { self.registry.list_project_ids() } /// 获取会话数量 pub fn session_count(&self) -> usize { self.registry.count() } /// 获取 registry 的 Arc 引用 pub fn registry(&self) -> Arc { self.registry.clone() } /// 获取 notifier 的 Arc 引用 pub fn notifier(&self) -> Arc { self.notifier.clone() } /// 规范化项目路径 /// /// - 如果是相对路径,先与当前目录拼接 /// - 去除路径中的 "./"(CurDir 组件) pub fn normalize_path(path: &PathBuf) -> PathBuf { let joined_path = if path.is_absolute() { path.clone() } else { std::env::current_dir().unwrap_or_default().join(path) }; joined_path .components() .filter(|c| !matches!(c, Component::CurDir)) .collect() } /// 确保项目目录存在 pub async fn ensure_project_dir(path: &PathBuf) -> Result<()> { if !path.exists() { info!("Project path does not exist, creating: {:?}", path); tokio::fs::create_dir_all(path).await?; } Ok(()) } /// 构建基础 Prompt 请求(仅文本内容) /// /// 使用 ACP 协议分离模式: /// - 系统提示词已在 NewSessionRequest._meta.systemPrompt 中传递 /// - 这里只构建纯用户提示词 /// /// 注意:附件转换由调用方处理(因为涉及文件 I/O) pub fn build_text_prompt_request( prompt: &PromptMessage, session_id: SessionId, ) -> Result { // 构建纯用户提示词 let final_prompt = if prompt.data_source_attachments.is_empty() { PromptBuilder::build_user_prompt(&prompt.content) } else { PromptBuilder::build_user_prompt_with_data_sources( &prompt.content, &prompt.data_source_attachments, ) }; // 创建文本内容块 let text_block = ContentBlock::Text(TextContent::new(final_prompt)); let content_blocks = vec![text_block]; // 将 request_id 放入 meta 字段 debug!( "🔧 [build_prompt] Putting request_id={} into PromptRequest.meta", prompt.request_id ); let mut meta = serde_json::Map::new(); meta.insert( "request_id".to_string(), serde_json::Value::String(prompt.request_id.clone()), ); Ok(PromptRequest::new(session_id, content_blocks).meta(meta)) } /// 构建带附件的 Prompt 请求 /// /// # Arguments /// * `prompt` - 提示消息 /// * `session_id` - 会话 ID /// * `attachment_blocks` - 已转换的附件内容块 pub fn build_prompt_request_with_attachments( prompt: &PromptMessage, session_id: SessionId, attachment_blocks: Vec, ) -> Result { // 构建纯用户提示词 let final_prompt = if prompt.data_source_attachments.is_empty() { PromptBuilder::build_user_prompt(&prompt.content) } else { PromptBuilder::build_user_prompt_with_data_sources( &prompt.content, &prompt.data_source_attachments, ) }; // 创建文本内容块 let text_block = ContentBlock::Text(TextContent::new(final_prompt)); let mut content_blocks = vec![text_block]; // 添加附件内容块 content_blocks.extend(attachment_blocks); // 将 request_id 放入 meta 字段 debug!( "🔧 [build_prompt] Putting request_id={} into PromptRequest.meta", prompt.request_id ); let mut meta = serde_json::Map::new(); meta.insert( "request_id".to_string(), serde_json::Value::String(prompt.request_id.clone()), ); Ok(PromptRequest::new(session_id, content_blocks).meta(meta)) } /// 创建新的 Agent 会话 (SACP 版本) /// /// 启动 Agent 进程并建立 SACP 连接 /// /// # 参数 /// - `project_id`: 项目 ID /// - `project_path`: 项目路径 /// - `model_provider`: 模型提供者配置 /// - `start_config`: Agent 启动配置 /// - `service_uuid`: 与此 Agent 关联的唯一 UUID /// /// # 返回值 /// - `R::Entry`: 会话条目 pub async fn create_session( &self, project_id: String, project_path: PathBuf, _session_id_hint: Option, model_provider: Option, start_config: AgentStartConfig, service_uuid: Option, ) -> Result { let agent_info = self .create_session_internal( project_id.clone(), project_path, model_provider, start_config, service_uuid, ) .await?; // 存储会话信息到 registry let session_id_str = agent_info.session_id().to_string(); self.registry .insert(&project_id, &session_id_str, agent_info.clone()); Ok(agent_info) } /// 内部方法:创建 Agent 会话但不插入到 registry /// /// 用于 entry API 优化,避免重复插入 async fn create_session_internal( &self, project_id: String, project_path: PathBuf, model_provider: Option, start_config: AgentStartConfig, service_uuid: Option, ) -> Result { info!( "Creating Agent session, project ID: {}", project_id ); // 创建 SACP 启动器 let launcher = ClaudeCodeLauncher::new(self.notifier.clone()); // 记录是否使用了 resume(仅用于日志) let has_resume = start_config.resume_session_id.is_some(); if has_resume { info!( "📌 Starting Agent with resume: session_id={:?}", start_config.resume_session_id ); } // 启动 Agent (SACP 版本) // 如果 resume 失败,直接返回错误,让上层(rcoder)决定是否降级重试 let connection_info = launcher .launch( project_id.clone(), project_path.clone(), model_provider.clone(), start_config.clone(), self.registry.clone(), service_uuid, ) .await?; info!( "✅ Agent session created successfully, session ID: {}", connection_info.session_id ); // 创建 ProjectAndAgentInfo let lifecycle_handle = Some(connection_info.lifecycle_guard.clone() as Arc); let now = Utc::now(); let agent_info = ProjectAndAgentInfo { project_id: project_id.clone(), session_id: connection_info.session_id.clone(), prompt_tx: connection_info.prompt_tx, cancel_tx: connection_info.cancel_tx, model_provider: model_provider.clone(), request_id: None, status: AgentStatus::Idle, last_activity: now, created_at: now, stop_handle: lifecycle_handle, }; // 返回 agent_info(不插入 registry,由调用方处理) Ok(agent_info.into()) } /// 获取或创建会话 (SACP 版本) /// /// 如果会话已存在且模型配置未变化,则复用;否则创建新会话 /// /// # 优化说明 /// 获取或创建会话 /// /// # 参数 /// - `project_id`: 项目 ID /// - `project_path`: 项目路径 /// - `session_id_hint`: 会话 ID 提示(用于恢复现有会话) /// - `model_provider`: 模型提供者配置 /// - `start_config`: Agent 启动配置 /// - `service_uuid`: 与此 Agent 关联的唯一 UUID /// /// # 返回值 /// - `R::Entry`: 会话条目 /// - `bool`: 是否是新创建的会话 /// /// # 并发安全性 /// /// 使用"检查-创建-插入"三阶段模式避免在持有 entry 期间调用 `.await`: /// /// 1. **快速检查**:检查会话是否已存在且有效 /// 2. **创建会话**:如果需要创建,在**不持有锁**的情况下创建会话(.await) /// 3. **原子性插入**:使用 entry API 原子性插入,如果其他线程已创建则使用已存在的 /// /// 这样确保: /// - 不会在持有 DashMap entry 期间跨越 await 点 /// - 同一 project_id 最多只会创建一个会话 /// - 高并发下不会阻塞其他 project_id 的访问(DashMap 分段锁特性) pub async fn get_or_create_session( &self, project_id: &str, project_path: PathBuf, session_id_hint: Option, // 🔥 修复:使用此参数查找现有会话 model_provider: Option, start_config: AgentStartConfig, service_uuid: Option, ) -> Result<(R::Entry, bool)> { use dashmap::mapref::entry::Entry; let project_id_key = project_id.to_string(); // 🆕 第一阶段:如果提供了 session_id_hint,先尝试通过 session_id 查找现有会话 // 🔥 优化:使用 get_entry_by_session() 避免两次调用之间的竞态窗口 if let Some(ref hint_sid) = session_id_hint { // 一次性查询:通过 session_id 直接获取 agent_info if let Some(existing) = self.registry.get_entry_by_session(hint_sid) { // 验证 project_id 是否匹配(防御性编程) if existing.project_id() == project_id { let channel_closed = existing.is_channel_closed(); let model_changed = existing.is_model_config_changed(&model_provider); if !channel_closed && !model_changed { info!( "[SESSION] Reusing existing session via session_id_hint: project_id={}, session_id={}", project_id, hint_sid ); return Ok((existing, false)); } if channel_closed { info!( "⚠️ [SESSION] Session channel closed for session_id_hint, need to rebuild: project_id={}, session_id={}", project_id, hint_sid ); } if model_changed { info!( "🔄 [SESSION] Model config changed for session_id_hint, need to rebuild: project_id={}, session_id={}", project_id, hint_sid ); } // 会话无效,继续后续逻辑(可能需要重建) } else { info!( "⚠️ [SESSION] session_id_hint belongs to different project: hint_project={}, current_project={}, session_id={}", existing.project_id(), project_id, hint_sid ); // session_id 属于其他 project,不能复用,继续后续逻辑 } } else { info!( "🔍 [SESSION] session_id_hint does not exist in registry: session_id={}", hint_sid ); // session_id 不存在,继续后续逻辑 } } // 第一阶段:快速检查现有会话 if let Entry::Occupied(occupied_entry) = self.registry.entry(project_id_key.clone()) { let existing = occupied_entry.get(); // 🔥 显式检查 Pending 状态 - PendingGuard 创建的占位符需要被替换 if *existing.status() == AgentStatus::Pending { info!( "🔄 [SESSION] Detected Pending placeholder, preparing to replace: project_id={}", project_id ); // 显式释放 entry 锁 drop(occupied_entry); // 第二阶段:创建新会话(不持有锁) let new_session = self .create_session_internal( project_id_key.clone(), project_path, model_provider, start_config, service_uuid, ) .await?; // 第三阶段:原子性替换(插入新会话,覆盖 pending 占位符) match self.registry.entry(project_id_key.clone()) { Entry::Vacant(entry) => { let new_session_id = new_session.session_id().to_string(); entry.insert(new_session.clone()); info!( "✅ [SESSION] Inserted new session to replace pending: project_id={}, session_id={}", project_id, new_session_id ); return Ok((new_session, true)); } Entry::Occupied(mut entry) => { // 检查是 Pending 占位符还是其他线程创建的真实会话 let current_session = entry.get(); let current_status = current_session.status(); if *current_status == AgentStatus::Pending { // 仍然是 Pending 占位符,替换它 let old_session_id = current_session.session_id().to_string(); let new_session_id = new_session.session_id().to_string(); entry.insert(new_session.clone()); info!( "✅ [SESSION] Replaced pending placeholder: project_id={}, {} → {}", project_id, old_session_id, new_session_id ); return Ok((new_session, true)); } else { // 其他线程已经创建了真实会话,使用已存在的(丢弃我们创建的) let existing_session = current_session.clone(); let created_session_id = new_session.session_id().to_string(); let existing_session_id = existing_session.session_id().to_string(); info!( "🔄 [SESSION] Detected concurrent creation (other thread already created real session): project_id={}, discarded session_id={}, using session_id={}", project_id, created_session_id, existing_session_id ); // new_session 会被 drop,AgentLifecycleGuard 会清理 Agent 进程 return Ok((existing_session, false)); } } } } let channel_closed = existing.is_channel_closed(); let model_changed = existing.is_model_config_changed(&model_provider); if !channel_closed && !model_changed { info!("Reuse Agent session, project ID: {}", project_id); return Ok((existing.clone(), false)); } // 需要重建会话,先克隆必要数据并释放锁 let session_id_str = existing.session_id().to_string(); drop(occupied_entry); // 显式释放 entry 锁 if channel_closed { info!( "⚠️ Detected session channel closed (Agent process exited), rebuilding session, project ID: {}, old session_id: {}", project_id, session_id_str ); } if model_changed { info!( "Model config changed, restarting Agent session, project ID: {}, old session_id: {}", project_id, session_id_str ); } // 第二阶段:在不持有锁的情况下创建新会话 let new_session = self .create_session_internal( project_id_key.clone(), project_path, model_provider, start_config, service_uuid, ) .await?; // 第三阶段:原子性插入(使用 entry API 防止并发创建) match self.registry.entry(project_id_key.clone()) { Entry::Vacant(entry) => { // 其他线程还没有创建,使用我们创建的会话 let new_session_id = new_session.session_id().to_string(); entry.insert(new_session.clone()); info!( "✅ Session rebuilt, project ID: {}, new session_id: {}", project_id, new_session_id ); return Ok((new_session, true)); } Entry::Occupied(entry) => { // 其他线程已经创建了会话,使用已存在的(丢弃我们创建的) let existing_session = entry.get().clone(); let created_session_id = new_session.session_id().to_string(); let existing_session_id = existing_session.session_id().to_string(); info!( "🔄 [SESSION] Detected concurrent creation, using other thread's session: project_id={}, discarded session_id={}, using session_id={}", project_id, created_session_id, existing_session_id ); // new_session 会被 drop,Session 的 Drop 实现会清理 Agent 进程 return Ok((existing_session, false)); } } } // 会话不存在,需要创建新会话 info!( "Session not found, creating new session, project ID: {}", project_id ); // 第二阶段:在不持有锁的情况下创建新会话 let new_session = self .create_session_internal( project_id_key.clone(), project_path, model_provider, start_config, service_uuid, ) .await?; // 第三阶段:原子性插入 match self.registry.entry(project_id_key.clone()) { Entry::Vacant(entry) => { // 其他线程还没有创建,使用我们创建的会话 let session_id_str = new_session.session_id().to_string(); entry.insert(new_session.clone()); info!( "✅ New session created, project ID: {}, session_id: {}", project_id, session_id_str ); Ok((new_session, true)) } Entry::Occupied(entry) => { // 其他线程已经创建了会话,使用已存在的(丢弃我们创建的) let existing_session = entry.get().clone(); let created_session_id = new_session.session_id().to_string(); let existing_session_id = existing_session.session_id().to_string(); info!( "🔄 [SESSION] Detected concurrent creation, using session created by other thread: project_id={}, discarding session_id={}, using session_id={}", project_id, created_session_id, existing_session_id ); // new_session 会被 drop,Session 的 Drop 实现会清理 Agent 进程 Ok((existing_session, false)) } } } /// 发送 Prompt 到指定会话(仅文本) /// /// 使用有界通道,提供背压保护。如果通道已满,会异步等待直到有空间。 pub async fn send_text_prompt(&self, project_id: &str, prompt: &PromptMessage) -> Result<()> { let session = self .get_session(project_id) .ok_or_else(|| anyhow::anyhow!("Session not found: {}", project_id))?; let prompt_request = Self::build_text_prompt_request(prompt, session.session_id().clone())?; session .prompt_tx() .send(prompt_request) .await .map_err(|e| { error!("send Prompt requestfailed: {:?}", e); anyhow::anyhow!("Failed to send Prompt request: {:?}", e) })?; info!("Prompt request already sent, project ID: {}", project_id); Ok(()) } /// 发送 Prompt 请求到指定会话 /// /// 使用有界通道,提供背压保护。如果通道已满,会异步等待直到有空间。 pub async fn send_prompt_request( &self, project_id: &str, prompt_request: PromptRequest, ) -> Result<()> { let session = self .get_session(project_id) .ok_or_else(|| anyhow::anyhow!("Session not found: {}", project_id))?; session .prompt_tx() .send(prompt_request) .await .map_err(|e| { error!("send Prompt requestfailed: {:?}", e); anyhow::anyhow!("Failed to send Prompt request: {:?}", e) })?; info!("Prompt request already sent, project ID: {}", project_id); Ok(()) } } impl std::fmt::Debug for AcpSessionManager { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("AcpSessionManager") .field("session_count", &self.registry.count()) .finish() } }