597 lines
24 KiB
Markdown
597 lines
24 KiB
Markdown
# Agent Resume via ACP 实现计划
|
||
|
||
## 方案概述
|
||
|
||
采用 **方式一:通过 NewSessionRequest 的 meta 参数** 实现 resume 功能。
|
||
|
||
核心思路:统一使用 `NewSessionRequest` + `_meta.claudeCode.options.resume` 传递 session_id,移除冗余的 `load_session` 尝试逻辑。
|
||
|
||
**重要**:Resume 是"尝试性"的,如果 session 不存在导致 Agent 启动失败,会自动降级为不传 resume 参数,创建新会话。
|
||
|
||
---
|
||
|
||
## Resume 失败降级方案(核心机制)
|
||
|
||
### 问题场景
|
||
|
||
当用户传入的 `session_id` 对应的会话不存在时(已过期、被清理、或从未存在),Agent 会启动失败:
|
||
|
||
```
|
||
No conversation found for session id: abc-123
|
||
```
|
||
|
||
### 降级策略
|
||
|
||
**优先尝试 resume,失败则降级为新会话**:
|
||
|
||
```
|
||
第一次尝试:带 resume 参数
|
||
↓
|
||
├─ 成功 → 恢复上下文,继续对话
|
||
│
|
||
└─ 失败(任何原因)
|
||
↓
|
||
第二次尝试:不带 resume 参数
|
||
↓
|
||
└─ 成功 → 创建新会话
|
||
```
|
||
|
||
**关键点**:不需要判断具体错误原因,只要 `has_resume && 启动失败`,就直接降级重试。
|
||
|
||
### 当前实现位置(需改动)
|
||
|
||
**文件**: `session_manager.rs:217-261`
|
||
|
||
**当前代码**(需简化):
|
||
```rust
|
||
// 检查是否因为 resume 导致的失败
|
||
if has_resume
|
||
&& (error_msg.contains("No conversation found")
|
||
|| error_msg.contains("session")
|
||
|| error_msg.contains("exited with code 1")) // ← 删除这些判断
|
||
{
|
||
// 降级...
|
||
}
|
||
```
|
||
|
||
**改动后代码**:
|
||
```rust
|
||
// 只要带 resume 且启动失败,就降级重试
|
||
if has_resume {
|
||
tracing::warn!(
|
||
"⚠️ Agent 启动失败(带 resume),降级为不使用 resume 重试: error={}",
|
||
error_msg
|
||
);
|
||
|
||
// 创建新的 config,不包含 resume_session_id
|
||
let retry_config = AgentStartConfig {
|
||
system_prompt: start_config.system_prompt,
|
||
mcp_servers: start_config.mcp_servers,
|
||
extra_meta: start_config.extra_meta,
|
||
service_type: start_config.service_type,
|
||
resume_session_id: None, // ← 关键:去掉 resume
|
||
};
|
||
|
||
tracing::info!("🔄 重试启动 Agent(不使用 resume)");
|
||
|
||
// 第二次尝试:不带 resume
|
||
launcher.launch(..., retry_config, ...).await?
|
||
} else {
|
||
// 不带 resume 的失败,直接返回错误
|
||
return Err(e);
|
||
}
|
||
```
|
||
|
||
### 降级流程图
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ ChatRequest { session_id: "abc-123" } │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ acp_worker.rs │
|
||
│ └─ AgentStartConfig { resume_session_id: Some("abc-123") } │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ session_manager.rs │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||
│ │ 第一次尝试 │ │
|
||
│ │ launcher.launch(start_config) │ │
|
||
│ │ → meta: { claudeCode.options.resume: "abc-123" } │ │
|
||
│ └─────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ┌───────────────┴───────────────┐ │
|
||
│ ▼ ▼ │
|
||
│ ┌────────────────┐ ┌────────────────────┐ │
|
||
│ │ ✅ 成功 │ │ ❌ 失败 │ │
|
||
│ │ session 存在 │ │ "No conversation │ │
|
||
│ │ 恢复上下文 │ │ found" │ │
|
||
│ └────────────────┘ └────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||
│ │ 第二次尝试(降级) │ │
|
||
│ │ retry_config.resume_session_id = None │ │
|
||
│ │ launcher.launch(retry_config) │ │
|
||
│ │ → meta: { } ← 不包含 resume │ │
|
||
│ └─────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌────────────────┐ │
|
||
│ │ ✅ 成功 │ │
|
||
│ │ 创建新会话 │ │
|
||
│ │ 返回新 session │ │
|
||
│ └────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 本次改动对降级机制的影响
|
||
|
||
**需要简化**。移除错误关键字判断,改为:只要 `has_resume && 启动失败` 就降级。
|
||
|
||
| 层级 | 改动 | 降级机制 |
|
||
|------|------|----------|
|
||
| session_manager.rs | 简化降级判断逻辑 | **需改动** |
|
||
| claude_code_launcher.rs | 简化会话创建逻辑 | 不涉及 |
|
||
|
||
---
|
||
|
||
## 当前代码状态分析
|
||
|
||
### ✅ 已正确实现的部分
|
||
|
||
| 文件 | 功能 | 状态 |
|
||
|------|------|------|
|
||
| `agent.rs:95-137` | `AgentStartConfig.build_meta()` 构建 `claudeCode.options.resume` | ✅ 完成 |
|
||
| `acp_worker.rs:158-180` | 判断是否需要 resume 并设置 `resume_session_id` | ✅ 完成 |
|
||
| `session_manager.rs:217-261` | Resume 失败降级重试机制(框架已完成,判断逻辑需简化) | ⚠️ 需简化 |
|
||
|
||
### ❌ 需要改动的部分
|
||
|
||
| 文件 | 问题 | 改动 |
|
||
|------|------|------|
|
||
| `claude_code_launcher.rs:438-480` | 冗余的 `load_session` 尝试逻辑 | 简化为统一 `new_session` |
|
||
| `claude_code_launcher.rs:291-299` | `session_id` 参数冗余 | 移除此参数 |
|
||
| `session_manager.rs:207-214, 247-254` | 调用 launch 时传递 `session_id_hint` | 移除此参数 |
|
||
| `session_manager.rs:224-227` | 降级判断过于复杂(检测错误关键字) | 简化为 `if has_resume` |
|
||
|
||
---
|
||
|
||
## 改动任务清单
|
||
|
||
### Task 0: 简化 session_manager.rs 降级判断逻辑(新增)
|
||
|
||
**文件**: `crates/agent_abstraction/src/session/session_manager.rs`
|
||
|
||
**改动位置**: 约 223-228 行
|
||
|
||
**当前代码**:
|
||
```rust
|
||
// 检查是否因为 resume 导致的失败
|
||
if has_resume
|
||
&& (error_msg.contains("No conversation found")
|
||
|| error_msg.contains("session")
|
||
|| error_msg.contains("exited with code 1"))
|
||
{
|
||
```
|
||
|
||
**改动后代码**:
|
||
```rust
|
||
// 只要带 resume 且启动失败,就降级重试(不判断具体错误原因)
|
||
if has_resume {
|
||
```
|
||
|
||
**同时更新日志信息**:
|
||
```rust
|
||
tracing::warn!(
|
||
"⚠️ Agent 启动失败(带 resume),降级为不使用 resume 重试: error={}",
|
||
error_msg
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### Task 1: 简化 claude_code_launcher.rs 会话创建逻辑
|
||
|
||
**文件**: `crates/agent_abstraction/src/compat/claude_code_launcher.rs`
|
||
|
||
**改动位置**: 约 438-480 行
|
||
|
||
**当前代码**:
|
||
```rust
|
||
// 创建会话
|
||
let session_id = match session_id_for_closure {
|
||
Some(sid) => {
|
||
debug!("尝试加载 ACP 会话[load_session]");
|
||
let given_session_id = SessionId::new(sid);
|
||
match client_conn
|
||
.load_session(LoadSessionRequest::new(
|
||
given_session_id.clone(),
|
||
project_path_for_closure.clone(),
|
||
))
|
||
.await
|
||
{
|
||
Ok(resp) => {
|
||
debug!("ACP 会话加载成功[load_session],{:?}", resp);
|
||
given_session_id
|
||
}
|
||
Err(e) => {
|
||
warn!(
|
||
"load_session 失败或未实现,回退创建新会话[new_session]: {:?}",
|
||
e
|
||
);
|
||
// 注意:即使 load_session 失败,仍然会创建 new_session
|
||
// resume_session_id 会通过 meta.claudeCode.options.resume 传递
|
||
let new_session_request =
|
||
NewSessionRequest::new(project_path_for_closure.clone())
|
||
.mcp_servers(mcp_servers.clone())
|
||
.meta(system_prompt_meta.clone());
|
||
let resp = client_conn.new_session(new_session_request).await?;
|
||
debug!("ACP 会话创建成功[new_session],{:?}", resp);
|
||
resp.session_id
|
||
}
|
||
}
|
||
}
|
||
None => {
|
||
debug!("创建 ACP 会话[new_session]");
|
||
let new_session_request =
|
||
NewSessionRequest::new(project_path_for_closure.clone())
|
||
.mcp_servers(mcp_servers)
|
||
.meta(system_prompt_meta);
|
||
let resp = client_conn.new_session(new_session_request).await?;
|
||
debug!("ACP 会话创建成功[new_session],{:?}", resp);
|
||
resp.session_id
|
||
}
|
||
};
|
||
```
|
||
|
||
**改动后代码**:
|
||
```rust
|
||
// 创建会话(统一使用 new_session,resume 通过 meta 传递)
|
||
// 如果 start_config.resume_session_id 有值,build_meta() 会自动构建
|
||
// _meta.claudeCode.options.resume 结构
|
||
debug!("创建 ACP 会话[new_session]");
|
||
let new_session_request = NewSessionRequest::new(project_path_for_closure.clone())
|
||
.mcp_servers(mcp_servers)
|
||
.meta(system_prompt_meta);
|
||
|
||
let resp = client_conn
|
||
.new_session(new_session_request)
|
||
.await
|
||
.context("ACP 会话创建失败")?;
|
||
|
||
debug!(
|
||
"ACP 会话创建成功[new_session], session_id={}",
|
||
resp.session_id.0
|
||
);
|
||
let session_id = resp.session_id;
|
||
```
|
||
|
||
**同时需要移除的变量**:
|
||
```rust
|
||
// 约 317 行,移除这行
|
||
let session_id_for_closure = session_id.clone();
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: 移除 launch 方法的 session_id 参数
|
||
|
||
**文件**: `crates/agent_abstraction/src/compat/claude_code_launcher.rs`
|
||
|
||
**改动位置**: 约 279-299 行(方法签名和文档)
|
||
|
||
**当前代码**:
|
||
```rust
|
||
/// 启动 Claude Code ACP Agent 服务
|
||
///
|
||
/// # 参数
|
||
/// - `project_id`: 项目 ID
|
||
/// - `project_path`: 项目工作目录
|
||
/// - `session_id`: 可选的会话 ID(用于恢复会话)
|
||
/// - `model_provider`: 模型提供商配置
|
||
/// - `start_config`: Agent 启动配置(包含系统提示词等)
|
||
/// - `client`: ACP 客户端实现
|
||
///
|
||
/// # 返回值
|
||
/// 返回 LauncherConnectionInfoComplete,包含会话信息和生命周期守卫
|
||
pub async fn launch(
|
||
&self,
|
||
project_id: String,
|
||
project_path: PathBuf,
|
||
session_id: Option<String>,
|
||
model_provider: Option<ModelProviderConfig>,
|
||
start_config: AgentStartConfig,
|
||
client: C,
|
||
) -> Result<LauncherConnectionInfoComplete> {
|
||
```
|
||
|
||
**改动后代码**:
|
||
```rust
|
||
/// 启动 Claude Code ACP Agent 服务
|
||
///
|
||
/// # 参数
|
||
/// - `project_id`: 项目 ID
|
||
/// - `project_path`: 项目工作目录
|
||
/// - `model_provider`: 模型提供商配置
|
||
/// - `start_config`: Agent 启动配置(包含系统提示词、resume_session_id 等)
|
||
/// - `client`: ACP 客户端实现
|
||
///
|
||
/// # Resume 机制
|
||
/// 如果需要恢复会话,通过 `start_config.resume_session_id` 传递 session_id,
|
||
/// 会自动构建 `_meta.claudeCode.options.resume` 结构传递给 Agent。
|
||
///
|
||
/// # 返回值
|
||
/// 返回 LauncherConnectionInfoComplete,包含会话信息和生命周期守卫
|
||
pub async fn launch(
|
||
&self,
|
||
project_id: String,
|
||
project_path: PathBuf,
|
||
model_provider: Option<ModelProviderConfig>,
|
||
start_config: AgentStartConfig,
|
||
client: C,
|
||
) -> Result<LauncherConnectionInfoComplete> {
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: 更新 session_manager.rs 的 launch 调用
|
||
|
||
**文件**: `crates/agent_abstraction/src/session/session_manager.rs`
|
||
|
||
**改动位置 1**: 约 207-214 行(第一次调用)
|
||
|
||
**当前代码**:
|
||
```rust
|
||
let result = launcher
|
||
.launch(
|
||
project_id.clone(),
|
||
project_path.clone(),
|
||
session_id_hint.clone(),
|
||
model_provider.clone(),
|
||
start_config.clone(),
|
||
client,
|
||
)
|
||
.await;
|
||
```
|
||
|
||
**改动后代码**:
|
||
```rust
|
||
let result = launcher
|
||
.launch(
|
||
project_id.clone(),
|
||
project_path.clone(),
|
||
model_provider.clone(),
|
||
start_config.clone(),
|
||
client,
|
||
)
|
||
.await;
|
||
```
|
||
|
||
**改动位置 2**: 约 247-254 行(降级重试调用)
|
||
|
||
**当前代码**:
|
||
```rust
|
||
launcher
|
||
.launch(
|
||
project_id.clone(),
|
||
project_path,
|
||
session_id_hint,
|
||
model_provider.clone(),
|
||
retry_config,
|
||
C::default(),
|
||
)
|
||
.await?
|
||
```
|
||
|
||
**改动后代码**:
|
||
```rust
|
||
launcher
|
||
.launch(
|
||
project_id.clone(),
|
||
project_path,
|
||
model_provider.clone(),
|
||
retry_config,
|
||
C::default(),
|
||
)
|
||
.await?
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: 清理 session_manager.rs 的方法签名(可选)
|
||
|
||
**文件**: `crates/agent_abstraction/src/session/session_manager.rs`
|
||
|
||
**改动位置**: `create_new_session` 方法签名(约 185-196 行)
|
||
|
||
**当前代码**:
|
||
```rust
|
||
pub async fn create_new_session(
|
||
&self,
|
||
project_id: String,
|
||
project_path: PathBuf,
|
||
session_id_hint: Option<String>,
|
||
model_provider: Option<ModelProviderConfig>,
|
||
start_config: AgentStartConfig,
|
||
client: C,
|
||
) -> Result<Arc<SessionInfo>> {
|
||
```
|
||
|
||
**评估**:
|
||
- `session_id_hint` 参数当前未被使用(只是传递给 launcher)
|
||
- 可以移除,但需要检查 `get_or_create_session` 等上游调用方
|
||
- **建议**: 暂时保留此参数,避免过大改动范围
|
||
|
||
---
|
||
|
||
### Task 5: 移除未使用的 import
|
||
|
||
**文件**: `crates/agent_abstraction/src/compat/claude_code_launcher.rs`
|
||
|
||
**改动位置**: 约 11-14 行
|
||
|
||
**当前代码**:
|
||
```rust
|
||
use agent_client_protocol::{
|
||
Agent, Client, ClientSideConnection, Implementation, InitializeRequest, LoadSessionRequest,
|
||
McpServer, McpServerStdio, NewSessionRequest, PromptRequest, SessionId,
|
||
};
|
||
```
|
||
|
||
**改动后代码**:
|
||
```rust
|
||
use agent_client_protocol::{
|
||
Agent, Client, ClientSideConnection, Implementation, InitializeRequest,
|
||
McpServer, McpServerStdio, NewSessionRequest, PromptRequest, SessionId,
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 改动验证清单
|
||
|
||
### 编译验证
|
||
- [ ] `cargo build -p agent_abstraction` 编译通过
|
||
- [ ] `cargo build --workspace` 全项目编译通过
|
||
- [ ] `cargo clippy` 无新增警告
|
||
|
||
### 单元测试
|
||
- [ ] `AgentStartConfig.build_meta()` 测试用例通过
|
||
- [ ] 现有测试用例通过
|
||
|
||
### 集成测试
|
||
- [ ] 新建会话:正常创建
|
||
- [ ] Resume 会话:传入有效 session_id,恢复上下文
|
||
- [ ] Resume 失败降级:传入无效 session_id,自动降级为新会话
|
||
|
||
---
|
||
|
||
## 实现顺序
|
||
|
||
```
|
||
Step 1: Task 0 - 简化 session_manager.rs 降级判断逻辑
|
||
│
|
||
└─ 移除错误关键字判断,改为 if has_resume
|
||
|
||
Step 2: Task 1 - 简化 claude_code_launcher.rs 会话创建逻辑
|
||
│
|
||
├─ 移除 load_session 尝试
|
||
├─ 统一使用 new_session
|
||
└─ 移除 session_id_for_closure 变量
|
||
|
||
Step 3: Task 2 - 移除 launch 方法的 session_id 参数
|
||
│
|
||
└─ 更新方法签名和文档
|
||
|
||
Step 4: Task 3 - 更新 session_manager.rs 的 launch 调用
|
||
│
|
||
├─ 更新第一次 launch 调用
|
||
└─ 更新降级重试调用
|
||
|
||
Step 5: Task 5 - 清理未使用的 import
|
||
│
|
||
└─ 移除 LoadSessionRequest
|
||
|
||
Step 6: 验证
|
||
│
|
||
├─ 编译验证
|
||
├─ 运行测试
|
||
└─ 手动测试 resume 流程
|
||
```
|
||
|
||
---
|
||
|
||
## 风险评估
|
||
|
||
| 风险 | 概率 | 影响 | 缓解措施 |
|
||
|------|------|------|----------|
|
||
| 移除 session_id 参数导致编译错误 | 低 | 低 | 搜索所有调用方并更新 |
|
||
| Resume 功能回归 | 低 | 中 | 简化降级机制更健壮 |
|
||
| 逻辑改动影响其他功能 | 低 | 中 | 充分测试 |
|
||
|
||
---
|
||
|
||
## 预估工作量
|
||
|
||
| Task | 预估代码行数 | 复杂度 |
|
||
|------|-------------|--------|
|
||
| Task 0 | -4, +2 | 低 |
|
||
| Task 1 | -30, +15 | 低 |
|
||
| Task 2 | -2, +5 | 低 |
|
||
| Task 3 | -2, +0 | 低 |
|
||
| Task 5 | -1, +0 | 低 |
|
||
| **总计** | **约 -39 行,+22 行** | **低** |
|
||
|
||
---
|
||
|
||
## 附录:完整数据流(改动后)
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ HTTP/gRPC Request │
|
||
│ ChatRequest { session_id: "abc-123", prompt: "..." } │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ acp_worker.rs │
|
||
│ ├─ 检查内存中是否存在会话 │
|
||
│ │ └─ 存在且 session_id 匹配 → with_resume_session_id() │
|
||
│ └─ 构建 AgentStartConfig { resume_session_id: Some("abc-123") }│
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ session_manager.rs │
|
||
│ └─ launcher.launch(project_id, project_path, │
|
||
│ model_provider, start_config, client) │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ claude_code_launcher.rs │
|
||
│ ├─ start_config.build_meta() 构建: │
|
||
│ │ { │
|
||
│ │ "claudeCode": { │
|
||
│ │ "options": { "resume": "abc-123" } │
|
||
│ │ } │
|
||
│ │ } │
|
||
│ └─ NewSessionRequest::new(cwd).meta(meta) ← 统一入口 │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼ ACP session/new
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ claude-code-acp │
|
||
│ ├─ newSession() 解析 _meta.claudeCode.options.resume │
|
||
│ └─ query({ options: { resume: "abc-123" } }) │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
┌───────────────┴───────────────┐
|
||
▼ ▼
|
||
┌────────────────┐ ┌────────────────────────┐
|
||
│ ✅ 会话存在 │ │ ❌ 会话不存在 │
|
||
│ → 恢复上下文 │ │ → 抛出错误 │
|
||
│ → 返回成功 │ │ "No conversation found"│
|
||
└────────────────┘ └────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────┐
|
||
│ session_manager.rs 降级处理 │
|
||
│ ├─ 检测错误关键字 │
|
||
│ ├─ 创建 retry_config { resume_session_id: None }│
|
||
│ └─ 第二次 launcher.launch(retry_config) │
|
||
└─────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────┐
|
||
│ ✅ 成功创建新会话 │
|
||
│ └─ 返回新的 session_id │
|
||
└─────────────────────────────────────────────────┘
|
||
```
|