摘要

本文用一个 Context Audit Extension 展示 Pi Tool、Command、Event 与 UI Status 的职责, 并给出来源契约、官方类型检查、注册与 Mock、真实 Runtime、TUI/模型路径、生产安全六层 证据阶梯。文中真实验证到 Pi v0.82.1 RPC 加载、/audit 发现与执行、 context_audit 进入活动工具面。未把 TUI、模型自主调用或生产安全写成已通过。

关键词: Pi、Agent Harness、Extension、Runtime、RPC、证据分级

目录

  1. Tool、Command、Event 与状态栏怎样协作
  2. 为什么 Factory 与模式判断是工程边界
  3. 从官方类型检查到真实 RPC 的六层证据
  4. 观察事件、阻断事件和安全边界
  5. 可复跑验收合同与故障定位

一份 Extension 已经有 Tool、Slash Command、事件监听和状态栏,TypeScript 也没有报错。 它算“完成”了吗?不一定。

代码可能只是在手写类型桩下通过。Mock 可能从未经过真实加载器。命令可能已注册,但 Tool 没有进入活动工具面。状态栏可能在 TUI 中有效,却让 RPC 或 Print 模式崩溃。把不同强度的 证据合并成一句“实战已通过”,会让团队无法判断产物究竟在哪一层成立。

本文用一个 Context Audit Extension 回答两个问题:完整扩展如何组织 Tool、Command、 Event 与 UI?又怎样把“源码对齐”一步步推进到可复现的真实 Runtime 证据?

核心结论是:Extension 不是按“能不能跑”二分,而要按证据层级验收。每一级只允许支撑 它亲自验证过的结论。

Extension 写完不等于可用

一、先定义一个能暴露边界的扩展

我们实现的 context-audit.ts 不负责业务写入,只读取当前 Runtime 的几个状态:工作目录、 Project Trust、Context 使用量、活动工具,以及可选的 System Prompt 预览。

这个扩展暴露四类接口:

context_audit Tool:模型可以自主调用
/audit Command:用户不经过模型直接触发
Lifecycle Events:观察 Session、Agent 与 Tool 执行阶段
Footer Status:把暂态运行状态显示给人

这四项看似都在“做审计”,实际控制权不同。Tool 把动作交给模型。Command 把动作交给用户。 Event 让 Runtime 在特定阶段通知扩展。Status 只是人类观察面。把它们混成一个功能,会掩盖 验收应该在哪一层发生。

二、Tool 与 Command 不是重复入口

Command 与 Tool 的控制权分离

Tool 需要名称、用途描述、参数 Schema、执行器和结构化结果。示例使用 TypeBox 描述可选参数:

parameters: Type.Object({
  includeSystemPrompt: Type.Optional(Type.Boolean())
})

执行结果分成两部分。content 提供模型可读的观察结果,details 提供应用可继续消费的结构化 快照。不要为了方便调试,把整个 Runtime 对象或完整 System Prompt 全塞回 Context。

Command 则处理 /audit。用户想立即看状态时,不应先向模型发送“请判断要不要调用审计 工具”。固定版本 RPC 文档也明确,Extension Command 可以作为 Prompt 中的 Slash Command 立即执行。两条入口因此需要分别验收:

get_commands 能发现 /audit
getActiveTools 能看到 context_audit
/audit 能执行并返回可核对结果
模型是否会自主选择 context_audit,另做模型路径测试

前三项不需要消耗模型调用。最后一项才与模型、提示和 Provider 行为有关。

三、状态栏是观察面,不是任务真相

固定版本公开类型的证据边界

示例监听 session_start、agent_start、tool_execution_start、 tool_execution_end 与 agent_settled,并用同一个 Status Key 更新状态:

Audit: ready
Audit: agent running
Audit: bash
Audit: idle · 18.4%

这些文本帮助人理解 Runtime 正在做什么,但它们不会因为显示在页脚就自动进入模型 Context, 也不会成为持久化 Session 状态。需要恢复、审计或跨进程共享的数据,必须写入明确的状态合同, 不能把 Footer 当数据库。

同时,UI 能力不是每个模式都有。实现先检查 ctx.mode === "tui",通知则检查 ctx.hasUI。 这使同一扩展可以在 RPC 中完成无模型命令验证,而不依赖一个不存在的交互终端。

四、为什么 Tool Factory 比模块级全局变量更稳

Tool Factory 的实例边界

早期示例为了让 Tool 访问 pi.getActiveTools(),把 ExtensionAPI 存进模块级变量。单实例运行 可能没问题,但 Reload、并行测试或同模块重复加载会让引用来源变得模糊。

更稳的写法是工厂:

function createAuditTool(pi: ExtensionAPI) {
  return defineTool({
    // schema and metadata
    async execute(_id, params, _signal, _update, ctx) {
      const activeTools = [...pi.getActiveTools()].sort();
      // return bounded snapshot
    },
  });
}

export default function contextAuditExtension(pi: ExtensionAPI) {
  pi.registerTool(createAuditTool(pi));
}

这不是风格偏好,而是把依赖绑定到 Extension Instance。测试时可以清楚知道 Tool 使用的是 哪个 API 实例,也避免模块状态替运行时生命周期背书。

五、六层证据阶梯

1. 来源契约

先固定版本、公开导出和事件定义。本文锁定 Pi v0.82.1 / b4f2936,只从公开包入口导入 defineTool、ExtensionAPI 与 ExtensionContext,并对照固定 Tag 的文档和官方示例。

这一层只能说明“我们理解了目标版本的合同”,不能说明示例能编译。

2. 官方类型检查

Batch 08 原验证使用最小手写声明桩,能检查局部 TypeScript 逻辑,却不能证明真实 npm 包的 声明兼容。此次在临时目录安装 @earendil-works/[email protected]、 @earendil-works/[email protected] 和 TypeScript 5.8.3,让示例直接连接官方声明执行严格检查。

结果通过。这里的结论是 VERIFIED_TYPECHECK,仍不能写成 Runtime 已加载。

3. 注册面与 Mock

Fake ExtensionAPI 可以记录注册了哪个 Tool、Command 和 Event,并直接测试 collectSnapshot()、renderSnapshot()、空 Context、输出截断等纯逻辑。

Mock 很适合快速回归,但它通常不会重现真实资源发现、TypeScript Loader、Project Trust、 模式能力和 Extension Runner。它证明的是注册意图和局部逻辑。

4. 真实 Runtime RPC

RPC 官方文档对 Extension Command 的说明

这是本次新增的关键证据。隔离目录中的 Node 为 v24.3.0,满足包要求的 >=22.19.0。 真实 CLI 返回 0.82.1。随后把扩展放入专用的 PI_CODING_AGENT_DIR,启动 RPC 且禁用 Session 持久化。

第一次请求 get_commands,Runtime 返回:

name: audit
source: extension
path: .../extensions/context-audit.ts

第二次通过 RPC 发送 /audit,扩展实际返回通知:

project trusted: true
context: 0/204800
context usage: 0.0%
active tools: bash, context_audit, edit, read, write

这证明了真实加载器接受该 TypeScript Extension、Command 已注册并执行、Tool 已进入活动工具面, 且非 TUI 模式没有因状态栏逻辑崩溃。整个路径不需要调用模型。

本地真实 Runtime RPC 捕获

六级证据阶梯

5. TUI 与模型路径

RPC 证据不能证明 Footer 在真实终端的视觉更新,也不能证明模型会在合适时机自主调用 Tool。 要升级这两项,需要分别记录:TUI 启动、命令补全、事件期间 Status 变化。以及真实 Provider 下 Tool Definition、Tool Call、Result 和 Agent 后续处理。

本次没有这些证据,因此保持 UNKNOWN,而不是从 RPC 推导通过。

6. 生产安全

能运行也不等于安全。Extension 与 Pi 进程拥有相同的宿主权限。Project Trust 控制项目资源 是否被加载,不是 OS Sandbox。System Prompt Preview 还可能暴露项目规则、Skill 描述、路径 或内部服务名。

生产级验收至少还需要输出脱敏、日志策略、依赖与来源固定、敏感目录权限、非交互默认行为、 错误与取消路径,以及独立隔离边界。完整威胁模型属于 PI-12,本文不借一个审计 Demo 宣称解决。

六、事件选择错误会让“观察”冒充“阻断”

观察事件与执行前阻断的边界

tool_execution_start 与 tool_execution_end 适合观察实际执行阶段。如果目标是阻止危险 Tool,应该在 tool_call 阶段判断并返回 block。等到 execution end 再记录“危险命令已执行”, 只获得审计记录,没有获得安全门禁。

字符串匹配同样不是完整权限模型。只拦截 rm -rf,无法识别 Python、脚本包装或等价系统 调用。Extension Gate 可以是纵深防御的一层,不能替代最小宿主权限、容器或虚拟机隔离。

七、把验收写成可复跑合同

一份可迁移的 Extension 验收表可以写成:

source_contract:
  pi_version: 0.82.1
  public_exports_only: true
typecheck:
  official_package_types: passed
registration:
  tool: context_audit
  command: audit
runtime_rpc:
  extension_loaded: passed
  command_discovered: passed
  command_executed: passed
  active_tool_visible: passed
tui:
  status_transition: unknown
model_path:
  autonomous_tool_call: unknown
security:
  production_isolation: unknown

这份 YAML 会阻止一句“全部通过”覆盖不同层级的事实。升级 Pi、调整 Loader 或增加 UI 组件时,只重跑受影响的层。没有被新证据覆盖的未知项继续保留。

八、失败时如何定位,而不是直接重写代码

按失败层定位而不是重写整个 Extension

如果类型检查失败,先看公开类型与目标版本,不要用 as any 把合同抹掉。如果注册面通过、 真实 Runtime 找不到命令,检查 Extension 来源、Trust、显式 --extension 与发现范围。如果 RPC 可以执行但 TUI 崩溃,优先检查 ctx.mode、ctx.hasUI 和自定义组件能力。

如果 Tool 已出现在 Active Tools 中但模型不调用,也不要立刻改 Loader。问题可能在 Tool 描述、任务触发、模型工具能力或竞争 Tool。分层证据让故障域可定位,也避免每次失败都重写 整个 Extension。

九、最终能确认什么

截至 2026-08-11,这个 Context Audit Extension 已完成三项实质升级:真实官方类型检查、 固定 Pi Runtime 加载、RPC 下的命令发现与无模型执行。返回结果也证明 context_audit 已进入 活动工具集合。

仍不能确认的是:TUI Footer 的真实视觉变化、模型自主 Tool Call、长会话全部事件顺序、 Extension Reload 边界以及生产安全性。

把“代码存在”“接口一致”“真实加载”和“生产可靠”分开后,系统才能判断下一份证据应补在 TUI、模型 Tool Call、Reload,还是生产隔离层。

参考资料