Appearance
🐣 小a的分店难题
第 21 章小a学会了"字节流上怎么传消息"。但老z接着问:"光有协议不够——谁来实现服务器?谁来实现客户端?它们怎么协作?"
小a:"这不就是……一个服务器程序 + 一个客户端程序,通过 protocol 通信?"
老z:"对。但魔鬼在细节——服务器怎么管多个会话?状态谁是权威?怎么保证一致性? 这就是
pi-server和pi-client两个包干的事。"
22.1 两个包的定位
先看清楚谁干什么。
📄 源码证据
pi-server:"experimental server package for pi"pi-client:"Transport-neutral client for remote pi sessions over framed CBOR bytes"
🧙 分工:
包 角色 干什么 pi-protocol(第21章) 契约 定义"消息长什么样" pi-server 服务器 跑 Agent、管会话、发快照 pi-client 客户端 连服务器、收快照、发请求 三者关系:protocol 定义规则,server 实现,client 消费。 经典的三层分工。
⚠️ 注意:server 包标注 "experimental"(实验性)——这意味着它还在早期,API 可能变。但它的设计思想值得学。
22.2 PiServer:服务器的核心
服务器入口是 PiServer 类。
📄 源码证据
packages/server/src/server.ts:38typescriptexport class PiServer { private readonly sessions: LiveSessionManager; // 会话管理器 private readonly snapshots: ServerSnapshotPublisher; // 快照发布器 constructor(...) { this.sessions = new LiveSessionManager({ // ... 配置 broadcastServerSnapshot: () => void this.snapshots.broadcast(), }); this.snapshots = new ServerSnapshotPublisher({ // ... 配置 listSessions: (connection) => this.sessions.listSummaries(connection), }); } }
🧙 PiServer 由两个核心组件拼成:
LiveSessionManager:管"活着的会话"——创建、附加、列表ServerSnapshotPublisher:管"发快照"——把权威状态广播给客户端这俩协作:sessions 管会话,状态变了就通知 snapshots 广播。职责分离,各管一摊。
22.3 会话池:服务器怎么管多个会话
LiveSessionManager 是服务器的关键——它管理多个并发的 Agent 会话。
📄 源码证据
packages/server/src/sessions.ts:43(真实命令分派)typescriptexport class LiveSessionManager { private readonly liveSessions = new Map<string, LiveSession>(); private readonly openingSessions = new Map<string, Promise<LiveSession>>(); async executeCommand(connection: ConnectionState, command: Command) { switch (command.command) { case "list": return { command: "list" as const, sessions: await this.listSummaries(connection) }; case "create": { const id = randomUUID(); const live = await this.acquire(id, () => this.options.backend.createSession(options)); await this.attach(connection, live); return { command: "create" as const, session }; } case "attach": { const live = await this.acquire(command.sessionId, () => this.options.backend.openSession(command.sessionId), ); await this.attach(connection, live); return { command: "attach" as const, session }; } // ... 还有 detach / prompt / steer / abort / set_model / set_thinking } } }
🧙 三个细节,都是产品级工程:
- 命令分派:服务器收到的每一条命令(list/create/attach/detach/prompt/steer/abort/set_model/set_thinking),都走这一个
executeCommand入口——一个入口,集中处理,好加日志、好做鉴权。acquire去重:openingSessions这个 Map 存"正在打开中的会话"的 Promise——两个请求同时 attach 同一个会话,只真正打开一次,另一个等同一个 Promise。这就是"并发去重"。- create 用
randomUUID():会话 ID 不靠自增,而是 UUID——全局唯一,不会撞号。
🧙 会话池的概念:
服务器上可能同时跑好几个 Agent 会话——用户 A 在改项目 1,用户 B 在改项目 2。
LiveSessionManager管理这些会话的生命周期:
- 创建:
create命令 → 开一个新会话- 附加:
attach命令 → 连到已有会话(比如手机重连)- 列表:
list命令 → 看有哪些会话老z打比方:像酒店的房间管理——客人来开房(create)、回房(attach)、查房态(list)。多个会话互不干扰,各自独立。
为什么需要"附加"?
🐣 小a问:"attach 是干嘛的?为什么不是每次都新建?"
🧙 "因为会话要能恢复。 想象:你在电脑上开了个 Agent 改代码,出门了用手机连——手机应该附加到那个正在跑的会话,而不是开新的。attach 让多设备共享一个会话。"(呼应附录 G 的 Paseo,也是同样思路)
22.4 快照发布:服务器是唯一真相源
第 21 章讲了"snapshot 是权威的"。看服务器怎么发快照。
📄 源码证据
packages/server/src/snapshots.ts——ServerSnapshotPublisher
🧙 快照发布的逻辑:
- 服务器维护权威状态(每个会话的真实状态)
- 状态变化时,
ServerSnapshotPublisher把 snapshot 广播给所有连着的客户端- 客户端收到 snapshot,据此重建自己的本地状态
核心原则:服务器是唯一真相源。 客户端只是"显示器"——它不维护独立状态,一切以服务器的 snapshot 为准。
这就是第 21 章说的"snapshot authoritative"——服务器主动发权威状态,客户端被动接收重建。避免了"客户端和服务器状态不一致"的问题。
22.5 传输层:多种通道
服务器怎么"听"客户端连接?通过传输层。
📄 源码证据
packages/server/src/transports/transports/ └── unix/ ← Unix socket 传输
🧙 transport 抽象:
pi 把"通信通道"抽象成 transport——目前实现了 Unix socket(本机高性能通信)。但接口是抽象的,未来可加 TCP、WebSocket 等。
关键:protocol(第14章)不绑定具体传输。 同一套协议,能跑在 Unix socket、TCP、任何字节流通道上。这就是"transport 中立"——一套逻辑,多种通道。
客户端那边也一样:
📄 源码证据
packages/client/src/├── transport.ts ← 传输抽象 ├── unix.ts ← Unix socket 客户端实现 ├── client.ts ← 客户端主逻辑 ├── session-handle.ts ← 会话句柄 └── promise.ts ← 请求/响应 promise 化
22.6 客户端:连服务器、收快照、发请求
客户端要做的三件事:
🧙 客户端的核心流程:
- 连接:
client.ts通过 transport(unix.ts)连到服务器- 收快照:收到
session_snapshot→ 用它重建本地状态(state.ts)- 发请求:通过
session-handle.ts发命令(创建会话、发消息等)
promise.ts把"请求-响应"模式 promise 化——发个请求,等对应的响应,用 Promise 包装,代码更直观(不用回调地狱)。
22.7 整体协作:一次远程会话的全流程
把 server/client/protocol 串起来:
客户端(pi-client) 服务器(pi-server)
│ │
│ ① 通过 transport(unix)连接 │
│ ───────────────────────→ │
│ │
│ ② 发 hello(协议版本+token) │
│ ──[帧+CBOR,第14章]──→ │
│ │
│ ③ PiServer 校验 │
│ LiveSessionManager│
│ 找/建会话 │
│ │
│ ④ 发 session_snapshot(权威) │
│ ←──[帧+CBOR]────────── │
│ │
│ ⑤ 客户端重建状态(state.ts) │
│ │
│ ⑥ 发请求"帮我改 config.ts" │
│ ──[帧+CBOR]──→ │
│ ⑦ 服务器跑 Agent │
│ (agent-core) │
│ │
│ ⑧ 发 progress(瞬态,刷显示) │
│ ←──[帧+CBOR]── │
│ ... │
│ ⑨ 任务完成,发新 snapshot │
│ ←──[帧+CBOR]── │
│ │
│ ⑩ 客户端以新 snapshot 为准更新 │
└────────────────────────────────┘🧙 看这张图,记住协作的本质:
- protocol 提供通信契约(第14章)
- server 是权威,跑 Agent、发快照
- client 是显示器,收快照、发请求
- transport 是通道,可替换(unix/tcp/websocket)
22.8 代价
🧙 代价一:实验性
server 包标注 experimental,意味着不稳定、可能大改。生产用要谨慎。
代价二:复杂度
C/S 架构比本地 Agent 复杂得多——会话池、快照同步、连接管理、多客户端一致性。只有真需要"远程/多设备"才值得上。
代价三:延迟
本地 Agent 调用是即时的;远程要经过网络,有延迟。虽然 Unix socket 很快,但毕竟多一跳。
本章小结
🐣 小a的第二十二课
┌────── server/client 的协作 ──────┐ │ │ │ • 三层分工: │ │ protocol=契约 │ │ server=实现(跑Agent) │ │ client=消费(显示+请求) │ │ │ │ • PiServer 两大组件: │ │ LiveSessionManager(会话池) │ │ ServerSnapshotPublisher │ │ (发权威快照) │ │ │ │ • 会话池:create/attach/list │ │ → 支持多会话、多设备共享 │ │ │ │ • 服务器=唯一真相源: │ │ 发 snapshot,客户端重建 │ │ progress 只是刷显示 │ │ │ │ • transport 抽象: │ │ unix(已实现)/可扩展TCP等 │ │ → 一套协议,多种通道 │ │ │ │ • 代价:实验性 + 复杂 + 延迟 │ └─────────────────────────────────┘
关键认知:server/client 把本地 Agent 变成远程服务。核心是"服务器权威 + 客户端显示"——状态只有一份(服务器上),客户端只是镜像。这保证了多设备、多客户端的一致性。
课后实验
- 读 PiServer:打开
server.ts,看PiServer怎么把LiveSessionManager和ServerSnapshotPublisher组合起来。 - 理解 attach:读
sessions.ts的 attach 逻辑,想想"手机重连电脑上的会话"这个场景怎么实现。 - 画 C/S 时序图:根据 22.7 的流程,自己画一张客户端和服务器的消息时序图。
下一章:Agent 跑起来了,但它到底"行不行"?怎么评测? → 第 23 章 · pi-evals 源码