# 任务系统 (Task System)
任务系统为 Agent 提供 **规划 → 执行跟踪 → 状态持久化 → 跨 turn 恢复** 的完整闭环,参考 Claude Code s12 Task System 和 s17 Autonomous Agents 设计。
---
## 概览
```mermaid
flowchart TD
subgraph RT["AgentRuntime (run_react_loop)"]
direction TB
S1["1. LLM 调用"]
S2["2. 检测 todo_write 调用"]
S3["3. persist_tasks() 持久化"]
S4["4. 每 3 步 nag reminder"]
S5["5. context.rs 恢复任务状态"]
end
RT --> TodoWrite["TodoWriteTool
(纯格式化, 并发安全, 无副作用)"]
RT --> Context["context.rs
restore_tasks_from_db()
(每 turn 开始时注入)"]
RT --> TaskBoard["TaskBoard
(跨 session 共享看板)
can_start() / claim_task() (原子)
list_available_tasks()"]
TodoWrite --> Persist["persist_tasks
INSERT OR REPLACE INTO
agent_tasks"]
Persist --> DB["SQLite: agent_tasks
(session_id, task_id UNIQUE)"]
```
---
## 数据库 Schema
```sql
-- migrations/20260616000000_agent_tasks.sql
CREATE TABLE agent_tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
task_id TEXT NOT NULL, -- LLM 生成的任务唯一标识
content TEXT NOT NULL DEFAULT '', -- 任务描述
status TEXT NOT NULL DEFAULT 'pending'
CHECK(status IN ('pending', 'in_progress', 'completed')),
blocked_by TEXT NOT NULL DEFAULT '[]', -- JSON 数组: ["task_1", "task_2"]
owner TEXT NOT NULL DEFAULT '', -- 归属 agent (lead / sub_xxx / teammate)
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (session_id) REFERENCES agent_sessions(session_id) ON DELETE CASCADE
);
-- 索引
CREATE UNIQUE INDEX idx_agent_tasks_session_task ON agent_tasks(session_id, task_id);
CREATE INDEX idx_agent_tasks_session ON agent_tasks(session_id);
CREATE INDEX idx_agent_tasks_status ON agent_tasks(session_id, status);
```
关键设计点:
- **(session_id, task_id) 联合唯一索引**:支持 `INSERT ... ON CONFLICT DO UPDATE` 的 upsert 语义
- **`blocked_by` 存 JSON 数组**:如 `["1", "2"]` 表示依赖任务 1 和 2 必须先完成,形成 DAG
- **`owner` 字段**(`migrations/20260618000000_agent_identity.sql` 引入):支持多 Agent 场景下的任务归属
---
## 任务状态机
```mermaid
stateDiagram-v2
[*] --> pending: 初始状态
pending --> pending: LLM 标记 completed
(回退)
pending --> in_progress: claim_task()
或 LLM 标记 in_progress
in_progress --> completed: LLM 标记 completed
completed --> [*]: 终点状态
note right of in_progress: 同一时刻只能有一个
```
约束规则:
1. **最多一个 `in_progress`**:TodoWriteTool 在格式化输出时检测并警告(`tools/todo.rs:115-118`)
2. **DAG 依赖**:TaskBoard::can_start() 检查所有 blockedBy 依赖是否为 completed(`task_board.rs:33-63`)
3. **原子认领**:claim_task() 使用乐观并发控制,`WHERE owner IS NULL` 保证不被重复认领
4. **应用层验证**:persist_tasks 只做自引用检测,不做完整的循环依赖检测(文档明确标注)
---
## 核心组件
### TodoWriteTool (`tools/todo.rs`)
LLM 通过 function calling 调用的工具,声明参数:
```json
{
"todos": [
{"id": "1", "content": "搜索相关文献", "status": "completed"},
{"id": "2", "content": "分析论文方法", "status": "in_progress", "blockedBy": ["1"]},
{"id": "3", "content": "撰写综述", "status": "pending", "blockedBy": ["2"]}
]
}
```
**设计原则 — 工具层与持久化层分离**:
- `TodoWriteTool::execute()` 只做格式化和约束校验,不写数据库
- `is_concurrency_safe()` 返回 `true`(纯格式化,无副作用)
- 实际的 SQLite 持久化由 `AgentRuntime::run_react_loop()` 在检测到 `todo_write` 调用后统一完成(`runtime/mod.rs:845-854`)
### persist_tasks() (`tools/todo.rs`)
```rust
pub async fn persist_tasks(
db: &SqlitePool,
session_id: &str,
todos: &[serde_json::Value],
owner: &str, // "lead" / "sub_xxx" / teammate name
) -> anyhow::Result<()>
```
- 使用 `INSERT ... ON CONFLICT(session_id, task_id) DO UPDATE SET ...` 实现 upsert
- 自动跳过任务对自身的引用(`blockedBy` 中包含自身 ID 时忽略并记录 warn)
- 作为公开 API 导出(`tools/mod.rs:49`),允许 teammate、外部调用者直接操作
### TaskBoard (`task_board.rs`)
共享任务看板,提供跨 Agent 的任务可见性:
| 方法 | 功能 | 并发策略 |
|:---|:---|:---|
| `can_start(session_id, task_id)` | 遍历 blockedBy,检查所有依赖是否 completed | 只读,天然安全 |
| `claim_task(session_id, task_id, claimant)` | 原子认领(status→in_progress, owner→claimant) | `WHERE owner IS NULL OR owner = ''` |
| `list_available_tasks(limit)` | 列出所有可认领的 pending 任务,**跨 session** | 只读,附加 can_start 解析 |
认领 SQL 的原子性保证:
```sql
UPDATE agent_tasks SET owner = ?, status = 'in_progress'
WHERE session_id = ? AND task_id = ?
AND (owner IS NULL OR owner = '' OR status = 'pending')
```
`rows_affected() > 0` 表示认领成功,否则已被他人认领。
---
## ReAct 循环中的集成点
在 `AgentRuntime::run_react_loop()` 中有三个关键集成点:
### todo_write 检测与持久化 (`mod.rs:752-855`)
```rust
// 检测 todo_write 调用
let called_todo_write = tool_calls.iter().any(|tc| tc.function.name == "todo_write");
if called_todo_write {
steps_since_last_todo = 0; // 重置 nag 计数器
}
// 工具执行完成后持久化
if called_todo_write {
for prep in &prepared_calls {
if prep.tool_name == "todo_write" {
if let Some(todos) = prep.args.get("todos").and_then(|t| t.as_array()) {
let _ = persist_tasks(db, sid, &todos_vec, "lead").await;
}
}
}
}
```
### 进度催促 Nag Reminder (`mod.rs:589-596`)
每 3 步未调用 `todo_write`,自动向 LLM 注入提醒:
```
提醒:你已经连续多步未更新任务计划。建议调用 todo_write 工具复盘当前进度并规划后续步骤。
```
`nag_after_steps = 3`,`steps_since_last_todo` 在每次非 todo_write 调用后递增。
### 跨 Turn 任务恢复 (`context.rs:32-34`)
每个新 turn 开始时,`restore_tasks_from_db()` 从 agent_tasks 表查询当前 session 的所有任务,格式化后注入 LLM 上下文:
```
[当前任务状态]
以下是上次会话中持久化的任务计划,请基于最新状态继续工作:
✅ [1] 文献检索
✅ [2] 文献分析
🔄 [3] 撰写综述 (依赖: 1,2)
⏳ [4] 最终校对 (依赖: 3)
使用 todo_write 工具更新任务进度。
```
System prompt 中也包含提示(`system_prompt.rs:66`):
> 任务状态会在每轮开始时从数据库恢复,请基于最新状态继续工作。
---
## 子代理与任务隔离
子代理 (`SubAgentRunner`) 与父代理在 Task 层面保持隔离:
- 子代理拥有独立的 ToolRegistry 和 ReAct 循环(`subagent.rs:248-254`)
- 子代理消息标记 `agent_name = "sub_xxx"`,父代理加载历史时只加载 `agent_name = "lead"` 的消息(`session.rs:78`)
- 子代理不直接操作父代理的 agent_tasks,只返回文本摘要
- `agent_tasks.owner` 字段已为 teammate 场景预留
---
## 辅助机制:工具结果持久化 (`tools/persist.rs`)
与 Task 系统协同工作的工具输出持久化:
| 维度 | Task 系统 | Persist 系统 |
|:---|:---|:---|
| 回答的问题 | "要做什么" | "看到了什么" |
| 触发条件 | LLM 调用 todo_write | 工具输出超过 max_output_chars |
| 存储位置 | SQLite agent_tasks 表 | 磁盘文件 |
| 幂等保证 | ON CONFLICT DO UPDATE | 独占创建 (create_new) |
当工具输出超过 `AGENT_MAX_TOOL_OUTPUT_CHARS`(默认 4000)时,完整内容写入 `{library_dir}/tool-results/{tool_call_id}.txt`,返回 `` XML 占位符,LLM 后续可通过 `read_file` 读取完整内容。
---
## 相关文件
| 文件 | 职责 |
|:---|:---|
| `src/agent/tools/todo.rs` | TodoWriteTool 定义 + persist_tasks() |
| `src/agent/task_board.rs` | TaskBoard — 跨 session 共享看板 |
| `src/agent/runtime/mod.rs` | ReAct 循环中的 nag/persist 集成 |
| `src/agent/runtime/context.rs` | restore_tasks_from_db — 跨 turn 恢复 |
| `src/agent/runtime/system_prompt.rs` | 系统提示词中的 Task 指引 |
| `src/agent/tools/persist.rs` | 工具输出磁盘持久化(辅助机制) |
| `src/agent/subagent.rs` | 子代理中的 Task 隔离 |
| `migrations/20260616000000_agent_tasks.sql` | agent_tasks 表 DDL |
| `migrations/20260618000000_agent_identity.sql` | owner 字段引入 |