Saltearse al contenido

Extensiones

pi puede crear extensiones. Pídale que cree uno para su caso de uso.

Las extensiones son módulos de TypeScript que amplían el comportamiento de pi. Pueden suscribirse a eventos del ciclo de vida, registrar herramientas personalizadas a las que puede llamar el LLM, agregar comandos y más.

Ubicación para /reload: Coloque extensiones en ~/.pi/agent/extensions/ (global) o .pi/extensions/ (proyecto-local) para el descubrimiento automático. Utilice pi -e ./path.ts sólo para pruebas rápidas. Las extensiones en ubicaciones descubiertas automáticamente se pueden recargar en caliente con /reload.

Capacidades clave:

  • Herramientas personalizadas - Registrar herramientas que el LLM puede llamar a través de pi.registerTool()
  • Interceptación de eventos - Bloquear o modificar llamadas a herramientas, inyectar contexto, personalizar la compactación
  • Interacción del usuario - Solicitar a los usuarios a través de ctx.ui (seleccionar, confirmar, ingresar, notificar)
  • Componentes de UI personalizados - Componentes TUI completos con entrada de teclado a través de ctx.ui.custom() para interacciones complejas
  • Comandos personalizados - Registra comandos como /mycommand a través de pi.registerCommand()
  • Persistencia de la sesión - Estado de la tienda que sobrevive a los reinicios mediante pi.appendEntry()
  • Representación personalizada - Controle cómo aparecen las llamadas/resultados de herramientas y los mensajes en TUI

Casos de uso de ejemplo:

  • Puertas de permiso (confirmar antes de rm -rf, sudo, etc.)
  • Git checkpointing (guardar en cada turno, restaurar en la rama)
  • Protección de ruta (bloque de escritura en .env, node_modules/)
  • Compactación personalizada (resume la conversación a tu manera)
  • Resúmenes de conversaciones (ver ejemplo summarize.ts)
  • Herramientas interactivas (preguntas, asistentes, cuadros de diálogo personalizados)
  • Herramientas con estado (listas de tareas pendientes, grupos de conexiones)
  • Integraciones externas (observadores de archivos, webhooks, activadores de CI)
  • Juegos mientras esperas (ver ejemplo snake.ts)

Consulte examples/extensions/ para ver implementaciones funcionales.

Crear ~/.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");
},
});
}

Prueba con el indicador --extension (o -e):

Ventana de terminal
pi -e ./my-extension.ts

Seguridad: Las extensiones se ejecutan con todos los permisos del sistema y pueden ejecutar código arbitrario. Instálelo únicamente desde fuentes en las que confíe.

Las extensiones se descubren automáticamente desde ubicaciones confiables. Las entradas .pi/extensions locales del proyecto se cargan solo después de que el proyecto sea confiable.

UbicaciónAlcance
~/.pi/agent/extensions/*.tsGlobal (todos los proyectos)
~/.pi/agent/extensions/*/index.tsGlobal (subdirectorio)
.pi/extensions/*.tsProyecto-local
.pi/extensions/*/index.tsProyecto local (subdirectorio)

Rutas adicionales a través de settings.json:

{
"packages": [
"npm:@foo/[email protected]",
"git:github.com/user/repo@v1"
],
"extensions": [
"/path/to/local/extension.ts",
"/path/to/local/extension/dir"
]
}

Para compartir extensiones a través de npm o git como paquetes pi, consulte packages.md.

PaquetePropósito
@earendil-works/pi-coding-agentTipos de extensión (ExtensionAPI, ExtensionContext, eventos)
typeboxDefiniciones de esquemas para parámetros de herramientas
@earendil-works/pi-aiUtilidades de IA (StringEnum para enumeraciones compatibles con Google)
@earendil-works/pi-tuiComponentes TUI para renderizado personalizado

Las dependencias de npm también funcionan. Agregue un package.json al lado de su extensión (o en un directorio principal), ejecute npm install y las importaciones desde node_modules/ se resolverán automáticamente.

Para paquetes pi distribuidos instalados con pi install (npm o git), los departamentos de tiempo de ejecución deben estar en dependencies. La instalación del paquete utiliza instalaciones de producción (npm install --omit=dev) de forma predeterminada, por lo que devDependencies no está disponible en tiempo de ejecución; cuando se configura npmCommand, los paquetes git usan install simple para compatibilidad con contenedores.

Las funciones integradas de Node.js (node:fs, node:path, etc.) también están disponibles.

Una extensión exporta una función predeterminada de fábrica que recibe ExtensionAPI. La fábrica puede ser síncrona o asíncrona:

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", { ... });
}

Las extensiones se cargan a través de jiti, por lo que TypeScript funciona sin compilación.

Si la fábrica devuelve un Promise, pi lo espera antes de continuar con el inicio. Eso significa que la inicialización asíncrona se completa antes de session_start, antes de resources_discover y antes de que se vacíen los registros de proveedores en cola a través de pi.registerProvider().

Utilice una fábrica asíncrona para trabajos de inicio únicos, como obtener una configuración remota o descubrir dinámicamente modelos disponibles.

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

Este patrón hace que los modelos recuperados estén disponibles durante el inicio normal y en pi --list-models.

Las fábricas de extensiones pueden ejecutarse en invocaciones que nunca inician una sesión. No inicie recursos en segundo plano, como procesos, sockets, observadores de archivos o temporizadores, desde fábrica.

Posponga el inicio de recursos en segundo plano hasta session_start o el comando/herramienta/evento que necesita el recurso. Registre un controlador session_shutdown idempotente para cerrar cualquier recurso con ámbito de sesión que inicie.

Archivo único - el más simple, para extensiones pequeñas:

~/.pi/agent/extensions/
└── my-extension.ts

Directorio con index.ts - para extensiones de varios archivos:

~/.pi/agent/extensions/
└── my-extension/
├── index.ts # Entry point (exports default function)
├── tools.ts # Helper module
└── utils.ts # Helper module

Paquete con dependencias - para extensiones que necesitan paquetes npm:

~/.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"]
}
}

Ejecute npm install en el directorio de extensión, luego las importaciones desde node_modules/ funcionan automáticamente.

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_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 │
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" }
/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

Disparado antes de que pi decida si confiar en un proyecto con configuraciones dinámicas (.pi o .agents/skills). Se ejecuta durante el inicio y cuando el reemplazo de sesión (por ejemplo /resume) ingresa un cwd cuya confianza no se ha resuelto en el proceso actual. Solo participan extensiones de usuario/globales y extensiones CLI -e; Las extensiones locales del proyecto no se cargan hasta que se resuelve la confianza.

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

Un controlador project_trust debe devolver { trusted: "yes" | "no" | "undecided" }. Un usuario/extensión global o CLI que devuelve "yes" o "no" es propietario de la decisión; la primera decisión de sí/no gana y suprime el mensaje de confianza incorporado. Utilice remember: true para persistir en una decisión de sí/no; de lo contrario, se aplica sólo al proceso actual. Devuelva "undecided" para permitir que los controladores posteriores o el flujo de confianza integrado decidan. Marque ctx.hasUI antes de solicitarlo. Si ningún controlador devuelve sí/no, la resolución de confianza normal continúa: las decisiones trust.json guardadas se aplican primero, luego defaultProjectTrust controla si pi pregunta, confía o rechaza de forma predeterminada.

Se disparó después de session_start para que las extensiones puedan contribuir con habilidades, indicaciones y rutas de temas adicionales. La ruta de inicio utiliza reason: "startup". La recarga utiliza reason: "reload".

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

Consulte Formato de sesión para conocer los aspectos internos del almacenamiento de sesiones y la API SessionManager.

Se activa cuando se inicia, carga o recarga una sesión.

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

Se dispara antes de iniciar una nueva sesión (/new) o cambiar de sesión (/resume).

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

Después de un cambio exitoso o una acción de nueva sesión, pi emite session_shutdown para la instancia de extensión anterior, recarga y vuelve a vincular las extensiones para la nueva sesión, luego emite session_start con reason: "new" | "resume" y previousSessionFile. Realice el trabajo de limpieza en session_shutdown y luego restablezca cualquier estado en memoria en session_start.

Se dispara al bifurcar mediante /fork o clonar mediante /clone.

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

Después de una bifurcación o clonación exitosa, pi emite session_shutdown para la instancia de extensión anterior, recarga y vuelve a vincular extensiones para la nueva sesión, luego emite session_start con reason: "fork" y previousSessionFile. Realice el trabajo de limpieza en session_shutdown y luego restablezca cualquier estado en memoria en session_start.

Cocido sobre compactación. Consulte compaction.md para obtener más detalles.

pi.on("session_before_compact", async (event, ctx) => {
const { preparation, branchEntries, customInstructions, signal } = event;
// Cancel:
return { cancel: true };
// Custom summary:
return {
compaction: {
summary: "...",
firstKeptEntryId: preparation.firstKeptEntryId,
tokensBefore: preparation.tokensBefore,
}
};
});
pi.on("session_compact", async (event, ctx) => {
// event.compactionEntry - the saved compaction
// event.fromExtension - whether extension provided it
});

Disparado en la navegación /tree. Consulte Sesiones para conocer los conceptos de navegación en árbol.

pi.on("session_before_tree", async (event, ctx) => {
const { preparation, signal } = event;
return { cancel: true };
// OR provide custom summary:
return { summary: { summary: "...", details: {} } };
});
pi.on("session_tree", async (event, ctx) => {
// event.newLeafId, oldLeafId, summaryEntry, fromExtension
});

Se activa antes de que se elimine el tiempo de ejecución de una sesión iniciada. Úselo para limpiar recursos abiertos desde session_start u otros enlaces con ámbito de sesión.

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

Se activa después de que el usuario envía el mensaje, antes del ciclo del agente. Puede inyectar un mensaje y/o modificar el mensaje del sistema.

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

El campo systemPromptOptions brinda a las extensiones acceso a los mismos datos estructurados que utiliza Pi para crear el indicador del sistema. Esto le permite inspeccionar lo que Pi ha cargado (indicaciones personalizadas, pautas, fragmentos de herramientas, archivos de contexto, habilidades) sin redescubrir recursos ni volver a analizar indicadores. Úselo cuando su extensión necesite realizar cambios profundos e informados en el mensaje del sistema respetando la configuración proporcionada por el usuario.

Dentro de before_agent_start, event.systemPrompt y ctx.getSystemPrompt() reflejan el mensaje del sistema encadenado a partir del controlador actual. Los controladores before_agent_start posteriores aún pueden modificarlo nuevamente.

Se dispara una vez por solicitud del usuario.

pi.on("agent_start", async (_event, ctx) => {});
pi.on("agent_end", async (event, ctx) => {
// event.messages - messages from this prompt
});

Disparado por cada turno (una respuesta LLM + llamadas de herramientas).

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

Sección titulada «message_start / message_update / message_end»

Activado por actualizaciones del ciclo de vida de los mensajes.

  • message_start y message_end se activan para mensajes de usuario, asistente y resultado de herramienta.
  • message_update se activa para actualizaciones de transmisión del asistente.
  • Los controladores message_end pueden devolver { message } para reemplazar el mensaje finalizado. El reemplazo debe mantener el mismo 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

Sección titulada «tool_execution_start / tool_execution_update / tool_execution_end»

Despedido por actualizaciones del ciclo de vida de ejecución de herramientas.

En modo de herramienta paralela:

  • tool_execution_start se emite en orden de fuente asistente durante la fase de verificación previa
  • Los eventos tool_execution_update pueden intercalarse entre herramientas.
  • tool_execution_end se emite en el orden de finalización de la herramienta después de finalizar cada herramienta.
  • Los eventos de mensajes finales toolResult aún se emiten más tarde en el orden de origen del asistente.
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
});

Se activa antes de cada llamada al LLM. Modifica mensajes de forma no destructiva. Consulte Formato de sesión para conocer los tipos de mensajes.

pi.on("context", async (event, ctx) => {
// event.messages - deep copy, safe to modify
const filtered = event.messages.filter(m => !shouldPrune(m));
return { messages: filtered };
});

Se activa después de crear la carga útil específica del proveedor, justo antes de enviar la solicitud. Los controladores se ejecutan en orden de carga de extensión. Devolver undefined mantiene la carga útil sin cambios. Devolver cualquier otro valor reemplaza la carga útil para controladores posteriores y para la solicitud real.

Este enlace puede reescribir las instrucciones del sistema a nivel de proveedor o eliminarlas por completo. Esos cambios a nivel de carga útil no se reflejan en ctx.getSystemPrompt(), que informa la cadena de aviso del sistema de Pi en lugar de la carga útil final serializada del proveedor.

pi.on("before_provider_request", (event, ctx) => {
console.log(JSON.stringify(event.payload, null, 2));
// Optional: replace payload
// return { ...event.payload, temperature: 0 };
});

Esto es principalmente útil para depurar la serialización del proveedor y el comportamiento de la caché.

Se activa después de recibir una respuesta HTTP y antes de que se consuma el cuerpo de la transmisión. Los controladores se ejecutan en orden de carga de extensión.

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

La disponibilidad del encabezado depende del proveedor y del transporte. Los proveedores que abstraen respuestas HTTP no pueden exponer los encabezados.

Se activa cuando el modelo cambia mediante el comando /model, ciclo de modelo (Ctrl+P) o restauración de sesión.

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

Utilícelo para actualizar elementos de la interfaz de usuario (barras de estado, pies de página) o realizar una inicialización específica del modelo cuando cambie el modelo activo.

Despedido cuando cambia el nivel de pensamiento. Esto es sólo de notificación; Los valores de retorno del controlador se ignoran.

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

Utilícelo para actualizar la interfaz de usuario de la extensión cuando pi.setThinkingLevel(), cambios de modelo o controles de nivel de pensamiento integrados cambien el nivel de pensamiento activo.

Se dispara después de tool_execution_start, antes de que se ejecute la herramienta. Puede bloquear. Utilice isToolCallEventType para restringir y obtener entradas escritas.

Antes de que se ejecute tool_call, pi espera a que los eventos del Agente emitidos previamente terminen de drenarse a través de AgentSession. Esto significa que ctx.sessionManager está actualizado a través del mensaje de llamada de herramienta asistente actual.

En el modo de ejecución de herramienta paralela predeterminado, las llamadas a herramientas hermanas desde el mismo mensaje del asistente se verifican previamente de forma secuencial y luego se ejecutan simultáneamente. No se garantiza que tool_call vea los resultados de las herramientas hermanas del mismo mensaje del asistente en ctx.sessionManager.

event.input es mutable. Mutéelo en su lugar para parchear los argumentos de la herramienta antes de la ejecución.

Garantías de comportamiento:

  • Las mutaciones en event.input afectan la ejecución real de la herramienta.
  • Los manejadores posteriores de tool_call ven mutaciones realizadas por manejadores anteriores.
  • No se realiza ninguna revalidación después de su mutación.
  • Los valores de retorno de tool_call solo controlan el bloqueo a través de { block: true, reason?: string }
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" };
}
}
if (isToolCallEventType("read", event)) {
// event.input is { path: string; offset?: number; limit?: number }
console.log(`Reading: ${event.input.path}`);
}
});

Escribir entrada de herramienta personalizada

Sección titulada «Escribir entrada de herramienta personalizada»

Las herramientas personalizadas deben exportar su tipo de entrada:

my-extension.ts
export type MyToolInput = Static<typeof myToolSchema>;

Utilice isToolCallEventType con parámetros de tipo explícitos:

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

Se activa después de que finaliza la ejecución de la herramienta y antes de que se emitan tool_execution_end más los eventos del mensaje de resultado final de la herramienta. Puede modificar el resultado.

En el modo de herramienta paralela, tool_result y tool_execution_end pueden intercalarse en el orden de finalización de la herramienta, mientras que los eventos de mensajes finales toolResult aún se emiten más tarde en el orden de origen del asistente.

tool_result maneja la cadena como middleware:

  • Los controladores se ejecutan en orden de carga de extensión.
  • Cada controlador ve el último resultado después de los cambios anteriores del controlador
  • Los controladores pueden devolver parches parciales (content, details o isError); Los campos omitidos mantienen sus valores actuales.

Utilice ctx.signal para trabajo asíncrono anidado dentro del controlador. Esto permite a Esc cancelar llamadas de modelo, fetch() y otras operaciones con detección de aborto iniciadas por la extensión.

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

Se activa cuando el usuario ejecuta los comandos ! o !!. Puede interceptar.

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

Se activa cuando se recibe la entrada del usuario, después de que se verifican los comandos de extensión pero antes de la expansión de habilidades y plantillas. El evento ve el texto de entrada sin formato, por lo que /skill:foo y /template aún no están expandidos.

Orden de procesamiento:

  1. Primero se verifican los comandos de extensión (/cmd); si se encuentran, se ejecuta el controlador y se omite el evento de entrada.
  2. Se activa el evento input: puede interceptar, transformar o manejar
  3. Si no se maneja: comandos de habilidad (/skill:name) ampliados al contenido de la habilidad
  4. Si no se maneja: plantillas de mensajes (/template) expandidas al contenido de la plantilla
  5. Comienza el procesamiento del agente (before_agent_start, etc.)
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
});

Resultados:

  • continue - pasa sin cambios (predeterminado si el controlador no devuelve nada)
  • transform - modificar texto/imágenes, luego continuar con la expansión
  • handled: omite el agente por completo (el primer controlador que devuelva esto gana)

Transforma la cadena entre controladores. Consulte input-transform.ts y input-transform-streaming.ts para conocer el enrutamiento compatible con streamingBehavior.

Todos los controladores reciben ctx: ExtensionContext.

Métodos de UI para la interacción del usuario. Consulte UI personalizada para obtener detalles completos.

Modo de ejecución actual: "tui", "rpc", "json" o "print". Utilice ctx.mode === "tui" para proteger funciones exclusivas del terminal, como custom(), fábricas de componentes, entrada del terminal y representación TUI directa.

true en modos TUI y RPC. false en modo impresión (-p) y modo JSON. Utilícelo para proteger los métodos de diálogo (select, confirm, input, editor) y los métodos de disparar y olvidar (notify, setStatus, setWidget, setTitle, setEditorText) que funcionan tanto en modo TUI como RPC. En el modo RPC, algunos métodos específicos de TUI no son operativos o devuelven valores predeterminados (consulte rpc.md).

Directorio de trabajo actual.

Utilice CONFIG_DIR_NAME en lugar de codificar .pi al construir rutas de configuración locales del proyecto. Las distribuciones renombradas pueden usar un nombre de directorio de configuración diferente.

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");
// ...
});
}

Devuelve si la confianza local del proyecto está activa para el contexto de la sesión actual. Esto incluye decisiones de confianza temporales y anulaciones de confianza de CLI, no solo decisiones guardadas en el almacén de confianza global.

Utilice esto antes de leer la configuración de la extensión local del proyecto que solo debe respetarse para proyectos confiables.

Acceso de solo lectura al estado de la sesión. Consulte Formato de sesión para conocer la API de SessionManager completa y los tipos de entrada.

Para tool_call, este estado se sincroniza a través del mensaje del asistente actual antes de que se ejecuten los controladores. En el modo de ejecución de herramientas paralelas, todavía no se garantiza que se incluyan los resultados de herramientas hermanas del mismo mensaje del asistente.

ctx.sessionManager.getEntries() // All entries
ctx.sessionManager.getBranch() // Current branch
ctx.sessionManager.getLeafId() // Current leaf entry ID

Acceso a modelos y claves API.

La señal de aborto del agente actual, o undefined cuando no hay ningún turno de agente activo.

Úselo para trabajos anidados con detección de abortos iniciados por controladores de extensiones, por ejemplo:

  • fetch(..., { signal: ctx.signal })
  • llamadas de modelo que aceptan signal
  • archivos o asistentes de proceso que aceptan AbortSignal

ctx.signal normalmente se define durante eventos de giro activos como tool_call, tool_result, message_update y turn_end. Por lo general, es undefined en contextos inactivos o sin turnos, como eventos de sesión, comandos de extensión y atajos activados mientras pi está inactivo.

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()

Sección titulada «ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()»

Controle los ayudantes de flujo.

Solicite un cierre elegante de pi.

  • Modo interactivo: Diferido hasta que el agente esté inactivo (después de procesar todos los mensajes de dirección y seguimiento en cola).
  • Modo RPC: Diferido hasta el siguiente estado inactivo (después de completar la respuesta del comando actual, mientras se espera el siguiente comando).
  • Modo de impresión: No operativo. El proceso sale automáticamente cuando se procesan todas las solicitudes.

Emite el evento session_shutdown a todas las extensiones antes de salir. Disponible en todos los contextos (controladores de eventos, herramientas, comandos, accesos directos).

pi.on("tool_call", (event, ctx) => {
if (isFatal(event.input)) {
ctx.shutdown();
}
});

Devuelve el uso del contexto actual para el modelo activo. Utiliza el último uso del asistente cuando está disponible y luego estima los tokens para los mensajes de seguimiento.

const usage = ctx.getContextUsage();
if (usage && usage.tokens > 100_000) {
// ...
}

Activar la compactación sin esperar a que finalice. Utilice onComplete y onError para acciones de seguimiento.

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

Devuelve la cadena de aviso del sistema actual de Pi.

  • Durante before_agent_start, esto refleja los cambios encadenados de mensajes del sistema realizados hasta el momento para el turno actual.
  • No incluye mutaciones posteriores del mensaje context.
  • No incluye reescrituras de carga útil before_provider_request.
  • Si las extensiones cargadas posteriormente se ejecutan después de la suya, aún pueden cambiar lo que se envía finalmente.
pi.on("before_agent_start", (event, ctx) => {
const prompt = ctx.getSystemPrompt();
console.log(`System prompt length: ${prompt.length}`);
});

Los controladores de comandos reciben ExtensionCommandContext, que amplía ExtensionContext con métodos de control de sesión. Estos sólo están disponibles en los comandos porque pueden bloquearse si se llaman desde los controladores de eventos.

Devuelve las entradas base que Pi utiliza actualmente para crear el indicador del sistema.

const options = ctx.getSystemPromptOptions();
const contextPaths = options.contextFiles?.map((file) => file.path) ?? [];

Tiene la misma forma y mutabilidad que before_agent_start event.systemPromptOptions: aviso personalizado, herramientas activas, fragmentos de herramientas, pautas de aviso, texto de aviso del sistema agregado, cwd, archivos de contexto cargados y habilidades cargadas. Puede incluir contenidos de archivos de contexto completo, así que trátelos como datos sensibles de extensión local y evite exponerlos a través de listas de comandos, registros o metadatos de autocompletar.

Esto informa las entradas del mensaje base actual. No incluye cambios de avisos del sistema encadenados before_agent_start por turno, mutaciones posteriores de mensajes de eventos context ni reescrituras de carga útil before_provider_request.

Espere a que el agente termine de transmitir:

pi.registerCommand("my-cmd", {
handler: async (args, ctx) => {
await ctx.waitForIdle();
// Agent is now idle, safe to modify session
},
});

Crea una nueva sesión:

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
}

Opciones:

  • parentSession: archivo de sesión principal para registrar en el encabezado de la nueva sesión
  • setup: muta el SessionManager de la nueva sesión antes de que se ejecute withSession.
  • withSession: ejecuta el trabajo posterior al cambio en un contexto de sesión de reemplazo nuevo. No utilice el antiguo pi / comando ctx capturado; consulte Ciclo de vida del reemplazo de sesión y trampas comunes.

Bifurca desde una entrada específica, creando un nuevo archivo de sesión:

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
}

Opciones:

  • position: "before" (predeterminado) se bifurca antes del mensaje del usuario seleccionado, restaurando ese mensaje en el editor.
  • position: "at" duplica la ruta activa a través de la entrada seleccionada sin restaurar el texto del editor.
  • withSession: ejecuta el trabajo posterior al cambio en un contexto de sesión de reemplazo nuevo. No utilice el antiguo pi / comando ctx capturado; consulte Ciclo de vida del reemplazo de sesión y trampas comunes.

Navegue a un punto diferente en el árbol de la sesión:

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

Opciones:

  • summarize: Si se debe generar un resumen de la rama abandonada
  • customInstructions: Instrucciones personalizadas para el resumidor.
  • replaceInstructions: si es verdadero, customInstructions reemplaza el mensaje predeterminado en lugar de agregarlo
  • label: Etiqueta para adjuntar a la entrada de resumen de la rama (o entrada de destino si no es un resumen)

Cambie a un archivo de sesión diferente:

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
}

Opciones:

Para descubrir sesiones disponibles, utilice los métodos estáticos SessionManager.list() o 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");
},
});
}
},
});

Ciclo de vida del reemplazo de sesión y trampas comunes

Sección titulada «Ciclo de vida del reemplazo de sesión y trampas comunes»

withSession recibe un ReplacedSessionContext nuevo, que extiende ExtensionCommandContext con ayudantes asíncronos sendMessage() y sendUserMessage() vinculados a la sesión de reemplazo.

Ciclo de vida y trampas comunes:

  • withSession se ejecuta solo después de que la sesión anterior haya emitido session_shutdown, el tiempo de ejecución anterior se haya eliminado, la sesión de reemplazo se haya recuperado y la nueva instancia de extensión ya haya recibido session_start.
  • La devolución de llamada aún se ejecuta en el cierre original, no dentro de la nueva instancia de extensión. Eso significa que es posible que su antigua instancia de extensión ya haya ejecutado su limpieza de apagado antes de que se inicie withSession.
  • Los objetos capturados antiguos pi/comando antiguo ctx vinculados a la sesión quedan obsoletos después del reemplazo y se lanzarán si se usan. Utilice únicamente el ctx pasado a withSession para trabajos vinculados a sesiones.
  • Los objetos en bruto previamente extraídos siguen siendo tu responsabilidad. Por ejemplo, si captura const sm = ctx.sessionManager antes del reemplazo, sm sigue siendo el antiguo objeto SessionManager. No lo reutilice después del reemplazo.
  • El código en withSession debe asumir que cualquier estado invalidado por su controlador session_shutdown ya desapareció. Capture únicamente datos simples que sobrevivan limpiamente al apagado, como cadenas, identificadores y configuraciones serializadas.

Patrón seguro:

pi.registerCommand("handoff", {
handler: async (_args, ctx) => {
const kickoff = "Continue from the replacement session";
await ctx.newSession({
withSession: async (ctx) => {
await ctx.sendUserMessage(kickoff);
},
});
},
});

Patrón inseguro:

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

Ejecute el mismo flujo de recarga que /reload.

pi.registerCommand("reload-runtime", {
description: "Reload extensions, skills, prompts, and themes",
handler: async (_args, ctx) => {
await ctx.reload();
return;
},
});

Comportamiento importante:

  • await ctx.reload() emite session_shutdown para el tiempo de ejecución de la extensión actual
  • Luego recarga recursos y emite session_start con reason: "reload" y resources_discover con motivo "reload"
  • El controlador de comandos actualmente en ejecución continúa en el marco de llamada anterior.
  • El código posterior a await ctx.reload() aún se ejecuta desde la versión previa a la recarga
  • El código posterior a await ctx.reload() no debe asumir que el antiguo estado de extensión en memoria sigue siendo válido
  • Después de que el controlador regrese, los comandos/eventos/llamadas a herramientas futuras usarán la nueva versión de la extensión.

Para un comportamiento predecible, trate la recarga como terminal para ese controlador (await ctx.reload(); return;).

Las herramientas se ejecutan con ExtensionContext, por lo que no pueden llamar a ctx.reload() directamente. Utilice un comando como punto de entrada de recarga y luego exponga una herramienta que ponga en cola ese comando como un mensaje de seguimiento del usuario.

Herramienta de ejemplo que el LLM puede llamar para activar la recarga:

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, and themes",
handler: async (_args, ctx) => {
await ctx.reload();
return;
},
});
pi.registerTool({
name: "reload_runtime",
label: "Reload Runtime",
description: "Reload extensions, skills, prompts, and themes",
parameters: Type.Object({}),
async execute() {
pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
return {
content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }],
};
},
});
}

Suscríbete a eventos. Consulte Eventos para conocer los tipos de eventos y los valores de retorno.

Registre una herramienta personalizada a la que pueda llamar el LLM. Consulte Herramientas personalizadas para obtener detalles completos.

pi.registerTool() funciona tanto durante la carga de la extensión como después del inicio. Puede llamarlo dentro de session_start, controladores de comandos u otros controladores de eventos. Las nuevas herramientas se actualizan inmediatamente en la misma sesión, por lo que aparecen en pi.getAllTools() y el LLM puede invocarlas sin /reload.

Utilice pi.setActiveTools() para habilitar o deshabilitar herramientas (incluidas las herramientas agregadas dinámicamente) en tiempo de ejecución.

Utilice promptSnippet para optar por una herramienta personalizada en una entrada de una línea en Available tools y promptGuidelines para agregar viñetas específicas de la herramienta a la sección predeterminada Guidelines cuando la herramienta esté activa.

Importante: Las viñetas promptGuidelines se agregan planas a la sección Guidelines sin prefijo de nombre de herramienta. Cada pauta debe nombrar la herramienta a la que se refiere; evite “Usar esta herramienta cuando…” porque el LLM no puede decir qué herramienta significa “esta”. En su lugar, escriba “Usar my_tool cuando…”.

Consulte dynamic-tools.ts para ver un ejemplo completo.

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

Inyecte un mensaje personalizado en la sesión.

pi.sendMessage({
customType: "my-extension",
content: "Message text",
display: true,
details: { ... },
}, {
triggerTurn: true,
deliverAs: "steer",
});

Opciones:

  • deliverAs - Modo de entrega:
    • "steer" (predeterminado): pone en cola el mensaje durante la transmisión. Se entrega después de que el turno actual del asistente termina de ejecutar sus llamadas a herramientas, antes de la siguiente llamada de LLM.
    • "followUp": espera a que finalice el agente. Se entrega solo cuando el agente no tiene más llamadas de herramientas.
    • "nextTurn": en cola para el siguiente mensaje de usuario. No interrumpe ni desencadena nada.
  • triggerTurn: true: si el agente está inactivo, activa una respuesta LLM inmediatamente. Solo se aplica a los modos "steer" y "followUp" (ignorado para "nextTurn").

Enviar un mensaje de usuario al agente. A diferencia de sendMessage(), que envía mensajes personalizados, este envía un mensaje de usuario real que aparece como si lo hubiera escrito el usuario. Siempre desencadena un turno.

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

Opciones:

  • deliverAs - Requerido cuando el agente está transmitiendo:
    • "steer": pone en cola el mensaje para su entrega después de que el turno actual del asistente termina de ejecutar sus llamadas de herramienta.
    • "followUp" - Espera a que el agente termine todas las herramientas

Cuando no se transmite, el mensaje se envía inmediatamente y desencadena un nuevo turno. Cuando se transmite sin deliverAs, se genera un error.

Consulte send-user-message.ts para obtener un ejemplo completo.

Estado de extensión persistente (NO participa en el contexto LLM).

pi.appendEntry("my-state", { count: 42 });
// 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
}
}
});

Establezca el nombre para mostrar de la sesión (que se muestra en el selector de sesión en lugar del primer mensaje).

pi.setSessionName("Refactor auth module");

Obtenga el nombre de la sesión actual, si está configurado.

const name = pi.getSessionName();
if (name) {
console.log(`Session: ${name}`);
}

Establecer o borrar una etiqueta en una entrada. Las etiquetas son marcadores definidos por el usuario para marcadores y navegación (que se muestran en el selector /tree).

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

Las etiquetas persisten en la sesión y sobreviven a los reinicios. Úsalos para marcar puntos importantes (giros, puntos de control) en el árbol de conversación.

Registre un comando.

Si varias extensiones registran el mismo nombre de comando, pi las conserva todas y asigna sufijos de invocación numéricos en orden de carga, por ejemplo /review:1 y /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");
}
});

Opcional: agregue el autocompletado de argumentos para /command ...:

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

Obtenga los comandos de barra diagonal disponibles para invocarlos a través de prompt en la sesión actual. Incluye comandos de extensión, plantillas de mensajes y comandos de habilidad. La lista coincide con el orden RPC get_commands: primero las extensiones, luego las plantillas y luego las habilidades.

const commands = pi.getCommands();
const bySource = commands.filter((command) => command.source === "extension");
const userScoped = commands.filter((command) => command.sourceInfo.scope === "user");

Cada entrada tiene esta forma:

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

Utilice sourceInfo como campo de procedencia canónica. No infiera la propiedad a partir de los nombres de los comandos ni del análisis de rutas ad hoc.

Los comandos interactivos integrados (como /model y /settings) no se incluyen aquí. Se manejan sólo en interactivo. modo y no se ejecutaría si se enviara a través de prompt.

pi.registerMessageRenderer(customType, renderer)

Sección titulada «pi.registerMessageRenderer(customType, renderer)»

Registre un procesador TUI personalizado para mensajes con su customType. Consulte UI personalizada.

Registre un atajo de teclado. Consulte keybindings.md para conocer el formato de acceso directo y las combinaciones de teclas integradas.

pi.registerShortcut("ctrl+shift+p", {
description: "Toggle plan mode",
handler: async (ctx) => {
ctx.ui.notify("Toggled!");
},
});

Registre una bandera CLI.

pi.registerFlag("plan", {
description: "Start in plan mode",
type: "boolean",
default: false,
});
// Check value
if (pi.getFlag("plan")) {
// Plan mode enabled
}

Ejecute un comando de shell.

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)

Sección titulada «pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)»

Administrar herramientas activas. Esto funciona tanto para herramientas integradas como para herramientas registradas dinámicamente. pi.getActiveTools() devuelve los nombres de las herramientas activas como string[]; pi.getAllTools() devuelve metadatos para todas las herramientas configuradas.

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() devuelve name, description, parameters, promptGuidelines y sourceInfo.

Valores típicos de sourceInfo.source:

  • builtin para herramientas integradas
  • sdk para herramientas pasadas mediante createAgentSession({ customTools })
  • metadatos de origen de extensión para herramientas registradas por extensiones

Establecer el modelo actual. Devuelve false si no hay ninguna clave API disponible para el modelo. Consulte models.md para configurar modelos personalizados.

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)

Sección titulada «pi.getThinkingLevel() / pi.setThinkingLevel(level)»

Obtenga o establezca el nivel de pensamiento. El nivel está sujeto a las capacidades del modelo (los modelos que no razonan siempre usan “apagado”). Los cambios emiten thinking_level_select.

const current = pi.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" | "high" | "xhigh"
pi.setThinkingLevel("high");

Bus de eventos compartido para comunicación entre extensiones:

pi.events.on("my:event", (data) => { ... });
pi.events.emit("my:event", { ... });

Registre o anule un proveedor de modelo dinámicamente. Útil para servidores proxy, puntos finales personalizados o configuraciones de modelos para todo el equipo.

Las llamadas realizadas durante la función de fábrica de extensiones se ponen en cola y se aplican una vez que se inicializa el corredor. Las llamadas realizadas después de eso (por ejemplo, desde un controlador de comandos que sigue un flujo de configuración de usuario) entran en vigor inmediatamente sin requerir un /reload.

Si necesita descubrir modelos desde un punto final remoto, prefiera una fábrica de extensiones asíncronas en lugar de diferir la búsqueda a session_start. pi espera a que llegue la fábrica antes de continuar con el inicio, por lo que los modelos registrados están disponibles de inmediato, incluido pi --list-models.

// 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
}
]
});
// 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) {
// Refresh logic
return credentials;
},
getApiKey(credentials) {
return credentials.access;
}
}
});

Opciones de configuración:

  • name: nombre para mostrar del proveedor en la interfaz de usuario, como /login.
  • baseUrl: URL del punto final de la API. Requerido al definir modelos.
  • apiKey: literal de clave API, interpolación de entorno ($ENV_VAR o ${ENV_VAR}) o !command inicial. Requerido al definir modelos (a menos que se proporcione oauth). $$ escapa de $ y $! escapa de un ! literal sin activar la ejecución del comando.
  • api - Tipo de API: "anthropic-messages", "openai-completions", "openai-responses", etc.
  • headers: encabezados personalizados para incluir en las solicitudes.
  • authHeader: si es verdadero, agrega el encabezado Authorization: Bearer automáticamente.
  • models - Matriz de definiciones de modelos. Si se proporciona, reemplaza todos los modelos existentes de este proveedor. Las definiciones de modelo pueden configurar baseUrl para anular el punto final del proveedor para ese modelo.
  • oauth: configuración del proveedor OAuth para compatibilidad con /login. Cuando se proporciona, el proveedor aparece en el menú de inicio de sesión.
  • streamSimple: implementación de transmisión personalizada para API no estándar.

Consulte custom-provider.md para temas avanzados: API de streaming personalizadas, detalles de OAuth, referencia de definición de modelo.

Eliminar un proveedor previamente registrado y sus modelos. Se restauran los modelos integrados que fueron anulados por el proveedor. No tiene efecto si el proveedor no estaba registrado.

Al igual que registerProvider, esto entra en vigor inmediatamente cuando se llama después de la fase de carga inicial, por lo que no se requiere un /reload.

pi.registerCommand("my-setup-teardown", {
description: "Remove the custom proxy provider",
handler: async (_args, _ctx) => {
pi.unregisterProvider("my-proxy");
},
});

Las extensiones con estado deben almacenarlo en el resultado de la herramienta details para un soporte de ramificación adecuado:

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

Registre las herramientas que el LLM puede llamar a través de pi.registerTool(). Las herramientas aparecen en el indicador del sistema y pueden tener una representación personalizada.

Utilice promptSnippet para una entrada breve de una línea en la sección Available tools en el indicador predeterminado del sistema. Si se omiten, las herramientas personalizadas quedan fuera de esa sección.

Utilice promptGuidelines para agregar viñetas específicas de herramientas a la sección Guidelines del indicador predeterminado del sistema. Estas viñetas se incluyen solo mientras la herramienta está activa (por ejemplo, después de pi.setActiveTools([...])).

Importante: Las viñetas promptGuidelines se agregan planas a la sección Guidelines sin prefijo ni agrupación de nombre de herramienta. Cada pauta debe nombrar la herramienta a la que se refiere; evite “Usar esta herramienta cuando…” porque el LLM no puede decir qué herramienta significa “esta”. En su lugar, escriba “Usar my_tool cuando…”.

Nota: Algunos modelos incluyen el prefijo @ en los argumentos de ruta de herramienta. Las herramientas integradas eliminan una @ inicial antes de resolver las rutas. Si tu herramienta personalizada acepta una ruta, normaliza también una @ inicial.

Si su herramienta personalizada muta archivos, use withFileMutationQueue() para que participe en la misma cola por archivo que los edit y write integrados. Esto es importante porque las llamadas a herramientas se ejecutan en paralelo de forma predeterminada. Sin la cola, dos herramientas pueden leer el mismo contenido de archivo antiguo, calcular diferentes actualizaciones y luego, la última escritura que llegue sobrescribe a la otra.

Ejemplo de caso de error: su herramienta personalizada edita foo.ts mientras que el edit integrado también cambia foo.ts en el mismo turno del asistente. Si su herramienta no participa en la cola, ambas pueden leer el foo.ts original, aplicar cambios separados y uno de esos cambios se pierde.

Pase la ruta real del archivo de destino a withFileMutationQueue(), no el argumento de usuario sin formato. Resuélvalo primero en una ruta absoluta, relativa a ctx.cwd o al directorio de trabajo de su herramienta. Para los archivos existentes, el asistente canonicaliza a través de realpath(), por lo que los alias de enlaces simbólicos para el mismo archivo comparten una cola. Para archivos nuevos, recurre a la ruta absoluta resuelta porque todavía no hay nada para realpath().

Ponga en cola toda la ventana de mutación en esa ruta de destino. Eso incluye la lógica de lectura, modificación y escritura, no solo la escritura final.

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: {},
};
});
}
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
// 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) { ... },
});

Errores de señalización: Para marcar la ejecución de una herramienta como fallida (establece isError: true en el resultado y lo informa al LLM), genera un error desde execute. Devolver un valor nunca establece el indicador de error independientemente de las propiedades que incluya en el objeto devuelto.

Terminación anticipada: Devuelve terminate: true de execute() para indicar que la llamada de LLM de seguimiento automático debe omitirse después del lote de herramientas actual. Esto solo tiene efecto cuando finaliza cada resultado de herramienta finalizada en ese lote. Consulte examples/extensions/structured-output.ts para ver un ejemplo mínimo en el que el agente finaliza con una llamada final a la herramienta de salida estructurada.

// 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: {} };
}

Importante: Utilice StringEnum de @earendil-works/pi-ai para enumeraciones de cadenas. Type.Union/Type.Literal no funciona con la API de Google.

Preparación de argumentos: prepareArguments(args) es opcional. Si está definido, se ejecuta antes de la validación del esquema y antes de execute(). Úselo para imitar una forma de entrada aceptada más antigua cuando pi reanude una sesión anterior cuyos argumentos de llamada de herramienta almacenados ya no coinciden con el esquema actual. Devuelve el objeto que deseas validar con parameters. Mantenga el esquema público estricto. No agregue campos de compatibilidad obsoletos a parameters solo para mantener funcionando las sesiones anteriores reanudadas.

Ejemplo: una sesión anterior puede contener una llamada a la herramienta edit con oldText y newText de nivel superior, mientras que el esquema actual solo acepta edits: [{ oldText, newText }].

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

Las extensiones pueden anular las herramientas integradas (read, bash, edit, write, grep, find, ls) registrando una herramienta con el mismo nombre. El modo interactivo muestra una advertencia cuando esto sucede.

Ventana de terminal
# Extension's read tool replaces built-in read
pi -e ./tool-override.ts

Alternativamente, use --no-builtin-tools para comenzar sin herramientas integradas mientras mantiene habilitadas las herramientas de extensión:

Ventana de terminal
# No built-in tools, only extension tools
pi --no-builtin-tools -e ./my-extension.ts

Consulte examples/extensions/tool-override.ts para obtener un ejemplo completo que anula read con registro y control de acceso.

Renderizado: La herencia del renderizador integrado se resuelve por ranura. La anulación de ejecución y la anulación de representación son independientes. Si su anulación omite renderCall, se utiliza el renderCall integrado. Si su anulación omite renderResult, se utiliza el renderResult integrado. Si su anulación omite ambos, el renderizador incorporado se usa automáticamente (resaltado de sintaxis, diferencias, etc.). Esto le permite empaquetar herramientas integradas para registro o control de acceso sin volver a implementar la interfaz de usuario.

Metadatos de solicitud: promptSnippet y promptGuidelines no se heredan de la herramienta integrada. Si su anulación debe conservar esas instrucciones rápidas, defínalas explícitamente en la anulación.

Su implementación debe coincidir exactamente con la forma del resultado, incluido el tipo details. La interfaz de usuario y la lógica de la sesión dependen de estas formas para la representación y el seguimiento del estado.

Implementaciones de herramientas integradas:

Las herramientas integradas admiten operaciones conectables para delegar a sistemas remotos (SSH, contenedores, etc.):

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

Interfaces de operaciones: ReadOperations, WriteOperations, EditOperations, BashOperations, LsOperations, GrepOperations, FindOperations

Para user_bash, las extensiones pueden reutilizar el backend del shell local de pi a través de createLocalBashOperations() en lugar de reimplementar la generación de procesos locales, la resolución del shell y la terminación del árbol de procesos.

La herramienta bash también admite un gancho de generación para ajustar el comando, cwd o env antes de la ejecución:

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

Consulte examples/extensions/ssh.ts para obtener un ejemplo SSH completo con el indicador --ssh.

Las herramientas DEBEN truncar su salida para evitar abrumar el contexto LLM. Grandes producciones pueden causar:

  • Errores de desbordamiento de contexto (mensaje demasiado largo)
  • Fallas de compactación
  • Rendimiento del modelo degradado

El límite incorporado es 50 KB (~10 000 tokens) y 2000 líneas, lo que se alcance primero. Utilice las utilidades de truncamiento exportadas:

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

Puntos clave:

  • Utilice truncateHead para contenido donde el comienzo importa (resultados de búsqueda, lecturas de archivos)
  • Utilice truncateTail para contenido donde el final importa (registros, salida de comando)
  • Informe siempre al LLM cuando se trunque el resultado y dónde encontrar la versión completa.
  • Documente los límites de truncamiento en la descripción de su herramienta.

Consulte examples/extensions/truncated-tool.ts para obtener un ejemplo completo que envuelve rg (ripgrep) con el truncamiento adecuado.

Una extensión puede registrar múltiples herramientas con estado compartido:

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

Las herramientas pueden proporcionar renderCall y renderResult para una visualización TUI personalizada. Consulte tui.md para conocer la API completa del componente y tool-execution.ts para conocer cómo se componen las filas de herramientas.

De forma predeterminada, la salida de la herramienta está envuelta en un Box que maneja el relleno y el fondo. Un renderCall o renderResult definido debe devolver un Component. Si no se define un renderizador de ranura, tool-execution.ts utiliza el renderizado alternativo para esa ranura.

Establezca renderShell: "self" cuando la herramienta deba representar su propio shell en lugar de usar el Box predeterminado. Esto es útil para herramientas que necesitan un control total sobre el encuadre o el comportamiento del fondo, por ejemplo, vistas previas grandes que deben permanecer visualmente estables después de que la herramienta se estabilice.

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 y renderResult reciben cada uno un objeto context con:

  • args - los argumentos de llamada de la herramienta actual
  • state: estado local de fila compartido entre renderCall y renderResult
  • lastComponent: el componente devuelto anteriormente para esa ranura, si corresponde
  • invalidate(): solicita una nueva representación de esta fila de herramientas
  • toolCallId, cwd, executionStarted, argsComplete, isPartial, expanded, showImages, isError

Utilice context.state para el estado compartido entre ranuras. Mantenga los cachés locales de ranura en la instancia del componente devuelto cuando desee reutilizar y mutar el mismo componente en diferentes renderizados.

Representa la llamada o encabezado de la herramienta:

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

Representa el resultado o salida de la herramienta:

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

Si una ranura no tiene contenido visible intencionalmente, devuelve un Component vacío, como un Container vacío.

Utilice keyHint() para mostrar sugerencias de combinación de teclas que respeten la configuración de combinación de teclas activa:

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

Funciones disponibles:

  • keyHint(keybinding, description): formatea una identificación de combinación de teclas configurada, como "app.tools.expand" o "tui.select.confirm".
  • keyText(keybinding): devuelve el texto clave configurado sin formato para una identificación de combinación de teclas
  • rawKeyHint(key, description) - Formatear una cadena de clave sin formato

Utilice identificadores de combinación de teclas con espacios de nombres:

  • Los identificadores de agente de codificación utilizan el espacio de nombres app.*, por ejemplo app.tools.expand, app.editor.external, app.session.rename.
  • Los ID de TUI compartidos utilizan el espacio de nombres tui.*, por ejemplo tui.select.confirm, tui.select.cancel, tui.input.tab.

Para obtener una lista exhaustiva de identificadores de combinaciones de teclas y valores predeterminados, consulte keybindings.md. keybindings.json usa esos mismos identificadores de espacio de nombres.

Los editores personalizados y los componentes ctx.ui.custom() reciben keybindings: KeybindingsManager como argumento inyectado. Deberían usar ese administrador inyectado directamente en lugar de llamar a getKeybindings() o setKeybindings().

  • Utilice Text con relleno (0, 0). El cuadro predeterminado maneja el relleno.
  • Utilice \n para contenido de varias líneas.
  • Manejar isPartial para el progreso de la transmisión.
  • Soporte expanded para detalles bajo demanda.
  • Mantenga compacta la vista predeterminada.
  • Leer context.args en renderResult en lugar de copiar argumentos en context.state.
  • Utilice context.state solo para datos que deben compartirse entre espacios de llamadas y resultados.
  • Reutilice context.lastComponent cuando la misma instancia de componente se pueda actualizar en su lugar.
  • Utilice renderShell: "self" sólo cuando el shell en caja predeterminado se interponga en su camino. En el modo self-shell, la herramienta es responsable de su propio marco, relleno y fondo.

Si un renderizador de ranura no está definido o arroja:

  • renderCall: Muestra el nombre de la herramienta.
  • renderResult: muestra texto sin formato de content

Las extensiones pueden interactuar con los usuarios a través de métodos ctx.ui y personalizar la forma en que se representan los mensajes/herramientas.

Para componentes personalizados, consulte tui.md que tiene patrones de copiar y pegar para:

  • Cuadros de diálogo de selección (SelectList)
  • Operaciones asíncronas con cancelación (BorderedLoader)
  • La configuración alterna (Lista de configuración)
  • Indicadores de estado (setStatus)
  • Mensaje de trabajo, visibilidad e indicador durante la transmisión (setWorkingMessage, setWorkingVisible, setWorkingIndicator)
  • Widgets arriba/abajo del editor (setWidget)
  • Proveedores de autocompletar superpuestos a la finalización de ruta/barra incorporada (addAutocompleteProvider)
  • Pies de página personalizados (setFooter)
// 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"

Diálogos cronometrados con cuenta regresiva

Sección titulada «Diálogos cronometrados con cuenta regresiva»

Los cuadros de diálogo admiten una opción timeout que se cierra automáticamente con una visualización de cuenta regresiva en vivo:

// 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
}

Valores devueltos en el tiempo de espera:

  • select() devuelve undefined
  • confirm() devuelve false
  • input() devuelve undefined

Para obtener más control (por ejemplo, para distinguir el tiempo de espera de la cancelación del usuario), utilice 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")
}

Consulte examples/extensions/timed-confirm.ts para ver ejemplos completos.

// 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

Los marcos de indicadores de trabajo personalizados se representan palabra por palabra. Si desea colores, agréguelos usted mismo a las cadenas del marco, por ejemplo con ctx.ui.theme.fg(...).

Utilice ctx.ui.addAutocompleteProvider() para apilar la lógica de autocompletar personalizada sobre el comando de barra diagonal integrado y el proveedor de ruta. Configure triggerCharacters para activadores naturales personalizados como $.

Patrón típico:

  • inspeccionar el texto antes del cursor
  • devolver sus propias sugerencias cuando la sintaxis específica de su extensión coincida
  • de lo contrario delega a current.getSuggestions(...)
  • delega applyCompletion(...) a menos que necesites un comportamiento de inserción personalizado
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;
},
}));
});

Consulte github-issue-autocomplete.ts para obtener un ejemplo completo que carga previamente los últimos problemas abiertos de GitHub con gh issue list y los filtra localmente para una rápida finalización de #.... Requiere GitHub CLI (gh) y un repositorio de GitHub.

Para una interfaz de usuario compleja, utilice ctx.ui.custom(). Esto reemplaza temporalmente el editor con su componente hasta que se llame a done():

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
}

La devolución de llamada recibe:

  • tui - Instancia TUI (para dimensiones de pantalla, gestión de enfoque)
  • theme - Tema actual para estilizar
  • keybindings - Administrador de combinación de teclas de la aplicación (para comprobar los accesos directos)
  • done(value) - Llamada para cerrar componente y devolver valor

Consulte tui.md para conocer la API completa del componente.

Pase { overlay: true } para representar el componente como un modal flotante sobre el contenido existente, sin borrar la pantalla:

const result = await ctx.ui.custom<string | null>(
(tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
{ overlay: true }
);

Para posicionamiento avanzado (anclajes, márgenes, porcentajes, visibilidad receptiva), pase overlayOptions. Utilice onHandle para controlar el foco o la visibilidad mediante programación:

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

Una superposición visible enfocada puede recuperar la entrada después de que se cierra la interfaz de usuario personalizada temporal sin superposición. Si intencionalmente desea que otro componente mantenga la entrada mientras la superposición permanece visible, llame a handle.unfocus({ target }). Al pasar { target: null } se libera la superposición sin enfocar otro componente.

Consulte tui.md para obtener la API OverlayOptions y OverlayHandle completa y overlay-qa-tests.ts para ver ejemplos.

Reemplace el editor de entrada principal con una implementación personalizada (modo vim, modo emacs, etc.):

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(theme, keybindings)
);
});
}

Puntos clave:

  • Extienda CustomEditor (no Editor base) para obtener combinaciones de teclas de aplicaciones (escape para cancelar, Ctrl+d, cambio de modelo)
  • Llame a super.handleInput(data) para llaves que no maneja
  • La fábrica recibe theme y keybindings desde la aplicación
  • Utilice ctx.ui.getEditorComponent() antes de setEditorComponent() para ajustar el editor personalizado configurado previamente.
  • Pase undefined para restaurar el valor predeterminado: ctx.ui.setEditorComponent(undefined)

Para componer con otra extensión que ya reemplazó al editor, captura la fábrica anterior antes de configurar la tuya:

const previous = ctx.ui.getEditorComponent();
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
new MyEditor(tui, theme, keybindings, { base: previous?.(tui, theme, keybindings) })
);

Consulte tui.md Patrón 7 para ver un ejemplo completo con indicador de modo.

Registre un procesador personalizado para mensajes con su customType:

import { Text } from "@earendil-works/pi-tui";
pi.registerMessageRenderer("my-extension", (message, options, theme) => {
const { expanded } = 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, 0, 0);
});

Los mensajes se envían a través de pi.sendMessage():

pi.sendMessage({
customType: "my-extension", // Matches registerMessageRenderer
content: "Status update",
display: true, // Show in TUI
details: { ... }, // Available in renderer
});

Todas las funciones de renderizado reciben un objeto theme. Consulte themes.md para crear temas personalizados y la paleta de colores completa.

// 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)

Para resaltar la sintaxis en los renderizadores de herramientas personalizadas:

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);
  • Se registran errores de extensión, el agente continúa
  • Los errores tool_call bloquean la herramienta (a prueba de fallos)
  • Los errores de la herramienta execute deben señalarse lanzando; el error arrojado se detecta, se informa al LLM con isError: true y la ejecución continúa
Modoctx.modectx.hasUINotas
Interactivo"tui"trueTUI completo con renderizado de terminal
RPC (--mode rpc)"rpc"trueDiálogos y notificaciones vía protocolo JSON; custom() devuelve undefined. Consulte rpc.md
JSON (--mode json)"json"falseFlujo de eventos a salida estándar; Los métodos de UI no son operativos
Imprimir (-p)"print"falseLas extensiones se ejecutan pero no pueden avisar

Utilice ctx.mode === "tui" antes de las funciones específicas de TUI (custom(), fábricas de componentes, entrada de terminal). Utilice ctx.hasUI antes de los métodos de diálogo y notificación que funcionan en los modos TUI y RPC.

Todos los ejemplos en examples/extensions/.

EjemploDescripciónAPI clave
Herramientas
hello.tsRegistro mínimo de herramientaregisterTool
question.tsHerramienta con interacción del usuarioregisterTool, ui.select
questionnaire.tsHerramienta asistente de varios pasosregisterTool, ui.custom
todo.tsHerramienta con estado y persistenciaregisterTool, appendEntry, renderResult, eventos de sesión
dynamic-tools.tsRegistrar herramientas después del inicio y durante los comandosregisterTool, session_start, registerCommand
structured-output.tsHerramienta final de salida estructurada con terminate: trueregisterTool, resultados de la herramienta de terminación
truncated-tool.tsEjemplo de truncamiento de salidaregisterTool, truncateHead
tool-override.tsAnular la herramienta de lectura incorporadaregisterTool (mismo nombre que el integrado)
Comandos
pirate.tsModificar el mensaje del sistema por turnoregisterCommand, before_agent_start
summarize.tsComando de resumen de conversaciónregisterCommand, ui.custom
handoff.tsTraspaso del modelo entre proveedoresregisterCommand, ui.editor, ui.custom
qna.tsPreguntas y respuestas con interfaz de usuario personalizadaregisterCommand, ui.custom, setEditorText
send-user-message.tsInyectar mensajes de usuarioregisterCommand, sendUserMessage
reload-runtime.tsRecargar comando y transferencia de herramientas LLMregisterCommand, ctx.reload(), sendUserMessage
shutdown-command.tsComando de apagado eleganteregisterCommand, shutdown()
Eventos y puertas
permission-gate.tsBloquear comandos peligrososon("tool_call"), ui.confirm
project-trust.tsDecidir o diferir la confianza del proyecto desde un usuario/global o extensión CLIon("project_trust"), interfaz de usuario de confianza, resultado de confianza requerido
protected-paths.tsBloquear escrituras en rutas específicason("tool_call")
confirm-destructive.tsConfirmar cambios de sesiónon("session_before_switch"), on("session_before_fork")
dirty-repo-guard.tsAdvertir sobre repositorio de git sucioon("session_before_*"), exec
input-transform.tsTransformar la entrada del usuarioon("input")
input-transform-streaming.tsTransformación de entrada compatible con streamingon("input"), streamingBehavior
model-status.tsReaccionar a los cambios de modeloon("model_select"), setStatus
provider-payload.tsInspeccionar cargas útiles y encabezados de respuesta del proveedoron("before_provider_request"), on("after_provider_response")
system-prompt-header.tsMostrar información de aviso del sistemaon("agent_start"), getSystemPrompt
claude-rules.tsCargar reglas desde archivoson("session_start"), on("before_agent_start")
prompt-customizer.tsAgregue guía de herramientas sensible al contexto usando systemPromptOptionson("before_agent_start"), BuildSystemPromptOptions
file-trigger.tsEl observador de archivos activa mensajessendMessage
Compactación y Sesiones
custom-compaction.tsResumen de compactación personalizadoon("session_before_compact")
trigger-compact.tsActivar la compactación manualmentecompact()
git-checkpoint.tsGit stash en turnoson("turn_start"), on("session_before_fork"), exec
git-merge-and-resolve.tsRecuperar, fusionar y resolver conflictoson("agent_end"), exec, sendUserMessage
auto-commit-on-exit.tsComprometerse a cerraron("session_shutdown"), exec
Componentes de la interfaz de usuario
status-line.tsIndicador de estado de pie de páginasetStatus, eventos de sesión
working-indicator.tsPersonaliza el indicador de funcionamiento del streamingsetWorkingIndicator, registerCommand
github-issue-autocomplete.tsAgregue la finalización de problemas #1234 además del autocompletado integrado precargando problemas abiertos recientes desde gh issue listaddAutocompleteProvider, on("session_start"), exec
custom-footer.tsReemplazar el pie de página por completoregisterCommand, setFooter
custom-header.tsReemplazar encabezado de inicioon("session_start"), setHeader
modal-editor.tsEditor modal estilo VimsetEditorComponent, CustomEditor
rainbow-editor.tsEstilo de editor personalizadosetEditorComponent
widget-placement.tsWidget arriba/abajo del editorsetWidget
overlay-test.tsComponentes de superposiciónui.custom con opciones de superposición
overlay-qa-tests.tsPruebas completas de superposiciónui.custom, todas las opciones de superposición
notify.tsNotificaciones sencillasui.notify
timed-confirm.tsDiálogos con tiempo de esperaui.confirm con tiempo de espera/señal
mac-system-theme.tsTema de cambio automáticosetTheme, exec
Extensiones complejas
plan-mode/Implementación del modo de plan completoTodos los tipos de eventos, registerCommand, registerShortcut, registerFlag, setStatus, setWidget, sendMessage, setActiveTools
preset.tsAjustes preestablecidos guardables (modelo, herramientas, pensamiento)registerCommand, registerShortcut, registerFlag, setModel, setActiveTools, setThinkingLevel, appendEntry
tools.tsActivar/desactivar herramientas UIregisterCommand, setActiveTools, SettingsList, eventos de sesión
Remoto y zona de pruebas
ssh.tsEjecución remota SSHregisterFlag, on("user_bash"), on("before_agent_start"), operaciones de herramientas
interactive-shell.tsSesión de shell persistenteon("user_bash")
sandbox/Ejecución de herramientas en espacio aisladoOperaciones con herramientas
gondolin/Enrutar herramientas integradas y comandos ! a una micro-VM GondolinOperaciones de herramientas, anulaciones de herramientas integradas, on("user_bash")
subagent/Generar subagentesregisterTool, exec
Juegos
snake.tsJuego de serpientesregisterCommand, ui.custom, manejo de teclado
space-invaders.tsJuego de invasores espacialesregisterCommand, ui.custom
doom-overlay/Doom en superposiciónui.custom con superposición
Proveedores
custom-provider-anthropic/Proxy antrópico personalizadoregisterProvider
custom-provider-gitlab-duo/Integración de GitLab DuoregisterProvider con OAuth
Mensajes y comunicación
message-renderer.tsRepresentación de mensajes personalizadosregisterMessageRenderer, sendMessage
event-bus.tsEventos entre extensionespi.events
Metadatos de la sesión
session-name.tsNombre de sesiones para selectorsetSessionName, getSessionName
bookmark.tsEntradas de marcadores para /treesetLabel
Varios
inline-bash.tsBash en línea en llamadas a herramientason("tool_call")
bash-spawn-hook.tsAjuste el comando bash, cwd y env antes de la ejecucióncreateBashTool, spawnHook
with-deps/Extensión con dependencias npmEstructura del paquete con package.json