GPU_GUARD_MONOREPO/docs/superpowers/specs/2026-04-14-tool-visibility-classification-design.md
2026-05-20 21:39:12 +08:00

198 lines
7.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 工具可见性分类机制设计
> 日期: 2026-04-14
> 状态: 待实施
## 问题背景
当前 Agent 的 todo任务规划工具在执行时每次更新任务状态都会触发 `tool_call` + `tool_result` 事件,前端以"工具调用成功"卡片的形式展示。而 `todo-card` 组件本身已经实时展示了任务的完成状态(划掉已完成项)。这导致:
1. **重复展示**: 4 个任务完成 → 4 次 todo-card 划线 + 4 个"工具完成"卡片
2. **前端 hack**: 当前通过 `sp.name !== 'todo'` 过滤隐藏,治标不治本
3. **skill 与工具混用同一组件**: skill 执行和普通工具调用都用 `skill-card.vue` 展示,缺乏区分
## 设计目标
1. 从架构层面区分"内部工具"和"用户可见工具",内部工具不产生对话流 UI 反馈
2. skill 和普通工具调用使用不同的展示组件
3. 机制通用,未来新增内部工具只需设置一个属性
## 方案工具可见性分类visibility 属性)
### 核心概念
在工具定义层引入 `visibility` 属性,从源头控制工具的 UI 可见性:
| visibility | 含义 | 前端行为 | 示例 |
|---|---|---|---|
| `internal` | 内部状态管理工具 | 不推送 `tool_call`/`tool_result` 事件,只走专属事件通道 | todo |
| `tool` | 普通工具(默认值) | 推送 `tool_call`/`tool_result`,用 `tool-card` 展示 | 搜索、代码执行 |
| `skill` | Skill 技能(预留值) | 当前 Skill 走独立通道(`skill_start`/`skill_end`),不使用此字段 | 预留 |
### 参考Hermes Agent 的做法
Hermes 项目中 todo 工具是纯内部状态管理,不向外部推送事件。返回值仅作为 LLM 工具响应返回给模型,状态变化只存储在内存中的 TodoStore 实例。子代理的中间工具调用对父代理完全隐藏。
## 详细设计
### 1. 后端:工具类型定义扩展
**文件**: `packages/backend/src/modules/netaclaw/tools/common.ts`
`AgentToolWithMeta` 类型上新增 `visibility` 字段:
```typescript
export type ToolVisibility = 'internal' | 'tool' | 'skill';
export type AgentToolWithMeta<TParams extends TSchema, TResult> =
AgentTool<TParams, TResult> & {
ownerOnly?: boolean;
displaySummary?: string;
visibility?: ToolVisibility; // 新增:默认 'tool'
};
```
### 2. 后端:执行层根据 visibility 过滤回调
**文件**: `packages/backend/src/modules/netaclaw/runtime/attempt.ts`
在工具执行循环中,根据工具的 `visibility` 决定是否触发 `onToolCall`/`onToolResult`
```typescript
for (const tc of response.toolCalls) {
toolCallCount++;
const tool = tools.find(t => t.name === tc.name);
const visibility = tool?.visibility ?? 'tool';
// 仅非 internal 工具触发 UI 回调
if (visibility !== 'internal') {
onToolCall?.(tc.name, JSON.parse(tc.arguments));
}
// ... 执行工具逻辑不变 ...
if (visibility !== 'internal') {
onToolResult?.(tc.name, resultText);
}
conversation.push({ role: 'tool', content: resultText, toolCallId: tc.id });
}
```
关键点:
- `internal` 工具的执行结果仍然写入 `conversation`LLM 需要看到工具返回值)
- 只是不触发 `onToolCall`/`onToolResult` 回调(不推送到前端 WebSocket
- todo 的 `onTodoUpdate` 专属回调不受影响todo-card 仍然实时更新
### 3. 后端todo 工具标记为 internal
**文件**: `packages/backend/src/modules/netaclaw/runtime/agent.ts`
```typescript
const todoTool: AnyAgentTool = {
...todoToolSchema,
label: '任务规划',
visibility: 'internal', // 标记为内部工具
execute: async (_id: string, args: any) => {
const result = executeTodo(todoStore, args);
params.onTodoUpdate?.(result);
return JSON.stringify(result);
},
};
```
### 4. 前端Store 层区分 skill 和 tool 来源
**文件**: `packages/frontend/src/modules/agent/store/chat.ts`
`SkillProgress` 类型中新增 `source` 字段:
```typescript
interface SkillProgress {
name: string;
label: string;
status: 'running' | 'done' | 'error';
/** 来源类型skill=Skill技能, tool=普通工具调用。历史数据可能无此字段 */
source?: 'skill' | 'tool'; // 新增:区分来源(可选,兼容历史数据)
result?: any;
tokens?: number;
percent?: number;
step?: string;
detail?: string;
```
事件处理调整:
- `skill_start` 事件 → `source: 'skill'`
- `tool_call` 事件 → `source: 'tool'`
移除 `visibleSkillProgress` 中的 `sp.name !== 'todo'` 过滤(后端已不再推送 todo 的 tool_call/tool_result
### 5. 前端:新建 tool-card.vue 组件
**文件**: `packages/frontend/src/modules/agent/components/tool-card.vue`(新建)
工具调用卡片,比 skill-card 更简洁:
- 只显示工具名 + 状态图标running 旋转 / done 勾 / error 叉)
- 无进度条、无步骤描述
- 可展开查看工具返回结果
### 6. 前端chat.vue 根据 source 选择组件
**文件**: `packages/frontend/src/modules/agent/views/chat.vue`
```vue
<div class="skill-card-row" v-for="sp in skillProgress" :key="sp.name">
<skill-card v-if="sp.source === 'skill'" v-bind="sp" />
<tool-card v-else v-bind="sp" />
</div>
```
移除 `visibleSkillProgress` 计算属性,直接使用 `skillProgress`
## 改动文件清单
| 文件 | 改动类型 | 说明 |
|------|---------|------|
| `packages/backend/src/modules/netaclaw/tools/common.ts` | 修改 | 新增 `ToolVisibility` 类型和 `visibility` 字段 |
| `packages/backend/src/modules/netaclaw/runtime/attempt.ts` | 修改 | 根据 visibility 过滤 onToolCall/onToolResult 回调 |
| `packages/backend/src/modules/netaclaw/runtime/agent.ts` | 修改 | todo 工具设置 `visibility: 'internal'` |
| `packages/frontend/src/modules/agent/store/chat.ts` | 修改 | SkillProgress 新增 source 字段,移除 todo 过滤 |
| `packages/frontend/src/modules/agent/views/chat.vue` | 修改 | 移除 visibleSkillProgress hack根据 source 选组件 |
| `packages/frontend/src/modules/agent/components/tool-card.vue` | 新建 | 工具调用卡片组件 |
## 数据流对比
### 改动前todo 工具)
```
Agent 调用 todo → executeTodo()
├→ onToolCall('todo', args) → WS: tool_call → 前端 skillProgress (被 filter 隐藏)
├→ onTodoUpdate(data) → WS: todo_update → 前端 todo-card ✓
└→ onToolResult('todo', res) → WS: tool_result → 前端 skillProgress (被 filter 隐藏)
```
### 改动后todo 工具, visibility: 'internal'
```
Agent 调用 todo → executeTodo()
├→ onToolCall 不触发 → 无 WS 事件
├→ onTodoUpdate(data) → WS: todo_update → 前端 todo-card ✓
└→ onToolResult 不触发 → 无 WS 事件
```
### 改动后(普通工具, visibility: 'tool'
```
Agent 调用搜索 → execute()
├→ onToolCall('search', args) → WS: tool_call → 前端 tool-card (source: 'tool')
└→ onToolResult('search', res) → WS: tool_result → 前端 tool-card ✓
```
### 改动后Skill, visibility: 'skill'
```
Skill 执行
├→ onSkillStart(name, label) → WS: skill_start → 前端 skill-card (source: 'skill')
├→ onProgress(...) → WS: progress → 前端 skill-card 进度更新
└→ onSkillEnd(name, status) → WS: skill_end → 前端 skill-card ✓
```
## 扩展性
未来如需新增内部工具(如 memory 记忆管理、context 上下文管理),只需在工具定义时设置 `visibility: 'internal'`,并为其建立专属事件通道和 UI 组件即可。无需修改执行引擎或前端过滤逻辑。