v1.1.0 Pi v1.1.0 源码分析 Analysis of Pi v1.1.0 · 2026-10-07
protocol protocol
远程 pi 会话的传输无关 CBOR 协议 Transport-neutral CBOR protocol for remote pi sessions
Run modes Infra
← → 翻页 · F 全屏 · O 目录 · T 明暗 · L 语言 ← → navigate · F fullscreen · O overview · T theme · L language
01 · 三层:信封、编解码、分帧 01 · Three layers: envelopes, codec, framing Run modes
三层:信封、编解码、分帧 Three layers: envelopes, codec, framing pi-protocol: 869 lines in three layers — envelopes, codec, framing packages/protocol/src cbor/ 依赖与使用者 Dependencies and users 22 index.ts 唯一导入路径 ".":cbor、codec、framing 全部导出,protocol.ts 导出类型与 PROTOCOL_VERSION the only import path ".": all of cbor, codec, framing; types and PROTOCOL_VERSION from protocol.ts 141 codec.ts parse* · encodeClientMessage / encodeServerMessage · ClientMessageDecoder / ServerMessageDecoder · ProtocolValidationError parse* · encodeClient/ServerMessage · Client/ServerMessageDecoder · ProtocolValidationError 110 protocol.ts TypeBox 信封 schema TypeBox envelope schemas PROTOCOL_VERSION = 8 151 framing.ts u32 大端长度前缀 u32 big-endian prefix FrameDecoder · 16 MiB 216 encoder.ts encodeCbor:定长子集 encodeCbor: definite subset 168 decoder.ts decodeCbor:严格解码 decodeCbor: strict decode 52 options.ts 上限默认值 · CborError default limits · CborError hello request cancel hello hello_error response service_update attachment @earendil-works/chord ^1.1.0 JsonValue · isJsonValue:严格 JSON 边界 JsonValue · isJsonValue: the strict-JSON boundary typebox 1.3.27 Type.Object(additionalProperties: false) + Check 5 client pi-client:Connection 握手、请求信封 pi-client: Connection handshake, request envelopes 5 server pi-server:ClientMessageDecoder、回应与更新 pi-server: ClientMessageDecoder, responses, updates 8 coding-agent experimental/:ServerId、DEFAULT_MAX_FRAME_LENGTH experimental/: ServerId, DEFAULT_MAX_FRAME_LENGTH 校验 validate 分帧 frame 客户端消息(3) Client messages (3) 服务端消息(5) Server messages (5) 依赖 Depends on 被导入(src 中导入它的文件数) Imported by (files in src) coding-agent 只把三个包列为 devDependencies, coding-agent lists all three packages only as 供实验性的 server / client 运行时使用。 devDependencies, for its experimental server / client runtime. README:默认上限 16 MiB / 帧、1,000,000 个数组元素或映射项、64 层嵌套;协议是实验性的,不保证兼容。 README: default limits 16 MiB per frame, 1,000,000 array elements or map entries, 64 nesting levels; experimental, no compatibility guarantees. 标签 = 行数 / 文件数 tag = lines / files 信封与编解码 envelopes & codec CBOR 依赖 dependencies 入口 entry 使用 uses
没有任何 Node 专用 API:同一份代码可以用在浏览器、Bun 或 worker 里;传输(Unix socket、WebSocket…)由 client / server 提供。 No Node-only APIs: the same code works in a browser, Bun or a worker; transports (Unix sockets, WebSockets…) come from client / server.
02 · 一条消息的往返 02 · One message, there and back Run modes
一条消息的往返 One message, there and back One message: validate → CBOR → frame → bytes → split → decode → validate 任何一步失败都是 ProtocolValidationError Any failure along the way is a ProtocolValidationError codec.ts ClientMessage 普通 JS 对象 a plain JS object parseClientMessage TypeBox Check + isJsonValue encodeCbor 上限 = maxFrameLength limit = maxFrameLength encodeFrame 前缀 u32 大端长度 prefix a u32 big-endian length Uint8Array 一整帧,交给传输 one whole frame, to the transport ByteTransport / ByteConnection:可以任意切分、合并字节,但必须保持顺序 ByteTransport / ByteConnection: may split and coalesce bytes arbitrarily, but must preserve their order chunk, chunk, … 任意大小的片段 fragments of any size FrameDecoder.push 凑齐 4 字节头 → 负载 4-byte header → payload decodeCbor 恰好一个定长 CBOR 项 exactly one definite-length item parseServerMessage 同一套 schema 再校验一次 the same schemas, checked again ServerMessage[] push() 返回 0..n 条消息 push() returns 0..n messages 发送方(encodeClientMessage / encodeServerMessage) Sender (encodeClientMessage / encodeServerMessage) 接收方(ClientMessageDecoder / ServerMessageDecoder) Receiver (ClientMessageDecoder / ServerMessageDecoder) schema 不符或含非 JSON 值 Schema mismatch or a non-JSON value 未知字段、Uint8Array、NaN、undefined、原型、环 unknown fields, Uint8Array, NaN, undefined, prototypes, cycles CBOR 或分帧出错 Malformed CBOR or framing CborError / FrameError 被包装,消息截断到 500 字符 CborError / FrameError are wrapped, messages cut to 500 chars 解码器一旦失败 Once a decoder fails 之后的 push() / end() 都直接抛错 every later push() / end() throws end() 时仍有半帧 end() with a partial frame "Truncated frame at end of stream" 协议代码 protocol code 传输 transport 数据流 data flow 失败 failure
任何失败都变成 ProtocolValidationError:CborError 与 FrameError 被包装,错误消息截断到 500 个字符。 Every failure becomes a ProtocolValidationError: CborError and FrameError are wrapped and the message is cut to 500 characters.
03 · 8 种信封 03 · The 8 envelopes Run modes
8 种信封 The 8 envelopes Protocol version 8: 8 envelopes — 3 client messages, 5 server messages ClientMessage 客户端 → 服务端 client → server ServerMessage 服务端 → 客户端 server → client «first» ClientHello type: "hello" version: integer ≥ 0 RequestEnvelope type: "request" id: string (non-empty) target: RpcTarget call: JSON (opaque) CancelEnvelope type: "cancel" id: string target: RpcTarget «first» ServerHello type: "hello" version: 8 (literal) serverId: ServerId «terminal» ServerHelloError type: "hello_error" error: ProtocolError ResponseEnvelope type: "response" id · ok: true result?: JSON (opaque) type: "response" id · ok: false error: ProtocolError ServiceEventEnvelope type: "service_update" subscriptionId: string update: JSON (opaque) «out of band» AttachmentEnvelope type: "attachment" attachment: SessionTarget | null ProtocolError code: string (non-empty) message: string 错误码对协议是不透明的: Codes are opaque to the protocol: version、invalid_request、cancelled、wrong_server … version, invalid_request, cancelled, wrong_server … 由 server 与 chord 定义。 are defined by the server and chord. 所有信封都是 additionalProperties: false 的 StrictObject;id 是非空字符串;call、result、update 只校验为严格 JSON(Type.Unknown + isJsonValue)。 Every envelope is a StrictObject with additionalProperties: false; ids are non-empty strings; call, result and update are only checked to be strict JSON (Type.Unknown + isJsonValue). response 的 ok: true 可以没有 result 字段(void 回应)。 A response with ok: true may omit result (a void response). 客户端发送 sent by the client 服务端发送 sent by the server 错误 error
客户端 hello 的 version 是任意非负整数,服务端的则是字面量 8:版本不符由服务端用 hello_error { code: "version" } 回答。 The client's hello.version is any non-negative integer, the server's the literal 8: a mismatch is answered by the server with hello_error { code: "version" }.
04 · 路由:serverId、sessionId、attachmentId 04 · Routes: serverId, sessionId, attachmentId Run modes
路由:serverId、sessionId、attachmentId Routes: serverId, sessionId, attachmentId RpcTarget: fencing a call to one logical server, one Session, one live attachment 逻辑服务器 Logical server serverId 持久 Session Durable Session sessionId 实时附着 Live attachment attachmentId 路由怎样到达客户端 How the route reaches the client 服务器拒绝的路由 Routes the server rejects { serverId } 服务器级调用:目录、管理 server-wide calls: directory, management { serverId, sessionId, attachmentId } Session 调用:每个请求都带完整路由 Session calls: every request carries the full route wrong_server session_not_attached server_draining invalid_request 规范的小写 UUIDv4;是逻辑身份,不是 socket 地址 a canonical lowercase UUIDv4; a logical identity, not a socket address Session 与它的 Harness 留在 Session worker 进程里 the Session and its Harness stay in the Session worker process 每次成功 attach 由服务器生成(randomUUID) generated by the server on every successful attach (randomUUID) 一个 Session 可以有多个 presentation 附着; A Session may have several presentation attachments; 同一连接重复 attach 同一 Session 是幂等的。 repeating attach for the same Session on one connection is idempotent. ^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ // 管理调用的结果里没有任何路由 id // management results carry no route ids await sessions.attach(sessionId) // 服务器装好路由后,带外发送: // once installed, the server sends out of band: { type: "attachment", attachment: { serverId, sessionId, attachmentId } } // detach / 断开 / 失效 → attachment: null // detach / disconnect / invalidation → attachment: null target.serverId 不是这台服务器(在访问仓库之前) target.serverId is another server (before any repository access) 本连接没有附着该 Session,或 attachmentId 已过期 this connection holds no such attachment, or the id is stale 服务器正在关闭 the server is shutting down 信封本身不合法 the envelope itself is invalid 切换或重新附着之后,带旧 attachmentId 的延迟帧会被拒绝:路由同时防止跨服务器、跨 Session 和过期的调用。 After switching or reattaching, delayed frames with an old attachmentId are rejected: the route prevents cross-server, cross-Session and stale calls alike. 服务器级 server-wide Session 附着 attachment 拒绝 rejected
管理服务的 attach() / detach() 不返回任何路由 id:服务器装好路由后才发 attachment,解除时发 attachment: null。 Management attach() / detach() return no route ids: the server sends attachment once the route is installed, and attachment: null when it is cleared.
05 · 一条连接上的消息顺序 05 · Message order on one connection Run modes
一条连接上的消息顺序 Message order on one connection Message order on one connection: handshake, requests, out-of-band attachment, subscriptions, cancellation Client pi-client Server pi-server hello { version: 8 } hello { version: 8, serverId } request { id: "request-1", target, call: attach } attachment { attachment: { serverId, sessionId, attachmentId } } response { id: "request-1", ok: true } request { id: "request-2", call: subscribe } response { id: "request-2", ok: true, result: snapshot } service_update { subscriptionId, update } cancel { id: "request-3", target } response { id: "request-3", ok: false, error: { code: "cancelled" } } ① 握手 ① Handshake 第一帧必须是 hello the first frame must be hello ② 管理调用 ② A management call attach 的结果里没有路由 id attach returns no route ids ③ 订阅 ③ A subscription 先快照,再更新 snapshot first, then updates ④ 取消 ④ Cancellation 同一 id、同一 target same id, same target 服务端 5 s 内没完成握手 No handshake within 5 s → hello_error,关闭连接 → hello_error, close 版本不符 → hello_error { version } Wrong version → hello_error { version } serverId 不符 → 客户端断开 serverId mismatch → client drops 带外:在 attach 的回应之前 Out of band: before the attach 送达,路由已经装好 response, route already installed 快照之前产生的更新 Updates produced before the 在服务端排队,回应之后才发 snapshot queue until after it 请求在服务端被 abort The request is aborted on the 客户端早已在本地 reject server; the client rejected locally 握手之后再发 hello、或在客户端发出 hello 之前收到服务端数据,都是协议错误:连接被关闭。 A second hello after the handshake, or server data before the client's hello was sent, is a protocol error: the connection closes. 客户端 → 服务端 client → server 服务端 → 客户端 server → client 错误回应 error response
服务端握手超时默认 5 秒;第一条消息不是 hello、或之后再发 hello,都会以 hello_error { code: "invalid_request" } 结束连接。 The server's handshake timeout is 5 seconds by default; a first message that is not hello, or a later hello, ends the connection with hello_error { code: "invalid_request" }.
06 · 分帧与 FrameDecoder 06 · Framing and FrameDecoder Run modes
分帧与 FrameDecoder Framing and FrameDecoder Framing: a u32 big-endian length + one CBOR item; 16 MiB by default 一段字节流(chunk 边界可以落在任何地方) A byte stream (chunk boundaries may fall anywhere) FrameDecoder 的状态 FrameDecoder states framing.ts push(chunk) 做的事 What push(chunk) does open ended failed 00 00 00 15 CBOR · 21 字节 CBOR · 21 bytes 00 00 00 00 00 02 00 00 负载 · 131,072 字节 … payload · 131,072 bytes … chunk 1 chunk 2 chunk 3 chunk 4 … 长度 = 21 length = 21 { type: "hello", version: 8 } 空帧 → Uint8Array(0) empty frame → Uint8Array(0) 长度 = 131,072 length = 131,072 = 2 个 64 KiB 块,收满后拼接 = two 64 KiB blocks, joined when complete 编码:encodeFrame(payload) 只加 4 字节头;负载超过 0xffffffff 字节 → RangeError。 Encoding: encodeFrame(payload) only adds the 4-byte header; a payload above 0xffffffff bytes → RangeError. end():没有半帧 end(): no partial frame 超限 · 截断 too long · truncated push(chunk) → Uint8Array[] 留在 open stays open 之后的 push / end → FrameError later push / end → FrameError 清空缓冲;之后一律 FrameError buffers cleared; FrameError from then on Client/ServerMessageDecoder 把它包装成 ProtocolValidationError。 Client/ServerMessageDecoder wrap it as ProtocolValidationError. 凑齐 4 字节头,可以跨多个 chunk Fill the 4-byte header, across chunks if needed 长度 > maxFrameLength → 立即失败 Length > maxFrameLength → fail at once 不等负载到达 without waiting for the payload 长度 0 → 立刻产出一个空帧 Length 0 → emit an empty frame now 负载按 64 KiB 的块收集 Collect the payload in 64 KiB blocks PAYLOAD_BLOCK_SIZE 收满:单块直接返回,否则拼成一个数组 Complete: one block as is, else joined 总是复制字节,不引用输入 chunk Bytes are always copied, never aliased 1 2 3 4 5 6 帧 frames 解码器 decoder 正常结束 clean end 失败 failure
负载总是被复制:返回的帧不引用调用方传入的 chunk,调用方可以复用自己的缓冲区。 Payloads are always copied: returned frames never alias the caller's chunks, so callers may reuse their buffers.
07 · 严格的 CBOR 子集 07 · A strict CBOR subset Run modes
严格的 CBOR 子集 A strict CBOR subset A strict RFC 8949 subset: definite-length items only, no tags 8 种主类型(major type) The 8 major types cbor/decoder.ts · encoder.ts CborOptions 与规则 CborOptions and rules options.ts 例子:encodeClientMessage({ type: "hello", version: 8 }) Example: encodeClientMessage({ type: "hello", version: 8 }) 0 ✓ 1 ✓ 2 ✓ 3 ✓ 4 ✓ 5 ✓ 6 ✕ 7 ✓ 31 ✕ 16 MiB maxByteLength 输入 / 输出与单个串的长度 input / output and any one string 1,000,000 maxContainerLength 数组元素或映射项 array elements or map entries 64 (≤ 512) maxDepth 嵌套层数 nesting levels 无符号整数 unsigned int number(安全整数) number (safe integer) 负整数 negative int number,-1 − n 必须安全 number; -1 − n must be safe 字节串 byte string Uint8Array;信封层再拒绝(非 JSON) Uint8Array; refused later by the envelope check 文本串 text string fatal UTF-8;保留开头的 BOM fatal UTF-8; a leading BOM is kept 数组 array 不能有空洞或 undefined no holes or undefined 映射 map 键必须是字符串且不重复;__proto__ 是普通数据 string keys, no duplicates; __proto__ is data tag 一律拒绝 always rejected 简单值 / 浮点 simple / float 只有 false、true、null 与有限的 float64 only false, true, null and finite float64 不定长(附加信息 31) indefinite length (info 31) 一律拒绝,break 也是 always rejected, break too 编码器 Encoder 整数用最短的参数;-0 与小数写成 float64 shortest argument for integers; -0 and fractions as float64 值为 undefined 的属性被省略,假值保留 undefined properties are omitted, falsy values kept 环、符号键、非普通对象 → CborError cycles, symbol keys, non-plain objects → CborError 字符串必须是合法的 Unicode 标量序列 strings must be valid Unicode scalar values 解码器 Decoder 先检查声明的长度与深度,再遍历 checks declared lengths and depth before traversing 恰好一个项:尾随字节 → 错误 exactly one item: trailing bytes → error 整数超出安全范围 → 错误 integers outside the safe range → error 00 00 00 15 a2 64 74 79 70 65 65 68 65 6c 6c 6f 67 76 65 72 73 69 6f 6e 08 25 字节:4 字节长度(21)· a2 = 2 项映射 · 64 "type" · 65 "hello" · 67 "version" · 08 = 8 25 bytes: 4-byte length (21) · a2 = map of 2 · 64 "type" · 65 "hello" · 67 "version" · 08 = 8 支持 supported 拒绝 rejected 上限 limits
编码器为整数选最短的参数;非整数与 -0 写成 float64(0xfb);非有限数、非安全整数直接报错。 The encoder picks the shortest argument for integers; non-integers and -0 are written as float64 (0xfb); non-finite numbers and unsafe integers are errors.
08 · protocol、chord 与应用的分界 08 · Where protocol, chord and the application meet Run modes
protocol、chord 与应用的分界 Where protocol, chord and the application meet Who owns which layer: the envelope is protocol's, the call is chord's, the data is the application's pi-protocol 校验的信封 The envelope pi-protocol validates RequestEnvelope chord 解释的 call The call chord interprets ServiceCall 应用自己的数据 Application data args · result · update 谁在哪里解析 chord 语法 Who parses the chord grammar, where 不在公共协议里 Not in the public protocol type: "request" id target: RpcTarget call: … serviceId instance? member args catalogue subscribe unsubscribe Session 目录状态 Session directory state 管理结果 management results transcript transcripts 模型 models 插件 plugins 其他服务值 other service values pi-client · client.ts parseServiceCall · parseServiceCatalogue createServiceStateDecoder · parseWire…Snapshot pi-server · server.ts decodeServiceControlCall · parseServiceCall createServiceStateEncoder · RemoteServiceError coding-agent · services/connection.ts createClientServiceTransport(client, route) → 应用自己的类型化 chord 服务绑定 → the app's own typed chord service bindings 或者一个 $chord.service 控制调用: or a $chord.service control call: 回应里的服务目录、订阅快照与更新、服务错误码、复制状态的 Delta 路径编解码都属于 chord。 Catalogues, subscription snapshots and updates, service error codes and Delta path codecs belong to chord. 对 protocol 与 server 都是不透明的 JSON; Opaque JSON to both the protocol and the server; 由 chord 绑定与应用在服务边界上校验。 validated by chord bindings and the app at the service boundary. 服务器与 worker 的生命周期:协调器只转发不透明消息 Server and worker lifecycle: the coordinator only relays opaque messages 对端认证与已认证的服务上下文:实验性传输没有实现 Peer authentication and authenticated contexts: not implemented 兼容性保证:协议是实验性的 Compatibility guarantees: the protocol is experimental protocol 只检查 call、result、update 是严格 JSON;它不导出、也不校验 chord 的语法——这让信封层可以在不了解任何服务契约的情况下路由。 The protocol only checks that call, result and update are strict JSON; it neither exports nor validates chord's grammar — so the envelope layer routes without knowing any service contract. protocol chord 应用 application 范围之外 out of scope
因此服务端可以把 Session 调用原样转发给 Session worker,而不需要加载那个服务的契约。 So the server can forward a Session call to the Session worker as is, without loading that service's contract.
扩展点 Extension points pi-coding-agent
protocol 的扩展点 Where to plug into protocol 1
maxFrameLength maxFrameLength
每一端的帧上限(默认 16 MiB);两端必须一致。 Each peer's frame limit (16 MiB by default); both peers must match.
2
encodeCbor / decodeCbor(…, CborOptions) encodeCbor / decodeCbor(…, CborOptions)
单独使用 CBOR 编解码,配置字节、容器与深度上限。 Use the CBOR codec on its own, with byte, container and depth limits.
3
FrameDecoder / encodeFrame FrameDecoder / encodeFrame
只用分帧,承载你自己的负载。 Use only the framing, carrying your own payloads.
4
parseClientMessage / parseServerMessage parseClientMessage / parseServerMessage
校验从别的通道得到的消息对象。 Validate message objects obtained through another channel.
5
ByteTransport (client) · ServerListener (server) ByteTransport (client) · ServerListener (server)
任何保持字节顺序的流都能承载协议:Unix socket、WebSocket 中继、管道。 Any order-preserving byte stream can carry the protocol: Unix sockets, a WebSocket relay, pipes.