10 KiB
10 KiB
plane-agentd 设计
Plane 看板驱动的通用 agent 调度服务:任何人/agent 在 Plane 建工作项并指派给一个"终端",agentd 在该终端起对应的 CLI agent(Codex、Claude Code、脚本…)执行,把结果贴回工作项并流转状态。调度是确定性代码,模型只出现在执行器和总指挥里。
1. 目标与非目标
- 目标:多项目隔离(一份配置一段)、多终端(本机/经 Warpgate 的远程机)、多 CLI(执行器适配器)、多轮(线程持久、进程不常驻)、失败可解释可重试、凭据只经 Vault 注入内存、任何会话可接手。
- 非目标:不是工作流引擎(无 DAG/分支,顺序交给 Plane "blocked by"——TODO:阶段 1 尚未实现,调度不读 blocked-by 关系,被阻塞的 Todo 项照样会被拾起);不替代 Plane/Temporal 的界面;不做账号系统;不存密钥。
2. 架构
Plane ──webhook v2(HMAC)/ 轮询兜底──► agentd(接收端 + 调度核心 + 执行器适配器)
│ 按 assignee→终端、label→执行器/模型,串行组,重试,超时,心跳
┌───────────────┼───────────────┐
codex(远程/本机) claude-code(本机/远程) shell
└───────────────┴───────────────┘
执行器只产出"最后消息 + 产物路径";agentd 统一回帖、改状态、通知(ntfy)
阶段 3 之后调度核心搬到 Temporal(workflow = 一个工作项的生命周期,activity = 起执行器/等结果/回帖),用 Temporal UI 看运行与重试。
3. 概念与契约
- 终端(terminal):怎么到达一台机器 —
transport(local | warpgate-ssh)、os、launch(subprocess | schtasks | nohup)、repo、plane_user(回帖身份,对应 Vault 键)、concurrency、capabilities、可用executors。 - 执行器(executor):某个 CLI 的适配器 — 起一轮的命令模板、续线程命令模板、
resume: true|false、可用模型、默认推理强度、沙箱参数、最后消息取法、线程 ID 取法。 - 路由(routing):
by_assignee(Plane 用户 → 终端)、by_label(exec:*、model:*、effort:*、needs-*串行组、cap:*能力、budget:*时限)、每终端默认执行器。 - 工作项契约:正文 = 任务书;评论 = 消息;执行器最后消息首行 ∈
[review] | [question] | [blocked] | [progress];agentd 回帖格式固定(轮次、run id、退出码、日志路径)。 - 状态机:Backlog → Todo(指派即派单)→ In Progress → Waiting(question/blocked,指派给应答者)→ Review(指派给验收者)→ Done / Cancelled。
[progress]保持 Todo 并续线程。 - 线程:每工作项一个执行器线程 ID(Codex
exec resume,Claude-p --resume);Plane 评论是长期记忆,线程只是缓存;换终端/换执行器/线程损坏/轮次超阈值 → 重建线程,首轮输入附自动交接摘要并在评论注明。
4. 失败策略
| 情况 | 动作 | 状态 |
|---|---|---|
执行器回 [blocked]/[question] |
原样回帖 | Waiting → 应答者 |
| 退出码≠0 / 无最后消息 | 结构化失败说明(类型 crash/timeout/env/logic、run、exit、重试 n/N、日志、最后一帧) | 临时性(网络/中继/429/5xx/超时)自动重试 ≤3,退避 2/5/10 min,续原线程;否则 Waiting |
超时(默认 4 h,budget:* 覆盖) |
结束任务进程,失败说明 | Waiting |
| 环境类(焦点丢失/弹窗/断网) | 当 blocked 处理,暂停同串行组的派发 | Waiting |
| 运行中工作项被移出 In Progress | 结束任务进程,回帖"运行已取消:工作项状态被改为 X",释放终端槽位/串行组,保留线程 | 不改 |
连续 max_progress_rounds(默认 12)轮 [progress] |
回帖说明,不再续跑 | Waiting → 应答者 |
| agentd 自身 | cron 拉起;心跳 10 min 无更新 → ntfy 报警 | — |
| 回到 Todo 的工作项无条件重新拾起;逻辑性失败永不自动重试。 |
阶段 1 的具体取舍(均有单测):
- 分类:超时 → timeout;env 模式匹配(stderr 尾部 + 最后消息)→ env;临时性模式只看 stderr 尾部/最后事件(不看 agent 自己写的文字);退出码 0 且无最后消息且尾部不像临时错误 → logic(永不重试),其余 → crash(尾部像临时错误才重试)。
- 成功判定:最后消息首行带合法标签即算成功,即使退出码≠0(失败的收尾钩子不应吞掉已完成的工作;回帖头里照写 exit);无标签则退出码 0 才算成功(按 review 处理并注明),否则走失败流程。
- 续线程失败:非临时性的 crash/logic 发生在续线程的一轮上 → 立即在新线程重跑一次(附交接摘要),不等退避;重建出来的那一轮若再失败就转 Waiting,不会二次重建。
- 重试与线程轮数:临时性重试是同一逻辑轮,不计入
max_rounds_per_thread,run id 仍递增(GW-12-4→GW-12-5)。 - 新评论:续线程时按评论 ID(
seen_comments)过滤,而不是按时间戳,避免时钟偏差/同秒评论漏发或重发。
5. 配置(示例,值用 Vault 键名)
plane: {base_url: https://plane.kappiee.com, workspace: kappiee}
vault: {client: ~/.local/share/server-access-client/agent-access.py, config: https://nas.tail9531b.ts.net:8443/identity/agent.json}
terminals:
win10: {transport: warpgate-ssh, target: win10, os: windows, launch: schtasks, repo: 'C:\Users\Administrator\Documents\GwentAssistantProject\gwent', plane_user: window10, token_key: PLANE_WINDOW10_API_TOKEN, concurrency: 1, capabilities: [android, game], executors: [codex, powershell]}
oregon: {transport: local, os: linux, launch: subprocess, repo: /home/ubuntu/Documents/wjj/project/gwent, plane_user: oregon, token_key: PLANE_OREGON_API_TOKEN, concurrency: 2, executors: [claude-code, codex, shell]}
executors:
codex: {run: 'codex exec --json -C {repo} -s workspace-write -m {model} -c model_reasoning_effort="{effort}" {tier} -o {out} -', resume: 'codex exec resume {thread} --json -m {model} -c model_reasoning_effort="{effort}" {tier} -o {out} -', thread_from: events.jsonl:thread.started.thread_id, last_message: '{out}', models: [gpt-6-astra, gpt-6.1-sol], default_effort: medium}
claude-code: {run: 'claude -p --model {model} --output-format json --permission-mode {perm} -', resume: 'claude -p --resume {thread} --model {model} --output-format json -', thread_from: json:session_id, models: [opus, sonnet, haiku]}
shell: {run: '{script}', resume: false}
projects:
GWENT:
id: 79cb5a60-aa4f-4138-9864-1dd70426e665
webhook_secret_key: PLANE_WEBHOOK_GWENT
states: {todo: Todo, running: In Progress, waiting: Waiting, review: Review, done: Done}
reviewer: oregon
routing:
by_assignee: {window10: win10, oregon: oregon}
by_label: {'model:astra': {model: gpt-6-astra, effort: high}, 'model:sol': {model: gpt-6.1-sol, tier: fast}, 'model:opus': {model: opus}, 'exec:claude': {executor: claude-code}, 'needs-game': {serialize_group: game}}
default_executor: {win10: codex, oregon: claude-code}
retry: {transient_max: 3, backoff_minutes: [2, 5, 10]}
timeout_hours: 4
notify: {ntfy_topic: gwent, on: [waiting, review, failed]}
6. 仓库布局
agentd/ 调度核心、plane 客户端、vault 注入、状态存储(json/sqlite)、心跳
agentd/executors/ codex.py claude_code.py shell.py(适配器契约见 docs/executor-contract.md)
agentd/transports/ local.py warpgate_ssh.py(含 Windows schtasks 启动)
agentd/web/ 配置页面(阶段 2):读写 YAML + schema 校验 + 连通/冒烟测试 + 最近运行
cli/ agentd new|show|comment|state|assign|smoke
config/ agentd.yaml(示例 + 实际)、schema.json
docs/ design.md executor-contract.md runbook.md
7. 阶段
- 核心:配置驱动调度(轮询)、codex-remote(win10) + claude-local(oregon) 两个执行器、线程续接与重建、失败说明与重试、心跳、CLI;gwent 项目作为首个租户切换过来(替换现有 board-worker-plane)。
- 配置页面:单页,三个标签(终端/执行器/路由)+ 状态条;保存即校验、提交 Git、热加载;不显示密钥。
- Webhook + ntfy:Plane webhook v2 签名校验、即时触发;ntfy 通知 Waiting/Review/失败;轮询保留兜底。前提:Plane VM 进 tailnet 或接收端放在可达位置。
- Temporal:调度核心迁为 workflow/activity,获得重试/超时/历史/UI;agentd 变为 worker + webhook 入口。
- Gitea 联动:执行器产物以分支/PR 提交并回帖链接;可选 Gitea webhook 回写 Plane。
8. 安全
配置里只有 Vault 键名;token 由 agent-access.py exec --secret-env 注入 agentd 进程;执行器不持有 Plane 凭据、不能直接改工作项;webhook 按项目签名校验;页面仅 tailnet 可达,登录走既有网关。
9. 部署位置
- Plane VM(plane-01)只跑 Plane:它是设备化节点(固定版本、每日备份停写路径),不放 agentd。
- 阶段 1–2:agentd 跑在 oregon(已有 Vault 客户端、Warpgate 中继脚本、Claude Code、现有 worker 状态),cron 拉起;配置页面同进程,只监听 tailnet 地址。
- 阶段 3 起:迁到 PVE 上的精简 LXC
agentd-01(1 vCPU / 1–2 GB,tailnet + 内网 10.77.10.0/24,与 Plane/Temporal 同网段,webhook 走内网)。LXC 上不装任何 agent CLI,只有 Python 3.10+、PyYAML、Agent Vault 客户端、ssh 客户端;所有终端(win10、oregon、mac…)一律远程启动(transport: warpgate-ssh),LXC 只负责调度、回帖、通知。对外访问经 Warpgate HTTP target,复用既有登录。 - 远程启动的通用实现(P1-6):Windows 用计划任务(已实现);Linux/macOS 经 Warpgate SSH 上传
run.sh,setsid nohup脱离会话,轮询exit文件取回last.md/线程 ID;中继目标名由终端配置给出,不写死。