AstroResearch/docs/architecture/agent/tasks.md
Asfmq f6df9d8136 feat: Agent 思考模式前端可控、子代理全链路持久化、权限系统、工具 ID 追踪体系、前端面板与文档架构重构
- AgentConfig/LlmClient 新增 enable_thinking 参数,前端 SSE 请求传递 thinking
  开关,仅千问/DashScope 时启用
  - 完善权限系统,支持细粒度的权限控制和用户权限申请
  - delegate_research 工具重命名为 subagent,SubAgentTool/SubAgentRunner 重构
  - 子代理消息(system/user/assistant/tool)持久化到 agent_messages 表,带 agent_name 标识
  - 子代理活动日志(工具调用列表+思考摘要)注入返回结果,Hooks 获得正确 session_id 和 subagent_name
  - LLM 工具调用 ID 回退生成 UUID(llm.rs),ToolCall/ToolResult SSE 事件增加 id/tool_call_id 双字段
  - ToolContext 扩展 session_id/sse_tx/enable_thinking 字段,executor 统一注入而非构造函数传参
  - agent_messages 新增 metadata+raw_json 列,agent_sessions 暴露 summary 字段
  - 删除文件级 transcript 快照(compact.rs),改为依赖 DB 持久化
  - ResearchAgentPanel 重写:TimelineItem 类型替代 StreamStep,支持会话历史回放
  - 新增 AgentMetricsPanel/AskUserQuestionCard/AuditLogViewer 三个前端组件,types.ts 完整类型定义
  - docs/architecture/ 分层重组:概览/核心模块/核心工作流 + agent/ 子目录 11 篇专题文档
  - docs/api.md 补充 RAG/Target/Agent 接口,docs/development.md 新建开发指南
  - .env.example 完全重写,补充 FALLBACK_MODEL 等变量说明
2026-06-18 01:21:02 +08:00

234 lines
8.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 任务系统 (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<br/>(纯格式化, 并发安全, 无副作用)"]
RT --> Context["context.rs<br/>restore_tasks_from_db()<br/>(每 turn 开始时注入)"]
RT --> TaskBoard["TaskBoard<br/>(跨 session 共享看板)<br/>can_start() / claim_task() (原子)<br/>list_available_tasks()"]
TodoWrite --> Persist["persist_tasks<br/>INSERT OR REPLACE INTO<br/>agent_tasks"]
Persist --> DB["SQLite: agent_tasks<br/>(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<br/>(回退)
pending --> in_progress: claim_task()<br/>或 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`,返回 `<persisted-output>` 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 字段引入 |