Files
sanguo_moziplus_v2/docs/design/02-main-session-delegation.md
T
2026-05-29 19:48:52 +08:00

414 lines
16 KiB
Markdown
Raw 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.
# 02-main-session-delegation.md — Main Session + Delegation 架构
**日期**: 2026-05-29
**作者**: 庞统
**状态**: 设计中
**前置**: `01-four-phase-loop.md`(四相循环 E2E 验证暴露 session 爆炸问题)
---
## 一、问题:现行 spawn 方案的 session 爆炸
### 现状
spawner 通过 `openclaw agent --session-id UUID --message "..." --json` spawn Agent
```
spawner → 新 UUID session → Agent 执行 → 进程退出 → session 残留
```
### 问题
| 问题 | 证据 | 影响 |
|------|------|------|
| **Session 爆炸** | 今天 E2E 测试 6 个 Agent 产生 68 个 session16~484 每个),其中 56 个 `gateway-fallback-*` | 文件系统堆积、磁盘浪费 |
| **空 payloads** | 绝大部分 spawn 返回 `status: None, payloads: []` | Daemon 以为任务完成了但实际没有产出 |
| **上下文丢失** | 每次 spawn 新 sessionAgent 无历史、无 workspace bootstrap | Agent 不知道自己是谁、不知道之前的决策 |
| **续杯浪费** | 续杯时 `reuse_session_id` 复用 UUID,但 Gateway 可能又创建 fallback | round_count 空转、Agent busy 但不产出 |
### 根因
`openclaw agent --session-id UUID` 的语义是"在指定 session 里执行 turn",但 Gateway 的实际行为是 find-or-create
- session 不存在 → 创建 `gateway-fallback-<UUID>` session
- 每次都是全新上下文
- Agent 没有历史,没有 SOUL.md 等身份注入
**这是结构性问题,不是 bug。** 在这个方案上继续修 bug 没有意义。
---
## 二、方案:Main Session + Delegation
### 核心思路
```
现行:Daemon 硬编码步骤 → spawner 直接 spawn 新 session → Agent 被动执行
改为:Daemon 投递任务 → Agent main session 收到 → Agent 自主分析 → sessions_spawn(subagent) 执行 → sub 完成 → Agent review → 写回黑板
```
### 架构对比
```
┌─ 现行 ──────────────────────────────────────────┐
│ │
│ Daemon/Ticker │
│ ├── broadcast → spawn_full_agent(UUID session) │
│ │ → Agent 执行固定步骤 prompt │
│ │ → 空 payloads / session 残留 │
│ ├── mention → spawn_full_agent(UUID session) │
│ └── review → spawn_full_agent(UUID session) │
│ │
└──────────────────────────────────────────────────┘
┌─ 改为 ───────────────────────────────────────────┐
│ │
│ Daemon/Ticker │
│ ├── broadcast → 投递到 Agent main session │
│ ├── mention → 投递到 Agent main session │
│ └── review → 投递到庞统 main session │
│ │
│ Agent Main Session(有完整身份+上下文) │
│ ├── 收到任务消息 │
│ ├── 分析黑板(API 读任务详情/上下文/依赖) │
│ ├── sessions_spawn(subagent, cleanup: "delete") │
│ │ └── sub 执行具体工作 → 完成后自动清理 │
│ ├── sessions_yield → 等待 sub 完成 │
│ ├── review sub 结果 │
│ └── 写回黑板(产出/状态/评论) │
│ │
└───────────────────────────────────────────────────┘
```
### 三条路径的变化
| 路径 | 现行 | 改为 |
|------|------|------|
| **Broadcast dispatch** | `spawn_full_agent(session_id=UUID)` 新 session | 投递到 main sessionAgent 自己决定执行方式 |
| **Mention** | `spawn_full_agent(session_id=UUID)` 新 session | 投递到 main sessionAgent 自主处理 |
| **Review**(庞统) | `spawn_full_agent(session_id=UUID)` 新 session | 投递到庞统 main session,庞统自主 review |
---
## 三、关键机制
### 3.1 投递到 Main Session
**方式**`openclaw agent --agent <id> --message "..."`(不传 `--session-id`
Gateway 行为(已确认):
- 不传 `--session-id` → 投递到 Agent 的 main session`agent:<id>:main`
- main session 正忙 → 消息排队,Agent 当前 turn 完成后处理
- Agent 有完整上下文(SOUL.md / MEMORY.md / workspace bootstrap
**spawner 改动**
- 新增 `deliver_to_main()` 方法,用 `openclaw agent --agent <id> --message "..." --json` 投递
- 不走 `spawn_full_agent` 的 session 管理,改用 Gateway 的原生投递机制
- counter 仍然生效(per main session 粒度,`max_per_session=1` 保证不并发)
### 3.2 Delegationsubagent-delegation skill
Agent 收到任务后,用 `sessions_spawn` 创建 subagent 执行具体工作:
```javascript
sessions_spawn({
task: "任务目标\n\n上下文信息",
cleanup: "delete", // 完成后自动清理 session
model: "zhipu/glm-5.1",
label: "T1-01"
})
```
**关键特性**
- `cleanup: "delete"` → sub 执行完 session 立即删除(transcript 保留可查)
- main session 通过 `sessions_yield` 等待 sub 完成(非阻塞,可被新消息打断)
- sub 的结果通过完成事件回传给 main session
### 3.3 续杯机制(已有,无需改动)
Agent 在 main session 中执行,如果超时/中断:
- 续杯机制(`_do_retry`)已经实现了重新投递逻辑
- counter 的 acquire/release 在 `spawn_full_agent` 内部管理
- 改用 main session 后,续杯变成"重新投递到 main session",逻辑更简单
### 3.4 Counter 调整
改用 main session 后:
- 所有 spawn 走同一个 session key`agent:main`
- `max_per_session=1` 保证同一时间只有一个 active turn
- `max_concurrent_sessions=3` 限制同一 Agent 最多 3 个不同 sessionmain + 可能的 sub
- `max_global=5` 限制全局并发
**注意**subagent 是 Agent 自己 spawn 的,不经过 Daemon counter。Daemon 只控制 main session 的投递。
---
## 四、Prompt 设计
### 4.1 投递消息格式
Daemon 投递到 main session 的消息需要包含足够的上下文,让 Agent 自主决策:
```markdown
📋 新任务到达
**任务**: {title}
**描述**: {description}
**Project**: {project_id}
**Task ID**: {task_id}
**类型**: {task_type} | **风险**: {risk_level}
**验收标准**: {must_haves}
**依赖产出**: {depends_on_outputs_summary}
**黑板 API**: http://localhost:8083/api/projects/{project_id}
- 读任务详情: GET /api/projects/{project_id}/tasks/{task_id}?expand=all
- 写产出: POST /api/projects/{project_id}/tasks/{task_id}/outputs
- 写评论: POST /api/projects/{project_id}/tasks/{task_id}/comments
- 更新状态: POST /api/projects/{project_id}/tasks/{task_id}/status
**约束**:
- 完成后必须写产出 + 标 review
- 失败了标 failed 并写明原因
- 使用 subagent-delegation skill 进行具体执行
- 禁止使用 sessions_send 直接发消息
```
### 4.2 Agent 自主决策
Agent 收到消息后的自主行为(不是硬编码步骤):
1. **分析上下文**:读黑板 API 获取任务详情、依赖、历史讨论
2. **决定执行策略**
- 简单任务 → 自己直接做
- 复杂任务 → `sessions_spawn` 拆分 sub task
- 需要协作 → @mention 其他 Agent
3. **执行**:通过 subagent 或直接执行
4. **Review**:检查 sub 结果质量
5. **写回**:产出 + 状态 + handoff comment
### 4.3 庞统 Review 消息格式
```markdown
🔍 一轮结束,需要你的 review
**Parent Task**: {title}ID: {task_id}
**Project**: {project_id}
**Round**: {round_num}/{max_rounds}
**本轮状态**:
- 完成: {done} | 失败: {failed} | 取消: {cancelled}
- 总计: {total}
**三问**:
1. Goal 还清晰吗?(是否有 goal drift)
2. 成果物覆盖 goal 了吗?(逐条检查验收标准)
3. 下一轮需要做什么?(创建新 sub tasks / 标记完成 / 调整方向)
**你可以**:
- 创建新 sub task: curl -X POST http://localhost:8083/api/projects/{project_id}/tasks ...
- 调整 goal
- 标记 GOAL_ACHIEVED
回复 GOAL_ACHIEVED 表示完成,否则系统会等待你创建新 sub tasks。
```
---
## 五、Daemon 侧改动
### 5.1 Spawner 新增方法
复用 Mail 的 `use_main_session=True` 模式,将三条路径统一:
```python
async def deliver_to_main(self, agent_id: str, message: str,
task_id: str = None,
on_complete: Callable = None) -> str:
"""投递到 Agent main session
复用 spawn_full_agent(use_main_session=True),不传 --session-id。
Gateway 自动路由到 main session。
"""
return await self.spawn_full_agent(
agent_id=agent_id,
message=message,
use_main_session=True, # 关键:走 main session
task_id=task_id,
on_complete=on_complete,
)
```
**不需要新建方法**,只需调用 `spawn_full_agent(use_main_session=True)`。Mail 路径已验证此模式。
### 5.2 Ticker 改动
| 方法 | 改动 |
|------|------|
| `_broadcast_and_dispatch` | `spawn_full_agent(..., use_main_session=True)` |
| `_process_mentions` | `spawn_full_agent(..., use_main_session=True)` |
| `_spawn_pangtong_review` | `spawn_full_agent(..., use_main_session=True)` |
| `_check_round_complete` | 保留 `reviewing` 中间态逻辑 |
| `_handle_review_conclusion` | 改为从黑板检查状态变化(复用 Mail 的 `_task_auto_complete` 模式) |
### 5.3 Dispatcher 改动
- 所有路由统一走 `use_main_session=True`
- 移除 `new_session` 参数(不再需要)
- 新增 `_task_auto_complete`(复用 Mail 的 `_mail_auto_complete` 模式,检查 outputs + status
- counter 逻辑不变(main session 粒度,per key = `agent:main`
### 5.4 废弃代码
- `spawn_full_agent``new_session` 参数标注废弃(所有调用改为 `use_main_session=True`
- `gateway-fallback-*` session 的产生路径消失
- 续杯简化:main session 的续杯由 Gateway 排队 + ticker 超时兜底处理
---
## 六、Session 生命周期对比
### 现行
```
每次 spawn → 创建新 session → 执行 → 残留 → 手动清理
gateway-fallback-* 堆积
```
### 改为
```
main session(常驻,有完整上下文)
├── 收到任务 A → spawn sub(A1, cleanup: delete) → yield → review → 写回
├── 收到任务 B → spawn sub(B1, cleanup: delete) → yield → review → 写回
└── 收到 mention → 自主处理 → 写回
sub session(临时,完成即删)
├── sub(A1) → 执行 → 完成 → session 删除(transcript 保留)
└── sub(B1) → 执行 → 完成 → session 删除(transcript 保留)
```
**Session 数量**6 个 main session(常驻)+ 临时 sub session(用完即删)
**对比现行**6 个 main + N 个 `gateway-fallback-*`(无限增长)
---
## 七、收益与风险
### 收益
| 维度 | 改善 |
|------|------|
| **Session 数量** | 从无限增长 → 6 个 main + 临时 sub |
| **Agent 上下文** | 从无历史 → 完整身份 + 记忆 + workspace |
| **代码简化** | 移除 `new_session` / `use_main_session` / `reuse_session_id` 逻辑 |
| **Prompt 灵活度** | 从硬编码步骤 → Agent 自主决策 |
| **空 payloads** | 消除(main session 有完整生命周期管理) |
### 风险
| 风险 | 缓解 |
|------|------|
| **main session 上下文膨胀** | OpenClaw 自带的 compact 机制处理;sub session 不膨胀(用完删) |
| **main session 被阻塞** | `sessions_yield` 非阻塞,可被新消息打断;Gateway 排队机制保证不丢消息 |
| **sub 质量不可控** | main session review sub 结果;reviewing 中间态保证 round 不重复触发 |
| **Prompt 依赖** | Agent 需要正确的 prompt 来知道怎么 delegationsubagent-delegation skill 已调优 |
| **Gateway 投递延迟** | main session 排队是异步的,Daemon 不同步等结果;改为"投递后等黑板状态变化" |
---
## 八、Mail 路径已验证的完整模式(可直接复用)
Mail 路径(`dispatcher.py` `_mail_on_checks_passed` + `_mail_auto_complete`)已经完整实现了 main session + 完成检测的全链路:
```
openclaw agent --agent xxx(不传 --session-id)→ Gateway 投递到 main session
→ Agent 处理 → 进程退出
→ on_complete 回调触发 → _mail_auto_complete
→ 幻觉门控:检查黑板是否有回复
→ 有回复 → 标 done
→ 无回复 → 留 working,等 ticker 超时兜底再查
```
### 已验证的机制
| 机制 | Mail 实现 | 普通 Task 复用 |
|------|----------|--------------|
| Main session 投递 | `use_main_session=True` | ✅ 直接复用 |
| `on_complete` 回调 | `_mail_on_complete``_mail_auto_complete` | ✅ 改为检查产出/状态而非邮件回复 |
| 幻觉门控 | `_mail_check_reply` 检查回复邮件 | ⚠️ 改为检查 `outputs` 表 + `status` 字段 |
| 超时兜底 | ticker 轮询 working 状态 + 再次检查 | ✅ 直接复用 |
| auto-working | `on_checks_passed` 回调中标 working | ✅ 直接复用 |
### 普通 Task 的完成检测(复用 Mail 模式)
Mail 的 `_mail_auto_complete` 检查的是"是否有回复邮件"。普通 Task 改为检查:
```python
def _task_auto_complete(self, task_id, agent_id, db_path):
"""普通 Task 完成检测(复用 Mail 模式)
检查逻辑:
1. outputs 表是否有产出(非空)
2. status 是否已由 Agent 自行更新(review/done/failed
3. 如果都没有 → 留 working,等 ticker 超时兜底
"""
conn = get_connection(db_path)
try:
row = conn.execute("SELECT status FROM tasks WHERE id=?", (task_id,)).fetchone()
if not row or row["status"] != "working":
return # Agent 已自行更新状态
# 检查是否有产出
output_count = conn.execute(
"SELECT COUNT(*) FROM outputs WHERE task_id=?", (task_id,)
).fetchone()[0]
if output_count > 0:
# 有产出 → Agent 忘了标状态,帮它标 review
conn.execute("UPDATE tasks SET status='review' WHERE id=?", (task_id,))
conn.commit()
else:
# 无产出 → 留 working 等 ticker 超时兜底
pass
finally:
conn.close()
```
### 待确认
| # | 问题 | 说明 |
|---|------|------|
| 1 | **投递消息的 prompt 模板** | 需要和 v2.8 方向的 Prompt 进化对齐(身份+能力+约束 vs 固定步骤) |
| 2 | **E2E 测试适配** | 现有 E2E 测试依赖 spawner 的 `--session-id UUID` 机制,需要适配 |
| 3 | **幻觉门控策略** | 普通 Task 用 outputs 表 + status 字段检查,还是其他方式? |
---
## 九、实施计划
### Phase 1:统一投递到 main session
- 所有 `spawn_full_agent` 调用改为 `use_main_session=True`
- 新增 `_task_auto_complete`(复用 Mail 模式)
- 单元测试覆盖
### Phase 2E2E 测试适配 + 验证
- 适配新的投递机制(不再依赖 `--session-id UUID`
- 验证 session 不爆炸
- 验证 Agent 自主决策行为
- 重跑 01-four-phase-loop 的 E2E 测试
---
## 十、参考文件
| 文件 | 说明 |
|------|------|
| `01-four-phase-loop.md` | 四相循环设计(现行方案) |
| `v2.8-direction-notes.md` | v2.8/v2.9 方向(Prompt 进化 + Daemon 退化) |
| `subagent-delegation/SKILL.md` | Delegation skillAgent 侧执行指南) |
| `counter.py` | 并发控制(per session 粒度) |
| `spawner.py` `_do_retry` | 续杯机制 |