Extensions
pi kann Erweiterungen erstellen. Bitten Sie es, eines für Ihren Anwendungsfall zu erstellen.
Extensions sind TypeScript Module, die das Verhalten von Pi erweitern. Sie können Lebenszyklusereignisse abonnieren, vom LLM aufrufbare benutzerdefinierte Tools registrieren, Befehle hinzufügen und vieles mehr.
Platzierung für /reload: Fügen Sie Erweiterungen für die automatische Erkennung in
~/.pi/agent/extensions/(global) oder.pi/extensions/(projektlokal) ein. Verwenden Siepi -e./path.tsnur für Schnelltests. Extensions an automatisch erkannten Orten kann mit/reloadim laufenden Betrieb neu geladen werden.
Hauptfunktionen:
- Benutzerdefinierte Tools – Registrieren Sie Tools, die das LLM über
pi.registerTool()aufrufen kann - Ereignisabfang – Toolaufrufe blockieren oder ändern, Kontext einfügen, Komprimierung anpassen
- Benutzerinteraktion – Benutzer über
ctx.uiauffordern (auswählen, bestätigen, eingeben, benachrichtigen) - Benutzerdefinierte UI-Komponenten – Vollständige TUI-Komponenten mit Tastatureingabe über
ctx.ui.custom()für komplexe Interaktionen - Benutzerdefinierte Befehle – Registrieren Sie Befehle wie
/mycommandüberpi.registerCommand() - Sitzungspersistenz – Speicherstatus, der Neustarts über
pi.appendEntry()übersteht - Benutzerdefiniertes Rendering – Steuern Sie, wie Toolaufrufe/Ergebnisse und Meldungen in TUI angezeigt werden.
Beispielhafte Anwendungsfälle:
- Berechtigungstore (Bestätigung vor
rm -rf,sudousw.) - Git Checkpointing (in jeder Runde verstauen, bei Zweig wiederherstellen)
- Pfadschutz (Schreibvorgänge auf
.env,node_modules/blockieren) - Benutzerdefinierte Komprimierung (Konversation nach Ihren Wünschen zusammenfassen)
- Gesprächszusammenfassungen (siehe Beispiel
summarize.ts) - Interaktive Tools (Fragen, Assistenten, benutzerdefinierte Dialoge)
- Zustandsbehaftete Tools (Todo-Listen, Verbindungspools)
- Externe Integrationen (File Watcher, Webhooks, CI-Trigger)
- Spiele während du wartest (siehe Beispiel
snake.ts)
Siehe examples/extensions/ für funktionierende Implementierungen.
Inhaltsverzeichnis
- Quick Start
- Extension Locations
- Available Imports
- Writing an Extension
- Events
- ExtensionContext
- ExtensionCommandContext
- ExtensionAPI Methods
- State Management
- Custom Tools
- Custom UI
- Error Handling
- Mode Behavior
- Examples Reference
Schnellstart
Erstellen Sie ~/.pi/agent/extensions/my-extension.ts:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
// React to events
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify("Extension loaded!", "info");
});
pi.on("tool_call", async (event, ctx) => {
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
if (!ok) return { block: true, reason: "Blocked by user" };
}
});
// Register a custom tool
pi.registerTool({
name: "greet",
label: "Greet",
description: "Greet someone by name",
parameters: Type.Object({
name: Type.String({ description: "Name to greet" }),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
return {
content: [{ type: "text", text: `Hello, ${params.name}!` }],
details: {},
};
},
});
// Register a command
pi.registerCommand("hello", {
description: "Say hello",
handler: async (args, ctx) => {
ctx.ui.notify(`Hello ${args || "world"}!`, "info");
},
});
}Test mit --extension (oder -e) Flag:
pi -e ./my-extension.tsErweiterungsstandorte
Sicherheit: Extensions wird mit Ihren vollständigen Systemberechtigungen ausgeführt und kann beliebigen Code ausführen. Installieren Sie nur von Quellen, denen Sie vertrauen.
Extensions werden von vertrauenswürdigen Standorten automatisch erkannt. Projektlokale .pi/extensions-Einträge werden erst geladen, nachdem das Projekt vertrauenswürdig ist.
| Standort | Umfang |
|---|---|
~/.pi/agent/extensions/*.ts |
Global (alle Projekte) |
~/.pi/agent/extensions/*/index.ts |
Global (Unterverzeichnis) |
.pi/extensions/*.ts |
Projektlokal |
.pi/extensions/*/index.ts |
Projektlokal (Unterverzeichnis) |
Zusätzliche Pfade über settings.json:
{
"packages": [
"npm:@foo/bar@1.0.0",
"git:github.com/user/repo@v1"
],
"extensions": [
"/path/to/local/extension.ts",
"/path/to/local/extension/dir"
]
}Informationen zum Teilen von Erweiterungen über npm oder Git als Pi-Pakete finden Sie unter packages.md.
Verfügbare Importe
| Paket | Zweck |
|---|---|
@earendil-works/pi-coding-agent |
Erweiterungstypen (ExtensionAPI, ExtensionContext, Ereignisse) |
typebox |
Schemadefinitionen für Werkzeugparameter |
@earendil-works/pi-ai |
KI-Dienstprogramme (StringEnum für Google-kompatible Aufzählungen) |
@earendil-works/pi-tui |
TUI Komponenten für benutzerdefiniertes Rendering |
npm Abhängigkeiten funktionieren auch. Fügen Sie ein package.json neben Ihrer Erweiterung (oder in einem übergeordneten Verzeichnis) hinzu, führen Sie npm install aus und Importe aus node_modules/ werden automatisch aufgelöst.
Für verteilte Pi-Pakete, die mit pi install (npm oder Git) installiert wurden, müssen sich die Laufzeitdeps in dependencies befinden. Bei der Paketinstallation werden standardmäßig Produktionsinstallationen (npm install --omit=dev) verwendet, sodass devDependencies zur Laufzeit nicht verfügbar sind. Wenn npmCommand konfiguriert ist, verwenden Git-Pakete einfaches install für die Kompatibilität mit Wrappern.
Node.js integrierte Funktionen (node:fs, node:path usw.) sind ebenfalls verfügbar.
Eine Erweiterung schreiben
Eine Erweiterung exportiert eine Standard-Factory-Funktion, die ExtensionAPI empfängt. Die Factory kann synchron oder asynchron sein:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
// Subscribe to events
pi.on("event_name", async (event, ctx) => {
// ctx.ui for user interaction
const ok = await ctx.ui.confirm("Title", "Are you sure?");
ctx.ui.notify("Done!", "info");
ctx.ui.setStatus("my-ext", "Processing..."); // Footer status
ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]); // Widget above editor (default)
});
// Register tools, commands, shortcuts, flags
pi.registerTool({ ... });
pi.registerCommand("name", { ... });
pi.registerShortcut("ctrl+x", { ... });
pi.registerFlag("my-flag", { ... });
}Extensions werden über jiti geladen, daher funktioniert TypeScript ohne Kompilierung.
Wenn die Fabrik eine Promise zurückgibt, wartet Pi darauf, bevor es mit dem Start fortfährt. Das bedeutet, dass die asynchrone Initialisierung vor session_start, vor resources_discover und bevor über pi.registerProvider() in die Warteschlange gestellte Anbieterregistrierungen gelöscht werden.
Asynchrone Factory-Funktionen
Verwenden Sie eine asynchrone Factory für einmalige Startarbeiten wie das Abrufen der Remote-Konfiguration oder das dynamische Erkennen verfügbarer Modelle.
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default async function (pi: ExtensionAPI) {
const response = await fetch("http://localhost:1234/v1/models");
const payload = (await response.json()) as {
data: Array<{
id: string;
name?: string;
context_window?: number;
max_tokens?: number;
}>;
};
pi.registerProvider("local-openai", {
baseUrl: "http://localhost:1234/v1",
apiKey: "$LOCAL_OPENAI_API_KEY",
api: "openai-completions",
models: payload.data.map((model) => ({
id: model.id,
name: model.name ?? model.id,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: model.context_window ?? 128000,
maxTokens: model.max_tokens ?? 4096,
})),
});
}Dieses Muster stellt die abgerufenen Modelle während des normalen Startvorgangs und für pi --list-models zur Verfügung.
Langlebige Ressourcen und Herunterfahren
Erweiterungsfabriken können in Aufrufen ausgeführt werden, die nie eine Sitzung starten. Starten Sie keine Hintergrundressourcen wie Prozesse, Sockets, Dateibeobachter oder Timer ab Werk.
Verschieben Sie den Start der Hintergrundressource bis session_start oder den Befehl/das Tool/das Ereignis, das die Ressource benötigt. Registrieren Sie einen idempotenten session_shutdown-Handler, um alle von Ihnen gestarteten sitzungsbezogenen Ressourcen zu schließen.
Erweiterungsstile
Einzelne Datei – am einfachsten, für kleine Erweiterungen:
~/.pi/agent/extensions/
└── my-extension.tsVerzeichnis mit index.ts – für Erweiterungen mit mehreren Dateien:
~/.pi/agent/extensions/
└── my-extension/
├── index.ts # Entry point (exports default function)
├── tools.ts # Helper module
└── utils.ts # Helper modulePaket mit Abhängigkeiten – für Erweiterungen, die npm Pakete benötigen:
~/.pi/agent/extensions/
└── my-extension/
├── package.json # Declares dependencies and entry points
├── package-lock.json
├── node_modules/ # After npm install
└── src/
└── index.ts// package.json
{
"name": "my-extension",
"dependencies": {
"zod": "^3.0.0",
"chalk": "^5.0.0"
},
"pi": {
"extensions": ["./src/index.ts"]
}
}Führen Sie npm install im Erweiterungsverzeichnis aus, dann funktionieren Importe aus node_modules/ automatisch.
Veranstaltungen
Lebenszyklusübersicht
pi starts
│
├─► project_trust (user/global and CLI extensions only, before project resources load)
├─► session_start { reason: "startup" }
└─► resources_discover { reason: "startup" }
│
▼
user sends prompt ─────────────────────────────────────────┐
│ │
├─► (extension commands checked first, bypass if found) │
├─► input (can intercept, transform, or handle) │
├─► (skill/template expansion if not handled) │
├─► before_agent_start (can inject message, modify system prompt)
├─► agent_start │
├─► message_start / message_update / message_end │
│ │
│ ┌─── turn (repeats while LLM calls tools) ───┐ │
│ │ │ │
│ ├─► turn_start │ │
│ ├─► context (can modify messages) │ │
│ ├─► before_provider_headers (can mutate headers) |
│ ├─► before_provider_request (can inspect or replace payload)
│ ├─► after_provider_response (status + headers, before stream consume)
│ │ │ │
│ │ LLM responds, may call tools: │ │
│ │ ├─► tool_execution_start │ │
│ │ ├─► tool_call (can block) │ │
│ │ ├─► tool_execution_update │ │
│ │ ├─► tool_result (can modify) │ │
│ │ └─► tool_execution_end │ │
│ │ │ │
│ └─► turn_end │ │
│ │
├─► agent_end │
└─► agent_settled (no retry/compaction/follow-up left) │
│
user sends another prompt ◄────────────────────────────────┘
/new (new session) or /resume (switch session)
├─► session_before_switch (can cancel)
├─► session_shutdown
├─► session_start { reason: "new" | "resume", previousSessionFile? }
└─► resources_discover { reason: "startup" }
/fork or /clone
├─► session_before_fork (can cancel)
├─► session_shutdown
├─► session_start { reason: "fork", previousSessionFile }
└─► resources_discover { reason: "startup" }
/name or pi.setSessionName()
└─► session_info_changed
/compact or auto-compaction
├─► session_before_compact (can cancel or customize)
└─► session_compact
/tree navigation
├─► session_before_tree (can cancel or customize)
└─► session_tree
/model or Ctrl+P (model selection/cycling)
├─► thinking_level_select (if model change changes/clamps thinking level)
└─► model_select
thinking level changes (settings, keybinding, pi.setThinkingLevel())
└─► thinking_level_select
exit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM)
└─► session_shutdownStartup-Events
project_trust
Wird ausgelöst, bevor pi entscheidet, ob einem Projekt dynamische Konfigurationen (.pi oder .agents/skills) vertraut werden sollen. Es wird während des Startvorgangs ausgeführt und wenn beim Sitzungsaustausch (z. B. /resume) ein CWD eingegeben wird, dessen Vertrauen im aktuellen Prozess nicht aufgelöst wurde. Es nehmen nur Benutzer-/globale Erweiterungen und CLI -e Erweiterungen teil; Projektlokale Erweiterungen werden erst geladen, nachdem die Vertrauensstellung aufgelöst wurde.
pi.on("project_trust", async (event, ctx) => {
// event.cwd - current working directory
// ctx has a limited trust context: cwd, mode, hasUI, and select/confirm/input/notify UI helpers
if (await ctx.ui.confirm("Trust project?", event.cwd)) {
return { trusted: "yes", remember: true };
}
return { trusted: "undecided" };
});Ein project_trust-Handler muss { trusted: "yes" | "no" | "undecided" } zurückgeben. Eine Benutzer-/Global- oder CLI-Erweiterung, die "yes" oder "no" zurückgibt, besitzt die Entscheidung; Die erste Ja/Nein-Entscheidung gewinnt und unterdrückt die integrierte Vertrauensaufforderung. Verwenden Sie remember: true, um eine Ja/Nein-Entscheidung beizubehalten; andernfalls gilt es nur für den aktuellen Prozess. Geben Sie "undecided" zurück, damit spätere Handler oder der integrierte Vertrauensfluss entscheiden können. Überprüfen Sie ctx.hasUI, bevor Sie dazu aufgefordert werden. Wenn kein Handler „Ja/Nein“ zurückgibt, wird die normale Vertrauensauflösung fortgesetzt: Gespeicherte trust.json-Entscheidungen gelten zuerst, dann defaultProjectTrust steuert, ob pi standardmäßig fragt, vertraut oder ablehnt.
Ressourcenereignisse
resources_discover
Wird nach session_start ausgelöst, damit Erweiterungen zusätzliche Fähigkeiten, Eingabeaufforderungen und Themenpfade beitragen können.
Der Startpfad verwendet reason: "startup". Beim Neuladen wird reason: "reload" verwendet.
pi.on("resources_discover", async (event, _ctx) => {
// event.cwd - current working directory
// event.reason - "startup" | "reload"
return {
skillPaths: ["/path/to/skills"],
promptPaths: ["/path/to/prompts"],
themePaths: ["/path/to/themes"],
};
});Sitzungsereignisse
Siehe Session Format für interne Informationen zum Sitzungsspeicher und zum SessionManager API.
session_start
Wird ausgelöst, wenn eine Sitzung gestartet, geladen oder neu geladen wird.
pi.on("session_start", async (event, ctx) => {
// event.reason - "startup" | "reload" | "new" | "resume" | "fork"
// event.previousSessionFile - present for "new", "resume", and "fork"
ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? "ephemeral"}`, "info");
});session_info_changed
Wird ausgelöst, wenn der Anzeigename der aktuellen Sitzung über /name, RPC oder pi.setSessionName() festgelegt wird.
pi.on("session_info_changed", async (event, ctx) => {
// event.name - current normalized name, or undefined if cleared
ctx.ui.notify(`Session renamed: ${event.name ?? "(none)"}`, "info");
});session_before_switch
Wird vor dem Starten einer neuen Sitzung (/new) oder dem Wechseln der Sitzung (/resume) ausgelöst.
pi.on("session_before_switch", async (event, ctx) => {
// event.reason - "new" or "resume"
// event.targetSessionFile - session we're switching to (only for "resume")
if (event.reason === "new") {
const ok = await ctx.ui.confirm("Clear?", "Delete all messages?");
if (!ok) return { cancel: true };
}
});Nach einem erfolgreichen Wechsel oder einer neuen Sitzungsaktion gibt pi session_shutdown für die alte Erweiterungsinstanz aus, lädt Erweiterungen für die neue Sitzung neu und bindet sie neu und gibt dann session_start mit reason: "new" | "resume" und previousSessionFile aus.
Führen Sie Bereinigungsarbeiten in session_shutdown durch und stellen Sie dann alle In-Memory-Zustände in session_start wieder her.
session_before_fork
Wird beim Forken über /fork oder Klonen über /clone ausgelöst.
pi.on("session_before_fork", async (event, ctx) => {
// event.entryId - ID of the selected entry
// event.position - "before" for /fork, "at" for /clone
return { cancel: true }; // Cancel fork/clone
// OR
return { skipConversationRestore: true }; // Reserved for future conversation restore control
});Nach einem erfolgreichen Fork oder Klon gibt Pi session_shutdown für die alte Erweiterungsinstanz aus, lädt und bindet Erweiterungen für die neue Sitzung neu und gibt dann session_start mit reason: "fork" und previousSessionFile aus.
Führen Sie Bereinigungsarbeiten in session_shutdown durch und stellen Sie dann alle In-Memory-Zustände in session_start wieder her.
session_before_compact / session_compact
Auf Verdichtung abgefeuert. Weitere Informationen finden Sie unter compaction.md.
pi.on("session_before_compact", async (event, ctx) => {
const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;
// reason - "manual" (/compact), "threshold", or "overflow"
// willRetry - whether the aborted turn is retried after compaction (overflow recovery)
// Cancel:
return { cancel: true };
// Custom summary:
return {
compaction: {
summary: "...",
firstKeptEntryId: preparation.firstKeptEntryId,
tokensBefore: preparation.tokensBefore,
// usage: summaryResponse.usage, // Optional; included in session totals
}
};
});
pi.on("session_compact", async (event, ctx) => {
// event.compactionEntry - the saved compaction
// event.fromExtension - whether extension provided it
// event.reason - "manual" (/compact), "threshold", or "overflow"
// event.willRetry - whether the aborted turn is retried after compaction (overflow recovery)
});session_before_tree / session_tree
Ausgelöst bei /tree Navigation. Siehe Sessions für Baumnavigationskonzepte.
pi.on("session_before_tree", async (event, ctx) => {
const { preparation, signal } = event;
return { cancel: true };
// OR provide custom summary:
return {
summary: {
summary: "...",
// usage: summaryResponse.usage, // Optional; included in session totals
details: {},
},
};
});
pi.on("session_tree", async (event, ctx) => {
// event.newLeafId, oldLeafId, summaryEntry, fromExtension
});session_shutdown
Wird ausgelöst, bevor eine gestartete Sitzungslaufzeit abgebrochen wird. Verwenden Sie dies, um Ressourcen zu bereinigen, die von session_start oder anderen sitzungsbezogenen Hooks geöffnet wurden.
pi.on("session_shutdown", async (event, ctx) => {
// event.reason - "quit" | "reload" | "new" | "resume" | "fork"
// event.targetSessionFile - destination session for session replacement flows
// Cleanup, save state, etc.
});Agentenereignisse
before_agent_start
Wird ausgelöst, nachdem der Benutzer eine Eingabeaufforderung übermittelt hat, vor der Agentenschleife. Kann eine Nachricht einfügen und/oder die Systemaufforderung ändern.
pi.on("before_agent_start", async (event, ctx) => {
// event.prompt - user's prompt text
// event.images - attached images (if any)
// event.systemPrompt - current chained system prompt for this handler
// (includes changes from earlier before_agent_start handlers)
// event.systemPromptOptions - structured options used to build the system prompt
// .customPrompt - any custom system prompt (from --system-prompt, SYSTEM.md, or custom templates)
// .selectedTools - tools currently active in the prompt
// .toolSnippets - one-line descriptions for each tool
// .promptGuidelines - custom guideline bullets
// .appendSystemPrompt - text from --append-system-prompt flags
// .cwd - working directory
// .contextFiles - AGENTS.md files and other loaded context files
// .skills - loaded skills
return {
// Inject a persistent message (stored in session, sent to LLM)
message: {
customType: "my-extension",
content: "Additional context for the LLM",
display: true,
},
// Replace the system prompt for this turn (chained across extensions)
systemPrompt: event.systemPrompt + "\n\nExtra instructions for this turn...",
};
});Das Feld systemPromptOptions gibt Erweiterungen Zugriff auf dieselben strukturierten Daten, die Pi zum Erstellen der Systemeingabeaufforderung verwendet. Auf diese Weise können Sie überprüfen, was Pi geladen hat – benutzerdefinierte Eingabeaufforderungen, Richtlinien, Tool-Snippets, context files, Fertigkeiten –, ohne Ressourcen erneut zu entdecken oder Flags erneut zu analysieren. Verwenden Sie es, wenn Ihre Erweiterung tiefgreifende, fundierte Änderungen an der Systemeingabeaufforderung unter Berücksichtigung der vom Benutzer bereitgestellten Konfiguration vornehmen muss.
Innerhalb von before_agent_start spiegeln event.systemPrompt und ctx.getSystemPrompt() beide die verkettete Systemaufforderung ab dem aktuellen Handler wider. Spätere before_agent_start-Handler können es immer noch erneut ändern.
agent_start / agent_end / agent_settled
agent_start wird ausgelöst, wenn die Ausführung eines Agenten auf niedriger Ebene beginnt. agent_end wird ausgelöst, wenn die Ausführung endet, aber Pi kann es dennoch automatisch wiederholen, automatisch komprimieren und erneut versuchen oder mit in der Warteschlange befindlichen Folgenachrichten fortfahren. Verwenden Sie agent_settled für Statusintegrationen, die wissen müssen, dass Pi nicht automatisch weiter ausgeführt wird.
pi.on("agent_start", async (_event, ctx) => {});
pi.on("agent_end", async (event, ctx) => {
// event.messages - messages from this low-level run
});
pi.on("agent_settled", async (_event, ctx) => {
// ctx.isIdle() is true here unless another extension started a new run.
});turn_start / turn_end
Wird für jede Runde abgefeuert (eine LLM-Antwort + Werkzeugaufrufe).
pi.on("turn_start", async (event, ctx) => {
// event.turnIndex, event.timestamp
});
pi.on("turn_end", async (event, ctx) => {
// event.turnIndex, event.message, event.toolResults
});message_start / message_update / message_end
Ausgelöst wegen Aktualisierungen des Nachrichtenlebenszyklus.
message_startundmessage_endwerden für Benutzer-, Assistenten- und ToolResult-Nachrichten ausgelöst.message_updatewird für Assistenten-Streaming-Updates ausgelöst.message_end-Handler können{ message }zurückgeben, um die endgültige Nachricht zu ersetzen. Der Ersatz muss gleich bleibenrole.
pi.on("message_start", async (event, ctx) => {
// event.message
});
pi.on("message_update", async (event, ctx) => {
// event.message
// event.assistantMessageEvent (token-by-token stream event)
});
pi.on("message_end", async (event, ctx) => {
if (event.message.role !== "assistant") return;
return {
message: {
...event.message,
usage: {
...event.message.usage,
cost: {
...event.message.usage.cost,
total: 0.123,
},
},
},
};
});tool_execution_start / tool_execution_update / tool_execution_end
Ausgelöst wegen Aktualisierungen des Tool-Ausführungslebenszyklus.
Im Parallelwerkzeugmodus:
tool_execution_startwird während der Preflight-Phase in der Reihenfolge der Hilfsquellen ausgegebentool_execution_updateEreignisse können sich über mehrere Tools hinweg verschachtelntool_execution_endwird in der Reihenfolge der Werkzeugvervollständigung ausgegeben, nachdem jedes Werkzeug fertiggestellt wurde- Letzte
toolResultNachrichtenereignisse werden später weiterhin in der Reihenfolge der Assistentenquelle ausgegeben
pi.on("tool_execution_start", async (event, ctx) => {
// event.toolCallId, event.toolName, event.args
});
pi.on("tool_execution_update", async (event, ctx) => {
// event.toolCallId, event.toolName, event.args, event.partialResult
});
pi.on("tool_execution_end", async (event, ctx) => {
// event.toolCallId, event.toolName, event.result, event.isError
});Kontext
Wird vor jedem LLM-Aufruf ausgelöst. Ändern Sie Nachrichten zerstörungsfrei. Informationen zu Nachrichtentypen finden Sie unter Session Format.
pi.on("context", async (event, ctx) => {
// event.messages - deep copy, safe to modify
const filtered = event.messages.filter(m => !shouldPrune(m));
return { messages: filtered };
});before_provider_headers
Wird ausgelöst, nachdem die ausgehenden HTTP-Header zusammengestellt wurden. Verwenden Sie es, um Anforderungsheader hinzuzufügen, zu überschreiben oder zu entfernen.
Handler mutieren event.headers an Ort und Stelle. Legen Sie einen Schlüssel auf eine Zeichenfolge fest, um sie hinzuzufügen oder zu überschreiben, oder auf null, um sie zu löschen.
pi.on("before_provider_headers", (event, ctx) => {
// Add or override — e.g. a session id for gateway tracing/attribution
event.headers["x-session-id"] = ctx.sessionManager.getSessionId();
// Drop a tracking header pi adds for this call
event.headers["X-OpenRouter-Title"] = null;
});Wird einmal pro Anbieteranforderung ausgeführt; Bei Wiederholungsversuchen werden dieselben Header erneut verwendet, anstatt den Hook erneut auszulösen.
before_provider_request
Wird ausgelöst, nachdem die anbieterspezifische Nutzlast erstellt wurde, unmittelbar bevor die Anfrage gesendet wird. Handler werden in der Reihenfolge des Ladens der Erweiterungen ausgeführt. Durch die Rückgabe von undefined bleibt die Nutzlast unverändert. Die Rückgabe eines anderen Werts ersetzt die Nutzlast für spätere Handler und für die eigentliche Anfrage.
Dieser Hook kann Systemanweisungen auf Anbieterebene umschreiben oder vollständig entfernen. Diese Änderungen auf Nutzlastebene werden von ctx.getSystemPrompt() nicht widergespiegelt, das die Systemaufforderungszeichenfolge von Pi und nicht die endgültige serialisierte Anbieternutzlast meldet.
pi.on("before_provider_request", (event, ctx) => {
console.log(JSON.stringify(event.payload, null, 2));
// Optional: replace payload
// return { ...event.payload, temperature: 0 };
});Dies ist hauptsächlich zum Debuggen der Provider-Serialisierung und des Cache-Verhaltens nützlich.
after_provider_response
Wird ausgelöst, nachdem eine HTTP-Antwort empfangen wurde und bevor der Stream-Body verbraucht wird. Handler werden in der Reihenfolge des Ladens der Erweiterungen ausgeführt.
pi.on("after_provider_response", (event, ctx) => {
// event.status - HTTP status code
// event.headers - normalized response headers
if (event.status === 429) {
console.log("rate limited", event.headers["retry-after"]);
}
});Die Header-Verfügbarkeit hängt vom Anbieter und Transport ab. Providers dass abstrakte HTTP-Antworten möglicherweise keine Header offenlegen.
Modellveranstaltungen
model_select
Wird ausgelöst, wenn sich das Modell über den Befehl /model, den Modellwechsel (Ctrl+P) oder die Sitzungswiederherstellung ändert.
pi.on("model_select", async (event, ctx) => {
// event.model - newly selected model
// event.previousModel - previous model (undefined if first selection)
// event.source - "set" | "cycle" | "restore"
const prev = event.previousModel
? `${event.previousModel.provider}/${event.previousModel.id}`
: "none";
const next = `${event.model.provider}/${event.model.id}`;
ctx.ui.notify(`Model changed (${event.source}): ${prev} -> ${next}`, "info");
});Verwenden Sie dies, um UI-Elemente (Statusleisten, Fußzeilen) zu aktualisieren oder eine modellspezifische Initialisierung durchzuführen, wenn sich das aktive Modell ändert.
think_level_select
Wird ausgelöst, wenn sich die Denkebene ändert. Dies ist nur eine Benachrichtigung; Rückgabewerte des Handlers werden ignoriert.
pi.on("thinking_level_select", async (event, ctx) => {
// event.level - newly selected thinking level
// event.previousLevel - previous thinking level
ctx.ui.setStatus("thinking", `thinking: ${event.level}`);
});Verwenden Sie dies, um die Benutzeroberfläche der Erweiterung zu aktualisieren, wenn pi.setThinkingLevel(), Modelländerungen oder integrierte Steuerelemente auf der Denkebene die Ebene des aktiven Denkens ändern.
Tool-Ereignisse
tool_call
Wird nach tool_execution_start ausgelöst, bevor das Tool ausgeführt wird. Kann blockieren. Verwenden Sie isToolCallEventType, um Eingaben einzugrenzen und getippte Eingaben zu erhalten.
Bevor tool_call ausgeführt wird, wartet pi darauf, dass zuvor ausgegebene Agent-Ereignisse den Ablauf durch AgentSession beenden. Dies bedeutet, dass ctx.sessionManager durch die aktuelle Assistenten-Tool-Aufrufnachricht auf dem neuesten Stand ist.
Im standardmäßigen parallelen Tool-Ausführungsmodus werden Geschwistertool-Aufrufe aus derselben Assistentennachricht nacheinander einem Preflight unterzogen und dann gleichzeitig ausgeführt. Es ist nicht garantiert, dass tool_call die Ergebnisse des Geschwistertools aus derselben Assistentennachricht in ctx.sessionManager sieht.
event.input ist veränderlich. Mutieren Sie es an Ort und Stelle, um Toolargumente vor der Ausführung zu patchen.
Verhaltensgarantien:
- Mutationen zu
event.inputwirken sich auf die tatsächliche Werkzeugausführung aus - Spätere
tool_call-Handler sehen Mutationen, die von früheren Handlern vorgenommen wurden - Nach Ihrer Mutation wird keine erneute Validierung durchgeführt
- Rückgabewerte von
tool_callsteuern Blockierung über{ block: true, reason?: string, terminate?: boolean } terminategilt nur für einen blockierten Anruf; Der Agent stoppt nur dann vorzeitig, wenn jedes endgültige Ergebnis im Stapel beendet wird
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
pi.on("tool_call", async (event, ctx) => {
// event.toolName - "bash", "read", "write", "edit", etc.
// event.toolCallId
// event.input - tool parameters (mutable)
// Built-in tools: no type params needed
if (isToolCallEventType("bash", event)) {
// event.input is { command: string; timeout?: number }
event.input.command = `source ~/.profile\n${event.input.command}`;
if (event.input.command.includes("rm -rf")) {
return { block: true, reason: "Dangerous command", terminate: true };
}
}
if (isToolCallEventType("read", event)) {
// event.input is { path: string; offset?: number; limit?: number }
console.log(`Reading: ${event.input.path}`);
}
});Eingabe benutzerdefinierter Tools eingeben
Benutzerdefinierte Tools sollten ihren Eingabetyp exportieren:
// my-extension.ts
export type MyToolInput = Static<typeof myToolSchema>;Verwenden Sie isToolCallEventType mit expliziten Typparametern:
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
import type { MyToolInput } from "my-extension";
pi.on("tool_call", (event) => {
if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) {
event.input.action; // typed
}
});tool_result
Wird nach Abschluss der Tool-Ausführung und vor tool_execution_end ausgelöst, außerdem werden die endgültigen Tool-Ergebnismeldungsereignisse ausgegeben. Kann das Ergebnis ändern.
Im parallelen Werkzeugmodus können tool_result und tool_execution_end in der Reihenfolge der Werkzeugvervollständigung verschachtelt sein, während die letzten toolResult-Nachrichtenereignisse noch später in der Reihenfolge der Assistentenquelle ausgegeben werden.
tool_result Handler verketten wie Middleware:
- Handler werden in der Reihenfolge des Ladens der Erweiterungen ausgeführt
- Jeder Handler sieht das neueste Ergebnis nach vorherigen Handleränderungen
- Handler können Teilpatches zurückgeben (
content,details,isErroroderusage); Ausgelassene Felder behalten ihre aktuellen Werte
Verwenden Sie ctx.signal für verschachtelte asynchrone Arbeit innerhalb des Handlers. Dadurch können Esc Modellaufrufe, fetch() und andere von der Erweiterung gestartete Abbruchvorgänge abbrechen.
import { isBashToolResult } from "@earendil-works/pi-coding-agent";
pi.on("tool_result", async (event, ctx) => {
// event.toolName, event.toolCallId, event.input
// event.content, event.details, event.isError, event.usage
if (isBashToolResult(event)) {
// event.details is typed as BashToolDetails
}
const response = await fetch("https://example.com/summarize", {
method: "POST",
body: JSON.stringify({ content: event.content }),
signal: ctx.signal,
});
// Modify result:
return { content: [...], details: {...}, isError: false, usage: nestedModelUsage };
});Benutzer-Bash-Ereignisse
user_bash
Wird ausgelöst, wenn der Benutzer die Befehle ! oder !! ausführt. Kann abfangen.
import { createLocalBashOperations } from "@earendil-works/pi-coding-agent";
pi.on("user_bash", (event, ctx) => {
// event.command - the bash command
// event.excludeFromContext - true if !! prefix
// event.cwd - working directory
// Option 1: Provide custom operations (e.g., SSH)
return { operations: remoteBashOps };
// Option 2: Wrap pi's built-in local bash backend
const local = createLocalBashOperations();
return {
operations: {
exec(command, cwd, options) {
return local.exec(`source ~/.profile\n${command}`, cwd, options);
}
}
};
// Option 3: Full replacement - return result directly
return { result: { output: "...", exitCode: 0, cancelled: false, truncated: false } };
});Eingabeereignisse
Eingang
Wird ausgelöst, wenn Benutzereingaben empfangen werden, nachdem Erweiterungsbefehle überprüft wurden, aber vor der Skill- und Vorlagenerweiterung. Das Ereignis sieht den rohen Eingabetext, daher sind /skill:foo und /template noch nicht erweitert.
Bearbeitungsreihenfolge:
- Erweiterungsbefehle (
/cmd) werden zuerst überprüft. Wenn sie gefunden werden, wird der Handler ausgeführt und das Eingabeereignis wird übersprungen inputEreignisfeuer – können abgefangen, transformiert oder verarbeitet werden- Wenn nicht behandelt: Fertigkeitsbefehle (
/skill:name) werden auf Fertigkeitsinhalte erweitert - Wenn nicht behandelt: prompt templates (
/template) auf Vorlageninhalt erweitert - Die Agentenverarbeitung beginnt (
before_agent_startusw.)
pi.on("input", async (event, ctx) => {
// event.text - raw input (before skill/template expansion)
// event.images - attached images, if any
// event.source - "interactive" (typed), "rpc" (API), or "extension" (via sendUserMessage)
// event.streamingBehavior - "steer" | "followUp" | undefined
// undefined when idle, "steer" for mid-stream interrupts,
// "followUp" for messages queued until the agent finishes
// Transform: rewrite input before expansion
if (event.text.startsWith("?quick "))
return { action: "transform", text: `Respond briefly: ${event.text.slice(7)}` };
// Handle: respond without LLM (extension shows its own feedback)
if (event.text === "ping") {
ctx.ui.notify("pong", "info");
return { action: "handled" };
}
// Route by source: skip processing for extension-injected messages
if (event.source === "extension") return { action: "continue" };
// Intercept skill commands before expansion
if (event.text.startsWith("/skill:")) {
// Could transform, block, or let pass through
}
return { action: "continue" }; // Default: pass through to expansion
});Ergebnisse:
continue– unverändert durchlaufen (Standard, wenn der Handler nichts zurückgibt)transform– Text/Bilder ändern, dann mit der Erweiterung fortfahrenhandled– Agent vollständig überspringen (der erste Handler, der dies zurückgibt, gewinnt)
Transformiert die Kette über Handler hinweg. Siehe input-transform.ts und input-transform-streaming.ts für streamingBehavior-fähiges Routing.
ExtensionContext
Alle Handler erhalten ctx: ExtensionContext.
ctx.ui
UI-Methoden für die Benutzerinteraktion. Ausführliche Informationen finden Sie unter Custom UI.
ctx.mode
Aktueller Laufmodus: "tui", "rpc", "json" oder "print". Verwenden Sie ctx.mode === "tui", um reine Terminalfunktionen wie custom(), Komponentenfabriken, Terminaleingabe und direktes TUI-Rendering zu schützen.
ctx.hasUI
true in den Modi TUI und RPC. false im Druckmodus (-p) und JSON Modus. Verwenden Sie dies, um Dialogmethoden (select, confirm, input, editor) und Fire-and-Forget-Methoden (notify, setStatus, setWidget, setTitle, setEditorText) zu schützen, die sowohl in TUI als auch funktionieren RPC Modi. Im RPC-Modus sind einige TUI-spezifische Methoden No-Ops oder geben Standardwerte zurück (siehe rpc.md).
ctx.cwd
Aktuelles Arbeitsverzeichnis.
Verwenden Sie CONFIG_DIR_NAME anstelle der Hartcodierung von .pi, wenn Sie projektlokale Konfigurationspfade erstellen. Umbenannte Distributionen können einen anderen Konfigurationsverzeichnisnamen verwenden.
import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { join } from "node:path";
export default function (pi: ExtensionAPI) {
pi.on("session_start", (_event, ctx) => {
const projectConfigPath = join(ctx.cwd, CONFIG_DIR_NAME, "my-extension.json");
// ...
});
}ctx.isProjectTrusted()
Gibt zurück, ob projektlokale Vertrauensstellung für den aktuellen Sitzungskontext aktiv ist. Dazu gehören temporäre Vertrauensentscheidungen und CLI Vertrauensüberschreibungen, nicht nur gespeicherte Entscheidungen im globalen Vertrauensspeicher.
Verwenden Sie dies, bevor Sie die projektlokale Erweiterungskonfiguration lesen, die nur für vertrauenswürdige Projekte berücksichtigt werden sollte.
ctx.sessionManager
Lesezugriff auf den Sitzungsstatus. Siehe Session Format für den vollständigen SessionManager API und die Eintragstypen.
Für tool_call wird dieser Status durch die aktuelle Assistentennachricht synchronisiert, bevor Handler ausgeführt werden. Im parallelen Tool-Ausführungsmodus ist es immer noch nicht garantiert, dass die Ergebnisse von Geschwistertools aus derselben Assistentenmeldung einbezogen werden.
ctx.sessionManager.getEntries() // All entries
ctx.sessionManager.getBranch() // Current branch
ctx.sessionManager.buildContextEntries() // Active branch entries with compaction applied
ctx.sessionManager.getLeafId() // Current leaf entry IDctx.modelRegistry / ctx.model / ctx.thinkingLevel / ctx.scopedModels
Zugriff auf Modelle, Anbieter und aufgelöste Authentifizierung. ctx.modelRegistry.getProvider(id) gibt den effektiven Pi-AI-Anbieter zurück, während getProviderAuth(id) seine aktuellen API key, Header, Basis-URL und anbieterbezogene Umgebung auflöst, ohne dass ein geladenes Modell erforderlich ist. ctx.model ist das aktive Modell und ctx.thinkingLevel ist seine aktuelle effektive Denkebene.
ctx.scopedModels ist die schreibgeschützte Liste der Modelle, die für die aktuelle Sitzung gelten – derselbe Satz, den der Befehl /scoped-models anzeigt. Es wird beim Sitzungsstart mit dem Flag --models CLI und der Einstellung enabledModels gelöst (abgeglichen mit dem verfügbaren Katalog mit Minimatch auf provider/modelId oder einem bloßen modelId). Es ist leer, wenn kein Scoping konfiguriert ist, was bedeutet, dass jedes verfügbare Modell verwendbar ist. Jeder Eintrag ist { model, thinkingLevel? }, wobei thinkingLevel nur gesetzt wird, wenn ein Muster ihn fixiert (z. B. anthropic/*:high). Verwenden Sie es, um eine Modellauswahl zu füllen, die die integrierte Modellauswahl widerspiegelt, anstatt den gesamten Katalog über ctx.modelRegistry.getAvailable() aufzulisten.
ctx.signal
Das aktuelle Agenten-Abbruchsignal oder undefined, wenn kein Agentenzug aktiv ist.
Verwenden Sie dies für abbruchbewusste verschachtelte Arbeiten, die von Erweiterungshandlern gestartet werden, zum Beispiel:
fetch(..., { signal: ctx.signal })- Modellaufrufe, die
signalakzeptieren - Datei- oder Prozesshilfsprogramme, die
AbortSignalakzeptieren
ctx.signal wird typischerweise bei aktiven Zugereignissen wie tool_call, tool_result, message_update und turn_end definiert.
In Leerlauf- oder Nicht-Turn-Kontexten wie Sitzungsereignissen, Erweiterungsbefehlen und ausgelösten Verknüpfungen, während Pi im Leerlauf ist, ist es normalerweise undefined.
pi.on("tool_result", async (event, ctx) => {
const response = await fetch("https://example.com/api", {
method: "POST",
body: JSON.stringify(event),
signal: ctx.signal,
});
const data = await response.json();
return { details: data };
});ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()
Kontrollflusshelfer. ctx.isIdle() ist falsch, während Pi eine Agentenausführung, einen automatischen Wiederholungsversuch, einen automatischen Komprimierungswiederholungsversuch oder eine Fortsetzung in der Warteschlange verarbeitet.
ctx.shutdown()
Fordern Sie ein ordnungsgemäßes Herunterfahren von Pi an.
- Interaktiver Modus: Wird verschoben, bis der Agent inaktiv wird (nachdem alle in der Warteschlange befindlichen Steuerungs- und Folgenachrichten verarbeitet wurden).
- RPC-Modus: Aufgeschoben bis zum nächsten Ruhezustand (nach Abschluss der aktuellen Befehlsantwort, beim Warten auf den nächsten Befehl).
- Druckmodus: Kein Betrieb. Der Prozess wird automatisch beendet, wenn alle Eingabeaufforderungen verarbeitet wurden.
Gibt vor dem Beenden das Ereignis session_shutdown an alle Erweiterungen aus. Verfügbar in allen Kontexten (Ereignishandler, Tools, Befehle, Verknüpfungen).
pi.on("tool_call", (event, ctx) => {
if (isFatal(event.input)) {
ctx.shutdown();
}
});ctx.getContextUsage()
Gibt die aktuelle Kontextverwendung für das aktive Modell zurück. Verwendet die letzte Assistentennutzung, sofern verfügbar, und schätzt dann die Token für nachfolgende Nachrichten.
const usage = ctx.getContextUsage();
if (usage && usage.tokens > 100_000) {
// ...
}ctx.compact()
Lösen Sie die Komprimierung aus, ohne den Abschluss abzuwarten. Verwenden Sie onComplete und onError für Folgeaktionen.
ctx.compact({
customInstructions: "Focus on recent changes",
onComplete: (result) => {
ctx.ui.notify("Compaction completed", "info");
},
onError: (error) => {
ctx.ui.notify(`Compaction failed: ${error.message}`, "error");
},
});ctx.getSystemPrompt()
Gibt die aktuelle Systemaufforderungszeichenfolge von Pi zurück.
- Während
before_agent_startspiegelt dies verkettete System-Prompt-Änderungen wider, die bisher für die aktuelle Runde vorgenommen wurden. - Spätere
context-Nachrichtenmutationen sind nicht enthalten. before_provider_requestPayload-Rewrites sind nicht enthalten.- Wenn später geladene Erweiterungen nach Ihren ausgeführt werden, können sie dennoch ändern, was letztendlich gesendet wird.
pi.on("before_agent_start", (event, ctx) => {
const prompt = ctx.getSystemPrompt();
console.log(`System prompt length: ${prompt.length}`);
});ExtensionCommandContext
Befehlshandler erhalten ExtensionCommandContext, was ExtensionContext um Sitzungssteuerungsmethoden erweitert. Diese sind nur in Befehlen verfügbar, da sie einen Deadlock verursachen können, wenn sie von Ereignishandlern aufgerufen werden.
ctx.getSystemPromptOptions()
Gibt die Basiseingaben zurück, die Pi derzeit zum Erstellen der Systemeingabeaufforderung verwendet.
const options = ctx.getSystemPromptOptions();
const contextPaths = options.contextFiles?.map((file) => file.path) ?? [];Dies hat die gleiche Form und Veränderlichkeit wie before_agent_start event.systemPromptOptions: benutzerdefinierte Eingabeaufforderung, aktive Tools, Tool-Snippets, Eingabeaufforderungsrichtlinien, angehängter Systemeingabeaufforderungstext, cwd, geladenes context files und geladene Fertigkeiten. Es kann vollständige Inhalte der Kontextdatei enthalten. Behandeln Sie es daher als vertrauliche erweiterungslokale Daten und vermeiden Sie die Offenlegung über Befehlslisten, Protokolle oder Metadaten zur automatischen Vervollständigung.
Hier werden die aktuellen Basiseingabeaufforderungseingaben gemeldet. Es umfasst keine before_agent_start verketteten System-Prompt-Änderungen pro Runde, spätere context Ereignismeldungsmutationen oder before_provider_request Nutzlastumschreibungen.
ctx.waitForIdle()
Warten Sie, bis sich der Agent vollständig beruhigt hat, einschließlich automatischer Wiederholungsversuche, automatischer Komprimierungswiederholungsversuche und Fortsetzungen in der Warteschlange:
pi.registerCommand("my-cmd", {
handler: async (args, ctx) => {
await ctx.waitForIdle();
// Agent is now idle, safe to modify session
},
});ctx.newSession(Optionen?)
Erstellen Sie eine neue Sitzung:
const parentSession = ctx.sessionManager.getSessionFile();
const kickoff = "Continue in the replacement session";
const result = await ctx.newSession({
parentSession,
setup: async (sm) => {
sm.appendMessage({
role: "user",
content: [{ type: "text", text: "Context from previous session..." }],
timestamp: Date.now(),
});
},
withSession: async (ctx) => {
// Use only the replacement-session ctx here.
await ctx.sendUserMessage(kickoff);
},
});
if (result.cancelled) {
// An extension cancelled the new session
}Optionen:
parentSession: übergeordnete Sitzungsdatei zur Aufzeichnung im neuen Sitzungsheadersetup: mutiertSessionManagerder neuen Sitzung, bevorwithSessionausgeführt wirdwithSession: Führen Sie die Arbeit nach dem Wechsel in einem neuen Ersetzungssitzungskontext aus. Verwenden Sie nicht den erfassten altenpi/ Befehlctx; siehe Session replacement lifecycle and footguns.
ctx.fork(entryId, Optionen?)
Verzweigen Sie von einem bestimmten Eintrag und erstellen Sie eine neue Sitzungsdatei:
const result = await ctx.fork("entry-id-123", {
withSession: async (ctx) => {
// Use only the replacement-session ctx here.
ctx.ui.notify("Now in the forked session", "info");
},
});
if (result.cancelled) {
// An extension cancelled the fork
}
const cloneResult = await ctx.fork("entry-id-456", { position: "at" });
if (cloneResult.cancelled) {
// An extension cancelled the clone
}Optionen:
position:"before"(Standard) verzweigt vor der ausgewählten Benutzernachricht und stellt diese Eingabeaufforderung im Editor wieder herposition:"at"dupliziert den aktiven Pfad durch den ausgewählten Eintrag, ohne den Editortext wiederherzustellenwithSession: Führen Sie die Arbeit nach dem Wechsel in einem neuen Ersetzungssitzungskontext aus. Verwenden Sie nicht den erfassten altenpi/ Befehlctx; siehe Session replacement lifecycle and footguns.
ctx.navigateTree(targetId, Optionen?)
Navigieren Sie zu einem anderen Punkt im session tree:
const result = await ctx.navigateTree("entry-id-456", {
summarize: true,
customInstructions: "Focus on error handling changes",
replaceInstructions: false, // true = replace default prompt entirely
label: "review-checkpoint",
});Optionen:
summarize: Ob eine Zusammenfassung des verlassenen Zweigs erstellt werden sollcustomInstructions: Benutzerdefinierte Anweisungen für die ZusammenfassungreplaceInstructions: Wenn wahr, ersetztcustomInstructionsdie Standardaufforderung, anstatt angehängt zu werdenlabel: Beschriftung zum Anhängen an den Zweigzusammenfassungseintrag (oder Zieleintrag, wenn nicht zusammenfassend)
ctx.switchSession(sessionPath, Optionen?)
Wechseln Sie zu einer anderen Sitzungsdatei:
const result = await ctx.switchSession("/path/to/session.jsonl", {
withSession: async (ctx) => {
await ctx.sendUserMessage("Resume work in the replacement session");
},
});
if (result.cancelled) {
// An extension cancelled the switch via session_before_switch
}Optionen:
withSession: Führen Sie die Arbeit nach dem Wechsel in einem neuen Ersetzungssitzungskontext aus. Verwenden Sie nicht den erfassten altenpi/ Befehlctx; siehe Session replacement lifecycle and footguns.
Um verfügbare Sitzungen zu ermitteln, verwenden Sie die statischen Methoden SessionManager.list() oder SessionManager.listAll():
import { SessionManager } from "@earendil-works/pi-coding-agent";
pi.registerCommand("switch", {
description: "Switch to another session",
handler: async (args, ctx) => {
const sessions = await SessionManager.list(ctx.cwd);
if (sessions.length === 0) return;
const choice = await ctx.ui.select(
"Pick session:",
sessions.map(s => s.file),
);
if (choice) {
await ctx.switchSession(choice, {
withSession: async (ctx) => {
ctx.ui.notify("Switched session", "info");
},
});
}
},
});Lebenszyklus des Sitzungsaustauschs und Fußfeuerwaffen
withSession erhält ein neues ReplacedSessionContext, das ExtensionCommandContext mit asynchronen sendMessage()- und sendUserMessage()-Helfern erweitert, die an die Ersatzsitzung gebunden sind.
Lebenszyklus und Fußfeuerwaffen:
withSessionwird erst ausgeführt, nachdem die alte Sitzungsession_shutdownausgegeben hat, die alte Laufzeit abgebaut wurde, die Ersatzsitzung neu gebunden wurde und die neue Erweiterungsinstanz bereitssession_startempfangen hat.- Der Rückruf wird weiterhin im ursprünglichen Abschluss ausgeführt, nicht innerhalb der neuen Erweiterungsinstanz. Das bedeutet, dass Ihre alte Erweiterungsinstanz möglicherweise bereits die Bereinigung beim Herunterfahren durchgeführt hat, bevor
withSessionstartet. - Erfasste alte
pi/ alte Befehls-ctxsitzungsgebundene Objekte sind nach dem Ersetzen veraltet und werden bei Verwendung ausgelöst. Verwenden Sie für sitzungsgebundene Arbeit nur das anwithSessionübergebenectx. - Zuvor extrahierte Rohobjekte liegen weiterhin in Ihrer Verantwortung. Wenn Sie beispielsweise
const sm = ctx.sessionManagervor dem Ersetzen erfassen, istsmimmer noch das alteSessionManager-Objekt. Nach dem Austausch nicht wiederverwenden. - Der Code in
withSessionsollte davon ausgehen, dass jeder von Ihremsession_shutdown-Handler ungültig gemachte Status bereits verschwunden ist. Erfassen Sie nur einfache Daten, die das Herunterfahren sauber überstehen, wie z. B. Zeichenfolgen, IDs und serialisierte Konfigurationen.
Sicheres Muster:
pi.registerCommand("handoff", {
handler: async (_args, ctx) => {
const kickoff = "Continue from the replacement session";
await ctx.newSession({
withSession: async (ctx) => {
await ctx.sendUserMessage(kickoff);
},
});
},
});Unsicheres Muster:
pi.registerCommand("handoff", {
handler: async (_args, ctx) => {
const oldSessionManager = ctx.sessionManager;
await ctx.newSession({
withSession: async (_ctx) => {
// stale old objects: do not do this
oldSessionManager.getSessionFile();
pi.sendUserMessage("wrong");
},
});
},
});ctx.reload()
Führen Sie den gleichen Neuladeablauf wie /reload aus.
pi.registerCommand("reload-runtime", {
description: "Reload extensions, skills, prompts, themes, and context files",
handler: async (_args, ctx) => {
await ctx.reload();
return;
},
});Wichtiges Verhalten:
await ctx.reload()gibtsession_shutdownfür die aktuelle Erweiterungslaufzeit aus- Anschließend werden die Ressourcen neu geladen und
session_startmitreason: "reload"undresources_discovermit Grund"reload"ausgegeben. - Der aktuell laufende Befehlshandler läuft weiterhin im alten Aufrufrahmen weiter
- Code nach
await ctx.reload()läuft weiterhin ab der Pre-Reload-Version - Code nach
await ctx.reload()darf nicht davon ausgehen, dass der alte In-Memory-Erweiterungsstatus noch gültig ist - Nachdem der Handler zurückgekehrt ist, verwenden zukünftige Befehle/Ereignisse/Toolaufrufe die neue Erweiterungsversion
Für vorhersehbares Verhalten behandeln Sie reload als Terminal für diesen Handler (await ctx.reload(); return;).
Tools werden mit ExtensionContext ausgeführt, sodass sie ctx.reload() nicht direkt aufrufen können. Verwenden Sie einen Befehl als Neulade-Einstiegspunkt und stellen Sie dann ein Tool bereit, das diesen Befehl als Folge-Benutzernachricht in die Warteschlange stellt.
Beispieltool, das das LLM aufrufen kann, um ein Neuladen auszulösen:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
pi.registerCommand("reload-runtime", {
description: "Reload extensions, skills, prompts, themes, and context files",
handler: async (_args, ctx) => {
await ctx.reload();
return;
},
});
pi.registerTool({
name: "reload_runtime",
label: "Reload Runtime",
description: "Reload extensions, skills, prompts, themes, and context files",
parameters: Type.Object({}),
async execute() {
pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
return {
content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }],
};
},
});
}ErweiterungAPI Methoden
pi.on(Ereignis, Handler)
Abonnieren Sie Veranstaltungen. Siehe Events für Ereignistypen und Rückgabewerte.
pi.registerTool(definition)
Registrieren Sie ein vom LLM aufrufbares benutzerdefiniertes Tool. Ausführliche Informationen finden Sie unter Custom Tools.
pi.registerTool() funktioniert sowohl während des Ladens der Erweiterung als auch nach dem Start. Sie können es innerhalb von session_start, Befehlshandlern oder anderen Ereignishandlern aufrufen. Neue Werkzeuge werden sofort in derselben Sitzung aktualisiert, erscheinen also in pi.getAllTools() und sind vom LLM ohne /reload aufrufbar.
Verwenden Sie pi.setActiveTools(), um Tools (einschließlich dynamisch hinzugefügter Tools) zur Laufzeit zu aktivieren oder zu deaktivieren.
Verwenden Sie promptSnippet, um ein benutzerdefiniertes Werkzeug für einen einzeiligen Eintrag in Available tools zu aktivieren, und promptGuidelines, um werkzeugspezifische Aufzählungszeichen an den Standardabschnitt Guidelines anzuhängen, wenn das Werkzeug aktiv ist.
Wichtig: promptGuidelines-Aufzählungszeichen werden flach an den Guidelines-Abschnitt angehängt, ohne Werkzeugnamen-Präfix. Jede Richtlinie muss das Werkzeug benennen, auf das sie sich bezieht – vermeiden Sie „Verwenden Sie dieses Werkzeug, wenn …“, da das LLM nicht erkennen kann, welches Werkzeug „dies“ bedeutet. Schreiben Sie stattdessen „My_tool verwenden, wenn…“.
Ein vollständiges Beispiel finden Sie unter dynamic-tools.ts.
import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai";
pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "What this tool does",
promptSnippet: "Summarize or transform text according to action",
promptGuidelines: ["Use my_tool when the user asks to summarize previously generated text."],
parameters: Type.Object({
action: StringEnum(["list", "add"] as const),
text: Type.Optional(Type.String()),
}),
prepareArguments(args) {
// Optional compatibility shim. Runs before schema validation.
// Return the current schema shape, for example to fold legacy fields
// into the modern parameter object.
return args;
},
async execute(toolCallId, params, signal, onUpdate, ctx) {
// Stream progress
onUpdate?.({ content: [{ type: "text", text: "Working..." }] });
return {
content: [{ type: "text", text: "Done" }],
details: { result: "..." },
};
},
// Optional: Custom rendering
renderCall(args, theme, context) { ... },
renderResult(result, options, theme, context) { ... },
});pi.sendMessage(Nachricht, Optionen?)
Fügen Sie eine benutzerdefinierte Nachricht in die Sitzung ein. Benutzerdefinierte Nachrichten nehmen am LLM-Kontext teil. Für dauerhafte TUI-Inhalte, die nicht an das LLM gesendet werden sollen, verwenden Sie pi.appendEntry() mit pi.registerEntryRenderer().
pi.sendMessage({
customType: "my-extension",
content: "Message text",
display: true,
details: { ... },
}, {
triggerTurn: true,
deliverAs: "steer",
});Optionen:
deliverAs- Liefermodus:"steer"(Standard) – Stellt die Nachricht während des Streamings in die Warteschlange. Wird geliefert, nachdem der aktuelle Assistentenzug die Ausführung seiner Werkzeugaufrufe abgeschlossen hat, und zwar vor dem nächsten LLM-Aufruf."followUp"– Wartet, bis der Agent fertig ist. Wird nur geliefert, wenn der Agent keine Tool-Aufrufe mehr hat."nextTurn"– In der Warteschlange für die nächste Benutzeraufforderung. Unterbricht oder löst nichts aus.
triggerTurn: true– Wenn der Agent inaktiv ist, wird sofort eine LLM-Antwort ausgelöst. Gilt nur für die Modi"steer"und"followUp"(wird für"nextTurn"ignoriert).
pi.sendUserMessage(Inhalt, Optionen?)
Senden Sie eine Benutzernachricht an den Agenten. Im Gegensatz zu sendMessage(), das benutzerdefinierte Nachrichten sendet, wird hiermit eine tatsächliche Benutzernachricht gesendet, die aussieht, als wäre sie vom Benutzer eingegeben worden. Löst immer eine Runde aus.
// Simple text message
pi.sendUserMessage("What is 2+2?");
// With content array (text + images)
pi.sendUserMessage([
{ type: "text", text: "Describe this image:" },
{ type: "image", source: { type: "base64", mediaType: "image/png", data: "..." } },
]);
// During streaming - must specify delivery mode
pi.sendUserMessage("Focus on error handling", { deliverAs: "steer" });
pi.sendUserMessage("And then summarize", { deliverAs: "followUp" });Optionen:
deliverAs– Erforderlich, wenn der Agent streamt:"steer"– Stellt die Nachricht zur Zustellung in die Warteschlange, nachdem der aktuelle Assistentenzug die Ausführung seiner Werkzeugaufrufe abgeschlossen hat"followUp"– Wartet darauf, dass der Agent alle Tools beendet
Wenn nicht gestreamt wird, wird die Nachricht sofort gesendet und löst eine neue Runde aus. Beim Streamen ohne deliverAs wird ein Fehler ausgegeben.
Ein vollständiges Beispiel finden Sie unter send-user-message.ts.
pi.appendEntry(customType, Daten?)
Erweiterungsdaten beibehalten. Benutzerdefinierte Einträge nehmen NICHT am LLM-Kontext teil. Im interaktiven Modus können sie in Verbindung mit pi.registerEntryRenderer() auch innerhalb des Chat-Transkripts gerendert werden.
pi.appendEntry("my-state", { count: 42 });
pi.appendEntry("status-card", { title: "Indexed files", count: 17 });
// Restore on reload
pi.on("session_start", async (_event, ctx) => {
for (const entry of ctx.sessionManager.getEntries()) {
if (entry.type === "custom" && entry.customType === "my-state") {
// Reconstruct from entry.data
}
}
});pi.setSessionName(name)
Legen Sie den Anzeigenamen der Sitzung fest (wird in der Sitzungsauswahl anstelle der ersten Nachricht angezeigt).
pi.setSessionName("Refactor auth module");pi.getSessionName()
Rufen Sie den aktuellen Sitzungsnamen ab, falls festgelegt.
const name = pi.getSessionName();
if (name) {
console.log(`Session: ${name}`);
}pi.setLabel(entryId, label)
Legen Sie eine Beschriftung für einen Eintrag fest oder löschen Sie sie. Beschriftungen sind benutzerdefinierte Markierungen für Lesezeichen und Navigation (angezeigt im /tree-Selektor).
// Set a label
pi.setLabel(entryId, "checkpoint-before-refactor");
// Clear a label
pi.setLabel(entryId, undefined);
// Read labels via sessionManager
const label = ctx.sessionManager.getLabel(entryId);Beschriftungen bleiben in der Sitzung bestehen und überleben Neustarts. Markieren Sie damit wichtige Punkte (Abbiegungen, Kontrollpunkte) im Konversationsbaum.
pi.registerCommand(name, Optionen)
Registrieren Sie einen Befehl.
Wenn mehrere Erweiterungen denselben Befehlsnamen registrieren, behält pi sie alle und weist numerische Aufrufsuffixe in der Ladereihenfolge zu, zum Beispiel /review:1 und /review:2.
pi.registerCommand("stats", {
description: "Show session statistics",
handler: async (args, ctx) => {
const count = ctx.sessionManager.getEntries().length;
ctx.ui.notify(`${count} entries`, "info");
}
});Optional: Argument-Autovervollständigung für /command... hinzufügen:
import type { AutocompleteItem } from "@earendil-works/pi-tui";
pi.registerCommand("deploy", {
description: "Deploy to an environment",
getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
const envs = ["dev", "staging", "prod"];
const items = envs.map((e) => ({ value: e, label: e }));
const filtered = items.filter((i) => i.value.startsWith(prefix));
return filtered.length > 0 ? filtered : null;
},
handler: async (args, ctx) => {
ctx.ui.notify(`Deploying: ${args}`, "info");
},
});pi.getCommands()
Holen Sie sich die slash commands, die für den Aufruf über prompt in der aktuellen Sitzung verfügbar ist. Enthält Erweiterungsbefehle, prompt templates und Fertigkeitsbefehle.
Die Liste entspricht der Reihenfolge RPC get_commands: zuerst Erweiterungen, dann Vorlagen, dann Fertigkeiten.
const commands = pi.getCommands();
const bySource = commands.filter((command) => command.source === "extension");
const userScoped = commands.filter((command) => command.sourceInfo.scope === "user");Jeder Eintrag hat diese Form:
{
name: string; // Invokable command name without the leading slash. May be suffixed like "review:1"
description?: string;
source: "extension" | "prompt" | "skill";
sourceInfo: {
path: string;
source: string;
scope: "user" | "project" | "temporary";
origin: "package" | "top-level";
baseDir?: string;
};
}Verwenden Sie sourceInfo als kanonisches Herkunftsfeld. Leiten Sie den Besitz nicht aus Befehlsnamen oder aus der Ad-hoc-Pfadanalyse ab.
Integrierte interaktive Befehle (wie /model und /settings) sind hier nicht enthalten. Sie werden ausschließlich interaktiv bearbeitet
Modus und würde nicht ausgeführt, wenn es über prompt gesendet würde.
pi.registerMessageRenderer(customType, Renderer)
Registrieren Sie einen benutzerdefinierten TUI-Renderer für benutzerdefinierte Nachrichten bei Ihrem customType. Benutzerdefinierte Nachrichten werden mit pi.sendMessage() erstellt und nehmen am LLM-Kontext teil. Siehe Custom UI.
pi.registerMarkdownTransformer(Transformer)
Registrieren Sie einen Transformator für die Markdown in normalem Benutzertext, Assistententext und Denkblöcken. Transformatoren laufen in der Reihenfolge der Erweiterungslasten, und jeder Transformator empfängt die vom vorherigen Transformator zurückgegebene Markdown. Nachdem die Kette abgeschlossen ist, rendert Pi den transformierten Inhalt mit seinem integrierten Renderer.
Der Transformator empfängt die Zeichenfolge Markdown und einen Kontext mit:
messageType–"user","assistant"oder"assistant-thinking"isStreaming–truefür teilweise Assistentenaktualisierungen;falsefür Benutzer, abgeschlossenen Assistenten und wiederhergestellte NachrichtenavailableWidth– genaue Terminalspalten, die für den transformierten Markdown-Inhalt verfügbar sind
Gib das transformierte Markdown zurück:
pi.registerMarkdownTransformer((markdown, { messageType, isStreaming }) => {
if (isStreaming || messageType === "assistant-thinking") return markdown;
return markdown.replaceAll("-->", "→");
});Wenn ein Transformator wirft, behält Pi das bisher produzierte Markdown und fährt mit dem nächsten Transformator fort. Der Hook kann nur angezeigt werden: Die ursprüngliche Nachricht bleibt im Sitzungs- und Modellkontext unverändert. Es wird für neue Benutzernachrichten, Assistenten-Streaming-Updates, wiederhergestellte Sitzungsnachrichten und Änderungen der Terminalbreite ausgeführt, sodass Transformer synchron und kostengünstig bleiben sollten.
pi.registerEntryRenderer(customType, Renderer)
Registrieren Sie einen benutzerdefinierten TUI-Renderer für benutzerdefinierte Einträge bei Ihrem customType. Benutzerdefinierte Einträge werden mit pi.appendEntry() erstellt und nehmen nicht am LLM-Kontext teil.
import { Box, Text } from "@earendil-works/pi-tui";
pi.registerEntryRenderer("status-card", (entry, { expanded }, theme) => {
const data = entry.data as { title: string; count: number };
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
box.addChild(new Text(`${theme.bold(data.title)}: ${data.count}`));
if (expanded) {
box.addChild(new Text(theme.fg("dim", JSON.stringify(data, null, 2))));
}
return box;
});
pi.appendEntry("status-card", { title: "Indexed files", count: 17 });pi.registerShortcut(Verknüpfung, Optionen)
Registrieren Sie eine Tastenkombination. Siehe keybindings.md für das Verknüpfungsformat und die integrierten Tastenkombinationen.
pi.registerShortcut("ctrl+shift+p", {
description: "Toggle plan mode",
handler: async (ctx) => {
ctx.ui.notify("Toggled!");
},
});pi.registerFlag(Name, Optionen)
Registrieren Sie ein CLI-Flag.
pi.registerFlag("plan", {
description: "Start in plan mode",
type: "boolean",
default: false,
});
// Check value
if (pi.getFlag("plan")) {
// Plan mode enabled
}pi.exec(Befehl, Argumente, Optionen?)
Führen Sie einen Shell-Befehl aus.
const result = await pi.exec("git", ["status"], { signal, timeout: 5000 });
// result.stdout, result.stderr, result.code, result.killedpi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)
Aktive Werkzeuge verwalten. Dies funktioniert sowohl für integrierte Tools als auch für dynamisch registrierte Tools. pi.getActiveTools() gibt die aktiven Werkzeugnamen als string[] zurück; pi.getAllTools() gibt Metadaten für alle konfigurierten Tools zurück.
const active = pi.getActiveTools(); // ["read", "bash", ...]
const all = pi.getAllTools();
// all = [{
// name: "read",
// description: "Read file contents...",
// parameters: ...,
// promptGuidelines: ["Use read to examine files instead of cat or sed."],
// sourceInfo: { path: "<builtin:read>", source: "builtin", scope: "temporary", origin: "top-level" }
// }, ...]
const builtinTools = all.filter((t) => t.sourceInfo.source === "builtin");
const extensionTools = all.filter((t) => t.sourceInfo.source !== "builtin" && t.sourceInfo.source !== "sdk");
pi.setActiveTools([...new Set([...active, "my_custom_tool"])]); // Keep current tools and enable my_custom_tool
pi.setActiveTools(["read", "bash"]); // Switch to read-onlypi.getAllTools() gibt name, description, parameters, promptGuidelines und sourceInfo zurück.
Typische sourceInfo.source-Werte:
builtinfür integrierte Werkzeugesdkfür übercreateAgentSession({ customTools })übergebene Werkzeuge- Metadaten der Erweiterungsquelle für Tools, die von Erweiterungen registriert werden
pi.setModel(Modell)
Stellen Sie das aktuelle Modell ein. Gibt false zurück, wenn für das Modell kein API key verfügbar ist. Informationen zum Konfigurieren benutzerdefinierter Modelle finden Sie unter models.md.
const model = ctx.modelRegistry.find("anthropic", "claude-sonnet-4-5");
if (model) {
const success = await pi.setModel(model);
if (!success) {
ctx.ui.notify("No API key for this model", "error");
}
}pi.getThinkingLevel() / pi.setThinkingLevel(level)
Holen Sie sich die Denkebene oder legen Sie sie fest. Die Ebene ist auf die Modellfähigkeiten beschränkt (nicht-begründende Modelle verwenden immer „aus“). Änderungen geben thinking_level_select aus.
const current = pi.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
pi.setThinkingLevel("high");pi.events
Gemeinsamer Ereignisbus für die Kommunikation zwischen Nebenstellen:
pi.events.on("my:event", (data) => { ... });
pi.events.emit("my:event", { ... });pi.registerProvider(name, config)
Registrieren oder überschreiben Sie einen Modellanbieter dynamisch. Nützlich für Proxys, benutzerdefinierte Endpunkte oder teamweite Modellkonfigurationen.
Während der Extension-Factory-Funktion getätigte Anrufe werden in die Warteschlange gestellt und angewendet, sobald der Runner initialisiert wird. Danach vorgenommene Aufrufe – beispielsweise von einem Befehlshandler nach einem Benutzer-Setup-Ablauf – werden sofort wirksam, ohne dass eine /reload erforderlich ist.
Dynamische Anbieter können refreshModels implementieren. Pi ruft es während der Modellaktualisierung auf, veröffentlicht die zurückgegebene Liste synchron über den Anbieter und übergibt den kanonischen Anmeldeinformations-/gespeicherten Katalog-/Netzwerk-/Signalkontext. Die Erweiterung entscheidet, ob Katalogmetadaten durch generierungsgeprüfte context.publish({ persist: entry }) beibehalten werden; Live-Server wie llama.cpp können Modelle zurückgeben, ohne sie beizubehalten.
context.signal ist immer ein konkretes Signal und Provider-Rückrufe müssen es an blockierende E/A weitergeben. Öffentliche ModelRuntime.refresh()- und ModelRegistry.refresh()-Aufrufe akzeptieren ein optionales Signal und sind unbegrenzt, wenn es weggelassen wird; Verlängerungen und Bewerbungen wählen ihre eigenen Fristen. Durch die Stornierung muss der Anrufer nicht mehr warten, selbst wenn ein Anbieter das Signal ignoriert. Es ist jedoch dennoch eine Zusammenarbeit erforderlich, um die zugrunde liegende Arbeit zu stoppen.
Extensions, die eine native Anbieterauthentifizierung, Filterung, Aktualisierung oder Stream-Verhalten benötigen, können eine vollständige Provider von @earendil-works/pi-ai registrieren. Der Anbieter wird zur Kompositionsbasis und es gelten weiterhin models.json Überschreibungen darüber.
import { createProvider, openAICompletionsApi } from "@earendil-works/pi-ai";
const provider = createProvider({
id: "local-server",
name: "Local Server",
baseUrl: "http://localhost:8080/v1",
auth: {
apiKey: {
name: "Local server setup",
async login(interaction) {
return {
type: "api_key",
key: await interaction.prompt({ type: "secret", message: "API key" }),
};
},
async resolve({ credential }) {
return credential?.key
? { auth: { apiKey: credential.key }, source: "stored API key" }
: undefined;
},
},
},
models: [],
api: openAICompletionsApi(),
});
pi.registerProvider(provider);
// Register a new provider with custom models
pi.registerProvider("my-proxy", {
name: "My Proxy",
baseUrl: "https://proxy.example.com",
apiKey: "$PROXY_API_KEY", // env var reference
api: "anthropic-messages",
models: [
{
id: "claude-sonnet-4-20250514",
name: "Claude 4 Sonnet (proxy)",
reasoning: false,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 16384
}
]
});
// Register a live llama.cpp catalog without persisting discovered models
pi.registerProvider("llama.cpp", {
baseUrl: "http://localhost:8080/v1",
apiKey: "local",
api: "openai-completions",
async refreshModels({ signal }) {
const response = await fetch("http://localhost:8080/v1/models", { signal });
const { data } = await response.json();
return data.map(({ id }) => ({
id,
name: id,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 16384
}));
}
});
// Override baseUrl for an existing provider (keeps all models)
pi.registerProvider("anthropic", {
baseUrl: "https://proxy.example.com"
});
// Register provider with OAuth support for /login
pi.registerProvider("corporate-ai", {
baseUrl: "https://ai.corp.com",
api: "openai-responses",
models: [...],
oauth: {
name: "Corporate AI (SSO)",
async login(callbacks) {
// Custom OAuth flow
callbacks.onAuth({ url: "https://sso.corp.com/..." });
const code = await callbacks.onPrompt({ message: "Enter code:" });
return { refresh: code, access: code, expires: Date.now() + 3600000 };
},
async refreshToken(credentials, signal) {
signal.throwIfAborted();
// Refresh logic
return credentials;
},
getApiKey(credentials) {
return credentials.access;
}
}
});Die Objektform akzeptiert ein vollständiges pi-ai Provider, einschließlich nativem auth-, getModels-, refreshModels-, filterModels-, stream- und streamSimple-Verhalten.
Legacy-Konfigurationsoptionen:
name– Anzeigename für den Anbieter in der Benutzeroberfläche, z. B./login.baseUrl- API Endpunkt-URL. Erforderlich beim Definieren von Modellen.apiKey- API key Literal, Umgebungsinterpolation ($ENV_VARoder${ENV_VAR}) oder führendes!command. Erforderlich beim Definieren von Modellen (sofernoauthnicht angegeben).$maskiert ``apiKey- API key Literal, Umgebungsinterpolation ($ENV_VARoder${ENV_VAR}) oder führendes!command. Erforderlich beim Definieren von Modellen (sofernoauthnicht angegeben).$maskiert und$!maskiert ein Literal!`, ohne die Befehlsausführung auszulösen.api- API Typ:"anthropic-messages","openai-completions","openai-responses"usw.headers– Benutzerdefinierte Header zur Einbindung in Anfragen.authHeader– Wenn wahr, wird der HeaderAuthorization: Bearerautomatisch hinzugefügt.models– Array von Modelldefinitionen. Falls bereitgestellt, ersetzt es alle vorhandenen Modelle für diesen Anbieter. Modelldefinitionen könnenbaseUrlfestlegen, um den Anbieterendpunkt für dieses Modell zu überschreiben.refreshModels– Asynchroner dynamischer Erkennungsrückruf. Die zurückgegebenen Modelle ersetzen die von der Erweiterung bereitgestellten Modelle.context.storedenthält den persistenten Anbieter-Snapshot; Verwenden Sie generationsüberprüftcontext.publish({ persist: entry })nur, wenn aktualisierte Katalogdaten bestehen bleiben sollen. Verwenden Siepersist: null, um diesen Schnappschuss zu löschen.oauth– OAuth Anbieterkonfiguration für/login-Unterstützung. Sofern angegeben, erscheint der Anbieter im Anmeldemenü.streamSimple– Benutzerdefinierte Streaming-Implementierung für nicht standardmäßige APIs.
Weitere Themen finden Sie unter custom-provider.md: benutzerdefiniertes Streaming APIs, OAuth Details, Referenz zur Modelldefinition.
pi.unregisterProvider(name)
Entfernen Sie einen zuvor registrierten Anbieter und seine Modelle. Integrierte Modelle, die vom Anbieter überschrieben wurden, werden wiederhergestellt. Hat keine Auswirkung, wenn der Anbieter nicht registriert wurde.
Wie registerProvider wird dies sofort wirksam, wenn es nach der anfänglichen Ladephase aufgerufen wird, sodass ein /reload nicht erforderlich ist.
pi.registerCommand("my-setup-teardown", {
description: "Remove the custom proxy provider",
handler: async (_args, _ctx) => {
pi.unregisterProvider("my-proxy");
},
});Staatsverwaltung
Extensions mit Status sollte es im Tool-Ergebnis details speichern, um eine ordnungsgemäße Verzweigungsunterstützung zu gewährleisten:
export default function (pi: ExtensionAPI) {
let items: string[] = [];
// Reconstruct state from session
pi.on("session_start", async (_event, ctx) => {
items = [];
for (const entry of ctx.sessionManager.getBranch()) {
if (entry.type === "message" && entry.message.role === "toolResult") {
if (entry.message.toolName === "my_tool") {
items = entry.message.details?.items ?? [];
}
}
}
});
pi.registerTool({
name: "my_tool",
// ...
async execute(toolCallId, params, signal, onUpdate, ctx) {
items.push("new item");
return {
content: [{ type: "text", text: "Added" }],
details: { items: [...items] }, // Store for reconstruction
};
},
});
}Benutzerdefinierte Werkzeuge
Registrieren Sie Tools, die das LLM über pi.registerTool() aufrufen kann. Werkzeuge werden in der Systemeingabeaufforderung angezeigt und können über ein benutzerdefiniertes Rendering verfügen.
Verwenden Sie promptSnippet für einen kurzen einzeiligen Eintrag im Abschnitt Available tools in der Standard-Systemeingabeaufforderung. Wenn es weggelassen wird, werden benutzerdefinierte Tools in diesem Abschnitt nicht berücksichtigt.
Verwenden Sie promptGuidelines, um werkzeugspezifische Aufzählungszeichen zum Standard-Systemaufforderungsabschnitt Guidelines hinzuzufügen. Diese Aufzählungszeichen werden nur eingefügt, während das Tool aktiv ist (z. B. nach pi.setActiveTools([...])).
Wichtig: promptGuidelines-Aufzählungszeichen werden flach an den Guidelines-Abschnitt angehängt, ohne Präfix oder Gruppierung des Werkzeugnamens. Jede Richtlinie muss das Werkzeug benennen, auf das sie sich bezieht – vermeiden Sie „Verwenden Sie dieses Werkzeug, wenn …“, da das LLM nicht erkennen kann, welches Werkzeug „dies“ bedeutet. Schreiben Sie stattdessen „My_tool verwenden, wenn…“.
Hinweis: Einige Modelle sind Idioten und enthalten das @-Präfix in Werkzeugpfadargumenten. Integrierte Tools entfernen ein führendes @, bevor Pfade aufgelöst werden. Wenn Ihr benutzerdefiniertes Tool einen Pfad akzeptiert, normalisieren Sie auch ein führendes @.
Wenn Ihr benutzerdefiniertes Tool Dateien mutiert, verwenden Sie withFileMutationQueue(), damit es an derselben Datei-Warteschlange teilnimmt wie die integrierten edit und write. Dies ist wichtig, da Toolaufrufe standardmäßig parallel ausgeführt werden. Ohne die Warteschlange können zwei Tools denselben alten Dateiinhalt lesen, unterschiedliche Aktualisierungen berechnen und dann der letzte Schreibvorgang den anderen überschreiben.
Beispiel für einen Fehlerfall: Ihr benutzerdefiniertes Werkzeug bearbeitet foo.ts, während das integrierte edit im selben Assistentenzug auch foo.ts ändert. Wenn Ihr Tool nicht an der Warteschlange teilnimmt, können beide das Original foo.ts lesen, separate Änderungen anwenden und eine dieser Änderungen geht verloren.
Übergeben Sie den tatsächlichen Zieldateipfad an withFileMutationQueue(), nicht das rohe Benutzerargument. Lösen Sie es zunächst in einen absoluten Pfad auf, relativ zu ctx.cwd oder dem Arbeitsverzeichnis Ihres Tools. Für vorhandene Dateien kanonisiert der Helfer durch realpath(), sodass Symlink-Aliase für dieselbe Datei eine Warteschlange gemeinsam nutzen. Bei neuen Dateien wird auf den aufgelösten absoluten Pfad zurückgegriffen, da noch nichts zu realpath() vorhanden ist.
Stellen Sie das gesamte Mutationsfenster auf diesem Zielpfad in die Warteschlange. Dazu gehört die Lese-, Änderungs- und Schreiblogik, nicht nur der endgültige Schreibvorgang.
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const absolutePath = resolve(ctx.cwd, params.path);
return withFileMutationQueue(absolutePath, async () => {
await mkdir(dirname(absolutePath), { recursive: true });
const current = await readFile(absolutePath, "utf8");
const next = current.replace(params.oldText, params.newText);
await writeFile(absolutePath, next, "utf8");
return {
content: [{ type: "text", text: `Updated ${params.path}` }],
details: {},
};
});
}Werkzeugdefinition
import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai";
import { Text } from "@earendil-works/pi-tui";
pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "What this tool does (shown to LLM)",
promptSnippet: "List or add items in the project todo list",
promptGuidelines: [
"Use my_tool for todo planning instead of direct file edits when the user asks for a task list."
],
parameters: Type.Object({
action: StringEnum(["list", "add"] as const), // Use StringEnum for Google compatibility
text: Type.Optional(Type.String()),
}),
prepareArguments(args) {
if (!args || typeof args !== "object") return args;
const input = args as { action?: string; oldAction?: string };
if (typeof input.oldAction === "string" && input.action === undefined) {
return { ...input, action: input.oldAction };
}
return args;
},
async execute(toolCallId, params, signal, onUpdate, ctx) {
// Check for cancellation
if (signal?.aborted) {
return { content: [{ type: "text", text: "Cancelled" }] };
}
// Stream progress updates
onUpdate?.({
content: [{ type: "text", text: "Working..." }],
details: { progress: 50 },
});
// Run commands via pi.exec (captured from extension closure)
const result = await pi.exec("some-command", [], { signal });
// Return result
return {
content: [{ type: "text", text: "Done" }], // Sent to LLM
details: { data: result }, // For rendering & state
// usage: nestedModelResponse.usage, // Optional nested LLM usage
// Optional: stop after this tool batch when every finalized tool result
// in the batch also returns terminate: true.
terminate: true,
};
},
// Optional: Custom rendering
renderCall(args, theme, context) { ... },
renderResult(result, options, theme, context) { ... },
});Nutzungsabrechnung: Wenn ein Tool verschachtelte LLM-Aufrufe durchführt, geben Sie deren kombinierte Usage als usage zurück. Pi behält es im Tool-Ergebnis bei und fügt es in die Fußzeile ein, /session und RPC Sitzungssummen. tool_result Handler können diesen Wert überprüfen oder ersetzen.
Signalisierungsfehler: Um eine Toolausführung als fehlgeschlagen zu markieren (setzt isError: true für das Ergebnis und meldet es an das LLM), werfen Sie einen Fehler von execute aus. Durch die Rückgabe eines Werts wird niemals das Fehlerflag gesetzt, unabhängig davon, welche Eigenschaften Sie in das Rückgabeobjekt aufnehmen.
Vorzeitige Beendigung: Geben Sie terminate: true von execute() zurück, um darauf hinzuweisen, dass der automatische Folge-LLM-Aufruf nach der aktuellen Werkzeugcharge übersprungen werden sollte. Dies wird nur wirksam, wenn jedes finalisierte Werkzeugergebnis in diesem Stapel beendet wird. Unter examples/extensions/structured-output.ts finden Sie ein Minimalbeispiel, bei dem der Agent mit einem abschließenden Toolaufruf mit strukturierter Ausgabe endet.
// Correct: throw to signal an error
async execute(toolCallId, params) {
if (!isValid(params.input)) {
throw new Error(`Invalid input: ${params.input}`);
}
return { content: [{ type: "text", text: "OK" }], details: {} };
}Wichtig: Verwenden Sie StringEnum von @earendil-works/pi-ai für String-Aufzählungen. Type.Union/Type.Literal funktioniert nicht mit Googles API.
Argumentvorbereitung: prepareArguments(args) ist optional. Falls definiert, wird es vor der Schemavalidierung und vor execute() ausgeführt. Verwenden Sie es, um eine ältere akzeptierte Eingabeform nachzuahmen, wenn Pi eine ältere Sitzung fortsetzt, deren gespeicherte Tool-Aufrufargumente nicht mehr mit dem aktuellen Schema übereinstimmen. Geben Sie das Objekt zurück, das anhand von parameters validiert werden soll. Halten Sie das öffentliche Schema streng. Fügen Sie keine veralteten Kompatibilitätsfelder zu parameters hinzu, nur damit alte fortgesetzte Sitzungen weiterhin funktionieren.
Beispiel: Eine ältere Sitzung kann einen edit-Tool-Aufruf mit oldText und newText der obersten Ebene enthalten, während das aktuelle Schema nur edits: [{ oldText, newText }] akzeptiert.
pi.registerTool({
name: "edit",
label: "Edit",
description: "Edit a single file using exact text replacement",
parameters: Type.Object({
path: Type.String(),
edits: Type.Array(
Type.Object({
oldText: Type.String(),
newText: Type.String(),
}),
),
}),
prepareArguments(args) {
if (!args || typeof args !== "object") return args;
const input = args as {
path?: string;
edits?: Array<{ oldText: string; newText: string }>;
oldText?: unknown;
newText?: unknown;
};
if (typeof input.oldText !== "string" || typeof input.newText !== "string") {
return args;
}
return {
...input,
edits: [...(input.edits ?? []), { oldText: input.oldText, newText: input.newText }],
};
},
async execute(toolCallId, params, signal, onUpdate, ctx) {
// params now matches the current schema
return {
content: [{ type: "text", text: `Applying ${params.edits.length} edit block(s)` }],
details: {},
};
},
});Überschreiben integrierter Tools
Extensions kann integrierte Tools (read, bash, edit, write, grep, find, ls) überschreiben, indem ein Tool mit demselben Namen registriert wird. Im interaktiven Modus wird in diesem Fall eine Warnung angezeigt.
# Extension's read tool replaces built-in read
pi -e ./tool-override.tsAlternativ können Sie --no-builtin-tools verwenden, um ohne integrierte Tools zu starten und gleichzeitig die Erweiterungstools aktiviert zu lassen:
# No built-in tools, only extension tools
pi --no-builtin-tools -e ./my-extension.tsUnter examples/extensions/tool-override.ts finden Sie ein vollständiges Beispiel, das read mit Protokollierung und Zugriffskontrolle überschreibt.
Rendering: Die integrierte Renderer-Vererbung wird pro Slot aufgelöst. Ausführungsüberschreibung und Rendering-Überschreibung sind unabhängig voneinander. Wenn Ihre Außerkraftsetzung renderCall weglässt, wird das integrierte renderCall verwendet. Wenn Ihre Außerkraftsetzung renderResult weglässt, wird das integrierte renderResult verwendet. Wenn Ihre Überschreibung beides weglässt, wird automatisch der integrierte Renderer verwendet (Syntaxhervorhebung, Unterschiede usw.). Dadurch können Sie integrierte Tools für die Protokollierung oder Zugriffskontrolle umschließen, ohne die Benutzeroberfläche neu implementieren zu müssen.
Prompt-Metadaten: promptSnippet und promptGuidelines werden nicht vom integrierten Tool geerbt. Wenn Ihre Außerkraftsetzung diese Eingabeaufforderungsanweisungen beibehalten soll, definieren Sie sie explizit in der Außerkraftsetzung.
Ihre Implementierung muss mit der genauen Ergebnisform übereinstimmen, einschließlich des Typs details. Die Benutzeroberfläche und die Sitzungslogik hängen für das Rendering und die Statusverfolgung von diesen Formen ab.
Integrierte Tool-Implementierungen:
- read.ts -
ReadToolDetails - bash.ts -
BashToolDetails - edit.ts
- write.ts
- grep.ts -
GrepToolDetails - find.ts -
FindToolDetails - ls.ts -
LsToolDetails
Remote-Ausführung
Integrierte Tools unterstützen steckbare Vorgänge zum Delegieren an Remote-Systeme (SSH, Container usw.):
import { createReadTool, createBashTool, type ReadOperations } from "@earendil-works/pi-coding-agent";
// Create tool with custom operations
const remoteRead = createReadTool(cwd, {
operations: {
readFile: (path) => sshExec(remote, `cat ${path}`),
access: (path) => sshExec(remote, `test -r ${path}`).then(() => {}),
}
});
// Register, checking flag at execution time
pi.registerTool({
...remoteRead,
async execute(id, params, signal, onUpdate, _ctx) {
const ssh = getSshConfig();
if (ssh) {
const tool = createReadTool(cwd, { operations: createRemoteOps(ssh) });
return tool.execute(id, params, signal, onUpdate);
}
return localRead.execute(id, params, signal, onUpdate);
},
});Betriebsschnittstellen: ReadOperations, WriteOperations, EditOperations, BashOperations, LsOperations, GrepOperations, FindOperations
Für user_bash können Erweiterungen das lokale Shell-Backend von pi über createLocalBashOperations() wiederverwenden, anstatt das Spawnen lokaler Prozesse, die Shell-Auflösung und die Beendigung des Prozessbaums neu zu implementieren.
Das bash-Tool unterstützt auch einen Spawn-Hook, um den Befehl, cwd oder env vor der Ausführung anzupassen:
import { createBashTool } from "@earendil-works/pi-coding-agent";
const bashTool = createBashTool(cwd, {
spawnHook: ({ command, cwd, env }) => ({
command: `source ~/.profile\n${command}`,
cwd: `/mnt/sandbox${cwd}`,
env: { ...env, CI: "1" },
}),
});createBashTool() macht die aktuelle Sitzung Befehlen über PI_SESSION_ID, PI_SESSION_FILE, PI_PROVIDER, PI_MODEL und PI_REASONING_LEVEL zugänglich. Die Injektion erfolgt vor spawnHook, sodass Hooks diese Werte in env erhalten und sie beibehalten, wenn sie die vorhandene Umgebung wie oben verbreiten. Stellen Sie exposeSessionEnvironment: false ein, um sie zu deaktivieren:
const bashTool = createBashTool(cwd, {
exposeSessionEnvironment: false,
});Siehe Bash tool session environment für Variablensemantik. Siehe examples/extensions/ssh.ts für ein vollständiges SSH-Beispiel mit --ssh-Flag.
Ausgabekürzung
Tools MÜSSEN ihre Ausgabe abschneiden, um eine Überlastung des LLM-Kontexts zu vermeiden. Große Ausgaben können Folgendes verursachen:
- Kontextüberlauffehler (Eingabeaufforderung zu lang)
- Verdichtungsfehler
- Beeinträchtigte Modellleistung
Das integrierte Limit beträgt 50 KB (~10.000 Token) und 2000 Zeilen, je nachdem, was zuerst erreicht wird. Verwenden Sie die exportierten Kürzungsdienstprogramme:
import {
truncateHead, // Keep first N lines/bytes (good for file reads, search results)
truncateTail, // Keep last N lines/bytes (good for logs, command output)
truncateLine, // Truncate a single line to maxBytes with ellipsis
formatSize, // Human-readable size (e.g., "50KB", "1.5MB")
DEFAULT_MAX_BYTES, // 50KB
DEFAULT_MAX_LINES, // 2000
} from "@earendil-works/pi-coding-agent";
async execute(toolCallId, params, signal, onUpdate, ctx) {
const output = await runCommand();
// Apply truncation
const truncation = truncateHead(output, {
maxLines: DEFAULT_MAX_LINES,
maxBytes: DEFAULT_MAX_BYTES,
});
let result = truncation.content;
if (truncation.truncated) {
// Write full output to temp file
const tempFile = writeTempFile(output);
// Inform the LLM where to find complete output
result += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
result += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;
result += ` Full output saved to: ${tempFile}]`;
}
return { content: [{ type: "text", text: result }] };
}Wichtige Punkte:
- Verwenden Sie
truncateHeadfür Inhalte, bei denen der Anfang wichtig ist (Suchergebnisse, Dateilesevorgänge). - Verwenden Sie
truncateTailfür Inhalte, bei denen es auf das Ende ankommt (Protokolle, Befehlsausgabe). - Informieren Sie das LLM immer, wenn die Ausgabe gekürzt wird und wo die Vollversion zu finden ist
- Dokumentieren Sie die Kürzungsgrenzen in der Beschreibung Ihres Tools
Unter examples/extensions/truncated-tool.ts finden Sie ein vollständiges Beispiel für das Umschließen von rg (ripgrep) mit korrekter Kürzung.
Mehrere Tools
Eine Erweiterung kann mehrere Tools mit gemeinsamem Status registrieren:
export default function (pi: ExtensionAPI) {
let connection = null;
pi.registerTool({ name: "db_connect", ... });
pi.registerTool({ name: "db_query", ... });
pi.registerTool({ name: "db_close", ... });
pi.on("session_shutdown", async () => {
connection?.close();
});
}Benutzerdefiniertes Rendering
Tools können renderCall und renderResult für die benutzerdefinierte TUI-Anzeige bereitstellen. Siehe tui.md für die vollständige Komponente API und tool-execution.ts für die Zusammensetzung der Werkzeugreihen.
Standardmäßig ist die Werkzeugausgabe in eine Box eingebunden, die den Abstand und den Hintergrund übernimmt. Ein definiertes renderCall oder renderResult muss ein Component zurückgeben. Wenn kein Slot-Renderer definiert ist, verwendet tool-execution.ts das Fallback-Rendering für diesen Slot.
Legen Sie renderShell: "self" fest, wenn das Tool seine eigene Shell rendern soll, anstatt die Standardeinstellung Box zu verwenden. Dies ist nützlich für Tools, die eine vollständige Kontrolle über den Rahmen oder das Hintergrundverhalten benötigen, beispielsweise große Vorschauen, die nach dem Einschwingen des Tools visuell stabil bleiben müssen.
pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "Custom shell example",
parameters: Type.Object({}),
renderShell: "self",
async execute() {
return { content: [{ type: "text", text: "ok" }], details: undefined };
},
renderCall(args, theme, context) {
return new Text(theme.fg("accent", "my custom shell"), 0, 0);
},
});renderCall und renderResult erhalten jeweils ein context-Objekt mit:
args– die aktuellen Werkzeugaufrufargumentestate– gemeinsamer zeilenlokaler Zustand überrenderCallundrenderResultlastComponent– die zuvor zurückgegebene Komponente für diesen Steckplatz, falls vorhandeninvalidate()– Erneutes Rendern dieser Werkzeugreihe anforderntoolCallId,cwd,executionStarted,argsComplete,isPartial,expanded,showImages,isError
Verwenden Sie context.state für den steckplatzübergreifenden Freigabestatus. Behalten Sie Slot-lokale Caches für die zurückgegebene Komponenteninstanz bei, wenn Sie dieselbe Komponente beim Rendern wiederverwenden und mutieren möchten.
renderCall
Rendert den Toolaufruf oder Header:
import { Text } from "@earendil-works/pi-tui";
renderCall(args, theme, context) {
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
let content = theme.fg("toolTitle", theme.bold("my_tool "));
content += theme.fg("muted", args.action);
if (args.text) {
content += " " + theme.fg("dim", `"${args.text}"`);
}
text.setText(content);
return text;
}renderResult
Rendert das Werkzeugergebnis oder die Ausgabe:
renderResult(result, { expanded, isPartial }, theme, context) {
if (isPartial) {
return new Text(theme.fg("warning", "Processing..."), 0, 0);
}
if (result.details?.error) {
return new Text(theme.fg("error", `Error: ${result.details.error}`), 0, 0);
}
let text = theme.fg("success", "✓ Done");
if (expanded && result.details?.items) {
for (const item of result.details.items) {
text += "\n " + theme.fg("dim", item);
}
}
return new Text(text, 0, 0);
}Wenn ein Slot absichtlich keinen sichtbaren Inhalt hat, geben Sie eine leere Component zurück, beispielsweise eine leere Container.
Hinweise zur Tastenkombination
Verwenden Sie keyHint(), um Tastenkombinationshinweise anzuzeigen, die die aktive Tastenkombinationskonfiguration berücksichtigen:
import { keyHint } from "@earendil-works/pi-coding-agent";
renderResult(result, { expanded }, theme, context) {
let text = theme.fg("success", "✓ Done");
if (!expanded) {
text += ` (${keyHint("app.tools.expand", "to expand")})`;
}
return new Text(text, 0, 0);
}Verfügbare Funktionen:
keyHint(keybinding, description)– Formatiert eine konfigurierte Tastenkombinations-ID wie"app.tools.expand"oder"tui.select.confirm"keyText(keybinding)– Gibt den roh konfigurierten Schlüsseltext für eine Tastenkombinations-ID zurückrawKeyHint(key, description)– Formatieren Sie eine Rohschlüsselzeichenfolge
Verwenden Sie namensraumbasierte Tastenkombinations-IDs:
- Coding-Agent-IDs verwenden den Namespace
app.*, zum Beispielapp.tools.expand,app.editor.external,app.session.rename - Geteilte TUI-IDs verwenden den Namespace
tui.*, zum Beispieltui.select.confirm,tui.select.cancel,tui.input.tab
Die vollständige Liste der Tastenkombinations-IDs und Standardeinstellungen finden Sie unter keybindings.md. keybindings.json verwendet dieselben Namespace-IDs.
Benutzerdefinierte Editoren und ctx.ui.custom()-Komponenten erhalten keybindings: KeybindingsManager als injiziertes Argument. Sie sollten diesen injizierten Manager direkt verwenden, anstatt getKeybindings() oder setKeybindings() aufzurufen.
Best Practices
- Verwenden Sie
Textmit Polsterung(0, 0). Die Standardbox übernimmt die Auffüllung. - Verwenden Sie
\nfür mehrzeilige Inhalte. - Behandeln Sie
isPartialfür den Streaming-Fortschritt. - Support
expandedfür Details auf Anfrage. - Halten Sie die Standardansicht kompakt.
- Lesen Sie
context.argsinrenderResult, anstatt Argumente incontext.statezu kopieren. - Verwenden Sie
context.statenur für Daten, die über Anruf- und Ergebnisslots hinweg gemeinsam genutzt werden müssen. context.lastComponentwiederverwenden, wenn dieselbe Komponenteninstanz direkt aktualisiert werden kann.- Verwenden Sie
renderShell: "self"nur, wenn die standardmäßige Box-Shell im Weg ist. Im Self-Shell-Modus ist das Tool für den Rahmen, die Polsterung und den Hintergrund selbst verantwortlich.
Zurückgreifen
Wenn ein Slot-Renderer nicht definiert ist oder Folgendes auslöst:
renderCall: Zeigt den Werkzeugnamen anrenderResult: Zeigt Rohtext voncontent
Dynamische Werkzeugbeladung
Extensions kann viele Werkzeuge registrieren, während nur ein kleiner Anfangssatz aktiv bleibt. Ein Werkzeug kann dann während der Ausführung weitere Werkzeuge mit pi.setActiveTools() hinzufügen. Pi erkennt rein additive Änderungen, zeichnet die neu verfügbaren Werkzeugnamen für dieses Werkzeugergebnis auf und wendet den aktualisierten aktiven Satz vor der nächsten Modellanforderung an.
Das funktioniert bei jedem Modell. Models mit nativer Unterstützung für verzögertes Laden behält das stabile Eingabeaufforderungspräfix bei und lädt die neuen Definitionen an der Tool-Ergebnisposition. Andere Modelle nutzen den unten beschriebenen Fallback.
Der Lebenszyklus ist:
- Registrieren Sie jedes Werkzeug mit
pi.registerTool(), damit es inpi.getAllTools()erscheint. - Lassen Sie Ladetools wie
search_toolsaktiv und durchsuchbare Tools inaktiv. - Rufen Sie während der Loader-Ausführung
pi.setActiveTools([...currentTools,...matchingTools])auf. Die Änderung muss additiv sein: Derzeit aktive Werkzeuge dürfen nicht im selben Aufruf entfernt werden. - Pi zeichnet auf, welche Werkzeuge zum Werkzeugergebnis des Laders hinzugefügt wurden.
- Vor der nächsten Modellantwort stellt Pi die hinzugefügten Definitionen mithilfe des nativen verzögerten Ladens bereit, sofern dies unterstützt wird, oder andernfalls der normalen Liste der aktiven Tools.
Sie müssen keine anbieterspezifischen Tool-Referenzen zurückgeben oder den Loader als spezielles Suchtool markieren. Der aktive Werkzeugwechsel ist das Signal. An pi.setActiveTools() übergebene Namen müssen bereits registriert sein; Unbekannte Namen werden ignoriert.
Models mit nativem verzögertem Laden
- Anthropisch
- Models: Sonett, Opus, Fable Version 4.5 oder neuer (ohne Haiku)
- Native Darstellung: Aufgeschobene Definitionen verwenden
defer_loading; Der Ladepunkt verwendettool_referenceInhalte.
- OpenAI
- Models:
gpt-5.4und neuere Familie - Native Darstellung: Pi fügt abgeschlossene Client-Elemente
tool_search_callundtool_search_outputam Ladepunkt hinzu.
- Models:
Für ein verifiziertes benutzerdefiniertes Modell oder einen Proxy kann die native Handhabung mit compat.supportsToolReferences: true für anthropic-messages oder compat.supportsToolSearch: true für openai-responses und openai-codex-responses aktiviert werden. Lassen Sie diese deaktiviert, es sei denn, der Endpunkt und das Modell akzeptieren das entsprechende native Protokoll.
Fallback-Verhalten
Bei allen anderen Modellen und Anbietern funktioniert die dynamische Aktivierung weiterhin: Pi sendet die komplette aktuell aktive Werkzeugliste normal bei der nächsten Anfrage. Das Modell kann die neu aktivierten Tools aufrufen, aber das Hinzufügen ihrer Definitionen kann dazu führen, dass das zwischengespeicherte Eingabeaufforderungspräfix des Anbieters ungültig wird.
Pi verwendet diesen sicheren Fallback auch, wenn der aktive Satz nicht rein additiv ist, beispielsweise beim Ersetzen einer Werkzeuggruppe durch eine andere. Daher funktionieren Werkzeugentfernungen, sie verwenden jedoch kein verzögertes Laden.
Um das beste Cache-Verhalten zu erzielen, lassen Sie das Loader-Tool während der gesamten Sitzung aktiv und fügen Sie Tools hinzu, anstatt den aktiven Satz zu ersetzen. Beachten Sie außerdem, dass durch die Aktivierung eines Tools mit promptSnippet oder promptGuidelines die Systemeingabeaufforderung neu erstellt wird; Diese systembedingte Änderung kann das Präfix ungültig machen, selbst wenn der Anbieter verzögerte Schemata unterstützt. Langsam geladene Tools sollten sich normalerweise auf ihr Tool description verlassen und nur aktive Eingabeaufforderungsmetadaten weglassen.
Beispiel für ein Suchtool
Die folgende Erweiterung registriert zwei durchsuchbare Tools, entfernt sie aus dem anfänglichen aktiven Satz und behält nur search_tools als Ladeprogramm bei. Das Beispiel verwendet einen einfachen Schlüsselwortabgleich, aber die Suchimplementierung könnte BM25, Einbettungen, einen Remote-Katalog oder projektspezifisches Routing verwenden.
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
const SEARCHABLE_TOOL_NAMES = new Set(["lookup_weather", "search_issues"]);
export default function (pi: ExtensionAPI) {
pi.registerTool({
name: "lookup_weather",
label: "Lookup Weather",
description: "Look up the current weather for a city",
parameters: Type.Object({ city: Type.String() }),
async execute(_toolCallId, params) {
return {
content: [{ type: "text", text: `Weather for ${params.city}: sunny` }],
details: {},
};
},
});
pi.registerTool({
name: "search_issues",
label: "Search Issues",
description: "Search project issues by keyword",
parameters: Type.Object({ query: Type.String() }),
async execute(_toolCallId, params) {
return {
content: [{ type: "text", text: `No open issues matching ${params.query}` }],
details: {},
};
},
});
pi.registerTool({
name: "search_tools",
label: "Search Tools",
description: "Search for and enable tools relevant to a task",
promptSnippet: "Search for additional tools when the active tools cannot perform the task",
promptGuidelines: [
"Use search_tools when a task requires a capability that is not currently available.",
],
parameters: Type.Object({
query: Type.String({ description: "Capability or task to search for" }),
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 10 })),
}),
async execute(_toolCallId, params) {
const terms = params.query.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
const matches = pi.getAllTools()
.filter((tool) => SEARCHABLE_TOOL_NAMES.has(tool.name))
.map((tool) => ({
tool,
score: terms.reduce(
(score, term) =>
score + (`${tool.name} ${tool.description}`.toLowerCase().includes(term) ? 1 : 0),
0,
),
}))
.filter((match) => match.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, params.limit ?? 3)
.map((match) => match.tool.name);
if (matches.length === 0) {
return {
content: [{ type: "text", text: `No tools found for: ${params.query}` }],
details: { matches: [] },
};
}
const active = pi.getActiveTools();
const added = matches.filter((name) => !active.includes(name));
pi.setActiveTools([...new Set([...active, ...added])]);
return {
content: [{
type: "text",
text: added.length > 0
? `Loaded tools: ${added.join(", ")}`
: `Matching tools already active: ${matches.join(", ")}`,
}],
details: { matches, added },
};
},
});
pi.on("session_start", () => {
// Keep searchable tools registered but initially inactive. Preserve built-ins
// and tools owned by other extensions, and keep the loader itself active.
const initialTools = pi.getActiveTools().filter(
(name) => !SEARCHABLE_TOOL_NAMES.has(name),
);
pi.setActiveTools([...new Set([...initialTools, "search_tools"])]);
});
}Wenn search_tools eine Übereinstimmung hinzufügt, erhält das Modell diese Definition bei der unmittelbar folgenden Anfrage. Bei einem nativfähigen Modell wird die Definition nach dem Suchergebnis verankert, ohne dass das anfängliche Tool-Schema-Präfix geändert wird. Bei anderen Modellen erscheint es auf derselben folgenden Anfrage in der normalen Werkzeugliste.
Benutzerdefinierte Benutzeroberfläche
Extensions kann über ctx.ui-Methoden mit Benutzern interagieren und anpassen, wie Nachrichten/Tools gerendert werden.
Für benutzerdefinierte Komponenten siehe tui.md, das Muster zum Kopieren und Einfügen enthält für:
- Auswahldialoge (SelectList)
- Asynchrone Vorgänge mit Abbrechen (BorderedLoader)
- Einstellungen umschalten (SettingsList)
- Statusanzeigen (setStatus)
- Arbeitsmeldung, Sichtbarkeit und Anzeige während des Streamings (
setWorkingMessage,setWorkingVisible,setWorkingIndicator) - Widgets über/unter dem Editor (setWidget)
- Autovervollständigungsanbieter, die über der integrierten Schrägstrich-/Pfadvervollständigung liegen (addAutocompleteProvider)
- Benutzerdefinierte Fußzeilen (setFooter)
Dialoge
// Select from options
const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);
// Confirm dialog
const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
// Text input
const name = await ctx.ui.input("Name:", "placeholder");
// Multi-line editor
const text = await ctx.ui.editor("Edit:", "prefilled text");
// Notification (non-blocking)
ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"Zeitgesteuerte Dialoge mit Countdown
Dialoge unterstützen eine timeout-Option, die automatisch mit einer Live-Countdown-Anzeige geschlossen wird:
// Dialog shows "Title (5s)" → "Title (4s)" → ... → auto-dismisses at 0
const confirmed = await ctx.ui.confirm(
"Timed Confirmation",
"This dialog will auto-cancel in 5 seconds. Confirm?",
{ timeout: 5000 }
);
if (confirmed) {
// User confirmed
} else {
// User cancelled or timed out
}Rückgabewerte bei Timeout:
select()gibtundefinedzurückconfirm()gibtfalsezurückinput()gibtundefinedzurück
Manuelle Entlassung mit AbortSignal
Für mehr Kontrolle (z. B. um Timeout von Benutzerabbruch zu unterscheiden) verwenden Sie AbortSignal:
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const confirmed = await ctx.ui.confirm(
"Timed Confirmation",
"This dialog will auto-cancel in 5 seconds. Confirm?",
{ signal: controller.signal }
);
clearTimeout(timeoutId);
if (confirmed) {
// User confirmed
} else if (controller.signal.aborted) {
// Dialog timed out
} else {
// User cancelled (pressed Escape or selected "No")
}Vollständige Beispiele finden Sie unter examples/extensions/timed-confirm.ts.
Widgets, Status und Fußzeile
// Status in footer (persistent until cleared)
ctx.ui.setStatus("my-ext", "Processing...");
ctx.ui.setStatus("my-ext", undefined); // Clear
// Working loader (shown during streaming)
ctx.ui.setWorkingMessage("Thinking deeply...");
ctx.ui.setWorkingMessage(); // Restore default
ctx.ui.setWorkingVisible(false); // Hide the built-in working loader row entirely
ctx.ui.setWorkingVisible(true); // Show the built-in working loader row
// Working indicator (shown during streaming)
ctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg("accent", "●")] }); // Static dot
ctx.ui.setWorkingIndicator({
frames: [
ctx.ui.theme.fg("dim", "·"),
ctx.ui.theme.fg("muted", "•"),
ctx.ui.theme.fg("accent", "●"),
ctx.ui.theme.fg("muted", "•"),
],
intervalMs: 120,
});
ctx.ui.setWorkingIndicator({ frames: [] }); // Hide indicator
ctx.ui.setWorkingIndicator(); // Restore default spinner
// Widget above editor (default)
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
// Widget below editor
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"], { placement: "belowEditor" });
ctx.ui.setWidget("my-widget", (tui, theme) => new Text(theme.fg("accent", "Custom"), 0, 0));
ctx.ui.setWidget("my-widget", undefined); // Clear
// Custom footer (replaces built-in footer entirely)
ctx.ui.setFooter((tui, theme) => ({
render(width) { return [theme.fg("dim", "Custom footer")]; },
invalidate() {},
}));
ctx.ui.setFooter(undefined); // Restore built-in footer
// Terminal title
ctx.ui.setTitle("pi - my-project");
// Editor text
ctx.ui.setEditorText("Prefill text");
const current = ctx.ui.getEditorText();
// Paste into editor (triggers paste handling, including collapse for large content)
ctx.ui.pasteToEditor("pasted content");
// Stack custom autocomplete behavior on top of the built-in provider
ctx.ui.addAutocompleteProvider((current) => ({
triggerCharacters: ["#"],
async getSuggestions(lines, line, col, options) {
const beforeCursor = (lines[line] ?? "").slice(0, col);
const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/);
if (!match) {
return current.getSuggestions(lines, line, col, options);
}
return {
prefix: `#${match[1] ?? ""}`,
items: [{ value: "#2983", label: "#2983", description: "Extension API for autocomplete" }],
};
},
applyCompletion(lines, line, col, item, prefix) {
return current.applyCompletion(lines, line, col, item, prefix);
},
shouldTriggerFileCompletion(lines, line, col) {
return current.shouldTriggerFileCompletion?.(lines, line, col) ?? true;
},
}));
// Tool output expansion
const wasExpanded = ctx.ui.getToolsExpanded();
ctx.ui.setToolsExpanded(true);
ctx.ui.setToolsExpanded(wasExpanded);
// Custom editor (vim mode, emacs mode, etc.)
ctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));
const currentEditor = ctx.ui.getEditorComponent();
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
new WrappedEditor(tui, theme, keybindings, currentEditor?.(tui, theme, keybindings))
);
ctx.ui.setEditorComponent(undefined); // Restore default editor
// Theme management (see themes.md for creating themes)
const themes = ctx.ui.getAllThemes(); // [{ name: "dark", path: "/..." | undefined }, ...]
const lightTheme = ctx.ui.getTheme("light"); // Load without switching
const result = ctx.ui.setTheme("light"); // Switch by name
if (!result.success) {
ctx.ui.notify(`Failed: ${result.error}`, "error");
}
ctx.ui.setTheme(lightTheme!); // Or switch by Theme object
ctx.ui.theme.fg("accent", "styled text"); // Access current themeBenutzerdefinierte Arbeitsindikatorrahmen werden wörtlich wiedergegeben. Wenn Sie Farben wünschen, fügen Sie diese selbst zu den Rahmenleisten hinzu, zum Beispiel mit ctx.ui.theme.fg(...).
Automatische Vervollständigung Providers
Verwenden Sie ctx.ui.addAutocompleteProvider(), um benutzerdefinierte Autovervollständigungslogik über den integrierten Schrägstrichbefehl und den Pfadanbieter zu stapeln. Legen Sie triggerCharacters für benutzerdefinierte natürliche Auslöser wie Verwenden Sie ctx.ui.addAutocompleteProvider(), um benutzerdefinierte Autovervollständigungslogik über den integrierten Schrägstrichbefehl und den Pfadanbieter zu stapeln. Legen Sie triggerCharacters` für benutzerdefinierte natürliche Auslöser wie fest.
Typisches Muster:
- Überprüfen Sie den Text vor dem Cursor
- Geben Sie Ihre eigenen Vorschläge zurück, wenn Ihre erweiterungsspezifische Syntax übereinstimmt
- andernfalls delegieren an
current.getSuggestions(...) - delegieren Sie
applyCompletion(...), es sei denn, Sie benötigen ein benutzerdefiniertes Einfügeverhalten
pi.on("session_start", (_event, ctx) => {
ctx.ui.addAutocompleteProvider((current) => ({
triggerCharacters: ["#"],
async getSuggestions(lines, cursorLine, cursorCol, options) {
const line = lines[cursorLine] ?? "";
const beforeCursor = line.slice(0, cursorCol);
const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/);
if (!match) {
return current.getSuggestions(lines, cursorLine, cursorCol, options);
}
return {
prefix: `#${match[1] ?? ""}`,
items: [
{ value: "#2983", label: "#2983", description: "Extension API for registering custom @ autocomplete providers" },
{ value: "#2753", label: "#2753", description: "Reload stale resource settings" },
],
};
},
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
},
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
},
}));
});Unter github-issue-autocomplete.ts finden Sie ein vollständiges Beispiel, das die neuesten offenen GitHub-Probleme mit gh issue list vorlädt und sie lokal filtert, um eine schnelle #...-Vervollständigung zu ermöglichen. Es erfordert GitHub CLI (gh) und einen GitHub Repository-Checkout.
Benutzerdefinierte Komponenten
Für eine komplexe Benutzeroberfläche verwenden Sie ctx.ui.custom(). Dadurch wird der Editor vorübergehend durch Ihre Komponente ersetzt, bis done() aufgerufen wird:
import { Text, Component } from "@earendil-works/pi-tui";
const result = await ctx.ui.custom<boolean>((tui, theme, keybindings, done) => {
const text = new Text("Press Enter to confirm, Escape to cancel", 1, 1);
text.onKey = (key) => {
if (key === "return") done(true);
if (key === "escape") done(false);
return true;
};
return text;
});
if (result) {
// User pressed Enter
}Der Rückruf erhält:
tui– TUI Instanz (für Bildschirmabmessungen, Fokusverwaltung)theme– Aktuelles Thema für das Stylingkeybindings– App-Tastenkombinationsmanager (zum Überprüfen von Verknüpfungen)done(value)– Aufruf zum Schließen der Komponente und Rückgabewert
Siehe tui.md für die vollständige Komponente API.
Overlay-Modus (experimentell)
Übergeben Sie { overlay: true }, um die Komponente als schwebendes Modal über dem vorhandenen Inhalt darzustellen, ohne den Bildschirm zu löschen:
const result = await ctx.ui.custom<string | null>(
(tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
{ overlay: true }
);Übergeben Sie für erweiterte Positionierung (Anker, Ränder, Prozentsätze, reaktionsfähige Sichtbarkeit) overlayOptions. Verwenden Sie onHandle, um Fokus oder Sichtbarkeit programmgesteuert zu steuern:
const result = await ctx.ui.custom<string | null>(
(tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
{
overlay: true,
overlayOptions: { anchor: "top-right", width: "50%", margin: 2 },
onHandle: (handle) => {
handle.focus(); // focus this overlay and bring it to the visual front
// handle.unfocus({ target: editorComponent }); // release input to a specific component
// handle.setHidden(true/false); // toggle visibility
// handle.hide(); // permanently remove
}
}
);Ein fokussiertes sichtbares Overlay kann Eingaben zurückfordern, nachdem die vorübergehende benutzerdefinierte Benutzeroberfläche ohne Overlay geschlossen wird. Wenn Sie absichtlich möchten, dass eine andere Komponente die Eingabe beibehält, während die Überlagerung sichtbar bleibt, rufen Sie handle.unfocus({ target }) auf. Das Übergeben von { target: null } gibt die Überlagerung frei, ohne eine andere Komponente zu fokussieren.
Siehe tui.md für die vollständigen OverlayOptions und OverlayHandle API und overlay-qa-tests.ts für Beispiele.
Benutzerdefinierter Editor
Ersetzen Sie den Haupteingabeeditor durch eine benutzerdefinierte Implementierung (VIM-Modus, Emacs-Modus usw.):
import { CustomEditor, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { matchesKey } from "@earendil-works/pi-tui";
class VimEditor extends CustomEditor {
private mode: "normal" | "insert" = "insert";
handleInput(data: string): void {
if (matchesKey(data, "escape") && this.mode === "insert") {
this.mode = "normal";
return;
}
if (this.mode === "normal" && data === "i") {
this.mode = "insert";
return;
}
super.handleInput(data); // App keybindings + text editing
}
}
export default function (pi: ExtensionAPI) {
pi.on("session_start", (_event, ctx) => {
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
new VimEditor(tui, theme, keybindings)
);
});
}Wichtige Punkte:
- Erweitern Sie
CustomEditor(nicht BasisEditor), um App-Tastenkombinationen zu erhalten (Escape zum Abbrechen, Strg+D, Modellwechsel). - Rufen Sie
super.handleInput(data)für Schlüssel an, die Sie nicht verwalten - Factory empfängt
tui,themeundkeybindingsvon der App - Verwenden Sie
ctx.ui.getEditorComponent()vorsetEditorComponent(), um den zuvor konfigurierten benutzerdefinierten Editor zu umschließen - Übergeben Sie
undefined, um den Standardwert wiederherzustellen:ctx.ui.setEditorComponent(undefined)
Um mit einer anderen Erweiterung zu komponieren, die den Editor bereits ersetzt hat, erfassen Sie die vorherige Factory, bevor Sie Ihre festlegen:
const previous = ctx.ui.getEditorComponent();
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
new MyEditor(tui, theme, keybindings, { base: previous?.(tui, theme, keybindings) })
);Ein vollständiges Beispiel mit Modusanzeige finden Sie unter tui.md Muster 7.
Nachrichten- und Eintragsrendering
Registrieren Sie einen benutzerdefinierten Renderer für Nachrichten bei Ihrem customType. Verwenden Sie Nachrichtenrenderer für Inhalte, die am LLM-Kontext teilnehmen sollen:
import { Text } from "@earendil-works/pi-tui";
pi.registerMessageRenderer("my-extension", (message, options, theme) => {
const { expanded, outputPad } = options;
let text = theme.fg("accent", `[${message.customType}] `);
text += message.content;
if (expanded && message.details) {
text += "\n" + theme.fg("dim", JSON.stringify(message.details, null, 2));
}
return new Text(text, outputPad, 0);
});Nachrichten werden über pi.sendMessage() gesendet:
pi.sendMessage({
customType: "my-extension", // Matches registerMessageRenderer
content: "Status update",
display: true, // Show in TUI
details: { ... }, // Available in renderer
});Für nur TUI-Inhalte, die nicht an das LLM gesendet werden sollen, rendern Sie stattdessen benutzerdefinierte Einträge:
pi.registerEntryRenderer("my-card", (entry, options, theme) => {
return new Text(theme.fg("accent", JSON.stringify(entry.data)));
});
pi.appendEntry("my-card", { status: "done" });Themenfarben
Alle Renderfunktionen erhalten ein theme-Objekt. Weitere Informationen zum Erstellen benutzerdefinierter Designs und der vollständigen Farbpalette finden Sie unter themes.md.
// Foreground colors
theme.fg("toolTitle", text) // Tool names
theme.fg("accent", text) // Highlights
theme.fg("success", text) // Success (green)
theme.fg("error", text) // Errors (red)
theme.fg("warning", text) // Warnings (yellow)
theme.fg("muted", text) // Secondary text
theme.fg("dim", text) // Tertiary text
// Text styles
theme.bold(text)
theme.italic(text)
theme.strikethrough(text)Zur Syntaxhervorhebung in benutzerdefinierten Tool-Renderern:
import { highlightCode, getLanguageFromPath } from "@earendil-works/pi-coding-agent";
// Highlight code with explicit language
const highlighted = highlightCode("const x = 1;", "typescript", theme);
// Auto-detect language from file path
const lang = getLanguageFromPath("/path/to/file.rs"); // "rust"
const highlighted = highlightCode(code, lang, theme);Fehlerbehandlung
- Erweiterungsfehler werden protokolliert, der Agent fährt fort
tool_callFehler blockieren das Tool (ausfallsicher)- Werkzeugfehler
executemüssen durch Werfen gemeldet werden; Der ausgegebene Fehler wird abgefangen, dem LLM mitisError: truegemeldet und die Ausführung wird fortgesetzt
Modusverhalten
| Modus | ctx.mode |
ctx.hasUI |
Notizen |
|---|---|---|---|
| Interaktiv | "tui" |
true |
Vollständig TUI mit Terminal-Rendering |
RPC (--mode rpc) |
"rpc" |
true |
Dialoge und Benachrichtigungen über das JSON-Protokoll; custom() gibt undefined zurück. Siehe rpc.md |
JSON (--mode json) |
"json" |
false |
Ereignisstrom zu stdout; UI-Methoden sind No-Ops |
Drucken (-p) |
"print" |
false |
Extensions wird ausgeführt, kann aber nicht aufgefordert werden |
Verwenden Sie ctx.mode === "tui" vor TUI-spezifischen Funktionen (custom(), Komponentenfabriken, Terminaleingabe). Verwenden Sie ctx.hasUI vor Dialog- und Benachrichtigungsmethoden, die sowohl im TUI- als auch im RPC-Modus funktionieren.
Beispielreferenz
Alle Beispiele in examples/extensions/.
| Beispiel | Beschreibung | Taste APIs |
|---|---|---|
| Werkzeuge | ||
hello.ts |
Minimale Werkzeugregistrierung | registerTool |
question.ts |
Tool mit Benutzerinteraktion | registerTool, ui.select |
questionnaire.ts |
Mehrstufiges Assistententool | registerTool, ui.custom |
todo.ts |
Zustandsbehaftetes Tool mit Persistenz | registerTool, appendEntry, renderResult, Sitzungsereignisse |
dynamic-tools.ts |
Registrieren Sie Tools nach dem Start und während Befehlen | registerTool, session_start, registerCommand |
structured-output.ts |
Endgültiges strukturiertes Ausgabetool mit terminate: true |
registerTool, Werkzeugergebnisse beenden |
truncated-tool.ts |
Beispiel für Ausgabekürzung | registerTool, truncateHead |
tool-override.ts |
Überschreiben Sie das integrierte Lesetool | registerTool (gleicher Name wie integriert) |
| Befehle | ||
pirate.ts |
Ändern Sie die Systemaufforderung pro Runde | registerCommand, before_agent_start |
summarize.ts |
Befehl zur Konversationszusammenfassung | registerCommand, ui.custom |
handoff.ts |
Anbieterübergreifende Modellübergabe | registerCommand, ui.editor, ui.custom |
qna.ts |
Fragen und Antworten mit benutzerdefinierter Benutzeroberfläche | registerCommand, ui.custom, setEditorText |
send-user-message.ts |
Benutzernachrichten einfügen | registerCommand, sendUserMessage |
reload-runtime.ts |
Befehl zum erneuten Laden und Übergabe des LLM-Tools | registerCommand, ctx.reload(), sendUserMessage |
shutdown-command.ts |
Befehl zum ordnungsgemäßen Herunterfahren | registerCommand, shutdown() |
| Veranstaltungen & Tore | ||
permission-gate.ts |
Blockieren Sie gefährliche Befehle | on("tool_call"), ui.confirm |
project-trust.ts |
Entscheiden oder verschieben Sie die Projektvertrauenswürdigkeit von einem Benutzer/einer globalen oder CLI-Erweiterung | on("project_trust"), Vertrauens-UI, erforderliches Vertrauensergebnis |
protected-paths.ts |
Schreibvorgänge in bestimmte Pfade blockieren | on("tool_call") |
confirm-destructive.ts |
Bestätigen Sie Sitzungsänderungen | on("session_before_switch"), on("session_before_fork") |
dirty-repo-guard.ts |
Warnung vor Dirty-Git-Repo | on("session_before_*"), exec |
input-transform.ts |
Benutzereingaben transformieren | on("input") |
input-transform-streaming.ts |
Streaming-fähige Eingabetransformation | on("input"), streamingBehavior |
model-status.ts |
React für Modelländerungen | on("model_select"), setStatus |
provider-payload.ts |
Untersuchen Sie Nutzlasten und Antwortheader des Anbieters | on("before_provider_request"), on("after_provider_response") |
system-prompt-header.ts |
Systemaufforderungsinformationen anzeigen | on("agent_start"), getSystemPrompt |
claude-rules.ts |
Laden Sie Regeln aus Dateien | on("session_start"), on("before_agent_start") |
prompt-customizer.ts |
Fügen Sie mit systemPromptOptions eine kontextbezogene Werkzeugführung hinzu |
on("before_agent_start"), BuildSystemPromptOptions |
file-trigger.ts |
File Watcher löst Meldungen aus | sendMessage |
| Verdichtung & Sitzungen | ||
custom-compaction.ts |
Zusammenfassung der benutzerdefinierten Komprimierung | on("session_before_compact") |
trigger-compact.ts |
Komprimierung manuell auslösen | compact() |
git-checkpoint.ts |
Git in Runden verstauen | on("turn_start"), on("session_before_fork"), exec |
git-merge-and-resolve.ts |
Konflikte abrufen, zusammenführen und lösen | on("agent_end"), exec, sendUserMessage |
auto-commit-on-exit.ts |
Commit beim Herunterfahren | on("session_shutdown"), exec |
| UI-Komponenten | ||
status-line.ts |
Statusanzeige für die Fußzeile | setStatus, Sitzungsereignisse |
working-indicator.ts |
Passen Sie die Streaming-Arbeitsanzeige an | setWorkingIndicator, registerCommand |
github-issue-autocomplete.ts |
Fügen Sie #1234 Problemabschlüsse zusätzlich zur integrierten automatischen Vervollständigung hinzu, indem Sie die letzten offenen Probleme von gh issue list vorab laden |
addAutocompleteProvider, on("session_start"), exec |
custom-footer.ts |
Fußzeile vollständig ersetzen | registerCommand, setFooter |
custom-header.ts |
Ersetzen Sie den Start-Header | on("session_start"), setHeader |
modal-editor.ts |
Modaler Editor im Vim-Stil | setEditorComponent, CustomEditor |
rainbow-editor.ts |
Benutzerdefiniertes Editor-Styling | setEditorComponent |
widget-placement.ts |
Widget über/unter dem Editor | setWidget |
overlay-test.ts |
Overlay-Komponenten | ui.custom mit Overlay-Optionen |
overlay-qa-tests.ts |
Umfangreiche Overlay-Tests | ui.custom, alle Overlay-Optionen |
notify.ts |
Einfache Benachrichtigungen | ui.notify |
timed-confirm.ts |
Dialoge mit Timeout | ui.confirm mit Timeout/Signal |
mac-system-theme.ts |
Thema automatisch wechseln | setTheme, exec |
| Komplex Extensions | ||
plan-mode/ |
Vollständige Implementierung des Planmodus | Alle Ereignistypen, registerCommand, registerShortcut, registerFlag, setStatus, setWidget, sendMessage, setActiveTools |
preset.ts |
Speicherbare Voreinstellungen (Modell, Werkzeuge, Denken) | registerCommand, registerShortcut, registerFlag, setModel, setActiveTools, setThinkingLevel, appendEntry |
tools.ts |
Schalten Sie die Benutzeroberfläche für Tools ein/aus | registerCommand, setActiveTools, SettingsList, Sitzungsereignisse |
| Remote & Sandbox | ||
ssh.ts |
SSH Fernausführung | registerFlag, on("user_bash"), on("before_agent_start"), Werkzeugoperationen |
interactive-shell.ts |
Persistente Shell-Sitzung | on("user_bash") |
sandbox/ |
Ausführung von Sandbox-Tools | Werkzeugoperationen |
gondolin/ |
Leiten Sie integrierte Tools und !-Befehle in eine Gondolin-Mikro-VM weiter |
Werkzeugoperationen, integrierte Werkzeugüberschreibungen, on("user_bash") |
subagent/ |
Unteragenten erzeugen | registerTool, exec |
| Spiele | ||
snake.ts |
Schlangenspiel | registerCommand, ui.custom, Tastaturbedienung |
space-invaders.ts |
Space Invaders-Spiel | registerCommand, ui.custom |
doom-overlay/ |
Untergang im Overlay | ui.custom mit Overlay |
| Providers | ||
custom-provider-anthropic/ |
Benutzerdefinierter Anthropic-Proxy | registerProvider |
custom-provider-gitlab-duo/ |
GitLab Duo-Integration | registerProvider mit OAuth |
| Nachrichten und Kommunikation | ||
message-renderer.ts |
Benutzerdefinierte Nachrichtenwiedergabe | registerMessageRenderer, sendMessage |
entry-renderer.ts |
TUI-nur benutzerdefiniertes Eintragsrendering | registerEntryRenderer, appendEntry |
event-bus.ts |
Ereignisse zwischen Erweiterungen | pi.events |
| Sitzungsmetadaten | ||
session-name.ts |
Benennen Sie Sitzungen für den Selektor | setSessionName, getSessionName |
bookmark.ts |
Lesezeicheneinträge für /tree | setLabel |
| Verschiedenes | ||
inline-bash.ts |
Inline bash in Werkzeugaufrufen | on("tool_call") |
bash-spawn-hook.ts |
Passen Sie bash command, cwd und env vor der Ausführung an | createBashTool, spawnHook |
with-deps/ |
Erweiterung mit npm Abhängigkeiten | Paketstruktur mit package.json |