Files
plane-agentd/docs/design.md
T
jj.wang ca2b47eb5f Docs: binary resolution, cancellation, progress cap, phase-1 deviations
- runbook: how executor binaries are resolved (bins or PATH) and what that
  assumes on oregon (no codex installed) and win10; cancellation and
  max_progress_rounds in the item lifecycle; secrets scrubbed from children.
- design: blocked-by ordering is a TODO (not read by the scheduler); failure
  classification, success-when-tagged, one-time immediate rebuild, retries
  outside the thread-round budget, ID-based new-comment filter.
- executor contract: how core reads ExecResult, kill used for cancel, Windows
  quoting assumptions, codex exec resume without -C/-s.
2026-10-21 07:14:20 +00:00

98 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 键名)
```yaml
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. 阶段
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 上的专用小节点 `agentd-01`(1–2 vCPU / 2 GB,tailnet + 内网 10.77.10.0/24,与 Plane/Temporal 同网段,webhook 走内网),装 Vault 客户端、中继脚本、Claude Code 与 Codex CLI,使其同时也是一个执行终端(跑核对/验收类任务)。对外访问经 Warpgate HTTP target,复用既有登录。