Pi Agent 事件系统
事件系统是扩展的核心机制。通过监听生命周期事件,你可以在 AI 工作的各个阶段插入自定义逻辑。
生命周期全景
以下是 Pi Agent 从启动到退出过程中触发的所有事件:
pi 启动
│
├─► project_trust(在加载项目资源之前,仅用户/全局和 CLI 扩展可参与)
├─► session_start { reason: "startup" }
└─► resources_discover { reason: "startup" }
│
▼
用户发送提示 ──────────────────────────┐
│ │
├─► input(可拦截、转换或处理) │
├─► before_agent_start(可注入消息) │
├─► agent_start │
│ │
│ ┌─── 每个轮次 (turn) ──────┐ │
│ │ │ │
│ ├─► turn_start │ │
│ ├─► context(可修改消息) │ │
│ ├─► before_provider_headers │ │
│ ├─► before_provider_request │ │
│ ├─► after_provider_response │ │
│ │ │ │
│ │ LLM 响应,可能调用工具: │ │
│ │ ├─► tool_execution_start │
│ │ ├─► tool_call(可阻止) │
│ │ ├─► tool_execution_update │
│ │ ├─► tool_result(可修改) │
│ │ └─► tool_execution_end │
│ │ │ │
│ └─► turn_end │ │
│ │
├─► agent_end │
└─► agent_settled │
│
用户发送另一条提示 ◄────────────────────┘
退出 (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM)
└─► session_shutdown
核心事件详解
input — 输入拦截
在用户输入被处理之前触发,可以转换、拦截或直接处理输入:
实例
pi.on("input", async (event, ctx) => {
// event.text - 原始输入文本
// event.images - 附带的图片
// event.source - "interactive" | "rpc" | "extension"
// 转换输入:在输入前添加指令
if (event.text.startsWith("?quick ")) {
return {
action: "transform",
text: `简短回复:${event.text.slice(7)}`
};
}
// 直接处理:不经过 LLM 直接响应
if (event.text === "ping") {
ctx.ui.notify("pong", "info");
return { action: "handled" };
}
// 跳过扩展注入的消息
if (event.source === "extension") {
return { action: "continue" };
}
return { action: "continue" }; // 默认:正常处理
});
// event.text - 原始输入文本
// event.images - 附带的图片
// event.source - "interactive" | "rpc" | "extension"
// 转换输入:在输入前添加指令
if (event.text.startsWith("?quick ")) {
return {
action: "transform",
text: `简短回复:${event.text.slice(7)}`
};
}
// 直接处理:不经过 LLM 直接响应
if (event.text === "ping") {
ctx.ui.notify("pong", "info");
return { action: "handled" };
}
// 跳过扩展注入的消息
if (event.source === "extension") {
return { action: "continue" };
}
return { action: "continue" }; // 默认:正常处理
});
| action 值 | 效果 |
|---|---|
| "continue" | 不改变输入,继续正常处理流程 |
| "transform" | 修改输入后继续处理,多个 handler 的转换会链式叠加 |
| "handled" | 完全接管,跳过 LLM 调用(第一个返回此值的 handler 生效) |
input 事件在 Skill 和模板展开之前触发,所以你看到的是原始输入。/skill:foo 和 /template 此时尚未展开。
tool_call — 工具调用拦截
在工具执行之前触发,可以修改参数或阻止执行:
实例
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
pi.on("tool_call", async (event, ctx) => {
// 拦截危险 bash 命令
if (isToolCallEventType("bash", event)) {
// event.input 是 { command: string; timeout?: number }
// 可以修改 command
event.input.command = `source ~/.profile\n${event.input.command}`;
const dangerous = ["rm -rf /", "mkfs.", "dd if=", "> /dev/sda"];
if (dangerous.some(cmd =>
event.input.command.includes(cmd))) {
return { block: true, reason: "危险命令已被阻止" };
}
}
// 记录所有文件读取操作
if (isToolCallEventType("read", event)) {
// event.input 是 { path: string; offset?: number; limit?: number }
console.log(`读取文件: ${event.input.path}`);
}
});
pi.on("tool_call", async (event, ctx) => {
// 拦截危险 bash 命令
if (isToolCallEventType("bash", event)) {
// event.input 是 { command: string; timeout?: number }
// 可以修改 command
event.input.command = `source ~/.profile\n${event.input.command}`;
const dangerous = ["rm -rf /", "mkfs.", "dd if=", "> /dev/sda"];
if (dangerous.some(cmd =>
event.input.command.includes(cmd))) {
return { block: true, reason: "危险命令已被阻止" };
}
}
// 记录所有文件读取操作
if (isToolCallEventType("read", event)) {
// event.input 是 { path: string; offset?: number; limit?: number }
console.log(`读取文件: ${event.input.path}`);
}
});
event.input 是可变的——你修改它会直接影响工具执行。
tool_result — 修改工具结果
在工具执行完成后触发,可以修改返回给 LLM 的结果:
实例
pi.on("tool_result", async (event, ctx) => {
// event.toolName - 工具名
// event.content - 返回给 LLM 的内容
// event.details - 详情数据
// event.isError - 是否为错误
// 例如:对 read 结果添加行号
if (event.toolName === "read" && !event.isError) {
const lines = event.content
.filter(c => c.type === "text")
.map(c => c.text)
.join("\n")
.split("\n")
.map((line, i) => `${i + 1}: ${line}`)
.join("\n");
return {
content: [{ type: "text", text: lines }],
};
}
});
// event.toolName - 工具名
// event.content - 返回给 LLM 的内容
// event.details - 详情数据
// event.isError - 是否为错误
// 例如:对 read 结果添加行号
if (event.toolName === "read" && !event.isError) {
const lines = event.content
.filter(c => c.type === "text")
.map(c => c.text)
.join("\n")
.split("\n")
.map((line, i) => `${i + 1}: ${line}`)
.join("\n");
return {
content: [{ type: "text", text: lines }],
};
}
});
context — 修改 LLM 上下文
在每次 LLM 调用之前触发,可以过滤或修改消息:
实例
pi.on("context", async (event, ctx) => {
// event.messages - 当前上下文消息的深拷贝,可安全修改
// 过滤掉特定类型的消息
const filtered = event.messages.filter(m => {
// 移除自定义类型为 "debug" 的消息
if (m.type === "custom" && m.customType === "debug") {
return false;
}
return true;
});
return { messages: filtered };
});
// event.messages - 当前上下文消息的深拷贝,可安全修改
// 过滤掉特定类型的消息
const filtered = event.messages.filter(m => {
// 移除自定义类型为 "debug" 的消息
if (m.type === "custom" && m.customType === "debug") {
return false;
}
return true;
});
return { messages: filtered };
});
before_agent_start — 注入消息
在 AI 开始处理用户输入之前触发,可以注入额外消息或修改系统提示词:
实例
pi.on("before_agent_start", async (event, ctx) => {
// event.prompt - 用户的提示文本
// event.systemPrompt - 当前配置的系统提示词
return {
// 注入一条持久化消息(存入会话,发送给 LLM)
message: {
customType: "my-context",
content: `当前时间是 ${new Date().toISOString()}`,
display: true,
},
// 追加到系统提示词
systemPrompt: event.systemPrompt
+ "\n\n请在所有回复中使用中文。",
};
});
// event.prompt - 用户的提示文本
// event.systemPrompt - 当前配置的系统提示词
return {
// 注入一条持久化消息(存入会话,发送给 LLM)
message: {
customType: "my-context",
content: `当前时间是 ${new Date().toISOString()}`,
display: true,
},
// 追加到系统提示词
systemPrompt: event.systemPrompt
+ "\n\n请在所有回复中使用中文。",
};
});
事件处理要点
- 多个扩展可以同时监听同一事件,按加载顺序依次执行
- 返回 { block: true, reason: "..." } 可以阻止操作
- 返回 { cancel: true } 可以取消会话切换等操作
- 使用 ctx.signal 可以在异步操作中响应取消信号
- 使用 ctx.ui 系列方法与用户交互
在扩展工厂函数中不要启动后台资源(进程、socket、定时器等)。将资源启动延迟到 session_start 事件中,并在 session_shutdown 中清理。
