工具契约、注册表与可见性

一个 ZCode 工具要声明哪些字段、其中哪些真正被读取;内置工具与 MCP 工具怎样进同一张注册表;每回合发给模型的工具清单怎样按端口、开关与模型能力层层筛选、排序并投影成 JSON Schema;参数不合格时模型收到什么。附内置工具全表。

作者 David更新于 14 篇(共 47 篇)

模型每一步能调用哪些工具、每个工具有什么副作用、出了错怎样告诉模型,都写在一份声明式的“工具契约”里。契约的类型和各工具的 zod schema 放在 @zcode/contracts,具体取值(权限、结果预算、超时、取消)大多写在 core 各 handler 的 ToolEntry 里,ToolEntry 同时挂上 handler 和一组可选钩子。40 个内置条目过了注册门之后,和运行时接入的 MCP 工具一起进同一张 ToolRegistry,再经过几道筛选变成发给模型的 ModelToolContract[]

本篇讲“声明、注册、可见”:契约有哪些字段、哪些真的有人读;注册表怎样组织;模型看到的清单怎样定下来;参数不合格时模型收到什么。执行器怎样调度、审批和收尾,留给下一篇执行器:调度、审批、超时与结果

位置内容
apps/zcode-cli/packages/contracts/src/tools契约类型 contract.ts、zod 转 JSON Schema 的 json-schema.ts、各工具的输入输出 schema 与名字常量
apps/zcode-cli/packages/core/src/tool/*.tsToolEntry 与执行上下文、注册表、可见性与排序、JSON Schema 校验器、参数错误投影、路径策略
apps/zcode-cli/packages/core/src/tool/handlers内置工具实现与注册总表 index.tsgenerated/ 下只有 Bash 只读判定用的命令注册表
apps/zcode-cli/packages/core/src/mcp把 MCP 工具描述投影成 ToolEntry
apps/zcode-cli/packages/core/src/runtime/helpers/runtime-tools.tstool-allowlist.ts运行时装配时的注册门

怎么用

注册表不对用户开放,能改变工具清单的入口是这些:

入口效果出处
--disallowedTools--disallowed-tools(TUI 与 -p 都认)按工具名整项剔除,注册时就不装apps/zcode-cli/packages/cli/src/arguments.ts:125
配置 features.skillfeatures.subagentfeatures.mcp 设为 false分别去掉 Skill、Agent 一族(Agent、Task、SendMessage)、全部 MCP 工具apps/zcode-cli/packages/bootstrap/src/app/create-app.ts:746apps/zcode-cli/packages/bootstrap/src/app/runtime-config.ts:156runtime-config.ts:168
启用官方 browser-use 插件打开 runtimeFeatures.nodeRepl,注册 js 工具apps/zcode-cli/packages/core/src/runtime/helpers/runtime-tools.ts:47
桌面、Web 等宿主创建的协议会话总是注入定时任务端口(Cron 工具);闲时工具与动态工作流工具要宿主显式开放,默认关闭apps/zcode-cli/packages/bootstrap/src/zcode-protocol/server-operations.ts:3337server-operations.ts:3370server-operations.ts:3373
协议会话参数 toolAllowlisttoolDenylist会话级白名单与黑名单,内置工具和 MCP 工具都受约束server-operations.ts:3342

--disallowedTools 接受 Bash(rm:*) 这类带括号的规则,但注册层只取括号前的名字(apps/zcode-cli/packages/core/src/tool/tool-visibility.ts:5)。从代码看,这样一条规则会让整个 Bash 都不注册,而不是只禁掉 rm。权限层怎样解释这些规则,见权限模式与规则

一个工具声明什么

契约的公共部分在 apps/zcode-cli/packages/contracts/src/tools/contract.ts:168

export interface ToolContractDeclaration {
  capability: string;
  executionMode?: ToolExecutionMode;
  providerNative?: ProviderNativeToolSpec;
  inputSchema: Record<string, unknown>;
  outputSchema: Record<string, unknown>;
  /**
   * 声明 `inputSchema` **有资格**走 provider 的严格模式(Anthropic `strict: true`:constrained
   * decoding 保证 tool_use.input 恰好满足 schema)。只是资格,不是命令:adapter 按 provider /
   * model 决定是否真的下发,并负责把 strict 子集表达不了的关键字折进 description。缺席即不严格。
   * 首个使用者是 dwf mono 子代理的 typed `submit_result`。
   */
  strict?: boolean;
  requiresUserInteraction?: boolean;
  permission: ToolPermissionSpec;
  resultBudget: ToolResultBudget;
  timeout: ToolTimeoutPolicy;
  cancellation: ToolCancellationPolicy;
  trace: ToolTracePolicy;
}

core 的 ToolEntry 继承它(apps/zcode-cli/packages/core/src/tool/types.ts:269),再加上必填的 metadatatypes.ts:65)、handler 和十来个可选钩子。按用途归一下类:

字段声明什么谁在读
metadata.namedescriptionmodelInstructions模型可见名与描述;modelInstructions 拼成 Usage: 列表接在描述后注册表投影(apps/zcode-cli/packages/core/src/tool/registry.ts:132
inputSchemaruntimeInputSchema发给模型的 JSON Schema;运行时用的 zod schema适配层;执行器的归一化与校验
outputSchemaruntimeOutputSchema输出形状执行器在 handler 返回后校验
metadata.readOnlydestructiveconcurrentSafesideEffectScoperiskLevelneedsApprovalrequiresUserInteractionallowedInPlanMode能力旗标调度器、权限服务、流式执行判定
permission权限名、理由、风险、副作用范围,以及 alwaysAsk(任何模式都要问,contract.ts:105)、askOptions(能否“始终允许”,contract.ts:114权限服务与审批闸
resultBudget给模型的字节阈值、截断方向、超限是否落盘(contract.ts:117执行器的结果序列化
timeoutmetadata.timeoutMskind: "none" 不计时,或默认值、上限、能否按调用覆盖、清理宽限(contract.ts:137执行器超时
cancellation是否支持取消、清理级别、取消时给用户的话只用到提示语和清理级别,supported 没有读取方
trace是否要求 trace、输入输出记录粒度全仓库没有读取方
metadata.maxOutputBytes最大输出只被投影进 ModelToolContract,不参与截断
metadata.providerVisiblealiases是否发给模型、兼容别名注册表
metadata.stopTurnOnSuccess成功即结束回合的终态工具(types.ts:85执行器的回合控制
strictexecutionModeproviderNative严格模式资格;由客户端执行还是由 provider 原生执行模型适配层
permissionCapabilityGroupmodelContentProtection只能由宿主验证过的来源写入的信任标记(官方 Computer Use)权限与结果投影

可选钩子让契约能“看入参说话”。全部 40 个内置条目里用到的情况如下:

钩子作用用到它的工具
resolveModelContract按当前模型能力改写描述与 schemaRead:模型支持 PDF 时才出现 pages 参数(apps/zcode-cli/packages/core/src/tool/handlers/read-pdf.ts:65
validateInput工具专属的语义校验,在钩子之前收口Read、TaskOutput,以及下面四个工作流创作工具
resolveInput把入参归一化成“将要发生的执行事实”(types.ts:318CreateWorkflow、AmendWorkflow、SaveWorkflow、EvalWorkflowSnippet
prepareApproval权限已判定 ask 之后,只能放行或补一张预览,不能把 allow 变成 ask(types.ts:339同上四个
resolveTimeoutBudgetMs按入参算超时Bash、Read
resolvePermissionCapabilityresolvePermissionRulePolicy按入参重算只读等能力、匹配规则Bash
formatModelContentformatPersistedModelContent输出怎样变成模型内容;落盘后怎样写摘要前者 30 个条目用到,后者只有 Bash 和 TaskOutput

看一个真实声明。Bash 的权限、结果预算和超时(apps/zcode-cli/packages/core/src/tool/handlers/bash.ts:470):

  permission: {
    permission: "bash",
    reason: "Bash can run subprocesses and may affect workspace, git, network, or system state",
    riskLevel: "high",
    sideEffectScope: "system",
    needsApproval: true,
    patternSources: ["command"],
    alwaysAllowPatternSources: ["command"],
    denyPriority: "beforeAsk",
  },
  resultBudget: {
    maxInlineBytes: MAX_INLINE_OUTPUT_BYTES,
    maxModelBytes: 30_000,
    strategy: "artifact",
    preview: {
      maxBytes: 30_000,
      direction: "tail",
    },
    artifact: {
      enabled: true,
      retention: "session",
    },
  },
  timeout: {
    defaultMs: DEFAULT_BASH_TIMEOUT_POLICY.defaultTimeoutMs,
    maxMs: DEFAULT_BASH_TIMEOUT_POLICY.maxTimeoutMs,
    allowCallOverride: true,
    cleanupGraceMs: 6_000,
  },

同一个条目的 metadata 里写着 maxOutputBytes: 10_000_000bash.ts:455),真正卡住模型输入的却是上面的 30000 字节。另外,patternSourcesalwaysAllowPatternSourcesdenyPriority 三个字段在全仓库都没有读取方,只在几处注释里被提到。

AGENTS.md 的要求与实际字段

apps/zcode-cli/AGENTS.md 专门有一节“工具与副作用契约”,第一条是(apps/zcode-cli/AGENTS.md:62):

每个 tool 都应声明明确的 inputSchemaoutputSchema、是否只读、是否破坏性、是否并发安全、最大输出大小、超时、取消语义和权限需求。

逐条对照代码:

AGENTS.md 的要求对应字段落实情况
inputSchemaoutputSchema同名字段,另有 zod 版 runtimeInputSchemaruntimeOutputSchema类型上必填;执行器校验输入和输出
只读、破坏性、并发安全metadata.readOnlydestructiveconcurrentSafe必填;调度与权限都读
最大输出大小metadata.maxOutputBytes(可选)与 resultBudget(必填)真正截断的是后者
超时、取消语义timeoutcancellation必填;cancellation.supported 无人读取
权限需求permissionmetadata.needsApproval权限服务读取
副作用范围(apps/zcode-cli/AGENTS.md:63sideEffectScope,七种取值(contract.ts:7调度、权限、钩子输入、动态工作流都读
幂等性与可恢复策略(apps/zcode-cli/AGENTS.md:64没有对应字段只有 MCP 的 idempotentHint 被折进 concurrentSafeapps/zcode-cli/packages/core/src/mcp/index.ts:170
大结果落盘(apps/zcode-cli/AGENTS.md:65resultBudget.strategy: "artifact"执行器落盘,见下一篇

七种副作用范围是 noneworkspacegitnetworksystemsessionuserInteraction。contracts 在这之上给了两条判定:isWorkspaceMutatingToolCall 判“写”,只认 workspacegitsystem,范围缺席按会写处理(contract.ts:30);isWorldTouchingToolCall 判“碰”,排除 sessionuserInteraction 两档协议范围,其余都算(contract.ts:57)。注释特意说明 Read 的范围是 none,但它的答案取决于工作区,所以读也算“碰”(contract.ts:54)。两条规则都是给动态工作流的导入缓存用的,见动态工作流(三):从工具调用到落库

注册表:一张表,两类来源

ToolRegistryImpl 只有两张 Map:规范名到条目、别名到规范名(registry.ts:30)。注册时规范名永远压过别名,同名重复注册会覆盖并告警;别名撞上已有工具或别的别名时直接跳过,以免一次调用被路由到错误的权限和 handler(registry.ts:34)。get 先查别名再查规范名(registry.ts:88),list 按插入顺序返回。toContracts 把条目投影成 ModelToolContract:去掉 providerVisible: false 的条目,拼好描述,带上能力旗标、权限和结果预算,execute 一律置空(registry.ts:104registry.ts:127),所以模型 SDK 永远不会自己执行工具。

图表加载中…

内置工具。注册总表是 builtInToolsapps/zcode-cli/packages/core/src/tool/handlers/index.ts:76),40 个条目;registerBuiltInTools 逐个过门(tool/handlers/index.ts:195),过了门的再交给 resolveBuiltInToolEntryForBranch 按配置造变体(tool/handlers/index.ts:271):Bash 的描述和 timeout 参数说明随超时策略变化,Agent、Task 的描述带上子 Agent 档案列表,动态工作流关闭时还会去掉“工作流请求必须改用 CreateWorkflow”一句(tool/handlers/index.ts:284)。ApplyPatch 的契约还在 contracts 里,旧的 Workflow 工具也留着注册门,但两者在总表里都被注释掉了(tool/handlers/index.ts:70tool/handlers/index.ts:80),这一版不会出现在模型面前。

注册入口有两个:运行时构造时的 registerRuntimeBuiltInToolsruntime-tools.ts:46),以及初始化 Shell 环境快照后再跑一次的 refreshBranchAwareBuiltInToolsapps/zcode-cli/packages/core/src/runtime/methods/embedded-search-branch.ts:21)。两处各写一份门的推导曾经漂过:刷新那份漏了动态工作流开关,而这个开关“缺席即开启”,结果灰度关闭的会话里模型仍然调到了 ListSavedWorkflows,于是推导被收进 tool-allowlist.ts 共用(apps/zcode-cli/packages/core/src/runtime/helpers/tool-allowlist.ts:70)。

别名TaskAgent 的完整拷贝,标了 providerVisible: false,描述写明是 “Claude Code-compatible alias”(apps/zcode-cli/packages/core/src/tool/handlers/agent.ts:287);TaskStopKillShellKillBashapps/zcode-cli/packages/core/src/tool/handlers/task-stop.ts:97),TaskOutputBashOutputAgentOutputTool 等四个名字(apps/zcode-cli/packages/contracts/src/tools/task-output.ts:5)。这些名字不发给模型,但模型或插件说明按旧名调用时照样能找到工具。钩子匹配也有一张别名表:匹配 Task 的钩子对 Agent 生效,反之亦然(apps/zcode-cli/packages/core/src/tool/compat.ts:6),钩子本身见生命周期 Hooks 与工作区信任

MCP 工具。回合循环每一轮都会调用 initializeMcpapps/zcode-cli/packages/core/src/runtime/methods/turn-loop.ts:106),但它每个运行时只注册一次:等待连接快照,调用 registerMcpTools 写进同一张注册表,有新工具就清掉工具缓存(apps/zcode-cli/packages/core/src/runtime/methods/mcp.ts:122)。README 说 MCP 工具“registered before the first model request”(apps/zcode-cli/README.md:180),与代码一致。名字是 mcp__<server>__<tool>,两段都把字母、数字、下划线、连字符以外的字符换成下划线(apps/zcode-cli/packages/core/src/mcp/name.ts:3)。能力旗标从 MCP 的 annotations 推出来(core/src/mcp/index.ts:108):

  • readOnlyHintdestructiveHint 直接映射;concurrentSafe 取“只读或幂等”;
  • needsApproval 一律为真,副作用范围一律记作 network,只有宿主 node_repljs 记作 system、风险为高;
  • 超时取描述里的 timeoutMs,缺省 30000 毫秒(core/src/mcp/index.ts:37);
  • 结果预算:普通 MCP 给模型 50000 字节、超出截断;宿主 node_repl 64 KiB、超出落盘并保留尾部;验明身份的官方 Computer Use 256 KiB(core/src/mcp/index.ts:125)。

官方 Computer Use 的服务在插件命名空间下,工具名本来是 mcp__plugin_zcode-cua_computer-use__*,只有通过不可伪造的宿主凭据校验后才改投成 mcp__computer-use__*,另挂一个 provider 拼写别名(core/src/mcp/index.ts:86)。MCP 的连接、传输与 OAuth 见 MCP

可见性:从注册表到请求

模型看到的清单要过五层:

位置做什么
注册门tool/handlers/index.ts:195按端口是否注入、功能开关、白名单与黑名单、会话类型决定装不装
契约投影registry.ts:106去掉 providerVisible: false 的条目(目前只有 Task)
getToolsapps/zcode-cli/packages/core/src/runtime/methods/config.ts:136搜索分支再滤一次;统一排序;WebSearch 只给支持原生搜索的模型;按模型能力投影契约;结果缓存
回合过滤turn-loop.ts:110本回合的黑名单;定时任务执行轮去掉 Cron 写工具,闲时执行轮去掉 OffPeakCreate、SendMessage 与 Workflow(apps/zcode-cli/packages/core/src/runtime/methods/turn-loop-state.ts:34);Cron 创建上限命中后清单置空
适配层apps/zcode-cli/packages/adapters/src/model/tool-transform.ts:20provider 原生工具、严格模式 schema、引用改写

回合过滤的细节属于回合循环与 TurnMachine,这里只看前三层里容易误会的几点。

默认没有 Glob 和 Grep。嵌入式搜索分支的总开关写死为 trueapps/zcode-cli/packages/core/src/embedded-search/capability.ts:4),全仓库没有调用方覆盖它;只要 Bash 没被白名单或黑名单拿掉,registerBuiltInTools 就跳过 Glob 和 Grep(tool/handlers/index.ts:206),搜索改由 Bash 里的 findgrep 接管。实现见读、写、改、搜

会话类型会收窄工具面toolsetexplore 时只注册探索工具集 Bash、Glob、Grep、Read、WebFetch、WebSearch、TodoWrite(apps/zcode-cli/packages/core/src/subagent/explore-tools.ts:4),另给了白名单就取交集,子 Agent 会话还会补回 RespondToCoordinatortool-allowlist.ts:79);工作流子会话固定禁用 CreateWorkflow、AmendWorkflow、SaveWorkflow、ResumeWorkflowRun、ResolveWorkflowQuestion(tool-allowlist.ts:26)。前三个声明了 alwaysAsk,而工作流子会话被强制成 yolo、交互事件不回传父界面,确认请求会隐形挂起到超时;后两个是“子会话内不得再编排、不得替主代理作答”的结构性禁令(tool-allowlist.ts:18tool-allowlist.ts:38)。

宿主形态靠端口表达。core 里没有“是不是桌面端”的判断,宿主注入了哪些端口,就有哪些工具。bootstrap 只是转发调用方给的定时任务端口和闲时端口(create-app.ts:775),TUI 与 -p 都不传,所以终端里没有 Cron 与 OffPeak 工具;协议会话总是注入定时任务端口,闲时端口看宿主开关(server-operations.ts:3370server-operations.ts:3373)。动态工作流的十个工具极性相反:终端不写开关,按“缺席即开启”保留全部工具(tool/handlers/index.ts:178);协议会话必须写出显式布尔,宿主不开就是 falseserver-operations.ts:3337,服务端默认值见 apps/zcode-cli/packages/bootstrap/src/zcode-protocol/server.ts:255)。

权限模式不参与筛选。Plan 模式下模型照样能看到 Write 和 Bash,模式只影响回合里插入的提醒(turn-loop.ts:126)和执行时的权限判定。拦截发生在执行器里,见权限模式与规则

模型能力。WebSearch 只在当前模型的 supportsNativeWebSearch 为真时出现(config.ts:268),它本身是借模型的原生搜索实现的,见 WebFetch 与 WebSearch;Read 的 pages 参数只对支持 PDF 的模型出现。执行器调用前也用同一个 resolveModelContract 投影一次(apps/zcode-cli/packages/core/src/tool/model-contract.ts:4),保证校验用的 schema 与模型看到的是同一份。

发给模型的契约:schema、描述与顺序

Schema。所有内置工具的 schema 都走 toToolJsonSchema,注释说这是为了让运行时校验与 provider 参数不漂移(apps/zcode-cli/packages/contracts/src/tools/json-schema.ts:20)。它用 zod-to-json-schema 生成 JSON Schema 7,不用 $ref、按输入侧展开 effect,再做一轮归一化:删掉 $schema$id$ref$defsdefinitionstools/json-schema.ts:9),没有 oneOf 时把 anyOf 改名为 oneOftools/json-schema.ts:66),缺 type 的节点按 propertiesitemsenum 等推断出类型,最后标上 draft 2020-12 的 $schema。改名有个副作用:core 自己的校验器对 oneOf 要求恰好命中一个分支(apps/zcode-cli/packages/core/src/tool/json-schema.ts:71),比 anyOf 严。

描述metadata.description 之外,声明了 modelInstructions 的工具(CronCreate、CronUpdate、OffPeakCreate、ReadSessionContext、RespondToCoordinator)会在描述后追加一段 Usage: 列表(registry.ts:132)。WebSearch 的描述是个 getter,每次读取重新生成,因为描述里带着当前月份,写死会过期(apps/zcode-cli/packages/core/src/tool/handlers/websearch.ts:134)。

顺序getTools 在生成缓存清单时统一排序(config.ts:264),规则在 apps/zcode-cli/packages/core/src/tool/provider-visible-order.ts:35

export function orderProviderVisibleToolContracts<T extends { name: string }>(
  tools: readonly T[],
): T[] {
  const referenceTools: T[] = [];
  const localTools: T[] = [];
  for (const tool of tools) {
    if (SORTED_PROVIDER_TOOL_NAMES.has(tool.name)) {
      referenceTools.push(tool);
    } else {
      localTools.push(tool);
    }
  }

  return [
    ...referenceTools.sort((left, right) => left.name.localeCompare(right.name)),
    ...localTools,
  ];
}

名单 SORTED_PROVIDER_TOOL_NAMES 有 31 个名字(provider-visible-order.ts:1),其中 EnterWorktreeExitWorktreeLSPNotebookEditScheduleWakeupTaskCreateTaskGetTaskListTaskUpdateWorkflow 这 10 个 ZCode 这一版并不注册。TaskCreateTaskGetTaskListTaskUpdate 这一组是 Claude Code 的任务工具(见 Claude Code 中文手册),从这些名字看,名单像是参照 Claude Code 的工具集列出的。效果是:名单内的工具按字母序排在最前,其余内置工具按注册顺序跟在后面,MCP 工具最后注册,自然落在末尾。

为什么要稳定?工具清单是请求前缀的一部分,任何增删、改名、换序都会让 provider 的提示词缓存从头失效。代码里有几处印证:getTools 的结果缓存在运行时上(config.ts:137),只在 MCP 注册或搜索分支刷新时清空;submit_result 的注释把“per-ask schema 不进工具声明”归因于 “frozen-tool 缓存不变式”(apps/zcode-cli/packages/core/src/tool/handlers/submit-result.ts:9);工作流 driver 也提到 “prompt-cache 的 frozen-tools 不变式”(apps/zcode-cli/packages/bootstrap/src/app/workflow-driver.ts:547)。

到 provider 为止toAiSdkTools 只把描述、inputSchema、严格模式标记和 needsApproval 交给 Vercel AI SDK(tool-transform.ts:35),只读、并发安全、副作用范围这些旗标都留在 ZCode 内部。严格模式要同时满足:契约声明 strict、provider 是 anthropic、模型是 Anthropic 首方模型(tool-transform.ts:85);声明了 requiresMfjsToolSchema 的模型只接受 #/$defs/ 形式的引用,适配层会把 MCP schema 里其他本地引用提升过去(tool-transform.ts:95)。适配层的其余部分见模型适配层

参数不合格时,模型收到什么

执行器拿到模型的入参后,先归一化:顶层是 JSON 字符串就解析(有的适配器和钩子路径会把入参交成字符串),再用 zod 的 runtimeInputSchema 跑一遍 safeParse,成功就用补过默认值的结果,失败就保留原始入参和 zod 的 issue(apps/zcode-cli/packages/core/src/tool/input-normalization.ts:40)。随后用 JSON Schema 做准入校验(apps/zcode-cli/packages/core/src/tool/executor/validation.ts:71)。这个校验器是个最小子集:支持 oneOfconstenumtype、字符串长度、数值范围、数组长度与 itemsrequiredadditionalProperties: false,不认 patternformatapps/zcode-cli/packages/core/src/tool/json-schema.ts:53)。正则、URL 这类约束靠 zod 的 issue 补进报错。注意准入的硬门是 JSON Schema:它通过而 zod 失败时,调用会继续往下走,交给工具的 validateInput 或 handler 兜底(validation.ts:77)。

校验失败时,给模型的内容由 apps/zcode-cli/packages/core/src/tool/input-validation-model-content.ts:12 组装:

export function createInitialInputValidationModelContent(
  entry: ToolEntry,
  jsonIssues: readonly ToolInputValidationIssue[],
  runtimeIssues: readonly RuntimeInputValidationIssue[] | undefined,
): string {
  const issues = projectInitialModelValidationIssues(entry.inputSchema, jsonIssues, runtimeIssues);
  return `<tool_use_error>InputValidationError: ${formatToolInputValidationError(
    entry.metadata.name,
    issues,
  )}</tool_use_error>`;
}
  // ...
  const lines: string[] = [
    ...missingParameters.map((parameter) => `The required parameter \`${parameter}\` is missing`),
    ...unexpectedParameters.map(
      (parameter) => `An unexpected parameter \`${parameter}\` was provided`,
    ),
    ...wrongTypes.map(
      ({ expected, param, received }) =>
        `The parameter \`${param}\` type is expected as \`${expected}\` but provided as \`${received}\``,
    ),
  ];

缺参数、多参数、类型不对三类问题各给一句人话,有这三类问题时其他问题一概省略(input-validation-model-content.ts:107);只剩取值不在枚举里、长度超限、正则不匹配这类问题时,才把 issue 列表整个序列化成 JSON 交给模型(input-validation-model-content.ts:65)。比如调用 Read 时漏了 file_path,模型收到的是:

<tool_use_error>InputValidationError: Read failed due to the following issue:
The required parameter `file_path` is missing</tool_use_error>

这些 issue 的形状值得一提:invalid_valueinvalid_format、带 origintoo_big,以及 Invalid input: expected string, received undefined 这样的措辞(apps/zcode-cli/packages/core/src/tool/tool-input-validation-issues.ts:5tool-input-validation-issues.ts:87),都是 Zod 4 的格式,而 contracts 依赖的是 Zod 3(apps/zcode-cli/packages/contracts/package.json:99)。投影层专门把 Zod 3 的码翻译成目标格式(input-validation-model-content.ts:331),并逐字段对齐顺序,注释反复强调这些字段的插入顺序“会直接进入 provider-visible JSON fallback,必须保持稳定”(tool-input-validation-issues.ts:1)。

另外几种失败的写法:

  • 工具专属校验(validateInputresolveInput)或 handler 以返回值表达业务失败时,模型看到 <tool_use_error> 包着的那句 message,错误码另记在结果里(apps/zcode-cli/packages/core/src/tool/executor/errors.ts:20)。
  • 工具名不存在时模型看到 Tool not found: <name>apps/zcode-cli/packages/core/src/tool/executor/call-runner.ts:128);名字是空白时则是 <tool_use_error>Error: No such tool available: </tool_use_error>,末尾原样保留模型返回的空白名(call-runner.ts:138)。
  • PreToolUse 钩子改写过的入参会再校验一次,但失败时不再生成上面这段面向模型的说明(validation.ts:79),因为那已经不是模型自己写出的参数。

路径策略

文件类工具都用 resolveWorkspacePath 解析路径(apps/zcode-cli/packages/core/src/tool/path-policy.ts:15):工作目录和工作区根必须是绝对路径,否则算配置错误;空路径直接报错;绝对路径只做 normalize,相对路径相对于当前工作目录解析。关键在它不做的事(path-policy.ts:32):

  const resolvedPath = isAbsolute(requestedPath)
    ? normalize(requestedPath)
    : resolve(workingDirectory, requestedPath);

  // Current release intentionally does not hard-block paths outside workspaceRoot.
  // Cause: subagents may need to inspect user-requested sibling repos or external files
  // before the filesystem permission adapter grows explicit ask/deny rules for them.
  return resolvedPath;

也就是说,工具层不以工作区为边界,工作区外的读写只受权限模式约束(Bash 的工作目录另有重置规则,见 Bash:解析、只读判定与后台任务)。比较路径时用的是 normalizeToolPathForComparisonapps/zcode-cli/packages/core/src/tool/path-normalization.ts:15):Windows 上把 Git Bash 风格的 /c/... 转成 C:\...path-normalization.ts:30),剥掉 \\?\ 长路径前缀但保留设备命名空间(path-normalization.ts:45),盘符统一大写;所有平台最后都做一次 Unicode NFC 归一。读文件状态表就用它生成键(apps/zcode-cli/packages/core/src/tool/read-file-state.ts:15),避免同一文件因写法不同被当成两个文件。

内置工具全表

下表覆盖 builtInTools 的全部 40 个条目。“只读”是 metadata.readOnly 的声明值;handler 列是 apps/zcode-cli/packages/core/src/tool/handlers/ 下的文件。

工具用途只读出现条件handler详见
Read读文本、图片、PDF、视频默认read.tsread-*.ts读、写、改、搜
Write新建或覆盖文件默认write.ts同上
Edit精确替换文件中的文本默认edit.ts同上
Glob、Grep按模式找文件、按正则搜内容默认不出现,见上文glob.tsgrep.ts同上
Bash执行 Shell 命令否,按命令重算默认bash.tsBash
WebFetch抓取公开 URL,转成 Markdown 后回答问题默认webfetch.tsWebFetch 与 WebSearch
WebSearch借模型的原生搜索查网页模型支持原生搜索websearch.ts同上
TodoRead、TodoWrite读取、整表替换会话 Todo是、是默认todo.tsTodo、提问与 Plan 模式
AskUserQuestion向用户发选择题并等答案默认ask-user-question.ts同上
EnterPlanMode、ExitPlanMode进入 Plan 模式;提交计划请用户批准默认plan-mode.ts同上
ReadSessionContext按会话 ID 读另一个会话的有界上下文默认read-session-context.ts同上
ListModels列出宿主配置的模型,给工作流挑子 Agent 模型动态工作流未关闭list-models.ts同上
CronCreate、CronUpdate、CronDelete增、改、删定时任务宿主注入定时任务端口,且非子 Agentcron.ts定时任务与闲时任务
CronList列出定时任务同上cron.ts同上
OffPeakCreate创建闲时任务宿主开放闲时工具,且非子 Agentoff-peak.ts同上
OffPeakList列出闲时任务同上off-peak.ts同上
Agent启动子 Agent有子 Agent 端口agent.ts子 Agent
TaskAgent 的 Claude Code 兼容别名,不发给模型同上agent.ts同上
SendMessage给本地 Agent 发短消息子 Agent 端口支持发消息send-message.ts同上
RespondToCoordinator子 Agent 回复它的协调者子 Agent 会话respond-to-coordinator.ts同上
submit_result、escalate工作流 actor 提交终态结果;把阻塞问题升级给主代理工作流 actor 会话submit-result.tsescalate.ts同上,及动态工作流(三)
TaskOutput读后台任务输出,描述已标为 DEPRECATED默认task-output.tsBash后台任务与通知
TaskStop按 ID 停止后台任务默认task-stop.ts同上
Skill把技能说明载入当前上下文有技能端口skill.ts技能与自定义命令
js在常驻 Node REPL 里执行 JavaScriptbrowser-use 插件打开 nodeReplnode-repl.tsnode_repl、Browser Use 与 Computer Use
CreateWorkflow、AmendWorkflow类型检查工作流脚本,确认后在后台启动或修订 run动态工作流未关闭create-workflow.tsamend-workflow.ts动态工作流(三)
SaveWorkflow把脚本存成项目内可复用的定义同上save-workflow.ts同上
EvalWorkflowSnippet同步试跑一小段工作流,不留状态同上eval-workflow-snippet.ts同上
ListWorkflowRuns、GetWorkflowRun列出本项目的 run;读一个 run 的进度与结果同上list-workflow-runs.tsget-workflow-run.ts同上
ResumeWorkflowRun以原 run ID 恢复已停止的 run同上resume-workflow-run.ts同上
ResolveWorkflowQuestion回答 actor 升级上来的阻塞问题同上resolve-workflow-question.ts同上
ListSavedWorkflows列出已保存的工作流定义同上list-saved-workflows.ts同上

几处“只读”要结合副作用范围看:TodoWrite、Agent、Task 都声明了 readOnly: true,范围却是 sessionapps/zcode-cli/packages/core/src/tool/handlers/todo.ts:140agent.ts:229),调度器只把“只读且范围为 none”的调用当作真正的只读(下一篇细说)。Bash 声明为非只读,但会按命令内容重算:判定为只读的命令(如 lsgit status)改记为只读、低风险、免审批、范围 nonebash.ts:77)。

下一篇:执行器:调度、审批、超时与结果——一批工具调用怎样分组并发,单个调用怎样走完校验、钩子、审批、执行与结果投影。

本页目录