添加qiming-rcoder模块

This commit is contained in:
Codex
2026-06-01 13:54:52 +08:00
parent 8092c4b1f8
commit 4b1a580132
539 changed files with 151650 additions and 0 deletions

View File

@@ -0,0 +1,191 @@
//! Agent connection wrapper for ACP protocol.
use std::sync::Arc;
use agent_client_protocol::schema::{CancelNotification, PromptRequest, SessionId};
use tokio::sync::{mpsc, oneshot};
// 重新导出 shared_types 中的统一类型
pub use shared_types::{CancelNotificationRequestWrapper, CancelResult};
/// Agent connection wrapper
#[derive(Debug)]
pub struct AgentConnection {
/// Project ID (业务主键)
/// project_id 是业务的唯一标识符,用于关联项目和对应的 agent 实例
/// 可以根据 project_id 找到对应的 session_id
pub project_id: String,
/// Service Type (服务类型)
/// 用于区分不同类型的服务,对应不同的 Docker 镜像和运行环境
/// 当前所有业务都使用 ServiceType::RCoder
pub service_type: shared_types::ServiceType,
/// Session ID (可空)
/// session_id 是 ACP 协议中的会话标识符,在 agent 的 newSession 成功后由 agent 返回
/// 创建 agent 时可能没有 session_id需要在会话建立后更新
pub session_id: Option<SessionId>,
/// Prompt sender channel
pub prompt_tx: Arc<mpsc::UnboundedSender<PromptRequest>>,
/// Cancel sender channel - wrapped in Arc to avoid Debug requirement
pub cancel_tx: Arc<mpsc::UnboundedSender<CancelNotificationRequestWrapper>>,
}
impl AgentConnection {
/// Create a new agent connection
///
/// # Arguments
/// * `project_id` - 业务主键,唯一标识项目
/// * `service_type` - 服务类型,对应不同的 Docker 镜像和运行环境
/// * `session_id` - ACP 会话 ID可空创建时可能没有
/// * `prompt_tx` - 提示消息发送通道
/// * `cancel_tx` - 取消消息发送通道
pub fn new(
project_id: String,
service_type: shared_types::ServiceType,
session_id: Option<SessionId>,
prompt_tx: Arc<mpsc::UnboundedSender<PromptRequest>>,
cancel_tx: Arc<mpsc::UnboundedSender<CancelNotificationRequestWrapper>>,
) -> Self {
Self {
project_id,
service_type,
session_id,
prompt_tx,
cancel_tx,
}
}
/// Get project ID (业务主键)
pub fn project_id(&self) -> &str {
&self.project_id
}
/// Get session ID (可空)
pub fn session_id(&self) -> Option<&SessionId> {
self.session_id.as_ref()
}
/// Update session ID after successful newSession
/// 在 agent 的 newSession 成功后调用,更新 session_id
pub fn set_session_id(&mut self, session_id: SessionId) {
self.session_id = Some(session_id);
}
/// Check if session is ready (has session_id)
pub fn has_session(&self) -> bool {
self.session_id.is_some()
}
/// Send prompt
///
/// 通过 channel 发送 prompt 到 LocalSet 中运行的 agent
/// session_id 由服务端自动管理,调用方无需关心
///
/// 设计说明:
/// - SACP 版本支持 Send trait可以在 tokio::spawn 中运行
/// - 使用 MPSC channel 解耦调用方和 agent 运行环境
/// - newSession 在 agent 启动后自动执行
/// - prompt handler 会自动处理 session_id 的覆盖
pub async fn send_prompt(
&self,
prompt: PromptRequest,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.prompt_tx.send(prompt).map_err(|e| {
Box::new(std::io::Error::new(std::io::ErrorKind::Other, e))
as Box<dyn std::error::Error + Send + Sync>
})
}
/// Send cancel
///
/// 通过 channel 发送取消请求到 LocalSet 中运行的 agent
/// 返回一个 receiver调用方可以通过它等待取消结果
///
/// # Returns
/// - `Ok(receiver)`: 请求已发送,通过 receiver.await 获取取消结果
/// - `Err`: 发送请求失败channel 已关闭)
pub async fn send_cancel(
&self,
cancel_notification: CancelNotification,
) -> Result<oneshot::Receiver<CancelResult>, Box<dyn std::error::Error + Send + Sync>> {
let (result_tx, result_rx) = oneshot::channel();
self.cancel_tx
.send(CancelNotificationRequestWrapper {
cancel_notification,
result_tx,
})
.map_err(|e| {
Box::new(std::io::Error::new(std::io::ErrorKind::Other, e))
as Box<dyn std::error::Error + Send + Sync>
})?;
Ok(result_rx)
}
/// Send cancel and wait for result
///
/// 发送取消请求并等待结果
pub async fn send_cancel_and_wait(
&self,
cancel_notification: CancelNotification,
) -> Result<CancelResult, Box<dyn std::error::Error + Send + Sync>> {
let result_rx = self.send_cancel(cancel_notification).await?;
result_rx.await.map_err(|e| {
Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to receive cancel result: {}", e),
)) as Box<dyn std::error::Error + Send + Sync>
})
}
}
/// Agent status type (re-exported from shared_types)
pub type AgentStatus = shared_types::AgentStatus;
/// Connection status enum
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ConnectionStatus {
/// Connection is being established
Connecting = 1,
/// Connection is active
Connected = 2,
/// Connection is idle
Idle = 3,
/// Connection has an error
Error = 4,
/// Connection is closed
Closed = 5,
}
impl ConnectionStatus {
/// Convert from u8
pub fn from_u8(value: u8) -> Self {
match value {
1 => ConnectionStatus::Connecting,
2 => ConnectionStatus::Connected,
3 => ConnectionStatus::Idle,
4 => ConnectionStatus::Error,
5 => ConnectionStatus::Closed,
_ => ConnectionStatus::Closed,
}
}
/// Convert to u8
pub fn to_u8(self) -> u8 {
self as u8
}
}
/// Connection statistics
#[derive(Debug, Clone)]
pub struct ConnectionStats {
pub total_connections: u64,
pub active_connections: u64,
pub idle_connections: u64,
pub error_connections: u64,
}

View File

@@ -0,0 +1,20 @@
//! ACP connection management module.
mod connection;
pub use connection::{
AgentConnection, AgentStatus, CancelNotificationRequestWrapper, CancelResult, ConnectionStats,
ConnectionStatus,
};
/// Legacy type alias for backward compatibility
pub type Connection = AgentConnection;
/// Placeholder error type
#[derive(Debug, thiserror::Error)]
pub enum AcpError {
#[error("Connection error: {0}")]
Connection(String),
#[error("Other error: {0}")]
Other(String),
}

View File

@@ -0,0 +1,31 @@
//! Agent abstraction layer error types
use thiserror::Error;
/// Agent abstraction layer error
#[derive(Error, Debug)]
pub enum AgentAbstractionError {
#[error("connection error: {0}")]
Connection(String),
#[error("registry error: {0}")]
Registry(String),
#[error("process error: {0}")]
Process(String),
#[error("other error: {0}")]
Other(String),
#[error("serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("type cast error")]
Cast,
#[error("not found: {0}")]
NotFound(String),
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,618 @@
//! Agent生命周期管理
//!
//! 基于RAII原则的简洁生命周期管理设计
//!
//! ## 僵尸进程问题解决方案
//!
//! 核心问题Drop trait 是同步的,无法 await child.wait()
//!
//! 解决方案:
//! 1. **后台回收任务**:立即启动后台任务 wait() 子进程
//! 2. **进程组终止**:使用 nix::kill 发送信号到进程组
//! 3. **三重保障**PID 1 的 process_reaper 模块兜底
//!
//! ## 进程组说明
//!
//! 使用 `process-wrap` crate 创建真正的进程组:
//! - 启动时使用 `ProcessGroup::leader()` 创建进程组
//! - 终止时发送 `kill(-pgid, SIGKILL)` 到整个进程组
//! - 能够正确清理子进程及其所有孙进程
use anyhow::Result;
use dashmap::DashMap;
use process_wrap::tokio::ChildWrapper;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use tracing::{debug, info, warn};
use agent_client_protocol::schema::SessionId;
use shared_types::{AgentLifecycle, ModelProviderConfig};
/// Agent生命周期守卫
///
/// 遵循RAII原则当守卫被drop时自动清理agent资源
///
/// ## 僵尸进程避免机制
///
/// 1. **后台回收任务**:构造时立即启动 tokio::spawn 等待子进程
/// 2. **进程组终止**Drop 时发送信号到进程组(使用 nix::kill
/// 3. **PID 1 兜底**process_reaper 模块自动回收所有孤儿进程
///
/// ## 进程组信号
///
/// 在 Unix 上,使用负的进程组 ID 发送信号:
/// - `kill(-pgid, SIGKILL)` 杀死整个进程组
/// - 这会终止子进程及其所有后代(如果子进程创建了真正的进程组)
pub struct AgentLifecycleGuard {
inner: Arc<AgentLifecycleInner>,
}
impl std::fmt::Debug for AgentLifecycleGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AgentLifecycleGuard")
.field("project_id", &self.inner.project_id)
.field("session_id", &self.inner.session_id)
.field("pgid", &self.inner.pgid)
.field("stopped", &self.inner.stopped.load(Ordering::SeqCst))
.finish_non_exhaustive()
}
}
struct AgentLifecycleInner {
project_id: String,
session_id: SessionId,
/// 🔥 进程组 ID当前实现使用 child.pid 作为伪进程组)
///
/// 注意:当前实现使用子进程的 PID 作为 PGID。
/// - 如果子进程通过 setsid() 创建了真正的进程组kill(-pgid) 会杀死整个进程树
/// - 如果子进程没有创建进程组kill(-pgid) 只会杀死子进程本身
/// - 未来可以使用 process-wrap 库创建真正的进程组
pgid: u32,
cancel_token: CancellationToken,
resources: AgentResources,
stopped: AtomicBool,
/// 🔥 共享的 API 密钥管理器引用(用于自动清理)
shared_api_key_manager: Option<Arc<DashMap<String, ModelProviderConfig>>>,
/// 🔥 project_id -> service_uuid 映射(用于清理时查找 UUID
project_uuid_map: Option<Arc<DashMap<String, String>>>,
/// 🔥 关联的 service_uuid用于清理时定位配置
service_uuid: Option<String>,
}
/// Agent资源管理枚举
///
/// ## 后台回收版本
///
/// 存储后台任务句柄,确保子进程被 wait() 回收
enum AgentResources {
Claude {
/// stderr 任务句柄
stderr_task: Arc<Mutex<Option<JoinHandle<()>>>>,
/// 后台回收任务(已启动,会 wait() 子进程)
_reaper_task: JoinHandle<()>,
},
}
impl AgentLifecycleGuard {
/// 为Claude Agent创建生命周期守卫兼容旧代码默认无密钥管理器
///
/// # 参数
///
/// * `child_process` - 已启动的子进程(必须是进程组组长)
/// * `stderr_task` - stderr 读取任务
/// * `cancel_token` - 取消令牌
///
/// # 僵尸进程避免
///
/// 此函数会立即启动后台任务等待子进程,确保子进程退出时被回收。
pub fn new_claude(
project_id: String,
session_id: SessionId,
child_process: Box<dyn ChildWrapper>,
stderr_task: JoinHandle<()>,
cancel_token: CancellationToken,
) -> Self {
Self::new_claude_with_key_manager(
project_id,
session_id,
child_process,
stderr_task,
cancel_token,
None, // 默认无密钥管理器
None, // 默认无 project_uuid_map
None, // 默认无 service_uuid
)
}
/// 🔥 新增:带异常退出标志的构造函数
///
/// 创建生命周期守卫时传入共享的 `abnormal_exit_flag`,当子进程异常退出时设置此标志。
/// 这使得 SACP 连接层可以检测到异常退出并发送相应的通知。
///
/// # 参数
///
/// * `abnormal_exit_flag` - 共享的原子布尔标志,子进程异常退出时设置为 true
///
/// # Panics
///
/// 如果子进程 PID 无效(为 0 或 None此函数会 panic因为这意味着进程启动失败。
pub fn new_claude_with_abnormal_flag(
project_id: String,
session_id: SessionId,
child_process: Box<dyn ChildWrapper>,
stderr_task: JoinHandle<()>,
cancel_token: CancellationToken,
abnormal_exit_flag: Arc<AtomicBool>,
) -> Self {
Self::new_claude_with_key_manager_and_abnormal_flag(
project_id,
session_id,
child_process,
stderr_task,
cancel_token,
None, // 默认无密钥管理器
None, // 默认无 project_uuid_map
None, // 默认无 service_uuid
Some(abnormal_exit_flag),
)
}
/// 🔥 新增:带密钥管理器和异常退出标志的构造函数
///
/// 创建生命周期守卫时传入共享的 API 密钥管理器和 service_uuid
/// 当 Agent 停止时Drop会自动清理对应的 API 密钥配置。
///
/// # 进程组管理
///
/// 当前实现使用子进程的 PID 作为 PGID
/// - `pgid = child_pid`(使用子进程 PID 作为进程组 ID
/// - 终止时发送 `kill(-pgid, SIGKILL)` 到进程组
/// - 使用 `process-wrap` 创建真正的进程组,能正确清理所有孙进程
///
/// # 参数
///
/// * `shared_api_key_manager` - 共享的 DashMap用于清理 API 密钥配置
/// * `project_uuid_map` - project_id -> service_uuid 映射,用于查找 UUID
/// * `service_uuid` - 与此 Agent 关联的 service UUID
/// * `abnormal_exit_flag` - 共享的原子布尔标志,子进程异常退出时设置为 true
///
/// # Panics
///
/// 如果子进程 PID 无效(为 0 或 None此函数会 panic因为这意味着进程启动失败。
#[allow(clippy::too_many_arguments)]
pub fn new_claude_with_key_manager(
project_id: String,
session_id: SessionId,
child_process: Box<dyn ChildWrapper>,
stderr_task: JoinHandle<()>,
cancel_token: CancellationToken,
shared_api_key_manager: Option<Arc<DashMap<String, ModelProviderConfig>>>,
project_uuid_map: Option<Arc<DashMap<String, String>>>,
service_uuid: Option<String>,
) -> Self {
Self::new_claude_with_key_manager_and_abnormal_flag(
project_id,
session_id,
child_process,
stderr_task,
cancel_token,
shared_api_key_manager,
project_uuid_map,
service_uuid,
None, // 默认无异常退出标志
)
}
/// 🔥 完整构造函数:带密钥管理器和异常退出标志
#[allow(clippy::too_many_arguments)]
fn new_claude_with_key_manager_and_abnormal_flag(
project_id: String,
session_id: SessionId,
mut child_process: Box<dyn ChildWrapper>,
stderr_task: JoinHandle<()>,
cancel_token: CancellationToken,
shared_api_key_manager: Option<Arc<DashMap<String, ModelProviderConfig>>>,
project_uuid_map: Option<Arc<DashMap<String, String>>>,
service_uuid: Option<String>,
abnormal_exit_flag: Option<Arc<AtomicBool>>,
) -> Self {
// 🔥 关键PID 有效性检查
// ChildWrapper 的 id() 返回 Option<u32>,当进程已终止或无效时返回 None
// 如果 PID 无效,这是一个严重的初始化错误,应该 panic
let pid = child_process.id().unwrap_or_else(|| {
panic!(
"[LifecycleGuard] 子进程 PID 无效None进程可能已终止: project_id={}",
project_id
)
});
// 🔥 额外检查PID 不应该为 0
// 虽然 id() 返回 Some(0) 理论上可能,但实际上 PID 0 是内核保留的
if pid == 0 {
panic!(
"[LifecycleGuard] 子进程 PID 为 0这是无效的 PID: project_id={}",
project_id
);
}
// 🔥 进程组 ID 等于组长进程的 PID
// process-wrap 的 ProcessGroup 使用 setpgid(0, 0) 创建新进程组,使进程成为组长
let pgid = pid;
let project_id_clone = project_id.clone();
let session_id_str = session_id.0.to_string();
// 🔥 关键:立即启动后台回收任务
// 这个任务会等待子进程退出,确保不会产生僵尸进程
// 当子进程退出时,设置 abnormal_exit_flag 并触发 cancel_token
// 让 SACP 连接层检测到并发送 SSE 通知
let cancel_token_for_reaper = cancel_token.clone();
let abnormal_exit_flag_clone = abnormal_exit_flag.clone();
let project_id_for_reaper = project_id.clone();
let reaper_task = tokio::spawn(async move {
info!(
"[ProcessReaper] 开始监控 Agent 进程: project_id={}, pid={}, pgid={}",
project_id_for_reaper, pid, pgid
);
// 🔥 优先等待子进程退出,而不是响应取消信号
// 这确保了即使收到取消信号,也能正确检测进程是否已退出
let wait_result = child_process.wait().await;
// 检查是否是外部取消(用户主动 stop
let was_cancelled = cancel_token_for_reaper.is_cancelled();
match wait_result {
Ok(status) => {
// 获取详细的退出信息
let exit_code = status.code();
#[cfg(unix)]
let signal = {
use std::os::unix::process::ExitStatusExt;
status.signal()
};
#[cfg(not(unix))]
let signal: Option<i32> = None;
if !status.success() {
// 🔥 非零退出码或被信号杀死 = 异常退出
if let Some(ref flag) = abnormal_exit_flag_clone {
// 只有非用户主动取消时才标记为异常
if !was_cancelled {
flag.store(true, Ordering::SeqCst);
}
}
warn!(
"[ProcessReaper] Agent 进程异常退出: project_id={}, pid={}, pgid={}, exit_code={:?}, signal={:?}, was_cancelled={}",
project_id_for_reaper, pid, pgid, exit_code, signal, was_cancelled
);
} else {
info!(
"[ProcessReaper] Agent 进程正常退出: project_id={}, pid={}, pgid={}, exit_code={:?}",
project_id_for_reaper, pid, pgid, exit_code
);
}
}
Err(e) => {
// wait 失败,可能是进程已被其他方式回收
if let Some(ref flag) = abnormal_exit_flag_clone {
if !was_cancelled {
flag.store(true, Ordering::SeqCst);
}
}
warn!(
"[ProcessReaper] Agent 进程 wait() 失败: project_id={}, pid={}, pgid={}, error={}, was_cancelled={}",
project_id_for_reaper, pid, pgid, e, was_cancelled
);
}
}
// 🔥 关键:触发 cancel_token通知 SACP 连接层进程已退出
// 这会让 SACP 连接检测到并发送 SSE 错误通知,然后断开连接
if !was_cancelled {
info!(
"[ProcessReaper] 触发 cancel_token通知 SACP 连接断开: project_id={}, pid={}",
project_id_for_reaper, pid
);
cancel_token_for_reaper.cancel();
} else {
debug!(
"[ProcessReaper] cancel_token 已被外部取消,跳过: project_id={}, pid={}",
project_id_for_reaper, pid
);
}
});
let resources = AgentResources::Claude {
stderr_task: Arc::new(Mutex::new(Some(stderr_task))),
_reaper_task: reaper_task,
};
let inner = Arc::new(AgentLifecycleInner {
project_id: project_id_clone,
session_id,
pgid,
cancel_token,
resources,
stopped: AtomicBool::new(false),
shared_api_key_manager,
project_uuid_map,
service_uuid,
});
info!(
"[LifecycleGuard] 创建 Claude Agent 守卫: project_id={}, pgid={}, session_id={}",
project_id, pgid, session_id_str
);
Self { inner }
}
/// 优雅停止agent
///
/// 带超时机制5秒超时后强制 kill 进程组
///
/// ## 进程组终止
///
/// 使用 `process-wrap` 创建真正的进程组,发送信号到 `-pgid` 会终止:
/// - 子进程(进程组组长)
/// - 所有孙进程(同一进程组中的进程)
pub async fn graceful_stop(&self) -> Result<()> {
// 🔥 使用原子 CAS 操作确保只执行一次清理
// compare_exchange 返回 Ok 表示成功将 false 改为 true即当前线程获得清理权
// 返回 Err 表示已经被其他地方清理Drop 或其他 graceful_stop 调用)
let should_cleanup = self
.inner
.stopped
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok();
if !should_cleanup {
debug!("Agent already stopped, skipping graceful stop");
return Ok(());
}
info!(
"Gracefully stopping Claude agent for project: {}, pgid={}",
self.inner.project_id, self.inner.pgid
);
// 1. 发送取消信号
self.inner.cancel_token.cancel();
// 2. 终止进程组
self.kill_process_group(false).await?;
info!(
"Gracefully stopped Claude agent for project: {}",
self.inner.project_id
);
Ok(())
}
/// 发送取消信号(非阻塞)
pub fn cancel(&self) {
debug!("Sending cancel signal to agent: {}", self.inner.project_id);
self.inner.cancel_token.cancel();
}
/// 检查是否已停止
pub fn is_stopped(&self) -> bool {
self.inner.stopped.load(Ordering::SeqCst)
}
/// 获取取消令牌
pub fn cancellation_token(&self) -> &CancellationToken {
&self.inner.cancel_token
}
/// 🔥 终止进程组
///
/// 向 `-pgid` 发送信号,杀死整个进程组
///
/// # Unix 信号语义
///
/// - `kill(pgid, SIGTERM)` - 发送给单个进程
/// - `kill(-pgid, SIGTERM)` - 发送给整个进程组
/// - `kill(0, SIGTERM)` - 发送给调用者自己的进程组(危险!)
///
/// # 参数
///
/// * `force` - 是否强制使用 SIGKILL否则使用 SIGTERM
async fn kill_process_group(&self, force: bool) -> Result<()> {
let pgid = self.inner.pgid;
#[cfg(unix)]
{
use nix::errno::Errno;
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
// 🔥 关键防御性检查pgid 不能为 0
// kill(0, SIGKILL) 会杀死调用者自己的进程组,这是危险的
if pgid == 0 {
warn!(
"[LifecycleGuard] 进程组 ID 为 0跳过进程组终止可能是初始化失败: project_id={}",
self.inner.project_id
);
return Ok(());
}
// 🔥 关键:使用负的进程组 ID真实的进程组 ID
// -pgid 表示发送信号到整个进程组,而不仅仅是进程组组长
let target = Pid::from_raw(-(pgid as i32));
let signal = if force {
Signal::SIGKILL
} else {
Signal::SIGTERM
};
match kill(target, signal) {
Ok(_) => {
debug!("already sent signal: pgid={}, signal={:?}", pgid, signal);
// 如果是 SIGTERM等待一段时间让进程优雅退出
if !force {
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// 强制杀死进程组
let _ = kill(target, Signal::SIGKILL);
debug!("already force killed: pgid={}", pgid);
}
}
Err(Errno::ESRCH) => {
// 进程组已退出,这是正常的
debug!("process group already exited: pgid={}", pgid);
}
Err(Errno::EPERM) => {
// 权限不足,无法终止进程组
warn!(
"[LifecycleGuard] 权限不足,无法终止进程组: pgid={}, project_id={}",
pgid, self.inner.project_id
);
}
Err(e) => {
// 其他错误(如 EINVAL、EFAULT 等)
debug!(" kill failed: pgid={}, error={:?}", pgid, e);
}
}
info!("Claude process group stopped: pgid={}", pgid);
}
#[cfg(not(unix))]
{
debug!("Unix platform, skipping process group stop");
}
Ok(())
}
}
impl Clone for AgentLifecycleGuard {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl Drop for AgentLifecycleGuard {
fn drop(&mut self) {
let strong_count = Arc::strong_count(&self.inner);
debug!(
"[Claude] AgentLifecycleGuard::drop 开始: project_id={}, pgid={}, strong_count={}",
self.inner.project_id, self.inner.pgid, strong_count
);
// 🔥 使用原子 CAS 操作确保只执行一次清理
// 不再依赖引用计数,因为引用计数可能因为多处 clone 而不准确
// compare_exchange 返回 Ok 表示成功将 false 改为 true即当前线程获得清理权
// 返回 Err 表示已经被其他线程清理,当前线程无需操作
let should_cleanup = self
.inner
.stopped
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok();
if should_cleanup {
debug!(
"[Claude] AgentLifecycleGuard 获得清理权,开始清理资源: {}",
self.inner.project_id
);
// 发送取消信号
self.inner.cancel_token.cancel();
// 注意API 密钥配置的清理由 agent_runner 层的 stop_agent 方法统一负责
// 包括:
// - shared_api_key_manager 中的配置
// - project_uuid_map 中的映射
//
// 这样避免双重清理,确保资源只被清理一次
// 🔥 同步终止进程组
#[cfg(unix)]
{
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
let pgid = self.inner.pgid;
// 🔥 关键防御性检查pgid 不能为 0
// kill(0, SIGKILL) 会杀死调用者自己的进程组,这是危险的
if pgid == 0 {
debug!(
"[Claude] 进程组 ID 为 0跳过进程组终止: project_id={}",
self.inner.project_id
);
} else {
let target = Pid::from_raw(-(pgid as i32));
if let Err(e) = kill(target, Signal::SIGKILL) {
// 进程可能已经退出,这是正常的
debug!(
"[Claude] 终止进程组失败(可能已退出): pgid={}, error={}",
pgid, e
);
} else {
info!(
"[Claude] 进程组已终止: pgid={}, project_id={}",
pgid, self.inner.project_id
);
}
}
}
#[cfg(not(unix))]
{
debug!("[Claude] Unix platform, skipping process group stop");
}
// 注意:后台回收任务 (reaper_task) 会自动完成
// 不需要在这里等待或取消
info!(
"[Claude] AgentLifecycleGuard 清理完成: project_id={}",
self.inner.project_id
);
} else {
debug!(
"[Claude] AgentLifecycleGuard 跳过清理(已被其他引用清理): project_id={}",
self.inner.project_id
);
}
debug!(
"[Claude] AgentLifecycleGuard::drop 完成: project_id={}",
self.inner.project_id
);
}
}
// 为AgentLifecycleGuard实现AgentLifecycle trait
impl AgentLifecycle for AgentLifecycleGuard {
fn graceful_stop(
&self,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + '_>> {
Box::pin(async move { AgentLifecycleGuard::graceful_stop(self).await })
}
fn cancel(&self) {
AgentLifecycleGuard::cancel(self);
}
fn is_stopped(&self) -> bool {
AgentLifecycleGuard::is_stopped(self)
}
fn cancellation_token(&self) -> &CancellationToken {
AgentLifecycleGuard::cancellation_token(self)
}
}

View File

@@ -0,0 +1,91 @@
//! # Agent 启动器模块
//!
//! 提供 Agent 的启动、生命周期管理和通道处理功能。
//!
//! ## SACP 迁移说明
//!
//! 本模块已迁移至 SACP (Symposium ACP) 实现,主要优势:
//! - **支持 Send trait**: 可使用标准 `tokio::spawn`,无需 `LocalSet` + `spawn_local`
//! - **Builder 模式**: 更清晰的连接构建和配置
//! - **回调式消息处理**: 通过 `on_receive_notification` / `on_receive_request` 宏
//!
//! ## 子模块职责
//!
//! | 子模块 | 职责 | 关键类型 |
//! |--------|------|---------|
//! | [`lifecycle`] | Agent 生命周期守卫RAII 资源管理)| `AgentLifecycleGuard` |
//! | [`claude_code_sacp`] | SACP 版本的 Claude Code Agent 启动器 | `SacpClaudeCodeLauncher` |
//!
//! ## Agent 启动流程 (SACP)
//!
//! ```text
//! AcpSessionManager.create_session()
//! │
//! │ 1. 创建 SacpClaudeCodeLauncher
//! ▼
//! SacpClaudeCodeLauncher.launch()
//! │
//! │ 2. 启动 Claude Code 子进程
//! │ 3. 使用 SACP Builder 建立连接
//! │ 4. 创建 AgentLifecycleGuard
//! ▼
//! SacpLauncherConnectionInfo
//! │
//! │ 5. 返回 session_id, prompt_tx, cancel_tx, lifecycle_guard
//! ▼
//! 存入 SessionRegistry
//! ```
//!
//! ## 与 shared_types::AgentLifecycleGuard 的关系
//!
//! `AgentLifecycleGuard` 的核心实现定义在 `shared_types::model::agent_model`。
//! 本模块的 `lifecycle.rs` 提供 Re-export
//! 方便从 `agent_abstraction::launcher` 统一导入。
//!
//! ## 生命周期管理 (RAII)
//!
//! 当 `AgentLifecycleGuard` 被 drop 时:
//! 1. 发送取消信号 (`cancel_token.cancel()`)
//! 2. 终止子进程 (`child.kill()`)
//! 3. 停止 stderr 任务
//!
//! 这确保了 Agent 资源的正确清理,即使在异常情况下也不会泄漏。
mod claude_code_sacp;
pub mod lifecycle;
#[cfg(windows)]
mod windows_launch;
// ============================================================================
// SACP 类型导出(推荐使用)
// ============================================================================
pub use lifecycle::AgentLifecycleGuard;
// 直接导出 SACP 类型
pub use claude_code_sacp::{
SacpAgentLaunchConfig, SacpClaudeCodeLauncher, SacpLauncherConnectionInfo,
convert_context_servers_sacp, get_default_sacp_agent_config, load_sacp_agent_config,
};
// ============================================================================
// 兼容性类型别名(向后兼容旧代码)
// ============================================================================
/// 兼容性别名ClaudeCodeLauncher -> SacpClaudeCodeLauncher
pub type ClaudeCodeLauncher<N> = SacpClaudeCodeLauncher<N>;
/// 兼容性别名LauncherConnectionInfoComplete -> SacpLauncherConnectionInfo
pub type LauncherConnectionInfoComplete = SacpLauncherConnectionInfo;
/// 兼容性别名AgentLaunchConfig -> SacpAgentLaunchConfig
pub type AgentLaunchConfig = SacpAgentLaunchConfig;
/// 兼容性别名load_agent_config -> load_sacp_agent_config
pub use claude_code_sacp::load_sacp_agent_config as load_agent_config;
/// 兼容性别名get_default_agent_config -> get_default_sacp_agent_config
pub use claude_code_sacp::get_default_sacp_agent_config as get_default_agent_config;
/// 兼容性别名convert_context_servers -> convert_context_servers_sacp
pub use claude_code_sacp::convert_context_servers_sacp as convert_context_servers;

View File

@@ -0,0 +1,411 @@
use std::path::{Path, PathBuf};
use tracing::{debug, info, warn};
pub const CREATE_NO_WINDOW_FLAG: u32 = 0x0800_0000;
fn resolve_windows_node_exe() -> Option<PathBuf> {
// 1. 显式指定的 node 可执行文件路径(最高优先级)
if let Ok(path) = std::env::var("NUWAX_NODE_PATH") {
let node = PathBuf::from(path);
if node.exists() {
info!(
"[SACP] Windows node resolved via NUWAX_NODE_PATH: {}",
node.display()
);
return Some(node);
}
}
// 1.5 Tauri 层设置的 NUWAX_NODE_EXEsidecar node-runtime.exe
if let Ok(path) = std::env::var("NUWAX_NODE_EXE") {
let node = PathBuf::from(&path);
if node.exists() {
info!(
"[SACP] Windows node resolved via NUWAX_NODE_EXE: {}",
node.display()
);
return Some(node);
}
}
// 2. 应用集成的 runtime 路径Tauri 打包后设置的 NUWAX_APP_RUNTIME_PATH
// 包含 runtime\node\bin 等目录,内含集成的 node.exe
if let Ok(runtime_path) = std::env::var("NUWAX_APP_RUNTIME_PATH") {
for dir in runtime_path.split(';') {
let dir = dir.trim();
if dir.is_empty() {
continue;
}
let node = PathBuf::from(dir).join("node.exe");
if node.exists() {
info!(
"[SACP] Windows node resolved via NUWAX_APP_RUNTIME_PATH: {}",
node.display()
);
return Some(node);
}
}
}
// 3. 从 APPDATA 推导默认的应用 runtime 路径NUWAX_APP_RUNTIME_PATH 未设置时的回退)
if let Ok(appdata) = std::env::var("APPDATA") {
let default_node = PathBuf::from(&appdata)
.join("com.nuwax.agent-tauri-client")
.join("runtime")
.join("node")
.join("bin")
.join("node.exe");
if default_node.exists() {
info!(
"[SACP] Windows node resolved via APPDATA default path: {}",
default_node.display()
);
return Some(default_node);
}
}
warn!(
"[SACP] Windows node resolution failed: NUWAX_NODE_PATH, NUWAX_NODE_EXE, NUWAX_APP_RUNTIME_PATH and APPDATA default paths all missed"
);
None
}
fn npm_package_entry_from_dir(
package_dir: &std::path::Path,
package_name: &str,
) -> Option<PathBuf> {
let package_json = package_dir.join("package.json");
let content = std::fs::read_to_string(package_json).ok()?;
let package_json: serde_json::Value = serde_json::from_str(&content).ok()?;
let bin_field = package_json.get("bin")?;
let rel_entry = if let Some(bin_str) = bin_field.as_str() {
Some(bin_str.to_string())
} else if let Some(bin_map) = bin_field.as_object() {
bin_map
.get(package_name)
.and_then(|v| v.as_str())
.map(str::to_string)
.or_else(|| {
bin_map
.values()
.find_map(|v| v.as_str())
.map(str::to_string)
})
} else {
None
}?;
let entry = package_dir.join(rel_entry);
if entry.exists() { Some(entry) } else { None }
}
fn get_windows_cmd_script_path(path: &std::path::Path) -> Option<PathBuf> {
match path.extension().and_then(|s| s.to_str()) {
Some(ext) if ext.eq_ignore_ascii_case("cmd") => Some(path.to_path_buf()),
None => {
// 先检查直接路径(绝对路径 or 当前目录)
let direct_candidate = path.with_extension("cmd");
if direct_candidate.exists() {
return Some(direct_candidate);
}
// which 未命中时 path 为裸命令名,需要在已知目录中搜索
let program_name = path.file_name().and_then(|s| s.to_str())?;
// 搜索应用自管的 node_modules/.bin
if let Ok(appdata) = std::env::var("APPDATA") {
let app_private = PathBuf::from(&appdata)
.join("com.nuwax.agent-tauri-client")
.join("node_modules")
.join(".bin")
.join(format!("{}.cmd", program_name));
if app_private.exists() {
return Some(app_private);
}
}
// 搜索 NUWAX_APP_RUNTIME_PATH 中的每个目录
if let Ok(runtime_path) = std::env::var("NUWAX_APP_RUNTIME_PATH") {
for dir in runtime_path.split(';').filter(|s| !s.trim().is_empty()) {
let candidate =
std::path::Path::new(dir.trim()).join(format!("{}.cmd", program_name));
if candidate.exists() {
return Some(candidate);
}
}
}
None
}
_ => None,
}
}
fn resolve_js_entry_from_cmd_shim(cmd_script: &std::path::Path) -> Option<PathBuf> {
let content = match std::fs::read_to_string(cmd_script) {
Ok(c) => c,
Err(e) => {
debug!(
"[SACP] cmd shim read failed: {} ({})",
cmd_script.display(),
e
);
return None;
}
};
let base_dir = cmd_script.parent()?;
for raw_line in content.lines() {
let line = raw_line.trim();
// 支持多种 npm shim 格式:%~dp0旧版、%dp0%(新版 npm >=7、%dp0变体
let base_token = if line.contains("%~dp0") {
"%~dp0"
} else if line.contains("%dp0%") {
"%dp0%"
} else if line.contains("%dp0") {
"%dp0"
} else {
continue;
};
let clean = line.replace('"', "");
let clean_lower = clean.to_ascii_lowercase();
let ext_end = [".cjs", ".mjs", ".js"]
.iter()
.filter_map(|ext| clean_lower.find(ext).map(|pos| pos + ext.len()))
.min();
let Some(end) = ext_end else {
continue;
};
let start = clean.find(base_token)?;
if end <= start + base_token.len() || end > clean.len() {
continue;
}
let rel = clean[start + base_token.len()..end].trim_start_matches(['\\', '/']);
if rel.is_empty() {
continue;
}
let rel = rel.replace('/', "\\");
let entry = base_dir.join(std::path::Path::new(&rel));
if entry.exists() {
debug!(
"[SACP] cmd shim resolved entry: {} -> {}",
cmd_script.display(),
entry.display()
);
return Some(entry);
}
}
debug!("[SACP] cmd shim not resolved: {}", cmd_script.display());
None
}
pub fn resolve_windows_node_cli_command(
command: &str,
args: &[String],
) -> Option<(String, Vec<String>)> {
let command_path = which::which(command).unwrap_or_else(|_| PathBuf::from(command));
let path = command_path.as_path();
info!(
"[SACP] Windows command resolution started: input={}, resolved={}",
command,
path.display()
);
let cmd_script = get_windows_cmd_script_path(path);
let node_exe = resolve_windows_node_exe()?;
info!(
"[SACP] Windows node.exe already resolved: {}",
node_exe.display()
);
if let Some(cmd_script) = cmd_script.as_ref() {
info!("[SACP] using cmd shim: {}", cmd_script.display());
if let Some(js_entry) = resolve_js_entry_from_cmd_shim(cmd_script) {
let mut actual_args = Vec::with_capacity(args.len() + 1);
actual_args.push(js_entry.to_string_lossy().to_string());
actual_args.extend(args.iter().cloned());
info!(
"[SACP] cmd shim resolution succeeded: {} -> {}",
cmd_script.display(),
js_entry.display()
);
return Some((node_exe.to_string_lossy().to_string(), actual_args));
}
info!(
"[SACP] cmd shim exists but JS entry not resolved, falling back to package.json bin: {}",
cmd_script.display()
);
} else {
info!(
"[SACP] cmd shim not found, falling back to package.json bin: command={}",
command
);
}
let package_name = match path.extension().and_then(|s| s.to_str()) {
Some(ext) if ext.eq_ignore_ascii_case("cmd") => path
.file_stem()
.and_then(|s| s.to_str())
.map(str::to_string),
None => path
.file_name()
.and_then(|s| s.to_str())
.map(str::to_string),
_ => None,
}?;
let mut package_dirs: Vec<PathBuf> = Vec::new();
// 从 which 解析到的 .bin 目录向上推导 node_modules
if let Some(parent) = path.parent() {
let is_npm_bin = parent
.file_name()
.map(|s| s.to_string_lossy().eq_ignore_ascii_case(".bin"))
.unwrap_or(false);
if is_npm_bin {
if let Some(node_modules_dir) = parent.parent() {
package_dirs.push(node_modules_dir.join(&package_name));
}
}
}
// 仅搜索应用自管的 node_modules
if let Ok(appdata) = std::env::var("APPDATA") {
package_dirs.push(
PathBuf::from(appdata)
.join("com.nuwax.agent-tauri-client")
.join("node_modules")
.join(&package_name),
);
}
for package_dir in package_dirs {
if !package_dir.exists() {
info!(
"[SACP] package directorynot found, skip: {}",
package_dir.display()
);
continue;
}
if let Some(js_entry) = npm_package_entry_from_dir(&package_dir, &package_name) {
let mut actual_args = Vec::with_capacity(args.len() + 1);
actual_args.push(js_entry.to_string_lossy().to_string());
actual_args.extend(args.iter().cloned());
info!(
"[SACP] package.json bin resolved: {} -> {}",
package_name,
js_entry.display()
);
return Some((node_exe.to_string_lossy().to_string(), actual_args));
}
info!(
"[SACP] package.json bin resolution failed: package={}, dir={}",
package_name,
package_dir.display()
);
}
warn!(
"[SACP] Windows command resolution failed, no JS entry found: command={}",
command
);
None
}
/// Windows 下将「命令 + 参数」规范化为不弹 CMD 窗口的形式。
///
/// 背景Windows 上 npm 全局安装会创建 .cmd/.bat直接执行会弹出 CMD 窗口。
/// 本函数按扩展名检测并尽可能转换为 `node.exe + JS 入口`,避免弹窗。
///
/// 检查顺序(从优到劣):
/// 1. `.exe` → 原生二进制,直接返回(不修改)
/// 2. `.cmd` / `.bat` → 尝试解析为 node.exe + JS成功则返回新 (path, args)
/// 3. 其他扩展名 → 直接返回
/// 4. 无扩展名 → 用 `which` 解析后再按 13 处理
///
/// 仅编译于 Windows调用方需使用 `#[cfg(windows)]`。
#[cfg(windows)]
pub fn normalize_windows_command_for_no_window(
command_path: String,
command_args: Vec<String>,
) -> (String, Vec<String>) {
let mut path = command_path;
let mut args = command_args;
let path_ref = Path::new(&path);
let ext = path_ref
.extension()
.and_then(|e| e.to_str())
.map(|s| s.to_ascii_lowercase());
match ext.as_deref() {
Some("exe") => {
info!(
"[SACP] Windows detected native .exe: {} - no popup window",
path
);
}
Some("cmd" | "bat") => {
info!("[SACP] 🔍 Windows detecting .cmd/.bat: {}", path);
info!("[SACP] 🔄 converting to node.exe + JS ...");
if let Some((node_path, js_args)) = resolve_windows_node_cli_command(&path, &args) {
info!("[SACP] conversion succeeded: {} + {:?}", node_path, js_args);
path = node_path;
args = js_args;
} else {
warn!(
"[SACP] ⚠️ conversion failed, will run original command (may show popup): {}",
path
);
}
}
Some(other) => {
info!(
"[SACP] Windows detected other format .{}: {} - trying direct execution",
other, path
);
}
None => {
info!("[SACP] 🔍 Windows detecting extension: {}", path);
if let Ok(resolved) = which::which(&path) {
let resolved_str = resolved.to_string_lossy().to_string();
info!("[SACP] 🔄 resolved: {}", resolved_str);
let resolved_ext = resolved
.extension()
.and_then(|e| e.to_str())
.map(|s| s.to_ascii_lowercase());
match resolved_ext.as_deref() {
Some("exe") => {
info!("[SACP] detected .exe - no popup window");
path = resolved_str;
}
Some("cmd" | "bat") => {
info!("[SACP] 🔍 detected .cmd/.bat, converting ...");
if let Some((node_path, js_args)) =
resolve_windows_node_cli_command(&resolved_str, &args)
{
info!("[SACP] conversion succeeded: {} + {:?}", node_path, js_args);
path = node_path;
args = js_args;
} else {
warn!("[SACP] ⚠️ conversion failed");
}
}
_ => {
info!("[SACP] unknown extension, keeping original");
path = resolved_str;
}
}
} else {
warn!("[SACP] ⚠️ unable to resolve path: {}", path);
}
}
}
(path, args)
}

View File

@@ -0,0 +1,86 @@
//! # Agent Abstraction Layer (SACP 版本)
//!
//! 此 crate 提供 Agent 管理的抽象接口,是 RCoder 架构的中间层。
//!
//! ## SACP 迁移说明
//!
//! 本模块已迁移至 SACP (Symposium ACP) 实现:
//! - **移除 Client trait**: `AcpSessionManager<N, R>` 不再需要 `Client` 泛型参数
//! - **支持 Send trait**: 可使用标准 `tokio::spawn`,无需 `LocalSet`
//! - **简化架构**: 移除了 channel handlerSACP 内部使用回调处理
//!
//! ## 架构位置
//!
//! ```text
//! shared_types (基础类型: ProjectAndAgentInfo, AgentLifecycleGuard)
//! ↓
//! agent_abstraction (抽象接口) ← 你在这里
//! ↓
//! agent_runner (具体实现: AgentSessionRegistry, SseSessionNotifier)
//! ```
//!
//! ## 模块职责
//!
//! | 模块 | 职责 | 关键类型 |
//! |------|------|---------|
//! | [`traits`] | 核心抽象接口定义 | `SessionRegistry`, `SessionNotifier` |
//! | [`session`] | 会话管理和 Worker 抽象 | `AcpSessionManager`, `AcpAgentWorker` |
//! | [`launcher`] | Agent 启动和生命周期管理 | `SacpClaudeCodeLauncher` |
//! | [`acp`] | ACP 协议连接抽象 | `CancelResult` |
//!
//! ## Trait 实现指南
//!
//! 本 crate 定义的 trait 预期在 `agent_runner` 中实现:
//!
//! - [`SessionRegistry`] → `agent_runner::service::AgentSessionRegistry` (全局 `AGENT_REGISTRY`)
//! - [`SessionNotifier`] → `agent_runner::service::SseSessionNotifier`
//!
//! ### 使用模式 (SACP)
//!
//! ```ignore
//! // 在 agent_runner 中:
//! // 1. 实现 SessionRegistry trait
//! impl SessionRegistry for AgentSessionRegistry { ... }
//!
//! // 2. 通过依赖注入传入 AcpSessionManager不再需要 Client 类型参数)
//! let session_manager = AcpSessionManager::new(
//! Arc::new(notifier), // SessionNotifier 实现
//! AGENT_REGISTRY.clone(), // SessionRegistry 实现
//! );
//! ```
//!
//! ## 设计模式
//!
//! - **依赖反转DIP**: Trait 在此定义,实现在上层 crate
//! - **泛型参数**: `AcpSessionManager<N, R>` 支持可插拔实现
//! - **RAII**: 通过 `AgentLifecycleGuard` 管理 Agent 资源生命周期
//!
//! ## 与 AGENT_REGISTRY 的关系
//!
//! `AGENT_REGISTRY` 是 `agent_runner` 中的全局单例,实现了 `SessionRegistry` trait。
//! 通过依赖注入传入 `AcpSessionManager`,使得抽象层不依赖具体实现。
//!
//! - **直接访问 AGENT_REGISTRY**: 仅在 `agent_runner` 内部使用(如清理任务、状态查询)
//! - **通过 SessionRegistry trait**: 在 `agent_abstraction` 内部使用(会话管理逻辑)
pub mod acp;
pub mod error;
pub mod launcher;
pub mod mirror_env;
pub mod path_env;
pub mod session;
pub mod traits;
// Re-export types from submodules
pub use acp::{CancelNotificationRequestWrapper, CancelResult};
pub use error::AgentAbstractionError;
pub use launcher::{
AgentLaunchConfig, AgentLifecycleGuard, ClaudeCodeLauncher, LauncherConnectionInfoComplete,
convert_context_servers, get_default_agent_config, load_agent_config,
};
pub use session::{
AcpAgentWorker, AcpSessionManager, AgentWorker, SessionHandles, WorkerRequest, WorkerResponse,
};
pub use traits::agent::{AgentStartConfig, PromptMessage};
pub use traits::session_notifier::SessionNotifier;
pub use traits::session_registry::SessionRegistry;

View File

@@ -0,0 +1,54 @@
//! 镜像源环境变量透传
//!
//! 收集当前进程中的镜像源配置,供 MCP 子进程npx/bunx/uvx使用。
//!
//! 变量来源优先级:
//! 1. 直接变量npm_config_registry / UV_INDEX_URL / PIP_INDEX_URL
//! 2. MCP_PROXY_* 中转变量MCP_PROXY_NPM_REGISTRY / MCP_PROXY_PYPI_INDEX_URL
/// 需要透传的镜像源环境变量映射
///
/// 格式: (目标变量名, 后备变量名)
const MIRROR_MAPPINGS: &[(&str, &str)] = &[
("npm_config_registry", "MCP_PROXY_NPM_REGISTRY"),
("UV_INDEX_URL", "MCP_PROXY_PYPI_INDEX_URL"),
("PIP_INDEX_URL", "MCP_PROXY_PYPI_INDEX_URL"),
];
/// 收集需要透传给子进程的镜像源环境变量
///
/// 每个目标变量优先读取自身,未设置时从对应的 MCP_PROXY_* 后备变量转换。
/// 返回 `Vec<(key, value)>`,仅包含有值的条目。
pub fn collect_mirror_env_vars() -> Vec<(String, String)> {
MIRROR_MAPPINGS
.iter()
.filter_map(|&(target, fallback)| {
let val = std::env::var(target)
.or_else(|_| std::env::var(fallback))
.ok()?;
Some((target.to_string(), val))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn collect_does_not_panic() {
let _ = collect_mirror_env_vars();
}
#[test]
fn mappings_have_correct_targets() {
// 确保目标变量名是工具直接使用的名称
assert!(
MIRROR_MAPPINGS
.iter()
.any(|(t, _)| *t == "npm_config_registry")
);
assert!(MIRROR_MAPPINGS.iter().any(|(t, _)| *t == "UV_INDEX_URL"));
assert!(MIRROR_MAPPINGS.iter().any(|(t, _)| *t == "PIP_INDEX_URL"));
}
}

View File

@@ -0,0 +1,120 @@
//! 子进程环境变量管理 - 供 Agent 子进程使用
//!
//! 提供 PATH 环境变量构建函数,供 rcoder 启动 Claude Code 等 Agent 时使用。
use std::path::PathBuf;
/// Tauri 桌面客户端在 APPDATA 下的应用数据目录名
/// Windows: %APPDATA%/{TAURI_APP_DATA_DIR}
pub const TAURI_APP_DATA_DIR: &str = "com.nuwax.agent-tauri-client";
/// 在系统 PATH 中查找可执行文件
fn find_in_path(executable: &str) -> Option<String> {
let output = if cfg!(windows) {
std::process::Command::new("where").arg(executable).output()
} else {
std::process::Command::new("which").arg(executable).output()
};
match output {
Ok(o) if o.status.success() => {
let binding = String::from_utf8_lossy(&o.stdout);
binding
.lines()
.next()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
}
_ => None,
}
}
/// 在系统 PATH 中查找可执行文件并返回其父目录
fn find_bin_dir(executable: &str) -> Option<String> {
find_in_path(executable).and_then(|path| {
let parent = PathBuf::from(&path).parent()?.to_path_buf();
let parent_str = parent.to_string_lossy().to_string();
// 如果父目录是 "cmd",尝试找同级的 "bin" 目录
// 例如: D:\Program Files\Git\cmd -> D:\Program Files\Git\bin
if parent_str.to_lowercase().ends_with("cmd") {
let bin_dir = parent
.file_name()
.and_then(|n| n.to_str())
.map(|n| {
if n.eq_ignore_ascii_case("cmd") {
parent
.parent()
.map(|p| p.join("bin").to_string_lossy().to_string())
} else {
None
}
})
.flatten();
if let Some(bin) = bin_dir {
if PathBuf::from(&bin).join("bash.exe").exists() {
return Some(bin);
}
}
}
Some(parent_str)
})
}
/// Windows: 查找 Git Bash 路径
///
/// 查找系统 PATH 中的 bash.exe返回其 bin 目录路径
/// 如果 bash.exe 在 Git\cmd 目录下,会自动查找同级的 Git\bin 目录
#[cfg(windows)]
pub fn find_git_bash_path() -> Option<String> {
find_bin_dir("bash.exe")
}
/// 构建适用于 rcoder 进程的 PATH 环境变量。
///
/// 优先级:
/// 1. NUWAX_APP_RUNTIME_PATH优先
/// 2. APPDATA 中的 node bin 和 npm bin回退
///
/// 返回 PATH 字符串,如果都未找到则返回 None
#[cfg(windows)]
pub fn build_rcoder_path_env() -> Option<String> {
use tracing::info;
let sep = ";";
// 1. 优先从 NUWAX_APP_RUNTIME_PATH 构建
if let Ok(runtime_path) = std::env::var("NUWAX_APP_RUNTIME_PATH") {
let runtime_path = runtime_path.trim().to_string();
if !runtime_path.is_empty() {
info!("[Env] Using NUWAX_APP_RUNTIME_PATH to build PATH");
return Some(runtime_path);
}
}
// 2. 从 APPDATA 推导默认路径
if let Ok(appdata) = std::env::var("APPDATA") {
let app_base = PathBuf::from(&appdata).join(TAURI_APP_DATA_DIR);
let mut paths: Vec<String> = Vec::new();
let node_bin = app_base.join("runtime").join("node").join("bin");
if node_bin.exists() {
paths.push(node_bin.to_string_lossy().to_string());
}
let npm_bin = app_base.join("node_modules").join(".bin");
if npm_bin.exists() {
paths.push(npm_bin.to_string_lossy().to_string());
}
if !paths.is_empty() {
let path_value = paths.join(sep);
info!("[Env] APPDATA built PATH: {}", path_value);
return Some(path_value);
}
}
None
}

View 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(),
))
}
}
}

View 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};

View File

@@ -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
}

View File

@@ -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 会被 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()
}
}

View 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;
}

View File

@@ -0,0 +1,413 @@
//! Agent trait definition.
use agent_client_protocol::schema::McpServer;
use tracing::{debug, info};
/// Agent startup configuration
///
/// Contains additional configuration information required to start an Agent,
/// such as system prompts, MCP server configurations, etc.
/// These configurations are passed to the Agent via ACP protocol's NewSessionRequest.
#[derive(Debug, Clone)]
pub struct AgentStartConfig {
/// System prompt (passed via meta.systemPrompt.append)
///
/// Uses the `_meta.systemPrompt.append` mode of the ACP protocol to preserve
/// the base capabilities of the claude_code preset while appending custom system prompts.
pub system_prompt: Option<String>,
/// MCP server configuration
pub mcp_servers: Vec<McpServer>,
/// Additional meta fields
///
/// Can pass any additional configuration information to the Agent
pub extra_meta: Option<serde_json::Map<String, serde_json::Value>>,
/// Service type (for loading corresponding config, required)
pub service_type: shared_types::ServiceType,
/// Session ID for resuming sessions
///
/// When resuming a previous session, pass the previous session_id,
/// which will be passed to the Agent via `_meta.claudeCode.options.resume`.
pub resume_session_id: Option<String>,
/// 🆕 Agent server config override (for custom Agent startup command)
///
/// When the user request specifies agent_server config, use this config
/// to override the default config. Includes command, args, env, etc.
pub agent_server_override: Option<shared_types::ChatAgentServerConfig>,
/// 🆕 ACP session creation timeout (seconds), default 100
///
/// Maximum wait time for Agent to create a new session
/// (may require more time when there are many MCP tools)
pub acp_session_create_timeout_secs: Option<u64>,
/// 🆕 Agent cancel call timeout (seconds), default 10
///
/// Maximum wait time for Agent internal cancel operations
pub agent_cancel_timeout_secs: Option<u64>,
}
impl AgentStartConfig {
/// Create a new AgentStartConfig
///
/// # Arguments
/// - `service_type`: Service type (required)
pub fn new(service_type: shared_types::ServiceType) -> Self {
Self {
system_prompt: None,
mcp_servers: Vec::new(),
extra_meta: None,
service_type,
resume_session_id: None,
agent_server_override: None,
acp_session_create_timeout_secs: None,
agent_cancel_timeout_secs: None,
}
}
/// Set system prompt
pub fn with_system_prompt(mut self, system_prompt: String) -> Self {
self.system_prompt = Some(system_prompt);
self
}
/// Set MCP server configuration
pub fn with_mcp_servers(mut self, mcp_servers: Vec<McpServer>) -> Self {
self.mcp_servers = mcp_servers;
self
}
/// Set additional meta fields
pub fn with_extra_meta(
mut self,
extra_meta: serde_json::Map<String, serde_json::Value>,
) -> Self {
self.extra_meta = Some(extra_meta);
self
}
/// Set service type
pub fn with_service_type(mut self, service_type: shared_types::ServiceType) -> Self {
self.service_type = service_type;
self
}
/// Set session ID for resuming sessions
pub fn with_resume_session_id(mut self, session_id: String) -> Self {
self.resume_session_id = Some(session_id);
self
}
/// 🆕 Set Agent server config override
pub fn with_agent_server_override(
mut self,
agent_server: shared_types::ChatAgentServerConfig,
) -> Self {
self.agent_server_override = Some(agent_server);
self
}
/// 🆕 Set ACP session creation timeout
pub fn with_acp_session_create_timeout(mut self, timeout_secs: u64) -> Self {
self.acp_session_create_timeout_secs = Some(timeout_secs);
self
}
/// 🆕 Set Agent cancel call timeout
pub fn with_agent_cancel_timeout(mut self, timeout_secs: u64) -> Self {
self.agent_cancel_timeout_secs = Some(timeout_secs);
self
}
/// Build the meta field for NewSessionRequest
///
/// Merges the system prompt and additional meta fields into a JSON Map
/// for passing to the ACP protocol's NewSessionRequest.
///
/// # Returns
/// Returns a Meta object containing `systemPrompt: { append: "..." }`
pub fn build_meta(&self) -> serde_json::Map<String, serde_json::Value> {
let mut meta = serde_json::Map::new();
// Add system prompt (using append mode)
if let Some(ref system_prompt) = self.system_prompt {
let mut system_prompt_obj = serde_json::Map::new();
system_prompt_obj.insert(
"append".to_string(),
serde_json::Value::String(system_prompt.clone()),
);
meta.insert(
"systemPrompt".to_string(),
serde_json::Value::Object(system_prompt_obj),
);
info!(
"[ACP] Sending system_prompt to agent: length={}",
system_prompt.len()
);
debug!(
"[ACP] system_prompt content (first 200 chars): \"{}\"",
&system_prompt[..system_prompt.len().min(200)]
);
} else {
info!("[ACP] No system_prompt to send to agent");
}
// Build claudeCode.options structure
// settingSources 控制 Claude Code Agent 从哪些来源加载配置和 skills。
// 可选值: "user"(全局 ~/.claude/), "project"(项目 .claude/), "local"
// 只加载 "project" 级别配置,避免全局 user 配置覆盖 ANTHROPIC_BASE_URL
// 同时允许项目目录下的 .claude/skills/ 自定义技能生效。
// Refer to the TypeScript code on the Agent side:
// resume: (params._meta as NewSessionMeta | undefined)?.claudeCode?.options?.resume
let mut options = serde_json::Map::new();
// Only load project-level settings (skills), block global settings
options.insert(
"settingSources".to_string(),
serde_json::Value::Array(vec![serde_json::Value::String("project".to_string())]),
);
// Add resume session_id if present
if let Some(ref session_id) = self.resume_session_id {
options.insert(
"resume".to_string(),
serde_json::Value::String(session_id.clone()),
);
}
let mut claude_code = serde_json::Map::new();
claude_code.insert("options".to_string(), serde_json::Value::Object(options));
meta.insert(
"claudeCode".to_string(),
serde_json::Value::Object(claude_code),
);
// Merge additional meta fields
if let Some(ref extra) = self.extra_meta {
for (key, value) in extra {
// Don't overwrite if key already exists (e.g., systemPrompt)
if !meta.contains_key(key) {
meta.insert(key.clone(), value.clone());
}
}
}
meta
}
/// Check if there is a system prompt
pub fn has_system_prompt(&self) -> bool {
self.system_prompt.is_some()
}
/// Check if there are MCP server configurations
pub fn has_mcp_servers(&self) -> bool {
!self.mcp_servers.is_empty()
}
/// Build meta without resume parameter
///
/// Used when the list_sessions check finds the session does not exist.
/// Similar to build_meta(), but without the claudeCode.options.resume field.
///
/// # Use Cases
/// - User passed a session_id, but verification via list_sessions API found it doesn't exist
/// - Create a new session while preserving other configs (system prompt, extra_meta, etc.)
pub fn build_meta_without_resume(&self) -> serde_json::Map<String, serde_json::Value> {
let mut meta = serde_json::Map::new();
// Only add system prompt (using append mode, consistent with build_meta)
if let Some(ref system_prompt) = self.system_prompt {
let mut system_prompt_obj = serde_json::Map::new();
system_prompt_obj.insert(
"append".to_string(),
serde_json::Value::String(system_prompt.clone()),
);
meta.insert(
"systemPrompt".to_string(),
serde_json::Value::Object(system_prompt_obj),
);
info!(
"[ACP] Sending system_prompt to agent (no resume): length={}",
system_prompt.len()
);
debug!(
"[ACP] system_prompt content (first 200 chars): \"{}\"",
&system_prompt[..system_prompt.len().min(200)]
);
}
// Only load project-level settings (consistent with build_meta)
let mut options = serde_json::Map::new();
options.insert(
"settingSources".to_string(),
serde_json::Value::Array(vec![serde_json::Value::String("project".to_string())]),
);
let mut claude_code = serde_json::Map::new();
claude_code.insert("options".to_string(), serde_json::Value::Object(options));
meta.insert(
"claudeCode".to_string(),
serde_json::Value::Object(claude_code),
);
// Merge additional meta fields (don't overwrite existing keys)
if let Some(ref extra) = self.extra_meta {
for (key, value) in extra {
if !meta.contains_key(key) {
meta.insert(key.clone(), value.clone());
}
}
}
meta
}
}
/// Agent common Prompt message
///
/// This is the core data structure of the Agent abstraction layer, containing only
/// the core information needed for the Agent to execute tasks.
/// Does not contain business layer configurations (such as model_provider, mcp configs, etc.).
#[derive(Debug, Clone)]
pub struct PromptMessage {
/// Main prompt text entered by the user
pub content: String,
/// Project ID
pub project_id: String,
/// Project working directory path
pub project_path: std::path::PathBuf,
/// Session ID (optional, if None the Agent will create a new session)
pub session_id: Option<String>,
/// Request tracking ID
pub request_id: String,
/// Attachment list (supports text, images, audio, documents, etc.)
pub attachments: Vec<shared_types::Attachment>,
/// Data source attachments (JSON string array)
pub data_source_attachments: Vec<String>,
/// Service type
pub service_type: shared_types::ServiceType,
// === New fields (v2) ===
/// System prompt override
///
/// If provided, will override the default system prompt configuration
pub system_prompt_override: Option<String>,
/// User prompt template override
///
/// If provided, will use this template to replace the `{user_prompt}` variable
pub user_prompt_template_override: Option<String>,
/// Agent runtime config override (MCP servers, etc.)
///
/// Contains Agent server configuration and MCP server configuration
pub agent_config_override: Option<shared_types::ChatAgentConfig>,
}
impl PromptMessage {
/// Create a new PromptMessage
pub fn new(
content: String,
project_id: String,
project_path: std::path::PathBuf,
request_id: String,
service_type: shared_types::ServiceType,
) -> Self {
Self {
content,
project_id,
project_path,
session_id: None,
request_id,
attachments: Vec::new(),
data_source_attachments: Vec::new(),
service_type,
// New fields default to None
system_prompt_override: None,
user_prompt_template_override: None,
agent_config_override: None,
}
}
/// Set session ID
pub fn with_session_id(mut self, session_id: Option<String>) -> Self {
self.session_id = session_id;
self
}
/// Set attachment list
pub fn with_attachments(mut self, attachments: Vec<shared_types::Attachment>) -> Self {
self.attachments = attachments;
self
}
/// Set data source attachments
pub fn with_data_source_attachments(mut self, data_source_attachments: Vec<String>) -> Self {
self.data_source_attachments = data_source_attachments;
self
}
/// Set system prompt override
pub fn with_system_prompt_override(mut self, system_prompt: Option<String>) -> Self {
self.system_prompt_override = system_prompt;
self
}
/// Set user prompt template override
pub fn with_user_prompt_template_override(mut self, template: Option<String>) -> Self {
self.user_prompt_template_override = template;
self
}
/// Set Agent config override
pub fn with_agent_config_override(
mut self,
config: Option<shared_types::ChatAgentConfig>,
) -> Self {
self.agent_config_override = config;
self
}
}
/// Convert from ChatPrompt to PromptMessage
impl From<shared_types::ChatPrompt> for PromptMessage {
fn from(chat_prompt: shared_types::ChatPrompt) -> Self {
info!(
"[agent_abstraction] Converting ChatPrompt to PromptMessage, project_id={:?}, session_id={:?}, has_model_provider={}, has_agent_config_override={}",
chat_prompt.project_id,
chat_prompt.session_id,
chat_prompt.model_provider.is_some(),
chat_prompt.agent_config_override.is_some(),
);
Self {
content: chat_prompt.prompt,
project_id: chat_prompt.project_id,
project_path: chat_prompt.project_path,
session_id: chat_prompt.session_id,
request_id: chat_prompt.request_id.unwrap_or_else(|| {
// Generate one if ChatPrompt doesn't have request_id
uuid::Uuid::new_v4().to_string().replace("-", "")
}),
attachments: chat_prompt.attachments,
data_source_attachments: chat_prompt.data_source_attachments,
service_type: chat_prompt.service_type,
// Map new fields
system_prompt_override: chat_prompt.system_prompt_override,
user_prompt_template_override: chat_prompt.user_prompt_template_override,
agent_config_override: chat_prompt.agent_config_override,
}
}
}

View File

@@ -0,0 +1,9 @@
//! Trait definitions for agent abstraction.
pub mod agent;
pub mod session_notifier;
pub mod session_registry;
pub use agent::{AgentStartConfig, PromptMessage};
pub use session_notifier::SessionNotifier;
pub use session_registry::SessionRegistry;

View File

@@ -0,0 +1,113 @@
//! # Session Notifier Trait
//!
//! 定义会话通知的抽象接口,用于推送 SSE 消息到前端。
//! `agent_runner` 模块实现此 trait 来完成实际的消息推送。
//!
//! ## 预期实现
//!
//! 此 trait 预期在 `agent_runner` 中实现:
//!
//! - **实现类**: `agent_runner::service::SseSessionNotifier`
//! - **包装类**: `agent_runner::service::StateAwareNotifier`(注入 project_id
//!
//! ## 架构说明
//!
//! ```text
//! agent_abstraction agent_runner
//! ┌─────────────────┐ ┌─────────────────────┐
//! │ SessionNotifier │◄────────────────│ SseSessionNotifier │
//! │ (trait) │ implements │ (struct) │
//! └────────┬────────┘ └──────────┬──────────┘
//! │ │
//! │ ┌──────────▼──────────┐
//! ┌────────▼────────┐ │ StateAwareNotifier │
//! │AcpSessionManager│◄────────────────│ (wrapper, 注入 │
//! │ notifier: N │ injects │ project_id) │
//! └─────────────────┘ └─────────────────────┘
//! ```
//!
//! ## 方法使用场景
//!
//! | 方法 | 触发时机 | 推送内容 |
//! |------|---------|---------|
//! | `notify_prompt_start` | Agent 开始处理请求 | 会话开始通知 |
//! | `notify_prompt_end` | Agent 完成处理 | 会话结束通知(含停止原因)|
//! | `notify_prompt_error` | Agent 处理出错 | 错误通知 |
//! | `notify_session_update` | Agent 输出内容 | 内容块更新(文本、工具调用等)|
//! | `notify` | 通用通知 | 任意 SessionNotify 类型 |
//!
//! ## 使用示例
//!
//! ```ignore
//! // 在 agent_runner 中定义实现
//! pub struct SseSessionNotifier;
//!
//! #[async_trait]
//! impl SessionNotifier for SseSessionNotifier {
//! async fn notify_prompt_start(...) -> Result<...> {
//! // 推送到 SESSION_CACHE由 SSE handler 分发
//! }
//! }
//!
//! // 使用 StateAwareNotifier 包装,自动注入 project_id
//! let notifier = StateAwareNotifier::new(project_id.clone());
//! let session_manager = AcpSessionManager::new(Arc::new(notifier), registry);
//! ```
use async_trait::async_trait;
use shared_types::SessionNotify;
/// 会话通知器 trait
///
/// 提供会话消息推送的抽象接口,解耦 agent_abstraction 和具体的 SSE 实现。
///
/// # 设计说明
/// - agent_abstraction 只依赖此 trait不依赖具体的 SSE 实现
/// - agent_runner 实现此 trait完成实际的消息推送
/// - 通过依赖注入的方式,在启动 prompt handler 时传入 notifier
#[async_trait]
pub trait SessionNotifier: Send + Sync + 'static {
/// 推送会话开始通知
async fn notify_prompt_start(
&self,
project_id: &str,
session_id: &str,
request_id: Option<String>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
/// 推送会话结束通知
async fn notify_prompt_end(
&self,
project_id: &str,
session_id: &str,
stop_reason: agent_client_protocol::schema::StopReason,
error_message: Option<String>,
request_id: Option<String>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
/// 推送会话错误通知
async fn notify_prompt_error(
&self,
project_id: &str,
session_id: &str,
error: agent_client_protocol::schema::Error,
request_id: Option<String>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
/// 推送 Agent 会话更新通知
async fn notify_session_update(
&self,
project_id: &str,
session_id: &str,
session_update: agent_client_protocol::schema::SessionUpdate,
request_id: Option<String>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
/// 推送通用会话通知
async fn notify(
&self,
project_id: &str,
session_id: &str,
notify: SessionNotify,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
}

View File

@@ -0,0 +1,216 @@
//! # Session Registry Trait
//!
//! 定义会话注册表的抽象接口,允许 `AcpSessionManager` 使用不同的存储实现。
//! 通过依赖注入避免 `agent_abstraction` 和 `agent_runner` 之间的循环依赖。
//!
//! ## 预期实现
//!
//! 此 trait 预期在 `agent_runner` 中实现:
//!
//! - **实现类**: `agent_runner::service::AgentSessionRegistry`
//! - **全局单例**: `agent_runner::service::AGENT_REGISTRY`
//!
//! ## 架构说明
//!
//! ```text
//! agent_abstraction agent_runner
//! ┌─────────────────┐ ┌─────────────────────┐
//! │ SessionRegistry │◄────────────────│ AgentSessionRegistry│
//! │ (trait) │ implements │ (struct) │
//! └────────┬────────┘ └──────────┬──────────┘
//! │ │
//! │ │
//! ┌────────▼────────┐ ┌──────────▼──────────┐
//! │AcpSessionManager│◄────────────────│ AGENT_REGISTRY │
//! │ registry: R │ injects │ (static LazyLock) │
//! └─────────────────┘ └─────────────────────┘
//! ```
//!
//! ## 与 AGENT_REGISTRY 的关系
//!
//! - **AGENT_REGISTRY**: `agent_runner` 中的全局单例,实现了此 trait
//! - **何时直接访问 AGENT_REGISTRY**: 在 `agent_runner` 内部进行状态查询、清理任务
//! - **何时通过 trait 访问**: 在 `agent_abstraction` 内部,通过泛型参数 `R: SessionRegistry`
//!
//! ## 使用示例
//!
//! ```ignore
//! // 在 agent_runner 中定义实现
//! pub struct AgentSessionRegistry {
//! agent_info_map: DashMap<String, ProjectAndAgentInfo>,
//! project_to_session: DashMap<String, String>,
//! session_to_project: DashMap<String, String>,
//! }
//!
//! impl SessionRegistry for AgentSessionRegistry {
//! type Entry = ProjectAndAgentInfo;
//! // ...
//! }
//!
//! // 创建全局单例
//! pub static AGENT_REGISTRY: LazyLock<Arc<AgentSessionRegistry>> = ...;
//!
//! // 注入到 AcpSessionManager
//! let session_manager = AcpSessionManager::new(notifier, AGENT_REGISTRY.clone());
//! ```
use std::sync::Arc;
use dashmap::mapref::entry::Entry;
use shared_types::SessionEntry;
/// 会话注册表 trait
///
/// 抽象 session 存储的 CRUD 操作,允许不同的实现(如 AGENT_REGISTRY
///
/// # 设计说明
/// - `SessionEntry` trait 定义在 `shared_types`,描述单个会话条目的数据访问
/// - `SessionRegistry` trait 定义在 `agent_abstraction`,描述会话存储的 CRUD 操作
/// - `agent_runner` 为 `AGENT_REGISTRY` 实现 `SessionRegistry`,并注入到 `AcpSessionManager`
///
/// # 使用示例
/// ```ignore
/// // 定义在 agent_runner
/// impl SessionRegistry for AgentSessionRegistry {
/// type Entry = ProjectAndAgentInfo;
/// // ...
/// }
///
/// // 注入到 AcpSessionManager
/// let session_manager = AcpSessionManager::new(notifier, Arc::new(registry));
/// ```
pub trait SessionRegistry: Send + Sync + 'static {
/// 会话条目类型
type Entry: SessionEntry;
/// 获取会话
///
/// # Arguments
/// * `project_id` - 项目 ID
///
/// # Returns
/// 如果存在则返回会话条目的克隆,否则返回 None
fn get(&self, project_id: &str) -> Option<Self::Entry>;
/// 插入或更新会话
///
/// # Arguments
/// * `project_id` - 项目 ID
/// * `session_id` - 会话 ID
/// * `entry` - 会话条目
fn insert(&self, project_id: &str, session_id: &str, entry: Self::Entry);
/// 移除会话
///
/// # Arguments
/// * `project_id` - 项目 ID
///
/// # Returns
/// 如果存在则返回被移除的会话条目,否则返回 None
fn remove(&self, project_id: &str) -> Option<Self::Entry>;
/// 检查会话是否存在
///
/// # Arguments
/// * `project_id` - 项目 ID
fn contains(&self, project_id: &str) -> bool;
/// 🆕 通过 session_id 获取 project_id反向查询
///
/// # Arguments
/// * `session_id` - 会话 ID
///
/// # Returns
/// 如果 session_id 存在,返回对应的 project_id否则返回 None
fn get_project_by_session(&self, session_id: &str) -> Option<String>;
/// 🆕 通过 session_id 直接获取会话条目(原子性操作)
///
/// # Arguments
/// * `session_id` - 会话 ID
///
/// # Returns
/// 如果 session_id 存在,返回对应的会话条目克隆;否则返回 None
///
/// # 优势
/// - 一次性查询,避免两次调用之间的竞态窗口
/// - 内部使用 DashMap 的原子性操作
fn get_entry_by_session(&self, session_id: &str) -> Option<Self::Entry>;
/// 获取所有项目 ID 列表
fn list_project_ids(&self) -> Vec<String>;
/// 获取会话数量
fn count(&self) -> usize;
/// 获取 DashMap entry用于原子性操作
///
/// # Arguments
/// * `project_id` - 项目 ID
///
/// # Returns
/// DashMap Entry支持原子性的 get/insert/update 操作
///
/// # 使用示例
/// ```ignore
/// use dashmap::mapref::entry::Entry;
///
/// match registry.entry(project_id.to_string()) {
/// Entry::Occupied(mut occupied) => {
/// // 已存在,可以检查和更新
/// let existing = occupied.get();
/// if needs_rebuild {
/// occupied.insert(new_value);
/// }
/// }
/// Entry::Vacant(vacant) => {
/// // 不存在,可以插入
/// vacant.insert(new_value);
/// }
/// }
/// ```
fn entry(&self, project_id: String) -> Entry<'_, String, Self::Entry>;
}
/// SessionRegistry 的 Arc 包装器实现
///
/// 允许 `Arc<R>` 作为 `SessionRegistry` 使用
impl<R: SessionRegistry> SessionRegistry for Arc<R> {
type Entry = R::Entry;
fn get(&self, project_id: &str) -> Option<Self::Entry> {
(**self).get(project_id)
}
fn insert(&self, project_id: &str, session_id: &str, entry: Self::Entry) {
(**self).insert(project_id, session_id, entry)
}
fn remove(&self, project_id: &str) -> Option<Self::Entry> {
(**self).remove(project_id)
}
fn contains(&self, project_id: &str) -> bool {
(**self).contains(project_id)
}
fn get_project_by_session(&self, session_id: &str) -> Option<String> {
(**self).get_project_by_session(session_id)
}
fn get_entry_by_session(&self, session_id: &str) -> Option<Self::Entry> {
(**self).get_entry_by_session(session_id)
}
fn list_project_ids(&self) -> Vec<String> {
(**self).list_project_ids()
}
fn count(&self) -> usize {
(**self).count()
}
fn entry(&self, project_id: String) -> Entry<'_, String, Self::Entry> {
(**self).entry(project_id)
}
}