Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

acproxy

acproxy 是一个使用官方 Rust ACP SDK 构建的 ACP Agent CLI demo。它通过 stdin/stdout 与 Zed 等 ACP Client 通信,目前不访问 HTTP 服务,而是直接调用本地 DirectLogic 生成回复。

  • Rust:1.88 或更高版本
  • Rust edition:2024
  • ACP SDK:agent-client-protocol = "2.0.0"
  • ACP 协议:稳定版 v1
  • Transport:stdio JSON-RPC

项目架构

flowchart LR
    Client["ACP Client<br/>Zed / smoke client"]
    Transport["官方 Rust ACP SDK<br/>stdio JSON-RPC"]
    Adapter["AcpAdapter<br/>协议转换与生命周期"]
    State["AgentState<br/>内存 Session Store"]
    Logger["DebugLogger<br/>追加写入日志文件"]
    Service["ConversationService<br/>业务接口"]
    Logic["DirectLogic<br/>当前演示实现"]

    Client <-->|"ACP request / response / notification"| Transport
    Transport <--> Adapter
    Adapter <--> State
    Adapter --> Logger
    Adapter --> Service
    Service --> Logic
    Adapter -->|"session/update"| Transport
Loading

各层职责:

  • main.rs:解析 servesmoke 子命令并启动程序。
  • agent.rs:注册 ACP handler、维护 Session 状态、校验请求、处理取消,并把业务回复转换为 session/update
  • debug_log.rs:线程安全地记录 ACP 请求、响应和通知,不占用协议 stdout。
  • logic.rs:定义与 ACP SDK 解耦的 ConversationService、请求/响应模型,以及当前 DirectLogic
  • smoke.rs:启动一个真实子进程,通过 stdio 执行端到端 ACP 验证。
src/
├── main.rs
├── agent.rs
├── debug_log.rs
├── logic.rs
└── smoke.rs

当前支持的 ACP 能力

ACP 方法或能力 状态 当前行为
initialize 支持 协商 ACP v1,并返回准确的 Agent capabilities
session/new 支持 创建内存 Session,生成 UUID,保存工作目录和附加目录
session/prompt 支持 接受文本和 Resource Link,调用 ConversationService
session/update 支持 使用 AgentMessageChunk 分段发送文本
session/cancel 支持 取消当前 Prompt、停止业务 future 和分段等待
session/list 支持 支持 cwd 过滤、每页 20 条和稳定的 keyset cursor
session/resume 支持 恢复本进程内已存在的 Session,不回放历史消息
session/close 支持 取消进行中的工作并关闭 Session,但仍保留在 Session List
session/delete 支持 删除 Session;若正在执行 Prompt,也会先取消
additionalDirectories 支持 校验、保存、列出,并传递给业务逻辑
Text Prompt 支持 多个 Text Block 使用换行拼接
Resource Link Prompt 支持 向业务层传递 nameurimimeType

同一个 Session 同一时间只允许一个活动 Prompt。Session 关闭后不能继续 Prompt,必须先调用 session/resume。只有 session/delete 会将它从 session/list 中移除。

当前未支持

  • session/load:它要求恢复并通过通知回放完整历史;当前没有持久化历史,因此只声明 session/resume
  • MCP Server:mcpServers 非空时返回明确的参数错误,不会静默忽略。
  • 图片、音频和 Embedded Resource Prompt。
  • Tool Call、终端命令执行和人工权限确认。
  • Session Mode、模型切换和配置选项。
  • 认证能力。
  • 跨进程 Session 恢复。

Session 状态

每个 Session 当前保存:

  • ACP Session ID
  • 工作目录 cwd
  • additionalDirectories
  • 业务侧 conversation_id
  • 根据第一次 Prompt 生成的标题
  • 最近更新时间
  • 当前 Prompt 的取消令牌
  • 打开或关闭状态

状态存放在进程内的 HashMap 中,程序退出后全部清空。conversation_id 用于以后映射真实 对话接口中的 Conversation ID。

输入校验规则:

  • cwd 必须是绝对路径。
  • additionalDirectories 中的每个路径必须是绝对路径。
  • 当前不支持非空 mcpServers
  • promptresumeclose 遇到未知 Session 时返回 resource_not_founddelete 采用幂等语义。
  • 已关闭 Session 或同 Session 中重复提交 Prompt 返回协议错误。

Prompt 与流式输出

当前 ConversationService 先生成完整回复,ACP adapter 再模拟流式输出:

  1. 将回复按 6 个 Unicode 字符拆分。
  2. 每段发送一条 session/update
  3. 相邻分段等待 120ms。
  4. 最后用 session/prompt response 返回 end_turn

示意:

session/prompt
    │
    ├── session/update: "Direct"
    ├── 等待 120ms
    ├── session/update: " respo"
    ├── 等待 120ms
    ├── session/update: "nse:你好"
    └── PromptResponse: stopReason = "end_turn"

业务调用和分段等待都同时监听 CancellationToken。收到 session/cancelsession/closesession/delete 后,会丢弃正在等待的业务 future,并返回 stopReason = "cancelled"。Prompt 状态使用对应的 token 做身份校验,旧 Prompt 的异步收尾 不会清除新 Prompt 的状态。

构建与运行

cd /Users/simon/dev/acproxy

cargo build
cargo test
cargo clippy --all-targets -- -D warnings

启动 ACP Server:

cargo run -- serve

默认将 Debug 日志追加写入启动工作目录下的 acproxy-debug.log。也可以指定路径:

cargo run -- serve --log-file /tmp/acproxy-debug.log

serve 是默认子命令,下面的命令效果相同:

cargo run

Release 构建:

cargo build --release
/Users/simon/dev/acproxy/target/release/acproxy serve \
  --log-file /Users/simon/dev/acproxy/acproxy-debug.log

ACP 协议独占 stdout。所有日志必须写入 stderr,不能使用 stdout 打印普通日志,否则会破坏 JSON-RPC 数据流。

Debug 日志

Server 会记录:

  • 所有进入 typed handler 的 ACP request。
  • 成功 response 和 error response。
  • 入站 session/cancel notification。
  • 出站 session/update notification,包括每个文本分段。

日志为单行追加格式并在每条记录后立即 flush:

2026-07-25T10:30:00.123Z DEBUG REQUEST method=session/prompt id=Number(3) payload=...
2026-07-25T10:30:00.124Z DEBUG NOTIFICATION_OUT method=session/update id=- payload=...
2026-07-25T10:30:00.488Z DEBUG RESPONSE method=session/prompt id=Number(3) payload=...

默认路径:

./acproxy-debug.log

路径相对于 Server 的启动工作目录。父目录不存在时会自动创建,已有日志文件不会覆盖,而是继续 追加。项目的 .gitignore 已忽略默认日志文件。

Prompt 内容、文件 URI 和工作目录可能出现在 Debug 日志中,因此生产环境应:

  • 将日志写入访问受控的目录。
  • 不在日志中记录 Token 等认证信息。
  • 配置外部日志轮转和保留周期。
  • 不需要调试时关闭或降低详细程度;当前 demo 暂未提供日志级别开关。

日志记录位于 typed ACP handler 边界。无法解析的 JSON 或没有注册 handler 的未知方法不会进入 当前 Debug 日志。

使用 smoke client 提问

所有普通问题都会触发分段输出:

cargo run -- smoke "请介绍一下 ACP 协议"

当前 DirectLogic 还提供三个演示命令:

cargo run -- smoke /help
cargo run -- smoke /about
cargo run -- smoke /error
  • /help:显示演示命令。
  • /about:显示 Session、Conversation、工作目录和资源信息。
  • /error:模拟业务错误,用于验证 ACP error response。

smoke client 实际执行以下生命周期:

initialize
  → session/new
  → session/list
  → session/prompt + session/update
  → session/close
  → session/list
  → session/resume
  → session/delete

Zed 配置

先构建 Release 版本:

cargo build --release

在 Zed 中配置自定义 Agent:

{
  "agent_servers": {
    "acproxy": {
      "type": "custom",
      "command": "/Users/simon/dev/acproxy/target/release/acproxy",
      "args": [
        "serve",
        "--log-file",
        "/Users/simon/dev/acproxy/acproxy-debug.log"
      ],
      "env": {
        "RUST_BACKTRACE": "1"
      }
    }
  }
}

serve 用于启动 ACP Server,--log-file 指定 Debug 日志路径。对应命令为:

/Users/simon/dev/acproxy/target/release/acproxy serve \
  --log-file /Users/simon/dev/acproxy/acproxy-debug.log

字段说明:

  • "type": "custom":注册自定义 External Agent。
  • "command"acproxy Release 可执行文件的绝对路径。
  • "args":依次传入 serve 和日志文件参数。
  • "env":传递给 Agent 进程的环境变量;RUST_BACKTRACE=1 用于在 panic 时输出堆栈, 不需要时可以删除。

如果 settings.json 已经包含主题、快捷键或其他 Agent 配置,只合并 agent_servers.acproxy 这一项,不要覆盖整个文件。

配置完成后:

  1. 重启 Zed 或重新加载配置。
  2. 打开 Agent Panel。
  3. 创建新会话。
  4. 从 External Agents 中选择 acproxy
  5. 输入任意问题,客户端将接收分段 session/update

实时查看 acproxy 自身的请求、响应和通知日志:

tail -f /Users/simon/dev/acproxy/acproxy-debug.log

如需查看 Zed 侧的完整 ACP 通信日志,在 Command Palette 中运行:

dev: open acp logs

如何实现真实业务逻辑

业务扩展点是 logic.rs 中的 ConversationService

use std::future::Future;

pub(crate) trait ConversationService: Clone + Send + Sync + 'static {
    fn chat(
        &self,
        request: ChatRequest,
        cancel: &CancellationToken,
    ) -> impl Future<Output = ConversationOutcome> + Send;
}

ChatRequest 提供:

pub(crate) struct ChatRequest {
    pub acp_session_id: String,
    pub conversation_id: Option<String>,
    pub message: String,
    pub cwd: PathBuf,
    pub additional_directories: Vec<PathBuf>,
    pub resources: Vec<ChatResource>,
}

业务实现返回以下结果之一:

  • ConversationOutcome::Completed(ChatCompletion):成功完成。
  • ConversationOutcome::Cancelled:主动取消。
  • ConversationOutcome::Failed(String):业务失败;详细错误写 stderr,客户端收到通用错误。

1. 新增业务实现

可以在 logic.rs 中新增实现:

#[derive(Clone)]
pub(crate) struct MyConversationService {
    // 后续可放 HTTP client、配置或认证信息
}

impl ConversationService for MyConversationService {
    async fn chat(
        &self,
        request: ChatRequest,
        cancel: &CancellationToken,
    ) -> ConversationOutcome {
        if cancel.is_cancelled() {
            return ConversationOutcome::Cancelled;
        }

        // 在这里调用真实对话逻辑。
        let content = format!("业务回复:{}", request.message);

        ConversationOutcome::Completed(ChatCompletion {
            conversation_id: request
                .conversation_id
                .or_else(|| Some(format!("remote-{}", request.acp_session_id))),
            content,
        })
    }
}

trait 使用 Rust 原生的 return-position impl Future 明确保证 future 为 Send,实现端仍可直接 使用 async fn,不需要引入 async-trait

2. 注入业务实现

agent.rs 提供泛型启动入口:

use std::path::Path;

agent::serve_with(
    MyConversationService { /* config */ },
    Path::new("/path/to/acproxy-debug.log"),
).await

例如在 main.rsServe 分支中替换:

Command::Serve { log_file } => {
    let service = logic::MyConversationService { /* config */ };
    agent::serve_with(service, &log_file).await.map_err(Into::into)
}

ACP handler、Session 生命周期、取消和 session/update 转换都不需要修改。

3. 后续接入 HTTP 对话接口

HTTP 实现通常需要完成以下映射:

ChatRequest.acp_session_id       → 本地链路追踪或请求 ID
ChatRequest.conversation_id      → HTTP 接口的 Conversation ID
ChatRequest.message              → 用户消息
ChatRequest.cwd                  → 当前工作目录
ChatRequest.additional_directories → 其他工作区根目录
ChatRequest.resources            → Prompt 中引用的资源
HTTP response.conversation_id    → ChatCompletion.conversation_id
HTTP response.content            → ChatCompletion.content

建议把 HTTP client 保存在 Service struct 中并复用连接,不要每次 Prompt 都创建新 client。认证 Token 应从环境变量或安全存储读取,不要放在命令行参数中。

ACP adapter 使用 tokio::select! 监听取消;取消时会 drop 正在等待的 chat future。业务实现如果 还启动了后台任务,也应监听 CancellationToken 并主动停止这些任务。

4. 接入真正的上游流式响应

当前 ConversationService 返回完整 content,再由 ACP 层模拟分段。如果真实接口是 SSE、 NDJSON 或其他流式协议,建议下一步把业务接口扩展为事件流或 channel:

HTTP/SSE chunk
    → ConversationService event
    → SessionUpdate::AgentMessageChunk
    → ACP Client

届时应直接转发上游 chunk,并移除 demo 中的 6 字符拆分和 120ms sleep,避免二次缓冲。

测试

当前测试覆盖:

  • ACP v1 版本协商
  • Text 与 Resource Link 解析
  • Unicode 分段
  • 非法 cursor
  • 路径和 MCP 参数校验
  • Prompt 取消
  • 旧 Prompt/新 Prompt 并发状态隔离
  • 业务成功、取消和失败
  • 完整 stdio ACP 生命周期 smoke

运行:

cargo test
cargo clippy --all-targets -- -D warnings
cargo run -- smoke "端到端验证"

主要依赖

  • agent-client-protocol:官方 ACP Rust SDK。
  • tokio:异步运行时、stdio、子进程和定时器。
  • tokio-utilCancellationToken
  • clap:CLI 参数和子命令。
  • chrono:Session RFC 3339 时间戳。
  • uuid:ACP Session ID。
  • serde_json:smoke client JSON-RPC 和 Session cursor。
  • anyhow:CLI 与 smoke client 错误处理。

About

acproxy is a Rust-based ACP Agent CLI demo that implements the official Agent Client Protocol SDK. It communicates with ACP clients (like Zed) via stdio/JSON-RPC and provides extensible business logic through a pluggable ConversationService trait for building real agent integrations.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages