开源 Agent 套件 ECC 为什么用 Rust 再写一层 ecc2
本文基于 ECC 仓库 commit 591ab5c(2026-07-29)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/affaan-m/ECC 最新代码与文档为准。
ECC 用 Rust 再写一层 ecc2,搬过去的不是 agent、技能和命令那些 markdown 资产,而是「模型说了不算」的那部分:进程还活着吗、状态写到哪、git 工作区归谁管、哪次工具调用该拦一下。 这条界线不是按语言性能划的,是按「出错时谁兜底」划的。看懂它比看懂 Rust 本身有用——你自己搭 agent 平台时,迟早要做同一道题。
站内的 /learn/ai-jishu-xuanxing/ 和 /learn/ai-xingneng-youhua/ 讲的是通用方法论:怎么在候选技术之间做取舍、性能问题从哪几个层次找。本篇不重复那套框架,只盯一个具体项目——它把方法论落到实处时,具体切在了哪一刀上,代价是什么。
一、先看清楚两边各自装着什么
ECC 仓库根目录是典型的 harness 增强件形态:agents/ 下 67 个 agent,skills/ 下 281 个技能,commands/ 下 94 个命令,另有 hooks/、rules/、scaffolds/、mcp-configs/ 等目录。这些东西的共同点是——它们本身不执行,是被 harness 读进去、影响模型行为的资产。
ecc2/ 是另一副面孔。README 里第一句就把它定位成「当前的 Rust 版 ECC 2.0 控制面脚手架」,并且明写它是可用于本地实验的 alpha,不是完成品。它的 src 下只有七个模块声明,在 main.rs 开头一眼看完:comms、config、notifications、observability、session、tui、worktree。
没有 prompt 目录,没有技能加载器,没有模型调用封装。这个缺席本身就是答案的一半。
| 组成部分 | 它负责什么 | 对应仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| markdown 资产层 | agent、技能、命令、钩子等由 harness 自己加载的内容 | agents/、skills/、commands/、hooks/ | 在 harness 内部干活时,一直在用 |
| 会话生命周期 | 建会话、排队、派生、停止、恢复 | ecc2/src/session/manager.rs | 执行 start、stop、resume 时 |
| 进程托管与输出捕获 | 拉起子进程、逐行收 stdout/stderr、打心跳、落终态 | ecc2/src/session/runtime.rs | 会话跑起来之后的全过程 |
| 状态存储 | SQLite 建表与读写,sessions、tool_log、session_output、decision_log 等 | ecc2/src/session/store.rs | 查 sessions、status 时 |
| 后台守护 | 循环体检、定时任务派发、积压协调、工作区自动合并与清理 | ecc2/src/session/daemon.rs | 执行 daemon 时 |
| git 工作区 | 建工作区、看差异、判断能不能合、冲突与清理 | ecc2/src/worktree/mod.rs | 带工作区跑会话时 |
| 工具调用记录与风险打分 | ToolCallEvent、RiskAssessment、SuggestedAction | ecc2/src/observability/mod.rs | 想知道 agent 刚才碰了什么时 |
| 配置解析 | ecc2.toml 的加载与合并 | ecc2/src/config/mod.rs | 配 harness_runners、agent_profiles 时 |
| 终端界面 | 面板、快捷键、多窗格布局 | ecc2/src/tui/dashboard.rs | 执行 dashboard 时 |
表里的位置都是真实路径,你 clone 下来就能一一对上。
二、被搬进 Rust 的,是哪三类职责
进程与状态:会话在你关掉终端之后还得活着
start 之后发生的事,比看上去绕一层。manager.rs 里并不是直接把 harness 的 CLI 拉起来,而是先用 std::env::current_exe() 拿到自己这个可执行文件的路径,再以 run-session 子命令重新拉起自己一份,把 --session-id、--task、--agent、--cwd 传过去。这个子命令在 CLI 定义里是隐藏的,正常用户不会直接敲。
为什么要绕这一层?因为要脱离调用者的 shell。代码里对两个平台分别做了处理:unix 下在 pre_exec 里调 setsid(),Windows 下拼 DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP 这两个创建标志。目的写在注释里——让 runner 在 start 返回之后仍然继续处理会话。
真正跑 harness 的动作在 run_session 里,它先按 agent 类型解析出程序名(claude、codex、opencode、gemini,或者你在 ecc2.toml 的 harness_runners 里自定义的那个),再由 build_agent_command 拼参数。runtime.rs 里的 capture_command_output 接手之后干四件事:把 stdout 和 stderr 分别接管、逐行写进 SQLite、按 heartbeat_interval_secs 打心跳、进程退出时按 exit status 把状态落成 Completed 或 Failed。
它写数据库的方式也有讲究:不是在异步任务里直接摸连接,而是单开一个线程跑 run_db_writer,其他地方通过 channel 发消息、等 ack。SQLite 的并发写在这里被收敛成了一条串行通道。
daemon.rs 是另一半。它启动时先跑 resume_crashed_sessions,用进程是否还活着来判断哪些会话是「记录说在跑,实际已经死了」,把这些会话直接标成失败,再进入按心跳间隔推进的循环:体检会话、派发到点的定时任务、处理远程队列、协调积压、自动合并与清理工作区、激活排队中的工作区会话。每一步都单独 catch 错误、打日志、继续下一步,任何一环炸掉不会把守护进程带走。
git 工作区:并发的前提是隔离
多个 agent 会话同时改同一个仓库,这事在没有隔离时基本无解。ECC 的做法是给会话配 git worktree,配置项就叫 auto_create_worktrees,默认开着,命令行上可以用 --worktree / --no-worktree 覆盖,优先级在 WorktreePolicyArgs::resolve 里写得很直白。数量上限由 max_parallel_worktrees 卡着,超了就进 pending_worktree_queue 表排队,等守护进程有空位再激活。
worktree/mod.rs 有个细节值得说:Cargo.toml 里明明依赖了 git2 这个绑定库,但翻遍 src 找不到一处 git2 的调用,所有 git 动作都是 Command::new("git") 直接拉外部命令完成的,创建草稿 PR 那一处则是调 gh 命令行。这是个务实的选择——分支状态、rebase、冲突中止这些操作,用官方命令行的语义比用库更稳,出了问题也能把同一条命令手敲一遍复现。代价你得认下来:它对宿主机的 git 版本和 PATH 有硬依赖,容器或 CI 里少装一个 gh,相关路径就直接失败;而那个没被用上的 git2 依赖仍然要参与编译,构建时间是白付的。
关于隔离本身的取舍,站内 /learn/agent-gongzuoqu-geli/ 有更一般化的讨论,这里只看 ECC 的落法。
观测与风险:给工具调用打分,而不是给模型打分
observability/mod.rs 里定义了 ToolCallEvent,字段包括工具名、输入摘要、输出摘要、触发说明、耗时和风险分。风险分由 compute_risk 算出来,思路是四项相加再截断:工具本身的基础风险(shell 执行高于写文件,写文件高于改文件)、输入里有没有碰到敏感文件特征(.env、密钥、凭据一类字样)、影响面有多大、这个动作可不可逆。
分数出来之后映射成 SuggestedAction 的四档:Allow、Review、RequireConfirmation、Block。分档的门槛在 Config 的 risk_thresholds 里,可以按项目改。
这套东西的定位要看清楚:它是启发式的字符串特征匹配,不是沙箱,也不是权限系统。它给你的是「这次调用值不值得看一眼」的排序信号。日志层面的通用做法可参考 /learn/agent-keguancha-rizhi/,权限该怎么收敛可参考 /learn/agent-zuixiao-quanxian-sheji/。
三、这条界线是按什么划出来的
把三类职责摆在一起看,共同点很清楚:它们都必须在 agent 出错、模型跑偏、终端被关掉之后仍然成立。 进程死没死不能靠模型自述,状态写没写进去不能靠模型确认,分支能不能合不能靠模型判断,工具调用有多危险不能只由发起调用的那一方说了算。这类事情需要一个不参与推理、只负责记录和执行的旁观者,而它的正确性要能被测试覆盖、被类型系统约束。
反过来,另一侧的证据同样明确:ecc2 没有试图接管模型行为,反而主动把 markdown 那一层塞回 harness。build_agent_command 拼命令时会调 apply_shared_harness_runtime_env,给子进程统一注入一批环境变量:
command.env("ECC_SESSION_ID", session_id);
command.env("ECC_HARNESS", &harness_label);
command.env("ECC_WORKING_DIR", working_dir);
command.env("ECC_PROJECT_DIR", working_dir);
command.env("CLAUDE_SESSION_ID", session_id);
command.env("CLAUDE_PROJECT_DIR", working_dir);
command.env("CLAUDE_CODE_ENTRYPOINT", "cli");
同一个函数后半段还有一段条件注入:只要 resolve_ecc_plugin_root 能从可执行文件路径逐级往上找到 ECC 插件根目录,就再补上 ECC_PLUGIN_ROOT 与 CLAUDE_PLUGIN_ROOT。而判断一个目录是不是插件根的函数只有一行:
fn is_ecc_plugin_root(candidate: &Path) -> bool {
candidate.join("scripts/lib/utils.js").is_file() && candidate.join("hooks/hooks.json").is_file()
}
它认的是 JS 脚本和钩子清单文件。也就是说,Rust 侧不但没打算取代原来那套资产,还专门去找它、把路径递给 harness。
agent_profiles 的处理方式也说明了同一件事。profile 里配的 model、allowed_tools、disallowed_tools、permission_mode、add_dirs 这些字段,最终都是被翻译成对应 harness 的命令行参数拼上去的——针对 Claude 拼一套,针对 Codex 拼另一套(exec、--sandbox、--cd 那一组),针对 OpenCode 和 Gemini 各拼各的。Rust 侧只做「把意图翻译成参数」,参数背后的行为归 harness 自己。
所以这条界线可以用一句话概括:能被表达成状态机、文件系统操作和进程信号的,搬进 Rust;需要靠语言理解、需要经常被人手改的,留在 markdown。
四、边界与代价:它明确不管什么
先说项目自己承认的部分。ecc2/README.md 有一节专门列还缺什么:更丰富的多 agent 编排、显式的 agent 间委派与摘要、可视化的工作区与 diff 审阅界面、更强的外部 harness 兼容、更深的记忆与路线规划层、发布打包与安装方案。文末还有一条仓库规矩,大意是不要因为脚手架能编译就把 ecc2/ 说成完成品。这种自我限定在开源项目里不多见,值得当成一手信息看待。
再说结构上的代价,这些是你用之前要自己认下来的:
跨 harness 是有梯度的。 HarnessKind 枚举里认得的类型不少,除了 Claude、Codex、OpenCode、Gemini,还有 Cursor、Kiro、Trae、Zed、FactoryDroid、Windsurf。但 supports_direct_execution 只对前四种返回真,其余的作用主要是靠项目里的 .cursor、.zed 这类标记目录做探测。想让 ecc2 真的去驱动它们,得自己在 harness_runners 里把程序名和各个参数开关配全。
它会往你的机器上写东西。 默认的状态库落在用户目录下 .claude/ecc2.db,工作区根目录默认在 /tmp/ecc-worktrees(这个默认值在 Windows 上显然需要你改),后台 runner 的 stderr 会写到工作目录下 .claude/ecc2/logs 里、以 .runner-stderr.log 结尾的文件。加上 git worktree 本身会在磁盘上建实体目录,这套东西的痕迹不算轻。
它会往外发请求,而且两条通道的默认值不一样。 notifications 模块认得五类事件:会话开始、完成、失败、预算告警、审批请求。webhook 这一路默认是关的,你得自己打开开关并填目标地址(代码里区分 Slack 与 Discord 两种载荷格式),一旦打开,会话标题和摘要就以 POST 请求离开了你的机器。桌面通知那一路正相反,默认是开着的——macOS 走 osascript 拼命令(代码里对引号和换行做了转义),Linux 走 notify-send,Windows 没有对应实现所以什么也不发。这个差异值得单独记一下:默认状态下你只会看到本机弹窗,不会有数据外发;真正需要你审一遍的是 webhook 那一栏。
远程派发是要开端口的。 remote serve 会绑一个地址监听、用 bearer token 校验请求,把外部提交的任务排进队列等守护进程处理。它走的是自己手写的明文 HTTP 应答,没有 TLS。这条路适合的场景是本机回环或你已经自己套了反向代理,直接暴露到公网并不合适。
detach 是双刃的。 会话进程脱离了你的 shell,好处是关终端不影响它,坏处是它不会随你的终端一起消失。停止路径上,unix 是先 SIGTERM 再 SIGKILL,Windows 是调 taskkill 带上强制和杀子树。真出问题时,你得知道去哪儿找它。
它不管内容质量。 这套控制面完全不看 agent 写出来的代码对不对、方案合不合理。风险打分看的是工具名和输入串的特征,不是语义。验收标准还是得你自己定。
五、上手清单与几个必踩的坑
先跑通再谈配置。 仓库根目录下进 ecc2 执行 cargo run 就能起来,README 给的常用命令是这些:
# Launch the dashboard
cargo run -- dashboard
# Start a new session
cargo run -- start --task "audit the repo and propose fixes" --agent claude --worktree
# List sessions
cargo run -- sessions
# Inspect a session
cargo run -- status latest
会踩的第一个坑:以为 agent 程序是 ecc2 提供的。 不是。agent_program 解析出来的只是一个程序名,claude、codex 这些得你自己装好、在 PATH 里能直接调起来。没装就是拉起子进程失败,会话直接落 Failed。上手前先在同一个 shell 里手敲一次那个命令,确认能跑。
第二个坑:在非 git 仓库或脏工作区里带 --worktree 起会话。 工作区创建走的是真实 git 命令,仓库状态不对它就建不出来。而 auto_create_worktrees 默认是开的,意味着你不加任何参数也可能触发这条路径。要么先把仓库整理干净,要么显式加 --no-worktree。
第三个坑:只看命令行不看守护进程。 排队、定时任务、远程队列、工作区自动合并与清理这些动作,全都在 daemon 循环里推进。你 start 了一堆会话却没跑守护进程,超过并发上限的那些就一直躺在队列表里不动。判断方法很简单:sessions 里状态迟迟不变的,多半是没人推它。
第四个坑:误判风险打分的作用。 它是启发式排序,不是准入控制。Block 这一档也只是「建议」——真正的拦截仍然要靠 harness 侧的权限模式和你自己的确认习惯。把它当成日志里的高亮标记来用,别当护栏。
第五个坑:Windows 上照抄默认配置。 工作区根目录的默认值是 unix 风格路径,进程终止路径也按平台分了两套实现。换平台时,配置里凡是带路径的项都要重新过一遍。
第六个坑:忘了它是 alpha。 版本号在 ecc2/Cargo.toml 里写着,包名是 ecc-tui。这个阶段的项目,数据表结构、配置键、命令参数都可能变。别把生产流程直接架在上面,也别指望升级时状态库无痛迁移。
收尾:接下来该读哪个文件
如果你只想确认「这套划分是否适合我」,按这个顺序读三个文件就够:先 ecc2/README.md 看清项目自己承认的边界,再 ecc2/src/main.rs 开头的模块声明和 CLI 定义看职责范围,最后 ecc2/src/session/manager.rs 里的 run_session 和 build_agent_command 看它到底怎么跟 harness 打交道。三个文件读完,你会发现它没有任何魔法——就是把「进程、状态、git、记录」这四件苦活接过去,把剩下的还给 harness。
对你自己的系统,可以拿三个问题自检:这件事情出错时,是模型兜底还是程序兜底?这件事情需不需要在会话崩溃之后仍然成立?这件事情是不是每周都要有人手改一版?第一个问题答「程序」、第二个答「需要」、第三个答「不是」,那它就该被写成代码,而不是写进提示词。ECC 划的那一刀,划的就是这三个答案的交集。
顺带一提,这个项目采用 MIT 许可证,仓库在 https://github.com/affaan-m/ECC 。它仍在高频改动,你读到的行数和结构大概率跟本文不完全一致——以仓库为准。
本文属于 ECC 开源 Agent 套件专题(共 40 篇,从选择性安装一直拆到内部机制)。同一批还写了另一种取向的项目——不给你资产、只给你纪律的 superpowers 方法论专题。