阶段二 · 状态与边界

事件驱动与扩展 —— 事件即契约

Pi 最核心的设计是 "事件是契约":Agent 每走一步都发出类型化事件,前端(TUI / JSON / RPC / 自定义)都消费同一个事件流,不直接读取核心内部状态。扩展则通过事件观察甚至改写行为。这一章讲清楚事件系统怎么工作、两条监听通道的区别,以及扩展生态长什么样。

学习目标

  • 理解"事件是契约":核心发事件、前端渲染,两者解耦。
  • 分清两条监听通道:session.subscribe(只读观察)与扩展的 pi.on(可拦截改写)。
  • 理解扩展能注册什么、生命周期事件有哪些,以及事件名的易错点。

一、问题:前端怎么知道 Agent 在干什么

Agent 在跑循环:调模型、执行工具、回填结果。TUI 要实时显示进度,RPC 要转发事件,日志要记录每一步——这些"观察者"怎么看到 Agent 内部?

最笨的办法:每个前端都去读 Agent 的内部状态。但那样前端就和核心耦合死了。Pi 的答案是事件流:核心每走一步发一个事件,观察者订阅事件。核心不知道也不关心谁在听——它只负责把"发生了什么"发出去。

事件按生命周期分组:

分组 事件 含义
Agent 级 agent_start / agent_end 一次完整运行的起止
Turn 级 turn_start / turn_end 一轮模型调用 + 工具批
消息级 message_start / message_update / message_end 消息的开始、流式更新、结束
工具级 tool_execution_start / tool_execution_update / tool_execution_end 工具执行的开始、进度、结束

四个运行模式(TUI/Print/JSON/RPC)都是这些事件的消费者——这就是"核心可移植"的机制保证:前端怎么变,核心不用改

二、两条监听通道的分水岭

Pi 有两套并行的监听机制,共享同一批事件源,但"Agent 等不等你的 listener"是分水岭:

维度 session.subscribe 扩展的 pi.on
用途 只读观察:日志、UI、统计 拦截与改写:权限、输入改写
Agent 是否等待 不等(fire-and-forget) 等(await 处理完再继续)
适用方 SDK 宿主程序 TypeScript 扩展

只学一套一定会遇到"代码写了却静默不生效":在 subscribe 里想拦截工具调用是做不到的——subscribe 是只读的,Agent 不等你;要拦截必须用 pi.on,Agent 会等你处理完再继续。这是 Pi 事件系统最容易被误解的点。

三、扩展 = 能力按需注入

扩展可以注册工具、命令、快捷键、UI 组件、Provider,以及监听生命周期事件。高级能力即工具:子代理、MCP、计划模式都是通过扩展用工具实现的,Pi 核心刻意不内置(见 01 章)。

一个真实扩展就是默认导出函数接收 ExtensionAPI——比如 hello.tspi.registerTool(helloTool) 注册工具、用 pi.on("session_shutdown", ...) 在会话关闭时清理资源。

事件名易错点

  • 会话关闭事件是 session_shutdown,不是 session_end
  • 没有 settings_change 事件。
  • 工具输入改写是在事件对象上原地进行,不存在返回 modifiedInput 的协议。
  • 压缩前定制用 session_before_compact

当前 Pi 行为

  • 扩展事件覆盖:会话生命周期(session_start/session_shutdown)、上下文注入(context)、Provider 请求改写(before_provider_request 等)、Agent 循环观察、用户操作观察(model_select/user_bash)。
  • 事件名与 payload 类型以固定基线的 extensions.mdsrc/core/extensions/types.ts 为准,不要从旧示例复制。
  • Agent 事件(核心发给前端的流)与扩展事件(扩展注册的钩子)是两套清单:前者只读观察,后者可返回值改变行为。agent_starttool_execution_end 属于前者,before_agent_start 属于后者。
  • before_agent_start 返回 { systemPrompt } 可替换本轮系统提示词——这是改写提示词的通用手段,也是 04b 章 里"关掉 cwd 那一行"的唯一途径。

源码证据表

教学结论 Pi 路径 / 符号 说明
Agent 事件判别联合 packages/agent/src/types.ts(433–446) agent_starttool_execution_end
事件消费入口 session.subscribe 只读,无返回值
扩展钩子清单 packages/coding-agent/src/core/extensions/types.ts 返回值即介入手段
提示词改写钩子 before_agent_start 返回 { systemPrompt } 链式覆盖
扩展工厂签名 ExtensionFactory (pi) => void,函数体内 pi.on 注册
关闭事件 session_shutdown 没有 settings_change(基线核实)

失败与边界实验

事件系统的三个经典故障,tests/test_07_extension_events 各有对应用例:

  1. 订阅者抛异常。 一个前端崩溃不能拖垮 Agent:事件总线对每个 handler 的异常做隔离,失败只属于那个订阅者。教学模型的 LessonEventBus 逐个调用订阅者并捕获异常——顺序继续,不中断广播。
  2. 钩子返回值被忽略。 before_agent_start 返回 { systemPrompt }约定:返回别的结构等于没改。类型系统在这里是唯一的防线,运行时不会报错——这也是"事件即契约"的代价:契约靠类型与文档维持,不靠运行时强校验。
  3. 未知事件类型。 新版本加了新事件,旧消费者必须能忽略它而不是崩溃。判别联合 + default 分支是 TypeScript 侧的纪律;JSON 输出侧则要求消费者按 type 字段分发并对未知值宽容。
python3 -m learn_pi_lab lab events
python3 -m unittest tests.test_07_extension_events -v

在 Pi 里怎么操作

  • 安装:pi install npm:包名pi install git:github.com/user/repo;查看 pi list;移除 pi remove;单次 pi -e

社区扩展参考

Pi 生态有 5500+ 包,生态地图见 awesome-pi。高 star 代表(star 以调研时为准):

  • 子代理:pi-subagents(3255★,官方异步子代理)、@tintinweb/pi-subagents(943★)
  • 记忆:pi-hermes-memory(366★,持久记忆 + 会话搜索 + 密钥扫描)
  • 手机连接:pi-telegram(281★,Telegram DM 桥)、pi-web(593★,手机浏览器监督)
  • Web 访问:pi-web-access(1190★,多 Provider 降级链)
  • 上下文:pi-context-prune(214★,工具调用树剪枝)
  • 精读推荐:pi-llama(HuggingFace 官方,单文件演示 registerProvider)、narumiruna/pi-extensions(27 包 monorepo,沉淀扩展工程约定)

国内渠道:飞书有 pi-feishu 系列(官方 Bot API + WebSocket 长连接,无需公网 IP);微信无(无官方 Bot API,个人号自动化有封号风险);钉钉仅雏形。star 不代表质量,安装前先读源码确认权限与网络行为。

Python 实验

extension_events.py 是课程自己的事件总线,演示按注册顺序派发、异常隔离(普通 Exception 变成数据)、幂等退订与深度快照:

bus = LessonEventBus()
bus.on("turn", lambda payload: "first")              # 返回 Subscription
outcomes = bus.emit("turn", {"lesson": "events"})    # 每个 handler 一个 EventOutcome

它不是 Pi ExtensionAPI 的 Python 绑定——Pi 扩展是 TypeScript 代码。Tau 的 tau_agent/harness.pysubscribe(listener) 返回退订函数,语义与本课程一致。

python3 -m learn_pi_lab lab events

这一模块的核心代码在核心代码导览 · 事件总线有逐段解读。

验证方式

python3 -m unittest tests.test_07_extension_events -v

边界与安全

  • 事件名、payload、返回约定以固定基线(0.85.1)为准,不要从旧 README 复制。
  • 扩展应验证所有外部输入、限制可执行操作,把异常变成明确的用户反馈或日志。
  • 社区项目随 Pi 版本演进;本课程的 Python 实验不绑定、不依赖任何 Pi 扩展。

回顾

  • 事件是契约:核心发事件、前端消费,核心不知道谁在听。
  • 两条通道subscribe 只读不等待;pi.on 可拦截会等待——分水岭是"Agent 等不等你"。
  • 高级能力即工具:子代理、MCP 都是工具,扩展按需注入。
  • 事件名有易错点session_shutdown 不是 session_end

窗口会满、对话会太长——下一章上下文压缩讲如何在有限窗口里装下无限对话。