先分清五种能力
06 最重要的是边界感。它们都可以叫“扩展”,但进入系统的位置完全不同。
skillSKILL.md / /skill:name<skill> block/template-namepi.registerCommand()pi.registerTool()_toolRegistry -> agent.state.toolspi.on(event, handler)resources_discover06 讲的是扩展边界,不是 agent 核心主循环。先能分清这些入口,再看 loader 和 runner 的代码会轻很多。
Skill 是按需指导文档
Skill 没有 execute,也没有参数 schema。它不是 tool,而是模型在需要时读取的操作说明。
发现位置
~/.pi/agent/skills.pi/skills- settings / CLI 传入的 skill paths
Skill 对象
name和descriptionfilePath和baseDirsourceInfodisableModelInvocation
这是渐进式披露。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 登记能力。
ResourceLoader
统一加载 skills、prompts、themes、extensions 路径。
Extension loader
发现入口、jiti 加载、执行 factory。
注册声明
收集 tool、command、hook、shortcut、flag。
ExtensionRunner
bind 到运行时,创建 ctx,派发 hooks。
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.cwdctx.modelctx.signalctx.sessionManagerctx.ui
emit
emitToolCall()emitToolResult()emitContext()emitBeforeProviderRequest()
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
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 扩展静态资源来源。