Extensions
pi puede crear extensiones. Pídale que cree uno para su caso de uso.
Extensions son módulos 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 /recargar: Coloque extensiones en
~/.pi/agent/extensions/(global) o.pi/extensions/(proyecto-local) para el descubrimiento automático. Utilicepi -e./path.tssolo para pruebas rápidas. Extensions en ubicaciones descubiertas automáticamente se puede recargar en caliente con/reload.
Capacidades clave:
- Herramientas personalizadas: registre herramientas a las que el LLM puede llamar a través de
pi.registerTool() - Interceptación de eventos: bloquear o modificar llamadas de herramientas, inyectar contexto, personalizar la compactación
- Interacción del usuario: avisar a los usuarios mediante
ctx.ui(seleccionar, confirmar, ingresar, notificar) - Componentes de interfaz de usuario personalizados: componentes TUI completos con entrada de teclado a través de
ctx.ui.custom()para interacciones complejas - Comandos personalizados: registre comandos como
/mycommandmediantepi.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 puntos de control (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 conocer implementaciones funcionales.
Tabla de contenido
- Quick Start
- Extension Locations
- Available Imports
- Writing an Extension
- Events
- ExtensionContext
- ExtensionCommandContext
- ExtensionAPI Methods
- State Management
- Custom Tools
- Custom UI
- Error Handling
- Mode Behavior
- Examples Reference
Inicio rápido
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");
},
});
}Pruebe con la bandera --extension (o -e):
pi -e ./my-extension.tsUbicaciones de extensión
Seguridad: Extensions se ejecuta con todos los permisos del sistema y puede ejecutar código arbitrario. Instálelo únicamente desde fuentes en las que confíe.
Extensions 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ón | Alcance |
|---|---|
~/.pi/agent/extensions/*.ts |
Global (todos los proyectos) |
~/.pi/agent/extensions/*/index.ts |
Global (subdirectorio) |
.pi/extensions/*.ts |
Proyecto local |
.pi/extensions/*/index.ts |
Proyecto local (subdirectorio) |
Rutas adicionales a través de settings.json:
{
"packages": [
"npm:@foo/bar@1.0.0",
"git:github.com/user/repo@v1"
],
"extensions": [
"/path/to/local/extension.ts",
"/path/to/local/extension/dir"
]
}Para compartir extensiones a través de npm o git como paquetes pi, consulte packages.md.
Importaciones disponibles
| Paquete | Objetivo |
|---|---|
@earendil-works/pi-coding-agent |
Tipos de extensión (ExtensionAPI, ExtensionContext, eventos) |
typebox |
Definiciones de esquemas para parámetros de herramientas |
@earendil-works/pi-ai |
Utilidades de IA (StringEnum para enumeraciones compatibles con Google) |
@earendil-works/pi-tui |
TUI componentes para renderizado personalizado |
npm las dependencias 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án disponibles en tiempo de ejecución; cuando se configura npmCommand, los paquetes git usan install simple para compatibilidad con contenedores.
Node.js integrados (node:fs, node:path, etc.) también están disponibles.
Escribir una extensión
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", { ... });
}Extensions 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().
Funciones de fábrica asíncronas
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 hasta pi --list-models.
Recursos de larga duración y cierre
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.
Estilos de extensión
Archivo único - el más simple, para extensiones pequeñas:
~/.pi/agent/extensions/
└── my-extension.tsDirectorio 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 modulePaquete con dependencias - para extensiones que necesitan npm paquetes:
~/.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 extensiones, luego las importaciones desde node_modules/ funcionan automáticamente.
Eventos
Descripción general del ciclo de vida
pi starts
│
├─► project_trust (user/global and CLI extensions only, before project resources load)
├─► session_start { reason: "startup" }
└─► resources_discover { reason: "startup" }
│
▼
user sends prompt ─────────────────────────────────────────┐
│ │
├─► (extension commands checked first, bypass if found) │
├─► input (can intercept, transform, or handle) │
├─► (skill/template expansion if not handled) │
├─► before_agent_start (can inject message, modify system prompt)
├─► agent_start │
├─► message_start / message_update / message_end │
│ │
│ ┌─── turn (repeats while LLM calls tools) ───┐ │
│ │ │ │
│ ├─► turn_start │ │
│ ├─► context (can modify messages) │ │
│ ├─► before_provider_headers (can mutate headers) |
│ ├─► before_provider_request (can inspect or replace payload)
│ ├─► after_provider_response (status + headers, before stream consume)
│ │ │ │
│ │ LLM responds, may call tools: │ │
│ │ ├─► tool_execution_start │ │
│ │ ├─► tool_call (can block) │ │
│ │ ├─► tool_execution_update │ │
│ │ ├─► tool_result (can modify) │ │
│ │ └─► tool_execution_end │ │
│ │ │ │
│ └─► turn_end │ │
│ │
├─► agent_end │
└─► agent_settled (no retry/compaction/follow-up left) │
│
user sends another prompt ◄────────────────────────────────┘
/new (new session) or /resume (switch session)
├─► session_before_switch (can cancel)
├─► session_shutdown
├─► session_start { reason: "new" | "resume", previousSessionFile? }
└─► resources_discover { reason: "startup" }
/fork or /clone
├─► session_before_fork (can cancel)
├─► session_shutdown
├─► session_start { reason: "fork", previousSessionFile }
└─► resources_discover { reason: "startup" }
/name or pi.setSessionName()
└─► session_info_changed
/compact or auto-compaction
├─► session_before_compact (can cancel or customize)
└─► session_compact
/tree navigation
├─► session_before_tree (can cancel or customize)
└─► session_tree
/model or Ctrl+P (model selection/cycling)
├─► thinking_level_select (if model change changes/clamps thinking level)
└─► model_select
thinking level changes (settings, keybinding, pi.setThinkingLevel())
└─► thinking_level_select
exit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM)
└─► session_shutdownEventos de inicio
proyecto_confianza
Despedido 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. Regrese "undecided" para permitir que los controladores posteriores o el flujo de confianza integrado decidan. Marque ctx.hasUI antes de preguntar. Si ningún controlador responde sí/no, la resolución de confianza normal continúa: las decisiones guardadas trust.json se aplican primero, luego defaultProjectTrust controla si pi pregunta, confía o rechaza de forma predeterminada.
Eventos de recursos
recursos_descubrir
Se activa 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". Recargar usa 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"],
};
});Eventos de sesión
Consulte Session Format para conocer los aspectos internos del almacenamiento de sesiones y el SessionManager API.
inicio_sesión
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");
});información_sesión_cambiada
Se activa cuando el nombre para mostrar de la sesión actual se establece mediante /name, RPC o pi.setSessionName().
pi.on("session_info_changed", async (event, ctx) => {
// event.name - current normalized name, or undefined if cleared
ctx.ui.notify(`Session renamed: ${event.name ?? "(none)"}`, "info");
});sesión_antes_de_cambio
Disparado 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, luego restablezca cualquier estado en memoria en session_start.
sesión_antes_de_fork
Se dispara al bifurcar vía /fork o clonar vía /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 las extensiones para la nueva sesión, luego emite session_start con reason: "fork" y previousSessionFile.
Realice el trabajo de limpieza en session_shutdown, luego restablezca cualquier estado en memoria en session_start.
sesión_antes_compact / sesión_compact
Cocido sobre compactación. Consulte compaction.md para obtener más detalles.
pi.on("session_before_compact", async (event, ctx) => {
const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;
// reason - "manual" (/compact), "threshold", or "overflow"
// willRetry - whether the aborted turn is retried after compaction (overflow recovery)
// Cancel:
return { cancel: true };
// Custom summary:
return {
compaction: {
summary: "...",
firstKeptEntryId: preparation.firstKeptEntryId,
tokensBefore: preparation.tokensBefore,
// usage: summaryResponse.usage, // Optional; included in session totals
}
};
});
pi.on("session_compact", async (event, ctx) => {
// event.compactionEntry - the saved compaction
// event.fromExtension - whether extension provided it
// event.reason - "manual" (/compact), "threshold", or "overflow"
// event.willRetry - whether the aborted turn is retried after compaction (overflow recovery)
});sesión_antes_árbol / sesión_árbol
Disparado en la navegación /tree. Consulte Sessions 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: "...",
// usage: summaryResponse.usage, // Optional; included in session totals
details: {},
},
};
});
pi.on("session_tree", async (event, ctx) => {
// event.newLeafId, oldLeafId, summaryEntry, fromExtension
});cierre_sesión
Se activa antes de que se elimine el tiempo de ejecución de una sesión iniciada. Utilice esto 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.
});Eventos de agentes
antes_agente_inicio
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 Pi usa para crear el mensaje del sistema. Esto le permite inspeccionar lo que se ha cargado Pi (indicaciones personalizadas, pautas, fragmentos de herramientas, context files, 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() ambos reflejan el mensaje del sistema encadenado a partir del controlador actual. Más tarde, los controladores before_agent_start aún pueden modificarlo nuevamente.
inicio_agente / fin_agente / agente_establecido
agent_start se activa cuando comienza la ejecución de un agente de bajo nivel. agent_end se activa cuando finaliza esa ejecución, pero Pi aún puede reintentar, compactar y reintentar automáticamente, o continuar con los mensajes de seguimiento en cola. Utilice agent_settled para integraciones de estado que necesite saber que Pi no continuarán ejecutándose automáticamente.
pi.on("agent_start", async (_event, ctx) => {});
pi.on("agent_end", async (event, ctx) => {
// event.messages - messages from this low-level run
});
pi.on("agent_settled", async (_event, ctx) => {
// ctx.isIdle() is true here unless another extension started a new run.
});turn_start / turn_end
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
});inicio_mensaje / actualización_mensaje / fin_mensaje
Activado por actualizaciones del ciclo de vida de los mensajes.
message_startymessage_endse activan para mensajes de usuario, asistente y resultado de herramienta.message_updateactiva las actualizaciones de transmisión del asistente.- Los controladores
message_endpueden devolver{ message }para reemplazar el mensaje finalizado. El sustituto debe seguir siendo el mismorole.
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,
},
},
},
};
});inicio_ejecución_herramienta / actualización_ejecución_herramienta / fin_ejecución_herramienta
Despedido por actualizaciones del ciclo de vida de ejecución de herramientas.
En modo de herramienta paralela:
tool_execution_startse emite en el orden de la fuente asistente durante la fase de verificación previatool_execution_updatelos eventos pueden intercalarse entre herramientastool_execution_endse emite en el orden de finalización de la herramienta después de finalizar cada herramienta- Los eventos de mensajes finales
toolResultaú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
});contexto
Despedido antes de cada convocatoria de LLM. Modificar mensajes de forma no destructiva. Consulte Session Format 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 };
});before_provider_headers
Se activa después de ensamblar los encabezados HTTP salientes. Úselo para agregar, anular o eliminar encabezados de solicitud.
Los controladores mutan event.headers en su lugar. Establezca una clave para una cadena para agregarla o anularla, o para null para eliminarla.
pi.on("before_provider_headers", (event, ctx) => {
// Add or override — e.g. a session id for gateway tracing/attribution
event.headers["x-session-id"] = ctx.sessionManager.getSessionId();
// Drop a tracking header pi adds for this call
event.headers["X-OpenRouter-Title"] = null;
});Se ejecuta una vez por solicitud del proveedor; Los reintentos reutilizan los mismos encabezados en lugar de volver a disparar el anzuelo.
solicitud_antes_del_proveedor
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 del proveedor serializado.
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é.
respuesta_después_del_proveedor
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. Providers que las respuestas HTTP abstractas no pueden exponer los encabezados.
Eventos modelo
selección_modelo
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.
selección_nivel_pensamiento
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}`);
});Utilice esto 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.
Eventos de herramientas
llamada_herramienta
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.inputafectan la ejecución real de la herramienta - Los manejadores posteriores
tool_callven mutaciones realizadas por manejadores anteriores - No se realiza ninguna revalidación después de su mutación.
- Valores de retorno de
tool_callbloqueo de control a través de{ block: true, reason?: string, terminate?: boolean } terminatesolo se aplica a una llamada bloqueada; el agente se detiene temprano solo cuando todos los resultados finalizados en el lote están terminando
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
pi.on("tool_call", async (event, ctx) => {
// event.toolName - "bash", "read", "write", "edit", etc.
// event.toolCallId
// event.input - tool parameters (mutable)
// Built-in tools: no type params needed
if (isToolCallEventType("bash", event)) {
// event.input is { command: string; timeout?: number }
event.input.command = `source ~/.profile\n${event.input.command}`;
if (event.input.command.includes("rm -rf")) {
return { block: true, reason: "Dangerous command", terminate: true };
}
}
if (isToolCallEventType("read", event)) {
// event.input is { path: string; offset?: number; limit?: number }
console.log(`Reading: ${event.input.path}`);
}
});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
}
});resultado_herramienta
Se activa después de que finaliza la ejecución de la herramienta y antes de que se emitan los eventos de mensaje tool_execution_end más el 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 cadena de controladores 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,isErrorousage); 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, event.usage
if (isBashToolResult(event)) {
// event.details is typed as BashToolDetails
}
const response = await fetch("https://example.com/summarize", {
method: "POST",
body: JSON.stringify({ content: event.content }),
signal: ctx.signal,
});
// Modify result:
return { content: [...], details: {...}, isError: false, usage: nestedModelUsage };
});Eventos de bash de usuario
usuario_bash
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 } };
});Eventos de entrada
aporte
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:
- Comandos de extensión (
/cmd) verificados primero; si se encuentran, el controlador se ejecuta y se omite el evento de entrada inputincendios de eventos: puede interceptar, transformar o manejar- Si no se maneja: comandos de habilidad (
/skill:name) ampliados al contenido de la habilidad - Si no se maneja: prompt templates (
/template) expandido al contenido de la plantilla - 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ónhandled- omitir al agente por completo (el primer manejador 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.
Contexto de extensión
Todos los manejadores reciben ctx: ExtensionContext.
ctx.ui
Métodos de UI para la interacción del usuario. Consulte Custom UI para obtener todos los detalles.
modo ctx
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 renderizado directo TUI.
ctx.hasUI
true en los modos TUI y RPC. false en modo impresión (-p) y modo JSON. Utilice esto 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 TUI como en RPC modos. En el modo RPC, algunos métodos específicos de TUI no son operativos o devuelven valores predeterminados (ver rpc.md).
ctx.cwd
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");
// ...
});
}ctx.isProjectTrusted()
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 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.
ctx.sessionManager
Acceso de solo lectura al estado de la sesión. Consulte Session Format para conocer el SessionManager completo API y los tipos de entrada.
Para tool_call, este estado se sincroniza a través del mensaje actual del asistente 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.buildContextEntries() // Active branch entries with compaction applied
ctx.sessionManager.getLeafId() // Current leaf entry IDctx.modelRegistry / ctx.model / ctx.thinkingLevel / ctx.scopedModels
Acceso a modelos, proveedores y autenticación resuelta. ctx.modelRegistry.getProvider(id) devuelve el proveedor pi-ai efectivo, mientras que getProviderAuth(id) resuelve su API key actual, encabezados, URL base y entorno de alcance del proveedor sin requerir un modelo cargado. ctx.model es el modelo activo y ctx.thinkingLevel es su nivel de pensamiento efectivo actual.
ctx.scopedModels es la lista de solo lectura de modelos con alcance para la sesión actual; el mismo conjunto que muestra el comando /scoped-models. Se resuelve al inicio de la sesión desde la bandera --models CLI y la configuración enabledModels (comparada con el catálogo disponible con minimatch en provider/modelId o un simple modelId). Está vacío cuando no se configura ningún alcance, lo que significa que todos los modelos disponibles son utilizables. Cada entrada es { model, thinkingLevel? }, donde thinkingLevel se establece solo cuando un patrón la fijó (por ejemplo, anthropic/*:high). Úselo para completar un selector de modelo que refleje el integrado en lugar de enumerar todo el catálogo a través de ctx.modelRegistry.getAvailable().
señal.ctx
La señal de cancelación 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 modelo que aceptan
signal - archivar o procesar ayudantes que acepten
AbortSignal
ctx.signal normalmente se define durante eventos de turnos 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()
Controle los ayudantes de flujo. ctx.isIdle() es falso mientras Pi está procesando la ejecución de un agente, un reintento automático, un reintento de compactación automática o una continuación en cola.
ctx.apagado()
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).
- RPC modo: 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();
}
});ctx.getContextUsage()
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) {
// ...
}ctx.compacto()
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");
},
});ctx.getSystemPrompt()
Devuelve la cadena de aviso del sistema actual de Pi.
- Durante
before_agent_start, esto refleja los cambios encadenados de indicaciones del sistema realizados hasta el momento para el turno actual. - No incluye mutaciones de mensajes
contextposteriores. - 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}`);
});Contexto de comando de extensión
Los controladores de comandos reciben ExtensionCommandContext, que extiende 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.
ctx.getSystemPromptOptions()
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, context files cargado 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 por turno before_agent_start, mutaciones posteriores de mensajes de eventos context o reescrituras de carga útil before_provider_request.
ctx.waitForIdle()
Espere a que el agente se estabilice por completo, incluidos los reintentos automáticos, los reintentos de compactación automática y las continuaciones en cola:
pi.registerCommand("my-cmd", {
handler: async (args, ctx) => {
await ctx.waitForIdle();
// Agent is now idle, safe to modify session
},
});ctx.newSession(¿opciones?)
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ónsetup: muta elSessionManagerde la nueva sesión antes de que se ejecutewithSessionwithSession: ejecute el trabajo posterior al cambio en un contexto de sesión de reemplazo nuevo. No utilice el antiguo comandopi/ctxcapturado; ver Session replacement lifecycle and footguns.
ctx.fork(entryId, ¿opciones?)
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 editorposition:"at"duplica la ruta activa a través de la entrada seleccionada sin restaurar el texto del editorwithSession: ejecute el trabajo posterior al cambio en un contexto de sesión de reemplazo nuevo. No utilice el antiguo comandopi/ctxcapturado; ver Session replacement lifecycle and footguns.
ctx.navigateTree(ID de destino, ¿opciones?)
Navega a un punto diferente en el session tree:
const result = await ctx.navigateTree("entry-id-456", {
summarize: true,
customInstructions: "Focus on error handling changes",
replaceInstructions: false, // true = replace default prompt entirely
label: "review-checkpoint",
});Opciones:
summarize: Si se debe generar un resumen de la rama abandonadacustomInstructions: Instrucciones personalizadas para el resumidorreplaceInstructions: si es verdadero,customInstructionsreemplaza el mensaje predeterminado en lugar de agregarlolabel: Etiqueta para adjuntar a la entrada de resumen de la rama (o entrada de destino si no es un resumen)
ctx.switchSession (ruta de sesión, ¿opciones?)
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:
withSession: ejecute el trabajo posterior al cambio en un contexto de sesión de reemplazo nuevo. No utilice el antiguo comandopi/ctxcapturado; ver Session replacement lifecycle and footguns.
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 de reemplazo de sesión y pistolas
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 pistolas:
withSessionse ejecuta solo después de que la sesión anterior haya emitidosession_shutdown, el tiempo de ejecución anterior haya sido eliminado, la sesión de reemplazo haya sido recuperada y la nueva instancia de extensión ya haya recibidosession_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 comience
withSession. - Los objetos antiguos capturados
pi/ comando antiguoctxvinculados a la sesión quedan obsoletos después del reemplazo y se lanzarán si se usan. Utilice solo elctxpasado awithSessionpara el trabajo vinculado a la sesión. - Los objetos en bruto previamente extraídos siguen siendo su responsabilidad. Por ejemplo, si captura
const sm = ctx.sessionManagerantes del reemplazo,smsigue siendo el antiguo objetoSessionManager. No lo reutilice después del reemplazo. - El código en
withSessiondebe asumir que cualquier estado invalidado por su controladorsession_shutdownya 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");
},
});
},
});ctx.recargar()
Ejecute el mismo flujo de recarga que /reload.
pi.registerCommand("reload-runtime", {
description: "Reload extensions, skills, prompts, themes, and context files",
handler: async (_args, ctx) => {
await ctx.reload();
return;
},
});Comportamiento importante:
await ctx.reload()emitesession_shutdownpara el tiempo de ejecución de la extensión actual- Luego recarga recursos y emite
session_startconreason: "reload"yresources_discovercon motivo"reload" - El controlador de comandos que se está ejecutando actualmente aún continúa en el marco de llamada anterior.
- El código después de
await ctx.reload()todavía se ejecuta desde la versión previa a la recarga - El código después de
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 futuros 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, themes, and context files",
handler: async (_args, ctx) => {
await ctx.reload();
return;
},
});
pi.registerTool({
name: "reload_runtime",
label: "Reload Runtime",
description: "Reload extensions, skills, prompts, themes, and context files",
parameters: Type.Object({}),
async execute() {
pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
return {
content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }],
};
},
});
}ExtensiónAPI Métodos
pi.on(evento, controlador)
Suscríbete a eventos. Consulte Events para conocer los tipos de eventos y los valores de retorno.
pi.registerTool(definición)
Registre una herramienta personalizada a la que pueda llamar el LLM. Consulte Custom Tools para obtener todos los detalles.
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 llamarlas 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) { ... },
});pi.sendMessage(mensaje, ¿opciones?)
Inyecte un mensaje personalizado en la sesión. Los mensajes personalizados participan en el contexto LLM. Para contenido duradero solo TUI que no debe enviarse al LLM, use pi.appendEntry() con pi.registerEntryRenderer().
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 termine 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").
pi.sendUserMessage(contenido, opciones?)
Enviar un mensaje de usuario al agente. A diferencia de sendMessage(), que envía mensajes personalizados, esto 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 termine de ejecutar sus llamadas a herramientas."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 ver un ejemplo completo.
pi.appendEntry(tipo personalizado, ¿datos?)
Persistir datos de extensión. Las entradas personalizadas NO participan en el contexto LLM. En modo interactivo, también pueden renderizarse dentro de la transcripción del chat cuando se combinan con pi.registerEntryRenderer().
pi.appendEntry("my-state", { count: 42 });
pi.appendEntry("status-card", { title: "Indexed files", count: 17 });
// Restore on reload
pi.on("session_start", async (_event, ctx) => {
for (const entry of ctx.sessionManager.getEntries()) {
if (entry.type === "custom" && entry.customType === "my-state") {
// Reconstruct from entry.data
}
}
});pi.setSessionName(nombre)
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");pi.getNombreSesión()
Obtenga el nombre de la sesión actual, si está configurado.
const name = pi.getSessionName();
if (name) {
console.log(`Session: ${name}`);
}pi.setLabel(entryId, etiqueta)
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.
pi.registerCommand(nombre, opciones)
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 argumento de autocompletado 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");
},
});pi.getCommands()
Obtenga el slash commands disponible para invocación a través de prompt en la sesión actual. Incluye comandos de extensión, prompt templates 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 (tipo personalizado, renderizador)
Registre un renderizador TUI personalizado para mensajes personalizados con su customType. Los mensajes personalizados se crean con pi.sendMessage() y participan en el contexto LLM. Ver Custom UI.
pi.registerMarkdownTransformer(transformador)
Registre un transformador para el Markdown en texto de usuario normal, texto de asistente y bloques de pensamiento. Los transformadores funcionan en orden de carga de extensión y cada transformador recibe el Markdown devuelto por el transformador anterior. Una vez finalizada la cadena, Pi representa el contenido transformado con su renderizador incorporado.
El transformador recibe la cadena Markdown y un contexto con:
messageType—"user","assistant"o"assistant-thinking"isStreaming—truepara actualizaciones parciales del asistente;falsepara usuario, asistente finalizado y mensajes restauradosavailableWidth: columnas terminales exactas disponibles para el contenido Markdown transformado
Devuelve el transformado Markdown:
pi.registerMarkdownTransformer((markdown, { messageType, isStreaming }) => {
if (isStreaming || messageType === "assistant-thinking") return markdown;
return markdown.replaceAll("-->", "→");
});Si un transformador arroja, Pi mantiene el Markdown producido hasta el momento y continúa con el siguiente transformador. El enlace es de solo visualización: el mensaje original permanece sin cambios en el contexto de la sesión y del modelo. Se ejecuta para mensajes de nuevos usuarios, actualizaciones de transmisión del asistente, mensajes de sesión restaurados y cambios en el ancho del terminal, por lo que los transformadores deben permanecer sincrónicos y económicos.
pi.registerEntryRenderer (tipo personalizado, renderizador)
Registre un renderizador TUI personalizado para entradas personalizadas con su customType. Las entradas personalizadas se crean con pi.appendEntry() y no participan en el contexto LLM.
import { Box, Text } from "@earendil-works/pi-tui";
pi.registerEntryRenderer("status-card", (entry, { expanded }, theme) => {
const data = entry.data as { title: string; count: number };
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
box.addChild(new Text(`${theme.bold(data.title)}: ${data.count}`));
if (expanded) {
box.addChild(new Text(theme.fg("dim", JSON.stringify(data, null, 2))));
}
return box;
});
pi.appendEntry("status-card", { title: "Indexed files", count: 17 });pi.registerShortcut(atajo, opciones)
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!");
},
});pi.registerFlag(nombre, opciones)
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
}pi.exec (comando, argumentos, opciones?)
Ejecute un comando de shell.
const result = await pi.exec("git", ["status"], { signal, timeout: 5000 });
// result.stdout, result.stderr, result.code, result.killedpi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(nombres)
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-onlypi.getAllTools() devuelve name, description, parameters, promptGuidelines y sourceInfo.
Valores típicos de sourceInfo.source:
builtinpara herramientas integradassdkpara herramientas pasadas porcreateAgentSession({ customTools })- Metadatos de origen de extensión para herramientas registradas por extensiones.
pi.setModel(modelo)
Establecer el modelo actual. Devuelve false si no hay API key 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(nivel)
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" | "max"
pi.setThinkingLevel("high");pi.eventos
Bus de eventos compartido para comunicación entre extensiones:
pi.events.on("my:event", (data) => { ... });
pi.events.emit("my:event", { ... });pi.registerProvider(nombre, configuración)
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.
Los proveedores dinámicos pueden implementar refreshModels. Pi lo llama durante la actualización del modelo, publica la lista devuelta sincrónicamente a través del proveedor y pasa el contexto de credencial canónica/catálogo almacenado/red/señal. La extensión decide si persisten los metadatos del catálogo hasta la generación verificada context.publish({ persist: entry }); Los servidores en vivo como llama.cpp pueden devolver modelos sin persistirlos.
context.signal es siempre una señal concreta y las devoluciones de llamada del proveedor deben pasarla al bloqueo de E/S. Las llamadas públicas ModelRuntime.refresh() y ModelRegistry.refresh() aceptan una señal opcional y son ilimitadas cuando se omite; Las prórrogas y las solicitudes eligen sus propios plazos. La cancelación detiene a la persona que llama esperando incluso si un proveedor ignora la señal, pero aún se requiere cooperación para detener el trabajo subyacente.
Extensions que necesitan autenticación de proveedor nativo, filtrado, actualización o comportamiento de transmisión pueden registrar un Provider completo desde @earendil-works/pi-ai. El proveedor se convierte en la base de la composición y las anulaciones models.json aún se aplican encima de él.
import { createProvider, openAICompletionsApi } from "@earendil-works/pi-ai";
const provider = createProvider({
id: "local-server",
name: "Local Server",
baseUrl: "http://localhost:8080/v1",
auth: {
apiKey: {
name: "Local server setup",
async login(interaction) {
return {
type: "api_key",
key: await interaction.prompt({ type: "secret", message: "API key" }),
};
},
async resolve({ credential }) {
return credential?.key
? { auth: { apiKey: credential.key }, source: "stored API key" }
: undefined;
},
},
},
models: [],
api: openAICompletionsApi(),
});
pi.registerProvider(provider);
// Register a new provider with custom models
pi.registerProvider("my-proxy", {
name: "My Proxy",
baseUrl: "https://proxy.example.com",
apiKey: "$PROXY_API_KEY", // env var reference
api: "anthropic-messages",
models: [
{
id: "claude-sonnet-4-20250514",
name: "Claude 4 Sonnet (proxy)",
reasoning: false,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 16384
}
]
});
// Register a live llama.cpp catalog without persisting discovered models
pi.registerProvider("llama.cpp", {
baseUrl: "http://localhost:8080/v1",
apiKey: "local",
api: "openai-completions",
async refreshModels({ signal }) {
const response = await fetch("http://localhost:8080/v1/models", { signal });
const { data } = await response.json();
return data.map(({ id }) => ({
id,
name: id,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 16384
}));
}
});
// Override baseUrl for an existing provider (keeps all models)
pi.registerProvider("anthropic", {
baseUrl: "https://proxy.example.com"
});
// Register provider with OAuth support for /login
pi.registerProvider("corporate-ai", {
baseUrl: "https://ai.corp.com",
api: "openai-responses",
models: [...],
oauth: {
name: "Corporate AI (SSO)",
async login(callbacks) {
// Custom OAuth flow
callbacks.onAuth({ url: "https://sso.corp.com/..." });
const code = await callbacks.onPrompt({ message: "Enter code:" });
return { refresh: code, access: code, expires: Date.now() + 3600000 };
},
async refreshToken(credentials, signal) {
signal.throwIfAborted();
// Refresh logic
return credentials;
},
getApiKey(credentials) {
return credentials.access;
}
}
});La forma del objeto acepta un pi-ai completo Provider, incluido el comportamiento nativo auth, getModels, refreshModels, filterModels, stream y streamSimple.
Opciones de configuración heredadas:
name: nombre para mostrar del proveedor en la interfaz de usuario, como/login.baseUrl- API URL del punto final. Requerido al definir modelos.apiKey- API key literal, interpolación ambiental ($ENV_VARo${ENV_VAR}) o inicial!command. Requerido al definir modelos (a menos que se proporcioneoauth).$escapa de ``apiKey- API key literal, interpolación ambiental ($ENV_VARo${ENV_VAR}) o inicial!command. Requerido al definir modelos (a menos que se proporcioneoauth).$escapa de y$!escapa de un literal!` sin activar la ejecución del comando.api- API tipo:"anthropic-messages","openai-completions","openai-responses", etc.headers: encabezados personalizados para incluir en las solicitudes.authHeader: si es verdadero, agrega el encabezadoAuthorization: Bearerautomáticamente.models- Matriz de definiciones de modelos. Si se proporciona, reemplaza todos los modelos existentes para este proveedor. Las definiciones de modelo pueden configurarbaseUrlpara anular el punto final del proveedor para ese modelo.refreshModels: devolución de llamada de descubrimiento dinámico asíncrono. Sus modelos devueltos reemplazan a los modelos proporcionados por extensión.context.storedcontiene la instantánea del proveedor persistente; utilice la generación comprobadacontext.publish({ persist: entry })solo cuando los datos del catálogo actualizados deban persistir. Utilicepersist: nullpara eliminar esa instantánea.oauth- OAuth configuración del proveedor para soporte/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: transmisión personalizada APIs, detalles OAuth, referencia de definición de modelo.
pi.unregisterProvider(nombre)
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");
},
});Gestión del Estado
Extensions con estado debería 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
};
},
});
}Herramientas personalizadas
Registre las herramientas a las 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 mensaje 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 mensaje 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 son idiotas e incluyen el prefijo @ en los argumentos de la ruta de la herramienta. Las herramientas integradas eliminan una @ inicial antes de resolver las rutas. Si su herramienta personalizada acepta una ruta, normalice también una @ inicial.
Si su herramienta personalizada muta archivos, use withFileMutationQueue() para que participe en la misma cola por archivo que las integradas edit y write. 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 la incorporada edit también cambia foo.ts en el mismo turno del asistente. Si su herramienta no participa en la cola, ambas pueden leer el original foo.ts, 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 mediante realpath(), por lo que los alias de enlaces simbólicos para el mismo archivo comparten una cola. Para archivos nuevos, vuelve a la ruta absoluta resuelta porque todavía no hay nada en 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: {},
};
});
}Definición de herramienta
import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai";
import { Text } from "@earendil-works/pi-tui";
pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "What this tool does (shown to LLM)",
promptSnippet: "List or add items in the project todo list",
promptGuidelines: [
"Use my_tool for todo planning instead of direct file edits when the user asks for a task list."
],
parameters: Type.Object({
action: StringEnum(["list", "add"] as const), // Use StringEnum for Google compatibility
text: Type.Optional(Type.String()),
}),
prepareArguments(args) {
if (!args || typeof args !== "object") return args;
const input = args as { action?: string; oldAction?: string };
if (typeof input.oldAction === "string" && input.action === undefined) {
return { ...input, action: input.oldAction };
}
return args;
},
async execute(toolCallId, params, signal, onUpdate, ctx) {
// Check for cancellation
if (signal?.aborted) {
return { content: [{ type: "text", text: "Cancelled" }] };
}
// Stream progress updates
onUpdate?.({
content: [{ type: "text", text: "Working..." }],
details: { progress: 50 },
});
// Run commands via pi.exec (captured from extension closure)
const result = await pi.exec("some-command", [], { signal });
// Return result
return {
content: [{ type: "text", text: "Done" }], // Sent to LLM
details: { data: result }, // For rendering & state
// usage: nestedModelResponse.usage, // Optional nested LLM usage
// Optional: stop after this tool batch when every finalized tool result
// in the batch also returns terminate: true.
terminate: true,
};
},
// Optional: Custom rendering
renderCall(args, theme, context) { ... },
renderResult(result, options, theme, context) { ... },
});Contabilidad de uso: Si una herramienta realiza llamadas LLM anidadas, devuelve su Usage combinado como usage. Pi lo conserva en el resultado de la herramienta y lo incluye en el pie de página, /session y RPC totales de sesión. tool_result los controladores pueden inspeccionar o reemplazar este valor.
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: Regrese 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 una llamada final a una 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 desde @earendil-works/pi-ai para enumeraciones de cadenas. Type.Union/Type.Literal no funciona con el 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 con el que deseas validarlo 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 niveles superiores oldText y newText, 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: {},
};
},
});Anulación de herramientas integradas
Extensions puede 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.
# Extension's read tool replaces built-in read
pi -e ./tool-override.tsAlternativamente, use --no-builtin-tools para comenzar sin herramientas integradas mientras mantiene habilitadas las herramientas de extensión:
# No built-in tools, only extension tools
pi --no-builtin-tools -e ./my-extension.tsConsulte examples/extensions/tool-override.ts para ver un ejemplo completo que anula read con el registro y el 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:
- read.ts -
ReadToolDetails - bash.ts -
BashToolDetails - edit.ts
- write.ts
- grep.ts -
GrepToolDetails - find.ts -
FindToolDetails - ls.ts -
LsToolDetails
Ejecución remota
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" },
}),
});createBashTool() expone la sesión actual a comandos a través de PI_SESSION_ID, PI_SESSION_FILE, PI_PROVIDER, PI_MODEL y PI_REASONING_LEVEL. La inyección ocurre antes de spawnHook, por lo que los ganchos reciben estos valores en env y los conservan cuando extienden el entorno existente como se indicó anteriormente. Configure exposeSessionEnvironment: false para desactivarlos:
const bashTool = createBashTool(cwd, {
exposeSessionEnvironment: false,
});Consulte Bash tool session environment para conocer la semántica de las variables. Consulte examples/extensions/ssh.ts para ver un ejemplo completo de SSH con la bandera --ssh.
Truncamiento de salida
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
truncateHeadpara contenido donde el comienzo importa (resultados de búsqueda, lecturas de archivos) - Utilice
truncateTailpara 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 ver un ejemplo completo de ajuste de rg (ripgrep) con el truncamiento adecuado.
Múltiples herramientas
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();
});
}Representación personalizada
Las herramientas pueden proporcionar renderCall y renderResult para una visualización personalizada de TUI. Consulte tui.md para conocer el componente completo API 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 debería 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 actualstate: estado local de fila compartido enrenderCallyrenderResultlastComponent: el componente devuelto anteriormente para esa ranura, si correspondeinvalidate(): solicita una nueva representación de esta fila de herramientastoolCallId,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.
renderLlamar
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;
}renderResultado
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 un espacio no tiene contenido visible intencionalmente, devuelve un Component vacío, como un Container vacío.
Sugerencias de combinación de teclas
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 teclasrawKeyHint(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 ejemploapp.tools.expand,app.editor.external,app.session.rename - Los identificadores TUI compartidos utilizan el espacio de nombres
tui.*, por ejemplotui.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().
Mejores prácticas
- Utilice
Textcon relleno(0, 0). El cuadro predeterminado maneja el relleno. - Utilice
\npara contenido de varias líneas. - Maneje
isPartialpara ver el progreso de la transmisión. - Soporte
expandedpara obtener detalles a pedido. - Mantenga compacta la vista predeterminada.
- Lea
context.argsenrenderResulten lugar de copiar argumentos encontext.state. - Utilice
context.statesolo para datos que deben compartirse entre espacios de llamadas y resultados. - Reutilice
context.lastComponentcuando la misma instancia de componente se pueda actualizar en su lugar. - Utilice
renderShell: "self"solo 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.
Retroceder
Si un renderizador de ranura no está definido o arroja:
renderCall: Muestra el nombre de la herramientarenderResult: muestra texto sin formato decontent
Carga dinámica de herramientas
Extensions puede registrar muchas herramientas manteniendo activo solo un pequeño conjunto inicial. Luego, una herramienta puede agregar más herramientas con pi.setActiveTools() durante la ejecución. Pi detecta cambios puramente aditivos, registra los nuevos nombres de herramientas disponibles en el resultado de esa herramienta y aplica el conjunto activo actualizado antes de la siguiente solicitud de modelo.
Esto funciona con todos los modelos. Models con soporte nativo de carga diferida conserva el prefijo de solicitud estable y carga las nuevas definiciones en la posición de resultado de la herramienta. Otros modelos utilizan el respaldo que se describe a continuación.
El ciclo de vida es:
- Registre cada herramienta con
pi.registerTool()para que aparezca enpi.getAllTools(). - Mantenga activas las herramientas de carga, como
search_tools, y deje inactivas las herramientas de búsqueda. - Durante la ejecución del cargador, llame a
pi.setActiveTools([...currentTools,...matchingTools]). El cambio debe ser aditivo: no eliminar herramientas actualmente activas en la misma llamada. - Pi registra qué herramientas se agregaron en el resultado de la herramienta del cargador.
- Antes de la siguiente respuesta del modelo, Pi expone las definiciones agregadas usando la carga diferida nativa cuando sea compatible, o la lista normal de herramientas activas en caso contrario.
No es necesario devolver referencias de herramientas específicas del proveedor ni marcar el cargador como una herramienta de búsqueda especial. El cambio de herramienta activa es la señal. Los nombres pasados a pi.setActiveTools() ya deben estar registrados; Los nombres desconocidos se ignoran.
Models con carga diferida nativa
- Antrópico
- Models: Sonnet, Opus, Fable versión 4.5 o posterior (sin Haiku)
- Representación nativa: Las definiciones diferidas utilizan
defer_loading; el punto de carga utiliza contenidotool_reference.
- AI abierta
- Models:
gpt-5.4y familia más nueva - Representación nativa: Pi agrega los elementos completos del cliente
tool_search_callytool_search_outputen el punto de carga.
- Models:
Para un modelo o proxy personalizado verificado, el manejo nativo se puede habilitar con compat.supportsToolReferences: true para anthropic-messages o compat.supportsToolSearch: true para openai-responses y openai-codex-responses. Déjelos deshabilitados a menos que el punto final y el modelo acepten el protocolo nativo correspondiente.
Comportamiento alternativo
Para todos los demás modelos y proveedores, la activación dinámica aún funciona: Pi envía la lista completa de herramientas activas actuales normalmente en la siguiente solicitud. El modelo puede llamar a las herramientas recién activadas, pero agregar sus definiciones puede invalidar el prefijo de aviso almacenado en caché del proveedor.
Pi también utiliza este respaldo seguro cuando el conjunto activo no es puramente aditivo, como al reemplazar un grupo de herramientas por otro. Por lo tanto, la extracción de herramientas funciona, pero no utiliza la carga diferida.
Para obtener el mejor comportamiento de la caché, mantenga activa la herramienta de carga durante toda la sesión y agregue herramientas en lugar de reemplazar el conjunto activo. También tenga en cuenta que activar una herramienta con promptSnippet o promptGuidelines reconstruye el indicador del sistema; ese cambio de aviso del sistema puede invalidar el prefijo incluso cuando el proveedor admite esquemas diferidos. Las herramientas cargadas de forma diferida normalmente deberían confiar en su herramienta description y omitir los metadatos de mensajes solo activos.
Ejemplo de herramienta de búsqueda
La siguiente extensión registra dos herramientas de búsqueda, las elimina del conjunto activo inicial y mantiene solo search_tools como su cargador. El ejemplo utiliza una simple concordancia de palabras clave, pero la implementación de la búsqueda podría usar BM25, incrustaciones, un catálogo remoto o enrutamiento específico del proyecto.
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
const SEARCHABLE_TOOL_NAMES = new Set(["lookup_weather", "search_issues"]);
export default function (pi: ExtensionAPI) {
pi.registerTool({
name: "lookup_weather",
label: "Lookup Weather",
description: "Look up the current weather for a city",
parameters: Type.Object({ city: Type.String() }),
async execute(_toolCallId, params) {
return {
content: [{ type: "text", text: `Weather for ${params.city}: sunny` }],
details: {},
};
},
});
pi.registerTool({
name: "search_issues",
label: "Search Issues",
description: "Search project issues by keyword",
parameters: Type.Object({ query: Type.String() }),
async execute(_toolCallId, params) {
return {
content: [{ type: "text", text: `No open issues matching ${params.query}` }],
details: {},
};
},
});
pi.registerTool({
name: "search_tools",
label: "Search Tools",
description: "Search for and enable tools relevant to a task",
promptSnippet: "Search for additional tools when the active tools cannot perform the task",
promptGuidelines: [
"Use search_tools when a task requires a capability that is not currently available.",
],
parameters: Type.Object({
query: Type.String({ description: "Capability or task to search for" }),
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 10 })),
}),
async execute(_toolCallId, params) {
const terms = params.query.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
const matches = pi.getAllTools()
.filter((tool) => SEARCHABLE_TOOL_NAMES.has(tool.name))
.map((tool) => ({
tool,
score: terms.reduce(
(score, term) =>
score + (`${tool.name} ${tool.description}`.toLowerCase().includes(term) ? 1 : 0),
0,
),
}))
.filter((match) => match.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, params.limit ?? 3)
.map((match) => match.tool.name);
if (matches.length === 0) {
return {
content: [{ type: "text", text: `No tools found for: ${params.query}` }],
details: { matches: [] },
};
}
const active = pi.getActiveTools();
const added = matches.filter((name) => !active.includes(name));
pi.setActiveTools([...new Set([...active, ...added])]);
return {
content: [{
type: "text",
text: added.length > 0
? `Loaded tools: ${added.join(", ")}`
: `Matching tools already active: ${matches.join(", ")}`,
}],
details: { matches, added },
};
},
});
pi.on("session_start", () => {
// Keep searchable tools registered but initially inactive. Preserve built-ins
// and tools owned by other extensions, and keep the loader itself active.
const initialTools = pi.getActiveTools().filter(
(name) => !SEARCHABLE_TOOL_NAMES.has(name),
);
pi.setActiveTools([...new Set([...initialTools, "search_tools"])]);
});
}Cuando search_tools agrega una coincidencia, el modelo recibe esa definición en la solicitud inmediatamente siguiente. En un modelo con capacidad nativa, la definición se ancla después del resultado de la búsqueda sin cambiar el prefijo del esquema de herramienta inicial. En otros modelos aparece en la lista normal de herramientas en el mismo pedido siguiente.
IU personalizada
Extensions puede interactuar con los usuarios a través de métodos ctx.ui y personalizar la forma en que se muestran 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)
- Alternancias de configuración (Lista de configuración)
- Indicadores de estado (setStatus)
- Mensaje de trabajo, visibilidad e indicador durante la transmisión (
setWorkingMessage,setWorkingVisible,setWorkingIndicator) - Widgets encima/debajo del editor (setWidget)
- Proveedores de autocompletar superpuestos a la finalización de ruta/barra incorporada (addAutocompleteProvider)
- Pies de página personalizados (setFooter)
Diálogos
// 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
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()devuelveundefinedconfirm()devuelvefalseinput()devuelveundefined
Despido manual con AbortSignal
Para tener más control (por ejemplo, para distinguir el tiempo de espera de la cancelación del usuario), use 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.
Widgets, estado y pie de página
// 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 themeLos 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(...).
Autocompletar Providers
Utilice ctx.ui.addAutocompleteProvider() para apilar la lógica de autocompletar personalizada encima del comando de barra diagonal y el proveedor de ruta integrados. Configure triggerCharacters para activadores naturales personalizados como Utilice ctx.ui.addAutocompleteProvider()para apilar la lógica de autocompletar personalizada encima del comando de barra diagonal y el proveedor de ruta integrados. ConfiguretriggerCharacters` 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(...) - delegar
applyCompletion(...)a menos que necesite 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 ver un ejemplo completo que precarga los últimos problemas abiertos GitHub con gh issue list y los filtra localmente para completarlos rápidamente #.... Requiere GitHub CLI (gh) y una salida del repositorio GitHub.
Componentes personalizados
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- TUI instancia (para dimensiones de pantalla, gestión de enfoque)theme- Tema actual para estilizarkeybindings- Administrador de combinaciones de teclas de la aplicación (para comprobar los accesos directos)done(value)- Llamada para cerrar el componente y devolver el valor
Consulte tui.md para ver el componente completo API.
Modo de superposición (experimental)
Pase { overlay: true } para representar el componente como un modal flotante encima del 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 enfoque 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 ver los OverlayOptions y OverlayHandle completos API y overlay-qa-tests.ts para ver ejemplos.
Editor personalizado
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(tui, theme, keybindings)
);
});
}Puntos clave:
- Extienda
CustomEditor(no la baseEditor) para obtener combinaciones de teclas de aplicaciones (escapar para cancelar, Ctrl+d, cambio de modelo) - Llame al
super.handleInput(data)para llaves que no maneja - La fábrica recibe
tui,themeykeybindingsde la aplicación - Utilice
ctx.ui.getEditorComponent()antes desetEditorComponent()para ajustar el editor personalizado previamente configurado - Pase
undefinedpara 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.
Representación de mensajes y entradas
Registre un renderizador personalizado para mensajes con su customType. Utilice renderizadores de mensajes para contenido que debería participar en el contexto LLM:
import { Text } from "@earendil-works/pi-tui";
pi.registerMessageRenderer("my-extension", (message, options, theme) => {
const { expanded, outputPad } = options;
let text = theme.fg("accent", `[${message.customType}] `);
text += message.content;
if (expanded && message.details) {
text += "\n" + theme.fg("dim", JSON.stringify(message.details, null, 2));
}
return new Text(text, outputPad, 0);
});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
});Para contenido exclusivo de TUI que no debe enviarse al LLM, presente entradas personalizadas en su lugar:
pi.registerEntryRenderer("my-card", (entry, options, theme) => {
return new Text(theme.fg("accent", JSON.stringify(entry.data)));
});
pi.appendEntry("my-card", { status: "done" });Colores del tema
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);Manejo de errores
- Se registran errores de extensión, el agente continúa
tool_calllos errores bloquean la herramienta (a prueba de fallos)- Los errores de la herramienta
executedeben señalarse lanzando; el error arrojado se detecta, se informa al LLM conisError: truey la ejecución continúa
Comportamiento del modo
| Modo | ctx.mode |
ctx.hasUI |
Notas |
|---|---|---|---|
| Interactivo | "tui" |
true |
Completo TUI con renderizado de terminal |
RPC (--mode rpc) |
"rpc" |
true |
Diálogos y notificaciones mediante protocolo JSON; custom() devuelve undefined. Ver rpc.md |
JSON (--mode json) |
"json" |
false |
Transmisión de eventos a stdout; Los métodos de UI no son operativos |
Imprimir (-p) |
"print" |
false |
Extensions se ejecuta pero no aparece |
Utilice ctx.mode === "tui" antes de TUI funciones específicas (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.
Referencia de ejemplos
Todos los ejemplos en examples/extensions/.
| Ejemplo | Descripción | Tecla APIs |
|---|---|---|
| Herramientas | ||
hello.ts |
Registro mínimo de herramientas | registerTool |
question.ts |
Herramienta con interacción del usuario. | registerTool, ui.select |
questionnaire.ts |
Herramienta asistente de varios pasos | registerTool, ui.custom |
todo.ts |
Herramienta con estado y persistencia | registerTool, appendEntry, renderResult, eventos de sesión |
dynamic-tools.ts |
Registrar herramientas después del inicio y durante los comandos | registerTool, session_start, registerCommand |
structured-output.ts |
Herramienta final de salida estructurada con terminate: true |
registerTool, terminando los resultados de la herramienta |
truncated-tool.ts |
Ejemplo de truncamiento de salida | registerTool, truncateHead |
tool-override.ts |
Anular la herramienta de lectura incorporada | registerTool (mismo nombre que el integrado) |
| Comandos | ||
pirate.ts |
Modificar el mensaje del sistema por turno | registerCommand, before_agent_start |
summarize.ts |
Comando de resumen de conversación | registerCommand, ui.custom |
handoff.ts |
Transferencia de modelo entre proveedores | registerCommand, ui.editor, ui.custom |
qna.ts |
Preguntas y respuestas con interfaz de usuario personalizada | registerCommand, ui.custom, setEditorText |
send-user-message.ts |
Inyectar mensajes de usuario | registerCommand, sendUserMessage |
reload-runtime.ts |
Recargar comando y transferencia de herramientas LLM | registerCommand, ctx.reload(), sendUserMessage |
shutdown-command.ts |
Comando de apagado elegante | registerCommand, shutdown() |
| Eventos y puertas | ||
permission-gate.ts |
Bloquear comandos peligrosos | on("tool_call"), ui.confirm |
project-trust.ts |
Decidir o diferir la confianza del proyecto desde un usuario/global o extensión CLI | on("project_trust"), UI de confianza, resultado de confianza requerido |
protected-paths.ts |
Bloquear escrituras en rutas específicas | on("tool_call") |
confirm-destructive.ts |
Confirmar cambios de sesión | on("session_before_switch"), on("session_before_fork") |
dirty-repo-guard.ts |
Advertir sobre repositorio git sucio | on("session_before_*"), exec |
input-transform.ts |
Transformar la entrada del usuario | on("input") |
input-transform-streaming.ts |
Transformación de entrada compatible con streaming | on("input"), streamingBehavior |
model-status.ts |
React para modelar cambios | on("model_select"), setStatus |
provider-payload.ts |
Inspeccionar cargas útiles y encabezados de respuesta del proveedor | on("before_provider_request"), on("after_provider_response") |
system-prompt-header.ts |
Mostrar información de aviso del sistema | on("agent_start"), getSystemPrompt |
claude-rules.ts |
Cargar reglas desde archivos | on("session_start"), on("before_agent_start") |
prompt-customizer.ts |
Agregue guía de herramientas contextual usando systemPromptOptions |
on("before_agent_start"), BuildSystemPromptOptions |
file-trigger.ts |
El observador de archivos activa mensajes | sendMessage |
| Compactación y Sesiones | ||
custom-compaction.ts |
Resumen de compactación personalizado | on("session_before_compact") |
trigger-compact.ts |
Activar la compactación manualmente | compact() |
git-checkpoint.ts |
Git esconderse en turnos | on("turn_start"), on("session_before_fork"), exec |
git-merge-and-resolve.ts |
Recuperar, fusionar y resolver conflictos | on("agent_end"), exec, sendUserMessage |
auto-commit-on-exit.ts |
Comprometerse al cierre | on("session_shutdown"), exec |
| Componentes de la interfaz de usuario | ||
status-line.ts |
Indicador de estado del pie de página | setStatus, eventos de sesión |
working-indicator.ts |
Personaliza el indicador de funcionamiento de streaming | setWorkingIndicator, registerCommand |
github-issue-autocomplete.ts |
Agregue #1234 finalización de problemas además del autocompletado integrado precargando problemas abiertos recientes desde gh issue list |
addAutocompleteProvider, on("session_start"), exec |
custom-footer.ts |
Reemplazar el pie de página por completo | registerCommand, setFooter |
custom-header.ts |
Reemplazar encabezado de inicio | on("session_start"), setHeader |
modal-editor.ts |
Editor modal estilo Vim | setEditorComponent, CustomEditor |
rainbow-editor.ts |
Estilo de editor personalizado | setEditorComponent |
widget-placement.ts |
Widget arriba/abajo del editor | setWidget |
overlay-test.ts |
Componentes de superposición | ui.custom con opciones de superposición |
overlay-qa-tests.ts |
Pruebas de superposición completas | ui.custom, todas las opciones de superposición |
notify.ts |
Notificaciones simples | ui.notify |
timed-confirm.ts |
Diálogos con tiempo de espera | ui.confirm con tiempo de espera/señal |
mac-system-theme.ts |
Tema de cambio automático | setTheme, exec |
| Complejo Extensions | ||
plan-mode/ |
Implementación del modo de plan completo | Todos los tipos de eventos, registerCommand, registerShortcut, registerFlag, setStatus, setWidget, sendMessage, setActiveTools |
preset.ts |
Ajustes preestablecidos guardables (modelo, herramientas, pensamiento) | registerCommand, registerShortcut, registerFlag, setModel, setActiveTools, setThinkingLevel, appendEntry |
tools.ts |
Activar o desactivar herramientas en la interfaz de usuario | registerCommand, setActiveTools, SettingsList, eventos de sesión |
| Remoto y zona de pruebas | ||
ssh.ts |
SSH ejecución remota | registerFlag, on("user_bash"), on("before_agent_start"), operaciones de herramienta |
interactive-shell.ts |
Sesión de shell persistente | on("user_bash") |
sandbox/ |
Ejecución de herramientas en espacio aislado | Operaciones de herramientas |
gondolin/ |
Enrute herramientas integradas y comandos ! a una micro-VM Gondolin |
Operaciones de herramientas, anulaciones de herramientas integradas, on("user_bash") |
subagent/ |
Generar subagentes | registerTool, exec |
| Juegos | ||
snake.ts |
juego de serpiente | registerCommand, ui.custom, manejo del teclado |
space-invaders.ts |
Juego Invasores Espaciales | registerCommand, ui.custom |
doom-overlay/ |
Perdición en superposición | ui.custom con superposición |
| Providers | ||
custom-provider-anthropic/ |
Proxy antrópico personalizado | registerProvider |
custom-provider-gitlab-duo/ |
GitIntegración de Lab Duo | registerProvider con OAuth |
| Mensajes y comunicación | ||
message-renderer.ts |
Representación de mensajes personalizados | registerMessageRenderer, sendMessage |
entry-renderer.ts |
TUI representación de entrada personalizada únicamente | registerEntryRenderer, appendEntry |
event-bus.ts |
Eventos entre extensiones | pi.events |
| Metadatos de la sesión | ||
session-name.ts |
Nombrar sesiones para el selector | setSessionName, getSessionName |
bookmark.ts |
Marcar entradas para /tree | setLabel |
| Varios | ||
inline-bash.ts |
bash en línea en llamadas a herramientas | on("tool_call") |
bash-spawn-hook.ts |
Ajuste el comando bash, cwd y env antes de la ejecución | createBashTool, spawnHook |
with-deps/ |
Extensión con dependencias npm | Estructura del paquete con package.json |