# 执行器契约 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. 在终端上建运行目录 `/`(清空旧文件),写 `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-`)。 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 ` + `schtasks /End /Delete`。脚本目录由 `agentd.helpers_dir`(或终端 `helpers_dir`)配置;脚本目前写死 `--target win10`。 ## 新增一个执行器 1. `agentd/executors/.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.`:`adapter`、`run`、`resume`、`models`、`default_effort` 等;把它加进相应终端的 `executors`,需要时在 `routing.by_label` 加 `exec:`。 4. 在 `tests/` 加一个用假二进制(`bins` 指向临时脚本)的本机测试,参照 `tests/test_local_runner.py`。 ## 新增一个终端 1. `terminals.`:`transport`、`os`、`launch`、`repo`、`plane_user`(该终端回帖身份的 Plane 显示名)、`token_key`(Vault 键名,同名环境变量)、`concurrency`、`capabilities`、`executors`,可选 `bins`、`run_root`、`busy_processes`(这些进程在跑而 agentd 没有在途运行时不派单)。 2. 项目 `routing.by_assignee` 加 `: `,`default_executor` 加默认执行器。 3. cron 的 `agent-access.py exec` 加 `--secret-env =`(`agentd check-config` 会打印完整参数)。 4. `agentd smoke ` 验证一轮往返。 新的 transport 类型(如 Linux 远程机)需在 `agentd/transports/` 实现上面的接口并在 `make_transport` 登记。