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

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" };  // 默认:正常处理
});
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}`);
  }
});

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 }],
    };
  }
});

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 };
});

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请在所有回复中使用中文。",
  };
});

事件处理要点

  • 多个扩展可以同时监听同一事件,按加载顺序依次执行
  • 返回 { block: true, reason: "..." } 可以阻止操作
  • 返回 { cancel: true } 可以取消会话切换等操作
  • 使用 ctx.signal 可以在异步操作中响应取消信号
  • 使用 ctx.ui 系列方法与用户交互

在扩展工厂函数中不要启动后台资源(进程、socket、定时器等)。将资源启动延迟到 session_start 事件中,并在 session_shutdown 中清理。