# 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 缺失问题。
