跳到主要内容

工具宇宙:FS 形状接口与 :// 内部 URL

30 秒导读: oh-my-pi 的 coding-agent 挂着三十余个工具,但读者不必逐个记。抓住一句话就够:它们共用一个「文件系统形状」的接口——绝大多数工具都只吃一个 path 字段,后面能接一套统一的 :N-M / :raw 选择器;而 path 不止是磁盘文件,还能是 pr://123agent://reviewer_0skill://humanizer 这样的内部 URL。于是「读一个 PR」「读子 agent 的输出」「读一份 skill」和「读 src/foo.ts」是同一个动作、同一个工具、同一个入口。本章讲清这套命名空间怎么装配、怎么分派、怎么落盘。

本章是 oh-my-pi 系列的第 3 章。它只讲工具面的形状与命名空间;edit 的编辑语言(hashline)留给 第 4 章,主循环怎么调度工具见 第 1 章,模型侧的 in-band 工具调用见 第 2 章


1. 这是什么(零基础也能懂)

一句话定义: 工具面(tool surface)是 agent 能对世界做的所有动作的集合;oh-my-pi 把这个集合设计成一个共享的文件系统命名空间,而不是一堆各说各话的 API。

它解决什么问题。 一个 coding-agent 想干的事很杂:读文件、跑命令、搜代码、看 PR、看 issue、翻子 agent 的报告、查一份 skill 文档、读项目记忆……最偷懒的做法是给每件事配一个专用工具(read_fileget_pull_requestget_subagent_outputread_skill……),模型要学几十套参数。oh-my-pi 反着来:能表达成「读一个地址」的,就都归到 read;地址长什么样由 URL scheme 决定。

用起来什么样。 下面几行都是同一个 read 工具,只是 path 的形状不同:

read src/foo.ts:50-100 # 磁盘文件的 50–100 行
read pr://123 # 123 号 PR(实时走 gh,带缓存)
read agent://reviewer_0 # 名为 reviewer_0 的子 agent 的输出
read skill://humanizer # humanizer 这份 skill 的 SKILL.md
read issue://can1357/oh-my-pi/1608 # 指定仓库的 1608 号 issue
read memory://root # 本项目的记忆摘要

一句话直觉。 把它想成 Unix 的哲学:「一切皆文件」。Unix 让设备、进程、网络都以文件路径示人,你用同一个 cat 就能读;oh-my-pi 让 PR、子 agent、skill、记忆都以 scheme://… 路径示人,你用同一个 read 就能读。scheme 是挂载点,handler 是那个挂载点的驱动。

本节不碰底层。记住三件事:① 工具吃 path;② path 可以是内部 URL;③ 于是异构资源被统一成「读/写一个地址」。


2. 顶层全景(它大概怎么转)

这套工具面有三个关切,分别由三组文件负责:

关切干什么核心文件
注册与装配决定「这个 session 挂哪些工具、哪些先亮哪些藏起来」tools/index.tstools/builtin-names.ts
FS 形状接口让工具吃统一的 path+选择器,并按形状分派到不同后端tools/read.tswrite.tsgrep.tsglob.ts
:// 内部 URL把 PR / 子 agent / skill / 记忆等异构资源统一成可 read/write 的地址internal-urls/router.tsparse.ts + 各 scheme handler

主线走一遍(高层,不进代码): 一次 read pr://123 的旅程——

模型发出 read{path:"pr://123"}


ReadTool.execute ── 按 path 的「形状」逐级判断该交给谁 ──┐
│ │
│ 是 http(s):// ? ─ 否 │ 分派链(命中即停):
│ 是 conflict:// ? ─ 否 │ file:// → conflict:// → http URL
│ 能被内部路由器认领? ── 是(pr://) │ → 内部 URL 路由器 → 归档 → sqlite
▼ │ → PDF → 普通磁盘文件
InternalUrlRouter.instance().resolve("pr://123") │
│ 查 scheme="pr" 的 handler ┘

PrProtocolHandler.resolve ── 走 gh 缓存,拉 123 号 PR,渲成 markdown


返回 InternalResource{ content, contentType, immutable:true }


ReadTool 把它当「一份只读文本」格式化 → 交回模型

怎么读这张图: 关键在 ReadTool.execute 那一段「逐级判断形状」。它不是先解析出「这是 PR」再调 PR 专用逻辑;它是把所有输入都当路径,顺着一条分派链往下问「你是不是这种形状」,内部 URL 只是其中一环。真源码见 tools/read.ts:2056-2143(execute 的分派头),分派链的顺序在下文 §5 展开。


3. 工具注册与装配(哪些工具、怎么亮出来)

这节讲什么: session 启动时,工具不是「全挂上」这么简单——有别名、有开关、有「先藏起来按需激活」。装配逻辑集中在 createTools

3.1 一张注册表,两类工具

所有内建工具是一张名字 → 工厂函数的表。可见的一类在 BUILTIN_TOOLS,藏起来的一类在 HIDDEN_TOOLS:

// 示意,非源码:注册表就是「名字 → 造一个工具实例」
export const BUILTIN_TOOLS = {
read: s => new ReadTool(s),
bash: s => new BashTool(s),
edit: s => new EditTool(s),
grep: s => new GrepTool(s),
glob: s => new GlobTool(s),
// …共 30 个
};
export const HIDDEN_TOOLS = {
yield: s => new YieldTool(s),
resolve: s => new ResolveTool(s), // 两段式落盘用,见 §8
// …
};

真源码:BUILTIN_TOOLStools/index.ts:441-472,HIDDEN_TOOLStools/index.ts:474-480。可见工具的规范名单(30 个)在 tools/builtin-names.ts:1-32BUILTIN_TOOL_NAMES;隐藏的 5 个是 yieldreport_findingreport_tool_issueresolvegoal「32 工具」是个约数——精确说是 30 个规范内建 + 若干隐藏工具,且实际挂载数还受下面的开关与 MCP/扩展工具影响。

3.2 老名字还能用:别名归一

历史上 grep 叫过 searchglob 叫过 find。为了不砸掉旧调用,注册前先过一遍归一化:

模型说实际路由到
searchgrep
findglob

映射表 LEGACY_BUILTIN_TOOL_NAME_ALIASESnormalizeToolNametools/builtin-names.ts:36-45。所以本章标题里说的 find,在当前代码里是 glob 的别名。

3.3 谁亮、谁不亮:开关 + 递归深度

createTools(tools/index.ts:487-680)不是无脑挂满 30 个。它用一个 isToolAllowed 谓词逐个筛(tools/index.ts:597-631),依据包括:

  • 设置开关:bash.enabled / glob.enabled / browser.enabled 等,任一关掉对应工具就不挂。
  • 递归深度:task(spawn 子 agent)受 task.maxRecursionDepth 限制,子 agent 到底就不再给 task;ircmanage_skilllearn 只在顶层(taskDepth === 0)开。
  • 后端可达性:eval 只要 JS/Python/Ruby/Julia 任一后端可用就挂,并在首次调用时才细分派到具体后端(tools/index.ts:544-549)。

两个「总会补上」的特例值得记:

  1. resolve 无条件补挂——不在请求列表里也会加(tools/index.ts:655-660),因为两段式落盘随时可能需要它。
  2. report_tool_issue 在 autoQA 开启时无条件注入,且它的枚举参数只收刚构造出的内建工具名,MCP/扩展工具永不进这个枚举(tools/index.ts:664-677)。

3.4 essential 与 discoverable:先亮一小撮

工具太多会撑爆上下文。oh-my-pi 给每个工具打一个 loadMode 标签:

  • essential(必备):默认永远亮着。默认必备集是 read / bash / edit / write / glob / eval(DEFAULT_ESSENTIAL_TOOL_NAMES,tools/index.ts:382-389)。
  • discoverable(可发现):平时藏起来,模型通过搜索按需激活(见 §7)。

当发现模式为 all 时,filterInitialToolsForDiscoveryAll(tools/index.ts:415-435)会把非必备的可发现工具从初始工具集里剔掉——除非它被显式请求、被上次会话恢复、或被某个强制 tool_choice 特性点名(forceActive,少了它 provider 会 400)。


4. FS 形状接口:一个 path,一套选择器

这节讲什么: 为什么说这些工具「共用一个文件系统形状」。核心是两点:参数形状统一 + 访问权限按 path 分级

4.1 参数就是一个 path(+ 内嵌选择器)

read 的入参 schema 简单到只有一个字段:

// tools/read.ts:712-716
const readSchema = type({
path: type("string").describe(
'Local path, internal URI (e.g. "omp://", "issue://123", "pr://123"), or URL; ' +
'append :<sel> for line ranges or raw mode (e.g. "src/foo.ts:50-100")',
),
});

writegrepglob 同样以 path 为主轴。行范围、原始模式等修饰不另开参数,而是内嵌进 path 的 :<sel> 后缀,由 parseSel 统一解析(tools/read.ts:779-815):

选择器含义
:50-100第 50–100 行
:50+20从第 50 行起 20 行
:50-从第 50 行到末尾
:raw原始输出,不加行号/hashline
:conflicts列出该文件的 git 冲突块
:raw:50-100组合:原始模式 + 行范围

一套选择器语法对所有 path 形状生效——磁盘文件能用,内部 URL 也能用(read pr://123:1-40 取 PR 渲染文本的前 40 行)。read 会先把选择器从 URL 上「剥」下来再交给 handler(splitInternalUrlSel,见 §5)。

4.2 权限按 path 的形状分级(read / write / exec)

同一个工具,对不同 path 可能是不同危险级别。oh-my-pi 用三档 tier 表达:

tier含义谁默认吃这档
read只读,最安全read(本地/内部 URL)、glob
write落盘,改本地或用户数据write(本地文件)、edit
exec执行/外联,最危险bashssh、任何 SSH 目标

妙处在于 tier 是按参数动态算的,不是钉死在工具上:

  • read 平时是 read tier,但 path 指向 ssh:// 远程主机时升到 exec(tools/read.ts:855-856)——因为那要开外联连接跑远程 shell。
  • write 平时是 write tier,但如果目标 URL 的 handler 没有 write 钩子(纯只读 scheme),它其实落不了盘,tier 降到 read;若 handler 有 write 钩子(如 vault:// 笔记,会改用户数据),才保持 write(tools/write.ts:272-290)。
  • bash 命中危险命令模式时,除了 exec 还带上 override:true,强制弹审批(tools/bash.ts:359-367)。

tier 怎么对上「审批模式」由 resolveApproval 裁决(tools/approval.ts:93-132),这是 §8 的两道门之一。


5. :// 内部 URL:把异构资源统一成地址

这节讲什么: 内部 URL 是这套设计的心脏。它让「一个 PR」「一份 skill」「一个子 agent 的输出」都变成 read/write 能吃的 path。机制是一个进程级路由器 + 每个 scheme 一个 handler

5.1 路由器:一个 scheme 一个驱动

InternalUrlRouter进程级单例,构造时把每个 scheme 的 handler 注册进一张 Map(internal-urls/router.ts:23-48)。共 13 个 scheme,各认领一个前缀:

InternalUrlRouter (进程唯一,13 个 scheme)
├─ "omp" → OmpProtocolHandler (内嵌文档)
├─ "agent" → AgentProtocolHandler (子 agent 输出)
├─ "memory" → MemoryProtocolHandler (项目记忆)
├─ "local" → LocalProtocolHandler (本会话产物,可写)
├─ "skill" → SkillProtocolHandler (skill 文档)
├─ "rule" → RuleProtocolHandler (激活的规则)
├─ "issue" → IssueProtocolHandler ┐ (GitHub,走 gh 缓存)
├─ "pr" → PrProtocolHandler ┘
├─ "history" → HistoryProtocolHandler (agent 转录)
└─ "ssh" · "mcp" · "vault" · "artifact" (余下 4 个)

判定与解析各一个入口:

  • canHandle(input)——正则抓 ^scheme://,查 Map 里有没有这个 scheme(internal-urls/router.ts:67-71)。工具就靠它判断「这个 path 是不是我该交给路由器的」。
  • resolve(input, ctx)——解析 URL、找 handler、调 handler.resolve,并由路由器统一盖上 immutable 标记(internal-urls/router.ts:92-106),handler 自己不用操心这个字段。

5.2 为什么要自己写 URL 解析器

标准 new URL() 会把冒号当端口分隔符,于是 skill://plugin:name 这种主机段里带冒号的命名空间 URL 会被解坏。parseInternalUrl(internal-urls/parse.ts:23-72)先用正则抽出 scheme/host/path,new URL() 失败时再退回一个手搓的 URL-like 对象,并保留原始大小写的 rawHost所有解析内部 URL 的地方都必须走这个函数,不许直接 new URL()(文件头注释明说)。

5.3 handler 的合同:resolve / write? / complete?

每个 scheme handler 实现同一个接口 ProtocolHandler(internal-urls/types.ts:129-172):

成员必选干什么
scheme认领哪个前缀(不带 ://)
immutable产出的资源能不能被 agent 编辑
resolve()把 URL 解析成 InternalResource(content + contentType + …)
write?()有则 write 工具可写回;无则该 scheme 只读
complete?()输入时的自动补全候选(必须快且本地)

resolve 的产物 InternalResource(internal-urls/types.ts:16-43)带三个关键旗标:contentType(markdown/json/plain)、immutable(只读资源会抑制 hashline 编辑锚点)、isDirectory(目录列表,grep 会拒绝对它做搜索)。

5.4 各 scheme 一句话

scheme读什么可写?关键实现
agent://<id>子 agent 的输出;支持 agent://id/path?q= 抽 JSON只读agent-protocol.ts:31-128,跨会话在注册表的 artifacts 目录里找 <id>.md
pr:// issue://GitHub PR/issue:单条、列表、PR diff只读issue-pr-protocol.ts,单条走 SQLite github-cache,列表实时 gh … list
skill://<name>某 skill 的 SKILL.md 或其目录内相对文件只读skill-protocol.ts:41-104,带路径穿越防护
rule://<name>某条激活规则的正文只读rule-protocol.ts:10-45
memory://root项目记忆摘要(命名空间只有 root)只读memory-protocol.ts:134-167,遍历各会话 memory root
omp://<file>构建期内嵌的文档;裸 omp:// 列目录只读omp-protocol.ts:19-93
history://<id>某 agent 的转录 markdown;裸 history:// 出索引表见注history-protocol.ts:36-107,live 从内存、parked 从会话文件只读加载
local://<name>本会话产物(如 PLAN.md)可写local-protocol.ts,会解析成真实磁盘文件

注:history://immutable=false(history-protocol.ts:38),但转录本身没有 write 钩子,所以实际不可写回;local:// 才是真正可写的那个——它 immutable=false 且能落到真实文件。

5.5 分派链:一切按形状排队

回到 §2 的核心。ReadTool.execute(tools/read.ts:2056-2143)不是 if-else 认类型,而是一条命中即停的分派链,内部 URL 只是其中一环:

① file:// → 展开成本地路径,继续往下
② conflict://<N> → 读某个 git 冲突块(#readConflictRegion)
③ http(s):// 等 → parseReadUrlTarget 命中 → 走网络抓取 + 缓存
④ 内部 URL → router.canHandle 命中 →
· local:// 特判:先解析成真实磁盘文件,回落到普通文件路径
· 其它 scheme:#handleInternalUrl → router.resolve
⑤ 归档(.zip 等) → #resolveArchiveReadPath
⑥ sqlite → #resolveSqliteReadPath
⑦ PDF 内嵌图 → splitPdfImageMemberReadPath
⑧ 普通磁盘文件 → 兜底

local:// 的特判(tools/read.ts:2127-2139)很能说明设计意图:它不把 local://PLAN.md 当成「虚拟资源」硬读,而是先还原成真实路径,让它享受和普通文件一模一样的行范围、hashline、编辑能力。内部 URL 不是平行世界,是同一个文件系统的别名。


6. 核心论点:PR 是路径,子 agent 是路径,skill 是路径

前面都是零件。把它们拼起来,得到本章要读者带走的一句话:

read pr://123read src/foo.ts 是同形的:同一个工具、同一个 path 字段、同一套 :sel 选择器、同一条分派链、同一种只读文本产物。 区别只在 scheme——而 scheme 只是选了一个不同的 handler。

这不止 read 一家。grep 也吃内部 URL:它对每个 path 先问 router.canHandle,命中就 router.resolve 出资源,再在其 sourcePath 上做搜索(tools/grep.ts:740-778)。于是 grep "TODO" pr://123grep "TODO" src/ 也是同形的——搜一个 PR 和搜一个目录走的是同一个入口。只有当资源是「无本地路径的目录列表」(如远程 ssh:// 目录)时才拒绝,因为那没有真实内容可搜(tools/grep.ts:772-776)。

为什么这很值: 模型不必学「PR 有个专用工具、子 agent 有个专用工具」。它只要会 read/grep/write,再知道「地址可以带 scheme」,就能读遍整个世界。工具数量对模型的认知负担被摊平成了「一个动作 × 一个地址空间」。

写回也对称:write local://PLAN.mdwrite vault://notewrite 工具里对内部 URL 的分派——有 write 钩子就调 handler 落盘,没有就回落或报错(tools/write.ts:801-820)。


7. 隐藏工具索引:藏起来 + 按需激活

这节讲什么: §3.4 说非必备工具「藏起来」,这节讲藏在哪、怎么被翻出来。这是控制上下文膨胀的关键机制。

动机。 把三十几个工具 + 若干 MCP 服务器的工具全塞进每次请求,既贵又稀释注意力。所以:只亮必备的,其余进一个可搜索索引,模型要用时自己搜、自己激活。

触发条件。 发现模式由 resolveEffectiveToolDiscoveryMode 决定(tool-discovery/mode.ts:18-24):显式设 all/mcp-only 直接生效;auto 模式下,当工具数超过阈值 40(TOOL_DISCOVERY_AUTO_THRESHOLD)才自动开启。开启后,搜索工具本身 search_tool_bm25 才会被挂上。

索引。 隐藏工具进一个 BM25 倒排索引(tool-discovery/tool-index.ts)。BM25(一种经典文本相关性打分,按词频/逆文档频率给「查询—文档」匹配度打分)在这里的语料是每个工具的名字、标签、summary、schema 字段名,以及仅 MCP 工具才有的 server 名与工具名,且分字段加权——名字最高(name 6),标签与 MCP 工具名并列次高(label 4、mcpToolName 4),summary 与 MCP server 名同档(summary 2、serverName 2),schema 键最低(schemaKey 1)(FIELD_WEIGHTS,tool-index.ts:54-61)。分词还会拆驼峰、拆缩写、去重音(tokenize,tool-index.ts:82-101)。

流程。

工具太多 → 非必备工具不进请求,进 BM25 索引

模型:search_tool_bm25{query:"screenshot browser"}

searchDiscoverableTools(index, query, limit) ── 打分排序取前 N

返回候选工具名 + 描述 + score

模型选中 → activateDiscoveredTools([...]) ── 把选中的工具并入本会话活跃工具集

下一回合起,这些工具就正式在册,可直接调用

打分实现见 tool-index.ts:236-271(searchDiscoverableTools),激活入口见 tools/search-tool-bm25.tsactivateTools(search-tool-bm25.ts:103-111)。这套索引是通用的——同时覆盖内建、MCP、扩展、自定义四种来源(DiscoverableToolSource,tool-index.ts:7)。


8. 落盘的两道门:审批 tier 与 preview→accept

这节讲什么: 一个会改世界的工具调用,要过两道独立的门。别把它们混为一谈。

第一道门:审批 tier(要不要问用户)

每次调用先算出 tier(read/write/exec,见 §4.2),再对上当前审批模式,由 resolveApproval 裁决(tools/approval.ts:93-132):

审批模式放行到哪档更高档怎么办
always-ask只自动放 readwrite/exec 弹窗
write放到 writeexec 弹窗
yolo全放(exec)不问

外加用户可对单个工具设 allow/deny/prompt 覆盖;deny 直接抛错拦掉(tools/approval.ts:140-157)。这道门管的是「这次调用要不要经过人」。

第二道门:preview→accept 两段式落盘(改动怎么生效)

有些工具(典型是 edit/ast_edit,plan 模式下更广)不直接落盘,而是先产出一个预览,等一个显式的 resolve 才真正生效。这由隐藏工具 resolve + 一套「待决调用者(pending invoker)」实现(tools/resolve.ts):

工具产出改动 → queueResolveHandler(...) 注册一个「待决处理器」到队列
│ (非强制:不改 tool_choice,不砸 prompt 缓存)

agent-loop 注入一条 system-reminder:「这是预览,调 resolve 落地或丢弃」

模型:resolve{action:"apply", reason:"…"} ← 或 "discard"

ResolveTool.execute 找到待决 invoker → runResolveInvocation

apply → 调用者的 apply() 真正落盘;成功后把该待决项消费掉一次
discard → 丢弃;没有待决项时,discard 视作「已是目标态」温和成功

设计上的两处讲究:

  • 非强制(non-forcing):注册待决处理器时不改 tool_choice,靠 agent-loop 的软性提醒去引导,只有模型不配合时才升级成强制一次 resolve。这样合规的一回合 tool_choice 变更,不会让 prompt 缓存前缀失效(tools/resolve.ts:54-91 注释)。
  • apply 失败可重试:apply() 抛错(如 ast_edit 重叠替换)时,onApplyError 会把待决项按同一 id 重新挂回,模型可以改了再试或 discard,不会卡死(tools/resolve.ts:69-85122-164)。

resolve 本身是隐藏、read tier 的工具(tools/resolve.ts:184-189),这也是 §3.3 里它被无条件补挂的原因——两段式随时要用它兜底。

编辑改动具体怎么算、怎么锚定(hashline 语言、内容哈希、no-op 循环防护)不在本章,见 第 4 章。本章只讲「改动是两段式落盘的」这个协议形状。


9. 结果与渲染:一套 builder + 一张渲染表

这节讲什么: 工具产出怎么回给模型、怎么在 TUI 画成卡片。

结果构造。 工具统一用 ToolResultBuilder(tools/tool-result.ts:11-102)拼产物:.text()/.content() 放内容,.truncation()/.limits() 记截断与配额元数据,.error() 标非抛出式失败,.useless() 标「用完可被压缩省掉」。done() 汇成 {content, details, isError?, useless?}

TUI 渲染。 每个工具的富卡片渲染器登记在一张表 toolRenderers(tools/renderers.ts:68-103),值是 {renderCall, renderResult, …}。渲染器还带几个「回合稳定性」旗标(如 provisionalPendingPreview),用来告诉转录层「这段流式中间态是不是临时的、能不能提交进原生 scrollback」——这是 TUI 增量渲染的正确性细节。task(子 agent)的渲染器用惰性 getter 取,以打破模块导入环(tools/renderers.ts:91-97 注释)。


10. 只点名不深挖:留给别处的几支

本章刻意不展开的工具,各记一句、指个去处:

  • edit / ast_edit——编辑机制是 hashline 内容哈希锚定,整套留给 第 4 章
  • lsp——接语言服务器取诊断/定义/引用;LspTool.createIf 注册(tools/index.ts:454),enableLsp + lsp.enabled 双开关。
  • debug(DAP)——接调试适配协议做断点级调试;DebugTool.createIf(tools/index.ts:448),debug.enabled 控。
  • browser——驱动浏览器(截图/点击/读页);browser.enabled 控(tools/index.ts:456613)。
  • eval——进程内跑 JS/Python/Ruby/Julia,任一后端可达即挂,首调再细分派(tools/index.ts:544-549)。原生、进程内执行的更多机理见 第 5 章

11. 边界与局限(诚实)

  • 内部 URL 是命名空间,不是权限沙箱。 路径穿越靠各 handler 自己防(skill:///memory:///omp:// 都有 .. 检查),但一旦解析成真实路径,读写就落到真实 FS。local:///vault:// 的写会改沙箱外的用户数据,所以 plan 模式会拦(tools/write.ts:806-810)。
  • 路由器是进程级单例。 多会话共享一个路由器;handler 无状态,靠 ResolveContext 把「哪个会话的 cwd/settings/local root」传进去,否则多 main 会话场景下「第一个命中者赢」会挑错 artifacts 目录(internal-urls/types.ts:83-100 注释,issue #1608)。
  • grep 不搜「纯列表」资源。 无本地路径的目录列表(远程 ssh:// 目录)会被拒,以免把「列表文本」误当成「目录内容」(tools/grep.ts:772-776)。
  • 「32 工具」是约数。 精确是 30 个规范内建 + 5 个隐藏,实际挂载数还随设置开关、递归深度、MCP/扩展工具、发现模式浮动。

12. 代码地图(导航索引)

主题文件路径关键符号
工具注册表(可见/隐藏)packages/coding-agent/src/tools/index.tsBUILTIN_TOOLSHIDDEN_TOOLS
装配与开关筛选packages/coding-agent/src/tools/index.tscreateToolsisToolAllowedcomputeEssentialBuiltinNames
必备/可发现过滤packages/coding-agent/src/tools/index.tsDEFAULT_ESSENTIAL_TOOL_NAMESfilterInitialToolsForDiscoveryAll
名字规范与别名packages/coding-agent/src/tools/builtin-names.tsBUILTIN_TOOL_NAMESnormalizeToolNameLEGACY_BUILTIN_TOOL_NAME_ALIASES
read 分派链packages/coding-agent/src/tools/read.tsReadTool.executereadSchemaparseSel
write 的 URL 分派packages/coding-agent/src/tools/write.tsWriteTool.approvalWriteTool.execute
grep 吃内部 URLpackages/coding-agent/src/tools/grep.tsinternalRouter.resolve 段(:740-778)
内部 URL 路由器packages/coding-agent/src/internal-urls/router.tsInternalUrlRoutercanHandleresolve
冒号安全的 URL 解析packages/coding-agent/src/internal-urls/parse.tsparseInternalUrl
handler 接口与资源类型packages/coding-agent/src/internal-urls/types.tsProtocolHandlerInternalResourceResolveContext
子 agent 输出 schemepackages/coding-agent/src/internal-urls/agent-protocol.tsAgentProtocolHandler
PR/issue schemepackages/coding-agent/src/internal-urls/issue-pr-protocol.tsPrProtocolHandlerIssueProtocolHandler
skill/rule/memory/omp/history schemeinternal-urls/{skill,rule,memory,omp,history}-protocol.ts*ProtocolHandler
审批 tier 裁决packages/coding-agent/src/tools/approval.tsresolveApprovalrequiresApproval
preview→accept 两段式packages/coding-agent/src/tools/resolve.tsqueueResolveHandlerrunResolveInvocationResolveTool
隐藏工具发现模式packages/coding-agent/src/tool-discovery/mode.tsresolveEffectiveToolDiscoveryModeTOOL_DISCOVERY_AUTO_THRESHOLD
BM25 工具索引packages/coding-agent/src/tool-discovery/tool-index.tsbuildDiscoverableToolSearchIndexsearchDiscoverableToolsFIELD_WEIGHTS
结果构造packages/coding-agent/src/tools/tool-result.tsToolResultBuildertoolResult
TUI 渲染表packages/coding-agent/src/tools/renderers.tstoolRenderersToolRenderer