06 Pi Agent Study
Advanced · 扩展系统
06 / Advanced extension boundary

把能力装进 Pi 的几种方式

这一节是进阶章节。第一遍只想理解 agent loop 的读者可以先跳过;回来时重点看 Pi 的可扩展边界:skill 是按需指导文档,command 是本地控制入口,tool 是模型可调用动作,hook 是生命周期插槽,resource 是扩展包贡献的静态资源路径。

route resource -> runtime
01

ResourceLoader

加载 skills、prompts、extensions。

02

Extension loader

执行 factory,收集注册声明。

03

ExtensionRunner

bind 运行时动作,派发 hooks。

04

AgentSession

刷新工具、处理命令、重建 prompt。

05

Agent loop

模型请求和工具执行中触发 hook。

先分清五种能力

06 最重要的是边界感。它们都可以叫“扩展”,但进入系统的位置完全不同。

能力
本质
入口
最后进入哪里
skill
教模型怎么做
SKILL.md / /skill:name
system prompt 目录,或 user message 的 <skill> block
prompt template
prompt 文本模板
/template-name
user message
extension command
用户直接触发的本地控制入口
pi.registerCommand()
立即执行 command handler
extension tool
模型可以调用的本地动作
pi.registerTool()
_toolRegistry -> agent.state.tools
extension hook
插入生命周期的拦截点
pi.on(event, handler)
agent / session / provider 流程中的事件点
resource
扩展包贡献的静态资源路径
resources_discover
ResourceLoader 继续加载 skills/prompts/themes
本章定位

06 讲的是扩展边界,不是 agent 核心主循环。先能分清这些入口,再看 loader 和 runner 的代码会轻很多。

Skill 是按需指导文档

Skill 没有 execute,也没有参数 schema。它不是 tool,而是模型在需要时读取的操作说明。

发现位置

  • ~/.pi/agent/skills
  • .pi/skills
  • settings / CLI 传入的 skill paths

Skill 对象

  • namedescription
  • filePathbaseDir
  • sourceInfo
  • disableModelInvocation
为什么不把所有 SKILL.md 正文塞进 system prompt

这是渐进式披露。Pi 先暴露 skill 目录,让模型知道有哪些能力;命中任务时再 read 具体文件,避免大量 skill 正文占用上下文并污染提示。

/skill:name 是 prompt 展开

它看起来像 slash command,但不执行本地代码,只把 skill 正文展开成一段用户消息。

AgentSession.prompt()
  -> extension command 有机会先接管
  -> input hook 可拦截或改写
  -> _expandSkillCommand()
  -> expandPromptTemplate()
  -> agent.prompt()

输入

/skill:docs-sync 更新 README

模型看到

<skill name="docs-sync" location="/.../SKILL.md">
References are relative to /.../docs-sync.

这里是 SKILL.md 正文
</skill>

更新 README

Extension loader 收集声明

Loader 的工作不是直接启动 agent 行为,而是执行 extension factory,让它通过 pi API 登记能力。

1

ResourceLoader

统一加载 skills、prompts、themes、extensions 路径。

2

Extension loader

发现入口、jiti 加载、执行 factory。

3

注册声明

收集 tool、command、hook、shortcut、flag。

4

ExtensionRunner

bind 到运行时,创建 ctx,派发 hooks。

5

AgentSession

刷新工具、处理命令、重建 prompt,最终接入 agent loop。

加载期的边界

registerTool() 是登记 metadata,可以在 load 阶段做;sendMessage() 是运行时动作,必须等 runner bind 后才能调用。

Runner 把声明接到运行时

ExtensionRunner 是扩展系统的胶水:bind 动作、创建 ctx、解析命令、派发 hook。

bindCore

  • sendMessage -> sendCustomMessage()
  • appendEntry -> sessionManager.appendCustomEntry()
  • setActiveTools -> setActiveToolsByName()
  • compact -> compact()

createContext

  • ctx.cwd
  • ctx.model
  • ctx.signal
  • ctx.sessionManager
  • ctx.ui

emit

  • emitToolCall()
  • emitToolResult()
  • emitContext()
  • emitBeforeProviderRequest()
为什么 ctx 用 getter + stale 检查

ctx 是运行时入口,不是静态配置。getter 保证每次拿最新状态;stale 检查防止 reload、fork、switch 后旧 ctx 继续误操作。

Extension tool 进入 agent.state.tools

extension 注册的是产品层 ToolDefinition,不是 agent core 的 AgentTool

pi.registerTool(toolDefinition)
-> extension.tools.set(name, { definition, sourceInfo })
-> runner.getAllRegisteredTools()
-> AgentSession._refreshToolRegistry()
-> wrapRegisteredTools()
-> _toolRegistry
-> setActiveToolsByName()
-> agent.state.tools

为什么要 refresh

  • 重算 definition registry。
  • 重算 wrapped AgentTool registry。
  • 应用 allowed/excluded tool 过滤。
  • 更新 sourceInfo、promptSnippet、promptGuidelines。
  • 重建 system prompt。

不要直接 push

直接改 agent.state.tools 会绕过 metadata、prompt、ctxFactory、过滤和 active tool 去重逻辑。

Hook 插进模型和工具循环

Runner 只负责派发 hook,真正插入 agent loop 的是 AgentSession 和 Agent config。

工具调用

prepareToolCallArguments()
validateToolArguments()
beforeToolCall()
tool.execute()
afterToolCall()
createToolResultMessage()

模型请求

transformContext(messages)
convertToLlm(messages)
build Context
streamFn(...)
before_provider_request
参数校验在 tool_call hook 前

hook 通常做权限、策略、审计和拦截,需要可信的结构化参数。让 hook 自己处理畸形参数会让职责混乱。

resources_discover 贡献路径

extension 可以告诉 Pi:我的包里还有额外 skills、prompts、themes,请让 ResourceLoader 统一加载。

session_start / reload
-> runner.emitResourcesDiscover(cwd, reason)
-> collect skillPaths / promptPaths / themePaths
-> resourceLoader.extendResources(...)
-> updateSkillsFromPaths()
-> _rebuildSystemPrompt()
为什么返回路径,不返回对象

ResourceLoader 统一负责读取、校验、去重、sourceInfo、diagnostics、ignore 规则和 system prompt 重建。直接返回对象会绕过这条资源管线。

常见误解和自测

如果下面几条能答清楚,06 的边界就基本过关了。

常见误解

  • skill 不是 tool;它没有参数 schema,也不会 execute。
  • extension command 不是 prompt template;它会先于 prompt 展开被本地 handler 接管。
  • registerTool 不是把工具直接 push 进 agent.state.tools;还要 refresh、wrap、过滤、重建 system prompt。
  • resources_discover 不返回资源对象,只返回路径给 ResourceLoader。

自测题

  • 为什么 skill 不是 tool?
  • extension command 为什么必须先于 skill/template 展开?
  • extension tool 经过哪几步进入 agent.state.tools
  • hook 可以观察、改写或 block 哪些阶段?
  • 为什么资源发现要回到 ResourceLoader?

最后记住一句话

Pi 的扩展系统不是一个点,而是一组进入 agent 的入口。

Skill / template

改 prompt 文本,影响模型如何理解任务。

Command / tool

command 是本地立即执行;tool 是给模型调用,进入 agent loop。

Hook / resources

hook 插入生命周期;resources_discover 扩展静态资源来源。