Appearance
4.2 客户端与连接
@pi/client 是远程会话的传输无关客户端。它把"串行化的 Agent 会话"重新变成好用的对象 API。这一节讲 PiClient 如何管理连接、请求响应、以及会话句柄。
PiClient 总览
packages/client/src/client.ts:51 的 PiClient 是客户端入口。它由几个内部组件组成:
PiClient
├── Connection 底层连接(握手、收发、状态)
├── ClientState 维护服务端快照(会话列表、模型列表)
├── pendingRequests id → 请求映射(匹配响应)
└── sessionLeases 会话租约管理(attach/detach 的所有权)核心抽象让客户端不关心传输细节:transportFactory 把 Client 和一个传输(TCP/Unix socket)连起来,Connection 负责定帧收发(基于 @pi/protocol)。
连接与握手
PiClient.connect() → Connection 建立底层传输 → 发送 ClientHello → 收到 ServerHello:
ts
static async connect(options: PiClientOptions): Promise<PiClient> {
const client = new PiClient(options);
await client.connect();
return client;
}握手成功后,Connection 把初始 ServerSnapshot 交给 ClientState.applyServerSnapshot(),客户端立刻知道有哪些会话、哪些模型。
请求-响应:如何匹配
PiClient 用 #requestSequence 自增 id,把每个请求存进 #pendingRequests:
ts
interface PendingRequest { command: Command; resolve; reject; }ts
async #request(command: Command): Promise<CommandResult> {
const id = String(++this.#requestSequence);
this.#pendingRequests.set(id, pending);
this.#connection.send(encodeClientMessage({ type: "request", id, request: command }));
// 收到对应 id 的 response 时 resolve / reject
}服务端回的 ResponseEnvelope 带相同 id,客户端据此把结果交给对应请求。即使请求乱序返回也能正确配对。
状态管理:ClientState
packages/client/src/state.ts 的 ClientState 维护客户端视角的权威状态:
- 会话列表:来自
ServerSnapshot.sessions。 - 模型列表:来自
ServerSnapshot.models。 - 事件分发:把
session_snapshot、session_progress、session_removed等事件分发给订阅者。
客户端遵守"快照重建 + 增量更新"策略:收到快照就重建视图,收到增量就局部打补丁。
会话句柄:PiSessionHandle
客户端不直接操作会话,而是通过 SessionHandle(packages/client/src/session-handle.ts):
ts
const handle = client.acquireSession({...}); // 获取会话租约
await handle.prompt("你好"); // 发起提问
await handle.steer("插句话"); // 中途插话
await handle.abort(); // 中止
handle.onEvent((event) => { ... }); // 订阅会话事件会话租约(Session Lease)
PiClient 用租约控制会话所有权:
- 一个会话同一时刻可能被多个客户端"看到",但活跃写操作需要租约。
- 服务端配合返回
session_locked错误(见 4.1)。 - 客户端内部用
#sessionLeaseCounts、#sessionAttachments等管理 attach/detach 的异步编排,处理重连、恢复等复杂场景。
事件订阅
订阅服务端推来的事件。onEvent 订阅的是 @pi/protocol 的 ServerEvent 联合类型(schemas.ts:395),共 4 种:
ts
export type ServerEvent =
| { type: "server_snapshot"; snapshot: ServerSnapshot } // ① 全局快照
| { type: "session_snapshot"; snapshot: SessionSnapshot } // ② 单会话快照
| { type: "session_progress"; sessionId; progress: TranscriptProgress } // ③ 增量
| { type: "session_removed"; sessionId } // ④ 会话被移除| 事件 | 携带内容 | 触发时机 | 客户端怎么用 |
|---|---|---|---|
server_snapshot | 全局快照(会话列表 + 模型 + revision) | 会话列表有增减时广播 | 重建"有哪些会话"的整体视图 |
session_snapshot | 单个会话的权威快照(完整转录) | 会话状态大改(prompt/steer/abort 完成) | 重建某个会话的完整视图 |
session_progress | 增量(转录条目变化) | Agent 边跑边推(模型吐字、工具执行) | 局部更新,实现实时效果 |
session_removed | 会话 id | 会话被删除 | 移除本地视图 |
ts
// 全局订阅:所有事件
client.onEvent((event) => {
switch (event.type) {
case "server_snapshot": ... // 会话列表变了
case "session_snapshot": ... // 某会话全貌变了
case "session_progress": ... // 某会话有增量
case "session_removed": ... // 某会话被删
}
});会话级订阅:也可以用 SessionHandle.onEvent 只关注某个会话:
ts
handle.onEvent((event) => { ... }); // 只收到该 sessionId 的事件其中 session_progress 的 progress 又分 4 种(schemas.ts:204):
ts
TranscriptProgress =
| { type: "item_started"; item } // 新条目开始(模型开始答/工具开始跑)
| { type: "assistant_delta"; messageId; contentIndex; kind; delta } // 助手文本/思考增量
| { type: "item_updated"; item } // 条目更新(如工具 running→complete)
| { type: "item_finished"; item } // 条目结束一句话:
onEvent订阅 4 种ServerEvent(全局快照 / 单会话快照 / 增量 / 删除),靠"快照重建 + 增量局部更新"维护视图。
与"本地 Agent"的关系
PiClient + PiServer 组合,让同一套 Agent 会话既能在本地跑,也能被远程终端连接:
- 本地:
coding-agent直接持有Agent实例。 - 远程:
coding-agent作为 server 的 backend,PiServer暴露协议接口,TUI 通过PiClient连接。
对上层而言,本地和远程的 API 保持一致(都是 prompt / steer / abort / 事件订阅),这就是"传输无关"的价值。
小结
PiClient= Connection + ClientState + 请求匹配 + 会话租约。- 请求用自增 id 与响应配对,支持乱序。
- 快照重建 + 增量更新维护客户端状态。
- 会话通过租约句柄操作,所有权受服务器约束。
真实源码位置
PiClient:packages/client/src/client.ts:51- 请求匹配:
packages/client/src/client.ts:55 ClientState:packages/client/src/state.tsSessionHandle:packages/client/src/session-handle.ts
下一步看 Demo:最小远程会话。