Skip to content

🐣 小a的分店难题

第 21 章小a学会了"字节流上怎么传消息"。但老z接着问:"光有协议不够——谁来实现服务器?谁来实现客户端?它们怎么协作?"

小a:"这不就是……一个服务器程序 + 一个客户端程序,通过 protocol 通信?"

老z:"对。但魔鬼在细节——服务器怎么管多个会话?状态谁是权威?怎么保证一致性? 这就是 pi-serverpi-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:38

typescript
export 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 由两个核心组件拼成:

  1. LiveSessionManager:管"活着的会话"——创建、附加、列表
  2. ServerSnapshotPublisher:管"发快照"——把权威状态广播给客户端

这俩协作:sessions 管会话,状态变了就通知 snapshots 广播。职责分离,各管一摊。


22.3 会话池:服务器怎么管多个会话

LiveSessionManager 是服务器的关键——它管理多个并发的 Agent 会话

📄 源码证据 packages/server/src/sessions.ts:43(真实命令分派)

typescript
export 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
    }
  }
}

🧙 三个细节,都是产品级工程:

  1. 命令分派:服务器收到的每一条命令(list/create/attach/detach/prompt/steer/abort/set_model/set_thinking),都走这一个 executeCommand 入口——一个入口,集中处理,好加日志、好做鉴权
  2. acquire 去重:openingSessions 这个 Map 存"正在打开中的会话"的 Promise——两个请求同时 attach 同一个会话,只真正打开一次,另一个等同一个 Promise。这就是"并发去重"。
  3. 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 客户端:连服务器、收快照、发请求

客户端要做的三件事:

🧙 客户端的核心流程:

  1. 连接:client.ts 通过 transport(unix.ts)连到服务器
  2. 收快照:收到 session_snapshot → 用它重建本地状态(state.ts)
  3. 发请求:通过 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 变成远程服务。核心是"服务器权威 + 客户端显示"——状态只有一份(服务器上),客户端只是镜像。这保证了多设备、多客户端的一致性。


课后实验

  1. 读 PiServer:打开 server.ts,看 PiServer 怎么把 LiveSessionManagerServerSnapshotPublisher 组合起来。
  2. 理解 attach:读 sessions.ts 的 attach 逻辑,想想"手机重连电脑上的会话"这个场景怎么实现。
  3. 画 C/S 时序图:根据 22.7 的流程,自己画一张客户端和服务器的消息时序图。

下一章:Agent 跑起来了,但它到底"行不行"?怎么评测? → 第 23 章 · pi-evals 源码