Paperclip 的 issues 与 agents 接口怎么用:字段、状态机与 409/400 的真实含义
拿到 Paperclip 的 API 之后,最先卡住人的往往不是鉴权,而是两组接口的语义:issues 是”工作单元”,agents 是”员工”,但文档里散落着一堆看起来很像细节、实际上会直接决定你代码对不对的规定。比如:PATCH 完一个 issue 到底还要不要再 GET 一次确认?checkout 返回 409 的时候该不该重试?上一次运行崩了、任务还挂在自己名下,新的运行怎么把它抢回来?
这些问题的共同点是:答案在官方文档里写死了,但如果你按”一般 REST API 的直觉”去写,几乎每一条都会写反。本文只讲 issues 和 agents 这两组接口的字段、状态流转和常见 4xx 的含义,鉴权方式与全局错误码表不在这里重复,那部分请看 Paperclip API 总览与鉴权。
issues 是什么:不只是一张任务表
官方对 issues 的定义是 Paperclip 里的工作单元,它支持层级关系、原子认领(checkout)、评论、issue 线程里的交互卡片、按 key 存的文本文档,以及文件附件。也就是说一个 issue 不是一行记录,而是一组挂在同一个 id 下的东西。
列表接口是 GET /api/companies/{companyId}/issues,只接受三个查询参数:
| 参数 | 作用 |
|---|---|
status | 按状态过滤,逗号分隔,例如 todo,in_progress |
assigneeAgentId | 按被指派的 agent 过滤 |
projectId | 按项目过滤 |
结果按优先级排序。单条查询用 GET /api/issues/{issueId},返回值里除了 issue 本身,还带 project、goal 和 ancestors(父链,且每一级都带上它自己的 project 和 goal)。另外还有三个和文档相关的字段:planDocument 是 key 为 plan 的 issue 文档全文(存在时才有),documentSummaries 是所有关联文档的元数据,legacyPlanDocument 是一个只读兜底——当 description 里还留着老式 <plan> 块时才会出现。
创建 issue 用 POST /api/companies/{companyId}/issues,可以带 title、description、status、priority、assigneeAgentId、parentId、projectId、goalId。
PATCH 的回执:写完不要再 GET 一次
更新走 PATCH /api/issues/{issueId},带上 X-Paperclip-Run-Id 头:
PATCH /api/issues/{issueId}
Headers: X-Paperclip-Run-Id: {runId}
{
"status": "done",
"comment": "Implemented caching with 90% hit rate."
}
可更新字段是这些:title、description、status、priority、assigneeAgentId、projectId、goalId、parentId、billingCode。其中 assigneeAgentId 在 PATCH 里比较宽松,既可以填 agent 的 UUID,也可以填同一家公司内的 shortname/urlKey。
这里有一条容易踩的规则:那个可选的 comment 字段不是”顺手加条评论”的便利功能。文档写明,涉及执行策略评审或审批决定时,决策评论必须放在同一次 PATCH 里;先单独调 POST /api/issues/{issueId}/comments 再 PATCH,是不满足阶段决策守卫的。
默认响应会返回完整的更新后 issue 行,外加两个附加字段:changes 是一张回执,只包含本次提交里真正发生变化的值;comment 是那次可选评论创建出来的对象,没有就是 null。changes 的每一项都有 from 和 to,请求里的空操作会被略过,所以没有可见变更时 changes 就是 {}。updatedAt 不会作为变更出现在里面。
{
"id": "issue-99",
"identifier": "PAP-99",
"title": "Implement caching layer",
"priority": "high",
"updatedAt": "2026-07-30T12:01:00.000Z",
"changes": {
"priority": { "from": "medium", "to": "high" }
},
"comment": null
}
回执里的 description 只取前 200 个字符,并带 updated: true 标记;title 的 from 或 to 任一超过 200 字符时,也按同样规则截断并加标记。注意截断只影响回执,完整响应体里的当前行值仍然是权威且未截断的。
如果只想要一张精简回执,用 Prefer: return=minimal,服务端会回 Preference-Applied: return=minimal,并且只返回 id、identifier、updatedAt、changes、comment 这五个字段。
请求里带了 blockedByIssueIds 时,响应还会多出三块:顶层的 blockedByIssueIds(规范化后已提交的 ID 数组)、blockedBy(阻塞本 issue 的那些 issue 摘要)、blocks(本 issue 阻塞的那些)。文档特意强调空数组是”确认为空”的状态,不是数据缺失——清空所有阻塞项后拿到 blockedByIssueIds: [] 和 blockedBy: [],就是真的没有了。
最关键的一句:PATCH 响应就是写入后的权威状态,2xx 之后再补一个 GET 确认是多余的。很多人习惯写”改完再读一遍”,在这里纯属浪费一次调用。
checkout 与 409:这个冲突不要重试
in_progress 状态不是随便 PATCH 出来的,它要求先认领:
POST /api/issues/{issueId}/checkout
Headers: X-Paperclip-Run-Id: {runId}
{
"agentId": "{yourAgentId}",
"expectedStatuses": ["todo", "backlog", "blocked", "in_review"]
}
这一步是原子的,成功后 issue 转到 in_progress。如果任务已经被别的 agent 持有,返回 409 Conflict,文档的原话是永远不要重试 409。这跟很多人写重试逻辑的默认习惯正好相反:409 在这里不是”暂时性失败”,而是”这活不是你的”,退避重试只会制造无意义的轮询。如果你本来就持有这个任务,再调一次是幂等的。
崩溃恢复是另一个容易漏的场景。上一次运行在持有任务、状态是 in_progress 时挂掉了,新的运行想把它抢回来,必须在 expectedStatuses 里显式写上 "in_progress":
POST /api/issues/{issueId}/checkout
Headers: X-Paperclip-Run-Id: {runId}
{
"agentId": "{yourAgentId}",
"expectedStatuses": ["in_progress"]
}
前一个运行确实已经不活跃时,服务端会接管这把陈旧的锁。还有一条硬规定:runId 不接受写在请求体里,它只能来自 X-Paperclip-Run-Id 头(由 agent 的 JWT 带出)。想主动交还所有权用 POST /api/issues/{issueId}/release。
关于 agent 卡住、心跳与看门狗的排查思路,另见 Agent 不干活怎么查。
状态机长什么样
官方给的生命周期图是这样:
backlog -> todo -> in_progress -> in_review -> done
| |
blocked in_progress
配套四条规则:in_progress 需要 checkout 且只能有单一负责人;started_at 在进入 in_progress 时自动写入;completed_at 在进入 done 时自动写入;终态是 done 和 cancelled。也就是说 in_review 可以退回 in_progress,blocked 是从 in_progress 岔出去的分支。整条流转在业务层怎么用,可以配合 任务从建到关的流转 看。
评论、交互卡片与文档
评论是 GET/POST /api/issues/{issueId}/comments,body 走 markdown。评论里的 @AgentName 提及会触发被提及 agent 的心跳——这是把消息变成”真的会被处理”的机制,不是单纯的文本标记。
交互(interactions)是 issue 线程里的结构化卡片,设计目的是让 agent 在需要队友挑任务、回答问题或确认提案时走 UI,而不是靠隐藏的 markdown 约定。支持的 kind 有五种:
| kind | 用途 |
|---|---|
suggest_tasks | 提议子 issue,交给董事会/用户接受或拒绝 |
ask_user_questions | 提出结构化问题并存下所选答案 |
request_confirmation | 请求接受或拒绝一个提案 |
request_checkbox_confirmation | 针对选中的选项 id 做一次接受/拒绝决定 |
request_item_verdicts | 逐项收集通过/拒绝/延后的裁定 |
创建时可以带 resolverPolicy,规范取值是 anyone、not_creator、human_only。不填就是 anyone,任何有普通 issue 访问权的队友都能响应;需要独立评审时用 not_creator;不允许 agent 拍板时用 human_only。旧的 board_or_agents 和 board_only 作为兼容别名保留,分别归一到 anyone 和 human_only。公司级治理通过 PATCH /api/companies/{companyId} 的 interactionResolverGovernance 按 kind 配置 defaultPolicy 和 cap,规则是治理只能收窄受众、不能放宽。
addresseeAgentId 可以点名同公司的某个 agent,被点名者会以 interaction_pending 唤醒,之后只有他或董事会用户能解决;创建者不能点自己,带 addressee 的工具动作确认会返回 400。解决路由有 accept / reject / respond / verdicts / withdraw 五个。文档还写明:payload.toolAction 的确认永远是 human_only;看门狗不享受任何特殊豁免,按普通 agent 评估;解决卡片只记录响应本身,后续的建任务、计划推进、工具调用、部署、花钱、招人、密钥等下游动作都要各自跑自己的授权与审批检查。
文档(documents)是按 key 存的、可修订的文本产物,key 例如 plan、design、notes。写入用 PUT /api/issues/{issueId}/documents/{key},规则很直接:新建时不要带 baseRevisionId,更新时必须带当前的 baseRevisionId,带了过期的值返回 409 Conflict。删除是 DELETE,当前实现里只有董事会能删。修订历史走 GET .../documents/{key}/revisions。
附件四个接口:上传是 POST /api/companies/{companyId}/issues/{issueId}/attachments(multipart/form-data),列表 GET /api/issues/{issueId}/attachments,下载 GET /api/attachments/{attachmentId}/content,删除 DELETE /api/attachments/{attachmentId}。注意上传路径带 companyId,其余三个不带。
agents 组:一个 agent 记录里有什么
GET /api/agents/me 返回当前认证 agent 的记录,字段结构最能说明 Paperclip 怎么建模”员工”:
{
"id": "agent-42",
"name": "BackendEngineer",
"role": "engineer",
"title": "Senior Backend Engineer",
"companyId": "company-1",
"reportsTo": "mgr-1",
"capabilities": "Node.js, PostgreSQL, API design",
"status": "running",
"budgetMonthlyCents": 5000,
"spentMonthlyCents": 1200,
"chainOfCommand": [
{ "id": "mgr-1", "name": "EngineeringLead", "role": "manager" },
{ "id": "ceo-1", "name": "CEO", "role": "ceo" }
]
}
reportsTo 加 chainOfCommand 就是汇报线,budgetMonthlyCents 和 spentMonthlyCents 把预算直接挂在 agent 上。创建时除了这些,还要给 adapterType(例如 claude_local)和 adapterConfig;更新用 PATCH /api/agents/{agentId},改 adapterConfig 和 budgetMonthlyCents 都走这里。agent 的日常增删改配置见 Agent 增删改与配置。
生命周期动作是一组独立的 POST 路由,语义各不相同:
| 路由 | 作用 |
|---|---|
POST /api/agents/{id}/pause | 暂时停止该 agent 的心跳 |
POST /api/agents/{id}/resume | 恢复被暂停 agent 的心跳 |
POST /api/agents/{id}/clear-error | 从 error 回到 idle,不删运行历史与运行时诊断;只有当前处于 error 的能清 |
POST /api/agents/{id}/terminate | 永久停用,官方标注为不可逆 |
POST /api/agents/{id}/keys | 生成长期 API key,完整值只显示一次 |
POST /api/agents/{id}/heartbeat/invoke | 手动触发一次心跳 |
另外三个查询类接口:GET /api/companies/{companyId}/org 返回完整组织树;GET /api/agents/{agentId}/config-revisions 看配置修订,配套 POST .../config-revisions/{revisionId}/rollback 回滚;GET /api/companies/{companyId}/adapters/{adapterType}/models 列出某个适配器类型可选的模型。模型列表这条有三点值得记:codex_local 在可用时会并入 OpenAI 的发现结果;opencode_local 从 opencode models 发现并以 provider/model 格式返回;opencode_local 没有静态兜底列表,发现不可用时这个列表可能是空的——所以前端不能假设它一定非空。
常见 4xx 对号入座
把两份文档里明确写了状态码的地方汇总一下:
| 状态码 | 出现位置 | 含义与处理 |
|---|---|---|
409 | POST /api/issues/{id}/checkout | 任务已被别的 agent 持有。不要重试,换一件事做 |
409 | PUT /api/issues/{id}/documents/{key} | baseRevisionId 过期。重新取最新修订再写 |
400 | GET /api/companies/{id}/agents | 该路由不接受任何查询过滤,传了不支持的参数就报错 |
400 | 创建带 addressee 的工具动作确认 | 工具动作确认不允许点名收件人 |
还有几处文档只说了”被拒绝”没给状态码:低信任与任务桥接(task-bridge)主体不能解决交互,低信任与任务看门狗的运行不能撤回交互,删除 issue 文档只有董事会可以。这几种情况官方文档未说明具体返回码,写客户端时建议按”权限类失败”统一处理,不要猜。
什么时候这篇不够用
这篇只覆盖 issues 和 agents 两组接口本身。有几类问题它答不了:
一是鉴权、scope 与全局错误约定。文档提到 agent 解决交互需要已认证的运行身份和 issue:mutate scope,但 scope 体系本身不在这两份文档里。
二是 goals、projects、approvals、costs、activity 这些相邻资源的字段,它们各有独立文档,这里只在 issue 的 goalId/projectId 上露了个头。
三是运行时行为。checkout 说”前一个运行不再活跃时会接管陈旧锁”,但”不再活跃”具体怎么判定、判定要多久,这两份 API 文档没有写明;同理,心跳的调度周期、看门狗的判定阈值也不在这里。
四是 UI 侧表现。交互卡片在界面上长什么样、董事会用户在哪儿看到待处理项,本文一概不涉及——我们只读了 API 文档,没有跑过这套系统。
最后提一句实践顺序:写客户端时建议先把 checkout/release 这对拿稳(409 不重试、崩溃后带 in_progress 重认领),再处理 PATCH 回执(信 changes、别补 GET),最后才去碰 interactions。前两块搞反了,后面的交互逻辑再漂亮也会在并发下漏任务。
延伸阅读
- 从头读起:Paperclip 是什么:一个自己不跑 Agent 的控制平面,怎么管住一整家 AI 公司
- 本专题共 40 篇,完整分组目录见专题页
- Paperclip 的目标与项目层怎么组织:goals、projects 与 workspaces 三层接口拆解
- Paperclip 流水线(pipelines)与例行任务(routines):阶段、关卡、触发器怎么配
本文依据 Paperclip 官方仓库(github.com/paperclipai/paperclip,MIT 协议)的 docs/ 用户文档
与 doc/ 下的规范、运维与连接器手册整理,核对日 2026-08-17。
我们没有部署或运行过 Paperclip,因此不涉及界面外观与操作手感;
部分规范文档描述的是目标架构而非当前实现,文中已就地标注,不构成对实际行为的保证。
请以仓库最新内容为准。