10/Documentation
持久传输协议(实验性)
面向构建者说明 Collie 与持久 agent 之间传递的内容:路由、hello 与纪元(epoch)、ask 的生命周期与请求 ID、socket 与 token 传输层、限制与版本规则,并提供指向 WIRE.md 的具体规则链接
实验性功能。 有线协议可能会在后续次版本中发生变化,或者该功能可能会被移除。版本与握手信息 说明了变更如何传递给你。
durable 有线协议是 Collie 与 durable agent 通信的方式。本页面向不使用 SDK 编写 durable agent、或想了解 SDK 内部机制的开发者。确切规则请参见 WIRE.md;本页与该文件不一致时,以 WIRE.md 为准。durable SDK 会代你处理所有通信细节。
传输内容
Collie 发起连接,durable agent 负责监听,每次交互都是一个带有 JSON 主体的 HTTP 请求。
durable agent 监听专用 Unix 套接字,或监听位于 URL(传输层)且处于 TLS 保护下的端口。Collie 对其进行轮询:没有推送,没有 WebSocket,也没有流。每个路径都以 /session/v1 开头,这是一个固定名称,不是版本号。除第一个 hello 之外,每次调用都在请求头 X-Collie-Session 中携带版本号。请求主体是一个最大 64 KiB 的 JSON 对象,空的 POST 主体读取为 {}。
| 方法与路径 | 作用 | 回复限制 |
|---|---|---|
GET /hello | durable agent 的名称、版本范围、epoch、就绪状态及功能特性 | 16 KiB |
GET /snapshot | 布局:工作区、标签页、带有自身状态的窗格、轮次和待处理的 ask,以及启动项。携带 ETag,无更改时返回 304 | 4 MiB |
GET /pane/:id/events?after=N&epoch=E | 光标之后的窗格事件 | 8 MiB |
POST /pane/:id/answer | 对 ask 的回答 | 16 KiB |
POST /pane/:id/send | 窗格的文本,用于引导正在运行的轮次或等待下一轮 | 16 KiB |
POST /pane/:id/abort | 停止正在运行的轮次 | 16 KiB |
POST /pane/:id/action | 点击窗格工具带上的某个按钮 | 16 KiB |
POST /rename | 工作区或标签页的新标题 | 16 KiB |
POST /close | 关闭窗格、标签页或工作区 | 16 KiB |
POST /starter/:id | 运行启动项。回复中会指定新窗格的名称 | 16 KiB |
GET /conversations?after=C&limit=N | durable agent 的对话,包括进行中和已关闭的,按时间从新到旧排序 | 1 MiB |
GET /conversation/:id/items?after=C&limit=N | 单个对话的条目,按时间从旧到新排序 | 8 MiB |
POST /reopen | 重新显示已关闭的对话 | 16 KiB |
所有错误都包含带有 code 的 JSON 主体,例如 409 stale 或 404 no-such-pane。任何其他路径均返回 404 not-found。超过限制的回复不会被读取;若 POST 请求的回复被截断,其结果未知(请求 ID)。
Hello 与 epoch
hello 说明 durable agent 的身份,epoch 则指明 Collie 当前正在与该 agent 的哪一次启动实例进行通信。
hello 的字段 | 包含的内容 |
|---|---|
service | 精确为 collie-durable。若缺少该字段,Collie 会将 durable agent 视为其他服务器,且不再发起任何调用 |
protocol | durable agent 支持的传输协议版本范围,{ min, max }。目前为 { min: 1, max: 1 } |
host.name | durable agent 自身的名称,仅作为显示文本,最多 64 字节 |
epoch | durable agent 每次启动时全新随机生成,至少 128 位,绝不复用 |
ready | 启动后 durable agent 排布窗格期间为 false |
caps | durable agent 具备的能力,均为布尔值。缺失的 cap 为 false |
Collie 首先在不带版本标头的情况下调用 hello,选取双方均支持的最高版本,并在后续每次调用中发送该版本。当 ready 为 false 时,除 hello 外的每条路由均返回 503 starting,手机端显示 durable agent 正在启动。durable agent 仅在重新显示所有会话并重新发起所有待处理 ask 后才会将 ready 设为 true,因此重启绝不会显示未完成的布局。
快照的 ETag 为 "<epoch>-<rev>"。读取方可见的每次更改都会使 rev 递增,因此返回 304 对 durable agent 几乎没有开销。新的 epoch 会通知 Collie 该 durable agent 已重启。Collie 会继续显示现有窗格,直到读取到新快照。随后 Collie 使用各窗格的旧光标读取窗格,并在一次响应中获取该窗格的全部当前视图。
成功读取一次后出现一次读取失败,本次轮询会保留上一个快照。连续两次失败会将 durable agent 显示为不可达。未通过隐私检查的套接字路径会立即判定为不可达。
Ask 与 answer
ask 是一个问题卡片,其开放状态自 durable agent 的 ask 事件开始,至其 ask-closed 结束。
| 状态 | 设置者 | 手机上显示的内容 |
|---|---|---|
| 已开放 | durable agent 发起 ask。只要有任何 ask 处于开放状态,窗格即显示 waiting | “需要你的回答”下方的卡片,包含 durable agent 的名称,并在 ask 具有 expiresAt 时显示剩余时间 |
| 已更改 | durable agent 就地更改 ask,其 revision 递增 | 卡片重绘。点击旧表单会收到 409 stale,卡片提示“此 ask 已更改。请重新阅读后再点击选项。” |
| 已采纳 | 手机端作出回答,durable agent 以 by: "phone" 关闭该 ask | 卡片消失,工具按选项指示运行或被拒绝 |
| 在 durable agent 中决定 | durable agent 自行关闭 ask,decidedBy: "program"。停止轮次也计入此项 | Chat 中以“在 durable agent 中决定”开头的一行,后接 ask 的标题 |
| 已超时 | 时钟超过 expiresAt,decidedBy: "timeout" | Chat 中以“在回答前已超时”开头的一行,后接 ask 的标题 |
在作出决定后点击会收到带有首次结果的 409 decided,且不运行任何操作。卡片随后显示最先作出决定的方:“在 durable agent 中决定。”、“在回答前已超时。”,或在再次点击后显示:“已回答。此卡片将在下次更新时消失。”durable agent 不再知晓的 ask 会返回 200 too-late。卡片随后显示“未采纳。该问题可能片刻后会再次显示。”,因为重启后 durable agent 可能会在相同 ID 下重新发起该问题。
| 规则 | 原因 |
|---|---|
| ask ID 是窗格 ID 与工具调用 ID 的哈希值 | 重启后重新发起的相同工具调用具有相同 ID,因此跨重启发送的回答仍能生效 |
permission 卡片根据工具名称及其参数构建,绝不采用模型的文本 | 期望获得肯定回答的模型会对调用进行委婉描述 |
TLS 后方的 durable agent 不应提供任何 allow-always | Token 是唯一的凭证,持有者的单次回答将允许后续所有调用通过 |
若卡片的文字已被截断或清除,或者卡片未指明工具,手机端不会提供 allow-always | 用户无法查看该授权覆盖的范围 |
expiresAt 是带时区的 ISO 8601 格式 | 超过该时间后,请求将由 timeout 裁决,快照中也不再将其列为打开状态 |
| durable agent 为每个窗格至少保留其最近裁决的 500 个请求的结果 | 延后的点击会收到 decided 和最初的结果,而不是普通的 too-late |
请求分为四种:permission、choice、text 或 form。每种都有其专属的响应结构,已在 SDK 的 提问 中列出。
请求 ID
来自手机端的每个操作都带有一个请求 ID,因此最多只执行一次。
手机端为每个意图生成一个请求 ID,Collie 在传递该 ID 之前会先写入自己的收据。durable agent 至少保留其纪元内的最后 500 个 ID。重复传入已完成的 ID 将通过 replayed: true 再次获取最初的答复且不会执行任何操作。相同的 ID 搭配不同的有效负载则会返回 409 request-id-reused。
Collie 本身从不重试 POST 请求。超时的 POST、断开连接或收到 5xx 状态码的操作,其结果均未知。对于答复,卡片随后会显示“无法确定答复是否送达。点击重试将重新发送一次。”,并且“重试”会发送相同的请求 ID,因此 durable agent 的内存会响应重复请求。
若重试时发现请求已关闭或轮次已更替,则无法判断第一次尝试是否生效。Collie 随后会向手机端回复“可能已应用”,卡片显示“可能已应用。请检查聊天。”,且该操作绝不会发送第三次。
传输层
durable agent 使用以下两种传输方式之一:Collie 所在机器上的专用 socket,或带有令牌的 TLS。
两者的字节数据相同:路由、请求体、版本标头、ETag 和错误代码均无差别。唯一的区别在于谁可以访问 durable agent。
| 通过 socket | 规则 |
|---|---|
| 路径 | 绝对路径,最多 103 字节。双方均从 COLLIE_DURABLE_SOCKET 读取该路径 |
| 模式 | socket 的权限模式为 0600,位于权限模式为 0700 的文件夹中。durable agent 在开始监听之前会先删除过期的 socket 文件 |
| 路径检查 | 在每次连接之前,Collie 都会使用 lstat 检查路径的每一部分。任何部分都不能是符号链接。socket 所在的文件夹属于当前用户,权限模式为 0700。除粘滞目录(如 /tmp)外,其上级目录均不允许 group 或 other 写入。socket 文件属于当前用户,不包含任何 group 或 other 权限 |
| 检查失败 | Collie 不会建立连接,并将 durable agent 显示为不可达,同时附带 socket-not-private 以及失败的路径部分 |
| Token | 无。专用 socket 本身即是安全屏障 |
| 操作权限 | 当前用户的任何进程。durable agent 可以使用 403 caller-refused 拒绝其自身的子进程 |
| 超时设置 | hello、snapshot 和 events 为 1000 毫秒。答复、发送、停止、操作、重命名或关闭为 3000 毫秒。启动器为 30000 毫秒 |
| Collie 的轮询频率 | 手机端处于查看状态时每 1 秒一次,否则每 3 秒一次。已停机的 durable agent 在每次轮询时只会被请求一次 |
| 支持系统 | Linux 和 macOS。Windows 没有 socket 传输 |
| 基于 TLS | 规则 |
|---|---|
| URL | https://,本地测试可用 http://127.0.0.1:<port> 或 http://localhost:<port>。最多 2048 字节,不含用户名或密码,无 query,无 fragment。Collie 会向其追加 /session/v1 |
| Token | 至少 32 字节,不含空格的可打印 ASCII。Collie 在每次请求时都会发送 Authorization: Bearer <token>,包括 hello |
| durable agent 检查 | 优先检查 token,早于版本检查、hello 和 ready,并采用恒定时间比较。缺失或错误的 token 仅返回纯粹的 401 { "code": "unauthorized" },无其他内容 |
| 重定向 | Collie 不跟随重定向。3xx 会作为状态码直接向上传递,因此 token 绝不会发送至其他主机 |
| ETag | 压缩响应的代理可能会将 tag 弱化为 W/"<epoch>-<rev>"。双方比对时都会去除 W/ |
| 拒绝访问 | 401 或 403 会将 durable agent 显示为不可达,并附带 token-refused |
| 超时设置 | 每次读写均为 5000 ms,因为休眠的 durable agent 唤醒时可能是冷启动。starter 为 30000 ms |
| Collie 的轮询频率 | 手机端查看时每 2 秒一次,否则每 10 秒一次。已停机或拒绝访问的 durable agent 最多每 10 秒请求一次 |
每个超时时间都是整个交互周期的截止期限,从建立连接持续到最后一个字节。在 TLS 上,每次轮询都是一次主机可能会计费的请求,304 也不例外。
限制
Collie 会截断或拒绝超出这些限制的内容,因此保持在限制范围内的 durable agent 不会丢失任何数据。
| 内容 | 限制 |
|---|---|
| 每个快照的工作区数 | 64 |
| 每个快照的标签页数 | 256 |
| 每个快照的窗格数 | 256 |
| 每个窗格未完成的 ask 数 | 50,最早的优先 |
permission 的选项数 | 6 |
choice 的选项数,以及 choice 字段的选项数 | 8 |
form 的字段数 | 6 |
| 每个快照的 starter 数 | 50 |
| 每个窗格的腰带按钮数 | 4,标签长度为 16 个字符 |
| id | 1 到 128 字节,无控制字符,无空白字符,且不包含 /、? 或 # |
| 请求体 | 64 KiB,否则返回 413 too-large |
| 响应 | hello 16 KiB,snapshot 4 MiB,events 8 MiB,conversations 1 MiB,items 8 MiB,POST 16 KiB |
| 每个响应的事件数 | 1000。落后的读取端获取当前视图,最多为最新的 2000 项 |
user、reply、thinking 或 notice 项的文本 | 64 KiB |
markdown 或 code 项的文本 | 32 KiB |
| 手机发送的文本 | 32 KiB |
link 项 | 仅限 https://,最多 2048 个字符 |
| 历史记录页 | 100 个会话,或 1000 项 |
| 历史记录游标 | 512 字节的可打印 ASCII,无空格 |
超出限制的字符串会被截断,并移除其控制字符与不可见字符。超出限制的列表保留其前面的条目。超出限制的历史记录页则完全不读取。
版本与握手信息
目前只有一个传输协议版本 1,Collie 使用 1..1。
版本 2 发布时,Collie 将在其后继续支持版本 1 一个次要版本,再下一个次要版本将其废弃。读取端会忽略未知的字段、事件类型、项种类或状态码。版本 1 内部新增的字段(例如 expiresAt 或 decidedBy)是可选的。新增必填字段、已有字段赋予新含义或删除字段都需要发布新版本。
手机无法识别的 ask 种类会显示为无法在手机上回复,且不显示按钮。
当两个范围没有交集时,Collie 不再从 durable agent 读取任何内容。在 durable agent 的每个纪元中,Collie 会额外发送一次 hello(此时携带自己的版本),以便 durable agent 在其端记录不匹配日志。durable agent 返回 409 incompatible,并带上指明两个范围的 refusal。这称为通知握手(telling hello)。
Collie 会针对每个拒绝的范围在日志中写入一行,且 collie doctor 会显示这两个范围。手机上 durable agent 的主页会显示同样的提示,例如 "The durable agent speaks wire v2, this Collie speaks v1. Update Collie, or the agent's SDK."。更新版本较旧的一端。
具体规则所在位置
WIRE.md 为契约,包含所有结构、状态码和规则。
背后的决策记录在代码仓库中: