ACP 集成设计文档¶
概述¶
ACP(Agent Communication Protocol)是 Ethan 委托复杂编码任务给专业 Coding Agent 的机制。
设计原则:Ethan 是通用个人 AI Agent,不专注于代码。遇到复杂编码任务时,自动检测并委托给本地 Coding Agent(Claude Code / OpenCode / Codex),把结果整合回对话,并支持多轮续接与过程可视化。
架构¶

复杂度判断(is_complex_coding_task)¶
启发式判断,两步过滤:
- 简单问题优先排除:
-
"什么是"、"解释"、"how does" → 直接回答,不委托
-
复杂度信号 + 代码关键词:
- implement / refactor / create / 实现 / 重构 + code / python / api 等 → 委托
| 输入 | 判断 |
|---|---|
| "什么是 asyncio" | 简单 → 自己回答 |
| "implement REST API with JWT" | 复杂 → 委托 |
| "帮我重构这个文件里的代码" | 复杂 → 委托 |
| "explain how decorators work" | 简单 → 自己回答 |
支持的 Coding Agent¶
| Agent | 命令 | 多轮 | 子步骤解析 | 安装 |
|---|---|---|---|---|
| Claude Code | claude |
✅ --resume <session_id> |
✅ stream-json | https://claude.ai/code |
| OpenCode | opencode |
✅ -s <session_id> |
✅ --format json |
https://opencode.ai |
| Codex | codex |
✅ exec resume <thread_id> |
✅ --json |
— |
优先级:Claude Code > OpenCode > Codex(按系统 PATH 检测)。可通过 prefer 参数指定(claude / opencode / codex / auto)。
三套现在都走 JSON 事件流:首轮从事件里取出会话 id(claude 的 session_id / opencode 的 sessionID / codex 的 thread_id),续轮带上该 id 续接。
Codex provider 注入¶
Codex 默认读 ~/.codex/config.toml 的 provider,但该 provider 可能失效(ChatGPT 登录态过期、内网代理不通等)。_codex_provider_overrides() 会复用 Ethan 自己的 cliproxy provider(OpenAI 兼容,wire_api=responses),通过 -c model_provider=... 覆盖注入,模型默认 gpt-5.5(可用环境变量 ETHAN_CODEX_MODEL 覆盖)。未配置 cliproxy 时退回 codex 自身 config,不破坏既有行为。
多轮会话¶
三套 Coding Agent 都支持续接。Ethan 按「Coding Agent × 工作目录」持久化会话 id:
- 持久化文件:
~/.ethan/<user>/acp_sessions.json,key 为"{agent}::{工作目录绝对路径}" - 按 agent 隔离:codex/claude/opencode 的 id 格式不同,分键存储避免在同一目录互相覆盖导致 resume 失败
- 连续的
delegate_coding调用若指向同一working_dir,自动续接同一会话 reset_session=true可清除该 (agent, 目录) 的会话记忆,从新会话开始
# 第一轮
delegate_coding(task="实现 JWT 认证", working_dir="/proj")
# → 创建 session A,持久化为 "claude::/proj"
# 第二轮(同目录,自动续接 session A)
delegate_coding(task="再加一个刷新 token 的接口", working_dir="/proj")
# 切换到无关任务,开新会话
delegate_coding(task="重构 utils", working_dir="/proj", reset_session=True)
超时与 thread 状态保护¶
Coding Agent 把「turn 是否进行中」记在自己的 session 文件里。若直接 kill() 超时进程,会让 thread 停在 'turn in progress',导致之后手动 codex resume / /resume 被拒(/resume is disabled while a task is in progress)。
_terminate_proc() 改为优雅终止:先 SIGTERM 给 CLI 收尾机会,超过宽限期仍未退出再 SIGKILL。此外超时后会清掉该 (agent, 目录) 持久化的会话 id,下次委派从新会话开始,避免续接到坏 thread。
镜像会话(MirrorSession)¶
文件:ethan/acp/mirror.py
每次 delegate() 调用会落成一条真正的 Ethan 会话,source 用真实的 coding agent 名(codex / claude / opencode),让用户在 Ethan 侧也能看到「下发给 Coding Agent 的 query + Coding Agent 的回复 + 中间工具步骤」,而不只是主对话里一行 delegate_coding 工具结果。web 侧边栏的渠道徽标据此显示是哪个工具(青色 Codex / 琥珀 Claude Code / 玫红 OpenCode)。
- 粒度:同一
(agent, cwd)的连续委派累加到同一条 Ethan 会话(多轮对话);reset_session=True(切换任务)时新建一条。映射存在acp_sessions.json的mirror::{agent}::{cwd}键。 - user 消息 = 每一轮下发的 query(委派一开始就落库)
- assistant 消息 = 每一轮 Coding Agent 的最终回复 + tool_steps(由 sub_steps 转来)
- 同库:走 per-user 的
SessionStore(user_sessions_db_path()),与普通 web 会话同库,侧边栏天然能列出(按source区分) - model 字段:存真实可用的 chat 模型(取
defaults.model),不能用 agent 名——否则用户在该会话里直接发消息时会被当成 chat 模型,导致unknown provider for model codex502。渠道归类由source表达,与 model 解耦。 - best-effort:写库/注册任何一步失败都吞掉异常,绝不影响主委派流程
- 可用
delegate(mirror=False)关闭
实时推送¶
镜像会话在 start() 时同步注册一个 RunManager 的 ChatRun,使委派过程可被 web 实时 attach:
- Coding Agent 跑动过程中,每个工具步骤(step)和 agent 文本块(text)经
on_event回调实时 emit 进 ChatRun 缓冲 - 前端用现成的
GET /chat/{session_id}/streamSSE 端点 attach,边跑边看;断线重连可回放完整缓冲 finish()时 emitdone并安排清理(宽限期内仍可重连回放)
事件映射到 chat SSE 词表:text → {"content": ...},step → 工具事件(用 id(step) 作稳定 id 配对)。
在镜像会话里直接续接对话¶
用户直接在某条镜像会话里发新消息时,这条消息不走普通 chat 模型,而是被当作新 prompt 续接对应的 coding agent(resume=True):
- 反向映射
mirrorinfo::{session_id} → {agent, cwd}在每次委派时写入acp_sessions.json POST /api/chat检测到session_id命中get_mirror_info()时,改走_run_delegate_generation,调用delegate(prefer=agent, cwd=cwd, resume=True, mirror=False, on_event=...)mirror=False避免为同一 session 重复注册 ChatRun(双 writer);过程的 step/text 经on_event实时推回这条会话- 效果:这条会话名副其实是「能持续对话的 coding agent 会话」——发一句它就接着让 codex/claude/opencode 干,而不是切回 Ethan 默认模型
沉浸式工具模式(codex / claude_code / opencode)¶
除了「临时委派 + 镜像会话」,还有一条对偶路径:沉浸式工具模式。在 ethan/core/modes.py 里,这三个模式带 delegate_agent(codex / claude / opencode)字段:
- 用户在 web 把对话 mode 切到 Codex / Claude Code / OpenCode 后,整条会话的每句话都直接续接该 coding agent(同一工具 session 多轮),而不是走 Ethan 的 chat 模型。
POST /api/chat在 stream 分支最前面解析resolve_mode(req.mode).delegate_agent:非空且有session_id时,走_run_delegate_generation。- 工作目录按会话隔离:
~/.ethan/agent-sessions/<会话id>/(user_agent_session_dir)。同一会话连续消息复用同一目录 → 复用同一工具 session;不同会话互不干扰。 - 三段路由优先级:(1) 沉浸式模式 → (2) 镜像会话续接(临时委派会话) → (3) 普通 chat。
- 两条路径的取舍:临时
delegate_coding(主对话里随手让工具干一件事,产出镜像会话)vs 沉浸式(明确切进某工具,持续对话)。
cwd 不存在时(临时目录被删等)
_run_delegate_generation会给出「工作目录已不存在」的清晰提示,而不是抛[Errno 2] No such file or directory;子进程在 emit 任何文本前就失败时,会把最终结果补推一次,避免 live 流空返回。
结构化输出(sub_steps)¶
三套 Coding Agent 的 JSON 事件流都解析为统一的结构化子步骤:
Claude Code(--output-format stream-json):
| 事件类型 | 解析结果 |
|---|---|
assistant + tool_use |
新增 sub_step(state=running) |
user + tool_result |
关闭 sub_step(state=done/error,填 duration_ms + result_preview) |
system/init |
提取 session_id |
result |
最终文本结果 + success/error |
Codex(--json):
| 事件类型 | 解析结果 |
|---|---|
thread.started |
提取 thread_id 作 session_id |
item.completed (command_execution) |
shell 步骤 |
item.completed (file_change/patch) |
edit 步骤 |
item.completed (agent_message) |
最终文本(取末条) |
item.completed (error) |
error 步骤(弃用/配置警告类自动过滤,不显示成 error) |
turn.failed / error |
标记失败 |
OpenCode(--format json):
| 事件类型 | 解析结果 |
|---|---|
含 sessionID 的事件 |
提取 sessionID |
text part |
累积最终文本 |
tool part |
工具步骤(按 state.status 判 done/error) |
error |
error 步骤 |
delegate() 返回的 ACPResult.sub_steps 经 ToolResult → ToolEvent 流到主对话 Web UI,在工具时间轴里折叠展示(见下)。
DelegateCodingTool¶
文件:ethan/tools/builtin/acp.py
LLM 可以直接调用的 tool:
delegate_coding(
task="Implement a user authentication system with JWT tokens...",
working_dir="/path/to/project", # optional,默认当前目录
reset_session=False, # True 则不复用该目录的历史会话
)
cacheable=False(有副作用,不可缓存)fast_path=False(只在 medium/full path 加载)- timeout 默认 180 秒
- 输出超 12000 字符自动截断
- 返回
[agent](session=xxxxxxxx) output格式,并携带sub_steps
Web UI 展示¶
delegate_coding 的工具调用在主对话 Web UI 时间轴里有专门处理(web/components/tool-timeline.tsx):
- 子步骤折叠:点击「N/总 步」展开 Coding Agent 内部的每次工具调用(如 Bash / Edit / Write),显示工具名、参数摘要、耗时、结果预览
- 最终结果高亮:
delegate_coding的result_preview用绿色高亮卡片展示,区别于普通工具的灰字预览 - 历史会话重载时,
sub_steps从tool_steps字段反序列化还原
除主对话时间轴外,镜像会话(见上)让委派过程本身成为一条可在侧边栏打开、可实时 attach 的独立 Ethan 会话。
REPL 模式下也会打印子步骤计数摘要:↳ N 步工具调用(M 成功)。
未来改进¶
- 更精准的复杂度判断:通过 LLM 预判是否需要委托(替代启发式)
- 双向通信:让 Coding Agent 在执行中回调 Ethan 澄清需求
- 上下文传递:把 Ethan 的记忆(用户偏好、项目信息)传给 Coding Agent
- 结果审查:Ethan 自动 review Coding Agent 的输出,提出改进建议
- 镜像会话前端区分:侧边栏对
source="delegate"的会话做分组/标记,与普通对话区分