这是「omp 编程工具使用探讨」专栏的又一篇追加文章(系列自第 9 篇「完结」后,陆续补了魔法关键词、Skill 蒸馏等源码级深挖)。前几篇里我一直在用
skill://、memory://、local://这些地址却没解释它们——本篇把背后的内部 URL scheme 一次讲透。
TL;DR:omp 定义了一批 scheme:// 形式的内部 URL,它们是 read/write 工具的 path 参数值——不是文件系统路径,用户终端也 cat 不了。主要消费者是 agent(模型)自己。xd:// 是其中特殊的一个:它把低频工具挂载成”虚拟设备”,用 read xd://<name> 读文档、write xd://<name> 带 JSON 执行,把工具 schema 的成本从每次请求摊到真正使用时。
一句话讲清楚
想象 agent 需要访问这些东西:内置文档、技能包、子代理产物、远程服务器文件、GitHub issue、MCP 资源、挂载的工具……如果每种资源一套寻址方式,agent 的脑子里得装几十种协议。omp 的答案是:统一成 URL,全部塞进 read/write 的 path 参数。
你在 TUI 输入框 agent 的动作───────────────────── ─────────────────────────────"读一下 python 模式技能" → read skill://python-patterns"看看 bash 工具怎么用" → read omp://tools/bash.md"打开 example.com 抓流量" → write xd://mcp__chrome_devtools_new_page {json}这里有个关键认知:URL 的消费方是 agent,不是你。你在 TUI 里输入的是自然语言,agent 把它翻译成一次 read/write 调用。这也是后面”常见坑”一节里”用户终端不认这些 scheme”的根源。
为什么需要一套”寻址词汇表”
先把问题摊开。一个 omp session 里,agent 可能要被要求访问:
| 资源 | 没有 scheme 时怎么做 |
|---|---|
内置工具文档(120 余篇,docs/) | 记文件名 + 路径,逐层翻目录 |
技能包(skill://thinking-in-uml) | 在 .omp/skills/ 里找 SKILL.md |
| 子代理产物 / 工具输出溢出 | 不知道去哪找,ID 记不住 |
| 远程服务器文件 | 手动 ssh + scp 拼路径 |
| GitHub issue / PR | curl API + 记 JSON 结构 |
| MCP 资源 | 记 server 名 + 资源 URI |
| 工具调用 | 记每个工具的完整参数 schema |
每一种都是独立的”寻址方言”。omp 的做法是把它们全部折叠进一个寻址语法——scheme://,agent 只需要会一个动作(read/write + 一个字符串),就能访问所有资源。这跟浏览器用 http:// 统一访问网页是同一个思路,只是这里的”网页”是 agent 视角的资源。
14 个内置 scheme 全清单
InternalUrlRouter(internal-urls/router.ts 的 constructor)注册了 14 个 scheme,每个 scheme 一个 handler。按用途分四类:
学知识 / 查文档
| Scheme | URL 形态 | 用途 |
|---|---|---|
omp:// | omp://、omp://<file>.md | harness 自带文档,omp:// 列目录 |
skill:// | skill://<名>、skill://<名>/路径 | 技能包 SKILL.md 与包内文件 |
rule:// | rule://<名> | 规则详情 |
skill:// 是本专栏的老朋友——Skill 蒸馏那篇里的 thinking-in-uml 就是通过它加载的。
看产物 / 会话
| Scheme | URL 形态 | 用途 |
|---|---|---|
agent:// | agent://<id>、agent://Parent/Child | 子代理输出;/路径 或 ?q= 做 JSON 提取 |
artifact:// | artifact://<id> | 工具输出溢出转存的产物内容 |
history:// | history://、history://<agentId> | 代理会话 transcript(只读 markdown) |
local:// | local://<name>.md | 会话本地沙箱文件(可写,~/.omp/agent/sessions/<id>/local/) |
local:// 是最常被低估的一个——它是可写的,是 agent 跨会话/跨子代理传大 payload 的通道(比如把一段超长指令 write local://plan.md 落盘,再在 task 里引用 local:// 路径)。
桥接外部
| Scheme | URL 形态 | 用途 |
|---|---|---|
mcp:// | mcp://资源URI | MCP 服务器暴露的资源(支持任意自定义 scheme、opaque URI) |
ssh:// | ssh://host/路径 | 远程文件(UTF-8 ≤1MiB),复用 ControlMaster 连接 |
vault:// | vault://、vault://search?q= | Obsidian 笔记库操作 |
issue:// | issue://123、issue://owner/repo/123?state=open | 读 issue,走 SQLite 缓存 |
pr:// | pr://123、pr://owner/repo,?comments=0 去掉评论 | 读 PR |
ssh:// 是部署运维场景的入口——本博客的部署链路(scripts/deploy.sh 用 rsync 同步 dist/ 到服务器)就是从 read ssh:// 读服务器状态、改远程配置开始的。
记忆 / 工具
| Scheme | URL 形态 | 用途 |
|---|---|---|
memory:// | memory://… | 记忆后端(默认 memory_summary.md,namespace root,另有 mnemopi 后端) |
xd:// | xd://、xd://<tool> | 挂载的工具设备:read 列/读文档,write 带 JSON 执行 |
memory:// 在记忆系统那篇讲过——这里补充一点源码事实:memory-protocol.ts 里 DEFAULT_MEMORY_FILE = "memory_summary.md"、MEMORY_NAMESPACE = "root",这就是为什么你总看到 memory://root/…。
不在 router 里的一个
上面四类加起来正好是 router 注册的 14 个 handler。除此之外还有一个工具层扩展不在 router 里——它由 read/write 工具本地处理,write.ts 源码原话是 “conflict:// has no router handler”:
| Scheme | 状态 | 说明 |
|---|---|---|
conflict:// | read/write 工具本地处理 | git 合并冲突解析:conflict://<N> 读块、write conflict://<N> 解决、conflict://* 批量 |
xd://:把工具挂载成”虚拟设备”
xd 是这份清单里最特殊的一个——它不是”读某类资源”,而是把工具本身变成可寻址的地址。三个操作:
read xd:// → 列出全部挂载设备(发现)read xd://<tool> → 返回工具文档 + JSON 参数 schemawrite xd://<tool> → 执行:content 就是 JSON 参数对象write xd://<name> 的 content 传空、? 或 help 时返回文档而不执行(xdev.ts 里的 HELP_CONTENT_RE)。
为什么 MCP 工具默认挂载
打开一个 session,你会发现 chrome devtools 之类的 MCP 工具全在 xd:// 下面,而不是顶层。判定逻辑(xdev.ts 的 isMountableUnderXdev):
// xdev.ts:69-79(简化)export function isMountableUnderXdev(tool: { name: string; loadMode?: ToolLoadMode }): boolean { if (tool.name in XDEV_TRANSPORT_TOOLS || tool.name in XDEV_KEEP_TOP_LEVEL) return false; return tool.loadMode === "discoverable";}两个排除名单(源码注释写了各自的原因):
XDEV_TRANSPORT_TOOLS = { read, write }——它们本身就是xd://的传输通道,挂载了就没入口访问其他设备了(issue #5764)XDEV_KEEP_TOP_LEVEL = { todo, ask, grep, web_search }——各自有 harness 集成:todo喂 prelude/prewalk 机制、ask是模型向用户提问的通道、grep是 bash 拦截规则的重定向目标、web_search被大多数模型直接调用,它没有xd://的概念,藏起来等于不可达(issue #5973)
MCP / 扩展工具经 adapter 注册,loadMode 默认 "discoverable" → 默认挂载。总开关是 tools.xdev(settings-schema.ts 默认 true),关掉则所有启用工具顶层暴露。
为什么用 xdev 而非顶层直接调用
配置里的原文说得很直白:“Mount rarely-used (discoverable) tools under xd:// … instead of shipping their schemas on every request.”
核心动机是系统提示的 token 预算。 顶层工具每次请求都带完整的 name + description + schema。MCP 服务器可能暴露几十上百个工具(chrome devtools 一个 server 就挂几十个),全内联到系统提示里不可承受。xdev 的方案是:
- 内置挂载工具的文档内联进系统提示(见下一节,总量有预算上限)
- 外部(MCP)工具只给一行目录摘要:
xd://<name> — summary,完整文档按需read xd://<name>再取
代价是每次调用多一次 read 往返——所以只有低频工具走 xdev,高频工具(read/write/bash/edit/web_search)强制顶层。频率分层,各得其所。
预算常量(xdev.ts):
export const XDEV_DOCS_TOTAL_BUDGET = 48_000; // 系统提示里整个 xd 文档区总量export const XDEV_DOCS_PER_DEVICE_CAP = 10_000; // 单个设备文档上限export const XDEV_EXTERNAL_DESCRIPTION_CAP = 200; // 外部工具一行摘要的字符上限注:我最初按文档理解写的是”系统提示里每个挂载设备只留一行目录”。对着源码核对后发现实际更精细——
tools.xdevDocs有三种模式(settings-schema.ts默认"builtins"):
模式 行为 inline所有挂载设备全文内联 builtins(默认)内置挂载工具文档内联,外部工具只给一行摘要 catalog全部只给一行摘要,文档全按需 read 判定在
shouldInlineXdevTool(xdev.ts):mode !== "catalog" && (mode === "inline" || builtInNames.has(name) || 匹配 xdevInlineDevices glob)。也就是说默认情况下,omp 自带的挂载工具(lsp、debug、browser 这些)文档是直接内联的,agent 不用先 read 就知道怎么用;只有 MCP 外部工具走”摘要 + 按需全文”。
动态挂载
MCP 服务器可以中途连接/断开。xd:// 目录是动态的——通过 xdev-mount-notice 注入 added/removed 设备通知,天然适配”运行时工具集会变”的场景。
同一条管线:参数 schema 的五层
说完了 xd:// 怎么把工具变成地址,再看它背后复用的参数 schema 管线——这五层决定了 write xd://<tool> 里的 JSON 长什么样,也解释了为什么工具作者只写一次 schema。xd:// 不是一套新 schema,它复用 toolWireSchema + jsonSchemaToTypeScript 这条现成管线,只是把”传参 + 文档”改走 read/write 通道。
普通读者可以跳过本节的实现细节,直接看「TUI 实操与常见坑」。只记住一个钩子就够:参数校验失败时返回的错误自带 schema,模型读到错误信息就能自我纠正,不用多一轮往返。
这套体系分五层:
| 层 | schema | 为什么存在 |
|---|---|---|
| 作者层 | Zod(canonical)/ ArkType / TypeBox·plain JSON Schema | 三种写法由 toolWireSchema 统一转换为 wire JSON Schema(ArkType 走原生 toJsonSchema) |
| 互操作层 | JSON Schema(toolWireSchema) | 所有 LLM API 的通用语言;空 schema 归一化为 true(issue #1179) |
| 厂商层 | provider sanitizer ×8 | 各家 strict / boolean subschema / additionalProperties 约束不同,不归一化会 400 |
| 模型可读层 | jsonSchemaToTypeScript | 给模型看的精简 TS 类型,比 JSON Schema 省 token |
| 运行时层 | .assert 校验(validateToolArguments) | 参数进 execute 前把关,报错带 schema 供模型修复 |
这个”错误即教程”的设计,跟魔法关键词那篇讲的”隐藏 notice 改行为”是同一个哲学:把修错的成本从人工挪给模型。
TUI 实操与常见坑
先说怎么上手。你不需要记任何 URL——在 TUI 里用自然语言让 agent 处理,它替你完成寻址:
你在输入框敲 agent 的动作────────────────────── ─────────────────────────────"把写作规范读一下" → read skill://blog-series → 会话里出现一条 tool 调用并返回内容"看下服务器当前状态" → read ssh://…(或 bash 里执行部署脚本)想亲眼确认这些 scheme 在跑,盯着 agent 的 tool 调用记录就行——每一条 read <scheme>://… 都是一次寻址。
三种使用场景与正确姿势
| 场景 | 正确做法 | 错误做法 |
|---|---|---|
| 在 TUI 里读资源 | 对话让 agent 处理(read local://demo-plan.md) | 自己终端跑 cat local://... |
| 在自己终端用这些文件 | 手动用真实路径(local:// 在 ~/.omp/agent/sessions/<id>/local/) | 敲 URL 字面量 |
| 写脚本 / 自定义工具 | 调 InternalUrlRouter API 或 expandInternalUrls 解析 | 硬编码 URL 当路径 |
关键坑:bash 展开只对 agent 的 bash 工具
bash-skill-urls.ts 的 expandInternalUrls() 只在 agent 调用 bash 工具时执行,把命令里的 URL 重写成真实路径。用户自己的终端是普通 shell,不认这些 scheme——会报 No such file or directory。
且展开只支持 7 个 scheme(源码 SUPPORTED_INTERNAL_SCHEMES):
const SUPPORTED_INTERNAL_SCHEMES = ["skill", "agent", "artifact", "plan", "memory", "rule", "local"] as const;名单里有个特例:plan:// 出现在展开名单里,却没有对应的 router handler——plan 文件实际走 local://PLAN.md(acp-agent.ts 的 DEFAULT_PLAN_FILE_URL)。其余 scheme(xd://、omp://、issue://、ssh://、vault://、pr://、mcp://、history://、conflict://)都不在展开名单里——即使 agent 的 bash 工具也不展开。想在自己的 shell 里用这些地址,唯一的办法是走真实路径。
read 的 selector
read 本身还支持局部读取,跟 scheme 叠加使用:
omp://tools/read.md:213-230— 只读某段文件.ts:conflicts— 列出合并冲突块:raw— 原样输出,跳过转换器
谁直接接触这些 scheme
| 角色 | 是否直接用 | 方式 |
|---|---|---|
| omp 用户 | ❌ | 对话让 agent 处理 |
| agent(模型) | ✅ | read/write 的 path 参数 |
| 扩展开发者 | ✅ | InternalUrlRouter 解析、local:// 传参(task payload)、MCP 自定义资源 scheme |
扩展开发者的真实场景有四个:custom tool 参数收 URL 值、子代理传大 payload(write local:// 再在 task 指令里引用)、MCP server 定义自定义资源 scheme(read 自动可读)、/tools 查看挂载状态。
本文本身就是个活例
写这篇的时候,我正在亲身体验这套体系:
- 加载写作规范 →
read skill://blog-series、read skill://blog-author - 查项目记忆 →
read memory://root/memory_summary.md - 查服务器/部署约束 → 项目规则里引用的
.env通过ssh://读 - 核对源码事实 →
read <oh-my-pi 仓库>/packages/coding-agent/src/tools/xdev.ts:69-79
也就是说,这篇文章的每一处”源码引用”都是 agent 用 read + 真实路径完成的,而它读文档的方式(skill://、memory://)正是本文讲的机制。寻址体系不仅服务 agent 的执行,也服务 agent 的自我学习——这是这套设计最优雅的地方:用同一套寻址语法,把”工具怎么用”这类元知识也变成可寻址资源。
总结
一句话:
omp 用 URL scheme 把”文档、技能、产物、远程文件、GitHub、MCP 资源、工具调用”统一成可
read/write的地址,让 agent 用一套寻址方式访问所有资源;xd://用”目录 + 按需 read + write”把低频工具的 schema 成本从每次请求摊到真正使用时。
几个值得带走的点:
- scheme 是给模型用的词汇表,不是给用户的文件系统——理解这一点,所有”为什么我的终端 cat 不了”的困惑都消失了
xd://的本质是 token 预算工程——用一次 read 往返换掉几十个工具 schema 的常驻成本,频率分层是关键- schema 管线五层一体——作者用 Zod/ArkType/TypeBox 写一次,wire/厂商/模型/运行时各取所需,
xd://只是换了传输通道 - 动态是设计的一部分——MCP 中途连接、挂载设备列表变化,系统提示通过 notice 跟随
下一篇想写**local:// 作为子代理 payload 通道的实操**——怎么用 write local://plan.md + task 指令引用,把超长上下文传给子代理而不撑爆对话。这也是整理本文的那场 omp session 留下的待办之一。
本文是「omp 编程工具使用探讨」专栏的追加篇。文中源码引用来自 oh-my-pi 当前版本(packages/coding-agent/src/internal-urls/、packages/coding-agent/src/tools/xdev.ts、packages/coding-agent/src/config/settings-schema.ts),具体实现可能随版本变化,建议以实际源码为准。