第 1 章 · 02 "薄 harness"哲学与顶层布局 本节摘要:本节把上一节的六条原则落到代码与目录。"薄 harness"(thin harness)的精确定义是——核心只懂 interface,具体模型和工具靠注册表按名字解析、在 config 中声明、由插件注入,核心不持有任何具体类型的硬编码分支。随后巡视 Reasonix 的顶层布局( 入口与 blank-import 自注册、 40+ 内核包、 第三方 sidecar、 Wails+React、 Cloudflare Workers),理解一个二进制如何装下这么多能力。最后与 Claude Code 做一次定位对比,并预告贯穿后五章的主线——"三前端共用单 Controller"。
本节摘要:本节把上一节的六条原则落到代码与目录。"薄 harness"(thin harness)的精确定义是——核心只懂 interface,具体模型和工具靠注册表按名字解析、在 config 中声明、由插件注入,核心不持有任何具体类型的硬编码分支。随后巡视 Reasonix 的顶层布局(
cmd/入口与 blank-import 自注册、internal/40+ 内核包、sdk/go第三方 sidecar、desktopWails+React、workersCloudflare Workers),理解一个二进制如何装下这么多能力。最后与 Claude Code 做一次定位对比,并预告贯穿后五章的主线——"三前端共用单 Controller"。
内容来源:原项目源码
docs/SPEC.md§2 Layout、cmd/reasonix/main.go、REASONIX.md(Conventions)、internal/目录树。
⚠️ 注意:"薄 harness"不是"功能少"。Reasonix 内核 Go 代码约 16.5 万行、40+ 包,桌面 React 约 12.6 万行。"薄"指的是核心抽象的薄——核心不认具体模型/工具,只认 interface;具体能力靠注册表按名注入。
阅读完本节,你应当能够:
switch model)。cmd/reasonix/main.go 中四行 blank-import 的作用。internal/ 至少 8 个核心包及其职责。sdk/go、desktop、workers 三者与内核的关系。上一节讲了六条原则,本节把"配置与插件驱动核心"翻译成可操作的代码定义。"薄 harness"指的是:
provider.New(kind, cfg)、tool.Registry.Get(name))。[[providers]] kind = "openai"),实现来自 init() 自注册或运行时插件。switch modelName { case "deepseek": ... } 这样的分支。反例一眼能认出来:如果某天你在 internal/agent/ 里看到 if provider == "deepseek" { ... },那就是破坏了"薄 harness"——这个分支应该被一次 config 编辑或一个 provider 实现替换掉。
💡 契约要点:SPEC §1.1 原文"No hardcoded
switch model"不是修辞,而是 code review 的检查项。"薄"是核心抽象的薄,不是代码量的薄。
SPEC §2 给出顶层目录(docs/SPEC.md:28-52),精简后如下:
reasonix/ ├── go.mod / go.sum # module reasonix; require BurntSushi/toml ├── Makefile # build / cross / vet / fmt / test ├── reasonix.example.toml # 配置示例(260 行,第 2 章精读) ├── docs/SPEC.md # 工程契约 ├── cmd/ │ ├── reasonix/ # 入口;blank-import 内置 provider/tool │ ├── reasonix-plugin-example/ # MCP stdio 插件参考实现 │ └── ...(launcher/migrator/signpath/e2ebench) ├── internal/ # 内核(40+ 包) ├── sdk/go/ # 第三方 Go 编写 sidecar 的 SDK ├── desktop/ # Wails + React 桌面应用 └── workers/ # Cloudflare Workers(accounts/forum/crash-report)
下面挑三个最关键的目录展开。
入口文件 cmd/reasonix/main.go 极简,核心只有十几行。最关键的是这四行 blank-import(cmd/reasonix/main.go:10-17):
// Blank imports wire compile-time built-ins into their registries. _ "reasonix/internal/provider/anthropic" _ "reasonix/internal/provider/openai" _ "reasonix/internal/provider/responses" _ "reasonix/internal/tool/builtin"
下划线 _ 表示"只为副作用而 import"。副作用就是这些包的 init() 函数会执行,把各自的 provider/tool 注册到全局注册表:
internal/provider/openai 的 init() 调用 provider.Register("openai", New)(internal/provider/openai/openai.go:57-58)。internal/provider/anthropic 注册 "anthropic" kind。internal/provider/responses 注册 "responses" 和 "dashscope-responses" 两个 kind。internal/tool/builtin 整个包的多个 init()(每个工具文件一个)注册全部内置工具。这是 Go 实现"插件自注册"的经典手法。核心(internal/agent、internal/cli)从不 import 这些子包,只 import internal/provider 和 internal/tool 这两个父包(里面只有 interface 和注册表)。依赖方向因此是单向的:子包 import 父包自注册,父包永不 import 子包。第 2 章第 1 节会专门讲这条"依赖方向无环"约束。
cmd/reasonix-plugin-example 是一个可运行的 MCP stdio 插件参考实现(实现了 echo、wordcount 两个工具),给第三方写插件做样板。
internal/ 是 Reasonix 的内核,约 40+ 包。下表按职责分组列出最核心的包(完整目录在源码 internal/ 下):
| 包 | 职责 | 章节 |
|---|---|---|
cli |
子命令路由、flag、装配、退出码 | 第 2、8 章 |
config |
TOML 加载(四级覆盖) | 第 2 章 |
provider |
Provider interface + 类型 + kind→factory 注册表 | 第 3 章 |
tool |
Tool interface + Registry | 第 4 章 |
permission |
每次调用的 Policy(allow/ask/deny→Decision) | 第 5 章 |
plugin |
stdio JSON-RPC(MCP)客户端;适配远端工具 | 第 7 章 |
extension |
Extension Protocol v1 sidecar | 第 7 章 |
agent |
Session + harness loop + fleet/coordinator | 第 5、9 章 |
control |
传输无关 Controller(三前端共享) | 第 8 章 |
boot |
系统提示装配(缓存稳定前缀) | 第 6 章 |
serve |
HTTP/SSE 前端 | 第 8、10 章 |
bot |
IM 机器人(飞书/QQ/微信) | 第 10 章 |
remote |
SSH Remote-SSH(forward/sftpfs/bootstrap) | 第 10 章 |
sandbox |
OS 级沙箱(Seatbelt/bubblewrap) | 第 5、9 章 |
memory / history |
长期记忆 / 会话历史检索 | 第 6 章 |
skill |
可调用 playbook(Skill 文件) | 第 9 章 |
注意 internal/ 还有一批"工具型"小包:shellparse/shellrun/shellsafe(shell 解析与安全)、frontmatter(Skill/命令前页解析)、diff(文件变更)、i18n(双语)、gitcmd、lsp(语言服务器)等。这些小包体现了原则 6"演进不过度工程"——每个包只做一件事,长出来才独立成包。
💡 契约要点:
internal/每个包都拥有一个 package comment 说明自己负责的 concern(见REASONIX.mdConventions 第一条)。编辑代码时"匹配周围注释密度与习惯"是项目明文约定。
sdk/go:第三方用 Go 编写 Extension sidecar 时使用的 SDK。它把 Extension Protocol v1 的 JSON 通信、事件拦截、Provider 注入封装成易用的 API。第 7 章详讲。
desktop:Wails(Go 后端 + Web 前端打包成桌面应用)+ React 19 的桌面客户端。后端 Go 约 11.1 万行,前端 React 约 12.6 万行,提供 8 套主题(graphite/aurora/slate/carbon/nocturne/amber 等)。桌面与 CLI/HTTP 共用同一个 internal/control.Controller,只是传输层不同。第 8、10 章详讲。
workers:Cloudflare Workers 部署的边缘服务,目前含 accounts(账户)、forum(社区论坛)、crash-report(崩溃上报)三个 worker。与内核解耦,独立部署。第 10 章发布部分提及。
三者都不在 CGO_ENABLED=0 单二进制的主路径上:sdk/go 是给别人的库,desktop 是另一个构建产物(Wails 打包),workers 是边缘 JS/TS。主二进制 cmd/reasonix 只包含 internal/。
REASONIX.md 的 Conventions 第二条(REASONIX.md:12-14)写下了一条全局约定:
One transport-agnostic
control.Controllersits behind every frontend (chat TUI, HTTP/SSE serve, Wails desktop). Add behavior to the controller, not a frontend, so all three inherit it.
翻译:一个传输无关的 control.Controller 位于所有前端之后(聊天 TUI、HTTP/SSE serve、Wails 桌面)。把行为加到 controller,而不是某个前端,这样三者自动继承。
这条约定是第 8 章的主题,本节先记住它的含义:
internal/control.Controller 不依赖任何具体传输(TTY、HTTP、WebSocket、IPC)。reasonix serve 的 HTTP/SSE、Wails 桌面——都把用户输入转给 Controller,Controller 把模型输出与工具事件转给前端。这条约定让 Reasonix 避免了"三个前端各写一遍逻辑"的常见陷阱,也是它能把这么多能力塞进一个项目的架构基石。
⚠️ 注意:
control.Controller是"传输无关",不是"线程安全无关"。多前端并发访问 Controller 时,前端层需自行处理并发;Controller 内部用sync保护自己的状态。
Reasonix 与 Claude Code 都是终端 AI coding agent,定位相近但有明显侧重:
| 维度 | Claude Code | Reasonix |
|---|---|---|
| 主模型 | Anthropic Claude | DeepSeek(首选),任何 OpenAI 兼容端点 |
| 协议 | 闭源 | MIT 开源 |
| 缓存优化 | Anthropic prompt cache | 围绕 DeepSeek prefix cache 调优(全书高潮★) |
| 插件协议 | MCP | MCP + Extension Protocol sidecar(两层) |
| 桌面 | 无(纯 CLI) | Wails + React 桌面客户端 |
| IM 集成 | 无 | 飞书/QQ/微信 bot |
| 社区 | Anthropic 官方 | 中国开发者社区(esengine 等) |
最本质的差异在第二行——Claude Code 闭源,Reasonix 开源,且把"prefix cache 友好的上下文维护"作为头号工程目标。这是第 6 章的主题,也是 Reasonix 区别于其他 coding agent 的灵魂。
另一个差异是 Reasonix 的"三前端共用单 Controller"——Claude Code 只有 CLI,没有多前端共享的问题;Reasonix 因为同时支持 CLI/HTTP/桌面/IM,被迫长出了 control.Controller 这层抽象,这是开源 + 多前端的副产品。
switch model。cmd/reasonix(入口 + 4 行 blank-import 自注册)、internal/(40+ 内核包)、sdk/go(sidecar SDK)、desktop(Wails+React)、workers(CF Workers)。_ "reasonix/internal/tool/builtin" 触发各 init() 调用 Register,父包永不 import 子包。internal/control.Controller 传输无关,新功能加在 Controller 三个前端自动继承。下一章我们正式进入配置体系——SPEC §2 的 Layout 与依赖方向无环是第一条硬约束,config 四级覆盖是第一条软约定。第 2 章共 2 节,先讲 Layout 与依赖方向,再精读
reasonix.example.toml260 行。