# 回顾

> 全书要点串联：一条消息穿过的各层，值得记住的设计，与站内其他终端 Agent 的对照，读代码时发现的文档出入与值得留意的行为，以及可迁移的经验和这份源码的短板。

- 作者：David（道雾轩）
- 专栏：ZCode 源码解读（https://daiw.net/manual/zcode.md）
- 最后更新：2026-09-21
- 原文：https://daiw.net/manual/zcode/recap
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

读到这里，ZCode 的形状可以归成一句话：**一个自研的 Agent 运行时，装进三种宿主**。回合循环、工具、权限、压缩、子 Agent、工作流都在 `apps/zcode-cli` 里自己写；终端 TUI 在进程内直接调用它，桌面端与 Web 服务端把它当子进程，隔着 ZCode Protocol 驱动它。产品一侧的五十多万行代码，大部分在做“怎样呈现、怎样调度这个运行时”，而运行时本身只关心一件事：把一个会话跑好。这一篇把前面各部分串起来。

## 一条消息穿过的各层

对照[一条消息的旅程](https://daiw.net/manual/zcode/message-lifecycle)：

1. **宿主**。TUI 经一组回调把输入交给 cli 的命令中心；桌面端与 Web 把它包成 `v4/command` 信封，经 Host 写进 Agent 子进程的标准输入，先过 `CommandInbox` 的查重与串行（[终端界面](https://daiw.net/manual/zcode/tui)、[ZCode Protocol V4](https://daiw.net/manual/zcode/zcode-protocol)、[桌面应用](https://daiw.net/manual/zcode/desktop)）。
2. **装配**。一个会话一个 `ZCodeApp`，里面恰好一个 `AgentRuntime`，端口由 bootstrap 按五层配置造好注入（[bootstrap](https://daiw.net/manual/zcode/bootstrap-assembly)、[AgentRuntime](https://daiw.net/manual/zcode/agent-runtime)）。
3. **受理**。空闲时当场建立启动预留，忙碌时分成并进当前回合的引导与排到之后的队列（[输入受理](https://daiw.net/manual/zcode/prompt-admission)）。
4. **回合**。冻结本轮模型，初始化上下文与钩子，写入用户消息，然后在循环里反复“整理、请求、执行”（[回合循环](https://daiw.net/manual/zcode/turn-loop)、[系统提示词](https://daiw.net/manual/zcode/context-builder)、[上下文压缩](https://daiw.net/manual/zcode/compaction)）。
5. **模型**。适配层把三种 API 形态统一成一套流式事件，只读工具边流边执行，截断续写、断流恢复与两层重试兜住异常（[一次模型请求](https://daiw.net/manual/zcode/model-step)、[模型适配层](https://daiw.net/manual/zcode/model-adapters)）。
6. **工具**。每个调用走一遍校验、钩子、权限、带超时执行、结果预算与投影的流水线（[执行器](https://daiw.net/manual/zcode/tool-executor)、[权限](https://daiw.net/manual/zcode/permission)、[钩子](https://daiw.net/manual/zcode/hooks)）。
7. **回流**。事件经 `appendEvent` 发给订阅者，消息与 part 并排写进 SQLite；TUI 直接渲染原始事件，协议层先归约成行与状态再攒帧推送（[会话事件流](https://daiw.net/manual/zcode/session-events)、[SQLite 会话库](https://daiw.net/manual/zcode/session-store)）。

## 值得记住的设计

**运行时**

1. **端口与原型安装**。`AgentRuntime` 只认 `contracts` 里的端口，近两百个方法分散在近百个文件里，再装到同一个类的原型上；宿主不注入审批端口，任何要问人的调用一律被拒（[AgentRuntime](https://daiw.net/manual/zcode/agent-runtime)）。
2. **引导与排队是两条车道**。运行中的新输入要么在下一个工具批次之后并进当前回合，要么等回合结束另起一轮；桌面端的“交互行为”设置决定走哪条（[输入受理](https://daiw.net/manual/zcode/prompt-admission)）。
3. **长程优先**。回合循环没有步数上限，工具调用的次数与重复只换来提醒；真正的刹车是上下文压缩与“压缩后迅速又满”的熔断、输出截断续写的次数、断流恢复的次数与用户取消（[回合循环](https://daiw.net/manual/zcode/turn-loop)）。
4. **断流时补齐工具结果**。已经发出的工具调用若得不到结果，就合成一条“按失败处理”或“副作用未知、先检查现状”的结果，保证历史里每个调用都有配对（[一次模型请求](https://daiw.net/manual/zcode/model-step)）。
5. **事件只在内存，消息落库**。唯一的事件存储是进程内的，流式事件按回合窗口淘汰；冷恢复时由持久化消息反推出等价事件，再喂给同一个投影（[会话事件流](https://daiw.net/manual/zcode/session-events)）。

**工具与安全**

6. **声明式工具契约**。每个工具声明 schema、只读、并发安全、副作用范围、结果预算与超时，调度、权限与流式执行都读这些声明；Glob 与 Grep 默认不给模型，搜索改由 Bash 里被替换成 bfs 与 ugrep 的 `find`、`grep` 承担（[工具契约](https://daiw.net/manual/zcode/tool-contract)、[读、写、改、搜](https://daiw.net/manual/zcode/file-tools)）。
7. **只读判定表与自动转后台**。Bash 命令先经 unbash 解析，再按一套简单命令、多词命令、git 子命令与 flag 的规则判定是否只读，命令注册表由 Fig 的补全规格生成；普通前台命令到了超时并不被杀，而是原地转成后台任务，模型之后再去读输出（`sleep` 开头的命令与闲时回合除外）（[Bash](https://daiw.net/manual/zcode/bash)）。
8. **把副作用摊开讲**。四种权限模式、七个生命周期钩子、按声明摘要信任的工作区钩子；`NOTICE.md` 逐项写明了哪些功能会联网、数据落在哪里，并直说共享执行适配器没有操作系统沙箱（[权限](https://daiw.net/manual/zcode/permission)、[钩子](https://daiw.net/manual/zcode/hooks)、[执行边界](https://daiw.net/manual/zcode/exec-boundary)）。

**长程与多 Agent**

9. **目标模式另起一次请求验证**。完成与否不让干活的那一轮自评，而是在它结束后另发一次不带工具的请求判断，没有轮数与时间上限（[目标模式](https://daiw.net/manual/zcode/goal-target)）。
10. **动态工作流**。模型写 TypeScript 编排脚本，编译器做类型检查、taint 分析、JSON Schema 合成与站点插桩，脚本在只有 ES 内建对象的 `vm` 上下文里运行，靠日志按“站点加序号”回放实现中断后续跑（[编译器](https://daiw.net/manual/zcode/dwf-compiler)、[引擎](https://daiw.net/manual/zcode/dwf-engine)、[工具链](https://daiw.net/manual/zcode/dwf-tools)）。
11. **三条工作流线并存**。固定八阶段的专家工作流存文件，早期的脚本工作流与现在的动态工作流各有一套 SQLite 表，后者由前者演化而来（[专家工作流](https://daiw.net/manual/zcode/expert-workflow)）。

**宿主与协议**

12. **命令信封与两种投递档位**。所有命令带客户端生成的 `commandId` 做幂等，按修订号做并发检查；桌面每 30 毫秒一帧、流式全发，Web 与手机 150 毫秒一帧、只流正文，终态逐字节一致（[ZCode Protocol V4](https://daiw.net/manual/zcode/zcode-protocol)）。
13. **把产品行为画成状态空间**。`packages/formal-proof` 枚举“某个状态下来了某种输入”的全部组合，协议的输入路由裁决表声称与它逐条对齐（[怎么读这份源码](https://daiw.net/manual/zcode/reading-the-source)）。
14. **窗口级 Host**。每个窗口一个 Host 进程，刷新页面不杀 Host，桌面、手机与远程工作区都挂在同一个 Host 的 attachment 上（[桌面应用](https://daiw.net/manual/zcode/desktop)、[远程工作区与手机远控](https://daiw.net/manual/zcode/remote)）。

## 与站内其他终端 Agent 对照

| 维度 | [OpenCode](https://daiw.net/manual/opencode) | [Kimi Code](https://daiw.net/manual/kimi-code) | [MiMo Code](https://daiw.net/manual/mimo-code) | [MiniMax Code](https://daiw.net/manual/minimax-code) | ZCode |
| --- | --- | --- | --- | --- | --- |
| Agent 循环 | 自研 | 自研 | 沿用 OpenCode | 内置 pi-mono，外包回合系统 | 自研 |
| 终端界面 | OpenTUI + SolidJS | 复制 pi-tui | 沿用 OpenTUI | 另 fork 的 pi-tui 0.84.2 | OpenTUI 的 React 渲染 |
| 存储 | SQLite + Drizzle | 追加日志 + 自研索引 | SQLite | SQLite + Drizzle | Node 内置 `node:sqlite` + 手写仓储与迁移 |
| 入口 | TUI、Web、桌面、ACP 等 | TUI、Web、VS Code、ACP 等 | 以 TUI 为主 | TUI、exec、ACP | TUI、`-p` 无头、桌面、Web（后两者经 ZCode Protocol） |
| 与上游的关系 | 自身即上游 | 复制一个库 | 分叉后独立演进 | vendor 源码并记补丁台账 | 无上游循环，复制了 VS Code IPC、Fig 补全规格等组件 |

## 文档与代码的出入

README、`AGENTS.md` 与代码注释大体可信，但以下几处与代码不一致，正文都按代码写：

- **规矩与现状**：CLI 的 `AGENTS.md` 规定单文件不超过 400 行，全仓仍有 478 个非测试文件超标，架构检查只对唯一一个受管模块生效（[仓库全景](https://daiw.net/manual/zcode/monorepo-map)）；两份 `AGENTS.md` 都强调测试，开源仓库却只有 4 个测试文件（[怎么读这份源码](https://daiw.net/manual/zcode/reading-the-source)）。
- **CLI 文档**：`apps/zcode-cli/README.md` 开头仍是起步模板的口吻，“零生产依赖”与列出的几个 npm 脚本都不存在；`--help` 漏掉 `hooks`、`agent-server`、`--output-format` 与 `inspect`，斜杠命令表 19 个只列了 15 个（[命令行入口](https://daiw.net/manual/zcode/cli-surface)、[ZCode 是什么](https://daiw.net/manual/zcode/what-is-zcode)）。
- **插件与钩子**：README 说插件配置只展开 `ZCODE_` 前缀的环境变量，实际任何环境变量都会展开，还认 `CLAUDE_` 前缀的别名（[插件](https://daiw.net/manual/zcode/plugins)）；钩子一节对 matcher、钩子类型与非 JSON 输出的描述与代码不符（[钩子](https://daiw.net/manual/zcode/hooks)）。
- **工具描述**：Read 说默认最多读 2000 行，实际只按字节与 token 截断；文件工具要求绝对路径，实际也收相对路径；WebFetch 说交给“小而快的模型”处理，实际用本轮的主模型；Edit 让模型改用并不存在的 NotebookEdit（[读、写、改、搜](https://daiw.net/manual/zcode/file-tools)、[Web 工具](https://daiw.net/manual/zcode/web-tools)）。
- **官方套餐网关**：文件头注释说“用户自建 provider 不受影响”，实际按最终 URL 改写，指向官方端点的个人 Provider 同样走网关（[模型适配层](https://daiw.net/manual/zcode/model-adapters)）。
- **记忆**：系统提示词说记忆的 `description` 用于召回时判断相关性，代码里没有按问题召回的逻辑，选哪条由主模型看索引自己决定（[项目记忆](https://daiw.net/manual/zcode/memory)）。
- **协议与工作流**：根 `AGENTS.md` 要求协议改动同步更新 `zcode-protocol/index.ts`，V4 的 schema 其实在另一个目录；注释说“20 命令全部原生”，实际有 34 种；动态工作流运行时的 README 对失败裁决与线协议的描述已经过时（[ZCode Protocol V4](https://daiw.net/manual/zcode/zcode-protocol)、[动态工作流（二）](https://daiw.net/manual/zcode/dwf-engine)）。
- **配置**：`features.compact` 能解析却关不掉自动压缩，microcompact 缺省根本不启用；`logging.*`、`features.rewind` 等只解析不生效（[上下文压缩](https://daiw.net/manual/zcode/compaction)、[bootstrap](https://daiw.net/manual/zcode/bootstrap-assembly)）。

## 值得留意的行为

<Callout type="warn">
  下面各条都是读代码得出的结论，多数没有实际运行验证；开源仓库几乎没有测试可以佐证。使用时以实测为准。
</Callout>

**与安全边界有关**

- 自定义命令里的 Shell 展开不经过权限系统，没有钩子、审批与 Plan 检查；TUI 空闲时拒绝，但忙碌时、App 协议与 `-p` 下都会执行（[技能与自定义命令](https://daiw.net/manual/zcode/skills-commands)）。
- 项目配置里的钩子要等工作区信任，但同一个文件里的 `plugins.dirs` 照常合并，本地插件默认启用、插件钩子总能运行，仓库可以借此绕开工作区钩子信任（[插件](https://daiw.net/manual/zcode/plugins)、[钩子](https://daiw.net/manual/zcode/hooks)）。
- 项目级 MCP 默认可信并自动连接；TUI 一启动就轮询 MCP 状态，项目配置里的 stdio 命令在你输入第一句话之前就会启动（[MCP](https://daiw.net/manual/zcode/mcp)）。
- Bash 的只读判定只看命令、不看路径：用绝对路径读工作区之外的文件，在 `build` 模式下也不询问（[Bash](https://daiw.net/manual/zcode/bash)）。
- Plan 模式放行所有没标 `destructiveHint` 的 MCP 工具，其中包括能执行任意 Node 代码的 `mcp__node_repl__js`；yolo 的放行排在配置文件里的禁用名单之前（[权限](https://daiw.net/manual/zcode/permission)）。
- 动态工作流的 actor 子会话强制跑在 yolo 下，批准一次 run，等于放行其中所有 actor 的命令与文件编辑（[动态工作流（三）](https://daiw.net/manual/zcode/dwf-tools)）；内置的 Explore 子 Agent 同样以 yolo 运行并保留 Bash，“只读”只靠提示词约束（[子 Agent](https://daiw.net/manual/zcode/subagents)）。
- SSH 远程工作区的连接配置里没有主机密钥校验回调，全仓也不读 `known_hosts`（[远程工作区与手机远控](https://daiw.net/manual/zcode/remote)）；从代码看，`pnpm dev:web` 的开发后端没设监听地址时监听所有网卡、也不带令牌（[Web 与服务端](https://daiw.net/manual/zcode/server-web)）；WebFetch 的出站护栏只看 URL 里的字面量地址，不做 DNS 解析（[Web 工具](https://daiw.net/manual/zcode/web-tools)）。

**疑似缺陷**

- 批执行器把 `automationTurn` 传给了每组工具调用，却漏了 `offPeakTurn`，闲时回合里拒绝 Bash 后台运行的保护因此不会触发（[执行器](https://daiw.net/manual/zcode/tool-executor)）。
- `TurnError.turnPhase` 恒为 `processing_input`；子 Agent 定义里的 `maxTurns` 被填上却从不读取（[回合循环](https://daiw.net/manual/zcode/turn-loop)）。
- WebFetch 的“始终允许”把整条 URL 存成规则，判定时却按主机名比对，存下的规则不会生效（[权限](https://daiw.net/manual/zcode/permission)）。
- 目标模式的完成验证一出故障就按通过处理：返回的不是合法 JSON、验证器想调用工具或请求出错，目标都直接记为完成（[目标模式](https://daiw.net/manual/zcode/goal-target)）。
- 专家工作流终审阶段的提示词只要求 Markdown，解析器却必须拿到 JSON 判决（[专家工作流](https://daiw.net/manual/zcode/expert-workflow)）。
- TUI 的 `/fork` 会把检查点里的文件写回改动前的内容，而父子会话共用同一个工作目录，父会话的文件也跟着变（[检查点、回退与分叉](https://daiw.net/manual/zcode/rewind-fork)）。
- 记忆抽取的触发条件按空白计词，一整句不带空格的中文只算一个词（[项目记忆](https://daiw.net/manual/zcode/memory)）。
- 进程内的 TUI 没有协议层的队列提升，运行中排进队列的输入之后是否会被执行，没能在代码里找到答案（[输入受理](https://daiw.net/manual/zcode/prompt-admission)）。

## 可迁移的经验

**把运行时做成进程，而不是库。** 终端、桌面、Web、手机都要驱动同一个 Agent 时，一条带幂等、快照与增量续传的线协议，比让每个宿主都去 import 运行时更好维护；ZCode 的产品一侧因此可以完全不依赖 Agent 一侧的任何包。

**受理与执行分开。** “这条输入能不能跑、跑在哪个回合里”应当在运行时内部原子地决定，外层只负责把它送进来；引导与排队在代码里是两条车道，界面才不会猜错用户的意图。

**长程任务的刹车要写成具体条件。** 与其给工具调用次数设上限，不如把刹车写成压缩熔断、续写次数、恢复次数与用户取消这样可解释的条件；每一条都有数字，出了问题知道是哪一道闸。

**把副作用写进文档。** `NOTICE.md` 逐项列出联网场景、数据位置与默认值的差异，连“没有操作系统沙箱”“凭据加密的密钥可以从本机信息派生”也写在明处，这比一句“注意安全”有用得多。

**让规则可以被机器检查。** `AGENTS.md` 写给编码 Agent 读，`architecture-policy.yaml` 把模块边界写成规则，formal-proof 把产品行为写成状态空间；它们的执行度参差不齐，但方向是对的：规则一旦能被脚本检查，就不再依赖人的记性。

## 这份源码的短板

- **没有测试可跑**：公开时拿掉了几乎全部测试与 spec 文档，读者无法靠跑测试来确认理解，本书许多“疑似”只能停在读代码的层面。
- **规矩与现状有距离**：400 行上限、I/O 只走 adapter、键盘优先、不靠错误文本判断，都在 `AGENTS.md` 里写着，代码里都有例外；架构检查几乎只对一个模块生效。
- **声明多于实现**：契约里有不少字段、事件类型与配置键没有读取方或产生方，读代码时得逐个确认它们是否真的生效。
- **开源版并不完整**：Computer Use 是占位包，手机远控的 relay 与手机端、若干官方插件的内容都不在仓库里，一些功能只能看到宿主这一侧。
- **没有开发历史**：仓库以一个提交整体公开，也没有 tag，读不到任何一个设计为什么变成今天这样，只能从注释里的“曾经”去拼。

## 继续读什么

- **想看另一种 TypeScript 生产实现**：[《OpenCode 源码解读》](https://daiw.net/manual/opencode)，以及在它之上二次开发的[《MiMo Code 源码解读》](https://daiw.net/manual/mimo-code)。
- **想看借用上游循环、自建运行时的做法**：[《MiniMax Code 源码解读》](https://daiw.net/manual/minimax-code)、[《Kimi Code 源码解读》](https://daiw.net/manual/kimi-code)。
- **想看 Rust 生产实现**：[《Grok Build 源码解读》](https://daiw.net/manual/grok-build)。
- **想从零理解 Agent 该做什么**：[《从 LLM 到 Coding Agent》](https://daiw.net/manual/llm-to-agent)。
- **想了解钩子与插件格式的来处**：[《Claude Code 中文手册》](https://daiw.net/manual/claude-code)、[《Codex 中文手册》](https://daiw.net/manual/codex)。
