返回博客2026年9月7日2 分钟阅读

我又给 Pi Agent 做了个 zvec-grep 扩展

摘要

承接 dsh-zvec-grep 的开发,聊聊 pi-zvec-grep 如何在命令行 Agent 中自动建立和更新代码索引,以及它与 DSH 插件在运行机制上的差别。

上一篇《我给 DeepSeek Harness 做了个 zvec-grep 插件》发布以后,我继续把同一套代码语义搜索能力接入了另一个 Agent:Pi。

这次做出来的扩展叫 pi-zvec-grep。目标没有变:安装一次,以后进入任何项目启动 Pi,索引都在后台自动建立和更新,不需要再运行 zg install、zg index 或 zg serve。

不过,Pi 是一个可以从任意目录启动的命令行 TUI,DSH 则有服务端、Web 客户端和明确的 Session workspace。相同的索引能力放进这两个宿主,生命周期、状态展示和文件监听都需要重新适配。这篇我会把 pi-zvec-grep 的工作机制,以及它和 DSH 版本的主要差别写清楚。期望对大家有所帮助。

安装以后直接启动 Pi

pi-zvec-grep 已经发布到 npm。全局安装只需要一条命令:

pi install npm:@sugarforever/pi-zvec-grep

以后进入项目,照常启动 Pi:

cd /path/to/project
pi

如果希望把扩展作为项目配置分享给其他开发者,也可以写进仓库的 .pi/settings.json:

{
  "packages": ["npm:@sugarforever/pi-zvec-grep"]
}

Pi 在受信任的项目中发现缺少这个 package 时,会自动安装。无论采用哪种方式,用户都不需要为每个项目单独初始化索引。

扩展源码在 sugarforever/yummy-pi-extensions,npm package 是 @sugarforever/pi-zvec-grep。代码、测试、README 和自动发布 workflow 都放在同一个目录中。

Pi 从当前目录得到 workspace

DSH 的插件可以从 session.header.cwd 取得 Session 对应的 workspace。Pi 的入口不一样:用户可能在仓库根目录启动 TUI,也可能在 packages/app 这样的子目录里启动。

pi-zvec-grep 会在 session_start 阶段读取 ctx.cwd,先转换成真实路径,再查询所在的 Git worktree。如果当前目录属于 Git 仓库,就以 worktree 根目录作为索引边界;如果不属于 Git,就使用 Pi 的启动目录。

Pi 启动目录
  -> realpath
  -> Git worktree 根目录,如果存在
  -> 否则使用当前目录
  -> createZvecGrep({ root })

因此,从 repo/packages/app 启动 Pi,索引的仍然是整个 repo。不同 Git worktree 的文件内容可能不同,所以它们分别维护自己的 .zvec-grep/ 索引。

和 DSH 版本一样,这里没有包装 zg CLI,也没有启动 MCP Server。扩展直接调用 zvec-grep 导出的 createZvecGrep()、index() 和 context()。文件扫描、ignore 规则、代码切分、embedding、BM25、向量检索与结果融合仍然由 zvec-grep 完成;Pi 扩展负责把这些能力接入自己的 Session 生命周期。

首次索引不会阻塞 TUI

workspace 确定以后,扩展立即创建一个 WorkspaceRuntime,在后台调用 index() 完成一次完整校准,同时启动文件 watcher。

这个过程不会阻塞 session_start。大型仓库第一次建立索引,或者本地 embedding 模型首次下载时,用户仍然可以继续使用 Pi。

此时如果 Agent 调用 zvec_search,工具也不会一直等待索引结束。它会立即返回结构化状态:

{
  "status": "indexing",
  "retryable": true,
  "root": "/path/to/workspace",
  "message": "The workspace index is not ready. Choose whether to use grep/read now or retry zvec_search later."
}

这不是一次工具错误。Agent 可以根据当前任务决定稍后重试,继续处理其他内容,或者改用精确搜索。异常情况下,即使索引任务迟迟没有完成,也不会把整个 Agent turn 卡在一次工具调用里。

扩展还会在每轮 Agent 开始前加入一小段工具分工说明:不知道准确措辞、需要理解架构或跨文件关系时,使用 zvec_search;已知函数名、配置项、字面量或正则时,继续使用 grep。搜索结果只向模型返回路径、行号、内容、匹配方式和分数,不会把底层完整的 diagnostics 一起放进上下文。

文件变化如何进入索引

Pi 运行期间,watcher 会监听 workspace 内的文件变化。短时间内连续出现的事件先进入 ChangeBatcher,相同路径会被去重;停止写入 750ms 后,整批路径通过 index({ changedPaths }) 交给 zvec-grep。持续写入超过 5 秒时也会强制提交一次,避免索引一直等不到安静窗口。

文件新增、修改或删除
  -> watcher
  -> 路径去重与 debounce
  -> changedPaths 增量索引

Pi 每次启动 + 每小时定时任务
  -> 完整校准

watcher 只负责及时发现变化,不是正确性的唯一来源。操作系统可能合并或丢弃文件事件,Linux 也可能遇到 inotify 数量限制。因此,扩展在每次启动时重新校准,并默认每小时再做一次完整校准。即使 Pi 关闭期间文件发生变化,下次启动也会修复索引。

macOS 和 Windows 支持递归 fs.watch 时,扩展只需要监听 workspace 根目录。Linux 则为允许进入索引的目录分别建立 watcher,同时跳过 .git、.zvec-grep、node_modules 和常见构建目录。watcher 报错后会请求完整校准,并尝试恢复监听。

命令行里的状态显示

DSH 版本有 Web 客户端,所以我为它实现了服务端状态接口和一个可以展开的状态胶囊。客户端需要定时读取状态,还要处理 Session 切换以后旧请求晚到的问题。

Pi 不需要这层结构。扩展可以直接调用 ctx.ui.setStatus(),把状态写进 TUI footer。WorkspaceRuntime 在状态变化时主动通知扩展,不需要浏览器轮询。

用户可能看到这些状态:

  • zvec: indexing - 正在建立首次索引。
  • zvec: updating 4 files - watcher 发现变化,等待或正在增量更新。
  • zvec: ready - 索引可用,watcher 正常运行。
  • zvec: degraded - 现有索引仍可搜索,但 watcher 正在恢复。
  • zvec: error - 索引操作失败。

这里特意区分了 degraded 和 error。watcher 暂时失效,并不代表磁盘上的现有索引也不可用。除了 footer,用户还可以运行 /zvec-status 查看 workspace 和当前状态;/zvec-reindex 可以手动要求完整校准,但正常使用不需要执行它。

Pi 与 DSH 的运行机制不同

两个版本共享同一个基本判断:索引自动建立,文件变化自动更新,耗时准备不阻塞搜索调用,语义搜索不替代精确 grep。真正不同的是宿主提供的边界。

DSH 服务端可能同时管理多个 workspace 和多个 Session。因此,dsh-zvec-grep 使用 workspace map,让相同目录下的 Session 共享 runtime,也允许多个目录同时保持活动。状态从服务端通过接口提供给 Web 客户端,再由状态胶囊显示。

Pi 的一个进程在同一时刻只有一个当前 Session runtime。执行 /new、/resume、/fork 或 /reload 时,扩展关闭旧 runtime,再按照新 Session 的 ctx.cwd 创建一个。这里不需要复制 DSH 的多 workspace map,一个进程对应一个 runtime 反而更符合 Pi 的使用方式。

DSH
  多个 Session
    -> workspace map
    -> 每个 workspace 一套 runtime
    -> 服务端状态接口
    -> Web 状态胶囊

Pi
  当前 Session
    -> 一个 runtime
    -> session_start / session_shutdown 管理生命周期
    -> runtime 主动通知状态变化
    -> TUI footer

文件监听也有所不同。DSH 插件运行在它固定的 Node 服务端环境中,watcher 实现可以相对集中。Pi 是直接分发给用户的命令行扩展,需要面对 macOS、Windows 和 Linux 的不同 fs.watch 行为,因此 Pi 版本增加了按平台选择递归监听或逐目录监听的逻辑。

关闭方式同样服从宿主生命周期。Pi 切换 Session 或退出时,runtime 会停止 watcher 和定时任务,通过 AbortController 取消正在执行的索引,并为关闭过程设置等待上限。debounce 队列里尚未提交的路径不会在退出时启动一轮新索引,它们会由下次启动的完整校准处理。

DSH 与 Pi 最终调用的是同一个 zvec-grep 引擎,但插件并不是把同一份代码换一个注册入口。宿主怎样定义 workspace,能否同时保留多个 Session,状态出现在哪里,关闭事件什么时候发生,这些条件共同决定了扩展应该怎样工作。

默认 embedding 仍然是本地的 local/potion-code-16m-v2。项目内容、模型推理和索引可以留在本机,索引保存在 workspace 的 .zvec-grep/ 目录。如果 watcher 暂时漏掉事件,下一次启动或定时校准会继续修复。


相关文章

2026年9月16日

fx:把 coding agent 做成一把瑞士军刀

fx 是一个用 Zig 写的原生 coding agent,二进制只有 6MB 出头。它能当终端 agent 用,也能作为 WASM 内核嵌进浏览器和 Node 应用。这篇文章拆解它的工具、权限、子 agent、上下文和嵌入设计,以及它和主流 coding agent 的差别。

最近一封 · Sample

OpenRig 冲上 Trending:把 Claude Code 和 Codex 编成一支队伍

“OpenRig 在 GitHub Trending Today 获得 622 个 stars 的窗口信号。它用可恢复的队列、席位与权限记录,把 Claude Code 和 Codex 放进同一套多 Agent 工程流程。”

—— william

Letters

来信

里面装的是

  • 新文章 — 写完一篇就寄一封,不攒货
  • 这周读到的、看到的、好用的工具
  • 正在折腾的实验,附带翻车记录

约莫 1–2 周一封 · 随时退订

合作伙伴

CompeteMap — 英国及爱尔兰学生竞赛一站式搜索

数学、编程、科学、写作等各类竞赛信息汇总,支持按年龄和科目筛选,再也不错过报名截止日。

准备开始了吗?

先简单说明目标,我会给出最合适的沟通方式。