agent-fleet/openspec/changes/dynamic-execution-mode/design.md
Zer4tul 48c93e2ce9 feat: dynamic execution mode — Undecided tasks, two-phase dispatch, assign API
- ExecutionMode enum adds Undecided variant (default for new tasks)
- Webhook creates tasks as Undecided instead of hardcoded SshCli
- Dispatch loop: Phase 1 matches ssh_cli hosts, Phase 2 marks remaining as HttpPull
- Dequeue now returns http_pull AND undecided tasks (atomic claim)
- New endpoint: POST /api/v1/tasks/{id}/assign for coordinator explicit assignment
- Backward compatible: existing SshCli/HttpPull tasks unaffected
- 37 tests passing (6 new)
2026-05-13 05:29:12 +08:00

2.8 KiB
Raw Blame History

Context

当前任务创建时硬编码 execution_mode = SshCli。这在只有 ssh_cli agent 时可行但在混合环境ssh_cli + http_pull中会阻塞 http_pull agent 接任务。

核心洞察:执行模式是调度决策,不是任务属性。任务只描述"需要什么能力",由调度器决定"怎么执行"。

Goals / Non-Goals

Goals:

  • 任务创建时不预设执行模式
  • Dispatch loop 根据注册的 agent 动态决定
  • Coordinator 可以显式指派任务给特定 agent
  • http_pull agent 能 dequeue 到未被 ssh_cli 认领的任务

Non-Goals:

  • 不实现智能调度负载均衡、亲和性等——Phase 2 再考虑
  • 不改变 receipt 验证流程
  • 不改变 Forgejo webhook 格式

Decisions

Decision 1: ExecutionMode 新增 Undecided

选择: 新增 Undecided 变体作为默认值

理由:

  • 向后兼容:已有的 SshCli/HttpPull 任务不受影响
  • 语义清晰:Undecided 表示"等待调度器决定"
  • dispatch loop 只处理 Undecided 任务,已决定的不再改变

替代方案:

  • Option<ExecutionMode>None 表示未决定)—— 语义等价但 enum 更明确
  • 去掉 execution_mode 字段,纯靠 runtime 状态—— 太激进,改太大

Decision 2: 两阶段 dispatch

选择: dispatch loop 分两阶段:

  1. ssh_cli 阶段:扫描 Undecided 任务,查找匹配的 ssh_cli host → 找到则标记 SshCli 并执行
  2. http_pull 阶段:剩余的 Undecided 任务标记为 HttpPull,等待 agent dequeue

理由:

  • ssh_cli 是主动调度orchestrator 控制),优先级高于被动等待
  • http_pull agent 通过 dequeue 自行认领,不需要 orchestrator 主动分配
  • 两阶段简单清晰,不需要复杂的调度算法

Decision 3: Coordinator 显式指派

选择: 新增 POST /api/v1/tasks/{id}/assign 端点

{
  "agent_id": "hermes-worker-01",
  "execution_mode": "http_pull"  // optional, auto-detect if omitted
}

理由:

  • coordinatorJeeves可能比自动调度更了解哪个 agent 适合
  • 支持跨任务指派(如"这个文档任务给 Hermes"
  • 指派后任务不再是 Undecided,直接进入执行

Decision 4: Dequeue 查询条件

选择: dequeue 查询改为 execution_mode IN ('http_pull', 'undecided')

理由:

  • 纯 http_pull 任务直接匹配
  • 被自动标记为 http_pull 的任务也能匹配
  • 如果调度器还没来得及处理 Undecidedagent 也能直接拉走(降级为 http_pull

Risks / Trade-offs

  • [竞争条件] ssh_cli dispatch 和 http_pull dequeue 可能同时抢同一个 Undecided 任务 → 用 SQLite 事务保证原子性dequeue 用 UPDATE ... RETURNING 原子操作
  • [调度延迟] Undecided 任务可能等一个 dispatch cycle 才被标记为 http_pull → dequeue 直接查 Undecided 可以缓解