Axl架构全景图:protocol、kernel、daemon、runtime分层如何保证客户端只是窗口
Axl架构全景图:protocol、kernel、daemon、runtime分层如何保证客户端只是窗口
Axl 是一个 local-first 的 AI 编码智能体(agent harness):单个 daemon 进程独占你的编码会话,终端、浏览器、SDK 客户端都只是它身上的"窗口"。本文带你拆解 Axl 架构中 protocol、kernel、daemon、runtime 四个核心包的分层职责,看懂客户端为什么不可能越权。
为什么"客户端只是窗口"是 Axl 的架构起点
大多数 agent 工具把会话绑死在启动它的客户端上:关掉终端,工作就停;打开浏览器,又多了一个各自为政的 agent,各自维护循环、工具和历史。
Axl 反其道而行(见 README.md):
One authority per session. 单个 daemon 拥有模型循环、工具、策略和规范事件历史。所有客户端渲染同一状态、提交意图,没有任何一个是独立 agent。
这个目标不是靠约定,而是靠严格的包边界 + CI 强制检查实现的。下面按层拆解。
Axl 架构全景:一图看懂五层分工
flowchart LR
subgraph Clients[Presentation clients]
TUI[Terminal UI]
Web[Web UI]
Yours[Your own client]
end
SDK[TypeScript SDK<br/>retry, cursors, projections]
Protocol[Protocol<br/>events, RPCs, validation]
subgraph Authority[Authoritative daemon process]
Daemon[Daemon<br/>sessions, presence, workspace RPCs]
Kernel[Kernel<br/>JSONL, agent loop, tools, policy]
AI[AI providers]
Extensions[Extensions<br/>prompts, Skills, MCP]
Sandbox[Sandbox<br/>native and OCI]
end
TUI --> SDK
Web --> SDK
Yours --> SDK
SDK -->|typed RPC and events| Daemon
SDK -. validates with .-> Protocol
Daemon --> Kernel
AI -. model port .-> Kernel
Extensions -. public extension API .-> Kernel
Sandbox -. command execution .-> Kernel
各包职责一览(完整说明见 CODE_STRUCTURE.md):
| 包 | 角色 | 依赖约束 |
|---|---|---|
packages/protocol |
事件、RPC、capability、校验的唯一定义 | 零运行时依赖 |
packages/kernel |
事件日志、agent loop、工具、策略 | 仅依赖 protocol + Node.js 内置 |
packages/daemon |
会话、操作协调、订阅、presence | 通过内核公开导出使用 kernel |
packages/runtime |
组装 provider、工具、沙箱并启动 daemon | 不导入任何展示客户端 |
packages/sdk |
类型化客户端:重连、游标、投影 | 只连接 daemon,永不启动 daemon |
protocol:零依赖的契约层,锁定 wire 格式
packages/protocol 是整个系统的"宪法":
- 定义版本化的 JSONL 事件、模型流消息和本地 wire 协议(当前 wire 版本 30,Unix socket 上的换行分隔 JSON)
- 没有任何运行时依赖,客户端和 daemon 都引用它做校验,杜绝"各说各话"
- 运行时解析器校验每一个来自不可信边界的值
核心类型定义在 canonical-event.ts,协议细节(握手、capability 协商、游标、快照分页)规范在 web-protocol.md。
关键点:客户端想"伪造"一个事件?它拿不到 kernel 的追加通道,而 protocol 的运行时校验会在边界上直接拒绝非法值。
kernel:agent 循环与追加式事件日志的确定性核心
packages/kernel 是 Axl 的确定性核心(agent-session.ts):
- 追加式 JSONL 历史:模型看到的每个操作都落盘为一条完整事件,先追加规范事件、再变更任何派生状态——崩溃后可恢复、可重放、可审计
- agent loop 与规范工具:标准工具集为
read、write、edit、bash、web_fetch、web_search - 安全策略:路径规范化、符号链接逃逸拒绝、密钥脱敏、web 工具固定公共 DNS 并拒绝私网目标
而 provider 细节(35+ 模型商的 API 方言、凭证)被刻意挡在 kernel 之外,放在 packages/ai(见 packages/ai/README.md)。kernel 的唯一依赖是 @axl/protocol 和 Node.js 内置模块——这由 CI 强制,见 decisions.md。
daemon:会话的唯一权威,校验、策略与分发
packages/daemon(daemon.ts)是权威进程:
- 拥有会话、agent loop 实例、事件日志和进行中的操作;客户端经本地 Unix socket 连接,分页加载历史、订阅实时事件流
- 每个操作都先校验 capability、策略和操作归属,再由 kernel 执行
- 持久命令日志保证幂等重试:响应丢失后 SDK 重放同一请求,daemon 返回原始结果而非重复执行副作用
- 订阅、presence、快照边界与游标确认,保证多个客户端投影收敛到同一状态
换句话说:终端里按回车,浏览器里看到的"助手正在写代码",是同一个规范事件的投影。
runtime:组装 provider、工具与沙箱的"装配工厂"
packages/runtime(local-runtime.ts)负责"开机":
- 选择沙箱后端:native(Bubblewrap/Landlock/seccomp、Seatbelt)、rootless Podman/Docker,或 unsafe(明确打标、独立状态)
- 构造 provider 与规范工具,加载 Agent Skills 和 MCP 服务器
- 跨本地 placement 发现会话历史,启动权威 daemon
README 强调:presentation clients must not be imported here——装配侧从结构上就不认识任何 UI 代码,终端、Web、headless 客户端只能走 daemon 协议附着,不能各自"另起炉灶"。
SDK 与客户端:TUI 和 Web 凭什么只是"窗口"
packages/sdk(client.ts)是客户端与 daemon 之间的受支持边界,它拥有:类型化 RPC、协议版本精确匹配、capability 协商、幂等键与重试、心跳与重连、分页快照、有序事件确认、游标恢复,以及确定性的会话投影器。
但它不拥有:会话与规范历史、模型循环、工具、提示队列、压缩、策略。SDK 的投影只是 daemon 规范事件的可丢弃视图——客户端缓存丢了、坏了,最多重新拉一份权威快照。
而 TUI(packages/tui)和 Web(packages/web)允许拥有的只是:草稿与编辑器状态、布局与对话框、主题与通知;禁止做的清单写在 client-boundaries.md:不得追加或伪造规范事件、不得跑 agent loop、不得本地实现提示队列/压缩/并发规则、不得决定文件系统、Git、网络、模型、凭证或沙箱策略。
甚至"关闭 daemon"这样的动作,TUI 也只是渲染 daemon 的关闭预览并提交确认——它从不直接发信号杀进程。
一次请求的旅程:从用户意图到全端刷新
每个操作走完全相同的路径:
| 步骤 | 发生地 |
|---|---|
| ① 客户端提交类型化用户意图 | SDK(TUI / Web / 你的客户端) |
| ② 校验 capability、策略、操作归属 | daemon |
| ③ 执行操作(模型调用、工具执行) | kernel + ai + sandbox |
| ④ 追加规范事件,先于任何派生状态变更 | kernel 事件日志 |
| ⑤ 每个订阅客户端收到并投影同一事件 | SDK projector |
这也解释了 Axl 的"会话韧性":/detach 只断开当前客户端,daemon 继续干活;重连时先给冻结的分页快照,再接上带确认的实时流,缺口自动检测并从权威快照修复。
如何保证?CI 强制的依赖边界
分层若只写在文档里,迟早会被"顺手 import"破坏。Axl 的做法是把边界变成构建门禁:
- 展示包运行时禁止依赖
kernel、daemon、runtime、ai、sandbox pnpm check中的边界检查(check-boundaries.ts)在 CI 里扫描跨包导入,越界即失败- 多客户端一致性由共享协议与 SDK 一致性测试证明:两个客户端订阅同一会话必须收敛到同一投影;断开重连不得丢失或重复规范事件;detach 一个客户端不得中断 daemon 的工作
依赖规则与包归属的完整定义见 CODE_STRUCTURE.md 第 3 节,权威模型见 client-boundaries.md。
小结:窗口架构的三层保险
Axl 让"客户端只是窗口"成立,靠的是层层递进的约束:
- 契约层:protocol 零依赖、wire 格式唯一事实源,非法值在边界被校验拒绝;
- 能力层:kernel 只暴露公开导出,daemon 独占会话权威与事件追加通道,SDK 投影是可丢弃视图;
- 工程层:runtime 不导入 UI,CI 边界检查把"客户端不持有循环、工具、策略、历史"变成硬性门禁。
结果就是开篇那句话:关掉终端,会话照常运行;换任何客户端接入,看到的都是同一个真相。想深入某一层,可直接从 docs/architecture/ 目录下的对应文档读起。