13 KiB
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 主流程
- 用户在普通聊天页向 Agent 发消息
ChatOrchestratorService创建或续用会话- 运行主 Agent
- 主 Agent 触发
delegate_task/delegate_parallel - 聊天页显示“子代理执行”卡片
- 子代理状态实时更新
- 主 Agent 汇总结果并生成最终回复
- assistant message 保存后,其数据库
id作为本轮subagent batch的稳定锚点 - 历史恢复时按 assistant message metadata 或数据库聚合重建卡片
4.2 稳定锚点规则
本次固定采用“预创建 assistant 占位消息”方案,不使用临时 UUID 作为批次锚点。
执行顺序:
- 保存 user message
- 立即预创建一条 assistant message,占位内容为空,拿到数据库
assistantMessageId - 本轮所有
subagent_session.parentMessageId、WebSocket 事件parentMessageId、前端卡片 key 都使用这个assistantMessageId - 主 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_sessionnetaclaw_message
因此本次必须同步扩展删除链:
- 删除单个 session 时删除对应
subagent_session - 删除全部 session 时同步清理
否则会出现孤儿记录。
6. Agent 配置模型
netaclaw_agent 新增字段:subagentConfig
type AgentSubagentConfig = {
enabled?: boolean;
maxConcurrent?: number;
allowedPresetAgentIds?: number[];
allowedToolNames?: string[];
};
6.1 配置优先级
Agent 级配置 > 全局默认配置 > 系统保底值
6.2 全局默认配置
在 config.default.ts 中新增:
netaclaw: {
subagent: {
enabled: true,
maxConcurrent: 3,
blockedToolNames: [
'delegate_task',
'delegate_parallel',
'clarify',
'memory_save',
'memory_recall',
],
}
}
7. 工具设计
7.1 delegate_task
type DelegateTaskArgs = {
goal: string;
context?: string;
agentId?: number;
mode?: 'preset';
toolNames?: string[];
maxToolRounds?: number;
};
7.2 delegate_parallel
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.tsservice/subagent.tsservice/chat_orchestrator.tstools/builtin/delegate_task.tstools/builtin/delegate_parallel.ts
9.2 ChatOrchestratorService
职责:
- 统一 WS 与 HTTP 执行入口
- 创建和保存 user / assistant message
- 构造主 Agent 运行时配置
- 注入
session-subagentcontext - 收集
subagent batch - 写 assistant metadata
- 返回给调用方:
- finalContent
- usage
- toolCallCount
- assistantMessageId
- subagentBatches
9.3 SubagentService
职责:
- 解析
preset - 限制可用工具
- 构建子 Agent prompt
- 创建 / 更新
subagent_session - 回调 supervisor:
onBatchStartonUpdateonDone
10. WebSocket 协议
新增事件:
subagent_batch_startsubagent_updatesubagent_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 结构:
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 恢复优先级
- 从 assistant message
metadata.subagents恢复 - 若 metadata 缺失,再按
(sessionId, parentMessageId)聚合subagent_session
11.2 前端恢复要求
store/chat.ts 现有逻辑只恢复:
todoStateskillExecutions
因此本次必须新增显式恢复函数:
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 聊天页顺序
- 当前 assistant message 关联的
todo-card - 当前 assistant message 关联的
subagent-batch-card - 当前 assistant message 关联的
skill-card/tool-card thinking-block- 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 缺失问题。