Files
qiming/qiming-rcoder/specs/agent-resume-acp/agent-resume-acp-plan.md
2026-06-01 13:54:52 +08:00

24 KiB
Raw Blame History

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

当前代码(需简化):

// 检查是否因为 resume 导致的失败
if has_resume
    && (error_msg.contains("No conversation found")
        || error_msg.contains("session")
        || error_msg.contains("exited with code 1"))  // ← 删除这些判断
{
    // 降级...
}

改动后代码:

// 只要带 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 行

当前代码:

// 检查是否因为 resume 导致的失败
if has_resume
    && (error_msg.contains("No conversation found")
        || error_msg.contains("session")
        || error_msg.contains("exited with code 1"))
{

改动后代码:

// 只要带 resume 且启动失败,就降级重试(不判断具体错误原因)
if has_resume {

同时更新日志信息:

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 行

当前代码:

// 创建会话
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
    }
};

改动后代码:

// 创建会话(统一使用 new_sessionresume 通过 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;

同时需要移除的变量:

// 约 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 行(方法签名和文档)

当前代码:

/// 启动 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> {

改动后代码:

/// 启动 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 行(第一次调用)

当前代码:

let result = launcher
    .launch(
        project_id.clone(),
        project_path.clone(),
        session_id_hint.clone(),
        model_provider.clone(),
        start_config.clone(),
        client,
    )
    .await;

改动后代码:

let result = launcher
    .launch(
        project_id.clone(),
        project_path.clone(),
        model_provider.clone(),
        start_config.clone(),
        client,
    )
    .await;

改动位置 2: 约 247-254 行(降级重试调用)

当前代码:

launcher
    .launch(
        project_id.clone(),
        project_path,
        session_id_hint,
        model_provider.clone(),
        retry_config,
        C::default(),
    )
    .await?

改动后代码:

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 行)

当前代码:

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 行

当前代码:

use agent_client_protocol::{
    Agent, Client, ClientSideConnection, Implementation, InitializeRequest, LoadSessionRequest,
    McpServer, McpServerStdio, NewSessionRequest, PromptRequest, SessionId,
};

改动后代码:

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                         │
                    └─────────────────────────────────────────────────┘