Files
plane-agentd/docs/executor-contract.md
T

10 KiB
Raw Blame History

执行器契约

agentd 不常驻:一轮运行比起它的那次 tick 活得久,所以执行器的一切都必须能从可 JSON 序列化的 handle 恢复(handle 存在 state/agentd-state.json 的 inflight.handle 里)。

接口(agentd/executors/base.py)

成员 说明
resume: bool 能否续线程。False 时每轮都是新线程(有前序轮次时附交接摘要)。
handle(run) -> handle 不接触终端,算出 start(run) 将返回的 handle(core 在启动前把它写进 launching 标记)。
start(run: Run) -> handle 起一轮,立即返回;不得阻塞到运行结束。抛异常不代表一定没启动:core 下一 tick 用 probe 判断。
probe(handle) -> [names] 运行目录里的启动证据(exit.txt/launch.pid/pid.txt 中存在的);空 = 没启动;终端不可达抛 TransportError。
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, dir_name):thread_id 非空即续该线程;dir_name 是运行目录名/计划任务名(缺省 = run_id,重新启动时为 <run_id>-r2 等)。 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>/<dir_name>(清空旧文件;目录里已有 pid.txt/launch.pid 时拒绝并报 HASPID),写 prompt.md。transport 有 start_run 时(远程终端),建目录、写 prompt.md 与 runner、启动由 transport 一起做:直连(direct-ssh)和远程 POSIX 是一次调用,Windows 经 Warpgate 时仍逐步上传。
  2. 用配置里的命令模板生成命令:run(新线程)或 resume(续线程)。占位符:{repo} {model} {effort} {tier} {out} {thread} {perm} {script} {run_dir};未知占位符原样保留。{tier} 展开为 tier_arg(如 -c 'service_tier="fast"'),无 tier 时为空。tier 来源:tier:<x> 标签或 routing.by_label 的 tier,否则执行器的 tier_defaults[<model>];tier:none 表示不加档位参数。模板首词若在终端 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 分离启动(本机 setsid -f bash run.sh;远程 Linux setsid nohup bash run.sh &,macOS 用 perl POSIX::setsid,$! 记入 launch.pid;Windows 一次性计划任务 agentd-<run_id>)。
  5. poll = exit.txt 是否存在;collect 由各适配器从终端上保存的文件解析(远程时读回,不依赖调度器本机有任何 CLI)。适配器把要读的东西列成 COLLECT(("read", 文件)、("grep", 文件, 子串)、("tail", 文件, n)),经 transport.fetch 一次调用取回;transport 没有 fetch 时逐个读:
适配器 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)(clean 遇到 pid 文件必须拒绝)、probe(run_dir, names)(存在的文件名列表,一次调用)、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 = 不可达);可选 start_run(task, run_dir, files, script_name)(建目录 + 写文件 + 启动)和 fetch(run_dir, ops)(一次读回多个结果,collect 用)。远程 transport 的脚本由一个"通道"送达:warpgate-ssh(agent-access.py run,只能走 argv)或 direct-ssh(Tailscale 直连 OpenSSH,脚本走 stdin,每个操作一次调用;可配 fallback: warpgate-ssh,见 design §9)。直连在命令运行前失败时抛 Unreached(RoutedTransport 据此换到兜底通道),主机公钥不符时抛 HostKeyError(不回退);process_count 不能吞掉 Unreached。

  • local:Linux 本机文件 + setsid -f;kill = 对 pid.txt 的进程组发 SIGTERM。用于调度器与执行器同机的部署和测试。
  • warpgate-ssh + os: linux|darwin(PosixWarpgateTransport):每个操作是一次 agent-access.py --config <vault.config> run --target <target> -- bash -c '<script>'(relay(),目标名取终端 target,缺省为终端名;客户端把 argv shlex.join 后交给目标账户的登录 shell,stdin 为 /dev/null)。文件:base64 放进 printf %s '<b64>' | base64 -d,超过 60000 字符分块追加再解码,回显字节数校验;读文件/grep_first/tail 的结果以 B64= 标记回传。runner 与本机相同(run.sh),setsid nohup 脱离 SSH 会话(pid = pgid = sid);kill = 对该进程组 kill -TERM -- -<pid> + pkill -TERM -s <pid>,最多等 5 s 后 KILL,exit.txt 已存在时不发信号(防 pid 复用);process_count = pgrep -x。repo/run_root 必须是远程绝对路径。每次调用约 10–14 s,所以启动只用一次调用。
  • warpgate-ssh + os: windows(WindowsWarpgateTransport):经 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(TODO:改走同一个 relay(),argv 为 powershell.exe -NoProfile -NonInteractive -EncodedCommand <b64>,去掉 helper 脚本)。路径都放进 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> 验证一轮往返。 远程 Linux/macOS 机器不需要新代码:transport: warpgate-ssh, target: <Warpgate target>, os: linux|darwin, launch: nohup,repo/run_root/bins 写远程绝对路径(步骤见 docs/deploy-lxc.md §7)。全新的 transport 类型需在 agentd/transports/ 实现上面的接口并在 make_transport 登记。