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:
parent
a18cb2824e
commit
48c93e2ce9
13 changed files with 639 additions and 13 deletions
2
openspec/changes/dynamic-execution-mode/.openspec.yaml
Normal file
2
openspec/changes/dynamic-execution-mode/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-05-12
|
||||
75
openspec/changes/dynamic-execution-mode/design.md
Normal file
75
openspec/changes/dynamic-execution-mode/design.md
Normal 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
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- coordinator(Jeeves)可能比自动调度更了解哪个 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 可以缓解
|
||||
36
openspec/changes/dynamic-execution-mode/proposal.md
Normal file
36
openspec/changes/dynamic-execution-mode/proposal.md
Normal 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_cli,dequeue 只查 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)
|
||||
- 新增 API:coordinator 可以显式指派任务给特定 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` 任务不受影响
|
||||
|
|
@ -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)
|
||||
52
openspec/changes/dynamic-execution-mode/tasks.md
Normal file
52
openspec/changes/dynamic-execution-mode/tasks.md
Normal 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
|
||||
- 错误处理:404(agent 不存在)、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 任务不受影响
|
||||
Loading…
Add table
Add a link
Reference in a new issue