Files
sanguo_moziplus_v2/docs/design/02-main-session-delegation.md
T
2026-05-29 20:10:37 +08:00

492 lines
19 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 | `spawn_full_agent(use_main_session=True)` main session |
| **Mention** | `spawn_full_agent(session_id=UUID)` 新 session | `spawn_full_agent(use_main_session=True)` main session |
| **Review**(庞统) | `spawn_full_agent(session_id=UUID)` 新 session | `spawn_full_agent(use_main_session=True)` main session |
---
## 三、关键机制
### 3.1 投递到 Main Session
**方式**`spawn_full_agent(use_main_session=True)`(不传 `--session-id`
底层命令行:`openclaw agent --agent <id> --message "..." --json --timeout N`
Gateway 行为(已确认):
- 不传 `--session-id` → 投递到 Agent 的 main session`agent:<id>:main`
- main session 正忙 → 消息排队,Agent 当前 turn 完成后处理
- Agent 有完整上下文(SOUL.md / MEMORY.md / workspace bootstrap
**调用链路**
```
spawner.spawn_full_agent(use_main_session=True)
→ session_id = None(不传 --session-id
→ counter.acquire(agent_id, "main")
→ asyncio.create_subprocess_exec("openclaw", "agent", "--agent", "xxx", ...)
→ asyncio.create_task(_monitor_process(...)) # 异步不阻塞
→ 返回 "main"
_monitor_process()
→ await proc.wait() + 读 stdout # 异步等子进程退出
→ 子进程退出 → _handle_exit()
→ 解析 --json 输出
→ on_complete 回调(幻觉门控、标状态等)
→ counter.release(agent_id, "main")
```
**关键:`openclaw agent` 子进程内部是同步阻塞的**——它会同步等 Gateway 投递消息到 main session → Agent 执行 turn → 回复 → 子进程退出。所以 `on_complete` 只在 Agent 真正完成 turn 后才触发。
### 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 完成(**非阻塞**,可被新消息打断,Gateway 还能投递新消息)
- sub 的结果通过完成事件回传给 main session
- subagent 是 Agent 自己 spawn 的,**不经过 Daemon counter**
### 3.3 续杯机制
改用 main session 后,续杯逻辑简化:
| 维度 | 现行 | 改为 |
|------|------|------|
| 续杯 session | `reuse_session_id=UUID` 复用原 session | `use_main_session=True` 重新投递到 main session |
| 上下文 | 新 session 无历史 | main session 有完整历史 |
| counter | acquire 新 session key | acquire 同一个 "main" key |
| 失败处理 | 重试时可能创建新的 fallback session | 直接重投 main session,无 fallback 问题 |
**改动**`_do_retry` 中把 `reuse_session_id` 改为 `use_main_session=True`,移除 `reuse_session_id` 参数。
### 3.4 Counter(无需改动)
改用 main session 后 counter 逻辑不变:
- 所有 spawn 走同一个 session key`"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 改动范围
**不需要新增方法**。直接复用 `spawn_full_agent(use_main_session=True)`Mail 路径已验证此模式。
改动集中在调用侧(把参数改对)和回调侧(把完成检测从邮件改为黑板):
| 文件 | 改动点 | 改动量 |
|------|--------|-------|
| `ticker.py` | 3 处 `spawn_full_agent` 调用加 `use_main_session=True`;移除 `new_session=True` | ~15 行 |
| `dispatcher.py` | 确定性路由改 `use_main_session=True`;新增 `_task_auto_complete` | ~40 行 |
| `spawner.py` | `_do_retry` 续杯改 `use_main_session=True`;标注 `new_session`/`reuse_session_id` 废弃 | ~10 行 |
| `ticker.py` | 新增 `_task_verify_completion`(第一层幻觉门控);review 前跑 `_verify_output_files`(第二层) | ~50 行 |
**总改动量**:~115 行,涉及 3 个文件。不涉及 DB schema 变更、不涉及前端。
### 5.2 Ticker 改动
| 方法 | 改动 |
|------|------|
| `_broadcast_and_dispatch` | `spawn_full_agent(..., use_main_session=True)` 替代当前的默认参数 |
| `_process_mentions` | 同上 |
| `_spawn_pangtong_review` | 同上 |
| `_check_round_complete` | 保留 `reviewing` 中间态逻辑(SYS-BUG-01 修复) |
| `_handle_review_conclusion` | 保留,从 `on_complete` 回调中的黑板状态变化触发 |
| 新增 `_task_verify_completion` | 第一层幻觉门控(三信号检查) |
| 新增 `_verify_output_files` | 第二层幻觉门控(文件存在性检查) |
### 5.3 Dispatcher 改动
- 确定性路由改 `use_main_session=True`
- 新增 `_task_auto_complete`(复用 Mail 的 `_mail_auto_complete` 模式)
- `new_session` 参数标注废弃
### 5.4 废弃代码
- `spawn_full_agent``new_session` 参数 → 标注废弃(保留接口兼容,不再有调用方传 True)
- `reuse_session_id` 参数 → 标注废弃(续杯改用 `use_main_session=True`
- `gateway-fallback-*` session 的产生路径 → 自然消失(不再传 `--session-id UUID`
---
## 六、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` / `reuse_session_id` 不再有调用方 |
| **Prompt 灵活度** | 从硬编码步骤 → Agent 自主决策 |
| **空 payloads** | 消除(main session 有完整生命周期管理) |
| **续杯可靠性** | 不再创建 fallback session,直接重投 main session |
### 风险与缓解
| 风险 | 缓解 |
|------|------|
| **main session 上下文膨胀** | OpenClaw 自带的 compact 机制处理;sub session 不膨胀(用完删) |
| **main session 被阻塞** | `sessions_yield` 非阻塞,可被新消息打断;Gateway 排队机制保证不丢消息 |
| **sub 质量不可控** | main session review sub 结果;reviewing 中间态保证 round 不重复触发 |
| **Prompt 依赖** | 投递消息中明确告诉 Agent "优先使用 subagent-delegation skill"skill 已调优 |
| **main session 排队延迟** | Daemon 投递后 `on_complete` 异步等结果;ticker 轮询黑板状态兜底 |
---
## 八、Mail 路径已验证的完整模式(可直接复用)
Mail 路径(`dispatcher.py` `_mail_on_checks_passed` + `_mail_auto_complete`)已经完整实现了 main session + 完成检测的全链路:
```
openclaw agent --agent xxx(不传 --session-id)→ Gateway 投递到 main session
→ Agent 处理 → 子进程退出
→ _monitor_process → _handle_exit
→ on_complete 回调 → _mail_auto_complete
→ 幻觉门控:检查黑板是否有回复
→ 有回复 → 标 done
→ 无回复 → 留 working,等 ticker 超时兜底再查
→ counter.release(agent_id, "main")
```
### 已验证的机制
| 机制 | Mail 实现 | 普通 Task 复用 |
|------|----------|--------------|
| Main session 投递 | `use_main_session=True` | ✅ 直接复用 |
| `on_complete` 回调 | `_mail_on_complete``_mail_auto_complete` | ✅ 改为 `_task_auto_complete`(检查产出/状态而非邮件回复) |
| 幻觉门控 | `_mail_check_reply` 检查回复邮件 | ⚠️ 改为三信号检查(status + outputs + comments |
| 超时兜底 | ticker 轮询 working 状态 + 再次检查 | ✅ 直接复用 |
| auto-working | `on_checks_passed` 回调中标 working | ✅ 直接复用 |
| Counter acquire/release | `"main"` session key | ✅ 直接复用 |
### 普通 Task 的完成检测
Mail 的 `_mail_auto_complete` 检查"是否有回复邮件"。普通 Task 改为 `_task_auto_complete`,检查黑板三信号(详见第九节幻觉门控方案)。
---
## 九、幻觉门控方案(三层)
### 第一层:Daemon 确定性检查(on_complete 触发)
**触发时机**`on_complete` 回调(`openclaw agent` 子进程退出时)。
Agent 在 main session 里执行时子进程还活着,不会触发。不存在长任务误判。
**三信号检查**status / outputs / comments 任一存在即视为有效完成。
```python
def _task_verify_completion(self, task_id, db_path) -> bool:
"""普通 Task 完成验证(泛化 Mail 幻觉门控)"""
conn = get_connection(db_path)
try:
# 信号 1Agent 已自行更新状态(review/done/failed
row = conn.execute(
"SELECT status FROM tasks WHERE id=?", (task_id,)
).fetchone()
if not row or row["status"] != "working":
return True # Agent 自己标了终态
# 信号 2outputs 表有产出
output_count = conn.execute(
"SELECT COUNT(*) FROM outputs WHERE task_id=?", (task_id,)
).fetchone()[0]
if output_count > 0:
return True
# 信号 3:有评论/handoff(Agent 至少写了交接文档)
comment_count = conn.execute(
"SELECT COUNT(*) FROM comments WHERE task_id=? AND author != 'system'",
(task_id,)
).fetchone()[0]
if comment_count > 0:
return True
# 三无 → 可能是幻觉
return False
finally:
conn.close()
```
**行为**
- 通过 → 标 review(等待庞统 review
- 不通过 → 留 working,ticker 重查(最多 3 次,然后标 failed)
### 第二层:Daemon 产出物文件验证(review 阶段触发)
`outputs` 表中有 `content_path` 的产出,验证文件是否真实存在。放在 Daemon(确定性代码,成本低、不依赖 AI):
```python
def _verify_output_files(self, task_id, db_path) -> list[str]:
"""验证产出物文件是否真实存在"""
conn = get_connection(db_path)
outputs = conn.execute(
"SELECT content_path FROM outputs WHERE task_id=? AND content_path IS NOT NULL",
(task_id,)
).fetchall()
missing = []
for o in outputs:
if not Path(o["content_path"]).exists():
missing.append(o["content_path"])
return missing
```
**触发时机**review 阶段(庞统 review 前),Daemon 先跑一次文件验证。
**行为**missing 非空 → 在黑板 comment 中标注缺失文件,供庞统 review 时参考。
### 第三层:AI 验证(review prompt 中要求)
庞统/司马懿 review 时,在 prompt 中要求:
1. 检查产出是否覆盖 must_haves 中的验收标准
2. 如果产出不可验证,在评论中标注
这一层由 AI 做,成本高但准确度高,只在 review 时触发。
### 三层关系
```
openclaw agent 子进程退出
→ on_complete 回调
→ 第一层(三信号:status / outputs / comments
→ 有信号 → 标 review
→ 三无 → 留 workingticker 重查,最多 3 次 → failed
庞统 review
→ 第二层(文件存在性:content_path 验证)
→ 第三层(AI 验证:产出是否覆盖 must_haves
→ 通过 → GOAL_ACHIEVED 或创建新 sub task
→ 不通过 → 打回重做
```
---
## 十、实施计划
### Phase 1:统一投递到 main session + 幻觉门控
- 所有 `spawn_full_agent` 调用改为 `use_main_session=True`3 处)
- 续杯 `_do_retry` 改为 `use_main_session=True`
- 新增 `_task_verify_completion`(第一层幻觉门控)
- 新增 `_verify_output_files`(第二层幻觉门控)
- 单元测试覆盖
### 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` | 续杯机制 |
| `dispatcher.py` `_mail_auto_complete` | Mail 幻觉门控(已验证,可复用) |