/reload 热重载。你要让 md-tools 扩展能正确初始化、保存状态、方便调试。这是从「能跑」到「好用」的跨越。生命周期:事件按什么顺序跑
官方给了一张完整的事件顺序图。关键主线:
pi 启动
└─► project_trust
└─► session_start
└─► resources_discover
用户发提示词
├─► (扩展命令先检查)
├─► input(可拦截)
├─► before_agent_start(可注入/改系统提示词)
├─► agent_start
├─► 每个回合: turn_start → context → tool_call → tool_result → turn_end
└─► agent_end → agent_settled
退出 → session_shutdown
两个容易混的点:
agent_start/agent_end:一次底层 agent 运行。但 Pi 可能还会自动重试、自动压缩后重试、处理排队的跟进消息。要看「确定不会再自动继续」,用agent_settled。session_start在会话「启动/加载/重载」时触发,带reason(startup / reload / new / resume / fork)。
会话切换:session_shutdown → session_start
/new、/resume、/fork、/clone 会触发会话替换,生命周期是:
- 先给旧实例发
session_shutdown(清理资源、保存状态) - 为新会话重载并重绑扩展
- 再发
session_start(reason 为 new / resume / fork)
官方给的原则:在 session_shutdown 里做清理,在 session_start 里重建内存状态。对 md-tools,如果你维护了「最近处理的文件」这类状态,就在这两个事件里分别释放和重建。
状态管理:存到哪、怎么恢复
有状态的扩展(比如带 todo 的)需要注意分支支持。官方给了一个清晰模式——把状态存进工具结果的 details,在 session_start 里从会话历史重建:
export default function (pi: ExtensionAPI) {
let items: string[] = [];
pi.on("session_start", async (_event, ctx) => {
items = [];
for (const entry of ctx.sessionManager.getBranch()) {
if (entry.type === "message" && entry.message.role === "toolResult") {
if (entry.message.toolName === "my_tool") {
items = entry.message.details?.items ?? [];
}
}
}
});
pi.registerTool({
name: "my_tool",
async execute(toolCallId, params, signal, onUpdate, ctx) {
items.push("new item");
return {
content: [{ type: "text", text: "Added" }],
details: { items: [...items] }, // 存起来供重建
};
},
});
}
关键认知:内存里的变量(items)不会被持久化,靠的是把状态写进 details 再从会话里读回来。 这样分支切换时状态才能跟上。
/reload 热重载:改代码不用重启
官方给了 ctx.reload(),行为等同 /reload:发出 session_shutdown、重载资源、再发 session_start(reason "reload")。
注意几个要点:
await ctx.reload()之后,当前命令处理器仍在旧的调用栈里继续跑await ctx.reload()之后的代码,不能假设旧的扩展内存状态还有效- 为了可预期,把它当「终点」:
await ctx.reload(); return;
所以调试扩展时,常用 ctx.reload() 让改动的代码生效,而不用整个重启 Pi。注意自动发现目录里的扩展支持热重载。
一处实用进阶:在 tool_call 里加安全拦截
前面 M04-1 提过权限门,这里给 md-tools 一个具体例子——拦截危险命令:
pi.on("tool_call", async (event, ctx) => {
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
const ok = await ctx.ui.confirm("危险!", "允许执行 rm -rf 吗?");
if (!ok) return { block: true, reason: "被用户拦截" };
}
});
event.input 是可变的,能就地修改参数;返回 { block: true, reason } 可阻塞。这正是给 rename 这类不可逆操作加「确认层」的好位置。
落地练习:让 md-tools 扩展有状态且可热重载
- 给
md_tools工具加一个「最近处理文件」状态,存进details,在session_start里重建 - 用
ctx.reload()或/reload验证改动能热重载生效 - 加一个
tool_call拦截,让rename走确认流程
怎么判断做对了?——重启/切分支后状态还在;改代码
/reload后新逻辑生效;rename会先弹确认。
卡住了怎么办? 状态丢 → 确认存进了 details 且 session_start 正确重建。reload 无效 → 确认扩展在自动发现目录。拦截没触发 → 检查 event.toolName 和 event.input.command 的取值。
常见坑:在 tool_call 里看「同一条消息的兄弟工具结果」
官方特别提醒:默认并行执行模式下,同一条助手消息里的兄弟工具调用,先顺序预检、再并发执行。tool_call 不保证能在 ctx.sessionManager 里看到同一条消息的兄弟工具结果。
简单说:别在 tool_call 里假设「这个助手消息里其他工具已经跑完了」。有这种依赖,就要重新设计,别在事件里赌执行顺序。
小结
- 事件顺序:session_start → input → before_agent_start → 各 turn → session_shutdown
agent_settled才表示「确定不会自动继续」- 会话切换:session_shutdown 清理、session_start 重建状态
- 状态存
details,从会话历史恢复,内存变量不持久 ctx.reload()热重载,别在之后依赖旧内存状态
下一节进入 M05 贯穿项目——把 md-tools 完整封装成 Skill。