Executor contract and runbook
This commit is contained in:
@@ -5,3 +5,8 @@ Plane 看板驱动的通用 agent 调度服务:在 Plane 建工作项并指派
|
|||||||
- 设计:`docs/design.md`(架构、契约、失败策略、配置、阶段)
|
- 设计:`docs/design.md`(架构、契约、失败策略、配置、阶段)
|
||||||
- 首个租户:GWENT 项目(看板 https://plane.kappiee.com/kappiee/projects/79cb5a60-aa4f-4138-9864-1dd70426e665/issues/ )
|
- 首个租户:GWENT 项目(看板 https://plane.kappiee.com/kappiee/projects/79cb5a60-aa4f-4138-9864-1dd70426e665/issues/ )
|
||||||
- 凭据全部经 Agent Vault 注入内存,仓库和配置里只有键名。
|
- 凭据全部经 Agent Vault 注入内存,仓库和配置里只有键名。
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
- 契约与扩展:`docs/executor-contract.md`;安装、cron、日志、故障:`docs/runbook.md`
|
||||||
|
- 入口:`cli/agentd tick|new|show|comment|state|assign|smoke|check-config`(在 `agent-access.py exec --secret-env …` 下运行)
|
||||||
|
- 测试:`python3 -m unittest`(离线,不访问 Plane/win10)
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# 执行器契约
|
||||||
|
|
||||||
|
agentd 不常驻:一轮运行比起它的那次 tick 活得久,所以执行器的一切都必须能从**可 JSON 序列化的 handle** 恢复(handle 存在 `state/agentd-state.json` 的 `inflight.handle` 里)。
|
||||||
|
|
||||||
|
## 接口(`agentd/executors/base.py`)
|
||||||
|
| 成员 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `resume: bool` | 能否续线程。`False` 时每轮都是新线程(有前序轮次时附交接摘要)。 |
|
||||||
|
| `start(run: Run) -> handle` | 起一轮,立即返回;不得阻塞到运行结束。 |
|
||||||
|
| `poll(handle) -> "running" \| "finished"` | 便宜的完成检测。 |
|
||||||
|
| `collect(handle) -> ExecResult` | 只在 `finished` 后调用,可重复调用(幂等):Plane 回帖失败时下一 tick 会再 collect。 |
|
||||||
|
| `kill(handle)` | 超时时结束整个进程树。 |
|
||||||
|
| `forget(handle)`(可选) | collect 之后清理(如删除计划任务)。 |
|
||||||
|
|
||||||
|
`Run(run_id, repo, prompt, model, effort, tier, thread_id, script)`:`thread_id` 非空即续该线程。
|
||||||
|
`ExecResult(last_message, exit_code, thread_id, log_paths, tail)`:`last_message` 原样回帖,首行应为 `[review]/[question]/[blocked]/[progress]`;`thread_id` 供下一轮续接;`tail` 是失败说明里的"最后一帧"(stderr 尾部/最后事件)。
|
||||||
|
|
||||||
|
执行器**不接触 Plane**:不持有 token、不改工作项。回帖、状态流转、重试、超时、线程重建都由 core 统一处理。
|
||||||
|
|
||||||
|
## RunnerExecutor(现有三个适配器的共同形态)
|
||||||
|
1. 在终端上建运行目录 `<run_root>/<run_id>`(清空旧文件),写 `prompt.md`。
|
||||||
|
2. 用配置里的命令模板生成命令:`run`(新线程)或 `resume`(续线程)。占位符:`{repo} {model} {effort} {tier} {out} {thread} {perm} {script} {run_dir}`;未知占位符原样保留。`{tier}` 展开为 `tier_arg`(如 `-c 'service_tier="fast"'`),无 tier 时为空。模板首词若在终端 `bins` 里,替换为绝对路径(Windows 前加 `&`)。参数请用单引号,bash 和 PowerShell 都认。
|
||||||
|
3. transport 生成 runner 脚本(Linux `run.sh`,Windows `run.ps1`):进入 repo,写 `pid.txt`/`started.txt`,把 `prompt.md` 从 stdin 喂给命令,stdout → `stdout_name`,stderr → `stderr.txt`,结束写 `exit.txt`(`exit=N at …`)。
|
||||||
|
4. transport 分离启动(Linux `setsid -f bash run.sh`,Windows 一次性计划任务 `agentd-<run_id>`)。
|
||||||
|
5. `poll` = `exit.txt` 是否存在;`collect` 由各适配器解析:
|
||||||
|
|
||||||
|
| 适配器 | stdout | 最后消息 | 线程 ID |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `codex` | `events.jsonl` | `last.md`(`-o {out}`) | `thread.started` 事件的 `thread_id` |
|
||||||
|
| `claude-code` | `out.json` | JSON 的 `result`(`is_error` 时为空,进失败流程) | JSON 的 `session_id` |
|
||||||
|
| `shell` | `stdout.txt` | `last.md`(脚本可写 `{out}`),否则 stdout 尾部 6000 字 | 无(`resume` 恒 false) |
|
||||||
|
|
||||||
|
配置里的 `thread_from` / `last_message` 目前只是说明性字段,解析逻辑在适配器里。
|
||||||
|
|
||||||
|
## Transport 接口(`agentd/transports/`)
|
||||||
|
`os`、`join`、`quote`、`default_run_root`、`mkdir(path, clean)`、`write_text`、`read_text`(不存在返回 None)、`grep_first(path, needle)`、`tail(path, n)`、`runner(run_dir, cwd, command, stdout_name)`、`launch(task, script, run_dir)`、`kill(task, run_dir)`、`forget(task)`、`process_count(name)`(None = 不可达)。
|
||||||
|
- `local`:Linux 本机文件 + `setsid -f`;kill = 对 `pid.txt` 的进程组发 SIGTERM。
|
||||||
|
- `warpgate-ssh`:Windows,经 `win10-ps.sh`(PowerShell `-EncodedCommand`)和 `win10-put.sh`(分块上传 + SHA-256 校验);文件读取用 base64 标记回传;kill = `taskkill /T /F /PID <pid.txt>` + `schtasks /End /Delete`。脚本目录由 `agentd.helpers_dir`(或终端 `helpers_dir`)配置;脚本目前写死 `--target win10`。
|
||||||
|
|
||||||
|
## 新增一个执行器
|
||||||
|
1. `agentd/executors/<name>.py`:继承 `RunnerExecutor`,设 `stdout_name`,实现 `collect`(必要时覆盖 `resume`/`command`)。完全不同的形态(如 HTTP API)可直接继承 `Executor`,只要 handle 可序列化。
|
||||||
|
2. 在 `agentd/executors/__init__.py:make_executor` 里登记 adapter 名;`config/schema.json` 的 `executor.adapter` enum 和 `config.semantic_errors` 的白名单同步加上。
|
||||||
|
3. 配置 `executors.<name>`:`adapter`、`run`、`resume`、`models`、`default_effort` 等;把它加进相应终端的 `executors`,需要时在 `routing.by_label` 加 `exec:<name>`。
|
||||||
|
4. 在 `tests/` 加一个用假二进制(`bins` 指向临时脚本)的本机测试,参照 `tests/test_local_runner.py`。
|
||||||
|
|
||||||
|
## 新增一个终端
|
||||||
|
1. `terminals.<name>`:`transport`、`os`、`launch`、`repo`、`plane_user`(该终端回帖身份的 Plane 显示名)、`token_key`(Vault 键名,同名环境变量)、`concurrency`、`capabilities`、`executors`,可选 `bins`、`run_root`、`busy_processes`(这些进程在跑而 agentd 没有在途运行时不派单)。
|
||||||
|
2. 项目 `routing.by_assignee` 加 `<plane_user>: <name>`,`default_executor` 加默认执行器。
|
||||||
|
3. cron 的 `agent-access.py exec` 加 `--secret-env <token_key>=<token_key>`(`agentd check-config` 会打印完整参数)。
|
||||||
|
4. `agentd smoke <PROJECT> <name>` 验证一轮往返。
|
||||||
|
新的 transport 类型(如 Linux 远程机)需在 `agentd/transports/` 实现上面的接口并在 `make_transport` 登记。
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# 运行手册
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
```bash
|
||||||
|
git clone https://git.kappiee.com/kappiee/plane-agentd.git && cd plane-agentd
|
||||||
|
python3 -m pip install --user pyyaml # 唯一依赖;有 jsonschema 时用它校验,否则用内置子集校验器
|
||||||
|
cp config/agentd.example.yaml config/agentd.local.yaml # git-ignored;或用 AGENTD_CONFIG 指向别处
|
||||||
|
cli/agentd check-config # 校验 + 列出各终端 token 环境变量是否已注入 + cron 所需 --secret-env 参数
|
||||||
|
python3 -m unittest # 离线单测
|
||||||
|
```
|
||||||
|
配置里只写 Vault 键名(`token_key`)。agentd 读同名环境变量,token 只由 `agent-access.py exec --secret-env KEY=KEY` 注入进程内存;不要 export 到 shell、不要写文件。
|
||||||
|
|
||||||
|
## 运行
|
||||||
|
单次 tick(cron 推荐):
|
||||||
|
```bash
|
||||||
|
cd ~/.local/share/server-access-client && python3 -I agent-access.py \
|
||||||
|
--config https://nas.tail9531b.ts.net:8443/identity/agent.json exec \
|
||||||
|
--secret-env PLANE_OREGON_API_TOKEN=PLANE_OREGON_API_TOKEN \
|
||||||
|
--secret-env PLANE_WINDOW10_API_TOKEN=PLANE_WINDOW10_API_TOKEN \
|
||||||
|
--timeout 900 -- /home/ubuntu/Documents/wjj/project/plane-agentd/cli/agentd tick --once
|
||||||
|
```
|
||||||
|
crontab(每 2 分钟;tick 有文件锁,重叠的 tick 直接跳过):
|
||||||
|
```
|
||||||
|
*/2 * * * * cd $HOME/.local/share/server-access-client && python3 -I agent-access.py --config https://nas.tail9531b.ts.net:8443/identity/agent.json exec --secret-env PLANE_OREGON_API_TOKEN=PLANE_OREGON_API_TOKEN --secret-env PLANE_WINDOW10_API_TOKEN=PLANE_WINDOW10_API_TOKEN --timeout 900 -- $HOME/Documents/wjj/project/plane-agentd/cli/agentd tick --once >> $HOME/Documents/wjj/project/plane-agentd/state/cron.log 2>&1
|
||||||
|
```
|
||||||
|
常驻替代:`... exec ... -- cli/agentd tick --loop --interval 120`(vault 会话超时要够长,cron 更稳)。
|
||||||
|
cron 的 PATH 很短:本机执行器的二进制用终端 `bins` 写绝对路径(如 `claude: /home/ubuntu/.local/bin/claude`)。
|
||||||
|
|
||||||
|
人工操作(同样在 `exec --secret-env` 下):
|
||||||
|
```
|
||||||
|
cli/agentd new GWENT "T-010 标题" --file brief.md --assign window10 --labels model:astra,needs-game --module M1 --state Todo
|
||||||
|
cli/agentd show GWENT GW-12 3 # 状态/指派/标签 + 最近 3 条评论
|
||||||
|
cli/agentd comment GWENT 12 reply.md # 或 - 读 stdin
|
||||||
|
cli/agentd state GWENT 12 Todo # Plane 状态名或配置键(todo/review…)
|
||||||
|
cli/agentd assign GWENT 12 window10
|
||||||
|
cli/agentd smoke GWENT oregon # 建一个要求回复 "[review] ok" 的工作项,tick 到结束,PASS 则移到 Done
|
||||||
|
```
|
||||||
|
CLI 以 `--as <terminal>` / `agentd.cli_terminal` / 第一个有 token 的终端的身份操作。
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
| 路径 | 内容 |
|
||||||
|
|---|---|
|
||||||
|
| `state/agentd-state.json` | 每项目每工作项:轮次、线程 ID 与所在终端/执行器、线程轮数、各轮标签、已见评论、重试计数/下次重试、在途运行(handle、deadline) |
|
||||||
|
| `state/agentd.log` | 每次 tick 的动作与错误 |
|
||||||
|
| `state/heartbeat.json` | 最近一次 tick 的时间、在途运行、错误;超过 10 分钟不更新 = agentd 没在跑 |
|
||||||
|
| `state/agentd.lock` | tick 互斥锁 |
|
||||||
|
| `state/runs/<run_id>/` | 本机终端的运行目录:`prompt.md run.sh pid.txt started.txt exit.txt stderr.txt` + 执行器输出 |
|
||||||
|
| `<repo>\runtime\agentd\<run_id>\`(win10) | 同上,`run.ps1`;codex 输出 `events.jsonl last.md` |
|
||||||
|
|
||||||
|
## 工作项生命周期
|
||||||
|
Todo + 指派给终端用户 → agentd 选最小编号、终端有空位、串行组空闲的项 → In Progress,评论 `[worker] 第 k 轮开始…` → 执行器结束后回帖 `[<executor>] 第 k 轮结束(exit, run, 终端, 日志)` + 最后消息:
|
||||||
|
- `[review]` → Review,指派 `reviewer`;`[question]`/`[blocked]` → Waiting,指派 `answerer`(默认 reviewer);`[progress]` → Todo,留在原终端,下一轮续线程;首行无标签 → 按 review 处理并注明。
|
||||||
|
- 回到 Todo 的项总会被重新拾起;续线程时只发送上次之后的新评论(`[worker]` 评论永远跳过)。
|
||||||
|
- 线程重建:换终端/执行器、续线程失败、线程轮数 ≥ `max_rounds_per_thread` → 新线程,首轮输入附交接摘要(各轮标签 + 上一轮最后消息),开始评论里注明原因。
|
||||||
|
|
||||||
|
## 常见故障
|
||||||
|
| 现象 | 原因 / 处理 |
|
||||||
|
|---|---|
|
||||||
|
| `no Plane token in the environment` / tick 报 `no Plane token` | 没在 `agent-access.py exec --secret-env …` 下运行,或键名不对;`check-config` 看 MISSING |
|
||||||
|
| `skip: another tick holds state/agentd.lock` | 上一 tick 还在跑(win10 上传慢);正常。持续出现则查是否有卡死的 agentd 进程 |
|
||||||
|
| `[worker] 第 k 轮失败:类型=crash(临时性)…重试 n/N` | 网络/中继/429/5xx:按 `backoff_minutes` 自动重试,续原线程;超过 `transient_max` 转 Waiting |
|
||||||
|
| `类型=timeout` | 超过 `timeout_hours`(或 `budget:2h` 标签);进程树已结束,转 Waiting。看运行目录日志决定拆分任务或加预算 |
|
||||||
|
| `类型=crash`(非临时)且是续线程 | 自动在新线程重跑一次(附交接摘要);再失败转 Waiting |
|
||||||
|
| `类型=env` | 匹配 `retry.env_patterns`(焦点丢失/弹窗/断网等),转 Waiting,同串行组暂停到该项离开 Waiting |
|
||||||
|
| `win10 unreachable; skipping` / `busy (N codex running…)` | 中继不通,或 win10 上有不是 agentd 起的 codex(`busy_processes`);等它结束 |
|
||||||
|
| `schtasks launch … failed` / `upload … failed` | 中继或 Windows 计划任务问题;手工 `win10-ps.sh "schtasks /Query /TN agentd-<run>"` 查看 |
|
||||||
|
| 工作项在运行中被人改了状态 | 结果照常回帖,但不再改状态/重试 |
|
||||||
|
| Plane 回帖失败 | 在途记录保留,下一 tick 重新 collect 并回帖(可能重复一条评论) |
|
||||||
|
| 想手工终止一轮 | 把工作项移出 In Progress(如 Waiting),再在终端上结束任务:本机 `kill -TERM -<pid.txt>`,win10 `taskkill /T /F /PID <pid.txt>`。被杀的 runner 不写 `exit.txt`,该运行占着终端槽位直到 deadline 才被收尾(不改状态);急用时停 cron 后从 `state/agentd-state.json` 删掉该项的 `inflight` |
|
||||||
Reference in New Issue
Block a user