arch-tool-registry.md 14.2 KB

Tool Registry · 79 个工具如何变成可调用的 JS

6 个 Agent skill 描述了 79 个工具签名(write_todolist · hash_contract · bug_open ...)但这些不是 Claude CLI 自带。本页讲清楚它们怎么从签名变成实际可调用的 JS,桥接到 LemonBoss 既有的 tool-registry.js,让 Claude CLI 通过 tool_use API 真的能调到。

① 现状 · 三层工具栈

Claude CLI 调工具走三层,越靠下越底层:

关键点:本次不动 L1 / L2。只在 L2 注册层追加 79 个 L3 工具定义。Claude CLI 进程启动时被告知"你可以调这些 + 既有 Read/Write/Bash",调用时 ToolRegistry 路由到对应 execute 函数。

② 79 个工具按目录组织

server/src/tools/
├── tool-registry.js                 # 既有,加载所有工具 (REUSE)
├── read-tools.js                    # 既有 (REUSE)
├── call-agent-tool.js               # 既有 (REUSE)
├── orchestrate-tool.js              # 既有 (REUSE)
├── marketplace-tools.js             # 既有 (REUSE)
├── train-skill-tool.js              # 既有 (REUSE)
└── devloop/                       # NEW 整个目录
    ├── persistence-tools.js         # 持久化 5 件套 (M-10/11/12/13)
    │   ├── write_todolist
    │   ├── update_todo_item
    │   ├── update_runtime_state
    │   ├── timeline_append
    │   └── record_memory
    ├── doc-tools.js                 # PM / Architect 文档工具
    │   ├── bootstrap_prd_site / bootstrap_sdd_site
    │   ├── write_chapter / write_mvcp_page
    │   ├── archive_doc
    │   ├── diff_html / diff_units
    │   ├── echo_diff
    │   ├── update_search_index
    │   ├── validate_prd_site / validate_sdd_consistency
    │   └── hash_contract
    ├── coder-tools.js               # Coder 工具
    │   ├── contract_bridge
    │   ├── sandbox_check
    │   ├── append_coder_log
    │   └── (jsonGuard 是 Bash 中间件,不在工具表)
    ├── tester-tools.js              # Tester 工具
    │   ├── parse_vitest
    │   ├── hint_builder
    │   ├── bug_open
    │   ├── bug_transition
    │   └── bug_list
    ├── integrator-tools.js          # Integrator 工具
    │   ├── port_picker
    │   ├── spawn_units
    │   ├── health_wait
    │   ├── run_case
    │   ├── parse_placeholder
    │   └── collect_stderr
    └── triager-tools.js             # BugTriager 工具
        ├── extract_evidence (→ verify_evidence)
        ├── category_classifier
        ├── diff_contract_vs_impl
        └── find_similar_bug

合计 79 个工具中:持久化 5 + doc 11 + coder 3 + tester 5 + integrator 6 + triager 4 = 34 个独立函数(很多 Agent 复用,所以签名是 79 但唯一实现是 34)。每个 ≈ 30-80 行 JS,总实现量 ≈ 1500-2500 行。

③ 标准 wrapper 模板(直接照抄)

每个工具都按下面这个模式注册到 tool-registry.js:

// server/src/tools/devloop/persistence-tools.js · 节选
const { Type } = require('@sinclair/typebox');
const fs   = require('fs');
const path = require('path');
const { projectsDir } = require('../../services/paths');

/**
 * 每个工具导出一个 ToolDefinition 对象:
 *   { name, description, inputSchema, execute }
 * 由 tool-registry.js 收集后注入 Claude CLI 启动参数。
 */

module.exports.write_todolist = {
  name: 'write_todolist',
  description: '初始化 agent-state/{agent}-todo.json,写入完整 todolist',
  inputSchema: {
    type: 'object',
    required: ['items'],
    properties: {
      items: {
        type: 'array',
        items: {
          type: 'object',
          required: ['id', 'description'],
          properties: {
            id:          { type: 'string', pattern: '^step-\\d{3}$' },
            description: { type: 'string' },
            depends_on:  { type: 'array', items: { type: 'string' } }
          }
        }
      }
    }
  },

  // ctx 由 ToolRegistry 注入,含 pid · agent · call_id · sessionId · loopController · sseBroker
  async execute({ items }, ctx) {
    const { pid, agent, call_id } = ctx;
    const p = path.join(projectsDir(pid), 'agent-state', `${agent}-todo.json`);

    const todolist = {
      agent, call_id, created_at: new Date().toISOString(),
      items: items.map(it => ({ ...it, status: 'todo' })),
      current: items[0]?.id || null,
      progress_pct: 0,
    };

    fs.mkdirSync(path.dirname(p), { recursive: true });
    fs.writeFileSync(p, JSON.stringify(todolist, null, 2));

    ctx.sseBroker?.emit(pid, 'agent.todolist', {
      agent, call_id, items: todolist.items,
    });

    return { ok: true, total: items.length, path: p };
  }
};

三个不变量每个 wrapper 都要遵守:

  1. inputSchema 是JSON Schema(Claude CLI native tool_use 格式),不是 TypeBox(虽然 TypeBox 可以序列化到 JSON Schema,方便统一来源)
  2. execute 接收第二个参数 ctx,含 pid · agent · call_id · sessionId 等运行时上下文(由 LoopController 注入)
  3. 返回值必须是纯 JSON 可序列化对象,给 Claude 看(不能含 Buffer / Stream / Function)

④ tool-registry.js 改造

LemonBoss 既有 server/src/tools/tool-registry.js,只加载新模块即可:

// server/src/tools/tool-registry.js · 仅追加 1 段

const { readDirImports } = require('./_util');

function loadAllTools() {
  const registry = new Map();

  // 既有(REUSE):
  register(registry, require('./read-tools'));
  register(registry, require('./call-agent-tool'));
  register(registry, require('./orchestrate-tool'));
  register(registry, require('./marketplace-tools'));
  register(registry, require('./train-skill-tool'));

  // NEW: devloop 工具批量加载
  for (const mod of readDirImports('./devloop')) {
    register(registry, mod);
  }

  return registry;
}

function register(reg, mod) {
  for (const key of Object.keys(mod)) {
    const tool = mod[key];
    if (tool && tool.name && tool.execute) {
      reg.set(tool.name, tool);
    }
  }
}

module.exports = { loadAllTools };

现有 PluginManager 已经在 server 启动时 require tool-registry,所以本改动不需要改 server/index.js。

⑤ Agent 模板的 toolPolicy

每个 Agent 在 system/templates/_registry.json 注册时声明能调哪些工具。Claude CLI 启动时 --allowedTools 参数据此生成。

// system/templates/_registry.json · 节选(追加 6 条)
{
  "templates": [
    {
      "id": "devloop-pm",
      "name": "PM Agent",
      "type": "executor",
      "devloop": {
        "loop": "L3",
        "upstream": ["human"],
        "downstream": ["architect"]
      },
      "cli": {
        "model": "claude-sonnet-4-6",
        "systemPromptFile": "system/templates/devloop-pm.md",
        "toolPolicy": [
          "Read", "Write",
          "bootstrap_prd_site", "write_chapter", "archive_doc",
          "diff_html", "update_search_index", "validate_prd_site",
          "echo_diff",
          "write_todolist", "update_todo_item", "update_runtime_state",
          "timeline_append", "record_memory"
        ]
      },
      "sandbox": {
        "cwd": "data/companies/{cid}/projects/{pid}/",
        "writeAllowList": ["prd/**", "memory/devloop/pm/**", "agent-state/**", "timeline.ndjson"]
      }
    },
    { "id": "devloop-architect",  ... },
    { "id": "devloop-coder",
      "cli": {
        "model": "claude-sonnet-4-6",
        "toolPolicy": [
          "Read", "Edit", "Write", "Bash",
          "hash_contract", "contract_bridge", "sandbox_check", "append_coder_log",
          "write_todolist", .../* 持久化 5 件套 */
        ]
      }, ...
    },
    { "id": "devloop-tester",     ... },
    { "id": "devloop-integrator", ... },
    { "id": "devloop-bug-triager",... }
  ]
}

关键观察:PM 不允许 Bash(不需要跑命令),Coder/Tester 允许 Bash(要跑 vitest / npm),Integrator 允许 Bash(要 spawn 进程),BugTriager 不允许 Write/Bash(只读 + 写 triage.json)。

⑥ Bash 工具的 jsonGuard 中间件

Coder / Tester / Integrator 都被允许 Bash。但禁止 inline JSON(CLAUDE.md L91-99 强约束)。这通过 Bash 工具的命令预检中间件实现,不依赖 Agent 自觉。

// server/src/tools/devloop/bash-json-guard.js · ~50 行
const INLINE_JSON_PATTERN = /-d\s+['"]\s*\{[\s\S]*?\}\s*['"]/;

function jsonGuard(cmd) {
  if (INLINE_JSON_PATTERN.test(cmd)) {
    return {
      ok: false,
      reason: 'inline JSON in Bash command is forbidden (see CLAUDE.md L91-99). ' +
              'Use: echo \'{"k":"v"}\' > /tmp/x.json && curl -d @/tmp/x.json',
    };
  }
  return { ok: true };
}

// 注册到 Bash 工具的 preExecuteHooks
module.exports.bashHooks = {
  preExecute: [jsonGuard],
};

挂载点:LemonBoss 既有的 Bash 工具实现里加一行 const checks = require('./devloop/bash-json-guard').bashHooks.preExecute;,在 execute 前跑一遍,任一返回 ok=false 直接拒绝。Agent 收到 error,自然走 retry 改正。

⑦ ctx 的内容(ToolRegistry 注入)

每次工具被调用,execute(input, ctx) 的 ctx 由 LoopController 在 callAgent 时设置:

const ctx = {
  // 标识
  pid:        "p-test001",
  agent:      "pm" | "architect" | ...,
  unit_id:    "todo-be",       // 只有 Coder/Tester 有
  call_id:    "call-...",
  session_id: "sess-...",      // ACP session

  // 注入的服务
  loopController,                 // 用于 update_runtime_state 等
  docStore,
  unitRegistry,
  bugTracker,
  timelineLogger,
  sseBroker,
  agentManager,                   // 工具内部一般不调,但有就备着

  // 配置
  companyDir,                     // data/companies/{cid}/
  projectDir,                     // data/companies/{cid}/projects/{pid}/
  unitDir,                        // (Coder/Tester) projects/{pid}/units/{unit_id}/

  // hooks
  log:        (level, msg) => ...,       // 写 timeline 简化版
};

所有 wrapper 都通过 ctx 获取依赖,不要直接 require server/services/xxx。这样工具可以脱离运行时单测(mock ctx 即可)。

⑧ 错误传播

工具 execute 的失败有三种,Agent 看到的形态不同:

失败类型 execute 返回 Agent 看到 Agent 该怎么办
预期错误(合法但失败) { ok: false, reason: "..." } JSON 含 ok=false 读 reason 调整后重试
沙箱越界 / Schema 错 抛 ValidationError tool_use error + 详细说明 必须改路径 / payload
系统崩(fs 失败、磁盘满) 抛 Error tool_use error + stack 报 LoopController 升 L3
// 标准化错误(所有 wrapper 复用)
class ToolValidationError extends Error {
  constructor(field, reason) {
    super(`${field}: ${reason}`);
    this.field = field; this.reason = reason;
  }
}

class ToolSandboxError extends Error {
  constructor(path, agent) {
    super(`Sandbox violation:${agent} tried to write ${path}`);
  }
}

⑨ ToolRegistry 给 Claude CLI 的注入

ACP 启动 Claude CLI 时,把 toolPolicy 翻译为 CLI 参数:

// packages/acp-core/protocol/ClaudeCliProtocol.js · 增量改动

function buildCliArgs({ agent, toolPolicy, toolRegistry }) {
  const tools = toolPolicy.map(name => {
    const def = toolRegistry.get(name);
    if (!def) throw new Error(`unknown tool: ${name}`);
    return {
      name:        def.name,
      description: def.description,
      input_schema: def.inputSchema,
    };
  });

  // 写入临时 tools.json,作为 --tools 参数传给 claude CLI
  const toolsFile = `/tmp/devloop-tools-${agent}-${Date.now()}.json`;
  fs.writeFileSync(toolsFile, JSON.stringify({ tools }));

  return [
    '--dangerously-skip-permissions',    // 既有约定
    '--tools', toolsFile,                // Claude CLI 加载工具定义
    '--tool-handler', 'jsonrpc://stdio', // 工具调用通过 stdio JSON-RPC 回到 server
  ];
}

Claude CLI 调任一工具时,通过 stdio 发 JSON-RPC 请求给 ACP,ACP 路由到 ToolRegistry.execute(name, input, ctx),返回值再 JSON-RPC 回 CLI。整个过程对 Agent 透明,它看到的就是标准 tool_use。

⑩ Phase 0 验收(接 plan-migration)

Phase 0 完成的判据是一个最小调用回环跑通:

# Phase 0 验证脚本: scripts/phase0-verify.js
node -e "
const { loadAllTools } = require('./server/src/tools/tool-registry');
const reg = loadAllTools();
const want = ['write_todolist','update_todo_item','hash_contract','bug_open',
              'archive_doc','update_search_index','validate_prd_site'];
for (const w of want) {
  const t = reg.get(w);
  if (!t) { console.error('MISSING:', w); process.exit(1); }
  console.log('✓', w, '·', t.description.slice(0,40));
}
console.log('All', want.length, 'critical tools registered');
"

# 期望: ✓ 所有 7 个 critical 工具就位

# 再跑一个最小 Agent 调用 (用 stub Claude CLI 模式)
node scripts/phase0-test-call.js
# 内部:用 mock ACP session 调 write_todolist,验证文件落盘 + SSE 事件发出

⑪ 工时估算(Phase 0)

任务 工时 难度
持久化 5 件套(persistence-tools.js) 1.5 天 低
doc-tools(11 个工具,PM/Architect 用) 2.5 天 中(cheerio 操作多)
coder/tester/integrator/triager-tools(18 个) 2 天 中
extract-agent-prompts.js(HTML → md) 0.5 天 低(正则)
_registry.json 加 6 条 + toolPolicy 0.5 天 低
ClaudeCliProtocol --tools 注入 1 天 中(要看 Claude CLI 的实际 tool 接口)
jsonGuard 中间件挂 Bash 工具 0.5 天 低
phase0-verify.js + phase0-test-call.js 1 天 低
调试 + 联调 2 天 —
合计 ≈ 11.5 天 (2.5 周) —

P0 完成后,PM Agent 才能真的跑出 prd/ 站点 — 这是 P1 的前置条件。

⑫ 关联页面