ca2b47eb5f
- 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.
59 lines
7.0 KiB
Markdown
59 lines
7.0 KiB
Markdown
# 执行器契约
|
||
|
||
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 统一处理。transport 启动 runner/中继脚本时传入去掉所有 `token_key`/`webhook_secret_key` 变量的环境(`Config.scrubbed_env()`);新 transport 必须同样处理。
|
||
|
||
## core 如何解读 `ExecResult`
|
||
- 成功 = 最后消息首行带合法标签(**不看退出码**),或无标签但退出码 0(按 review 处理并注明)。首行判定跳过空行、BOM、开头的 ```/~~~ 代码围栏行和 `---` 分隔线,容忍 `**`、`>`、`#`、反引号等修饰,标签后可跟任意文字(`[progress] 第 2 步`)。
|
||
- 失败分类:`tail` 决定是否临时性(`retry.transient_patterns`),`tail` + `last_message` 决定 env(`retry.env_patterns`)。所以适配器应把 stderr 尾部/最后一个事件放进 `tail`,不要把 agent 正文塞进去。
|
||
- 续线程(`thread_id` 非空)的一轮非临时性失败 → core 立即在新线程重跑一次(附交接摘要),只一次。
|
||
- `kill(handle)` 既用于超时,也用于取消(工作项在运行中被移出 In Progress);必须能在运行未结束时调用。取消后 core 会尝试 `collect(handle)` 取线程 ID,所以 `collect` 在未结束的运行上也不能抛出致命错误(抛异常会被忽略)。
|
||
|
||
## 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`。路径都放进 PowerShell 单引号字面量(`'` 双写);`schtasks /TR` 里脚本路径用 `\"…\"` 包裹,依赖 Windows PowerShell 5.1 的旧式原生参数传递(PowerShell 7.3+ 的 Standard 模式下需改写)。runner 进不了 repo 时写 `exit=97` 结束。
|
||
- Codex 续线程:`codex exec resume <thread>` 不接受 `-C`/`-s`,`resume` 模板里不要写;工作目录由 runner 先进入 repo 保证,沙箱沿用 Codex 对该会话/全局配置的默认值(若需强制,用 `-c 'sandbox_mode="workspace-write"'`,上线前在 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` 登记。
|