Files
plane-agentd/docs/design.md
T
jj.wang 20e4e4ed73 Docs: LXC deployment guide, remote POSIX transport in the executor contract
deploy-lxc.md: Debian 12 packages, vault client + identity, Warpgate host key
(pinned by the identity), helper scripts and protocol file, all-remote config,
cron under exec --secret-env, logs/heartbeat, adding a terminal.
design.md 9: P1-6 done for Linux/macOS; Windows still on the helper scripts (TODO unify).
2026-10-21 08:25:14 +00:00

11 KiB
Raw Blame History

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: warpgate-ssh, target: oregon, os: linux, launch: nohup, repo: /home/ubuntu/Documents/wjj/project/gwent, run_root: /home/ubuntu/.gwent-board/agentd-runs, plane_user: oregon, token_key: PLANE_OREGON_API_TOKEN, concurrency: 1, executors: [claude-code]}
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(远程 Linux/macOS:bash -c 经中继;Windows:helper 脚本 + 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. 阶段

  1. 核心:配置驱动调度(轮询)、codex-remote(win10) + claude-local(oregon) 两个执行器、线程续接与重建、失败说明与重试、心跳、CLI;gwent 项目作为首个租户切换过来(替换现有 board-worker-plane)。
  2. 配置页面:单页,三个标签(终端/执行器/路由)+ 状态条;保存即校验、提交 Git、热加载;不显示密钥。
  3. Webhook + ntfy:Plane webhook v2 签名校验、即时触发;ntfy 通知 Waiting/Review/失败;轮询保留兜底。前提:Plane VM 进 tailnet 或接收端放在可达位置。
  4. Temporal:调度核心迁为 workflow/activity,获得重试/超时/历史/UI;agentd 变为 worker + webhook 入口。
  5. 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,已实现):Linux/macOS 经 Warpgate SSH(agent-access.py run --target <终端 target> -- bash -c …)上传 prompt.md/run.sh 并一次调用内启动,setsid nohup 脱离会话(macOS 用 perl POSIX::setsid),轮询 exit.txt,从终端上的输出文件取回最后消息/线程 ID,kill 按进程组 + 会话;中继目标名由终端配置给出。oregon 已切到这条路径(调度器暂仍在 oregon,启动经 Warpgate 回到本机)。部署步骤:docs/deploy-lxc.md。
  • Windows 仍用 helper 脚本 win10-ps.sh / win10-put.sh(写死 --target win10)+ 计划任务。TODO(统一):Windows 也改走 relay()(argv = powershell.exe -EncodedCommand …,文件同样 base64 分块),删掉对 helper 脚本与 helpers_dir 的依赖,LXC 上就不必再复制这两个脚本。