怎么读这份源码

怎样把 ZCode 的桌面端、Web 与 Agent CLI 分别跑起来,各层有哪些检查命令,开源仓库为何几乎不带测试、formal-proof 又是什么,两份 AGENTS.md 立了哪些规矩,以及一条沿 Agent 主线的阅读路线。

作者 David更新于 3 篇(共 47 篇)

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 devtsx 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 typechecktsc -b 逐个检查 packages/ 下的包,不含 apps/zcode-clipackage.json:29
产品一侧pnpm lintpnpm fmt:checkoxlint 与 oxfmt;oxlint 的忽略列表里有整个 apps/zcode-cli.oxlintrc.json:63
全仓pnpm architecture:check --changedarchitecture-policy.yaml 检查模块边界,见上一篇
全仓pnpm knippnpm dep:refs --list-exports <file>找未使用的依赖与导出、查一个导出被谁引用
全仓pnpm verify:pre-pushlint 加变更范围内的架构检查(package.json:19
Agent 一侧pnpm --dir apps/zcode-cli typechecklint经 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.mjsAGENTS.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:12CLI 代码拆得很碎,但全仓仍有 478 个文件超标
常量提取为命名常量apps/zcode-cli/AGENTS.md:13超时、上限几乎都是具名常量,搜常量名就能找到数字
外部 I/O 收敛到 adapterapps/zcode-cli/AGENTS.md:53core 依赖端口,adapters 做实现
每个工具声明只读、破坏性、并发安全、超时等apps/zcode-cli/AGENTS.md:62工具契约
大体积工具结果落盘,只回灌摘要与引用apps/zcode-cli/AGENTS.md:65执行器
TUI 不保存业务状态apps/zcode-cli/AGENTS.md:71终端界面
所有任务携带可传播的 traceIdapps/zcode-cli/AGENTS.md:74遥测与调试
兼容 .agents 协议与 AGENTS.mdapps/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、legacyZCode Protocol 的第四版与旧版,两套实现分别在 bootstrap/src/zcode-protocol-v4zcode-protocol
dwfdynamic workflow,动态工作流
expert专家工作流,/expert 命令
target、goal目标模式,CLI 参数叫 --target,斜杠命令叫 /goal,两者是别名
mcsmid-conversation system,会话中途插入的系统消息(--force-mcs
cuaComputer Use,开源版是占位实现
surface呈现面:terminalzcode_desktop,决定提示词与交互的差异(run.ts:146
off-peak闲时:闲时计划与闲时任务

顺着端口读。 core 需要的外部能力都声明在 apps/zcode-cli/packages/contracts/src/interfaces/ 下,AgentRuntime 的构造依赖集中在 AgentRuntimeDeps 里。想知道运行时能碰外部世界的哪些东西,看这两处就够了;想知道某个端口怎么实现,再去 adapters 找同名文件。

先分清两条路。 TUI 在进程内调用 createZCodeAppapps/zcode-cli/packages/bootstrap/src/app/create-app.ts:146);桌面端与 Web 走 app-server,入口是 runZCodeProtocolAgentapps/zcode-cli/packages/bootstrap/src/index.ts:68)。两条路最终都落到同一个 AgentRuntime,但前者经过的是 bootstrap/src/app/,后者经过的是 bootstrap/src/zcode-protocol-v4/,读错了路就会在别人的分支里打转。

推荐阅读路线

顺序从哪读起对应篇目
1apps/zcode-cli/packages/cli/src/main.tsrun.tsarguments.ts命令行入口
2bootstrap/src/app/create-app.tsadapters/src/config/bootstrap
3core/src/runtime/agent-runtime.tsruntime/methods/index.tsAgentRuntime
4runtime/methods/prompt-admission.tsruntime/command-queue.ts输入受理
5runtime/methods/turn.tsturn-loop.tsturn-model-step.ts回合循环一次模型请求
6core/src/tool/executor/tool/handlers/index.ts工具契约执行器
7adapters/src/model/模型适配层
8bootstrap/src/zcode-protocol-v4/packages/shared/src/zcode-protocol-v4/ZCode Protocol V4
9packages/services/src/zcode-agent/packages/desktop/src/桌面应用

第 1 到第 7 步正是下一篇要走的路:一条用户输入从终端出发,进入运行时,经过模型与工具,再以事件的形式回到屏幕;桌面端的那条路则在第 8、9 步汇合。

下一篇:一条消息的旅程——从回车到屏幕,一条输入在 TUI 与桌面端两条路上各经过哪些层,模型与工具的结果又怎样以事件流回界面。

本页目录