Files
codex-app-server/docs/marker-tools.md
T
host 3b2fef95fa 任务内标记工具:task-create/task-poll/task-read 移植、wait-threads 接通、新增 task-cancel/task-retry
- 新模块 cloud/marker_tools.py:纯逻辑解析、校验、快照格式化、
  控制门禁、派生代数与直接上限计数
- sandbox 两处解析提取五种标记块;worker 在 5 处投递点与
  _deliver_peer_messages 同款位置执行,报告排成后续消息下一轮回父任务
- wait 每块 cap 到 8 个目标(与服务端 /wait 对齐,spawn 原先的 10 个收紧),
  每轮最多 2 块;取消/重试门禁:同项目、非自己、非 goal、
  派生链或同创建人,另查 create_task / approve_merge 权限,
  查不到时收紧到仅派生链内
- goal 与协调员任务不解析;单测 30 个,cloud 全量 1043 个、4 跳过、0 失败
2026-10-28 10:13:40 +00:00

11 KiB
Raw Blame History

任务内标记工具(marker tools)

适用基线:main d129f7b 上的 feat-marker-tools 分支(本地提交,未推、 未建 PR、未部署)。本批只做四类标记工具,不要把本文档读成 “所有 Codex app 线程工具都已实现”:handoff、worktree 创建状态、 PR 检查诊断、automation、clock-sleep 都不在本批;MCP 真工具路 (同轮函数返回)也不在本批。

一句话原理

agent 不用联网、不用凭证。agent 在回合输出里写标记块,本地与 Docker 执行器都经 sandbox 的结果解析提取标记块(与 task-message 同款两处解析),可信的 worker 在 completed 轮次后执行。

异步语义(必须先讲清,否则会被当成函数调用):

  • 写块 → 回合结束 → worker 执行 → 报告拼成一条后续消息、 下一轮才回到父任务。不是同轮函数返回。
  • 轮询与等待在 worker 执行期间循环查库、占 worker 槽位(与 协调员等齐同款代价);超时或父任务被取消时中止。
  • 只有 completed 轮次的块被执行;失败 / 超时 / 取消 / 配额中断 的轮次不执行;父任务执行期间被取消时也不执行。
  • 拒绝只记报告与审计、不炸任务、不改目标状态。

边界:谁能用、谁不能用

  • 能用:普通任务。普通任务当项目经理、自己派生员工、轮询员工、 等一批员工、取消或重试员工,走的就是这批工具。
  • 不能用:goal 任务与协调员任务不解析这些标记块(只收不发)。 协调员任务本身不跑模型回合,等齐循环也不会在中途排空后续 消息做判断,所以协调员 PM 不能因此获得任务内工具。
  • 容易混淆的一点:协调员的子任务本身是普通任务(不是协调员 任务),它写标记块会被解析;它的派生代数从 0 计(见“派生护栏”)。

四个工具的标记格式

task-create:新建平级主线任务

```task-create
{"name": "test-xxx", "prompt": "...", "model": null,
 "effort": null, "executor": "docker"}

- 新任务是平级主线,不是子代理、不是 fork;不继承父任务历史;
  环境基线继承父任务的 environment_id,但 worker 给它独立的
  tenant/worktree,不动父任务的 cwd;父任务未提交的改动不会
  自动带过去。
- prompt 必填、1-20000 字;name 可选、1-100 字(仅用于派生报告,
  tasks 表没有 name 列,不落库、不改 prompt);model 不做白名单;
  effort 只能是 low / medium / high / xhigh / max;executor 只能是
  local / docker / cloud,未指定时继承父任务。
- 只建普通任务:不通过标记建协调员任务与 goal 任务。
- 每轮最多 5 块,多余的忽略;非法 JSON、prompt 缺失/空串/超长、
  name 超长、effort/executor 非法时整块跳过;正文里的行首围栏
  会把块截断成非法 JSON 而被跳过(与 task-message 同款非贪婪匹配)。

### task-poll / task-read:按任务 id 轮询阅读

```text
```task-poll
{"task_id": "<目标任务 id>", "interval_seconds": 5,
 "timeout_seconds": 300, "chars": 2000}
{"task_id": "<目标任务 id>", "wait_seconds": 30}

- task-read 是 task-poll 的别名;read-thread / read_thread 也是
  同款别名。不传 wait_seconds 时立即读一次快照;传了则等这么久
  再读一次(等价于 interval = timeout = wait_seconds)。
- interval 钳制 1-60 秒,timeout 钳制 0-3600 秒(0 = 立即快照),
  chars 钳制 200-8000 字;各类块合计每轮最多 5 块。
- 轮询目标不存在、跨项目、轮询自己时,报告里写清拒绝原因,
  不改任何状态。轮询目标是只读的,goal 任务也可以被轮询。

### wait-threads:一次等多个目标任务

```text
```wait-threads
{"task_ids": ["B", "C"], "interval_seconds": 5,
 "timeout_seconds": 300, "chars": 2000}

- wait_threads 是同款写法;task_ids 也接受 thread_ids / threads
  两个别名;去重保序。
- 每块最多 8 个目标,每轮最多 2 块。8 这个数是刻意与服务端
  POST /tasks/{id}/wait 的“一次最多 8 个线程”对齐的;spawn
  分支原先的每块 10 个在本批收紧到 8 个,不要再用 10。
- 并行查库:每轮查一次全部目标,不是逐个串行等超时相加;
  全部终态或超时后把每目标快照一起排回父任务。
- 缺失、跨项目、等待自己的目标当场记为已处理(写清原因),
  不让它们把整批拖到超时;其余目标继续等到终态或超时。
- 超时口径与服务端 /wait 不同,且是故意的:服务端 /wait 是给
  外部调用方持 HTTP 连接等的,上限 120 秒;标记 wait 是 worker
  在回合后直连库循环查,不持 HTTP 连接,超时钳制与 task-poll
  同款(0-3600 秒),占的是 worker 槽位而不是 HTTP 连接。
  两者不要混用、不要互抄上限。

### task-cancel / task-retry:取消 / 重试已存在的任务

```text
```task-cancel
{"task_id": "<目标任务 id>"}
{"task_id": "<目标任务 id>"}

- task_cancel / task_retry 是同款写法;task_id 也接受 to /
  to_task_id / thread_id 别名;每种每轮最多 5 块。
- 取消复用 worker 现有的取消路径语义:取消后状态为 interrupted,
  正在跑的进程组 / 容器按现有取消路径真杀,不只改库状态。
- 重试与服务端 POST /tasks/{id}/retry 对齐:仅 failed /
  interrupted / merge-failed 可重试,重试时清空上一轮
  result_text / error_text / actual_model、状态回 queued、
  prompt 与模型等原样保留、记审计。重试本身不花模型钱,
  重试后的新一轮照常计费。
- 执行顺序:创建 → 取消 → 重试 → 轮询 → 等待。创建在前让同一轮
  刚创建的任务立刻能被轮询;控制操作在快照之前让轮询/等待读到
  取消或重试后的新状态。

## 门禁(task-cancel / task-retry,缺一不可)

纯函数层(cloud/marker_tools.py check_control_gate)与 worker
权限层(worker.py _deliver_marker_tools)两道,拒绝时只记报告
与审计、不改目标状态:

1. 目标存在;同项目;不是自己。
2. 非 goal 任务(verify_command 非空或 goal_mode 为真都算 goal)。
3. 取消要求目标非终态;重试要求目标仅为 failed / interrupted /
   merge-failed。已合并(merged)两者都拒绝;merge-failed 本身
   是终态,所以取消拒绝、重试接受。
4. 关系二选一:目标在发送方派生链上(沿 parent_task_id 链最多
   32 步能追溯到发送方),或目标 created_by 与发送方 created_by
   相同。“同项目猜到 id 就能取消任何人的任务”在此被挡住。
5. 权限层:取消要求发送方创建人有项目 create_task 权限(与
   服务端取消口径同款),重试要求有 approve_merge 权限(与
   服务端重试口径同款)。权限检查本身抛异常时收紧到仅派生链内
   可用,同创建人-only 的目标一律拒绝,并把原因写进报告。

## 派生护栏与费用

- 派生深度上限 3 代(父任务本身是派生任务时逐代计;协调员子任务
  的代数从 0 计,见“边界”);单个父任务直接派生上限 20 个
  (不含协调员子任务);超限的块跳过。
- 派生任务是独立主线:父任务完成或取消都不自动杀派生任务;
  想停派生任务,用 task-cancel 或控制台 / API 显式取消它。
  协调员孤儿收口只扫 coordinator_part 非空的协调员子任务,
  不扫派生任务。
- 费用与配额:派生任务 created_by 跟父任务走,认领时照常过
  key 门与配额门;创建时 worker 先查一次父任务配额,超限时
  整轮创建跳过(排队本身不花钱,但不让超限方再批量排队)。

## 诚实边界(与服务端口径不一致处,逐条列出)

- 重试的 goal 拒绝比服务端严:服务端 POST /tasks/{id}/retry
  没有单独的 goal 检查,标记重试为了不破坏 goal 循环语义,
  明确拒绝 goal 目标。这是标记口径比服务端口径严,不是遗漏。
- 取消的 goal 拒绝同理:服务端取消也没有单独的 goal 检查,
  标记取消同样拒绝 goal 目标。
- 服务端重试是双人门(触发人与原任务创建人任一超限即拦,
  且触发人须有 approve_merge 权限);标记重试的触发方是发送
  方任务本身,其 created_by 与派生任务的 created_by 天然相同
  (派生时跟父任务走),所以 worker 侧只查发送方创建人这一道
  approve_merge,效果与双人门在派生链内等价;同创建人-only
  的目标同样只查这一道,权限查不到时则收紧到仅派生链内。
- wait 的超时口径与服务端 /wait 不同(见上文),数量口径已
  对齐到 8 个;不要把标记 wait 的 3600 秒上限套到 HTTP /wait 上。
- 本批没有 MCP 真工具:标记工具永远是异步的(下一轮才拿到
  报告),做不到同轮函数返回;想做同轮返回要另搭任务级鉴权的
  MCP 服务器,不在本批。
- 签名门残留:worker 的 _code_signature 只签 worker / sandbox /
  coordinator / cost / server / host 共 6 个文件,新模块
  marker_tools.py 不在签名候选里。本批部署时 worker.py 与
  sandbox.py 本身都变了,签名门照常生效;但将来只改
  marker_tools.py 的补丁不会单独触发签名门,届时要么把
  marker_tools.py 加进签名候选并同步改单测的 6 个断言,要么
  接受这一处缺口——本批不顺手改签名,避免动无关安全测试的
  预期。
- spawn 分支的未提交半成品没有原样合并进来:本批只移植了
  已提交的 task-create / task-poll / task-read 实现与未提交的
  wait 解析(收紧到 8 个后接通执行),handoff / worktree /
  PR 检查 / automation / clock-sleep 的解析与 send-message-to-thread
  别名均未移植;spawn 工作区本身未动。

## 关键实现位置

- cloud/marker_tools.py:纯逻辑(解析、校验、快照格式化、
  门禁、派生代数与直接上限计数),单测不需要 worker/server。
- cloud/sandbox.py:本地 _result_from_runner 与 Docker
  _parse_host_result 两处提取五种标记块,worker 侧按结果
  dict 里的 task_creates / task_polls / wait_threads /
  task_cancels / task_retries 执行。
- cloud/worker.py:_deliver_marker_tools(5 处投递点,与
  _deliver_peer_messages 同款位置)、_poll_task_snapshot、
  _wait_task_targets、_queue_self_followup、_marker_auth。
- cloud/tests/test_marker_tools.py:解析上限与钳制、非法块跳过、
  跨项目拒绝、自操作拒绝、已终态拒绝、goal / 已合并拒绝、
  重试仅三态且清空旧结果、wait 多目标终态与超时快照、派生
  深度与直接上限、权限检查失败收紧。