怎么读这份源码
怎样把 ZCode 的桌面端、Web 与 Agent CLI 分别跑起来,各层有哪些检查命令,开源仓库为何几乎不带测试、formal-proof 又是什么,两份 AGENTS.md 立了哪些规矩,以及一条沿 Agent 主线的阅读路线。
ZCode 的源码难读,不在算法,而在“两个项目叠在一起”:84 万行里有一半多是桌面端与 Web 的界面代码,Agent 运行时又拆成了上百个小文件;仓库以一个提交整体公开,没有开发历史可翻,测试也几乎没有带出来。好在它是一份写给 Agent 读的源码——约定写在 AGENTS.md 里,原因写在中文注释里。这一篇讲怎么把它跑起来、有哪些检查可做,以及怎样只沿着 Agent 这条主线读。
跑起来
工具版本以 mise.toml 为准:Node.js 24.14.0、pnpm 10.33.2。所有命令在仓库根目录执行,第一步是(README.md:27):
pnpm bootstrap它安装依赖、准备桌面端的本地运行资源,再串行构建各个包(scripts/bootstrap.mjs)。之后按要看的形态选入口:
| 目标 | 命令 | 说明 |
|---|---|---|
| 桌面应用 | pnpm dev:desktop | 默认等同 dev:desktop:prod,连生产服务;dev:desktop:test 连测试环境 |
| Web 与后端 | pnpm dev:web | 同时起 Web 开发服务器(5173)与后端(3030),见 package.json:8 |
| Agent CLI(源码) | pnpm --filter @zcode/cli dev | 即 tsx src/main.ts,直接跑 TypeScript 源码 |
| Agent CLI(构建产物) | pnpm --filter @zcode/cli... build | 产出 apps/zcode-cli/packages/cli/dist/zcode.cjs,用 node 运行 |
| 命令行发行包 | pnpm build:zcode | 打出含 TUI、Web 与 Agent 的整包,需要先配 ZCODE_DIST_BASE_URL |
读 Agent 的代码,最快的反馈回路是第三行:改完直接 pnpm --filter @zcode/cli dev,不必构建。注意 --filter @zcode/cli... 末尾的三个点表示“连同它依赖的 workspace 包一起构建”,桌面端与 Web 用的 Agent 就是这份构建产物,改了 Agent 代码要重新构建再重启服务(README.md:82)。
数据目录要当心混用。桌面端可以用 ZCODE_DATA_BASE_DIR 换一个独立的数据基目录(README.md:59);独立 CLI 的会话库、配置与日志则直接放在用户主目录下的 ~/.zcode/cli,会话库路径写死为 join(homedir(), ".zcode", "cli", "db", "db.sqlite")(apps/zcode-cli/packages/adapters/src/storage/session-store/paths.ts:7)。想让调试会话和日常会话互不干扰,最省事的办法是临时换一个 HOME 再运行。跑之前可以先 zcode doctor 看一眼运行时假设:版本、Node、平台、是否是 SEA 单文件(apps/zcode-cli/packages/cli/src/run.ts:188)。
检查命令
根目录与 CLI 各有一套检查,而且互不覆盖:
| 范围 | 命令 | 做什么 |
|---|---|---|
| 产品一侧 | pnpm typecheck | tsc -b 逐个检查 packages/ 下的包,不含 apps/zcode-cli(package.json:29) |
| 产品一侧 | pnpm lint、pnpm fmt:check | oxlint 与 oxfmt;oxlint 的忽略列表里有整个 apps/zcode-cli(.oxlintrc.json:63) |
| 全仓 | pnpm architecture:check --changed | 按 architecture-policy.yaml 检查模块边界,见上一篇 |
| 全仓 | pnpm knip、pnpm dep:refs --list-exports <file> | 找未使用的依赖与导出、查一个导出被谁引用 |
| 全仓 | pnpm verify:pre-push | lint 加变更范围内的架构检查(package.json:19) |
| Agent 一侧 | pnpm --dir apps/zcode-cli typecheck、lint | 经 turbo 在各子包里跑 tsc --noEmit 与 oxlint |
| Agent 一侧 | pnpm --dir apps/zcode-cli check | 先确认 Bash 命令注册表与生成脚本一致,再做类型检查(apps/zcode-cli/package.json:10) |
根 AGENTS.md 还要求开工前先跑 node scripts/check-workspace-freshness.mjs(AGENTS.md:10):本地分支落后远端就直接失败。脚本开头的注释交代了来由——本地的 zcode-cua 仓库曾落后主线 140 个提交,还有人在上面开工(scripts/check-workspace-freshness.mjs:5)。对只读源码的人,这一步可以跳过。
测试去哪了
AGENTS.md 把测试看得很重:“有行为改动时先补充对应测试;交互改动需要 E2E 场景”(AGENTS.md:42),CLI 那份也说“测试 case 很关键”(apps/zcode-cli/AGENTS.md:8)。可开源仓库里,全仓只有 4 个测试文件、631 行,分别在 packages/services/test/ 与 packages/ui/test/,所有 package.json 里也找不到一个测试脚本。
测试是在公开时被拿掉的,痕迹还在:dynamic-workflow 的 README 用了两节介绍 tests/workflows/ 与 tests/graphs/ 的夹具测试(apps/zcode-cli/packages/dynamic-workflow/README.md:82),目录本身却不存在;桌面端还留着 E2E 覆盖率采集(packages/desktop/src/main/e2eCoverage.ts)和只在 E2E 运行时才打开的测试桥(packages/shared/src/e2e-test-bridge.ts:10);AGENTS.md 反复要求的 spec 文档也一样没有公开,config/README.md:28 指向的 docs/ui/… 在仓库里并不存在。读代码时要记住这一点:你无法靠跑测试来验证理解,只能靠读。
formal-proof:把产品行为画成状态空间
有一件测试相关的工具倒是留了下来,而且相当少见。packages/formal-proof 的 README 说它是“产品行为状态空间枚举器”,聚焦对话场景里压缩、分叉、目标、消息队列与编辑提问的组合(packages/formal-proof/README.md:3)。它把会话抽象成几个维度(packages/formal-proof/src/model.ts:1):
export type RunPhase = "idle" | "running" | "completed" | "compacting" | "goalVerifying";
export type QueueState = "empty" | "text" | "goal" | "compact" | "mixed";
export type CompactMemory = "never" | "compactable" | "justCompacted" | "notNeeded";
export type GoalState = "none" | "active" | "verifying" | "verified" | "failed";
export type TurnTarget = "latest" | "old" | "none";
export type CandidateKind = "user" | "system";
// held 状态输入不静默入队,
// 由用户选择「清空 queue 后发送 / 保留 queue 立即发送」。
export type DecisionKind = "allow" | "reject" | "enqueue" | "choice" | "system" | "undefined";再枚举“在某个状态下来了某种输入”的全部组合,给每个组合一个裁决:放行、拒绝、入队、让用户选、交给系统,或者“未定义”——最后一类正是要人去补规则的地方。pnpm --filter @zcode/formal-proof dev 会在本机 4176 端口起一个用 d3 画的可视化界面。这张表不只是文档:ZCode Protocol 的受理规则声称与它“逐条对齐”(apps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/projection-state.ts:115),注释里提到的黄金测试 formal-proof-consistency 同样没有公开。协议怎样用这张表受理输入,见 ZCode Protocol V4。
AGENTS.md 立的规矩
仓库有两份 AGENTS.md:根目录那份管全仓,apps/zcode-cli/AGENTS.md 补充 CLI 的规则。它们写给在这里干活的编码 Agent,也是最好的读者指南。挑对读代码最有用的几条:
| 规矩 | 出处 | 读代码时的体现 |
|---|---|---|
| 先写 spec,明确状态所有者、接口与验收场景 | AGENTS.md:3 | 注释里常见“唯一所有者”“单一写入路径”的说法 |
| 避免重复状态和多条写入路径,不用超时掩盖同步问题 | AGENTS.md:41 | 队列、快照、租约都有明确的所有者 |
| 长程任务优先,不以工具调用次数硬停 | apps/zcode-cli/AGENTS.md:11 | 回合循环里没有步数上限 |
| 单文件默认不超过 400 行 | apps/zcode-cli/AGENTS.md:12 | CLI 代码拆得很碎,但全仓仍有 478 个文件超标 |
| 常量提取为命名常量 | apps/zcode-cli/AGENTS.md:13 | 超时、上限几乎都是具名常量,搜常量名就能找到数字 |
| 外部 I/O 收敛到 adapter | apps/zcode-cli/AGENTS.md:53 | core 依赖端口,adapters 做实现 |
| 每个工具声明只读、破坏性、并发安全、超时等 | apps/zcode-cli/AGENTS.md:62 | 见工具契约 |
| 大体积工具结果落盘,只回灌摘要与引用 | apps/zcode-cli/AGENTS.md:65 | 见执行器 |
| TUI 不保存业务状态 | apps/zcode-cli/AGENTS.md:71 | 见终端界面 |
所有任务携带可传播的 traceId | apps/zcode-cli/AGENTS.md:74 | 见遥测与调试 |
兼容 .agents 协议与 AGENTS.md | apps/zcode-cli/AGENTS.md:80 | 技能目录同时认 .zcode/skills 与 .agents/skills |
| 错误默认向上冒泡,不靠错误文本做判断 | apps/zcode-cli/AGENTS.md:87 | 错误多带结构化的 code,而不是字符串匹配 |
| 修 bug 时用中文注释写明原因和依据 | AGENTS.md:43 | 见下一节 |
几个读码的小窍门
先读注释。 “修 bug 要写原因”这条执行得很认真:core/src 里 5426 行注释,有 4023 行是中文,大多在解释“为什么”而不是“做什么”——为什么某个判断放在这一步之前、这里曾经出过什么问题。比如入口文件里的一句(apps/zcode-cli/packages/cli/src/main.ts:30):
// app-server/agent-server 的 stdout 是严格的 ZCode Protocol 帧通道,三方 SDK 的
// console.debug 等普通输出不能直接写入 stdout。必须在加载 run/bootstrap 之前将
// 进程级 console 统一引导到 stderr,否则任意依赖的一行普通日志都会触发传输层 JSON 解析崩溃。认名字。 同一个概念常有几种写法:
| 名字 | 指什么 |
|---|---|
| V4、legacy | ZCode Protocol 的第四版与旧版,两套实现分别在 bootstrap/src/zcode-protocol-v4 与 zcode-protocol |
| dwf | dynamic workflow,动态工作流 |
| expert | 专家工作流,/expert 命令 |
| target、goal | 目标模式,CLI 参数叫 --target,斜杠命令叫 /goal,两者是别名 |
| mcs | mid-conversation system,会话中途插入的系统消息(--force-mcs) |
| cua | Computer Use,开源版是占位实现 |
| surface | 呈现面:terminal 或 zcode_desktop,决定提示词与交互的差异(run.ts:146) |
| off-peak | 闲时:闲时计划与闲时任务 |
顺着端口读。 core 需要的外部能力都声明在 apps/zcode-cli/packages/contracts/src/interfaces/ 下,AgentRuntime 的构造依赖集中在 AgentRuntimeDeps 里。想知道运行时能碰外部世界的哪些东西,看这两处就够了;想知道某个端口怎么实现,再去 adapters 找同名文件。
先分清两条路。 TUI 在进程内调用 createZCodeApp(apps/zcode-cli/packages/bootstrap/src/app/create-app.ts:146);桌面端与 Web 走 app-server,入口是 runZCodeProtocolAgent(apps/zcode-cli/packages/bootstrap/src/index.ts:68)。两条路最终都落到同一个 AgentRuntime,但前者经过的是 bootstrap/src/app/,后者经过的是 bootstrap/src/zcode-protocol-v4/,读错了路就会在别人的分支里打转。
推荐阅读路线
| 顺序 | 从哪读起 | 对应篇目 |
|---|---|---|
| 1 | apps/zcode-cli/packages/cli/src/main.ts、run.ts、arguments.ts | 命令行入口 |
| 2 | bootstrap/src/app/create-app.ts、adapters/src/config/ | bootstrap |
| 3 | core/src/runtime/agent-runtime.ts、runtime/methods/index.ts | AgentRuntime |
| 4 | runtime/methods/prompt-admission.ts、runtime/command-queue.ts | 输入受理 |
| 5 | runtime/methods/turn.ts、turn-loop.ts、turn-model-step.ts | 回合循环、一次模型请求 |
| 6 | core/src/tool/executor/、tool/handlers/index.ts | 工具契约、执行器 |
| 7 | adapters/src/model/ | 模型适配层 |
| 8 | bootstrap/src/zcode-protocol-v4/、packages/shared/src/zcode-protocol-v4/ | ZCode Protocol V4 |
| 9 | packages/services/src/zcode-agent/、packages/desktop/src/ | 桌面应用 |
第 1 到第 7 步正是下一篇要走的路:一条用户输入从终端出发,进入运行时,经过模型与工具,再以事件的形式回到屏幕;桌面端的那条路则在第 8、9 步汇合。
下一篇:一条消息的旅程——从回车到屏幕,一条输入在 TUI 与桌面端两条路上各经过哪些层,模型与工具的结果又怎样以事件流回界面。