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:
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
// 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
});
| Aktionswert | Wirkung |
|---|---|
| "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
// 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
// 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
// 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
// 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.
| Ereignis | Auslösezeitpunkt | Änderbar | Blockierbar |
|---|---|---|---|
| project_trust | Vor dem Laden von Projektressourcen; bestätigen, ob dem aktuellen Projekt vertraut werden soll | nein | Ja (Projekterweiterungen werden bei Ablehnung nicht geladen) |
| session_start | Wenn die Sitzung startet oder fortgesetzt wird | nein | nein |
| resources_discover | Wird 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 |
| input | Bevor Benutzereingaben verarbeitet werden | Ja (kann Eingaben transformieren) | Ja (handled übernimmt) |
| before_agent_start | Bevor die KI beginnt, Benutzereingaben zu verarbeiten | Ja (kann Nachrichten einfügen, System-Prompt ändern) | nein |
| agent_start | Wenn die Agent-Schleife startet | nein | nein |
| message_start / message_update | Während der Nachrichtengenerierung (message_update ist das Streaming-Delta des Assistenten) | nein | nein |
| message_end | Wenn user-, assistant- und toolResult-Nachrichten finalisiert werden | Ja (kann { message } zurückgeben, um die finale Nachricht zu ersetzen; role muss übereinstimmen) | nein |
| turn_start | Zu Beginn jedes Turns | nein | nein |
| context | Vor jedem LLM-Aufruf | Ja (kann Nachrichten filtern) | nein |
| before_provider_headers | Vor dem Senden der HTTP-Request-Header | Ja (kann Request-Header hinzufügen oder ändern) | nein |
| before_provider_request | Bevor der Request-Body an das Modell gesendet wird | Ja (die Rückgabe eines nicht-undefined-Werts ersetzt die gesamte Request-Payload, anstatt die Anfrage abzubrechen) | nein |
| after_provider_response | Nach dem Empfang der HTTP-Antwort, bevor der Streaming-Body konsumiert wird | Nein (kann nur Status/Header zur Beobachtung lesen) | nein |
| tool_execution_start | Wenn das Tool mit der Ausführung beginnt | nein | nein |
| tool_call | Bevor das Tool ausgeführt wird | Ja (event.input ist änderbar) | Ja (block: true) |
| tool_execution_update | Wenn während der Tool-Ausführung Fortschritt generiert wird | nein | nein |
| tool_result | Nach Abschluss der Tool-Ausführung | Ja (Rückgabeinhalt kann geändert werden) | nein |
| tool_execution_end | Wenn die Tool-Ausführung endet | nein | nein |
| turn_end | Am Ende jeder Runde | nein | nein |
| agent_end | Wenn die Agent-Schleife endet | nein | nein |
| agent_settled | Nachdem eine Interaktionsrunde vollständig abgeschlossen ist | nein | nein |
| session_before_switch | Bevor /new oder /resume ausgeführt wird | nein | Ja (return { cancel: true }) |
| session_before_fork | Bevor /fork oder /clone ausgeführt wird | nein | Ja (return { cancel: true }) |
| session_before_compact | Bevor die Komprimierung ausgeführt wird (manuelles /compact, Schwellenwert, Kontextüberlauf) | Ja (anpassbare Komprimierungszusammenfassung) | Ja (return { cancel: true }) |
| session_shutdown | Wird 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) | nein | nein |
Wichtige Punkte zur Ereignisbehandlung
Beim Schreiben von Event-Handlern entscheiden die folgenden Punkte, ob der Code robust ist.
| Methode | Zweck |
|---|---|
| Mehrere Erweiterungen lauschen auf dasselbe Ereignis | Wird 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.signal | Reagiere auf Abbruchsignale in asynchronen Operationen; brich ab, wenn der Benutzer Escape drückt |
| Verwendectx.uiReihe von Methoden | Interagiere mit dem Benutzer, z. B. notify, confirm, select usw. |
| Verwendectx.hasUIzum Absichern von Dialogaufrufen | In 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 APIs | custom(), Komponentenfabriken, Terminaleingabe usw. sind nur im interaktiven Modus gültig |
Weitere ErweiterungenStarten 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.