添加qiming-rcoder模块
This commit is contained in:
242
qiming-rcoder/crates/agent_abstraction/src/session/acp_worker.rs
Normal file
242
qiming-rcoder/crates/agent_abstraction/src/session/acp_worker.rs
Normal file
@@ -0,0 +1,242 @@
|
||||
//! ACP Agent Worker 实现 (SACP 版本)
|
||||
//!
|
||||
//! 封装 Agent 请求处理的核心业务逻辑
|
||||
//!
|
||||
//! ## SACP 迁移说明
|
||||
//!
|
||||
//! - 移除了 `Client` trait 泛型参数(SACP 内部处理)
|
||||
//! - 简化了会话创建参数
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_config::{AgentServersConfig, PromptConfigAssembler};
|
||||
use anyhow::Result;
|
||||
use shared_types::{ProjectAndAgentInfo, SessionEntry};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use super::{AcpSessionManager, AgentWorker, SessionHandles, WorkerRequest, WorkerResponse};
|
||||
use crate::launcher::convert_context_servers;
|
||||
use crate::traits::{AgentStartConfig, SessionNotifier, SessionRegistry};
|
||||
|
||||
/// ACP Agent Worker (SACP 版本)
|
||||
///
|
||||
/// 处理 ACP Agent 请求的核心业务逻辑实现
|
||||
///
|
||||
/// # 类型参数
|
||||
/// - `N`: SessionNotifier 实现,用于推送 SSE 消息
|
||||
/// - `R`: SessionRegistry 实现,用于存储会话数据
|
||||
#[derive(Clone)]
|
||||
pub struct AcpAgentWorker<N: SessionNotifier, R: SessionRegistry> {
|
||||
session_manager: Arc<AcpSessionManager<N, R>>,
|
||||
}
|
||||
|
||||
impl<N: SessionNotifier + 'static, R: SessionRegistry> AcpAgentWorker<N, R> {
|
||||
/// 创建新的 ACP Agent Worker
|
||||
pub fn new(session_manager: Arc<AcpSessionManager<N, R>>) -> Self {
|
||||
Self { session_manager }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<N: SessionNotifier + Send + Sync + 'static, R: SessionRegistry + Send + Sync + 'static>
|
||||
AgentWorker for AcpAgentWorker<N, R>
|
||||
where
|
||||
R::Entry: Into<ProjectAndAgentInfo> + From<ProjectAndAgentInfo>,
|
||||
{
|
||||
fn name(&self) -> &'static str {
|
||||
"AcpAgentWorker"
|
||||
}
|
||||
|
||||
async fn process_request(&self, request: WorkerRequest) -> Result<WorkerResponse> {
|
||||
let project_id = request.project_id().to_string();
|
||||
let project_path = request.project_path().clone();
|
||||
|
||||
info!(
|
||||
"📨 AcpAgentWorker received request, project_id: {}",
|
||||
project_id
|
||||
);
|
||||
|
||||
// 1. 路径规范化
|
||||
let normalized_path = AcpSessionManager::<N, R>::normalize_path(&project_path);
|
||||
debug!("📂 path: {:?}", normalized_path);
|
||||
|
||||
// 2. 确保项目目录存在
|
||||
AcpSessionManager::<N, R>::ensure_project_dir(&normalized_path)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("createdprojectdirectoryfailed: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
// 3. 使用 PromptConfigAssembler 组装配置
|
||||
let default_agent_id = "claude-code-acp-ts";
|
||||
|
||||
// 根据请求中的 service_type 加载对应配置
|
||||
let servers_config =
|
||||
AgentServersConfig::load_or_default_for_service(&request.prompt_message.service_type)
|
||||
.await;
|
||||
|
||||
let assembler = PromptConfigAssembler::new(servers_config)
|
||||
.with_system_prompt(request.prompt_message.system_prompt_override.clone())
|
||||
.with_user_prompt_template(request.prompt_message.user_prompt_template_override.clone())
|
||||
.with_agent_config(request.prompt_message.agent_config_override.clone());
|
||||
|
||||
// 🆕 获取用户指定的 agent_id(如果有)
|
||||
let agent_id = assembler.get_agent_id(default_agent_id);
|
||||
debug!("🎯 Resolved Agent ID: {}", agent_id);
|
||||
|
||||
// 获取最终的系统提示词(入参有值则使用入参,否则使用默认配置)
|
||||
// 🆕 使用实际 agent_id 而不是 default_agent_id
|
||||
let system_prompt = assembler.get_system_prompt(&agent_id);
|
||||
// 应用用户提示词模板(如果有)
|
||||
// 🆕 使用实际 agent_id 而不是 default_agent_id
|
||||
let final_user_prompt =
|
||||
assembler.apply_user_prompt(&agent_id, &request.prompt_message.content);
|
||||
|
||||
info!(
|
||||
"📝 Prompt processing - System prompt: has_override={}, length={} | User prompt: has_template={}, original_len={}, final_len={}",
|
||||
assembler.has_system_prompt_override(),
|
||||
system_prompt.len(),
|
||||
assembler.has_user_prompt_template_override(),
|
||||
request.prompt_message.content.len(),
|
||||
final_user_prompt.len()
|
||||
);
|
||||
|
||||
debug!(
|
||||
"📝 System prompt: has_override={}, length={}, content={}",
|
||||
assembler.has_system_prompt_override(),
|
||||
system_prompt.len(),
|
||||
system_prompt
|
||||
);
|
||||
debug!(
|
||||
"📝 User prompt: has_template={}, original_len={}, final_len={}, final_content={}",
|
||||
assembler.has_user_prompt_template_override(),
|
||||
request.prompt_message.content.len(),
|
||||
final_user_prompt.len(),
|
||||
final_user_prompt
|
||||
);
|
||||
|
||||
// 获取 MCP 服务器配置(入参有值则使用入参,否则使用默认配置)
|
||||
let context_servers = assembler.get_context_servers();
|
||||
debug!(
|
||||
"🔌 MCP server config: has_override={}, count={}",
|
||||
assembler.has_agent_config_override(),
|
||||
context_servers.len()
|
||||
);
|
||||
|
||||
// 将 context_servers 转换为 ACP 协议的 McpServer 格式
|
||||
let mcp_servers = convert_context_servers(&context_servers);
|
||||
debug!("🔌 MCP servers: {}", mcp_servers.len());
|
||||
|
||||
// 构建 AgentStartConfig 并传递 MCP 服务器、service_type
|
||||
let mut start_config = AgentStartConfig::new(request.prompt_message.service_type.clone())
|
||||
.with_system_prompt(system_prompt)
|
||||
.with_mcp_servers(mcp_servers);
|
||||
|
||||
// 🆕 如果用户指定了 agent_server 配置,添加到 start_config
|
||||
// 注意:这里直接使用用户传入的配置,由 launcher 层负责与默认配置合并
|
||||
if let Some(ref override_config) = request.prompt_message.agent_config_override {
|
||||
if let Some(ref agent_server) = override_config.agent_server {
|
||||
debug!(
|
||||
"📝 Using user-specified Agent server config: command={:?}, args={:?}",
|
||||
agent_server.command, agent_server.args
|
||||
);
|
||||
start_config = start_config.with_agent_server_override(agent_server.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Resume 策略:直接传递 session_id,由 LoadSessionRequest 在 Agent 层面检查
|
||||
//
|
||||
// 工作原理:
|
||||
// 1. 用户传入 session_id → 直接设置 resume_session_id
|
||||
// 2. claude_code_sacp.rs 尝试 LoadSessionRequest 加载历史会话
|
||||
// 3. 如果 Agent 返回成功 → 恢复上下文
|
||||
// 4. 如果 Agent 返回错误/超时 → 降级到 NewSessionRequest 创建新会话
|
||||
//
|
||||
// 优势:
|
||||
// - Agent 层面统一处理,无需本地检查文件
|
||||
// - 与 Agent 行为保持一致
|
||||
// - 自动降级(有会话就恢复,没有就创建)
|
||||
if let Some(ref session_id) = request.prompt_message.session_id {
|
||||
info!("Setting resume_session_id: {}", session_id);
|
||||
start_config = start_config.with_resume_session_id(session_id.clone());
|
||||
}
|
||||
|
||||
// 4. 更新 prompt_message 的 content 为处理后的用户提示词
|
||||
let mut prompt_message = request.prompt_message.clone();
|
||||
prompt_message.content = final_user_prompt;
|
||||
|
||||
// 5. 获取或创建会话 (SACP 版本 - 不需要 client 和 shared_api_key_manager)
|
||||
let (session_entry, is_new) = self
|
||||
.session_manager
|
||||
.get_or_create_session(
|
||||
&project_id,
|
||||
normalized_path,
|
||||
prompt_message.session_id.clone(),
|
||||
request.model_provider.clone(),
|
||||
start_config.clone(),
|
||||
request.service_uuid.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to create session: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
// 使用 SessionEntry trait 方法访问会话信息
|
||||
info!(
|
||||
"✅ Session ready, session_id: {}, is_new: {}",
|
||||
session_entry.session_id(),
|
||||
is_new
|
||||
);
|
||||
|
||||
// 6. 构建 Prompt 请求
|
||||
let prompt_request = if let Some(ref attachment_blocks) = request.attachment_blocks {
|
||||
debug!("📎 Built Prompt request with attachments");
|
||||
AcpSessionManager::<N, R>::build_prompt_request_with_attachments(
|
||||
&prompt_message,
|
||||
session_entry.session_id().clone(),
|
||||
attachment_blocks.clone(),
|
||||
)?
|
||||
} else {
|
||||
debug!("📝 Built text Prompt request");
|
||||
AcpSessionManager::<N, R>::build_text_prompt_request(
|
||||
&prompt_message,
|
||||
session_entry.session_id().clone(),
|
||||
)?
|
||||
};
|
||||
|
||||
// 7. 发送 Prompt(异步,支持背压)
|
||||
self.session_manager
|
||||
.send_prompt_request(&project_id, prompt_request)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("send Prompt requestfailed: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
info!("Prompt request already sent, project_id: {}", project_id);
|
||||
|
||||
// 8. 构建响应
|
||||
if is_new {
|
||||
Ok(WorkerResponse::new_session_success(
|
||||
project_id,
|
||||
session_entry.session_id().to_string(),
|
||||
Some(request.request_id().to_string()),
|
||||
prompt_message.service_type.clone(),
|
||||
SessionHandles {
|
||||
prompt_tx: session_entry.prompt_tx().clone(),
|
||||
cancel_tx: session_entry.cancel_tx().clone(),
|
||||
lifecycle_handle: session_entry.lifecycle_handle().cloned(),
|
||||
},
|
||||
))
|
||||
} else {
|
||||
Ok(WorkerResponse::reuse_session_success(
|
||||
project_id,
|
||||
session_entry.session_id().to_string(),
|
||||
Some(request.request_id().to_string()),
|
||||
prompt_message.service_type.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
23
qiming-rcoder/crates/agent_abstraction/src/session/mod.rs
Normal file
23
qiming-rcoder/crates/agent_abstraction/src/session/mod.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
//! Session 管理模块
|
||||
//!
|
||||
//! 提供 ACP 会话的统一管理能力,包括:
|
||||
//! - 会话信息存储和查询
|
||||
//! - 会话生命周期管理
|
||||
//! - 模型配置变化检测
|
||||
//! - Agent Worker 抽象
|
||||
//!
|
||||
//! ## 架构说明
|
||||
//!
|
||||
//! 会话信息统一使用 `ProjectAndAgentInfo`(定义在 `shared_types`),
|
||||
//! 通过 `SessionEntry` trait 抽象访问接口。
|
||||
//! 会话存储通过 `SessionRegistry` trait 抽象,允许注入不同实现(如 `AGENT_REGISTRY`)。
|
||||
|
||||
mod acp_worker;
|
||||
mod session_file_scanner;
|
||||
mod session_manager;
|
||||
mod worker;
|
||||
|
||||
pub use acp_worker::AcpAgentWorker;
|
||||
pub use session_file_scanner::check_session_file_exists;
|
||||
pub use session_manager::AcpSessionManager;
|
||||
pub use worker::{AgentWorker, SessionHandles, WorkerRequest, WorkerResponse};
|
||||
@@ -0,0 +1,352 @@
|
||||
//! Session 文件扫描模块
|
||||
//!
|
||||
//! 扫描 ~/.claude/projects/ 目录,验证 session 文件是否存在
|
||||
//! 作为官方 listSessions API 的降级方案
|
||||
//!
|
||||
//! 特性:
|
||||
//! - 使用 moka 缓存扫描结果(TTL 3分钟兜底)
|
||||
//! - 使用 notify 监听文件系统变化,自动刷新缓存
|
||||
//! - 防抖机制:100ms 内的多个事件合并为一次刷新
|
||||
//! - 文件变化时清除缓存,下次查询时重新扫描
|
||||
|
||||
use moka::future::Cache;
|
||||
use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
/// 全局 session 文件存在性缓存
|
||||
/// Key: project_path, Value: 该项目下的 session_id 集合
|
||||
static FILE_SCAN_CACHE: OnceLock<Cache<String, HashSet<String>>> = OnceLock::new();
|
||||
|
||||
/// 文件监听器(全局单例)
|
||||
static FILE_WATCHER: OnceLock<Mutex<Option<RecommendedWatcher>>> = OnceLock::new();
|
||||
|
||||
/// 监听器是否已启动
|
||||
static WATCHER_STARTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// 防抖时间(毫秒)
|
||||
const DEBOUNCE_MS: u64 = 100;
|
||||
|
||||
/// 获取或初始化缓存(TTL 3分钟兜底,主要由文件监听触发刷新)
|
||||
fn get_file_scan_cache() -> &'static Cache<String, HashSet<String>> {
|
||||
FILE_SCAN_CACHE.get_or_init(|| {
|
||||
Cache::builder()
|
||||
.time_to_live(Duration::from_secs(180)) // 3分钟兜底 TTL
|
||||
.build()
|
||||
})
|
||||
}
|
||||
|
||||
/// Claude 配置目录
|
||||
fn get_claude_config_dir() -> PathBuf {
|
||||
std::env::var("CLAUDE_CONFIG_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("/home/user"))
|
||||
.join(".claude")
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取 projects 目录路径
|
||||
fn get_projects_dir() -> PathBuf {
|
||||
get_claude_config_dir().join("projects")
|
||||
}
|
||||
|
||||
/// 编码项目路径为目录名
|
||||
/// 规则:将 `/` 替换为 `-`
|
||||
fn encode_project_path(project_path: &str) -> String {
|
||||
project_path.replace('/', "-")
|
||||
}
|
||||
|
||||
/// 从文件路径中提取项目目录名
|
||||
/// 例如:~/.claude/projects/-home-user-project/abc123.jsonl -> -home-user-project
|
||||
fn extract_project_dir_from_path(file_path: &Path) -> Option<String> {
|
||||
let projects_dir = get_projects_dir();
|
||||
|
||||
// 检查文件是否在 projects 目录下
|
||||
if !file_path.starts_with(&projects_dir) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 获取相对于 projects 目录的路径
|
||||
if let Ok(relative) = file_path.strip_prefix(&projects_dir) {
|
||||
// 第一个组件就是项目目录名
|
||||
if let Some(first_component) = relative.components().next() {
|
||||
return Some(first_component.as_os_str().to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// 扫描项目目录,获取所有 session_id(异步版本)
|
||||
///
|
||||
/// 使用 tokio::fs 异步 I/O,避免阻塞 Tokio 工作线程
|
||||
async fn scan_project_sessions(project_path: &str) -> HashSet<String> {
|
||||
let projects_dir = get_projects_dir();
|
||||
let encoded_path = encode_project_path(project_path);
|
||||
|
||||
let mut session_ids = HashSet::new();
|
||||
|
||||
if !projects_dir.exists() {
|
||||
debug!(
|
||||
"🔍 [FILE_SCAN] Projects directory does not exist: {}",
|
||||
projects_dir.display()
|
||||
);
|
||||
return session_ids;
|
||||
}
|
||||
|
||||
// 使用异步 I/O 遍历目录
|
||||
let Ok(mut dir_entries) = tokio::fs::read_dir(&projects_dir).await else {
|
||||
warn!(
|
||||
"🔍 [FILE_SCAN] Unable to read project directory: {}",
|
||||
projects_dir.display()
|
||||
);
|
||||
return session_ids;
|
||||
};
|
||||
|
||||
while let Ok(Some(entry)) = dir_entries.next_entry().await {
|
||||
let dir_name = entry.file_name();
|
||||
|
||||
// 匹配编码后的路径(精确或带哈希后缀)
|
||||
// 使用 OsStr 避免不必要的 String 分配
|
||||
if dir_name
|
||||
.to_string_lossy()
|
||||
.starts_with(encoded_path.as_str())
|
||||
{
|
||||
let Ok(mut files) = tokio::fs::read_dir(entry.path()).await else {
|
||||
warn!(
|
||||
"🔍 [file_scanner] Unable to scan session directory: {}",
|
||||
entry.path().display()
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
while let Ok(Some(file)) = files.next_entry().await {
|
||||
if let Some(filename) = file.file_name().to_str() {
|
||||
if filename.ends_with(".jsonl") {
|
||||
// 提取 session_id(去掉 .jsonl 后缀)
|
||||
let session_id = filename.trim_end_matches(".jsonl").to_string();
|
||||
session_ids.insert(session_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!(
|
||||
"🔍 [FILE_SCAN] Scan completed: project_path={}, found {} sessions",
|
||||
project_path,
|
||||
session_ids.len()
|
||||
);
|
||||
|
||||
session_ids
|
||||
}
|
||||
|
||||
/// 清除指定项目目录相关的缓存
|
||||
///
|
||||
/// 由于 moka 不支持按前缀遍历/删除,采用 invalidate_all 策略
|
||||
/// 下次查询时会用原始 project_path 重新扫描
|
||||
async fn invalidate_project_cache(project_dir_name: &str) {
|
||||
let cache = get_file_scan_cache();
|
||||
|
||||
debug!(
|
||||
"🔄 [FILE_WATCH] Detected project directory change: {}, clearing cache",
|
||||
project_dir_name
|
||||
);
|
||||
|
||||
// 由于无法从目录名还原 project_path,直接清除所有缓存
|
||||
// 这是一个权衡:简单但可能影响其他项目的缓存
|
||||
// 实际影响较小,因为缓存会在下次查询时重建
|
||||
cache.invalidate_all();
|
||||
}
|
||||
|
||||
/// 启动文件系统监听器
|
||||
///
|
||||
/// 监听 ~/.claude/projects/ 目录的变化,当 .jsonl 文件创建/删除时刷新缓存
|
||||
pub fn start_file_watcher() {
|
||||
// 确保只启动一次
|
||||
if WATCHER_STARTED.swap(true, Ordering::SeqCst) {
|
||||
debug!("[filelisten] File watcher already started");
|
||||
return;
|
||||
}
|
||||
|
||||
let projects_dir = get_projects_dir();
|
||||
|
||||
// 创建目录(如果不存在)
|
||||
if !projects_dir.exists() {
|
||||
if let Err(e) = std::fs::create_dir_all(&projects_dir) {
|
||||
warn!("[filelisten] Unable to create projects directory: {}", e);
|
||||
WATCHER_STARTED.store(false, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建异步通道
|
||||
let (tx, mut rx) = mpsc::channel::<Event>(100);
|
||||
|
||||
// 创建文件监听器
|
||||
let watcher_result = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
|
||||
match res {
|
||||
Ok(event) => {
|
||||
// 只关心 .jsonl 文件的创建和删除
|
||||
let has_jsonl = event
|
||||
.paths
|
||||
.iter()
|
||||
.any(|p| p.extension().is_some_and(|ext| ext == "jsonl"));
|
||||
|
||||
if has_jsonl {
|
||||
match event.kind {
|
||||
EventKind::Create(_) | EventKind::Remove(_) | EventKind::Modify(_) => {
|
||||
if let Err(e) = tx.blocking_send(event) {
|
||||
error!("[filelisten] Failed to send event: {}", e);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("[filelisten] Listener error: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut watcher = match watcher_result {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
error!("[filelisten] Failed to create listener: {}", e);
|
||||
WATCHER_STARTED.store(false, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// 开始监听目录
|
||||
if let Err(e) = watcher.watch(&projects_dir, RecursiveMode::Recursive) {
|
||||
error!("[filelisten] Failed to watch directory: {}", e);
|
||||
WATCHER_STARTED.store(false, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存 watcher 到全局变量(防止被 drop)
|
||||
let watcher_holder = FILE_WATCHER.get_or_init(|| Mutex::new(None));
|
||||
if let Ok(mut guard) = watcher_holder.lock() {
|
||||
*guard = Some(watcher);
|
||||
}
|
||||
|
||||
info!(
|
||||
"👁️ [filelisten] startinglistendirectory: {}",
|
||||
projects_dir.display()
|
||||
);
|
||||
|
||||
// 启动异步任务处理事件(带防抖)
|
||||
tokio::spawn(async move {
|
||||
let mut pending_dirs: HashSet<String> = HashSet::new();
|
||||
let mut debounce_timer: Option<tokio::time::Instant> = None;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// 接收新事件
|
||||
event = rx.recv() => {
|
||||
match event {
|
||||
Some(event) => {
|
||||
// 提取变化文件所属的项目目录
|
||||
for path in &event.paths {
|
||||
if let Some(project_dir) = extract_project_dir_from_path(path) {
|
||||
pending_dirs.insert(project_dir);
|
||||
}
|
||||
}
|
||||
// 重置防抖计时器
|
||||
debounce_timer = Some(tokio::time::Instant::now() + Duration::from_millis(DEBOUNCE_MS));
|
||||
}
|
||||
None => {
|
||||
warn!("[filelisten] Received event but channel already closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 防抖计时器触发
|
||||
_ = async {
|
||||
if let Some(timer) = debounce_timer {
|
||||
tokio::time::sleep_until(timer).await;
|
||||
} else {
|
||||
// 没有计时器时永远等待
|
||||
std::future::pending::<()>().await;
|
||||
}
|
||||
} => {
|
||||
if !pending_dirs.is_empty() {
|
||||
debug!(
|
||||
"📁 [FILE_WATCH] Debounce triggered, refreshing {} project directories",
|
||||
pending_dirs.len()
|
||||
);
|
||||
// 刷新所有待处理的项目目录
|
||||
for project_dir in pending_dirs.drain() {
|
||||
invalidate_project_cache(&project_dir).await;
|
||||
}
|
||||
}
|
||||
debounce_timer = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
info!("[FILE_WATCH] File watcher started");
|
||||
}
|
||||
|
||||
/// 停止文件系统监听器
|
||||
#[allow(dead_code)]
|
||||
pub fn stop_file_watcher() {
|
||||
if !WATCHER_STARTED.swap(false, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
let watcher_holder = FILE_WATCHER.get_or_init(|| Mutex::new(None));
|
||||
if let Ok(mut guard) = watcher_holder.lock() {
|
||||
*guard = None;
|
||||
}
|
||||
|
||||
info!("🛑 [filelisten] File watcher already stopped");
|
||||
}
|
||||
|
||||
/// 通过文件扫描检查 session 是否存在(带缓存)
|
||||
pub async fn check_session_file_exists(session_id: &str, project_path: &str) -> bool {
|
||||
// 确保监听器已启动
|
||||
start_file_watcher();
|
||||
|
||||
let cache = get_file_scan_cache();
|
||||
|
||||
// 尝试从缓存获取
|
||||
if let Some(session_ids) = cache.get(project_path).await {
|
||||
let exists = session_ids.contains(session_id);
|
||||
debug!(
|
||||
"🔍 [FILE_SCAN_CACHE] session={} -> {}",
|
||||
session_id,
|
||||
if exists { "exists" } else { "not found" }
|
||||
);
|
||||
return exists;
|
||||
}
|
||||
|
||||
// 缓存未命中,执行扫描
|
||||
debug!(
|
||||
"🔍 [FILE_SCAN] Cache miss, scanning directory: project_path={}",
|
||||
project_path
|
||||
);
|
||||
let session_ids = scan_project_sessions(project_path).await;
|
||||
let exists = session_ids.contains(session_id);
|
||||
|
||||
// 存入缓存
|
||||
cache.insert(project_path.to_string(), session_ids).await;
|
||||
|
||||
if exists {
|
||||
info!("[file_scan] Session file found: {}", session_id);
|
||||
} else {
|
||||
debug!("[file_scan] Not a session file: {}", session_id);
|
||||
}
|
||||
|
||||
exists
|
||||
}
|
||||
@@ -0,0 +1,665 @@
|
||||
//! # 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 会被 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<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()
|
||||
}
|
||||
}
|
||||
221
qiming-rcoder/crates/agent_abstraction/src/session/worker.rs
Normal file
221
qiming-rcoder/crates/agent_abstraction/src/session/worker.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
//! Agent Worker 抽象
|
||||
//!
|
||||
//! 定义请求处理的通用接口
|
||||
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use dashmap::DashMap;
|
||||
use agent_client_protocol::schema::{ContentBlock, PromptRequest};
|
||||
use shared_types::{
|
||||
AgentLifecycle, CancelNotificationRequestWrapper, ModelProviderConfig, ServiceType,
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::PromptMessage;
|
||||
|
||||
/// Worker 请求
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorkerRequest {
|
||||
/// Agent 提示消息
|
||||
pub prompt_message: PromptMessage,
|
||||
/// 模型提供商配置
|
||||
pub model_provider: Option<ModelProviderConfig>,
|
||||
/// 预处理的附件内容块
|
||||
/// 由 agent_runner 使用 ContentBuilder 预处理
|
||||
pub attachment_blocks: Option<Vec<ContentBlock>>,
|
||||
/// 🔥 关联的 service UUID(用于 API 密钥管理)
|
||||
pub service_uuid: Option<String>,
|
||||
/// 🔥 共享的 API 密钥管理器(用于自动清理)
|
||||
pub shared_api_key_manager: Option<Arc<DashMap<String, ModelProviderConfig>>>,
|
||||
}
|
||||
|
||||
impl WorkerRequest {
|
||||
/// 创建新的 Worker 请求
|
||||
pub fn new(prompt_message: PromptMessage, model_provider: Option<ModelProviderConfig>) -> Self {
|
||||
Self {
|
||||
prompt_message,
|
||||
model_provider,
|
||||
attachment_blocks: None,
|
||||
service_uuid: None,
|
||||
shared_api_key_manager: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置 service_uuid
|
||||
pub fn with_service_uuid(mut self, service_uuid: Option<String>) -> Self {
|
||||
self.service_uuid = service_uuid;
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置 shared_api_key_manager
|
||||
pub fn with_key_manager(
|
||||
mut self,
|
||||
key_manager: Option<Arc<DashMap<String, ModelProviderConfig>>>,
|
||||
) -> Self {
|
||||
self.shared_api_key_manager = key_manager;
|
||||
self
|
||||
}
|
||||
|
||||
/// 获取项目 ID
|
||||
pub fn project_id(&self) -> &str {
|
||||
&self.prompt_message.project_id
|
||||
}
|
||||
|
||||
/// 获取项目路径
|
||||
pub fn project_path(&self) -> &PathBuf {
|
||||
&self.prompt_message.project_path
|
||||
}
|
||||
|
||||
/// 获取请求 ID
|
||||
pub fn request_id(&self) -> &str {
|
||||
&self.prompt_message.request_id
|
||||
}
|
||||
}
|
||||
|
||||
/// 会话句柄
|
||||
///
|
||||
/// 用于传递会话句柄给 agent_runner 更新全局 MAP
|
||||
#[derive(Clone)]
|
||||
pub struct SessionHandles {
|
||||
pub prompt_tx: mpsc::Sender<PromptRequest>,
|
||||
pub cancel_tx: mpsc::Sender<CancelNotificationRequestWrapper>,
|
||||
pub lifecycle_handle: Option<Arc<dyn AgentLifecycle>>,
|
||||
}
|
||||
|
||||
// 手动实现 Debug,跳过不支持 Debug 的 lifecycle_handle
|
||||
impl std::fmt::Debug for SessionHandles {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SessionHandles")
|
||||
.field("prompt_tx", &"Sender<PromptRequest>")
|
||||
.field("cancel_tx", &"Sender<CancelNotificationRequestWrapper>")
|
||||
.field(
|
||||
"lifecycle_handle",
|
||||
&self
|
||||
.lifecycle_handle
|
||||
.as_ref()
|
||||
.map(|_| "Some(AgentLifecycle)"),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Worker 响应
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorkerResponse {
|
||||
/// 项目 ID
|
||||
pub project_id: String,
|
||||
/// 会话 ID
|
||||
pub session_id: String,
|
||||
/// 错误信息(如果有)
|
||||
pub error: Option<String>,
|
||||
/// 请求 ID
|
||||
pub request_id: Option<String>,
|
||||
/// 服务类型
|
||||
pub service_type: ServiceType,
|
||||
/// 标识是否是新创建的会话
|
||||
pub is_new_session: bool,
|
||||
/// 会话句柄(仅新会话时有值)
|
||||
pub session_handles: Option<SessionHandles>,
|
||||
}
|
||||
|
||||
impl WorkerResponse {
|
||||
/// 创建新会话的成功响应
|
||||
pub fn new_session_success(
|
||||
project_id: String,
|
||||
session_id: String,
|
||||
request_id: Option<String>,
|
||||
service_type: ServiceType,
|
||||
handles: SessionHandles,
|
||||
) -> Self {
|
||||
Self {
|
||||
project_id,
|
||||
session_id,
|
||||
error: None,
|
||||
request_id,
|
||||
service_type,
|
||||
is_new_session: true,
|
||||
session_handles: Some(handles),
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建复用会话的成功响应
|
||||
pub fn reuse_session_success(
|
||||
project_id: String,
|
||||
session_id: String,
|
||||
request_id: Option<String>,
|
||||
service_type: ServiceType,
|
||||
) -> Self {
|
||||
Self {
|
||||
project_id,
|
||||
session_id,
|
||||
error: None,
|
||||
request_id,
|
||||
service_type,
|
||||
is_new_session: false,
|
||||
session_handles: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建成功响应(旧版本,保持向后兼容)
|
||||
#[deprecated(note = "Use new_session_success or reuse_session_success instead")]
|
||||
pub fn success(
|
||||
project_id: String,
|
||||
session_id: String,
|
||||
request_id: Option<String>,
|
||||
service_type: ServiceType,
|
||||
) -> Self {
|
||||
Self {
|
||||
project_id,
|
||||
session_id,
|
||||
error: None,
|
||||
request_id,
|
||||
service_type,
|
||||
is_new_session: false,
|
||||
session_handles: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建错误响应
|
||||
pub fn error(
|
||||
project_id: String,
|
||||
session_id: String,
|
||||
error: String,
|
||||
request_id: Option<String>,
|
||||
service_type: ServiceType,
|
||||
) -> Self {
|
||||
Self {
|
||||
project_id,
|
||||
session_id,
|
||||
error: Some(error),
|
||||
request_id,
|
||||
service_type,
|
||||
is_new_session: false,
|
||||
session_handles: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查是否成功
|
||||
pub fn is_success(&self) -> bool {
|
||||
self.error.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Agent Worker Trait
|
||||
///
|
||||
/// 定义请求处理的抽象接口,允许不同的实现策略
|
||||
#[async_trait]
|
||||
pub trait AgentWorker: Send + Sync {
|
||||
/// 处理单个请求
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request` - Worker 请求
|
||||
///
|
||||
/// # Returns
|
||||
/// 处理结果
|
||||
async fn process_request(&self, request: WorkerRequest) -> Result<WorkerResponse>;
|
||||
|
||||
/// 获取 Worker 名称(用于日志和调试)
|
||||
fn name(&self) -> &'static str;
|
||||
}
|
||||
Reference in New Issue
Block a user