Skip to content

fix: toWireToolName 取错了别名表来源,且作用方向与 CLI 相反(messages 该改没改、tools 声明不该改却改了) #37

Description

@xelr233

结论

d063b47 引入的 TOOL_NAME_ALIASES 重命名表取错了来源,而且作用方向也反了。对照 command-code@1.54.0 的 dist/cli.mjs(未混淆,可直读):

  1. 线上重命名只有一个,不是四个;
  2. 另外三个来自 resolveToolNameAlias,那是客户端修复模型调用旧工具名的逻辑,还带 defaults 语义 —— 与 wire 无关;
  3. CLI 在 messages 里重命名,不在 tools 声明里;当前的实现正好反过来。

(#36 报的下游症状是同一个 bug 的另一面,这里补上根因。)


证据:CLI 1.54.0 的四处源码

① 线上重命名只有一项

nw="search_tools", rw="tool_search",   // 两个常量
function toWireToolName(e){return e===rw?nw:e}

② 那张四项表是入站别名,不是出站重命名

ow={bash_output:{to:"shell_output"},
    task_output:{to:"shell_output",defaults:{wait:"exit"}},
    [rw]:{to:nw},
    read_multiple_files:{to:"read_file"}}

它的消费者是 resolveToolNameAlias:

function resolveToolNameAlias(e){
  const t=ow[e.toolName]; if(void 0===t) return e;
  const n={...e.input};
  for(const[e,r] of Object.entries(t.defaults??{}))
    void 0!==n[e]&&null!==n[e]||"wait"===e&&blockCancelsWait(n)||(n[e]=r);
  return {toolName:t.to, input:n,
    note:\`Repair note: the tool \`\${e.toolName}\` is now \`\${t.to}\`; this call ran as \${t.to} with your arguments carried over. Call \`\${t.to}\` directly next time.\`};
}

调用点在工具执行器里(参数是 {toolName,input,signal,onUpdate,toolCallId,permissionMode}),用途是「模型调了退役名 → 本地按新名字跑,并回一句 Repair note 让模型下次改口」。两条与 wire 无关的铁证:

  • 它产出 note —— 一段给模型看的自然语言;
  • 它带 defaults(task_output 会补 wait:"exit")—— 补默认参数是执行语义,不是重命名。

③ toWireTools 完全不做重命名

function toWireTools(e){return e.map(e=>({name:e.name,description:e.description,input_schema:e.input_schema}))}

④ toWireMessages 里 tool-call 与 tool-result 都用别名,且缓存的是别名后的名字

if("tool_use"===t.type){
  if(t.providerExecuted) continue;
  const r=toWireToolName(t.name);
  n.set(t.id,r);                                   // ← 存别名后的名字
  e.push({type:"tool-call",toolCallId:t.id,toolName:r,input:t.input});
  continue}
...
"tool_result"===t.type && e.push({type:"tool-result",toolCallId:t.tool_use_id,
  toolName:n.get(t.tool_use_id)??"unknown",        // ← 查出来的也是别名
  output:toWireToolOutput(t.content)})

当前实现的三处偏差

proxy.mjs:

位置 现状 CLI
681 行 TOOL_NAME_ALIASES 4 项 线上只有 1 项(tool_search→search_tools)
657 行 params.tools[].name 应用别名 不应用(toWireTools 原样)
525 / 580 行 toolNameMap[tc.id] 存原名 存别名后的名字
633 行 assistant tool-call.toolName 原名 别名
646 行 tool-result.toolName 原名(查 map) 别名

也就是说:该改名的地方(messages)没改,不该改的地方(tools 声明)改了。

影响

下游 OpenAI 客户端声明的是 bash_output,wire 上 params.tools 变成 shell_output,而模型如果调用了它,回流的 tool-call.toolName 又是 shell_output —— 客户端按自己声明的 bash_output 找不到对应工具。这正是 #36 描述的现象。

read_multiple_files → read_file、task_output → shell_output 同理。


建议修法

// 线上唯一的重命名(CLI 的 toWireToolName,rw→nw)
const WIRE_TOOL_ALIASES = { tool_search: 'search_tools' };
function toWireToolName(name) { return WIRE_TOOL_ALIASES[name] || name; }

// ① tools 声明原样下发,不套别名
body.params.tools = (tools || []).map(t => ({
  name: t.function?.name || t.name || '',
  description: ..., input_schema: ...,
}));

// ② messages 里 tool-call / tool-result 都套别名,
//    并且 map 里存**别名后**的名字(CLI 的 n.set(t.id, r))
toolNameMap[tc.id] = toWireToolName(tc.function?.name || '');
parts.push({ type: 'tool-call', toolCallId: tc.id,
             toolName: toWireToolName(tc.function?.name || ''), input: ... });

至于 bash_output / task_output / read_multiple_files:那是模型侧的退役名,要不要修复是产品决策(CLI 会连 defaults 一起补),但不该出现在 wire 上。


复验方式

curl -sSL "$(curl -sS https://registry.npmjs.org/command-code/latest \
  | grep -o 'https://registry.npmjs.org/command-code/-/command-code-[^"]*\.tgz' | head -1)" \
  | tar xzO package/dist/cli.mjs > cli.mjs
grep -o 'function toWireToolName([^}]*}'  cli.mjs   # → e===rw?nw:e
grep -o 'ow={[^}]*}[^}]*}'                 cli.mjs   # → 四项表
grep -o 'function toWireTools([^}]*}'      cli.mjs   # → 无别名

附:顺带提一句,README 里引用的 PROTOCOL-FACTS-1.53.1.md 在仓库里不存在(git ls-tree -r upstream/master 只有 12 个文件),是悬空引用。

Activity

  1. xelr233 commented on Sep 14, 2026

    @xelr233
    ContributorAuthor

    结论:这张别名表应该整体删掉,toWireToolName 改回恒等

    补充 #37 里没拿到的那块拼图 —— tool_search 是 CLI 自己退役的名字,而且不可见:

    function createSearchToolsTool(e){
      return { ...,
        schema: { name: nw,                    // nw = "search_tools",当前名
                  label: "SEARCH TOOLS", ... } }
    }
    
    function createRetiredToolSearchTool(e){
      const t = createSearchToolsTool(e);
      return { ...t,
        visible: () => false,                  // ← 不进面向模型的 catalog
        schema: { ...t.schema,
          name: rw,                            // rw = "tool_search",退役名
          description: \`Retired: use \${nw} instead. Calls to the \${rw} NAME are
                        routed to \${nw} by the runner automatically. ...\` } }
    }

    它确实从不上网:

    function createToolRunner(e){
      const t = e.tools ?? gg;
      const n = createSearchToolsTool(...);        // search_tools
      const r = createRetiredToolSearchTool(...);  // tool_search(visible:false)
      const o = () => [...t, n, r];                // allTools:只为 runner 按名查找
      const s = createToolCatalog({ tools: o, ... });
    }
    
    const o = t => { const n = dedupeTools(e.tools());
                     const r = t.includeHidden ? n : n.filter(isVisible); ... };
    function isVisible(e){ return e.visible?.() !== false }

    调用方那边,发请求用的那次 getSchemas 不带 includeHidden,只有本地查 schema 才开:

    const T = r.toolRunner.getSchemas({ mode: e });                               // → 声明给上游
    const M = t => (P ??= r.toolRunner.getSchemas({ mode: e, includeHidden: true }))
                     .find(e => e.name === t);                                    // → 纯本地查找

    所以 params.tools 里永远没有 tool_search。四个函数排在一起就完全自洽了:

    函数 对名字做什么 为什么
    toWireTools 原样 {name,description,input_schema} 声明列表里只有当前名,无需归一化
    toWireMessages tool-call 与 tool-result 都用 toWireToolName,map 存改后的名字 重放旧会话时历史里可能有退役名
    resolveToolNameAlias(ow 表) 改的是执行目标,回一句给模型看的 Repair note,还补 defaults 模型喊了退役名 → 本地按新名字跑

    toWireToolName 不是「工具重命名设施」,而是「把自家 catalog 里那一个退役名字的历史归一化」。 它成立的前提是:CLI 自己退役过工具名,而且可能在重放用旧名字录下来的会话。


    proxy 没有这个前提

    • params.tools 来自下游客户端,proxy 没有 catalog、没有退役名;
    • 请求里出现的每个名字,对 proxy 来说都是「当前名」——没有东西需要归一化;
    • 如果客户端真声明了一个叫 tool_search 的工具,把它改掉是纯粹的错。

    而且现在的改法(只改声明、不改消息)连内部自洽都做不到:CLI 是「声明用当前名 + 消息也用当前名」,现在是「声明改名 + 消息不改名」,两头都不对。


    建议改法

    ① 默认(推荐):删掉整张表。

    // 删除 TOOL_NAME_ALIASES / WIRE_TOOL_ALIASES 与 toWireToolName,三处调用点退回原名
    body.params.tools = (tools || []).map(t => ({
      name: t.function?.name || t.name || '',        // 原样
      description: ..., input_schema: ...,
    }));
    toolNameMap[tc.id] = tc.function?.name || '';    // 原样
    parts.push({ type:'tool-call', toolCallId: tc.id,
                 toolName: tc.function?.name || '', input: ... });
    toolName: toolNameMap[msg.tool_call_id] || msg.name || ''   // 原样

    声明与消息天然一致,#36 的症状直接消失,也不需要任何「改回去」的对称逻辑。

    ⚠️ 只保留一项别名(tool_search→search_tools)并不能解决问题:一旦下游客户端恰好声明了一个名为 tool_search 的工具,就会复现同一个 bug —— 声明是 tool_search、消息是 search_tools。要么两端都不改,要么两端一起改;而「两端一起改」会把客户端自己的 API 面改写掉,所以只剩「都不改」。

    ② 若确实要支持「重放真实 CLI 旧会话」:那也不该是出站改写,而应是入站归一化 + 显式开关(只有下游本身就是 CC CLI 形态的 harness 时才开),并且连 defaults 一起补,与 resolveToolNameAlias 同语义。这样声明与消息仍然一致。唯独不要做成「上行改、下行不改」。


    关于 #36

    症状描述是对的(声明与消息不一致 → 下游派发不了),但有两处需要更正:

    1. 「cli 也不会强制改名」—— 对 bash_output 成立,推广不成立。CLI 确实会改 tool_search,只是只改这一项、只在 messages 里改;
    2. 「本质是兼容旧工具名」—— 比这更彻底:那个旧名字 CLI 自己都不声明(visible:()=>false),纯粹是重放历史用的。

    至于「在原工具名上加几个随机字符(bash_output-cmdc-m)」这个提议,方向是反的:

    • 下游必须能认出回传的工具名才能派发,加后缀直接破坏契约;
    • 工具名是客户端自己的 API 面,不是指纹维度。真实 CLI 每个用户发出的 params.tools 名字完全一样(shell_output/read_file/edit_file…),「多账号工具名相同」本来就是常态,加随机后缀反而制造了一个所有真实 CLI 都没有的偏差;
    • CLI 的 toWireTools 是原样下发。

    复验

    curl -sSL "$(curl -sS https://registry.npmjs.org/command-code/latest \
      | grep -o 'https://registry.npmjs.org/command-code/-/command-code-[^"]*\.tgz' | head -1)" \
      | tar xzO package/dist/cli.mjs > cli.mjs
    grep -o 'function toWireToolName([^}]*}'          cli.mjs   # e===rw?nw:e
    grep -o 'function createRetiredToolSearchTool([^}]*}}' cli.mjs  # visible:()=>false
    grep -o 'function toWireTools([^}]*}'             cli.mjs   # 无别名

    (我这边已按 ① 改完并在真机 mock 上验证:声明与消息现在都原样透传;新增 4 条 wire 契约断言覆盖「声明不改名 / tool_search 在消息里也不改名 / tool-result 与 tool-call 名字一致」。)

  2. jinyu2022 commented on Sep 15, 2026

    @jinyu2022
    Contributor

    我提议的增加随机字符,主要是为了方便回传。最初我只是简单地在回传逻辑中增加了一张反向映射表,但很快发现,如果直接进行新旧值映射,下游传入新值时,也可能被错误地映射回旧值。
    因此,还需要额外增加一个字段,用于标识下游传入的是否为新的工具名;如果是新工具名,则不再进行映射。这样一来,整体实现的代码量就会增加不少,不如直接自定义一个随机的下游不可能传的后缀。

    当然我也同意直接删去映射,直接请求。

    特征方面近乎无解,最好是让下游别用这几个工具名:

    1. 上游可以检测到调用参数不一致,而且旧工具还会有提示词注入,这都和cli表现不一致
    2. 加后缀或者修改成随机字符也是明显特征
  3. MAXeaglet commented on Oct 9, 2026

    @MAXeaglet
    Owner

    已随 PR #58 合入修复:\ oWireToolName\ 单向别名表已被彻底废除,所有工具名在 wire 层面全程原样透传,声明与消息保持同名一致。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions