476 lines
13 KiB
Markdown
476 lines
13 KiB
Markdown
# Neta 会话内 Subagent 设计
|
||
|
||
日期: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 缺失问题。
|