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

7.5 KiB
Raw Permalink Blame History

工具可见性分类机制设计

日期: 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 字段:

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

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 工具的执行结果仍然写入 conversationLLM 需要看到工具返回值)
  • 只是不触发 onToolCall/onToolResult 回调(不推送到前端 WebSocket
  • todo 的 onTodoUpdate 专属回调不受影响todo-card 仍然实时更新

3. 后端todo 工具标记为 internal

文件: packages/backend/src/modules/netaclaw/runtime/agent.ts

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 字段:

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

<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 组件即可。无需修改执行引擎或前端过滤逻辑。