从 OMP 配置到领域 Agent
上一章用 Pi 的公开接口组装了仓库维护 Agent。OMP 保留了 Pi 的 Agent 和 Extension 机制,又加入集中式工具注册、Hashline 编辑、审批分级、多后端记忆、浏览器监督与结构化 Task。
这些能力已经能支撑一个有独立工作方法的 Agent。开始时不要急着复制仓库或全局替换品牌名,先从配置和 Extension 做出可运行的版本。
本章会做一个 my-omp-agent。它维护项目里的 work item,可以查询和更新状态,写操作需要审批,文件发生变化时拒绝猜测编辑位置。
OMP 在 Pi 上增加了什么
Section titled “OMP 在 Pi 上增加了什么”OMP 的 README 明确说明它是 Pi fork,查看固定 revision 的说明。它没有完全重写 Pi,而是保留相近的包结构和 Agent 抽象,再做系统性替换:
- package scope 迁移到
@oh-my-pi/*; - 运行时以 Bun 为中心;
- Extension 改用 Bun 原生动态 import;
- 工具由
BUILTIN_TOOLS与createTools(session)集中装配; - 单凭证文件扩展为 SQLite 多凭证、轮询和会话亲和;
- 增加 Hashline、MCP、LSP、Browser、Task 与多种 Memory backend。
这些差异由项目自己的移植文档记录,查看 intentional divergences 与新增能力。文档里的 Pi 同步 commit 是历史移植锚点,不代表 OMP HEAD 与当前 Pi HEAD 可以逐文件直接对齐。
一次请求大致经过:
runCli(argv) ↓main / settings / approval / theme ↓createAgentSession() ↓Agent.prompt() ↓agentLoop ↓ExtensionToolWrapper ↓approval → safety → tool_call → execute → tool_result会话 composition root 位于 sdk.ts,Loop 的流式模型与工具阶段位于 agent-loop.ts。
macOS 与 Linux 可以使用官方安装脚本,Windows 使用 PowerShell 安装脚本:
# macOS / Linuxcurl -fsSL https://omp.sh/install | sh
# 安装后记录本机版本omp --version# Windows PowerShellirm https://omp.sh/install.ps1 | iexomp --version本章的源码结论固定在 revision 667111575ebba136dadfd6989379e7f67e0d40d9;你安装的交互产品可能更新。命令或设置项不一致时,先保存 omp --version,再对照本章固定源码,不要把版本差异误判为操作错误。查看固定 revision 的安装入口。
先建一个独立 Profile
Section titled “先建一个独立 Profile”Profile 会重定位用户级设置、Extension、Skill、MCP、Session、blob 与认证数据库。先确认实验 profile 的位置:
omp --profile my-omp-agent config path再写入一组保守设置:
omp --profile my-omp-agent config set tools.approvalMode always-askomp --profile my-omp-agent config set memory.backend offomp --profile my-omp-agent config set browser.enabled falseomp --profile my-omp-agent config set edit.mode hashlineomp --profile my-omp-agent config set edit.enforceSeenLines trueOMP 固定版本的默认 approval mode 是 yolo,实验必须显式改为 always-ask。查看 settings schema 中的审批默认值。
Profile 不是安全沙箱。项目里的 .omp/*、其他工具配置与宿主机进程权限仍然共享;处理不可信仓库时,还需要独立 HOME、容器或其他操作系统隔离。
在 profile 的 agent 目录创建 APPEND_SYSTEM.md:
# my-omp-agent
你维护项目中的 work item。
- 更新状态前说明依据。- 不补造 owner、截止日期或测试结果。- 读取可以直接执行,写入与命令执行遵循工具审批。- 信息冲突时保留原值并报告冲突。这里使用 APPEND_SYSTEM.md,保留 OMP 内建工具说明、Skill 列表和动态项目环境。SYSTEM.md 会替换稳定 system block,适合完全接管系统提示的少数场景。查看组合顺序。
这一层已经能改变模型、工具开关、审批、Memory backend、Browser、Task 并发、system prompt 和主题。它不会增加新的运行时代码。
再用 Skill 写下 work item 方法
Section titled “再用 Skill 写下 work item 方法”在 profile 下建立一层 Skill 目录:
skills/└── work-item/ ├── SKILL.md └── references/ └── state-machine.mdSKILL.md 规定查询、检查、更新、再次查询的顺序;state-machine.md 只允许:
open → in_progressin_progress → blockedblocked → in_progressin_progress → done并规定只有 evidence 非空才能进入 done。OMP native Skill 只扫描 <skills-root>/<name>/SKILL.md 这一层;更深的团队目录不会自动成为 Skill。查看 Skill 发现规则。
显式实验:
/skill:work-item 请说明 CASE-001 从 open 到 done 需要经过哪些状态答案应包含 open → in_progress → done,并指出 done 需要 evidence。再读取 skill://work-item/references/state-machine.md,然后尝试 skill://work-item/../other-file。后者应被路径穿越检查拒绝。查看 skill:// 路径保护。
Skill 此时只描述方法。项目仍然缺少可以查询和更新 work item 的工具。
最后用 Extension 增加领域工具
Section titled “最后用 Extension 增加领域工具”把下面完整文件保存为 profile agent 目录下的 extensions/work-items.ts。它只依赖 OMP 自带的 Extension API,注册一个查询工具和一个更新工具:
import { randomUUID } from "node:crypto";import { readFile, rename, writeFile } from "node:fs/promises";import { join } from "node:path";import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
type Status = "open" | "in_progress" | "blocked" | "done";type WorkItem = { id: string; status: Status; evidence?: string };
const transitions: Record<Status, Status[]> = { open: ["in_progress"], in_progress: ["blocked", "done"], blocked: ["in_progress"], done: [],};
async function readItems(file: string): Promise<WorkItem[]> { return JSON.parse(await readFile(file, "utf8")) as WorkItem[];}
async function writeItems(file: string, items: WorkItem[]) { const temporary = `${file}.${randomUUID()}.tmp`; await writeFile(temporary, `${JSON.stringify(items, null, 2)}\n`, "utf8"); await rename(temporary, file);}
export default function workItems(pi: ExtensionAPI) { const { z } = pi.zod; const id = z.string().regex(/^CASE-[0-9]+$/); const status = z.enum(["open", "in_progress", "blocked", "done"]);
pi.registerTool({ name: "work_item_query", label: "Work Item Query", description: "读取 work-items.json 中的一条记录。", approval: "read", parameters: z.object({ id }), async execute(_callId, params, _signal, _onUpdate, ctx) { const items = await readItems(join(ctx.cwd, "work-items.json")); const item = items.find((candidate) => candidate.id === params.id); return item ? { content: [{ type: "text", text: JSON.stringify(item) }], details: item } : { content: [{ type: "text", text: `${params.id} not found` }], isError: true }; }, });
pi.registerTool({ name: "work_item_update", label: "Work Item Update", description: "按状态机更新 work item,并原子替换 JSON 文件。", approval: "write", parameters: z.object({ id, status, evidence: z.string().min(1).optional(), }), async execute(_callId, params, _signal, _onUpdate, ctx) { const file = join(ctx.cwd, "work-items.json"); const items = await readItems(file); const index = items.findIndex((candidate) => candidate.id === params.id); if (index < 0) { return { content: [{ type: "text", text: `${params.id} not found` }], isError: true }; }
const current = items[index]; if (!transitions[current.status].includes(params.status)) { return { content: [{ type: "text", text: `不允许 ${current.status} → ${params.status}`, }], isError: true, }; } if (params.status === "done" && !params.evidence?.trim()) { return { content: [{ type: "text", text: "done 需要 evidence" }], isError: true }; }
const updated = { ...current, status: params.status, evidence: params.evidence }; items[index] = updated; await writeItems(file, items); return { content: [{ type: "text", text: JSON.stringify(updated) }], details: updated, }; }, });}在准备测试的仓库根目录创建 fixture:
[ {"id": "CASE-001", "status": "open"}]重启 omp --profile my-omp-agent 后,要求它查询 CASE-001,再更新为 in_progress。Extension loader 会发现 profile extensions/ 下的 TS/JS 文件;开发时也可以使用 -e 显式加载。查看 Extension 加载说明。
OMP 的 Extension tool 如果没有声明 approval tier,会按 exec 处理。本例明确把查询设为 read,把更新设为 write。查看 ToolDefinition 的 approval 字段。
在 always-ask 模式下,query 可直接运行,update 会请求确认。拒绝后文件必须保持原值;同意后再查询一次,比较持久化结果。
写入链路按下面的顺序运行:
- OMP approval policy;
- Provider safety;
- Extension
tool_call; - Tool execute;
- Extension
tool_result。
统一包装发生在 ExtensionToolWrapper。显式 deny 的优先级最高;没有可交互 UI 时,Provider safety 会失败关闭。
Hashline 解决哪种编辑事故
Section titled “Hashline 解决哪种编辑事故”普通 diff 依赖行号和邻近文本。Agent 读取文件后,另一进程插入一行,旧 patch 可能改错位置。Hashline 为读取快照生成 [PATH#TAG],再用行锚点执行 SWAP、DEL、INS 等操作。
Patcher 先读取实时文件,与 SnapshotStore 中的旧快照比较,只有映射连续且唯一时才恢复;锚点语义已经改变或映射不唯一时会拒绝。查看恢复的 fail-closed 条件。
建议做四轮实验:
| 场景 | 预期结果 |
|---|---|
| 文件未变化 | 修改成功 |
| 只在前面插入无关行 | 唯一映射时恢复并提示 |
| 目标行内容已被人修改 | 拒绝,不猜测 |
| Agent 没看过目标行 | seen-line guard 拒绝 |
多文件 section 会先统一 prepare,再开始写入,常见 stale 错误不会造成前半批落盘。查看 Patcher 的预检与提交。但真实文件系统若在 commit 循环中途失败,源码没有事务回滚,不能把 Hashline 描述成绝对原子写入。
Memory、Browser 与 Task 何时加入
Section titled “Memory、Browser 与 Task 何时加入”my-omp-agent 的业务工具稳定以后,再逐项打开附加能力,方便定位问题:
- Memory backend 一次只选
off、local、mnemopi或hindsight。查看 backend selector。这个 selector 只证明后端四选一;召回文本的指令注入风险要针对所选 backend 单独测试。 - Browser 是 exec tier,会创建受监督的 tab 与会话资源。即使只是“打开网页”,也可能触发登录态和外部动作。查看 Browser tool。
- Task 会发现用户级与项目级 Agent Definition,支持单个、批量和异步执行。子 Agent 通过
createAgentSession()建立独立会话,并受到递归深度、并发与工具集合限制。查看 Task executor。
Task 子 Agent 没有主界面的逐次确认体验。它在无界面会话中以 yolo 运行,父级 task 调用是授权点;需要限制子 Agent 时,应在 Agent Definition 中收窄 tools,并配置明确的 tools.approval.<tool> 策略。
每打开一项附加能力,都会增加权限、资源清理、超时和评测分支。
什么时候才需要 Fork
Section titled “什么时候才需要 Fork”按改动触及的边界选择入口:
| 需求 | 选择 |
|---|---|
| 改模型、主题、审批、目录或功能开关 | Config / Profile |
| 增加步骤、参考资料与触发说明 | Skill |
| 定义 Task 子 Agent 的模型、tools、spawns 与输出 schema | Agent Definition |
| 增加工具、命令、事件、Provider 或运行时代码 | Extension |
| 修改 Loop、内建工具、Hashline、认证存储或品牌发布面 | Fork |
第十个 Lab 会把一组需求归到这五层:
cd agent-harness-labnpm run lab -- 10结果中,model 与 theme 进入 config,subagent role 进入 agent definition,tool 与 policy hook 进入 extension,只有 replaceAgentLoop 进入 fork。
以下改动才值得承担长期分叉:
- 改 Loop 的停止、steering 或调度语义;
- 改 Hashline grammar 与恢复算法;
- 改认证数据库与多凭证调度;
- 改
.omp、profile、环境变量和配置合并规则; - 发布独立 CLI、package scope 与 native binary。
如果要彻底改品牌,还要迁移 APP_NAME、配置根、bin、package scope、日志、User-Agent、telemetry、安装脚本、文档与 assets。查看品牌常量入口。
验收时应在空的临时 HOME 中启动新 binary,确认只创建新配置根;旧品牌字符串只能留在 License、上游归属与明确的兼容 allowlist 中。OMP 使用 MIT License,分叉必须保留 Mario Zechner 与 Can Bölük 的版权说明。查看固定版本 LICENSE。
完成这些步骤后,my-omp-agent 有独立 Profile、工作方法、领域工具、审批策略和稳健编辑。下一章把 Pi 与 OMP 当作两个外部 CLI,交给 Orca 管理 worktree、终端和代码审阅。