Paperclip 的 issues 与 agents 接口怎么用:字段、状态机与 409/400 的真实含义

2026-08-17

拿到 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 本身,还带 projectgoalancestors(父链,且每一级都带上它自己的 project 和 goal)。另外还有三个和文档相关的字段:planDocument 是 key 为 plan 的 issue 文档全文(存在时才有),documentSummaries 是所有关联文档的元数据,legacyPlanDocument 是一个只读兜底——当 description 里还留着老式 <plan> 块时才会出现。

创建 issue 用 POST /api/companies/{companyId}/issues,可以带 titledescriptionstatuspriorityassigneeAgentIdparentIdprojectIdgoalId

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."
}

可更新字段是这些:titledescriptionstatuspriorityassigneeAgentIdprojectIdgoalIdparentIdbillingCode。其中 assigneeAgentId 在 PATCH 里比较宽松,既可以填 agent 的 UUID,也可以填同一家公司内的 shortname/urlKey。

这里有一条容易踩的规则:那个可选的 comment 字段不是”顺手加条评论”的便利功能。文档写明,涉及执行策略评审或审批决定时,决策评论必须放在同一次 PATCH 里;先单独调 POST /api/issues/{issueId}/comments 再 PATCH,是不满足阶段决策守卫的。

默认响应会返回完整的更新后 issue 行,外加两个附加字段:changes 是一张回执,只包含本次提交里真正发生变化的值;comment 是那次可选评论创建出来的对象,没有就是 nullchanges 的每一项都有 fromto,请求里的空操作会被略过,所以没有可见变更时 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 标记;titlefromto 任一超过 200 字符时,也按同样规则截断并加标记。注意截断只影响回执,完整响应体里的当前行值仍然是权威且未截断的。

如果只想要一张精简回执,用 Prefer: return=minimal,服务端会回 Preference-Applied: return=minimal,并且只返回 ididentifierupdatedAtchangescomment 这五个字段。

请求里带了 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 时自动写入;终态是 donecancelled。也就是说 in_review 可以退回 in_progressblocked 是从 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,规范取值是 anyonenot_creatorhuman_only。不填就是 anyone,任何有普通 issue 访问权的队友都能响应;需要独立评审时用 not_creator;不允许 agent 拍板时用 human_only。旧的 board_or_agentsboard_only 作为兼容别名保留,分别归一到 anyonehuman_only。公司级治理通过 PATCH /api/companies/{companyId}interactionResolverGovernance 按 kind 配置 defaultPolicycap,规则是治理只能收窄受众、不能放宽。

addresseeAgentId 可以点名同公司的某个 agent,被点名者会以 interaction_pending 唤醒,之后只有他或董事会用户能解决;创建者不能点自己,带 addressee 的工具动作确认会返回 400。解决路由有 accept / reject / respond / verdicts / withdraw 五个。文档还写明:payload.toolAction 的确认永远是 human_only;看门狗不享受任何特殊豁免,按普通 agent 评估;解决卡片只记录响应本身,后续的建任务、计划推进、工具调用、部署、花钱、招人、密钥等下游动作都要各自跑自己的授权与审批检查。

文档(documents)是按 key 存的、可修订的文本产物,key 例如 plandesignnotes。写入用 PUT /api/issues/{issueId}/documents/{key},规则很直接:新建时不要带 baseRevisionId,更新时必须带当前的 baseRevisionId,带了过期的值返回 409 Conflict。删除是 DELETE,当前实现里只有董事会能删。修订历史走 GET .../documents/{key}/revisions

附件四个接口:上传是 POST /api/companies/{companyId}/issues/{issueId}/attachmentsmultipart/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" }
  ]
}

reportsTochainOfCommand 就是汇报线,budgetMonthlyCentsspentMonthlyCents 把预算直接挂在 agent 上。创建时除了这些,还要给 adapterType(例如 claude_local)和 adapterConfig;更新用 PATCH /api/agents/{agentId},改 adapterConfigbudgetMonthlyCents 都走这里。agent 的日常增删改配置见 Agent 增删改与配置

生命周期动作是一组独立的 POST 路由,语义各不相同:

路由作用
POST /api/agents/{id}/pause暂时停止该 agent 的心跳
POST /api/agents/{id}/resume恢复被暂停 agent 的心跳
POST /api/agents/{id}/clear-errorerror 回到 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_localopencode models 发现并以 provider/model 格式返回;opencode_local 没有静态兜底列表,发现不可用时这个列表可能是空的——所以前端不能假设它一定非空。

常见 4xx 对号入座

把两份文档里明确写了状态码的地方汇总一下:

状态码出现位置含义与处理
409POST /api/issues/{id}/checkout任务已被别的 agent 持有。不要重试,换一件事做
409PUT /api/issues/{id}/documents/{key}baseRevisionId 过期。重新取最新修订再写
400GET /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 官方仓库(github.com/paperclipai/paperclip,MIT 协议)的 docs/ 用户文档 与 doc/ 下的规范、运维与连接器手册整理,核对日 2026-08-17。 我们没有部署或运行过 Paperclip,因此不涉及界面外观与操作手感; 部分规范文档描述的是目标架构而非当前实现,文中已就地标注,不构成对实际行为的保证。 请以仓库最新内容为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。