GPU_GUARD_MONOREPO/docs/superpowers/plans/2026-04-16-tool-registry-patch-clarify.md
2026-05-20 21:39:12 +08:00

36 KiB
Raw Blame History

Patch/Clarify 工具 + 工具目录 + 提示词优化 实现计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking. Post-task review: After each task commit, run the simplify skill to review changed code for reuse, quality, and efficiency.

Goal: 为 Neta Agent 引擎新增 patch模糊补丁和 clarify阻塞式提问工具引入轻量工具目录重写提示词提升工具触发率。

Architecture: 新建 tools/catalog.ts 管理工具 schema 元数据不管理实例。patch 工具依赖 tools/fuzzy_match.ts 九级匹配引擎。clarify 工具通过 attempt.tsonClarifyRequest 回调实现阻塞gateway 层用 Promise+Map 协调 WebSocket 消息REST 入口降级为文本输出。

Tech Stack: TypeScript, @sinclair/typebox, Midway.js Socket.IO, Node.js fs/promises

Base path: packages/backend/src/modules/netaclaw


Task 1: 工具目录 catalog.ts

Files:

  • Create: tools/catalog.ts

  • Modify: runtime/prompt_builder.ts

  • Step 1: 创建 tools/catalog.ts

// tools/catalog.ts
/**
 * 轻量工具目录 — 只注册 schema 元数据,不管理工具实例。
 * 用于 prompt_builder 查询可用工具名列表,替代硬编码 BASE_TOOLS。
 */

export interface ToolSchema {
  name: string;
  toolset: string;
  description: string;
}

const catalog = new Map<string, ToolSchema>();

export function registerSchema(schema: ToolSchema): void {
  catalog.set(schema.name, schema);
}

export function getToolNamesByToolsets(toolsets: string[]): string[] {
  const names: string[] = [];
  for (const entry of catalog.values()) {
    if (toolsets.includes(entry.toolset)) names.push(entry.name);
  }
  return names;
}

/** 默认启用的工具集(所有 Agent 都有) */
export const TOOLSET_DEFAULTS = ['base', 'planning', 'interaction'] as const;
  • Step 2: 修改 prompt_builder.ts — 从 catalog 查询工具名

runtime/prompt_builder.ts 中硬编码的 BASE_TOOLScollectAvailableToolNames 改为从 catalog 查询:

// runtime/prompt_builder.ts — 替换原有的 BASE_TOOLS 和 collectAvailableToolNames

import { getToolNamesByToolsets, TOOLSET_DEFAULTS } from '../tools/catalog.js';

// 删除: const BASE_TOOLS = ['bash', 'read_file', 'write_file', 'list_dir', 'todo'];

export function collectAvailableToolNames(opts: CollectToolNamesOpts): string[] {
  const toolsets = [...TOOLSET_DEFAULTS];
  if (opts.memoryEnabled) toolsets.push('memory');
  if (opts.hasSkills) toolsets.push('skill');
  if (opts.crewRole === 'master') toolsets.push('crew');
  return getToolNamesByToolsets(toolsets);
}
  • Step 3: 现有工具注册到 catalog — bash.ts

tools/builtin/bash.ts 文件末尾追加:

import { registerSchema } from '../catalog.js';
registerSchema({ name: 'bash', toolset: 'base', description: bashTool.description });
  • Step 4: 现有工具注册到 catalog — file.ts

tools/builtin/file.ts 文件末尾追加:

import { registerSchema } from '../catalog.js';
registerSchema({ name: 'read_file', toolset: 'base', description: readFileTool.description });
registerSchema({ name: 'write_file', toolset: 'base', description: writeFileTool.description });
registerSchema({ name: 'list_dir', toolset: 'base', description: listDirTool.description });
  • Step 5: 现有工具注册到 catalog — memory.ts

tools/builtin/memory.ts 文件末尾追加:

import { registerSchema } from '../catalog.js';
registerSchema({ name: 'memory_save', toolset: 'memory', description: '存储、更新或删除长期记忆。' });
registerSchema({ name: 'memory_recall', toolset: 'memory', description: '搜索长期记忆中的相关信息。' });
  • Step 6: 现有工具注册到 catalog — todo_tool.ts

tools/todo_tool.ts 文件末尾追加:

import { registerSchema } from './catalog.js';
registerSchema({ name: 'todo', toolset: 'planning', description: todoToolSchema.description });
  • Step 7: 工厂工具注册到 catalog — skill 工具

在以下文件末尾各追加 registerSchema 调用schema 是静态的,只有 execute 需要运行时依赖):

tools/builtin/read_skill.ts 末尾:

import { registerSchema } from '../catalog.js';
registerSchema({ name: 'read_skill', toolset: 'skill', description: '读取指定技能的 SKILL.md 内容' });

tools/builtin/read_skill_file.ts 末尾:

import { registerSchema } from '../catalog.js';
registerSchema({ name: 'read_skill_file', toolset: 'skill', description: '读取技能的附属文件内容' });

tools/builtin/skill_manage.ts 末尾:

import { registerSchema } from '../catalog.js';
registerSchema({ name: 'skill_manage', toolset: 'skill', description: '创建、更新或删除技能' });
  • Step 8: 工厂工具注册到 catalog — crew 工具

tools/builtin/delegate_task.ts 末尾:

import { registerSchema } from '../catalog.js';
registerSchema({ name: 'delegate_task', toolset: 'crew', description: '委派任务给指定成员 Agent' });

tools/builtin/delegate_parallel.ts 末尾:

import { registerSchema } from '../catalog.js';
registerSchema({ name: 'delegate_parallel', toolset: 'crew', description: '并行委派多个任务给不同成员' });

tools/builtin/escalate.ts 末尾:

import { registerSchema } from '../catalog.js';
registerSchema({ name: 'escalate', toolset: 'crew', description: '将问题升级给用户或上级 Agent' });
  • Step 9: catalog.ts 自注册所有工具文件

消除 side-effect import 维护负担catalog.ts 自身 import 所有工具文件触发注册。

tools/catalog.ts 底部追加:

// --- 集中注册入口import 触发各工具文件的 registerSchema ---
import './builtin/bash.js';
import './builtin/file.js';
import './builtin/patch.js';
import './builtin/clarify.js';
import './builtin/memory.js';
import './builtin/read_skill.js';
import './builtin/read_skill_file.js';
import './builtin/skill_manage.js';
import './builtin/delegate_task.js';
import './builtin/delegate_parallel.js';
import './builtin/escalate.js';
import './todo_tool.js';

注意:这些 import 放在 catalog.ts 底部registerSchema 函数定义之后),确保函数已可用。消费方只需 import { getToolNamesByToolsets } from '../tools/catalog.js'

  • Step 10: 验证构建

Run: cd packages/backend && npm run build Expected: 编译成功,无类型错误

  • Step 11: Commit
git add packages/backend/src/modules/netaclaw/tools/catalog.ts \
  packages/backend/src/modules/netaclaw/tools/builtin/bash.ts \
  packages/backend/src/modules/netaclaw/tools/builtin/file.ts \
  packages/backend/src/modules/netaclaw/tools/builtin/memory.ts \
  packages/backend/src/modules/netaclaw/tools/builtin/read_skill.ts \
  packages/backend/src/modules/netaclaw/tools/builtin/read_skill_file.ts \
  packages/backend/src/modules/netaclaw/tools/builtin/skill_manage.ts \
  packages/backend/src/modules/netaclaw/tools/builtin/delegate_task.ts \
  packages/backend/src/modules/netaclaw/tools/builtin/delegate_parallel.ts \
  packages/backend/src/modules/netaclaw/tools/builtin/escalate.ts \
  packages/backend/src/modules/netaclaw/tools/todo_tool.ts \
  packages/backend/src/modules/netaclaw/runtime/prompt_builder.ts \
  packages/backend/src/modules/netaclaw/gateway/server.ts \
  packages/backend/src/modules/netaclaw/service/agent_executor.ts
git commit -m "feat(netaclaw): add tool catalog for schema-based tool name resolution"
  • Step 12: 运行 simplify skill 审查本次变更

Task 2: 九级模糊匹配引擎 fuzzy_match.ts

Files:

  • Create: tools/fuzzy_match.ts

  • Step 1: 创建 tools/fuzzy_match.ts — 类型定义和策略框架

// tools/fuzzy_match.ts
/**
 * 九级模糊匹配引擎
 * 按优先级依次尝试 9 种策略,首个成功即返回。
 */

export interface FuzzyMatchResult {
  strategy: string;
  startIndex: number;
  endIndex: number;
  matchedText: string;
}

type Strategy = {
  name: string;
  find: (content: string, search: string) => FuzzyMatchResult[];
};

/** 在 content 中查找所有匹配 search 的位置,按策略优先级 */
export function fuzzyFindAll(content: string, search: string): FuzzyMatchResult[] {
  for (const strategy of strategies) {
    const results = strategy.find(content, search);
    if (results.length > 0) return results;
  }
  return [];
}

// --- 辅助函数 ---

/** Levenshtein 距离 */
function levenshtein(a: string, b: string): number {
  const m = a.length, n = b.length;
  const dp: number[][] = Array.from({ length: m + 1 }, (_, i) =>
    Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
  );
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      dp[i][j] = a[i - 1] === b[j - 1]
        ? dp[i - 1][j - 1]
        : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
    }
  }
  return dp[m][n];
}

/** 两字符串相似度 0-1 */
function similarity(a: string, b: string): number {
  if (a === b) return 1;
  const maxLen = Math.max(a.length, b.length);
  if (maxLen === 0) return 1;
  return 1 - levenshtein(a, b) / maxLen;
}

/** 将 content 按 normalizer 转换后查找 search映射回原始索引 */
function findByNormalization(
  content: string,
  search: string,
  normalizer: (s: string) => string,
  strategyName: string,
): FuzzyMatchResult[] {
  const normContent = normalizer(content);
  const normSearch = normalizer(search);
  if (normSearch.length === 0) return [];

  // 构建字符映射normIndex → origIndex
  // 逐字符对比原始和标准化后的内容,建立位置映射
  const charMap = buildCharMap(content, normContent);

  const results: FuzzyMatchResult[] = [];
  let pos = 0;
  while (true) {
    const idx = normContent.indexOf(normSearch, pos);
    if (idx === -1) break;
    const endIdx = idx + normSearch.length;
    const origStart = charMap[idx] ?? 0;
    const origEnd = (endIdx < charMap.length ? charMap[endIdx] : content.length);
    results.push({
      strategy: strategyName,
      startIndex: origStart,
      endIndex: origEnd,
      matchedText: content.slice(origStart, origEnd),
    });
    pos = idx + 1;
  }
  return results;
}

/**
 * 构建标准化索引 → 原始索引的映射表。
 * 对于行级 normalizer不改变行数按行对齐映射。
 * 对于可能改变行数的 normalizer逐字符扫描。
 */
function buildCharMap(original: string, normalized: string): number[] {
  // 简单实现:如果行数相同,按行内偏移映射
  const origLines = original.split('\n');
  const normLines = normalized.split('\n');

  if (origLines.length === normLines.length) {
    // 行级映射:每行内按比例映射
    const map: number[] = [];
    let origOffset = 0;
    let normOffset = 0;
    for (let i = 0; i < normLines.length; i++) {
      const origLen = origLines[i].length;
      const normLen = normLines[i].length;
      for (let j = 0; j < normLen; j++) {
        map[normOffset + j] = origOffset + Math.round((j / Math.max(normLen, 1)) * origLen);
      }
      // 换行符映射
      if (i < normLines.length - 1) {
        map[normOffset + normLen] = origOffset + origLen;
      }
      origOffset += origLen + 1;
      normOffset += normLen + 1;
    }
    return map;
  }

  // 行数不同(如 whitespace_normalized按字符比例映射
  const map: number[] = [];
  const ratio = original.length / Math.max(normalized.length, 1);
  for (let i = 0; i < normalized.length; i++) {
    map[i] = Math.round(i * ratio);
  }
  return map;
}
  • Step 2: 实现 9 个策略

tools/fuzzy_match.ts 底部追加策略数组:

// --- 9 级策略 ---

const strategies: Strategy[] = [
  // 1. exact
  {
    name: 'exact',
    find(content, search) {
      const results: FuzzyMatchResult[] = [];
      let pos = 0;
      while (true) {
        const idx = content.indexOf(search, pos);
        if (idx === -1) break;
        results.push({ strategy: 'exact', startIndex: idx, endIndex: idx + search.length, matchedText: search });
        pos = idx + 1;
      }
      return results;
    },
  },

  // 2. line_trimmed — 每行 trim 后匹配
  {
    name: 'line_trimmed',
    find(content, search) {
      return findByNormalization(content, search, s => s.split('\n').map(l => l.trim()).join('\n'), 'line_trimmed');
    },
  },

  // 3. whitespace_normalized — 连续空白合并为单空格
  {
    name: 'whitespace_normalized',
    find(content, search) {
      return findByNormalization(content, search, s => s.replace(/\s+/g, ' '), 'whitespace_normalized');
    },
  },

  // 4. indent_flexible — 去除行首所有空白
  {
    name: 'indent_flexible',
    find(content, search) {
      return findByNormalization(content, search, s => s.split('\n').map(l => l.trimStart()).join('\n'), 'indent_flexible');
    },
  },

  // 5. escape_normalized — \\n → \n, \\t → \t
  {
    name: 'escape_normalized',
    find(content, search) {
      const norm = (s: string) => s.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
      return findByNormalization(content, search, norm, 'escape_normalized');
    },
  },

  // 6. trimmed_boundary — 仅首尾行 trim
  {
    name: 'trimmed_boundary',
    find(content, search) {
      const norm = (s: string) => {
        const lines = s.split('\n');
        if (lines.length > 0) lines[0] = lines[0].trim();
        if (lines.length > 1) lines[lines.length - 1] = lines[lines.length - 1].trim();
        return lines.join('\n');
      };
      return findByNormalization(content, search, norm, 'trimmed_boundary');
    },
  },

  // 7. unicode_normalized — 智能引号/em-dash/省略号 → ASCII
  {
    name: 'unicode_normalized',
    find(content, search) {
      const norm = (s: string) => s
        .replace(/[\u2018\u2019\u201A]/g, "'")
        .replace(/[\u201C\u201D\u201E]/g, '"')
        .replace(/[\u2013\u2014]/g, '--')
        .replace(/\u2026/g, '...');
      return findByNormalization(content, search, norm, 'unicode_normalized');
    },
  },

  // 8. block_anchor — 首尾行锚定 + 中间 Levenshtein ≥60%
  {
    name: 'block_anchor',
    find(content, search) {
      const searchLines = search.split('\n');
      if (searchLines.length < 3) return [];
      const firstLine = searchLines[0].trim();
      const lastLine = searchLines[searchLines.length - 1].trim();
      const contentLines = content.split('\n');
      const results: FuzzyMatchResult[] = [];

      for (let i = 0; i < contentLines.length; i++) {
        if (contentLines[i].trim() !== firstLine) continue;
        for (let j = i + searchLines.length - 1; j < contentLines.length && j < i + searchLines.length + 5; j++) {
          if (contentLines[j].trim() !== lastLine) continue;
          const candidateMiddle = contentLines.slice(i + 1, j).join('\n');
          const searchMiddle = searchLines.slice(1, -1).join('\n');
          if (similarity(candidateMiddle, searchMiddle) >= 0.6) {
            const startIndex = content.split('\n').slice(0, i).join('\n').length + (i > 0 ? 1 : 0);
            const matchedLines = contentLines.slice(i, j + 1);
            const matchedText = matchedLines.join('\n');
            results.push({ strategy: 'block_anchor', startIndex, endIndex: startIndex + matchedText.length, matchedText });
          }
        }
      }
      return results;
    },
  },

  // 9. context_aware — 逐行相似度 ≥80%,整体 ≥50%
  {
    name: 'context_aware',
    find(content, search) {
      const searchLines = search.split('\n');
      const contentLines = content.split('\n');
      if (searchLines.length === 0) return [];
      const results: FuzzyMatchResult[] = [];

      for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
        let matchCount = 0;
        for (let j = 0; j < searchLines.length; j++) {
          if (similarity(contentLines[i + j].trim(), searchLines[j].trim()) >= 0.8) matchCount++;
        }
        if (matchCount / searchLines.length >= 0.5) {
          const startIndex = contentLines.slice(0, i).join('\n').length + (i > 0 ? 1 : 0);
          const matchedText = contentLines.slice(i, i + searchLines.length).join('\n');
          results.push({ strategy: 'context_aware', startIndex, endIndex: startIndex + matchedText.length, matchedText });
        }
      }
      return results;
    },
  },
];
  • Step 3: 验证构建

Run: cd packages/backend && npm run build Expected: 编译成功

  • Step 4: Commit
git add packages/backend/src/modules/netaclaw/tools/fuzzy_match.ts
git commit -m "feat(netaclaw): add 9-level fuzzy match engine for patch tool"
  • Step 5: 运行 simplify skill 审查本次变更

Task 3: Patch 工具

Files:

  • Create: tools/builtin/patch.ts

  • Modify: gateway/server.ts

  • Modify: service/agent_executor.ts

  • Step 1: 创建 tools/builtin/patch.ts

// tools/builtin/patch.ts
import { Type } from '@sinclair/typebox';
import * as fs from 'fs/promises';
import { AgentToolWithMeta, textResult } from '../common.js';
import { fuzzyFindAll } from '../fuzzy_match.js';
import { registerSchema } from '../catalog.js';

const PatchParams = Type.Object({
  path: Type.String({ description: '文件绝对路径' }),
  old_string: Type.String({ description: '要查找的文本片段' }),
  new_string: Type.String({ description: '替换为的文本' }),
  replace_all: Type.Optional(Type.Boolean({ description: '替换所有匹配,默认 false' })),
});

export const patchTool: AgentToolWithMeta<typeof PatchParams, unknown> = {
  name: 'patch',
  label: '模糊补丁',
  description: '对文件进行局部查找替换,支持模糊匹配。比 write_file 更安全、更省 token。',
  parameters: PatchParams,
  async execute(_id, params) {
    try {
      const content = await fs.readFile(params.path, 'utf-8');
      const matches = fuzzyFindAll(content, params.old_string);

      if (matches.length === 0) {
        return textResult(`未找到匹配。请检查 old_string 是否正确,或提供更多上下文。`);
      }
      if (matches.length > 1 && !params.replace_all) {
        return textResult(`找到 ${matches.length} 处匹配(策略: ${matches[0].strategy}),请提供更多上下文确保唯一匹配,或设置 replace_all=true。`);
      }

      // 从后往前替换,避免索引偏移
      let result = content;
      const sorted = [...matches].sort((a, b) => b.startIndex - a.startIndex);
      for (const m of sorted) {
        result = result.slice(0, m.startIndex) + params.new_string + result.slice(m.endIndex);
      }

      await fs.writeFile(params.path, result, 'utf-8');

      const count = params.replace_all ? matches.length : 1;
      return textResult(`已替换 ${count} 处(策略: ${matches[0].strategy}: ${params.path}`);
    } catch (err: any) {
      return textResult(`patch 失败: ${err.message}`);
    }
  },
};

registerSchema({ name: 'patch', toolset: 'base', description: patchTool.description });
  • Step 2: gateway/server.ts — defaultTools 加入 patchTool
// gateway/server.ts 顶部 import 追加
import { patchTool } from '../tools/builtin/patch.js';

// 修改 defaultTools第 50 行)
private defaultTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool];
  • Step 3: agent_executor.ts — defaultTools 加入 patchTool
// service/agent_executor.ts 顶部 import 追加
import { patchTool } from '../tools/builtin/patch.js';

// 修改 defaultTools第 40 行)
private readonly defaultTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool];
  • Step 3.5: crew_orchestrator.ts + crew_delegate.ts — builtinTools 加入 patchTool

Crew 子 Agent 也需要 patch 进行文件编辑(但不加 clarifyCrew 无用户交互通道)。

// service/crew_orchestrator.ts 顶部 import 追加
import { patchTool } from '../tools/builtin/patch.js';

// 修改第 24 行 BUILTIN_TOOL_NAMES 追加 'patch'
const BUILTIN_TOOL_NAMES = ['bash', 'read_file', 'write_file', 'list_dir', 'patch', 'delegate_task', 'delegate_parallel', 'escalate'];

// 修改第 175 行 builtinTools 追加 patchTool
const builtinTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool];
// service/crew_delegate.ts 顶部 import 追加
import { patchTool } from '../tools/builtin/patch.js';

// 修改第 17 行 BUILTIN_TOOL_NAMES 追加 'patch'
const BUILTIN_TOOL_NAMES = ['bash', 'read_file', 'write_file', 'list_dir', 'patch'];

// 修改第 40 行 builtinTools 追加 patchTool
const builtinTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool];
  • Step 4: 验证构建

Run: cd packages/backend && npm run build Expected: 编译成功

  • Step 5: Commit
git add packages/backend/src/modules/netaclaw/tools/builtin/patch.ts \
  packages/backend/src/modules/netaclaw/gateway/server.ts \
  packages/backend/src/modules/netaclaw/service/agent_executor.ts \
  packages/backend/src/modules/netaclaw/service/crew_orchestrator.ts \
  packages/backend/src/modules/netaclaw/service/crew_delegate.ts
git commit -m "feat(netaclaw): add patch tool with fuzzy matching"
  • Step 6: 运行 simplify skill 审查本次变更

Task 4: Clarify 工具 + WebSocket 协议 + 阻塞机制

Files:

  • Create: tools/builtin/clarify.ts

  • Modify: gateway/protocol.ts

  • Modify: runtime/attempt.ts

  • Modify: runtime/agent.ts

  • Modify: gateway/server.ts

  • Step 1: 创建 tools/builtin/clarify.ts

// tools/builtin/clarify.ts
import { Type } from '@sinclair/typebox';
import { AgentToolWithMeta, textResult } from '../common.js';
import { registerSchema } from '../catalog.js';

const ClarifyParams = Type.Object({
  question: Type.String({ description: '要问用户的问题' }),
  choices: Type.Optional(Type.Array(Type.String(), { maxItems: 4, description: '预设选项最多4个' })),
});

/** 降级 execute当无 WebSocket 回调时,返回问题文本让 Agent 作为回复输出 */
export const clarifyTool: AgentToolWithMeta<typeof ClarifyParams, unknown> = {
  name: 'clarify',
  label: '向用户提问',
  description: '当任务需求不明确时,向用户提出澄清问题。支持选择题和开放式问题。',
  parameters: ClarifyParams,
  async execute(_id, params) {
    const choicesText = params.choices?.length
      ? `\n选项: ${params.choices.map((c, i) => `${i + 1}. ${c}`).join(', ')}`
      : '';
    return textResult(`[需要用户确认] ${params.question}${choicesText}`);
  },
};

registerSchema({ name: 'clarify', toolset: 'interaction', description: clarifyTool.description });
  • Step 2: 扩展 gateway/protocol.ts

protocol.tsClientMessageServerEvent 联合类型中追加:

// --- 客户端 → 服务端 追加 ---

export interface ClientClarifyResponseMessage {
  type: 'clarify_response';
  sessionId: string;
  requestId: string;
  answer: string;
}

export type ClientMessage = ClientChatMessage | ClientPingMessage | ClientSetThinkingLevelMessage | ClientClarifyResponseMessage;

// --- 服务端 → 客户端 追加 ---

export interface ServerClarifyRequestEvent {
  type: 'clarify_request';
  sessionId: string;
  data: { requestId: string; question: string; choices?: string[] };
}

// ServerEvent 联合类型追加 ServerClarifyRequestEvent
  • Step 3: 修改 runtime/attempt.ts — 新增 onClarifyRequest 回调
// attempt.ts — AttemptParams 接口追加字段
export interface AttemptParams {
  // ... 现有字段不变 ...
  onClarifyRequest?: (question: string, choices?: string[]) => Promise<string>;
}

// attempt.ts — 工具执行循环中(第 69-79 行区域),在 tool.execute 之前插入 clarify 分支
if (tc.name === 'clarify' && params.onClarifyRequest) {
  const args = JSON.parse(tc.arguments);
  try {
    resultText = await params.onClarifyRequest(args.question, args.choices);
  } catch {
    resultText = '用户未回答,请自行判断并继续。';
  }
} else if (!tool) {
  resultText = `错误: 工具 "${tc.name}" 不存在`;
} else {
  try {
    const result = await tool.execute(tc.id, JSON.parse(tc.arguments));
    resultText = typeof result === 'string' ? result : JSON.stringify(result);
  } catch (err: any) {
    resultText = `工具执行错误: ${err.message}`;
  }
}
  • Step 4: 修改 runtime/agent.ts — 透传 onClarifyRequest
// agent.ts — AgentRunParams 接口追加
export interface AgentRunParams {
  // ... 现有字段不变 ...
  onClarifyRequest?: (question: string, choices?: string[]) => Promise<string>;
}

// agent.ts — runAttempt 调用处(第 86-96 行)追加透传
return runAttempt({
  // ... 现有参数不变 ...
  onClarifyRequest: params.onClarifyRequest,
});
  • Step 5: 修改 gateway/server.ts — 注入 clarify 阻塞回调

Critical: Midway.js @WSController 是 request-scope每连接一个实例clarifyResolvers 必须是模块级 Map否则 clarify_response 可能路由到不同实例导致 resolve 永远不被调用。超时设为 20s前端适配前快速降级

// gateway/server.ts — 顶部 import 追加
import { randomUUID } from 'crypto';
import { clarifyTool } from '../tools/builtin/clarify.js';

// 模块级 Map不是实例属性@WSController 每连接一个实例)
const clarifyResolvers = new Map<string, { resolve: (answer: string) => void; timer: NodeJS.Timeout }>();

// defaultTools 追加 clarifyTool
private defaultTools: AnyAgentTool[] = [bashTool, readFileTool, writeFileTool, listDirTool, patchTool, clarifyTool];

// runAgent 调用处(第 219 行区域)追加 onClarifyRequest
onClarifyRequest: async (question, choices) => {
  const requestId = randomUUID();
  this.send({ type: 'clarify_request', sessionId: sid, data: { requestId, question, choices } });
  return new Promise<string>((resolve) => {
    const timer = setTimeout(() => {
      clarifyResolvers.delete(requestId);
      resolve('用户未在规定时间内回答。请根据已有信息自行判断并继续执行。');
    }, 20_000);  // 前端适配前用短超时快速降级
    clarifyResolvers.set(requestId, { resolve, timer });
  });
},

// onMessage 方法中(第 80 行区域)追加 clarify_response 处理
if (msg.type === 'clarify_response') {
  const entry = clarifyResolvers.get(msg.requestId);
  if (entry) {
    clearTimeout(entry.timer);
    clarifyResolvers.delete(msg.requestId);
    entry.resolve(msg.answer);
  }
}
  • Step 6: 验证构建

Run: cd packages/backend && npm run build Expected: 编译成功

  • Step 7: Commit
git add packages/backend/src/modules/netaclaw/tools/builtin/clarify.ts \
  packages/backend/src/modules/netaclaw/gateway/protocol.ts \
  packages/backend/src/modules/netaclaw/runtime/attempt.ts \
  packages/backend/src/modules/netaclaw/runtime/agent.ts \
  packages/backend/src/modules/netaclaw/gateway/server.ts
git commit -m "feat(netaclaw): add clarify tool with blocking WebSocket interaction"
  • Step 8: 运行 simplify skill 审查本次变更

Task 5: 提示词重写 + todo TypeBox 迁移

Files:

  • Modify: runtime/prompt_guidance.ts

  • Modify: tools/todo_tool.ts

  • Step 1: 重写 prompt_guidance.ts — TOOL_USE_ENFORCEMENT 改为动态函数

Critical: 原设计硬编码了 clarify/patch 工具名Crew 子 Agent 没有这些工具会被误导调用不存在的工具。改为根据实际可用工具列表动态生成。

删除 prompt_guidance.ts 第 29-34 行的 export const TOOL_USE_ENFORCEMENT = ...,替换为:

/**
 * 根据实际可用工具列表动态生成工具使用规范。
 * Crew 子 Agent 没有 clarify/patch不应在提示词中出现这些工具。
 */
export function getToolUseEnforcement(toolNames: string[]): string {
  const rules = `# 工具使用规范

你必须通过工具采取行动 - 不要只描述你打算做什么。

## 强制规则
- 当你说"我来检查"、"让我执行"时,必须在同一回复中立即发起工具调用
- 不要以"下一步我会..."结束回复 - 现在就执行
- 持续工作直到任务真正完成,不要停在计划阶段
- 每条回复要么包含推进任务的工具调用,要么向用户交付最终结果`;

  const scenarios: string[] = [];
  if (toolNames.includes('read_file')) scenarios.push('需要读取文件内容时 → read_file不要凭记忆猜测');
  if (toolNames.includes('patch')) scenarios.push('需要修改已有文件时 → patch局部修改或 write_file新建/全量重写)');
  else if (toolNames.includes('write_file')) scenarios.push('需要修改文件时 → write_file');
  if (toolNames.includes('list_dir')) scenarios.push('需要了解目录结构时 → list_dir');
  if (toolNames.includes('bash')) scenarios.push('需要执行命令时 → bash');
  if (toolNames.includes('clarify')) scenarios.push('任务需求不明确时 → clarify主动提问不要猜测');
  if (toolNames.includes('todo')) scenarios.push('复杂任务开始前 → todo创建任务列表');

  return scenarios.length > 0
    ? `${rules}\n\n## 必须使用工具的场景\n${scenarios.map(s => `- ${s}`).join('\n')}\n\n## 操作前先确认\n- 修改文件前先 read_file 确认当前内容\n- 执行命令前确认工作目录`
    : rules;
}

同时修改 runtime/prompt_builder.ts 中 Layer 2 的调用:

// prompt_builder.ts — 将 TOOL_USE_ENFORCEMENT 常量引用改为函数调用
// 旧: import { TOOL_USE_ENFORCEMENT, ... } from './prompt_guidance.js';
// 新: import { getToolUseEnforcement, ... } from './prompt_guidance.js';

// Layer 2 中:
// 旧: parts.push(TOOL_USE_ENFORCEMENT);
// 新: parts.push(getToolUseEnforcement(params.availableToolNames));
  • Step 2: 重写 prompt_guidance.ts — TOOL_BEHAVIOR

替换 prompt_guidance.ts 第 104-117 行的 TOOL_BEHAVIOR 对象:

const TOOL_BEHAVIOR: Record<string, string> = {
  memory_save: `## 记忆使用策略
使用 memory_save 存储对未来对话有价值的持久信息:用户偏好、环境细节、项目约束。
保持记忆紧凑,聚焦于减少用户未来重复纠正的事实。
不要存储任务进度、会话结果、临时 TODO 状态。更新已有记忆而非创建重复条目。`,

  todo: `## 任务规划策略
收到用户请求后,先评估任务复杂度:
**必须使用 todo** 涉及 2 个以上步骤、修改多个文件、需要先调研再实施、用户一次提出多个需求。
**无需使用 todo** 单步操作(查看文件、回答问题、一条命令)、简单单文件小修改。
列表顺序=优先级。同一时间只标记一个 in_progress。完成立即标记 completed。
任务列表是你的工作契约 - 它让用户看到你的计划并跟踪进度。`,

  skill_manage: `## 技能管理策略
完成复杂任务5步以上工具调用考虑用 skill_manage 将方法保存为技能以便复用。
使用技能时如发现过时或错误,立即修补,不要等用户要求。`,

  patch: `## 文件编辑策略
修改已有文件时,优先使用 patch 进行局部替换,而非 write_file 全量重写。
**用 patch** 修改函数、修复 bug、调整配置、添加/删除代码片段。
**用 write_file** 创建新文件、文件需要完全重写。
old_string 提供足够上下文确保唯一匹配。不需要精确匹配缩进和空白。`,

  clarify: `## 主动提问策略
任务需求不明确、存在多种合理解读、或缺少关键信息时,使用 clarify 向用户提问。
**应该提问:** 指令模糊有多种解读、缺少关键参数、操作有风险需确认。
**不应提问:** 任务明确可直接执行、可从上下文推断、琐碎实现细节。
提供 choices 选项让用户快速选择。一次只问一个问题。`,
};
  • Step 3: todo_tool.ts — TypeBox 迁移

替换 tools/todo_tool.ts 的 schema 定义(第 3-41 行):

import { Type, Static } from '@sinclair/typebox';
import { TodoStore } from '../runtime/todo_store.js';
import { registerSchema } from './catalog.js';

const TodoItemSchema = Type.Object({
  id: Type.String({ description: '唯一标识' }),
  content: Type.String({ description: '任务描述' }),
  status: Type.Union([
    Type.Literal('pending'),
    Type.Literal('in_progress'),
    Type.Literal('completed'),
    Type.Literal('cancelled'),
  ], { description: '当前状态' }),
});

const TodoParams = Type.Object({
  todos: Type.Optional(Type.Array(TodoItemSchema, { description: '任务项数组。省略则读取当前列表。' })),
  merge: Type.Optional(Type.Boolean({ description: 'true=按 id 增量更新。false(默认)=全量替换。', default: false })),
});

export const todoToolSchema = {
  name: 'todo' as const,
  description:
    '管理当前会话的任务列表。用于复杂任务或用户提供多个任务时。\n'
    + '不传参数=读取当前列表。传 todos 数组=写入。\n'
    + 'merge=false(默认)全量替换merge=true 按 id 增量更新。\n'
    + '列表顺序=优先级。同一时间只能有一个 in_progress。',
  parameters: TodoParams,
};

type TodoExecArgs = Static<typeof TodoParams>;

/** 执行 todo 工具 */
export function executeTodo(
  store: TodoStore,
  args: TodoExecArgs,
): { todos: any[]; summary: any } {
  if (args.todos && args.todos.length > 0) {
    store.write(args.todos, args.merge ?? false);
  }
  return {
    todos: store.read(),
    summary: store.getSummary(),
  };
}

registerSchema({ name: 'todo', toolset: 'planning', description: todoToolSchema.description });
  • Step 4: 验证构建

Run: cd packages/backend && npm run build Expected: 编译成功

  • Step 5: Commit
git add packages/backend/src/modules/netaclaw/runtime/prompt_guidance.ts \
  packages/backend/src/modules/netaclaw/tools/todo_tool.ts
git commit -m "feat(netaclaw): rewrite tool prompts and migrate todo schema to TypeBox"
  • Step 6: 运行 simplify skill 审查本次变更

Task 6: 集成验证

Files: 无新文件,验证现有改动的集成正确性

  • Step 1: 完整构建验证

Run: cd packages/backend && npm run build Expected: 零错误,零警告

  • Step 2: 验证 catalog 工具名解析

在构建产物中检查 collectAvailableToolNames 是否正确返回所有工具名。手动验证方式:

Run: cd packages/backend && node -e "import('./dist/modules/netaclaw/tools/builtin/bash.js').then(() => import('./dist/modules/netaclaw/tools/builtin/file.js')).then(() => import('./dist/modules/netaclaw/tools/builtin/patch.js')).then(() => import('./dist/modules/netaclaw/tools/builtin/clarify.js')).then(() => import('./dist/modules/netaclaw/tools/builtin/memory.js')).then(() => import('./dist/modules/netaclaw/tools/todo_tool.js')).then(() => import('./dist/modules/netaclaw/runtime/prompt_builder.js')).then(m => console.log(m.collectAvailableToolNames({ memoryEnabled: true, hasSkills: true, crewRole: 'master' })))"

Expected: ['bash', 'read_file', 'write_file', 'list_dir', 'patch', 'todo', 'clarify', 'memory_save', 'memory_recall', 'read_skill', 'read_skill_file', 'skill_manage', 'delegate_task', 'delegate_parallel', 'escalate']

  • Step 3: 验证 prompt_guidance 输出

Run: cd packages/backend && node -e "import('./dist/modules/netaclaw/runtime/prompt_guidance.js').then(m => { console.log('=== ENFORCEMENT ==='); console.log(m.getToolUseEnforcement(['bash','read_file','write_file','list_dir','patch','todo','clarify']).slice(0, 300)); console.log('=== BEHAVIOR ==='); console.log(m.getToolBehaviorGuidance(['todo', 'patch', 'clarify']).slice(0, 300)); })"

Expected: ENFORCEMENT 输出包含所有 7 个工具的场景描述BEHAVIOR 输出包含 todo/patch/clarify 策略

  • Step 4: 最终 Commit如有修复
git add -A
git commit -m "fix(netaclaw): integration fixes for tool catalog and prompts"