工具让 Agent 能做事,扩展让用户能改变 Agent 的行为,资源加载则决定启动时有哪些工具、技能、提示模板和上下文文件进入运行时。
这三者放在一起理解,会比逐个看文件更清楚:工具是能力,扩展是插槽,ResourceLoader 是装配线。
| 机制 | 核心职责 | 什么时候运行 |
|---|---|---|
| Tool | 被模型调用,执行本地副作用或查询 | Agent Loop 发现 toolCall 后 |
| Extension | 监听事件、注册工具/命令、拦截输入和工具 | 会话生命周期、模型请求、工具执行、UI 交互中 |
| ResourceLoader | 发现扩展、技能、prompt templates、context files | 创建 session 或 reload 时 |
flowchart TB Settings["settings / cwd / agentDir"] --> Loader["DefaultResourceLoader"] Loader --> Extensions["Extensions"] Loader --> Skills["Skills"] Loader --> Templates["Prompt Templates"] Loader --> ContextFiles["AGENTS.md / CLAUDE.md"] Extensions --> Runner["ExtensionRunner"] Runner --> Tools["Registered Tools"] Skills --> Prompt["System Prompt"] ContextFiles --> Prompt Templates --> PromptInput["prompt() preflight"] Tools --> Loop["Agent Loop"]
Pi 的工具大致有两类:
| 来源 | 例子 | 特点 |
|---|---|---|
| 内置工具 | read、bash、edit、write、grep、find、ls |
coding agent 的基础能力 |
| 扩展工具 | 用户通过 pi.registerTool() 注册 |
可以接外部服务、公司内部系统或自定义工作流 |
工具定义不仅是给模型看的“说明”,也是运行时的安全边界:
type ToolDefinition = {
name: string;
description: string;
parameters: unknown;
execute(input: unknown): Promise<unknown>;
};
真正执行前,Pi 会找工具、处理参数兼容、校验 schema,并触发工具事件。任何一步失败,都应该变成 tool result,而不是让进程直接崩溃。
官方扩展文档把扩展定义为 TypeScript 模块,可以订阅生命周期事件、注册工具、增加命令、自定义 UI 和持久化状态。理解它时,可以把事件看成一排检查点:
flowchart LR A["resources_discover"] --> B["session_start"] B --> C["input"] C --> D["before_agent_start"] D --> E["before_provider_request"] E --> F["message_update / message_end"] F --> G["tool_call"] G --> H["tool_result"] H --> I["turn_end / agent_end"] I --> J["session_before_compact / session_before_tree"]
不同扩展只需要挂在自己关心的位置:
| 需求 | 更适合的扩展点 |
|---|---|
禁止写 .env |
tool_call |
| 给每次运行自动加上下文 | before_agent_start |
| 改写 provider 请求 | before_provider_request |
| 自定义压缩摘要格式 | session_before_compact |
加一个 /deploy 命令 |
registerCommand() |
| 保存扩展自己的状态 | appendEntry() |
一个常见冲动是把所有产品需求都写进 loop:权限审批、Git 快照、远程执行、公司内部工具、UI 渲染。这样短期快,长期会让 loop 无法测试。
Pi 把这些需求放到扩展系统里,核心 loop 只保留最稳定的控制流:
flowchart LR A["稳定核心: 请求模型/执行工具/回写结果"] --> B["事件"] B --> C["扩展订阅"] C --> D["改变输入、工具、输出或持久化"]
这和浏览器事件、Express 中间件、数据库 hook 的思路类似:核心做少一点,插槽设计清楚一点。
| 机制 | 本质 | 典型用途 | 是否执行代码 |
|---|---|---|---|
| Extension | TypeScript 运行时代码 | 工具、命令、事件拦截、UI | 是 |
| Skill | 按需加载的说明书和资产 | 可复用工作流、操作步骤、脚本说明 | 可包含脚本,但由 Agent 决定调用 |
| Prompt Template | 可复用用户提示 | 常用任务模板 | 否 |
| Context File | 项目长期规则 | 编码规范、运行命令、仓库约定 | 否 |
初学者最容易混用这几个东西。判断方法很简单:
| 文件 | 看什么 |
|---|---|
packages/coding-agent/src/core/resource-loader.ts |
资源如何从 cwd、agentDir、settings 进入运行时 |
packages/coding-agent/src/core/extensions/types.ts |
扩展 API、事件、工具定义和上下文类型 |
packages/coding-agent/src/core/extensions/runner.ts |
扩展事件如何分发和收集结果 |
packages/coding-agent/src/core/tools/index.ts |
内置工具如何组装 |
packages/coding-agent/src/core/tools/tool-definition-wrapper.ts |
工具定义如何适配 Agent 内核 |
packages/coding-agent/src/core/skills.ts |
Skills 如何发现和描述 |
packages/coding-agent/src/core/prompt-templates.ts |
Prompt template 如何展开 |
读这部分时,建议先看“注册”和“触发”两条线:
| 线索 | 你要找的问题 |
|---|---|
| 注册线 | 工具、命令、事件 handler 是什么时候加入 runtime 的? |
| 触发线 | 用户输入、模型请求、工具执行、压缩时分别触发哪些事件? |
教学版没有实现完整扩展系统,但保留了三个关键思想:
| Pi | 教学版 |
|---|---|
registerTool() |
ToolRegistry.register() |
tool_call / tool_result 事件 |
tool_start / tool_end 事件时间线 |
| ResourceLoader 动态发现 | 静态注册 list_files、read_file、write_note |
这已经足够让读者理解:模型不能直接做副作用,所有能力都要通过受控工具暴露。
在教学版里加一个非常小的 hook:执行 write_note 前,如果文件名不是 .md 结尾,就返回错误 tool result。这个练习能帮你体会为什么生产级 Agent 需要 tool_call 拦截点。