现在位置: 首页 > Pi Agent > 正文

Pi Agent 事件系统

事件系统是扩展的核心机制。

通过监听生命周期事件,你可以在 AI 工作的各个阶段插入自定义逻辑。


生命周期全景

以下是 Pi Agent 从启动到退出过程中触发的所有事件:

Pi Agent 扩展事件生命周期:启动、项目信任、会话启动、turn 循环与会话关闭

每个 turn 内部,context、provider、tool 三类事件按序触发,有工具调用时循环多轮。

虚线部分表示该阶段可被多个事件多次触发。

input、before_agent_start、resources_discover、agent_settled 等其余事件的触发时机见下方全事件速查表。


核心事件详解

这一节挑选使用频率最高的 5 个事件,说明它们的触发时机和返回值约定。

input — 输入拦截

在用户输入被处理之前触发,可以转换、拦截或直接处理输入:

实例

// 文件路径:~/.pi/agent/extensions/quick-input.ts
// 以下片段位于扩展工厂函数体内
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" };  // 默认:正常处理
});
action 值效果
"continue"不改变输入,继续正常处理流程
"transform"修改输入后继续处理,多个 handler 的转换会链式叠加
"handled"完全接管,跳过 LLM 调用(第一个返回此值的 handler 生效)

两种典型写法的实际效果如下。

输入 ?quick hello world 时,实际发给 LLM 的文本已被改写:

简短回复:hello world

输入 ping 时不会调用 LLM,终端里只弹出一条通知:

pong

input 事件在 Skill 和模板展开之前触发,所以你看到的是原始输入。

/skill:foo 和 /template 此时尚未展开。


tool_call — 工具调用拦截

在工具执行之前触发,可以修改参数或阻止执行:

实例

// 文件路径:~/.pi/agent/extensions/guard-bash.ts
// import 写在模块顶层,下面的 pi.on 片段位于扩展工厂函数体内
// isToolCallEventType 是类型守卫,需从官方包导入
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 }
    const dangerous = ["rm -rf /", "mkfs.", "dd if=", "> /dev/sda"];
    // 先判断是否危险,再决定是否放行
    if (dangerous.some(cmd =>
        event.input.command.includes(cmd))) {
      return { block: true, reason: "危险命令已被阻止" };
    }
    // 判断通过后才追加环境变量加载
    event.input.command = `source ~/.profile\n${event.input.command}`;
  }

  // 记录所有文件读取操作
  if (isToolCallEventType("read", event)) {
    // event.input 是 { path: string; offset?: number; limit?: number }
    ctx.ui.notify(`读取文件: ${event.input.path}`, "info");
  }
});

若 AI 试图执行 rm -rf /,工具不会运行,终端中会显示阻断原因:

危险命令已被阻止

event.input 是可变的——你修改它会直接影响工具执行。


tool_result — 修改工具结果

在工具执行完成后触发,可以修改返回给 LLM 的结果:

实例

// 文件路径:~/.pi/agent/extensions/read-linenum.ts
// 以下片段位于扩展工厂函数体内
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 }],
    };
  }
});

原本两行的文件内容,交给 LLM 时会变成带行号的形式:

1: import { readFile } from "node:fs/promises";
2: export async function load() {

context — 修改 LLM 上下文

在每次 LLM 调用之前触发,可以过滤或修改消息:

实例

// 文件路径:~/.pi/agent/extensions/context-filter.ts
// 以下片段位于扩展工厂函数体内
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 };
});

before_agent_start — 注入消息

在 AI 开始处理用户输入之前触发,可以注入额外消息或修改系统提示词:

实例

// 文件路径:~/.pi/agent/extensions/context-inject.ts
// 以下片段位于扩展工厂函数体内
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请在所有回复中使用中文。",
  };
});

全事件速查表

上一节的 ASCII 图里还出现了不少事件,这里按触发顺序集中列出,方便查阅。

事件触发时机是否可修改是否可阻断
project_trust加载项目资源之前,确认是否信任当前项目是(拒绝则不加载项目扩展)
session_start会话启动或恢复时
resources_discoversession_start 之后触发(startup/reload 时)是(仅可追加 Skill、提示词模板、主题路径,不能增删扩展)
input用户输入被处理之前是(可转换输入)是(handled 接管)
before_agent_startAI 开始处理用户输入之前是(可注入消息、改系统提示词)
agent_startagent 循环开始时
message_start / message_update消息生成期间(message_update 为助手流式增量)
message_end用户、助手、toolResult 消息定稿时是(可返回 { message } 替换最终消息,role 必须一致)
turn_start每个轮次开始时
context每次 LLM 调用之前是(可过滤消息)
before_provider_headers发送 HTTP 请求头之前是(可增改请求头)
before_provider_request请求体发给模型之前是(返回非 undefined 值是替换整个请求 payload,而非中止请求)
after_provider_response收到 HTTP 响应之后,流式 body 尚未消费否(仅可读取 status/headers 做观测)
tool_execution_start工具开始执行时
tool_call工具执行之前是(event.input 可变)是(block: true)
tool_execution_update工具执行中产生进度时
tool_result工具执行完成后是(可改返回内容)
tool_execution_end工具执行结束时
turn_end每个轮次结束时
agent_endagent 循环结束时
agent_settled一轮交互完全平息之后
session_before_switch/new 或 /resume 执行之前是(返回 { cancel: true })
session_before_fork/fork 或 /clone 执行之前是(返回 { cancel: true })
session_before_compact压缩执行之前(手动 /compact、阈值、上下文溢出)是(可自定义压缩摘要)是(返回 { cancel: true })
session_shutdown会话销毁前触发,退出、/new、/resume、/fork、/reload 都会触发(reason 为 quit/reload/new/resume/fork)

事件处理要点

写事件 handler 时,下面这几点决定了代码是否健壮。

方法作用
多个扩展监听同一事件按扩展加载顺序依次执行,前一个的返回值会影响后续 handler
返回 { block: true, reason: "..." }阻止该操作,reason 会展示给用户和 AI
返回 { cancel: true }取消会话切换等操作
使用 ctx.signal在异步操作中响应取消信号,用户按 Escape 时中止
使用 ctx.ui 系列方法与用户交互,如 notify、confirm、select 等
ctx.hasUI 守卫对话框调用Print/JSON/RPC 模式下对话框类方法不可用或为空操作,先判断再调用
ctx.mode === "tui" 守卫 TUI 专属 APIcustom()、组件工厂、终端输入等只在交互模式有效

在扩展工厂函数中不要启动后台资源(进程、socket、定时器等)。

将资源启动延迟到 session_start 事件中,并在 session_shutdown 中清理。