跳转至正文
ColliePWA

09/Documentation

持久 SDK(实验性)

逐项介绍 npm 包 @colliepwa/durable-sdk:入口与运行时、openCollie、窗格方法、四类提问与 requireApproval、hook、历史记录、权能(caps)、辅助函数、Pi 粘合层以及版本

实验性功能。 该软件包、其 API 以及它所使用的传输协议可能会在后续次要版本中发生变更,该功能也可能会被移除。版本 说明了这对你的 durable agent 意味着什么。

@colliepwa/durable-sdk 是 durable agent 用来通过 Collie 在手机上展示自身的 npm 软件包。本页列出了它的所有组成部分。持久 agent 提供了快速上手指南以及 Collie 端的设置,durable 线路 解释了两者之间传输的内容。

安装与入口

一次安装提供五个入口:适用于大多数 durable agent 的根导入,以及适用于更特殊需求的四个入口。

npm install @colliepwa/durable-sdk@experimental
# or
bun add @colliepwa/durable-sdk@experimental
导入包含的内容
@colliepwa/durable-sdkopenCollie 及完整 API。在 Bun 上,它使用 Bun 服务器提供套接字服务,在其他环境下则使用 node:http 服务器
@colliepwa/durable-sdk/bun相同的 API,始终使用 Bun 服务器
@colliepwa/durable-sdk/node相同的 API,始终使用 node:http 服务器,加上 NO_DESCENDANT_CHECK(即启动时写入的行)
@colliepwa/durable-sdk/core将布局和连接整合为一个函数 handle(request),适用于自带传输层的 durable agent。无运行时导入,因此只要支持 Web Request 即可运行
@colliepwa/durable-sdk/pi基于 Pi Durable (Pi 辅助工具) 构建的 durable agent 胶水代码

该包需要 Node 22.19 或更高版本,或 Bun 1.4 或更高版本,且没有运行时依赖。./pi 需要 @earendil-works/pi-durable、@earendil-works/pi-ai 和 @earendil-works/chord,版本需为 1.1 或更高且低于 2。入口文件已在 Pi Durable 1.1.0 和 1.2.0 上测试。这三个包为可选对等依赖:从不导入 ./pi 的 durable agent 无需安装任何一个。

运行时决定 durable agent 能否判断应答者身份。后代检查会拒绝来自 durable agent 本身或其启动进程的操作,返回 403 caller-refused。它涵盖 answer、send、stop、action、rename、close、reopen 和 starter。读取操作保持打开状态。

运行时后代检查设置的值
Linux 上的 Bun默认开启。Bun 从套接字读取调用者的进程 ID无。callerCheck: "off" 将其关闭
macOS 上的 Bun,或除 Linux 外的任何其他系统无法运行。启动失败并报错,错误信息会指明 callerCheckcallerCheck: "off"
Node,任意系统无法运行:Node 无法读取套接字的调用者。durable agent 在启动时会写入一行说明此情况无。不会读取 callerCheck

如果不进行检查,当前用户的任何进程都可以应答卡片,包括 agent 运行的工具。请在无法访问套接字的沙箱中运行 agent 的工具。该套接字需要支持文件所有者的系统,因此在 Windows 上 openCollie 会在启动时停止。在 Windows 上,durable agent 改为在 TLS 后通过令牌提供 ./core 服务 (访问 durable agent 的两种方式)。

打开连接

openCollie 创建专用套接字,其返回的句柄负责排版手机上显示的内容。

import { openCollie } from "@colliepwa/durable-sdk";

const collie = await openCollie({ hostName: "desk" });
const space = collie.workspace("support", { title: "Support" });
const tab = space.tab("main", { title: "Tickets" });
const pane = tab.pane("ticket-1", { title: "Maria Lopez", harness: "desk" });
collie.ready();

如果不带 socket 选项,路径来自 COLLIE_DURABLE_SOCKET,这也是 Collie 读取的变量。

选项作用默认值
socket套接字路径:绝对路径,最多 103 字节。其目录以模式 0700 创建,或者必须已归你所有且权限模式为 0700COLLIE_DURABLE_SOCKET 变量。若两者均未设置:启动失败
hostNamedurable agent 在 hello 中声明的自身名称,截短至 64 字节。必填无
tailLimit每个窗格为 Collie 读取保留的原始事件数量500,最多 1000
capsdurable agent 声明其支持的能力 (能力集 (Caps))SDK 支持的所有功能
callerCheckdescendant 或 off。仅在 Bun 上读取descendant
hooks手机发送的每个操作对应一个具名位置(钩子与历史记录)无
warnSDK 写入单行备注的位置,例如记录使用其他传输协议版本的 Collieconsole.warn
onRename已弃用。hooks.onRename 的旧名称,保留一个版本,不包含请求 ID无

仅 createCore 上存在另外两个选项,openCollie 上没有:receipts 和 allowAlways(提问)。

在句柄上具体含义
address其监听的 socket 路径
epoch每次启动时生成全新的随机值,128 位
coresocket 背后的核心。测试或第二传输层会直接调用 core.handle
workspace(id, { title, cwd? })工作区,每个 ID 创建一次。其 tab(id, { title }) 产生一个标签页,标签页的 pane(id, options) 产生一个窗格
starters(list)手机可从“新建”页面启动的内容,最多 50 项。每项为 { id, label, run },且 run 解析为其所显示窗格的 { paneId }
history(provider)提供持久化 agent 自身的会话记录服务(钩子与历史记录)
ready()在显示每个窗格后调用一次,并在重启且所有未完成的 ask 重新发起后调用一次。在此之前,除 hello 外的所有路由均返回 503 starting
close()停止服务器。正在发送途中的回复仍会发出,最多持续 2 秒,随后 socket 文件被移除

close() 特意保留未完成的 ask:关机并不算作回复,由持久化 agent 决定如何处理等待该 ask 的工作。

存在对应 ID 的窗格时,tab.pane(id, { title, harness, cwd?, sessionId? }) 会返回该窗格,因此持久化 agent 无需第二个窗格即可将同一会话显示两次。标题截断至 256 字节,harness 截断至 64 字节。cwd 必须为绝对路径,否则会被忽略。

每个 ID 均遵循 ID 规则:1 到 128 字节,无控制字符,无空白字符,且不包含 /、? 或 #。无效 ID 会导致 TypeError。工作区或标签页 ID 不能包含 :,因为协议传输格式将标签页命名为 <workspace>:<tab>。

窗格

窗格对应单个会话,其方法用于设置手机上显示的内容。

方法手机上显示的内容限制
state(s)working、waiting 或 idle。更改 working 会开启新一轮交互存在任何未完成的 ask 时,窗格显示 waiting
event(e)Chat 的条目:item、remove、settled、reset 和 synced 事件参见下方的条目表
subtitle(text)标题下方的一行内容。null 可将其清除120 个字符
badge(value)标题旁的 { count } 或 { text }。null 可将其清除0 到 9999 的计数,或 12 个字符的文本
progress(value)窗格运行或等待时绘制的 { kind: "steps", steps } 或 { kind: "percent", value, label? }。null 可将其清除12 个步骤,每个标签 60 个字符,取值范围 0 到 100
actions(list)位于 belt 中 durable agent 区域的按钮:{ id, label, icon?, busy?, disabled? }。null 可将其清除4 个按钮,标签长度为 16 个字符
onAction(fn)对其中一个按钮执行点击。返回 false 可予以拒绝手机不会对 busy 或 disabled 按钮发送点击事件
onSend(fn)接收手机文本,类型为 fn(text, { steer, requestId })32 KiB 文本
onAbort(fn)接收 Stop,类型为 fn({ requestId })无
ask(input, { signal? })问题卡片(提问)每个窗格允许 50 个未完成的 ask
updateAsk、decideAsk、outcomeOf修改、决定或回读 ask(提问)无
close()从布局中移除该窗格。其未完成的 ask 会作为已停止状态关闭无

窗格还包含四个只读字段:id、sessionId、turnId(正在运行的轮次,或最后一个轮次)以及 openAsks。操作的 icon 为 play、pause、check、x、refresh、archive、flag、star、send、note、search 和 more 之一。使用其他名称则不绘制图标。

item 事件携带一个包含 id 和可选 ts 的项目。带有已知 ID 的项目会就地替换原有项目。

类型包含的内容限制
user用户发送的一行文本。requestId 指定了其来源的发送操作,因此手机会用该行替换自身的行64 KiB 文本
reply、thinking、noticeAgent 输出的内容64 KiB 文本
markdown手机渲染为 Markdown 的文本:标题、列表、强调、行内代码和链接32 KiB 文本
code代码块,包含诸如 ts 的 lang32 KiB 文本。lang 为 1 到 24 个字母、数字或 _ + # . -
link一个 href 和一个可选的 labelhref 仅限 https://,最多 2048 个字符。label 为 120 个字符
tool工具调用:tool: { kind: "other", name, summary, output? } 以及状态为 running、done、failed 或 denied 的 statusname 128 字节,summary 1 KiB,output 16 KiB

reply、thinking、notice 或 markdown 项目在其文本增长时可携带 partial: true。在文本增长期间重复写入相同的 ID(最多每 250 毫秒一次),并在窗格离开 working 之前,以不带 partial 的最终文本再次写入一次。

超出限制的文本会被截断,控制字符和不可见字符会被移除。无效的 lang、无效的 href、无效的徽章或无效的进度会被改为 TypeError 或 RangeError,因为 Collie 会将其丢弃。durable agent 本身不得追加 ask、ask-closed 或 state 事件:这些由 ask() 和 state() 写入。

提问

pane.ask 显示卡片并返回答案,而 requireApproval 则用一行询问单个工具调用。

import { requireApproval } from "@colliepwa/durable-sdk";

const args = { order: "5521", amount: 40 };
const approval = await requireApproval("refund", args, {
  pane,
  toolCallId: "call-1",
  timeoutMs: 10 * 60_000,
});
if (!approval.approved) throw new Error(`refund not approved: ${approval.decidedBy}`);

requireApproval(toolName, args, options) 根据调用本身构建卡片,绝不根据模型对此调用的描述构建。标题为“refund wants to run”,工具行为 refund(order: "5521", amount: 40),推送提示为“refund needs a yes”,按钮为 Allow 和 Deny。它返回 { approved, decidedBy, decidedAt, askId }。拒绝、超时、停止和中止均返回 approved: false。

requireApproval 的选项作用默认值
pane发起询问的窗格。必填无
toolCallId工具调用自身的 ID,重启后保持一致。可以是任意字符串:不符合 ID 规则的字符串会被转义以保证安全必填
itemId卡片所属的项工具调用 ID
timeoutMs手机拥有的超时时间。超过后调用会被拒绝,由 timeout 决定无限制
signal一个 AbortSignal。中止操作会拒绝调用,由 program 决定无
labels两个按钮的文本,{ allow?, deny? }允许和拒绝

callSummary(name, args) 和 argumentsView(args) 是它使用的两个部分:单行的调用信息,以及作为 key: value 键值对的参数,每个值均为 JSON。

pane.ask(input) 接受四种类型之一。Promise 会解析为对应类型结构的结果。

kind卡片显示的内容回答限制
permission每个选项对应一个按钮,各带有一个 effect:allow、allow-always、deny 或 cancel{ optionId, note? }1 到 6 个选项
choice每个选项对应一个按钮。使用 multiple: true 时用户可以选择多项{ optionIds }1 到 8 个选项
text一个文本字段,带有可选的 placeholder{ text }maxLength 取值 1 到 2000,默认为 500。占位符为 60 个字符
form1 到 6 个字段,每个字段类型为 text、choice 或 toggle,部分为 required{ values },以字段 ID 为键单选字段包含 1 到 8 个选项

每种类型均接受 toolCallId、itemId 和 title(256 字节)。还可以接受 reason(2 KiB)、tool、push: { title?, line? }(60 和 120 个字符,通知文本)以及 expiresAt(Date 或带时区的 ISO 8601 字符串)。

省略 id。SDK 会通过 askIdFor(paneId, toolCallId) 推导该值:对两个 ID 计算 SHA-256 并取前 32 个十六进制字符。因此重启后再次发起的相同工具调用会获得相同 ID,手机在重启期间发送的回答也能正确送达。发起一个已处于打开状态的 ID 会返回 TypeError。

在窗格上作用
updateAsk(askId, patch)就地修改处于打开状态的询问:title、reason、tool、options、placeholder、maxLength 或 fields。其版本号会加一,因此针对旧表单的回答会收到 409 stale,手机端会要求重新点击。若询问未处于打开状态则为 false
decideAsk(askId, answer?)持久化 agent 在手机之外的其他位置自行决定了该询问。若带有 answer,等待中的 ask() 会随之解析。若不带,则会拒绝并抛出 AskClosed。随后在手机上的点击会收到 409 decided,卡片显示“已在持久化 agent 中处理”。若询问未处于打开状态则为 false
outcomeOf(askId)首次决策:{ decidedBy, decidedAt, optionId?, effect? }。窗格会保留最近 500 条记录。对于从未发起或已遗忘的询问,返回 undefined

decidedBy 为 phone、program 或 timeout。询问超过其 expiresAt 后会关闭,判定结果为 timeout:ask() 拒绝并抛出 AskClosed,其 decidedBy 为 timeout。卡片显示截止前的剩余时间。超时或在持久化 agent 中决定的问题会在 Chat 中留下一行内容,例如“超时未回答:refund 想要运行”。

中止的 signal、pane.close() 以及手机端的关闭操作都会关闭该询问,判定结果为 program,且 ask() 拒绝并抛出 AskClosed。

注意。 带有 allow-always 的选项在回答一次后会放行后续的所有调用。在 https 下,token 是唯一的校验屏障,因此持久化 agent 在此时仅提供“允许”和“拒绝”。带有 allowAlways: false 的 createCore 会使 allow-always 选项变为 TypeError。对于文本被截断、清理,或者未指定工具名称的询问,手机端不会提供 allow-always。

手机端发起的每个动作都带有请求 ID,核心层会保留其回执:重复请求不会执行任何操作,而是直接返回第一次的回复并带有 replayed: true。默认的 MemoryReceiptStore 会为每个窗格保留最新的 RECEIPTS_PER_GROUP(1000 个),并为启动项、重命名、关闭和重新打开操作按组保留相应数量。若重复请求已被丢弃的 ID,则会收到 500 receipt-dropped 且不执行任何操作。该存储会随进程一起消失。

若要在重启后保留回执,请编写一个包含 get(requestId) 和 put(requestId, record, group) 的 ReceiptStore,并将其作为 receipts 传给 createCore。openCollie 始终将其保存在内存中。

钩子与历史记录

hook 是承接手机端所发送动作的唯一具名入口,history provider 则用于提供你自己的记录。

hook运行时机拒绝方式
onSend(pane, { text, steer, turnId, requestId })发送给没有 pane.onSend 的窗格的文本抛出异常:500 internal,对手机端而言结果未知
onAbort(pane, { turnId, requestId })针对没有 pane.onAbort 的窗格执行停止抛出异常,同上
onAction(pane, { actionId, turnId?, requestId })在没有 pane.onAction 的窗格上点击 belt返回 false:422 action-refused
onRename({ kind, id, title }, requestId)工作区或标签页的新标题返回 false:422 rename-refused
onClose({ kind, id, paneIds }, requestId)手机端关闭窗格、标签页或工作区返回 false:422 close-refused
onReopen({ id, paneId? }, requestId)手机端重新打开已关闭的会话返回 false:422 reopen-refused

hook 仅在其对应 cap 处于开启状态且请求 ID 为全新时才会运行。拒绝请求不会存储任何内容,因此相同的请求 ID 可以再次尝试。窗格专属的 handler 优先级高于 hook。两者均未配置的窗格在收到发送、停止或点击操作时,会回复 409 not-supported。

SDK 会自行应用已被接受的动作。重命名操作会设置标题。关闭操作会移除目标及其内部的所有内容,并将其未完成的请求按已停止状态关闭。在没有 hook 的情况下,重新打开操作会恢复当前进程关闭的窗格;在重启前关闭的会话则需要 onReopen。界面布局在每次启动时都会重新生成,因此如果 durable agent 希望标题或关闭状态在进程退出后继续保留,应将其存储在自己的 hook 中。

collie.history(provider) 会从 durable agent 自身的存储中提供所有已打开和已关闭的会话。当省略 caps 或指定为 history: true 时,它会开启 history cap。

HistoryProvider返回值
list(after, limit){ items, next? }:会话行列表,最新的排在最前,最多 limit(100)条
items(id, after, limit){ items, next? }:该会话的条目,最旧的排在最前,每个条目均为最新版本,最多 limit(1000)条。对于未知的 ID 返回 undefined

一行对应 { id, title, subtitle?, workspaceId?, tabId?, startedAt, endedAt?, state, items },其中 state 为 open 或 closed,items 为数量。游标(after、next)为你自定义的字符串,由 1 到 512 字节的不含空格的可打印 ASCII 字符组成。缺少 next 即表示列表结束。

设置 provider 之后,每个窗格的 sessionId 即为其窗格 ID,history 会在该 ID 下提供该窗格的会话。向 tab.pane() 传入 sessionId 可以指定其他 ID,如果在重启后无法保持相同则传入 null。

tailLimit 不是 history。它是每个窗格为 Collie 读取事件保留的原始末尾记录,默认保留 500 个事件。落后于该记录的读取端将转而获取该窗格当前的完整视图,最多为最新的 2000 个条目。

能力集 (Caps)

cap 表示 durable agent 具备执行某项操作的能力,手机端仅会针对 durable agent 声明过的 cap 绘制对应控件。

上限durable agent 接收的内容
asks提出问题并接收回答
starters列出启动项,并从“新建”页面运行其中一个
send接收来自手机端的文本
steer将文本输入到正在运行的轮次中。需要 send
abort停止正在运行的 turn
rename为工作区或标签页设置新标题
close关闭窗格、标签页或工作区
actions列出窗格上的 belt 按钮并运行其中一个
history提供其对话服务,包括已打开和已关闭的对话
reopen重新显示已关闭的对话

若省略,caps 会启用 SDK 能执行的所有功能,且一旦 history 拥有 provider 也会启用 history()。若指定,则为完整列表:未列出的 cap 会被关闭,因此 { asks: true } 仅声明 ask。若要关闭某项并保留其余项,请展开 ALL_CAPS:{ ...ALL_CAPS, steer: false }。cap 已关闭的路由会返回 409 not-supported。terminal 始终为 false。

辅助函数与常量

根导入还会导出构成 openCollie 的各部分及其应用的规则。

export具体含义
createCore(options)不带服务器的核心。它接受 openCollie 选项(除 socket 和 callerCheck 外),外加 receipts 和 allowAlways。它返回 epoch、workspace、starters、history、ready、isReady 和 handle(request)
handle单函数形式的传输层:传入 Web Request,输出 Response。core.handle(request) 是常用的入口方式
wireListener(core)作为 node:http 请求侦听器的 core.handle,带有用于干净关闭的 drained()
hello、versionLinehello 回复,以及持久化 agent 在拒绝版本时写入的单行内容
mintId()新的随机 ID,128 位,表示为 32 个十六进制字符
askIdFor(paneId, toolCallId)派生的 ask ID
AskClosed未收到回答时 ask() reject 抛出的错误。它携带 by 和 decidedBy
idOk(id)、assertId(id, what)、safeId(raw)ID 规则:检查项、抛出异常的检查项,以及从任意字符串生成的安全 ID(每次生成均相同)
cursorOk(cursor)历史记录游标规则
cut(text, max, multiline?)清理字符串并截断至 max 字节,末尾追加 …
hrefOf、langOf、badgeOf、progressOf链接目标、代码语言、角标和进度,按窗格的检查方式进行检查
subtitleOf、pushTextOf、renameTitleOf副标题、ask 的推送词和重命名标题,经清理并截断
CAPS、LIMITS每个字段的字节上限,以及每个列表的容量上限
ACTION_LABEL_MAX、BADGE_TEXT_MAX、BADGE_COUNT_MAX16、12 和 9999
SUBTITLE_MAX、LINK_LABEL_MAX、RENAME_TITLE_MAX各 120 个字符
PROGRESS_STEPS_MAX、PROGRESS_LABEL_MAX12 个步骤,60 个字符
PUSH_TITLE_MAX、PUSH_LINE_MAX、PLACEHOLDER_MAX60、120 和 60 个字符
ANSWER_TEXT_MAX、ANSWER_TEXT_DEFAULT、ANSWER_TEXT_BYTES2000 和 500 个字符,以及 8000 字节
ALL_CAPS、PROTOCOL_RANGE、SOCKET_ENV启用所有 cap、SDK 支持的传输协议版本,以及名称 COLLIE_DURABLE_SOCKET
MemoryReceiptStore、RECEIPTS_PER_GROUP默认回执存储,以及每组 1000 条的限制

线缆传输上的每个类型和每个挂钩有效负载也都由 src/core/types.ts 导出。

Pi 辅助工具

./pi 将 Pi Durable 对话转换为窗格,将高风险工具调用转换为卡片,并将 Pi 存储转换为历史记录。

export作用
showConversation(loc, harness, conversation, o)将对话显示为窗格,先显示历史记录,再显示实时事件。发送操作会带上请求 ID 转到 Pi 的 submit,停止操作会转到其 abort。重复调用是安全的
PaneRegistry哪个窗格显示哪个对话。重启后,reRaised(risky) 会等待每个没有结果的高风险调用重新发起询问,因此 ready() 位于询问之后
riskGuard({ panes, risky, words?, timeoutMs?, onTimeout? })一个 Pi 扩展,其 beforeTool 挂钩将对 risky 中工具的调用转换为“允许”和“拒绝”卡片。被拒绝的调用会被拦截。配合 timeoutMs,卡片会过期,无人回应的调用会被拦截,且 onTimeout(tool, title) 允许 durable agent 记录该事件
askTools({ panes, words, timeoutMs?, onTimeout? })服务台的两个询问工具:ask_queue(单选)和 ask_customer(文本询问)。配合 timeoutMs,未回复的询问会过期,工具返回 "timed out"
spawnHelperTool(o)工具 spawn_helper:显示为新窗格的子对话,返回助手的回答
piHistory({ harness, panes, describe })读取每个 Pi 对话中每条条目的 HistoryProvider
firstMessageOf(conversation)对话首条消息的文本,用于命名其窗格
paneIdOf(id)、conversationOfPaneId(paneId)Pi 对话的窗格 ID,conv-<id>,以及反向映射
summaryOf(args, name?)工具调用的文字参数,最多 200 个字符
Mapper作为项呈现的 Pi 条目和代理事件。递增生成的回复保留同一个项 ID
PARTIAL_MS、RE_RAISE_MS递增生成回复的两次写入间隔 250 毫秒,以及 reRaised 最多等待的 5000 毫秒
ESCALATED示例词汇:showConversation 使用 escalate: true 添加的“升级”按钮的服务台通知
ASKING_TOOLS示例词汇:服务台两个询问工具的名称
Submit、ReplyExtras自定义 submit 的类型,以及 durable agent 在已完成回复下添加的项的类型

卡片显示的文字作为选项传入,因此除这两个常量外,./pi 不包含示例的任何文本。它是针对 Pi Durable 1.1.0 编写的。examples/durable-member/ 中的服务台将所有部分组合在一起。

版本

该软件包有独立于 Collie 的版本,并声明其支持的有线协议版本。

今天含义
SDK_VERSION0.1.0软件包版本。在软件包脱离 dist-tag experimental 之前,次版本更新可能会更改 API
WIRE_VERSION1软件包支持的 durable 有线协议版本

Collie 支持一个有线协议版本,并在新版本发布后的一个次版本内兼容前一个版本。当 Collie 不支持你的任何版本时,核心会通过 warn 输出一行内容,每个版本一次,例如 collie: Collie speaks wire v2, this durable agent speaks v1. Update the agent's SDK, or Collie.。手机端会在 durable agent 主页上用 Collie 自己的提示语说明不匹配情况。请更新较旧的一方。

该软件包发布在 npm dist-tag experimental 下,绝不会发布为 latest。如果 durable 有线协议弃用并停留在 退出,该软件包将被弃用(ADR 0105)。

在 GitHub 上编辑本页

The rest of the documentation