# GPU Guard 会话内 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 缺失问题。