05/Documentation
持久 agent(实验性)
在手机上显示你自己的持久 agent:快速入门、collie durable attach 与 sources.json、三个动词、token、Collie 保留的文件以及版本
实验性功能。 底层协议、SDK 和此页面可能会在后续的次要版本中发生变动,或该功能可能会被移除。目前仅 Collie 自身的示例支持该协议。退出 说明了决定其是否保留的条件。如果你未挂载 durable agent,Collie 的外观和工作方式与以往一致。
如果你使用 Pi Durable 或任何支持该协议的程序构建了 durable agent,Collie 可以在手机上展示它。无需任何 UI 代码,它即可获得审批提示、历史记录和聊天视图。
durable agent 是 session 的一个来源,其每个对话都是一个 session。术语说明见 术语表。Collie 不保存模型密钥:durable agent 使用其自己的 API 密钥或 ChatGPT 登录凭据,且 Anthropic 订阅不能通过第三方框架使用。
快速入门
一个十行的 durable agent、一个 collie durable attach,以及手机上的一张问题卡片。
你需要在本机运行 Collie 1.22.0 或更高版本,并配对手机(安装)。
durable agent 运行在 Linux 或 macOS 上,需要 Node 22.19 或更高版本,或 Bun 1.4 或更高版本。在 durable agent 的目录中安装 SDK:
npm install @colliepwa/durable-sdk@experimental
# or
bun add @colliepwa/durable-sdk@experimental该软件包在 npm dist-tag experimental 下发布,绝非 latest,并遵循 ADR 0104 中的退出策略:如果 durable 线路废弃,该软件包即被弃用(ADR 0105)。
SDK 的 Pi Durable 入口已通过 Pi Durable 1.1.0 和 1.2.0 测试。Node 22.19 是 Pi Durable 1.2.0 的最低版本要求。
将此 durable agent 保存为 hello.mjs。它只有十行:
import { openCollie } from "@colliepwa/durable-sdk";
const socket = process.env.COLLIE_DURABLE_SOCKET ?? `${process.env.XDG_RUNTIME_DIR}/hello/app.sock`;
const collie = await openCollie({ socket, hostName: "hello", caps: { asks: true } });
const pane = collie.workspace("hello", { title: "Hello" }).tab("main", { title: "Questions" }).pane("deploy", { title: "Deploy", harness: "hello" });
collie.ready();
console.log(`listening ${collie.address}`);
const options = [{ id: "yes", label: "Yes" }, { id: "no", label: "No" }];
const answer = await pane.ask({ kind: "choice", toolCallId: "deploy-1", itemId: "deploy-1", title: "Deploy to staging?", options });
console.log(`answer: ${answer.optionIds[0]}`);
await collie.close();使用 node hello.mjs 或 bun hello.mjs 运行它。它会输出 listening /run/user/1000/hello/app.sock 并等待。同一个 durable agent 在 Collie 的克隆中对应 examples/durable-member/hello.ts。
在 Node 上运行时,它会先多输出一行:Node 无法读取谁连接到了 durable agent 的套接字,因此属于你用户的任何进程都可以回答其问题。Linux 上的 Bun 可以读取,并会拒绝来自 durable agent 自身或其启动进程的回答。
在 macOS 上,请使用 Node 运行它。macOS 上的 Bun 需要 callerCheck: "off",而此 durable agent 未设置该项。macOS 没有 XDG_RUNTIME_DIR,因此请先将 COLLIE_DURABLE_SOCKET 设置为绝对路径。
在第二个终端中,将其挂载到你的 Collie,然后重启以便 Collie 读取新配置项:
collie durable attach hello --socket /run/user/1000/hello/app.sock
collie restart使用 durable agent 输出的路径。在第一个命令中添加 --restart 可以在同一步骤中完成重启。--restart 会重启 Collie 桥接:你的手机会在几秒内重新连接,终端 session 保持运行。
在手机上打开 Collie。「Durable agents」部分会显示一行:hello,带有 Experimental 标记,并有一个待处理的问题。点击该行:它会打开 durable agent 的对话列表,其中对话 Deploy 正等待回答 "Deploy to staging?"。打开它并点击 Yes 或 No。durable agent 输出你的回答并退出。
要编写自己的 durable agent,请阅读 durable SDK 和 durable 线路。
挂载 durable agent
每个 durable agent 对应 sources.json 中的一个配置项,由 collie durable attach 写入。
collie durable attach desk --socket /run/user/1000/desk/app.sock
export EDGE_TOKEN=your-32-byte-random-token-here
collie durable attach edge --url https://edge.example.com --token-env EDGE_TOKEN
collie restart在 Windows 上仅支持 HTTPS 形式:attach --socket 会以退出码 2 拒绝执行,并提示使用 --url 和 --token-env,因为私有套接字需要 Unix 用户 ID。
sources.json 位于 .env 旁的配置目录中:若由 Herdr 管理安装,则位于 Herdr 插件配置目录(~/.config/herdr/plugins/config/herdr.collie/),否则位于 ~/.config/collie/。attach 会输出其写入的路径。上面的两条命令会写入此文件:
[
{ "name": "desk", "kind": "durable", "socket": "/run/user/1000/desk/app.sock" },
{
"name": "edge",
"kind": "durable",
"url": "https://edge.example.com",
"token": "your-32-byte-random-token-here"
}
]| 字段 | 包含的内容 |
|---|---|
name | durable agent 在手机上显示的名称及其日志目录的名称。1 到 32 个字符:小写字母、数字和连字符,以字母或数字开头。在文件中唯一 |
kind | durable,当前唯一支持的类型 |
socket | durable agent 在本机上的私有 Unix 套接字绝对路径。不能与 url 一起使用 |
url | 部署在 TLS 之后的 durable agent 地址。不能与 socket 一起使用,且始终与 token 一起使用 |
token | Collie 发送给 url 的 Bearer 令牌。仅与 url 一起使用(Token) |
bridge 会在启动时读取该文件,因此条目在重启后生效。你的终端外观和工作方式与之前一致,位于 durable agent 旁边。
Collie 对该文件的校验很严格,因为被静默跳过的 durable agent 将永远不会显示:
- 表中未命名的字段会导致 bridge 停止运行。 错误的
kind、违反规则的名称、重复使用的名称、同时存在socket和url、两者皆无,或者缺少token的url也会导致同样的结果。 - 损坏的文件会导致 bridge 在启动时停止并输出一行提示。 该行以
[bridge] FATAL:开头,并指明条目和字段。collie logs会显示该信息。attach和detach也会拒绝覆盖无法读取的文件,因此拼写错误绝不会影响其他条目。 - 文件权限保持为
0600模式。attach会以这种方式写入。当文件权限更宽松时,bridge 会在启动时收紧权限并在日志中记录。 - 名称不能与你的终端会话相同。 在大多数安装中为
default,在纯 durable 实例上,default是其自身 durable agent 的名称。bridge 会在启动时停止并指出该条目。 - 面对 Herdr 会话,durable agent 优先保留其名称。 当 Herdr 会话与 durable agent 同名时,手机端会显示 durable agent,隐藏该 Herdr 会话,并在日志中记录一次。
attach 按照以下规则检查套接字路径、URL 和令牌。手动写入的条目会在 bridge 访问 durable agent 时进行检查:若违反规则,届时会将 durable agent 显示为不可达并给出原因。
访问 durable agent 的两种方式
| 专用 socket | 带 token 的 https | |
|---|---|---|
| durable agent 的运行位置 | 本机,以你的用户身份运行 | 任何你部署它的地方 |
| 防范机制 | 该 socket 归你所有,模式为 0600 | 仅凭 token 认证 |
| 谁可以作为你的手机端 | 属于你当前用户的任何进程 | 持有该 token 的任何人 |
| Collie 的轮询频率 | 手机端查看时每 1 秒一次,其他情况每 3 秒一次 | 手机端查看时每 2 秒一次,其他情况每 10 秒一次 |
套接字传输适用于 Linux 和 macOS。在 Windows 上请使用 URL 和令牌。套接字路径必须是绝对路径且最多 103 字节。请将其保留在 $XDG_RUNTIME_DIR 下,不要放在 /tmp 下:Linux 上的 Collie 服务使用私有 /tmp 运行。
Collie 仅在整条路径均为私有时才会连接到套接字,因为该套接字不携带令牌。该套接字归你所有,没有用户组或其他权限。其所在文件夹归你所有且权限为 0700。路径的任何部分都不能是符号链接,且其上层文件夹除 /tmp 等带有 sticky 位的目录外,均不可被用户组或其他用户写入。SDK 会在套接字所在文件夹不存在时以 0700 模式创建它。
URL 必须以 https:// 开头。唯一的例外是同一台机器上的 durable agent 可以使用带端口的 http://127.0.0.1:<port> 或 http://localhost:<port>。Collie 会拒绝任何其他 http:// 地址,因为令牌将未经加密直接在网络中传输。
注意。 在 https 上,令牌是唯一的访问屏障。任何拥有该令牌的人都可以读取所有对话并回答所有问题,且宿主机可以看到这些文本。以此方式访问的 durable agent 应仅提供 Allow 和 Deny,绝不能提供 always-allow。
Durable agent 安全性 说明了 durable agent 可以执行的操作以及 Collie 记录的内容。
通过 https 进行的每次轮询都是一次可能由宿主服务商计费的请求,返回“未发生更改”的应答也计入其中。在没有打开手机端时,空闲轮询每 10 秒运行一次,因此 Collie 每天会发起 8,640 次请求。请查看你的宿主服务商套餐对此类用量的限制。
位于 URL 后面的 durable agent 必须支持 Collie 传输协议并检查 bearer 令牌。原生的 Pi Durable 演示版两者都不支持,因此 Collie 无法直接挂载它。
Collie 会轮询托管的 durable agent。在手机端处于查看状态时每 2 秒请求一次,否则每 10 秒请求一次。每次轮询都会唤醒处于休眠状态的 Durable Object,因此在 Collie 查看期间托管对象会保持唤醒状态。该协议不使用 WebSocket,因此 Durable Object 的休眠和 WebSocket 特性在此处不起作用。
Cloudflare 示例目前搁置,不属于发布版本的一部分。它位于 experiments/durable-member-cloudflare/。
三个动词
attach、detach 和 list 用于修改或读取 sources.json,除非收到要求,否则它们不会重启任何内容。
collie durable attach <name> --socket <path> [--restart]
collie durable attach <name> --url <url> --token-env <VAR> [--restart]
collie durable detach <name> [--restart]
collie durable list| 动词 | 参数 | 输出内容 | 退出代码 |
|---|---|---|---|
attach <name> | --socket <path>,或者带 --token-env <VAR> 的 --url <url>。--restart。flag 的值可以是下一个词,也可以跟在 = 后面 | added <name> in <path>,或者对于已存在的名称使用 updated <name> in <path>。然后是 restart to apply: collie restart 以及要在手机上打开的 URL | 0 已写入。2 用法错误,或者路径、URL、token 违反了规则。1 文件无法读取。指定 --restart 时,返回重启的退出代码 |
detach <name> | --restart | removed <name> from <path>,然后 restart to apply: collie restart | 0 已删除。2 用法错误。3 不存在具有该名称的 durable agent。1 文件无法读取。指定 --restart 时,返回重启的退出代码 |
list | 无 | 每个 durable agent 一行:其名称、socket <path> 或 url <url> (token set),以及其状态。文件为空或缺失时显示 no durable agents attached (<path>) | 0。1 文件无法读取 |
collie durable 未指定动词或使用了未知动词时,打印用法并退出,代码为 2。指定 --restart 时,attach 和 detach 会执行与 collie restart 相同的重启操作。
list 会向正在运行的 bridge 查询每个条目,并最多等待 3 秒:
| 状态 | 含义 |
|---|---|
loaded | 正在运行的 bridge 已加载此条目。这是 bridge 的反馈,而非 durable agent 本身的反馈:durable agent 可能仍无法访问。collie doctor 会查询 durable agent |
not loaded (restart to apply) | bridge 在你挂载之前已启动。运行 collie restart |
refused (<status>) | bridge 返回了此 HTTP 状态码而非状态 |
bridge not answering | 3 秒内未收到响应:Collie 已停止或正忙。collie status 可显示它是否正在运行 |
当 durable agent 未出现时,运行 collie doctor,它会指明失败的检查项。常见原因有三个:条目尚未加载(重启)、socket 不是私有的(socket-not-private),以及崩溃留下的残留 socket 文件(connection refused:将其删除并启动 durable agent)。Durable agent 故障排查 涵盖 durable agent 的各种情况。问题排查 涵盖 Collie 整体。
Token
url 条目携带其 token,且 token 是唯一的访问关卡。
openssl rand -hex 32结果为 64 个十六进制字符,高于 32 字节的下限。
将 token 设置为 32 个或更多随机字节(如上所述),并为 durable agent 提供相同的值。attach 会从 --token-env 指定的变量中读取它,因此它不会出现在任何命令行和 shell 历史记录中。运行 attach 时必须已设置该变量。
attach 会将该值以纯文本形式复制到 sources.json 中。因此该文件与 token 具有相同的机密性:Collie 将其权限保持为 0600,配置文件夹的备份或同步都会携带该 token。Collie 会在每次请求中发送 token,且绝不会打印它:list 显示为 (token set)。
| 规则 | |
|---|---|
| Token | 去除首尾空白后为 32 到 4096 字节,不含空格的可打印 ASCII 字符 |
| URL | https://,或者带端口的 http://127.0.0.1:<port> 或 http://localhost:<port>。最多 2048 字节。不包含用户名、密码、查询参数或分段。仅限 durable agent 的基础路径:Collie 会自行添加 /session/v1 |
token 不会过期。若要更改它,请使用相同的名称和新 token 重新运行 attach,然后重启:
export EDGE_TOKEN=your-new-32-byte-random-token-here
collie durable attach edge --url https://edge.example.com --token-env EDGE_TOKEN
collie restartattach 会打印 updated edge。在重启 Collie 之前,请先为 durable agent 提供新 token,否则手机端会提示 durable agent 拒绝了该 token。在轮换期间,durable agent 可以接受第二个 token,以确保双方都不会被锁定。停放的 Cloudflare 示例接收 COLLIE_TOKEN,并且在设置该项时也接收 COLLIE_TOKEN_NEXT(ADR 0103)。在 durable agent 上将新 token 设置为 COLLIE_TOKEN_NEXT,重新运行 attach 并重启 Collie,然后将该值移动到 COLLIE_TOKEN 并取消设置 COLLIE_TOKEN_NEXT。
单个 Collie 中仅有一个 durable agent
没有终端的实例(例如 Windows 主机或托管的 durable agent)可以从其 .env 驱动一个 durable agent。
# a durable agent on this machine, as you
COLLIE_MUX=durable
COLLIE_DURABLE_SOCKET=/run/user/1000/durable/session.sock
# or a durable agent you deployed behind TLS
COLLIE_MUX=durable
COLLIE_DURABLE_URL=https://durable-member.example.com
COLLIE_DURABLE_TOKEN=your-32-byte-random-token-here此类实例将其 durable agent 命名为 default,该 durable agent 即为其主会话。sources.json 中的条目仍会以其他名称在其旁运行。
.env | config.toml | 包含的内容 |
|---|---|---|
COLLIE_MUX=durable | [mux] mux | 使该实例仅运行 durable agent |
COLLIE_DURABLE_SOCKET | [mux] durable_socket | durable agent 的专用套接字。不可与 URL 同时配置 |
COLLIE_DURABLE_URL | [mux] durable_url | TLS 保护下的 durable agent 地址。必须附带令牌 |
COLLIE_DURABLE_TOKEN | [mux] durable_token | 用于 URL 的 Bearer 令牌。保存该令牌的文件权限必须为 0600 |
配置套接字,或配置带有令牌的 URL,二者不可兼得。若同时配置两者、配置了 URL 却无令牌、或配置了令牌却无 URL,均属于配置错误:Collie 会启动,在日志中记录该错误,并将 durable agent 显示为不可达及对应原因。
旧版本会直接将纯 durable 实例的日志保存在 <state>/durable/ 中。自此版本首次启动起,它们会一次性迁移到 durable/default/,并在日志中记录一行。
要更换令牌,请将新令牌部署到 durable agent,在 .env 中设置该令牌,然后运行 collie restart。纯 durable 实例会使用其主机名加入 crew,因此当它与 lead 共享主机时,请使用 collie crew join <lead-address> --label durable 为其指定名称。
若要暂时关闭 sources.json 中的所有 durable agent,请在 .env 中设置 COLLIE_PROGRAMS=off 并运行 collie restart。随后 bridge 会忽略该文件,记录一行日志,并以不包含 durable agent 的安装状态运行,因此手机上不会显示 Durable agents 部分。collie doctor 和 collie durable list 会提示 durable agent 已关闭。该文件保持原样:取消设置该变量并重启即可重新启用 durable agent。该变量不会影响上述旧版环境配置形式。
Collie 保存 durable agent 数据的位置
durable agent 保存其对话记录。Collie 在其状态文件夹中保存缓存、回执及其推送记忆。
状态文件夹为 ~/.local/state/collie/,若设置了 COLLIE_STATE_DIR 则为该路径。<program> 为 sources.json 中的名称,或为 default。每个 durable agent 的文件夹权限为 0700,每个日志的权限为 0600。
| 文件 | 包含的内容 | 保留时长 |
|---|---|---|
durable/<program>/<id>.jsonl | Chat 对某个活动对话的副本,其中 <id> 为 base64url 编码的对话 ID | 超过 8 MiB 后截断保留最新的 2000 条消息。所有日志合计保持在 256 MiB 以下:优先删除最早的对话,并在日志中记录一行 |
durable/<program>/<id>.cursor.json | Collie 停止读取的位置,以便重启后在此处继续 | 与对应的日志相同 |
answer-receipts.json | 手机发送的每条回答:其请求 ID、所回答的 ID 以及结果。不包含键入的文本 | 一周,至多保留最新的 1000 条。仍在传输中的回答至少保留一天 |
control-receipts.json | 每次 send、steer、stop 和 belt 操作:其请求 ID 及结果。文本仅保留摘要 | 同上 |
starter-receipts.json | 每次点击 starter 及其打开的对话 | 同上 |
durable-asks.json | 已推送的问题记录(按主机、durable agent 和工具调用区分),以确保重启后不会重复推送 | 问题关闭后保留一小时。至多 2000 条 |
缓存是 Chat 能快速打开并在 durable agent 响应缓慢时仍可正常工作的原因。已关闭的对话始终直接从 durable agent 读取,绝不读取自缓存。
durable agent 仅保留长对话的最新部分。若在 Collie 读取前它已丢弃了对话开头,Chat 会在对应位置显示一行文字:"Older messages were left out: the durable agent no longer keeps them." 手机端未丢失任何内容:Collie 从未获取过这些消息。
手机显示的内容
仪表盘上的 Durable agents 部分,以及每个 durable agent 对应的对话列表。
每个 durable agent 在 Durable agents 部分各占一行,包含其名称、Experimental 徽标、运行时以及运行在本地还是别处。右侧统计其未结对话数,并以需要您处理的颜色高亮等待您操作的对话。有待处理问题时,该部分会移至您的 space 之上。正在启动或不可达的 durable agent 会在计数位置显示状态说明。令牌被拒和传输协议版本被拒在手机上有专门的提示;collie doctor 会列出所有其他原因。
点击某一行可打开该 durable agent 的对话列表,类似于即时通讯应用的聊天列表。等待您处理的排在最前,其次是其他未结对话,最后是带有 Closed 标签的已结对话。未结对话在 Chat 中打开,已结对话以只读方式打开。durable agent 在其握手信息中声明其支持的功能(asks、starters、send、steer、abort、rename、close、actions、history、reopen),手机仅针对其声明的功能渲染对应控件。durable agent 没有屏幕,因此没有 Terminal 视图,也不支持按键输入。
向 durable agent 发送消息时,queued 标签是 Collie 自身对轮次的判定。Collie 不会显示 Pi 收件箱中排队的内容。休眠直至到期时间的任务会显示为空闲或工作中,绝不显示为等待中。详见 会话。
问题卡片会标明 durable agent 的名称,并在 durable agent 设置了超时时显示剩余时间。在 durable agent 中做出决定的问题或超时的问题会在聊天中留下一行。手机上的持久 agent 用图表展示了其中的每一种情况。
通过 bridge 进行 Send 和 Ask
durable agent 没有屏幕,因此 Collie 不会为其读取屏幕。bridge 改为读取自身的状态。durable agent 获得与终端窗格相同的 Send 和 Ask。
- 状态。 轮次来自其窗格状态。问题来自其最早的未决 ask。输入框始终为空,因为 durable agent 没有输入框。手机在获取每个窗格时都会收到该状态,结构与终端窗格相同。
- 一次发送。
POST /api/send从手机获取请求 id。bridge 将请求写入磁盘并锁定会话。然后,针对它所读取的轮次,将文本向 durable agent 发送一次。它的应答即为凭据,且收据中会标明它所获取的请求 id。在轮次进行期间,durable agent 会保留该文本以在该轮次之后使用,收据显示为queued。 - 结果。 发送的结束状态为
sent、带原因的refused或unknown。unknown表示 durable agent 未表明已接收该文本。在重新发送前请先查看聊天。当 durable agent 将发送内容写入会话时,该发送变为effective。其user项会标明该次发送的请求 id。只要有任何未决问题(即使是 Collie 无法读取的问题),普通发送就会被拒绝。回答该问题,或使用 Steer 或 Stop。在你查看之后才开始的轮次会拒绝此次发送,并且没有任何内容会送达 durable agent。 - 一个问题。
POST /api/ask/<id>回答它。该 ask id 仅供单次使用。回答携带你所看到问题的哈希。bridge 将其与 durable agent 当前列出的问题进行比对。当两者不一致时,任何内容都不会送达 durable agent。回复会显示当前状态的问题。回答通过其自身的 ask id 发送给 durable agent。绝不会发送任何密钥。收据显示为channel: program并保留该哈希。 - 卡片上保留的内容。 部分问题无法通过单次点击完成,因此状态将其显示为 Collie 无法读取的问题(
modal: unreadable)。你在其专属卡片上回答它们。- 文本问题保留在 durable agent 自身的卡片上,因为回答是你输入的一行内容。- 多项选择保留在 durable agent 自身的卡片上,因为回答包含多个选项。- 表单保留在 durable agent 自身的卡片上,因为回答是一组字段。- “始终允许”绝不是契约选项。其授权在该问题之后依然有效。因此 durable agent 的卡片会先要求进行第二次点击,通过/api/ask的单次点击不得跳过该点击。 - 计数器。
collie doctor在一行中统计这些读取和结果:program。
在此机器上,手机使用这些路由。普通发送走 /api/send,点击问题卡片走 /api/ask。编辑器在大拇指旁以 durable agent 的原话告知结果。当问题阻塞发送时,会显示一个跳转至其卡片的按钮。窗格的“最近发送”列表显示每张收据,排队的发送会注明状态。Steer 和 Stop 保持在会话自身的路由上,因为 steer 不属于 Send Protocol。crew 成员上的 durable agent 在进行所有写入时均保持使用会话自身的路由。每个路由都会在文本送达 durable agent 之前检查轮次。对单个会话的所有写入共享同一个锁。发送运行时,Steer 和卡片回答会因繁忙而被拒绝。Stop 会等待该锁,最多等到其时间限制,因此挂起的发送绝不会将 Stop 挡在外面。
版本
Collie 和 durable agent 在 hello 握手时协商传输协议版本,目前只有一个:版本 1。
Collie 支持 1..1。当版本 2 发布时,Collie 将在其后的一个次要版本中继续接受版本 1,再随后的次要版本则会弃用它。目前没有旧版本需要兼容。
当两个范围没有交集时,Collie 不再从 durable agent 读取任何内容。其日志会为每个拒绝的范围记录一行,例如 [durable] The durable agent speaks wire v2, this Collie speaks v1. Update Collie, or the agent's SDK. 手机上 durable agent 的主页会在警告框中显示相同的句子,其在仪表盘上的对应行会显示“版本不匹配”而非“无法访问”。collie doctor 在其 durable-hello 行显示两个范围:incompatible: the durable agent speaks 2..3, this Collie speaks 1..1。每当 durable agent 启动一次,Collie 也会向其发送一个带有自身版本的 hello,以便 durable agent 能在其侧记录该不匹配。请更新较旧的一方。
退出
仅当本仓库之外有人使用时,该特性才会保留。
到 1.24.0 版本发布时,必须满足以下两个条件之一:存在支持该传输协议的公共仓库,或者存在关于 durable agent 的外部 issue 或 discussion。如果两者都不存在,durable 相关的代码将移出正式发布并进入 experiments/,且此页面将被移除(ADR 0104)。移除时,sources.json 条目将在一个次要版本内被忽略并输出一行日志,socket 仍可用于你的 durable agent,npm 包将被废弃而非删除(ADR 0105)。