GPU_GUARD_MONOREPO/docs/superpowers/specs/2026-04-18-session-subagent-design.md

476 lines
13 KiB
Markdown
Raw Permalink Normal View History

2026-05-21 11:20:19 +08:00
# GPU Guard 会话内 Subagent 设计
2026-05-20 21:39:12 +08:00
日期2026-04-18
## 1. 背景
当前 `netaclaw` 存在两条普通 Agent 执行入口:
- WebSocket 聊天入口:`gateway/server.ts`
- HTTP 聊天入口:`service/agent_executor.ts` + `controller/chat.ts`
同时,系统已具备:
- 普通会话内的 `todo` 任务规划能力
- 独立的 `crew` 编排体系
本次目标是为普通 Agent 会话增加类似 Hermes 的会话内 `subagent` 能力,但必须满足以下架构原则:
- 不复用 `crew_*` 数据表和 `/crew` 监控体系
- 不让 WebSocket 与 HTTP 两条入口行为分叉
- 不让实时态与历史恢复态使用不同批次锚点
- 不让普通会话只在 prompt 层“声明可委派”,而运行时没有可执行工具
## 2. 目标与非目标
### 2.1 目标
- 在普通 Agent 会话中支持动态派生 `subagent`
- `subagent` 支持:
- 预设 Agent
- 临时克隆 Agent
- 引入独立的会话级持久化模型
- 在聊天页增加专属 `subagent` 卡片并支持历史恢复
- 支持全局默认配置与 Agent 级覆盖配置
- 统一 WebSocket 与 HTTP 聊天执行链路
- 第一版仅支持单层委派
### 2.2 非目标
- 不接入 `crew` 画布、监控页、调度体系
- 不开放 `subagent` 独立详情页
- 不支持 `subagent` 调用 `clarify`
- 不支持 `subagent` 递归调用 `delegate_task` / `delegate_parallel`
- 不提供第一版人工中止、人工接管、失败重试 UI
## 3. 架构结论
本次功能不能直接在现有网关路径上“补一个工具”完成,必须先统一普通会话执行编排。
### 3.1 新的普通会话编排层
新增共享服务:`ChatOrchestratorService`
职责:
- 统一普通会话执行入口
- 装配 Agent 运行时上下文
- 为普通会话注入 `subagent supervisor context`
- 保存 user / assistant message
- 生成 assistant message 的稳定锚点
- 写回 assistant metadata
- 对外暴露事件回调接口
调用关系:
- WebSocket 网关调用 `ChatOrchestratorService.executeChat(...)`
- HTTP `agent_executor.ts` 也调用 `ChatOrchestratorService.executeChat(...)`
这样可以保证:
- 同一个 Agent 在 WS 和 HTTP 下行为一致
- `subagent` 逻辑只实现一次
- metadata 写回、工具注入、prompt 组装规则一致
### 3.2 场景化工具实例化
现有 `tool_resolver` 不能只返回 `toolNames`,因为 `delegate_task` / `delegate_parallel` 当前只在 `crewRole === 'master'` 时创建运行时实例。
因此本次必须明确区分两层:
- 工具可见性层:决定名字是否进入 prompt
- 工具实例化层:决定当前场景下是否存在实际 handler
普通会话新增场景:
- `delegationRole: 'supervisor'`
- `delegationRole: 'subagent'`
结论:
- 普通会话 supervisor 可见且可执行 `delegate_task` / `delegate_parallel`
- `subagent` 运行时必须剥离这些工具
- 不能再用“把工具名放入 toolNames 即代表可执行”这一假设
## 4. 用户体验
### 4.1 主流程
1. 用户在普通聊天页向 Agent 发消息
2. `ChatOrchestratorService` 创建或续用会话
3. 运行主 Agent
4. 主 Agent 触发 `delegate_task` / `delegate_parallel`
5. 聊天页显示“子代理执行”卡片
6. 子代理状态实时更新
7. 主 Agent 汇总结果并生成最终回复
8. assistant message 保存后,其数据库 `id` 作为本轮 `subagent batch` 的稳定锚点
9. 历史恢复时按 assistant message metadata 或数据库聚合重建卡片
### 4.2 稳定锚点规则
本次固定采用“预创建 assistant 占位消息”方案,不使用临时 UUID 作为批次锚点。
执行顺序:
1. 保存 user message
2. 立即预创建一条 assistant message占位内容为空拿到数据库 `assistantMessageId`
3. 本轮所有 `subagent_session.parentMessageId`、WebSocket 事件 `parentMessageId`、前端卡片 key 都使用这个 `assistantMessageId`
4. 主 Agent 完成后,更新这条 assistant message 的最终 `content``thinking``metadata`
这样可以保证:
- 实时事件与历史恢复使用同一个稳定键
- 不需要 `clientBatchId -> assistantMessageId` 的二次归并
- 多轮对话中,每个 subagent 卡片都能精确归属到对应 assistant 回复
## 5. 数据模型
新增实体:`netaclaw_subagent_session`
建议字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | int | 主键 |
| `sessionId` | varchar | 所属会话 ID |
| `parentMessageId` | int | 归属 assistant message ID |
| `parentToolCallId` | varchar nullable | 归属工具调用 ID |
| `parentAgentId` | int | 主 Agent ID |
| `sourceType` | varchar | `preset` |
| `presetAgentId` | int nullable | 预设 Agent ID |
| `name` | varchar | 展示名称 |
| `goal` | text | 委派目标 |
| `context` | longtext nullable | 委派上下文 |
| `status` | varchar | `queued` / `running` / `completed` / `failed` / `cancelled` |
| `model` | varchar | 实际执行模型 |
| `toolNames` | json nullable | 工具列表 |
| `summary` | longtext nullable | 结果摘要 |
| `resultPayload` | json nullable | 结构化结果 |
| `error` | longtext nullable | 错误信息 |
| `tokenUsage` | json nullable | token 用量 |
| `sortOrder` | int | 同批次排序 |
| `startedAt` | datetime nullable | 开始时间 |
| `endedAt` | datetime nullable | 结束时间 |
| `createTime` | datetime | 创建时间 |
| `updateTime` | datetime | 更新时间 |
### 5.1 索引要求
必须显式设计以下索引:
- `(sessionId, parentMessageId, sortOrder)`
- `(sessionId, status)`
- `(parentAgentId, createTime)`
理由:
- 历史恢复按会话 + 父消息聚合
- 会话清理和排障按 sessionId / status 检索
- 后续监控和排查按 parentAgentId / 时间检索
### 5.2 清理要求
现有 session 删除逻辑只会删除:
- `netaclaw_session`
- `netaclaw_message`
因此本次必须同步扩展删除链:
- 删除单个 session 时删除对应 `subagent_session`
- 删除全部 session 时同步清理
否则会出现孤儿记录。
## 6. Agent 配置模型
`netaclaw_agent` 新增字段:`subagentConfig`
```ts
type AgentSubagentConfig = {
enabled?: boolean;
maxConcurrent?: number;
allowedPresetAgentIds?: number[];
allowedToolNames?: string[];
};
```
### 6.1 配置优先级
`Agent 级配置 > 全局默认配置 > 系统保底值`
### 6.2 全局默认配置
`config.default.ts` 中新增:
```ts
netaclaw: {
subagent: {
enabled: true,
maxConcurrent: 3,
blockedToolNames: [
'delegate_task',
'delegate_parallel',
'clarify',
'memory_save',
'memory_recall',
],
}
}
```
## 7. 工具设计
### 7.1 `delegate_task`
```ts
type DelegateTaskArgs = {
goal: string;
context?: string;
agentId?: number;
mode?: 'preset';
toolNames?: string[];
maxToolRounds?: number;
};
```
### 7.2 `delegate_parallel`
```ts
type DelegateParallelArgs = {
tasks: Array<{
goal: string;
context?: string;
agentId?: number;
mode?: 'preset';
toolNames?: string[];
maxToolRounds?: number;
}>;
};
```
### 7.3 工具实例化原则
- supervisor 场景:有 schema有实际 runtime handler
- subagent 场景:无 schema无 runtime handler
- 普通 agent 关闭 subagent 配置时:不暴露 schema
因此实现不应依赖“复用 `crew` toolset 名称”作为唯一机制,而应在 resolver 内部引入会话委派场景的显式分支。
## 8. Prompt 设计
第一版不复用 `crewRole: 'sub'`
新增 `delegationRole: 'subagent'` prompt guidance。
子 Agent prompt 必须明确:
- 当前身份是会话内 `subagent`
- 只能完成当前目标
- 不得向用户发问
- 不得再次委派
- 输出必须是返回给父 Agent 的摘要
注意:
- 临时克隆不能简单复用父 Agent 原始 `systemPrompt`
- 必须追加一层强约束 guidance
- 否则父 Agent 指令中的“向用户提问”“自己规划 todo”等行为会泄漏到子 Agent
## 9. 运行时设计
### 9.1 新模块
- `entity/subagent_session.ts`
- `service/subagent.ts`
- `service/chat_orchestrator.ts`
- `tools/builtin/delegate_task.ts`
- `tools/builtin/delegate_parallel.ts`
### 9.2 `ChatOrchestratorService`
职责:
- 统一 WS 与 HTTP 执行入口
- 创建和保存 user / assistant message
- 构造主 Agent 运行时配置
- 注入 `session-subagent` context
- 收集 `subagent batch`
- 写 assistant metadata
- 返回给调用方:
- finalContent
- usage
- toolCallCount
- assistantMessageId
- subagentBatches
### 9.3 `SubagentService`
职责:
- 解析 `preset`
- 限制可用工具
- 构建子 Agent prompt
- 创建 / 更新 `subagent_session`
- 回调 supervisor
- `onBatchStart`
- `onUpdate`
- `onDone`
## 10. WebSocket 协议
新增事件:
- `subagent_batch_start`
- `subagent_update`
- `subagent_done`
### 10.1 payload 原则
实时事件必须直接使用 `parentMessageId = assistantMessageId`,不引入 `clientBatchId`
事件形态:
- `subagent_batch_start`: `{ parentMessageId, title, total }`
- `subagent_update`: `{ parentMessageId, subagent }`
- `subagent_done`: `{ parentMessageId, summary }`
因为 assistant 占位消息在 Agent 运行前已经创建所以实时状态、数据库记录、assistant metadata、前端消息流中的卡片都使用同一锚点。
## 11. 消息持久化与历史恢复
assistant message 的 `metadata.subagents` 结构:
```ts
type SubagentBatch = {
parentMessageId: number;
title: string;
items: Array<{
id: number | string;
name: string;
sourceType: 'preset';
status: 'queued' | 'running' | 'completed' | 'failed' | 'cancelled';
goal: string;
summary?: string;
error?: string;
tokenUsage?: { inputTokens: number; outputTokens: number };
sortOrder: number;
}>;
summary: {
total: number;
completed: number;
running: number;
failed: number;
};
};
```
### 11.1 恢复优先级
1. 从 assistant message `metadata.subagents` 恢复
2. 若 metadata 缺失,再按 `(sessionId, parentMessageId)` 聚合 `subagent_session`
### 11.2 前端恢复要求
`store/chat.ts` 现有逻辑只恢复:
- `todoState`
- `skillExecutions`
因此本次必须新增显式恢复函数:
- `restoreSubagentBatches(msgs)`
不能只依赖实时事件。
### 11.3 消息级内联展示要求
`subagent` 卡片必须归属于具体 assistant message而不是作为会话级全局面板展示。
前端数据结构应支持:
- 每条 assistant message 从 `metadata.subagents` 读取自己的 subagent batches
- 当前流式 assistant message 使用 `parentMessageId` 匹配实时 subagent batches
- 渲染时卡片跟随对应 assistant message 出现在消息流中
多轮会话中,如果第 2 轮和第 5 轮都触发 subagent则页面应在第 2 轮 assistant 回复附近展示第 2 轮卡片,在第 5 轮 assistant 回复附近展示第 5 轮卡片,不应把所有卡片集中显示在当前输入区上方。
## 12. 前端设计
### 12.1 新组件
- `components/subagent-batch-card.vue`
### 12.2 聊天页顺序
1. 当前 assistant message 关联的 `todo-card`
2. 当前 assistant message 关联的 `subagent-batch-card`
3. 当前 assistant message 关联的 `skill-card` / `tool-card`
4. `thinking-block`
5. assistant 消息内容
### 12.3 Agent 编辑页
`agent-edit.vue` 中增加“子代理”区块。
但要注意:
- 当前页面没有现成的 “published agent options” 数据源
- 需要新增后端 options API或复用 admin page 数据并做轻量转换
因此这是一个明确的子任务,不是现成字段拼装。
## 13. 错误处理
- 单个 subagent 失败不终止主 Agent
- 并行委派允许部分失败
- subagent 超时仅影响自身
- subagent 调用 `clarify` 直接失败
- 会话删除必须删除 `subagent_session`
## 14. 测试要求
### 14.1 后端
- resolver 的场景化工具实例化
- chat orchestrator 在 WS / HTTP 下行为一致
- subagent service 的来源解析、工具约束、状态持久化
- session 删除时清理 `subagent_session`
- assistant metadata 写回与恢复字段一致
### 14.2 前端
- store 实时事件合并
- store 历史 metadata 恢复
- `subagent-batch-card` 渲染
- 刷新后恢复
## 15. 风险与取舍
### 15.1 主要风险
- 普通会话与 `crew` 语义混淆
- WS 与 HTTP 行为分叉
- 实时批次键与历史恢复键不一致
- toolNames 可见但 runtime handler 缺失
### 15.2 对应措施
- 独立实体、独立服务、独立组件
- 引入 `ChatOrchestratorService` 统一入口
- 使用 assistant message id 作为最终锚点
- resolver 显式区分“工具可见”和“工具实例化”
## 16. 结论
本方案保留“普通会话内 subagent”目标但修正为一个可在 `Neta` 现有架构中落地的版本:
- 普通会话新增共享编排层 `ChatOrchestratorService`
- `subagent_session` 独立持久化
- `delegate_task` / `delegate_parallel` 采用场景化工具实例化
- assistant message id 作为唯一稳定历史锚点
- 聊天页新增独立 `subagent-batch-card`
- WebSocket 与 HTTP 执行链路统一
这版方案可以避免当前代码结构下最容易出现的前后端断裂、数据库孤儿记录、历史恢复错位和 runtime handler 缺失问题。