Configuración, personalización, ajustes de plataforma y referencias de API para Pi.

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. Utilice pi -e./path.ts solo 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 /mycommand mediante pi.registerCommand()
  • Persistencia de la sesión: estado de la tienda que sobrevive a los reinicios mediante pi.appendEntry()
  • Representación personalizada: controle cómo aparecen las llamadas/resultados de herramientas y los mensajes en TUI

Casos de uso de ejemplo:

  • Puertas de permiso (confirmar antes de rm -rf, sudo, etc.)
  • Git 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

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

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

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

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

Paquete con dependencias - para extensiones que necesitan 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_shutdown

Eventos 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_start y message_end se activan para mensajes de usuario, asistente y resultado de herramienta.
  • message_update activa las actualizaciones de transmisión del asistente.
  • Los controladores message_end pueden devolver { message } para reemplazar el mensaje finalizado. El sustituto debe seguir siendo el mismo role.
pi.on("message_start", async (event, ctx) => {
  // event.message
});

pi.on("message_update", async (event, ctx) => {
  // event.message
  // event.assistantMessageEvent (token-by-token stream event)
});

pi.on("message_end", async (event, ctx) => {
  if (event.message.role !== "assistant") return;

  return {
    message: {
      ...event.message,
      usage: {
        ...event.message.usage,
        cost: {
          ...event.message.usage.cost,
          total: 0.123,
        },
      },
    },
  };
});

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_start se emite en el orden de la fuente asistente durante la fase de verificación previa
  • tool_execution_update los eventos pueden intercalarse entre herramientas
  • tool_execution_end se emite en el orden de finalización de la herramienta después de finalizar cada herramienta
  • Los eventos de mensajes finales toolResult aún se emiten más tarde en el orden de origen del asistente.
pi.on("tool_execution_start", async (event, ctx) => {
  // event.toolCallId, event.toolName, event.args
});

pi.on("tool_execution_update", async (event, ctx) => {
  // event.toolCallId, event.toolName, event.args, event.partialResult
});

pi.on("tool_execution_end", async (event, ctx) => {
  // event.toolCallId, event.toolName, event.result, event.isError
});

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.input afectan la ejecución real de la herramienta
  • Los manejadores posteriores tool_call ven mutaciones realizadas por manejadores anteriores
  • No se realiza ninguna revalidación después de su mutación.
  • Valores de retorno de tool_call bloqueo de control a través de { block: true, reason?: string, terminate?: boolean }
  • terminate solo 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, isError o usage); 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:

  1. Comandos de extensión (/cmd) verificados primero; si se encuentran, el controlador se ejecuta y se omite el evento de entrada
  2. input incendios de eventos: puede interceptar, transformar o manejar
  3. Si no se maneja: comandos de habilidad (/skill:name) ampliados al contenido de la habilidad
  4. Si no se maneja: prompt templates (/template) expandido al contenido de la plantilla
  5. Comienza el procesamiento del agente (before_agent_start, etc.)
pi.on("input", async (event, ctx) => {
  // event.text - raw input (before skill/template expansion)
  // event.images - attached images, if any
  // event.source - "interactive" (typed), "rpc" (API), or "extension" (via sendUserMessage)
  // event.streamingBehavior - "steer" | "followUp" | undefined
  //   undefined when idle, "steer" for mid-stream interrupts,
  //   "followUp" for messages queued until the agent finishes

  // Transform: rewrite input before expansion
  if (event.text.startsWith("?quick "))
    return { action: "transform", text: `Respond briefly: ${event.text.slice(7)}` };

  // Handle: respond without LLM (extension shows its own feedback)
  if (event.text === "ping") {
    ctx.ui.notify("pong", "info");
    return { action: "handled" };
  }

  // Route by source: skip processing for extension-injected messages
  if (event.source === "extension") return { action: "continue" };

  // Intercept skill commands before expansion
  if (event.text.startsWith("/skill:")) {
    // Could transform, block, or let pass through
  }

  return { action: "continue" };  // Default: pass through to expansion
});

Resultados:

  • continue - pasa sin cambios (predeterminado si el controlador no devuelve nada)
  • transform - modificar texto/imágenes, luego continuar con la expansión
  • handled - 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 ID

ctx.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 context posteriores.
  • 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ón
  • setup: muta el SessionManager de la nueva sesión antes de que se ejecute withSession
  • withSession: ejecute el trabajo posterior al cambio en un contexto de sesión de reemplazo nuevo. No utilice el antiguo comando pi / ctx capturado; 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 editor
  • position: "at" duplica la ruta activa a través de la entrada seleccionada sin restaurar el texto del editor
  • withSession: ejecute el trabajo posterior al cambio en un contexto de sesión de reemplazo nuevo. No utilice el antiguo comando pi / ctx capturado; 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 abandonada
  • customInstructions: Instrucciones personalizadas para el resumidor
  • replaceInstructions: si es verdadero, customInstructions reemplaza el mensaje predeterminado en lugar de agregarlo
  • label: Etiqueta para adjuntar a la entrada de resumen de la rama (o entrada de destino si no es un resumen)

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:

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:

  • withSession se ejecuta solo después de que la sesión anterior haya emitido session_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 recibido session_start.
  • La devolución de llamada aún se ejecuta en el cierre original, no dentro de la nueva instancia de extensión. Eso significa que es posible que su antigua instancia de extensión ya haya ejecutado su limpieza de apagado antes de que comience withSession.
  • Los objetos antiguos capturados pi / comando antiguo ctx vinculados a la sesión quedan obsoletos después del reemplazo y se lanzarán si se usan. Utilice solo el ctx pasado a withSession para 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.sessionManager antes del reemplazo, sm sigue siendo el antiguo objeto SessionManager. No lo reutilice después del reemplazo.
  • El código en withSession debe asumir que cualquier estado invalidado por su controlador session_shutdown ya desapareció. Capture únicamente datos simples que sobrevivan limpiamente al apagado, como cadenas, identificadores y configuraciones serializadas.

Patrón seguro:

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

Patrón inseguro:

pi.registerCommand("handoff", {
  handler: async (_args, ctx) => {
    const oldSessionManager = ctx.sessionManager;
    await ctx.newSession({
      withSession: async (_ctx) => {
        // stale old objects: do not do this
        oldSessionManager.getSessionFile();
        pi.sendUserMessage("wrong");
      },
    });
  },
});

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() emite session_shutdown para el tiempo de ejecución de la extensión actual
  • Luego recarga recursos y emite session_start con reason: "reload" y resources_discover con motivo "reload"
  • El controlador de comandos 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"
  • isStreamingtrue para actualizaciones parciales del asistente; false para usuario, asistente finalizado y mensajes restaurados
  • availableWidth: 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.killed

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

pi.getAllTools() devuelve name, description, parameters, promptGuidelines y sourceInfo.

Valores típicos de sourceInfo.source:

  • builtin para herramientas integradas
  • sdk para herramientas pasadas por createAgentSession({ 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_VAR o ${ENV_VAR}) o inicial !command. Requerido al definir modelos (a menos que se proporcione oauth). $ escapa de ``apiKey - API key literal, interpolación ambiental ($ENV_VARo${ENV_VAR}) o inicial !command. Requerido al definir modelos (a menos que se proporcione oauth). $escapa de y$!escapa de un literal!` sin activar la ejecución del comando.
  • api - API tipo: "anthropic-messages", "openai-completions", "openai-responses", etc.
  • headers: encabezados personalizados para incluir en las solicitudes.
  • authHeader: si es verdadero, agrega el encabezado Authorization: Bearer automáticamente.
  • models - Matriz de definiciones de modelos. Si se proporciona, reemplaza todos los modelos existentes para este proveedor. Las definiciones de modelo pueden configurar baseUrl para 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.stored contiene la instantánea del proveedor persistente; utilice la generación comprobada context.publish({ persist: entry }) solo cuando los datos del catálogo actualizados deban persistir. Utilice persist: null para 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.ts

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

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

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

Consulte examples/extensions/truncated-tool.ts para 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 actual
  • state: estado local de fila compartido en renderCall y renderResult
  • lastComponent: el componente devuelto anteriormente para esa ranura, si corresponde
  • invalidate(): solicita una nueva representación de esta fila de herramientas
  • toolCallId, cwd, executionStarted, argsComplete, isPartial, expanded, showImages, isError

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

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 teclas
  • rawKeyHint(key, description) - Formatear una cadena de clave sin formato

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

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

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

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

Mejores prácticas

  • Utilice Text con relleno (0, 0). El cuadro predeterminado maneja el relleno.
  • Utilice \n para contenido de varias líneas.
  • Maneje isPartial para ver el progreso de la transmisión.
  • Soporte expanded para obtener detalles a pedido.
  • Mantenga compacta la vista predeterminada.
  • Lea context.args en renderResult en lugar de copiar argumentos en context.state.
  • Utilice context.state solo para datos que deben compartirse entre espacios de llamadas y resultados.
  • Reutilice context.lastComponent cuando la misma instancia de componente se pueda actualizar en su lugar.
  • Utilice renderShell: "self" 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 herramienta
  • renderResult: muestra texto sin formato de content

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:

  1. Registre cada herramienta con pi.registerTool() para que aparezca en pi.getAllTools().
  2. Mantenga activas las herramientas de carga, como search_tools, y deje inactivas las herramientas de búsqueda.
  3. 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.
  4. Pi registra qué herramientas se agregaron en el resultado de la herramienta del cargador.
  5. 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 contenido tool_reference.
  • AI abierta
    • Models: gpt-5.4 y familia más nueva
    • Representación nativa: Pi agrega los elementos completos del cliente tool_search_call y tool_search_output en el punto de carga.

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() devuelve undefined
  • confirm() devuelve false
  • input() devuelve undefined

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 theme

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

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 estilizar
  • keybindings - 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 base Editor) 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, theme y keybindings de la aplicación
  • Utilice ctx.ui.getEditorComponent() antes de setEditorComponent() para ajustar el editor personalizado previamente configurado
  • Pase undefined para restaurar el valor predeterminado: ctx.ui.setEditorComponent(undefined)

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

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

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

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_call los errores bloquean la herramienta (a prueba de fallos)
  • Los errores de la herramienta execute deben señalarse lanzando; el error arrojado se detecta, se informa al LLM con isError: true y la ejecución continúa

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