Konfiguration, Anpassung, Plattform-Einrichtung und API-Referenzen für Pi.

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 Sie pi -e./path.ts nur für Schnelltests. Extensions an automatisch erkannten Orten kann mit /reload im 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.ui auffordern (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 über pi.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, sudo usw.)
  • 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

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.ts

Erweiterungsstandorte

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.ts

Verzeichnis 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 module

Paket 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_shutdown

Startup-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_start und message_end werden für Benutzer-, Assistenten- und ToolResult-Nachrichten ausgelöst.
  • message_update wird 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 bleiben role.
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_start wird während der Preflight-Phase in der Reihenfolge der Hilfsquellen ausgegeben
  • tool_execution_update Ereignisse können sich über mehrere Tools hinweg verschachteln
  • tool_execution_end wird in der Reihenfolge der Werkzeugvervollständigung ausgegeben, nachdem jedes Werkzeug fertiggestellt wurde
  • Letzte toolResult Nachrichtenereignisse 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.input wirken 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_call steuern Blockierung über { block: true, reason?: string, terminate?: boolean }
  • terminate gilt 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, isError oder usage); 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:

  1. Erweiterungsbefehle (/cmd) werden zuerst überprüft. Wenn sie gefunden werden, wird der Handler ausgeführt und das Eingabeereignis wird übersprungen
  2. input Ereignisfeuer – können abgefangen, transformiert oder verarbeitet werden
  3. Wenn nicht behandelt: Fertigkeitsbefehle (/skill:name) werden auf Fertigkeitsinhalte erweitert
  4. Wenn nicht behandelt: prompt templates (/template) auf Vorlageninhalt erweitert
  5. Die Agentenverarbeitung beginnt (before_agent_start usw.)
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 fortfahren
  • handled – 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 ID

ctx.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 signal akzeptieren
  • Datei- oder Prozesshilfsprogramme, die AbortSignal akzeptieren

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_start spiegelt dies verkettete System-Prompt-Änderungen wider, die bisher für die aktuelle Runde vorgenommen wurden.
  • Spätere context-Nachrichtenmutationen sind nicht enthalten.
  • before_provider_request Payload-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 Sitzungsheader
  • setup: mutiert SessionManager der neuen Sitzung, bevor withSession ausgeführt wird
  • withSession: Führen Sie die Arbeit nach dem Wechsel in einem neuen Ersetzungssitzungskontext aus. Verwenden Sie nicht den erfassten alten pi / Befehl ctx; 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 her
  • position: "at" dupliziert den aktiven Pfad durch den ausgewählten Eintrag, ohne den Editortext wiederherzustellen
  • withSession: Führen Sie die Arbeit nach dem Wechsel in einem neuen Ersetzungssitzungskontext aus. Verwenden Sie nicht den erfassten alten pi / Befehl ctx; 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 soll
  • customInstructions: Benutzerdefinierte Anweisungen für die Zusammenfassung
  • replaceInstructions: Wenn wahr, ersetzt customInstructions die Standardaufforderung, anstatt angehängt zu werden
  • label: 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:

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:

  • withSession wird erst ausgeführt, nachdem die alte Sitzung session_shutdown ausgegeben hat, die alte Laufzeit abgebaut wurde, die Ersatzsitzung neu gebunden wurde und die neue Erweiterungsinstanz bereits session_start empfangen 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 withSession startet.
  • Erfasste alte pi / alte Befehls-ctx sitzungsgebundene Objekte sind nach dem Ersetzen veraltet und werden bei Verwendung ausgelöst. Verwenden Sie für sitzungsgebundene Arbeit nur das an withSession übergebene ctx.
  • Zuvor extrahierte Rohobjekte liegen weiterhin in Ihrer Verantwortung. Wenn Sie beispielsweise const sm = ctx.sessionManager vor dem Ersetzen erfassen, ist sm immer noch das alte SessionManager-Objekt. Nach dem Austausch nicht wiederverwenden.
  • Der Code in withSession sollte davon ausgehen, dass jeder von Ihrem session_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() gibt session_shutdown für die aktuelle Erweiterungslaufzeit aus
  • Anschließend werden die Ressourcen neu geladen und session_start mit reason: "reload" und resources_discover mit 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"
  • isStreamingtrue für teilweise Assistentenaktualisierungen; false für Benutzer, abgeschlossenen Assistenten und wiederhergestellte Nachrichten
  • availableWidth – 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.killed

pi.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-only

pi.getAllTools() gibt name, description, parameters, promptGuidelines und sourceInfo zurück.

Typische sourceInfo.source-Werte:

  • builtin für integrierte Werkzeuge
  • sdk für über createAgentSession({ 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_VAR oder ${ENV_VAR}) oder führendes !command. Erforderlich beim Definieren von Modellen (sofern oauth nicht angegeben). $ maskiert ``apiKey - API key Literal, Umgebungsinterpolation ($ENV_VARoder${ENV_VAR}) oder führendes !command. Erforderlich beim Definieren von Modellen (sofern oauthnicht 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 Header Authorization: Bearer automatisch hinzugefügt.
  • models – Array von Modelldefinitionen. Falls bereitgestellt, ersetzt es alle vorhandenen Modelle für diesen Anbieter. Modelldefinitionen können baseUrl festlegen, 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.stored enthält den persistenten Anbieter-Snapshot; Verwenden Sie generationsüberprüft context.publish({ persist: entry }) nur, wenn aktualisierte Katalogdaten bestehen bleiben sollen. Verwenden Sie persist: 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.ts

Alternativ 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.ts

Unter 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:

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 truncateHead für Inhalte, bei denen der Anfang wichtig ist (Suchergebnisse, Dateilesevorgänge).
  • Verwenden Sie truncateTail fü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 Werkzeugaufrufargumente
  • state – gemeinsamer zeilenlokaler Zustand über renderCall und renderResult
  • lastComponent – die zuvor zurückgegebene Komponente für diesen Steckplatz, falls vorhanden
  • invalidate() – Erneutes Rendern dieser Werkzeugreihe anfordern
  • toolCallId, 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ück
  • rawKeyHint(key, description) – Formatieren Sie eine Rohschlüsselzeichenfolge

Verwenden Sie namensraumbasierte Tastenkombinations-IDs:

  • Coding-Agent-IDs verwenden den Namespace app.*, zum Beispiel app.tools.expand, app.editor.external, app.session.rename
  • Geteilte TUI-IDs verwenden den Namespace tui.*, zum Beispiel tui.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 Text mit Polsterung (0, 0). Die Standardbox übernimmt die Auffüllung.
  • Verwenden Sie \n für mehrzeilige Inhalte.
  • Behandeln Sie isPartial für den Streaming-Fortschritt.
  • Support expanded für Details auf Anfrage.
  • Halten Sie die Standardansicht kompakt.
  • Lesen Sie context.args in renderResult, anstatt Argumente in context.state zu kopieren.
  • Verwenden Sie context.state nur für Daten, die über Anruf- und Ergebnisslots hinweg gemeinsam genutzt werden müssen.
  • context.lastComponent wiederverwenden, 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 an
  • renderResult: Zeigt Rohtext von content

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:

  1. Registrieren Sie jedes Werkzeug mit pi.registerTool(), damit es in pi.getAllTools() erscheint.
  2. Lassen Sie Ladetools wie search_tools aktiv und durchsuchbare Tools inaktiv.
  3. 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.
  4. Pi zeichnet auf, welche Werkzeuge zum Werkzeugergebnis des Laders hinzugefügt wurden.
  5. 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 verwendet tool_reference Inhalte.
  • OpenAI
    • Models: gpt-5.4 und neuere Familie
    • Native Darstellung: Pi fügt abgeschlossene Client-Elemente tool_search_call und tool_search_output am Ladepunkt hinzu.

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() gibt undefined zurück
  • confirm() gibt false zurück
  • input() gibt undefined zurü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 theme

Benutzerdefinierte 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 Styling
  • keybindings – 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 Basis Editor), 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, theme und keybindings von der App
  • Verwenden Sie ctx.ui.getEditorComponent() vor setEditorComponent(), 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_call Fehler blockieren das Tool (ausfallsicher)
  • Werkzeugfehler execute müssen durch Werfen gemeldet werden; Der ausgegebene Fehler wird abgefangen, dem LLM mit isError: true gemeldet 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