Files
qiming/qiming-rcoder/crates/agent_abstraction/src/session/session_manager.rs
2026-06-01 13:54:52 +08:00

666 lines
25 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! # 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<N: SessionNotifier, R: SessionRegistry> {
/// 会话注册表(注入的 SessionRegistry
registry: Arc<R>,
/// 会话通知器
notifier: Arc<N>,
}
impl<N: SessionNotifier + 'static, R: SessionRegistry> AcpSessionManager<N, R>
where
R::Entry: Into<ProjectAndAgentInfo> + From<ProjectAndAgentInfo>,
{
/// 创建新的会话管理器
///
/// # 参数
/// - `notifier`: 会话通知器
/// - `registry`: 会话注册表(通常注入 AGENT_REGISTRY
pub fn new(notifier: Arc<N>, registry: Arc<R>) -> Self {
Self { registry, notifier }
}
/// 获取会话信息
pub fn get_session(&self, project_id: &str) -> Option<R::Entry> {
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<R::Entry> {
self.registry.remove(project_id)
}
/// 获取所有会话的 project_id 列表
pub fn list_sessions(&self) -> Vec<String> {
self.registry.list_project_ids()
}
/// 获取会话数量
pub fn session_count(&self) -> usize {
self.registry.count()
}
/// 获取 registry 的 Arc 引用
pub fn registry(&self) -> Arc<R> {
self.registry.clone()
}
/// 获取 notifier 的 Arc 引用
pub fn notifier(&self) -> Arc<N> {
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<PromptRequest> {
// 构建纯用户提示词
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<ContentBlock>,
) -> Result<PromptRequest> {
// 构建纯用户提示词
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<String>,
model_provider: Option<ModelProviderConfig>,
start_config: AgentStartConfig,
service_uuid: Option<String>,
) -> Result<R::Entry> {
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<ModelProviderConfig>,
start_config: AgentStartConfig,
service_uuid: Option<String>,
) -> Result<R::Entry> {
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<dyn AgentLifecycle>);
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<String>, // 🔥 修复:使用此参数查找现有会话
model_provider: Option<ModelProviderConfig>,
start_config: AgentStartConfig,
service_uuid: Option<String>,
) -> 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 会被 dropAgentLifecycleGuard 会清理 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 会被 dropSession 的 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 会被 dropSession 的 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<N: SessionNotifier + 'static, R: SessionRegistry> std::fmt::Debug for AcpSessionManager<N, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AcpSessionManager")
.field("session_count", &self.registry.count())
.finish()
}
}