Files
plane-agentd/docs/executor-contract.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

7.0 KiB
Raw Blame History

执行器契约

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 登记。