- 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.
7.0 KiB
执行器契约
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(现有三个适配器的共同形态)
- 在终端上建运行目录
<run_root>/<run_id>(清空旧文件),写prompt.md。 - 用配置里的命令模板生成命令:
run(新线程)或resume(续线程)。占位符:{repo} {model} {effort} {tier} {out} {thread} {perm} {script} {run_dir};未知占位符原样保留。{tier}展开为tier_arg(如-c 'service_tier="fast"'),无 tier 时为空。模板首词若在终端bins里,替换为绝对路径(Windows 前加&)。参数请用单引号,bash 和 PowerShell 都认。 - transport 生成 runner 脚本(Linux
run.sh,Windowsrun.ps1):进入 repo,写pid.txt/started.txt,把prompt.md从 stdin 喂给命令,stdout →stdout_name,stderr →stderr.txt,结束写exit.txt(exit=N at …)。 - transport 分离启动(Linux
setsid -f bash run.sh,Windows 一次性计划任务agentd-<run_id>)。 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 上验证)。
新增一个执行器
agentd/executors/<name>.py:继承RunnerExecutor,设stdout_name,实现collect(必要时覆盖resume/command)。完全不同的形态(如 HTTP API)可直接继承Executor,只要 handle 可序列化。- 在
agentd/executors/__init__.py:make_executor里登记 adapter 名;config/schema.json的executor.adapterenum 和config.semantic_errors的白名单同步加上。 - 配置
executors.<name>:adapter、run、resume、models、default_effort等;把它加进相应终端的executors,需要时在routing.by_label加exec:<name>。 - 在
tests/加一个用假二进制(bins指向临时脚本)的本机测试,参照tests/test_local_runner.py。
新增一个终端
terminals.<name>:transport、os、launch、repo、plane_user(该终端回帖身份的 Plane 显示名)、token_key(Vault 键名,同名环境变量)、concurrency、capabilities、executors,可选bins、run_root、busy_processes(这些进程在跑而 agentd 没有在途运行时不派单)。- 项目
routing.by_assignee加<plane_user>: <name>,default_executor加默认执行器。 - cron 的
agent-access.py exec加--secret-env <token_key>=<token_key>(agentd check-config会打印完整参数)。 agentd smoke <PROJECT> <name>验证一轮往返。 新的 transport 类型(如 Linux 远程机)需在agentd/transports/实现上面的接口并在make_transport登记。