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)
This commit is contained in:
Zer4tul 2026-05-13 05:29:12 +08:00
parent a18cb2824e
commit 48c93e2ce9
13 changed files with 639 additions and 13 deletions

View file

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-12

View file

@ -0,0 +1,75 @@
## 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` 端点
```json
{
"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 的任务也能匹配
- 如果调度器还没来得及处理 `Undecided`agent 也能直接拉走(降级为 http_pull
## Risks / Trade-offs
- **[竞争条件]** ssh_cli dispatch 和 http_pull dequeue 可能同时抢同一个 `Undecided` 任务 → 用 SQLite 事务保证原子性dequeue 用 `UPDATE ... RETURNING` 原子操作
- **[调度延迟]** Undecided 任务可能等一个 dispatch cycle 才被标记为 http_pull → dequeue 直接查 Undecided 可以缓解

View file

@ -0,0 +1,36 @@
## Why
任务创建时硬编码 `execution_mode = SshCli`forgejo.rs:234导致所有从 webhook 创建的任务都走 ssh_cli 路径。
实际场景中,执行模式应该在 dispatch 时动态决定:
- Hermes 是 http_pull agent有自己的调度器无法被 SSH 调度
- Claude Code 在 WSL2 上可以被 SSH 调度,但 arm0 到 WSL2 的连通性可能变化
- 未来可能一个任务同时有 ssh_cli 和 http_pull 的 agent 都能做
当前硬编码导致的问题:
- Hermes dequeue 拿不到任务(因为任务被标记为 ssh_clidequeue 只查 http_pull
- 需要手动改 DB 才能让 http_pull agent 接任务
- coordinator 无法显式指派任务给特定 agent
## What Changes
- `ExecutionMode` enum 新增 `Undecided` 变体(任务创建时的默认值)
- Webhook 创建任务时不再硬编码 `SshCli`
- Dispatch loop 动态决定执行模式:
- 有匹配的 ssh_cli host 且 agent online → ssh_cli立即执行
- 没有匹配的 ssh_cli host → 标记为 http_pull等待 agent dequeue
- 新增 APIcoordinator 可以显式指派任务给特定 agent指定 agent_id
- dequeue API 查询条件更新:`execution_mode IN ('http_pull', 'undecided')`
## Capabilities
### Modified Capabilities
- `task-lifecycle`: 任务状态机增加 undecided → ssh_cli/http_pull 的自动转换
- `agent-registry`: dispatch 逻辑改为两阶段匹配
## Impact
- **数据模型**`ExecutionMode` enum 新增 `Undecided`
- **API**新增指派端点dequeue 查询条件变更
- **dispatch loop**:核心调度逻辑重写
- **向后兼容**:已有的 `ssh_cli`/`http_pull` 任务不受影响

View file

@ -0,0 +1,82 @@
## ADDED Requirements
### Requirement: ExecutionMode enum includes Undecided variant
`ExecutionMode` enum SHALL include an `Undecided` variant as the default for newly created tasks.
#### Scenario: Task created via Forgejo webhook
- **WHEN** a Forgejo Issue webhook creates a task
- **THEN** `execution_mode` SHALL be `Undecided`
- **AND** the task SHALL be eligible for both ssh_cli dispatch and http_pull dequeue
#### Scenario: Task created via API
- **WHEN** a task is created via direct API call without specifying execution_mode
- **THEN** `execution_mode` SHALL default to `Undecided`
### Requirement: Two-phase dispatch loop
The dispatch loop SHALL use a two-phase approach to handle `Undecided` tasks.
#### Scenario: Undecided task with matching ssh_cli host
- **GIVEN** an `Undecided` task with labels `["agent:code"]`
- **AND** a registered ssh_cli host with agent capabilities matching `["agent:code"]`
- **WHEN** the dispatch loop runs
- **THEN** the task SHALL be assigned `execution_mode = SshCli`
- **AND** the task SHALL be dispatched via SSH for execution
#### Scenario: Undecided task with no matching ssh_cli host
- **GIVEN** an `Undecided` task with labels `["agent:review", "agent:document"]`
- **AND** no registered ssh_cli host with matching capabilities
- **WHEN** the dispatch loop runs
- **THEN** the task SHALL be assigned `execution_mode = HttpPull`
- **AND** the task SHALL become available for http_pull dequeue
#### Scenario: Undecided task with matching ssh_cli host but agent offline
- **GIVEN** an `Undecided` task with matching ssh_cli host
- **AND** the ssh_cli host is unreachable or agent is offline
- **WHEN** the dispatch loop runs
- **THEN** the task SHALL remain `Undecided` (retry next cycle)
- **AND** the task SHALL also be available for http_pull dequeue (fallback)
### Requirement: Coordinator explicit assignment
The API SHALL provide an endpoint for coordinators to explicitly assign tasks to specific agents.
#### Scenario: Coordinator assigns task to specific agent
- **GIVEN** a task in `Created` or `Undecided` status
- **WHEN** coordinator calls `POST /api/v1/tasks/{id}/assign` with `{"agent_id": "hermes-worker-01"}`
- **THEN** the task SHALL be assigned to the specified agent
- **AND** execution_mode SHALL be auto-detected from the agent's registration type (http_pull for registered agents, ssh_cli for configured hosts)
- **AND** the task status SHALL transition to `Assigned`
#### Scenario: Coordinator assigns to non-existent agent
- **WHEN** coordinator calls assign with an unknown agent_id
- **THEN** the API SHALL return 404 Not Found
#### Scenario: Coordinator assigns already-running task
- **WHEN** coordinator calls assign on a task in `Running` or `Completed` status
- **THEN** the API SHALL return 400 Bad Request
### Requirement: Dequeue accepts Undecided tasks
The dequeue endpoint SHALL return tasks with `execution_mode` of either `HttpPull` or `Undecided`.
#### Scenario: Agent dequeues Undecided task
- **GIVEN** an `Undecided` task matching the agent's capabilities
- **WHEN** an http_pull agent calls dequeue
- **THEN** the task SHALL be returned
- **AND** `execution_mode` SHALL be atomically updated to `HttpPull`
- **AND** the task SHALL be assigned to the dequeuing agent
#### Scenario: No race condition between dispatch and dequeue
- **GIVEN** an `Undecided` task
- **WHEN** both ssh_cli dispatch and http_pull dequeue attempt to claim it simultaneously
- **THEN** exactly one SHALL succeed (atomic claim via DB transaction)
- **AND** the other SHALL get no task / skip the task
### Requirement: Backward compatibility
Existing tasks with `execution_mode = SshCli` or `HttpPull` SHALL continue to work without changes.
#### Scenario: Pre-existing SshCli task
- **WHEN** a task already has `execution_mode = SshCli`
- **THEN** the dispatch loop SHALL process it as before (no change)
#### Scenario: Pre-existing HttpPull task
- **WHEN** a task already has `execution_mode = HttpPull`
- **THEN** the dequeue endpoint SHALL return it as before (no change)

View file

@ -0,0 +1,52 @@
## 1. 数据模型
- [ ] 1.1 `ExecutionMode` enum 新增 `Undecided` 变体
- [ ] 1.2 Task 默认 execution_mode 改为 `Undecided`
- [ ] 1.3 DB schema 更新(如需要)
- [ ] 1.4 单元测试Undecided 序列化/反序列化
## 2. Forgejo Webhook
- [ ] 2.1 移除 `forgejo.rs` 中硬编码的 `ExecutionMode::SshCli`
- [ ] 2.2 改为 `ExecutionMode::Undecided`
- [ ] 2.3 测试webhook 创建的任务 execution_mode 为 Undecided
## 3. Dispatch Loop 重写
- [ ] 3.1 Phase 1扫描 Undecided 任务,尝试匹配 ssh_cli host
- 匹配成功 → 标记 SshCli + 执行
- 匹配失败或 host offline → 保持 Undecided
- [ ] 3.2 Phase 2超时未匹配的 Undecided 任务标记为 HttpPull
- 超时阈值可配置(默认 30s即 3 个 dispatch cycle
- 或者:直接让 dequeue 也能拉 Undecided更简单
- [ ] 3.3 单元测试:两个阶段的各种场景
- [ ] 3.4 集成测试:混合 ssh_cli + http_pull 环境
## 4. Dequeue API 更新
- [ ] 4.1 SQL 查询改为 `execution_mode IN ('http_pull', 'undecided')`
- [ ] 4.2 Dequeue 时原子更新 execution_mode 为 HttpPull如果原为 Undecided
- [ ] 4.3 测试dequeue Undecided 任务返回 200 + 正确赋值
## 5. Coordinator 指派 API
- [ ] 5.1 新增 `POST /api/v1/tasks/{id}/assign`
- 请求体:`{"agent_id": "...", "execution_mode": "..."(可选)}`
- 自动检测:注册的 http_pull agent → HttpPull配置的 ssh_cli host → SshCli
- 错误处理404agent 不存在、400任务状态不允许
- [ ] 5.2 路由注册
- [ ] 5.3 测试:指派成功、指派失败的各种场景
## 6. 文档更新
- [ ] 6.1 API 参考新增 assign 端点
- [ ] 6.2 Skill 更新dequeue 现在也能拿到 Undecided 任务
- [ ] 6.3 架构文档更新:两阶段 dispatch 说明
## 7. 验证
- [ ] 7.1 端到端测试webhook 创建任务 → dispatch → ssh_cli 执行
- [ ] 7.2 端到端测试webhook 创建任务 → 无 ssh_cli 匹配 → http_pull dequeue
- [ ] 7.3 端到端测试coordinator 指派 → agent 执行
- [ ] 7.4 竞争条件测试dispatch 和 dequeue 同时抢任务
- [ ] 7.5 向后兼容:已有 SshCli/HttpPull 任务不受影响