Pi Agent Ereignissystem

Das Ereignissystem ist der Kernmechanismus für Erweiterungen.

Durch das Abhören von Lebenszyklus-Ereignissen können Sie benutzerdefinierte Logik in verschiedenen Phasen der Arbeit der KI einfügen.


Lebenszyklus-Übersicht

Nachfolgend finden Sie alle Ereignisse, die während des Lebenszyklus von Pi Agent vom Start bis zum Beenden ausgelöst werden:

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

Innerhalb jedes Turns werden die context-, provider- und tool-Ereignisse nacheinander ausgelöst, wobei bei Tool-Aufrufen mehrere Runden durchlaufen werden.

Die gestrichelten Teile zeigen an, dass die Phase durch mehrere Ereignisse mehrmals ausgelöst werden kann.

Für den Auslösezeitpunkt anderer Ereignisse wie input, before_agent_start, resources_discover und agent_settled siehe die vollständige Ereignis-Kurzreferenz unten.


Kernereignisse erklärt

Dieser Abschnitt wählt die 5 am häufigsten verwendeten Ereignisse aus und erklärt ihren Auslösezeitpunkt und ihre Rückgabewert-Konventionen.

input — Eingabe-Abfangen

Wird ausgelöst, bevor die Benutzereingabe verarbeitet wird; du kannst die Eingabe umwandeln, abfangen oder direkt verarbeiten:

Beispiel

// Dateipfad: ~/.pi/agent/extensions/quick-input.ts
// Der folgende Ausschnitt befindet sich im Funktionskörper der Erweiterungs-Factory
pi.on("input", async (event, ctx) => {
  // event.text - roher Eingabetext
  // event.images - angehängte Bilder
  // event.source - "interactive" | "rpc" | "extension"

  // Eingabe umwandeln: Eine Anweisung an die Eingabe voranstellen
  if (event.text.startsWith("?quick ")) {
    return {
      action: "transform",
      text:`Kurze Antwort: ${event.text.slice(7)}`
    };
  }

  // Direkt behandeln: antworten, ohne das LLM zu durchlaufen
  if (event.text === "ping") {
    ctx.ui.notify("pong", "info");
    return { action: "handled" };
  }

  // Von der Erweiterung eingefügte Nachrichten überspringen
  if (event.source === "extension") {
    return { action: "continue" };
  }

  return { action: "continue" };  // Standard: normal verarbeiten
});
AktionswertWirkung
"continue"Ändert die Eingabe nicht; setzt den normalen Verarbeitungsablauf fort
"transform"Setzt die Verarbeitung nach Änderung der Eingabe fort; Transformationen mehrerer Handler werden verkettet
"handled"Übernimmt vollständig, überspringt den LLM-Aufruf (der erste Handler, der diesen Wert zurückgibt, wird wirksam)

Die tatsächlichen Auswirkungen zweier typischer Implementierungen sind wie folgt.

Eingabe?quick hello worldwenn , der tatsächlich an das LLM gesendete Text umgeschrieben wurde:

简短回复:hello world

Eingabepingwenn , das LLM nicht aufgerufen wird; nur eine Benachrichtigung erscheint im Terminal:

pong

Das Eingabeereignis wird vor der Skill- und Vorlagenerweiterung ausgelöstbevor, sodass du die Rohdaten siehst.

/skill:foo und /template sind an dieser Stelle noch nicht erweitert.


tool_call — Abfangen von Tool-Aufrufen

Wird ausgelöst, bevor das Tool ausgeführt wird; du kannst Argumente ändern oder die Ausführung blockieren:

Beispiel

// Dateipfad: ~/.pi/agent/extensions/guard-bash.ts
// import-Anweisungen stehen am Anfang des Moduls; der folgende pi.on-Ausschnitt befindet sich im Rumpf der Erweiterungs-Factory-Funktion
// isToolCallEventType ist ein Type Guard und muss aus dem offiziellen Paket importiert werden
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";

pi.on("tool_call", async (event, ctx) => {
  // Gefährliche Bash-Befehle blockieren
  if (isToolCallEventType("bash", event)) {
    // event.input ist { command: string; timeout?: number }
    const dangerous = ["rm -rf /", "mkfs.", "dd if=", "> /dev/sda"];
    // Zuerst prüfen, ob es gefährlich ist, dann entscheiden, ob es zugelassen werden soll
    if (dangerous.some(cmd =>
        event.input.command.includes(cmd))) {
      return { block: true, reason: "Gefährlicher Befehl wurde blockiert" };
    }
    // Laden von Umgebungsvariablen erst anhängen, nachdem die Prüfung bestanden wurde
    event.input.command = `source ~/.profile\n${event.input.command}`;
  }

  // Alle Dateileseoperationen protokollieren
  if (isToolCallEventType("read", event)) {
    // event.input ist { path: string; offset?: number; limit?: number }
    ctx.ui.notify(`Datei wird gelesen: ${event.input.path}`, "info");
  }
});

Wenn die KI versucht,rm -rf /auszuführen, wird das Tool nicht ausgeführt, und das Terminal zeigt den Blockierungsgrund an:

Dangerous command has been blocked.

event.inputist veränderbar — eine direkte Änderung wirkt sich auf die Tool-Ausführung aus.


tool_result — Tool-Ergebnisse ändern

Wird ausgelöst, nachdem das Tool die Ausführung beendet hat; Sie können das an das LLM zurückgegebene Ergebnis ändern:

Beispiel

// Dateipfad: ~/.pi/agent/extensions/read-linenum.ts
// Der folgende Ausschnitt befindet sich im Funktionskörper der Erweiterungs-Factory
pi.on("tool_result", async (event, ctx) => {
  // event.toolName - Tool-Name
  // event.content - an das LLM zurückgegebener Inhalt
  // event.details - Detaildaten
  // event.isError - ob ein Fehler vorliegt

  // Beispiel: Zeilennummern zu read-Ergebnissen hinzufügen
  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 }],
    };
  }
});

Der ursprüngliche zweizeilige Dateiinhalt wird in eine mit Zeilennummern versehene Form umgewandelt, wenn er an das LLM übergeben wird:

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

context — LLM-Kontext ändern

Wird vor jedem LLM-Aufruf ausgelöst; Sie können Nachrichten filtern oder ändern:

Beispiel

// Dateipfad: ~/.pi/agent/extensions/context-filter.ts
// Der folgende Ausschnitt befindet sich im Funktionskörper der Erweiterungs-Factory
pi.on("context", async (event, ctx) => {
  // event.messages - tiefe Kopie der aktuellen Kontextnachrichten; kann sicher geändert werden

  // Nachrichten eines bestimmten Typs herausfiltern
  const filtered = event.messages.filter(m => {
    // Nachrichten mit dem benutzerdefinierten Typ "debug" entfernen
    if (m.type === "custom" && m.customType === "debug") {
      return false;
    }
    return true;
  });

  return { messages: filtered };
});

before_agent_start — Nachrichten injizieren

Wird ausgelöst, bevor die KI mit der Verarbeitung der Benutzereingabe beginnt; du kannst zusätzliche Nachrichten injizieren oder den System-Prompt ändern:

Beispiel

// Dateipfad: ~/.pi/agent/extensions/context-inject.ts
// Der folgende Ausschnitt befindet sich im Funktionskörper der Erweiterungs-Factory
pi.on("before_agent_start", async (event, ctx) => {
  // event.prompt - der Prompt-Text des Benutzers
  // event.systemPrompt - der aktuell konfigurierte System-Prompt

  return {
    // Eine persistente Nachricht injizieren (in der Sitzung gespeichert und an das LLM gesendet)
    message: {
      customType: "my-context",
      content:`Aktuelle Zeit ist ${new Date().toISOString()}`,
      display: true,
    },
    // An den System-Prompt anhängen
    systemPrompt: event.systemPrompt
      + "\n\nBitte verwenden Sie in allen Antworten Chinesisch.",
  };
});

Vollständige Ereignis-Schnellreferenz

Einige Ereignisse erschienen auch im ASCII-Diagramm im vorherigen Abschnitt; hier sind sie zum leichteren Nachschlagen in der Reihenfolge ihrer Auslösung zusammengestellt.

EreignisAuslösezeitpunktÄnderbarBlockierbar
project_trustVor dem Laden von Projektressourcen; bestätigen, ob dem aktuellen Projekt vertraut werden sollneinJa (Projekterweiterungen werden bei Ablehnung nicht geladen)
session_startWenn die Sitzung startet oder fortgesetzt wirdneinnein
resources_discoverWird nach session_start ausgelöst (beim Start/Neuladen)Ja (kann nur Skills, Prompt-Vorlagen und Theme-Pfade anhängen; kann keine Erweiterungen hinzufügen oder entfernen)nein
inputBevor Benutzereingaben verarbeitet werdenJa (kann Eingaben transformieren)Ja (handled übernimmt)
before_agent_startBevor die KI beginnt, Benutzereingaben zu verarbeitenJa (kann Nachrichten einfügen, System-Prompt ändern)nein
agent_startWenn die Agent-Schleife startetneinnein
message_start / message_updateWährend der Nachrichtengenerierung (message_update ist das Streaming-Delta des Assistenten)neinnein
message_endWenn user-, assistant- und toolResult-Nachrichten finalisiert werdenJa (kann { message } zurückgeben, um die finale Nachricht zu ersetzen; role muss übereinstimmen)nein
turn_startZu Beginn jedes Turnsneinnein
contextVor jedem LLM-AufrufJa (kann Nachrichten filtern)nein
before_provider_headersVor dem Senden der HTTP-Request-HeaderJa (kann Request-Header hinzufügen oder ändern)nein
before_provider_requestBevor der Request-Body an das Modell gesendet wirdJa (die Rückgabe eines nicht-undefined-Werts ersetzt die gesamte Request-Payload, anstatt die Anfrage abzubrechen)nein
after_provider_responseNach dem Empfang der HTTP-Antwort, bevor der Streaming-Body konsumiert wirdNein (kann nur Status/Header zur Beobachtung lesen)nein
tool_execution_startWenn das Tool mit der Ausführung beginntneinnein
tool_callBevor das Tool ausgeführt wirdJa (event.input ist änderbar)Ja (block: true)
tool_execution_updateWenn während der Tool-Ausführung Fortschritt generiert wirdneinnein
tool_resultNach Abschluss der Tool-AusführungJa (Rückgabeinhalt kann geändert werden)nein
tool_execution_endWenn die Tool-Ausführung endetneinnein
turn_endAm Ende jeder Rundeneinnein
agent_endWenn die Agent-Schleife endetneinnein
agent_settledNachdem eine Interaktionsrunde vollständig abgeschlossen istneinnein
session_before_switchBevor /new oder /resume ausgeführt wirdneinJa (return { cancel: true })
session_before_forkBevor /fork oder /clone ausgeführt wirdneinJa (return { cancel: true })
session_before_compactBevor die Komprimierung ausgeführt wird (manuelles /compact, Schwellenwert, Kontextüberlauf)Ja (anpassbare Komprimierungszusammenfassung)Ja (return { cancel: true })
session_shutdownWird vor der Zerstörung der Sitzung ausgelöst; quit, /new, /resume, /fork, /reload lösen es alle aus (reason ist quit/reload/new/resume/fork)neinnein

Wichtige Punkte zur Ereignisbehandlung

Beim Schreiben von Event-Handlern entscheiden die folgenden Punkte, ob der Code robust ist.

MethodeZweck
Mehrere Erweiterungen lauschen auf dasselbe EreignisWird in der Reihenfolge der Erweiterungsladung ausgeführt; der vorherige Rückgabewert beeinflusst nachfolgende Handler.
Gib zurück{ block: true, reason: "..." }Blockiere die Operation; der Grund wird dem Benutzer und der KI angezeigt
Gib zurück{ cancel: true }Brich Operationen wie Sitzungswechsel ab
Verwendectx.signalReagiere auf Abbruchsignale in asynchronen Operationen; brich ab, wenn der Benutzer Escape drückt
Verwendectx.uiReihe von MethodenInteragiere mit dem Benutzer, z. B. notify, confirm, select usw.
Verwendectx.hasUIzum Absichern von DialogaufrufenIn Print/JSON/RPC-Modi sind dialogartige Methoden nicht verfügbar oder No-Ops; vor dem Aufruf prüfen
Verwendectx.mode === "tui"zum Absichern TUI-spezifischer APIscustom(), Komponentenfabriken, Terminaleingabe usw. sind nur im interaktiven Modus gültig

Starten Sie keine Hintergrundressourcen (Prozesse, Sockets, Timer usw.) in der Extension-Factory-Funktion.

Verzögern Sie den Start von Ressourcen bis zumsession_startEreignis und räumen Sie insession_shutdown auf.

Weitere Erweiterungen